@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,
): Object3DGiven 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:
- Re-syncs
materialCachefrom the model's current material records. - Calls
collectRenderFrame(result.nodes)— one slice per source node. - For each slice, reconciles a
${key}_groupcontainer against that slice'sleaves(matched by content key, viadiffEntityFrame— kept / updated / added / removed) andgroups(the instancing lane, below). - Removes containers for slices that disappeared (a node deleted from the model).
- 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:
| Function | Suffix constant | Shows |
|---|---|---|
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/diffEntityFramecontract 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
geometryshape each leaf entity carries before tessellation. - UI overview — the reference panels that typically drive a
ViewportthroughBuilderHost.viewport.