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·200Entities 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
- Guide: Parametric authoring — lanes, operations, chains, expressions, the node catalog.
- Guide: Rendering with three.js — get these entities on screen.
- Node catalog reference:
REFERENCE.mdandENTITIES.mdinpackages/modelon GitHub.