graphic design tool
git clone https://git.lucas.co/cce-designer.git
refactor: the generators' normal attribute is N, not Norm
The Sphere, Box, Plane and their kin wrote Norm where the Normal node, the
exporter and a wrangle's @N already said N. Project format 2 migrates an
older save once: attribute rows naming Norm, Norm in an Attributes list,
and @Norm in wrangle code. migrate_param_refs is a step of the new
migrate_format, which every load path calls.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CLAUDE.md | 14 ++++++--
src/app.rs | 81 ++++++++++++++++++++++++++++++++++++++++-----
src/export_cli.rs | 2 +-
src/geometry.rs | 16 ++++-----
src/main.rs | 99 +++++++++++++++++++++++++++++++++++++++++--------------
src/project.rs | 4 +--
src/render.rs | 2 +-
src/shapes.rs | 8 ++---
src/thumbnail.rs | 2 +-
9 files changed, 176 insertions(+), 52 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index 80e1f28..cb079a0 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -992,9 +992,17 @@ into a choice its option name, into a spinbox an integer.
whole value had to be one reference, nothing else). `Project::format` is
the version that tells the two apart: 0 (absent) loads through
`migrate_param_refs`, which turns each old reference into an expression
-with `../` added to a bare name, and saves as 1 — beside
-`sanitize_node_names` on every load path, and never twice, since a bare
-name in a format-1 file is the node's own parameter. Templates go through
+with `../` added to a bare name — beside `sanitize_node_names` on every
+load path, and never twice, since a bare name in a format-1 file is the
+node's own parameter. It is a step of `Project::migrate_format`, which
+takes a file through every step it is behind. **Format 2** (2026-10-01):
+the generators' normal attribute is `N` — the Sphere, Box, Plane and
+their kin wrote `Norm`, where the Normal node, the exporter and a
+wrangle's `@N` already said `N` — and `migrate_norm_to_n` rewrites what
+names `Norm` in an older save: an attribute row, a name in a comma list of
+attributes (`Attributes`), `@Norm` in a wrangle's Code (not `@Normal`).
+Once, by the version, so an attribute someone names `Norm` afterwards is
+theirs (`a_save_naming_norm_names_n`). Templates go through
`infer_template_exprs` instead: a default that READS as a reference is one
(`embryo.json` says `chf("../Radius")` now). The same inference applies to a
value typed into a plain row or scripted through `set_param`: a reference
diff --git a/src/app.rs b/src/app.rs
index cabac52..f7cad64 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -367,14 +367,16 @@ pub struct Project {
/// The file format this project was saved in, so a load can tell an old
/// meaning from a new one. 0 (absent) is every save before 2026-09-24,
/// when a bare `ch("Name")` meant the PARENT's parameter; 1 is Houdini's
- /// semantics, where it means the node's own — `migrate_param_refs` is
- /// the step between them, and it must not run twice.
+ /// semantics, where it means the node's own; 2 (2026-10-01) is the
+ /// generators' normal attribute called `N` where it was `Norm`.
+ /// `migrate_format` takes a file through each step it is behind, and a
+ /// step must not run twice.
#[serde(default)]
pub format: u32,
}
/// The format `Project` saves in — see its `format` field.
-pub const PROJECT_FORMAT: u32 = 1;
+pub const PROJECT_FORMAT: u32 = 2;
/// One entry in a node's right-click context menu, parallel to the visible
/// labels shown via `context_menu::show`.
@@ -1185,6 +1187,10 @@ impl Project {
}
impl Project {
+ /// Take a loaded project through every format step it is behind, then
+ /// call it current. Runs beside `sanitize_node_names` on every load
+ /// path; a step must not run twice, which is what the version is for.
+ ///
/// Format 0 → 1: every parameter that was a pre-expression reference —
/// the whole value `ch("Name")`, `chf` / `chi` / `chb`, with `../` per
/// level — becomes an expression with Houdini's semantics. A bare name
@@ -1193,10 +1199,18 @@ impl Project {
/// path, and on a format-1 file does nothing, which is what the version
/// is for: a bare name in a NEW file is the node's own parameter and
/// must not be rewritten.
- pub fn migrate_param_refs(&mut self) {
- if self.format >= PROJECT_FORMAT {
- return;
+ pub fn migrate_format(&mut self) {
+ if self.format < 1 {
+ self.migrate_param_refs();
+ }
+ if self.format < 2 {
+ self.migrate_norm_to_n();
}
+ self.format = PROJECT_FORMAT;
+ }
+
+ /// Format 0 → 1, as above.
+ fn migrate_param_refs(&mut self) {
fn walk(node: &mut FsNode) {
for p in &mut node.params {
if !p.is_expr() {
@@ -1211,8 +1225,59 @@ impl Project {
}
}
walk(&mut self.root);
- self.format = PROJECT_FORMAT;
}
+
+ /// Format 1 → 2: the generators' normal attribute is `N`, as the Normal
+ /// node, the exporter and a wrangle's `@N` already named it, where the
+ /// Sphere, Box and Plane wrote `Norm`. What names it in a save follows:
+ /// a parameter naming an attribute (`ParamKind::Attribute`) that says
+ /// `Norm`, a name in a comma list of attributes (Transfer's and the
+ /// Remesh's `Attributes`), and `@Norm` in a wrangle's Code. Once, by
+ /// the version — an attribute someone names `Norm` after this is theirs.
+ fn migrate_norm_to_n(&mut self) {
+ fn walk(node: &mut FsNode) {
+ for p in &mut node.params {
+ if p.is_expr() {
+ continue;
+ }
+ let text = p.text().to_string();
+ let new = if p.kind() == ParamKind::Attribute && text.trim() == "Norm" {
+ Some("N".to_string())
+ } else if p.name == "Attributes" && text.split(',').any(|a| a.trim() == "Norm") {
+ Some(text.split(',').map(|a| if a.trim() == "Norm" { a.replace("Norm", "N") } else { a.to_string() }).collect::<Vec<_>>().join(","))
+ } else if p.kind() == ParamKind::Code && text.contains("@Norm") {
+ Some(rename_at_attribute(&text, "Norm", "N"))
+ } else {
+ None
+ };
+ if let Some(new) = new.filter(|n| *n != text) {
+ p.set_text(new);
+ }
+ }
+ for c in &mut node.children {
+ walk(c);
+ }
+ }
+ walk(&mut self.root);
+ }
+}
+
+/// `code` with every `@old` that is a whole name (not `@Normal` for
+/// `@Norm`) written `@new`.
+fn rename_at_attribute(code: &str, old: &str, new: &str) -> String {
+ let pat = format!("@{old}");
+ let renamed = format!("@{new}");
+ let mut out = String::with_capacity(code.len());
+ let mut rest = code;
+ while let Some(i) = rest.find(&pat) {
+ let after = &rest[i + pat.len()..];
+ let whole = !after.chars().next().is_some_and(|c| c.is_alphanumeric() || c == '_');
+ out.push_str(&rest[..i]);
+ out.push_str(if whole { &renamed } else { &pat });
+ rest = after;
+ }
+ out.push_str(rest);
+ out
}
/// A template's parameters whose defaults READ as expressions become ones:
@@ -7428,7 +7493,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
if let Ok(content) = fs::read_to_string(&default_proj_path) {
if let Ok(mut proj) = serde_json::from_str::<Project>(&content) {
proj.sanitize_node_names();
- proj.migrate_param_refs();
+ proj.migrate_format();
merge_template_defs(&mut proj.root, &node_templates);
loaded_project = Some(proj);
}
diff --git a/src/export_cli.rs b/src/export_cli.rs
index 7a0ba11..09940d8 100644
--- a/src/export_cli.rs
+++ b/src/export_cli.rs
@@ -32,7 +32,7 @@ pub fn run(
.map_err(|e| format!("parse {}: {e}", state_file.display()))?;
let templates = crate::app::flatten_node_templates(&crate::app::load_fs_tree());
proj.sanitize_node_names();
- proj.migrate_param_refs();
+ proj.migrate_format();
crate::app::merge_template_defs(&mut proj.root, &templates);
let mut ocl_error = None;
diff --git a/src/geometry.rs b/src/geometry.rs
index 97289af..e5e109a 100644
--- a/src/geometry.rs
+++ b/src/geometry.rs
@@ -290,7 +290,7 @@ pub fn smooth_lit_vertices(d: &Detail) -> Vec<Vertex3D> {
/// express neither: it emitted `lat_steps * lon_steps * 6` loose corners, of
/// which the two pole bands were zero-area triangles.
///
-/// `Norm` and `UV` are POINT attributes, computed from the surface normal
+/// `N` and `UV` are POINT attributes, computed from the surface normal
/// exactly as before. Both are pure functions of the normal, so a welded point
/// has one answer — including at the seam, where the old per-corner UVs
/// already agreed because they were derived from the normal rather than from
@@ -362,7 +362,7 @@ pub fn sphere_detail(center: Vec3, radius: f32, lat_steps: usize, lon_steps: usi
.map(|n| [0.35 + n.x.abs() * 0.35, 0.45 + n.y.abs() * 0.35, 0.85])
.collect();
let points = d.points_mut();
- let _ = points.insert("Norm", AttribData::Float3(norms));
+ let _ = points.insert("N", AttribData::Float3(norms));
let _ = points.insert("UV", AttribData::Float2(uvs));
let _ = points.insert(CD, AttribData::Float3(cds));
d
@@ -415,7 +415,7 @@ pub fn box_detail(start: Vec3, end: Vec3, thickness: f32) -> Detail {
// Wound counter-clockwise seen from outside, like the sphere and the
// template meshes. The soup's order was the reverse, which meant every
- // face of a native box wound against the `Norm` attribute the same
+ // face of a native box wound against the `N` attribute the same
// function attached to it — the geometry and its own normal disagreed.
let faces: [([u32; 4], Vec3); 6] = [
([3, 2, 1, 0], -dir), // start cap
@@ -434,7 +434,7 @@ pub fn box_detail(start: Vec3, end: Vec3, thickness: f32) -> Detail {
norms.extend(std::iter::repeat(normal.to_array()).take(4));
}
let (num_verts, num_points) = (d.num_verts(), d.num_points());
- let _ = d.verts_mut().insert("Norm", AttribData::Float3(norms));
+ let _ = d.verts_mut().insert("N", AttribData::Float3(norms));
let _ = d
.verts_mut()
.insert("UV", AttribData::Float2(vec![[0.0, 0.0]; num_verts]));
@@ -6719,13 +6719,13 @@ mod tests {
// Three faces meet at every corner with three different normals, so
// the normal cannot live on the point. This is what the vertex class
// is for.
- assert!(d.verts().has("Norm"));
- assert!(!d.points().has("Norm"));
+ assert!(d.verts().has("N"));
+ assert!(!d.points().has("N"));
assert_eq!(d.point_prims(0).len(), 3);
assert_eq!(d.topology().valence(0), 3);
let normals: Vec<[f32; 3]> = (0..d.num_verts())
- .filter_map(|v| match d.verts().value("Norm", v) {
+ .filter_map(|v| match d.verts().value("N", v) {
Some(crate::detail::AttribValue::Float3(n)) => Some(n),
_ => None,
})
@@ -6733,7 +6733,7 @@ mod tests {
assert_eq!(normals.len(), 24);
let corner_0_normals: Vec<[f32; 3]> = (0..d.num_prims())
.filter(|&p| d.prim_points(p).contains(&0))
- .filter_map(|p| match d.verts().value("Norm", d.prim_verts(p).start) {
+ .filter_map(|p| match d.verts().value("N", d.prim_verts(p).start) {
Some(crate::detail::AttribValue::Float3(n)) => Some(n),
_ => None,
})
diff --git a/src/main.rs b/src/main.rs
index e5d95d8..258c787 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -665,7 +665,7 @@ mod tests {
proj.sanitize_node_names();
- proj.migrate_param_refs();
+ proj.migrate_format();
let names: Vec<&str> = proj.root.children.iter().map(|c| c.name.as_str()).collect();
assert!(names.contains(&"camera1"));
@@ -682,10 +682,57 @@ mod tests {
// A clean file is left exactly alone.
let before = serde_json::to_string(&proj).unwrap();
proj.sanitize_node_names();
- proj.migrate_param_refs();
+ proj.migrate_format();
assert_eq!(serde_json::to_string(&proj).unwrap(), before);
}
+ /// Format 1 → 2: the generators' normal attribute is `N`, and what names
+ /// `Norm` in a save follows it — an attribute row, a name in a comma
+ /// list of attributes, `@Norm` in a wrangle (not `@Normal`) — once: a
+ /// format-2 file naming `Norm` is left alone, since that attribute is
+ /// someone's own.
+ #[test]
+ fn a_save_naming_norm_names_n() {
+ use crate::app::{FsNode, ParamDef, Project, PROJECT_FORMAT};
+ let node = |name: &str, ty: &str, params: Vec<ParamDef>| FsNode {
+ id: name.into(),
+ name: name.into(),
+ node_type: ty.into(),
+ children: vec![],
+ params,
+ geometry_visible: true,
+ bypassed: false,
+ position: (0.0, 0.0),
+ inputs: 1,
+ outputs: 1,
+ };
+ let mut root = node("root", "subnet", vec![]);
+ root.children = vec![
+ node("vis", "visualize", vec![ParamDef::new("Attribute", "attribute", "Norm")]),
+ node("xfer", "transfer", vec![ParamDef::new("Attributes", "text", "Cd, Norm,UV")]),
+ node("w", "wrangle", vec![ParamDef::new("Code", "code", "@P += @Norm * 0.1; @Normal = 1;")]),
+ node("keep", "visualize", vec![ParamDef::new("Attribute", "attribute", "Normx")]),
+ ];
+ let mut proj = Project { name: "p".into(), root, view_state: Default::default(), format: 1 };
+ proj.migrate_format();
+ assert_eq!(proj.format, PROJECT_FORMAT);
+ let text = |proj: &Project, i: usize| proj.root.children[i].params[0].text().to_string();
+ assert_eq!(text(&proj, 0), "N");
+ assert_eq!(text(&proj, 1), "Cd, N,UV");
+ assert_eq!(text(&proj, 2), "@P += @N * 0.1; @Normal = 1;");
+ assert_eq!(text(&proj, 3), "Normx", "only the whole name");
+
+ // Once: a format-2 file's Norm is its own attribute.
+ let mut again = proj.clone();
+ again.root.children[0].params[0].set_text("Norm".to_string());
+ again.migrate_format();
+ assert_eq!(text(&again, 0), "Norm");
+
+ // And the generators write N.
+ let s = crate::geometry::sphere_detail(glam::Vec3::ZERO, 1.0, 4, 6);
+ assert!(s.points().has("N") && !s.points().has("Norm"));
+ }
+
#[test]
fn test_dock_swap_repositions_plates() {
use crate::app::Dock;
@@ -3653,7 +3700,7 @@ mod tests {
let r = (pos[0].powi(2) + (pos[1] - 0.55).powi(2) + pos[2].powi(2)).sqrt();
assert!((r - 0.5).abs() < 1e-4, "point {pos:?} is {r} from the centre");
}
- assert!(geom.points().has("Norm") && geom.points().has("UV") && geom.points().has("Cd"));
+ assert!(geom.points().has("N") && geom.points().has("UV") && geom.points().has("Cd"));
}
/// The native curve node: a Catmull-Rom strip through the "Points"
@@ -4444,7 +4491,7 @@ mod tests {
};
let attr_ty = row("Attribute Name");
assert!(attr_ty.starts_with("textpick:"), "got {attr_ty}");
- for expected in ["Norm", "UV", "Pos", "Col"] {
+ for expected in ["N", "UV", "Pos", "Col"] {
assert!(attr_ty.contains(expected), "{expected} missing from {attr_ty}");
}
assert_eq!(row("Group"), "textpick:group1");
@@ -5777,8 +5824,8 @@ mod tests {
// A box line fans to 36 renderer vertices: 6 faces * 2 triangles * 3 corners.
assert_eq!(detail_vertices(&d).len(), 36);
- // Norm and UV ride the vertices, one per corner.
- assert!(d.verts().has("Norm"));
+ // N and UV ride the vertices, one per corner.
+ assert!(d.verts().has("N"));
assert!(d.verts().has("UV"));
}
@@ -7680,7 +7727,7 @@ mod tests {
state.rebuild_positions();
state.apply_layout();
state.rebuild_scene_geometry();
- assert!(state.scene_attributes.iter().any(|a| a.name == "Norm"), "{:?}", state.scene_attributes);
+ assert!(state.scene_attributes.iter().any(|a| a.name == "N"), "{:?}", state.scene_attributes);
let colours = |state: &State| state.rt_sphere_verts.iter().map(|v| v.color).collect::<Vec<_>>();
let plain = colours(&state);
let nodes_before = serde_json::to_string(&state.fs_root).unwrap();
@@ -7699,8 +7746,8 @@ mod tests {
let has = |state: &State, f: &str| state.slots.dialog.rows.iter().any(|r| r.id == field(f));
assert!(has(&state, "ramp") && has(&state, "opacity") && !has(&state, "scale"), "Ramp's rows");
- // On Norm, a Ramp recolours the scene, which gained no node.
- state.set_visualizer_field(0, "attribute", "Norm", true);
+ // On N, a Ramp recolours the scene, which gained no node.
+ state.set_visualizer_field(0, "attribute", "N", true);
assert_ne!(colours(&state), plain, "the ramp is on the scene");
assert_eq!(serde_json::to_string(&state.fs_root).unwrap(), nodes_before, "no node in the graph");
@@ -7726,7 +7773,7 @@ mod tests {
assert_eq!(state.slots.dialog.mode, Mode::Visualizers);
let id = format!("{VIS_ROW_PREFIX}0");
let row = state.slots.dialog.rows.iter().find(|r| r.id == id).expect("its row").clone();
- assert!(row.label.starts_with("Norm — Vector"), "{}", row.label);
+ assert!(row.label.starts_with("N — Vector"), "{}", row.label);
assert_eq!(row.toggle(), Some(true));
// Kept with the display settings.
@@ -7759,7 +7806,7 @@ mod tests {
let mut state = State::new(false);
state.rebuild_scene_geometry();
let base = state.scene_base.clone().expect("a scene");
- let mut v = crate::visualizer::Visualizer::new("Norm");
+ let mut v = crate::visualizer::Visualizer::new("N");
v.ramp = "Heat".into();
let mut by_vis = base.clone();
crate::visualizer::apply_all(&[v.clone()], &mut by_vis);
@@ -9244,14 +9291,14 @@ mod tests {
format: 0,
};
proj.root.children[0].children[0].params[0].set_expr(false);
- proj.migrate_param_refs();
+ proj.migrate_format();
let r = &proj.root.children[0].children[0].params[0];
assert_eq!(r.text(), "chf(\"../Size\")");
assert!(r.is_expr());
assert_eq!(proj.format, crate::app::PROJECT_FORMAT);
// A NEW file's bare name is the node's own parameter and stays.
proj.root.children[0].children[0].params[0].set_text("chf(\"Radius\")");
- proj.migrate_param_refs();
+ proj.migrate_format();
assert_eq!(proj.root.children[0].children[0].params[0].text(), "chf(\"Radius\")");
}
@@ -10732,7 +10779,11 @@ mod tests {
);
let (g, err) = eval_node(&root, "normal 1");
assert!(err.is_none(), "{err:?}");
- assert!(!g.points().has("N"), "the points' is not written");
+ // The points' N is the sphere's own, as it came in: Vertices writes
+ // the vertices and leaves it alone.
+ let (input, _) = eval_node(&root, "sphere 1");
+ let pn = |d: &crate::detail::Detail| (0..d.num_points()).map(|p| d.points().value("N", p).map(|v| v.as_vec3())).collect::<Vec<_>>();
+ assert_eq!(pn(&g), pn(&input), "the points' is not written");
let n = crate::geometry::own_vertex_normals(&g).expect("a normal a vertex");
assert_eq!(n.len(), g.num_verts());
let of_points = crate::geometry::point_normals(&g);
@@ -11006,7 +11057,7 @@ mod tests {
&[
("Input", "points 1"),
("From", "sphere 1"),
- ("Attributes", "Norm"),
+ ("Attributes", "N"),
("Maximum Distance", "0.00"),
],
),
@@ -11015,9 +11066,9 @@ mod tests {
let (g, err) = eval_node(&root, "transfer 1");
assert!(err.is_none(), "{err:?}");
// No limit: everything finds a nearest source point however far.
- assert!(g.points().has("Norm"));
+ assert!(g.points().has("N"));
assert!(
- (0..g.num_points()).any(|p| g.points().value("Norm", p).unwrap().as_vec3() != Vec3::ZERO),
+ (0..g.num_points()).any(|p| g.points().value("N", p).unwrap().as_vec3() != Vec3::ZERO),
"nothing was transferred"
);
@@ -11035,9 +11086,9 @@ mod tests {
.unwrap()
.set_text("0.01");
let (g, _) = eval_node(&limited, "transfer 1");
- assert!(g.points().has("Norm"), "the column exists even where nothing was near");
+ assert!(g.points().has("N"), "the column exists even where nothing was near");
assert!(
- (0..g.num_points()).all(|p| g.points().value("Norm", p).unwrap().as_vec3() == Vec3::ZERO),
+ (0..g.num_points()).all(|p| g.points().value("N", p).unwrap().as_vec3() == Vec3::ZERO),
"something transferred from out of range"
);
@@ -13218,7 +13269,7 @@ mod tests {
let mut proj: crate::app::Project = serde_json::from_str(&std::fs::read_to_string(&file).expect("reads")).expect("parses");
let templates = crate::app::flatten_node_templates(&crate::app::load_fs_tree());
proj.sanitize_node_names();
- proj.migrate_param_refs();
+ proj.migrate_format();
crate::app::merge_template_defs(&mut proj.root, &templates);
fn find<'a>(n: &'a mut FsNode, ty: &str) -> Option<&'a mut FsNode> {
@@ -13293,7 +13344,7 @@ mod tests {
let mut proj: crate::app::Project = serde_json::from_str(&std::fs::read_to_string(&file).expect("reads")).expect("parses");
let templates = crate::app::flatten_node_templates(&crate::app::load_fs_tree());
proj.sanitize_node_names();
- proj.migrate_param_refs();
+ proj.migrate_format();
crate::app::merge_template_defs(&mut proj.root, &templates);
fn simnet(n: &mut FsNode) -> Option<&mut FsNode> {
if n.node_type == "simnet" {
@@ -16681,7 +16732,7 @@ mod tests {
/// presents it as a control as wide as its target — over the wide
/// span around its value (`value_row_span`) — a slider for one, the float group with two, three
/// or four rows for more: Modify on Pos (the pull node), on an input
- /// Float3 (Norm) or Float2 (UV), Create by its Type. A single number is
+ /// Float3 (N) or Float2 (UV), Create by its Type. A single number is
/// spread over the components, as the node spreads it, and the pane
/// writing it back unchanged is not an edit. A text that fits no width,
/// an attribute the input lacks and an expression keep the text box;
@@ -16724,7 +16775,7 @@ mod tests {
let set = |state: &mut State, name: &str, val: &str| {
state.fs_root.children[1].params.iter_mut().find(|p| p.name == name).unwrap().set_text(val.to_string());
};
- set(&mut state, "Attribute Name", "Norm");
+ set(&mut state, "Attribute Name", "N");
assert_eq!(value_row(&mut state), wide, "Modify on an input Float3");
set(&mut state, "Attribute Name", "UV");
assert_eq!(value_row(&mut state), "text", "three numbers do not fit an input Float2");
@@ -16787,7 +16838,7 @@ mod tests {
let rows = state.param_mut().node_params();
assert!(rows.iter().all(|r| r.0 != "Value"), "no Value row");
let from = rows.iter().find(|r| r.0 == "From Attribute").expect("a From Attribute row");
- assert!(from.2.starts_with("textpick:") && from.2.contains("Norm"), "{}", from.2);
+ assert!(from.2.starts_with("textpick:") && from.2["textpick:".len()..].split(',').any(|a| a == "N"), "{}", from.2);
set(&mut state, "Value From", "Constant");
// The parameter itself never changed kind: it is text in the node.
diff --git a/src/project.rs b/src/project.rs
index 44e794a..f1a2129 100644
--- a/src/project.rs
+++ b/src/project.rs
@@ -448,7 +448,7 @@ impl State {
let content = fs::read_to_string(path)?;
let mut proj: Project = serde_json::from_str(&content)?;
proj.sanitize_node_names();
- proj.migrate_param_refs();
+ proj.migrate_format();
crate::app::merge_template_defs(&mut proj.root, &self.node_templates);
self.fs_root = proj.root;
// Another window's edits, or the bundled file: not this one's to undo.
@@ -530,7 +530,7 @@ impl State {
let content = fs::read_to_string(&state_file_path)?;
let mut proj: Project = serde_json::from_str(&content)?;
proj.sanitize_node_names();
- proj.migrate_param_refs();
+ proj.migrate_format();
crate::app::merge_template_defs(&mut proj.root, &self.node_templates);
self.fs_root = proj.root;
self.edit_history.clear();
diff --git a/src/render.rs b/src/render.rs
index 8ea245a..0ed2527 100644
--- a/src/render.rs
+++ b/src/render.rs
@@ -1437,7 +1437,7 @@ pub(crate) fn scene_point_overlays(
// Smooth point normals: for each point, the normalized sum of the
// face normals of the primitives touching it. Template meshes wind
// CCW seen from outside (the raster culling convention), so the
- // plain cross(B-A, C-A) points outward. The kernel outputs' Norm
+ // plain cross(B-A, C-A) points outward. The kernel outputs' N
// attribute is a default up-vector — useless here.
//
// The soup had to reconstruct "which triangles touch this point" by
diff --git a/src/shapes.rs b/src/shapes.rs
index 2fe52c2..ab50fed 100644
--- a/src/shapes.rs
+++ b/src/shapes.rs
@@ -113,7 +113,7 @@ pub fn sphere_node_detail(target: &FsNode, legacy_center: Option<Vec3>) -> Detai
d
}
-/// `Norm`, `UV` and `Cd` from the surface normal, as the sphere has always
+/// `N`, `UV` and `Cd` from the surface normal, as the sphere has always
/// carried them. Colour is the kernel's: the SIGNED normal folded into
/// 0..1, world-anchored so a point keeps its colour as the sphere turns.
fn finish_sphere(d: &mut Detail, center: Vec3, colored: bool) {
@@ -128,7 +128,7 @@ fn finish_sphere(d: &mut Detail, center: Vec3, colored: bool) {
.map(|n| if colored { [0.5 + n.x * 0.5, 0.5 + n.y * 0.5, 0.5 + n.z * 0.5] } else { DEFAULT_COLOR })
.collect();
let points = d.points_mut();
- let _ = points.insert("Norm", AttribData::Float3(norms));
+ let _ = points.insert("N", AttribData::Float3(norms));
let _ = points.insert("UV", AttribData::Float2(uvs));
let _ = points.insert(CD, AttribData::Float3(cds));
}
@@ -251,7 +251,7 @@ pub fn cuboid_detail(center: Vec3, half: Vec3, color: [f32; 3]) -> Detail {
d.add_prim(quad);
norms.extend(std::iter::repeat(normal.to_array()).take(4));
}
- let _ = d.verts_mut().insert("Norm", AttribData::Float3(norms));
+ let _ = d.verts_mut().insert("N", AttribData::Float3(norms));
let _ = d.verts_mut().insert("UV", AttribData::Float2(vec![[0.0, 0.0]; 24]));
let _ = d.points_mut().insert(CD, AttribData::Float3(vec![color; 8]));
d
@@ -326,7 +326,7 @@ pub fn plane_node_detail(target: &FsNode) -> Detail {
}
let n = d.num_points();
let points = d.points_mut();
- let _ = points.insert("Norm", AttribData::Float3(vec![[0.0, 1.0, 0.0]; n]));
+ let _ = points.insert("N", AttribData::Float3(vec![[0.0, 1.0, 0.0]; n]));
let _ = points.insert("UV", AttribData::Float2(uvs));
let _ = points.insert(CD, AttribData::Float3(cds));
d
diff --git a/src/thumbnail.rs b/src/thumbnail.rs
index 7a7dbbb..325440f 100644
--- a/src/thumbnail.rs
+++ b/src/thumbnail.rs
@@ -29,7 +29,7 @@ pub fn run(project: &Path, out: &Path, size: u32, samples: Option<u32>, frame: O
// scene shows what opening it would show.
let templates = crate::app::flatten_node_templates(&crate::app::load_fs_tree());
proj.sanitize_node_names();
- proj.migrate_param_refs();
+ proj.migrate_format();
crate::app::merge_template_defs(&mut proj.root, &templates);
let mut ocl_error = None;