graphic design tool
git clone https://git.lucas.co/cce-designer.git
src/export_cli.rs (4.3K)
1 //! `--export`: a project in, a mesh file out, no window.
2 //!
3 //! The counterpart of `--thumbnail`. Both exist for the same reason: work that
4 //! can only leave the app through a window is work that cannot be scripted,
5 //! diffed, or checked by a test.
6
7 use crate::export::{self, Format};
8
9 /// Evaluate `project` and write its geometry to `out`.
10 ///
11 /// Without `--node` the whole visible scene is written, which is what the
12 /// viewport shows. With it, that one node's output is written whether or not
13 /// it is visible — an Export node's input is normally drawn by something else,
14 /// so the node itself usually has its geometry flag off.
15 pub fn run(
16 project: &std::path::Path,
17 out: &std::path::Path,
18 frame: Option<i32>,
19 node: Option<String>,
20 scale: f32,
21 ) -> Result<String, String> {
22 // Loaded the same way --thumbnail does: a project is a directory holding
23 // state.json, or that file directly.
24 let state_file = if project.is_dir() {
25 project.join("state.json")
26 } else {
27 project.to_path_buf()
28 };
29 let content = std::fs::read_to_string(&state_file)
30 .map_err(|e| format!("read {}: {e}", state_file.display()))?;
31 let mut proj: crate::app::Project = serde_json::from_str(&content)
32 .map_err(|e| format!("parse {}: {e}", state_file.display()))?;
33 let templates = crate::app::flatten_node_templates(&crate::app::load_fs_tree());
34 proj.sanitize_node_names();
35 proj.migrate_param_refs();
36 crate::app::merge_template_defs(&mut proj.root, &templates);
37
38 let mut ocl_error = None;
39 let mut sim_cache = crate::geometry::SimCache::default();
40 // Start frame 1, the playbar's default, so a frame number here means what
41 // it means in the window — the same contract --thumbnail makes.
42 let mut sim = crate::geometry::EvalSim::new(frame.unwrap_or(0), 1, &mut sim_cache);
43
44 // A named node in the PAGE context writes a PNG instead — the same rule
45 // the Export node follows: what is being exported decides the format, not
46 // the file name. Without a node name there is no page to mean, since a
47 // level can hold several.
48 if let Some(name) = &node {
49 let target = crate::geometry::find_node_by_name(&proj.root, name)
50 .ok_or_else(|| format!("no node named '{name}'"))?;
51 if let Some(page) = crate::page::resolve_page(&proj.root, target, &mut Vec::new()) {
52 page.write_png(out)?;
53 return Ok(format!(
54 "page {}x{} at {} DPI ({:.2} x {:.2} in) -> {}",
55 page.width,
56 page.height,
57 page.dpi,
58 page.size[0],
59 page.size[1],
60 out.display()
61 ));
62 }
63 }
64
65 let geom = match &node {
66 Some(name) => {
67 // Borrowed, NOT cloned. Several generators find their place in the
68 // scene with `find_sphere_index`, which identifies the target by
69 // POINTER — so a clone is a node the walk never recognizes, and a
70 // sphere exported this way silently produced nothing.
71 let target = crate::geometry::find_node_by_name(&proj.root, name)
72 .ok_or_else(|| format!("no node named '{name}'"))?;
73 let mut visited = Vec::new();
74 crate::geometry::generate_single_node_geometry_with_errors(
75 &proj.root,
76 target,
77 &mut visited,
78 &mut ocl_error,
79 &mut sim,
80 )
81 .ok_or_else(|| format!("'{name}' produced no geometry"))?
82 }
83 None => crate::geometry::network_sphere_vertices_with_errors(
84 &proj.root,
85 &proj.root,
86 &mut ocl_error,
87 &mut sim,
88 ),
89 };
90 if let Some(e) = ocl_error {
91 // Non-fatal, like the thumbnail: the rest of the scene still exports,
92 // and a silent partial file would be worse than a warning.
93 eprintln!("cce-designer --export: node error (geometry partially skipped): {e}");
94 }
95 if geom.num_prims() == 0 {
96 return Err("the geometry has no primitives".to_string());
97 }
98
99 let format = Format::from_path(out);
100 let bytes = export::write(&geom, out, format, scale)?;
101 Ok(format!(
102 "{} points, {} primitives -> {} as {} ({bytes} bytes)",
103 geom.num_points(),
104 geom.num_prims(),
105 out.display(),
106 format.label()
107 ))
108 }