graphic design tool
git clone https://git.lucas.co/cce-designer.git
refactor: a parameter's name is an identifier, and the pane shows its label
Every template parameter has a snake_case name (base_resolution) - what a
ch() path, a show_when condition, MCP and the code spell - and a label
(Base Resolution), the only thing the params pane shows. Format 4 renames
an older save's parameters and the paths in its expressions and wrangle
code. MCP finds a parameter by name, label or old name; add_param refuses
a name that is not one. Templates with a misnamed parameter are refused,
and under test find_param asserts every name it is asked for.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CLAUDE.md | 84 ++-
nodes/analysis.json | 16 +-
nodes/attribute.json | 103 +--
nodes/boolean.json | 8 +-
nodes/bounds.json | 6 +-
nodes/box.json | 9 +-
nodes/camera.json | 9 +-
nodes/collapse_edges.json | 6 +-
nodes/collision.json | 21 +-
nodes/connectivity.json | 4 +-
nodes/copy.json | 12 +-
nodes/cull.json | 12 +-
nodes/curve.json | 6 +-
nodes/deform.json | 10 +-
nodes/detangle.json | 20 +-
nodes/develop.json | 12 +-
nodes/distance.json | 12 +-
nodes/embryo.json | 158 +++--
nodes/export.json | 10 +-
nodes/extrude.json | 9 +-
nodes/flip_edges.json | 6 +-
nodes/grid.json | 10 +-
nodes/group.json | 65 +-
nodes/hull.json | 2 +-
nodes/line.json | 4 +-
nodes/mold_shell.json | 10 +-
nodes/neighbour.json | 55 +-
nodes/normal.json | 10 +-
nodes/output.json | 2 +-
nodes/page.json | 16 +-
nodes/page_border.json | 8 +-
nodes/page_grid.json | 12 +-
nodes/page_shape.json | 32 +-
nodes/page_text.json | 20 +-
nodes/plane.json | 24 +-
nodes/points.json | 9 +-
nodes/polygon.json | 10 +-
nodes/project.json | 6 +-
nodes/relax.json | 39 +-
nodes/remesh.json | 114 ++--
nodes/repeat.json | 11 +-
nodes/scatter.json | 22 +-
nodes/simnet.json | 14 +-
nodes/soft_transform.json | 14 +-
nodes/sphere.json | 38 +-
nodes/split_edges.json | 6 +-
nodes/subdivide.json | 4 +-
nodes/subnet.json | 2 +-
nodes/suture.json | 10 +-
nodes/switch.json | 10 +-
nodes/time.json | 10 +-
nodes/transfer.json | 23 +-
nodes/transform.json | 4 +-
nodes/valence.json | 6 +-
nodes/visualize.json | 47 +-
nodes/volume.json | 10 +-
nodes/wrangle.json | 11 +-
src/api.rs | 9 +-
src/app.rs | 201 ++++--
src/curve_tool.rs | 4 +-
src/detangle.rs | 22 +-
src/expr.rs | 12 +-
src/geometry.rs | 1112 +++++++++++++++----------------
src/image_handles.rs | 32 +-
src/image_tools.rs | 28 +-
src/main.rs | 1582 +++++++++++++++++++++++---------------------
src/page.rs | 100 +--
src/param.rs | 72 +-
src/render.rs | 2 +-
src/shapes.rs | 44 +-
src/soft_transform_tool.rs | 8 +-
src/visualizer.rs | 20 +-
src/window.rs | 21 +-
src/wrangle.rs | 2 +-
74 files changed, 2482 insertions(+), 2012 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index c327627..d627046 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -648,6 +648,55 @@ diagnosis is in the git history of this section (commit `8fd0c29`) if the
pattern ever recurs with another driver: a `read` returning EBADF on a file
nothing is wrong with, in a process that has loaded a vendor ICD.
+### A parameter has a name and a label (since 2026-10-01)
+
+A parameter's **name** is an identifier — lowercase letters, digits and
+underscores (`param::is_param_name`): `base_resolution`, `input_2`,
+`attribute_name`. It is what a `ch()` path spells (`../sphere1/radius`),
+what `show_when` conditions name (`operation == Create`), what MCP and
+the code use, and it is NEVER shown in the params pane. The pane shows
+the **label** (`Base Resolution`; the Attribute node's `attribute_name`
+is labelled just `Name`), which may say anything. The label is the
+template's UI metadata like the description: `adopt_ui_from` hands it to
+every instance. Node names follow the same convention, so a path reads
+as one thing.
+
+- **Every template parameter carries both**, and a template whose
+ parameter name is not one is refused at load (`misnamed_params`, beside
+ the unknown-kind refusal). Until this, no template set a label: the
+ name WAS the pane's text, Title Case with spaces, and every `ch()` path
+ had to spell it that way.
+- **`ParamDef::shown_name`** is the one reading of what the pane shows —
+ the label, or the name where there is none (a parameter added by hand
+ or over MCP). The pane's rows are keyed by it, so the write-back,
+ `param_row_at` and the expression tint resolve a row through it;
+ `param_row_at` hands back the NAME. Messages a person reads beside the
+ pane say the label (the refused-value status line, the load's invalid
+ values); an expression's error says the name it was written with.
+- **Format 4** (`Project::migrate_param_names`) renames a save's
+ parameters by `param_name_of` (`Relax in 3D Space` →
+ `relax_in_3d_space`) and every channel path in an expression and in a
+ wrangle's Code with them — the last segment, less a `.x` component.
+ `show_when` is left alone (the merge replaces it, and
+ `page::migrate_preset_rows` recognises an old page by its condition),
+ and so are the retired `meta` / `session` / `utility` nodes, which the
+ meta migration reads by their saved names after it. Format-1–3 steps
+ run before it and so still spell the old names (`rename_attribute`'s
+ `Attributes`). Checked on the user's project and both bundled ones:
+ the old build and the new export the same mesh at frames 1, 30 and 120.
+- **MCP**: `set_param` / `delete_param` take the name, else the label
+ (any case), else `param_name_of` of what was given — so a script
+ written against `Base Resolution` still lands
+ (`param_by_name_or_label`). `add_param` refuses a name that is not one,
+ suggesting the form, and takes an optional `label`.
+- **Under test, `find_param` asserts the name it is asked for is one**,
+ and matches exactly (it was case-insensitive). A reader still spelling
+ a label finds nothing and reads its fallback — a node quietly deaf to
+ a row — so the suite fails it loudly instead. `param_visible` matches
+ names exactly too.
+
+`parameters_have_a_name_and_a_label` is the test.
+
### Parameter kinds and typed values
`src/param.rs` owns `ParamDef`, `ParamKind`, `ParamValue` and `ParamSlot`
@@ -904,7 +953,7 @@ before the row is off, so a saved Transfer carries what it carried.
by default) with From, Attributes, Transfer Groups, Groups and Maximum
Distance rows shown while it is on — since the Remesh became a subnet
(below) that is its `transfer1` child, an ordinary Transfer node whose
-rows are expressions on the subnet's, behind a switch on `chi("../Transfer")`;
+rows are expressions on the subnet's, behind a switch on `chi("../transfer")`;
the native node's copy is `remesh_transfer`: once the mesh is
remeshed, a source's attributes and groups laid over the NEW points by
nearest point — the node's own input when From names nothing, which
@@ -973,16 +1022,16 @@ error)` hands back a clone whose `expr` params are VALUES, at the top of
the kernel path's parent read.
**Paths are Houdini's.** Relative to the node holding the expression: a bare
-name is the node's OWN parameter, `..` its parent, `../sphere1/Radius` a
+name is the node's OWN parameter, `..` its parent, `../sphere1/radius` a
sibling's, a leading `/` the root. `.x` / `.y` / `.z` reads a float3
component. `ch()` / `chf()` read a number (a toggle 1 or 0, a choice its
-option INDEX — `chi("../Method")` is what lets a subnet's dropdown drive a
+option INDEX — `chi("../method")` is what lets a subnet's dropdown drive a
child switch's Index), `chi()` truncates, `chb()` is 1 or 0, `chs()` the
string (a choice's option text). The rest is `+ - * / % ^`, comparisons,
`&& || !`, `$F` (the evaluation's frame), strings with `+`, and a fixed
function set (`if(c, a, b)`, `clamp`, `fit`, `lerp`, `min`/`max`, `rand(seed)`,
the usual math). No ternary — `:` separates a float3's components, which
-are three expressions each (`chf("../a/Size.x"):0:0`). An expression that
+are three expressions each (`chf("../a/size.x"):0:0`). An expression that
reads an expression follows the chain; a circle is an error on the node,
never a stack overflow. The written-back value is formatted for the
TARGET row (`format_for_param`): a number into a toggle is `true`/`false`,
@@ -1005,9 +1054,11 @@ in an older save: an attribute row, a name in a comma list of attributes
(`Attributes`), `@old` in a wrangle's Code as a whole name (not `@Normal`,
not `@UVW`) — and never a choice row, so the Sphere's Method keeps its
`UV` option. Once, by the version, so an attribute someone names `Norm` or
-`UV` afterwards is theirs (`a_save_naming_norm_or_uv_names_n_or_uv`). Templates go through
+`UV` afterwards is theirs (`a_save_naming_norm_or_uv_names_n_or_uv`).
+**Format 4** (the same day): parameter names are identifiers — see "A
+parameter has a name and a label". 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
+(`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
becomes an expression; bare arithmetic does not, and is asked for through
the row menu.
@@ -1022,9 +1073,10 @@ the latter bakes the CURRENT value back as a value, as Delete Channels
does. `copied_param` holds a node ID, not a path, so a rename between copy
and paste still pastes the right path. **Header rows read the
parameter out** (since 2026-09-28, `param_menu_rows`): `Name:` is the
-parameter's name, what a `ch()` path spells, with `Label:` under it only
-when the template gives one (the pane shows the name otherwise, and a
-Label row repeating it would claim there is one), then **what the
+parameter's name, what a `ch()` path spells, with `Label:` under it — what
+the pane shows — when there is one (every template parameter, since the
+names became identifiers; a parameter added without one shows its name),
+then **what the
parameter does** (since 2026-09-30): the template's `description`, a
sentence or two in prose, wrapped to `PARAM_DESCRIPTION_WIDTH` (44)
characters over as many unprefixed rows as it takes, since the menu is as
@@ -1053,7 +1105,7 @@ not fit its kind, shown only then, `Default:`
the template's value as written there
(`State::template_default`, which takes a subnet template's override for
a child inside an instance — the Embryo's `sphere1` defaults its Radius to
-`chf("../Radius")` — and is absent for a parameter no template names),
+`chf("../radius")` — and is absent for a parameter no template names),
then for a slider, float3 or spinbox `Min:` / `Max:` / `Step:` as the
template DECLARES them (`ParamDef::declared_range`, an inline
`slider:-2:2` included, `none` where it says nothing) followed by
@@ -1201,7 +1253,7 @@ way — both in `src/geometry.rs`:
Collision's Collider, the Remesh's From). A wire whose row is hidden
keeps its port and draws no line. An expression wire is drawn to what it
evaluates to at the current frame (`node_wires_at`) — the Remesh subnet's
- transfer reads its From through `if(chs("../From"), …, "input1")` and is
+ transfer reads its From through `if(chs("../from"), …, "input1")` and is
drawn from input1 — and to nothing when that fails. A connection dropped on
port k sets the k-th wire (`State::connect_port`).
`every_wire_is_drawn_into_its_own_port` is the test.
@@ -1274,10 +1326,10 @@ family's first Pre-Simulation operator — "the seed geometry a simulation
starts from" — as a SUBNET of ten ordinary nodes wired the way the HDA's
network is, its controls reaching the children through parameter references
(above). Dive in and the pipeline is there to read, break and reuse: `input1`
-and a `sphere1` (Radius `chf("../Radius")`, Rows and Columns
-`chi("../Base Resolution")`) behind `source1`, a `switch` whose Index is
-`chi("../Source")`; `scatter1` in Surface mode reading the Scatter folder's
-controls, `hull1` behind it, and `method1`, a switch on `chi("../Method")`
+and a `sphere1` (Radius `chf("../radius")`, Rows and Columns
+`chi("../base_resolution")`) behind `source1`, a `switch` whose Index is
+`chi("../source")`; `scatter1` in Surface mode reading the Scatter folder's
+controls, `hull1` behind it, and `method1`, a switch on `chi("../method")`
between the source and the hull; then `relax1` in Repel mode, `subdivide1`,
`normal1`, `output1`. The defaults are the HDA's, and
`embryo_template_builds_a_sphere_a_hull_or_the_input` drives the template
@@ -1329,7 +1381,7 @@ into and its passes read, bypassed and rewired. `nodes/remesh.json`:
```
remesh1 (node) input1 → repeat1 → transfer1 ─┐
└──────────── transfer_switch1 (on Transfer) → output1
-repeat1 (repeat, Iterations = chi("../Iterations"), Stop When Unchanged on)
+repeat1 (repeat, Iterations = chi("../iterations"), Stop When Unchanged on)
input1 → split1 → collapse1 → flip1 → relax1 → project1 → output1
seed1 ───────────────────────────────────────┘ (Surface)
```
diff --git a/nodes/analysis.json b/nodes/analysis.json
index e5bec57..966aede 100644
--- a/nodes/analysis.json
+++ b/nodes/analysis.json
@@ -5,29 +5,33 @@
"outputs": 1,
"params": [
{
- "name": "Input",
+ "name": "input",
+ "label": "Input",
"type": "node",
"default": "",
"description": "The node whose geometry is measured; the results are added as detail attributes and the geometry passed on."
},
{
- "name": "Source",
+ "name": "source",
+ "label": "Source",
"type": "choice:Attribute,Edge Lengths",
"default": "Attribute",
"description": "What is measured: a point attribute's values, or the lengths of every mesh edge."
},
{
- "name": "Attribute",
+ "name": "attribute",
+ "label": "Attribute",
"type": "attribute",
"default": "mass",
- "show_when": "Source == Attribute",
+ "show_when": "source == Attribute",
"description": "The point attribute measured. Results go to detail attributes named after it with _min, _max, _sum, _average, _spread and _count."
},
{
- "name": "Group",
+ "name": "group",
+ "label": "Group",
"type": "group",
"default": "",
- "show_when": "Source == Attribute",
+ "show_when": "source == Attribute",
"description": "A point group limiting which points are measured. Empty means every point.",
"group": "where"
}
diff --git a/nodes/attribute.json b/nodes/attribute.json
index 2ab52f3..238a660 100644
--- a/nodes/attribute.json
+++ b/nodes/attribute.json
@@ -5,175 +5,196 @@
"outputs": 1,
"params": [
{
- "name": "Input",
+ "name": "input",
+ "label": "Input",
"type": "node",
"default": "",
"description": "The node whose geometry this reads; its point attributes are created, changed or deleted and the result passed on."
},
{
- "name": "Operation",
+ "name": "operation",
+ "label": "Operation",
"type": "choice:Create,Modify,Delete,Remap,Clip,Normalize,Composite,Promote",
"default": "Create",
"description": "What is done to the named point attribute: create it, modify it by Value, delete it, remap or clip its range, normalize it, combine it with Source B, or promote it between point and detail."
},
{
- "name": "Attribute Name",
+ "name": "attribute_name",
"label": "Name",
"type": "attribute",
"default": "attr1",
"description": "The point attribute to work on. Pos and Col are the built-in position and colour; they can be modified but not created or deleted."
},
{
- "name": "Type",
+ "name": "type",
+ "label": "Type",
"type": "choice:Float,Float2,Float3,Float4",
"default": "Float",
- "show_when": "Operation == Create",
+ "show_when": "operation == Create",
"description": "How many components the new attribute has: one number, or a vector of two, three or four."
},
{
- "name": "Value From",
+ "name": "value_from",
+ "label": "Value From",
"type": "choice:Constant,Attribute",
"default": "Constant",
- "show_when": "Operation == Create|Modify",
+ "show_when": "operation == Create|Modify",
"description": "Where the value comes from: Constant uses the Value row for every point; Attribute reads each point's own value of From Attribute.",
"group": "value"
},
{
- "name": "From Attribute",
+ "name": "from_attribute",
+ "label": "From Attribute",
"type": "attribute",
"default": "",
- "show_when": "Operation == Create|Modify && Value From == Attribute",
+ "show_when": "operation == Create|Modify && value_from == Attribute",
"description": "The point attribute each point's value is read from when Value From is Attribute: copied as it is when it is as wide as the result, a single number spread to every component, otherwise as many components as fit, the rest zero. Pos and Col name the position and the colour.",
"group": "value"
},
{
- "name": "Value",
+ "name": "value",
+ "label": "Value",
"type": "text",
"default": "1.00",
- "show_when": "Operation == Create|Modify && Value From == Constant",
+ "show_when": "operation == Create|Modify && value_from == Constant",
"description": "The number or numbers written, separated by spaces, commas or colons. One number fills every component; otherwise the count must match the attribute's width.",
"group": "value"
},
{
- "name": "Combine",
+ "name": "combine",
+ "label": "Combine",
"type": "choice:Set,Add,Multiply",
"default": "Set",
- "show_when": "Operation == Modify",
+ "show_when": "operation == Modify",
"description": "How Value meets the current value: Set replaces it, Add adds to it, Multiply multiplies it, componentwise.",
"group": "amount"
},
{
- "name": "Strength",
+ "name": "strength",
+ "label": "Strength",
"type": "slider",
"default": "1.00",
"min": 0.0,
"max": 2.0,
"step": 0.05,
- "show_when": "Operation == Modify",
+ "show_when": "operation == Modify",
"description": "How much of the change lands: 1 is the full change, 0 none, 2 twice as much. Scales the difference the node makes under every Combine.",
"group": "amount"
},
{
- "name": "Scale By",
+ "name": "scale_by",
+ "label": "Scale By",
"type": "attribute",
"default": "",
- "show_when": "Operation == Modify",
+ "show_when": "operation == Modify",
"description": "A point attribute whose value weights the change per point, multiplied with Strength. Empty weights every point equally; a name that is not an attribute is an error.",
"group": "amount"
},
{
- "name": "Per Frame",
+ "name": "per_frame",
+ "label": "Per Frame",
"type": "toggle",
"default": "true",
- "show_when": "Operation == Modify && Combine != Set",
+ "show_when": "operation == Modify && combine != Set",
"description": "Inside a simulation, makes Add and Multiply a rate per frame, so the substep count does not change how far a frame moves. Off lands the full amount every substep.",
"group": "amount"
},
{
- "name": "Group",
+ "name": "group",
+ "label": "Group",
"type": "group",
"default": "",
- "show_when": "Operation != Promote",
+ "show_when": "operation != Promote",
"description": "A point group limiting which points are changed or measured. Empty means every point.",
"group": "where"
},
{
- "name": "From Min",
+ "name": "from_min",
+ "label": "From Min",
"type": "float",
"default": "0.00",
- "show_when": "Operation == Remap|Clip",
+ "show_when": "operation == Remap|Clip",
"description": "Remap: the input value that maps to To Min. Clip: the lower bound values are clamped to.",
"group": "range"
},
{
- "name": "From Max",
+ "name": "from_max",
+ "label": "From Max",
"type": "float",
"default": "1.00",
- "show_when": "Operation == Remap|Clip",
+ "show_when": "operation == Remap|Clip",
"description": "Remap: the input value that maps to To Max. Clip: the upper bound values are clamped to. Must differ from From Min for a remap.",
"group": "range"
},
{
- "name": "To Min",
+ "name": "to_min",
+ "label": "To Min",
"type": "float",
"default": "0.00",
- "show_when": "Operation == Remap",
+ "show_when": "operation == Remap",
"description": "The value that From Min maps to; values between map linearly, and values outside the From range extend past it.",
"group": "range"
},
{
- "name": "To Max",
+ "name": "to_max",
+ "label": "To Max",
"type": "float",
"default": "1.00",
- "show_when": "Operation == Remap|Normalize",
+ "show_when": "operation == Remap|Normalize",
"description": "Remap: the value that From Max maps to. Normalize: the value the chosen measure (sum, maximum or range) is scaled to hit.",
"group": "range"
},
{
- "name": "Source B",
+ "name": "source_b",
+ "label": "Source B",
"type": "attribute",
"default": "",
- "show_when": "Operation == Composite",
+ "show_when": "operation == Composite",
"description": "The second point attribute combined into this one, point by point. A single-number Source B applies to every component.",
"group": "composite"
},
{
- "name": "Combine Op",
+ "name": "combine_op",
+ "label": "Combine Op",
"type": "choice:Add,Subtract,Multiply,Divide,Minimum,Maximum,Average,Difference,Dot,Distance,Length",
"default": "Add",
- "show_when": "Operation == Composite",
+ "show_when": "operation == Composite",
"description": "How Source B is folded in, componentwise. Dot, Distance and Length produce one number written into every component.",
"group": "composite"
},
{
- "name": "To Class",
+ "name": "to_class",
+ "label": "To Class",
"type": "choice:Detail,Point",
"default": "Detail",
- "show_when": "Operation == Promote",
+ "show_when": "operation == Promote",
"description": "Where the attribute is moved: Detail reduces the per-point values to one number for the whole geometry; Point copies a detail value onto every point.",
"group": "promote"
},
{
- "name": "Method",
+ "name": "method",
+ "label": "Method",
"type": "choice:Average,Sum,Minimum,Maximum,First",
"default": "Average",
- "show_when": "Operation == Promote",
+ "show_when": "operation == Promote",
"description": "How point values are reduced to one detail value when promoting to Detail: their average, sum, minimum, maximum or the first point's.",
"group": "promote"
},
{
- "name": "Target",
+ "name": "target",
+ "label": "Target",
"type": "choice:Sum,Maximum,Range",
"default": "Maximum",
- "show_when": "Operation == Normalize",
+ "show_when": "operation == Normalize",
"description": "What is measured before scaling: the values' sum, their maximum, or their range (max minus min). Every value is scaled so this measure equals To Max.",
"group": "normalize"
},
{
- "name": "Kind",
+ "name": "kind",
+ "label": "Kind",
"type": "choice:Live,Derivative",
"default": "Live",
- "show_when": "Operation == Create",
+ "show_when": "operation == Create",
"description": "Live keeps the attribute's values from one simulation step to the next; Derivative zeroes it at the start of every step, for values the chain rebuilds each step.",
"group": "kind"
}
diff --git a/nodes/boolean.json b/nodes/boolean.json
index 90748e8..5a17c82 100644
--- a/nodes/boolean.json
+++ b/nodes/boolean.json
@@ -4,9 +4,9 @@
"inputs": 2,
"outputs": 1,
"params": [
- { "name": "Input", "type": "node", "default": "", "description": "The node whose geometry is the first operand, the shape the second is combined with or cut from." },
- { "name": "With", "type": "node", "default": "", "description": "The node whose geometry is the second operand: joined for Union, kept in common for Intersect, or cut away for Subtract." },
- { "name": "Operation", "type": "choice:Union,Intersect,Subtract", "default": "Union", "description": "Union joins both shapes, Intersect keeps only where they overlap, and Subtract removes the With shape from the Input." },
- { "name": "Voxel Size", "type": "slider", "default": "0.05", "min": 0.005, "max": 1.0, "step": 0.005, "description": "Grid spacing in world units of the distance fields both shapes are sampled on. Smaller gives finer, cleaner edges but costs more.", "group": "resolution" }
+ { "name": "input", "label": "Input", "type": "node", "default": "", "description": "The node whose geometry is the first operand, the shape the second is combined with or cut from." },
+ { "name": "with", "label": "With", "type": "node", "default": "", "description": "The node whose geometry is the second operand: joined for Union, kept in common for Intersect, or cut away for Subtract." },
+ { "name": "operation", "label": "Operation", "type": "choice:Union,Intersect,Subtract", "default": "Union", "description": "Union joins both shapes, Intersect keeps only where they overlap, and Subtract removes the With shape from the Input." },
+ { "name": "voxel_size", "label": "Voxel Size", "type": "slider", "default": "0.05", "min": 0.005, "max": 1.0, "step": 0.005, "description": "Grid spacing in world units of the distance fields both shapes are sampled on. Smaller gives finer, cleaner edges but costs more.", "group": "resolution" }
]
}
diff --git a/nodes/bounds.json b/nodes/bounds.json
index 52ad048..c2cfba6 100644
--- a/nodes/bounds.json
+++ b/nodes/bounds.json
@@ -4,8 +4,8 @@
"inputs": 1,
"outputs": 1,
"params": [
- { "name": "Input", "type": "node", "default": "", "description": "The node whose geometry is measured; its bounding box is added as detail attributes and the geometry passed on." },
- { "name": "Prefix", "type": "text", "default": "bounds", "description": "The start of the four detail attribute names written: <prefix>_min, _max, _size and _center. Empty writes nothing." },
- { "name": "Group", "type": "group", "default": "", "description": "A point group whose points alone are measured. Empty measures every point.", "group": "where" }
+ { "name": "input", "label": "Input", "type": "node", "default": "", "description": "The node whose geometry is measured; its bounding box is added as detail attributes and the geometry passed on." },
+ { "name": "prefix", "label": "Prefix", "type": "text", "default": "bounds", "description": "The start of the four detail attribute names written: <prefix>_min, _max, _size and _center. Empty writes nothing." },
+ { "name": "group", "label": "Group", "type": "group", "default": "", "description": "A point group whose points alone are measured. Empty measures every point.", "group": "where" }
]
}
diff --git a/nodes/box.json b/nodes/box.json
index c4d589d..2c9ca48 100644
--- a/nodes/box.json
+++ b/nodes/box.json
@@ -5,19 +5,22 @@
"outputs": 1,
"params": [
{
- "name": "Scale",
+ "name": "scale",
+ "label": "Scale",
"default": "1.0",
"type": "slider",
"description": "Edge length of the cube in world units; the box extends half this distance from Center on every axis."
},
{
- "name": "Wireframe",
+ "name": "wireframe",
+ "label": "Wireframe",
"default": "false",
"type": "toggle",
"description": "When on, builds only the twelve edges as thin bars and the eight corners as small cubes instead of a solid box, useful as a reference frame."
},
{
- "name": "Center",
+ "name": "center",
+ "label": "Center",
"type": "float3",
"default": "0.00:0.55:0.00",
"description": "Position of the box's center in world units."
diff --git a/nodes/camera.json b/nodes/camera.json
index 3a5a743..ead399e 100644
--- a/nodes/camera.json
+++ b/nodes/camera.json
@@ -5,7 +5,8 @@
"outputs": 0,
"params": [
{
- "name": "Position",
+ "name": "position",
+ "label": "Position",
"type": "float3",
"default": "2.50:1.80:2.50",
"min": -10.0,
@@ -13,7 +14,8 @@
"description": "Where the camera sits in world units. Its direction and distance from Pivot set the starting view; panning the view rewrites it."
},
{
- "name": "Rotation",
+ "name": "rotation",
+ "label": "Rotation",
"type": "float3",
"default": "0.00:0.00:0.00",
"min": -180.0,
@@ -21,7 +23,8 @@
"description": "Extra orbit around the Pivot in degrees: X tilts up or down, Y turns around the vertical axis, Z rolls the view. Dragging the viewport writes it."
},
{
- "name": "Pivot",
+ "name": "pivot",
+ "label": "Pivot",
"type": "float3",
"default": "0.00:0.00:0.00",
"min": -10.0,
diff --git a/nodes/collapse_edges.json b/nodes/collapse_edges.json
index 3461baf..2e8a5b3 100644
--- a/nodes/collapse_edges.json
+++ b/nodes/collapse_edges.json
@@ -5,13 +5,15 @@
"outputs": 1,
"params": [
{
- "name": "Input",
+ "name": "input",
+ "label": "Input",
"type": "node",
"default": "",
"description": "The node whose mesh is coarsened. The surviving point of each collapsed edge keeps its values and joins the other end's groups."
},
{
- "name": "Target Length",
+ "name": "target_length",
+ "label": "Target Length",
"type": "slider",
"default": "0.10",
"min": 0.001,
diff --git a/nodes/collision.json b/nodes/collision.json
index 22634f5..6ad041a 100644
--- a/nodes/collision.json
+++ b/nodes/collision.json
@@ -4,16 +4,18 @@
"inputs": 1,
"outputs": 1,
"params": [
- { "name": "Input", "default": "", "type": "node", "description": "The geometry whose points or primitives are tested against the collider and grouped." },
- { "name": "Collider", "default": "", "type": "node", "description": "The geometry to test against, read as triangles. With nothing connected, or a collider without faces, the input passes through unchanged." },
+ { "name": "input", "label": "Input", "default": "", "type": "node", "description": "The geometry whose points or primitives are tested against the collider and grouped." },
+ { "name": "collider", "label": "Collider", "default": "", "type": "node", "description": "The geometry to test against, read as triangles. With nothing connected, or a collider without faces, the input passes through unchanged." },
{
- "name": "Method",
+ "name": "method",
+ "label": "Method",
"type": "choice:Inside,Proximity",
"default": "Inside",
"description": "Inside marks elements enclosed by the collider, so it needs a closed collider. Proximity marks elements within Distance of the collider's surface."
},
{
- "name": "Distance",
+ "name": "distance",
+ "label": "Distance",
"type": "slider",
"default": "0.05",
"min": 0.0,
@@ -22,22 +24,25 @@
"description": "For Proximity, how near the collider's surface an element must be to count, in world units. Unused by Inside."
},
{
- "name": "Element Type",
+ "name": "element_type",
+ "label": "Element Type",
"type": "choice:Points,Primitives",
"default": "Points",
"description": "Points tests each point. Primitives tests each primitive's centroid and groups the hit primitives and their points.",
"group": "output"
},
- { "name": "Group Name", "default": "collisions", "type": "group", "description": "Name of the group written with the elements that hit. Also written as a primitive group when primitives are tested.", "group": "output" },
+ { "name": "group_name", "label": "Group Name", "default": "collisions", "type": "group", "description": "Name of the group written with the elements that hit. Also written as a primitive group when primitives are tested.", "group": "output" },
{
- "name": "Invert",
+ "name": "invert",
+ "label": "Invert",
"type": "toggle",
"default": "false",
"description": "Groups the elements that did NOT hit the collider instead of those that did.",
"group": "output"
},
{
- "name": "Highlight",
+ "name": "highlight",
+ "label": "Highlight",
"type": "toggle",
"default": "true",
"description": "Tints the grouped points red so the contact shows in the viewport. Off leaves colours untouched.",
diff --git a/nodes/connectivity.json b/nodes/connectivity.json
index 1a48bd6..2cf5b5f 100644
--- a/nodes/connectivity.json
+++ b/nodes/connectivity.json
@@ -4,7 +4,7 @@
"inputs": 1,
"outputs": 1,
"params": [
- { "name": "Input", "type": "node", "default": "", "description": "The node whose geometry is split into connected pieces; the piece number is written per point and the geometry passed on." },
- { "name": "Attribute", "type": "attribute", "default": "piece", "description": "The integer point attribute holding each point's piece number: 0 is the largest piece, 1 the next, and so on. Empty writes nothing." }
+ { "name": "input", "label": "Input", "type": "node", "default": "", "description": "The node whose geometry is split into connected pieces; the piece number is written per point and the geometry passed on." },
+ { "name": "attribute", "label": "Attribute", "type": "attribute", "default": "piece", "description": "The integer point attribute holding each point's piece number: 0 is the largest piece, 1 the next, and so on. Empty writes nothing." }
]
}
diff --git a/nodes/copy.json b/nodes/copy.json
index 58477bb..f40565d 100644
--- a/nodes/copy.json
+++ b/nodes/copy.json
@@ -4,11 +4,11 @@
"inputs": 2,
"outputs": 1,
"params": [
- { "name": "Input", "type": "node", "default": "", "description": "The node whose geometry is copied; one copy is placed at each target point." },
- { "name": "To", "type": "node", "default": "", "description": "The node whose points receive a copy of the Input geometry each. Unresolved, the Input passes through uncopied." },
- { "name": "Orient", "type": "choice:None,Normal", "default": "None", "description": "None keeps every copy upright. Normal turns each copy so its +Y axis follows the target surface's normal at that point." },
- { "name": "Scale", "type": "float", "default": "1.00", "description": "Uniform scale applied to every copy about its own origin before it is placed." },
- { "name": "Scale Attribute", "type": "attribute", "default": "", "description": "Name of a point attribute on the target whose value multiplies Scale per copy, so a field can drive their size. Empty uses Scale alone." },
- { "name": "Group", "type": "group", "default": "", "description": "Point group on the target that receives copies; other points get none. Empty copies onto every target point.", "group": "where" }
+ { "name": "input", "label": "Input", "type": "node", "default": "", "description": "The node whose geometry is copied; one copy is placed at each target point." },
+ { "name": "to", "label": "To", "type": "node", "default": "", "description": "The node whose points receive a copy of the Input geometry each. Unresolved, the Input passes through uncopied." },
+ { "name": "orient", "label": "Orient", "type": "choice:None,Normal", "default": "None", "description": "None keeps every copy upright. Normal turns each copy so its +Y axis follows the target surface's normal at that point." },
+ { "name": "scale", "label": "Scale", "type": "float", "default": "1.00", "description": "Uniform scale applied to every copy about its own origin before it is placed." },
+ { "name": "scale_attribute", "label": "Scale Attribute", "type": "attribute", "default": "", "description": "Name of a point attribute on the target whose value multiplies Scale per copy, so a field can drive their size. Empty uses Scale alone." },
+ { "name": "group", "label": "Group", "type": "group", "default": "", "description": "Point group on the target that receives copies; other points get none. Empty copies onto every target point.", "group": "where" }
]
}
diff --git a/nodes/cull.json b/nodes/cull.json
index b49d15c..9ab40f5 100644
--- a/nodes/cull.json
+++ b/nodes/cull.json
@@ -4,11 +4,11 @@
"inputs": 1,
"outputs": 1,
"params": [
- { "name": "Input", "type": "node", "default": "", "description": "The node whose points are culled; faces using a removed point go with it." },
- { "name": "Group", "type": "group", "default": "", "description": "Point group to cull from; points outside it are kept. Empty considers every point. With Attribute also empty, nothing is removed." },
- { "name": "Attribute", "type": "attribute", "default": "", "description": "Name of a point attribute compared against Threshold to choose which points are removed. Empty removes the whole Group." },
- { "name": "Comparison", "type": "choice:Below,Above", "default": "Below", "description": "Below removes points whose attribute value is less than Threshold; Above removes those whose value is greater." },
- { "name": "Threshold", "type": "float", "default": "0.50", "description": "The value the Attribute is compared against to decide which points are removed." },
- { "name": "Invert", "type": "choice:false,true", "default": "false", "description": "Reverse the selection: keep the points that would have been removed and remove all the others." }
+ { "name": "input", "label": "Input", "type": "node", "default": "", "description": "The node whose points are culled; faces using a removed point go with it." },
+ { "name": "group", "label": "Group", "type": "group", "default": "", "description": "Point group to cull from; points outside it are kept. Empty considers every point. With Attribute also empty, nothing is removed." },
+ { "name": "attribute", "label": "Attribute", "type": "attribute", "default": "", "description": "Name of a point attribute compared against Threshold to choose which points are removed. Empty removes the whole Group." },
+ { "name": "comparison", "label": "Comparison", "type": "choice:Below,Above", "default": "Below", "description": "Below removes points whose attribute value is less than Threshold; Above removes those whose value is greater." },
+ { "name": "threshold", "label": "Threshold", "type": "float", "default": "0.50", "description": "The value the Attribute is compared against to decide which points are removed." },
+ { "name": "invert", "label": "Invert", "type": "choice:false,true", "default": "false", "description": "Reverse the selection: keep the points that would have been removed and remove all the others." }
]
}
diff --git a/nodes/curve.json b/nodes/curve.json
index 12530bf..ec64ae9 100644
--- a/nodes/curve.json
+++ b/nodes/curve.json
@@ -4,8 +4,8 @@
"inputs": 0,
"outputs": 1,
"params": [
- { "name": "Points", "default": "-0.75 0.05 0; -0.25 1.05 0; 0.25 0.05 0; 0.75 1.05 0", "type": "text", "description": "Control points as x y z triples in world coordinates, separated by semicolons. The curve passes through each one; Edit Handles lets you drag them in the viewport." },
- { "name": "Segments", "default": "8", "type": "spinbox", "min": 1, "max": 64, "step": 1, "description": "Number of straight pieces sampled between each pair of control points. Higher values give a smoother curve.", "group": "shape" },
- { "name": "Thickness", "default": "0.02", "type": "slider", "description": "Width of the curve's square cross-section in world units.", "group": "shape" }
+ { "name": "points", "label": "Points", "default": "-0.75 0.05 0; -0.25 1.05 0; 0.25 0.05 0; 0.75 1.05 0", "type": "text", "description": "Control points as x y z triples in world coordinates, separated by semicolons. The curve passes through each one; Edit Handles lets you drag them in the viewport." },
+ { "name": "segments", "label": "Segments", "default": "8", "type": "spinbox", "min": 1, "max": 64, "step": 1, "description": "Number of straight pieces sampled between each pair of control points. Higher values give a smoother curve.", "group": "shape" },
+ { "name": "thickness", "label": "Thickness", "default": "0.02", "type": "slider", "description": "Width of the curve's square cross-section in world units.", "group": "shape" }
]
}
diff --git a/nodes/deform.json b/nodes/deform.json
index 7d39674..0c7bb64 100644
--- a/nodes/deform.json
+++ b/nodes/deform.json
@@ -4,10 +4,10 @@
"inputs": 1,
"outputs": 1,
"params": [
- { "name": "Input", "type": "node", "default": "", "description": "The node whose geometry is twisted, bent or tapered." },
- { "name": "Mode", "type": "choice:Twist,Bend,Taper", "default": "Twist", "description": "Twist rotates slices about the axis, Bend curves the shape along the axis, and Taper scales slices so one end narrows and the other widens." },
- { "name": "Axis", "type": "choice:X,Y,Z", "default": "Y", "description": "The axis the deformation runs along. The middle of the geometry's extent on this axis stays still and the two ends deform in opposite ways." },
- { "name": "Amount", "type": "slider", "default": "1.00", "min": -4.0, "max": 4.0, "step": 0.05, "description": "Strength of the deformation over the geometry's full length: radians of rotation for Twist and Bend, change in scale for Taper. Negative reverses it." },
- { "name": "Group", "type": "group", "default": "", "description": "Point group to deform; other points stay where they are. Empty deforms every point.", "group": "where" }
+ { "name": "input", "label": "Input", "type": "node", "default": "", "description": "The node whose geometry is twisted, bent or tapered." },
+ { "name": "mode", "label": "Mode", "type": "choice:Twist,Bend,Taper", "default": "Twist", "description": "Twist rotates slices about the axis, Bend curves the shape along the axis, and Taper scales slices so one end narrows and the other widens." },
+ { "name": "axis", "label": "Axis", "type": "choice:X,Y,Z", "default": "Y", "description": "The axis the deformation runs along. The middle of the geometry's extent on this axis stays still and the two ends deform in opposite ways." },
+ { "name": "amount", "label": "Amount", "type": "slider", "default": "1.00", "min": -4.0, "max": 4.0, "step": 0.05, "description": "Strength of the deformation over the geometry's full length: radians of rotation for Twist and Bend, change in scale for Taper. Negative reverses it." },
+ { "name": "group", "label": "Group", "type": "group", "default": "", "description": "Point group to deform; other points stay where they are. Empty deforms every point.", "group": "where" }
]
}
diff --git a/nodes/detangle.json b/nodes/detangle.json
index 618d050..3226439 100644
--- a/nodes/detangle.json
+++ b/nodes/detangle.json
@@ -4,15 +4,15 @@
"inputs": 1,
"outputs": 1,
"params": [
- { "name": "Input", "type": "node", "default": "", "description": "The geometry to untangle. Its points are pushed apart where the surface comes closer to itself than the Thickness." },
- { "name": "Method", "type": "choice:Points,Surface", "default": "Points", "description": "Points pushes pairs of nearby points apart. Surface tests points against nearby triangles, and inside a simnet also puts back points that passed through the surface." },
- { "name": "Thickness", "type": "slider", "default": "1.00", "min": 0.0, "max": 8.0, "step": 0.05, "description": "How close the surface may come to itself, in mean edge lengths, so the setting holds after a remesh. Larger keeps sheets further apart; 0 turns the node off." },
- { "name": "Rings", "type": "spinbox", "default": "2", "min": 0.0, "max": 6.0, "step": 1.0, "description": "How many edge rings around each point are excluded from contact, since near neighbours are close by construction. More rings ignore more of the local surface." },
- { "name": "Iterations", "type": "spinbox", "default": "4", "min": 1.0, "max": 32.0, "step": 1.0, "description": "Most separation passes per evaluation. More resolves deeper contacts; the solve stops early once a pass finds nothing to separate." },
- { "name": "Edge Contact", "type": "toggle", "default": "true", "show_when": "Method == Surface", "description": "Also tests edges against nearby edges, catching two edges that cross with no point near a triangle. Much more thorough on dense contact, and slower.", "group": "surface" },
- { "name": "Fold Contact", "type": "toggle", "default": "true", "show_when": "Method == Surface", "description": "Inside a simnet, also catches the surface folding through its own excluded neighbourhood since the step began, and puts it back. Has no effect outside a simnet.", "group": "surface" },
- { "name": "Step Limit", "type": "slider", "default": "0.00", "min": 0.0, "max": 2.0, "step": 0.05, "show_when": "Method == Surface", "description": "Inside a simnet, caps how far each point may move in one step, in thicknesses, before contacts are resolved. 0 means no limit; meant for use with substeps.", "group": "surface" },
- { "name": "Group", "type": "group", "default": "", "description": "Only points in this group are moved; the rest still push but stay put. Empty moves every point.", "group": "where" },
- { "name": "Tangled Group", "type": "group", "default": "", "description": "If set, a point group of this name is written with the points of edges and triangles still crossing each other after the solve. Empty skips this extra search.", "group": "output" }
+ { "name": "input", "label": "Input", "type": "node", "default": "", "description": "The geometry to untangle. Its points are pushed apart where the surface comes closer to itself than the Thickness." },
+ { "name": "method", "label": "Method", "type": "choice:Points,Surface", "default": "Points", "description": "Points pushes pairs of nearby points apart. Surface tests points against nearby triangles, and inside a simnet also puts back points that passed through the surface." },
+ { "name": "thickness", "label": "Thickness", "type": "slider", "default": "1.00", "min": 0.0, "max": 8.0, "step": 0.05, "description": "How close the surface may come to itself, in mean edge lengths, so the setting holds after a remesh. Larger keeps sheets further apart; 0 turns the node off." },
+ { "name": "rings", "label": "Rings", "type": "spinbox", "default": "2", "min": 0.0, "max": 6.0, "step": 1.0, "description": "How many edge rings around each point are excluded from contact, since near neighbours are close by construction. More rings ignore more of the local surface." },
+ { "name": "iterations", "label": "Iterations", "type": "spinbox", "default": "4", "min": 1.0, "max": 32.0, "step": 1.0, "description": "Most separation passes per evaluation. More resolves deeper contacts; the solve stops early once a pass finds nothing to separate." },
+ { "name": "edge_contact", "label": "Edge Contact", "type": "toggle", "default": "true", "show_when": "method == Surface", "description": "Also tests edges against nearby edges, catching two edges that cross with no point near a triangle. Much more thorough on dense contact, and slower.", "group": "surface" },
+ { "name": "fold_contact", "label": "Fold Contact", "type": "toggle", "default": "true", "show_when": "method == Surface", "description": "Inside a simnet, also catches the surface folding through its own excluded neighbourhood since the step began, and puts it back. Has no effect outside a simnet.", "group": "surface" },
+ { "name": "step_limit", "label": "Step Limit", "type": "slider", "default": "0.00", "min": 0.0, "max": 2.0, "step": 0.05, "show_when": "method == Surface", "description": "Inside a simnet, caps how far each point may move in one step, in thicknesses, before contacts are resolved. 0 means no limit; meant for use with substeps.", "group": "surface" },
+ { "name": "group", "label": "Group", "type": "group", "default": "", "description": "Only points in this group are moved; the rest still push but stay put. Empty moves every point.", "group": "where" },
+ { "name": "tangled_group", "label": "Tangled Group", "type": "group", "default": "", "description": "If set, a point group of this name is written with the points of edges and triangles still crossing each other after the solve. Empty skips this extra search.", "group": "output" }
]
}
diff --git a/nodes/develop.json b/nodes/develop.json
index 5fc4203..6a8eef1 100644
--- a/nodes/develop.json
+++ b/nodes/develop.json
@@ -4,11 +4,11 @@
"inputs": 1,
"outputs": 1,
"params": [
- { "name": "Input", "type": "node", "default": "", "description": "The node whose surface is displaced by a growth attribute." },
- { "name": "Attribute", "type": "attribute", "default": "growth", "description": "Name of the point attribute giving how far each point moves; its value times Scale is the distance. Empty leaves the geometry unchanged." },
- { "name": "Scale", "type": "slider", "default": "0.10", "min": -2.0, "max": 2.0, "step": 0.01, "description": "Multiplier on the attribute's value, giving the displacement in world units per unit of attribute. Negative moves points the opposite way." },
- { "name": "Direction", "type": "choice:Normal,Attribute", "default": "Normal", "description": "Normal moves each point along its surface normal. Attribute moves it along a vector attribute named by Source instead." },
- { "name": "Source", "type": "attribute", "default": "", "description": "Name of the vector point attribute used as the direction when Direction is Attribute. Its length also scales the move." },
- { "name": "Group", "type": "group", "default": "", "description": "Point group to displace; other points stay put. Empty displaces every point.", "group": "where" }
+ { "name": "input", "label": "Input", "type": "node", "default": "", "description": "The node whose surface is displaced by a growth attribute." },
+ { "name": "attribute", "label": "Attribute", "type": "attribute", "default": "growth", "description": "Name of the point attribute giving how far each point moves; its value times Scale is the distance. Empty leaves the geometry unchanged." },
+ { "name": "scale", "label": "Scale", "type": "slider", "default": "0.10", "min": -2.0, "max": 2.0, "step": 0.01, "description": "Multiplier on the attribute's value, giving the displacement in world units per unit of attribute. Negative moves points the opposite way." },
+ { "name": "direction", "label": "Direction", "type": "choice:Normal,Attribute", "default": "Normal", "description": "Normal moves each point along its surface normal. Attribute moves it along a vector attribute named by Source instead." },
+ { "name": "source", "label": "Source", "type": "attribute", "default": "", "description": "Name of the vector point attribute used as the direction when Direction is Attribute. Its length also scales the move." },
+ { "name": "group", "label": "Group", "type": "group", "default": "", "description": "Point group to displace; other points stay put. Empty displaces every point.", "group": "where" }
]
}
diff --git a/nodes/distance.json b/nodes/distance.json
index 5ee7d81..9d2bbc4 100644
--- a/nodes/distance.json
+++ b/nodes/distance.json
@@ -4,11 +4,11 @@
"inputs": 2,
"outputs": 1,
"params": [
- { "name": "Input", "type": "node", "default": "", "description": "The geometry whose points are measured; each point gets its distance to the nearest point on the To surface." },
- { "name": "To", "type": "node", "default": "", "description": "The geometry to measure to, read as a surface. If it cannot be found, the input passes through and an error is reported." },
- { "name": "Attribute", "type": "attribute", "default": "dist", "description": "Name of the float point attribute written with each point's distance. Empty skips writing it." },
- { "name": "Direction", "type": "attribute", "default": "", "description": "If set, a vector point attribute of this name is written with the unit direction from each point toward its nearest surface point. Empty skips it." },
- { "name": "Signed", "type": "choice:false,true", "default": "false", "description": "When true, points behind the nearest face (inside the surface) get negative distances. Judged by that face's normal, so it can misread in concave creases." },
- { "name": "Maximum", "type": "slider", "default": "0.00", "min": 0.0, "max": 10.0, "step": 0.05, "description": "Clamps the distance to at most this many world units, keeping a falloff bounded. 0 means no clamp." }
+ { "name": "input", "label": "Input", "type": "node", "default": "", "description": "The geometry whose points are measured; each point gets its distance to the nearest point on the To surface." },
+ { "name": "to", "label": "To", "type": "node", "default": "", "description": "The geometry to measure to, read as a surface. If it cannot be found, the input passes through and an error is reported." },
+ { "name": "attribute", "label": "Attribute", "type": "attribute", "default": "dist", "description": "Name of the float point attribute written with each point's distance. Empty skips writing it." },
+ { "name": "direction", "label": "Direction", "type": "attribute", "default": "", "description": "If set, a vector point attribute of this name is written with the unit direction from each point toward its nearest surface point. Empty skips it." },
+ { "name": "signed", "label": "Signed", "type": "choice:false,true", "default": "false", "description": "When true, points behind the nearest face (inside the surface) get negative distances. Judged by that face's normal, so it can misread in concave creases." },
+ { "name": "maximum", "label": "Maximum", "type": "slider", "default": "0.00", "min": 0.0, "max": 10.0, "step": 0.05, "description": "Clamps the distance to at most this many world units, keeping a falloff bounded. 0 means no clamp." }
]
}
diff --git a/nodes/embryo.json b/nodes/embryo.json
index 01ad6a9..9054ed0 100644
--- a/nodes/embryo.json
+++ b/nodes/embryo.json
@@ -5,118 +5,131 @@
"outputs": 1,
"params": [
{
- "name": "Input",
+ "name": "input",
+ "label": "Input",
"type": "node",
"default": "",
"description": "Node whose geometry is used as the seed shape when Source is set to Input."
},
{
- "name": "Source",
+ "name": "source",
+ "label": "Source",
"type": "choice:Internal,Input",
"default": "Internal",
"description": "Starting shape: Internal builds a UV sphere from Radius and Base Resolution; Input uses the geometry wired into Input."
},
{
- "name": "Method",
+ "name": "method",
+ "label": "Method",
"type": "choice:Basic,Scatter",
"default": "Basic",
"description": "Basic passes the source shape on as it is; Scatter scatters points over its surface and wraps them in a convex hull, giving an evenly spaced mesh."
},
{
- "name": "Base Resolution",
+ "name": "base_resolution",
+ "label": "Base Resolution",
"type": "spinbox",
"default": "50",
"min": 3,
"max": 100,
"step": 1,
- "show_when": "Source == Internal",
+ "show_when": "source == Internal",
"description": "Rows and columns of the internal UV sphere. Higher values give a smoother, denser seed.",
"group": "source"
},
{
- "name": "Radius",
+ "name": "radius",
+ "label": "Radius",
"type": "slider",
"default": "0.5",
"min": 0.01,
"max": 10.0,
"step": 0.01,
- "show_when": "Source == Internal",
+ "show_when": "source == Internal",
"description": "Radius of the internal sphere in world units, centered at the origin.",
"group": "source"
},
{
- "name": "Scatter Count",
+ "name": "scatter_count",
+ "label": "Scatter Count",
"type": "spinbox",
"default": "1000",
"min": 10,
"max": 10000,
"step": 10,
- "show_when": "Method == Scatter",
+ "show_when": "method == Scatter",
"description": "Number of points scattered over the source surface before the hull is built. More points give a finer hull mesh.",
"group": "scatter"
},
{
- "name": "Scatter Seed",
+ "name": "scatter_seed",
+ "label": "Scatter Seed",
"type": "slider",
"default": "1.1",
"min": 0.0,
"max": 10.0,
"step": 0.1,
- "show_when": "Method == Scatter",
+ "show_when": "method == Scatter",
"description": "Random seed for the scatter. Change it for a different but equally spaced point layout.",
"group": "scatter"
},
{
- "name": "Relax Points",
+ "name": "relax_points",
+ "label": "Relax Points",
"type": "toggle",
"default": "true",
- "show_when": "Method == Scatter",
+ "show_when": "method == Scatter",
"description": "When on, pushes the scattered points apart across the surface so they are evenly spaced before the hull is built.",
"group": "scatter"
},
{
- "name": "Scatter Relax Iterations",
+ "name": "scatter_relax_iterations",
+ "label": "Scatter Relax Iterations",
"type": "spinbox",
"default": "50",
"min": 0,
"max": 100,
"step": 1,
- "show_when": "Method == Scatter && Relax Points == true",
+ "show_when": "method == Scatter && relax_points == true",
"description": "How many push-apart passes the scattered points get. More passes give a more even spacing; 0 does none.",
"group": "scatter"
},
{
- "name": "Scale Radii By",
+ "name": "scale_radii_by",
+ "label": "Scale Radii By",
"type": "slider",
"default": "1.248",
"min": 0.0,
"max": 2.0,
"step": 0.001,
- "show_when": "Method == Scatter && Relax Points == true",
+ "show_when": "method == Scatter && relax_points == true",
"description": "Multiplier on each scattered point's push radius, which is derived from the surface area per point. Larger values spread points more forcefully.",
"group": "scatter"
},
{
- "name": "Use Max Relax Radius",
+ "name": "use_max_relax_radius",
+ "label": "Use Max Relax Radius",
"type": "toggle",
"default": "true",
- "show_when": "Method == Scatter && Relax Points == true",
+ "show_when": "method == Scatter && relax_points == true",
"description": "When on, the scattered points' push radius is capped at Scatter Relax Radius.",
"group": "scatter"
},
{
- "name": "Scatter Relax Radius",
+ "name": "scatter_relax_radius",
+ "label": "Scatter Relax Radius",
"type": "slider",
"default": "10",
"min": 0.0,
"max": 100.0,
"step": 0.1,
- "show_when": "Method == Scatter && Relax Points == true && Use Max Relax Radius == true",
+ "show_when": "method == Scatter && relax_points == true && use_max_relax_radius == true",
"description": "Upper limit on the scattered points' push radius in world units, used when Use Max Relax Radius is on.",
"group": "scatter"
},
{
- "name": "Relax Iterations",
+ "name": "relax_iterations",
+ "label": "Relax Iterations",
"type": "spinbox",
"default": "0",
"min": 0,
@@ -126,7 +139,8 @@
"group": "relax"
},
{
- "name": "Relax Radius",
+ "name": "relax_radius",
+ "label": "Relax Radius",
"type": "slider",
"default": "1",
"min": 0.001,
@@ -136,14 +150,16 @@
"group": "relax"
},
{
- "name": "Relax in 3D Space",
+ "name": "relax_in_3d_space",
+ "label": "Relax in 3D Space",
"type": "toggle",
"default": "false",
"description": "When off, the final relax slides points along the surface to keep its shape; when on, points may move freely in 3D.",
"group": "relax"
},
{
- "name": "Subdivision Depth",
+ "name": "subdivision_depth",
+ "label": "Subdivision Depth",
"type": "spinbox",
"default": "0",
"min": 0,
@@ -173,22 +189,22 @@
],
"params": [
{
- "name": "Radius",
- "default": "chf(\"../Radius\")",
+ "name": "radius",
+ "default": "chf(\"../radius\")",
"expr": true
},
{
- "name": "Rows",
- "default": "chi(\"../Base Resolution\")",
+ "name": "rows",
+ "default": "chi(\"../base_resolution\")",
"expr": true
},
{
- "name": "Columns",
- "default": "chi(\"../Base Resolution\")",
+ "name": "columns",
+ "default": "chi(\"../base_resolution\")",
"expr": true
},
{
- "name": "Center Y",
+ "name": "center_y",
"default": "0.0"
}
],
@@ -203,16 +219,16 @@
],
"params": [
{
- "name": "Input",
+ "name": "input",
"default": "sphere1"
},
{
- "name": "Input 2",
+ "name": "input_2",
"default": "input1"
},
{
- "name": "Index",
- "default": "chi(\"../Source\")",
+ "name": "index",
+ "default": "chi(\"../source\")",
"expr": true
}
],
@@ -227,50 +243,50 @@
],
"params": [
{
- "name": "Input",
+ "name": "input",
"default": "source1"
},
{
- "name": "Mode",
+ "name": "mode",
"default": "Surface"
},
{
- "name": "Points",
- "default": "chi(\"../Scatter Count\")",
+ "name": "points",
+ "default": "chi(\"../scatter_count\")",
"expr": true
},
{
- "name": "Seed",
- "default": "chf(\"../Scatter Seed\")",
+ "name": "seed",
+ "default": "chf(\"../scatter_seed\")",
"expr": true
},
{
- "name": "Relax Points",
- "default": "chb(\"../Relax Points\")",
+ "name": "relax_points",
+ "default": "chb(\"../relax_points\")",
"expr": true
},
{
- "name": "Relax Iterations",
- "default": "chi(\"../Scatter Relax Iterations\")",
+ "name": "relax_iterations",
+ "default": "chi(\"../scatter_relax_iterations\")",
"expr": true
},
{
- "name": "Scale Radii By",
- "default": "chf(\"../Scale Radii By\")",
+ "name": "scale_radii_by",
+ "default": "chf(\"../scale_radii_by\")",
"expr": true
},
{
- "name": "Use Max Relax Radius",
- "default": "chb(\"../Use Max Relax Radius\")",
+ "name": "use_max_relax_radius",
+ "default": "chb(\"../use_max_relax_radius\")",
"expr": true
},
{
- "name": "Max Relax Radius",
- "default": "chf(\"../Scatter Relax Radius\")",
+ "name": "max_relax_radius",
+ "default": "chf(\"../scatter_relax_radius\")",
"expr": true
},
{
- "name": "Markers",
+ "name": "markers",
"default": "false"
}
],
@@ -285,7 +301,7 @@
],
"params": [
{
- "name": "Input",
+ "name": "input",
"default": "scatter1"
}
],
@@ -300,16 +316,16 @@
],
"params": [
{
- "name": "Input",
+ "name": "input",
"default": "source1"
},
{
- "name": "Input 2",
+ "name": "input_2",
"default": "hull1"
},
{
- "name": "Index",
- "default": "chi(\"../Method\")",
+ "name": "index",
+ "default": "chi(\"../method\")",
"expr": true
}
],
@@ -324,26 +340,26 @@
],
"params": [
{
- "name": "Input",
+ "name": "input",
"default": "method1"
},
{
- "name": "Mode",
+ "name": "mode",
"default": "Repel"
},
{
- "name": "Iterations",
- "default": "chi(\"../Relax Iterations\")",
+ "name": "iterations",
+ "default": "chi(\"../relax_iterations\")",
"expr": true
},
{
- "name": "Radius",
- "default": "chf(\"../Relax Radius\")",
+ "name": "radius",
+ "default": "chf(\"../relax_radius\")",
"expr": true
},
{
- "name": "In 3D Space",
- "default": "chb(\"../Relax in 3D Space\")",
+ "name": "in_3d_space",
+ "default": "chb(\"../relax_in_3d_space\")",
"expr": true
}
],
@@ -358,12 +374,12 @@
],
"params": [
{
- "name": "Input",
+ "name": "input",
"default": "relax1"
},
{
- "name": "Depth",
- "default": "chi(\"../Subdivision Depth\")",
+ "name": "depth",
+ "default": "chi(\"../subdivision_depth\")",
"expr": true
}
],
@@ -378,7 +394,7 @@
],
"params": [
{
- "name": "Input",
+ "name": "input",
"default": "subdivide1"
}
],
@@ -393,7 +409,7 @@
],
"params": [
{
- "name": "Input",
+ "name": "input",
"default": "normal1"
}
],
diff --git a/nodes/export.json b/nodes/export.json
index f2701d7..cdb816d 100644
--- a/nodes/export.json
+++ b/nodes/export.json
@@ -4,10 +4,10 @@
"inputs": 1,
"outputs": 1,
"params": [
- { "name": "Input", "type": "node", "default": "", "description": "The node whose output is written. A page chain is written as a PNG; geometry is written in the mesh format chosen by Format. Passed through unchanged." },
- { "name": "File", "type": "text", "default": "", "description": "Path of the file to write; ~/ means your home folder and missing folders are created. An existing file is overwritten. Required before exporting." },
- { "name": "Format", "type": "choice:STL,STL (ASCII),OBJ", "default": "STL", "description": "Mesh format for geometry: binary STL, ASCII STL (readable text, larger), or OBJ, which keeps shared points and quads. Ignored for pages, always PNG." },
- { "name": "Scale", "type": "slider", "default": "1.00", "min": 0.001, "max": 100.0, "step": 0.01, "description": "Multiplies every coordinate as the mesh is written, for example to convert units for a slicer. Does not affect the scene, or a page's PNG." },
- { "name": "Export", "type": "button", "default": "", "description": "Writes the input to File now, at the current frame, and reports the result on the status line. Nothing is written when the node is merely evaluated.", "group": "action" }
+ { "name": "input", "label": "Input", "type": "node", "default": "", "description": "The node whose output is written. A page chain is written as a PNG; geometry is written in the mesh format chosen by Format. Passed through unchanged." },
+ { "name": "file", "label": "File", "type": "text", "default": "", "description": "Path of the file to write; ~/ means your home folder and missing folders are created. An existing file is overwritten. Required before exporting." },
+ { "name": "format", "label": "Format", "type": "choice:STL,STL (ASCII),OBJ", "default": "STL", "description": "Mesh format for geometry: binary STL, ASCII STL (readable text, larger), or OBJ, which keeps shared points and quads. Ignored for pages, always PNG." },
+ { "name": "scale", "label": "Scale", "type": "slider", "default": "1.00", "min": 0.001, "max": 100.0, "step": 0.01, "description": "Multiplies every coordinate as the mesh is written, for example to convert units for a slicer. Does not affect the scene, or a page's PNG." },
+ { "name": "export", "label": "Export", "type": "button", "default": "", "description": "Writes the input to File now, at the current frame, and reports the result on the status line. Nothing is written when the node is merely evaluated.", "group": "action" }
]
}
diff --git a/nodes/extrude.json b/nodes/extrude.json
index 53df1b9..6b1b492 100644
--- a/nodes/extrude.json
+++ b/nodes/extrude.json
@@ -5,13 +5,15 @@
"outputs": 1,
"params": [
{
- "name": "Input",
+ "name": "input",
+ "label": "Input",
"default": "",
"type": "node",
"description": "The node whose surface is extruded as a whole: its points move along their normals and walls rise from its open edges."
},
{
- "name": "Distance",
+ "name": "distance",
+ "label": "Distance",
"default": "0.2",
"type": "slider",
"min": -1.0,
@@ -20,7 +22,8 @@
"description": "How far, in world units, each point moves along its point normal to form the top. Negative extrudes inward, against the normals."
},
{
- "name": "Keep Base",
+ "name": "keep_base",
+ "label": "Keep Base",
"default": "true",
"type": "toggle",
"description": "Keep the original faces, reversed, as the bottom so a sheet becomes a closed slab. Off leaves only the top and the side walls."
diff --git a/nodes/flip_edges.json b/nodes/flip_edges.json
index 55d7575..d2a9ae4 100644
--- a/nodes/flip_edges.json
+++ b/nodes/flip_edges.json
@@ -5,13 +5,15 @@
"outputs": 1,
"params": [
{
- "name": "Input",
+ "name": "input",
+ "label": "Input",
"type": "node",
"default": "",
"description": "The node whose mesh has its edges flipped to even out the triangles."
},
{
- "name": "Target Length",
+ "name": "target_length",
+ "label": "Target Length",
"type": "slider",
"default": "0.10",
"min": 0.001,
diff --git a/nodes/grid.json b/nodes/grid.json
index f9086b0..c054e16 100644
--- a/nodes/grid.json
+++ b/nodes/grid.json
@@ -4,10 +4,10 @@
"inputs": 0,
"outputs": 1,
"params": [
- { "name": "Rows", "type": "spinbox", "default": "10", "min": 1.0, "max": 500.0, "step": 1.0, "description": "Number of quad divisions along the length (Z). More rows give a finer grid." },
- { "name": "Columns", "type": "spinbox", "default": "10", "min": 1.0, "max": 500.0, "step": 1.0, "description": "Number of quad divisions across the width (X). More columns give a finer grid." },
- { "name": "Width", "type": "slider", "default": "1.00", "min": 0.01, "max": 20.0, "step": 0.05, "description": "Size of the grid along X in world units.", "group": "size" },
- { "name": "Length", "type": "slider", "default": "1.00", "min": 0.01, "max": 20.0, "step": 0.05, "description": "Size of the grid along Z in world units.", "group": "size" },
- { "name": "Center", "type": "float3", "default": "0.00:0.00:0.00", "description": "Position of the grid's center in world units; the grid lies flat in the XZ plane through this point.", "group": "size" }
+ { "name": "rows", "label": "Rows", "type": "spinbox", "default": "10", "min": 1.0, "max": 500.0, "step": 1.0, "description": "Number of quad divisions along the length (Z). More rows give a finer grid." },
+ { "name": "columns", "label": "Columns", "type": "spinbox", "default": "10", "min": 1.0, "max": 500.0, "step": 1.0, "description": "Number of quad divisions across the width (X). More columns give a finer grid." },
+ { "name": "width", "label": "Width", "type": "slider", "default": "1.00", "min": 0.01, "max": 20.0, "step": 0.05, "description": "Size of the grid along X in world units.", "group": "size" },
+ { "name": "length", "label": "Length", "type": "slider", "default": "1.00", "min": 0.01, "max": 20.0, "step": 0.05, "description": "Size of the grid along Z in world units.", "group": "size" },
+ { "name": "center", "label": "Center", "type": "float3", "default": "0.00:0.00:0.00", "description": "Position of the grid's center in world units; the grid lies flat in the XZ plane through this point.", "group": "size" }
]
}
diff --git a/nodes/group.json b/nodes/group.json
index ae873c3..1f2adef 100644
--- a/nodes/group.json
+++ b/nodes/group.json
@@ -5,126 +5,141 @@
"outputs": 1,
"params": [
{
- "name": "Input",
+ "name": "input",
+ "label": "Input",
"type": "node",
"default": "",
"description": "The node whose geometry this reads; the selected elements are added to the named group and the geometry passed on."
},
{
- "name": "Group Name",
+ "name": "group_name",
+ "label": "Group Name",
"type": "group",
"default": "group1",
"description": "The name of the group the selection is written to, which downstream nodes can pick by name."
},
{
- "name": "Element Type",
+ "name": "element_type",
+ "label": "Element Type",
"type": "choice:Points,Primitives,Edges",
"default": "Points",
- "show_when": "Mode != Expand|Attribute",
+ "show_when": "mode != Expand|Attribute",
"description": "What is selected: points, primitives (tested at their centre, with their points), or edges (both ends must match; their points are grouped)."
},
{
- "name": "Mode",
+ "name": "mode",
+ "label": "Mode",
"type": "choice:Box,Random,Attribute,Expand",
"default": "Box",
"description": "How elements are chosen: inside a box, a random count, by comparing an attribute to a threshold, or by growing or shrinking an existing group."
},
{
- "name": "Count",
+ "name": "count",
+ "label": "Count",
"type": "spinbox",
"default": "1",
"min": 1.0,
"max": 500.0,
"step": 1.0,
- "show_when": "Mode == Random",
+ "show_when": "mode == Random",
"description": "How many elements are picked at random. More than there are selects them all.",
"group": "mode"
},
{
- "name": "Seed",
+ "name": "seed",
+ "label": "Seed",
"type": "spinbox",
"default": "0",
"min": 0.0,
"max": 999.0,
"step": 1.0,
- "show_when": "Mode == Random",
+ "show_when": "mode == Random",
"description": "Changes which elements are picked at random; the same seed always picks the same ones.",
"group": "mode"
},
{
- "name": "Center",
+ "name": "center",
+ "label": "Center",
"type": "float3",
"default": "0.00:0.00:0.00",
"min": -10.0,
"max": 10.0,
- "show_when": "Mode == Box",
+ "show_when": "mode == Box",
"description": "The centre of the selection box, in world units.",
"group": "mode"
},
{
- "name": "Size",
+ "name": "size",
+ "label": "Size",
"type": "float3",
"default": "1.00:1.00:1.00",
"min": 0.0,
"max": 20.0,
- "show_when": "Mode == Box",
+ "show_when": "mode == Box",
"description": "The full width, height and depth of the selection box, in world units. Elements inside or on its faces are selected.",
"group": "mode"
},
{
- "name": "Invert",
+ "name": "invert",
+ "label": "Invert",
"type": "toggle",
"default": "false",
"description": "Selects everything the mode would not select, and nothing it would.",
"group": "output"
},
{
- "name": "Highlight",
+ "name": "highlight",
+ "label": "Highlight",
"type": "toggle",
"default": "true",
"description": "Tints the selected points amber so the group can be seen in the viewport. Changes their colour in the output.",
"group": "output"
},
{
- "name": "Attribute",
+ "name": "attribute",
+ "label": "Attribute",
"type": "attribute",
"default": "",
- "show_when": "Mode == Attribute",
+ "show_when": "mode == Attribute",
"description": "The point attribute compared against Threshold; points without it are not selected.",
"group": "mode"
},
{
- "name": "Comparison",
+ "name": "comparison",
+ "label": "Comparison",
"type": "choice:Below,Above",
"default": "Above",
- "show_when": "Mode == Attribute",
+ "show_when": "mode == Attribute",
"description": "Whether points whose attribute is below or above Threshold are selected. A value equal to Threshold is never selected.",
"group": "mode"
},
{
- "name": "Threshold",
+ "name": "threshold",
+ "label": "Threshold",
"type": "float",
"default": "0.50",
- "show_when": "Mode == Attribute",
+ "show_when": "mode == Attribute",
"description": "The value the attribute is compared against.",
"group": "mode"
},
{
- "name": "Source Group",
+ "name": "source_group",
+ "label": "Source Group",
"type": "group",
"default": "",
- "show_when": "Mode == Expand",
+ "show_when": "mode == Expand",
"description": "The existing point group that is grown or shrunk across the mesh.",
"group": "mode"
},
{
- "name": "Rings",
+ "name": "rings",
+ "label": "Rings",
"type": "spinbox",
"default": "1",
"min": -8.0,
"max": 8.0,
"step": 1.0,
- "show_when": "Mode == Expand",
+ "show_when": "mode == Expand",
"description": "How many rings of neighbouring points the source group grows by. Negative shrinks it by peeling its boundary; 0 copies it unchanged.",
"group": "mode"
}
diff --git a/nodes/hull.json b/nodes/hull.json
index 17ee5fb..f33af57 100644
--- a/nodes/hull.json
+++ b/nodes/hull.json
@@ -4,6 +4,6 @@
"inputs": 1,
"outputs": 1,
"params": [
- { "name": "Input", "type": "node", "default": "", "description": "Node whose points are wrapped in a convex hull. Input points that are flat or in a line cannot enclose a volume and pass through unchanged." }
+ { "name": "input", "label": "Input", "type": "node", "default": "", "description": "Node whose points are wrapped in a convex hull. Input points that are flat or in a line cannot enclose a volume and pass through unchanged." }
]
}
diff --git a/nodes/line.json b/nodes/line.json
index 1d2b12b..d4ad919 100644
--- a/nodes/line.json
+++ b/nodes/line.json
@@ -4,7 +4,7 @@
"inputs": 0,
"outputs": 1,
"params": [
- { "name": "Length", "default": "1.0", "type": "slider", "description": "Height of the vertical bar in world units, measured up the Y axis from its start. The start is placed automatically by the node's order in the network." },
- { "name": "Thickness", "default": "0.02", "type": "slider", "description": "Width of the bar's square cross-section in world units." }
+ { "name": "length", "label": "Length", "default": "1.0", "type": "slider", "description": "Height of the vertical bar in world units, measured up the Y axis from its start. The start is placed automatically by the node's order in the network." },
+ { "name": "thickness", "label": "Thickness", "default": "0.02", "type": "slider", "description": "Width of the bar's square cross-section in world units." }
]
}
diff --git a/nodes/mold_shell.json b/nodes/mold_shell.json
index 82361b5..4b64fda 100644
--- a/nodes/mold_shell.json
+++ b/nodes/mold_shell.json
@@ -4,10 +4,10 @@
"inputs": 1,
"outputs": 1,
"params": [
- { "name": "Input", "type": "node", "default": "", "description": "The node whose surface is the cast; a shell is built inward from it with a thickness that follows its curvature." },
- { "name": "Maximum Thickness", "type": "slider", "default": "0.75", "min": 0.001, "max": 5.0, "step": 0.01, "description": "Shell thickness in world units where the surface is most concave, cupping inward, where a mold needs the most material." },
- { "name": "Minimum Thickness", "type": "slider", "default": "0.60", "min": 0.001, "max": 5.0, "step": 0.01, "description": "Shell thickness in world units where the surface is most convex, bulging outward." },
- { "name": "Remesh Division Size", "type": "slider", "default": "0.90", "min": 0.01, "max": 5.0, "step": 0.01, "description": "Edge length in world units the surface is remeshed to before thickening, which sets how finely the thickness can vary.", "group": "remesh" },
- { "name": "Ramp", "type": "choice:Linear,Smooth,Constant", "default": "Linear", "description": "How curvature maps to thickness between the minimum and maximum. Linear is proportional, Smooth eases at both ends, Constant uses the maximum everywhere." }
+ { "name": "input", "label": "Input", "type": "node", "default": "", "description": "The node whose surface is the cast; a shell is built inward from it with a thickness that follows its curvature." },
+ { "name": "maximum_thickness", "label": "Maximum Thickness", "type": "slider", "default": "0.75", "min": 0.001, "max": 5.0, "step": 0.01, "description": "Shell thickness in world units where the surface is most concave, cupping inward, where a mold needs the most material." },
+ { "name": "minimum_thickness", "label": "Minimum Thickness", "type": "slider", "default": "0.60", "min": 0.001, "max": 5.0, "step": 0.01, "description": "Shell thickness in world units where the surface is most convex, bulging outward." },
+ { "name": "remesh_division_size", "label": "Remesh Division Size", "type": "slider", "default": "0.90", "min": 0.01, "max": 5.0, "step": 0.01, "description": "Edge length in world units the surface is remeshed to before thickening, which sets how finely the thickness can vary.", "group": "remesh" },
+ { "name": "ramp", "label": "Ramp", "type": "choice:Linear,Smooth,Constant", "default": "Linear", "description": "How curvature maps to thickness between the minimum and maximum. Linear is proportional, Smooth eases at both ends, Constant uses the maximum everywhere." }
]
}
diff --git a/nodes/neighbour.json b/nodes/neighbour.json
index c88ad04..9787512 100644
--- a/nodes/neighbour.json
+++ b/nodes/neighbour.json
@@ -5,55 +5,62 @@
"outputs": 1,
"params": [
{
- "name": "Input",
+ "name": "input",
+ "label": "Input",
"type": "node",
"default": "",
"description": "The geometry whose point attribute is changed by comparing each point with the points around it."
},
{
- "name": "Attribute",
+ "name": "attribute",
+ "label": "Attribute",
"type": "attribute",
"default": "mass",
"description": "The point attribute to operate on; must already exist. Works on floats, integers and vectors; integers stay whole."
},
{
- "name": "Mode",
+ "name": "mode",
+ "label": "Mode",
"type": "choice:Diffuse,Concentrate,Migrate,Bleed,Align,Lead,Charge",
"default": "Diffuse",
"description": "Diffuse smooths toward neighbours, Concentrate sharpens away from them, Migrate moves value along Direction, Bleed decays to zero, Align and Lead turn vectors, Charge accumulates and discharges."
},
{
- "name": "Neighbourhood",
+ "name": "neighbourhood",
+ "label": "Neighbourhood",
"type": "choice:Connectivity,Radius,Global",
"default": "Connectivity",
- "show_when": "Mode != Bleed",
+ "show_when": "mode != Bleed",
"description": "Which points count as neighbours: those within Rings edges on the mesh, those within Radius in space (across folds too), or every point.",
"group": "neighbourhood"
},
{
- "name": "Rings",
+ "name": "rings",
+ "label": "Rings",
"type": "spinbox",
"default": "1",
"min": 1.0,
"max": 8.0,
"step": 1.0,
- "show_when": "Neighbourhood == Connectivity && Mode != Bleed",
+ "show_when": "neighbourhood == Connectivity && mode != Bleed",
"description": "How many edge rings out from a point count as its neighbours. More rings spread the effect wider per evaluation.",
"group": "neighbourhood"
},
{
- "name": "Radius",
+ "name": "radius",
+ "label": "Radius",
"type": "slider",
"default": "0.20",
"min": 0.0,
"max": 2.0,
"step": 0.01,
- "show_when": "Neighbourhood == Radius && Mode != Bleed",
+ "show_when": "neighbourhood == Radius && mode != Bleed",
"description": "Distance in world units within which points count as neighbours, regardless of mesh connectivity.",
"group": "neighbourhood"
},
{
- "name": "Amount",
+ "name": "amount",
+ "label": "Amount",
"type": "slider",
"default": "0.50",
"min": 0.0,
@@ -63,52 +70,58 @@
"group": "amount"
},
{
- "name": "Direction",
+ "name": "direction",
+ "label": "Direction",
"type": "attribute",
"default": "",
- "show_when": "Mode == Migrate",
+ "show_when": "mode == Migrate",
"description": "A vector point attribute giving the direction value flows in Migrate mode; neighbours in front of each point receive what it gives up.",
"group": "amount"
},
{
- "name": "Group",
+ "name": "group",
+ "label": "Group",
"type": "group",
"default": "",
"description": "Only points in this group are changed; neighbours outside it are still read. Empty changes every point.",
"group": "where"
},
{
- "name": "Target",
+ "name": "target",
+ "label": "Target",
"type": "choice:Local Average,Global Average,Constant,Attribute,Surface Tangent",
"default": "Local Average",
- "show_when": "Mode == Align",
+ "show_when": "mode == Align",
"description": "What vectors turn toward: the neighbours' average, the whole geometry's average, the Constant vector, the Source attribute, or the vector's own projection onto the surface.",
"group": "mode"
},
{
- "name": "Constant",
+ "name": "constant",
+ "label": "Constant",
"type": "float3",
"default": "0.00:1.00:0.00",
- "show_when": "Mode == Align && Target == Constant",
+ "show_when": "mode == Align && target == Constant",
"description": "The fixed direction vectors turn toward when Target is Constant.",
"group": "mode"
},
{
- "name": "Source",
+ "name": "source",
+ "label": "Source",
"type": "attribute",
"default": "",
- "show_when": "Mode == Align|Lead",
+ "show_when": "mode == Align|Lead",
"description": "In Align with Target set to Attribute, the vector attribute to turn toward. In Lead, a number weighting each neighbour's influence; empty weighs all equally.",
"group": "mode"
},
{
- "name": "Release",
+ "name": "release",
+ "label": "Release",
"type": "slider",
"default": "1.00",
"min": 0.0,
"max": 10.0,
"step": 0.05,
- "show_when": "Mode == Charge",
+ "show_when": "mode == Charge",
"description": "Threshold at which a point's charge discharges, split evenly among its neighbours. Higher stores more before firing; 0 never discharges.",
"group": "mode"
}
diff --git a/nodes/normal.json b/nodes/normal.json
index 70f55dd..99f055e 100644
--- a/nodes/normal.json
+++ b/nodes/normal.json
@@ -4,10 +4,10 @@
"inputs": 1,
"outputs": 1,
"params": [
- { "name": "Input", "type": "node", "default": "", "description": "The node whose normals are computed from its faces." },
- { "name": "Class", "type": "choice:Points,Vertices", "default": "Points", "description": "Points writes one averaged normal per point. Vertices writes one per face corner, so edges sharper than the Cusp Angle stay hard." },
- { "name": "Attribute", "type": "attribute", "default": "N", "description": "Name of the attribute the normals are written to. Empty leaves the geometry unchanged." },
- { "name": "Cusp Angle", "type": "slider", "default": "60", "min": 0.0, "max": 180.0, "step": 1.0, "show_when": "Class == Vertices", "description": "In degrees: faces meeting at less than this angle are averaged and read smooth, sharper ones keep their own normal. 180 is fully smooth." },
- { "name": "Flip", "type": "choice:false,true", "default": "false", "description": "Reverse the computed normals so they point the other way." }
+ { "name": "input", "label": "Input", "type": "node", "default": "", "description": "The node whose normals are computed from its faces." },
+ { "name": "class", "label": "Class", "type": "choice:Points,Vertices", "default": "Points", "description": "Points writes one averaged normal per point. Vertices writes one per face corner, so edges sharper than the Cusp Angle stay hard." },
+ { "name": "attribute", "label": "Attribute", "type": "attribute", "default": "N", "description": "Name of the attribute the normals are written to. Empty leaves the geometry unchanged." },
+ { "name": "cusp_angle", "label": "Cusp Angle", "type": "slider", "default": "60", "min": 0.0, "max": 180.0, "step": 1.0, "show_when": "class == Vertices", "description": "In degrees: faces meeting at less than this angle are averaged and read smooth, sharper ones keep their own normal. 180 is fully smooth." },
+ { "name": "flip", "label": "Flip", "type": "choice:false,true", "default": "false", "description": "Reverse the computed normals so they point the other way." }
]
}
diff --git a/nodes/output.json b/nodes/output.json
index 4f4425e..419801a 100644
--- a/nodes/output.json
+++ b/nodes/output.json
@@ -4,6 +4,6 @@
"inputs": 1,
"outputs": 0,
"params": [
- { "name": "Input", "default": "", "type": "node", "description": "Node whose geometry becomes the result of the enclosing subnet, what nodes outside it read." }
+ { "name": "input", "label": "Input", "default": "", "type": "node", "description": "Node whose geometry becomes the result of the enclosing subnet, what nodes outside it read." }
]
}
diff --git a/nodes/page.json b/nodes/page.json
index fcb5cf2..5830ecb 100644
--- a/nodes/page.json
+++ b/nodes/page.json
@@ -4,13 +4,13 @@
"inputs": 0,
"outputs": 1,
"params": [
- { "name": "Preset", "type": "choice:Letter,A4,Legal,Tabloid,HD,4K,Square", "default": "Letter", "description": "Picking one writes its size into Width and Height, in Units: Letter, A4, Legal and Tabloid as portrait sheets, HD, 4K and Square as pixel sizes. Shows the last one picked." },
- { "name": "Units", "type": "choice:Inches,Millimetres,Centimetres,Pixels", "default": "Inches", "description": "Unit Width and Height, and every length on the nodes drawing onto this page, are written in. Changing it converts Width and Height, so the page keeps its size." },
- { "name": "Width", "type": "float", "default": "8.5", "description": "Page width in Units; with Height, the page's size. A preset writes it, and any other width can be typed; swap it with Height for landscape. In Pixels its physical size depends on Resolution." },
- { "name": "Height", "type": "float", "default": "11.0", "description": "Page height in Units; with Width, the page's size. A preset writes it, and any other height can be typed. In Pixels its physical size depends on Resolution." },
- { "name": "Resolution", "type": "spinbox", "default": "300", "min": 18, "max": 1200, "step": 6, "description": "Pixels per inch (DPI). More pixels for a paper size; for pixel presets it sets the physical size. Stored in exported PNGs so prints come out at size." },
- { "name": "Color", "type": "float3", "default": "1.00:1.00:1.00", "min": 0.0, "max": 1.0, "description": "Background colour the whole page is filled with before anything is drawn on it.", "group": "look" },
- { "name": "Opacity", "type": "slider", "default": "1.00", "min": 0.0, "max": 1.0, "step": 0.01, "description": "Opacity of the background fill, 0 to 1. At 0 the page is clear and only what is drawn on it shows; clear areas are see-through in the viewport and PNG.", "group": "look" },
- { "name": "Position", "type": "float3", "default": "0.00:0.00:0.00", "min": -1000.0, "max": 1000.0, "description": "Where the centre of the page stands in the 3D scene, in world units. The page lies in the XY plane facing +Z at its physical size.", "group": "place" }
+ { "name": "preset", "label": "Preset", "type": "choice:Letter,A4,Legal,Tabloid,HD,4K,Square", "default": "Letter", "description": "Picking one writes its size into Width and Height, in Units: Letter, A4, Legal and Tabloid as portrait sheets, HD, 4K and Square as pixel sizes. Shows the last one picked." },
+ { "name": "units", "label": "Units", "type": "choice:Inches,Millimetres,Centimetres,Pixels", "default": "Inches", "description": "Unit Width and Height, and every length on the nodes drawing onto this page, are written in. Changing it converts Width and Height, so the page keeps its size." },
+ { "name": "width", "label": "Width", "type": "float", "default": "8.5", "description": "Page width in Units; with Height, the page's size. A preset writes it, and any other width can be typed; swap it with Height for landscape. In Pixels its physical size depends on Resolution." },
+ { "name": "height", "label": "Height", "type": "float", "default": "11.0", "description": "Page height in Units; with Width, the page's size. A preset writes it, and any other height can be typed. In Pixels its physical size depends on Resolution." },
+ { "name": "resolution", "label": "Resolution", "type": "spinbox", "default": "300", "min": 18, "max": 1200, "step": 6, "description": "Pixels per inch (DPI). More pixels for a paper size; for pixel presets it sets the physical size. Stored in exported PNGs so prints come out at size." },
+ { "name": "color", "label": "Color", "type": "float3", "default": "1.00:1.00:1.00", "min": 0.0, "max": 1.0, "description": "Background colour the whole page is filled with before anything is drawn on it.", "group": "look" },
+ { "name": "opacity", "label": "Opacity", "type": "slider", "default": "1.00", "min": 0.0, "max": 1.0, "step": 0.01, "description": "Opacity of the background fill, 0 to 1. At 0 the page is clear and only what is drawn on it shows; clear areas are see-through in the viewport and PNG.", "group": "look" },
+ { "name": "position", "label": "Position", "type": "float3", "default": "0.00:0.00:0.00", "min": -1000.0, "max": 1000.0, "description": "Where the centre of the page stands in the 3D scene, in world units. The page lies in the XY plane facing +Z at its physical size.", "group": "place" }
]
}
diff --git a/nodes/page_border.json b/nodes/page_border.json
index 49bcf5f..3c4eb67 100644
--- a/nodes/page_border.json
+++ b/nodes/page_border.json
@@ -4,9 +4,9 @@
"inputs": 1,
"outputs": 1,
"params": [
- { "name": "Input", "type": "node", "default": "", "description": "The page node (or page chain) whose image the border is drawn onto. Without one the node produces nothing." },
- { "name": "Width", "type": "float", "default": "0.06", "description": "Thickness of the border band, in the page's Units, measured inward from its outer edge. Limited to half the page's shorter side." },
- { "name": "Inset", "type": "float", "default": "0.40", "description": "Distance from the page edge to the outer edge of the border, in the page's Units. 0 puts the border flush with the edge." },
- { "name": "Color", "type": "float3", "default": "0.00:0.00:0.00", "min": 0.0, "max": 1.0, "description": "Colour of the border band, drawn fully opaque over the page." }
+ { "name": "input", "label": "Input", "type": "node", "default": "", "description": "The page node (or page chain) whose image the border is drawn onto. Without one the node produces nothing." },
+ { "name": "width", "label": "Width", "type": "float", "default": "0.06", "description": "Thickness of the border band, in the page's Units, measured inward from its outer edge. Limited to half the page's shorter side." },
+ { "name": "inset", "label": "Inset", "type": "float", "default": "0.40", "description": "Distance from the page edge to the outer edge of the border, in the page's Units. 0 puts the border flush with the edge." },
+ { "name": "color", "label": "Color", "type": "float3", "default": "0.00:0.00:0.00", "min": 0.0, "max": 1.0, "description": "Colour of the border band, drawn fully opaque over the page." }
]
}
diff --git a/nodes/page_grid.json b/nodes/page_grid.json
index d9918db..79151b3 100644
--- a/nodes/page_grid.json
+++ b/nodes/page_grid.json
@@ -4,11 +4,11 @@
"inputs": 1,
"outputs": 1,
"params": [
- { "name": "Input", "type": "node", "default": "", "description": "The page node (or page chain) whose image this grid is drawn onto. Without one the node produces nothing." },
- { "name": "Cell Size", "type": "float", "default": "0.25", "description": "Spacing between grid lines, in the page's Units. Lines start at the top-left corner; smaller values give a denser grid, never finer than one pixel." },
- { "name": "Line Width", "type": "float", "default": "0.01", "description": "Thickness of each grid line, in the page's Units, centred on its coordinate. Very small values are drawn as a hairline at least half a pixel wide." },
- { "name": "Line Color", "type": "float3", "default": "0.00:0.00:0.00", "min": 0.0, "max": 1.0, "description": "Colour of the grid lines, drawn fully opaque over the page." },
- { "name": "Fill Cells", "type": "toggle", "default": "false", "description": "When on, fills the whole page with Cell Color before ruling the lines, covering what the input had drawn. When off, lines are drawn over the input as is.", "group": "fill" },
- { "name": "Cell Color", "type": "float3", "default": "1.00:1.00:1.00", "min": 0.0, "max": 1.0, "show_when": "Fill Cells == true", "description": "Colour the page is filled with under the grid lines when Fill Cells is on.", "group": "fill" }
+ { "name": "input", "label": "Input", "type": "node", "default": "", "description": "The page node (or page chain) whose image this grid is drawn onto. Without one the node produces nothing." },
+ { "name": "cell_size", "label": "Cell Size", "type": "float", "default": "0.25", "description": "Spacing between grid lines, in the page's Units. Lines start at the top-left corner; smaller values give a denser grid, never finer than one pixel." },
+ { "name": "line_width", "label": "Line Width", "type": "float", "default": "0.01", "description": "Thickness of each grid line, in the page's Units, centred on its coordinate. Very small values are drawn as a hairline at least half a pixel wide." },
+ { "name": "line_color", "label": "Line Color", "type": "float3", "default": "0.00:0.00:0.00", "min": 0.0, "max": 1.0, "description": "Colour of the grid lines, drawn fully opaque over the page." },
+ { "name": "fill_cells", "label": "Fill Cells", "type": "toggle", "default": "false", "description": "When on, fills the whole page with Cell Color before ruling the lines, covering what the input had drawn. When off, lines are drawn over the input as is.", "group": "fill" },
+ { "name": "cell_color", "label": "Cell Color", "type": "float3", "default": "1.00:1.00:1.00", "min": 0.0, "max": 1.0, "show_when": "fill_cells == true", "description": "Colour the page is filled with under the grid lines when Fill Cells is on.", "group": "fill" }
]
}
diff --git a/nodes/page_shape.json b/nodes/page_shape.json
index 6d5350a..7329ba8 100644
--- a/nodes/page_shape.json
+++ b/nodes/page_shape.json
@@ -4,21 +4,21 @@
"inputs": 1,
"outputs": 1,
"params": [
- { "name": "Input", "type": "node", "default": "", "description": "The page node (or page chain) whose image the shape is drawn onto. Without one the node produces nothing." },
- { "name": "Shape", "type": "choice:Rectangle,Ellipse,Line,Polygon", "default": "Rectangle", "description": "Outline to draw: a rectangle, an ellipse, a straight line, or a regular polygon fitted inside the Width by Height box." },
- { "name": "X", "type": "float", "default": "4.25", "description": "Horizontal position of the shape's centre, in the page's Units from the page's left edge.", "group": "geometry" },
- { "name": "Y", "type": "float", "default": "5.50", "description": "Vertical position of the shape's centre, in the page's Units from the page's top edge (y runs down).", "group": "geometry" },
- { "name": "Width", "type": "float", "default": "3.00", "description": "Width of the shape's box before rotation, in the page's Units. For a line, its length.", "group": "geometry" },
- { "name": "Height", "type": "float", "default": "2.00", "show_when": "Shape != Line", "description": "Height of the shape's box before rotation, in the page's Units. A line's thickness comes from Stroke Width instead.", "group": "geometry" },
- { "name": "Rotation", "type": "slider", "default": "0", "min": -180.0, "max": 180.0, "step": 1.0, "description": "Turns the shape about its centre, in degrees; positive is clockwise as the page is viewed.", "group": "geometry" },
- { "name": "Corner Radius", "type": "float", "default": "0.00", "show_when": "Shape == Rectangle", "description": "Rounds the rectangle's corners, in the page's Units. 0 gives sharp corners; limited to half the shorter side.", "group": "geometry" },
- { "name": "Sides", "type": "spinbox", "default": "3", "min": 3, "max": 64, "step": 1, "show_when": "Shape == Polygon", "description": "Number of corners of the polygon, 3 to 64, with the first corner at the top. More sides approach an ellipse.", "group": "geometry" },
- { "name": "Fill", "type": "toggle", "default": "true", "show_when": "Shape != Line", "description": "When on, the inside of the shape is filled with Fill Color. Turn off for an outline only.", "group": "fill" },
- { "name": "Fill Color", "type": "float3", "default": "0.20:0.45:0.85", "min": 0.0, "max": 1.0, "show_when": "Shape != Line && Fill == true", "description": "Colour of the shape's fill; how solid it is comes from Fill Opacity.", "group": "fill" },
- { "name": "Fill Opacity", "type": "slider", "default": "1.00", "min": 0.0, "max": 1.0, "step": 0.01, "show_when": "Shape != Line && Fill == true", "description": "Opacity of the fill, 0 (invisible) to 1 (solid), blended over what is already on the page.", "group": "fill" },
- { "name": "Stroke", "type": "toggle", "default": "true", "description": "When on, the outline is drawn in Stroke Color at Stroke Width. A line shape is drawn only while this is on.", "group": "stroke" },
- { "name": "Stroke Color", "type": "float3", "default": "0.00:0.00:0.00", "min": 0.0, "max": 1.0, "show_when": "Stroke == true", "description": "Colour of the outline, and of the whole line for the Line shape.", "group": "stroke" },
- { "name": "Stroke Opacity", "type": "slider", "default": "1.00", "min": 0.0, "max": 1.0, "step": 0.01, "show_when": "Stroke == true", "description": "Opacity of the outline, 0 (invisible) to 1 (solid), blended over the fill and the page.", "group": "stroke" },
- { "name": "Stroke Width", "type": "float", "default": "0.02", "show_when": "Stroke == true", "description": "Thickness of the outline in the page's Units, centred on the shape's edge (half inside, half out). For a line, its thickness.", "group": "stroke" }
+ { "name": "input", "label": "Input", "type": "node", "default": "", "description": "The page node (or page chain) whose image the shape is drawn onto. Without one the node produces nothing." },
+ { "name": "shape", "label": "Shape", "type": "choice:Rectangle,Ellipse,Line,Polygon", "default": "Rectangle", "description": "Outline to draw: a rectangle, an ellipse, a straight line, or a regular polygon fitted inside the Width by Height box." },
+ { "name": "x", "label": "X", "type": "float", "default": "4.25", "description": "Horizontal position of the shape's centre, in the page's Units from the page's left edge.", "group": "geometry" },
+ { "name": "y", "label": "Y", "type": "float", "default": "5.50", "description": "Vertical position of the shape's centre, in the page's Units from the page's top edge (y runs down).", "group": "geometry" },
+ { "name": "width", "label": "Width", "type": "float", "default": "3.00", "description": "Width of the shape's box before rotation, in the page's Units. For a line, its length.", "group": "geometry" },
+ { "name": "height", "label": "Height", "type": "float", "default": "2.00", "show_when": "shape != Line", "description": "Height of the shape's box before rotation, in the page's Units. A line's thickness comes from Stroke Width instead.", "group": "geometry" },
+ { "name": "rotation", "label": "Rotation", "type": "slider", "default": "0", "min": -180.0, "max": 180.0, "step": 1.0, "description": "Turns the shape about its centre, in degrees; positive is clockwise as the page is viewed.", "group": "geometry" },
+ { "name": "corner_radius", "label": "Corner Radius", "type": "float", "default": "0.00", "show_when": "shape == Rectangle", "description": "Rounds the rectangle's corners, in the page's Units. 0 gives sharp corners; limited to half the shorter side.", "group": "geometry" },
+ { "name": "sides", "label": "Sides", "type": "spinbox", "default": "3", "min": 3, "max": 64, "step": 1, "show_when": "shape == Polygon", "description": "Number of corners of the polygon, 3 to 64, with the first corner at the top. More sides approach an ellipse.", "group": "geometry" },
+ { "name": "fill", "label": "Fill", "type": "toggle", "default": "true", "show_when": "shape != Line", "description": "When on, the inside of the shape is filled with Fill Color. Turn off for an outline only.", "group": "fill" },
+ { "name": "fill_color", "label": "Fill Color", "type": "float3", "default": "0.20:0.45:0.85", "min": 0.0, "max": 1.0, "show_when": "shape != Line && fill == true", "description": "Colour of the shape's fill; how solid it is comes from Fill Opacity.", "group": "fill" },
+ { "name": "fill_opacity", "label": "Fill Opacity", "type": "slider", "default": "1.00", "min": 0.0, "max": 1.0, "step": 0.01, "show_when": "shape != Line && fill == true", "description": "Opacity of the fill, 0 (invisible) to 1 (solid), blended over what is already on the page.", "group": "fill" },
+ { "name": "stroke", "label": "Stroke", "type": "toggle", "default": "true", "description": "When on, the outline is drawn in Stroke Color at Stroke Width. A line shape is drawn only while this is on.", "group": "stroke" },
+ { "name": "stroke_color", "label": "Stroke Color", "type": "float3", "default": "0.00:0.00:0.00", "min": 0.0, "max": 1.0, "show_when": "stroke == true", "description": "Colour of the outline, and of the whole line for the Line shape.", "group": "stroke" },
+ { "name": "stroke_opacity", "label": "Stroke Opacity", "type": "slider", "default": "1.00", "min": 0.0, "max": 1.0, "step": 0.01, "show_when": "stroke == true", "description": "Opacity of the outline, 0 (invisible) to 1 (solid), blended over the fill and the page.", "group": "stroke" },
+ { "name": "stroke_width", "label": "Stroke Width", "type": "float", "default": "0.02", "show_when": "stroke == true", "description": "Thickness of the outline in the page's Units, centred on the shape's edge (half inside, half out). For a line, its thickness.", "group": "stroke" }
]
}
diff --git a/nodes/page_text.json b/nodes/page_text.json
index 3a73679..4016fe6 100644
--- a/nodes/page_text.json
+++ b/nodes/page_text.json
@@ -4,15 +4,15 @@
"inputs": 1,
"outputs": 1,
"params": [
- { "name": "Input", "type": "node", "default": "", "description": "The page node (or page chain) whose image the text is drawn onto. Without one the node produces nothing." },
- { "name": "Text", "type": "text", "default": "Title", "description": "The text to draw. It is not wrapped: a long line runs past the page edge rather than breaking." },
- { "name": "Font", "type": "text", "default": "", "description": "Font family name, such as a system font installed on this machine. Empty uses the default font.", "group": "type" },
- { "name": "Size", "type": "float", "default": "0.25", "description": "Font size in the page's Units. The same size prints the same at any Resolution; only the pixel count changes.", "group": "type" },
- { "name": "Color", "type": "float3", "default": "0.00:0.00:0.00", "min": 0.0, "max": 1.0, "description": "Colour of the text, drawn fully opaque over the page.", "group": "type" },
- { "name": "X", "type": "float", "default": "4.25", "description": "Horizontal position of the text's anchor, in the page's Units from the left edge. Horizontal decides which part of the text sits here.", "group": "layout" },
- { "name": "Y", "type": "float", "default": "0.80", "description": "Vertical position of the text's anchor, in the page's Units from the top edge (y runs down). Vertical decides which part of the text sits here.", "group": "layout" },
- { "name": "Horizontal", "type": "choice:Left,Center,Right", "default": "Center", "description": "Which part of the text is placed at X: its left edge, its centre, or its right edge.", "group": "layout" },
- { "name": "Vertical", "type": "choice:Top,Middle,Bottom", "default": "Top", "description": "Which part of the text block is placed at Y: its top, its middle, or its bottom.", "group": "layout" },
- { "name": "Leading", "type": "slider", "default": "1.25", "min": 0.5, "max": 3.0, "step": 0.05, "description": "Line spacing as a multiple of Size. 1 sets lines tightly; larger values space multi-line text further apart.", "group": "layout" }
+ { "name": "input", "label": "Input", "type": "node", "default": "", "description": "The page node (or page chain) whose image the text is drawn onto. Without one the node produces nothing." },
+ { "name": "text", "label": "Text", "type": "text", "default": "Title", "description": "The text to draw. It is not wrapped: a long line runs past the page edge rather than breaking." },
+ { "name": "font", "label": "Font", "type": "text", "default": "", "description": "Font family name, such as a system font installed on this machine. Empty uses the default font.", "group": "type" },
+ { "name": "size", "label": "Size", "type": "float", "default": "0.25", "description": "Font size in the page's Units. The same size prints the same at any Resolution; only the pixel count changes.", "group": "type" },
+ { "name": "color", "label": "Color", "type": "float3", "default": "0.00:0.00:0.00", "min": 0.0, "max": 1.0, "description": "Colour of the text, drawn fully opaque over the page.", "group": "type" },
+ { "name": "x", "label": "X", "type": "float", "default": "4.25", "description": "Horizontal position of the text's anchor, in the page's Units from the left edge. Horizontal decides which part of the text sits here.", "group": "layout" },
+ { "name": "y", "label": "Y", "type": "float", "default": "0.80", "description": "Vertical position of the text's anchor, in the page's Units from the top edge (y runs down). Vertical decides which part of the text sits here.", "group": "layout" },
+ { "name": "horizontal", "label": "Horizontal", "type": "choice:Left,Center,Right", "default": "Center", "description": "Which part of the text is placed at X: its left edge, its centre, or its right edge.", "group": "layout" },
+ { "name": "vertical", "label": "Vertical", "type": "choice:Top,Middle,Bottom", "default": "Top", "description": "Which part of the text block is placed at Y: its top, its middle, or its bottom.", "group": "layout" },
+ { "name": "leading", "label": "Leading", "type": "slider", "default": "1.25", "min": 0.5, "max": 3.0, "step": 0.05, "description": "Line spacing as a multiple of Size. 1 sets lines tightly; larger values space multi-line text further apart.", "group": "layout" }
]
}
diff --git a/nodes/plane.json b/nodes/plane.json
index 4c15d2f..13fc896 100644
--- a/nodes/plane.json
+++ b/nodes/plane.json
@@ -5,19 +5,22 @@
"outputs": 1,
"params": [
{
- "name": "Width",
+ "name": "width",
+ "label": "Width",
"default": "1.0",
"type": "slider:0:4",
"description": "Size of the sheet along X in world units."
},
{
- "name": "Length",
+ "name": "length",
+ "label": "Length",
"default": "1.0",
"type": "slider:0:4",
"description": "Size of the sheet along Z in world units."
},
{
- "name": "Columns",
+ "name": "columns",
+ "label": "Columns",
"default": "16",
"type": "spinbox",
"min": 1,
@@ -26,7 +29,8 @@
"description": "Number of quad divisions across the width (X). More columns give a finer sheet."
},
{
- "name": "Rows",
+ "name": "rows",
+ "label": "Rows",
"default": "16",
"type": "spinbox",
"min": 1,
@@ -35,28 +39,32 @@
"description": "Number of quad divisions along the length (Z). More rows give a finer sheet."
},
{
- "name": "Center X",
+ "name": "center_x",
+ "label": "Center X",
"default": "0.0",
"type": "slider:-2:2",
"description": "X position of the sheet's center in world units.",
"group": "place"
},
{
- "name": "Center Y",
+ "name": "center_y",
+ "label": "Center Y",
"default": "0.0",
"type": "slider:-2:2",
"description": "Height of the flat sheet in world units; the plane lies level in XZ at this Y.",
"group": "place"
},
{
- "name": "Center Z",
+ "name": "center_z",
+ "label": "Center Z",
"default": "0.0",
"type": "slider:-2:2",
"description": "Z position of the sheet's center in world units.",
"group": "place"
},
{
- "name": "Color",
+ "name": "color",
+ "label": "Color",
"default": "true",
"type": "toggle",
"description": "When on, colors the points with a gradient across the sheet; when off, the plane uses the plain default color.",
diff --git a/nodes/points.json b/nodes/points.json
index 4265826..51f1bb7 100644
--- a/nodes/points.json
+++ b/nodes/points.json
@@ -5,7 +5,8 @@
"outputs": 1,
"params": [
{
- "name": "Points",
+ "name": "points",
+ "label": "Points",
"type": "spinbox",
"default": "100",
"min": 10.0,
@@ -14,14 +15,16 @@
"description": "Number of points to generate along the chosen Shape. With Markers on, each one also gets a small marker sphere."
},
{
- "name": "Shape",
+ "name": "shape",
+ "label": "Shape",
"type": "choice:None,Spiral,Line,Circle,Grid",
"default": "None",
"description": "Pattern the points are laid out in: Spiral, Line, Circle or a square Grid. None puts every point at the same spot. Placed automatically by the node's order in the network.",
"group": "display"
},
{
- "name": "Markers",
+ "name": "markers",
+ "label": "Markers",
"type": "choice:true,false",
"default": "true",
"description": "true draws a small sphere at each point so they can be seen; false outputs bare points only, which is what Copy and other nodes should read.",
diff --git a/nodes/polygon.json b/nodes/polygon.json
index 20ef604..0436930 100644
--- a/nodes/polygon.json
+++ b/nodes/polygon.json
@@ -4,10 +4,10 @@
"inputs": 0,
"outputs": 1,
"params": [
- { "name": "Sides", "type": "spinbox", "default": "4", "min": 3.0, "max": 256.0, "step": 1.0, "description": "Number of corners: 3 is a triangle, 4 a square, high counts approach a circle. With an Inner Radius it is the number of star points." },
- { "name": "Radius", "type": "slider", "default": "0.50", "min": 0.01, "max": 10.0, "step": 0.01, "description": "Distance from the center to each outer corner, in world units." },
- { "name": "Inner Radius", "type": "slider", "default": "0.00", "min": 0.0, "max": 10.0, "step": 0.01, "description": "Distance of the inner corners for a star, in world units. 0 makes a regular polygon; above 0 every other corner is pulled to this radius." },
- { "name": "Fill", "type": "choice:true,false", "default": "true", "description": "true builds a filled shape of triangles fanned from the center; false builds only the outline as edges." },
- { "name": "Center", "type": "float3", "default": "0.00:0.00:0.00", "description": "Position of the shape's center in world units; the polygon lies flat in the XZ plane through this point.", "group": "place" }
+ { "name": "sides", "label": "Sides", "type": "spinbox", "default": "4", "min": 3.0, "max": 256.0, "step": 1.0, "description": "Number of corners: 3 is a triangle, 4 a square, high counts approach a circle. With an Inner Radius it is the number of star points." },
+ { "name": "radius", "label": "Radius", "type": "slider", "default": "0.50", "min": 0.01, "max": 10.0, "step": 0.01, "description": "Distance from the center to each outer corner, in world units." },
+ { "name": "inner_radius", "label": "Inner Radius", "type": "slider", "default": "0.00", "min": 0.0, "max": 10.0, "step": 0.01, "description": "Distance of the inner corners for a star, in world units. 0 makes a regular polygon; above 0 every other corner is pulled to this radius." },
+ { "name": "fill", "label": "Fill", "type": "choice:true,false", "default": "true", "description": "true builds a filled shape of triangles fanned from the center; false builds only the outline as edges." },
+ { "name": "center", "label": "Center", "type": "float3", "default": "0.00:0.00:0.00", "description": "Position of the shape's center in world units; the polygon lies flat in the XZ plane through this point.", "group": "place" }
]
}
diff --git a/nodes/project.json b/nodes/project.json
index 915b96a..079ea86 100644
--- a/nodes/project.json
+++ b/nodes/project.json
@@ -5,13 +5,15 @@
"outputs": 1,
"params": [
{
- "name": "Input",
+ "name": "input",
+ "label": "Input",
"type": "node",
"default": "",
"description": "The node whose points are moved onto the Surface."
},
{
- "name": "Surface",
+ "name": "surface",
+ "label": "Surface",
"type": "node",
"default": "",
"description": "The node whose surface each point is moved to, at the nearest place on it. Unconnected, the input passes through unchanged."
diff --git a/nodes/relax.json b/nodes/relax.json
index 3ab9f20..6fd1b6d 100644
--- a/nodes/relax.json
+++ b/nodes/relax.json
@@ -5,46 +5,52 @@
"outputs": 1,
"params": [
{
- "name": "Input",
+ "name": "input",
+ "label": "Input",
"default": "",
"type": "node",
"description": "The geometry to relax. Its points are moved and everything else passes through."
},
{
- "name": "Mode",
+ "name": "mode",
+ "label": "Mode",
"type": "choice:Springs,Repel,Tangential",
"default": "Springs",
"description": "Springs pulls edge lengths back toward those of the Rest geometry. Repel pushes points apart until spheres of Radius stop overlapping. Tangential evens out triangles along the surface."
},
{
- "name": "Rest",
+ "name": "rest",
+ "label": "Rest",
"default": "",
"type": "node",
- "show_when": "Mode == Springs",
+ "show_when": "mode == Springs",
"description": "The geometry whose edge lengths the springs restore; inside a simnet usually its input node. Must have the same point count as Input, or nothing moves.",
"group": "springs"
},
{
- "name": "Pin Group",
+ "name": "pin_group",
+ "label": "Pin Group",
"default": "",
"type": "group",
- "show_when": "Mode == Springs",
+ "show_when": "mode == Springs",
"description": "Points in this group keep their input position and push the others instead, so a displacement applied to them spreads through the surface. Empty pins nothing.",
"group": "springs"
},
{
- "name": "Stiffness",
+ "name": "stiffness",
+ "label": "Stiffness",
"type": "slider",
"default": "0.50",
"min": 0.0,
"max": 1.0,
"step": 0.05,
- "show_when": "Mode == Springs",
+ "show_when": "mode == Springs",
"description": "Fraction of each edge's length error corrected per pass. Higher values relax faster and stiffer; 0 leaves the points where they are.",
"group": "springs"
},
{
- "name": "Iterations",
+ "name": "iterations",
+ "label": "Iterations",
"type": "spinbox",
"default": "8",
"min": 0.0,
@@ -54,32 +60,35 @@
"group": "solve"
},
{
- "name": "Radius",
+ "name": "radius",
+ "label": "Radius",
"type": "slider",
"default": "0.05",
"min": 0.001,
"max": 1.0,
"step": 0.001,
- "show_when": "Mode == Repel",
+ "show_when": "mode == Repel",
"description": "Radius of the sphere around each point, in world units; points are pushed apart until these spheres stop overlapping. Larger spaces points further apart.",
"group": "repel"
},
{
- "name": "In 3D Space",
+ "name": "in_3d_space",
+ "label": "In 3D Space",
"type": "toggle",
"default": "false",
- "show_when": "Mode == Repel",
+ "show_when": "mode == Repel",
"description": "When off, points slide only along the surface (in their tangent plane) as they repel. When on, they may move freely in 3D and leave the surface.",
"group": "repel"
},
{
- "name": "Amount",
+ "name": "amount",
+ "label": "Amount",
"type": "slider",
"default": "0.50",
"min": 0.0,
"max": 1.0,
"step": 0.01,
- "show_when": "Mode == Tangential",
+ "show_when": "mode == Tangential",
"description": "How far each point moves toward the centroid of its neighbours per pass, 0 to 1, less the part along its normal, so triangles even out while the shape stays. 0 turns it off.",
"group": "tangential"
}
diff --git a/nodes/remesh.json b/nodes/remesh.json
index 1fe1eb4..8c28b2c 100644
--- a/nodes/remesh.json
+++ b/nodes/remesh.json
@@ -5,13 +5,15 @@
"outputs": 1,
"params": [
{
- "name": "Input",
+ "name": "input",
+ "label": "Input",
"type": "node",
"default": "",
"description": "The node whose surface is remeshed. Its triangles are rebuilt toward an even edge length; point attributes ride through splits and collapses."
},
{
- "name": "Target Length",
+ "name": "target_length",
+ "label": "Target Length",
"type": "slider",
"default": "0.10",
"min": 0.001,
@@ -20,7 +22,8 @@
"description": "Edge length, in world units, the mesh is steered toward. Edges over 4/3 of it are split and edges under 4/5 collapsed; smaller means denser."
},
{
- "name": "Iterations",
+ "name": "iterations",
+ "label": "Iterations",
"type": "spinbox",
"default": "3",
"min": 1.0,
@@ -29,7 +32,8 @@
"description": "How many times the split, collapse, flip, relax and project passes repeat. More rounds settle the mesh closer to the target; it stops early once a round changes nothing."
},
{
- "name": "Relax",
+ "name": "relax",
+ "label": "Relax",
"type": "slider",
"default": "0.50",
"min": 0.0,
@@ -38,52 +42,58 @@
"description": "How far each point slides toward its neighbours' centroid along the surface in every round, evening out the triangles. 0 turns the relaxation off; 1 is full strength."
},
{
- "name": "Transfer",
+ "name": "transfer",
+ "label": "Transfer",
"type": "toggle",
"default": "false",
"description": "After remeshing, copy point attributes (and optionally groups) onto the new points from the nearest point of a source mesh, so they survive the rebuild.",
"group": "transfer"
},
{
- "name": "From",
+ "name": "from",
+ "label": "From",
"type": "node",
"default": "",
- "show_when": "Transfer == true",
+ "show_when": "transfer == true",
"description": "The node whose point attributes and groups are copied onto the remeshed points. Empty uses the remesh's own input, as it was before remeshing.",
"group": "transfer"
},
{
- "name": "Attributes",
+ "name": "attributes",
+ "label": "Attributes",
"type": "text",
"default": "",
- "show_when": "Transfer == true",
+ "show_when": "transfer == true",
"description": "Comma-separated names of the point attributes to copy from the source. Empty copies every point attribute the source has.",
"group": "transfer"
},
{
- "name": "Transfer Groups",
+ "name": "transfer_groups",
+ "label": "Transfer Groups",
"type": "toggle",
"default": "false",
- "show_when": "Transfer == true",
+ "show_when": "transfer == true",
"description": "Also copy point group membership from the nearest source point, joining and leaving groups to match the source.",
"group": "transfer"
},
{
- "name": "Groups",
+ "name": "groups",
+ "label": "Groups",
"type": "text",
"default": "",
- "show_when": "Transfer == true && Transfer Groups == true",
+ "show_when": "transfer == true && transfer_groups == true",
"description": "Comma-separated names of the point groups to copy. Empty copies every point group the source has.",
"group": "transfer"
},
{
- "name": "Maximum Distance",
+ "name": "maximum_distance",
+ "label": "Maximum Distance",
"type": "slider",
"default": "0.00",
"min": 0.0,
"max": 10.0,
"step": 0.01,
- "show_when": "Transfer == true",
+ "show_when": "transfer == true",
"description": "Only points within this many world units of a source point receive values; farther points keep their own. 0 means no limit.",
"group": "transfer"
}
@@ -108,16 +118,16 @@
],
"params": [
{
- "name": "Input",
+ "name": "input",
"default": "input1"
},
{
- "name": "Iterations",
- "default": "chi(\"../Iterations\")",
+ "name": "iterations",
+ "default": "chi(\"../iterations\")",
"expr": true
},
{
- "name": "Stop When Unchanged",
+ "name": "stop_when_unchanged",
"default": "true"
}
],
@@ -151,12 +161,12 @@
],
"params": [
{
- "name": "Input",
+ "name": "input",
"default": "input1"
},
{
- "name": "Target Length",
- "default": "chf(\"../../Target Length\")",
+ "name": "target_length",
+ "default": "chf(\"../../target_length\")",
"expr": true
}
],
@@ -171,12 +181,12 @@
],
"params": [
{
- "name": "Input",
+ "name": "input",
"default": "split1"
},
{
- "name": "Target Length",
- "default": "chf(\"../../Target Length\")",
+ "name": "target_length",
+ "default": "chf(\"../../target_length\")",
"expr": true
}
],
@@ -191,12 +201,12 @@
],
"params": [
{
- "name": "Input",
+ "name": "input",
"default": "collapse1"
},
{
- "name": "Target Length",
- "default": "chf(\"../../Target Length\")",
+ "name": "target_length",
+ "default": "chf(\"../../target_length\")",
"expr": true
}
],
@@ -211,20 +221,20 @@
],
"params": [
{
- "name": "Input",
+ "name": "input",
"default": "flip1"
},
{
- "name": "Mode",
+ "name": "mode",
"default": "Tangential"
},
{
- "name": "Iterations",
+ "name": "iterations",
"default": "1"
},
{
- "name": "Amount",
- "default": "chf(\"../../Relax\")",
+ "name": "amount",
+ "default": "chf(\"../../relax\")",
"expr": true
}
],
@@ -239,11 +249,11 @@
],
"params": [
{
- "name": "Input",
+ "name": "input",
"default": "relax1"
},
{
- "name": "Surface",
+ "name": "surface",
"default": "seed1"
}
],
@@ -258,7 +268,7 @@
],
"params": [
{
- "name": "Input",
+ "name": "input",
"default": "project1"
}
],
@@ -276,32 +286,32 @@
],
"params": [
{
- "name": "Input",
+ "name": "input",
"default": "repeat1"
},
{
- "name": "From",
- "default": "if(chs(\"../From\"), chs(\"../From\"), \"input1\")",
+ "name": "from",
+ "default": "if(chs(\"../from\"), chs(\"../from\"), \"input1\")",
"expr": true
},
{
- "name": "Attributes",
- "default": "chs(\"../Attributes\")",
+ "name": "attributes",
+ "default": "chs(\"../attributes\")",
"expr": true
},
{
- "name": "Transfer Groups",
- "default": "chb(\"../Transfer Groups\")",
+ "name": "transfer_groups",
+ "default": "chb(\"../transfer_groups\")",
"expr": true
},
{
- "name": "Groups",
- "default": "chs(\"../Groups\")",
+ "name": "groups",
+ "default": "chs(\"../groups\")",
"expr": true
},
{
- "name": "Maximum Distance",
- "default": "chf(\"../Maximum Distance\")",
+ "name": "maximum_distance",
+ "default": "chf(\"../maximum_distance\")",
"expr": true
}
],
@@ -316,16 +326,16 @@
],
"params": [
{
- "name": "Input",
+ "name": "input",
"default": "repeat1"
},
{
- "name": "Input 2",
+ "name": "input_2",
"default": "transfer1"
},
{
- "name": "Index",
- "default": "chi(\"../Transfer\")",
+ "name": "index",
+ "default": "chi(\"../transfer\")",
"expr": true
}
]
@@ -339,7 +349,7 @@
],
"params": [
{
- "name": "Input",
+ "name": "input",
"default": "transfer_switch1"
}
],
diff --git a/nodes/repeat.json b/nodes/repeat.json
index 31d03cb..b235418 100644
--- a/nodes/repeat.json
+++ b/nodes/repeat.json
@@ -5,13 +5,15 @@
"outputs": 1,
"params": [
{
- "name": "Input",
+ "name": "input",
+ "label": "Input",
"type": "node",
"default": "",
"description": "The node whose geometry the first pass of the loop reads, through the input node inside."
},
{
- "name": "Iterations",
+ "name": "iterations",
+ "label": "Iterations",
"type": "spinbox",
"default": "4",
"min": 0.0,
@@ -20,7 +22,8 @@
"description": "How many times the chain inside runs, each pass reading the last one's result. 0 passes the input through; at most 1000."
},
{
- "name": "Stop When Unchanged",
+ "name": "stop_when_unchanged",
+ "label": "Stop When Unchanged",
"type": "toggle",
"default": "false",
"description": "Ends the loop early at a pass that changes nothing, since every later pass would find the same."
@@ -51,7 +54,7 @@
"type": "output",
"params": [
{
- "name": "Input",
+ "name": "input",
"default": "input1"
}
],
diff --git a/nodes/scatter.json b/nodes/scatter.json
index 6572521..be03982 100644
--- a/nodes/scatter.json
+++ b/nodes/scatter.json
@@ -4,16 +4,16 @@
"inputs": 1,
"outputs": 1,
"params": [
- { "name": "Input", "default": "", "type": "node", "description": "Node whose geometry the points are scattered inside (Volume) or on the surface of (Surface)." },
- { "name": "Mode", "type": "choice:Volume,Surface", "default": "Volume", "description": "Volume places points inside the closed input shape; Surface places them on its surface, spread by area, with optional relaxing." },
- { "name": "Points", "type": "spinbox", "default": "100", "min": 10.0, "max": 10000.0, "step": 10.0, "description": "Number of points to scatter. In Volume mode fewer may be produced if the shape is thin or not closed." },
- { "name": "Seed", "type": "slider", "default": "1.1", "min": 0.0, "max": 10.0, "step": 0.1, "show_when": "Mode == Surface", "description": "Random seed for Surface mode. Change it for a different point layout; the same seed always gives the same points." },
- { "name": "Relax Points", "type": "toggle", "default": "false", "show_when": "Mode == Surface", "description": "When on, pushes the scattered points apart across the surface so they end up evenly spaced.", "group": "relax" },
- { "name": "Relax Iterations", "type": "spinbox", "default": "50", "min": 0, "max": 100, "step": 1, "show_when": "Mode == Surface && Relax Points == true", "description": "How many push-apart passes the points get. More passes give a more even spacing; 0 does none.", "group": "relax" },
- { "name": "Scale Radii By", "type": "slider", "default": "1.248", "min": 0.0, "max": 2.0, "step": 0.001, "show_when": "Mode == Surface && Relax Points == true", "description": "Multiplier on each point's push radius, which is derived from the surface area per point. Larger values spread points more forcefully.", "group": "relax" },
- { "name": "Use Max Relax Radius", "type": "toggle", "default": "true", "show_when": "Mode == Surface && Relax Points == true", "description": "When on, the push radius is capped at Max Relax Radius.", "group": "relax" },
- { "name": "Max Relax Radius", "type": "slider", "default": "10", "min": 0.0, "max": 100.0, "step": 0.1, "show_when": "Mode == Surface && Relax Points == true && Use Max Relax Radius == true", "description": "Upper limit on each point's push radius in world units, used when Use Max Relax Radius is on.", "group": "relax" },
- { "name": "Radius", "type": "slider", "default": "0.02", "min": 0.005, "max": 0.1, "step": 0.005, "show_when": "Markers == true", "description": "Size of the marker sphere drawn at each point, in world units.", "group": "display" },
- { "name": "Markers", "type": "choice:true,false", "default": "true", "description": "true draws a small sphere at each point so they can be seen; false outputs bare points only, which is what Copy and other nodes should read.", "group": "display" }
+ { "name": "input", "label": "Input", "default": "", "type": "node", "description": "Node whose geometry the points are scattered inside (Volume) or on the surface of (Surface)." },
+ { "name": "mode", "label": "Mode", "type": "choice:Volume,Surface", "default": "Volume", "description": "Volume places points inside the closed input shape; Surface places them on its surface, spread by area, with optional relaxing." },
+ { "name": "points", "label": "Points", "type": "spinbox", "default": "100", "min": 10.0, "max": 10000.0, "step": 10.0, "description": "Number of points to scatter. In Volume mode fewer may be produced if the shape is thin or not closed." },
+ { "name": "seed", "label": "Seed", "type": "slider", "default": "1.1", "min": 0.0, "max": 10.0, "step": 0.1, "show_when": "mode == Surface", "description": "Random seed for Surface mode. Change it for a different point layout; the same seed always gives the same points." },
+ { "name": "relax_points", "label": "Relax Points", "type": "toggle", "default": "false", "show_when": "mode == Surface", "description": "When on, pushes the scattered points apart across the surface so they end up evenly spaced.", "group": "relax" },
+ { "name": "relax_iterations", "label": "Relax Iterations", "type": "spinbox", "default": "50", "min": 0, "max": 100, "step": 1, "show_when": "mode == Surface && relax_points == true", "description": "How many push-apart passes the points get. More passes give a more even spacing; 0 does none.", "group": "relax" },
+ { "name": "scale_radii_by", "label": "Scale Radii By", "type": "slider", "default": "1.248", "min": 0.0, "max": 2.0, "step": 0.001, "show_when": "mode == Surface && relax_points == true", "description": "Multiplier on each point's push radius, which is derived from the surface area per point. Larger values spread points more forcefully.", "group": "relax" },
+ { "name": "use_max_relax_radius", "label": "Use Max Relax Radius", "type": "toggle", "default": "true", "show_when": "mode == Surface && relax_points == true", "description": "When on, the push radius is capped at Max Relax Radius.", "group": "relax" },
+ { "name": "max_relax_radius", "label": "Max Relax Radius", "type": "slider", "default": "10", "min": 0.0, "max": 100.0, "step": 0.1, "show_when": "mode == Surface && relax_points == true && use_max_relax_radius == true", "description": "Upper limit on each point's push radius in world units, used when Use Max Relax Radius is on.", "group": "relax" },
+ { "name": "radius", "label": "Radius", "type": "slider", "default": "0.02", "min": 0.005, "max": 0.1, "step": 0.005, "show_when": "markers == true", "description": "Size of the marker sphere drawn at each point, in world units.", "group": "display" },
+ { "name": "markers", "label": "Markers", "type": "choice:true,false", "default": "true", "description": "true draws a small sphere at each point so they can be seen; false outputs bare points only, which is what Copy and other nodes should read.", "group": "display" }
]
}
diff --git a/nodes/simnet.json b/nodes/simnet.json
index 7a8644a..c623b15 100644
--- a/nodes/simnet.json
+++ b/nodes/simnet.json
@@ -5,13 +5,15 @@
"outputs": 1,
"params": [
{
- "name": "Input",
+ "name": "input",
+ "label": "Input",
"default": "",
"type": "node",
"description": "The seed geometry the simulation starts from. It is what the simnet shows at its start frame and what the first step reads."
},
{
- "name": "Substeps",
+ "name": "substeps",
+ "label": "Substeps",
"type": "spinbox",
"default": "1",
"min": 1.0,
@@ -20,13 +22,15 @@
"description": "How many times the chain inside runs per frame. A dt detail attribute of 1/substeps is set each run, so rates scaled by it stay steady rather than speeding up."
},
{
- "name": "Start Frame",
+ "name": "start_frame",
+ "label": "Start Frame",
"type": "text",
"default": "",
"description": "The frame at which the simulation shows its seed; earlier frames also show the seed. Empty follows the timeline's start frame."
},
{
- "name": "Cache",
+ "name": "cache",
+ "label": "Cache",
"type": "choice:false,true",
"default": "false",
"description": "When true, the latest solved frame is saved to disk so a later session can resume from it instead of solving from the seed.",
@@ -48,7 +52,7 @@
"type": "output",
"params": [
{
- "name": "Input",
+ "name": "input",
"default": "input1"
}
],
diff --git a/nodes/soft_transform.json b/nodes/soft_transform.json
index 1fd83f4..7703a30 100644
--- a/nodes/soft_transform.json
+++ b/nodes/soft_transform.json
@@ -4,12 +4,12 @@
"inputs": 1,
"outputs": 1,
"params": [
- { "name": "Input", "type": "node", "default": "", "description": "The node whose geometry is moved, with the move fading out smoothly around the affected region." },
- { "name": "Translation", "type": "float3", "default": "0.00:0.20:0.00", "description": "Offset in world units applied at full strength to the affected points, and faded by the falloff for points around them." },
- { "name": "Center", "type": "float3", "default": "0.00:0.00:0.00", "description": "Point in world space the falloff is measured from when no Group is given. Points within Radius of it are moved." },
- { "name": "Group", "type": "group", "default": "", "description": "Point group to move at full strength; the falloff is measured from its nearest member rather than from Center. Empty uses Center.", "group": "where" },
- { "name": "Radius", "type": "slider", "default": "0.50", "min": 0.0, "max": 10.0, "step": 0.01, "description": "Distance in world units over which the move fades to nothing. Points farther than this from the center or group are not moved.", "group": "falloff" },
- { "name": "Falloff", "type": "choice:Smooth,Linear,Constant", "default": "Smooth", "description": "Shape of the fade with distance. Smooth eases in with no crease, Linear falls off evenly, and Constant moves everything inside the radius fully.", "group": "falloff" },
- { "name": "Attribute", "type": "attribute", "default": "", "description": "Name of a point attribute to write each point's falloff weight to, from 1 at the center to 0 at the radius. Empty writes nothing.", "group": "output" }
+ { "name": "input", "label": "Input", "type": "node", "default": "", "description": "The node whose geometry is moved, with the move fading out smoothly around the affected region." },
+ { "name": "translation", "label": "Translation", "type": "float3", "default": "0.00:0.20:0.00", "description": "Offset in world units applied at full strength to the affected points, and faded by the falloff for points around them." },
+ { "name": "center", "label": "Center", "type": "float3", "default": "0.00:0.00:0.00", "description": "Point in world space the falloff is measured from when no Group is given. Points within Radius of it are moved." },
+ { "name": "group", "label": "Group", "type": "group", "default": "", "description": "Point group to move at full strength; the falloff is measured from its nearest member rather than from Center. Empty uses Center.", "group": "where" },
+ { "name": "radius", "label": "Radius", "type": "slider", "default": "0.50", "min": 0.0, "max": 10.0, "step": 0.01, "description": "Distance in world units over which the move fades to nothing. Points farther than this from the center or group are not moved.", "group": "falloff" },
+ { "name": "falloff", "label": "Falloff", "type": "choice:Smooth,Linear,Constant", "default": "Smooth", "description": "Shape of the fade with distance. Smooth eases in with no crease, Linear falls off evenly, and Constant moves everything inside the radius fully.", "group": "falloff" },
+ { "name": "attribute", "label": "Attribute", "type": "attribute", "default": "", "description": "Name of a point attribute to write each point's falloff weight to, from 1 at the center to 0 at the radius. Empty writes nothing.", "group": "output" }
]
}
diff --git a/nodes/sphere.json b/nodes/sphere.json
index 4bd290b..35bd2da 100644
--- a/nodes/sphere.json
+++ b/nodes/sphere.json
@@ -5,84 +5,94 @@
"outputs": 1,
"params": [
{
- "name": "Method",
+ "name": "method",
+ "label": "Method",
"default": "UV",
"type": "choice:UV,Icosphere,Cube",
"description": "How the sphere is tessellated: UV uses latitude rows and longitude columns, Icosphere subdivides an icosahedron into even triangles, Cube projects a gridded cube into quads."
},
{
- "name": "Radius",
+ "name": "radius",
+ "label": "Radius",
"default": "0.5",
"type": "slider",
"description": "Radius of the sphere in world units, the distance from Center to every point on its surface."
},
{
- "name": "Rows",
+ "name": "rows",
+ "label": "Rows",
"default": "16",
"type": "spinbox",
"min": 2,
"max": 128,
"step": 1,
- "show_when": "Method == UV",
+ "show_when": "method == UV",
"description": "Number of latitude bands from pole to pole for the UV method. More rows give a smoother profile and more points.",
"group": "resolution"
},
{
- "name": "Columns",
+ "name": "columns",
+ "label": "Columns",
"default": "24",
"type": "spinbox",
"min": 3,
"max": 128,
"step": 1,
- "show_when": "Method == UV",
+ "show_when": "method == UV",
"description": "Number of longitude segments around the UV sphere. More columns give a rounder silhouette and more points.",
"group": "resolution"
},
{
- "name": "Frequency",
+ "name": "frequency",
+ "label": "Frequency",
"default": "4",
"type": "spinbox",
"min": 1,
"max": 16,
"step": 1,
- "show_when": "Method == Icosphere",
+ "show_when": "method == Icosphere",
"description": "How many times each icosahedron edge is divided for the Icosphere method; each of the 20 faces becomes Frequency squared triangles.",
"group": "resolution"
},
{
- "name": "Resolution",
+ "name": "resolution",
+ "label": "Resolution",
"default": "8",
"type": "spinbox",
"min": 1,
"max": 64,
"step": 1,
- "show_when": "Method == Cube",
+ "show_when": "method == Cube",
"description": "Grid divisions along each edge of every cube face for the Cube method; each face gets Resolution squared quads.",
"group": "resolution"
},
{
- "name": "Center X",
+ "name": "center_x",
+ "label": "Center X",
"default": "0.0",
"type": "slider:-2:2",
"description": "X position of the sphere's center in world units.",
"group": "place"
},
{
- "name": "Center Y",
+ "name": "center_y",
+ "label": "Center Y",
"default": "0.55",
"type": "slider:-2:2",
"description": "Y (height) position of the sphere's center in world units.",
"group": "place"
},
{
- "name": "Center Z",
+ "name": "center_z",
+ "label": "Center Z",
"default": "0.0",
"type": "slider:-2:2",
"description": "Z position of the sphere's center in world units.",
"group": "place"
},
{
- "name": "Color",
+ "name": "color",
+ "label": "Color",
"default": "true",
"type": "toggle",
"description": "When on, colors each point by its surface direction (normal mapped to RGB); when off, the sphere uses the plain default color.",
diff --git a/nodes/split_edges.json b/nodes/split_edges.json
index f5e11cb..ea951aa 100644
--- a/nodes/split_edges.json
+++ b/nodes/split_edges.json
@@ -5,13 +5,15 @@
"outputs": 1,
"params": [
{
- "name": "Input",
+ "name": "input",
+ "label": "Input",
"type": "node",
"default": "",
"description": "The node whose mesh is refined. Point values ride to the new midpoints by interpolation."
},
{
- "name": "Target Length",
+ "name": "target_length",
+ "label": "Target Length",
"type": "slider",
"default": "0.10",
"min": 0.001,
diff --git a/nodes/subdivide.json b/nodes/subdivide.json
index 458b75c..04276cf 100644
--- a/nodes/subdivide.json
+++ b/nodes/subdivide.json
@@ -4,7 +4,7 @@
"inputs": 1,
"outputs": 1,
"params": [
- { "name": "Input", "type": "node", "default": "", "description": "The node whose triangles are subdivided." },
- { "name": "Depth", "type": "spinbox", "default": "1", "min": 0.0, "max": 6.0, "step": 1.0, "description": "How many times each triangle is split into four at its edge midpoints. Each level quadruples the count; points do not move. 0 passes the input through." }
+ { "name": "input", "label": "Input", "type": "node", "default": "", "description": "The node whose triangles are subdivided." },
+ { "name": "depth", "label": "Depth", "type": "spinbox", "default": "1", "min": 0.0, "max": 6.0, "step": 1.0, "description": "How many times each triangle is split into four at its edge midpoints. Each level quadruples the count; points do not move. 0 passes the input through." }
]
}
diff --git a/nodes/subnet.json b/nodes/subnet.json
index b0bf005..2d8ca99 100644
--- a/nodes/subnet.json
+++ b/nodes/subnet.json
@@ -4,6 +4,6 @@
"inputs": 1,
"outputs": 1,
"params": [
- { "name": "Input", "default": "", "type": "node", "description": "Node whose geometry is handed into this subnet; the input node inside reads it." }
+ { "name": "input", "label": "Input", "default": "", "type": "node", "description": "Node whose geometry is handed into this subnet; the input node inside reads it." }
]
}
diff --git a/nodes/suture.json b/nodes/suture.json
index 58416a0..a7d4b6f 100644
--- a/nodes/suture.json
+++ b/nodes/suture.json
@@ -4,10 +4,10 @@
"inputs": 2,
"outputs": 1,
"params": [
- { "name": "Input", "type": "node", "default": "", "description": "The surface to resolve. Points too close to the Against geometry are pushed out, and points in sustained contact are fused." },
- { "name": "Against", "type": "node", "default": "", "description": "The geometry the input is kept apart from, read as a surface. With nothing connected no contact is found and nothing is pushed." },
- { "name": "Distance Threshold", "type": "slider", "default": "0.05", "min": 0.0, "max": 1.0, "step": 0.005, "description": "Contact distance in world units: nearer points are pushed back out to it, and fusing points within it merge. 0 turns the node off." },
- { "name": "Fusion Threshold", "type": "spinbox", "default": "3", "min": 1.0, "max": 100.0, "step": 1.0, "description": "How many evaluations in a row a point must stay in contact, usually simulation steps, before it can fuse with nearby fusing points. Higher fuses later." },
- { "name": "Counter", "type": "attribute", "default": "contact", "description": "Integer point attribute counting consecutive steps in contact; it resets to 0 when contact breaks. Empty turns the node off.", "group": "output" }
+ { "name": "input", "label": "Input", "type": "node", "default": "", "description": "The surface to resolve. Points too close to the Against geometry are pushed out, and points in sustained contact are fused." },
+ { "name": "against", "label": "Against", "type": "node", "default": "", "description": "The geometry the input is kept apart from, read as a surface. With nothing connected no contact is found and nothing is pushed." },
+ { "name": "distance_threshold", "label": "Distance Threshold", "type": "slider", "default": "0.05", "min": 0.0, "max": 1.0, "step": 0.005, "description": "Contact distance in world units: nearer points are pushed back out to it, and fusing points within it merge. 0 turns the node off." },
+ { "name": "fusion_threshold", "label": "Fusion Threshold", "type": "spinbox", "default": "3", "min": 1.0, "max": 100.0, "step": 1.0, "description": "How many evaluations in a row a point must stay in contact, usually simulation steps, before it can fuse with nearby fusing points. Higher fuses later." },
+ { "name": "counter", "label": "Counter", "type": "attribute", "default": "contact", "description": "Integer point attribute counting consecutive steps in contact; it resets to 0 when contact breaks. Empty turns the node off.", "group": "output" }
]
}
diff --git a/nodes/switch.json b/nodes/switch.json
index 4d92fcc..145cc81 100644
--- a/nodes/switch.json
+++ b/nodes/switch.json
@@ -4,10 +4,10 @@
"inputs": 4,
"outputs": 1,
"params": [
- { "name": "Input", "type": "node", "default": "", "description": "Node whose geometry is passed through unchanged when Index is 0." },
- { "name": "Input 2", "type": "node", "default": "", "description": "Node whose geometry is passed through unchanged when Index is 1." },
- { "name": "Input 3", "type": "node", "default": "", "description": "Node whose geometry is passed through unchanged when Index is 2." },
- { "name": "Input 4", "type": "node", "default": "", "description": "Node whose geometry is passed through unchanged when Index is 3." },
- { "name": "Index", "type": "spinbox", "default": "0", "min": 0, "max": 3, "step": 1, "description": "Which input to pass on: 0 is Input, 1 is Input 2, and so on. Values past the range are clamped; an empty slot passes nothing." }
+ { "name": "input", "label": "Input", "type": "node", "default": "", "description": "Node whose geometry is passed through unchanged when Index is 0." },
+ { "name": "input_2", "label": "Input 2", "type": "node", "default": "", "description": "Node whose geometry is passed through unchanged when Index is 1." },
+ { "name": "input_3", "label": "Input 3", "type": "node", "default": "", "description": "Node whose geometry is passed through unchanged when Index is 2." },
+ { "name": "input_4", "label": "Input 4", "type": "node", "default": "", "description": "Node whose geometry is passed through unchanged when Index is 3." },
+ { "name": "index", "label": "Index", "type": "spinbox", "default": "0", "min": 0, "max": 3, "step": 1, "description": "Which input to pass on: 0 is Input, 1 is Input 2, and so on. Values past the range are clamped; an empty slot passes nothing." }
]
}
diff --git a/nodes/time.json b/nodes/time.json
index 97b97bd..344a293 100644
--- a/nodes/time.json
+++ b/nodes/time.json
@@ -4,10 +4,10 @@
"inputs": 1,
"outputs": 1,
"params": [
- { "name": "Input", "type": "node", "default": "", "description": "The geometry that passes through with a time detail attribute added." },
- { "name": "Attribute", "type": "attribute", "default": "t", "description": "Name of the detail attribute written with the 0 to 1 progress through the frame range. Empty writes nothing." },
- { "name": "Start Frame", "type": "spinbox", "default": "1", "min": -9999.0, "max": 9999.0, "step": 1.0, "description": "The frame at which the attribute reads 0.", "group": "range" },
- { "name": "End Frame", "type": "spinbox", "default": "100", "min": -9999.0, "max": 9999.0, "step": 1.0, "description": "The frame at which the attribute reads 1. If equal to Start Frame, it jumps from 0 to 1 at that frame.", "group": "range" },
- { "name": "Clamp", "type": "choice:true,false", "default": "true", "description": "When true the value stays between 0 and 1 outside the range. When false it keeps running, below 0 before the start and above 1 after the end.", "group": "range" }
+ { "name": "input", "label": "Input", "type": "node", "default": "", "description": "The geometry that passes through with a time detail attribute added." },
+ { "name": "attribute", "label": "Attribute", "type": "attribute", "default": "t", "description": "Name of the detail attribute written with the 0 to 1 progress through the frame range. Empty writes nothing." },
+ { "name": "start_frame", "label": "Start Frame", "type": "spinbox", "default": "1", "min": -9999.0, "max": 9999.0, "step": 1.0, "description": "The frame at which the attribute reads 0.", "group": "range" },
+ { "name": "end_frame", "label": "End Frame", "type": "spinbox", "default": "100", "min": -9999.0, "max": 9999.0, "step": 1.0, "description": "The frame at which the attribute reads 1. If equal to Start Frame, it jumps from 0 to 1 at that frame.", "group": "range" },
+ { "name": "clamp", "label": "Clamp", "type": "choice:true,false", "default": "true", "description": "When true the value stays between 0 and 1 outside the range. When false it keeps running, below 0 before the start and above 1 after the end.", "group": "range" }
]
}
diff --git a/nodes/transfer.json b/nodes/transfer.json
index d28c8a9..ef6d47f 100644
--- a/nodes/transfer.json
+++ b/nodes/transfer.json
@@ -5,40 +5,46 @@
"outputs": 1,
"params": [
{
- "name": "Input",
+ "name": "input",
+ "label": "Input",
"type": "node",
"default": "",
"description": "The geometry that receives values: each of its points takes them from the nearest point of the From geometry."
},
{
- "name": "From",
+ "name": "from",
+ "label": "From",
"type": "node",
"default": "",
"description": "The geometry to copy point attributes and groups from, by nearest point. Nothing is copied if it is unconnected or has no points."
},
{
- "name": "Attributes",
+ "name": "attributes",
+ "label": "Attributes",
"type": "text",
"default": "",
"description": "Comma-separated names of the point attributes to copy. Empty copies every point attribute the source has."
},
{
- "name": "Transfer Groups",
+ "name": "transfer_groups",
+ "label": "Transfer Groups",
"type": "toggle",
"default": "false",
"description": "Also copies point group membership from the nearest source point, so points join or leave each group to match the source.",
"group": "groups"
},
{
- "name": "Groups",
+ "name": "groups",
+ "label": "Groups",
"type": "text",
"default": "",
- "show_when": "Transfer Groups == true",
+ "show_when": "transfer_groups == true",
"description": "Comma-separated names of the point groups to copy. Empty copies every point group the source has.",
"group": "groups"
},
{
- "name": "Maximum Distance",
+ "name": "maximum_distance",
+ "label": "Maximum Distance",
"type": "slider",
"default": "0.00",
"min": 0.0,
@@ -48,7 +54,8 @@
"group": "reach"
},
{
- "name": "Group",
+ "name": "group",
+ "label": "Group",
"type": "group",
"default": "",
"description": "Only points of the input in this group receive values. Empty lets every point receive.",
diff --git a/nodes/transform.json b/nodes/transform.json
index 346bef5..0b04d24 100644
--- a/nodes/transform.json
+++ b/nodes/transform.json
@@ -4,7 +4,7 @@
"inputs": 1,
"outputs": 1,
"params": [
- { "name": "Input", "default": "", "type": "node", "description": "The node whose geometry is moved by the Translation." },
- { "name": "Translation", "default": "0.00:0.00:0.00", "type": "float3", "min": -10.0, "max": 10.0, "description": "Offset in world units added to every point's position." }
+ { "name": "input", "label": "Input", "default": "", "type": "node", "description": "The node whose geometry is moved by the Translation." },
+ { "name": "translation", "label": "Translation", "default": "0.00:0.00:0.00", "type": "float3", "min": -10.0, "max": 10.0, "description": "Offset in world units added to every point's position." }
]
}
diff --git a/nodes/valence.json b/nodes/valence.json
index 43f0301..a99da76 100644
--- a/nodes/valence.json
+++ b/nodes/valence.json
@@ -4,8 +4,8 @@
"inputs": 1,
"outputs": 1,
"params": [
- { "name": "Input", "type": "node", "default": "", "description": "The node whose geometry is measured; each point's valence is written as a point attribute and the geometry passed on." },
- { "name": "Attribute", "type": "attribute", "default": "valence", "description": "The name of the integer point attribute the count is written to. Empty writes nothing." },
- { "name": "Measure", "type": "choice:Neighbours,Primitives", "default": "Neighbours", "description": "What is counted per point: its neighbouring points along edges, or the primitives that use it." }
+ { "name": "input", "label": "Input", "type": "node", "default": "", "description": "The node whose geometry is measured; each point's valence is written as a point attribute and the geometry passed on." },
+ { "name": "attribute", "label": "Attribute", "type": "attribute", "default": "valence", "description": "The name of the integer point attribute the count is written to. Empty writes nothing." },
+ { "name": "measure", "label": "Measure", "type": "choice:Neighbours,Primitives", "default": "Neighbours", "description": "What is counted per point: its neighbouring points along edges, or the primitives that use it." }
]
}
diff --git a/nodes/visualize.json b/nodes/visualize.json
index 1f687c6..4f1a5e0 100644
--- a/nodes/visualize.json
+++ b/nodes/visualize.json
@@ -5,87 +5,98 @@
"outputs": 1,
"params": [
{
- "name": "Input",
+ "name": "input",
+ "label": "Input",
"type": "node",
"default": "",
"description": "The node whose geometry this reads; its point colours are set, or vector lines added, and the geometry passed on."
},
{
- "name": "Attribute",
+ "name": "attribute",
+ "label": "Attribute",
"type": "attribute",
"default": "mass",
"description": "The point attribute shown. Ramp colours points by its value (first component); Vector draws it as a line from each point."
},
{
- "name": "Mode",
+ "name": "mode",
+ "label": "Mode",
"type": "choice:Ramp,Vector",
"default": "Ramp",
"description": "Ramp colours each point by the attribute's value; Vector draws a line from each point along the attribute as a direction."
},
{
- "name": "Ramp",
+ "name": "ramp",
+ "label": "Ramp",
"type": "choice:Grayscale,Heat,Spectrum,Viridis",
"default": "Viridis",
- "show_when": "Mode == Ramp",
+ "show_when": "mode == Ramp",
"description": "The colour scale values are mapped through, from the low end to the high end.",
"group": "ramp"
},
{
- "name": "Range",
+ "name": "range",
+ "label": "Range",
"type": "choice:Auto,Manual",
"default": "Auto",
- "show_when": "Mode == Ramp",
+ "show_when": "mode == Ramp",
"description": "Auto spreads the ramp from the attribute's lowest to highest value over all points; Manual uses From and To.",
"group": "ramp"
},
{
- "name": "From",
+ "name": "from",
+ "label": "From",
"type": "float",
"default": "0.00",
- "show_when": "Mode == Ramp && Range == Manual",
+ "show_when": "mode == Ramp && range == Manual",
"description": "The value shown at the low end of the ramp. Values below it are clamped to that end.",
"group": "ramp"
},
{
- "name": "To",
+ "name": "to",
+ "label": "To",
"type": "float",
"default": "1.00",
- "show_when": "Mode == Ramp && Range == Manual",
+ "show_when": "mode == Ramp && range == Manual",
"description": "The value shown at the high end of the ramp. Values above it are clamped to that end.",
"group": "ramp"
},
{
- "name": "Blend",
+ "name": "blend",
+ "label": "Blend",
"type": "choice:Set,Multiply,Add",
"default": "Set",
- "show_when": "Mode == Ramp",
+ "show_when": "mode == Ramp",
"description": "How the ramp colour meets the existing colour: Set replaces it, Multiply tints it, Add brightens it. Opacity then fades toward the result.",
"group": "ramp"
},
{
- "name": "Opacity",
+ "name": "opacity",
+ "label": "Opacity",
"type": "slider",
"default": "1.00",
"min": 0.0,
"max": 1.0,
"step": 0.01,
- "show_when": "Mode == Ramp",
+ "show_when": "mode == Ramp",
"description": "How far each point's colour moves from its old colour toward the blended ramp colour: 1 is fully, 0 not at all.",
"group": "ramp"
},
{
- "name": "Scale",
+ "name": "scale",
+ "label": "Scale",
"type": "slider",
"default": "0.20",
"min": 0.0,
"max": 2.0,
"step": 0.01,
- "show_when": "Mode == Vector",
+ "show_when": "mode == Vector",
"description": "Multiplies the attribute's vector to get the drawn line's length, in world units per unit of the attribute.",
"group": "vector"
},
{
- "name": "Group",
+ "name": "group",
+ "label": "Group",
"type": "group",
"default": "",
"description": "A point group limiting which points are coloured or get a line. Empty means every point; Auto range still measures all points.",
diff --git a/nodes/volume.json b/nodes/volume.json
index 68cd091..f7d6d72 100644
--- a/nodes/volume.json
+++ b/nodes/volume.json
@@ -4,10 +4,10 @@
"inputs": 1,
"outputs": 1,
"params": [
- { "name": "Input", "type": "node", "default": "", "description": "The node whose closed surface is turned into a distance field and rebuilt as an offset surface or a shell." },
- { "name": "Mode", "type": "choice:Offset,Shell", "default": "Offset", "description": "Offset moves the whole surface out or in. Shell keeps only a wall between the offset surface and a copy moved inward by Thickness." },
- { "name": "Voxel Size", "type": "slider", "default": "0.05", "min": 0.005, "max": 1.0, "step": 0.005, "description": "Grid spacing of the distance field in world units. Smaller keeps finer detail but costs more; the grid is capped at 256 samples per axis." },
- { "name": "Offset", "type": "slider", "default": "0.00", "min": -1.0, "max": 1.0, "step": 0.005, "description": "How far, in world units, the surface is moved: positive grows it outward, negative shrinks it inward. 0 rebuilds it in place." },
- { "name": "Thickness", "type": "slider", "default": "0.05", "min": 0.001, "max": 1.0, "step": 0.005, "show_when": "Mode == Shell", "description": "Wall thickness of the shell in world units, measured inward from the offset surface." }
+ { "name": "input", "label": "Input", "type": "node", "default": "", "description": "The node whose closed surface is turned into a distance field and rebuilt as an offset surface or a shell." },
+ { "name": "mode", "label": "Mode", "type": "choice:Offset,Shell", "default": "Offset", "description": "Offset moves the whole surface out or in. Shell keeps only a wall between the offset surface and a copy moved inward by Thickness." },
+ { "name": "voxel_size", "label": "Voxel Size", "type": "slider", "default": "0.05", "min": 0.005, "max": 1.0, "step": 0.005, "description": "Grid spacing of the distance field in world units. Smaller keeps finer detail but costs more; the grid is capped at 256 samples per axis." },
+ { "name": "offset", "label": "Offset", "type": "slider", "default": "0.00", "min": -1.0, "max": 1.0, "step": 0.005, "description": "How far, in world units, the surface is moved: positive grows it outward, negative shrinks it inward. 0 rebuilds it in place." },
+ { "name": "thickness", "label": "Thickness", "type": "slider", "default": "0.05", "min": 0.001, "max": 1.0, "step": 0.005, "show_when": "mode == Shell", "description": "Wall thickness of the shell in world units, measured inward from the offset surface." }
]
}
diff --git a/nodes/wrangle.json b/nodes/wrangle.json
index 88ba921..a7738ea 100644
--- a/nodes/wrangle.json
+++ b/nodes/wrangle.json
@@ -4,13 +4,14 @@
"inputs": 1,
"outputs": 1,
"params": [
- { "name": "Input", "default": "", "type": "node", "description": "The node whose geometry the script runs over. With nothing connected the script runs on empty geometry and can build its own." },
- { "name": "Class", "type": "choice:Points,Primitives,Detail", "default": "Points", "description": "What the script runs once for: each point, each primitive, or once for the whole geometry (Detail). Sets what @P and @name refer to." },
- { "name": "Group", "default": "", "type": "group", "description": "A group of the chosen class limiting which elements the script runs on. Empty means all; ignored in Detail class." },
+ { "name": "input", "label": "Input", "default": "", "type": "node", "description": "The node whose geometry the script runs over. With nothing connected the script runs on empty geometry and can build its own." },
+ { "name": "class", "label": "Class", "type": "choice:Points,Primitives,Detail", "default": "Points", "description": "What the script runs once for: each point, each primitive, or once for the whole geometry (Detail). Sets what @P and @name refer to." },
+ { "name": "group", "label": "Group", "default": "", "type": "group", "description": "A group of the chosen class limiting which elements the script runs on. Empty means all; ignored in Detail class." },
{
- "name": "Code",
+ "name": "code",
+ "label": "Code",
"type": "code",
- "default": "// Runs once per point. @P is the position, @name any attribute;\n// ch(\"Name\") reads a parameter, neighbours(@ptnum) the topology.\n@P.y += sin(@P.x * 4.0) * 0.15;\n",
+ "default": "// Runs once per point. @P is the position, @name any attribute;\n// ch(\"name\") reads a parameter, neighbours(@ptnum) the topology.\n@P.y += sin(@P.x * 4.0) * 0.15;\n",
"description": "The Rhai script run per element. @name reads or writes an attribute, ch(\"path\") reads a parameter. Applies on ctrl+enter or when leaving the editor; an error leaves the input unchanged.",
"group": "code"
}
diff --git a/src/api.rs b/src/api.rs
index 1e45797..867041e 100644
--- a/src/api.rs
+++ b/src/api.rs
@@ -60,12 +60,12 @@ pub(crate) fn mcp_tools() -> Vec<McpTool> {
),
tool(
"set_param",
- "Set a parameter on the node at the given slot. All values are strings (e.g. \"1.5\", \"0.2,0.4,1\"). A value that reads as an expression — ch(\"../sphere1/Radius\") * 2, $F / 24 — becomes one (Houdini paths: relative to the node, .. its parent, / the root, a bare name its own parameter).",
+ "Set a parameter on the node at the given slot. All values are strings (e.g. \"1.5\", \"0.2,0.4,1\"). A value that reads as an expression — ch(\"../sphere1/radius\") * 2, $F / 24 — becomes one (Houdini paths: relative to the node, .. its parent, / the root, a bare name its own parameter).",
json!({
"type": "object",
"properties": {
"slot": slot("Child index in the current network level"),
- "name": { "type": "string", "description": "Parameter name" },
+ "name": { "type": "string", "description": "Parameter name (radius, base_resolution); its label (Base Resolution) is accepted too" },
"value": { "type": "string", "description": "New value, as a string" },
},
"required": ["slot", "name", "value"],
@@ -163,7 +163,8 @@ pub(crate) fn mcp_tools() -> Vec<McpTool> {
"type": "object",
"properties": {
"slot": slot("Child index in the current network level"),
- "name": { "type": "string" },
+ "name": { "type": "string", "description": "The parameter's name, what a ch() path spells: lowercase letters, digits and underscores (base_resolution)" },
+ "label": { "type": "string", "description": "What the params pane shows for it (Base Resolution); the name when absent" },
"param_type": { "type": "string", "description": "One of text, float, slider, spinbox, float3, choice, toggle, button, code, node, attribute, group — optionally with detail after a colon (slider:-2:2, choice:A,B,C)" },
"default": { "type": "string", "description": "Default value, as a string" },
},
@@ -177,7 +178,7 @@ pub(crate) fn mcp_tools() -> Vec<McpTool> {
"type": "object",
"properties": {
"slot": slot("Child index in the current network level"),
- "name": { "type": "string", "description": "Parameter name" },
+ "name": { "type": "string", "description": "Parameter name (radius, base_resolution); its label (Base Resolution) is accepted too" },
},
"required": ["slot", "name"],
}),
diff --git a/src/app.rs b/src/app.rs
index 3ce737d..f33f694 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -74,7 +74,7 @@ pub const MIN_COLUMN: f32 = 120.0;
pub const BREADCRUMB_H: f32 = 24.0;
pub const PLAYBAR_H: f32 = 36.0;
-pub use crate::param::{invalid_params, unknown_param_kinds, ParamDef, ParamKind, ParamSlot, ParamValue};
+pub use crate::param::{invalid_params, is_param_name, misnamed_params, param_name_of, unknown_param_kinds, ParamDef, ParamKind, ParamSlot, ParamValue};
/// Expand a leading `~` to the home directory. A path typed into a text field
/// is typed by a person, and `~/models/thing.stl` is what a person writes.
@@ -111,7 +111,7 @@ pub fn param_visible(params: &[ParamDef], cond: &str) -> bool {
None => return false,
},
};
- let Some(sibling) = params.iter().find(|p| p.name.eq_ignore_ascii_case(name)) else {
+ let Some(sibling) = params.iter().find(|p| p.name == name) else {
return false;
};
let matches = wanted
@@ -377,7 +377,7 @@ pub struct Project {
}
/// The format `Project` saves in — see its `format` field.
-pub const PROJECT_FORMAT: u32 = 3;
+pub const PROJECT_FORMAT: u32 = 4;
/// One entry in a node's right-click context menu, parallel to the visible
/// labels shown via `context_menu::show`.
@@ -586,7 +586,15 @@ pub enum McpAction {
DeleteNode { slot: usize },
RenameNode { slot: usize, new_name: String },
MoveNode { slot: usize, x: f32, y: f32 },
- AddParam { slot: usize, name: String, param_type: String, default: String },
+ AddParam {
+ slot: usize,
+ name: String,
+ param_type: String,
+ default: String,
+ /// What the params pane shows for it; the name when absent.
+ #[serde(default)]
+ label: String,
+ },
DeleteParam { slot: usize, name: String },
ToggleCircularPane,
MenuClick { widget_idx: usize, menu_idx: usize, item_idx: usize },
@@ -820,7 +828,7 @@ pub fn same_value_row_text(kept: &str, shown: &str) -> bool {
/// A wire whose row is hidden (`show_when`) names nothing here: it keeps its
/// port and draws no line, since what is not on screen should not be. An
/// expression wire names what it EVALUATES to at `frame` — the Remesh
-/// subnet's transfer reads its From through `if(chs("../From"), …,
+/// subnet's transfer reads its From through `if(chs("../from"), …,
/// "input1")`, and is drawn from input1 — and nothing when it fails.
/// Auto-layout reads the same wires.
pub fn node_wires_at(root: &FsNode, node: &FsNode, frame: i32) -> Vec<(String, String)> {
@@ -844,7 +852,7 @@ pub(crate) fn splice_out(dir: &mut FsNode, slot: usize) {
let Some(upstream) = gone
.params
.iter()
- .find(|p| p.name == "Input" && p.kind() == ParamKind::Node && !p.is_expr() && param_visible(&gone.params, &p.show_when))
+ .find(|p| p.name == "input" && p.kind() == ParamKind::Node && !p.is_expr() && param_visible(&gone.params, &p.show_when))
.map(|p| p.text().trim().to_string())
.filter(|u| !u.is_empty() && *u != name)
else {
@@ -875,13 +883,13 @@ pub(crate) fn splice_into_wire(dir: &mut FsNode, mid_id: &str, src_name: String,
/// [`splice_into_wire`] for a chain: its `head` takes the wire's upstream,
/// and the downstream node reads its `tail`. A pasted chain goes in whole.
pub(crate) fn splice_chain_into_wire(dir: &mut FsNode, head_id: &str, tail_id: &str, src_name: String, dest_id: &str) -> bool {
- let has_input = |id: &str| dir.children.iter().any(|c| c.id == id && c.params.iter().any(|p| p.name == "Input"));
+ let has_input = |id: &str| dir.children.iter().any(|c| c.id == id && c.params.iter().any(|p| p.name == "input"));
let Some(tail_name) = dir.children.iter().find(|c| c.id == tail_id).map(|c| c.name.clone()) else { return false };
if !has_input(head_id) || !has_input(dest_id) {
return false;
}
for (id, wire) in [(head_id, src_name), (dest_id, tail_name)] {
- if let Some(p) = dir.children.iter_mut().find(|c| c.id == id).and_then(|c| c.params.iter_mut().find(|p| p.name == "Input")) {
+ if let Some(p) = dir.children.iter_mut().find(|c| c.id == id).and_then(|c| c.params.iter_mut().find(|p| p.name == "input")) {
p.set_text(wire);
}
}
@@ -952,14 +960,14 @@ pub fn param_display(params: &[ParamDef]) -> Vec<(String, String, String)> {
/// shows it, and the row type.
fn param_row(p: &ParamDef) -> (String, String, String) {
{
- let key = if p.label.is_empty() { &p.name } else { &p.label };
+ let key = p.shown_name();
let kind = p.kind();
let value = if p.ty() == "choice" && !p.options().is_empty() && p.text().is_empty() {
p.options()[0].clone()
} else {
p.text().to_string()
};
- // An expression (`ch("../sphere1/Radius") * 2`) is shown as the text
+ // An expression (`ch("../sphere1/radius") * 2`) is shown as the text
// it is: a slider cannot hold it, and a spinbox would show zero and
// then write zero back over it.
let ptype = if p.is_expr() {
@@ -988,7 +996,7 @@ fn param_row(p: &ParamDef) -> (String, String, String) {
} else {
p.ty().to_string()
};
- (key.clone(), value, ptype)
+ (key.to_string(), value, ptype)
}
}
@@ -1125,6 +1133,42 @@ pub fn sanitize_node_name(name: &str) -> String {
}
}
+/// The parameter MCP means by `name`: the one of that name, else the one
+/// whose label it is (in any case — what the pane shows is what a person
+/// reads off it), else the one its [`param_name_of`] names, so a script
+/// written against the old names (`Base Resolution`) still reaches
+/// `base_resolution`.
+pub fn param_by_name_or_label<'a>(params: &'a mut [ParamDef], name: &str) -> Option<&'a mut ParamDef> {
+ let i = params
+ .iter()
+ .position(|p| p.name == name)
+ .or_else(|| params.iter().position(|p| !p.label.is_empty() && p.label.eq_ignore_ascii_case(name)))
+ .or_else(|| {
+ let snake = param_name_of(name);
+ params.iter().position(|p| p.name == snake)
+ })?;
+ params.get_mut(i)
+}
+
+/// A channel path with its parameter segment — the last, less a `.x` /
+/// `.y` / `.z` component — renamed by [`param_name_of`]: format 4's
+/// rewrite of what an expression names. None when nothing changes.
+fn param_path_renamed(path: &str) -> Option<String> {
+ let (head, last) = match path.rfind('/') {
+ Some(i) => (&path[..=i], &path[i + 1..]),
+ None => ("", path),
+ };
+ let (name, comp) = match last.rfind('.') {
+ Some(i) if i > 0 && matches!(&last[i..], ".x" | ".y" | ".z") => (&last[..i], &last[i..]),
+ _ => (last, ""),
+ };
+ if name.is_empty() || name == "." || name == ".." {
+ return None;
+ }
+ let renamed = param_name_of(name);
+ (renamed != name && !renamed.is_empty()).then(|| format!("{head}{renamed}{comp}"))
+}
+
impl Project {
/// Bring a loaded project's node names under [`sanitize_node_name`],
/// and follow every reference to a renamed node.
@@ -1210,9 +1254,47 @@ impl Project {
if self.format < 3 {
self.rename_attribute("UV", "uv");
}
+ if self.format < 4 {
+ self.migrate_param_names();
+ }
self.format = PROJECT_FORMAT;
}
+ /// Format 3 → 4: a parameter's NAME is an identifier
+ /// (`base_resolution`), where it was the text the pane showed (`Base
+ /// Resolution`) — that is the template's label now. Every parameter is
+ /// renamed by [`param_name_of`], and what spells a name follows: each
+ /// channel path in an expression and in a wrangle's Code, whose last
+ /// segment is a parameter (`chf("../sphere1/Radius")` →
+ /// `chf("../sphere1/radius")`). `show_when` is left alone: the template
+ /// merge replaces it, and `page::migrate_preset_rows` recognises an old
+ /// page by the condition as it was. The retired settings nodes are
+ /// skipped — `Project::migrate_meta_settings_node` reads them by the
+ /// names they were saved with, after this.
+ fn migrate_param_names(&mut self) {
+ fn walk(node: &mut FsNode) {
+ if matches!(node.node_type.as_str(), "session" | "meta" | "utility") {
+ return;
+ }
+ for p in &mut node.params {
+ let name = param_name_of(&p.name);
+ if !name.is_empty() {
+ p.name = name;
+ }
+ if p.is_expr() || p.kind() == ParamKind::Code {
+ let text = crate::expr::rewrite_paths(p.text(), param_path_renamed);
+ if text != p.text() {
+ p.set_text(text);
+ }
+ }
+ }
+ for c in &mut node.children {
+ walk(c);
+ }
+ }
+ walk(&mut self.root);
+ }
+
/// Format 0 → 1, as above.
fn migrate_param_refs(&mut self) {
fn walk(node: &mut FsNode) {
@@ -1288,7 +1370,7 @@ fn rename_at_attribute(code: &str, old: &str, new: &str) -> String {
}
/// A template's parameters whose defaults READ as expressions become ones:
-/// `chf("../Radius")` in `embryo.json` is a reference by any reading, and a
+/// `chf("../radius")` in `embryo.json` is a reference by any reading, and a
/// template author should not have to say `"expr": true` beside it (though
/// they may). `looks_like_expression` is the inference, and it is the same
/// one a typed or scripted value gets — arithmetic alone is not enough.
@@ -1482,7 +1564,7 @@ pub fn merge_template_defs(root: &mut FsNode, templates: &[NodeTemplate]) {
// settings are the viewport's, and ride the project's view state.
// Retired, and dropped from a save that still has them.
if node.node_type == "camera" {
- node.params.retain(|p| p.name != "Square Aspect" && p.name != "Show Camera Pivot");
+ node.params.retain(|p| p.name != "square_aspect" && p.name != "show_camera_pivot");
}
// A page's size is its Width and Height now, Preset has no Custom
// and there is no Orientation row: a save from before is carried
@@ -1495,7 +1577,7 @@ pub fn merge_template_defs(root: &mut FsNode, templates: &[NodeTemplate]) {
// every blend alike) and is retired; a save holding it is Set, or
// it would load as a choice the row no longer offers.
if node.node_type == "visualize" {
- if let Some(p) = node.params.iter_mut().find(|p| p.name == "Blend" && p.text().trim().eq_ignore_ascii_case("mix")) {
+ if let Some(p) = node.params.iter_mut().find(|p| p.name == "blend" && p.text().trim().eq_ignore_ascii_case("mix")) {
p.set_text("Set".to_string());
}
}
@@ -1513,8 +1595,8 @@ pub fn merge_template_defs(root: &mut FsNode, templates: &[NodeTemplate]) {
.iter_mut()
.find(|ic| ic.name == tc.name && ic.node_type == tc.node_type)
.expect("children_match checked above");
- if let Some(t_code) = tc.params.iter().find(|p| p.name == "Code") {
- if let Some(i_code) = ic.params.iter_mut().find(|p| p.name == "Code") {
+ if let Some(t_code) = tc.params.iter().find(|p| p.name == "code") {
+ if let Some(i_code) = ic.params.iter_mut().find(|p| p.name == "code") {
i_code.set_text(t_code.text().to_string());
}
}
@@ -1567,6 +1649,17 @@ pub fn load_fs_tree() -> FsNode {
.collect::<Vec<_>>()
.join(", ")
),
+ // So is a parameter whose name is not one: the name is
+ // what a path spells, and the pane shows the label.
+ Ok(node) if !misnamed_params(&node).is_empty() => eprintln!(
+ "cce-designer: dropping node template {} — a parameter name must be lowercase letters, digits and underscores: {}",
+ path.display(),
+ misnamed_params(&node)
+ .iter()
+ .map(|(at, name)| format!("{at} '{name}' (say '{}', with the text as its label)", param_name_of(name)))
+ .collect::<Vec<_>>()
+ .join(", ")
+ ),
Ok(mut node) => {
infer_template_exprs(&mut node);
raw_nodes.push(node);
@@ -1616,7 +1709,7 @@ pub fn load_fs_tree() -> FsNode {
base_p.set_text(override_p.text().to_string());
// The flag travels with the value: an override
// that is a reference (the Embryo's sphere1
- // reading `chf("../Radius")`) stays one.
+ // reading `chf("../radius")`) stays one.
base_p.set_expr(override_p.is_expr());
}
}
@@ -3552,11 +3645,11 @@ impl State {
// From what the last pan wrote in full, while the node
// still says it; from the node otherwise.
let (from_pivot, from_pos) = match exact {
- Some((name, p, e)) if name == active && text(node, "Pivot") == Some(fmt3(p)) && text(node, "Position") == Some(fmt3(e)) => (p, e),
+ Some((name, p, e)) if name == active && text(node, "pivot") == Some(fmt3(p)) && text(node, "position") == Some(fmt3(e)) => (p, e),
_ => (pivot, pos),
};
let (to_pivot, to_pos) = (from_pivot + by, from_pos + by);
- for (name, v) in [("Pivot", to_pivot), ("Position", to_pos)] {
+ for (name, v) in [("pivot", to_pivot), ("position", to_pos)] {
if let Some(p) = node.params.iter_mut().find(|p| p.name == name) {
p.set_text(fmt3(v));
}
@@ -4313,13 +4406,13 @@ impl State {
// set, else name), so resolve back to the param by that
// key rather than by name alone.
if let Some(p) = child.params.iter_mut().find(|p| {
- let key = if p.label.is_empty() { &p.name } else { &p.label };
+ let key = p.shown_name();
key == u_name
}) {
// A value row presents a single number spread
// over its components; read back unchanged, it
// is the text it was, not an edit.
- let presented = p.name == "Value" && !p.is_expr() && same_value_row_text(p.text(), u_val);
+ let presented = p.name == "value" && !p.is_expr() && same_value_row_text(p.text(), u_val);
if p.text() != *u_val && !presented {
// A reference typed into a plain row becomes
// an expression — the one way to make one
@@ -4330,7 +4423,7 @@ impl State {
|| (p.takes_expressions() && crate::expr::looks_like_expression(u_val));
if !as_expr {
if let Err(why) = p.check(u_val) {
- rejected.push((u_name.clone(), p.text().to_string(), format!("{}: {why}", p.name)));
+ rejected.push((u_name.clone(), p.text().to_string(), format!("{}: {why}", p.shown_name())));
continue;
}
}
@@ -4378,7 +4471,7 @@ impl State {
// and Height; what they overwrite is part of the step.
let mut followed = false;
if child.node_type == "page" {
- let setters: Vec<ParamDef> = was.iter().filter(|w| matches!(w.name.as_str(), "Preset" | "Units")).cloned().collect();
+ let setters: Vec<ParamDef> = was.iter().filter(|w| matches!(w.name.as_str(), "preset" | "units")).cloned().collect();
for w in setters {
for r in crate::page::follow_page_rows(child, &w) {
followed = true;
@@ -4476,7 +4569,7 @@ impl State {
return;
};
let node = node.clone();
- let file = crate::geometry::node_param_str(&node, "File", "");
+ let file = crate::geometry::node_param_str(&node, "file", "");
let file = file.trim().to_string();
if file.is_empty() {
self.update_status_text("Export: set a File first.");
@@ -4883,7 +4976,7 @@ impl State {
.params
.iter()
.filter(|p| matches!(p.kind(), ParamKind::Attribute | ParamKind::Group))
- .map(|p| (if p.label.is_empty() { p.name.clone() } else { p.label.clone() }, p.kind()))
+ .map(|p| (p.shown_name().to_string(), p.kind()))
.collect();
let value_row = Self::attribute_value_target(node);
if kinds.is_empty() && value_row.is_none() {
@@ -4940,12 +5033,12 @@ impl State {
if !node.node_type.eq_ignore_ascii_case("attribute") {
return None;
}
- let p = node.params.iter().find(|p| p.name == "Value")?;
+ let p = node.params.iter().find(|p| p.name == "value")?;
if p.is_expr() {
return None;
}
// Read from an attribute, the row is not shown at all.
- if node_param_str(node, "Value From", "Constant").eq_ignore_ascii_case("attribute") {
+ if node_param_str(node, "value_from", "Constant").eq_ignore_ascii_case("attribute") {
return None;
}
let comps = p
@@ -4958,12 +5051,12 @@ impl State {
if comps == 0 || comps > 4 || raw != comps {
return None;
}
- let key = if p.label.is_empty() { p.name.clone() } else { p.label.clone() };
- let name = node_param_str(node, "Attribute Name", "");
+ let key = p.shown_name().to_string();
+ let name = node_param_str(node, "attribute_name", "");
let name = name.trim().to_string();
let ball = p.wants_trackball(!name.eq_ignore_ascii_case("Col"));
- let target = match node_param_str(node, "Operation", "Create").to_lowercase().as_str() {
- "create" => ValueTarget::Width(match node_param_str(node, "Type", "Float").to_lowercase().as_str() {
+ let target = match node_param_str(node, "operation", "Create").to_lowercase().as_str() {
+ "create" => ValueTarget::Width(match node_param_str(node, "type", "Float").to_lowercase().as_str() {
"float3" => 3,
"float2" => 2,
"float4" => 4,
@@ -4988,7 +5081,7 @@ impl State {
/// is what decides the control the Attribute node's Value row gets.
fn input_pick_lists(&mut self, node_id: &str) -> PickLists {
let input_id = crate::viewer_state::find_node_by_id(&self.fs_root, node_id)
- .and_then(|n| crate::geometry::param_node(&self.fs_root, n, "Input"))
+ .and_then(|n| crate::geometry::param_node(&self.fs_root, n, "input"))
.map(|n| n.id.clone());
let Some(input_id) = input_id else { return PickLists::default() };
let key = (input_id.clone(), self.rt_geometry_version);
@@ -5380,10 +5473,10 @@ impl State {
}
None
};
- let pos = node.params.iter().find(|p| p.name == "Position")
+ let pos = node.params.iter().find(|p| p.name == "position")
.and_then(|p| parse3(p.text()))
.unwrap_or(Vec3::new(2.5, 1.8, 2.5));
- let piv = node.params.iter().find(|p| p.name == "Pivot")
+ let piv = node.params.iter().find(|p| p.name == "pivot")
.and_then(|p| parse3(p.text()))
.unwrap_or(Vec3::ZERO);
let offset = pos - piv;
@@ -5394,10 +5487,10 @@ impl State {
};
let new_pos = center + dir_unit * dist;
let fmt3 = |v: Vec3| format!("{:.2}:{:.2}:{:.2}", v.x, v.y, v.z);
- if let Some(p) = node.params.iter_mut().find(|p| p.name == "Pivot") {
+ if let Some(p) = node.params.iter_mut().find(|p| p.name == "pivot") {
p.set_text(fmt3(center));
}
- if let Some(p) = node.params.iter_mut().find(|p| p.name == "Position") {
+ if let Some(p) = node.params.iter_mut().find(|p| p.name == "position") {
p.set_text(fmt3(new_pos));
}
self.viewport_mut().zoom = 1.0;
@@ -5476,16 +5569,16 @@ impl State {
}
None
};
- let pos = node.params.iter().find(|p| p.name == "Position")
+ let pos = node.params.iter().find(|p| p.name == "position")
.and_then(|p| parse3(p.text()))
.unwrap_or(Vec3::new(2.5, 1.8, 2.5));
- let piv = node.params.iter().find(|p| p.name == "Pivot")
+ let piv = node.params.iter().find(|p| p.name == "pivot")
.and_then(|p| parse3(p.text()))
.unwrap_or(Vec3::ZERO);
let offset = pos - piv;
let dir_unit = if offset.length() > 1e-4 { offset.normalize() } else { Vec3::new(2.5, 1.8, 2.5).normalize() };
let new_pos = piv + dir_unit * dist;
- if let Some(p) = node.params.iter_mut().find(|p| p.name == "Position") {
+ if let Some(p) = node.params.iter_mut().find(|p| p.name == "position") {
p.set_text(format!("{:.3}:{:.3}:{:.3}", new_pos.x, new_pos.y, new_pos.z));
}
self.viewport_mut().zoom = 1.0;
@@ -5525,7 +5618,7 @@ impl State {
return None;
}
let p = child.params.iter().find(|p| {
- let key = if p.label.is_empty() { &p.name } else { &p.label };
+ let key = p.shown_name();
*key == row.0
})?;
Some((slot, p.name.clone()))
@@ -5541,7 +5634,7 @@ impl State {
/// What the template says `pname` on `node` defaults to, where `dir` is
/// the level `node` sits in. A child of a subnet instance (the Embryo's
/// `sphere1`) takes the SUBNET template's word for it — its override
- /// (`chf("../Radius")`) is the default that instance was built with,
+ /// (`chf("../radius")`) is the default that instance was built with,
/// and the merge refreshes the child from there — and any other node
/// its own template's. `None` for a parameter no template names, such
/// as one added over MCP.
@@ -5676,7 +5769,7 @@ impl State {
/// float3 presentations `add_pick_lists` makes are seen), what
/// `param_display` alone would give otherwise.
fn display_row_type(&self, slot: usize, param: &ParamDef) -> String {
- let key = if param.label.is_empty() { ¶m.name } else { ¶m.label };
+ let key = param.shown_name();
if self.param_editor_selected() == Some(slot) {
if let Some(row) = self.param().node_params().into_iter().find(|r| r.0 == *key) {
return row.2;
@@ -5981,7 +6074,7 @@ impl State {
{
p.set_expr(true);
}
- self.update_status_text(&format!("{pname} is an expression — type ch(\"../node/Param\"), $F, arithmetic."));
+ self.update_status_text(&format!("{pname} is an expression — type ch(\"../node/param\"), $F, arithmetic."));
}
ParamMenuAction::DeleteExpression => {
// The expression's value NOW becomes the value, as Houdini's
@@ -6830,17 +6923,17 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
}
None
};
- let pos = node.params.iter().find(|p| p.name == "Position")
+ let pos = node.params.iter().find(|p| p.name == "position")
.and_then(|p| parse3(p.text()))
.unwrap_or(Vec3::new(2.5, 1.8, 2.5));
- let piv = node.params.iter().find(|p| p.name == "Pivot")
+ let piv = node.params.iter().find(|p| p.name == "pivot")
.and_then(|p| parse3(p.text()))
.unwrap_or(Vec3::ZERO);
let offset = pos - piv;
let pitch0_deg = (offset.y / offset.length().max(1e-5)).asin().to_degrees();
let max_pitch_deg = crate::viewport_3d::Viewport3D::MAX_PITCH.to_degrees();
- if let Some(p) = node.params.iter_mut().find(|p| p.name == "Rotation") {
+ if let Some(p) = node.params.iter_mut().find(|p| p.name == "rotation") {
let mut rx = 0.0f32;
let mut ry = 0.0f32;
let mut rz = 0.0f32;
@@ -6876,7 +6969,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
let camera_name = self.active_camera.clone();
let dir = self.current_dir_mut();
if let Some(node) = dir.children.iter_mut().find(|c| c.node_type == "camera" && c.name == camera_name) {
- if let Some(p) = node.params.iter_mut().find(|p| p.name == "Rotation") {
+ if let Some(p) = node.params.iter_mut().find(|p| p.name == "rotation") {
p.set_text("0.00:0.00:0.00".to_string());
self.sync_nodes();
// Through the one pane-sync path, so the pick-list upgrade
@@ -7045,7 +7138,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
// A node with no wire parameter at all (none that says so) takes
// it as its Input, as a connection always did.
let slot = child.params.iter().enumerate().filter(|(_, p)| p.kind() == ParamKind::Node).map(|(i, _)| i).nth(port)
- .or_else(|| child.params.iter().position(|p| p.name == "Input"));
+ .or_else(|| child.params.iter().position(|p| p.name == "input"));
let Some(slot) = slot else { return false };
child.params[slot].set_text(output);
self.sync_nodes();
@@ -7196,7 +7289,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
if group_key != self.last_group_points_key {
let mut member_verts = Vec::new();
if let Some(node) = selected_node.filter(|n| n.node_type.eq_ignore_ascii_case("group")) {
- let group_name = node_param_str(node, "Group Name", "group1");
+ let group_name = node_param_str(node, "group_name", "group1");
let mut ocl_error = None;
let mut sim = crate::geometry::EvalSim::new(sim_frame, sim_start, &mut sim_cache);
if let Some(geom) = crate::geometry::node_geometry_as_shown(&self.fs_root, node, &mut ocl_error, &mut sim) {
@@ -7564,7 +7657,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
param: ParametersBg::new(),
canvas: Canvas::new(),
left_menubar: MenuBar::new(0.0, 0.0, 0.0, MENUBAR_H).with_title("0: Network").with_label("Network Menu Bar").with_item("File", &["New", "Save", "Save As"]).with_item("Edit", &["Undo", "Redo"]).with_item("View", &["Zoom In", "Zoom Out", "Network Plate", "Circular Pane", "Detach Pane", "Close Pane"]).with_context_options(context_opts.clone(), 0),
- right_menubar: MenuBar::new(0.0, 0.0, 0.0, MENUBAR_H).with_title("1: Viewport").with_label("Viewport Menu Bar").with_item("Camera", &["Perspective", "Orthographic"]).with_item("Display", &["Square Aspect"]).with_item("Guides", &["Show Grid", "Origin", "Camera Pivot"]).with_item("View", &["Close Pane"]).with_context_options(context_opts.clone(), 1),
+ right_menubar: MenuBar::new(0.0, 0.0, 0.0, MENUBAR_H).with_title("1: Viewport").with_label("Viewport Menu Bar").with_item("Camera", &["Perspective", "Orthographic"]).with_item("Display", &["square_aspect"]).with_item("Guides", &["Show Grid", "Origin", "Camera Pivot"]).with_item("View", &["Close Pane"]).with_context_options(context_opts.clone(), 1),
param_menubar: MenuBar::new(0.0, 0.0, 0.0, MENUBAR_H).with_title("2: Parameters").with_label("Parameters Menu Bar").with_item("Preset", &["Default"]).with_item("Reset", &["All"]).with_item("View", &["Close Pane"]).with_context_options(context_opts.clone(), 2),
status: StatusBar::new().with_text("Ready"),
breadcrumb: {
@@ -9247,7 +9340,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
fn splice_paste(&mut self, ids: &[String], src_id: &str, dest_id: &str) {
let dir = self.current_dir();
let nodes: Vec<&FsNode> = dir.children.iter().filter(|c| ids.contains(&c.id)).collect();
- let input = |n: &FsNode| crate::geometry::node_param_node(n, "Input");
+ let input = |n: &FsNode| crate::geometry::node_param_node(n, "input");
let heads: Vec<&FsNode> = nodes.iter().copied().filter(|n| !input(n).is_some_and(|i| nodes.iter().any(|m| m.name == i))).collect();
let tails: Vec<&FsNode> = nodes.iter().copied().filter(|n| !nodes.iter().any(|m| input(m).as_deref() == Some(n.name.as_str()))).collect();
let ([head], [tail], Some(src)) = (heads.as_slice(), tails.as_slice(), dir.children.iter().find(|c| c.id == src_id)) else { return };
@@ -12005,7 +12098,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
let mut cy = 1.8f32;
let mut cz = 2.5f32;
for p in &node.params {
- if p.name == "Position" {
+ if p.name == "position" {
let parts: Vec<&str> = p.text()
.split(|c| c == ':' || c == ',' || c == ' ')
.filter(|s| !s.is_empty())
@@ -12017,7 +12110,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
cz = vz;
}
}
- } else if p.name == "Rotation" {
+ } else if p.name == "rotation" {
let parts: Vec<&str> = p.text()
.split(|c| c == ':' || c == ',' || c == ' ')
.filter(|s| !s.is_empty())
@@ -12029,7 +12122,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
rz = vz;
}
}
- } else if p.name == "Pivot" {
+ } else if p.name == "pivot" {
let parts: Vec<&str> = p.text()
.split(|c| c == ':' || c == ',' || c == ' ')
.filter(|s| !s.is_empty())
diff --git a/src/curve_tool.rs b/src/curve_tool.rs
index cf45446..f9066a4 100644
--- a/src/curve_tool.rs
+++ b/src/curve_tool.rs
@@ -44,12 +44,12 @@ impl HandleSource for CurveHandles {
}
fn read(&self, node: &FsNode, _ctx: &HandleCtx) -> Vec<Vec3> {
- parse_curve_points(&node_param_str(node, "Points", ""))
+ parse_curve_points(&node_param_str(node, "points", ""))
}
fn write(&self, node: &mut FsNode, handles: &[Vec3], _ctx: &HandleCtx) {
let formatted = format_curve_points(handles);
- if let Some(p) = node.params.iter_mut().find(|p| p.name == "Points") {
+ if let Some(p) = node.params.iter_mut().find(|p| p.name == "points") {
p.set_text(formatted);
}
}
diff --git a/src/detangle.rs b/src/detangle.rs
index 3b694c5..9119354 100644
--- a/src/detangle.rs
+++ b/src/detangle.rs
@@ -379,13 +379,13 @@ pub fn apply_from(geom: &mut Detail, before: Option<&Detail>, target: &FsNode) -
if n == 0 || geom.num_prims() == 0 {
return work;
}
- let rings = node_param_f32(target, "Rings", 2.0).clamp(0.0, 6.0) as usize;
+ let rings = node_param_f32(target, "rings", 2.0).clamp(0.0, 6.0) as usize;
let topo = topo_for(geom, rings);
if topo.edges.is_empty() {
return work;
}
// A node without the row is one from before it, and solves as it did.
- if node_param_str(target, "Method", "Points").trim().eq_ignore_ascii_case("Surface") {
+ if node_param_str(target, "method", "Points").trim().eq_ignore_ascii_case("Surface") {
let before: Option<Vec<Vec3>> = before
.filter(|b| b.num_points() == n && b.num_prims() == geom.num_prims() && topology_key(b) == topo.key)
.map(|b| (0..n).map(|p| b.pos(p)).collect());
@@ -395,7 +395,7 @@ pub fn apply_from(geom: &mut Detail, before: Option<&Detail>, target: &FsNode) -
}
// What is STILL crossed once the solve is done, which is the part worth
// looking at. Only when asked for: it is a second search of the mesh.
- let mark = node_param_str(target, "Tangled Group", "");
+ let mark = node_param_str(target, "tangled_group", "");
let mark = mark.trim();
if !mark.is_empty() {
let found = intersections_of(geom, &topo, false);
@@ -412,7 +412,7 @@ pub fn apply_from(geom: &mut Detail, before: Option<&Detail>, target: &FsNode) -
/// means the same thing before and after a remesh.
fn thickness_of(geom: &Detail, target: &FsNode, topo: &Topo) -> f32 {
let mean_edge = mean_edge(geom, topo);
- node_param_f32(target, "Thickness", 1.0).max(0.0) * mean_edge
+ node_param_f32(target, "thickness", 1.0).max(0.0) * mean_edge
}
fn mean_edge(geom: &Detail, topo: &Topo) -> f32 {
@@ -430,8 +430,8 @@ fn solve_points(geom: &mut Detail, target: &FsNode, topo: &Topo, work: &mut Work
if thickness <= 0.0 {
return;
}
- let iterations = node_param_f32(target, "Iterations", 4.0).clamp(1.0, 32.0) as usize;
- let group = node_param_str(target, "Group", "");
+ let iterations = node_param_f32(target, "iterations", 4.0).clamp(1.0, 32.0) as usize;
+ let group = node_param_str(target, "group", "");
let group = group.trim().to_string();
let movable: Vec<bool> = (0..n).map(|p| group.is_empty() || geom.points().in_group(&group, p)).collect();
@@ -657,10 +657,10 @@ fn solve_surface(geom: &mut Detail, before: Option<&[Vec3]>, target: &FsNode, to
if thickness <= 0.0 || topo.tris.is_empty() {
return;
}
- let iterations = node_param_f32(target, "Iterations", 4.0).clamp(1.0, 32.0) as usize;
+ let iterations = node_param_f32(target, "iterations", 4.0).clamp(1.0, 32.0) as usize;
// A node without the row is one from before it: points and triangles.
- let edges_too = crate::geometry::node_param_bool(target, "Edge Contact", false);
- let group = node_param_str(target, "Group", "");
+ let edges_too = crate::geometry::node_param_bool(target, "edge_contact", false);
+ let group = node_param_str(target, "group", "");
let group = group.trim().to_string();
let movable: Vec<bool> = (0..n).map(|p| group.is_empty() || geom.points().in_group(&group, p)).collect();
let free = |p: usize| if movable[p] { 1.0f32 } else { 0.0 };
@@ -668,7 +668,7 @@ fn solve_surface(geom: &mut Detail, before: Option<&[Vec3]>, target: &FsNode, to
let mut pos: Vec<Vec3> = (0..n).map(|p| geom.pos(p)).collect();
let mut moved_at_all = false;
// A node without the row is one from before it, and is not limited.
- let limit = node_param_f32(target, "Step Limit", 0.0).max(0.0) * thickness;
+ let limit = node_param_f32(target, "step_limit", 0.0).max(0.0) * thickness;
if let (Some(before), true) = (before, limit > 0.0) {
for p in (0..n).filter(|&p| movable[p]) {
let d = pos[p] - before[p];
@@ -687,7 +687,7 @@ fn solve_surface(geom: &mut Detail, before: Option<&[Vec3]>, target: &FsNode, to
work.edges_searched += near.sides_looked_at;
// What has folded through its own neighbourhood since the step began.
// A node without the row is one from before it.
- let folds_too = before.is_some() && crate::geometry::node_param_bool(target, "Fold Contact", false);
+ let folds_too = before.is_some() && crate::geometry::node_param_bool(target, "fold_contact", false);
let mut folds: Vec<Fold> = Vec::new();
if let (Some(was), true) = (before, folds_too) {
folded(topo, was, &pos, edges_too, 0.0, None, &mut folds);
diff --git a/src/expr.rs b/src/expr.rs
index 41e1281..f84ecbd 100644
--- a/src/expr.rs
+++ b/src/expr.rs
@@ -1,8 +1,8 @@
//! Parameter expressions — Houdini's channel references, with arithmetic.
//!
//! A parameter whose `expr` flag is set holds an EXPRESSION rather than a
-//! value, and is evaluated every time the node is: `ch("../sphere1/Radius")
-//! * 2 + 1`, `$F / 24`, `chs("../text1/Font")`. The flag is the model, not a
+//! value, and is evaluated every time the node is: `ch("../sphere1/radius")
+//! * 2 + 1`, `$F / 24`, `chs("../text1/font")`. The flag is the model, not a
//! guess about the text: a kernel's Code contains `chf(`, a node name is an
//! identifier and `0.5` is an expression too, so anything that decided by
//! looking at the string would be wrong somewhere. Houdini makes the same
@@ -433,7 +433,7 @@ pub fn parse(src: &str) -> Result<Expr, String> {
/// Whether `s` reads as an expression that REFERS to something — a channel
/// or a variable. This is the inference applied to a value typed or scripted
-/// into a parameter that has no expression yet: `ch("../a/Radius")` becomes
+/// into a parameter that has no expression yet: `ch("../a/radius")` becomes
/// one, while `1+2`, a node name and a kernel do not — arithmetic on a
/// literal is asked for through the row's Edit Expression, not guessed.
pub fn looks_like_expression(s: &str) -> bool {
@@ -445,7 +445,7 @@ pub fn looks_like_expression(s: &str) -> bool {
return e.refers();
}
// A float3: three components, each a number or an expression, at least
- // one of which refers — `chf("../a/Size.x"):0:0`.
+ // one of which refers — `chf("../a/size.x"):0:0`.
let parts: Vec<&str> = t.split(':').collect();
parts.len() == 3 && {
let parsed: Vec<Option<Expr>> = parts.iter().map(|p| parse(p.trim()).ok()).collect();
@@ -660,7 +660,9 @@ pub fn rewrite_paths(src: &str, mut f: impl FnMut(&str) -> Option<String>) -> St
let at_ident_start = i == 0 || !(bytes[i - 1].is_ascii_alphanumeric() || bytes[i - 1] == b'_');
let mut matched = None;
if at_ident_start && bytes[i] == b'c' {
- for name in ["chf", "chi", "chb", "chs", "ch"] {
+ // `chv` is a wrangle's (a float3 off a path); it is here so the
+ // same rewrite serves a wrangle's Code.
+ for name in ["chf", "chi", "chb", "chs", "chv", "ch"] {
if src[i..].starts_with(name) {
let rest = &src[i + name.len()..];
let trimmed = rest.trim_start();
diff --git a/src/geometry.rs b/src/geometry.rs
index 50cd726..207b475 100644
--- a/src/geometry.rs
+++ b/src/geometry.rs
@@ -520,9 +520,9 @@ pub fn sample_catmull_rom(pts: &[Vec3], segs: usize) -> Vec<Vec3> {
/// one surface, which is a modeling decision the node has never made and is
/// not this migration's to make.
pub fn curve_detail(node: &FsNode) -> Detail {
- let pts = parse_curve_points(&node_param_str(node, "Points", ""));
- let segs = node_param_f32(node, "Segments", 8.0).max(1.0) as usize;
- let thickness = node_param_f32(node, "Thickness", 0.02).max(0.001);
+ let pts = parse_curve_points(&node_param_str(node, "points", ""));
+ let segs = node_param_f32(node, "segments", 8.0).max(1.0) as usize;
+ let thickness = node_param_f32(node, "thickness", 0.02).max(0.001);
let samples = sample_catmull_rom(&pts, segs);
let mut d = Detail::new();
for w in samples.windows(2) {
@@ -531,8 +531,14 @@ pub fn curve_detail(node: &FsNode) -> Detail {
d
}
+/// The parameter of that NAME — `base_resolution`, never the label the
+/// pane shows. Under test a name that cannot be one fails loudly: a reader
+/// still spelling a label (`"Base Resolution"`) finds nothing and reads its
+/// fallback, which is a quiet way for a node to stop listening to a row.
fn find_param<'a>(node: &'a FsNode, name: &str) -> Option<&'a ParamDef> {
- node.params.iter().find(|p| p.name.eq_ignore_ascii_case(name))
+ #[cfg(test)]
+ assert!(crate::app::is_param_name(name), "'{name}' is not a parameter name: the code spells names, the pane shows labels");
+ node.params.iter().find(|p| p.name == name)
}
/// A number: the parsed value of a number, whole-number or toggle
@@ -688,7 +694,7 @@ pub fn choice_options(p: &ParamDef) -> Vec<String> {
/// and an expression's `ch()` both read. A toggle is 0 or 1; a choice is its
/// option INDEX, the position in the template's list, the way an ordinal
/// menu evaluates in Houdini. The index is what lets a subnet's dropdown
-/// drive a child switch's Index or a kernel's `chi("Method")`: the option
+/// drive a child switch's Index or a kernel's `chi("method")`: the option
/// text parses as nothing, and until 2026-09-24 the kernel path parsed it
/// anyway, so every choice read as 0 from inside a kernel.
pub fn param_number(p: &ParamDef) -> f32 {
@@ -796,7 +802,7 @@ fn find_ref_param<'a>(node: &'a FsNode, name: &str) -> Option<(&'a ParamDef, Opt
/// that comes back round to itself is an error rather than a stack overflow.
///
/// Public so a SCRIPT on a node (the wrangle) can read channels through
-/// the same scope its parameters do: a `ch("../a/Radius")` in a script
+/// the same scope its parameters do: a `ch("../a/radius")` in a script
/// then sees an expression-valued Radius evaluated, not its text.
pub struct TreeScope<'a> {
root: &'a FsNode,
@@ -894,7 +900,7 @@ impl Evaluated {
/// One parameter's expression evaluated. A float3 is three expressions
/// separated by `:` — each component its own, as Houdini's channels are —
-/// so `chf("../a/Size.x"):0:0` reads naturally.
+/// so `chf("../a/size.x"):0:0` reads naturally.
fn eval_param_value(scope: &mut TreeScope, p: &ParamDef) -> Result<Evaluated, String> {
if p.kind() == crate::app::ParamKind::Float3 {
let parts: Vec<&str> = p.text().split(':').collect();
@@ -1323,9 +1329,9 @@ pub fn generate_single_node_geometry_with_errors(
// Parameter references resolve here, once, for every resolver below:
// a child of a composed subnet reads its parent's controls through
- // `ch("Name")` and the resolvers never know.
+ // `ch("name")` and the resolvers never know.
if is_bypassed(target) {
- let res = param_node(root, target, "Input").and_then(|input| generate_single_node_geometry_with_errors(root, input, visited, ocl_error, sim));
+ let res = param_node(root, target, "input").and_then(|input| generate_single_node_geometry_with_errors(root, input, visited, ocl_error, sim));
visited.pop();
return res;
}
@@ -1351,8 +1357,8 @@ pub fn generate_single_node_geometry_with_errors(
} else if target.node_type.eq_ignore_ascii_case("line") {
let idx = find_sphere_index(root, target)?;
let start = Vec3::new((idx % 4) as f32 * 1.25 - 1.875, 0.55, -((idx / 4) as f32) * 1.25);
- let length = node_param_f32(target, "Length", 1.0);
- let thickness = node_param_f32(target, "Thickness", 0.02);
+ let length = node_param_f32(target, "length", 1.0);
+ let thickness = node_param_f32(target, "thickness", 0.02);
let end = start + Vec3::new(0.0, length, 0.0);
Some(box_detail(start, end, thickness))
} else if target.node_type.eq_ignore_ascii_case("curve") {
@@ -1455,7 +1461,7 @@ pub fn generate_single_node_geometry_with_errors(
} else if target.node_type.eq_ignore_ascii_case("output") {
// Sibling-first, then anywhere — `find_input_node`'s own rule,
// which this arm spelled out by hand before that function existed.
- param_node(root, target, "Input")
+ param_node(root, target, "input")
.and_then(|node| generate_single_node_geometry_with_errors(root, node, visited, ocl_error, sim))
} else if target.node_type.eq_ignore_ascii_case("seed") {
// What the enclosing loop began from, whichever pass it is on: a
@@ -1498,9 +1504,9 @@ pub fn resolve_transform_geometry_with_errors(
ocl_error: &mut Option<String>,
sim: &mut EvalSim,
) -> Option<Detail> {
- let input_node = param_node(root, target, "Input")?;
+ let input_node = param_node(root, target, "input")?;
let mut geom = generate_single_node_geometry_with_errors(root, input_node, visited, ocl_error, sim)?;
- let translation = node_param_vec3(target, "Translation", Vec3::ZERO).to_array();
+ let translation = node_param_vec3(target, "translation", Vec3::ZERO).to_array();
// Moving points changes no topology, so the cache rides along.
for p in geom.positions_mut() {
for k in 0..3 {
@@ -1557,15 +1563,15 @@ pub fn resolve_group_geometry_with_errors(
ocl_error: &mut Option<String>,
sim: &mut EvalSim,
) -> Option<Detail> {
- let input_node = param_node(root, target, "Input")?;
+ let input_node = param_node(root, target, "input")?;
let mut geom = generate_single_node_geometry_with_errors(root, input_node, visited, ocl_error, sim)?;
- let group_name = node_param_str(target, "Group Name", "group1").trim().to_string();
- let etype = node_param_str(target, "Element Type", "Points").to_lowercase();
- let center = node_param_vec3(target, "Center", Vec3::ZERO);
- let half = node_param_vec3(target, "Size", Vec3::ONE) * 0.5;
- let invert = node_param_bool(target, "Invert", false);
- let highlight = node_param_bool(target, "Highlight", true);
+ let group_name = node_param_str(target, "group_name", "group1").trim().to_string();
+ let etype = node_param_str(target, "element_type", "Points").to_lowercase();
+ let center = node_param_vec3(target, "center", Vec3::ZERO);
+ let half = node_param_vec3(target, "size", Vec3::ONE) * 0.5;
+ let invert = node_param_bool(target, "invert", false);
+ let highlight = node_param_bool(target, "highlight", true);
let inside = |p: Vec3| -> bool {
(p.x - center.x).abs() <= half.x
@@ -1610,16 +1616,16 @@ fn select_elements(
pts.iter().map(|&p| geom.pos(p as usize)).sum::<Vec3>() / pts.len() as f32
};
- let mode = node_param_str(target, "Mode", "Box").to_lowercase();
+ let mode = node_param_str(target, "mode", "Box").to_lowercase();
if mode == "attribute" {
// Select by what a point IS rather than where it is. This is what
// makes the measuring nodes composable: Distance, Connectivity or a
// solver's own attribute becomes a named selection that Cull, Relax's
// pin, Attribute's group and Soft Transform all already read.
- let attr = node_param_str(target, "Attribute", "");
+ let attr = node_param_str(target, "attribute", "");
let attr = attr.trim().to_string();
- let below = node_param_str(target, "Comparison", "Above").eq_ignore_ascii_case("below");
- let threshold = node_param_f32(target, "Threshold", 0.5);
+ let below = node_param_str(target, "comparison", "Above").eq_ignore_ascii_case("below");
+ let threshold = node_param_f32(target, "threshold", 0.5);
if geom.points().has(&attr) {
for p in 0..geom.num_points() {
let v = geom.points().value(&attr, p).map(|v| v.as_f32()).unwrap_or(0.0);
@@ -1633,9 +1639,9 @@ fn select_elements(
// signed: positive walks outward from the members, negative peels the
// boundary off, which is how an erode/dilate pair reads without two
// nodes.
- let src = node_param_str(target, "Source Group", "");
+ let src = node_param_str(target, "source_group", "");
let src = src.trim().to_string();
- let rings = node_param_f32(target, "Rings", 1.0).clamp(-8.0, 8.0) as i32;
+ let rings = node_param_f32(target, "rings", 1.0).clamp(-8.0, 8.0) as i32;
let mut inside: Vec<bool> = (0..geom.num_points())
.map(|p| !src.is_empty() && geom.points().in_group(&src, p))
.collect();
@@ -1662,10 +1668,10 @@ fn select_elements(
member[p] = m;
}
} else if mode == "random" {
- let count = node_param_f32(target, "Count", 1.0).max(0.0) as usize;
+ let count = node_param_f32(target, "count", 1.0).max(0.0) as usize;
// Seed offsets the stream, and the element type joins it so switching
// type reshuffles instead of replaying the same index sequence.
- let mut rng = node_param_f32(target, "Seed", 0.0) as u64 ^ 0xCCE0;
+ let mut rng = node_param_f32(target, "seed", 0.0) as u64 ^ 0xCCE0;
// Partial Fisher-Yates: draw `count` distinct indices out of `m`.
let mut draw = |m: usize, count: usize| -> Vec<usize> {
let mut idx: Vec<usize> = (0..m).collect();
@@ -1842,13 +1848,13 @@ pub fn resolve_collision_geometry_with_errors(
ocl_error: &mut Option<String>,
sim: &mut EvalSim,
) -> Option<Detail> {
- let input_node = param_node(root, target, "Input")?;
+ let input_node = param_node(root, target, "input")?;
let mut geom = generate_single_node_geometry_with_errors(root, input_node, visited, ocl_error, sim)?;
// Sibling-first like every other wire: this was a whole-tree
// `find_node_by_name`, so inside a second copy of a subnet it found the
// first copy's collider.
- let Some(collider_node) = param_node(root, target, "Collider") else { return Some(geom) };
+ let Some(collider_node) = param_node(root, target, "collider") else { return Some(geom) };
let Some(collider) = generate_single_node_geometry_with_errors(root, collider_node, visited, ocl_error, sim) else {
return Some(geom);
};
@@ -1865,8 +1871,8 @@ pub fn resolve_collision_geometry_with_errors(
.map(|t| [t[0], t[1], t[2]])
.collect();
- let method = node_param_str(target, "Method", "Inside").to_lowercase();
- let distance = node_param_f32(target, "Distance", 0.05).max(0.0);
+ let method = node_param_str(target, "method", "Inside").to_lowercase();
+ let distance = node_param_f32(target, "distance", 0.05).max(0.0);
let test = if method == "proximity" { crate::collide::Test::Proximity(distance) } else { crate::collide::Test::Inside };
// The test runs as ONE batch over every element the type asks about —
@@ -1874,8 +1880,8 @@ pub fn resolve_collision_geometry_with_errors(
// (`crate::collide`, the second Phase 7 step 4 operator); the
// per-element closure `select_elements` takes would have asked one
// point at a time. Edges test both endpoints, as before.
- let etype = node_param_str(target, "Element Type", "Points").to_lowercase();
- let invert = node_param_bool(target, "Invert", false);
+ let etype = node_param_str(target, "element_type", "Points").to_lowercase();
+ let invert = node_param_bool(target, "invert", false);
let queries: Vec<Vec3> = if etype == "primitives" {
(0..geom.num_prims())
.map(|prim| {
@@ -1936,8 +1942,8 @@ pub fn resolve_collision_geometry_with_errors(
}
}
- let group_name = node_param_str(target, "Group Name", "collisions").trim().to_string();
- let highlight = node_param_bool(target, "Highlight", true);
+ let group_name = node_param_str(target, "group_name", "collisions").trim().to_string();
+ let highlight = node_param_bool(target, "highlight", true);
apply_group(
&mut geom,
&group_name,
@@ -1975,28 +1981,28 @@ pub fn resolve_relax_geometry_with_errors(
ocl_error: &mut Option<String>,
sim: &mut EvalSim,
) -> Option<Detail> {
- let input_node = param_node(root, target, "Input")?;
+ let input_node = param_node(root, target, "input")?;
let mut geom = generate_single_node_geometry_with_errors(root, input_node, visited, ocl_error, sim)?;
// Tangential mode: the remesh's relaxation — each point toward the
// centroid of its neighbours by Amount, less the part along its normal,
// so the triangles even out and the shape stays. Zero iterations or a
// zero Amount is off.
- if node_param_str(target, "Mode", "Springs").eq_ignore_ascii_case("tangential") {
- let iterations = node_param_f32(target, "Iterations", 1.0).max(0.0) as usize;
- return Some(crate::remesh::relax_tangential(&geom, node_param_f32(target, "Amount", 0.5), iterations));
+ if node_param_str(target, "mode", "Springs").eq_ignore_ascii_case("tangential") {
+ let iterations = node_param_f32(target, "iterations", 1.0).max(0.0) as usize;
+ return Some(crate::remesh::relax_tangential(&geom, node_param_f32(target, "amount", 0.5), iterations));
}
// Repel mode: the Relax SOP — spheres of Radius pushed apart until they
// stop overlapping, each point sliding in its tangent plane unless In 3D
// Space lets it leave. No rest shape, no springs; zero iterations is off.
- if node_param_str(target, "Mode", "Springs").eq_ignore_ascii_case("repel") {
- let iterations = node_param_f32(target, "Iterations", 8.0).max(0.0) as usize;
- let radius = node_param_f32(target, "Radius", 0.05);
+ if node_param_str(target, "mode", "Springs").eq_ignore_ascii_case("repel") {
+ let iterations = node_param_f32(target, "iterations", 8.0).max(0.0) as usize;
+ let radius = node_param_f32(target, "radius", 0.05);
if iterations == 0 || radius <= 0.0 || geom.num_points() < 2 {
return Some(geom);
}
- let in_3d = node_param_bool(target, "In 3D Space", false);
+ let in_3d = node_param_bool(target, "in_3d_space", false);
let normals = if in_3d { None } else { Some(point_normals(&geom)) };
let mut pts: Vec<Vec3> = (0..geom.num_points()).map(|p| geom.pos(p)).collect();
crate::scatter::relax_points(&mut pts, normals.as_deref(), radius, iterations);
@@ -2008,7 +2014,7 @@ pub fn resolve_relax_geometry_with_errors(
// Sibling-first, as the collider is: a simnet's `Rest: input1` has to
// be ITS input1, not the first one in the tree.
- let Some(rest_node) = param_node(root, target, "Rest") else { return Some(geom) };
+ let Some(rest_node) = param_node(root, target, "rest") else { return Some(geom) };
let Some(rest) = generate_single_node_geometry_with_errors(root, rest_node, visited, ocl_error, sim) else {
return Some(geom);
};
@@ -2016,9 +2022,9 @@ pub fn resolve_relax_geometry_with_errors(
return Some(geom);
}
- let stiffness = node_param_f32(target, "Stiffness", 0.5).clamp(0.0, 1.0);
- let iterations = node_param_f32(target, "Iterations", 8.0).max(1.0) as usize;
- let pin = node_param_str(target, "Pin Group", "").trim().to_string();
+ let stiffness = node_param_f32(target, "stiffness", 0.5).clamp(0.0, 1.0);
+ let iterations = node_param_f32(target, "iterations", 8.0).max(1.0) as usize;
+ let pin = node_param_str(target, "pin_group", "").trim().to_string();
// The edges come from the REST shape's topology. This is where the soup
// cost the most: it had to weld both shapes by position and rebuild the
@@ -2078,18 +2084,18 @@ pub fn resolve_normal_geometry_with_errors(
ocl_error: &mut Option<String>,
sim: &mut EvalSim,
) -> Option<Detail> {
- let input_node = param_node(root, target, "Input")?;
+ let input_node = param_node(root, target, "input")?;
let mut geom = generate_single_node_geometry_with_errors(root, input_node, visited, ocl_error, sim)?;
- let name = node_param_str(target, "Attribute", "N").trim().to_string();
+ let name = node_param_str(target, "attribute", "N").trim().to_string();
if name.is_empty() {
return Some(geom);
}
- let flip = node_param_bool(target, "Flip", false);
+ let flip = node_param_bool(target, "flip", false);
let sign = if flip { -1.0 } else { 1.0 };
// A node without the Class row is one from before it, and writes the
// points' as it always did.
- if node_param_str(target, "Class", "Points").trim().eq_ignore_ascii_case("Vertices") {
- let cusp = node_param_f32(target, "Cusp Angle", 60.0);
+ if node_param_str(target, "class", "Points").trim().eq_ignore_ascii_case("Vertices") {
+ let cusp = node_param_f32(target, "cusp_angle", 60.0);
let normals: Vec<[f32; 3]> = vertex_normals(&geom, cusp).iter().map(|n| (*n * sign).to_array()).collect();
geom.verts_mut().create(&name, AttribValue::Float3([0.0; 3]));
let _ = geom.verts_mut().insert(&name, AttribData::Float3(normals));
@@ -2114,14 +2120,14 @@ pub fn resolve_bounds_geometry_with_errors(
ocl_error: &mut Option<String>,
sim: &mut EvalSim,
) -> Option<Detail> {
- let input_node = param_node(root, target, "Input")?;
+ let input_node = param_node(root, target, "input")?;
let mut geom = generate_single_node_geometry_with_errors(root, input_node, visited, ocl_error, sim)?;
- let prefix = node_param_str(target, "Prefix", "bounds").trim().to_string();
+ let prefix = node_param_str(target, "prefix", "bounds").trim().to_string();
if prefix.is_empty() {
return Some(geom);
}
- let group = node_param_str(target, "Group", "");
+ let group = node_param_str(target, "group", "");
let group = group.trim().to_string();
let pts: Vec<Vec3> = (0..geom.num_points())
.filter(|&p| group.is_empty() || geom.points().in_group(&group, p))
@@ -2166,10 +2172,10 @@ pub fn resolve_distance_geometry_with_errors(
ocl_error: &mut Option<String>,
sim: &mut EvalSim,
) -> Option<Detail> {
- let input_node = param_node(root, target, "Input")?;
+ let input_node = param_node(root, target, "input")?;
let mut geom = generate_single_node_geometry_with_errors(root, input_node, visited, ocl_error, sim)?;
- let to_name = node_param_node(target, "To").unwrap_or_default();
+ let to_name = node_param_node(target, "to").unwrap_or_default();
let Some(other) = find_input_node(root, target, &to_name)
.and_then(|n| generate_single_node_geometry_with_errors(root, n, visited, ocl_error, sim))
else {
@@ -2179,14 +2185,14 @@ pub fn resolve_distance_geometry_with_errors(
return Some(geom);
};
- let name = node_param_str(target, "Attribute", "dist").trim().to_string();
- let dir_name = node_param_str(target, "Direction", "");
+ let name = node_param_str(target, "attribute", "dist").trim().to_string();
+ let dir_name = node_param_str(target, "direction", "");
let dir_name = dir_name.trim().to_string();
- let signed = node_param_bool(target, "Signed", false);
+ let signed = node_param_bool(target, "signed", false);
// Zero means no clamp: a maximum is for keeping a falloff bounded, and a
// node whose default quietly flattened every measurement to zero would be
// a trap.
- let maximum = node_param_f32(target, "Maximum", 0.0).max(0.0);
+ let maximum = node_param_f32(target, "maximum", 0.0).max(0.0);
let grid = crate::spatial::TriGrid::build(&other);
// A point ON the surface has no direction to it, and the vector between
@@ -2246,7 +2252,7 @@ pub fn resolve_connectivity_geometry_with_errors(
ocl_error: &mut Option<String>,
sim: &mut EvalSim,
) -> Option<Detail> {
- let input_node = param_node(root, target, "Input")?;
+ let input_node = param_node(root, target, "input")?;
let mut geom = generate_single_node_geometry_with_errors(root, input_node, visited, ocl_error, sim)?;
apply_connectivity(&mut geom, target);
Some(geom)
@@ -2258,7 +2264,7 @@ pub fn apply_connectivity_for_test(geom: &mut Detail, target: &FsNode, _err: &mu
}
pub(crate) fn apply_connectivity(geom: &mut Detail, target: &FsNode) {
- let name = node_param_str(target, "Attribute", "piece").trim().to_string();
+ let name = node_param_str(target, "attribute", "piece").trim().to_string();
if name.is_empty() {
return;
}
@@ -2315,12 +2321,12 @@ pub fn resolve_cull_geometry_with_errors(
ocl_error: &mut Option<String>,
sim: &mut EvalSim,
) -> Option<Detail> {
- let input_node = param_node(root, target, "Input")?;
+ let input_node = param_node(root, target, "input")?;
let mut geom = generate_single_node_geometry_with_errors(root, input_node, visited, ocl_error, sim)?;
- let group = node_param_str(target, "Group", "");
+ let group = node_param_str(target, "group", "");
let group = group.trim().to_string();
- let attr = node_param_str(target, "Attribute", "");
+ let attr = node_param_str(target, "attribute", "");
let attr = attr.trim().to_string();
if group.is_empty() && attr.is_empty() {
return Some(geom);
@@ -2335,9 +2341,9 @@ pub fn resolve_cull_geometry_with_errors(
return Some(geom);
}
- let below = node_param_str(target, "Comparison", "Below").eq_ignore_ascii_case("below");
- let threshold = node_param_f32(target, "Threshold", 0.5);
- let invert = node_param_bool(target, "Invert", false);
+ let below = node_param_str(target, "comparison", "Below").eq_ignore_ascii_case("below");
+ let threshold = node_param_f32(target, "threshold", 0.5);
+ let invert = node_param_bool(target, "invert", false);
let selected: Vec<bool> = (0..geom.num_points())
.map(|p| {
@@ -2375,16 +2381,16 @@ pub fn resolve_volume_geometry_with_errors(
ocl_error: &mut Option<String>,
sim: &mut EvalSim,
) -> Option<Detail> {
- let input_node = param_node(root, target, "Input")?;
+ let input_node = param_node(root, target, "input")?;
let geom = generate_single_node_geometry_with_errors(root, input_node, visited, ocl_error, sim)?;
if geom.num_prims() == 0 {
return Some(geom);
}
- let voxel = node_param_f32(target, "Voxel Size", 0.05).max(1e-3);
- let offset = node_param_f32(target, "Offset", 0.0);
- let shell = node_param_str(target, "Mode", "Offset").eq_ignore_ascii_case("shell");
- let thickness = node_param_f32(target, "Thickness", 0.05).max(1e-4);
+ let voxel = node_param_f32(target, "voxel_size", 0.05).max(1e-3);
+ let offset = node_param_f32(target, "offset", 0.0);
+ let shell = node_param_str(target, "mode", "Offset").eq_ignore_ascii_case("shell");
+ let thickness = node_param_f32(target, "thickness", 0.05).max(1e-4);
// Room for everything the operation will ask the field to reach: the
// offset itself, and for a shell the wall's thickness beyond it.
@@ -2432,10 +2438,10 @@ pub fn resolve_boolean_geometry_with_errors(
ocl_error: &mut Option<String>,
sim: &mut EvalSim,
) -> Option<Detail> {
- let input_node = param_node(root, target, "Input")?;
+ let input_node = param_node(root, target, "input")?;
let a = generate_single_node_geometry_with_errors(root, input_node, visited, ocl_error, sim)?;
- let with_name = node_param_node(target, "With").unwrap_or_default();
+ let with_name = node_param_node(target, "with").unwrap_or_default();
let Some(b) = find_input_node(root, target, &with_name)
.and_then(|n| generate_single_node_geometry_with_errors(root, n, visited, ocl_error, sim))
else {
@@ -2448,8 +2454,8 @@ pub fn resolve_boolean_geometry_with_errors(
return Some(a);
}
- let voxel = node_param_f32(target, "Voxel Size", 0.05).max(1e-3);
- let op = node_param_str(target, "Operation", "Union").to_lowercase();
+ let voxel = node_param_f32(target, "voxel_size", 0.05).max(1e-3);
+ let op = node_param_str(target, "operation", "Union").to_lowercase();
// One grid over BOTH, so the two fields line up sample for sample.
let (alo, ahi) = a.bounds()?;
let (blo, bhi) = b.bounds()?;
@@ -2474,14 +2480,14 @@ pub fn resolve_mold_shell_geometry_with_errors(
ocl_error: &mut Option<String>,
sim: &mut EvalSim,
) -> Option<Detail> {
- let input_node = param_node(root, target, "Input")?;
+ let input_node = param_node(root, target, "input")?;
let input = generate_single_node_geometry_with_errors(root, input_node, visited, ocl_error, sim)?;
let shell = crate::mold::mold_shell(
&input,
- node_param_f32(target, "Minimum Thickness", 0.6),
- node_param_f32(target, "Maximum Thickness", 0.75),
- node_param_f32(target, "Remesh Division Size", 0.9),
- crate::mold::Ramp::parse(&node_param_str(target, "Ramp", "Linear")),
+ node_param_f32(target, "minimum_thickness", 0.6),
+ node_param_f32(target, "maximum_thickness", 0.75),
+ node_param_f32(target, "remesh_division_size", 0.9),
+ crate::mold::Ramp::parse(&node_param_str(target, "ramp", "Linear")),
);
// A node with nothing to thicken passes its input through rather than
// vanishing: an empty result in the middle of a chain reads as a broken
@@ -2500,7 +2506,7 @@ pub fn resolve_hull_geometry_with_errors(
ocl_error: &mut Option<String>,
sim: &mut EvalSim,
) -> Option<Detail> {
- let input_node = param_node(root, target, "Input")?;
+ let input_node = param_node(root, target, "input")?;
let input = generate_single_node_geometry_with_errors(root, input_node, visited, ocl_error, sim)?;
let pts: Vec<Vec3> = (0..input.num_points()).map(|i| input.pos(i)).collect();
Some(crate::hull::convex_hull(&pts).unwrap_or(input))
@@ -2511,7 +2517,7 @@ pub fn resolve_hull_geometry_with_errors(
/// A wrangle with no input still runs — in Detail class, once, which is how
/// a script builds geometry from nothing with `addpoint` / `addprim`. The
/// channels the script names as literals are resolved HERE, before the run,
-/// through the expression scope: `ch("../Radius")` on a parameter that is
+/// through the expression scope: `ch("../radius")` on a parameter that is
/// itself an expression sees the evaluated value, and neither language has
/// to know the other exists. A failing script reports through the error
/// slot and the input passes through unchanged.
@@ -2522,13 +2528,13 @@ pub fn resolve_wrangle_geometry_with_errors(
ocl_error: &mut Option<String>,
sim: &mut EvalSim,
) -> Option<Detail> {
- let input = match param_node(root, target, "Input") {
+ let input = match param_node(root, target, "input") {
Some(n) => generate_single_node_geometry_with_errors(root, n, visited, ocl_error, sim).unwrap_or_default(),
None => Detail::new(),
};
- let code = node_param_str(target, "Code", "");
- let class = crate::wrangle::parse_class(&node_param_str(target, "Class", "Points"));
- let group = node_param_str(target, "Group", "");
+ let code = node_param_str(target, "code", "");
+ let class = crate::wrangle::parse_class(&node_param_str(target, "class", "Points"));
+ let group = node_param_str(target, "group", "");
let mut chans = std::collections::HashMap::new();
{
@@ -2562,10 +2568,10 @@ pub fn resolve_extrude_geometry_with_errors(
ocl_error: &mut Option<String>,
sim: &mut EvalSim,
) -> Option<Detail> {
- let input_node = param_node(root, target, "Input")?;
+ let input_node = param_node(root, target, "input")?;
let input = generate_single_node_geometry_with_errors(root, input_node, visited, ocl_error, sim)?;
- let distance = node_param_f32(target, "Distance", 0.2);
- let keep_base = node_param_bool(target, "Keep Base", true);
+ let distance = node_param_f32(target, "distance", 0.2);
+ let keep_base = node_param_bool(target, "keep_base", true);
Some(crate::shapes::extrude_detail(&input, distance, keep_base))
}
@@ -2584,14 +2590,14 @@ pub fn retired_opencl_node(
if ocl_error.is_none() {
*ocl_error = Some(format!("{}: OpenCL nodes are retired; rewrite the kernel as a wrangle", target.name));
}
- let input_node = param_node(root, target, "Input")?;
+ let input_node = param_node(root, target, "input")?;
generate_single_node_geometry_with_errors(root, input_node, visited, ocl_error, sim)
}
/// The Switch node: one of up to four inputs, chosen by Index.
///
/// What a composed subnet puts behind a choice: the Embryo's Source is a
-/// switch whose Index reads `chi("Source")`, so Internal is input 0 and
+/// switch whose Index reads `chi("source")`, so Internal is input 0 and
/// Input is input 1. Nothing else about it — it passes the chosen geometry
/// through untouched, and an empty slot passes nothing.
///
@@ -2601,7 +2607,7 @@ pub fn retired_opencl_node(
pub const SWITCH_INPUTS: usize = 4;
pub fn switch_input_param(index: usize) -> String {
- if index == 0 { "Input".to_string() } else { format!("Input {}", index + 1) }
+ if index == 0 { "input".to_string() } else { format!("input_{}", index + 1) }
}
pub fn resolve_switch_geometry_with_errors(
@@ -2611,7 +2617,7 @@ pub fn resolve_switch_geometry_with_errors(
ocl_error: &mut Option<String>,
sim: &mut EvalSim,
) -> Option<Detail> {
- let index = (node_param_f32(target, "Index", 0.0).round().max(0.0) as usize).min(SWITCH_INPUTS - 1);
+ let index = (node_param_f32(target, "index", 0.0).round().max(0.0) as usize).min(SWITCH_INPUTS - 1);
let input_node = param_node(root, target, &switch_input_param(index))?;
generate_single_node_geometry_with_errors(root, input_node, visited, ocl_error, sim)
}
@@ -2630,18 +2636,18 @@ pub fn resolve_export_geometry_with_errors(
ocl_error: &mut Option<String>,
sim: &mut EvalSim,
) -> Option<Detail> {
- let input_node = param_node(root, target, "Input")?;
+ let input_node = param_node(root, target, "input")?;
generate_single_node_geometry_with_errors(root, input_node, visited, ocl_error, sim)
}
/// The format and scale an Export node is configured for.
pub fn export_settings(target: &FsNode) -> (crate::export::Format, f32) {
- let format = match node_param_str(target, "Format", "STL").as_str() {
+ let format = match node_param_str(target, "format", "STL").as_str() {
"OBJ" => crate::export::Format::Obj,
"STL (ASCII)" => crate::export::Format::StlAscii,
_ => crate::export::Format::StlBinary,
};
- (format, node_param_f32(target, "Scale", 1.0).max(1e-6))
+ (format, node_param_f32(target, "scale", 1.0).max(1e-6))
}
/// The Grid node: a flat sheet of quads in the XZ plane.
@@ -2656,11 +2662,11 @@ pub fn export_settings(target: &FsNode) -> (crate::export::Format, f32) {
/// which is surprising the first time and unadjustable after; a parameter says
/// what it is.
pub fn grid_detail(target: &FsNode) -> Detail {
- let rows = node_param_f32(target, "Rows", 10.0).clamp(1.0, 500.0) as usize;
- let cols = node_param_f32(target, "Columns", 10.0).clamp(1.0, 500.0) as usize;
- let width = node_param_f32(target, "Width", 1.0).max(1e-4);
- let length = node_param_f32(target, "Length", 1.0).max(1e-4);
- let centre = node_param_vec3(target, "Center", Vec3::ZERO);
+ let rows = node_param_f32(target, "rows", 10.0).clamp(1.0, 500.0) as usize;
+ let cols = node_param_f32(target, "columns", 10.0).clamp(1.0, 500.0) as usize;
+ let width = node_param_f32(target, "width", 1.0).max(1e-4);
+ let length = node_param_f32(target, "length", 1.0).max(1e-4);
+ let centre = node_param_vec3(target, "center", Vec3::ZERO);
let mut d = Detail::new();
for r in 0..=rows {
@@ -2694,11 +2700,11 @@ pub fn grid_detail(target: &FsNode) -> Detail {
/// triangles cross the concave notches and the shape renders as its own convex
/// hull.
pub fn polygon_detail(target: &FsNode) -> Detail {
- let sides = node_param_f32(target, "Sides", 4.0).clamp(3.0, 256.0) as usize;
- let radius = node_param_f32(target, "Radius", 0.5).max(1e-4);
- let inner = node_param_f32(target, "Inner Radius", 0.0).max(0.0);
- let fill = node_param_bool(target, "Fill", true);
- let centre = node_param_vec3(target, "Center", Vec3::ZERO);
+ let sides = node_param_f32(target, "sides", 4.0).clamp(3.0, 256.0) as usize;
+ let radius = node_param_f32(target, "radius", 0.5).max(1e-4);
+ let inner = node_param_f32(target, "inner_radius", 0.0).max(0.0);
+ let fill = node_param_bool(target, "fill", true);
+ let centre = node_param_vec3(target, "center", Vec3::ZERO);
let mut d = Detail::new();
// A star alternates between the two radii, so it has twice the corners.
@@ -2750,10 +2756,10 @@ pub fn resolve_transfer_geometry_with_errors(
ocl_error: &mut Option<String>,
sim: &mut EvalSim,
) -> Option<Detail> {
- let input_node = param_node(root, target, "Input")?;
+ let input_node = param_node(root, target, "input")?;
let mut geom = generate_single_node_geometry_with_errors(root, input_node, visited, ocl_error, sim)?;
- let from_name = node_param_node(target, "From").unwrap_or_default();
+ let from_name = node_param_node(target, "from").unwrap_or_default();
let Some(source) = find_input_node(root, target, &from_name)
.and_then(|n| generate_single_node_geometry_with_errors(root, n, visited, ocl_error, sim))
else {
@@ -2766,14 +2772,14 @@ pub fn resolve_transfer_geometry_with_errors(
return Some(geom);
}
- let groups = node_param_bool(target, "Transfer Groups", false).then(|| name_list(&node_param_str(target, "Groups", "")));
+ let groups = node_param_bool(target, "transfer_groups", false).then(|| name_list(&node_param_str(target, "groups", "")));
transfer_onto(
&mut geom,
&source,
- &name_list(&node_param_str(target, "Attributes", "")),
+ &name_list(&node_param_str(target, "attributes", "")),
groups.as_deref(),
- node_param_f32(target, "Maximum Distance", 0.0),
- node_param_str(target, "Group", "").trim(),
+ node_param_f32(target, "maximum_distance", 0.0),
+ node_param_str(target, "group", "").trim(),
);
Some(geom)
}
@@ -2874,13 +2880,13 @@ pub fn resolve_valence_geometry_with_errors(
ocl_error: &mut Option<String>,
sim: &mut EvalSim,
) -> Option<Detail> {
- let input_node = param_node(root, target, "Input")?;
+ let input_node = param_node(root, target, "input")?;
let mut geom = generate_single_node_geometry_with_errors(root, input_node, visited, ocl_error, sim)?;
- let name = node_param_str(target, "Attribute", "valence").trim().to_string();
+ let name = node_param_str(target, "attribute", "valence").trim().to_string();
if name.is_empty() {
return Some(geom);
}
- let by_prims = node_param_str(target, "Measure", "Neighbours").eq_ignore_ascii_case("primitives");
+ let by_prims = node_param_str(target, "measure", "Neighbours").eq_ignore_ascii_case("primitives");
let data: Vec<i32> = (0..geom.num_points())
.map(|p| {
if by_prims {
@@ -2911,7 +2917,7 @@ pub fn resolve_deform_geometry_with_errors(
ocl_error: &mut Option<String>,
sim: &mut EvalSim,
) -> Option<Detail> {
- let input_node = param_node(root, target, "Input")?;
+ let input_node = param_node(root, target, "input")?;
let mut geom = generate_single_node_geometry_with_errors(root, input_node, visited, ocl_error, sim)?;
apply_deform(&mut geom, target);
Some(geom)
@@ -2919,7 +2925,7 @@ pub fn resolve_deform_geometry_with_errors(
pub(crate) fn apply_deform(geom: &mut Detail, target: &FsNode) {
let Some((lo, hi)) = geom.bounds() else { return };
- let axis = match node_param_str(target, "Axis", "Y").to_uppercase().as_str() {
+ let axis = match node_param_str(target, "axis", "Y").to_uppercase().as_str() {
"X" => 0,
"Z" => 2,
_ => 1,
@@ -2933,9 +2939,9 @@ pub(crate) fn apply_deform(geom: &mut Detail, target: &FsNode) {
if span.abs() < 1e-9 {
return;
}
- let amount = node_param_f32(target, "Amount", 1.0);
- let mode = node_param_str(target, "Mode", "Twist").to_lowercase();
- let group = node_param_str(target, "Group", "");
+ let amount = node_param_f32(target, "amount", 1.0);
+ let mode = node_param_str(target, "mode", "Twist").to_lowercase();
+ let group = node_param_str(target, "group", "");
let group = group.trim().to_string();
let centre = (lo + hi) * 0.5;
@@ -2996,10 +3002,10 @@ pub fn resolve_copy_geometry_with_errors(
ocl_error: &mut Option<String>,
sim: &mut EvalSim,
) -> Option<Detail> {
- let source_node = param_node(root, target, "Input")?;
+ let source_node = param_node(root, target, "input")?;
let source = generate_single_node_geometry_with_errors(root, source_node, visited, ocl_error, sim)?;
- let to_name = node_param_node(target, "To").unwrap_or_default();
+ let to_name = node_param_node(target, "to").unwrap_or_default();
let Some(onto) = find_input_node(root, target, &to_name)
.and_then(|n| generate_single_node_geometry_with_errors(root, n, visited, ocl_error, sim))
else {
@@ -3009,7 +3015,7 @@ pub fn resolve_copy_geometry_with_errors(
return Some(source);
};
- let group = node_param_str(target, "Group", "");
+ let group = node_param_str(target, "group", "");
let group = group.trim().to_string();
let targets: Vec<usize> = (0..onto.num_points())
.filter(|&p| group.is_empty() || onto.points().in_group(&group, p))
@@ -3034,9 +3040,9 @@ pub fn resolve_copy_geometry_with_errors(
return Some(Detail::new());
}
- let orient = node_param_str(target, "Orient", "None").eq_ignore_ascii_case("normal");
- let scale = node_param_f32(target, "Scale", 1.0);
- let scale_attr = node_param_str(target, "Scale Attribute", "");
+ let orient = node_param_str(target, "orient", "None").eq_ignore_ascii_case("normal");
+ let scale = node_param_f32(target, "scale", 1.0);
+ let scale_attr = node_param_str(target, "scale_attribute", "");
let scale_attr = scale_attr.trim().to_string();
let normals = orient.then(|| point_normals(&onto));
@@ -3083,7 +3089,7 @@ pub fn resolve_soft_transform_geometry_with_errors(
ocl_error: &mut Option<String>,
sim: &mut EvalSim,
) -> Option<Detail> {
- let input_node = param_node(root, target, "Input")?;
+ let input_node = param_node(root, target, "input")?;
let mut geom = generate_single_node_geometry_with_errors(root, input_node, visited, ocl_error, sim)?;
apply_soft_transform(&mut geom, target);
Some(geom)
@@ -3094,19 +3100,19 @@ pub(crate) fn apply_soft_transform(geom: &mut Detail, target: &FsNode) {
if n == 0 {
return;
}
- let translation = node_param_vec3(target, "Translation", Vec3::ZERO);
- let radius = node_param_f32(target, "Radius", 0.5).max(0.0);
- let falloff = node_param_str(target, "Falloff", "Smooth").to_lowercase();
- let group = node_param_str(target, "Group", "");
+ let translation = node_param_vec3(target, "translation", Vec3::ZERO);
+ let radius = node_param_f32(target, "radius", 0.5).max(0.0);
+ let falloff = node_param_str(target, "falloff", "Smooth").to_lowercase();
+ let group = node_param_str(target, "group", "");
let group = group.trim().to_string();
- let write_attr = node_param_str(target, "Attribute", "");
+ let write_attr = node_param_str(target, "attribute", "");
let write_attr = write_attr.trim().to_string();
let members: Vec<Vec3> = (0..n)
.filter(|&p| !group.is_empty() && geom.points().in_group(&group, p))
.map(|p| geom.pos(p))
.collect();
- let centre = node_param_vec3(target, "Center", Vec3::ZERO);
+ let centre = node_param_vec3(target, "center", Vec3::ZERO);
let grid = (!members.is_empty()).then(|| crate::spatial::PointGrid::build(&members, radius.max(1e-4)));
let mut weights = vec![0.0f32; n];
@@ -3159,9 +3165,9 @@ pub fn resolve_subdivide_geometry_with_errors(
ocl_error: &mut Option<String>,
sim: &mut EvalSim,
) -> Option<Detail> {
- let input_node = param_node(root, target, "Input")?;
+ let input_node = param_node(root, target, "input")?;
let geom = generate_single_node_geometry_with_errors(root, input_node, visited, ocl_error, sim)?;
- let depth = node_param_f32(target, "Depth", 1.0).clamp(0.0, 6.0) as usize;
+ let depth = node_param_f32(target, "depth", 1.0).clamp(0.0, 6.0) as usize;
Some(crate::remesh::subdivide(&geom, depth))
}
@@ -3186,7 +3192,7 @@ pub fn resolve_detangle_geometry_with_errors(
ocl_error: &mut Option<String>,
sim: &mut EvalSim,
) -> Option<Detail> {
- let input_node = param_node(root, target, "Input")?;
+ let input_node = param_node(root, target, "input")?;
let mut geom = generate_single_node_geometry_with_errors(root, input_node, visited, ocl_error, sim)?;
// Inside a simnet that is mid-solve, what the substep consumed is where
// the points were when the step began: the nearest simnet above this
@@ -3230,13 +3236,13 @@ pub(crate) fn apply_detangle_reference(geom: &mut Detail, target: &FsNode) {
.map(|e| (geom.pos(e[1] as usize) - geom.pos(e[0] as usize)).length())
.sum::<f32>()
/ edges.len() as f32;
- let thickness = node_param_f32(target, "Thickness", 1.0).max(0.0) * mean_edge;
+ let thickness = node_param_f32(target, "thickness", 1.0).max(0.0) * mean_edge;
if thickness <= 0.0 {
return;
}
- let rings = node_param_f32(target, "Rings", 2.0).clamp(0.0, 6.0) as usize;
- let iterations = node_param_f32(target, "Iterations", 4.0).clamp(1.0, 32.0) as usize;
- let group = node_param_str(target, "Group", "");
+ let rings = node_param_f32(target, "rings", 2.0).clamp(0.0, 6.0) as usize;
+ let iterations = node_param_f32(target, "iterations", 4.0).clamp(1.0, 32.0) as usize;
+ let group = node_param_str(target, "group", "");
let group = group.trim().to_string();
let movable: Vec<bool> = (0..n)
.map(|p| group.is_empty() || geom.points().in_group(&group, p))
@@ -3332,10 +3338,10 @@ pub fn resolve_suture_geometry_with_errors(
ocl_error: &mut Option<String>,
sim: &mut EvalSim,
) -> Option<Detail> {
- let input_node = param_node(root, target, "Input")?;
+ let input_node = param_node(root, target, "input")?;
let mut geom = generate_single_node_geometry_with_errors(root, input_node, visited, ocl_error, sim)?;
- let against = param_node(root, target, "Against")
+ let against = param_node(root, target, "against")
.and_then(|n| generate_single_node_geometry_with_errors(root, n, visited, ocl_error, sim));
apply_suture(&mut geom, against.as_ref(), target);
Some(geom)
@@ -3346,9 +3352,9 @@ pub(crate) fn apply_suture(geom: &mut Detail, against: Option<&Detail>, target:
if n == 0 {
return;
}
- let distance = node_param_f32(target, "Distance Threshold", 0.05).max(0.0);
- let fusion = node_param_f32(target, "Fusion Threshold", 3.0).max(1.0) as i32;
- let counter = node_param_str(target, "Counter", "contact");
+ let distance = node_param_f32(target, "distance_threshold", 0.05).max(0.0);
+ let fusion = node_param_f32(target, "fusion_threshold", 3.0).max(1.0) as i32;
+ let counter = node_param_str(target, "counter", "contact");
let counter = counter.trim().to_string();
if counter.is_empty() || distance <= 0.0 {
return;
@@ -3439,7 +3445,7 @@ pub fn resolve_remesh_geometry_with_errors(
ocl_error: &mut Option<String>,
sim: &mut EvalSim,
) -> Option<Detail> {
- let input_node = param_node(root, target, "Input")?;
+ let input_node = param_node(root, target, "input")?;
let geom = generate_single_node_geometry_with_errors(root, input_node, visited, ocl_error, sim)?;
let mut out = crate::remesh::remesh(&geom, remesh_settings(target));
remesh_transfer(&mut out, &geom, root, target, visited, ocl_error, sim);
@@ -3465,10 +3471,10 @@ fn remesh_transfer(
ocl_error: &mut Option<String>,
sim: &mut EvalSim,
) {
- if !node_param_bool(target, "Transfer", false) {
+ if !node_param_bool(target, "transfer", false) {
return;
}
- let from_name = node_param_node(target, "From").unwrap_or_default();
+ let from_name = node_param_node(target, "from").unwrap_or_default();
let named = if from_name.is_empty() {
None
} else {
@@ -3483,13 +3489,13 @@ fn remesh_transfer(
}
};
let source = named.as_ref().unwrap_or(input);
- let groups = node_param_bool(target, "Transfer Groups", false).then(|| name_list(&node_param_str(target, "Groups", "")));
+ let groups = node_param_bool(target, "transfer_groups", false).then(|| name_list(&node_param_str(target, "groups", "")));
transfer_onto(
out,
source,
- &name_list(&node_param_str(target, "Attributes", "")),
+ &name_list(&node_param_str(target, "attributes", "")),
groups.as_deref(),
- node_param_f32(target, "Maximum Distance", 0.0),
+ node_param_f32(target, "maximum_distance", 0.0),
"",
);
}
@@ -3498,17 +3504,17 @@ fn remesh_transfer(
/// repeat that each one is: what the load bypasses when it recomposes a
/// native remesh whose switch was off.
pub const REMESH_PASS_SWITCHES: [(&str, &str); 4] =
- [("Split", "split1"), ("Collapse", "collapse1"), ("Flip", "flip1"), ("Project", "project1")];
+ [("split", "split1"), ("collapse", "collapse1"), ("flip", "flip1"), ("project", "project1")];
pub(crate) fn remesh_settings(target: &FsNode) -> crate::remesh::Settings {
crate::remesh::Settings {
- target: node_param_f32(target, "Target Length", 0.1).max(1e-4),
- iterations: node_param_f32(target, "Iterations", 3.0).clamp(1.0, 20.0) as usize,
- relax: node_param_f32(target, "Relax", 0.5),
- split: node_param_bool(target, "Split", true),
- collapse: node_param_bool(target, "Collapse", true),
- flip: node_param_bool(target, "Flip", true),
- project: node_param_bool(target, "Project", true),
+ target: node_param_f32(target, "target_length", 0.1).max(1e-4),
+ iterations: node_param_f32(target, "iterations", 3.0).clamp(1.0, 20.0) as usize,
+ relax: node_param_f32(target, "relax", 0.5),
+ split: node_param_bool(target, "split", true),
+ collapse: node_param_bool(target, "collapse", true),
+ flip: node_param_bool(target, "flip", true),
+ project: node_param_bool(target, "project", true),
}
}
@@ -3529,7 +3535,7 @@ fn level_input(
// Looked up from the SUBNET's level, not from inside it: the name is
// the subnet's wire, and a child that happens to share it (the
// Embryo's sphere1 beside an outer sphere1) is not what it names.
- node_param_node(parent, "Input")
+ node_param_node(parent, "input")
.and_then(|name| find_input_node(root, parent, &name))
.and_then(|input_node| generate_single_node_geometry_with_errors(root, input_node, visited, ocl_error, sim))
};
@@ -3550,9 +3556,9 @@ pub fn resolve_edge_pass_geometry_with_errors(
ocl_error: &mut Option<String>,
sim: &mut EvalSim,
) -> Option<Detail> {
- let input_node = param_node(root, target, "Input")?;
+ let input_node = param_node(root, target, "input")?;
let geom = generate_single_node_geometry_with_errors(root, input_node, visited, ocl_error, sim)?;
- Some(crate::remesh::edge_pass(&geom, pass, node_param_f32(target, "Target Length", 0.1).max(1e-4)))
+ Some(crate::remesh::edge_pass(&geom, pass, node_param_f32(target, "target_length", 0.1).max(1e-4)))
}
/// The Project node: every point of the Input moved to the nearest place on
@@ -3566,9 +3572,9 @@ pub fn resolve_project_geometry_with_errors(
ocl_error: &mut Option<String>,
sim: &mut EvalSim,
) -> Option<Detail> {
- let input_node = param_node(root, target, "Input")?;
+ let input_node = param_node(root, target, "input")?;
let geom = generate_single_node_geometry_with_errors(root, input_node, visited, ocl_error, sim)?;
- let Some(surface_name) = node_param_node(target, "Surface") else { return Some(geom) };
+ let Some(surface_name) = node_param_node(target, "surface") else { return Some(geom) };
let Some(surface) = find_input_node(root, target, &surface_name)
.and_then(|n| generate_single_node_geometry_with_errors(root, n, visited, ocl_error, sim))
else {
@@ -3613,11 +3619,11 @@ fn run_repeat(
sim: &mut EvalSim,
) -> Option<(Detail, Detail)> {
let output_node = target.children.iter().find(|c| c.node_type.eq_ignore_ascii_case("output"))?;
- let seed = param_node(root, target, "Input")
+ let seed = param_node(root, target, "input")
.and_then(|n| generate_single_node_geometry_with_errors(root, n, visited, ocl_error, sim))
.unwrap_or_default();
- let iterations = (node_param_f32(target, "Iterations", 1.0).round().max(0.0) as usize).min(REPEAT_MAX);
- let stop = node_param_bool(target, "Stop When Unchanged", false);
+ let iterations = (node_param_f32(target, "iterations", 1.0).round().max(0.0) as usize).min(REPEAT_MAX);
+ let stop = node_param_bool(target, "stop_when_unchanged", false);
sim.seeds.push((target.id.clone(), Some(seed.clone())));
let mut state = seed.clone();
let mut prev = seed;
@@ -3728,14 +3734,14 @@ pub fn resolve_develop_geometry_with_errors(
ocl_error: &mut Option<String>,
sim: &mut EvalSim,
) -> Option<Detail> {
- let input_node = param_node(root, target, "Input")?;
+ let input_node = param_node(root, target, "input")?;
let mut geom = generate_single_node_geometry_with_errors(root, input_node, visited, ocl_error, sim)?;
apply_develop(&mut geom, target, ocl_error);
Some(geom)
}
pub(crate) fn apply_develop(geom: &mut Detail, target: &FsNode, ocl_error: &mut Option<String>) {
- let name = node_param_str(target, "Attribute", "").trim().to_string();
+ let name = node_param_str(target, "attribute", "").trim().to_string();
if name.is_empty() {
return;
}
@@ -3749,11 +3755,11 @@ pub(crate) fn apply_develop(geom: &mut Detail, target: &FsNode, ocl_error: &mut
return;
}
- let scale = node_param_f32(target, "Scale", 0.1);
- let group = node_param_str(target, "Group", "");
+ let scale = node_param_f32(target, "scale", 0.1);
+ let group = node_param_str(target, "group", "");
let group = group.trim().to_string();
- let by_attr = node_param_str(target, "Direction", "Normal").eq_ignore_ascii_case("attribute");
- let src = node_param_str(target, "Source", "");
+ let by_attr = node_param_str(target, "direction", "Normal").eq_ignore_ascii_case("attribute");
+ let src = node_param_str(target, "source", "");
let src = src.trim().to_string();
// Normals come off the geometry as it arrives, so every point is displaced
@@ -3864,7 +3870,7 @@ pub fn vis_marker_vertices(geom: &Detail, linearize: impl Fn([f32; 3]) -> [f32;
/// pull arrows while one is selected.
pub fn moves_points(node: &FsNode) -> bool {
node.node_type.eq_ignore_ascii_case("attribute")
- && node_param_str(node, "Attribute Name", "").trim().eq_ignore_ascii_case("Pos")
+ && node_param_str(node, "attribute_name", "").trim().eq_ignore_ascii_case("Pos")
}
/// `target`'s geometry as the scene SHOWS it: a node inside a simnet as the
@@ -3906,7 +3912,7 @@ pub fn node_geometry_as_shown(
/// node was rewired onto something that adds or removes points) give nothing,
/// since the indices no longer pair up.
pub fn point_displacements(root: &FsNode, target: &FsNode, sim: &mut EvalSim) -> Vec<(Vec3, Vec3)> {
- let Some(input_node) = param_node(root, target, "Input") else {
+ let Some(input_node) = param_node(root, target, "input") else {
return Vec::new();
};
let mut ocl_error = None;
@@ -4010,14 +4016,14 @@ pub fn resolve_visualize_geometry_with_errors(
ocl_error: &mut Option<String>,
sim: &mut EvalSim,
) -> Option<Detail> {
- let input_node = param_node(root, target, "Input")?;
+ let input_node = param_node(root, target, "input")?;
let mut geom = generate_single_node_geometry_with_errors(root, input_node, visited, ocl_error, sim)?;
apply_visualize(&mut geom, target, ocl_error);
Some(geom)
}
pub(crate) fn apply_visualize(geom: &mut Detail, target: &FsNode, ocl_error: &mut Option<String>) {
- let name = node_param_str(target, "Attribute", "").trim().to_string();
+ let name = node_param_str(target, "attribute", "").trim().to_string();
if name.is_empty() {
return;
}
@@ -4031,14 +4037,14 @@ pub(crate) fn apply_visualize(geom: &mut Detail, target: &FsNode, ocl_error: &mu
return;
}
- let group = node_param_str(target, "Group", "");
+ let group = node_param_str(target, "group", "");
let group = group.trim().to_string();
let affected: Vec<usize> = (0..geom.num_points())
.filter(|&p| group.is_empty() || geom.points().in_group(&group, p))
.collect();
- if node_param_str(target, "Mode", "Ramp").eq_ignore_ascii_case("vector") {
- let scale = node_param_f32(target, "Scale", 0.2);
+ if node_param_str(target, "mode", "Ramp").eq_ignore_ascii_case("vector") {
+ let scale = node_param_f32(target, "scale", 0.2);
let staged: Vec<[f32; 3]> = (0..geom.num_points())
.map(|p| {
if !affected.contains(&p) {
@@ -4066,10 +4072,10 @@ pub(crate) fn apply_visualize(geom: &mut Detail, target: &FsNode, ocl_error: &mu
// Ramp. Auto measures across EVERY point, not just the group: a group's
// colours should sit where they belong on the whole picture's scale, or
// two Visualize nodes over two groups would each claim the full ramp.
- let (from, to) = if node_param_str(target, "Range", "Auto").eq_ignore_ascii_case("manual") {
+ let (from, to) = if node_param_str(target, "range", "Auto").eq_ignore_ascii_case("manual") {
(
- node_param_f32(target, "From", 0.0),
- node_param_f32(target, "To", 1.0),
+ node_param_f32(target, "from", 0.0),
+ node_param_f32(target, "to", 1.0),
)
} else {
let vals: Vec<f32> = (0..geom.num_points())
@@ -4083,9 +4089,9 @@ pub(crate) fn apply_visualize(geom: &mut Detail, target: &FsNode, ocl_error: &mu
};
let span = to - from;
- let ramp = node_param_str(target, "Ramp", "Viridis").to_lowercase();
- let blend = node_param_str(target, "Blend", "Set").to_lowercase();
- let opacity = node_param_f32(target, "Opacity", 1.0).clamp(0.0, 1.0);
+ let ramp = node_param_str(target, "ramp", "Viridis").to_lowercase();
+ let blend = node_param_str(target, "blend", "Set").to_lowercase();
+ let opacity = node_param_f32(target, "opacity", 1.0).clamp(0.0, 1.0);
for p in affected {
let v = geom.points().value(&name, p).map(|v| v.as_f32()).unwrap_or(0.0);
@@ -4131,16 +4137,16 @@ pub fn resolve_analysis_geometry_with_errors(
ocl_error: &mut Option<String>,
sim: &mut EvalSim,
) -> Option<Detail> {
- let input_node = param_node(root, target, "Input")?;
+ let input_node = param_node(root, target, "input")?;
let mut geom = generate_single_node_geometry_with_errors(root, input_node, visited, ocl_error, sim)?;
apply_analysis(&mut geom, target, ocl_error);
Some(geom)
}
pub(crate) fn apply_analysis(geom: &mut Detail, target: &FsNode, ocl_error: &mut Option<String>) {
- let edges = node_param_str(target, "Source", "Attribute").eq_ignore_ascii_case("edge lengths");
- let name = node_param_str(target, "Attribute", "").trim().to_string();
- let group = node_param_str(target, "Group", "");
+ let edges = node_param_str(target, "source", "Attribute").eq_ignore_ascii_case("edge lengths");
+ let name = node_param_str(target, "attribute", "").trim().to_string();
+ let group = node_param_str(target, "group", "");
let group = group.trim().to_string();
// The column to reduce, and the name its answers hang off.
@@ -4219,7 +4225,7 @@ pub fn resolve_time_geometry_with_errors(
ocl_error: &mut Option<String>,
sim: &mut EvalSim,
) -> Option<Detail> {
- let input_node = param_node(root, target, "Input")?;
+ let input_node = param_node(root, target, "input")?;
let frame = sim.frame;
let mut geom = generate_single_node_geometry_with_errors(root, input_node, visited, ocl_error, sim)?;
apply_time(&mut geom, target, frame);
@@ -4227,12 +4233,12 @@ pub fn resolve_time_geometry_with_errors(
}
pub(crate) fn apply_time(geom: &mut Detail, target: &FsNode, frame: i32) {
- let name = node_param_str(target, "Attribute", "t").trim().to_string();
+ let name = node_param_str(target, "attribute", "t").trim().to_string();
if name.is_empty() {
return;
}
- let start = node_param_f32(target, "Start Frame", 1.0);
- let end = node_param_f32(target, "End Frame", 100.0);
+ let start = node_param_f32(target, "start_frame", 1.0);
+ let end = node_param_f32(target, "end_frame", 100.0);
let span = end - start;
// A zero-length range is 1.0 from its first frame on, not a division by
// zero: "the whole range has elapsed" is the only reading that composes.
@@ -4241,7 +4247,7 @@ pub(crate) fn apply_time(geom: &mut Detail, target: &FsNode, frame: i32) {
} else {
(frame as f32 - start) / span
};
- let t = if node_param_bool(target, "Clamp", true) {
+ let t = if node_param_bool(target, "clamp", true) {
t.clamp(0.0, 1.0)
} else {
t
@@ -4386,7 +4392,7 @@ pub fn resolve_neighbour_geometry_with_errors(
ocl_error: &mut Option<String>,
sim: &mut EvalSim,
) -> Option<Detail> {
- let input_node = param_node(root, target, "Input")?;
+ let input_node = param_node(root, target, "input")?;
let mut geom = generate_single_node_geometry_with_errors(root, input_node, visited, ocl_error, sim)?;
apply_neighbour(&mut geom, target, ocl_error);
Some(geom)
@@ -4397,7 +4403,7 @@ pub fn resolve_neighbour_geometry_with_errors(
/// Split from the resolver so the operator can be exercised on geometry a test
/// controls, rather than only on whatever a graph happens to produce.
pub(crate) fn apply_neighbour(geom: &mut Detail, target: &FsNode, ocl_error: &mut Option<String>) {
- let name = node_param_str(target, "Attribute", "").trim().to_string();
+ let name = node_param_str(target, "attribute", "").trim().to_string();
if name.is_empty() {
return;
}
@@ -4413,8 +4419,8 @@ pub(crate) fn apply_neighbour(geom: &mut Detail, target: &FsNode, ocl_error: &mu
let n = geom.num_points();
let k = ty.components();
- let amount = node_param_f32(target, "Amount", 0.5).clamp(0.0, 1.0);
- let mode = node_param_str(target, "Mode", "Diffuse").to_lowercase();
+ let amount = node_param_f32(target, "amount", 0.5).clamp(0.0, 1.0);
+ let mode = node_param_str(target, "mode", "Diffuse").to_lowercase();
// The attribute as a flat n x k matrix of components, so one body serves
// every type. Integers ride through as floats and round on the way back.
@@ -4433,16 +4439,16 @@ pub(crate) fn apply_neighbour(geom: &mut Detail, target: &FsNode, ocl_error: &mu
// A Group narrows which points are EDITED. Their neighbours are still read
// from the whole geometry — a diffusion that could only see inside its own
// group would bend away from the boundary rather than across it.
- let group = node_param_str(target, "Group", "");
+ let group = node_param_str(target, "group", "");
let group = group.trim().to_string();
let edits: Vec<bool> = (0..n)
.map(|p| group.is_empty() || geom.points().in_group(&group, p))
.collect();
- let hood = match node_param_str(target, "Neighbourhood", "Connectivity").to_lowercase().as_str() {
- "radius" => Hood::Radius(node_param_f32(target, "Radius", 0.2).max(0.0)),
+ let hood = match node_param_str(target, "neighbourhood", "Connectivity").to_lowercase().as_str() {
+ "radius" => Hood::Radius(node_param_f32(target, "radius", 0.2).max(0.0)),
"global" => Hood::Global,
- _ => Hood::Connectivity(node_param_f32(target, "Rings", 1.0).max(1.0) as usize),
+ _ => Hood::Connectivity(node_param_f32(target, "rings", 1.0).max(1.0) as usize),
};
let mut out = val.clone();
@@ -4455,7 +4461,7 @@ pub(crate) fn apply_neighbour(geom: &mut Detail, target: &FsNode, ocl_error: &mu
}
}
"migrate" => {
- let dir_name = node_param_str(target, "Direction", "");
+ let dir_name = node_param_str(target, "direction", "");
let dir_name = dir_name.trim().to_string();
if dir_name.is_empty() || !geom.points().has(&dir_name) {
if ocl_error.is_none() {
@@ -4520,11 +4526,11 @@ pub(crate) fn apply_neighbour(geom: &mut Detail, target: &FsNode, ocl_error: &mu
let read = |val: &[f32], p: usize| {
Vec3::new(val[p * k], val[p * k + 1], if k > 2 { val[p * k + 2] } else { 0.0 })
};
- let src_name = node_param_str(target, "Source", "");
+ let src_name = node_param_str(target, "source", "");
let src_name = src_name.trim().to_string();
let targets: Vec<Vec3> = if mode == "align" {
- let kind = node_param_str(target, "Target", "Local Average").to_lowercase();
+ let kind = node_param_str(target, "target", "Local Average").to_lowercase();
align_targets(geom, &val, &hood, &kind, target, &src_name, &read)
} else {
// Lead: each point turns toward the neighbouring vector
@@ -4609,7 +4615,7 @@ pub(crate) fn apply_neighbour(geom: &mut Detail, target: &FsNode, ocl_error: &mu
// spend. Integrate-and-fire, which is how an excitable medium makes a
// wave out of a gradient.
"charge" => {
- let release = node_param_f32(target, "Release", 1.0);
+ let release = node_param_f32(target, "release", 1.0);
for p in (0..n).filter(|&p| edits[p]) {
for c in 0..k {
out[p * k + c] = val[p * k + c] + amount;
@@ -4738,7 +4744,7 @@ fn align_targets(
.collect()
}
"constant" => {
- let c = node_param_vec3(target, "Constant", Vec3::Y);
+ let c = node_param_vec3(target, "constant", Vec3::Y);
vec![c; n]
}
"attribute" => (0..n)
@@ -4852,7 +4858,7 @@ pub fn resolve_attribute_geometry_with_errors(
// pushes the target's id before dispatching to this resolver, so a local
// `visited.contains` check would see it and refuse every call (the trap
// that broke this node's first draft).
- let input_node = param_node(root, target, "Input")?;
+ let input_node = param_node(root, target, "input")?;
let mut geom = generate_single_node_geometry_with_errors(root, input_node, visited, ocl_error, sim)?;
apply_attribute(&mut geom, target, ocl_error);
Some(geom)
@@ -4861,18 +4867,18 @@ pub fn resolve_attribute_geometry_with_errors(
/// The Attribute operator itself, over geometry already in hand — split from
/// the resolver for the same reason `apply_neighbour` is.
pub(crate) fn apply_attribute(geom: &mut Detail, target: &FsNode, ocl_error: &mut Option<String>) {
- let name = node_param_str(target, "Attribute Name", "attr1").trim().to_string();
+ let name = node_param_str(target, "attribute_name", "attr1").trim().to_string();
if name.is_empty() {
return;
}
- let op = node_param_str(target, "Operation", "Create").to_lowercase();
- let combine_mode = node_param_str(target, "Combine", "Set").to_lowercase();
+ let op = node_param_str(target, "operation", "Create").to_lowercase();
+ let combine_mode = node_param_str(target, "combine", "Set").to_lowercase();
let is_pos = name.eq_ignore_ascii_case("Pos");
let is_col = name.eq_ignore_ascii_case("Col");
let builtin = is_pos || is_col;
let mut fail = String::new();
- let group = node_param_str(target, "Group", "");
+ let group = node_param_str(target, "group", "");
let group = group.trim().to_string();
let affected: Vec<usize> = (0..geom.num_points())
.filter(|&p| group.is_empty() || geom.points().in_group(&group, p))
@@ -4880,7 +4886,7 @@ pub(crate) fn apply_attribute(geom: &mut Detail, target: &FsNode, ocl_error: &mu
// Value, as raw components. Delete never reads it; Create/Modify reject
// the edit outright when any component fails to parse.
- let value_str = node_param_str(target, "Value", "");
+ let value_str = node_param_str(target, "value", "");
let raw: Vec<&str> = value_str
.split(|c| c == ':' || c == ',' || c == ' ')
.filter(|p| !p.is_empty())
@@ -4903,8 +4909,8 @@ pub(crate) fn apply_attribute(geom: &mut Detail, target: &FsNode, ocl_error: &mu
// attribute being written. Pos and Col (P and Cd) are the position and
// the colour. `None` for a constant Value.
let from_attr = matches!(op.as_str(), "create" | "modify")
- && node_param_str(target, "Value From", "Constant").eq_ignore_ascii_case("attribute");
- let src_name = node_param_str(target, "From Attribute", "").trim().to_string();
+ && node_param_str(target, "value_from", "Constant").eq_ignore_ascii_case("attribute");
+ let src_name = node_param_str(target, "from_attribute", "").trim().to_string();
let src_rows: Option<Vec<Vec<f32>>> = if !from_attr {
None
} else if src_name.is_empty() {
@@ -4967,8 +4973,8 @@ pub(crate) fn apply_attribute(geom: &mut Detail, target: &FsNode, ocl_error: &mu
// much of Value, a Set goes that far toward it, a Multiply that
// far toward the product. At exactly one the combined value is
// written as it always was, bit for bit.
- let strength = node_param_f32(target, "Strength", 1.0);
- let scale_by = node_param_str(target, "Scale By", "");
+ let strength = node_param_f32(target, "strength", 1.0);
+ let scale_by = node_param_str(target, "scale_by", "");
let scale_by = scale_by.trim().to_string();
let weights: Option<Vec<f32>> = if scale_by.is_empty() {
None
@@ -4997,7 +5003,7 @@ pub(crate) fn apply_attribute(geom: &mut Detail, target: &FsNode, ocl_error: &mu
// before it, and the chains that Add one to a counter to count
// the runs of the chain, which a rate would make a count of
// frames.
- let per_frame = node_param_bool(target, "Per Frame", false);
+ let per_frame = node_param_bool(target, "per_frame", false);
let dt = if per_frame { geom.detail().value("dt", 0).map_or(1.0, |v| v.as_f32()) } else { 1.0 };
let dt = if dt.is_finite() && dt > 0.0 { dt } else { 1.0 };
let (adds, multiplies) = (combine_mode == "add", combine_mode == "multiply");
@@ -5065,12 +5071,12 @@ pub(crate) fn apply_attribute(geom: &mut Detail, target: &FsNode, ocl_error: &mu
// always remapped before anything reads it.
"remap" => {
let (f0, f1) = (
- node_param_f32(target, "From Min", 0.0),
- node_param_f32(target, "From Max", 1.0),
+ node_param_f32(target, "from_min", 0.0),
+ node_param_f32(target, "from_max", 1.0),
);
let (t0, t1) = (
- node_param_f32(target, "To Min", 0.0),
- node_param_f32(target, "To Max", 1.0),
+ node_param_f32(target, "to_min", 0.0),
+ node_param_f32(target, "to_max", 1.0),
);
let span = f1 - f0;
if span.abs() < 1e-9 {
@@ -5085,8 +5091,8 @@ pub(crate) fn apply_attribute(geom: &mut Detail, target: &FsNode, ocl_error: &mu
// and Clip read the same way and chain without renaming anything.
"clip" => {
let (lo, hi) = (
- node_param_f32(target, "From Min", 0.0),
- node_param_f32(target, "From Max", 1.0),
+ node_param_f32(target, "from_min", 0.0),
+ node_param_f32(target, "from_max", 1.0),
);
let (lo, hi) = if lo <= hi { (lo, hi) } else { (hi, lo) };
edit_components(geom, &name, &affected, |v| v.clamp(lo, hi));
@@ -5101,8 +5107,8 @@ pub(crate) fn apply_attribute(geom: &mut Detail, target: &FsNode, ocl_error: &mu
.filter_map(|&p| geom.points().value(&name, p))
.map(|v| v.as_f32())
.collect();
- let goal = node_param_f32(target, "To Max", 1.0);
- let measure = match node_param_str(target, "Target", "Maximum").to_lowercase().as_str() {
+ let goal = node_param_f32(target, "to_max", 1.0);
+ let measure = match node_param_str(target, "target", "Maximum").to_lowercase().as_str() {
"sum" => vals.iter().sum::<f32>(),
"range" => {
let hi = vals.iter().copied().fold(f32::NEG_INFINITY, f32::max);
@@ -5122,12 +5128,12 @@ pub(crate) fn apply_attribute(geom: &mut Detail, target: &FsNode, ocl_error: &mu
// and Length collapse to a scalar written into every component, since
// the destination keeps its own type.
"composite" => {
- let b_name = node_param_str(target, "Source B", "");
+ let b_name = node_param_str(target, "source_b", "");
let b_name = b_name.trim().to_string();
if !geom.points().has(&b_name) {
fail = format!("Source B '{}' is not a point attribute", b_name);
} else {
- let op = node_param_str(target, "Combine Op", "Add").to_lowercase();
+ let op = node_param_str(target, "combine_op", "Add").to_lowercase();
let Some(ty) = geom.points().get(&name).map(|a| a.ty()) else {
*ocl_error = Some(format!("Attribute '{}': '{}' is missing", target.name, name));
return;
@@ -5193,8 +5199,8 @@ pub(crate) fn apply_attribute(geom: &mut Detail, target: &FsNode, ocl_error: &mu
// gets back into per-point arithmetic.
"promote" => {
let to_detail =
- node_param_str(target, "To Class", "Detail").eq_ignore_ascii_case("detail");
- let method = node_param_str(target, "Method", "Average").to_lowercase();
+ node_param_str(target, "to_class", "Detail").eq_ignore_ascii_case("detail");
+ let method = node_param_str(target, "method", "Average").to_lowercase();
if to_detail {
match geom.points().get(&name).map(|a| a.ty()) {
None => fail = format!("'{}' is not a point attribute", name),
@@ -5230,7 +5236,7 @@ pub(crate) fn apply_attribute(geom: &mut Detail, target: &FsNode, ocl_error: &mu
} else if !value_ok {
fail = format!("Value '{}' does not parse as numbers", value_str);
} else {
- let ty = match node_param_str(target, "Type", "Float").to_lowercase().as_str() {
+ let ty = match node_param_str(target, "type", "Float").to_lowercase().as_str() {
"float2" => crate::detail::AttribType::Float2,
"float3" => crate::detail::AttribType::Float3,
"float4" => crate::detail::AttribType::Float4,
@@ -5242,7 +5248,7 @@ pub(crate) fn apply_attribute(geom: &mut Detail, target: &FsNode, ocl_error: &mu
// Declared where the author knows the answer: at the
// point of creation, not in a list somewhere else that
// has to be kept in step.
- let kind = if node_param_str(target, "Kind", "Live")
+ let kind = if node_param_str(target, "kind", "Live")
.eq_ignore_ascii_case("derivative")
{
crate::detail::AttribKind::Derivative
@@ -5362,28 +5368,28 @@ pub fn resolve_scatter_geometry_with_errors(
// geometry evaluated to None for the spreadsheet and for any downstream
// consumer, while the scene walk's direct call (fresh `visited`) kept the
// node LOOKING healthy. Cycles stay guarded by the dispatch itself.
- let input_node = param_node(root, target, "Input")?;
+ let input_node = param_node(root, target, "input")?;
let geom = generate_single_node_geometry_with_errors(root, input_node, visited, ocl_error, sim)?;
- let num_points = node_param_f32(target, "Points", 100.0) as usize;
- let radius = node_param_f32(target, "Radius", 0.02);
+ let num_points = node_param_f32(target, "points", 100.0) as usize;
+ let radius = node_param_f32(target, "radius", 0.02);
// See points_detail: markers are for looking at, bare points are for
// working with.
- let markers = node_param_bool(target, "Markers", true);
+ let markers = node_param_bool(target, "markers", true);
// Surface mode: points ON the surface, by area, optionally pushed apart
// across it — the Scatter SOP with Relax Points, which is what a seed
// for a hull wants. Volume mode below is what this node did first:
// points INSIDE the shape, by parity.
- if node_param_str(target, "Mode", "Volume").eq_ignore_ascii_case("surface") {
- let seed = node_param_f32(target, "Seed", 1.1);
+ if node_param_str(target, "mode", "Volume").eq_ignore_ascii_case("surface") {
+ let seed = node_param_f32(target, "seed", 1.1);
let mut pts = crate::scatter::scatter_on_surface(&geom, num_points, seed);
- let relax = node_param_bool(target, "Relax Points", false);
- let iterations = node_param_f32(target, "Relax Iterations", 50.0).max(0.0) as usize;
+ let relax = node_param_bool(target, "relax_points", false);
+ let iterations = node_param_f32(target, "relax_iterations", 50.0).max(0.0) as usize;
if relax && iterations > 0 && !pts.is_empty() {
- let scale = node_param_f32(target, "Scale Radii By", 1.248);
- let max = (node_param_bool(target, "Use Max Relax Radius", true))
- .then(|| node_param_f32(target, "Max Relax Radius", 10.0));
+ let scale = node_param_f32(target, "scale_radii_by", 1.248);
+ let max = (node_param_bool(target, "use_max_relax_radius", true))
+ .then(|| node_param_f32(target, "max_relax_radius", 10.0));
let r = crate::scatter::relax_radius(&geom, pts.len(), scale, max);
let grid = crate::spatial::TriGrid::build(&geom);
crate::scatter::relax_on_surface(&mut pts, &grid, r, iterations);
@@ -5691,8 +5697,8 @@ fn walk_level(root: &FsNode, start: &FsNode, ocl_error: &mut Option<String>, sim
*count += 1;
if is_visible {
let start = Vec3::new((idx % 4) as f32 * 1.25 - 1.875, 0.55, -((idx / 4) as f32) * 1.25);
- let length = node_param_f32(node, "Length", 1.0);
- let thickness = node_param_f32(node, "Thickness", 0.02);
+ let length = node_param_f32(node, "length", 1.0);
+ let thickness = node_param_f32(node, "thickness", 0.02);
let end = start + Vec3::new(0.0, length, 0.0);
out.merge(&box_detail(start, end, thickness));
}
@@ -6083,14 +6089,14 @@ fn walk_level(root: &FsNode, start: &FsNode, ocl_error: &mut Option<String>, sim
/// markers that happen to land on the same spot are still two points with two
/// identities rather than one welded blob.
pub fn points_detail(node: &FsNode, center: Vec3) -> Detail {
- let num_points = node_param_f32(node, "Points", 100.0) as i32;
- let shape = node_param_str(node, "Shape", "None");
+ let num_points = node_param_f32(node, "points", 100.0) as i32;
+ let shape = node_param_str(node, "shape", "None");
// Markers off emits BARE POINTS — no marker geometry at all. Everything
// that generates locations drew little spheres at them, which is right for
// looking at and wrong for working with: Copy placed one instance per
// marker vertex rather than one per location, because the markers were the
// only points there were.
- let markers = node_param_bool(node, "Markers", true);
+ let markers = node_param_bool(node, "markers", true);
let mut d = Detail::new();
for i in 0..num_points {
let t = i as f32 / num_points.max(1) as f32;
@@ -6755,7 +6761,7 @@ mod tests {
name: "Curve".into(),
node_type: "curve".into(),
children: vec![],
- params: [("Points", "0 0 0; 1 0 0"), ("Segments", "2"), ("Thickness", "0.02")]
+ params: [("points", "0 0 0; 1 0 0"), ("segments", "2"), ("thickness", "0.02")]
.into_iter()
.map(|(name, default)| crate::app::ParamDef::new(name.to_string(), "text".to_string(), default.to_string()))
.collect(),
@@ -6790,8 +6796,8 @@ mod tests {
node_type: "points".to_string(),
children: vec![],
params: vec![
- crate::app::ParamDef::new("Points".to_string(), "spinbox".to_string(), "5".to_string()).with_range(Some(1.0), Some(10.0)).with_step(Some(1.0)),
- crate::app::ParamDef::new("Shape".to_string(), "choice:None,Spiral,Line,Circle,Grid".to_string(), shape.to_string()),
+ crate::app::ParamDef::new("points".to_string(), "spinbox".to_string(), "5".to_string()).with_range(Some(1.0), Some(10.0)).with_step(Some(1.0)),
+ crate::app::ParamDef::new("shape".to_string(), "choice:None,Spiral,Line,Circle,Grid".to_string(), shape.to_string()),
],
geometry_visible: true,
bypassed: false,
@@ -6847,7 +6853,7 @@ mod tests {
node_type: "sphere".to_string(),
children: vec![],
params: vec![
- crate::app::ParamDef::new("Radius".to_string(), "slider".to_string(), "0.5".to_string())
+ crate::app::ParamDef::new("radius".to_string(), "slider".to_string(), "0.5".to_string())
],
geometry_visible: true,
bypassed: false,
@@ -6861,8 +6867,8 @@ mod tests {
node_type: "transform".to_string(),
children: vec![],
params: vec![
- crate::app::ParamDef::new("Input".to_string(), "text".to_string(), "Sphere 1".to_string()),
- crate::app::ParamDef::new("Translation".to_string(), "float3".to_string(), "1.00:2.00:3.00".to_string())
+ crate::app::ParamDef::new("input".to_string(), "text".to_string(), "Sphere 1".to_string()),
+ crate::app::ParamDef::new("translation".to_string(), "float3".to_string(), "1.00:2.00:3.00".to_string())
],
geometry_visible: true,
bypassed: false,
@@ -6901,8 +6907,8 @@ mod tests {
node_type: "transform".to_string(),
children: vec![],
params: vec![
- crate::app::ParamDef::new("Input".to_string(), "text".to_string(), "Transform 1".to_string()),
- crate::app::ParamDef::new("Translation".to_string(), "float3".to_string(), "-1.00:-1.00:-1.00".to_string())
+ crate::app::ParamDef::new("input".to_string(), "text".to_string(), "Transform 1".to_string()),
+ crate::app::ParamDef::new("translation".to_string(), "float3".to_string(), "-1.00:-1.00:-1.00".to_string())
],
geometry_visible: true,
bypassed: false,
@@ -6938,8 +6944,8 @@ mod tests {
node_type: "transform".to_string(),
children: vec![],
params: vec![
- crate::app::ParamDef::new("Input".to_string(), "text".to_string(), "Transform Loop".to_string()),
- crate::app::ParamDef::new("Translation".to_string(), "float3".to_string(), "1.00:1.00:1.00".to_string())
+ crate::app::ParamDef::new("input".to_string(), "text".to_string(), "Transform Loop".to_string()),
+ crate::app::ParamDef::new("translation".to_string(), "float3".to_string(), "1.00:1.00:1.00".to_string())
],
geometry_visible: true,
bypassed: false,
@@ -6972,7 +6978,7 @@ mod tests {
node_type: "sphere".to_string(),
children: vec![],
params: vec![
- crate::app::ParamDef::new("Radius".to_string(), "slider".to_string(), "0.5".to_string())
+ crate::app::ParamDef::new("radius".to_string(), "slider".to_string(), "0.5".to_string())
],
geometry_visible: true,
bypassed: false,
@@ -6987,9 +6993,9 @@ mod tests {
node_type: "scatter".to_string(),
children: vec![],
params: vec![
- crate::app::ParamDef::new("Input".to_string(), "text".to_string(), "Sphere 1".to_string()),
- crate::app::ParamDef::new("Points".to_string(), "spinbox".to_string(), "15".to_string()),
- crate::app::ParamDef::new("Radius".to_string(), "slider".to_string(), "0.02".to_string())
+ crate::app::ParamDef::new("input".to_string(), "text".to_string(), "Sphere 1".to_string()),
+ crate::app::ParamDef::new("points".to_string(), "spinbox".to_string(), "15".to_string()),
+ crate::app::ParamDef::new("radius".to_string(), "slider".to_string(), "0.02".to_string())
],
geometry_visible: true,
bypassed: false,
@@ -7056,7 +7062,7 @@ pub fn resolve_simnet_geometry_with_errors(
.clone();
let seed = {
- param_node(root, target, "Input")
+ param_node(root, target, "input")
.and_then(|n| generate_single_node_geometry_with_errors(root, n, visited, ocl_error, sim))
.unwrap_or_default()
};
@@ -7066,7 +7072,7 @@ pub fn resolve_simnet_geometry_with_errors(
// which is what every sim did before this parameter existed; a number
// decouples when a simulation starts from when the shot does, so two sims
// in one scene can begin at different times.
- let start_frame = node_param_f32(target, "Start Frame", sim.start_frame as f32).round() as i32;
+ let start_frame = node_param_f32(target, "start_frame", sim.start_frame as f32).round() as i32;
let due = (sim.frame - start_frame).max(0);
// Resume from what is kept of this solve: the latest state when it has
@@ -7126,7 +7132,7 @@ pub fn resolve_simnet_geometry_with_errors(
if let Some(c) = checkpoints.behind(due, in_hand) {
cached = Some((c.state.clone(), c.prev.clone(), c.frame));
}
- let caching = node_param_bool(target, "Cache", false);
+ let caching = node_param_bool(target, "cache", false);
// What the last substep consumed, carried with the solve so the interior
// view can be drawn without re-solving: resumed from the cache when the
// cache is what we resume from, the seed otherwise.
@@ -7149,7 +7155,7 @@ pub fn resolve_simnet_geometry_with_errors(
// Clamped, and deliberately not by trusting the parameter's declared
// range: a hand-edited project file reaches here too, and a solve of a
// hundred thousand substeps is indistinguishable from a hang.
- let substeps = (node_param_f32(target, "Substeps", 1.0).round() as i64).clamp(1, 64);
+ let substeps = (node_param_f32(target, "substeps", 1.0).round() as i64).clamp(1, 64);
// What one substep is worth, as a fraction of a frame. The chain reads it
// by promoting it onto points and compositing it into a rate — halve the
// step and a rate scaled by `dt` covers the same ground in twice as many
@@ -7356,7 +7362,7 @@ mod simnet_tests {
/// Apply one Attribute-node operation to geometry in hand.
fn run_attr(before: &Detail, params: &[(&str, &str)]) -> (Detail, Option<String>) {
let mut geom = before.clone();
- let mut ps = vec![param("Input", "In"), param("Attribute Name", "mass")];
+ let mut ps = vec![param("input", "In"), param("attribute_name", "mass")];
for (k, v) in params {
match ps.iter_mut().find(|p| p.name == *k) {
Some(p) => p.set_text(v.to_string()),
@@ -7391,11 +7397,11 @@ mod simnet_tests {
let (remapped, err) = run_attr(
&before,
&[
- ("Operation", "Remap"),
- ("From Min", "0.00"),
- ("From Max", &last.to_string()),
- ("To Min", "0.00"),
- ("To Max", "1.00"),
+ ("operation", "Remap"),
+ ("from_min", "0.00"),
+ ("from_max", &last.to_string()),
+ ("to_min", "0.00"),
+ ("to_max", "1.00"),
],
);
assert!(err.is_none(), "{err:?}");
@@ -7406,7 +7412,7 @@ mod simnet_tests {
// renaming anything between them.
let (clipped, err) = run_attr(
&before,
- &[("Operation", "Clip"), ("From Min", "2.00"), ("From Max", "5.00")],
+ &[("operation", "Clip"), ("from_min", "2.00"), ("from_max", "5.00")],
);
assert!(err.is_none(), "{err:?}");
assert_eq!(mass(&clipped, 0), 2.0);
@@ -7416,7 +7422,7 @@ mod simnet_tests {
// A degenerate source range is refused rather than dividing by zero.
let (_, err) = run_attr(
&before,
- &[("Operation", "Remap"), ("From Min", "1.00"), ("From Max", "1.00")],
+ &[("operation", "Remap"), ("from_min", "1.00"), ("from_max", "1.00")],
);
assert!(err.is_some(), "a zero-width source range must be reported");
}
@@ -7431,7 +7437,7 @@ mod simnet_tests {
let before = ramped_mass();
let n = before.num_points();
let get = |d: &Detail, name: &str, p: usize| attrib_components(d.points().value(name, p).unwrap());
- let from = |src: &str| [("Value From", "Attribute"), ("From Attribute", src)].map(|(k, v)| (k, v.to_string()));
+ let from = |src: &str| [("value_from", "Attribute"), ("from_attribute", src)].map(|(k, v)| (k, v.to_string()));
let run = |extra: &[(&str, &str)], src: &str| {
let f = from(src);
let mut ps: Vec<(&str, &str)> = f.iter().map(|(k, v)| (*k, v.as_str())).collect();
@@ -7440,39 +7446,39 @@ mod simnet_tests {
};
// A Float copied: weight = mass, point by point.
- let (d, err) = run(&[("Operation", "Create"), ("Attribute Name", "weight"), ("Type", "Float")], "mass");
+ let (d, err) = run(&[("operation", "Create"), ("attribute_name", "weight"), ("type", "Float")], "mass");
assert!(err.is_none(), "{err:?}");
for p in 0..n {
assert_eq!(get(&d, "weight", p), vec![p as f32]);
}
// Spread over a Float3.
- let (d, _) = run(&[("Operation", "Create"), ("Attribute Name", "v"), ("Type", "Float3")], "mass");
+ let (d, _) = run(&[("operation", "Create"), ("attribute_name", "v"), ("type", "Float3")], "mass");
assert_eq!(get(&d, "v", 3), vec![3.0, 3.0, 3.0]);
// Pos into a Float2: as many components as fit.
- let (d, _) = run(&[("Operation", "Create"), ("Attribute Name", "xy"), ("Type", "Float2")], "Pos");
+ let (d, _) = run(&[("operation", "Create"), ("attribute_name", "xy"), ("type", "Float2")], "Pos");
let pos = before.pos(5);
assert_eq!(get(&d, "xy", 5), vec![pos.x, pos.y]);
// Pos into a Float4: the rest zero.
- let (d, _) = run(&[("Operation", "Create"), ("Attribute Name", "xyzw"), ("Type", "Float4")], "Pos");
+ let (d, _) = run(&[("operation", "Create"), ("attribute_name", "xyzw"), ("type", "Float4")], "Pos");
assert_eq!(get(&d, "xyzw", 5), vec![pos.x, pos.y, pos.z, 0.0]);
// Modify: Pos plus the point's own displacement, mass spread.
- let (d, err) = run(&[("Operation", "Modify"), ("Attribute Name", "Pos"), ("Combine", "Add")], "mass");
+ let (d, err) = run(&[("operation", "Modify"), ("attribute_name", "Pos"), ("combine", "Add")], "mass");
assert!(err.is_none(), "{err:?}");
let moved = d.pos(4) - before.pos(4);
assert!((moved - Vec3::splat(4.0)).length() < 1e-5, "{moved:?}");
// From itself: read before it is written.
- let (d, _) = run(&[("Operation", "Modify"), ("Attribute Name", "mass"), ("Combine", "Add")], "mass");
+ let (d, _) = run(&[("operation", "Modify"), ("attribute_name", "mass"), ("combine", "Add")], "mass");
assert_eq!(get(&d, "mass", 6), vec![12.0]);
// Nothing named, or nothing there: an error, and nothing written.
- let (d, err) = run(&[("Operation", "Create"), ("Attribute Name", "w2"), ("Type", "Float")], "nope");
+ let (d, err) = run(&[("operation", "Create"), ("attribute_name", "w2"), ("type", "Float")], "nope");
assert!(err.is_some_and(|e| e.contains("nope")));
assert!(!d.points().has("w2"));
- let (_, err) = run(&[("Operation", "Create"), ("Attribute Name", "w3")], "");
+ let (_, err) = run(&[("operation", "Create"), ("attribute_name", "w3")], "");
assert!(err.is_some());
// An operation that has no Value is untouched by the switch.
- let (_, err) = run(&[("Operation", "Clip"), ("From Min", "0"), ("From Max", "1")], "nope");
+ let (_, err) = run(&[("operation", "Clip"), ("from_min", "0"), ("from_max", "1")], "nope");
assert!(err.is_none(), "{err:?}");
}
@@ -7485,18 +7491,18 @@ mod simnet_tests {
// Unlike Remap, Normalize needs no knowledge of the values — which is
// what lets it sit in a solve whose range moves every frame.
- let (by_max, err) = run_attr(&before, &[("Operation", "Normalize"), ("Target", "Maximum"), ("To Max", "1.00")]);
+ let (by_max, err) = run_attr(&before, &[("operation", "Normalize"), ("target", "Maximum"), ("to_max", "1.00")]);
assert!(err.is_none(), "{err:?}");
assert!((mass(&by_max, n - 1) - 1.0).abs() < 1e-5);
- let (by_sum, _) = run_attr(&before, &[("Operation", "Normalize"), ("Target", "Sum"), ("To Max", "1.00")]);
+ let (by_sum, _) = run_attr(&before, &[("operation", "Normalize"), ("target", "Sum"), ("to_max", "1.00")]);
assert!((sum(&by_sum) - 1.0).abs() < 1e-4, "sum is {}", sum(&by_sum));
// An all-zero attribute has no scale to hit, and says so instead of
// filling the geometry with infinities.
let mut flat = before.clone();
flat.points_mut().create("mass", AttribValue::Float(0.0));
- let (_, err) = run_attr(&flat, &[("Operation", "Normalize")]);
+ let (_, err) = run_attr(&flat, &[("operation", "Normalize")]);
assert!(err.is_some(), "normalizing nothing must be reported");
}
@@ -7506,10 +7512,10 @@ mod simnet_tests {
before.points_mut().create("other", AttribValue::Float(2.0));
let mass = |d: &Detail, p: usize| d.points().value("mass", p).unwrap().as_f32();
- for (op, want) in [("Multiply", 6.0), ("Add", 5.0), ("Subtract", 1.0), ("Maximum", 3.0)] {
+ for (op, want) in [("Multiply", 6.0), ("Add", 5.0), ("Subtract", 1.0), ("maximum", 3.0)] {
let (g, err) = run_attr(
&before,
- &[("Operation", "Composite"), ("Source B", "other"), ("Combine Op", op)],
+ &[("operation", "Composite"), ("source_b", "other"), ("combine_op", op)],
);
assert!(err.is_none(), "{op}: {err:?}");
assert_eq!(mass(&g, 3), want, "{op}");
@@ -7521,13 +7527,13 @@ mod simnet_tests {
zeroed.points_mut().create("other", AttribValue::Float(0.0));
let (g, _) = run_attr(
&zeroed,
- &[("Operation", "Composite"), ("Source B", "other"), ("Combine Op", "Divide")],
+ &[("operation", "Composite"), ("source_b", "other"), ("combine_op", "Divide")],
);
assert_eq!(mass(&g, 3), 3.0);
let (_, err) = run_attr(
&before,
- &[("Operation", "Composite"), ("Source B", "nope"), ("Combine Op", "Add")],
+ &[("operation", "Composite"), ("source_b", "nope"), ("combine_op", "Add")],
);
assert!(err.is_some(), "a missing Source B must be reported");
}
@@ -7550,7 +7556,7 @@ mod simnet_tests {
let with = |b: &str, op: &str| {
let (g, err) = run_attr(
&before,
- &[("Attribute Name", "v"), ("Operation", "Composite"), ("Source B", b), ("Combine Op", op)],
+ &[("attribute_name", "v"), ("operation", "Composite"), ("source_b", b), ("combine_op", op)],
);
assert!(err.is_none(), "{op} with {b}: {err:?}");
v(&g)
@@ -7561,7 +7567,7 @@ mod simnet_tests {
("Subtract", [0.5, 1.5, 2.5]),
("Divide", [2.0, 4.0, 6.0]),
("Minimum", [0.5, 0.5, 0.5]),
- ("Maximum", [1.0, 2.0, 3.0]),
+ ("maximum", [1.0, 2.0, 3.0]),
("Average", [0.75, 1.25, 1.75]),
("Difference", [0.5, 1.5, 2.5]),
] {
@@ -7579,7 +7585,7 @@ mod simnet_tests {
// which differs from point to point, times the same vector.
let (g, err) = run_attr(
&before,
- &[("Attribute Name", "v"), ("Operation", "Composite"), ("Source B", "mass"), ("Combine Op", "Multiply")],
+ &[("attribute_name", "v"), ("operation", "Composite"), ("source_b", "mass"), ("combine_op", "Multiply")],
);
assert!(err.is_none(), "{err:?}");
for p in [0, 3, g.num_points() - 1] {
@@ -7598,7 +7604,7 @@ mod simnet_tests {
"id-an",
"An",
"analysis",
- vec![param("Input", "In"), param("Source", "Attribute"), param("Attribute", "mass")],
+ vec![param("input", "In"), param("source", "attribute"), param("attribute", "mass")],
vec![],
);
let mut err = None;
@@ -7623,7 +7629,7 @@ mod simnet_tests {
"id-an",
"An",
"analysis",
- vec![param("Input", "In"), param("Source", "Edge Lengths")],
+ vec![param("input", "In"), param("source", "Edge Lengths")],
vec![],
);
apply_analysis(&mut edges, &an, &mut None);
@@ -7639,7 +7645,7 @@ mod simnet_tests {
"id-an",
"An",
"analysis",
- vec![param("Input", "In"), param("Attribute", "nope")],
+ vec![param("input", "In"), param("attribute", "nope")],
vec![],
);
let mut err = None;
@@ -7658,7 +7664,7 @@ mod simnet_tests {
apply_analysis(
&mut geom,
- &node("a", "A", "analysis", vec![param("Attribute", "mass")], vec![]),
+ &node("a", "A", "analysis", vec![param("attribute", "mass")], vec![]),
&mut None,
);
let measured_max = geom.detail().value("mass_max", 0).unwrap().as_f32();
@@ -7666,7 +7672,7 @@ mod simnet_tests {
let (geom, err) = run_attr(
&geom,
- &[("Operation", "Promote"), ("Attribute Name", "mass_max"), ("To Class", "Point")],
+ &[("operation", "Promote"), ("attribute_name", "mass_max"), ("to_class", "Point")],
);
assert!(err.is_none(), "{err:?}");
assert_eq!(
@@ -7677,7 +7683,7 @@ mod simnet_tests {
let (geom, err) = run_attr(
&geom,
- &[("Operation", "Composite"), ("Source B", "mass_max"), ("Combine Op", "Divide")],
+ &[("operation", "Composite"), ("source_b", "mass_max"), ("combine_op", "Divide")],
);
assert!(err.is_none(), "{err:?}");
let mass = |p: usize| geom.points().value("mass", p).unwrap().as_f32();
@@ -7693,12 +7699,12 @@ mod simnet_tests {
("Average", (0..n).map(|p| p as f32).sum::<f32>() / n as f32),
("Sum", (0..n).map(|p| p as f32).sum::<f32>()),
("Minimum", 0.0),
- ("Maximum", (n - 1) as f32),
+ ("maximum", (n - 1) as f32),
("First", 0.0),
] {
let (g, err) = run_attr(
&before,
- &[("Operation", "Promote"), ("To Class", "Detail"), ("Method", method)],
+ &[("operation", "Promote"), ("to_class", "Detail"), ("method", method)],
);
assert!(err.is_none(), "{method}: {err:?}");
let got = g.detail().value("mass", 0).unwrap().as_f32();
@@ -7714,7 +7720,7 @@ mod simnet_tests {
"id-t",
"T",
"time",
- vec![param("Attribute", "t"), param("Start Frame", "1"), param("End Frame", "11")],
+ vec![param("attribute", "t"), param("start_frame", "1"), param("end_frame", "11")],
vec![],
);
let t_at = |frame: i32| {
@@ -7735,7 +7741,7 @@ mod simnet_tests {
"id-t",
"T",
"time",
- vec![param("Attribute", "t"), param("Start Frame", "5"), param("End Frame", "5")],
+ vec![param("attribute", "t"), param("start_frame", "5"), param("end_frame", "5")],
vec![],
);
let mut g = before.clone();
@@ -7747,7 +7753,7 @@ mod simnet_tests {
fn run_vis(before: &Detail, params: &[(&str, &str)]) -> (Detail, Option<String>) {
let mut geom = before.clone();
- let mut ps = vec![param("Input", "In"), param("Attribute", "mass")];
+ let mut ps = vec![param("input", "In"), param("attribute", "mass")];
for (k, v) in params {
match ps.iter_mut().find(|p| p.name == *k) {
Some(p) => p.set_text(v.to_string()),
@@ -7788,7 +7794,7 @@ mod simnet_tests {
before.points_mut().set_value("mass", p, AttribValue::Float(p as f32)).unwrap();
}
- let (g, err) = run_vis(&before, &[("Ramp", "Grayscale"), ("Range", "Auto")]);
+ let (g, err) = run_vis(&before, &[("ramp", "Grayscale"), ("range", "Auto")]);
assert!(err.is_none(), "{err:?}");
// The measured range spreads across the whole ramp regardless of what
// the numbers happen to be — which is the setting a simulation wants,
@@ -7802,7 +7808,7 @@ mod simnet_tests {
for p in 0..n {
scaled.points_mut().set_value("mass", p, AttribValue::Float(p as f32 * 10.0)).unwrap();
}
- let (h, _) = run_vis(&scaled, &[("Ramp", "Grayscale"), ("Range", "Auto")]);
+ let (h, _) = run_vis(&scaled, &[("ramp", "Grayscale"), ("range", "Auto")]);
for p in 0..n {
assert_eq!(g.color(p), h.color(p), "point {p}");
}
@@ -7811,7 +7817,7 @@ mod simnet_tests {
// the honest picture of "nothing varies here", not a division by zero.
let mut flat = before.clone();
flat.points_mut().create("mass", AttribValue::Float(3.0));
- let (f, err) = run_vis(&flat, &[("Ramp", "Grayscale"), ("Range", "Auto")]);
+ let (f, err) = run_vis(&flat, &[("ramp", "Grayscale"), ("range", "Auto")]);
assert!(err.is_none(), "{err:?}");
assert_eq!(f.color(0), [0.0; 3]);
}
@@ -7830,25 +7836,25 @@ mod simnet_tests {
// attributes read at once come from STACKING nodes, not from one node
// growing a list of layers — so any one of them can be bypassed to see
// what it was contributing.
- let (base, _) = run_vis(&before, &[("Ramp", "Grayscale"), ("Blend", "Set")]);
+ let (base, _) = run_vis(&before, &[("ramp", "Grayscale"), ("blend", "Set")]);
assert_eq!(base.color(0), [0.0; 3], "a flat attribute floors the ramp");
- let (over, _) = run_vis(&base, &[("Attribute", "heat"), ("Ramp", "Grayscale"), ("Blend", "Set"), ("Opacity", "0.50")]);
+ let (over, _) = run_vis(&base, &[("attribute", "heat"), ("ramp", "Grayscale"), ("blend", "Set"), ("opacity", "0.50")]);
// Half-strength Set is a half-way mix with what was already there.
assert!((over.color(n - 1)[0] - 0.5).abs() < 1e-5, "{:?}", over.color(n - 1));
// Opacity 0 changes nothing at all, whatever the blend.
for blend in ["Set", "Multiply", "Add"] {
- let (none, _) = run_vis(&base, &[("Attribute", "heat"), ("Blend", blend), ("Opacity", "0.00")]);
+ let (none, _) = run_vis(&base, &[("attribute", "heat"), ("blend", blend), ("opacity", "0.00")]);
for p in 0..n {
assert_eq!(none.color(p), base.color(p), "{blend} at zero opacity, point {p}");
}
}
// Multiply darkens toward the ramp, Add brightens away from it.
- let mid = run_vis(&before, &[("Ramp", "Grayscale"), ("Range", "Manual"), ("From", "0.00"), ("To", "2.00")]).0;
- let (mul, _) = run_vis(&mid, &[("Attribute", "heat"), ("Ramp", "Grayscale"), ("Blend", "Multiply")]);
- let (add, _) = run_vis(&mid, &[("Attribute", "heat"), ("Ramp", "Grayscale"), ("Blend", "Add")]);
+ let mid = run_vis(&before, &[("ramp", "Grayscale"), ("range", "Manual"), ("from", "0.00"), ("to", "2.00")]).0;
+ let (mul, _) = run_vis(&mid, &[("attribute", "heat"), ("ramp", "Grayscale"), ("blend", "Multiply")]);
+ let (add, _) = run_vis(&mid, &[("attribute", "heat"), ("ramp", "Grayscale"), ("blend", "Add")]);
assert!(mul.color(0)[0] <= mid.color(0)[0] + 1e-6);
assert!(add.color(n - 1)[0] >= mid.color(n - 1)[0] - 1e-6);
}
@@ -7858,7 +7864,7 @@ mod simnet_tests {
let mut before = sphere_detail(Vec3::ZERO, 0.5, 4, 6);
before.points_mut().create("vel", AttribValue::Float3([0.0, 4.0, 0.0]));
- let (g, err) = run_vis(&before, &[("Attribute", "vel"), ("Mode", "Vector"), ("Scale", "0.25")]);
+ let (g, err) = run_vis(&before, &[("attribute", "vel"), ("mode", "Vector"), ("scale", "0.25")]);
assert!(err.is_none(), "{err:?}");
let vis = format!("{}vel", crate::detail::VIS_PREFIX);
assert!(g.points().has(&vis), "the marker request rides the geometry");
@@ -7873,7 +7879,7 @@ mod simnet_tests {
let mut grouped = before.clone();
grouped.points_mut().create_group("some");
grouped.points_mut().add_to_group("some", 2);
- let (h, _) = run_vis(&grouped, &[("Attribute", "vel"), ("Mode", "Vector"), ("Group", "some")]);
+ let (h, _) = run_vis(&grouped, &[("attribute", "vel"), ("mode", "Vector"), ("group", "some")]);
assert_ne!(h.points().value(&vis, 2), Some(AttribValue::Float3([0.0; 3])));
assert_eq!(h.points().value(&vis, 0), Some(AttribValue::Float3([0.0; 3])));
}
@@ -7883,7 +7889,7 @@ mod simnet_tests {
let mut d = sphere_detail(Vec3::ZERO, 0.5, 4, 6);
d.points_mut().create("vel", AttribValue::Float3([0.0, 1.0, 0.0]));
// Only two points get a marker; the rest stage a zero vector.
- let (mut g, _) = run_vis(&d, &[("Attribute", "vel"), ("Mode", "Vector"), ("Scale", "0.50")]);
+ let (mut g, _) = run_vis(&d, &[("attribute", "vel"), ("mode", "Vector"), ("scale", "0.50")]);
let vis = format!("{}vel", crate::detail::VIS_PREFIX);
for p in 2..g.num_points() {
g.points_mut().set_value(&vis, p, AttribValue::Float3([0.0; 3])).unwrap();
@@ -7915,8 +7921,8 @@ mod simnet_tests {
// Spheres sit on the resolver's own 1.25-spaced layout, so a radius of
// 0.8 makes them overlap by 0.35 — a lens neither operation can
// mistake for the other.
- let a = node("id-a", "sphere1", "sphere", vec![param("Radius", "0.8")], vec![]);
- let b = node("id-b", "sphere2", "sphere", vec![param("Radius", "0.8")], vec![]);
+ let a = node("id-a", "sphere1", "sphere", vec![param("radius", "0.8")], vec![]);
+ let b = node("id-b", "sphere2", "sphere", vec![param("radius", "0.8")], vec![]);
for (op, expect) in [("Union", "wider"), ("Intersect", "narrower"), ("Subtract", "narrower")] {
let bool_node = node(
@@ -7924,10 +7930,10 @@ mod simnet_tests {
"bool1",
"boolean",
vec![
- param("Input", "sphere1"),
- param("With", "sphere2"),
- param("Operation", op),
- param("Voxel Size", "0.08"),
+ param("input", "sphere1"),
+ param("with", "sphere2"),
+ param("operation", op),
+ param("voxel_size", "0.08"),
],
vec![],
);
@@ -7971,17 +7977,17 @@ mod simnet_tests {
/// The Volume node's two modes, likewise through the resolver.
#[test]
fn test_the_volume_node_offsets_and_shells() {
- let sphere = node("id-s", "sphere1", "sphere", vec![param("Radius", "0.8")], vec![]);
+ let sphere = node("id-s", "sphere1", "sphere", vec![param("radius", "0.8")], vec![]);
let grown = node(
"id-v",
"vol1",
"volume",
vec![
- param("Input", "sphere1"),
- param("Mode", "Offset"),
- param("Voxel Size", "0.08"),
- param("Offset", "0.2"),
+ param("input", "sphere1"),
+ param("mode", "offset"),
+ param("voxel_size", "0.08"),
+ param("offset", "0.2"),
],
vec![],
);
@@ -8013,11 +8019,11 @@ mod simnet_tests {
"vol2",
"volume",
vec![
- param("Input", "sphere1"),
- param("Mode", "Shell"),
- param("Voxel Size", "0.08"),
- param("Offset", "0.0"),
- param("Thickness", "0.15"),
+ param("input", "sphere1"),
+ param("mode", "Shell"),
+ param("voxel_size", "0.08"),
+ param("offset", "0.0"),
+ param("thickness", "0.15"),
],
vec![],
);
@@ -8051,7 +8057,7 @@ mod simnet_tests {
#[test]
fn test_visualize_reports_a_missing_attribute_and_leaves_colour_alone() {
let before = sphere_detail(Vec3::ZERO, 0.5, 4, 6);
- let (g, err) = run_vis(&before, &[("Attribute", "nope")]);
+ let (g, err) = run_vis(&before, &[("attribute", "nope")]);
assert!(err.as_deref().unwrap_or("").contains("nope"), "{err:?}");
for p in 0..before.num_points() {
assert_eq!(g.color(p), before.color(p), "point {p}");
@@ -8061,24 +8067,24 @@ mod simnet_tests {
/// A sphere, one point given a spike of `mass`, then a Neighbour node.
/// Returns (before, after) so a test can compare the two directly.
fn neighbour_chain(extra: &[(&str, &str)]) -> (Detail, Detail) {
- let sphere = node("id-sphere", "Sphere 1", "sphere", vec![param("Radius", "0.5")], vec![]);
+ let sphere = node("id-sphere", "Sphere 1", "sphere", vec![param("radius", "0.5")], vec![]);
let seed = node(
"id-seed",
"Seed 1",
"attribute",
vec![
- param("Input", "Sphere 1"),
- param("Operation", "Create"),
- param("Attribute Name", "mass"),
- param("Type", "Float"),
- param("Value", "0.00"),
+ param("input", "Sphere 1"),
+ param("operation", "Create"),
+ param("attribute_name", "mass"),
+ param("type", "Float"),
+ param("value", "0.00"),
],
vec![],
);
let mut params = vec![
- param("Input", "Seed 1"),
- param("Attribute", "mass"),
- param("Amount", "0.50"),
+ param("input", "Seed 1"),
+ param("attribute", "mass"),
+ param("amount", "0.50"),
];
for (k, v) in extra {
match params.iter_mut().find(|p| p.name == *k) {
@@ -8109,7 +8115,7 @@ mod simnet_tests {
/// bypassing the graph so the spike survives.
fn run_neighbour(before: &Detail, params: &[(&str, &str)]) -> Detail {
let mut geom = before.clone();
- let mut params_vec = vec![param("Input", "In"), param("Attribute", "mass")];
+ let mut params_vec = vec![param("input", "In"), param("attribute", "mass")];
for (k, v) in params {
match params_vec.iter_mut().find(|p| p.name == *k) {
Some(p) => p.set_text(v.to_string()),
@@ -8127,7 +8133,7 @@ mod simnet_tests {
let spike_nbrs: Vec<u32> = before.point_neighbours(0).to_vec();
assert!(!spike_nbrs.is_empty());
- let after = run_neighbour(&before, &[("Mode", "Diffuse"), ("Amount", "0.50")]);
+ let after = run_neighbour(&before, &[("mode", "Diffuse"), ("amount", "0.50")]);
let mass = |d: &Detail, p: usize| d.points().value("mass", p).unwrap().as_f32();
// The spike falls toward its neighbours' average (zero) by Amount, and
@@ -8146,8 +8152,8 @@ mod simnet_tests {
#[test]
fn test_neighbour_concentrate_is_diffuse_with_the_sign_flipped() {
let (before, _) = neighbour_chain(&[]);
- let diffused = run_neighbour(&before, &[("Mode", "Diffuse"), ("Amount", "0.40")]);
- let sharpened = run_neighbour(&before, &[("Mode", "Concentrate"), ("Amount", "0.40")]);
+ let diffused = run_neighbour(&before, &[("mode", "Diffuse"), ("amount", "0.40")]);
+ let sharpened = run_neighbour(&before, &[("mode", "Concentrate"), ("amount", "0.40")]);
let mass = |d: &Detail, p: usize| d.points().value("mass", p).unwrap().as_f32();
// Same distance from the starting value, opposite directions.
@@ -8175,7 +8181,7 @@ mod simnet_tests {
let after = run_neighbour(
&before,
- &[("Mode", "Migrate"), ("Direction", "dir"), ("Amount", "0.50")],
+ &[("mode", "Migrate"), ("direction", "dir"), ("amount", "0.50")],
);
// The sender loses exactly what the receivers gain — that is what makes
@@ -8199,13 +8205,13 @@ mod simnet_tests {
#[test]
fn test_neighbour_bleed_decays_toward_zero_and_ignores_the_hood() {
let (before, _) = neighbour_chain(&[]);
- let after = run_neighbour(&before, &[("Mode", "Bleed"), ("Amount", "0.25")]);
+ let after = run_neighbour(&before, &[("mode", "Bleed"), ("amount", "0.25")]);
let mass = |d: &Detail, p: usize| d.points().value("mass", p).unwrap().as_f32();
assert!((mass(&after, 0) - 7.5).abs() < 1e-4);
// Applying it repeatedly approaches zero without crossing it.
let mut g = before.clone();
for _ in 0..40 {
- g = run_neighbour(&g, &[("Mode", "Bleed"), ("Amount", "0.25")]);
+ g = run_neighbour(&g, &[("mode", "Bleed"), ("amount", "0.25")]);
}
assert!(mass(&g, 0) > 0.0 && mass(&g, 0) < 1e-3, "{}", mass(&g, 0));
}
@@ -8216,12 +8222,12 @@ mod simnet_tests {
let mass = |d: &Detail, p: usize| d.points().value("mass", p).unwrap().as_f32();
let touched = |d: &Detail| (0..d.num_points()).filter(|&p| mass(d, p) != 0.0).count();
- let one = run_neighbour(&before, &[("Mode", "Diffuse"), ("Neighbourhood", "Connectivity"), ("Rings", "1")]);
- let two = run_neighbour(&before, &[("Mode", "Diffuse"), ("Neighbourhood", "Connectivity"), ("Rings", "2")]);
+ let one = run_neighbour(&before, &[("mode", "Diffuse"), ("neighbourhood", "Connectivity"), ("rings", "1")]);
+ let two = run_neighbour(&before, &[("mode", "Diffuse"), ("neighbourhood", "Connectivity"), ("rings", "2")]);
assert!(touched(&two) > touched(&one), "a second ring must reach further");
// Global reaches everything: every point but the spike rises off zero.
- let global = run_neighbour(&before, &[("Mode", "Diffuse"), ("Neighbourhood", "Global"), ("Amount", "1.00")]);
+ let global = run_neighbour(&before, &[("mode", "Diffuse"), ("neighbourhood", "Global"), ("amount", "1.00")]);
assert_eq!(touched(&global), before.num_points() - 1);
// The spike lands on exactly zero, because a point is not its own
// neighbour and every OTHER point holds zero.
@@ -8230,11 +8236,11 @@ mod simnet_tests {
// Radius ignores connectivity, and the rules are continuous with each
// other: a radius wide enough to swallow the sphere IS Global. That
// only holds because neither includes the point itself.
- let wide = run_neighbour(&before, &[("Mode", "Diffuse"), ("Neighbourhood", "Radius"), ("Radius", "5.00"), ("Amount", "1.00")]);
+ let wide = run_neighbour(&before, &[("mode", "Diffuse"), ("neighbourhood", "Radius"), ("radius", "5.00"), ("amount", "1.00")]);
for p in 0..before.num_points() {
assert!((mass(&wide, p) - mass(&global, p)).abs() < 1e-6, "point {p}");
}
- let none = run_neighbour(&before, &[("Mode", "Diffuse"), ("Neighbourhood", "Radius"), ("Radius", "0.00")]);
+ let none = run_neighbour(&before, &[("mode", "Diffuse"), ("neighbourhood", "Radius"), ("radius", "0.00")]);
assert_eq!(touched(&none), 1, "no neighbours means no change");
}
@@ -8246,7 +8252,7 @@ mod simnet_tests {
before.points_mut().create_group("inner");
before.points_mut().add_to_group("inner", q);
- let after = run_neighbour(&before, &[("Mode", "Diffuse"), ("Group", "inner"), ("Amount", "1.00")]);
+ let after = run_neighbour(&before, &[("mode", "Diffuse"), ("group", "inner"), ("amount", "1.00")]);
let mass = |d: &Detail, p: usize| d.points().value("mass", p).unwrap().as_f32();
assert_eq!(mass(&after, 0), 10.0, "a point outside the group is not edited");
@@ -8265,14 +8271,14 @@ mod simnet_tests {
g.points_mut().create("count", AttribValue::Int(0));
g.points_mut().set_value("count", 0, AttribValue::Int(10)).unwrap();
- let v = run_neighbour(&g, &[("Attribute", "vel"), ("Mode", "Bleed"), ("Amount", "0.50")]);
+ let v = run_neighbour(&g, &[("attribute", "vel"), ("mode", "Bleed"), ("amount", "0.50")]);
assert_eq!(
v.points().value("vel", 0),
Some(AttribValue::Float3([1.5, 3.0, 4.5])),
"every component decays alike"
);
- let c = run_neighbour(&g, &[("Attribute", "count"), ("Mode", "Bleed"), ("Amount", "0.25")]);
+ let c = run_neighbour(&g, &[("attribute", "count"), ("mode", "Bleed"), ("amount", "0.25")]);
// An integer count stays an integer: 10 * 0.75 = 7.5 rounds rather
// than silently becoming a float nobody can index with.
assert!(matches!(c.points().value("count", 0), Some(AttribValue::Int(_))));
@@ -8301,7 +8307,7 @@ mod simnet_tests {
// average turns it around.
let after = run_neighbour(
&before,
- &[("Attribute", "vel"), ("Mode", "Align"), ("Target", "Local Average"), ("Amount", "1.00")],
+ &[("attribute", "vel"), ("mode", "Align"), ("target", "Local Average"), ("amount", "1.00")],
);
assert!(vel(&after, 0).y > 0.0, "the odd one out did not turn: {:?}", vel(&after, 0));
// Steering is a statement about heading only: a vector attribute
@@ -8321,13 +8327,13 @@ mod simnet_tests {
fn test_align_targets_are_five_different_answers_to_agree_with_what() {
let before = vectored();
let run = |extra: &[(&str, &str)]| {
- let mut params = vec![("Attribute", "vel"), ("Mode", "Align"), ("Amount", "1.00")];
+ let mut params = vec![("attribute", "vel"), ("mode", "Align"), ("amount", "1.00")];
params.extend_from_slice(extra);
run_neighbour(&before, ¶ms)
};
// Constant: everyone ends up pointing the same way.
- let c = run(&[("Target", "Constant"), ("Constant", "1.00:0.00:0.00")]);
+ let c = run(&[("target", "Constant"), ("constant", "1.00:0.00:0.00")]);
for p in 0..before.num_points() {
assert!((vel(&c, p).normalize() - Vec3::X).length() < 1e-4, "point {p}");
}
@@ -8337,13 +8343,13 @@ mod simnet_tests {
with_goal.points_mut().create("goal", AttribValue::Float3([0.0, 0.0, 1.0]));
let a = run_neighbour(
&with_goal,
- &[("Attribute", "vel"), ("Mode", "Align"), ("Amount", "1.00"), ("Target", "Attribute"), ("Source", "goal")],
+ &[("attribute", "vel"), ("mode", "Align"), ("amount", "1.00"), ("target", "Attribute"), ("source", "goal")],
);
assert!((vel(&a, 5).normalize() - Vec3::Z).length() < 1e-4);
// Surface tangent: the result lies in the surface, so it is
// perpendicular to the point's normal.
- let t = run(&[("Target", "Surface Tangent")]);
+ let t = run(&[("target", "Surface Tangent")]);
let normals = point_normals(&before);
let mut turned = 0usize;
let mut left_alone = 0usize;
@@ -8374,7 +8380,7 @@ mod simnet_tests {
// Global average excludes the point itself, exactly as the
// neighbourhoods do — otherwise the dissenter would average partly
// with itself and could never be turned all the way.
- let g = run(&[("Target", "Global Average")]);
+ let g = run(&[("target", "Global Average")]);
assert!(vel(&g, 0).y > 0.0);
}
@@ -8386,7 +8392,7 @@ mod simnet_tests {
// annihilate it.
let after = run_neighbour(
&before,
- &[("Attribute", "vel"), ("Mode", "Align"), ("Target", "Constant"), ("Constant", "0.00:-1.00:0.00"), ("Amount", "0.50")],
+ &[("attribute", "vel"), ("mode", "Align"), ("target", "Constant"), ("constant", "0.00:-1.00:0.00"), ("amount", "0.50")],
);
assert!((vel(&after, 5).length() - 2.0).abs() < 1e-4, "{:?}", vel(&after, 5));
assert_ne!(vel(&after, 5), Vec3::ZERO);
@@ -8398,7 +8404,7 @@ mod simnet_tests {
"id-n",
"N",
"neighbour",
- vec![param("Attribute", "mass"), param("Mode", "Align")],
+ vec![param("attribute", "mass"), param("mode", "Align")],
vec![],
);
g.points_mut().create("mass", AttribValue::Float(1.0));
@@ -8414,7 +8420,7 @@ mod simnet_tests {
let after = run_neighbour(
&before,
- &[("Attribute", "vel"), ("Mode", "Lead"), ("Amount", "1.00")],
+ &[("attribute", "vel"), ("mode", "Lead"), ("amount", "1.00")],
);
// The dissenter's neighbours each see one vector that disagrees with
// them — point 0's — so they turn onto it. That is the mechanism:
@@ -8435,7 +8441,7 @@ mod simnet_tests {
weighted.points_mut().set_value("clout", 0, AttribValue::Float(0.0)).unwrap();
let quiet = run_neighbour(
&weighted,
- &[("Attribute", "vel"), ("Mode", "Lead"), ("Amount", "1.00"), ("Source", "clout")],
+ &[("attribute", "vel"), ("mode", "Lead"), ("amount", "1.00"), ("source", "clout")],
);
for &q in &spread {
assert!(vel(&quiet, q as usize).y > 0.0, "a silenced dissenter still led {q}");
@@ -8451,7 +8457,7 @@ mod simnet_tests {
let total = |d: &Detail| (0..n).map(|p| mass(d, p)).sum::<f32>();
// Below the threshold it simply fills.
- let charged = run_neighbour(&before, &[("Mode", "Charge"), ("Amount", "0.30"), ("Release", "1.00")]);
+ let charged = run_neighbour(&before, &[("mode", "Charge"), ("amount", "0.30"), ("release", "1.00")]);
assert!((mass(&charged, 0) - 0.3).abs() < 1e-5);
assert!((total(&charged) - 0.3 * n as f32).abs() < 1e-3);
@@ -8462,7 +8468,7 @@ mod simnet_tests {
for p in 0..n {
primed.points_mut().set_value("mass", p, AttribValue::Float(0.9)).unwrap();
}
- let fired = run_neighbour(&primed, &[("Mode", "Charge"), ("Amount", "0.20"), ("Release", "1.00")]);
+ let fired = run_neighbour(&primed, &[("mode", "Charge"), ("amount", "0.20"), ("release", "1.00")]);
let expected_after_fill = total(&primed) + 0.2 * n as f32;
assert!(
(total(&fired) - expected_after_fill).abs() < 1e-2,
@@ -8501,7 +8507,7 @@ mod simnet_tests {
before.points_mut().set_value("mass", 0, AttribValue::Float(5.0)).unwrap();
let mass = |d: &Detail, p: usize| d.points().value("mass", p).unwrap().as_f32();
- let after = run_neighbour(&before, &[("Mode", "Charge"), ("Amount", "0.00"), ("Release", "1.00")]);
+ let after = run_neighbour(&before, &[("mode", "Charge"), ("amount", "0.00"), ("release", "1.00")]);
assert_eq!(mass(&after, 0), 0.0, "the primed point emptied");
let nbrs = before.point_neighbours(0).to_vec();
let share = 5.0 / nbrs.len() as f32;
@@ -8521,7 +8527,7 @@ mod simnet_tests {
"id-n",
"N",
"neighbour",
- vec![param("Input", "In"), param("Attribute", "nope")],
+ vec![param("input", "In"), param("attribute", "nope")],
vec![],
);
apply_neighbour(&mut geom, &nbr, &mut err);
@@ -8535,8 +8541,8 @@ mod simnet_tests {
/// subnet, only its own children do.
#[test]
fn test_scene_walk_scoped_to_start_level() {
- let outer = node("id-outer", "Sphere 1", "sphere", vec![param("Radius", "0.5")], vec![]);
- let inner = node("id-inner", "Sphere 2", "sphere", vec![param("Radius", "0.5")], vec![]);
+ let outer = node("id-outer", "Sphere 1", "sphere", vec![param("radius", "0.5")], vec![]);
+ let inner = node("id-inner", "Sphere 2", "sphere", vec![param("radius", "0.5")], vec![]);
let sub = node("id-sub", "Sub 1", "node", vec![], vec![inner]);
let root = node("id-root", "root", "node", vec![], vec![outer, sub]);
@@ -8559,21 +8565,21 @@ mod simnet_tests {
/// the same offset, so the solved position reads back the step COUNT. Uses
/// transform, not an OpenCL node, so the test is pure CPU.
fn stepping_graph() -> FsNode {
- let sphere = node("id-sphere", "Sphere 1", "sphere", vec![param("Radius", "0.5")], vec![]);
+ let sphere = node("id-sphere", "Sphere 1", "sphere", vec![param("radius", "0.5")], vec![]);
let inner_input = node("id-in", "input1", "input", vec![], vec![]);
let step = node(
"id-step",
"step1",
"transform",
- vec![param("Input", "input1"), param("Translation", "1.00:0.00:0.00")],
+ vec![param("input", "input1"), param("translation", "1.00:0.00:0.00")],
vec![],
);
- let inner_output = node("id-out", "output1", "output", vec![param("Input", "step1")], vec![]);
+ let inner_output = node("id-out", "output1", "output", vec![param("input", "step1")], vec![]);
let sim = node(
"id-sim",
"Simnet 1",
"simnet",
- vec![param("Input", "Sphere 1")],
+ vec![param("input", "Sphere 1")],
vec![inner_input, step, inner_output],
);
node("id-root", "root", "node", vec![], vec![sphere, sim])
@@ -8596,18 +8602,18 @@ mod simnet_tests {
/// A sim whose step adds 1 to `acc` every frame. The seed declares `acc`
/// with the given kind, which is the only difference between the two runs.
fn accumulating_graph(kind: &str) -> FsNode {
- let sphere = node("id-sphere", "Sphere 1", "sphere", vec![param("Radius", "0.5")], vec![]);
+ let sphere = node("id-sphere", "Sphere 1", "sphere", vec![param("radius", "0.5")], vec![]);
let seed = node(
"id-seed",
"Seed 1",
"attribute",
vec![
- param("Input", "Sphere 1"),
- param("Operation", "Create"),
- param("Attribute Name", "acc"),
- param("Type", "Float"),
- param("Value", "0.00"),
- param("Kind", kind),
+ param("input", "Sphere 1"),
+ param("operation", "Create"),
+ param("attribute_name", "acc"),
+ param("type", "Float"),
+ param("value", "0.00"),
+ param("kind", kind),
],
vec![],
);
@@ -8617,20 +8623,20 @@ mod simnet_tests {
"step1",
"attribute",
vec![
- param("Input", "input1"),
- param("Operation", "Modify"),
- param("Attribute Name", "acc"),
- param("Combine", "Add"),
- param("Value", "1.00"),
+ param("input", "input1"),
+ param("operation", "Modify"),
+ param("attribute_name", "acc"),
+ param("combine", "Add"),
+ param("value", "1.00"),
],
vec![],
);
- let inner_output = node("id-out", "output1", "output", vec![param("Input", "step1")], vec![]);
+ let inner_output = node("id-out", "output1", "output", vec![param("input", "step1")], vec![]);
let sim = node(
"id-sim",
"Simnet 1",
"simnet",
- vec![param("Input", "Seed 1")],
+ vec![param("input", "Seed 1")],
vec![inner_input, step, inner_output],
);
node("id-root", "root", "node", vec![], vec![sphere, seed, sim])
@@ -8638,17 +8644,17 @@ mod simnet_tests {
/// A sim that adds a fixed 1.0 to `acc` every time the chain runs.
fn substep_graph(substeps: &str) -> FsNode {
- let sphere = node("id-sphere", "Sphere 1", "sphere", vec![param("Radius", "0.5")], vec![]);
+ let sphere = node("id-sphere", "Sphere 1", "sphere", vec![param("radius", "0.5")], vec![]);
let seed = node(
"id-seed",
"Seed 1",
"attribute",
vec![
- param("Input", "Sphere 1"),
- param("Operation", "Create"),
- param("Attribute Name", "acc"),
- param("Type", "Float"),
- param("Value", "0.00"),
+ param("input", "Sphere 1"),
+ param("operation", "Create"),
+ param("attribute_name", "acc"),
+ param("type", "Float"),
+ param("value", "0.00"),
],
vec![],
);
@@ -8658,20 +8664,20 @@ mod simnet_tests {
"step1",
"attribute",
vec![
- param("Input", "input1"),
- param("Operation", "Modify"),
- param("Attribute Name", "acc"),
- param("Combine", "Add"),
- param("Value", "1.00"),
+ param("input", "input1"),
+ param("operation", "Modify"),
+ param("attribute_name", "acc"),
+ param("combine", "Add"),
+ param("value", "1.00"),
],
vec![],
);
- let inner_output = node("id-out", "output1", "output", vec![param("Input", "step1")], vec![]);
+ let inner_output = node("id-out", "output1", "output", vec![param("input", "step1")], vec![]);
let sim = node(
"id-sim",
"Simnet 1",
"simnet",
- vec![param("Input", "Seed 1"), param("Substeps", substeps)],
+ vec![param("input", "Seed 1"), param("substeps", substeps)],
vec![inner_input, step, inner_output],
);
node("id-root", "root", "node", vec![], vec![sphere, seed, sim])
@@ -8688,10 +8694,10 @@ mod simnet_tests {
"promote1",
"attribute",
vec![
- param("Input", "input1"),
- param("Operation", "Promote"),
- param("Attribute Name", "dt"),
- param("To Class", "Point"),
+ param("input", "input1"),
+ param("operation", "Promote"),
+ param("attribute_name", "dt"),
+ param("to_class", "Point"),
],
vec![],
);
@@ -8700,11 +8706,11 @@ mod simnet_tests {
"step1",
"attribute",
vec![
- param("Input", "promote1"),
- param("Operation", "Composite"),
- param("Attribute Name", "acc"),
- param("Source B", "dt"),
- param("Combine Op", "Add"),
+ param("input", "promote1"),
+ param("operation", "Composite"),
+ param("attribute_name", "acc"),
+ param("source_b", "dt"),
+ param("combine_op", "Add"),
],
vec![],
);
@@ -8712,7 +8718,7 @@ mod simnet_tests {
node("id-in", "input1", "input", vec![], vec![]),
promote,
step,
- node("id-out", "output1", "output", vec![param("Input", "step1")], vec![]),
+ node("id-out", "output1", "output", vec![param("input", "step1")], vec![]),
];
root
}
@@ -8722,7 +8728,7 @@ mod simnet_tests {
let with_start = |start: &str| {
let mut root = substep_graph("1");
let sim = root.children.iter_mut().find(|c| c.node_type == "simnet").unwrap();
- sim.params.push(param("Start Frame", start));
+ sim.params.push(param("start_frame", start));
root
};
let acc = |root: &FsNode, frame: i32| -> f32 {
@@ -9055,7 +9061,7 @@ mod simnet_tests {
{
let sim_mut = root.children.iter_mut().find(|c| c.node_type == "simnet").unwrap();
let step = sim_mut.children.iter_mut().find(|c| c.name == "step1").unwrap();
- step.params.iter_mut().find(|p| p.name == "Translation").unwrap().set_text("2.00:0.00:0.00".to_string());
+ step.params.iter_mut().find(|p| p.name == "translation").unwrap().set_text("2.00:0.00:0.00".to_string());
}
let sim_node = root.children.iter().find(|c| c.node_type == "simnet").unwrap().clone();
// Frame 5 is as it was: no step is run for it.
@@ -9119,7 +9125,7 @@ mod simnet_tests {
let with_only = |name: &str| {
let mut root = stepping_graph();
let sim_node = root.children.iter_mut().find(|c| c.node_type == "simnet").unwrap();
- sim_node.children.push(node("id-seed", "seed1", "sphere", vec![param("Radius", "0.25")], vec![]));
+ sim_node.children.push(node("id-seed", "seed1", "sphere", vec![param("radius", "0.25")], vec![]));
for c in &mut sim_node.children {
c.geometry_visible = c.name == name;
}
@@ -9160,10 +9166,10 @@ mod simnet_tests {
/// arms add no second copy to outer views.
#[test]
fn test_scene_walk_draws_passthrough_subnet_interior() {
- let sphere = node("id-sphere", "Sphere 1", "sphere", vec![param("Radius", "0.5")], vec![]);
+ let sphere = node("id-sphere", "Sphere 1", "sphere", vec![param("radius", "0.5")], vec![]);
let inner_input = node("id-in", "input1", "input", vec![], vec![]);
- let inner_output = node("id-out", "output1", "output", vec![param("Input", "input1")], vec![]);
- let sub = node("id-sub", "Subnet 1", "node", vec![param("Input", "Sphere 1")],
+ let inner_output = node("id-out", "output1", "output", vec![param("input", "input1")], vec![]);
+ let sub = node("id-sub", "Subnet 1", "node", vec![param("input", "Sphere 1")],
vec![inner_input, inner_output]);
let root = node("id-root", "root", "node", vec![], vec![sphere, sub]);
@@ -9198,18 +9204,18 @@ mod simnet_tests {
#[test]
fn test_group_random_point_is_welded_and_deterministic() {
let make_root = |seed: &str| {
- let sphere = node("id-sphere", "Sphere 1", "sphere", vec![param("Radius", "0.5")], vec![]);
+ let sphere = node("id-sphere", "Sphere 1", "sphere", vec![param("radius", "0.5")], vec![]);
let group = node(
"id-group",
"Group 1",
"group",
vec![
- param("Input", "Sphere 1"),
- param("Group Name", "pull"),
- param("Mode", "Random"),
- param("Count", "1"),
- param("Seed", seed),
- param("Highlight", "false"),
+ param("input", "Sphere 1"),
+ param("group_name", "pull"),
+ param("mode", "Random"),
+ param("count", "1"),
+ param("seed", seed),
+ param("highlight", "false"),
],
vec![],
);
@@ -9241,18 +9247,18 @@ mod simnet_tests {
#[test]
fn test_collision_inside_marks_enclosed_points() {
let build = |collider: &str, method: &str| {
- let s1 = node("id-s1", "Sphere 1", "sphere", vec![param("Radius", "0.7")], vec![]);
- let s2 = node("id-s2", "Sphere 2", "sphere", vec![param("Radius", "0.7")], vec![]);
+ let s1 = node("id-s1", "Sphere 1", "sphere", vec![param("radius", "0.7")], vec![]);
+ let s2 = node("id-s2", "Sphere 2", "sphere", vec![param("radius", "0.7")], vec![]);
let col = node(
"id-col",
"Collision 1",
"collision",
vec![
- param("Input", "Sphere 1"),
- param("Collider", collider),
- param("Method", method),
- param("Group Name", "collisions"),
- param("Highlight", "false"),
+ param("input", "Sphere 1"),
+ param("collider", collider),
+ param("method", method),
+ param("group_name", "collisions"),
+ param("highlight", "false"),
],
vec![],
);
@@ -9317,18 +9323,18 @@ mod simnet_tests {
/// point keeps its pulled position, neighbors follow part of the way.
#[test]
fn test_relax_spreads_a_pinned_pull() {
- let sphere = node("id-sphere", "Sphere 1", "sphere", vec![param("Radius", "0.5")], vec![]);
+ let sphere = node("id-sphere", "Sphere 1", "sphere", vec![param("radius", "0.5")], vec![]);
let group = node(
"id-group",
"Group 1",
"group",
vec![
- param("Input", "Sphere 1"),
- param("Group Name", "pull"),
- param("Mode", "Random"),
- param("Count", "1"),
- param("Seed", "3"),
- param("Highlight", "false"),
+ param("input", "Sphere 1"),
+ param("group_name", "pull"),
+ param("mode", "Random"),
+ param("count", "1"),
+ param("seed", "3"),
+ param("highlight", "false"),
],
vec![],
);
@@ -9337,12 +9343,12 @@ mod simnet_tests {
"Pull 1",
"attribute",
vec![
- param("Input", "Group 1"),
- param("Operation", "Modify"),
- param("Attribute Name", "Pos"),
- param("Value", "0.00:0.50:0.00"),
- param("Combine", "Add"),
- param("Group", "pull"),
+ param("input", "Group 1"),
+ param("operation", "Modify"),
+ param("attribute_name", "Pos"),
+ param("value", "0.00:0.50:0.00"),
+ param("combine", "Add"),
+ param("group", "pull"),
],
vec![],
);
@@ -9351,11 +9357,11 @@ mod simnet_tests {
"Relax 1",
"relax",
vec![
- param("Input", "Pull 1"),
- param("Rest", "Group 1"),
- param("Pin Group", "pull"),
- param("Stiffness", "0.50"),
- param("Iterations", "8"),
+ param("input", "Pull 1"),
+ param("rest", "Group 1"),
+ param("pin_group", "pull"),
+ param("stiffness", "0.50"),
+ param("iterations", "8"),
],
vec![],
);
@@ -9389,18 +9395,18 @@ mod simnet_tests {
/// for the points it left alone.
#[test]
fn pull_arrows_measure_what_the_node_moves() {
- let sphere = node("id-sphere", "Sphere 1", "sphere", vec![param("Radius", "0.5")], vec![]);
+ let sphere = node("id-sphere", "Sphere 1", "sphere", vec![param("radius", "0.5")], vec![]);
let group = node(
"id-group",
"Group 1",
"group",
vec![
- param("Input", "Sphere 1"),
- param("Group Name", "pull"),
- param("Mode", "Random"),
- param("Count", "30"),
- param("Seed", "3"),
- param("Highlight", "false"),
+ param("input", "Sphere 1"),
+ param("group_name", "pull"),
+ param("mode", "Random"),
+ param("count", "30"),
+ param("seed", "3"),
+ param("highlight", "false"),
],
vec![],
);
@@ -9409,12 +9415,12 @@ mod simnet_tests {
"Pull 1",
"attribute",
vec![
- param("Input", "Group 1"),
- param("Operation", "Modify"),
- param("Attribute Name", "Pos"),
- param("Value", "0.00:0.06:0.00"),
- param("Combine", "Add"),
- param("Group", "pull"),
+ param("input", "Group 1"),
+ param("operation", "Modify"),
+ param("attribute_name", "Pos"),
+ param("value", "0.00:0.06:0.00"),
+ param("combine", "Add"),
+ param("group", "pull"),
],
vec![],
);
@@ -9455,14 +9461,14 @@ mod simnet_tests {
#[test]
fn strength_and_scale_by_scale_the_pulls_effect() {
let pull_of = |extra: Vec<(&str, &str)>, combine: &str, value: &str| {
- let sphere = node("id-sphere", "Sphere 1", "sphere", vec![param("Radius", "0.5")], vec![]);
+ let sphere = node("id-sphere", "Sphere 1", "sphere", vec![param("radius", "0.5")], vec![]);
let mut params = vec![
- param("Input", "Sphere 1"),
- param("Operation", "Modify"),
- param("Attribute Name", "Pos"),
- param("Value", value),
- param("Combine", combine),
- param("Group", ""),
+ param("input", "Sphere 1"),
+ param("operation", "Modify"),
+ param("attribute_name", "Pos"),
+ param("value", value),
+ param("combine", combine),
+ param("group", ""),
];
params.extend(extra.into_iter().map(|(k, v)| param(k, v)));
let pull = node("id-pull", "Pull 1", "attribute", params, vec![]);
@@ -9482,16 +9488,16 @@ mod simnet_tests {
// Absent (an older save) and at one: the pull as it always was.
let (d, err) = moved_by(&pull_of(vec![], "Add", "0.00:0.06:0.00"), 1);
assert!(err.is_none() && all(&d, up), "{err:?}");
- let (d, _) = moved_by(&pull_of(vec![("Strength", "1.00")], "Add", "0.00:0.06:0.00"), 1);
+ let (d, _) = moved_by(&pull_of(vec![("strength", "1.00")], "Add", "0.00:0.06:0.00"), 1);
assert!(all(&d, up));
// Half, none, double.
for (strength, want) in [("0.50", up * 0.5), ("0.00", Vec3::ZERO), ("2.00", up * 2.0)] {
- let (d, _) = moved_by(&pull_of(vec![("Strength", strength)], "Add", "0.00:0.06:0.00"), 1);
+ let (d, _) = moved_by(&pull_of(vec![("strength", strength)], "Add", "0.00:0.06:0.00"), 1);
assert!(all(&d, want), "strength {strength}: {:?}", d[0]);
}
// Under Set it is how far toward Value each point goes: at a half,
// halfway from where it was to (0, 2, 0).
- let root = pull_of(vec![("Strength", "0.50")], "Set", "0.00:2.00:0.00");
+ let root = pull_of(vec![("strength", "0.50")], "Set", "0.00:2.00:0.00");
let base = eval(&root, "Sphere 1");
let out = eval(&root, "Pull 1");
for p in 0..out.num_points() {
@@ -9501,7 +9507,7 @@ mod simnet_tests {
// Scale By: each point by its own value of the attribute — the
// sphere's uv, whose first component runs around it — times Strength.
- let root = pull_of(vec![("Strength", "0.50"), ("Scale By", "uv")], "Add", "0.00:0.06:0.00");
+ let root = pull_of(vec![("strength", "0.50"), ("scale_by", "uv")], "Add", "0.00:0.06:0.00");
let base = eval(&root, "Sphere 1");
let (d, err) = moved_by(&root, 1);
assert!(err.is_none(), "{err:?}");
@@ -9515,14 +9521,14 @@ mod simnet_tests {
assert!(hi - lo > 0.5, "the fixture's weights vary, or this proves nothing: {lo}..{hi}");
// An attribute the input lacks is said, and nothing moves.
- let (d, err) = moved_by(&pull_of(vec![("Scale By", "nothing_here")], "Add", "0.00:0.06:0.00"), 1);
+ let (d, err) = moved_by(&pull_of(vec![("scale_by", "nothing_here")], "Add", "0.00:0.06:0.00"), 1);
assert!(err.as_deref().is_some_and(|e| e.contains("Scale By") && e.contains("nothing_here")), "{err:?}");
assert!(all(&d, Vec3::ZERO));
// Strength is a number, so it takes an expression: a pull that
// ramps in over ten frames.
- let mut root = pull_of(vec![("Strength", "$F / 10")], "Add", "0.00:0.06:0.00");
- let strength = root.children[1].params.iter_mut().find(|p| p.name == "Strength").unwrap();
+ let mut root = pull_of(vec![("strength", "$F / 10")], "Add", "0.00:0.06:0.00");
+ let strength = root.children[1].params.iter_mut().find(|p| p.name == "strength").unwrap();
strength.set_type("float");
strength.set_expr(true);
for frame in [2, 5, 10] {
@@ -9540,21 +9546,21 @@ mod simnet_tests {
#[test]
fn per_frame_makes_the_pull_independent_of_the_substep_count() {
let sim_of = |substeps: &str, combine: &str, value: &str, extra: Vec<(&str, &str)>| {
- let sphere = node("id-sphere", "Sphere 1", "sphere", vec![param("Radius", "0.5")], vec![]);
+ let sphere = node("id-sphere", "Sphere 1", "sphere", vec![param("radius", "0.5")], vec![]);
let mover = node("id-shift", "Shift 1", "attribute", vec![
- param("Input", "Sphere 1"), param("Operation", "Modify"), param("Attribute Name", "Pos"),
- param("Value", "3.00:0.00:0.00"), param("Combine", "Add"), param("Group", ""),
+ param("input", "Sphere 1"), param("operation", "Modify"), param("attribute_name", "Pos"),
+ param("value", "3.00:0.00:0.00"), param("combine", "Add"), param("group", ""),
], vec![]);
let inner_input = node("id-in", "input1", "input", vec![], vec![]);
let mut params = vec![
- param("Input", "input1"), param("Operation", "Modify"), param("Attribute Name", "Pos"),
- param("Value", value), param("Combine", combine), param("Group", ""),
+ param("input", "input1"), param("operation", "Modify"), param("attribute_name", "Pos"),
+ param("value", value), param("combine", combine), param("group", ""),
];
params.extend(extra.into_iter().map(|(k, v)| param(k, v)));
let pull = node("id-pull", "pull1", "attribute", params, vec![]);
- let inner_output = node("id-out", "output1", "output", vec![param("Input", "pull1")], vec![]);
+ let inner_output = node("id-out", "output1", "output", vec![param("input", "pull1")], vec![]);
let sim_node = node("id-sim", "Simnet 1", "simnet",
- vec![param("Input", "Shift 1"), param("Substeps", substeps)],
+ vec![param("input", "Shift 1"), param("substeps", substeps)],
vec![inner_input, pull, inner_output]);
node("id-root", "root", "node", vec![], vec![sphere, mover, sim_node])
};
@@ -9563,14 +9569,14 @@ mod simnet_tests {
// Add: five frames on from the seed, one unit a frame, at any count.
for substeps in ["1", "4", "16"] {
- let root = sim_of(substeps, "Add", "1.00:0.00:0.00", vec![("Per Frame", "true")]);
+ let root = sim_of(substeps, "Add", "1.00:0.00:0.00", vec![("per_frame", "true")]);
assert!((at(&root, 6) - (seed + 5.0)).abs() < 1e-3, "{substeps} substeps: {}", at(&root, 6) - seed);
}
// With Strength: half a unit a frame.
- let root = sim_of("4", "Add", "1.00:0.00:0.00", vec![("Per Frame", "true"), ("Strength", "0.50")]);
+ let root = sim_of("4", "Add", "1.00:0.00:0.00", vec![("per_frame", "true"), ("strength", "0.50")]);
assert!((at(&root, 6) - (seed + 2.5)).abs() < 1e-3);
// Off, or on a node without the row: per step, four times as far.
- for extra in [vec![("Per Frame", "false")], vec![]] {
+ for extra in [vec![("per_frame", "false")], vec![]] {
let root = sim_of("4", "Add", "1.00:0.00:0.00", extra);
assert!((at(&root, 6) - (seed + 20.0)).abs() < 1e-3, "{}", at(&root, 6) - seed);
}
@@ -9579,19 +9585,19 @@ mod simnet_tests {
// whatever the count. (The seed sits well clear of zero.)
assert!(seed > 0.1, "the fixture's points are on the positive side: {seed}");
for substeps in ["1", "4", "16"] {
- let root = sim_of(substeps, "Multiply", "2.00:1.00:1.00", vec![("Per Frame", "true")]);
+ let root = sim_of(substeps, "Multiply", "2.00:1.00:1.00", vec![("per_frame", "true")]);
let got = at(&root, 4);
assert!((got - seed * 8.0).abs() < seed * 8.0 * 1e-3, "{substeps} substeps: {got} against {}", seed * 8.0);
}
// A Set does not accumulate: it sets, at any count, switch or no.
- let root = sim_of("4", "Set", "7.00:0.00:0.00", vec![("Per Frame", "true")]);
+ let root = sim_of("4", "Set", "7.00:0.00:0.00", vec![("per_frame", "true")]);
assert!((at(&root, 3) - 7.0).abs() < 1e-4);
// Outside a simnet there is no step to account for.
- let sphere = node("id-sphere", "Sphere 1", "sphere", vec![param("Radius", "0.5")], vec![]);
+ let sphere = node("id-sphere", "Sphere 1", "sphere", vec![param("radius", "0.5")], vec![]);
let pull = node("id-pull", "Pull 1", "attribute", vec![
- param("Input", "Sphere 1"), param("Operation", "Modify"), param("Attribute Name", "Pos"),
- param("Value", "1.00:0.00:0.00"), param("Combine", "Add"), param("Group", ""), param("Per Frame", "true"),
+ param("input", "Sphere 1"), param("operation", "Modify"), param("attribute_name", "Pos"),
+ param("value", "1.00:0.00:0.00"), param("combine", "Add"), param("group", ""), param("per_frame", "true"),
], vec![]);
let root = node("id-root", "root", "node", vec![], vec![sphere, pull]);
assert!((min_x(&eval(&root, "Pull 1")) - (min_x(&eval(&root, "Sphere 1")) + 1.0)).abs() < 1e-5);
@@ -9619,19 +9625,19 @@ mod simnet_tests {
#[test]
fn a_detangle_in_a_simnet_knows_where_the_step_began() {
let graph = |nested: bool| {
- let sphere = node("id-sphere", "Sphere 1", "sphere", vec![param("Radius", "0.5")], vec![]);
+ let sphere = node("id-sphere", "Sphere 1", "sphere", vec![param("radius", "0.5")], vec![]);
let pull = |input: &str| {
node(
"id-pull",
"pull1",
"attribute",
vec![
- param("Input", input),
- param("Operation", "Modify"),
- param("Attribute Name", "Pos"),
- param("Value", "1.00:0.00:0.00"),
- param("Combine", "Add"),
- param("Group", ""),
+ param("input", input),
+ param("operation", "Modify"),
+ param("attribute_name", "Pos"),
+ param("value", "1.00:0.00:0.00"),
+ param("combine", "Add"),
+ param("group", ""),
],
vec![],
)
@@ -9641,7 +9647,7 @@ mod simnet_tests {
"id-detangle",
"detangle1",
"detangle",
- vec![param("Input", input), param("Method", "Surface"), param("Thickness", "1.00"), param("Step Limit", "0.50")],
+ vec![param("input", input), param("method", "surface"), param("thickness", "1.00"), param("step_limit", "0.50")],
vec![],
)
};
@@ -9650,28 +9656,28 @@ mod simnet_tests {
"id-sub",
"sub1",
"node",
- vec![param("Input", "pull1")],
+ vec![param("input", "pull1")],
vec![
node("id-sub-in", "input1", "input", vec![], vec![]),
detangle("input1"),
- node("id-sub-out", "output1", "output", vec![param("Input", "detangle1")], vec![]),
+ node("id-sub-out", "output1", "output", vec![param("input", "detangle1")], vec![]),
],
);
vec![
node("id-in", "input1", "input", vec![], vec![]),
pull("input1"),
inside,
- node("id-out", "output1", "output", vec![param("Input", "sub1")], vec![]),
+ node("id-out", "output1", "output", vec![param("input", "sub1")], vec![]),
]
} else {
vec![
node("id-in", "input1", "input", vec![], vec![]),
pull("input1"),
detangle("pull1"),
- node("id-out", "output1", "output", vec![param("Input", "detangle1")], vec![]),
+ node("id-out", "output1", "output", vec![param("input", "detangle1")], vec![]),
]
};
- let sim = node("id-sim", "Simnet 1", "simnet", vec![param("Input", "Sphere 1")], chain);
+ let sim = node("id-sim", "Simnet 1", "simnet", vec![param("input", "Sphere 1")], chain);
let outside = detangle("pull1");
node("id-root", "root", "node", vec![], vec![sphere, sim, pull("Sphere 1"), outside])
};
@@ -9698,28 +9704,28 @@ mod simnet_tests {
/// would give (the `input` node reads the seed with no feedback pushed).
#[test]
fn pull_arrows_inside_a_simnet_start_from_this_frames_state() {
- let sphere = node("id-sphere", "Sphere 1", "sphere", vec![param("Radius", "0.5")], vec![]);
+ let sphere = node("id-sphere", "Sphere 1", "sphere", vec![param("radius", "0.5")], vec![]);
let inner_input = node("id-in", "input1", "input", vec![], vec![]);
let pull = node(
"id-pull",
"pull1",
"attribute",
vec![
- param("Input", "input1"),
- param("Operation", "Modify"),
- param("Attribute Name", "Pos"),
- param("Value", "1.00:0.00:0.00"),
- param("Combine", "Add"),
- param("Group", ""),
+ param("input", "input1"),
+ param("operation", "Modify"),
+ param("attribute_name", "Pos"),
+ param("value", "1.00:0.00:0.00"),
+ param("combine", "Add"),
+ param("group", ""),
],
vec![],
);
- let inner_output = node("id-out", "output1", "output", vec![param("Input", "pull1")], vec![]);
+ let inner_output = node("id-out", "output1", "output", vec![param("input", "pull1")], vec![]);
let sim_node = node(
"id-sim",
"Simnet 1",
"simnet",
- vec![param("Input", "Sphere 1")],
+ vec![param("input", "Sphere 1")],
vec![inner_input, pull, inner_output],
);
let root = node("id-root", "root", "node", vec![], vec![sphere, sim_node]);
@@ -9747,28 +9753,28 @@ mod simnet_tests {
/// feedback, so a visible chain node draws where the output does too.
#[test]
fn pull_arrows_inside_a_simnet_anchor_at_the_last_substep() {
- let sphere = node("id-sphere", "Sphere 1", "sphere", vec![param("Radius", "0.5")], vec![]);
+ let sphere = node("id-sphere", "Sphere 1", "sphere", vec![param("radius", "0.5")], vec![]);
let inner_input = node("id-in", "input1", "input", vec![], vec![]);
let pull = node(
"id-pull",
"pull1",
"attribute",
vec![
- param("Input", "input1"),
- param("Operation", "Modify"),
- param("Attribute Name", "Pos"),
- param("Value", "1.00:0.00:0.00"),
- param("Combine", "Add"),
- param("Group", ""),
+ param("input", "input1"),
+ param("operation", "Modify"),
+ param("attribute_name", "Pos"),
+ param("value", "1.00:0.00:0.00"),
+ param("combine", "Add"),
+ param("group", ""),
],
vec![],
);
- let inner_output = node("id-out", "output1", "output", vec![param("Input", "pull1")], vec![]);
+ let inner_output = node("id-out", "output1", "output", vec![param("input", "pull1")], vec![]);
let sim_node = node(
"id-sim",
"Simnet 1",
"simnet",
- vec![param("Input", "Sphere 1"), param("Substeps", "4")],
+ vec![param("input", "Sphere 1"), param("substeps", "4")],
vec![inner_input, pull, inner_output],
);
let root = node("id-root", "root", "node", vec![], vec![sphere, sim_node]);
@@ -9801,28 +9807,28 @@ mod simnet_tests {
/// under `cfg(test)`, so the node id here reaches no user cache.
#[test]
fn a_disk_resume_landing_on_the_frame_keeps_its_last_substeps_input() {
- let sphere = node("id-sphere", "Sphere 1", "sphere", vec![param("Radius", "0.5")], vec![]);
+ let sphere = node("id-sphere", "Sphere 1", "sphere", vec![param("radius", "0.5")], vec![]);
let inner_input = node("id-in", "input1", "input", vec![], vec![]);
let pull = node(
"id-pull",
"pull1",
"attribute",
vec![
- param("Input", "input1"),
- param("Operation", "Modify"),
- param("Attribute Name", "Pos"),
- param("Value", "1.00:0.00:0.00"),
- param("Combine", "Add"),
- param("Group", ""),
+ param("input", "input1"),
+ param("operation", "Modify"),
+ param("attribute_name", "Pos"),
+ param("value", "1.00:0.00:0.00"),
+ param("combine", "Add"),
+ param("group", ""),
],
vec![],
);
- let inner_output = node("id-out", "output1", "output", vec![param("Input", "pull1")], vec![]);
+ let inner_output = node("id-out", "output1", "output", vec![param("input", "pull1")], vec![]);
let sim_node = node(
"id-sim-disk-resume",
"Simnet 1",
"simnet",
- vec![param("Input", "Sphere 1"), param("Substeps", "2"), param("Cache", "true")],
+ vec![param("input", "Sphere 1"), param("substeps", "2"), param("cache", "true")],
vec![inner_input, pull, inner_output],
);
let root = node("id-root", "root", "node", vec![], vec![sphere, sim_node]);
@@ -9863,15 +9869,15 @@ mod simnet_tests {
#[test]
fn a_scrub_resumes_from_a_checkpoint_and_arrives_at_the_same_state() {
let sim_of = |value: &str, substeps: &str| {
- let sphere = node("id-sphere", "Sphere 1", "sphere", vec![param("Radius", "0.5")], vec![]);
+ let sphere = node("id-sphere", "Sphere 1", "sphere", vec![param("radius", "0.5")], vec![]);
let inner_input = node("id-in", "input1", "input", vec![], vec![]);
let pull = node("id-pull", "pull1", "attribute", vec![
- param("Input", "input1"), param("Operation", "Modify"), param("Attribute Name", "Pos"),
- param("Value", value), param("Combine", "Multiply"), param("Group", ""),
+ param("input", "input1"), param("operation", "Modify"), param("attribute_name", "Pos"),
+ param("value", value), param("combine", "Multiply"), param("group", ""),
], vec![]);
- let inner_output = node("id-out", "output1", "output", vec![param("Input", "pull1")], vec![]);
+ let inner_output = node("id-out", "output1", "output", vec![param("input", "pull1")], vec![]);
let sim_node = node("id-sim-checkpoints", "Simnet 1", "simnet",
- vec![param("Input", "Sphere 1"), param("Substeps", substeps)],
+ vec![param("input", "Sphere 1"), param("substeps", substeps)],
vec![inner_input, pull, inner_output]);
node("id-root", "root", "node", vec![], vec![sphere, sim_node])
};
@@ -9967,18 +9973,18 @@ mod simnet_tests {
/// sphere toward. Rest equal to the input is a relax with nothing to do.
#[test]
fn a_rest_wire_resolves_to_its_own_sibling() {
- let small = node("id-a-shape", "shape", "sphere", vec![param("Radius", "0.5")], vec![]);
+ let small = node("id-a-shape", "shape", "sphere", vec![param("radius", "0.5")], vec![]);
let a = node("id-a", "a", "node", vec![], vec![small]);
- let big = node("id-b-shape", "shape", "sphere", vec![param("Radius", "1.0")], vec![]);
+ let big = node("id-b-shape", "shape", "sphere", vec![param("radius", "1.0")], vec![]);
let relax = node(
"id-b-relax",
"relax1",
"relax",
vec![
- param("Input", "shape"),
- param("Rest", "shape"),
- param("Stiffness", "1.00"),
- param("Iterations", "8"),
+ param("input", "shape"),
+ param("rest", "shape"),
+ param("stiffness", "1.00"),
+ param("iterations", "8"),
],
vec![],
);
@@ -9986,7 +9992,7 @@ mod simnet_tests {
let root = node("id-root", "root", "node", vec![], vec![a, b]);
let relax = &root.children[1].children[1];
- assert_eq!(param_node(&root, relax, "Rest").map(|n| n.id.as_str()), Some("id-b-shape"));
+ assert_eq!(param_node(&root, relax, "rest").map(|n| n.id.as_str()), Some("id-b-shape"));
let mut cache = SimCache::default();
let mut sim = EvalSim::new(1, 1, &mut cache);
let input = generate_single_node_geometry_with_errors(&root, &root.children[1].children[0], &mut Vec::new(), &mut None, &mut sim).unwrap();
diff --git a/src/image_handles.rs b/src/image_handles.rs
index 1489927..53a4ad4 100644
--- a/src/image_handles.rs
+++ b/src/image_handles.rs
@@ -71,15 +71,15 @@ fn set(node: &mut FsNode, name: &str, value: String) {
}
fn is_line(node: &FsNode) -> bool {
- node_param_str(node, "Shape", "Rectangle").eq_ignore_ascii_case("Line")
+ node_param_str(node, "shape", "Rectangle").eq_ignore_ascii_case("Line")
}
/// The shape's centre, box and turn, as its rows have them.
fn shape_rows(node: &FsNode) -> ([f32; 2], [f32; 2], f32) {
(
- [node_param_f32(node, "X", 0.0), node_param_f32(node, "Y", 0.0)],
- [node_param_f32(node, "Width", 1.0).abs(), node_param_f32(node, "Height", 1.0).abs()],
- node_param_f32(node, "Rotation", 0.0),
+ [node_param_f32(node, "x", 0.0), node_param_f32(node, "y", 0.0)],
+ [node_param_f32(node, "width", 1.0).abs(), node_param_f32(node, "height", 1.0).abs()],
+ node_param_f32(node, "rotation", 0.0),
)
}
@@ -109,12 +109,12 @@ impl HandleSource for ShapeHandles {
let Some(frame) = ctx.page else { return };
let page = |p: &Vec3| frame.from_world(*p, ctx.world_unit_mm);
let least = one_pixel(&frame);
- let kept = node_param_f32(node, "Rotation", 0.0);
+ let kept = node_param_f32(node, "rotation", 0.0);
let (c, size, turn) = match handles {
[c, end] => {
let (c, along) = (page(c), sub(page(end), page(c)));
let turn = angle_of(along, &frame).unwrap_or(kept);
- (c, [length(along) * 2.0, node_param_f32(node, "Height", 1.0)], turn)
+ (c, [length(along) * 2.0, node_param_f32(node, "height", 1.0)], turn)
}
[c, corner, edge] => {
let c = page(c);
@@ -125,13 +125,13 @@ impl HandleSource for ShapeHandles {
}
_ => return,
};
- set(node, "X", frame.row(c[0]));
- set(node, "Y", frame.row(c[1]));
- set(node, "Width", frame.row(size[0].max(least)));
+ set(node, "x", frame.row(c[0]));
+ set(node, "y", frame.row(c[1]));
+ set(node, "width", frame.row(size[0].max(least)));
if handles.len() == 3 {
- set(node, "Height", frame.row(size[1].max(least)));
+ set(node, "height", frame.row(size[1].max(least)));
}
- set(node, "Rotation", format!("{turn:.1}"));
+ set(node, "rotation", format!("{turn:.1}"));
}
fn drag(&self, handles: &mut Vec<Vec3>, moved: usize, to: Vec3, ctx: &HandleCtx) {
@@ -222,8 +222,8 @@ impl HandleSource for TextHandles {
/// Size below it, so the type's size is a length that can be seen.
fn read(&self, node: &FsNode, ctx: &HandleCtx) -> Vec<Vec3> {
let Some(frame) = ctx.page else { return Vec::new() };
- let at = [node_param_f32(node, "X", 0.0), node_param_f32(node, "Y", 0.0)];
- let size = node_param_f32(node, "Size", 0.25).abs();
+ let at = [node_param_f32(node, "x", 0.0), node_param_f32(node, "y", 0.0)];
+ let size = node_param_f32(node, "size", 0.25).abs();
vec![
frame.to_world(at, ctx.world_unit_mm),
frame.to_world(add(at, [0.0, size]), ctx.world_unit_mm),
@@ -235,9 +235,9 @@ impl HandleSource for TextHandles {
let [at, below] = handles else { return };
let at = frame.from_world(*at, ctx.world_unit_mm);
let size = length(sub(frame.from_world(*below, ctx.world_unit_mm), at));
- set(node, "X", frame.row(at[0]));
- set(node, "Y", frame.row(at[1]));
- set(node, "Size", frame.row(size.max(one_pixel(&frame))));
+ set(node, "x", frame.row(at[0]));
+ set(node, "y", frame.row(at[1]));
+ set(node, "size", frame.row(size.max(one_pixel(&frame))));
}
fn drag(&self, handles: &mut Vec<Vec3>, moved: usize, to: Vec3, _ctx: &HandleCtx) {
diff --git a/src/image_tools.rs b/src/image_tools.rs
index bafb861..2892f43 100644
--- a/src/image_tools.rs
+++ b/src/image_tools.rs
@@ -112,11 +112,11 @@ impl State {
dir.children.iter_mut().find(|c| c.node_type == "camera" && c.name == camera_name)
{
let fmt3 = |v: Vec3| format!("{:.3}:{:.3}:{:.3}", v.x, v.y, v.z);
- set_param(node, "Pivot", fmt3(center));
- set_param(node, "Position", fmt3(center + Vec3::Z * dist));
+ set_param(node, "pivot", fmt3(center));
+ set_param(node, "position", fmt3(center + Vec3::Z * dist));
set_param(
node,
- "Rotation",
+ "rotation",
format!("{:.3}:{:.3}:0.000", orbit_x.to_degrees(), orbit_y.to_degrees()),
);
reached = Some(dist);
@@ -285,7 +285,7 @@ impl State {
continue;
}
for p in sibling.params.iter_mut() {
- if p.name == "Input" && p.text().trim() == target_name {
+ if p.name == "input" && p.text().trim() == target_name {
p.set_text(new_name.clone());
}
}
@@ -297,20 +297,20 @@ impl State {
let (w, h) = (page.in_unit(page.size[0]), page.in_unit(page.size[1]));
let num = |v: f32| row_number(v, unit);
let node = &mut self.current_dir_mut().children[slot];
- set_param(node, "Input", target_name);
- set_param(node, "X", num(w * 0.5));
- set_param(node, "Y", num(h * 0.5));
+ set_param(node, "input", target_name);
+ set_param(node, "x", num(w * 0.5));
+ set_param(node, "y", num(h * 0.5));
match layer {
ImageLayer::Text => {
- set_param(node, "Text", "Text".to_string());
- set_param(node, "Size", num((h * 0.05).max(page.in_unit(1.0 / page.scale()))));
- set_param(node, "Vertical", "Middle".to_string());
+ set_param(node, "text", "Text".to_string());
+ set_param(node, "size", num((h * 0.05).max(page.in_unit(1.0 / page.scale()))));
+ set_param(node, "vertical", "Middle".to_string());
}
shape => {
let side = w.min(h) / 3.0;
- set_param(node, "Shape", shape.label().to_string());
- set_param(node, "Width", num(if shape == ImageLayer::Line { w / 3.0 } else { side }));
- set_param(node, "Height", num(side));
+ set_param(node, "shape", shape.label().to_string());
+ set_param(node, "width", num(if shape == ImageLayer::Line { w / 3.0 } else { side }));
+ set_param(node, "height", num(side));
// A hairline that survives the raster: a four-hundredth of
// the short side, and never under a pixel and a half.
let stroke = (page.size[0].min(page.size[1]) / 400.0).max(1.5 / page.scale());
@@ -320,7 +320,7 @@ impl State {
} else {
format!("{stroke:.3}")
};
- set_param(node, "Stroke Width", stroke);
+ set_param(node, "stroke_width", stroke);
}
}
self.show_and_select(slot);
diff --git a/src/main.rs b/src/main.rs
index f51bb51..20bd4a9 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -318,7 +318,7 @@ mod tests {
fn test_add_node_migrates_to_points() {
let legacy: FsNode = serde_json::from_str(
r#"{"name":"Add 3","type":"add","params":[
- {"name":"Points","type":"spinbox","default":"250"}
+ {"name":"points","type":"spinbox","default":"250"}
]}"#,
)
.unwrap();
@@ -334,9 +334,9 @@ mod tests {
let node = &root.children[0];
assert_eq!(node.node_type, "points");
assert_eq!(node.name, "Add 3", "instance name is the wire identity — never rewritten");
- let points = node.params.iter().find(|p| p.name == "Points").unwrap();
+ let points = node.params.iter().find(|p| p.name == "points").unwrap();
assert_eq!(points.text(), "250", "instance owns its values");
- let shape = node.params.iter().find(|p| p.name == "Shape").expect("Shape appended");
+ let shape = node.params.iter().find(|p| p.name == "shape").expect("Shape appended");
assert_eq!(shape.text(), "None");
}
@@ -655,7 +655,7 @@ mod tests {
group.name = "My Region".into();
group.node_type = "group".into();
group.children.clear();
- group.params = vec![crate::app::ParamDef::new("Input", "text", "Sphere 1").with_label("Input")];
+ group.params = vec![crate::app::ParamDef::new("input", "text", "Sphere 1").with_label("Input")];
let mut clash = group.clone();
clash.id = "c".into();
clash.name = "sphere1".into();
@@ -685,6 +685,87 @@ mod tests {
proj.migrate_format();
assert_eq!(serde_json::to_string(&proj).unwrap(), before);
}
+ /// A parameter has a NAME — an identifier a path spells, never shown —
+ /// and a LABEL, what the pane shows. Every template carries both, an
+ /// older save is renamed on load with what spells its names, MCP finds
+ /// a parameter by either, and a name that is not one is refused.
+ #[test]
+ fn parameters_have_a_name_and_a_label() {
+ use crate::app::{is_param_name, misnamed_params, param_name_of, FsNode, McpAction, ParamDef, Project};
+ assert_eq!(param_name_of("Base Resolution"), "base_resolution");
+ assert_eq!(param_name_of("Relax in 3D Space"), "relax_in_3d_space");
+ assert_eq!(param_name_of("Input 2"), "input_2");
+ assert_eq!(param_name_of(" Odd -- Spacing! "), "odd_spacing");
+ assert!(is_param_name("input_2") && !is_param_name("Input 2") && !is_param_name("Radius") && !is_param_name(""));
+
+ // Every shipped template: a name, and a label for the pane.
+ let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("nodes");
+ let mut n = 0;
+ for f in fs::read_dir(&dir).unwrap().flatten() {
+ let t: FsNode = serde_json::from_str(&fs::read_to_string(f.path()).unwrap()).unwrap();
+ assert!(misnamed_params(&t).is_empty(), "{}: {:?}", f.path().display(), misnamed_params(&t));
+ for p in &t.params {
+ assert!(!p.label.is_empty(), "{}: {} has no label", f.path().display(), p.name);
+ n += 1;
+ }
+ }
+ assert!(n > 300, "walked {n}");
+ let mut bad = ref_node("t", "Bad", "node", vec![("Some Row", "float", "1")], vec![]);
+ assert_eq!(misnamed_params(&bad), vec![("Bad".to_string(), "Some Row".to_string())]);
+ bad.params[0].name = "some_row".into();
+ assert!(misnamed_params(&bad).is_empty());
+
+ // The pane shows labels.
+ let mut state = State::new(false);
+ let mut redraw = false;
+ state.apply_action(McpAction::AddNode { template_name: "Embryo".into(), name: Some("embryo1".into()), x: 3.0, y: 8.0 }, &mut redraw).unwrap();
+ let slot = state.current_dir().children.iter().position(|c| c.name == "embryo1").unwrap();
+ let rows = crate::app::param_display(&state.current_dir().children[slot].params);
+ assert!(rows.iter().any(|r| r.0 == "Base Resolution"), "{rows:?}");
+ assert!(rows.iter().all(|r| !r.0.contains('_')), "no name in the pane: {rows:?}");
+
+ // MCP: by name, by label, by the old name.
+ for (asked, value) in [("base_resolution", "20"), ("Base Resolution", "21"), ("base resolution", "22")] {
+ state.apply_action(McpAction::SetParam { slot, name: asked.into(), value: value.into() }, &mut redraw).unwrap();
+ assert_eq!(crate::geometry::node_param_str(&state.current_dir().children[slot], "base_resolution", ""), value, "{asked}");
+ }
+ let err = state
+ .apply_action(McpAction::AddParam { slot, name: "My Row".into(), param_type: "float".into(), default: "1".into(), label: String::new() }, &mut redraw)
+ .unwrap_err();
+ assert!(err.contains("my_row"), "the refusal says the form: {err}");
+ state
+ .apply_action(McpAction::AddParam { slot, name: "my_row".into(), param_type: "float".into(), default: "1".into(), label: "My Row".into() }, &mut redraw)
+ .unwrap();
+ let rows = crate::app::param_display(&state.current_dir().children[slot].params);
+ assert!(rows.iter().any(|r| r.0 == "My Row"));
+ assert!(state
+ .apply_action(McpAction::AddParam { slot, name: "my_row".into(), param_type: "float".into(), default: "1".into(), label: String::new() }, &mut redraw)
+ .is_err(), "a name the node has is refused");
+
+ // Format 3 → 4: names and what spells them.
+ let mut meta = ref_node("m", "view", "utility", vec![("Show Grid", "toggle", "true")], vec![]);
+ meta.params[0].label.clear();
+ let mut root = ref_node("root", "root", "node", vec![], vec![
+ ref_node("a", "ball", "sphere", vec![("Base Resolution", "spinbox", "12"), ("Center", "float3", "0:1:0")], vec![]),
+ ref_node("b", "box1", "box", vec![("Size X", "slider", "ch(\"../ball/Base Resolution\") * 2 + chf(\"../ball/Center.y\")")], vec![]),
+ ref_node("w", "wrangle1", "wrangle", vec![("Code", "code", "@P.y += chv(\"../ball/Center\").y; // ch(\"Base Resolution\")")], vec![]),
+ meta,
+ ]);
+ root.children[1].params[0].set_expr(true);
+ let mut proj = Project { name: "p".into(), root, view_state: Default::default(), format: 3 };
+ proj.migrate_format();
+ let names = |n: &FsNode| n.params.iter().map(|p| p.name.clone()).collect::<Vec<_>>();
+ assert_eq!(names(&proj.root.children[0]), ["base_resolution", "center"]);
+ assert_eq!(proj.root.children[1].params[0].name, "size_x");
+ assert_eq!(proj.root.children[1].params[0].text(), "ch(\"../ball/base_resolution\") * 2 + chf(\"../ball/center.y\")");
+ assert!(proj.root.children[1].params[0].is_expr());
+ assert_eq!(proj.root.children[2].params[0].text(), "@P.y += chv(\"../ball/center\").y; // ch(\"base_resolution\")");
+ assert_eq!(names(&proj.root.children[3]), ["Show Grid"], "a retired settings node keeps what its own migration reads");
+ let p = ParamDef::new("radius", "slider", "1").with_label("Radius");
+ assert_eq!((p.shown_name(), p.name.as_str()), ("Radius", "radius"));
+ }
+
+
/// Format 1 → 2 and 2 → 3: the generators' normal attribute is `N`
/// and their texture coordinates `uv`, and what names `Norm` / `UV` in
@@ -709,6 +790,7 @@ mod tests {
};
let mut root = node("root", "subnet", vec![]);
root.children = vec![
+ // A format-1 file's names are what the pane showed.
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;")]),
@@ -722,6 +804,8 @@ mod tests {
assert_eq!(text(&proj, 1), "Cd, N,uv", "every step a file is behind: N, then uv");
assert_eq!(text(&proj, 2), "@P += @N * 0.1; @Normal = 1;");
assert_eq!(text(&proj, 3), "Normx", "only the whole name");
+ let names: Vec<&str> = proj.root.children.iter().map(|c| c.params[0].name.as_str()).collect();
+ assert_eq!(names, ["attribute", "attributes", "code", "attribute"], "and then every name is one (format 4)");
// Once: a format-2 file's Norm is its own attribute.
let mut again = proj.clone();
@@ -737,10 +821,10 @@ mod tests {
// (the Sphere's Method), and a format-2 file takes only this step.
let mut root = node("root", "subnet", vec![]);
root.children = vec![
- node("vis", "visualize", vec![ParamDef::new("Attribute", "attribute", "UV")]),
- node("w", "wrangle", vec![ParamDef::new("Code", "code", "@P.y = @UV.x; @UVW = 1;")]),
- node("ball", "sphere", vec![ParamDef::new("Method", "choice:UV,Icosphere,Cube", "UV")]),
- node("n", "visualize", vec![ParamDef::new("Attribute", "attribute", "Norm")]),
+ node("vis", "visualize", vec![ParamDef::new("attribute", "attribute", "UV")]),
+ node("w", "wrangle", vec![ParamDef::new("code", "code", "@P.y = @UV.x; @UVW = 1;")]),
+ node("ball", "sphere", vec![ParamDef::new("method", "choice:UV,Icosphere,Cube", "UV")]),
+ node("n", "visualize", vec![ParamDef::new("attribute", "attribute", "Norm")]),
];
let mut proj = Project { name: "p".into(), root, view_state: Default::default(), format: 2 };
proj.migrate_format();
@@ -3386,7 +3470,7 @@ mod tests {
let node = state.current_dir().children.iter().find(|c| c.name == "camera1").unwrap();
crate::geometry::node_param_vec3(node, name, Vec3::ZERO)
};
- let (pivot0, pos0, rot0) = (read(&state, "Pivot"), read(&state, "Position"), read(&state, "Rotation"));
+ let (pivot0, pos0, rot0) = (read(&state, "pivot"), read(&state, "position"), read(&state, "rotation"));
let mark = pivot0;
let before = on_screen(&state, mark);
for _ in 0..100 {
@@ -3394,9 +3478,9 @@ mod tests {
}
let after = on_screen(&state, mark);
assert!((after.0 - before.0 - 70.0).abs() < 0.5 && (after.1 - before.1 - 30.0).abs() < 0.5, "{before:?} -> {after:?}");
- let (moved_pivot, moved_pos) = (read(&state, "Pivot") - pivot0, read(&state, "Position") - pos0);
+ let (moved_pivot, moved_pos) = (read(&state, "pivot") - pivot0, read(&state, "position") - pos0);
assert!(moved_pivot.length() > 0.01 && (moved_pivot - moved_pos).length() < 1e-3, "{moved_pivot:?} against {moved_pos:?}");
- assert_eq!(read(&state, "Rotation"), rot0);
+ assert_eq!(read(&state, "rotation"), rot0);
// The three ways of asking, by pointer.
state.set_active_camera("Default Camera");
@@ -3542,14 +3626,14 @@ mod tests {
let sphere1 = embryo.children.iter().find(|c| c.name == "sphere1").unwrap();
assert_eq!(sphere1.node_type, "sphere");
assert!(sphere1.children.is_empty(), "a native node has no children");
- assert!(sphere1.params.iter().any(|p| p.name == "Method"), "the base template's params arrive");
- let radius = sphere1.params.iter().find(|p| p.name == "Radius").unwrap();
- assert!(radius.is_expr() && radius.text().contains("Radius"), "the override is the reference: {} (expr {})", radius.text(), radius.is_expr());
- assert_eq!(sphere1.params.iter().find(|p| p.name == "Center Y").unwrap().text(), "0.0");
+ assert!(sphere1.params.iter().any(|p| p.name == "method"), "the base template's params arrive");
+ let radius = sphere1.params.iter().find(|p| p.name == "radius").unwrap();
+ assert!(radius.is_expr() && radius.text().contains("radius"), "the override is the reference: {} (expr {})", radius.text(), radius.is_expr());
+ assert_eq!(sphere1.params.iter().find(|p| p.name == "center_y").unwrap().text(), "0.0");
let output1 = embryo.children.iter().find(|c| c.name == "output1").unwrap();
assert_eq!(output1.node_type, "output");
- let output_input = output1.params.iter().find(|p| p.name == "Input").unwrap();
+ let output_input = output1.params.iter().find(|p| p.name == "input").unwrap();
assert_eq!(output_input.text(), "normal1");
}
@@ -3680,7 +3764,7 @@ mod tests {
.iter()
.find(|t| t.name == template_name)
.unwrap_or_else(|| panic!("{template_name} template should be loaded"));
- let color = template.params.iter().find(|p| p.name == "Color").expect("a Color param");
+ let color = template.params.iter().find(|p| p.name == "color").expect("a Color param");
assert_eq!(color.ty(), "toggle");
assert_eq!(color.text(), "true", "coloured by default, as it always was");
@@ -3690,7 +3774,7 @@ mod tests {
for child in &mut inst.children {
child.id = format!("{}_{}", inst.id, child.name);
}
- inst.params.iter_mut().find(|p| p.name == "Color").unwrap().set_text(on.to_string());
+ inst.params.iter_mut().find(|p| p.name == "color").unwrap().set_text(on.to_string());
let root = FsNode {
id: "root".to_string(),
name: "root".to_string(),
@@ -3839,7 +3923,7 @@ mod tests {
// Two points: one span, 8 boxes. The scene walk agrees with the
// single-node path.
- instance.params.iter_mut().find(|p| p.name == "Points").unwrap().set_text("0 0 0; 1 0 0".to_string());
+ instance.params.iter_mut().find(|p| p.name == "points").unwrap().set_text("0 0 0; 1 0 0".to_string());
let root = make_root(instance.clone());
assert_eq!(eval(&root).num_points(), 8 * 8);
assert_eq!(
@@ -3849,7 +3933,7 @@ mod tests {
);
// No parseable points: empty geometry, not a panic.
- instance.params.iter_mut().find(|p| p.name == "Points").unwrap().set_text("not points".to_string());
+ instance.params.iter_mut().find(|p| p.name == "points").unwrap().set_text("not points".to_string());
assert_eq!(eval(&make_root(instance)).num_points(), 0);
}
@@ -3910,7 +3994,7 @@ mod tests {
.expect("set curve points");
let pts = crate::geometry::parse_curve_points(&crate::geometry::node_param_str(
&state.current_dir().children[slot],
- "Points",
+ "points",
"",
));
assert_eq!(
@@ -3935,7 +4019,7 @@ mod tests {
assert_eq!(
crate::geometry::parse_curve_points(&crate::geometry::node_param_str(
&state.current_dir().children[slot],
- "Points",
+ "points",
"",
))
.len(),
@@ -3987,7 +4071,7 @@ mod tests {
let points_of = |state: &State, slot: usize| {
crate::geometry::parse_curve_points(&crate::geometry::node_param_str(
&state.current_dir().children[slot],
- "Points",
+ "points",
"",
))
};
@@ -4054,7 +4138,7 @@ mod tests {
let points_of = |state: &State| {
crate::geometry::parse_curve_points(&crate::geometry::node_param_str(
&state.current_dir().children[slot],
- "Points",
+ "points",
"",
))
};
@@ -4165,9 +4249,9 @@ mod tests {
assert!(extrude_template.children.is_empty());
assert_eq!(extrude_template.inputs, 1);
- let plane = ref_node("p", "plane1", "plane", vec![("Rows", "spinbox", "2"), ("Columns", "spinbox", "2"), ("Width", "slider", "1"), ("Length", "slider", "1")], vec![]);
+ let plane = ref_node("p", "plane1", "plane", vec![("rows", "spinbox", "2"), ("columns", "spinbox", "2"), ("width", "slider", "1"), ("length", "slider", "1")], vec![]);
let extrude = |keep: &str| {
- ref_node("e", "extrude1", "extrude", vec![("Input", "text", "plane1"), ("Distance", "slider", "0.2"), ("Keep Base", "toggle", keep)], vec![])
+ ref_node("e", "extrude1", "extrude", vec![("input", "text", "plane1"), ("distance", "slider", "0.2"), ("keep_base", "toggle", keep)], vec![])
};
let root = ref_node("root", "root", "node", vec![], vec![plane.clone(), extrude("true")]);
let (g, err) = eval(&root, &root.children[1]);
@@ -4198,8 +4282,8 @@ mod tests {
// A closed input: no boundary, so no walls — an outer and an inner
// skin, the farthest points Distance beyond the sphere.
- let sphere = ref_node("s", "sphere1", "sphere", vec![("Radius", "slider", "0.5"), ("Center X", "slider", "0"), ("Center Y", "slider", "0.55"), ("Center Z", "slider", "0")], vec![]);
- let ext = ref_node("e", "extrude1", "extrude", vec![("Input", "text", "sphere1"), ("Distance", "slider", "0.2"), ("Keep Base", "toggle", "true")], vec![]);
+ let sphere = ref_node("s", "sphere1", "sphere", vec![("radius", "slider", "0.5"), ("center_x", "slider", "0"), ("center_y", "slider", "0.55"), ("center_z", "slider", "0")], vec![]);
+ let ext = ref_node("e", "extrude1", "extrude", vec![("input", "text", "sphere1"), ("distance", "slider", "0.2"), ("keep_base", "toggle", "true")], vec![]);
let root3 = ref_node("root", "root", "node", vec![], vec![sphere, ext]);
let g3 = eval(&root3, &root3.children[1]).0.unwrap();
let base = crate::geometry::sphere_point_len(16, 24);
@@ -4241,9 +4325,9 @@ mod tests {
let set = |inst: &mut FsNode, name: &str, val: &str| {
inst.params.iter_mut().find(|p| p.name == name).unwrap().set_text(val.to_string());
};
- set(&mut group_instance, "Input", "Sphere 1");
- set(&mut group_instance, "Center", "0.00:0.80:0.00");
- set(&mut group_instance, "Size", "2.00:0.50:2.00");
+ set(&mut group_instance, "input", "Sphere 1");
+ set(&mut group_instance, "center", "0.00:0.80:0.00");
+ set(&mut group_instance, "size", "2.00:0.50:2.00");
let root = FsNode {
id: "root".to_string(),
@@ -4399,23 +4483,23 @@ mod tests {
let root = root_with(vec![
instance(sphere_t, "s", "Sphere 1", &[]),
instance(attr_t, "a1", "Attr 1", &[
- ("Input", "Sphere 1"),
- ("Operation", "Create"),
- ("Attribute Name", "mass"),
- ("Type", "Float"),
- ("Value", "2.50"),
+ ("input", "Sphere 1"),
+ ("operation", "Create"),
+ ("attribute_name", "mass"),
+ ("type", "Float"),
+ ("value", "2.50"),
]),
instance(attr_t, "a2", "Attr 2", &[
- ("Input", "Attr 1"),
- ("Operation", "Modify"),
- ("Attribute Name", "mass"),
- ("Combine", "Multiply"),
- ("Value", "2.00"),
+ ("input", "Attr 1"),
+ ("operation", "Modify"),
+ ("attribute_name", "mass"),
+ ("combine", "Multiply"),
+ ("value", "2.00"),
]),
instance(attr_t, "a3", "Attr 3", &[
- ("Input", "Attr 2"),
- ("Operation", "Delete"),
- ("Attribute Name", "mass"),
+ ("input", "Attr 2"),
+ ("operation", "Delete"),
+ ("attribute_name", "mass"),
]),
]);
let (geom, err) = eval(&root, 1);
@@ -4443,11 +4527,11 @@ mod tests {
let root = root_with(vec![
instance(sphere_t, "s", "Sphere 1", &[]),
instance(attr_t, "a1", "Tint", &[
- ("Input", "Sphere 1"),
- ("Operation", "Modify"),
- ("Attribute Name", "Col"),
- ("Combine", "Multiply"),
- ("Value", "0.50"),
+ ("input", "Sphere 1"),
+ ("operation", "Modify"),
+ ("attribute_name", "Col"),
+ ("combine", "Multiply"),
+ ("value", "0.50"),
]),
]);
let (geom, err) = eval(&root, 1);
@@ -4463,11 +4547,11 @@ mod tests {
let root = root_with(vec![
instance(sphere_t, "s", "Sphere 1", &[]),
instance(attr_t, "a1", "Lift", &[
- ("Input", "Sphere 1"),
- ("Operation", "Modify"),
- ("Attribute Name", "Pos"),
- ("Combine", "Add"),
- ("Value", "0.00:0.10:0.00"),
+ ("input", "Sphere 1"),
+ ("operation", "Modify"),
+ ("attribute_name", "Pos"),
+ ("combine", "Add"),
+ ("value", "0.00:0.10:0.00"),
]),
]);
let (geom, err) = eval(&root, 1);
@@ -4481,16 +4565,16 @@ mod tests {
let root = root_with(vec![
instance(sphere_t, "s", "Sphere 1", &[]),
instance(group_t, "g", "Group 1", &[
- ("Input", "Sphere 1"),
- ("Center", "0.00:0.80:0.00"),
- ("Size", "2.00:0.50:2.00"),
+ ("input", "Sphere 1"),
+ ("center", "0.00:0.80:0.00"),
+ ("size", "2.00:0.50:2.00"),
]),
instance(attr_t, "a1", "Attr 1", &[
- ("Input", "Group 1"),
- ("Operation", "Create"),
- ("Attribute Name", "mass"),
- ("Value", "1.00"),
- ("Group", "group1"),
+ ("input", "Group 1"),
+ ("operation", "Create"),
+ ("attribute_name", "mass"),
+ ("value", "1.00"),
+ ("group", "group1"),
]),
]);
let (geom, err) = eval(&root, 2);
@@ -4515,10 +4599,10 @@ mod tests {
let root = root_with(vec![
instance(sphere_t, "s", "Sphere 1", &[]),
instance(attr_t, "a1", "Attr 1", &[
- ("Input", "Sphere 1"),
- ("Operation", "Create"),
- ("Attribute Name", "mass"),
- ("Value", "abc"),
+ ("input", "Sphere 1"),
+ ("operation", "Create"),
+ ("attribute_name", "mass"),
+ ("value", "abc"),
]),
]);
let (geom, err) = eval(&root, 1);
@@ -4558,12 +4642,12 @@ mod tests {
state.fs_root.children = vec![
instance(find("Sphere"), "s", "Sphere 1", &[]),
instance(find("Group"), "g", "Group 1", &[
- ("Input", "Sphere 1"),
- ("Center", "0.00:0.80:0.00"),
- ("Size", "2.00:0.50:2.00"),
+ ("input", "Sphere 1"),
+ ("center", "0.00:0.80:0.00"),
+ ("size", "2.00:0.50:2.00"),
]),
- instance(find("Attribute"), "a", "Attr 1", &[("Input", "Group 1")]),
- instance(find("Visualize"), "v", "Vis 1", &[("Input", "Group 1"), ("Range", "Manual")]),
+ instance(find("Attribute"), "a", "Attr 1", &[("input", "Group 1")]),
+ instance(find("Visualize"), "v", "Vis 1", &[("input", "Group 1"), ("range", "Manual")]),
];
state.sync_nodes();
@@ -4588,7 +4672,7 @@ mod tests {
state.param_mut().set_display_params(&edited);
state.sync_parameters_to_project();
let attr = &state.fs_root.children[2];
- assert_eq!(attr.params.iter().find(|p| p.name == "Attribute Name").unwrap().text(), "weight");
+ assert_eq!(attr.params.iter().find(|p| p.name == "attribute_name").unwrap().text(), "weight");
state.sync_parameters_pane();
let rows = state.param_mut().node_params();
// The Input row stays plain text.
@@ -4817,10 +4901,10 @@ mod tests {
assert!(base.positions().iter().all(|p| p[1].abs() < 1e-6));
// Resolution: 3 columns x 2 rows.
- assert_eq!(build(&[("Rows", "2"), ("Columns", "3")]).num_points(), 4 * 3);
+ assert_eq!(build(&[("rows", "2"), ("columns", "3")]).num_points(), 4 * 3);
// Center: lifts to y = 0.3 and shifts x by 1 (span [0.5, 1.5]).
- let moved = build(&[("Center X", "1.0"), ("Center Y", "0.3")]);
+ let moved = build(&[("center_x", "1.0"), ("center_y", "0.3")]);
let (mut min_x, mut max_x) = (f32::MAX, f32::MIN);
for pos in moved.positions() {
assert!((pos[1] - 0.3).abs() < 1e-5);
@@ -4843,9 +4927,9 @@ mod tests {
#[test]
fn a_value_parses_by_its_kind_and_keeps_its_text() {
use crate::app::{ParamDef, ParamSlot, ParamValue as V};
- let v = |ty: &str, text: &str| ParamDef::new("P", ty, text).slot().clone();
+ let v = |ty: &str, text: &str| ParamDef::new("p", ty, text).slot().clone();
assert_eq!(v("slider", "0.50"), ParamSlot::Value(V::Number(0.5)));
- assert_eq!(ParamDef::new("P", "slider", "0.50").text(), "0.50");
+ assert_eq!(ParamDef::new("p", "slider", "0.50").text(), "0.50");
assert_eq!(v("float", " -3 "), ParamSlot::Value(V::Number(-3.0)));
assert_eq!(v("spinbox", "16"), ParamSlot::Value(V::Int(16)));
assert_eq!(v("float3", "0.00:0.20:-1"), ParamSlot::Value(V::Vec3([0.0, 0.2, -1.0])));
@@ -4854,16 +4938,16 @@ mod tests {
assert_eq!(v("choice:UV,Icosphere,Cube", ""), ParamSlot::Value(V::Choice("UV".into())), "empty is the first option");
assert_eq!(v("node", "sphere1"), ParamSlot::Value(V::Text("sphere1".into())));
for (ty, text) in [("slider", "abc"), ("float", ""), ("spinbox", "4.5"), ("float3", "1:2"), ("toggle", "maybe"), ("choice:UV,Cube", "Torus")] {
- let p = ParamDef::new("P", ty, text);
+ let p = ParamDef::new("p", ty, text);
assert!(p.invalid().is_some(), "{ty} {text:?} should not fit");
assert_eq!(p.text(), text, "an invalid text is kept verbatim");
assert!(p.check(text).is_err());
}
// An invalid value reads as it always did: `4.5` in a spinbox is still 4.5.
- let node = FsNode { params: vec![ParamDef::new("Count", "spinbox", "4.5")], ..crate::app::load_fs_tree() };
- assert_eq!(crate::geometry::node_param_f32(&node, "Count", 0.0), 4.5);
+ let node = FsNode { params: vec![ParamDef::new("count", "spinbox", "4.5")], ..crate::app::load_fs_tree() };
+ assert_eq!(crate::geometry::node_param_f32(&node, "count", 0.0), 4.5);
// An expression is not parsed as a value, and is not flagged.
- let e = ParamDef::new("P", "slider", "ch(\"../a/Radius\") * 2").as_expr();
+ let e = ParamDef::new("p", "slider", "ch(\"../a/radius\") * 2").as_expr();
assert!(e.is_expr() && e.invalid().is_none());
}
@@ -4930,10 +5014,10 @@ mod tests {
});
}
assert!(n > 300, "walked {n} parameters");
- let written = serde_json::to_string(&crate::app::ParamDef::new("N", "slider", "0.50").as_expr()).unwrap();
+ let written = serde_json::to_string(&crate::app::ParamDef::new("n", "slider", "0.50").as_expr()).unwrap();
assert_eq!(
written,
- r#"{"name":"N","label":"","type":"slider","default":"0.50","options":[],"min":null,"max":null,"step":null,"show_when":"","expr":true}"#
+ r#"{"name":"n","label":"","type":"slider","default":"0.50","options":[],"min":null,"max":null,"step":null,"show_when":"","expr":true}"#
);
}
@@ -4942,9 +5026,9 @@ mod tests {
#[test]
fn the_template_kind_reparses_an_old_value() {
use crate::app::{ParamDef, ParamValue};
- let mut old = ParamDef::new("Center", "text", "0.00:1.50:0.00");
+ let mut old = ParamDef::new("center", "text", "0.00:1.50:0.00");
assert_eq!(old.value(), Some(&ParamValue::Text("0.00:1.50:0.00".into())));
- old.adopt_ui_from(&ParamDef::new("Center", "float3", "0:0:0"));
+ old.adopt_ui_from(&ParamDef::new("center", "float3", "0:0:0"));
assert_eq!(old.value(), Some(&ParamValue::Vec3([0.0, 1.5, 0.0])));
assert_eq!(old.text(), "0.00:1.50:0.00", "the instance keeps its value");
}
@@ -4958,7 +5042,7 @@ mod tests {
fn an_expression_result_takes_the_rows_kind() {
use crate::app::{ParamDef, ParamValue as V};
use crate::expr::Value;
- let p = |ty: &str| ParamDef::new("P", ty, "");
+ let p = |ty: &str| ParamDef::new("p", ty, "");
assert_eq!(p("toggle").value_from_expr(&Value::Num(2.0)), Some(V::Bool(true)));
assert_eq!(p("choice:UV,Icosphere,Cube").value_from_expr(&Value::Num(1.0)), Some(V::Choice("Icosphere".into())));
assert_eq!(p("choice:UV,Icosphere,Cube").value_from_expr(&Value::Str("cube".into())), Some(V::Choice("Cube".into())));
@@ -4969,12 +5053,12 @@ mod tests {
// Through the resolver: the clone's parameter holds the typed value.
let mut node = crate::app::load_fs_tree().children.into_iter().find(|t| t.node_type == "cull").unwrap();
- node.params.retain(|q| q.name != "Invert");
- node.params.push(ParamDef::new("Invert", "toggle", "1 + 1").as_expr());
+ node.params.retain(|q| q.name != "invert");
+ node.params.push(ParamDef::new("invert", "toggle", "1 + 1").as_expr());
let root = FsNode { children: vec![node.clone()], ..crate::app::load_fs_tree() };
let mut err = None;
let resolved = crate::geometry::resolve_param_refs(&root, &root.children[0], 1, &mut err).unwrap();
- let inv = resolved.params.iter().find(|q| q.name == "Invert").unwrap();
+ let inv = resolved.params.iter().find(|q| q.name == "invert").unwrap();
assert_eq!((inv.value(), inv.text(), inv.is_expr()), (Some(&V::Bool(true)), "true", false));
assert!(err.is_none(), "{err:?}");
}
@@ -4989,20 +5073,20 @@ mod tests {
let mut redraw = false;
s.apply_action(crate::app::McpAction::AddNode { template_name: "Cull".into(), name: None, x: 9.0, y: 9.0 }, &mut redraw).unwrap();
let slot = s.current_dir().children.iter().position(|c| c.node_type == "cull").unwrap();
- let threshold = |s: &State| crate::geometry::node_param_str(&s.current_dir().children[slot], "Threshold", "");
+ let threshold = |s: &State| crate::geometry::node_param_str(&s.current_dir().children[slot], "threshold", "");
let before = threshold(&s);
let set = |s: &mut State, v: &str| {
- s.apply_action(crate::app::McpAction::SetParam { slot, name: "Threshold".into(), value: v.into() }, &mut false)
+ s.apply_action(crate::app::McpAction::SetParam { slot, name: "threshold".into(), value: v.into() }, &mut false)
};
let err = set(&mut s, "abc").unwrap_err();
- assert!(err.contains("Threshold") && err.contains("not a number"), "{err}");
+ assert!(err.contains("threshold") && err.contains("not a number"), "{err}");
assert_eq!(threshold(&s), before, "nothing was written");
set(&mut s, "0.7").unwrap();
assert_eq!(threshold(&s), "0.7");
- set(&mut s, "ch(\"../x/Radius\")").expect("an expression is not checked as a value");
- assert!(s.current_dir().children[slot].params.iter().find(|p| p.name == "Threshold").unwrap().is_expr());
+ set(&mut s, "ch(\"../x/radius\")").expect("an expression is not checked as a value");
+ assert!(s.current_dir().children[slot].params.iter().find(|p| p.name == "threshold").unwrap().is_expr());
set(&mut s, "0.7").unwrap();
- s.current_dir_mut().children[slot].params.iter_mut().find(|p| p.name == "Threshold").unwrap().set_expr(false);
+ s.current_dir_mut().children[slot].params.iter_mut().find(|p| p.name == "threshold").unwrap().set_expr(false);
// The pane.
s.apply_action(crate::app::McpAction::Select { slot }, &mut redraw).unwrap();
@@ -5063,14 +5147,14 @@ mod tests {
let mut redraw = false;
a.apply_action(crate::app::McpAction::AddNode { template_name: "Cull".into(), name: None, x: 9.0, y: 9.0 }, &mut redraw).unwrap();
let slot = a.current_dir().children.iter().position(|c| c.node_type == "cull").unwrap();
- a.current_dir_mut().children[slot].params.iter_mut().find(|p| p.name == "Threshold").unwrap().set_text("half");
+ a.current_dir_mut().children[slot].params.iter_mut().find(|p| p.name == "threshold").unwrap().set_text("half");
a.save_to_file(&dir).expect("save");
let mut b = State::new(false);
b.load_from_file(&dir).expect("load");
assert!(b.last_status_text.contains("Threshold") && b.last_status_text.contains("not a number"), "{}", b.last_status_text);
let kept = b.current_dir().children.iter().find(|c| c.node_type == "cull").unwrap();
- assert_eq!(crate::geometry::node_param_str(kept, "Threshold", ""), "half", "kept verbatim");
+ assert_eq!(crate::geometry::node_param_str(kept, "threshold", ""), "half", "kept verbatim");
let _ = fs::remove_dir_all(&dir);
}
@@ -5097,10 +5181,10 @@ mod tests {
// Found anywhere in a template's tree, children included.
let mut t = crate::app::load_fs_tree().children.into_iter().find(|t| t.name == "Embryo").unwrap();
assert!(crate::app::unknown_param_kinds(&t).is_empty());
- t.children[0].params.push(crate::app::ParamDef::new("Count", "int", "1"));
+ t.children[0].params.push(crate::app::ParamDef::new("count", "int", "1"));
let bad = crate::app::unknown_param_kinds(&t);
assert_eq!(bad.len(), 1);
- assert_eq!((bad[0].1.as_str(), bad[0].2.as_str()), ("Count", "int"));
+ assert_eq!((bad[0].1.as_str(), bad[0].2.as_str()), ("count", "int"));
}
/// Every shipped template's every parameter names a kind — walked from
@@ -5144,56 +5228,56 @@ mod tests {
.kind()
};
for (ty, name) in [
- ("switch", "Input 2"), ("switch", "Input 3"), ("switch", "Input 4"),
- ("boolean", "With"), ("collision", "Collider"), ("relax", "Rest"),
- ("suture", "Against"), ("copy", "To"), ("distance", "To"), ("transfer", "From"), ("remesh", "From"), ("project", "Surface"),
+ ("switch", "input_2"), ("switch", "input_3"), ("switch", "input_4"),
+ ("boolean", "with"), ("collision", "collider"), ("relax", "rest"),
+ ("suture", "against"), ("copy", "to"), ("distance", "to"), ("transfer", "from"), ("remesh", "from"), ("project", "surface"),
] {
assert_eq!(kind(ty, name), K::Node, "{ty}'s {name}");
}
- for (ty, name) in [("grid", "Center"), ("polygon", "Center"), ("soft_transform", "Center"), ("soft_transform", "Translation")] {
+ for (ty, name) in [("grid", "center"), ("polygon", "center"), ("soft_transform", "center"), ("soft_transform", "translation")] {
assert_eq!(kind(ty, name), K::Float3, "{ty}'s {name}");
}
for (ty, name) in [
- ("cull", "Threshold"), ("group", "Threshold"), ("copy", "Scale"), ("visualize", "From"), ("visualize", "To"),
- ("attribute", "From Min"), ("attribute", "From Max"), ("attribute", "To Min"), ("attribute", "To Max"),
+ ("cull", "threshold"), ("group", "threshold"), ("copy", "scale"), ("visualize", "from"), ("visualize", "to"),
+ ("attribute", "from_min"), ("attribute", "from_max"), ("attribute", "to_min"), ("attribute", "to_max"),
] {
assert_eq!(kind(ty, name), K::Float, "{ty}'s {name}");
}
- assert_eq!(kind("neighbour", "Constant"), K::Float3);
+ assert_eq!(kind("neighbour", "constant"), K::Float3);
// The pull's Strength is a slider over none-to-double, one in the
// middle: a number set by hand and by eye, where the `float` it
// was for a day is a box to type into. It still takes an
// expression, shown as text like any other.
let strength = root.children.iter().find(|t| t.node_type == "attribute").unwrap()
- .params.iter().find(|p| p.name == "Strength").expect("attribute has a Strength");
+ .params.iter().find(|p| p.name == "strength").expect("attribute has a Strength");
assert_eq!(strength.kind(), K::Slider);
assert_eq!(strength.range().map(|(lo, hi, _)| (lo, hi)), Some((0.0, 2.0)));
assert_eq!(strength.text(), "1.00");
for (ty, name) in [
- ("attribute", "Attribute Name"), ("attribute", "Source B"), ("visualize", "Attribute"), ("neighbour", "Attribute"),
- ("neighbour", "Direction"), ("neighbour", "Source"), ("distance", "Direction"), ("develop", "Source"),
- ("copy", "Scale Attribute"), ("normal", "Attribute"), ("suture", "Counter"), ("time", "Attribute"),
+ ("attribute", "attribute_name"), ("attribute", "source_b"), ("visualize", "attribute"), ("neighbour", "attribute"),
+ ("neighbour", "direction"), ("neighbour", "source"), ("distance", "direction"), ("develop", "source"),
+ ("copy", "scale_attribute"), ("normal", "attribute"), ("suture", "counter"), ("time", "attribute"),
] {
assert_eq!(kind(ty, name), K::Attribute, "{ty}'s {name}");
}
for (ty, name) in [
- ("attribute", "Group"), ("group", "Group Name"), ("group", "Source Group"), ("relax", "Pin Group"),
- ("collision", "Group Name"), ("wrangle", "Group"), ("visualize", "Group"), ("cull", "Group"),
+ ("attribute", "group"), ("group", "group_name"), ("group", "source_group"), ("relax", "pin_group"),
+ ("collision", "group_name"), ("wrangle", "group"), ("visualize", "group"), ("cull", "group"),
] {
assert_eq!(kind(ty, name), K::Group, "{ty}'s {name}");
}
// Every row called Group, on every template, names a group.
for t in &root.children {
- for p in t.params.iter().filter(|p| p.name == "Group") {
+ for p in t.params.iter().filter(|p| p.name == "group") {
assert_eq!(p.kind(), K::Group, "{}'s Group", t.name);
}
}
- for (ty, name) in [("attribute", "Value"), ("transfer", "Attributes"), ("transfer", "Groups"), ("remesh", "Attributes"), ("remesh", "Groups"), ("simnet", "Start Frame"), ("bounds", "Prefix")] {
+ for (ty, name) in [("attribute", "value"), ("transfer", "attributes"), ("transfer", "groups"), ("remesh", "attributes"), ("remesh", "groups"), ("simnet", "start_frame"), ("bounds", "prefix")] {
assert_eq!(kind(ty, name), K::Text, "{ty}'s {name} stays text on purpose");
}
// Every template's Input is a wire, top level and composed children alike.
fn inputs(n: &FsNode, out: &mut Vec<(String, crate::app::ParamKind)>) {
- for p in n.params.iter().filter(|p| p.name == "Input") {
+ for p in n.params.iter().filter(|p| p.name == "input") {
out.push((n.name.clone(), p.kind()));
}
n.children.iter().for_each(|c| inputs(c, out));
@@ -5213,13 +5297,13 @@ mod tests {
let templates_root = crate::app::load_fs_tree();
let templates = crate::app::flatten_node_templates(&templates_root);
let mut relax = templates_root.children.iter().find(|t| t.node_type == "relax").unwrap().clone();
- for p in relax.params.iter_mut().filter(|p| p.name == "Input" || p.name == "Rest") {
+ for p in relax.params.iter_mut().filter(|p| p.name == "input" || p.name == "rest") {
p.set_type("text");
p.set_text("sphere1");
}
let mut root = FsNode { children: vec![relax], ..templates_root.clone() };
crate::app::merge_template_defs(&mut root, &templates);
- for name in ["Input", "Rest"] {
+ for name in ["input", "rest"] {
let p = root.children[0].params.iter().find(|p| p.name == name).unwrap();
assert_eq!(p.kind(), crate::app::ParamKind::Node, "{name}");
assert_eq!(p.text(), "sphere1", "the value is the instance's");
@@ -5232,7 +5316,7 @@ mod tests {
/// `add_pick_lists` has candidates to offer.
#[test]
fn node_and_float_rows_show_as_text() {
- let row = |ty: &str| crate::app::ParamDef::new("X", ty, "1");
+ let row = |ty: &str| crate::app::ParamDef::new("x", ty, "1");
let shown = crate::app::param_display(&[row("node"), row("float"), row("string"), row("attribute"), row("group"), row("toggle")]);
let types: Vec<&str> = shown.iter().map(|r| r.2.as_str()).collect();
// The leading wire is the inputs, a separator under it.
@@ -5257,22 +5341,22 @@ mod tests {
crate::app::param_display(ps).into_iter().map(|r| if r.2 == "separator" { "|".to_string() } else { r.0 }).collect()
};
let params = vec![
- p("Input", "node", "", ""),
- p("With", "node", "", ""),
- p("Mode", "choice:A,B", "", ""),
- p("Rest", "node", "", ""),
- p("Size", "float", "shape", ""),
- p("Sides", "float", "shape", "Mode == B"),
- p("Hidden", "float", "extra", "Mode == B"),
- p("Group", "group", "where", ""),
+ p("input", "node", "", ""),
+ p("with", "node", "", ""),
+ p("mode", "choice:A,B", "", ""),
+ p("rest", "node", "", ""),
+ p("size", "float", "shape", ""),
+ p("sides", "float", "shape", "mode == B"),
+ p("Hidden", "float", "extra", "mode == B"),
+ p("group", "group", "where", ""),
];
- assert_eq!(rows(¶ms), ["Input", "With", "|", "Mode", "Rest", "|", "Size", "|", "Group"]);
+ assert_eq!(rows(¶ms), ["input", "with", "|", "mode", "rest", "|", "size", "|", "group"]);
// A node with no wires and no groups has no line at all.
assert_eq!(rows(&[p("A", "float", "", ""), p("B", "float", "", "")]), ["A", "B"]);
// A template-named group on a wire takes it out of the inputs.
- assert_eq!(rows(&[p("Input", "node", "", ""), p("Surface", "node", "x", ""), p("Size", "float", "x", "")]), ["Input", "|", "Surface", "Size"]);
+ assert_eq!(rows(&[p("input", "node", "", ""), p("surface", "node", "x", ""), p("size", "float", "x", "")]), ["input", "|", "surface", "size"]);
// The group is the template's: it rides the merge, never the file.
- let mut inst = crate::app::ParamDef::new("Size", "float", "2");
+ let mut inst = crate::app::ParamDef::new("size", "float", "2");
inst.adopt_ui_from(¶ms[4]);
assert_eq!(inst.group, "shape");
assert!(!serde_json::to_string(&inst).unwrap().contains("shape"), "not written");
@@ -5285,19 +5369,19 @@ mod tests {
fn node_param_bool_reads_a_toggle_and_falls_back_on_anything_else() {
use crate::geometry::node_param_bool;
let node = |v: &str| FsNode {
- params: vec![crate::app::ParamDef::new("On", "toggle", v)],
+ params: vec![crate::app::ParamDef::new("on", "toggle", v)],
..crate::app::load_fs_tree()
};
for v in ["true", "TRUE", " True ", "1", "on"] {
- assert!(node_param_bool(&node(v), "On", false), "{v:?}");
+ assert!(node_param_bool(&node(v), "on", false), "{v:?}");
}
for v in ["false", "False", "0", "off"] {
- assert!(!node_param_bool(&node(v), "On", true), "{v:?}");
+ assert!(!node_param_bool(&node(v), "on", true), "{v:?}");
}
for v in ["", "yes please", "2"] {
- assert!(node_param_bool(&node(v), "On", true) && !node_param_bool(&node(v), "On", false), "{v:?}");
+ assert!(node_param_bool(&node(v), "on", true) && !node_param_bool(&node(v), "on", false), "{v:?}");
}
- assert!(node_param_bool(&node("true"), "Missing", true) && !node_param_bool(&node("true"), "Missing", false));
+ assert!(node_param_bool(&node("true"), "missing", true) && !node_param_bool(&node("true"), "missing", false));
}
/// MCP's add_param refuses a type that names no kind, and says which do.
@@ -5307,12 +5391,12 @@ mod tests {
let mut redraw = false;
let before = state.current_dir().children[0].params.len();
let err = state
- .apply_action(crate::app::McpAction::AddParam { slot: 0, name: "N".into(), param_type: "int".into(), default: "1".into() }, &mut redraw)
+ .apply_action(crate::app::McpAction::AddParam { slot: 0, name: "n".into(), param_type: "int".into(), default: "1".into(), label: String::new() }, &mut redraw)
.unwrap_err();
assert!(err.contains("int") && err.contains("spinbox"), "{err}");
assert_eq!(state.current_dir().children[0].params.len(), before);
state
- .apply_action(crate::app::McpAction::AddParam { slot: 0, name: "N".into(), param_type: "spinbox".into(), default: "1".into() }, &mut redraw)
+ .apply_action(crate::app::McpAction::AddParam { slot: 0, name: "n".into(), param_type: "spinbox".into(), default: "1".into(), label: String::new() }, &mut redraw)
.expect("a known kind is added");
}
@@ -5325,15 +5409,15 @@ mod tests {
let templates = crate::app::flatten_node_templates(&templates_root);
let t = templates_root.children.iter().find(|t| t.node_type == "camera").unwrap();
let names: Vec<&str> = t.params.iter().map(|p| p.name.as_str()).collect();
- assert_eq!(names, ["Position", "Rotation", "Pivot"]);
+ assert_eq!(names, ["position", "rotation", "pivot"]);
let mut old = t.clone();
- old.params.push(crate::app::ParamDef::new("Square Aspect", "toggle", "true"));
- old.params.push(crate::app::ParamDef::new("Show Camera Pivot", "toggle", "true"));
+ old.params.push(crate::app::ParamDef::new("square_aspect", "toggle", "true"));
+ old.params.push(crate::app::ParamDef::new("show_camera_pivot", "toggle", "true"));
let mut root = templates_root.clone();
root.children = vec![old];
crate::app::merge_template_defs(&mut root, &templates);
let names: Vec<&str> = root.children[0].params.iter().map(|p| p.name.as_str()).collect();
- assert_eq!(names, ["Position", "Rotation", "Pivot"]);
+ assert_eq!(names, ["position", "rotation", "pivot"]);
}
/// Visualize's Blend has no Mix: it was Set under another name, Opacity
@@ -5344,17 +5428,17 @@ mod tests {
let templates_root = crate::app::load_fs_tree();
let templates = crate::app::flatten_node_templates(&templates_root);
let t = templates_root.children.iter().find(|t| t.node_type == "visualize").unwrap();
- let blend = t.params.iter().find(|p| p.name == "Blend").unwrap();
+ let blend = t.params.iter().find(|p| p.name == "blend").unwrap();
assert_eq!(blend.choice_options(), vec!["Set", "Multiply", "Add"]);
let mut old = t.clone();
- let p = old.params.iter_mut().find(|p| p.name == "Blend").unwrap();
+ let p = old.params.iter_mut().find(|p| p.name == "blend").unwrap();
p.set_type("choice:Set,Mix,Multiply,Add");
p.set_text("Mix".to_string());
assert!(p.invalid().is_none(), "the old row took Mix");
let mut root = templates_root.clone();
root.children = vec![old];
crate::app::merge_template_defs(&mut root, &templates);
- let p = root.children[0].params.iter().find(|p| p.name == "Blend").unwrap();
+ let p = root.children[0].params.iter().find(|p| p.name == "blend").unwrap();
assert_eq!(p.text(), "Set");
assert!(p.invalid().is_none(), "{:?}", p.invalid());
}
@@ -5374,7 +5458,7 @@ mod tests {
name: "opencl1".to_string(),
node_type: "opencl".to_string(),
children: vec![],
- params: vec![crate::app::ParamDef::new("Code", "code", "OLD KERNEL")],
+ params: vec![crate::app::ParamDef::new("code", "code", "OLD KERNEL")],
geometry_visible: true,
bypassed: false,
position: (4.0, 2.0),
@@ -5384,13 +5468,13 @@ mod tests {
let mut output1 = output_t.clone();
output1.id = "s_output1".to_string();
output1.name = "output1".to_string();
- output1.params.iter_mut().find(|p| p.name == "Input").unwrap().set_text("opencl1".to_string());
+ output1.params.iter_mut().find(|p| p.name == "input").unwrap().set_text("opencl1".to_string());
let old_sphere = FsNode {
id: "s".to_string(),
name: "Sphere 3".to_string(),
node_type: "node".to_string(),
children: vec![opencl1, output1],
- params: vec![crate::app::ParamDef::new("Radius", "slider", "0.70")],
+ params: vec![crate::app::ParamDef::new("radius", "slider", "0.70")],
geometry_visible: true,
bypassed: false,
position: (3.0, 1.0),
@@ -5402,8 +5486,8 @@ mod tests {
let mut old_group = group_t.clone();
old_group.id = "g".to_string();
old_group.name = "My Region".to_string(); // renamed: native nodes match by TYPE
- old_group.params.retain(|p| p.name != "Highlight");
- old_group.params.iter_mut().find(|p| p.name == "Center").unwrap().set_text("0.00:0.80:0.00".to_string());
+ old_group.params.retain(|p| p.name != "highlight");
+ old_group.params.iter_mut().find(|p| p.name == "center").unwrap().set_text("0.00:0.80:0.00".to_string());
// A hand-built subnet that happens to share the Sphere name.
let lookalike = FsNode {
@@ -5441,16 +5525,16 @@ mod tests {
assert_eq!((s.node_type.as_str(), s.id.as_str(), s.name.as_str(), s.position), ("sphere", "s", "Sphere 3", (3.0, 1.0)));
assert!(s.children.is_empty(), "the opencl and output children go");
let names: Vec<&str> = s.params.iter().map(|p| p.name.as_str()).collect();
- assert_eq!(names, ["Method", "Radius", "Rows", "Columns", "Frequency", "Resolution", "Center X", "Center Y", "Center Z", "Color"]);
- assert_eq!(s.params.iter().find(|p| p.name == "Radius").unwrap().text(), "0.70", "instance value survives");
+ assert_eq!(names, ["method", "radius", "rows", "columns", "frequency", "resolution", "center_x", "center_y", "center_z", "color"]);
+ assert_eq!(s.params.iter().find(|p| p.name == "radius").unwrap().text(), "0.70", "instance value survives");
// And the merged instance evaluates with the new controls live.
let mut merged_sphere_root = root.clone();
merged_sphere_root.children.truncate(1);
merged_sphere_root.children[0].params.iter_mut()
- .find(|p| p.name == "Rows").unwrap().set_text("4".to_string());
+ .find(|p| p.name == "rows").unwrap().set_text("4".to_string());
merged_sphere_root.children[0].params.iter_mut()
- .find(|p| p.name == "Columns").unwrap().set_text("6".to_string());
+ .find(|p| p.name == "columns").unwrap().set_text("6".to_string());
let mut visited = Vec::new();
let mut err = None;
let mut cache = crate::geometry::SimCache::default();
@@ -5466,8 +5550,8 @@ mod tests {
// Group (renamed, matched by type): Highlight restored, value kept.
let g = &root.children[1];
- assert!(g.params.iter().any(|p| p.name == "Highlight" && p.text() == "true"));
- assert_eq!(g.params.iter().find(|p| p.name == "Center").unwrap().text(), "0.00:0.80:0.00");
+ assert!(g.params.iter().any(|p| p.name == "highlight" && p.text() == "true"));
+ assert_eq!(g.params.iter().find(|p| p.name == "center").unwrap().text(), "0.00:0.80:0.00");
// Lookalike: untouched — no params gained, no children injected.
let l = &root.children[2];
@@ -5523,12 +5607,12 @@ mod tests {
assert_eq!(build(&[]).num_points(), crate::geometry::sphere_point_len(16, 24));
// A coarse 4x6 tessellation.
- let coarse = build(&[("Rows", "4"), ("Columns", "6")]);
+ let coarse = build(&[("rows", "4"), ("columns", "6")]);
assert_eq!(coarse.num_points(), crate::geometry::sphere_point_len(4, 6));
// Center X shifts the whole sphere: default spans x in [-0.5, 0.5],
// shifted spans [0.5, 1.5].
- let shifted = build(&[("Center X", "1.0")]);
+ let shifted = build(&[("center_x", "1.0")]);
let (mut min_x, mut max_x) = (f32::MAX, f32::MIN);
for pos in shifted.positions() {
min_x = min_x.min(pos[0]);
@@ -5539,7 +5623,7 @@ mod tests {
// Degenerate resolutions clamp instead of emitting nothing.
assert_eq!(
- build(&[("Rows", "0"), ("Columns", "0")]).num_points(),
+ build(&[("rows", "0"), ("columns", "0")]).num_points(),
crate::geometry::sphere_point_len(2, 3)
);
}
@@ -5557,16 +5641,16 @@ mod tests {
fn sphere_method_builds_a_uv_ico_or_cube_sphere() {
let templates_root = crate::app::load_fs_tree();
let sphere_t = templates_root.children.iter().find(|t| t.name == "Sphere").unwrap();
- let method = sphere_t.params.iter().find(|p| p.name == "Method").expect("a Method dropdown");
+ let method = sphere_t.params.iter().find(|p| p.name == "method").expect("a Method dropdown");
assert_eq!(method.ty(), "choice:UV,Icosphere,Cube");
assert_eq!(method.text(), "UV", "the default stays the sphere every saved project was built with");
- assert_eq!(sphere_t.params[0].name, "Method", "the method heads the pane, above the radius it governs");
+ assert_eq!(sphere_t.params[0].name, "method", "the method heads the pane, above the radius it governs");
// And it heads the pane of a sphere SAVED before it existed too: the
// bundled project's sphere1 gains it through the loader's merge, at
// the template's position rather than below Color.
let state = State::new(false);
let saved = state.current_dir().children.iter().find(|c| c.name == "sphere1").expect("the bundled sphere1");
- assert_eq!(saved.params[0].name, "Method", "merged order: {:?}", saved.params.iter().map(|p| &p.name).collect::<Vec<_>>());
+ assert_eq!(saved.params[0].name, "method", "merged order: {:?}", saved.params.iter().map(|p| &p.name).collect::<Vec<_>>());
let build = |params: &[(&str, &str)]| {
let mut inst = sphere_t.clone();
inst.id = "s".to_string();
@@ -5609,11 +5693,11 @@ mod tests {
}
};
- let uv = build(&[("Method", "UV")]);
+ let uv = build(&[("method", "UV")]);
assert_eq!(uv.num_points(), crate::geometry::sphere_point_len(16, 24));
for (freq, expect) in [("1", 12), ("2", 42), ("4", 162), ("7", 492)] {
- let ico = build(&[("Method", "Icosphere"), ("Frequency", freq)]);
+ let ico = build(&[("method", "Icosphere"), ("frequency", freq)]);
assert_eq!(ico.num_points(), expect, "icosphere at frequency {freq}");
assert_eq!(ico.num_prims(), 20 * freq.parse::<usize>().unwrap().pow(2));
assert!(ico.is_closed(), "icosphere at frequency {freq} is not closed");
@@ -5621,7 +5705,7 @@ mod tests {
}
for (res, expect) in [("1", 8), ("3", 56), ("8", 386)] {
- let cube = build(&[("Method", "Cube"), ("Resolution", res), ("Radius", "0.8")]);
+ let cube = build(&[("method", "Cube"), ("resolution", res), ("radius", "0.8")]);
assert_eq!(cube.num_points(), expect, "cube sphere at resolution {res}");
assert_eq!(cube.num_prims(), 6 * res.parse::<usize>().unwrap().pow(2), "one quad per cell — the kernel fanned them");
assert!(cube.is_closed(), "cube sphere at resolution {res} is not closed");
@@ -5629,7 +5713,7 @@ mod tests {
}
// The out-of-range guards: a frequency of 0 builds the icosahedron.
- assert_eq!(build(&[("Method", "Icosphere"), ("Frequency", "0")]).num_points(), 12);
+ assert_eq!(build(&[("method", "Icosphere"), ("frequency", "0")]).num_points(), 12);
}
/// `param_number` is what a kernel's `chi()` reads: a choice is its
@@ -5637,7 +5721,7 @@ mod tests {
#[test]
fn a_choice_reads_as_its_option_index_from_a_kernel() {
use crate::geometry::param_number;
- let p = |ty: &str, val: &str| crate::app::ParamDef::new("X", ty, val);
+ let p = |ty: &str, val: &str| crate::app::ParamDef::new("x", ty, val);
assert_eq!(param_number(&p("choice:UV,Icosphere,Cube", "Cube")), 2.0);
assert_eq!(param_number(&p("choice:UV,Icosphere,Cube", "icosphere")), 1.0, "case-insensitive, like the reference path");
assert_eq!(param_number(&p("choice:UV,Icosphere,Cube", "Nope")), 0.0, "an unknown option is the first");
@@ -5674,12 +5758,12 @@ mod tests {
node_type: "node".to_string(),
children: vec![
instance(find("Sphere"), "s", "Sphere 1", &[]),
- instance(find("Scatter"), "sc", "Scatter 1", &[("Input", "Sphere 1")]),
+ instance(find("Scatter"), "sc", "Scatter 1", &[("input", "Sphere 1")]),
instance(find("Attribute"), "a", "Attr 1", &[
- ("Input", "Scatter 1"),
- ("Operation", "Create"),
- ("Attribute Name", "mass"),
- ("Value", "1.00"),
+ ("input", "Scatter 1"),
+ ("operation", "Create"),
+ ("attribute_name", "mass"),
+ ("value", "1.00"),
]),
],
params: vec![],
@@ -5783,14 +5867,14 @@ mod tests {
assert!((max_z - 0.5).abs() < 0.01, "Expected half-length 0.5 on Z, got {}", max_z);
// Width and Length size their axes independently.
- let geom_2 = generate(&[("Width", "2.0"), ("Length", "3.0")], "plane_inst_2");
+ let geom_2 = generate(&[("width", "2.0"), ("length", "3.0")], "plane_inst_2");
let max_x_2 = geom_2.positions().iter().map(|p| p[0].abs()).fold(0.0f32, f32::max);
let max_z_2 = geom_2.positions().iter().map(|p| p[2].abs()).fold(0.0f32, f32::max);
assert!((max_x_2 - 1.0).abs() < 0.01, "Expected half-width 1.0 on X, got {}", max_x_2);
assert!((max_z_2 - 1.5).abs() < 0.01, "Expected half-length 1.5 on Z, got {}", max_z_2);
// Columns/Rows control the cell counts per axis.
- let geom_3 = generate(&[("Columns", "4"), ("Rows", "8")], "plane_inst_3");
+ let geom_3 = generate(&[("columns", "4"), ("rows", "8")], "plane_inst_3");
assert_eq!(geom_3.num_points(), 5 * 9, "a 4x8 cell grid is 5x9 points");
}
@@ -6494,7 +6578,7 @@ mod tests {
"1.0",
vec![phase3_node(
"grid",
- &[("Rows", "4"), ("Columns", "6"), ("Width", "2.00"), ("Length", "3.00")],
+ &[("rows", "4"), ("columns", "6"), ("width", "2.00"), ("length", "3.00")],
)],
);
let (g, err) = eval_node(&root, "grid 1");
@@ -6530,7 +6614,7 @@ mod tests {
#[test]
fn test_polygon_is_four_create_operators_with_one_parameter_varying() {
let build = |params: &[(&str, &str)]| {
- let mut ps = vec![("Radius", "1.00")];
+ let mut ps = vec![("radius", "1.00")];
ps.extend_from_slice(params);
let root = modelling_root("1.0", vec![phase3_node("polygon", &ps)]);
eval_node(&root, "polygon 1").0
@@ -6538,14 +6622,14 @@ mod tests {
// Three sides is a triangle, four a square, thirty-two a circle: the
// same shape with one number changed.
- let tri = build(&[("Sides", "3")]);
+ let tri = build(&[("sides", "3")]);
assert_eq!(tri.num_prims(), 3, "a filled triangle is three fan triangles");
assert_eq!(tri.num_points(), 4, "three corners and a hub");
- let circle = build(&[("Sides", "32")]);
+ let circle = build(&[("sides", "32")]);
assert_eq!(circle.num_points(), 33);
// A circle's corners all sit at the radius; a square's do too.
- for d in [&circle, &build(&[("Sides", "4")])] {
+ for d in [&circle, &build(&[("sides", "4")])] {
for p in 0..d.num_points() {
let r = d.pos(p).length();
assert!(r < 1.0 + 1e-4, "point {p} is outside the radius at {r}");
@@ -6561,7 +6645,7 @@ mod tests {
// A star alternates the two radii, so it has twice the corners and
// half of them sit on the inner circle.
- let star = build(&[("Sides", "5"), ("Inner Radius", "0.40")]);
+ let star = build(&[("sides", "5"), ("inner_radius", "0.40")]);
assert_eq!(star.num_prims(), 10);
let inner = (0..star.num_points())
.filter(|&p| (star.pos(p).length() - 0.4).abs() < 1e-4)
@@ -6582,7 +6666,7 @@ mod tests {
// Unfilled is the outline: one two-point primitive per edge, a closed
// loop, and nothing to shade.
- let ring = build(&[("Sides", "6"), ("Fill", "false")]);
+ let ring = build(&[("sides", "6"), ("fill", "false")]);
assert_eq!(ring.num_points(), 6, "no hub when there is no fill");
assert_eq!(ring.num_prims(), 6);
assert_eq!(ring.edges().len(), 6, "the outline closes");
@@ -6601,14 +6685,14 @@ mod tests {
fn test_a_row_shows_only_when_its_condition_holds() {
use crate::app::{param_display, param_visible};
let params = vec![
- pd("Mode", "Twist", ""),
- pd("Angle", "1.0", "Mode == Twist"),
- pd("Bend Axis", "Y", "Mode == Bend"),
- pd("Shared", "x", "Mode == Twist|Bend"),
- pd("Not Bleed", "x", "Mode != Bleed"),
+ pd("mode", "Twist", ""),
+ pd("angle", "1.0", "mode == Twist"),
+ pd("bend_axis", "Y", "mode == Bend"),
+ pd("shared", "x", "mode == Twist|Bend"),
+ pd("not_bleed", "x", "mode != Bleed"),
];
let shown: Vec<String> = param_display(¶ms).into_iter().map(|r| r.0).collect();
- assert_eq!(shown, vec!["Mode", "Angle", "Shared", "Not Bleed"]);
+ assert_eq!(shown, vec!["mode", "angle", "shared", "not_bleed"]);
// Flip the driving parameter and a different set applies. This is the
// whole point: collapsing fifty operators into ten traded node count
@@ -6617,30 +6701,30 @@ mod tests {
let mut bent = params.clone();
bent[0].set_text("Bend");
let shown: Vec<String> = param_display(&bent).into_iter().map(|r| r.0).collect();
- assert_eq!(shown, vec!["Mode", "Bend Axis", "Shared", "Not Bleed"]);
+ assert_eq!(shown, vec!["mode", "bend_axis", "shared", "not_bleed"]);
// Bleed matches none of the conditions, so only the driving row is
// left — which is a node with one relevant control showing one.
let mut bleeding = params.clone();
bleeding[0].set_text("Bleed");
let shown: Vec<String> = param_display(&bleeding).into_iter().map(|r| r.0).collect();
- assert_eq!(shown, vec!["Mode"]);
+ assert_eq!(shown, vec!["mode"]);
// The condition is evaluated against siblings' CURRENT values, which
// is where this app keeps them.
- assert!(param_visible(¶ms, "Mode == Twist"));
- assert!(!param_visible(&bleeding, "Mode == Twist"));
+ assert!(param_visible(¶ms, "mode == Twist"));
+ assert!(!param_visible(&bleeding, "mode == Twist"));
}
#[test]
fn test_conditions_and_together_and_compare_without_case() {
use crate::app::param_visible;
let params = vec![
- pd("Mode", "Align", ""),
- pd("Target", "Constant", ""),
+ pd("mode", "Align", ""),
+ pd("target", "Constant", ""),
];
- assert!(param_visible(¶ms, "Mode == Align && Target == Constant"));
- assert!(!param_visible(¶ms, "Mode == Align && Target == Attribute"));
+ assert!(param_visible(¶ms, "mode == Align && target == Constant"));
+ assert!(!param_visible(¶ms, "mode == Align && target == Attribute"));
// Case does not matter: a template author writing `twist` and a choice
// reading `Twist` is not a bug worth having.
assert!(param_visible(¶ms, "mode == ALIGN"));
@@ -6652,13 +6736,13 @@ mod tests {
#[test]
fn test_a_broken_condition_hides_its_row_rather_than_hiding_the_mistake() {
use crate::app::param_visible;
- let params = vec![pd("Mode", "Twist", "")];
+ let params = vec![pd("mode", "Twist", "")];
// A misspelled sibling, and a clause that is not a comparison at all.
// Both are template bugs; showing the row unconditionally would let
// them pass unnoticed, and the row going missing is a complaint you
// can act on.
assert!(!param_visible(¶ms, "Moed == Twist"));
- assert!(!param_visible(¶ms, "Mode"));
+ assert!(!param_visible(¶ms, "mode"));
assert!(!param_visible(¶ms, "Mode ~ Twist"));
}
@@ -6711,8 +6795,8 @@ mod tests {
// user who sets a Remap range, switches to Clip and switches back must
// find their numbers still there.
let mut params = vec![
- pd("Operation", "Remap", ""),
- pd("To Max", "7.5", "Operation == Remap"),
+ pd("operation", "Remap", ""),
+ pd("to_max", "7.5", "operation == Remap"),
];
assert_eq!(param_display(¶ms).len(), 2);
params[0].set_text("Clip");
@@ -6866,9 +6950,9 @@ mod tests {
"page1",
"page",
&[
- ("Preset", "Letter"),
- ("Resolution", "72"),
- ("Color", "1.00:1.00:1.00"),
+ ("preset", "Letter"),
+ ("resolution", "72"),
+ ("color", "1.00:1.00:1.00"),
],
),
pnode(
@@ -6876,18 +6960,18 @@ mod tests {
"grid1",
"page_grid",
&[
- ("Input", "page1"),
- ("Cell Size", "0.5"),
- ("Line Width", "0.02"),
- ("Line Color", "0.00:0.00:0.00"),
- ("Fill Cells", "false"),
+ ("input", "page1"),
+ ("cell_size", "0.5"),
+ ("line_width", "0.02"),
+ ("line_color", "0.00:0.00:0.00"),
+ ("fill_cells", "false"),
],
),
pnode(
"b",
"border1",
"page_border",
- &[("Input", "grid1"), ("Width", "0.1"), ("Inset", "0.25"), ("Color", "1.00:0.00:0.00")],
+ &[("input", "grid1"), ("width", "0.1"), ("inset", "0.25"), ("color", "1.00:0.00:0.00")],
),
];
@@ -6908,7 +6992,7 @@ mod tests {
// An orphan composite is not a page: a border with nothing under it
// resolves to nothing rather than inventing a sheet.
- let orphan = pnode("o", "border2", "page_border", &[("Input", "nothing")]);
+ let orphan = pnode("o", "border2", "page_border", &[("input", "nothing")]);
let mut lone = root.clone();
lone.children.push(orphan);
assert!(
@@ -6918,7 +7002,7 @@ mod tests {
// A cycle terminates rather than recursing forever.
let mut looped = root.clone();
- looped.children[0] = pnode("p", "page1", "page_border", &[("Input", "border1")]);
+ looped.children[0] = pnode("p", "page1", "page_border", &[("input", "border1")]);
assert!(resolve_page(&looped, &looped.children[2], &mut Vec::new()).is_none());
// The level's LAST visible page node is what gets displayed.
@@ -7397,12 +7481,12 @@ mod tests {
state.graph_mut().set_selected_node(Some(slot));
let default = {
let dir = state.current_dir();
- state.template_default(dir, &dir.children[slot], "Radius").expect("no Radius default").text().to_string()
+ state.template_default(dir, &dir.children[slot], "radius").expect("no Radius default").text().to_string()
};
let radius = |state: &State| {
- state.current_dir().children[slot].params.iter().find(|p| p.name == "Radius").unwrap().text().to_string()
+ state.current_dir().children[slot].params.iter().find(|p| p.name == "radius").unwrap().text().to_string()
};
- state.current_dir_mut().children[slot].params.iter_mut().find(|p| p.name == "Radius").unwrap().set_text("3.25".to_string());
+ state.current_dir_mut().children[slot].params.iter_mut().find(|p| p.name == "radius").unwrap().set_text("3.25".to_string());
assert!(state.run_command("reset_parameters"));
assert_eq!(radius(&state), default);
assert!(crate::command::by_id("custom_preset").is_none(), "the custom preset is retired");
@@ -7434,8 +7518,8 @@ mod tests {
};
{
let node = &mut state.current_dir_mut().children[slot];
- node.params.iter_mut().find(|p| p.name == "Radius").unwrap().set_text("3.25".to_string());
- let rows = node.params.iter_mut().find(|p| p.name == "Rows").unwrap();
+ node.params.iter_mut().find(|p| p.name == "radius").unwrap().set_text("3.25".to_string());
+ let rows = node.params.iter_mut().find(|p| p.name == "rows").unwrap();
rows.set_text("$F + 4".to_string());
rows.set_expr(true);
}
@@ -7512,8 +7596,8 @@ mod tests {
state.param_mut().set_display_params(&rows);
state.sync_parameters_to_project();
};
- let radius = param(&state, "Radius");
- let rows = param(&state, "Rows");
+ let radius = param(&state, "radius");
+ let rows = param(&state, "rows");
// One drag: three motions, one step.
for v in ["1.10", "1.20", "1.30"] {
@@ -7534,18 +7618,18 @@ mod tests {
assert_eq!(state.edit_history.undo_len(), 3);
assert!(state.run_command("undo"));
- assert_eq!(param(&state, "Rows"), rows);
- assert_eq!(param(&state, "Radius").0, "1.50", "undoing Rows left Radius alone");
+ assert_eq!(param(&state, "rows"), rows);
+ assert_eq!(param(&state, "radius").0, "1.50", "undoing Rows left Radius alone");
assert!(state.run_command("undo"));
- assert_eq!(param(&state, "Radius").0, "1.30");
+ assert_eq!(param(&state, "radius").0, "1.30");
assert!(state.run_command("undo"));
- assert_eq!(param(&state, "Radius"), radius);
+ assert_eq!(param(&state, "radius"), radius);
assert!(!state.history_step(true), "three steps were recorded");
let shown = state.param().node_params().into_iter().find(|r| r.0 == "Radius").unwrap().1;
assert_eq!(shown, radius.0, "the pane shows the restored value");
for want in ["1.30", "1.50"] {
assert!(state.run_command("redo"));
- assert_eq!(param(&state, "Radius").0, want);
+ assert_eq!(param(&state, "radius").0, want);
}
// An edit after an undo forks: what was undone is not redone over it.
assert!(state.run_command("undo"));
@@ -7553,29 +7637,29 @@ mod tests {
pane(&mut state, "Radius", "2.00");
assert!(!state.history_step(false), "a new edit left the redo branch standing");
assert!(state.run_command("undo"));
- assert_eq!(param(&state, "Radius").0, "1.30");
+ assert_eq!(param(&state, "radius").0, "1.30");
// A step restores what it changed and nothing else on the node.
pane(&mut state, "Radius", "2.50");
- state.current_dir_mut().children[slot].params.iter_mut().find(|p| p.name == "Rows").unwrap().set_text("21".to_string());
+ state.current_dir_mut().children[slot].params.iter_mut().find(|p| p.name == "rows").unwrap().set_text("21".to_string());
assert!(state.run_command("undo"));
- assert_eq!(param(&state, "Radius").0, "1.30");
- assert_eq!(param(&state, "Rows").0, "21", "undoing Radius took back an edit to Rows");
+ assert_eq!(param(&state, "radius").0, "1.30");
+ assert_eq!(param(&state, "rows").0, "21", "undoing Radius took back an edit to Rows");
// set_param, and one that is refused.
let before = state.edit_history.undo_len();
- state.apply_action(McpAction::SetParam { slot, name: "Radius".into(), value: "3.00".into() }, &mut redraw).unwrap();
- assert!(state.apply_action(McpAction::SetParam { slot, name: "Radius".into(), value: "abc".into() }, &mut redraw).is_err());
+ state.apply_action(McpAction::SetParam { slot, name: "radius".into(), value: "3.00".into() }, &mut redraw).unwrap();
+ assert!(state.apply_action(McpAction::SetParam { slot, name: "radius".into(), value: "abc".into() }, &mut redraw).is_err());
assert_eq!(state.edit_history.undo_len(), before + 1, "a refused value recorded a step");
assert!(state.run_command("undo"));
- assert_eq!(param(&state, "Radius").0, "1.30");
+ assert_eq!(param(&state, "radius").0, "1.30");
// The row menu: the expression flag is part of what comes back.
- state.run_param_action(&node_id, "Radius", ParamMenuAction::EditExpression);
- assert!(param(&state, "Radius").1);
- state.run_param_action(&node_id, "Radius", ParamMenuAction::CopyParameter);
+ state.run_param_action(&node_id, "radius", ParamMenuAction::EditExpression);
+ assert!(param(&state, "radius").1);
+ state.run_param_action(&node_id, "radius", ParamMenuAction::CopyParameter);
assert!(state.run_command("undo"));
- assert_eq!(param(&state, "Radius"), ("1.30".to_string(), false), "Copy Parameter is no edit, and Edit Expression is one");
+ assert_eq!(param(&state, "radius"), ("1.30".to_string(), false), "Copy Parameter is no edit, and Edit Expression is one");
}
/// Adding, deleting, moving and wiring nodes can be taken back, in the
@@ -7594,7 +7678,7 @@ mod tests {
.children
.iter()
.map(|c| {
- let input = c.params.iter().find(|p| p.name == "Input").map(|p| p.text().to_string()).unwrap_or_default();
+ let input = c.params.iter().find(|p| p.name == "input").map(|p| p.text().to_string()).unwrap_or_default();
(c.name.clone(), c.position, input, c.geometry_visible, c.bypassed)
})
.collect()
@@ -7612,7 +7696,7 @@ mod tests {
let name = added[slot].0.clone();
// Wire it to the sphere, by the parameter, and move it.
- state.apply_action(McpAction::SetParam { slot, name: "Radius".into(), value: "1".into() }, &mut redraw).ok();
+ state.apply_action(McpAction::SetParam { slot, name: "radius".into(), value: "1".into() }, &mut redraw).ok();
let steps = state.edit_history.undo_len();
state.current_dir_mut().children[slot].position = (12.0, 9.0);
state.record_structure_changes();
@@ -7692,17 +7776,17 @@ mod tests {
.current_dir()
.children
.iter()
- .position(|c| c.params.iter().any(|p| p.name == "Input" && p.kind() == crate::app::ParamKind::Node))
+ .position(|c| c.params.iter().any(|p| p.name == "input" && p.kind() == crate::app::ParamKind::Node))
.expect("a Normal node has an Input");
let input = |state: &State| {
- state.current_dir().children[wired].params.iter().find(|p| p.name == "Input").unwrap().text().to_string()
+ state.current_dir().children[wired].params.iter().find(|p| p.name == "input").unwrap().text().to_string()
};
let was = input(&state);
- state.current_dir_mut().children[wired].params.iter_mut().find(|p| p.name == "Input").unwrap().set_text("camera1".to_string());
+ state.current_dir_mut().children[wired].params.iter_mut().find(|p| p.name == "input").unwrap().set_text("camera1".to_string());
state.record_structure_changes();
assert_eq!(state.edit_history.undo_len(), 1);
state
- .apply_action(McpAction::SetParam { slot: wired, name: "Input".into(), value: String::new() }, &mut redraw)
+ .apply_action(McpAction::SetParam { slot: wired, name: "input".into(), value: String::new() }, &mut redraw)
.unwrap();
assert_eq!(state.edit_history.undo_len(), 2, "a wire set as a parameter is one step, not two");
assert!(state.run_command("undo"));
@@ -7738,9 +7822,9 @@ mod tests {
let old = state.current_dir().children[sphere].name.clone();
let normal = add(&mut state, "Normal", 3.0);
let embryo = add(&mut state, "Embryo", 5.0);
- state.apply_action(McpAction::SetParam { slot: normal, name: "Input".into(), value: old.clone() }, &mut redraw).unwrap();
+ state.apply_action(McpAction::SetParam { slot: normal, name: "input".into(), value: old.clone() }, &mut redraw).unwrap();
// An expression a level down, reaching up and across to the sphere.
- let reference = format!("ch(\"../../{old}/Radius\") * 2");
+ let reference = format!("ch(\"../../{old}/radius\") * 2");
let inside = state.current_dir().children[embryo]
.children
.iter()
@@ -7762,7 +7846,7 @@ mod tests {
let dir = state.current_dir();
(
dir.children[sphere].name.clone(),
- dir.children[normal].params.iter().find(|p| p.name == "Input").unwrap().text().to_string(),
+ dir.children[normal].params.iter().find(|p| p.name == "input").unwrap().text().to_string(),
dir.children[embryo].children[inside].params[0].text().to_string(),
state.active_camera.clone(),
state.viewport().active_camera.clone(),
@@ -7776,7 +7860,7 @@ mod tests {
let after = names(&state);
assert_eq!(after.0, "ball");
assert_eq!(after.1, "ball", "the wire followed the rename");
- assert!(after.2.contains("../../ball/Radius"), "{}", after.2);
+ assert!(after.2.contains("../../ball/radius"), "{}", after.2);
assert_eq!((after.3.as_str(), after.4.as_str()), ("lens", "lens"), "both copies of the camera's name");
assert_eq!(state.edit_history.undo_len(), 2, "each rename is one step, its wires with it");
@@ -7936,7 +8020,7 @@ mod tests {
// A group of five points on the sphere, shown.
state.apply_action(McpAction::AddNode { template_name: "Group".into(), name: Some("tagged".into()), x: 7.0, y: 8.0 }, &mut redraw).unwrap();
let tagged = state.current_dir().children.iter().position(|c| c.name == "tagged").unwrap();
- for (name, value) in [("Input", "sphere1"), ("Group Name", "five"), ("Mode", "Random"), ("Count", "5")] {
+ for (name, value) in [("input", "sphere1"), ("group_name", "five"), ("mode", "Random"), ("count", "5")] {
state.apply_action(McpAction::SetParam { slot: tagged, name: name.into(), value: value.into() }, &mut redraw).unwrap();
}
state.current_dir_mut().set_child_geometry_visible(tagged, true);
@@ -7975,7 +8059,7 @@ mod tests {
let sphere = state.current_dir().children.iter().position(|c| c.name == "sphere1").unwrap();
let far = |state: &State| state.marked_group_verts.iter().map(|v| (v.position[0].powi(2) + v.position[2].powi(2)).sqrt()).fold(0.0f32, f32::max);
let before = far(&state);
- state.apply_action(McpAction::SetParam { slot: sphere, name: "Radius".into(), value: "2.0".into() }, &mut redraw).unwrap();
+ state.apply_action(McpAction::SetParam { slot: sphere, name: "radius".into(), value: "2.0".into() }, &mut redraw).unwrap();
assert!(far(&state) > before * 1.5, "{} against {before}", far(&state));
assert_eq!(state.marked_group_verts.len(), one);
@@ -8224,8 +8308,8 @@ mod tests {
state.cursor_y = sy(0.0);
assert!(state.viewer_tool_drag_motion());
assert!(state.viewer_tool_release());
- assert_eq!(param(&state, "Center"), "0.00:0.00:0.00", "the centre moved");
- assert_eq!(param(&state, "Translation"), "0.40:0.00:0.00");
+ assert_eq!(param(&state, "center"), "0.00:0.00:0.00", "the centre moved");
+ assert_eq!(param(&state, "translation"), "0.40:0.00:0.00");
// Fixed handles: a press on empty space adds nothing, and Delete
// removes nothing — a third handle would mean nothing.
@@ -8246,14 +8330,14 @@ mod tests {
state.cursor_y = sy(0.0);
assert!(state.viewer_tool_drag_motion());
assert!(state.viewer_tool_release());
- assert_eq!(param(&state, "Center"), "0.10:0.00:0.00");
- assert_eq!(param(&state, "Translation"), "0.30:0.00:0.00", "the tip should not have moved");
+ assert_eq!(param(&state, "center"), "0.10:0.00:0.00");
+ assert_eq!(param(&state, "translation"), "0.30:0.00:0.00", "the tip should not have moved");
// And undo restores BOTH parameters, which is the case a "keep the
// translation when the centre moves" rule would have broken.
assert!(state.viewer_tool_undo());
- assert_eq!(param(&state, "Center"), "0.00:0.00:0.00");
- assert_eq!(param(&state, "Translation"), "0.40:0.00:0.00");
+ assert_eq!(param(&state, "center"), "0.00:0.00:0.00");
+ assert_eq!(param(&state, "translation"), "0.40:0.00:0.00");
}
/// Snapping rounds a dragged handle to a world increment, and only when it
@@ -8276,7 +8360,7 @@ mod tests {
let points = |state: &State| {
crate::geometry::parse_curve_points(&crate::geometry::node_param_str(
&state.current_dir().children[slot],
- "Points",
+ "points",
"",
))
};
@@ -8374,12 +8458,12 @@ mod tests {
let base = s.current_dir().children.len();
// A column of its own, clear of the bundled project's nodes: the
// chain's shape from the report, Output at the bottom.
- for (t, y) in [("Attribute", 3.0), ("Relax", 5.0), ("Detangle", 6.0), ("Output", 7.0)] {
+ for (t, y) in [("attribute", 3.0), ("relax", 5.0), ("Detangle", 6.0), ("Output", 7.0)] {
s.apply_action(McpAction::AddNode { template_name: t.into(), name: None, x: 13.0, y }, &mut r).unwrap();
}
let names: Vec<String> = s.current_dir().children[base..].iter().map(|c| c.name.clone()).collect();
for i in 1..4 {
- s.apply_action(McpAction::SetParam { slot: base + i, name: "Input".into(), value: names[i - 1].clone() }, &mut r)
+ s.apply_action(McpAction::SetParam { slot: base + i, name: "input".into(), value: names[i - 1].clone() }, &mut r)
.unwrap();
}
s.focused_pane = LEFT_MENUBAR_IDX;
@@ -8409,7 +8493,7 @@ mod tests {
s.param_mut().set_display_params(&rows);
s.sync_parameters_to_project();
let relax = &s.current_dir().children[base + 1];
- assert_eq!(crate::geometry::node_param_str(relax, "Iterations", ""), "12");
+ assert_eq!(crate::geometry::node_param_str(relax, "iterations", ""), "12");
}
#[test]
@@ -8610,7 +8694,7 @@ mod tests {
// The Remesh's From naming a node outside: not on this level, no line.
state.apply_action(McpAction::Up, &mut redraw).unwrap();
- state.current_dir_mut().children[slot].params.iter_mut().find(|p| p.name == "From").unwrap().set_text("elsewhere");
+ state.current_dir_mut().children[slot].params.iter_mut().find(|p| p.name == "from").unwrap().set_text("elsewhere");
state.apply_action(McpAction::Enter { slot }, &mut redraw).unwrap();
state.sync_nodes();
let level = state.graph().get_nodes();
@@ -8623,8 +8707,8 @@ mod tests {
let id = switch.id.clone();
assert!(state.connect_port(&path, &id, "input1".into(), 2));
let sw = state.current_dir().children.iter().find(|c| c.id == id).unwrap();
- assert_eq!(sw.params.iter().find(|p| p.name == "Input 3").unwrap().text(), "input1");
- assert_eq!(sw.params.iter().find(|p| p.name == "Input").unwrap().text(), "repeat1", "the Input is untouched");
+ assert_eq!(sw.params.iter().find(|p| p.name == "input_3").unwrap().text(), "input1");
+ assert_eq!(sw.params.iter().find(|p| p.name == "input").unwrap().text(), "repeat1", "the Input is untouched");
// A second operand sets the row, not the column.
use crate::layout::{arrange, LayoutNode};
@@ -8949,7 +9033,7 @@ mod tests {
let mut redraw = false;
let sphere = state.current_dir().children.iter().position(|c| c.name.starts_with("sphere")).expect("a sphere");
state
- .apply_action(crate::app::McpAction::SetParam { slot: sphere, name: "Radius".into(), value: "0.7".into() }, &mut redraw)
+ .apply_action(crate::app::McpAction::SetParam { slot: sphere, name: "radius".into(), value: "0.7".into() }, &mut redraw)
.expect("set a sphere param");
assert_eq!(flag(&state), !before, "{command} was undone by a parameter edit");
}
@@ -9194,33 +9278,33 @@ mod tests {
crate::app::node_wires(n)
};
state.current_dir_mut().children = vec![
- ref_node("a", "a", "sphere", vec![("Radius", "float", "1")], vec![]),
- ref_node("b", "b", "transform", vec![("Input", "node", "a")], vec![]),
- ref_node("c", "c", "transform", vec![("Input", "node", "b")], vec![]),
- ref_node("d", "d", "boolean", vec![("Input", "node", "c"), ("With", "node", "b")], vec![]),
+ ref_node("a", "a", "sphere", vec![("radius", "float", "1")], vec![]),
+ ref_node("b", "b", "transform", vec![("input", "node", "a")], vec![]),
+ ref_node("c", "c", "transform", vec![("input", "node", "b")], vec![]),
+ ref_node("d", "d", "boolean", vec![("input", "node", "c"), ("with", "node", "b")], vec![]),
];
state.sync_nodes();
state.record_structure_changes();
assert!(state.delete_node(1));
state.record_structure_changes();
- assert_eq!(wires(&state, "c"), vec![("Input".to_string(), "a".to_string())], "A -> C");
+ assert_eq!(wires(&state, "c"), vec![("input".to_string(), "a".to_string())], "A -> C");
assert_eq!(
wires(&state, "d"),
- vec![("Input".to_string(), "c".to_string()), ("With".to_string(), "a".to_string())],
+ vec![("input".to_string(), "c".to_string()), ("with".to_string(), "a".to_string())],
"a second operand follows too"
);
// A generator has nothing to splice: what read it is left as it was.
assert!(state.delete_node(0));
- assert_eq!(wires(&state, "c"), vec![("Input".to_string(), "a".to_string())]);
+ assert_eq!(wires(&state, "c"), vec![("input".to_string(), "a".to_string())]);
// Undo puts the node back and the wires with it.
state.record_structure_changes();
assert!(state.run_command("undo"));
assert!(state.run_command("undo"));
- assert_eq!(wires(&state, "c"), vec![("Input".to_string(), "b".to_string())]);
- assert_eq!(wires(&state, "d")[1], ("With".to_string(), "b".to_string()));
+ assert_eq!(wires(&state, "c"), vec![("input".to_string(), "b".to_string())]);
+ assert_eq!(wires(&state, "d")[1], ("with".to_string(), "b".to_string()));
}
fn ref_node(id: &str, name: &str, node_type: &str, params: Vec<(&str, &str, &str)>, children: Vec<FsNode>) -> FsNode {
@@ -9274,10 +9358,10 @@ mod tests {
impl Scope for Table {
fn channel(&mut self, path: &str, kind: ChKind) -> Result<Value, String> {
let v = match path {
- "../Radius" => Value::Num(0.5),
- "../sphere1/Rows" => Value::Num(16.0),
- "Mode" => Value::Num(1.0),
- "../text1/Font" => Value::Str("Inter".into()),
+ "../radius" => Value::Num(0.5),
+ "../sphere1/rows" => Value::Num(16.0),
+ "mode" => Value::Num(1.0),
+ "../text1/font" => Value::Str("Inter".into()),
_ => return Err(format!("no {path}")),
};
Ok(match kind {
@@ -9296,10 +9380,10 @@ mod tests {
assert_eq!(ev("2 ^ 3 ^ 2"), Value::Num(512.0), "power is right-associative");
assert_eq!(ev("-2 ^ 2"), Value::Num(-4.0), "unary minus binds looser than power, as in Houdini");
assert_eq!(ev("7 % 4"), Value::Num(3.0));
- assert_eq!(ev("ch(\"../Radius\") * 2 + 1"), Value::Num(2.0));
- assert_eq!(ev("chi(\"../sphere1/Rows\") / 3"), Value::Num(16.0 / 3.0));
- assert_eq!(ev("chb(\"Mode\")"), Value::Num(1.0));
- assert_eq!(ev("chs(\"../text1/Font\") + \" Bold\""), Value::Str("Inter Bold".into()));
+ assert_eq!(ev("ch(\"../radius\") * 2 + 1"), Value::Num(2.0));
+ assert_eq!(ev("chi(\"../sphere1/rows\") / 3"), Value::Num(16.0 / 3.0));
+ assert_eq!(ev("chb(\"mode\")"), Value::Num(1.0));
+ assert_eq!(ev("chs(\"../text1/font\") + \" Bold\""), Value::Str("Inter Bold".into()));
assert_eq!(ev("$F / 24"), Value::Num(0.5));
assert_eq!(ev("if($F > 10, 1, 0)"), Value::Num(1.0));
assert_eq!(ev("$F > 10 && $F < 20"), Value::Num(1.0));
@@ -9334,15 +9418,15 @@ mod tests {
#[test]
fn a_typed_reference_is_an_expression_and_a_kernel_is_not() {
use crate::expr::looks_like_expression;
- assert!(looks_like_expression("ch(\"../sphere1/Radius\")"));
- assert!(looks_like_expression("chf(\"../Radius\") * 2"));
+ assert!(looks_like_expression("ch(\"../sphere1/radius\")"));
+ assert!(looks_like_expression("chf(\"../radius\") * 2"));
assert!(looks_like_expression("$F / 24"));
assert!(!looks_like_expression("1 + 2"), "arithmetic alone is asked for through Edit Expression");
assert!(!looks_like_expression("0.5"));
assert!(!looks_like_expression("sphere1"));
assert!(!looks_like_expression("true"));
assert!(!looks_like_expression("0.00:0.80:0.00"));
- assert!(!looks_like_expression("float r = chf(\"Radius\", 0.5);"), "a kernel is not a reference");
+ assert!(!looks_like_expression("float r = chf(\"radius\", 0.5);"), "a kernel is not a reference");
let kernel = std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/nodes/sphere.json")).unwrap();
assert!(!looks_like_expression(&kernel));
}
@@ -9353,9 +9437,9 @@ mod tests {
#[test]
fn rename_rewrites_the_paths_through_a_node() {
use crate::expr::rewrite_paths;
- let src = "ch( \"../sphere1/Radius\" ) * chs('../text1/Font') + chf(\"/sphere1/Rows\")";
+ let src = "ch( \"../sphere1/radius\" ) * chs('../text1/Font') + chf(\"/sphere1/rows\")";
let out = rewrite_paths(src, |path| path.contains("sphere1").then(|| path.replace("sphere1", "ball")));
- assert_eq!(out, "ch( \"../ball/Radius\" ) * chs('../text1/Font') + chf(\"/ball/Rows\")");
+ assert_eq!(out, "ch( \"../ball/radius\" ) * chs('../text1/Font') + chf(\"/ball/rows\")");
assert_eq!(rewrite_paths("touch(\"x\")", |_| Some("no".into())), "touch(\"x\")", "only channel calls are paths");
}
@@ -9389,29 +9473,31 @@ mod tests {
proj.root.children[0].children[0].params[0].set_expr(false);
proj.migrate_format();
let r = &proj.root.children[0].children[0].params[0];
- assert_eq!(r.text(), "chf(\"../Size\")");
+ // Format 0 → 1 gives the reference its parent; 3 → 4 the name.
+ assert_eq!(r.text(), "chf(\"../size\")");
+ assert_eq!(r.name, "radius");
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.root.children[0].children[0].params[0].set_text("chf(\"radius\")");
proj.migrate_format();
- assert_eq!(proj.root.children[0].children[0].params[0].text(), "chf(\"Radius\")");
+ assert_eq!(proj.root.children[0].children[0].params[0].text(), "chf(\"radius\")");
}
#[test]
fn param_references_resolve_against_the_enclosing_subnet() {
use crate::geometry::resolve_param_refs;
- let sphere = ref_node("s", "sphere1", "sphere", vec![("Radius", "slider", "ch(\"../Radius\")")], vec![]);
- let output = ref_node("o", "output1", "output", vec![("Input", "text", "sphere1")], vec![]);
+ let sphere = ref_node("s", "sphere1", "sphere", vec![("radius", "slider", "ch(\"../radius\")")], vec![]);
+ let output = ref_node("o", "output1", "output", vec![("input", "text", "sphere1")], vec![]);
let inner = ref_node("sub", "shape1", "node",
- vec![("Radius", "slider", "chf(\"../Size\")"), ("Mode", "choice:Basic,Scatter", "Scatter"), ("On", "toggle", "true")],
+ vec![("radius", "slider", "chf(\"../size\")"), ("mode", "choice:Basic,Scatter", "Scatter"), ("On", "toggle", "true")],
vec![sphere, output]);
let probe = ref_node("p", "probe", "switch",
- vec![("Index", "spinbox", "chi(\"../Mode\")"), ("Flag", "text", "chb(\"../On\")"), ("Name", "text", "chs(\"../Mode\")"), ("Plain", "text", "kept")],
+ vec![("index", "spinbox", "chi(\"../mode\")"), ("Flag", "text", "chb(\"../on\")"), ("Name", "text", "chs(\"../mode\")"), ("Plain", "text", "kept")],
vec![]);
let mut inner = inner;
inner.children.push(probe);
- let outer = ref_node("outer", "outer1", "node", vec![("Size", "slider", "0.8")], vec![inner]);
+ let outer = ref_node("outer", "outer1", "node", vec![("size", "slider", "0.8")], vec![inner]);
let root = ref_node("root", "root", "node", vec![], vec![outer]);
// The probe's params, resolved against shape1.
@@ -9420,13 +9506,13 @@ mod tests {
let resolved = resolve_param_refs(&root, probe, 0, &mut err).expect("it has references");
assert!(err.is_none(), "{err:?}");
let get = |n: &str| resolved.params.iter().find(|p| p.name == n).unwrap().text().to_string();
- assert_eq!(get("Index"), "1", "chi on a choice is its option index");
+ assert_eq!(get("index"), "1", "chi on a choice is its option index");
assert_eq!(get("Flag"), "1", "chb into a text row is 1 or 0");
assert_eq!(get("Name"), "Scatter");
assert_eq!(get("Plain"), "kept");
// Evaluated, the sphere's Radius chains: sphere1 → shape1's Radius,
- // which is itself chf("../Size") → outer1's 0.8.
+ // which is itself chf("../size") → outer1's 0.8.
let shape = &root.children[0].children[0];
let (g, err) = eval(&root, shape);
assert!(err.is_none(), "{err:?}");
@@ -9438,15 +9524,15 @@ mod tests {
assert!(none.is_none());
// A reference to nothing is reported, and the value left as written.
- let bad = ref_node("b", "bad1", "sphere", vec![("Radius", "slider", "ch(\"../Nope\")")], vec![]);
+ let bad = ref_node("b", "bad1", "sphere", vec![("radius", "slider", "ch(\"../nope\")")], vec![]);
let holder = ref_node("h", "holder1", "node", vec![], vec![bad]);
let root2 = ref_node("root", "root", "node", vec![], vec![holder]);
let mut err = None;
let r = resolve_param_refs(&root2, &root2.children[0].children[0], 0, &mut err).unwrap();
- assert_eq!(r.params[0].text(), "ch(\"../Nope\")");
- assert!(err.as_deref().unwrap_or("").contains("names no parameter Nope on holder1"), "{err:?}");
+ assert_eq!(r.params[0].text(), "ch(\"../nope\")");
+ assert!(err.as_deref().unwrap_or("").contains("names no parameter nope on holder1"), "{err:?}");
// Too many levels up, likewise.
- let far = ref_node("f", "far1", "sphere", vec![("Radius", "slider", "ch(\"../../../X\")")], vec![]);
+ let far = ref_node("f", "far1", "sphere", vec![("radius", "slider", "ch(\"../../../x\")")], vec![]);
let root3 = ref_node("root", "root", "node", vec![], vec![far]);
let mut err = None;
resolve_param_refs(&root3, &root3.children[0], 0, &mut err);
@@ -9461,69 +9547,69 @@ mod tests {
#[test]
fn channel_paths_resolve_over_the_tree() {
use crate::geometry::resolve_param_refs;
- let a = ref_node("a", "a1", "sphere", vec![("Radius", "slider", "0.25"), ("Center", "float3", "1:2:3")], vec![]);
+ let a = ref_node("a", "a1", "sphere", vec![("radius", "slider", "0.25"), ("center", "float3", "1:2:3")], vec![]);
let b = ref_node("b", "b1", "sphere", vec![
- ("Radius", "slider", "ch(\"../a1/Radius\") * 2"),
- ("Rows", "spinbox", "ch(\"Radius\") * 100"),
- ("Y", "slider", "ch(\"../a1/Center.y\") + ch(\"/sub1/a1/Center.z\")"),
+ ("radius", "slider", "ch(\"../a1/radius\") * 2"),
+ ("rows", "spinbox", "ch(\"radius\") * 100"),
+ ("y", "slider", "ch(\"../a1/center.y\") + ch(\"/sub1/a1/center.z\")"),
("Frame", "slider", "$F / 2"),
- ("Up", "slider", "ch(\"../Size\") + ch(\"/Top\")"),
- ("Mode", "choice:Basic,Scatter", "1"),
- ("On", "toggle", "ch(\"../a1/Radius\") > 0"),
- ("Label", "text", "chs(\"../a1/Radius\") + \" units\""),
- ("Center", "float3", "chf(\"../a1/Center.x\"):0:ch(\"../Size\")"),
+ ("Up", "slider", "ch(\"../size\") + ch(\"/top\")"),
+ ("mode", "choice:Basic,Scatter", "1"),
+ ("On", "toggle", "ch(\"../a1/radius\") > 0"),
+ ("Label", "text", "chs(\"../a1/radius\") + \" units\""),
+ ("center", "float3", "chf(\"../a1/center.x\"):0:ch(\"../size\")"),
], vec![]);
- let sub = ref_node("sub", "sub1", "node", vec![("Size", "slider", "0.5")], vec![a, b]);
+ let sub = ref_node("sub", "sub1", "node", vec![("size", "slider", "0.5")], vec![a, b]);
let root = ref_node("root", "root", "node", vec![("Top", "slider", "10")], vec![sub]);
// The choice's value is an index written as an expression; flag it.
let mut root = root;
- root.children[0].children[1].params.iter_mut().find(|p| p.name == "Mode").unwrap().set_expr(true);
+ root.children[0].children[1].params.iter_mut().find(|p| p.name == "mode").unwrap().set_expr(true);
let b = &root.children[0].children[1];
let mut err = None;
let r = resolve_param_refs(&root, b, 12, &mut err).expect("b1 has expressions");
assert!(err.is_none(), "{err:?}");
let get = |n: &str| r.params.iter().find(|p| p.name == n).unwrap().text().to_string();
- assert_eq!(get("Radius"), "0.5", "a sibling by path");
- assert_eq!(get("Rows"), "50", "a bare name is the node's OWN parameter, read through its expression");
- assert_eq!(get("Y"), "5", "components, relative and absolute");
+ assert_eq!(get("radius"), "0.5", "a sibling by path");
+ assert_eq!(get("rows"), "50", "a bare name is the node's OWN parameter, read through its expression");
+ assert_eq!(get("y"), "5", "components, relative and absolute");
assert_eq!(get("Frame"), "6");
assert_eq!(get("Up"), "10.5", "the parent and the root");
- assert_eq!(get("Mode"), "Scatter", "a number into a choice picks the option");
+ assert_eq!(get("mode"), "Scatter", "a number into a choice picks the option");
assert_eq!(get("On"), "true", "a number into a toggle is true or false");
assert_eq!(get("Label"), "0.25 units");
- assert_eq!(get("Center"), "1:0:0.5", "a float3 is three expressions");
+ assert_eq!(get("center"), "1:0:0.5", "a float3 is three expressions");
assert!(r.params.iter().all(|p| !p.is_expr()), "the resolved clone holds values");
// A circle: two parameters reading each other.
- let x = ref_node("x", "x1", "sphere", vec![("Radius", "slider", "ch(\"../y1/Radius\")")], vec![]);
- let y = ref_node("y", "y1", "sphere", vec![("Radius", "slider", "ch(\"../x1/Radius\") + 1")], vec![]);
+ let x = ref_node("x", "x1", "sphere", vec![("radius", "slider", "ch(\"../y1/radius\")")], vec![]);
+ let y = ref_node("y", "y1", "sphere", vec![("radius", "slider", "ch(\"../x1/radius\") + 1")], vec![]);
let ring = ref_node("root", "root", "node", vec![], vec![x, y]);
let mut err = None;
let r = resolve_param_refs(&ring, &ring.children[0], 0, &mut err).unwrap();
assert!(err.as_deref().unwrap_or("").contains("circular"), "{err:?}");
- assert_eq!(r.params[0].text(), "ch(\"../y1/Radius\")", "left as written");
+ assert_eq!(r.params[0].text(), "ch(\"../y1/radius\")", "left as written");
// A parameter reading itself is the shortest circle.
- let me = ref_node("m", "me", "sphere", vec![("Radius", "slider", "ch(\"Radius\") + 1")], vec![]);
+ let me = ref_node("m", "me", "sphere", vec![("radius", "slider", "ch(\"radius\") + 1")], vec![]);
let solo = ref_node("root", "root", "node", vec![], vec![me]);
let mut err = None;
resolve_param_refs(&solo, &solo.children[0], 0, &mut err);
assert!(err.as_deref().unwrap_or("").contains("circular"), "{err:?}");
// A path to a node that is not there names the step that failed.
- let lost = ref_node("l", "lost", "sphere", vec![("Radius", "slider", "ch(\"../nope/Radius\")")], vec![]);
+ let lost = ref_node("l", "lost", "sphere", vec![("radius", "slider", "ch(\"../nope/radius\")")], vec![]);
let root4 = ref_node("root", "root", "node", vec![], vec![lost]);
let mut err = None;
resolve_param_refs(&root4, &root4.children[0], 0, &mut err);
assert!(err.as_deref().unwrap_or("").contains("no node `nope`"), "{err:?}");
// A syntax error names the parameter.
- let broken = ref_node("k", "broken", "sphere", vec![("Radius", "slider", "1 +")], vec![]);
+ let broken = ref_node("k", "broken", "sphere", vec![("radius", "slider", "1 +")], vec![]);
let mut broken = broken;
broken.params[0].set_expr(true);
let root5 = ref_node("root", "root", "node", vec![], vec![broken]);
let mut err = None;
resolve_param_refs(&root5, &root5.children[0], 0, &mut err);
- assert!(err.as_deref().unwrap_or("").contains("broken: Radius"), "{err:?}");
+ assert!(err.as_deref().unwrap_or("").contains("broken: radius"), "{err:?}");
}
/// The paths a paste writes, and what a rename does to the paths that
@@ -9533,16 +9619,16 @@ mod tests {
#[test]
fn renaming_a_node_carries_its_references() {
use crate::geometry::{absolute_ref_path, relative_ref_path, rename_node_in_tree};
- let a = ref_node("a", "a1", "sphere", vec![("Radius", "slider", "0.25")], vec![]);
+ let a = ref_node("a", "a1", "sphere", vec![("radius", "slider", "0.25")], vec![]);
let b = ref_node("b", "b1", "sphere", vec![
- ("Radius", "slider", "ch( \"../a1/Radius\" ) * 2"),
- ("Input", "text", "a1"),
+ ("radius", "slider", "ch( \"../a1/radius\" ) * 2"),
+ ("input", "text", "a1"),
], vec![]);
- let deep = ref_node("d", "deep1", "sphere", vec![("Radius", "slider", "chf(\"/sub1/a1/Radius\") + ch(\"../../a1/Radius\")")], vec![]);
+ let deep = ref_node("d", "deep1", "sphere", vec![("radius", "slider", "chf(\"/sub1/a1/radius\") + ch(\"../../a1/radius\")")], vec![]);
let inner = ref_node("in", "inner1", "node", vec![], vec![deep]);
- let other = ref_node("oa", "a1", "sphere", vec![("Radius", "slider", "ch(\"../a1/Radius\")")], vec![]);
+ let other = ref_node("oa", "a1", "sphere", vec![("radius", "slider", "ch(\"../a1/radius\")")], vec![]);
let elsewhere = ref_node("el", "elsewhere", "node", vec![], vec![other]);
- let sub = ref_node("sub", "sub1", "node", vec![("Size", "slider", "0.5")], vec![a, b, inner]);
+ let sub = ref_node("sub", "sub1", "node", vec![("size", "slider", "0.5")], vec![a, b, inner]);
let mut root = ref_node("root", "root", "node", vec![], vec![sub, elsewhere]);
assert_eq!(relative_ref_path(&root, "b", "a").as_deref(), Some("../a1"));
@@ -9561,10 +9647,10 @@ mod tests {
node.params.iter().find(|p| p.name == n).unwrap().text().to_string()
};
assert_eq!(root.children[0].children[0].name, "ball");
- assert_eq!(get(&root, &[0, 1], "Radius"), "ch( \"../ball/Radius\" ) * 2", "spacing kept");
- assert_eq!(get(&root, &[0, 1], "Input"), "ball", "the wire follows");
- assert_eq!(get(&root, &[0, 2, 0], "Radius"), "chf(\"/sub1/ball/Radius\") + ch(\"../../ball/Radius\")");
- assert_eq!(get(&root, &[1, 0], "Radius"), "ch(\"../a1/Radius\")", "the OTHER a1 is not this one");
+ assert_eq!(get(&root, &[0, 1], "radius"), "ch( \"../ball/radius\" ) * 2", "spacing kept");
+ assert_eq!(get(&root, &[0, 1], "input"), "ball", "the wire follows");
+ assert_eq!(get(&root, &[0, 2, 0], "radius"), "chf(\"/sub1/ball/radius\") + ch(\"../../ball/radius\")");
+ assert_eq!(get(&root, &[1, 0], "radius"), "ch(\"../a1/radius\")", "the OTHER a1 is not this one");
assert!(!rename_node_in_tree(&mut root, "a", "ball"), "a rename to the same name is nothing");
assert!(!rename_node_in_tree(&mut root, "zzz", "x"), "and so is one of a node that is not there");
}
@@ -9618,33 +9704,33 @@ mod tests {
templates.iter().find(|t| t.node.name == name).unwrap().node.params.iter()
.find(|p| p.name == pname).unwrap().description.clone()
};
- let radius = template("Sphere", "Radius");
+ let radius = template("Sphere", "radius");
let want = wrap_words(&radius, PARAM_DESCRIPTION_WIDTH);
assert!(want.len() > 1, "a sentence spans rows: {want:?}");
- let (rows, _, headers) = state.param_menu_rows(sphere, "Radius");
- assert_eq!(rows[0], "Name: Radius");
- assert_eq!(&rows[1..1 + want.len()], &want[..], "the description sits under the name");
- assert_eq!(rows[1 + want.len()], "Control: slider");
- assert!(1 + want.len() < headers, "the description rows are headers, and run nothing");
+ let (rows, _, headers) = state.param_menu_rows(sphere, "radius");
+ assert_eq!(rows[..2], ["Name: radius".to_string(), "Label: Radius".to_string()], "the name a path spells, then what the pane shows");
+ assert_eq!(&rows[2..2 + want.len()], &want[..], "the description sits under the name and label");
+ assert_eq!(rows[2 + want.len()], "Control: slider");
+ assert!(2 + want.len() < headers, "the description rows are headers, and run nothing");
// A child inside a subnet template takes its base template's.
state.apply_action(McpAction::AddNode { template_name: "Embryo".into(), name: Some("embryo1".into()), x: 3.0, y: 8.0 }, &mut redraw).unwrap();
let embryo = slot_of(&state, "embryo1");
- let (rows, _, _) = state.param_menu_rows(embryo, "Radius");
- let own = wrap_words(&template("Embryo", "Radius"), PARAM_DESCRIPTION_WIDTH);
+ let (rows, _, _) = state.param_menu_rows(embryo, "radius");
+ let own = wrap_words(&template("Embryo", "radius"), PARAM_DESCRIPTION_WIDTH);
assert_ne!(own, want, "the Embryo's Radius is described as the Embryo's");
- assert_eq!(&rows[1..1 + own.len()], &own[..]);
+ assert_eq!(&rows[2..2 + own.len()], &own[..]);
state.apply_action(McpAction::Enter { slot: embryo }, &mut redraw).unwrap();
let inner = slot_of(&state, "sphere1");
- let (rows, _, _) = state.param_menu_rows(inner, "Radius");
- assert_eq!(&rows[1..1 + want.len()], &want[..], "the Embryo's sphere1 says what a Sphere's Radius does");
+ let (rows, _, _) = state.param_menu_rows(inner, "radius");
+ assert_eq!(&rows[2..2 + want.len()], &want[..], "the Embryo's sphere1 says what a Sphere's Radius does");
state.apply_action(McpAction::Up, &mut redraw).unwrap();
// A parameter no template names says nothing, and the menu goes
// straight from the name to the readouts.
- state.apply_action(McpAction::AddParam { slot: sphere, name: "Extra".into(), param_type: "float".into(), default: "3".into() }, &mut redraw).unwrap();
- let (rows, _, _) = state.param_menu_rows(sphere, "Extra");
- assert_eq!(rows[1], "Control: text box");
+ state.apply_action(McpAction::AddParam { slot: sphere, name: "extra".into(), param_type: "float".into(), default: "3".into(), label: "Extra".into() }, &mut redraw).unwrap();
+ let (rows, _, _) = state.param_menu_rows(sphere, "extra");
+ assert_eq!(rows[2], "Control: text box");
// A save never carries one, so the file is what it was.
let saved = serde_json::to_string(&state.current_dir().children[sphere]).unwrap();
@@ -9706,17 +9792,17 @@ mod tests {
};
show(&mut state, sphere);
let (x, y) = row_center(&state, "Radius");
- assert_eq!(state.param_row_at(x, y), Some((sphere, "Radius".to_string())));
+ assert_eq!(state.param_row_at(x, y), Some((sphere, "radius".to_string())), "the row's NAME, though the pane shows its label");
assert_eq!(state.param_row_at(x, state.positions[crate::slots::PARAM_IDX].1 - 5.0), None, "above the pane is no row");
state.handle_event(&WindowEvent::CursorMoved { position: LocalPosition { x: x as f64, y: y as f64 } });
state.handle_event(&WindowEvent::MouseInput { state: ElementState::Pressed, button: MouseButton::Right });
assert!(state.param_menu_open(), "a right press on a row opens its menu");
assert!(!state.viewport_menu_open());
- let radius_desc = described(&state, sphere, "Radius");
+ let radius_desc = described(&state, sphere, "radius");
assert_eq!(
state.param_menu_actions,
- vec![ParamMenuAction::Info; 9 + radius_desc.len()].into_iter().chain([ParamMenuAction::Separator, ParamMenuAction::CopyParameter, ParamMenuAction::Separator, ParamMenuAction::EditExpression]).collect::<Vec<_>>(),
+ vec![ParamMenuAction::Info; 10 + radius_desc.len()].into_iter().chain([ParamMenuAction::Separator, ParamMenuAction::CopyParameter, ParamMenuAction::Separator, ParamMenuAction::EditExpression]).collect::<Vec<_>>(),
"nothing copied yet, and the row holds a value"
);
// The header rows read the parameter out: its name, the control
@@ -9729,16 +9815,16 @@ mod tests {
let shown: Vec<String> =
cce_ui::widget::context_menu::options().into_iter().filter(|r| !radius_desc.contains(r)).collect();
assert_eq!(
- &shown[..10],
+ &shown[..11],
&[
- "Name: Radius".to_string(), "Control: slider".to_string(), "Type: float".to_string(),
+ "Name: radius".to_string(), "Label: Radius".to_string(), "Control: slider".to_string(), "Type: float".to_string(),
"Expression: false".to_string(), "Default: 0.5".to_string(), "Min: none".to_string(), "Max: none".to_string(),
"Step: none".to_string(), "Range: 0..2".to_string(), "-".to_string(),
]
);
assert!(shown.iter().all(|r| !r.starts_with("Value:")), "{shown:?}");
- let (_, headers) = fields(&state, sphere, "Radius");
- assert_eq!(headers, 9);
+ let (_, headers) = fields(&state, sphere, "radius");
+ assert_eq!(headers, 10);
// The headers are the rows before the separator; each one is
// looked up by its readout, not its position.
let headers_of = |state: &State, pname: &str| -> Vec<String> {
@@ -9748,84 +9834,84 @@ mod tests {
};
// An inline range, a spinbox's range and step, a choice's options,
// and a conditional row's condition.
- let rows = headers_of(&state, "Center X");
+ let rows = headers_of(&state, "center_x");
for want in ["Control: slider", "Type: float", "Min: -2", "Max: 2", "Step: none", "Range: -2..2"] {
assert!(rows.contains(&want.to_string()), "{want} missing from {rows:?}");
}
- let rows = headers_of(&state, "Rows");
- for want in ["Default: 16", "Min: 2", "Max: 128", "Step: 1", "Range: 2..128, step 1", "Shown when: Method == UV"] {
+ let rows = headers_of(&state, "rows");
+ for want in ["Default: 16", "Min: 2", "Max: 128", "Step: 1", "Range: 2..128, step 1", "Shown when: method == UV"] {
assert!(rows.contains(&want.to_string()), "{want} missing from {rows:?}");
}
- let rows = headers_of(&state, "Method");
+ let rows = headers_of(&state, "method");
for want in ["Control: dropdown", "Type: enum", "Default: UV", "Options: UV, Icosphere, Cube"] {
assert!(rows.contains(&want.to_string()), "{want} missing from {rows:?}");
}
- assert_eq!(headers_of(&state, "Color"), vec!["Name: Color", "Control: toggle", "Type: boolean", "Expression: false", "Default: true"], "a toggle has neither a range nor options");
- // A parameter no template names has no default row; one with a
- // label shows it under the name.
- state.apply_action(McpAction::AddParam { slot: sphere, name: "Extra".into(), param_type: "float".into(), default: "3".into() }, &mut redraw).unwrap();
+ assert_eq!(headers_of(&state, "color"), vec!["Name: color", "Label: Color", "Control: toggle", "Type: boolean", "Expression: false", "Default: true"], "a toggle has neither a range nor options");
+ // A parameter no template names has no default row, and its label
+ // is under its name as a template's is.
+ state.apply_action(McpAction::AddParam { slot: sphere, name: "extra".into(), param_type: "float".into(), default: "3".into(), label: "Extra".into() }, &mut redraw).unwrap();
// A `float` parameter is drawn as a text box, and sets a float.
- assert_eq!(headers_of(&state, "Extra"), vec!["Name: Extra", "Control: text box", "Type: float", "Expression: false"]);
- state.current_dir_mut().children[sphere].params.iter_mut().find(|p| p.name == "Extra").unwrap().label = "Extra Size".into();
- assert_eq!(headers_of(&state, "Extra")[..2], ["Name: Extra".to_string(), "Label: Extra Size".to_string()]);
+ assert_eq!(headers_of(&state, "extra"), vec!["Name: extra", "Label: Extra", "Control: text box", "Type: float", "Expression: false"]);
+ state.current_dir_mut().children[sphere].params.iter_mut().find(|p| p.name == "extra").unwrap().label = "Extra Size".into();
+ assert_eq!(headers_of(&state, "extra")[..2], ["Name: extra".to_string(), "Label: Extra Size".to_string()]);
// Inside a subnet instance the SUBNET template's override is the
// default: the Embryo's sphere1 was built with an expression.
state.apply_action(McpAction::AddNode { template_name: "Embryo".into(), name: Some("embryo1".into()), x: 3.0, y: 8.0 }, &mut redraw).unwrap();
let embryo = slot_of(&state, "embryo1");
state.apply_action(McpAction::Enter { slot: embryo }, &mut redraw).unwrap();
let inner = slot_of(&state, "sphere1");
- let (rows, h) = fields(&state, inner, "Radius");
- assert!(rows[..h].contains(&"Default: chf(\"../Radius\")".to_string()), "{rows:?}");
+ let (rows, h) = fields(&state, inner, "radius");
+ assert!(rows[..h].contains(&"Default: chf(\"../radius\")".to_string()), "{rows:?}");
state.apply_action(McpAction::Up, &mut redraw).unwrap();
// A click on a header runs nothing.
- state.run_param_action(&state.current_dir().children[sphere].id.clone(), "Radius", ParamMenuAction::Info);
+ state.run_param_action(&state.current_dir().children[sphere].id.clone(), "radius", ParamMenuAction::Info);
state.handle_event(&WindowEvent::MouseInput { state: ElementState::Released, button: MouseButton::Right });
// Copy, then paste onto ball's Radius — a sibling, so `../sphere1`.
let sphere_id = state.current_dir().children[sphere].id.clone();
let ball_id = state.current_dir().children[ball].id.clone();
- state.run_param_action(&sphere_id, "Radius", ParamMenuAction::CopyParameter);
- assert_eq!(state.copied_param, Some((sphere_id.clone(), "Radius".to_string())));
+ state.run_param_action(&sphere_id, "radius", ParamMenuAction::CopyParameter);
+ assert_eq!(state.copied_param, Some((sphere_id.clone(), "radius".to_string())));
show(&mut state, ball);
let (x, y) = row_center(&state, "Radius");
state.handle_event(&WindowEvent::CursorMoved { position: LocalPosition { x: x as f64, y: y as f64 } });
state.handle_event(&WindowEvent::MouseInput { state: ElementState::Pressed, button: MouseButton::Right });
assert!(state.param_menu_actions.contains(&ParamMenuAction::PasteRelative), "with a copy, paste is offered");
state.handle_event(&WindowEvent::MouseInput { state: ElementState::Released, button: MouseButton::Right });
- state.run_param_action(&ball_id, "Radius", ParamMenuAction::PasteRelative);
- let radius = |state: &State, slot: usize| state.current_dir().children[slot].params.iter().find(|p| p.name == "Radius").unwrap().clone();
- assert_eq!(radius(&state, ball).text(), "ch(\"../sphere1/Radius\")");
+ state.run_param_action(&ball_id, "radius", ParamMenuAction::PasteRelative);
+ let radius = |state: &State, slot: usize| state.current_dir().children[slot].params.iter().find(|p| p.name == "radius").unwrap().clone();
+ assert_eq!(radius(&state, ball).text(), "ch(\"../sphere1/radius\")");
assert!(radius(&state, ball).is_expr());
let rows = crate::app::para
diff truncated