Skip to content
Shapemetry

Parametric Authoring

A parametric model is a ModelJSON document. This page is the authoring model; the runnable intro is Quickstart: parametric. The exhaustive node catalog lives in REFERENCE.md (node grammar, expression rules, operation catalog) and ENTITIES.md (entity shapes) in packages/model.

Document shape: lanes

ModelJSON is title plus named lanes — arrays of nodes with distinct roles:

const doc = {
  title: 'bracket',
  parameters: [/* named values + computed expressions */],
  materials:  [/* looks */],
  layers:     [/* organization */],
  objects:    [/* the scene — geometry is built here */],
  textures:   [/* procedural texture chains */],
}

The full lane list: materials, layers, styles, cameras, skeletons, animations, parameters, objects, opening, views, components, textures, tables, sheets, documents. Only title is required. Runtime lanes collapse to four roles — parameters, scene (stored as objects), component, flat.

Nodes: operations with method + args

One recursive node shape. An operation names a registry method; every arg input is an expression string, even constants:

{
  key: 'plate',
  operations: [
    { method: 'rectangle', args: [
      { key: 'point1', input: '[-(w)/2,-(60)/2,0]' },
      { key: 'point2', input: '[(w)/2,(60)/2,0]' },
    ] },
    { method: 'curveExtrude', args: [{ key: 'thickness', input: '8' }] },
    { method: 'solidUnion', args: [{ key: 'solid', input: 'boss' }] }, // references another node
  ],
}

A container swaps method for { operations: [...] } children that fold into one stream. Args may carry UI metadata (min, max, step, options) beside input.

References and expressions

  • Bare key"boss" references that node's output stream.
  • Arithmetic over parameters"[-(w)/2,-(60)/2,0]".
  • Computation namespaces — the geometry namespaces are callable: "NurbsCurve.length(centerline)", "Axis.fromPointDirection([0,0,0],[0,0,1])".
  • Evaluation overridesevaluateModel(doc, { w: 800 }, { registry }) overrides parameters transiently for that pass (visible in result.params and the evaluated graph); the document is never written back, and the lazy geometry bake re-applies the same overrides when read, so it reflects the overridden pass too.

Chains and lane records

Two record spines recur across lanes (types from @huukhanhnguyen/types):

// Chain — the spine of skeletons / animations / textures:
type Chain = { key: string; label?: string; parameters?: (Operation | Container)[]; operations: (Operation | Container)[] }

// LaneNode — one cameras[] entry; method bound to the lane's vocabulary:
type LaneNode<M extends string = string> = {
  key: string; label?: string; enabled?: string; method: M; args?: Argument[]
}

Runtime doors

import { Model, createRegistry, evaluateModel, inspectModel } from '@huukhanhnguyen/model'
import { nodeRegistry } from '@huukhanhnguyen/model/nodes'

const registry = createRegistry(nodeRegistry)      // once per app
const model = Model.fromJSON(doc, { registry })    // validates; throws on structural errors
const result = model.evaluate({ w: 800 })          // re-evaluate with overrides, cheap to repeat

// Diagnostics without rendering: per-group bounds (BoundingBox = { min, max }), counts
const report = inspectModel(model)
console.log(report.groups[0]?.bounds)

result.geometry gives baked render items per scene key; result.nodes is the evaluated graph; result.diagnostics collects evaluation issues. Kind-sniff entities with entityTypeOf / isMesh / isShell from @huukhanhnguyen/model.

Where next

Last updated: 📖 2 min readEdit on GitHub