Parametron

ParaShape Parametric Model Reference

Source of truth for building JSON v22 models — for humans and AI alike.

Two layers

ParaShape splits everything into two layers, and the boundary is the single most important thing to internalize:

  • COMPUTATION layer — the expression namespaces (Point.* / Vector.* / Curve.* / Math) where all math lives. It computes values from values: transient, never rendered, never exported. AI writes it as text inside args expression strings.
  • SCENE ENTITIES layer — the thin render/export boundary. Operations (producers + transforms) produce and transform scene entities that flow to the renderer (Three.js in the builder, faces/edges/groups in SketchUp).

Naming convention — value vs entity

The two layers name themselves so you can never confuse a transient value with a rendered entity:

  • Computation values keep bare PascalCase namespaces: Point, Curve, Face, Vector, Surface, Axis, Plane — pure values, the function substrate. Curve.length is a transient value, not something on screen.
  • Scene-entity namespaces use camelCase *Entity: pointEntity, curveEntity, faceEntity — producers that materialize + render. A curveEntity is bound into the scene graph and drawn.
  • A scene entity is { id, geometry, visible?, layer?, material?, label?, sourceKey? } with NO type tag — its kind is sniffed from the geometry shape (isPoint/isCurve/isFace, or entityKind(e) → the *Entity name = namespace = the operation's return). The TS types stay CurveJSON / FaceJSON / PointJSON, alongside the Curve.* value namespace.

So Curve.length (a value) is never mistaken for curveEntity (bound + rendered); pointEntity vs Point already drew this line, and curve / face now follow as curveEntity / faceEntity.

Boundary criterion: does a feature produce a value (→ computation) or a renderable entity (→ scene)? Decision rule for any new feature: a value goes in an expression namespace; a renderable goes in an operation. The scene-entity set is exactly the renderer primitive set — wire / filled region / point.

The scene-entity layer converges on the SketchUp model (edges, faces, points, groups; tessellate curves; smooth-by-angle), so Face[] → SketchUp faces is a 1 lossless export. The COMPUTATION layer — expression-first, AI-native — is the part SketchUp has no equivalent of; it is the differentiator.

Model structure

{
  "title": "My Model",
  "camera": { "position": [0, 0, 0], "target": [0, 0, 0] },
  "materials": [ ...MaterialJSON[] ],
  "layers": [ ...LayerJSON[] ],
  "fonts": [ ...FontJSON[] ],
  "parameters": [ ...NodeJSON[] ],
  "operations": [ ...NodeJSON[] ],
  "unit": { "length": "mm" }
}

The root holds TWO node arrays plus three STATIC ASSET LANES. parameters (the header — an environment of named VALUE bindings, order is display-only) and operations (the body — THE graph, a single flat fold executed top-down, order is execution order). Omit any array when empty. A node references an earlier node by key — keep referenced-before-referencing order within operations (parameters may be referenced from anywhere, since they're pulled by reference, not position). There is no id field on any node (a node's identity in a document is its key; a model's own id is the only per-object identifier, and it names the whole document).

materials / layers / fonts are static named records, not nodes — see Assets below. Sub-model materials/layers/fonts are automatically namespaced on load (door + Oakdoor:Oak), so two models defining the same name never collide.


Grammar — ONE recursive node shape

Every node in parameters[] or operations[] is one of exactly two forms, discriminated by which REQUIRED field is present:

{ "key": "profile", "method": "rectangle", "args": [ ... ] }          // operation
{ "key": "cabinet", "operations": [ ...NodeJSON[] ] }                  // container
FormHasRole
operationmethod (required), args?Runs one registry method against the fold state. key (if present) names the stream after this step.
containeroperations (required array, no method)Runs its OWN operations[] from an empty stream, top-down, the same rule as the model root. key (if present) names the container's own fold result.

Both forms may carry label? (an expression — literal text single-quoted) and enabled? (an expression — falsy bypasses the node; see below). There is no nodeType field, no generators[]/modifiers[] split, no method:"container" or method:"group" — a container is a grammar shape, not a registry method, and it never appears in a registry.

The fold — how operations[] runs

operations[] is ONE flat list, executed top-down over a stream that starts empty ([]):

  • A producer operation (polyline, rectangle, extrude, clone, placeModel, …) CONCATS: stream = [...stream, ...created]. It never replaces the stream, only adds to it.
  • A transform operation (move, curveSelfUnion, curveExtrude, applyMaterial, …) PROCESSES the stream: stream = transform(stream, args). Its input is simply whatever came before it in the SAME operations[] list — there is no separate "this modifier's parent" concept, just position in the list.
  • A container runs its OWN operations[] from its own empty stream, and its result concats into the PARENT stream — exactly like a producer. One nesting meaning only.

This is why the old "generator + its modifiers[]" shape collapses into one flat list: a rectangle producer followed by a fillet transform followed by a move transform is just three consecutive entries — no separate arrays, no special case for "the first one must be a producer" (though it usually is, since a transform on an empty stream is a no-op).

{
  "key": "shelf",
  "operations": [
    { "method": "rectangle", "args": [{ "key": "point1", "input": "[0,0,0]" }, { "key": "point2", "input": "[width,height,0]" }] },
    { "method": "extrude", "args": [{ "key": "profile", "input": "" }, { "key": "thickness", "input": "thickness" }] },
    { "method": "edgeFillet", "args": [{ "key": "edgeFilter", "input": "(p1, p2) => true" }, { "key": "radius", "input": "2" }] },
    { "method": "applyMaterial", "args": [{ "key": "name", "input": "'Oak'" }] }
  ]
}

Note: a chain-internal producer step (like extrude here, consuming the profile the rectangle just added to the SAME stream) takes its profile arg as ""/omitted when it means "whatever is already in the stream" — see each operation's own args in the registry reference below; some producers (like extrude) accept an explicit profile arg instead when profile and wall are built as separate keyed nodes (the more common authoring style — see examples throughout this doc).

A container's own key works exactly like a generator's key used to: an expression referencing it receives the container's ending fold result (its value), e.g. Curve.length(profile) where profile is a container producing one curve. An operation's own key names the stream at that exact point — useful for tapping an intermediate step from outside a container (array1.length where array1 is an arrayLinear operation's own key). Missing keys are auto-assigned on load from the registry's friendly defaultKey base (arc1, fillet2, …), model-wide unique including nested containers.

enabled

Both forms carry an optional enabled expression ("enabled": "hasDoors"):

  • Falsy on an operation = the step is BYPASSED — the stream passes through unchanged, same identity (downstream per-step caches stay valid).
  • Falsy on a container = it contributes NOTHING to its parent's fold (as if absent).

Being an expression, enabled can reference a boolean parameter to toggle a step/container from the configurator. A value-lane node (a parameter) never has enabled — disabling one would break every expression that references it.

Grouping — when to use a container

Use a container ({ key, operations: [...] }) whenever more than one step needs to travel together as one authorable unit: a producer plus its transforms, or several child containers plus a shared trailing chain. This is the node unit you see as ONE card in the builder panel, ONE render slice, and ONE SketchUp export name. A single bare operation entry with its own key is fine standalone (e.g. one raw producer with no follow-up steps) but as soon as you add a second step under the same identity, wrap it in a container — this mirrors the old "generator (with its modifiers array)" concept, now expressed as ordinary fold nesting instead of a special node shape.

{
  "key": "cabinet",
  "operations": [
    { "key": "panel", "operations": [
      { "method": "rectangle", "args": [ ... ] }
    ] }
  ]
}

A container's children are its own recursive operations[] — this is what used to be called a "generator group" (method:"container" + generators[] + modifiers[]); it is now just ordinary nesting, no special method.

Copies and instancing

A node's value is its plain entity array. A "copy" is a nested groupEntity referencing a shared GroupDefinition — that structure IS the instancing (SketchUp component semantics). The transform operations map over the WHOLE array, nested groups included:

TransformEffect
arrayLinear / arrayRadial / arrayCurve / arrayDistancesAdd copies: a group input copies the cheap wrapper (shared definition = instancing); raw geometry bakes real transformed copies.
mirror / move / rotateTransform each entity — composes onto a group's placement, transforms raw geometry directly.
applyMaterial / applyLayer / name / visibleStamp each entity (a group wrapper = per-copy); a (e,i)=>value callback varies by entity index.

Steps apply in listed order (a plain fold). Root fold: model.operations IS the root fold — the exact same rule as any container, run directly over the model's top-level operations; a root-level transform (applyMaterial/applyLayer included) is an ordinary fold step, no special root-only machinery. To get cheap instanced copies, group first (looseToGroup) then array; arrays over loose geometry bake real copies — exactly SketchUp's loose-vs-component behavior.

Materials/layers are static records on the document (see Assets), not nodes — applyMaterial/applyLayer stamp entities with a NAME string that looks up a record in the materials[]/layers[] lane at render time.

groupExplode dissolves the NESTED groups inside a node's content — a placed sub-model (placeModel), a looseToGroup cluster or an instanced array — into raw geometry (SketchUp Explode). nested (default true) dissolves recursively; false = one level.

Operations are a pipeline — entity KIND is free to change mid-fold

A curve container's fold may end in Face[]: curveExtrude pushes the stream's curves into solids in place (rectangle → arrayLinear → curveSelfUnion → curveExtrude → edgeFillet is one container), and curveFill is the same gesture minus the thickness — closed curves fill into FLAT faces (a closed curve nested inside another becomes a hole in its face; each outermost ring emits its own face; open curves pass through). Use it when a no-thickness region is the end product — 2D layouts, fills, texture masks. The domain prefix on a transform's method (curve* / face* / edge* / group* / solid*) names what the op CONSUMES, so the fold self-documents its kind transitions; a producer's return describes its CREATION step, not the container's ending value (every step's key keeps it referenceable). The dividing line between a transform and a producer: an op whose primary input is the fold itself is a transform (curveExtrude); an op that composes several other nodes' outputs is a producer (loft between profiles, sweep along a path node, patchFromCurve).

Planarity rules (per op family — verified behavior, not aspiration):

  • curveExtrude / extrude: NO planarity requirement, no plane grouping — per-curve, each curve is its own profile. An OPEN curve (planar or true-3D) → walls only, each wall planar by construction (segment × one sweep vector = parallelogram); caps is ignored. A CLOSED planar loop → walls + one-piece flat caps (a solid). A CLOSED non-planar (3D) loop still runs: the cap cannot be one flat face, so it auto-folds into planar triangles — the diagonal crease lines are correct geometry, not a tessellation bug. Extrude direction = the curve's own Newell best-fit normal.
  • 2D curve booleans (curveUnion/curveSubtract/curveIntersect/ curveSelfUnion): per-plane. curveSelfUnion PARTITIONS the stream into coplanar groups, booleans each group in its own 2D frame, groups on different planes stay independent. The binary ops consume only operand regions coplanar with the subject (a non-coplanar operand passes the subject through unchanged). A NON-planar closed curve fed to a boolean is force-flattened internally first (via the same curve-to-face coercion extrude/loft use — see below).
  • Curve-to-face coercion force-planarizes, silently. Any op that needs a Face but receives a curve fits the loop's best Newell plane and PROJECTS every vertex onto it — a non-planar input gets snapped flat (vertices move). It never errors and never checks; feed it planar curves or accept the snap. This is also why manually pre-flattening a profile "for safety" before an extrude is wrong: it destroys the open-curve walls case and moves 3D vertices silently, while curveExtrude handles both correctly as-is.

By cardinality, transforms come in four shapes:

  • map (n → n): move, rotate, curveReverse / faceReverse, applyMaterial, faceExtrude (face push), edgeFillet, edgeChamfer, …; resize is also map but operates on all entities as one unit (scales from bbox center)
  • multiply (n → m > n): arrayLinear / arrayRadial / arrayCurve, mirror, curveSplit
  • reduce (n → m < n): the boolean ops (curveUnion / curveSubtract / curveIntersect, solidUnion / solidSubtract / solidIntersect, curveSelfUnion / solidSelfUnion)
  • structural: groupExplode — dissolves the nested groups inside the node's content into raw geometry; looseToGroup — the inverse: bakes pending instances, then collects loose curves/faces into nested groups by connectivity (shared welded vertex; curves group with curves, faces with faces; a singleton becomes a group of 1; points and existing nested groups pass through); makeGroup — wraps the WHOLE stream into one nested group unconditionally, no connectivity clustering, no kind filtering

A transform applies in any context: an op that doesn't fit the entities currently in the stream simply filters itself out (a solidSelfUnion on a stream with no solids is a no-op; an edgeFillet only touches edges that match its filter). A nested group (a clone / placed sub-model) passes through every geometry op untouched — a group is a structural wrapper, not geometry; add groupExplode before the op to dissolve it and let the op reach the geometry inside. There is no "generic vs type-specific" transform distinction — every method lives in one flat namespace keyed by method name (globally unique, domain-prefixed: curve*/face*/edge*/group*/solid*, plus apply* for the two resource-assignment props applyMaterial/applyLayer).


Scene entities

The scene-entity layer exposes exactly THREE entity kinds — the renderer primitive set (wire / filled region / point):

EntityGeometry shapeNamespaceWhat it is
CurveNurbsCurveJSON (controlPoints)curveEntity1D wire AND closed loop (polyline / spline / arc / circle / rectangle / polygon / textCurve). A closed loop is still a Curve.
Face{ loops, uvMatrix }faceEntityA flat 3-D polygon region; holes via extra loops. A solid is Face[] (a box = 6 faces). There is no Block entity.
Point[x,y,z]pointEntityRender-only position marker(s) emitted by the pointEntity producer.

Every entity is { id, geometry, visible?, layer?, material?, label?, sourceKey? } with no type tag — kind = the geometry shape (sniff via isCurve/isFace/isPoint/entityKind).

face / edge / vertex / loop are derived selection-targets, not entities. Every operation/container returns an array of entities; cardinality = array length at runtime, never a type.

NURBS is not a scene entity. The shapemetry kernel still computes NURBS, but ParaShape does not expose a NURBS scene entity — a NURBS surface tessellates before it can render, so it belongs to the computation layer, not the scene. The curved producers (loft / revolve / patchFromCurve / patchFromCorners) build a kernel NURBS surface internally, then tessellate to per-triangle Face[] (nurbsToFaces). They have return: "faceEntity" like every other solid producer.

The Face.* (area/normal/centroid over a face value) computation namespace is registered alongside Point.* / Vector.* / Curve.* / Surface.* — they return recipe-based values, never scene entities.


Expressions

Every args value is an expression string evaluated in a JS-like scope.

Scope

  • All parameter keys and any earlier keys in the current fold (operations or containers)
  • Built-in math namespaces: Point, Vector, Curve, Surface, Face, Axis, Plane
  • Standard math: Math.sin, Math.cos, Math.PI, Math.abs, Math.max, Math.min, Math.round, Math.floor, Math.ceil, Math.sqrt, Math.pow

Literals

TypeSyntax
Number"42", "3.14"
String"'hello'" (single-quoted inside the JSON string)
Boolean"true", "false"
Array / point"[0, 0, 0]"
Color"[255, 180, 60]"
Null"null"

Operations

"width - 2 * thickness"
"count > 0 ? spacing : 0"
"Math.sin(angle * Math.PI / 180) * radius"
"[x, y, height / 2]"
"Point.distance([0,0,0], [width, 0, 0])"

Lambda (for filter args)

"(p, i) => i % 2 === 0"
"(points, normal) => normal[2] > 0.9"
"(p1, p2) => p1[2] > 0 && p2[2] > 0"

Rules

  • String literals must be single-quoted: "'Oak'" not "Oak" — unquoted Oak would be treated as a variable reference.
  • Expressions are pure — no side effects, no async, no DOM.
  • All lengths are in mm by default.

Parameters

Parameters are header nodes — they bind key → value and live in the model's parameters[] array. They use the SAME operation grammar shape as everything else ({ key, method, args }); a container form there ({ key, operations: [...] }) is display grouping only — its children are still parameters, all globally referenceable regardless of nesting depth.

methodarg nameUINotes
lengthlengthTextbox + slidermm
numbernumberTextbox + sliderUnitless
angleangleTextboxDegrees
booleanbooleanCheckbox0 or 1 — multiply with a dimension to collapse it
texttextTextboxValue is single-quoted literal
colorcolorColor picker'#rrggbb' string
functioninputCode editorLambda expression
loadModelinputFile pickerURI to a sub-model (bare-id / https / data)

Value and constraints live in args[0] (a single ArgumentJSON object) — min/max/step on numeric-typed args, options on enum-style args:

{
  "key": "width", "method": "length",
  "args": [{
    "key": "length", "input": "600",
    "min": "50", "max": "3000", "step": "1",
    "options": [
      { "label": "Small", "input": "300" },
      { "label": "Large", "input": "600" }
    ]
  }]
}

Animation (numeric parameters)

A numeric parameter (length/number/angle) may carry an animation block — keyframes over its input value, time in seconds (sorted on load; playback eases per segment, holds outside the timeline):

{
  "key": "openAngle", "method": "angle",
  "animation": { "keyframes": [{ "time": 0, "value": 0 }, { "time": 0.8, "value": 90 }] },
  "args": [{ "key": "angle", "input": "0" }]
}

Playback lives OUTSIDE the graph (AnimationPlayer): each tick sets the parameter's input and re-evaluates — the engine and renderers never know animation exists (a frame is indistinguishable from a slider edit). The parameter's VALUE is the state: play toggles between the first and last keyframe values (door open ↔ closed), and what's on screen is what saves. Absolute times sequence parameters on the model's shared Play clock ({"time": 3} starts at second 3); the Interact tool's direct click runs a parameter's keyframes relative to now instead. loop: true repeats while playing. For smooth playback animate transforms over GROUPS (makeGrouprotate) — matrix-only render updates, no re-tessellation per frame.


Assets

Materials, layers, and fonts are static named records on the document — plain values, never nodes, never expressions. name is unique per lane and is how an operation references the record (applyMaterial/applyLayer's name arg, textCurve's font arg). Editing a record is an editor act (render-only refresh) — it never re-evaluates the graph. There are no createMaterial/createLayer/loadFont/loadImage methods.

{
  "materials": [
    { "name": "Oak", "baseColor": "#c8a064", "opacity": 1, "texture": "https://example.com/oak.jpg" }
  ],
  "layers": [
    { "name": "Carcass", "color": [0.2, 0.2, 0.2] }
  ],
  "fonts": [
    { "name": "Poppins", "url": "https://example.com/poppins.ttf" }
  ]
}
  • MaterialJSON = { name, baseColor?, texture?, colorFactor?, opacity?, roughness?, metallic? }texture is an image URI (https:// or data:); images have no lane of their own, the URI is held directly.
  • LayerJSON = { name, color?, lineStyle? }.
  • FontJSON = { name, url }url is pre-loaded async (Store) before the graph evaluates.

Sub-model materials/layers/fonts are automatically namespaced on load (door

  • Oakdoor:Oak), so two models declaring the same name never collide.

Operations

Curves & planar profiles

Every profile producer — open wires AND closed loops (rectangle / circle / polygon / textCurve / arc / nurbsCurve / spline / polyline) — has return: "curveEntity" and outputs a CurveJSON. Producers with return: "faceEntity" are the FILLED layer: extrude/loft/patchFromCurve/the 2D boolean ops (curveUnion / curveSubtract / curveIntersect) all accept a closed curve directly and fill it into a Face region internally — there is no separate "fill the curve first" step to reach for. Coordinates are in mm on the XY plane.

rectangle — closed rectangle loop from two opposite corners; the axis with the smallest spread between point1/point2 becomes the plane (XY/XZ/YZ), so no separate move/rotate is needed to place it on a different principal plane

{ "key": "profile", "operations": [
  { "method": "rectangle", "args": [{ "key": "point1", "input": "[0,0,0]" }, { "key": "point2", "input": "[width,height,0]" }] }
] }

circle — closed circle loop

{ "method": "circle", "args": [{ "key": "center", "input": "[0,0]" }, { "key": "radius", "input": "radius" }] }

polygon — closed regular-polygon loop

{ "method": "polygon", "args": [{ "key": "center", "input": "[0,0]" }, { "key": "radius", "input": "50" }, { "key": "sides", "input": "6" }] }

ellipseArcangle = 360 → full closed ellipse, < 360 → open arc

{ "method": "ellipseArc", "args": [{ "key": "center", "input": "[0,0]" }, { "key": "xAxis", "input": "[100,0]" }, { "key": "yAxis", "input": "[0,50]" }, { "key": "angle", "input": "360" }] }

polyline — open or closed polyline; closed: true makes a fillable loop

{ "method": "polyline", "args": [{ "key": "points", "input": "[[0,0],[width,0],[width,height],[0,height]]" }, { "key": "closed", "input": "true" }] }

spline — smooth NURBS curve through control points

{ "method": "spline", "args": [{ "key": "points", "input": "[[0,0],[50,80],[100,0]]" }, { "key": "degree", "input": "3" }] }

arcCenterStartEnd

{ "method": "arcCenterStartEnd", "args": [{ "key": "center", "input": "[0,0]" }, { "key": "startPoint", "input": "[radius,0]" }, { "key": "endPoint", "input": "[0,radius]" }] }

arcThreePoints

{ "method": "arcThreePoints", "args": [{ "key": "startPoint", "input": "[0,0]" }, { "key": "middlePoint", "input": "[50,30]" }, { "key": "endPoint", "input": "[100,0]" }] }

arcTangentEnd — arc tangent from an axis to an endpoint

{ "method": "arcTangentEnd", "args": [{ "key": "axis", "input": "{ origin: [0,0,0], direction: [0,1,0] }" }, { "key": "endPoint", "input": "[100,0]" }] }

nurbsCurve — B-spline / Bézier from control points. knots/weights are optional (default [] = auto clamped-uniform knots, uniform weight 1) — supply them only to author a non-uniform knot spacing or a rational curve (a weight > 1 pulls the curve toward that control point).

{ "method": "nurbsCurve", "args": [{ "key": "points", "input": "[[0,0,0],[20,60,0],[80,60,0],[100,0,0]]" }, { "key": "degree", "input": "3" }] }

textCurve — outline text as curves

{ "method": "textCurve", "args": [{ "key": "text", "input": "'Hello'" }, { "key": "font", "input": "fontParam" }, { "key": "fontSize", "input": "20" }] }

(To mirror, use the mirror transform — it keeps the original and adds a winding-corrected reflection. Works on any entity type.)

curveUnion / curveSubtract / curveIntersect (transforms) — 2D closed-curve boolean. curve references the other node's key.

{ "method": "curveSubtract", "args": [{ "key": "curve", "input": "holeProfile" }] }

Curve-edit transforms — placed after a curve producer in the same operations[] list:

curveSplit — split a curve at parameter (0..1)

{ "method": "curveSplit", "args": [{ "key": "parameter", "input": "0.5" }] }

curveFillet — round selected corners with an arc of radius. vertexFilter (p, i) narrows which corners.

{ "method": "curveFillet", "args": [{ "key": "radius", "input": "5" }, { "key": "vertexFilter", "input": "(p, i) => true" }] }

curveChamfer — bevel selected corners by distance. vertexFilter (p, i) narrows which corners.

{ "method": "curveChamfer", "args": [{ "key": "distance", "input": "5" }, { "key": "vertexFilter", "input": "(p, i) => true" }] }

curveOffset — offset a closed curve inward (negative) / outward (positive) by distance

{ "method": "curveOffset", "args": [{ "key": "distance", "input": "-thickness" }] }

curveThicken — give a path a flat wall of distance width (produces a closed region)

{ "method": "curveThicken", "args": [{ "key": "distance", "input": "10" }, { "key": "joinType", "input": "'miter'" }, { "key": "endType", "input": "'butt'" }] }

Path (3D wire)

Use paths as sweep rails or loft rails — same producers, 3D points:

polyline

{ "method": "polyline", "args": [{ "key": "points", "input": "[[0,0,0],[width,0,0],[width,depth,0]]" }] }

spline

{ "method": "spline", "args": [{ "key": "points", "input": "[[0,0,0],[50,0,50],[100,0,0]]" }, { "key": "degree", "input": "3" }] }

Curved producers (tessellated Face[])

These build a kernel NURBS surface internally, then tessellate to per-triangle Face[] (nurbsToFaces). There is no NURBS scene entity — the curved result is rendered/exported as flat faces, exactly like every other solid. They have return: "faceEntity". (There is no standalone surface extrude — that name belongs to the solid extrude below — and no fromSurfaces: a curved producer already returns Face[], so no conversion step is needed.)

loft — loft through curve rails

{ "method": "loft", "args": [{ "key": "curves", "input": "[rail1, rail2]" }, { "key": "degreeV", "input": "3" }] }

revolve — revolve profile around axis

{ "method": "revolve", "args": [{ "key": "curves", "input": "[profile]" }, { "key": "axis", "input": "{ origin: [0,0,0], direction: [0,0,1] }" }, { "key": "angle", "input": "360" }] }

patchFromCurve — fill boundary curves with a smooth surface

{ "method": "patchFromCurve", "args": [{ "key": "curves", "input": "[c1, c2, c3, c4]" }] }

patchFromCorners — bilinear surface through 4 explicit corner points (winding order 0→1→2→3)

{ "method": "patchFromCorners", "args": [{ "key": "corner0", "input": "[0,0,0]" }, { "key": "corner1", "input": "[100,0,0]" }, { "key": "corner2", "input": "[100,100,0]" }, { "key": "corner3", "input": "[0,100,0]" }] }

Solids (a solid is Face[])

A solid is just an array of faces — there is no Block entity. A box = 6 Face faces. Z is up. There are deliberately no primitive solids (box/cylinder/sphere/cone/torus): build geometry from a profile via extrude / loft / revolve / sweep, then boolean / edgeFillet / edgeChamfer. (Profile-as-parameter is easier to reshape and AI-safer.)

extrude — extrude a profile into a solid. CLOSED profile → solid (walls + caps); OPEN profile → walls only.

{ "key": "box", "operations": [
  { "method": "rectangle", "args": [{ "key": "point1", "input": "[0,0,0]" }, { "key": "point2", "input": "[width,depth,0]" }] },
  { "method": "extrude", "args": [{ "key": "profile", "input": "" }, { "key": "thickness", "input": "height" }, { "key": "caps", "input": "true" }] }
] }

sweep — sweep a cross-section profile along a path

{ "method": "sweep", "args": [{ "key": "path", "input": "railPath" }, { "key": "profile", "input": "crossSection" }, { "key": "caps", "input": "true" }] }

Solid transforms — placed after a solid-producing step in the same operations[] list, each treats the stream's whole face-set as one solid:

solidUnion / solidSubtract / solidIntersect — 3D CSG on closed solids (Face[]). solid references another node's key. (The 2D curve-boolean equivalents are the separate curveUnion / curveSubtract / curveIntersect methods — not context-sensitive aliases of these.)

{ "method": "solidSubtract", "args": [{ "key": "solid", "input": "cutoutKey" }] }

solidSelfUnion — fuse all faces in the stream into one merged solid (no solid operand needed).

{ "method": "solidSelfUnion", "args": [] }

(To round every edge, use edgeFillet with edgeFilter: "(p1, p2) => true".)

edgeFillet — round selected edges with a rolling-ball arc

{ "method": "edgeFillet",
  "args": [{ "key": "edgeFilter", "input": "(p1, p2) => p2[2] > 0 && p1[2] > 0" }, { "key": "radius", "input": "2" }, { "key": "segments", "input": "12" }] }

edgeFilter receives two [x,y,z] endpoints of each edge.

edgeChamfer — flat-cut (45°-style bevel) selected edges

{ "method": "edgeChamfer",
  "args": [{ "key": "edgeFilter", "input": "(p1, p2) => p2[2] > 0 && p1[2] > 0" }, { "key": "distance", "input": "2" }] }

faceExtrude (transform) — push selected faces along their normal

{ "method": "faceExtrude",
  "args": [{ "key": "thickness", "input": "5" }, { "key": "faceFilter", "input": "(points, normal) => normal[2] > 0.9" }] }

faceFilter receives points (array of [x,y,z]) and normal ([nx,ny,nz]).

(To mirror a solid, use the mirror transform.)

faceLattice — hollow a solid into a frame/lattice shell

{ "method": "faceLattice",
  "args": [{ "key": "thickness", "input": "3" }, { "key": "boundary", "input": "1" }] }

(To invert face winding/normals, use the faceReverse transform.)


Entities (arrays and grouping)

To group child nodes, use a container ({ key, operations: [...] }) — see Grammar above.

arrayLineartransform (multiply): copies ADDED placements stepping by vector (the original is not counted — copies: 1 duplicates once, result = copies + 1)

{ "method": "arrayLinear",
  "args": [{ "key": "vector", "input": "[0,0,shelfSpacing]" }, { "key": "copies", "input": "shelfCount - 1" }] }

Each copy steps by vector — its magnitude IS the spacing (unlike arrayDistances, where direction is normalized).

arrayRadialtransform (multiply): repeat around an axis

{ "method": "arrayRadial",
  "args": [{ "key": "axis", "input": "{ origin: [0,0,0], direction: [0,0,1] }" }, { "key": "copies", "input": "5" }] }

arrayCurvetransform (multiply): repeat along a curve

{ "method": "arrayCurve",
  "args": [{ "key": "curve", "input": "railKey" }, { "key": "copies", "input": "9" }, { "key": "angle", "input": "0" }] }

mirrortransform (multiply): add a winding-corrected reflection across a plane. clone: false reflects in place (flip).

{ "method": "mirror",
  "args": [{ "key": "plane", "input": "{ point: [0,0,0], normal: [1,0,0] }" }] }

cloneproducer: independent deep copy of another node's output (new ids). Can be moved/rotated without changing the original.

{ "key": "shelfCopy", "operations": [
  { "method": "clone", "args": [{ "key": "source", "input": "shelf" }] },
  { "method": "move", "args": [{ "key": "vector", "input": "[0,0,shelfSpacing]" }] }
] }

(To reverse a single entity's direction/winding, use curveReverse / faceReverse — per-entity transforms, not collection ops.)


Point

pointEntity — visualize 3D points as markers

{ "method": "pointEntity", "args": [{ "key": "positions", "input": "[[0,0,0],[100,0,0]]" }] }

Material textures

There is no Image scene entity and no image lane. A material's texture field holds an image URI directly (https:// or data:) — see Assets.

{ "materials": [
  { "name": "Oak", "baseColor": "#c8a064", "opacity": 1, "texture": "https://example.com/oak.jpg" }
] }

Entity transforms (apply to any entity type in scope)

move

{ "method": "move", "args": [{ "key": "vector", "input": "[offsetX, offsetY, offsetZ]" }] }

rotate

{ "method": "rotate",
  "args": [{ "key": "axis", "input": "{ origin: [0,0,0], direction: [0,0,1] }" }, { "key": "angle", "input": "45" }] }

resize

{ "method": "resize", "args": [{ "key": "sizeX", "input": "1200" }, { "key": "sizeY", "input": "600" }, { "key": "sizeZ", "input": "900" }] }

Resize to absolute target dimensions (mm). Computes scale factors from the container's bounding box and scales from the bbox center, so the center stays in place. Flat axes (bbox ≈ 0) keep their scale. When added via the UI, defaults are pre-filled with the current bbox dimensions.

(To reflect in place — a "flip" — use mirror with clone: false.)

curveReverse / faceReverse — flip a curve's direction or a face's winding (1→1). curveReverse flips a curve's direction (use to flip a sweep/loft rail); faceReverse flips winding (inverts the face normal).

{ "method": "faceReverse", "args": [] }

applyMaterial

{ "method": "applyMaterial", "args": [{ "key": "name", "input": "'Oak'" }] }

name must match a materials[] lane record's name field.

applyLayer

{ "method": "applyLayer", "args": [{ "key": "name", "input": "'Carcass'" }] }

visible — show or hide entities; enabled: "false" hides.

{ "method": "visible", "args": [{ "key": "enabled", "input": "showLegs" }] }

name

{ "method": "name", "args": [{ "key": "name", "input": "'Front Panel'" }] }

Registry reference (auto-generated)

The blocks below are generated from the SSOT registries (src/schema/types.ts, src/nodesRegistry/catalog.ts, src/schema.ts) by scripts/gen-reference.mjs (run pnpm gen:docs). Do not hand-edit between the AUTOGEN markers — edits are overwritten and the docsSync test fails on drift. The prose and examples above/below stay hand-written.

Types

<!-- AUTOGEN:types -->
TypeKindShapeNote
curveEntitysceneEntity{ id: string, geometry: NurbsCurveJSON, visible?: boolean, layer?: string, material?: string, label?: string, sourceKey?: string }1D wire — line / arc / bezier / nurbs curve. NO type tag — kind is sniffed from the geometry shape (curve geometry = NurbsCurveJSON, has controlPoints).
faceEntitysceneEntity{ id: string, geometry: { loops: [x,y,z][][], uvMatrix?: number[16] }, visible?: boolean, layer?: string, material?: string, label?: string, sourceKey?: string }Flat bounded region; a solid is faceEntity[]. NO type tag — kind is sniffed from the geometry shape (face geometry has loops). loops = 3D world polygons, outer loop first then holes; uvMatrix = row-major 4×4 plane→world frame for UV / 2D projection.
pointEntitysceneEntity{ id: string, geometry: [x,y,z], visible?: boolean, layer?: string, material?: string, label?: string, sourceKey?: string }Point marker — render-only, from the pointEntity generator. NO type tag — kind is sniffed from the geometry shape (point geometry = [x,y,z]).
groupEntitysceneEntity{ id: string, definition: string, transformation: number[16], material?: string, layer?: string, name?: string, visible?: boolean }ONE placement of a GroupDefinition (SketchUp component model). definition = id of the shared content { id: string, entities: EntityJSON[] } in the model's runtime definitions map — child entities stored ONCE, the definition itself is NOT a scene entity. transformation = column-major 4×4 (translation at indices 12,13,14). A node's value is groupEntity[]: N placements, 1 shared definition. Kind = has definition (no geometry).
entitynamespace(curveEntity|faceEntity|pointEntity|groupEntity)[]Generic ops over any scene entity (move/rotate/array/mirror…). An entity has NO type tag — kind is discriminated by its geometry shape (a groupEntity by its definition field).
materialnamespaceMaterialJSONMaterial record — a static materials[] lane entry, referenced by name (applyMaterial).
layernamespaceLayerJSONLayer record — a static layers[] lane entry, referenced by name (applyLayer).
numbervaluenumberUnitless number
lengthvaluenumber (mm)Distance (mm)
anglevaluenumber (deg)Degrees
binaryvalue0 | 10 or 1 — multiply with a dimension to collapse it
stringvaluestringText (single-quoted literal)
colorvalue#rrggbb#rrggbb hex string
vectorvalue[x,y,z]Direction [x, y, z] or [x, y] (z defaults to 0)
pointvalue[x,y,z]Position [x, y, z] or [x, y] (z defaults to 0)
planevalue{point:[x,y,z],normal:[nx,ny,nz]}Infinite plane: a point on the plane + unit normal. Build via Plane.fromPointNormal.
axisvalue{origin:[x,y,z],direction:[dx,dy,dz]}Directed half-line: origin + unit direction (magnitude dropped). Build via Axis.fromPointDirection / Axis.fromTwoPoints.
curvevalueNurbsCurveJSON { degree, knots, controlPoints, weights }Curve geometry value (NURBS) — the geometry of a curveEntity. Use with Curve.* functions; a curveEntity arg unwraps to this.
facevalue{ loops: [x,y,z][][], uvMatrix?: number[16] }Face geometry value — the geometry of a faceEntity. Use with Face.* functions; a faceEntity arg unwraps to this.
surfacevalueNurbsSurfaceJSON { degreeU, degreeV, knotsU, knotsV, controlPoints, weights }NURBS surface value (computation only — not a scene entity). Use with Surface.* functions; a curved generator tessellates one to faceEntity[].
functionvalue(args) => valueLambda expression
modelvalueModelJSON (embedded)Embedded sub-model. loadModel's input holds the full instanced ModelJSON; its value is the live built sub-model, consumed by placeModel via the loadModel key.
objectvalueobjectParameter group
groupvalueobjectVisual group
areavaluenumberArea unit
volumevaluenumberVolume unit
massvaluenumberMass unit
currencyvaluenumberCurrency
fontvalueFontDataOpentype-parsed font object (fetch + parse happens async in Phase 1 via store.loadFonts, fed by the document's static fonts[] lane). Consumed synchronously by the text generator's font arg, which names a fonts[] record.
fnvalueFunctionCallback function: (entity, index) => value. Accepted alongside a static value for per-entity variation.
<!-- /AUTOGEN:types -->

Nodes

<!-- AUTOGEN:nodes -->

Producers — by return (the entity kind produced; concat into the stream)

curveEntity

  • curveEntity.polyline — Polyline
    • 0 : point[] — Ordered vertices [x,y,z] (z optional, defaults 0).
    • 1 : binary — true closes the loop back to the first point; false leaves it open.
  • curveEntity.spline — Spline
    • 0 : point[] — Control points [x,y,z] the smooth curve passes through/near.
    • 1 : number — Curve smoothness: 1 = polyline, 2 = quadratic, 3 = cubic (most common).
  • curveEntity.rectangle — Rectangle
    • 0 : point — One corner of the rectangle.
    • 1 : point — Opposite corner. The axis with the smallest difference from Point 1 becomes the rectangle's plane (XY/XZ/YZ) — no separate rotate needed to place it on a different plane.
  • curveEntity.polygon — Polygon
    • 0 : point — Polygon center [x,y] in the XY plane.
    • 1 : length — Circumradius — distance from center to each vertex.
    • 2 : number — Number of edges, minimum 3.
  • curveEntity.arcCenterStartEnd — Arc (Center)
    • 0 : point — Arc center [x,y]; the radius = distance from center to start.
    • 1 : point — Where the arc begins; sets the radius.
    • 2 : point — Direction the arc sweeps toward (snapped onto the radius).
  • curveEntity.arcThreePoints — Arc (3 Points)
    • 0 : point — First endpoint the arc passes through.
    • 1 : point — Point on the arc between start and end (defines the bulge).
    • 2 : point — Last endpoint the arc passes through.
  • curveEntity.arcTangentEnd — Arc (Tangent)
    • 0 : axis — Start point + leaving tangent direction, as an axis (magnitude irrelevant).
    • 1 : point — Where the arc ends.
  • curveEntity.circle — Circle
    • 0 : point — Circle center [x,y] in the XY plane.
    • 1 : length — Circle radius.
  • curveEntity.ellipseArc — Ellipse Arc
    • 0 : point — Ellipse center [x,y].
    • 1 : vector — Major-axis vector; its length is the X radius.
    • 2 : vector — Minor-axis vector (perpendicular to xAxis); its length is the Y radius.
    • 3 : angle — Sweep in degrees: 360 = full closed ellipse, under 360 = open arc.
  • curveEntity.cubicBezier — Cubic Bezier
    • 0 : point — Start point (on curve).
    • 1 : point — First tangent control point.
    • 2 : point — Second tangent control point.
    • 3 : point — End point (on curve).
  • curveEntity.quadraticBezier — Quadratic Bezier
    • 0 : point — Start point (on curve).
    • 1 : point — Tangent control (pulled toward, not on curve).
    • 2 : point — End point (on curve).
  • curveEntity.nurbsCurve — NURBS Curve
    • 0 : point[] — Control points [x,y,z] of the hull; the curve is pulled toward them, not through.
    • 1 : number — Polynomial degree (clamped to points.length-1): 1 = polyline, 3 = smooth cubic.
    • 2 : number[] — Knot vector (length = points.length + degree + 1, non-decreasing). Empty = auto clamped-uniform.
    • 3 : number[] — Per-point weight, > 0 (length = points.length). Empty = uniform 1; higher pulls the curve toward that point.
  • curveEntity.textCurve — Text
    • 0 : string — The string to render as outline curves; single-quote the literal.
    • 1 : font — Font reference; leave '' to use the built-in fallback font.
    • 2 : length — Cap height of the glyphs in model units.

entity

  • entity.clone — Clone
    • 0 : entity[] — Node to copy — returns an independent deep copy with fresh ids.
  • entity.entities — Entities
    • 0 : entity[] — Ready-made scene entities as a literal EntityJSON[] (faces/curves/points/groups, UV kept) — emitted verbatim with fresh ids. The static-geometry producer: imports, bakes, AI-authored fixed geometry.
  • entity.placeModel — Place Model
    • 0 : model — Model ID to place — emits an independent placement with fresh ids.

faceEntity

  • faceEntity.extrude — Extrude
    • 0 : point[] | curveEntity[] | faceEntity[] — Closed loop becomes a solid with caps; open becomes walls. Accepts points, curves, or faces.
    • 1 : length — Extrusion distance along the profile's normal.
    • 2 : binary — 1 = closed profiles get top/bottom caps (a solid); 0 = walls only (open shell).
  • faceEntity.sweep — Sweep
    • 0 : curveEntity — Path the profile is swept along (open or closed curve).
    • 1 : curveEntity — Closed cross-section curve carried along the path; yields a solid tube.
    • 2 : binary — Close the two ends of the swept tube (false = open duct).
  • faceEntity.loft — Loft
    • 0 : curveEntity[] — Ordered cross-section curves the skin passes through (2 or more).
    • 1 : number — Smoothness across sections: 1 = straight (ruled) between curves, 3 = smooth.
  • faceEntity.revolve — Revolve
    • 0 : curveEntity[] — Profile curve(s) spun about the axis; a closed profile yields a solid.
    • 1 : axis — Revolution axis (origin on the axis, direction = spin axis).
    • 2 : angle — Sweep in degrees: 360 = full solid, under 360 = partial.
  • faceEntity.patchFromCurve — Patch From Curve
    • 0 : curveEntity[] — Boundary curves enclosing the region to fill with a smooth surface.
  • faceEntity.patchFromCorners — Patch From Corners
    • 0 : point — First corner [x,y,z]; corners wind in order 0→1→2→3.
    • 1 : point — Second corner [x,y,z].
    • 2 : point — Third corner [x,y,z], diagonal from corner 0.
    • 3 : point — Fourth corner [x,y,z].

pointEntity

  • pointEntity.pointEntity — Render Points
    • 0 : point[] — Marker positions [x,y,z] to visualize (render-only, not construction geometry).

Transforms — by namespace (the entity kind acted on; process the stream)

CurveEntity

  • curveOffset — Offset Curve (namespace: CurveEntity, shape: reduce)
    • 0 : length — Offset amount: positive = outward, negative = inward.
  • curveThicken — Thicken Curve (namespace: CurveEntity, shape: reduce)
    • 0 : length — Strip width (the curve is offset half this to each side).
    • 1 : string — How outer corners are joined: 'round' (arc), 'miter' (sharp point), 'square' (clipped).
    • 2 : string — How open ends are capped: 'round' (semicircle), 'square' (half-width), 'butt' (flush).
  • curveUnion — Curve Union (namespace: CurveEntity, shape: reduce)
    • 0 : curveEntity — The closed curve to merge with this one (2D boolean).
  • curveSubtract — Curve Subtract (namespace: CurveEntity, shape: reduce)
    • 0 : curveEntity — The closed curve to cut away from this one (2D boolean).
  • curveIntersect — Curve Intersect (namespace: CurveEntity, shape: reduce)
    • 0 : curveEntity — The closed curve to keep only the overlap with (2D boolean).
  • curveSelfUnion — Curve Self Union (namespace: CurveEntity, shape: reduce)
    • (no args)
  • curveFill — Fill (namespace: CurveEntity, shape: map)
    • (no args)
  • curveExtrude — Extrude (namespace: CurveEntity, shape: multiply)
    • 0 : length — Extrusion distance along the profile's normal.
    • 1 : binary — 1 = closed profiles get top/bottom caps (a solid); 0 = walls only (open shell).
  • curveReverse — Reverse (namespace: CurveEntity, shape: map)
    • 0 : binary — true flips direction (curve order / face normal); false leaves it.
  • curveSplit — Curve Split (namespace: CurveEntity, shape: multiply)
    • 0 : number — Normalized split position from start (0) to end (1).
  • curveFillet — Curve Fillet (namespace: CurveEntity, shape: map)
    • 0 : length — Arc radius rounding each selected corner (curved).
    • 1 : function — Predicate over a corner vertex and its index selecting which to round; default = all.
  • curveChamfer — Curve Chamfer (namespace: CurveEntity, shape: map)
    • 0 : length — Straight setback from each selected corner (flat cut, not an arc).
    • 1 : function — Predicate over a corner vertex and its index selecting which to cut; default = all.

FaceEntity

  • solidUnion — Solid Union (namespace: FaceEntity, shape: reduce)
    • 0 : faceEntity[] — The closed solid (Face[]) to fuse with this one (3D CSG).
  • solidSubtract — Solid Subtract (namespace: FaceEntity, shape: reduce)
    • 0 : faceEntity[] — The closed solid (Face[]) to carve out of this one (3D CSG).
  • solidIntersect — Solid Intersect (namespace: FaceEntity, shape: reduce)
    • 0 : faceEntity[] — The closed solid (Face[]); keeps only the volume shared by both (3D CSG).
  • solidSelfUnion — Solid Self Union (namespace: FaceEntity, shape: reduce)
    • (no args)
  • faceLattice — Lattice (namespace: FaceEntity, shape: map)
    • 0 : length — Strut/wall thickness of the lattice shell.
    • 1 : binary — Close the open border edges of the lattice with caps.
  • faceExtrude — Face Extrude (namespace: FaceEntity, shape: map)
    • 0 : length — Distance to push the selected faces along their normal (negative = inward).
    • 1 : function — Predicate over a face's points and normal selecting which faces to push; default = all.
  • edgeFillet — Edge Fillet (namespace: FaceEntity, shape: map)
    • 0 : function — Predicate over an edge's two endpoints selecting which edges to round; default = all.
    • 1 : length — Rolling-ball arc radius of the rounded edge (curved).
    • 2 : number — Number of facets across the arc; higher = smoother.
  • edgeChamfer — Edge Chamfer (namespace: FaceEntity, shape: map)
    • 0 : function — Predicate over an edge's two endpoints selecting which edges to cut; default = all.
    • 1 : length — Flat-cut setback from the edge (a straight 45°-style bevel, not an arc).
  • faceReverse — Reverse (namespace: FaceEntity, shape: map)
    • 0 : binary — true flips direction (curve order / face normal); false leaves it.
  • faceOffset — Offset Face (namespace: FaceEntity, shape: map)
    • 0 : length — Inset (positive) or outset (negative) distance.

Instance

  • arrayLinear — Linear Array (namespace: Instance, shape: multiply)
    • 0 : vector — Per-copy step — magnitude IS the spacing (unlike Array Along Distances, where direction is normalized).
    • 1 : number — Copies to add — the original stays, so 1 duplicates once.
  • arrayRadial — Radial Array (namespace: Instance, shape: multiply)
    • 0 : axis — Rotation axis: origin = pivot point, direction = spin axis.
    • 1 : number — Copies to add around the axis — original + copies spaced evenly over 360°.
  • arrayCurve — Array Along Curve (namespace: Instance, shape: multiply)
    • 0 : curveEntity — Path curve the copies are distributed along.
    • 1 : number — Copies to add along the curve — original at the start, spaced evenly.
    • 2 : angle — Extra rotation in degrees applied to each copy about the curve tangent.
  • arrayDistances — Array by Distances (namespace: Instance, shape: multiply)
    • 0 : vector — Direction to array along — magnitude ignored, see Distances for spacing.
    • 1 : length[] — Gap after each copy, in order, along Direction. Copy count = number of distances (a partition — no separate Copies arg).
  • mirror — Mirror (namespace: Instance, shape: multiply)
    • 0 : plane — Mirror plane (point on plane + normal).
    • 1 : binary — 1 keeps the original and adds the reflection; 0 flips in place.
  • move — Move (namespace: Instance, shape: map)
    • 0 : vector — Displacement to apply.
  • rotate — Rotate (namespace: Instance, shape: map)
    • 0 : axis — Rotation axis (origin = pivot point, direction = axis; magnitude irrelevant).
    • 1 : angle | fn — Rotation angle in degrees (CCW about the axis), or a callback (entity, index) => number, per entity.
  • resize — Resize (namespace: Instance, shape: map)
    • 0 : length | fn — Target width along X, or a callback (entity, index) => number, per entity.
    • 1 : length | fn — Target depth along Y, or a callback (entity, index) => number, per entity.
    • 2 : length | fn — Target height along Z, or a callback (entity, index) => number, per entity.
  • applyMaterial — Material (namespace: Instance, shape: map)
    • 0 : material | fn — A materials[] record name to assign (single-quote literals), or a callback (entity, index) => string, per entity.
  • applyLayer — Layer (namespace: Instance, shape: map)
    • 0 : string | fn — Layer name to assign (single-quote literals), or a callback (entity, index) => string, per entity.
  • visible — Visible (namespace: Instance, shape: map)
    • 0 : binary | fn — true shows, false hides the entities; or a callback (entity, index) => boolean, per entity.
  • name — Name (namespace: Instance, shape: map)
    • 0 : string | fn — Display/export name tag (single-quote literals), or a callback (entity, index) => string, per entity.

Group

  • groupExplode — Explode (namespace: Group, shape: structural)
    • 0 : binary — Dissolve this node's nested groups (a placed sub-model / clone) into raw geometry — SketchUp Explode. 1 = recursive; 0 = one level.
  • looseToGroup — Loose to Groups (namespace: Group, shape: structural)
    • (no args)
  • makeGroup — Make Group (namespace: Group, shape: structural)
    • (no args)

Parameters — value bindings (header lane)

angle

  • angle.angle — Angle
    • 0 : angle

boolean

  • boolean.boolean — Boolean
    • 0 : binary

color

  • color.color — Color
    • 0 : color

length

  • length.length — Length
    • 0 : length

loadModel

  • loadModel.loadModel — Load Model
    • 0 : model — The embedded instanced ModelJSON (installed by the editor — upload, URL, or catalog pick). Built once and cached; place copies with the placeModel operation.

number

  • number.number — Number
    • 0 : number

text

  • text.text — Text
    • 0 : string
<!-- /AUTOGEN:nodes -->

Model schema (TypeScript)

The developer-facing model contract, extracted verbatim from src/schema.ts:

<!-- AUTOGEN:schema -->
type expression = string // any JS expression: arithmetic, parameter key refs, Math.* calls, ternary, arrow functions; literal text single-quoted ('Oak')

type ValueMethod = "angle" | "arrayLength" | "boolean" | "color" | "function" | "length" | "loadModel" | "number" | "text"

type EntityMethod = "applyLayer" | "applyMaterial" | "arcCenterStartEnd" | "arcTangentEnd" | "arcThreePoints" | "arrayCurve" | "arrayDistances" | "arrayLinear" | "arrayRadial" | "circle" | "clone" | "cubicBezier" | "curveChamfer" | "curveExtrude" | "curveFill" | "curveFillet" | "curveIntersect" | "curveOffset" | "curveReverse" | "curveSelfUnion" | "curveSplit" | "curveSubtract" | "curveThicken" | "curveUnion" | "edgeChamfer" | "edgeFillet" | "ellipseArc" | "entities" | "extrude" | "faceExtrude" | "faceLattice" | "faceOffset" | "faceReverse" | "groupExplode" | "loft" | "looseToGroup" | "makeGroup" | "mirror" | "move" | "name" | "nurbsCurve" | "patchFromCorners" | "patchFromCurve" | "placeModel" | "pointEntity" | "polygon" | "polyline" | "quadraticBezier" | "rectangle" | "resize" | "revolve" | "rotate" | "solidIntersect" | "solidSelfUnion" | "solidSubtract" | "solidUnion" | "spline" | "sweep" | "textCurve" | "visible"

type Method = ValueMethod | EntityMethod

type ModelJSON = {
    id?: string
    title: string
    camera?: CameraJSON
    materials?: {
        name: string
        baseColor?: string
        texture?: string
        tileWidth?: number
        tileHeight?: number
        colorFactor?: number
        opacity?: number
        roughness?: number
        metallic?: number
    }[]
    layers?: ({
        name: string
        color?: unknown[]
        lineStyle?: "solid" | "dashed" | "dotted"
    })[]
    fonts?: {
        name: string
        url: string
    }[]
    parameters?: NodeJSON[]
    operations?: NodeJSON[]
    unit: {
        length: "mm" | "cm" | "m" | "in" | "ft" /* default: "mm" */
        area?: "mm2" | "cm2" | "m2" | "in2" | "ft2"
        volume?: "mm3" | "cm3" | "m3" | "in3" | "ft3" | "L"
        mass?: "mg" | "g" | "kg" | "lb" | "oz"
    } /* default: {"length":"mm"} */
}

type CameraJSON = {
    type?: "perspective" | "orthographic"
    position: unknown[]
    target: unknown[]
    zoom?: number
}

type NodeJSON = OperationJSON | ContainerJSON

type OperationJSON = {
    key?: string
    label?: expression
    enabled?: expression
    method: Method
    args?: ArgumentJSON[]
    animation?: {
        keyframes: {
            time: number
            value: number
        }[]
        loop?: boolean
    }
}

type ContainerJSON = {
    key?: string
    label?: expression
    enabled?: expression
    operations: (OperationJSON | ContainerJSON)[]
}

type ArgumentJSON = {
    key: string
    input: string | Record<string, unknown>
    label?: string
    min?: expression
    max?: expression
    step?: expression
    options?: OptionJSON[]
}

type OptionJSON = {
    label: string
    input: expression
}

// JS globals in expression scope — using as node key shadows the global
const JS_RESERVED = ["Math","Number","String","Boolean","Array","Object","isNaN","parseFloat","parseInt"] as const

// ECMAScript reserved words & special identifiers — forbidden as node keys
const JS_KEYWORDS = ["break","case","catch","continue","debugger","default","delete","do","else","finally","for","function","if","in","instanceof","new","return","switch","this","throw","try","typeof","var","void","while","with","class","const","enum","export","extends","import","let","static","super","yield","async","await","implements","interface","package","private","protected","public","of","null","true","false","undefined","NaN","Infinity","arguments","eval"] as const
<!-- /AUTOGEN:schema -->

Common patterns

All JSON examples below use v21 format: args is an array of { key, input } objects; every node is { method, args } (operation) or { operations: [...] } (container) — no nodeType, no id, no generators[]/modifiers[] split.

Making a box (no box primitive)

There is no box/cylinder/sphere primitive — build a solid from a profile.

[
  { "key": "profile", "operations": [
    { "method": "rectangle", "args": [{ "key": "width", "input": "width" }, { "key": "height", "input": "depth" }] }
  ] },
  { "key": "box", "operations": [
    { "method": "extrude", "args": [{ "key": "profile", "input": "profile" }, { "key": "thickness", "input": "height" }] }
  ] }
]

Panel (flat board, positioned and materialed)

[
  { "key": "panelProfile", "operations": [
    { "method": "rectangle", "args": [{ "key": "width", "input": "width" }, { "key": "height", "input": "depth" }] }
  ] },
  { "key": "panel", "operations": [
    { "method": "extrude", "args": [{ "key": "profile", "input": "panelProfile" }, { "key": "thickness", "input": "thickness" }] },
    { "method": "move", "args": [{ "key": "vector", "input": "[0, 0, zOffset]" }] },
    { "method": "applyMaterial", "args": [{ "key": "name", "input": "'Wood'" }] }
  ] }
]

Panel with hole (boolean subtract)

[
  { "key": "holeProfile", "operations": [
    { "method": "rectangle", "args": [{ "key": "width", "input": "holeW" }, { "key": "height", "input": "holeH" }] },
    { "method": "move", "args": [{ "key": "vector", "input": "[holeX, 0, 0]" }] }
  ] },
  { "key": "hole", "operations": [
    { "method": "extrude", "args": [{ "key": "profile", "input": "holeProfile" }, { "key": "thickness", "input": "thickness + 2" }] },
    { "method": "move", "args": [{ "key": "vector", "input": "[0, -1, 0]" }] }
  ] },
  { "key": "panelProfile", "operations": [
    { "method": "rectangle", "args": [{ "key": "width", "input": "width" }, { "key": "height", "input": "height" }] }
  ] },
  { "key": "panel", "operations": [
    { "method": "extrude", "args": [{ "key": "profile", "input": "panelProfile" }, { "key": "thickness", "input": "thickness" }] },
    { "method": "solidSubtract", "args": [{ "key": "solid", "input": "hole" }] }
  ] }
]

Extruded profile with rounded edges

[
  { "key": "profile", "operations": [
    { "method": "rectangle", "args": [{ "key": "width", "input": "width" }, { "key": "height", "input": "depth" }] }
  ] },
  { "key": "body", "operations": [
    { "method": "extrude", "args": [{ "key": "profile", "input": "profile" }, { "key": "thickness", "input": "height" }] },
    { "method": "edgeFillet",
      "args": [{ "key": "edgeFilter", "input": "(p1, p2) => true" }, { "key": "radius", "input": "cornerRadius" }, { "key": "segments", "input": "8" }] }
  ] }
]

Mirror — symmetric pair

{ "key": "legs", "operations": [
  { "method": "extrude", "args": [{ "key": "profile", "input": "legProfile" }, { "key": "thickness", "input": "height" }] },
  { "method": "move", "args": [{ "key": "vector", "input": "[legX, 0, 0]" }] },
  { "method": "mirror", "args": [{ "key": "plane", "input": "{ point: [0,0,0], normal: [1,0,0] }" }] }
] }

This emits the leg at +legX and its mirror at -legX.

Linear array of shelves

{ "key": "shelves", "operations": [
  { "method": "extrude", "args": [{ "key": "profile", "input": "shelfProfile" }, { "key": "thickness", "input": "thickness" }] },
  { "method": "applyMaterial", "args": [{ "key": "name", "input": "'Plywood'" }] },
  { "method": "arrayLinear",
    "args": [{ "key": "vector", "input": "[0,0,shelfSpacing]" }, { "key": "copies", "input": "shelfCount - 1" }] }
] }

Revolved vase / column

[
  { "key": "profile", "operations": [
    { "method": "spline", "args": [{ "key": "points", "input": "[[bottomR,0],[topR,height*0.3],[topR*0.8,height*0.7],[topR,height]]" }, { "key": "degree", "input": "3" }] }
  ] },
  { "key": "vase", "operations": [
    { "method": "revolve", "args": [{ "key": "curves", "input": "[profile]" }, { "key": "axis", "input": "{ origin: [0,0,0], direction: [0,1,0] }" }, { "key": "angle", "input": "360" }] }
  ] }
]

Cabinet body (5 panels)

Pattern: top, bottom, left side, right side, back — positioned via move.

{
  "parameters": [
    { "key": "W", "method": "length", "args": [{ "key": "length", "input": "600" }] },
    { "key": "H", "method": "length", "args": [{ "key": "length", "input": "720" }] },
    { "key": "D", "method": "length", "args": [{ "key": "length", "input": "560" }] },
    { "key": "T", "method": "length", "args": [{ "key": "length", "input": "18" }] }
  ],
  "operations": [
    { "key": "hProfile", "operations": [
      { "method": "rectangle", "args": [{ "key": "point1", "input": "[-(W)/2,-(D)/2,0]" }, { "key": "point2", "input": "[(W)/2,(D)/2,0]" }] }
    ] },
    { "key": "vProfile", "operations": [
      { "method": "rectangle", "args": [{ "key": "point1", "input": "[-(T)/2,-(D)/2,0]" }, { "key": "point2", "input": "[(T)/2,(D)/2,0]" }] }
    ] },
    { "key": "bottom", "operations": [
      { "method": "extrude", "args": [{ "key": "profile", "input": "hProfile" }, { "key": "thickness", "input": "T" }] }
    ] },
    { "key": "top", "operations": [
      { "method": "extrude", "args": [{ "key": "profile", "input": "hProfile" }, { "key": "thickness", "input": "T" }] },
      { "method": "move", "args": [{ "key": "vector", "input": "[0, 0, H - T]" }] }
    ] },
    { "key": "left", "operations": [
      { "method": "extrude", "args": [{ "key": "profile", "input": "vProfile" }, { "key": "thickness", "input": "H - 2 * T" }] },
      { "method": "move", "args": [{ "key": "vector", "input": "[-(W - T) / 2, 0, T]" }] }
    ] },
    { "key": "right", "operations": [
      { "method": "extrude", "args": [{ "key": "profile", "input": "vProfile" }, { "key": "thickness", "input": "H - 2 * T" }] },
      { "method": "move", "args": [{ "key": "vector", "input": "[(W - T) / 2, 0, T]" }] }
    ] },
    { "key": "back", "operations": [
      { "method": "extrude", "args": [{ "key": "profile", "input": "hProfile" }, { "key": "thickness", "input": "H - 2 * T" }] },
      { "method": "move", "args": [{ "key": "vector", "input": "[0, -(D - T) / 2, T]" }] }
    ] }
  ]
}

Pipe / tube (curveSubtract ring + sweep)

[
  { "key": "outerCircle", "operations": [
    { "method": "circle", "args": [{ "key": "center", "input": "[0,0]" }, { "key": "radius", "input": "outerR" }] }
  ] },
  { "key": "innerCircle", "operations": [
    { "method": "circle", "args": [{ "key": "center", "input": "[0,0]" }, { "key": "radius", "input": "outerR - wallT" }] }
  ] },
  { "key": "section", "operations": [
    { "method": "clone", "args": [{ "key": "source", "input": "outerCircle" }] },
    { "method": "curveSubtract", "args": [{ "key": "curve", "input": "innerCircle" }] }
  ] },
  { "key": "tube", "operations": [
    { "method": "sweep", "args": [{ "key": "path", "input": "railPath" }, { "key": "profile", "input": "section" }, { "key": "caps", "input": "true" }] }
  ] }
]

Note: a node can reference an earlier node only — order matters within the operations[] array.


Key rules

  • key: short camelCase name used in expressions. Must be unique within the model (including inside nested containers). Rename freely — update all expressions that reference it. There is no per-node idkey is the only identifier a document carries (the model itself has an optional top-level id).
  • Parameters: keep key short and descriptive (width, thickness, shelfCount).
  • Containers: name by what they produce (panel, leg, shelf, body).

AI generation tips

  • Start with parameters for every dimensional value. No magic numbers in args.
  • args is an array of { "key": "argName", "input": "expression" } — never an object map.
  • label is a top-level field (expression) on either node form, NOT inside meta.
  • Box is centered at origin — use a move transform to position it.
  • To position a box on top of another: move vector = [0, 0, baseHeight / 2 + thisHeight / 2].
  • A solidSubtract/solidUnion/solidIntersect transform's solid arg (or a curveSubtract/curveUnion/curveIntersect transform's curve arg) must reference a node defined before this one in operations[].
  • solidSubtract / solidUnion / solidIntersect (3D CSG on closed face arrays) and curveSubtract / curveUnion / curveIntersect (2D boolean on closed curves) are separate methods — pick by the entity kind you're operating on, not by context.
  • arrayLinear vector is the per-copy step: vector=[0,0,spacing] repeats every spacing mm along Z (magnitude IS the spacing).
  • move vector = [dx,dy,dz] = displacement vector in mm, applied to each entity.
  • For hollow objects (cabinet panels), use individual panels positioned by move — more predictable than boolean subtract.
  • mirror plane is an object { point: [x,y,z], normal: [nx,ny,nz] } — not a function call.
  • arrayRadial axis is an object { origin: [x,y,z], direction: [dx,dy,dz] } — not separate origin/axis args.
  • edgeFillet (with edgeFilter) rounds selected edges; edgeChamfer does a flat cut. Both are transform methods applied after the solid-producing step in the same operations[] list.
  • revolve needs curves (a curve node key) and axis (object). Returns tessellated Face[].
  • Transforms follow a producer in the SAME operations[] array (not a separate modifiers field). Group several steps under one key with a container ({ key, operations: [...] }).
📖 38 min read