Parametron
A parametric modeling SDK. A model is a JSON document (ModelJSON) — named parameters plus a fold of geometry operations — and the engine turns it into a live, incrementally-re-evaluated entity graph. Text is the source format: models are authored by UIs, by hand, or by an LLM, and every dimension is an expression ("lengthX - thickness * 2"), never a magic number.
@parametron/parametric THE CORE — schema, validation, expression language,
pull-based evaluation graph, registry contract.
Everything else is replaceable.
@parametron/ui Reference UI (builder, configurator, catalog, font &
material editors) built on Domphy.
@parametron/three Reference renderer built on Three.js.
@parametron/svg Reference renderer to SVG — plan-view (XY) SceneJSON
to a standalone SVG string. No DOM, no deps.ui, three and svg are reference implementations. They are production-grade (they power parashape.com, the ParaShape SketchUp plugin and texturic), but nothing in the core depends on them: a host can render with Babylon.js, PlayCanvas, a Blender add-on, or SketchUp Ruby by consuming the same engine contracts this document describes. Use them when they fit; replace them when they don't.
The geometry kernel (NURBS, meshes, 2D booleans) is @huukhanhnguyen/shapemetry, a separate package.
Quickstart — evaluate a model, no UI, no renderer
import { createRegistry, evaluateModel } from "@parametron/parametric"
import { nodeRegistry } from "@parametron/parametric/nodes"
const registry = createRegistry(nodeRegistry)
const model = {
title: "Plate",
unit: { length: "mm" },
parameters: [
{ method: "length", key: "width", args: [{ key: "length", input: "200" }] },
],
operations: [
{
key: "plate",
operations: [
{ method: "rectangle", args: [
{ key: "point1", input: "[0,0,0]" },
{ key: "point2", input: "[width, width / 2, 0]" },
]},
{ method: "curveExtrude", args: [{ key: "thickness", input: "18" }] },
],
},
],
}
const result = evaluateModel(model, { width: 300 }, { registry })
// result.geometry — baked world-space entities, one slice per node
// result.params — the declared parameters with current valuesEverything is synchronous. Async work (loading fonts, sub-models) happens BEFORE evaluation, through the Store — the graph only ever does sync lookups.
The document model (ModelJSON, v21/v22)
ModelJSON = {
title: string
unit: { length: "mm" | ... }
parameters?: NodeJSON[] // HEADER — named value bindings (map algebra)
operations?: NodeJSON[] // BODY — one fold over a stream of entities
materials?: MaterialJSON[] // static asset lanes, consumed by NAME
layers?: LayerJSON[]
fonts?: FontJSON[]
}
NodeJSON = { key?, label?, enabled?, method, args? } // operation
| { key?, label?, enabled?, operations: NodeJSON[] } // containerOne recursive node shape. An operation either produces entities (concat into the stream) or transforms the stream; a container runs its own fold and concats the result. Every arg input is an expression string evaluated against the parameter environment and every other node's key. The full authoring guide is /parametric/reference; the per-entity API table is /parametric/entities.
Scene entities are plain JSON with no type tag — kind is sniffed from the geometry shape: point = [x,y,z], curve = NURBS JSON, face = {loops, uvMatrix}, group = shared-definition placement (isPoint / isCurve / isFace / entityKind).
The registry is the engine's INPUT
The core has no built-in CAD catalog. You inject one per model:
import { createRegistry, Model } from "@parametron/parametric"
import { nodeRegistry } from "@parametron/parametric/nodes" // ParaShape's stock catalog
const registry = createRegistry(nodeRegistry)
const model = Model.fromJSON(json, { registry })RegistryInput is a flat, serializable description: value methods (parameter types), entity methods (create for producers, compute for transforms), expression namespaces (Point.*, Curve.*, …), and display attributes the engine never reads. To extend, append OperationDefinitions to the input — the stock catalog's compute functions are reusable via @parametron/parametric/compute (pure geometry modules, no engine imports).
Writing your own renderer
The engine outputs plain data; a renderer is a consumer of one of these contracts (pick by how much incrementality you need):
- One-shot —
model.toScene()returnsSceneJSON: a tree of nodes with tessellatedMeshentities (positions/indices/uvs), curves as polylines, points, and material NAMES (bodies live on the model's asset lanes). Simplest possible integration — this is what thumbnails and the SketchUp export use, and@parametron/svg(sceneToSvg(scene)→ SVG string) is its minimal reference consumer. Incremental (recommended for live editing) — after
model.evaluate():import { collectRenderFrame, contentKey, diffEntityFrame } from "@parametron/parametric" const frame = collectRenderFrame(model.nodes) // RenderFrameSlice[] // slice.leaves — baked world-space entities (render + forget) // slice.groups — instancing lane: per placement, shared `leaves` array // reference (tessellate ONCE per definition) + a transform const diff = diffEntityFrame(previousCells, slice.leaves) // diff.kept / propsChanged / added / removed — reconcile like a virtual DOMcontentKey(entity)hashes geometry only (ids/props excluded), so an unchanged shape is never re-tessellated and a moved group is a matrix-only update.@parametron/three'sRendereris the reference consumer of exactly this loop — port that file's structure to Babylon / PlayCanvas / WebGPU and you have a live host.- Change notifications — subscribe on the collection (
model.nodes.notifier), coalesce into one sync per frame (rAF), rebuild the render frame, diff, apply. The graph's per-node dirty tracking makes re-evaluation cheap; the renderer never needs node-level wiring.
Writing your own UI
A UI is a projection of the same document: read registry.nodes for the method catalog (labels, args, tooltips — everything is data), mutate the model through node APIs (setInput, setMethod, addDynamicArg, NodeCollection structural ops), and re-evaluate. Expressions are strings — a text field IS a complete editor. @parametron/ui is the reference: panels receive a host interface (callbacks for persistence, viewport bridge, catalog data) and never touch the network or the DOM outside their root.
Locale note: @parametron/ui ships its own string catalog on @domphy/i18n (initI18n / setLocale exported from the barrel). A host app composes it alongside its own instance — see apps/web/src/i18n.ts in the parashape repo.
Repo layout & commands
packages/parametric core engine (tsup build: index + /compute + /nodes)
packages/ui reference UI (main: src, Domphy)
packages/three reference renderer (main: src, Three.js)
packages/svg reference renderer (main: src, SVG string out)pnpm install
pnpm test # typecheck all packages + vitest (374 tests)
pnpm --dir packages/parametric build # dist for consumers that need built outputConsumers link via pnpm workspace globs (../parametron/packages/*) — see parashape for the wiring pattern (vite aliases to src/ for HMR, a committed _vendor copy of the parametric dist for serverless functions).
Design laws (the short list)
- Schema is the product. One format, no back-compat shims; correctness before scale. Geometry code is test-first from math invariants, never from observed output.
- Value vs entity: bare PascalCase namespaces (
Curve.length) compute VALUES inside expressions; camelCase methods produce/transform renderable ENTITIES in the fold. A solid isFace[]— there is no block primitive. - Assets are static records consumed by name (
materials[]/layers[]/fonts[]lanes); parameters are live values consumed by reference; operations are the stream. - Compute never mutates its input — per-step caches key on identity.
- The engine never fetches. Hosts pre-load assets into the
Store; a missing asset at eval time is a lookup error, not a network call.