git.lucas.co / cce-designer
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 }