Skip to content
Shapemetry

Quickstart: Your First Parametric Model

A parametric model is a ModelJSON document: named lanes of operations whose args are expression strings. The @huukhanhnguyen/model runtime evaluates it against a node registry and returns scene entities. This quickstart builds the smallest real one — a 400 × 300 × 200 box — adapted verbatim from the engine's own tests (packages/model/src/__tests__/quickInsertPrimitives.test.ts).

1. The document

Two operations in the objects lane: sketch a rectangle on the XY plane, then extrude it. Every input is an expression string, even constants.

const doc = {
  title: 'first-box',
  objects: [
    {
      key: 'box',
      operations: [
        {
          method: 'rectangle',
          args: [
            { key: 'point1', input: '[-200,-150,0]' },
            { key: 'point2', input: '[200,150,0]' },
          ],
        },
        { method: 'curveExtrude', args: [{ key: 'thickness', input: '200' }] },
      ],
    },
  ],
}

2. Evaluate it

Evaluation needs a registry — the catalog of node implementations. The default catalog ships as the optional ./nodes subpath; it is never pulled in unless you import it.

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

const registry = createRegistry(nodeRegistry) // build once, reuse

const result = evaluateModel(doc, {}, { registry })
// ^ validateModelJSON runs first — structural errors throw here.

evaluateModel(json, params, { registry }) is the one-shot door. For a model you keep editing, use const model = Model.fromJSON(doc, { registry }) and later model.evaluate(params).

3. Read the result

result.geometry is the evaluated scene: one group per objects key, whose items are plain JSON entities — solids already baked to render meshes. Kind is sniffed from shape with the scene guards:

import { isMesh } from '@huukhanhnguyen/model'
import { meshMetrics } from '@huukhanhnguyen/model'

const items = result.geometry.find((g) => g.key === 'box')?.items ?? []
const meshes = items.filter(isMesh).map((e) => e.geometry)
// meshes: { positions: number[], faces: number[][] }[] — n-gon render meshes

const metrics = meshMetrics(meshes)
console.log(metrics.sealed)                    // true
console.log(Math.abs(metrics.volume))          // ≈ 400·300·200

Entities are { id, geometry, visible?, layer?, material?, label?, sourceKey? } — no type tag; use entityTypeOf(e) from @huukhanhnguyen/model when you need the kind name.

4. Add a parameter

Parameters live in their own lane and are referenced by bare key inside expressions:

const parametric = {
  title: 'sized-box',
  parameters: [
    { method: 'number', key: 'width', args: [{ key: 'value', input: '400' }] },
  ],
  objects: [
    {
      key: 'box',
      operations: [
        {
          method: 'rectangle',
          args: [
            { key: 'point1', input: '[-(width)/2,-150,0]' },
            { key: 'point2', input: '[(width)/2,150,0]' },
          ],
        },
        { method: 'curveExtrude', args: [{ key: 'thickness', input: '200' }] },
      ],
    },
  ],
}

// Transient override — never written back into the document:
const wide = evaluateModel(parametric, { width: 800 }, { registry })
console.log(wide.params.find((p) => p.key === 'width')?.value) // 800
// The evaluated graph (wide.nodes) reflects the override, and so does the
// lazy `geometry` bake: reading `wide.geometry` re-applies the same transient
// overrides for the bake, so its meshes span ±400 here. The document itself
// is untouched — a later evaluate() without params recomputes from 400.

Expressions can also call the computation namespaces ("NurbsCurve.length(centerline)", "Axis.fromPointDirection([0,0,0],[0,0,1])") and reference other nodes' streams by key.

Where next

Last updated: 📖 1 min readEdit on GitHub