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 }