git.lucas.co / cce-designer
graphic design tool
git clone https://git.lucas.co/cce-designer.git

src/context.rs (12K)

  1 //! Where a node may stand: the OBJECT level and the GEOMETRY context.
  2 //!
  3 //! The root is the object level, Houdini's `/obj`. Geometry is not built
  4 //! there: it is built inside a `geometry` node, a container that stands at
  5 //! the root and is what the root draws, and everything that makes or
  6 //! changes geometry is placed inside one (at any depth — a subnet, a simnet
  7 //! or a repeat inside a geometry node is in its context too). Since
  8 //! 2026-10-02; until then every node could stand anywhere and the root was
  9 //! one big geometry level.
 10 //!
 11 //! Three placements, by node type ([`placement`]):
 12 //!
 13 //! - **Object** — the root only: the `geometry` container itself, cameras,
 14 //!   which are seen from every level (`State::camera_level`), and the
 15 //!   `environment` node, the scene's light (`crate::environment`).
 16 //! - **Geometry** — inside a geometry node only: every operator, subnets,
 17 //!   simnets, repeats and the subnet templates (the Embryo, the Remesh).
 18 //! - **Any** — the page nodes, which are a 2D context of their own and stay
 19 //!   where they always could, and `export`, which writes a page or a mesh.
 20 //!
 21 //! The rule is held where a node ARRIVES at a level — Add Node, paste, MCP's
 22 //! `add_node` — and by the load ([`wrap_root_geometry`], format 5). It is not
 23 //! held by the evaluator: a hand-built tree with a sphere at the root still
 24 //! draws, which is what keeps the suite's fixtures meaning what they meant.
 25 
 26 use crate::app::{FsNode, ParamKind, Project};
 27 
 28 /// The container's node type.
 29 pub const GEOMETRY: &str = "geometry";
 30 
 31 /// What a level is for.
 32 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
 33 pub enum Context {
 34     Object,
 35     Geometry,
 36 }
 37 
 38 /// Where a node of some type may stand.
 39 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
 40 pub enum Placement {
 41     Object,
 42     Geometry,
 43     Any,
 44 }
 45 
 46 pub fn is_geometry_container(node_type: &str) -> bool {
 47     node_type.eq_ignore_ascii_case(GEOMETRY)
 48 }
 49 
 50 /// Where a node of `node_type` may stand. Everything not named is an
 51 /// operator, so a new node type is a geometry node without a line here.
 52 pub fn placement(node_type: &str) -> Placement {
 53     if is_geometry_container(node_type)
 54         || node_type.eq_ignore_ascii_case("camera")
 55         || node_type.eq_ignore_ascii_case(crate::environment::ENVIRONMENT)
 56     {
 57         Placement::Object
 58     } else if crate::page::is_page_node(node_type) || node_type.eq_ignore_ascii_case("export") {
 59         Placement::Any
 60     } else {
 61         Placement::Geometry
 62     }
 63 }
 64 
 65 /// The context of the level a network editor's path addresses. The root is
 66 /// the object level and every level under it is inside a geometry node:
 67 /// the root's only enterable nodes are geometry containers, since a subnet
 68 /// is a geometry node. (A hand-built tree with a subnet at the root is in
 69 /// the geometry context inside it too, which is what it was built to be.)
 70 pub fn context_at(path: &[usize]) -> Context {
 71     if path.is_empty() { Context::Object } else { Context::Geometry }
 72 }
 73 
 74 pub fn fits(placement: Placement, context: Context) -> bool {
 75     match placement {
 76         Placement::Any => true,
 77         Placement::Object => context == Context::Object,
 78         Placement::Geometry => context == Context::Geometry,
 79     }
 80 }
 81 
 82 /// Why `label` (a template's label or a node's name) cannot go into a level
 83 /// of `context` — the status line's text — or None when it can.
 84 pub fn refusal(label: &str, node_type: &str, context: Context) -> Option<String> {
 85     match placement(node_type) {
 86         p if fits(p, context) => None,
 87         Placement::Geometry => Some(format!("{label} goes inside a Geometry node: add one here and dive in")),
 88         _ => Some(format!("{label} goes at the root")),
 89     }
 90 }
 91 
 92 /// The root's first geometry container, made when there is none: where a
 93 /// geometry node that has arrived at the root is re-homed. Its slot.
 94 pub fn geometry_home(root: &mut FsNode) -> usize {
 95     if let Some(i) = root.children.iter().position(|c| is_geometry_container(&c.node_type)) {
 96         return i;
 97     }
 98     let name = unused_name(root, "geometry");
 99     let position = free_cell(root, (0.0, 0.0));
100     root.children.push(container(name, position));
101     root.children.len() - 1
102 }
103 
104 fn container(name: String, position: (f32, f32)) -> FsNode {
105     FsNode {
106         id: crate::app::generate_node_id(),
107         name,
108         node_type: GEOMETRY.to_string(),
109         children: Vec::new(),
110         params: Vec::new(),
111         geometry_visible: true,
112         bypassed: false,
113         position,
114         inputs: 0,
115         outputs: 0,
116     }
117 }
118 
119 fn unused_name(level: &FsNode, base: &str) -> String {
120     (1..)
121         .map(|i| format!("{base}{i}"))
122         .find(|n| !level.children.iter().any(|c| &c.name == n))
123         .unwrap()
124 }
125 
126 /// The first cell at or right of `at` no child of `level` stands on.
127 pub fn free_cell(level: &FsNode, at: (f32, f32)) -> (f32, f32) {
128     let mut cell = at;
129     while level.children.iter().any(|c| c.position == cell) {
130         cell.0 += 1.0;
131     }
132     cell
133 }
134 
135 /// Whether a root child stays at the root when an older save is carried
136 /// into the object level. The retired settings nodes stay so that
137 /// `migrate_meta_settings_node`, which runs after, finds them where they
138 /// were; an export stays only when it reads a page standing at the root,
139 /// since what it writes is then a page.
140 fn stays_at_root(root: &FsNode, node: &FsNode) -> bool {
141     if matches!(node.node_type.as_str(), "session" | "meta" | "utility") {
142         return true;
143     }
144     if node.node_type.eq_ignore_ascii_case("export") {
145         let input = crate::geometry::node_param_node(node, "input");
146         return input.is_some_and(|name| {
147             root.children.iter().any(|c| c.name == name && crate::page::is_page_node(&c.node_type))
148         });
149     }
150     placement(&node.node_type) != Placement::Geometry
151 }
152 
153 /// Format 4 → 5: the root becomes the object level. Every root child that
154 /// is a geometry node goes into one new `geometry` container at the root,
155 /// in its order, with its position, wires and flags — so it reads what it
156 /// read, since a wire looks among its siblings first and they came along.
157 /// Cameras, pages and the retired settings nodes stay.
158 ///
159 /// What NAMES a moved node from somewhere else is re-pointed: a channel
160 /// path, in an expression or a wrangle's Code, that crosses between the
161 /// container and the root (relative) or reaches into it from the top
162 /// (absolute) is resolved where it stood and written again from where it
163 /// stands. And the view follows: an editor looking at the root looks into
164 /// the container, at the node it had selected, and a path into a moved
165 /// subnet goes through the container.
166 pub fn wrap_root_geometry(project: &mut Project) {
167     let old = project.root.clone();
168     let moving: Vec<bool> = old.children.iter().map(|c| !stays_at_root(&old, c)).collect();
169     if !moving.contains(&true) {
170         return;
171     }
172 
173     // Every channel path anywhere, resolved in the tree as it stands.
174     let refs = channel_refs(&old);
175 
176     let mut kept = Vec::new();
177     let mut moved = Vec::new();
178     // Old slot → where it went: (in the container?, new slot).
179     let mut map = Vec::new();
180     for (child, go) in old.children.iter().cloned().zip(&moving) {
181         if *go {
182             map.push((true, moved.len()));
183             moved.push(child);
184         } else {
185             map.push((false, kept.len()));
186             kept.push(child);
187         }
188     }
189     let corner = moved.iter().fold((f32::MAX, f32::MAX), |(x, y), n| (x.min(n.position.0), y.min(n.position.1)));
190     let mut root = old.clone();
191     root.children = kept;
192     let name = unused_name(&root, GEOMETRY);
193     let position = free_cell(&root, corner);
194     let mut geo = container(name, position);
195     geo.children = moved;
196     let geo_slot = root.children.len();
197     root.children.push(geo);
198 
199     repoint_channel_refs(&mut root, &refs);
200     project.root = root;
201 
202     let remap = |path: &[usize]| -> Vec<usize> {
203         match path.split_first() {
204             None => vec![geo_slot],
205             Some((&first, rest)) => match map.get(first) {
206                 Some(&(true, slot)) => [&[geo_slot, slot][..], rest].concat(),
207                 Some(&(false, slot)) => [&[slot][..], rest].concat(),
208                 None => Vec::new(),
209             },
210         }
211     };
212     let view = &mut project.view_state;
213     if view.current_path.is_empty() {
214         match view.selected_node.and_then(|s| map.get(s).copied()) {
215             // A camera or a page selected at the root: the editor stays.
216             Some((false, slot)) => view.selected_node = Some(slot),
217             Some((true, slot)) => {
218                 view.current_path = vec![geo_slot];
219                 view.selected_node = Some(slot);
220             }
221             None => {
222                 view.current_path = vec![geo_slot];
223                 view.selected_node = None;
224             }
225         }
226     } else {
227         view.current_path = remap(&view.current_path);
228     }
229 }
230 
231 /// One channel path as it resolved before a move.
232 struct ChannelRef {
233     holder: String,
234     param: String,
235     path: String,
236     target: String,
237     absolute: bool,
238     param_part: String,
239 }
240 
241 /// The parameters whose text spells channel paths: expressions, and a
242 /// wrangle's Code.
243 fn spells_paths(p: &crate::app::ParamDef) -> bool {
244     p.is_expr() || p.kind() == ParamKind::Code
245 }
246 
247 fn channel_refs(root: &FsNode) -> Vec<ChannelRef> {
248     fn walk(root: &FsNode, node: &FsNode, out: &mut Vec<ChannelRef>) {
249         for p in node.params.iter().filter(|p| spells_paths(p)) {
250             crate::expr::rewrite_paths(p.text(), |path| {
251                 if let Some((target, absolute, param_part)) = crate::geometry::ref_path_target(root, node, path) {
252                     out.push(ChannelRef {
253                         holder: node.id.clone(),
254                         param: p.name.clone(),
255                         path: path.to_string(),
256                         target,
257                         absolute,
258                         param_part,
259                     });
260                 }
261                 None
262             });
263         }
264         for c in &node.children {
265             walk(root, c, out);
266         }
267     }
268     let mut out = Vec::new();
269     walk(root, root, &mut out);
270     out
271 }
272 
273 /// Write each path in `refs` again from where its holder now stands, where
274 /// the move changed what it has to say.
275 fn repoint_channel_refs(root: &mut FsNode, refs: &[ChannelRef]) {
276     let inside = |root: &FsNode, id: &str| -> bool {
277         crate::geometry::node_chain(root, id)
278             .and_then(|chain| chain.first().map(|n| is_geometry_container(&n.node_type) && n.id != id))
279             .unwrap_or(false)
280     };
281     let mut edits: Vec<(String, String, String)> = Vec::new();
282     for holder in refs.iter().map(|r| r.holder.as_str()).collect::<std::collections::BTreeSet<_>>() {
283         let Some(node) = crate::viewer_state::find_node_by_id(root, holder) else { continue };
284         for p in node.params.iter().filter(|p| spells_paths(p)) {
285             let text = crate::expr::rewrite_paths(p.text(), |path| {
286                 let r = refs.iter().find(|r| r.holder == holder && r.param == p.name && r.path == path)?;
287                 let target_moved = inside(root, &r.target);
288                 let needed = if r.absolute { target_moved } else { inside(root, holder) != target_moved };
289                 if !needed {
290                     return None;
291                 }
292                 let node_path = if r.absolute {
293                     crate::geometry::absolute_ref_path(root, &r.target)?
294                 } else {
295                     crate::geometry::relative_ref_path(root, holder, &r.target)?
296                 };
297                 Some(match node_path.as_str() {
298                     "" => r.param_part.clone(),
299                     "/" => format!("/{}", r.param_part),
300                     np => format!("{np}/{}", r.param_part),
301                 })
302             });
303             if text != p.text() {
304                 edits.push((holder.to_string(), p.name.clone(), text));
305             }
306         }
307     }
308     for (holder, param, text) in edits {
309         if let Some(n) = crate::viewer_state::find_node_by_id_mut(root, &holder) {
310             if let Some(p) = n.params.iter_mut().find(|p| p.name == param) {
311                 p.set_text(text);
312             }
313         }
314     }
315 }