Graph Evaluation
The Node Sculptor graph runs inside a Web Worker so heavy nodes (Marching Cubes, Mesh Boolean, dense Loft) never freeze the UI. A quick mental model of the evaluator makes debugging, optimising and template-writing a lot less mysterious.
What Triggers a Re-Evaluation
The worker kicks off a new evaluation whenever any of the following changes:
Graph topology- Adding or removing nodes and edges.
Node parameters- Any param change on any node. Rapid drag-updates on numeric fields are debounced so the worker isn't spammed every pixel.
Printer config- Layer height, nozzle width, print speed, … handed to executors via
EvalContext.printer. Template apply- An atomic replace of the config. The undo stack records a single entry, so Ctrl+Z rolls back the whole thing.
Every evaluation gets a monotonically increasing generationId. When a new evaluation starts before the previous one finishes, the running one\u2019s shouldCancel() flips to true, and long-running executors bail at their next checkpoint so you never see a stale result flash past.
Traversal Order
The evaluator performs a topological sort rooted at the Output node and runs executors in dependency order. Nodes with no path to Output are skipped entirely \u2014 they cost nothing at runtime. A few nice consequences:
Dead branches are free- Keep exploratory wiring around without worrying about performance. If it isn't feeding Output, it doesn't run.
Diamond dependencies are shared- If two downstream nodes both read from the same upstream node, that node runs once and its outputs are cached for the second reader.
Cycles are rejected- The compiler refuses any graph with a cycle and surfaces a problem on the Problems panel \u2014 the evaluator never runs, so you never get stuck in an infinite loop.
Per-node progress- After each executor finishes,
ctx.onProgress(pct)is called once; the worker posts a PROGRESS message so the status bar advances smoothly. Executors themselves don't emit intra-node progress \u2014 they either finish or cancel.
Problems Panel
Executors never throw. They report problems instead and fall back to empty / identity values so the rest of the graph keeps running. The Problems tab in the right dock lists every problem reported by the most recent evaluation.
Problem kinds
Cycle- The graph contains a cycle (some node\u2019s output eventually feeds back into its input). The evaluator rejects the whole graph and surfaces this as a single problem. Severity: error.
Missing input- A required input socket has no upstream connection and no default. The affected node returns empty / identity output so the rest keeps running. Severity: warning. Example: "Revolve needs a profile path."
Invalid parameter- A parameter is outside its declared min / max, or an enum value is unknown, or an expression contains an unresolved identifier. Severity: warning.
Type mismatch- An implicit conversion was expected but the upstream socket returned an unexpected value. Rare \u2014 usually shows up after a node upgrade or a custom executor that returns the wrong shape. Severity: error.
Execution error- An executor threw an exception. The evaluator catches it, logs the original error in the problem\u2019s message, and keeps the graph running. Severity: error.
Output unconnected- The final Output node has no input wired to either Toolpath or Preview Mesh. Non-blocking \u2014 empty exports are valid but not useful. Severity: info.
Using the Problems tab
Tab header count- The tab header shows a live count of current problems. 0 means the graph evaluated cleanly; anything higher means at least one issue was detected in the most recent run.
Row layout- Each problem shows a kind label (colour-coded: red = error, amber = warning, muted = info), an optional monospace nodeId tag, an optional socket name, the error message, and a muted hint when present.
Click-to-jump- Problems that carry a
nodeIdare clickable \u2014 clicking the row selects the offending node on the canvas and fits the viewport to it, so you can find it instantly even in a dense graph. Empty state- When the graph evaluates cleanly the tab shows a concise empty state. Nothing to click, nothing to dismiss.
No persistent history- Problems are cleared at the start of every evaluation. Stale problems from a prior graph state never linger \u2014 if it\u2019s on the panel right now, the current run produced it.
Cancellation & Long Runs
Heavy nodes check ctx.shouldCancel() at regular checkpoints in their inner loops:
Marching Cubes- Between Z slabs \u2014 so cancelling a 128\u00b3 grid is effectively free.
Mesh Subdivide- Between iterations.
Mesh Boolean- Before the BSP build, so cancellation happens up-front when it still matters.
Mesh Shell- Before the inner shell is generated.
Loft / Sweep / Revolve- Before dense surface stitching.
Spiralize Contours / Travel Optimize- Between contour groups.
Vase Spiralizer / Layer Stacker / Adaptive Layers- Between layers \u2014 cancelled runs truncate their output instead of leaving undefined slots behind.
If you\u2019re iterating on a slow graph, the worker usually cancels the previous run within milliseconds of your next parameter change. Keep dragging the slider \u2014 you\u2019ll always see the latest generation\u2019s output.
Viewing the Result
The worker hands two things back to the main thread after every evaluation:
Toolpath bundle- The segments wired into
Output.Toolpath, ready for the G-code compiler and the toolpath preview layer. This also drives the top-toolbar's "G-code" export button. Preview Mesh- The mesh wired into
Output.previewMesh, rendered in the 3D viewport as a shaded surface. The toolbar's "Export Mesh" button (STL / OBJ / GLTF) writes out the same mesh \u2014 what you see IS what you export.
The preview layer tracks which socket is connected and turns the renderer on/off automatically: a Toolpath-only graph shows polylines, a Mesh-only graph shows shaded surfaces, and wiring both overlays the polylines on top of the mesh. The toolbar\u2019s primary Export button swaps between G-code and Export Mesh to match.
Worker Robustness
A few invisible things make the worker feel trustworthy:
Finite-value guards- Scalar and vector pickers treat NaN / \u00b1Infinity as "no value" and fall back to declared defaults. You can't poison the graph by accidentally dividing by zero upstream.
Dense arrays on cancellation- Generators that pre-size their output (layer stackers, spiralisers) truncate to the number of entries they actually filled before cancelling \u2014 never leaving
undefinedslots that would crash the G-code compiler downstream. Chain-keep-alive- If a job rejects for any reason, the next one still runs. The worker never silently stops accepting jobs.
Uncaught-error surfacing- Worker-level crashes (onerror, messageerror) are posted back as structured ERROR messages, so the UI status bar can show what went wrong instead of just freezing.