Parametron

@parametron/three is the reference renderer built on Three.js — the production consumer behind parashape.com and the ParaShape SketchUp plugin's web preview. It consumes the core's incremental render contract (see Render contract for collectRenderFrame/ diffEntityFrame) and turns an EvaluateResult into a live Three.js scene graph that stays in sync as the model changes. Nothing in @parametron/parametric depends on it, and three never imports ui — the two reference packages are siblings, each consuming the core contracts independently.

Entry points

The package exposes two subpaths. . (this page) re-exports Scene, Group, Object3D from three plus renderGeometryTree and its supporting utilities — this package is the only place in the repo that imports "three" directly, so every consumer gets Three types/classes through here instead of pinning their own version:

import { Scene, renderGeometryTree, disposeModelRoot, nodeKeyFromObject } from "@parametron/three"

The root entry deliberately stays cheap: it only imports from Renderer.ts, never OrbitControls/TransformControls/LineSegments2 or a WebGLRenderer construction. Those live behind ./viewport — the Viewport class, documented on Viewport — which consumers dynamic-import lazily so a page that only needs one-shot rendering never pays for the full 3D viewport's dependency weight.

renderGeometryTree — the render sync loop

function renderGeometryTree(
    scene: Scene,
    result: EvaluateResult,
    placedId: string,
    onUpdate?: () => void,
    displayName?: string,
): Object3D

Given a model's EvaluateResult (from model.evaluate() — see Model & pull graph), builds a Group named displayName || placedId, tags it userData.placedId = placedId, adds it to scene, and keeps it live for as long as the model keeps changing:

import { Model, createRegistry } from "@parametron/parametric"
import { nodeRegistry } from "@parametron/parametric/nodes"
import { Scene, renderGeometryTree, disposeModelRoot } from "@parametron/three"

const registry = createRegistry(nodeRegistry)
const model = Model.fromJSON(modelJson, { registry })
const result = model.evaluate({ width: 300 })

const scene = new Scene()
const root = renderGeometryTree(scene, result, placedId, () => requestRender())

// later — switching or removing a placed model:
disposeModelRoot(scene, root)

Internally it subscribes to "change" on every ContainerNode/OperationNode in result.nodes, plus the collection's own "change" (covers structural add/remove and re-subscribes nodes added later). Any number of change notifications in one tick coalesce into a single requestAnimationFrame sync — never more than one rebuild per frame. Each sync:

  1. Re-syncs materialCache from the model's current material records.
  2. Calls collectRenderFrame(result.nodes) — one slice per source node.
  3. For each slice, reconciles a ${key}_group container against that slice's leaves (matched by content key, via diffEntityFrame — kept / updated / added / removed) and groups (the instancing lane, below).
  4. Removes containers for slices that disappeared (a node deleted from the model).
  5. Calls onUpdate?.() — the hook a host uses to request a render frame.

This is why a slider edit that only moves a node is a matrix update, not a re-tessellation. renderGeometryTree is the reference consumer of the engine's incremental contract; porting its structure (collect → diff → build/update/dispose per lane) is how you'd write the equivalent loop for Babylon.js, PlayCanvas, or SketchUp Ruby.

Group lane — template + matrix-only clones

Each group placement is keyed by definition content (its leaves' content keys, joined): one hidden, tessellated-once Group template per definition, and one visible clone per placement (matrixAutoUpdate = false, matrix set straight from placement.transformation). Reconcile runs in passes over the previous frame's cells — exact match (same definition + same placement + same material override) reused untouched, same-definition-but-moved reused via a matrix/props-only update, and only a genuinely new definition clones from a (possibly freshly built) template. Templates live inside the render container, not detached, so disposeModelRoot's traversal still reaches their geometry even though only clones reference it; an orphaned template (no live cell references it) is disposed once nothing points at it anymore.

Materials

materialCache is a Map<string, MeshStandardMaterial> keyed by material name, kept in sync from the model's materials[] records on every renderGeometryTree sync pass. Entries are updated in place — editing a material's color/roughness reaches every mesh already holding that instance without re-tessellating anything. A leaf entity with no material prop (or a name missing from the cache) falls back to a shared default MeshStandardMaterial (white, roughness 0.95).

Textures referenced by a material record's texture field (a URL or data: URI) load once and cache by URI with RepeatWrapping; tiling is derived from the record's tileWidth/tileHeight (plane millimetres — 1 tile per metre by default), so geometry scale never stretches the grain. colorFactor blends white (texture shows through unmodified) toward baseColor (solid tint) — 1 is fully tinted, 0 is texture-only.

Debug helpers

Four visibility toggles, each operating on an already-built scene subtree:

FunctionSuffix constantShows
setWireframeVisible(visible, root)SKETCH_EDGES_SUFFIX ("_sketch_edges")Hides each mesh's own material, leaving only the permanent SketchUp-style edge lines (attached at build time from a Face's boundary loops, or EdgesGeometry as a fallback)
setNormalsVisible(visible, root)NORMALS_SUFFIX ("_normals")Per-vertex normal lines on meshes, plus a curve-normal helper on Line objects
setFaceNormalsVisible(visible, root)FACE_NORMALS_SUFFIX ("_facenormals")Per-triangle face-normal lines
setSurfaceGridVisible(visible, root)SURFACE_GRID_SUFFIX ("_surfgrid")Toggles visibility of any object already named with this suffix — this package only defines the naming convention and the toggle, nothing here builds grid geometry

Sketch edges are attached once, at build time, for every mesh — they're what setWireframeVisible reuses rather than deriving a separate wireframe. Normals/face-normals helper geometry, by contrast, is built lazily on the first toggle-on, then just hidden/shown afterward — a feature that's almost always off never pays per-mesh helper-build cost.

Picking & disposal

nodeKeyFromObject(object, root) walks an Object3D up to the ${key}_group container renderGeometryTree created directly under root, returning that node's key (or null if object isn't under root). Pair it with a raycast (Viewport.pick, see Viewport) to map a screen click back to the model node that produced it.

disposeModelRoot(scene, root) removes root from scene, releases the change-listener subscriptions renderGeometryTree registered, and disposes every geometry/material it owns. Shared cache/singleton materials (materialCache entries, the default/edge/normals materials) are skipped — disposing them would break every other mesh still referencing them. Call it whenever a placed model is removed or swapped.

Units

Viewport (@parametron/three/viewport) wires up a SceneUnit per scene as viewport.unit — a standalone conversion helper (no dependency on the parametric engine or Three) that also stamps scene.userData.unit / scene.userData.metersPerUnit for anything else reading the scene directly:

viewport.unit.set("mm")
viewport.unit.toScene(1, "in")       // 25.4   (inches → mm)
viewport.unit.fromScene(25.4, "in")  // 1      (mm → inches)
viewport.unit.convert(1, "ft", "mm") // 304.8  (unit-to-unit, no scene state)
viewport.unit.format(25.4)           // "25.40 mm"
viewport.unit.label("vi")            // "Milimét"

Unit is one of "mm" | "cm" | "m" | "in" | "ft". current, metersPerUnit, symbol, and label(locale) read the active unit; set(unit) changes it (and re-stamps scene.userData).

See also

  • Render contract — the collectRenderFrame/ diffEntityFrame contract this package's sync loop consumes.
  • Viewport — camera, controls, transform gizmo, and hover/highlight preview, built on top of this page's renderGeometryTree.
  • Entity API — the geometry shape each leaf entity carries before tessellation.
  • UI overview — the reference panels that typically drive a Viewport through BuilderHost.viewport.
📖 5 min read