@parametron/parametric
Parametric engine — node graph, expression evaluation, and model system for ParaShape.
Architecture
Model JSON
│
▼
┌──────────┐ ┌───────────┐ ┌───────────────┐
│ Schema │────▶│ Model │────▶│ NodeCollection │
│ (parse) │ │ (fromJSON) │ │ (build graph) │
└──────────┘ └───────────┘ └───────────────┘
│
▼
┌──────────────┐
│ evaluate() │
│ (pull-based) │
└──────┬───────┘
│
┌───────────┴───────────┐
▼ ▼
parameters (header) operations (body)
key → VALUE bindings ONE fold, top-down:
(resolve values) producers concat, transforms
│ process, containers nest
└───────────┬───────────┘
▼
EvaluateResult
{ geometry, materials, params }Node Graph (v21 — operations model, v22 — static asset lanes)
ONE recursive node shape, two forms discriminated by a required field:
operation { key?, label?, enabled?, method, args? } ← does something to the stream
container { key?, label?, enabled?, operations: NodeJSON[] } ← holds a list of nodesModelJSON = { id?, title, unit, camera?, materials?: MaterialJSON[], layers?: LayerJSON[], fonts?: FontJSON[], parameters?: NodeJSON[], operations?: NodeJSON[] } — parameters is the header (named VALUE bindings, order display-only), operations is the body: one flat fold executed top-down over a stream that starts empty. The root IS a container. materials/layers/fonts are static named records, not nodes — see Assets below.
The fold
container "cabinet"
├── operation rectangle ← producer: concats its result into the stream
├── operation extrude ← producer (a solid = Face[])
├── operation edgeFillet ← transform: processes the stream (map)
├── operation arrayLinear ← transform (multiply, n → m)
└── container "doors" ← nested fold: runs its own list from an
├── operation rectangle empty stream, result concats into the
└── operation curveExtrude parent stream like a producer- A producer (registry def with
create) adds[...input, ...created]— never replaces. - A transform (registry def with
compute) processes the whole stream — map / multiply (n→m>n) / reduce (n→m<n) / structural shapes as before. - An operation's
keynames the stream AFTER that step; a container'skeynames its own fold result — both referenceable from any expression. enabledfalsy: operation → bypassed (input passes through), container → contributes nothing.
Summary
| form | lane | Description |
|---|---|---|
operation, return: "value" method | parameters | Named value binding (length/number/angle/boolean/color/text/arrayLength/function/loadModel). Runtime class ParameterNode. |
| operation, entity-return method | operations | One fold step (producer or transform). Runtime class OperationNode. |
| container | either | In operations: its own fold (runtime ContainerNode + FoldRunner). In parameters: display grouping of value nodes. |
Materials/layers/fonts are static named records on the document (ModelJSON.materials/layers/fonts), not nodes — plain values, never expressions. name is unique per lane and is how an operation references the record (applyMaterial/applyLayer's name arg, textCurve's font arg). There are no createMaterial/createLayer/loadFont/loadImage methods.
Schema types: OperationJSON | ContainerJSON = NodeJSON — there is no nodeType field; the two forms are discriminated by method vs operations, and the value-vs-entity lane by the method's registry return.
Method naming
Every method is globally unique across the whole registry — a domain prefix convention (curve* / face* / edge* / group* / solid*) keeps the names collision-free (curveUnion vs solidUnion), so the engine dispatches by the bare method alone. Transform families:
- Whole-stream transforms:
move,rotate,resize,mirror,arrayLinear,arrayRadial,arrayCurve,arrayDistances, propsapplyMaterial/applyLayer/visible/name - Curve:
curveReverse,curveSplit,curveFillet,curveChamfer,curveOffset,curveThicken,curveExtrude(kind-changing: Curve[] → Face[] solid),curveUnion/curveSubtract/curveIntersect(2D boolean, binary — needscurve),curveSelfUnion(no-operand reduce) - Face/solid:
faceReverse,solidUnion/solidSubtract/solidIntersect(3D CSG, binary — needssolid),solidSelfUnion,faceExtrude,faceOffset,edgeFillet,edgeChamfer,faceLattice - Structural:
groupExplode(dissolve nested groups),looseToGroup(cluster by connectivity),makeGroup(wrap the whole stream into one group)
Expression System
All args values are expression strings evaluated at runtime:
"1200" → literal number
"lengthX - thickness" → references parameters by key
"flag ? 10 : 20" → ternary
"'Oak'" → literal text (single-quoted)
"[0,0,lengthZ]" → array/vectorPowered by jsep with ternary, arrow, and object plugins.
Evaluation Flow
1. Model.fromJSON(json)
├── Parse ModelJSON (schema.ts)
├── Build NodeCollection (flat → tree)
└── Wire expression dependencies
2. model.evaluate(params, resolveDep?)
├── Resolve parameters (with overrides from params)
│ └── Expression evaluation (jsep parse → resolve refs → compute)
├── Run the body fold (operations, top-down)
│ └── producers create geometry, transforms process the stream
└── Collect results → EvaluateResult
├── geometry: GeometryNode[] (tree of entities)
├── materials: MaterialData[] (PBR materials)
└── params: ParamInfo[] (exposed parameters)Scene Entities
Exactly 3 scene entities, each an entity return kind. Every body node's value is an array of these (a single result = an array of 1). A solid is Face[] — a box is 6 Faces; there is no Block entity.
An entity is { id, geometry, visible?, layer?, material?, label?, sourceKey? } with no type tag — its kind is sniffed from the geometry shape (isPoint/isCurve/isFace/entityKind).
Point (geometry [x,y,z], namespace pointEntity)
position markers for visualizing locations (render-only)
producers: pointEntity
Curve (geometry NurbsCurveJSON, namespace curveEntity)
1D wires AND closed loops (a closed loop is still a Curve)
producers: polyline, spline, rectangle, polygon,
arcCenterStartEnd, arcThreePoints, arcTangentEnd,
circle, ellipseArc, cubicBezier, quadraticBezier,
nurbsCurve, textCurve
Face (geometry { loops, uvMatrix }, namespace faceEntity)
flat 3-D polygon region (holes via extra loops); a solid = Face[]
producers: extrude, sweep, loft, revolve,
patchFromCurve, patchFromCornersThe generic entity-namespace producers are clone (references another node, adds an independent deep copy with new ids), addMesh (bakes a literal faceEntity[] array into a fresh, independently-transformable copy), and placeModel (places a loadModel value node's sub-model, load-once/place-many). Grouping is the container FORM ({ operations: [...] }), not a method. Materials/layers are static document lanes (see Summary above), not producers.
Curved construction (loft / revolve / sweep / patchFromCurve / patchFromCorners) builds a NURBS surface inside the shapemetry kernel, then tessellates to Face[] for the graph — NURBS is a kernel concept, not a scene entity. Solid topology ops (solidUnion/solidSubtract/solidIntersect booleans, faceExtrude/edgeFillet/edgeChamfer/faceLattice transforms) build a kernel Mesh, then decompose back to Face[].
Rule: the graph only ever holds Curves, Faces, and Points; exact NURBS/mesh math is an internal kernel side-path that always resolves back to these three entities.
Entity Mapping
| ParaShape entity | Three.js | SketchUp |
|---|---|---|
Curve | Line | Edges |
Face | Mesh (filled) | Face |
Point | Points (markers) | ConstructionPoint |
TS JSON types: CurveJSON, FaceJSON, PointJSON.
Available Nodes
The engine has NO built-in node catalog — it takes a flat RegistryInput (node definitions + expression namespaces + extra value types) as INPUT (createRegistry(input) → Model.fromJSON(json, { registry })). The engine's own CORE_NODES contribute the builtins (scalar parameters, loadModel, container/clone/addMesh/placeModel, groupExplode/looseToGroup/makeGroup); ParaShape's own node set ships as the optional @parametron/parametric/nodes subpath (src/nodesRegistry/catalog.ts is the catalog SSOT, src/nodesRegistry/wiring.ts wires each entry's compute to the standalone functions from @parametron/parametric/compute — the no-engine-coupling reuse surface, namespaced per domain module). This subpath is never pulled in unless a consumer explicitly imports it, so an external app can reuse the bare engine with its own node set. Computation namespaces (Point.*, Vector.*, Curve.*, Surface.*, Face.*) are registered via RegistryInput.namespaces — never at module load. registry.nodes (NodeDefinition[]) IS the serializable view docs/UI menus render from — no separate catalog type.
Source Structure
src/
├── schema.ts # TypeScript types for JSON format
├── schema/types.ts # Core value/entity type catalog (TypeDef; a RegistryInput can add inert types)
├── registry/ # Node contract + registry (the engine's INPUT)
│ ├── contract.ts # RegistryInput / NodeDefinition / ChainGroup / ArgDef
│ ├── Registry.ts # createRegistry(input) — merge + validate, per-Model instance
│ ├── builtins.ts # CORE_NODES (engine builtin node definitions + preload hooks)
│ └── helpers.ts # chain helpers (group/applyGeometry/multiplyInstances/mapInstances/…)
├── validate.ts # Model/node validation (Zod structure + registry pass)
├── Model.ts # Model class (fromJSON({registry}), evaluate)
├── evaluate.ts # evaluateModel(json, params, { registry, assets }) — sync
├── domains/ # Producer/transform implementations per domain
│ ├── curve.ts # curveEntity producers
│ ├── face.ts # faceEntity producers
│ ├── solid.ts # solid/face transforms (solidUnion/solidSubtract/solidIntersect/solidSelfUnion, faceExtrude/edgeFillet/edgeChamfer…)
│ ├── surface.ts # curved producers (loft/revolve/sweep/patchFromCurve/patchFromCorners → Face[])
│ ├── point.ts # pointEntity producers
│ ├── entity.ts # generic per-entity transforms (reverse/resize/applyMaterial/applyLayer/…)
│ ├── entities.ts # whole-stream ops (move/rotate/array*/mirror/clone/…)
│ ├── font.ts # font lookup/fallback (fonts[] static lane, not a parameter)
│ └── … # (namespaces are registered per RegistryInput — see nodesRegistry/)
├── nodesRegistry/ # ParaShape's own node set (the `/nodes` subpath — optional, never
│ ├── catalog.ts # pulled in unless imported)
│ ├── functionRegistry.ts
│ ├── namespaces.ts
│ └── wiring.ts # catalog entry → OperationDefinition, wired to domains/* compute
├── core/ # BaseObject, collections, ModelScope (model), sanitizer
├── nodes/ # Runtime node classes
│ ├── BaseNode.ts
│ ├── ParameterNode.ts
│ ├── OperationNode.ts # one fold step (producer/transform, per-step cache)
│ ├── ContainerNode.ts # container form + FoldRunner
│ ├── ExpressionNode.ts
│ ├── ArgumentNode.ts
│ ├── OptionNode.ts
│ └── NodeCollection.ts
└── utils/
├── expression.ts # Expression parser/evaluator
├── convertExpression.ts # Literal-only unit conversion (convertLiteral)
└── unitConvert.ts # Unit conversion