Skip to content
Shapemetry

Types - Document

API reference for ./model/modelDocument on @huukhanhnguyen/types.

See the Guide for the package's role.

API reference

Signatures are generated from the live package .d.ts - not hand-written.

AreaUnit

type

type AreaUnit
// = "mm2" | "cm2" | "m2" | "in2" | "ft2"

Argument

type

type Argument
// = {
    key: string;
    /** ALWAYS an expression string — an object-shaped VALUE (e.g.
     *  placeComponent's `args` override) is authored as expression-string
     *  SYNTAX like `"{ width: 500 }"`, never a raw JS object. Empty is allowed
     *  (file parameter with no file selected); validity is enforced by
     *  validateExpressions(). */
    input: string;
    label?: string;
    /** Constraint fields — parameter args (numeric methods) only; ignored elsewhere. */
    min?: string;
    max?: string;
    step?: string;
    options?: Option[];
}

Chain

type

The ONE record spine of the chain lanes: key + optional label/parameters + required operations. The skeleton / animation / texture lanes are all exactly this shape, so they all carry this type — there are no per-lane aliases (a second name for the same shape is debt). TableChain (columns instead of parameters) and model's FlatComponentSource (no label) diverge deliberately and stay explicit. The per-lane zod schemas in model differ (Skeleton/Animation default operations to [], Texture requires min(1)) — zod stays the validation SSOT.

type Chain
// = {
    key: string;
    label?: string;
    parameters?: (Operation | Container)[];
    operations: (Operation | Container)[];
}

Component

type

A reusable DEFINITION — 2D or 3D. key is unique within the components[] lane (per-lane uniqueness via uniqueKeyArray — not a model-wide key space shared with parameters/operations). Placed via placeComponent in an ordinary 3D chain (root scene / another component's body): a real instanced GROUP placement + the opening cut. NOT on paper — a sheet places a VIEW (a views[] entry with a viewDetail head), never a definition (2026-07-26).

Flatness contract: DECLARED (flat: true on this record — the ONLY flat declaration there is, since a step cannot declare itself flat) WINS over INFERRED isFlat (bbox z-extent at current params). A declared-flat component enforces the flat contract over its whole body (FLAT_LANE_METHODS, point-arg z=0, force-flatten created entities) so a slider cannot silently un-flatten it.

type Component
// = {
    key: string;
    /** Editor-chrome display name (not printed on the artifact — see
     *  model's title-vs-label law). */
    label?: string;
    /** Chain-level flat declaration — with `ModelJSON.flat` and the "flat"
     *  LANE, one of the only three ways 2D context is established. When true,
     *  the whole `operations` body runs under the flat contract (vocabulary
     *  gate + z=0 point args + force-flatten). Empty `operations` is valid.
     *  `opening` under flat is meaningless (validate warning). */
    flat?: boolean;
    /** This component's own input arguments — a function signature. Evaluated
     *  in an ISOLATED scope (never sees the host model's `parameters[]`); a
     *  placement's `args` overrides these by key, so the SAME component
     *  placed twice can render differently. Value methods only
     *  (length/number/…). */
    parameters?: (Operation | Container)[];
    /** The component's own body — an entity-streaming chain. Lane "component"
     *  when undeclared; when `flat: true`, the whole body is flat-enforced.
     *  Undeclared content falls back to INFERRED isFlat (bbox z-extent). */
    operations: (Entities | Container)[];
    /**
     * Baked static geometry (import/AI) — plain Entity[], never an
     * expression. Spliced by placeComponent alongside evaluated `operations`.
     * A component may have baked content, authored content, or both; only
     * `entities` (empty operations) is a valid imported asset.
     */
    entities?: unknown[];
    /** Optional cut volumes — evaluated in the SAME isolated scope + `args`,
     *  with the SAME placement transform (`at`/`rotate`/`scale`) as the body.
     *  THE OPENING LAW (cut-at-placement): in an ordinary 3D chain, each
     *  `placeComponent` step SUBTRACTS these (closed solid) volumes from the
     *  stream entities PRESENT BEFORE that step in that SAME chain (a wall
     *  placed earlier gets a doorway cut through it; a wall placed AFTER is
     *  unaffected — "what is behind does not know"). The opening volumes themselves
     *  never render and never flow downstream — they exist only to cut.
     *  Ignored entirely in a FLAT placement context (no volume to cut there).
     *  Meaningless when `flat: true` (validate warning). */
    opening?: (Entities | Container)[];
    /** Rig chains scoped to this component (joint trees) — materialize per
     *  placement. One skeleton chain: nested `joint` nodes fold to
     *  flat Skeleton (DFS pre-order); key = sole identity
     *  (skin.skeleton / track.skeleton). See ModelJSON.skeletons. */
    skeletons?: Chain[];
    /** Animation chains scoped to this component — track nodes fold to
     *  AnimationClip + Track exactly like ModelJSON.animations, and
     *  materialize per placement like `skeletons` just above.
     *
     *  This lane USED to be a second animation vocabulary: a baked
     *  interchange record whose track declared its own `target`+`property`,
     *  whose clip stored `duration`/`loop`, whose `values` were flat, and
     *  which admitted CUBICSPLINE — every one of those removed from the
     *  graph model on purpose (a Track carries no target: the CONSUMER holds
     *  the reference; a clip's length is `groupDuration` over its members and
     *  looping belongs to a USE). `skeletons` was unified with the model lane
     *  already; animations now match, so one clip shape exists, not two.
     *  An importer authors track NODES here (the same way it authors joint
     *  nodes for `skeletons`), not a baked record. */
    animations?: Chain[];
}

Container

type

A keyed grouping of operations. ONE shape, in two positions.

  • At the root (ModelJSON.objects[]) it is a scene OBJECT: the root is a SET of these, not one chain, and every entry folds its own operations from an EMPTY stream — so no object can ever see another object's entities. That independence is what makes the object the unit of change (root CLAUDE.md, "The object is the unit of change").
  • Nested inside a chain it is display grouping, and stays anonymous.

key is optional HERE because a nested container needs none. At the root it is required — the root addresses its objects by key for selection, render binding and the per-object channels — and that is enforced by VALIDATION, not by a second type. There used to be a SceneObject = Container & { key: string }; it was deleted 2026-08-15 because it named no second concept, and both shapes always became the same ContainerNode anyway.

type Container
// = {
    key?: string;
    label?: string;
    enabled?: string;
    operations: (Entities | Container)[];
}

Entities

type

ONE step in an entity-streaming chain: it folds an entity stream to an entity stream, and that is the whole of it.

Which is why it carries neither authoring markers nor a child list. There is no flat (2D context comes from the LANE — ModelLane === "flat" — or from the chain-level owner declaration Component.flat / ModelJSON.flat; a step cannot declare itself into another dimension), no locked (immunity is a property of the ENTITY — EntityBase.locked, stamped by applyLocked — because it is entities the later steps skip, not nodes), and no children (a chain is a flat pipeline; grouping is a Container, whose own operations is the nested list).

type Entities
// = {
    key?: string;
    label?: string;
    /** Optional enable expression — falsy = this step is bypassed / emits nothing. */
    enabled?: string;
    /** Method VALIDITY is registry-dependent (which node registries are
     *  loaded) — the schema only checks the string shape; model's
     *  validateModelJSON(json, registry) rejects unknown methods. */
    method: string;
    args?: Argument[];
}

LaneNode

type

One cameras[] entry (a lane node — not the viewport's live camera state). args is the full Argument the lane zod schema accepts (it reuses NodeSchema); nothing narrows it.

type LaneNode
// = {
    key: string;
    label?: string;
    enabled?: string;
    method: M;
    args?: Argument[];
}

LengthUnit

type

type LengthUnit
// = "mm" | "cm" | "m" | "in" | "ft"

MassUnit

type

type MassUnit
// = "mg" | "g" | "kg" | "lb" | "oz"

ModelJSON

type

type ModelJSON
// = {
    id?: string;
    /** Schema shape this document was written against — model's
     *  SCHEMA_VERSION at save time. Optional and inert on load (absent =
     *  pre-stamp vintage, sniffed by shape). */
    version?: number;
    /** Content title printed on the artifact (title-vs-label law). */
    title: string;
    /** Chain-level flat declaration — with `Component.flat` and the "flat"
     *  LANE, one of the only three ways 2D context is established (a single
     *  step cannot declare itself flat). The whole `operations` body runs
     *  under the flat contract: FLAT_LANE_METHODS vocabulary gate, point-arg
     *  z=0, created entities force-flattened. Same field as `Component.flat`
     *  — model's `installModel` copies it onto the installed component.
     *  Empty body + flat is trivially valid. */
    flat?: boolean;
    /** Model-level settings — see ModelSettings's doc. Display units live
     *  at `settings.units` (not a top-level field). Same family as `title`:
     *  data the body reads, not a graph node. */
    settings?: ModelSettings;
    /** Material nodes (method `"material"`, expression args for every
     *  Material field). **`name` = the record's identity**, referenced by
     *  name from applyMaterial; **`key` = optional node graph id** (editor
     *  use only, never a lookup key). Resolved: evaluateMaterials() →
     *  Material. */
    materials?: (Operation | Container)[];
    /** Layer nodes (method `"layer"`, expression args name/color/lineWidth/
     *  lineStyle). **`name` = by-name identity** (applyLayer); **`key` =
     *  optional graph id only**. Resolved: evaluateLayers() → Layer. */
    layers?: (Operation | Container)[];
    /** Style nodes (method `"style"`, expression args name/background/
     *  shading/exposure). **`name` = by-name identity**
     *  (viewStyle); **`key` = optional graph id only**. Resolved:
     *  evaluateStyles() → Style. */
    styles?: (Operation | Container)[];
    /** Text style nodes (method `"textStyle"`, expression args name/font/
     *  height/widthFactor/oblique). **`name` = by-name identity** (a `text`
     *  node's `style` arg, and every annotation style's `textStyle`).
     *  Resolved: collectTextStyles() → TextStyle. */
    textStyles?: (Operation | Container)[];
    /** Annotation style nodes (method `"annotationStyle"`, expression args
     *  for arrow/extension geometry and measurement formatting). **`name` =
     *  by-name identity** (a dimension or leader node's `style` arg).
     *  Resolved: collectAnnotationStyles() → AnnotationStyle. A SEPARATE lane
     *  from `textStyles` on purpose — DXF's STYLE vs DIMSTYLE split, so one
     *  text style serves dimensions, notes and leaders alike. */
    annotationStyles?: (Operation | Container)[];
    /** Named camera poses (camera method nodes). A view3d head may reference by key.
     *  NOT product: never in the entity stream. A LUMINAIRE is not a lane node
     *  either — it is an ordinary object in `objects[]` carrying
     *  `EntityBase.emission` (the `lights[]` lane was deleted 2026-08-16, and
     *  the `helpers[]` datum lane the same day — see RETIRED_ROOT_MESSAGES). */
    cameras?: LaneNode<CameraM>[];
    /** Skeleton chains — nested joint nodes fold to flat Skeleton
     *  (DFS pre-order). Key = sole identity (skin.skeleton references it). */
    skeletons?: Chain[];
    /** Animation chains — track nodes fold to AnimationClip
     *  (expression args). Baked import clips stay on Component.animations. */
    animations?: Chain[];
    parameters?: (Operation | Container)[];
    /** Root body — the SET of scene objects (ModelLane role "scene"). Each
     *  entry folds its OWN `operations` from an empty stream; the root is not
     *  itself a chain (SCHEMA 46, see Container / migrateRootObjects). */
    objects?: Container[];
    /** Texture lane: math->image chains in the operations grammar, but a
     *  fully separate stream (image buffers, never entities) with its own
     *  method vocabulary. A material whose `texture` names a texture key
     *  receives the rasterized pixels at evaluate time. Sampling (wrap/
     *  filter/uvSet) rides the chain's builtin params. */
    textures?: Chain[];
    /** Reporting lane: named table chains (Revit-Schedule-like pipelines,
     *  see TableChain's doc). Referenced from any expression via
     *  `Table.get('key')` — never part of the entity graph. */
    tables?: TableChain[];
    /** Named views: CHAIN lane — key + operations of typed heads
     *  (viewPlan/viewSection/viewElevation/view3d) + viewFilter/viewStyle
     *  steps. Family is the head method (VIEW_FAMILY_OF map). Consumed by
     *  resolveSheet/drawingSetToSvg / evaluateView. */
    views?: View[];
    /** Named reusable definitions — 2D or 3D, whichever they evaluate to; see
     *  Component's doc for the full contract (isolated scope, emergent
     *  flatness, placement via `placeComponent`, the `opening` cut-at-
     *  placement law). Placed into root `operations` / another component's
     *  own `operations` (ordinary 3D chain — instanced group placement).
     *  Reaches PAPER only through a views[] entry (a viewDetail head frames
     *  it), never a sheet-level `placeComponent`. */
    components?: Component[];
    /** Sheet set: an ordered construction-drawing set (A-101 GROUND FLOOR
     *  PLAN, A-201 SECTION A-A …). Each sheet's `operations` places its own
     *  views/tables/images by key (see Sheet's own doc) plus
     *  any sheet-local ad hoc flat content; model's
     *  drawingSetToSvg renders one SVG per sheet plus a drawing index. */
    sheets?: Sheet[];
}

ModelLane

type

Which section of the model a node was declared in (drives lane validation and serialization). parameters = value bindings; scene = the root fold ROLE (storage field is operations); component = a Component's own operations/opening (full 3D grammar — an ISOLATED scope, not a method restriction); flat = a sheet's ad hoc content — entity methods only, restricted to FLAT_LANE_METHODS (no solid/3D).

type ModelLane
// = "parameters" | "scene" | "component" | "flat"

ModelSettings

type

Model-level settings: ONE document-config block (USD/FBX/DXF industry pattern). Two natures of field:

  • content semantics: units (display/config units; internal length is ALWAYS mm — never a runtime scale conversion)
  • presentation state: activeView, cameraTour Permanent CONVENTIONS (never config, never fields here): Z-up world frame and internal mm — glTF-style fixed convention, zero upAxis/scale knob.

Display chord deflection is NOT a document setting — callers pass it as the optional argument to render() / tessellateFaces (engine default when omitted). Nesting and DXF export own separate legitimate knobs.

  • units: optional display unit config; absent = {length:"mm"}. Read only via Model.unit (the ONE accessor — never N call-site fallbacks).
  • activeView: which views[] entry the 3D viewport restores on load.
  • cameraTour: ordered camera legs for viewer present-mode playback (host-owned; engine never plays it).
  • environment: the light that has no place in space — the site and the moment the sun is DERIVED from, plus the sky fill and any HDRI. A document FACT like units (Revit project location / SketchUp geo-locate), and ONE per document on purpose: a building has one sky, so this cannot be a per-view field. See Environment. (It replaced the bare site field and the retired per-style sun on 2026-08-16.)

Retired (v41 migrateMeshNgon deletes when present): curveTolerance.

type ModelSettings
// = {
    units?: UnitConfig;
    activeView?: string;
    cameraTour?: {
        legs: {
            view: string;
            duration: number;
        }[];
    };
    environment?: Environment;
}

Operation

type

type Operation
// = {
    key?: string;
    label?: string;
    /** Optional enable expression — falsy = this node is bypassed / emits nothing. */
    enabled?: string;
    /** Method VALIDITY is registry-dependent (which node registries are
     *  loaded) — the schema only checks the string shape; model's
     *  validateModelJSON(json, registry) rejects unknown methods. */
    method: string;
    args?: Argument[];
    /** Child nodes for CONTAINER methods. Recursive — the same Operation | Container
     *  shape as every other chain. The entity-fold engine never reads this
     *  (a scene[] operation with children silently drops them). */
    children?: (Operation | Container)[];
    /** Numeric parameters only — this parameter's USE of a track: which
     *  `formulaTrack`/`numberTrack` in animations[] drives it (a numeric
     *  parameter is one number over time, so only the two 1-number track
     *  methods may), and on
     *  what terms. Keys are plain, not expressions: the same "just the id, no
     *  evaluation" convention `BlockEntity.definition` / skin's `skeleton`
     *  already use. The terms live here rather than on the track because two
     *  parameters may share one curve and disagree about them — see
     *  {@link AnimationBinding}. */
    animation?: AnimationBinding;
}

Option

type

type Option
// = {
    label: string;
    /** An expression string (model's exprSchema). */
    input: string;
}

RawModel

type

Loose (pre-validation) shape of a model document — Partial so every real lane stays visible at the type level (cannot drift when ModelJSON gains fields). unit is the only extra: pre-migration top-level unit still recognized by offline tooling (settings.units is the live field).

type RawModel
// = Partial<ModelJSON<CameraM>> & { title: string; /** Pre-migration top-level unit — offline tooling only; live docs use settings.units. */ unit?: UnitConfig; }

Sheet

type

type Sheet
// = {
    /** Sheet identifier / drawing number, e.g. "A-101". Free-form (NOT
     *  generatorKeySchema) — drawing numbers contain hyphens and must stay
     *  author-facing strings. Unique within the set; also the index row key. */
    key: string;
    /** Sheet title block name, e.g. "GROUND FLOOR PLAN" — CONTENT printed on
     *  the artifact (title-vs-label law). An EXPRESSION (titleValue law):
     *  `"'PLAN LEVEL ' + level"` renders evaluated, a plain name stays
     *  literal. */
    title?: string;
    /** Builtin paper/scale params (paperWidth/paperHeight/scale) + free params. */
    parameters?: (Operation | Container)[];
    /** This sheet's content — lane "flat" (see model's FLAT_LANE_METHODS). A
     *  top-level `viewEntity` node with no `at` arg auto-tiles into a scaled
     *  row (resolveSheet extracts these before running the rest); every
     *  other node (a positioned `viewEntity`, `tableEntity`, `imageEntity`,
     *  or ordinary curve/point/annotation content) is
     *  authored in the SAME y-up bottom-left paper-space convention every
     *  other flat context uses, flipped once into sheet space at resolve
     *  time. */
    operations?: (Operation | Container)[];
}

TableChain

type

A reporting stream — header columns (signature) + body operations (row producers + transforms), fold law same as the root body. Referenced via Table.get('key'). Chain-level provenance is banned (the imported CSV stays discoverable as its url-method parameter). Record spine is Chain with columns instead of parameters — a deliberate divergence, kept explicit (see Chain).

type TableChain
// = {
    key: string;
    label?: string;
    /** HEADER — one `column` node per column (key on the node). */
    columns?: (Operation | Container)[];
    /** BODY — `row` producers + verb transforms (tableFilter/Group/Sort/…). */
    operations: (Operation | Container)[];
}

UnitConfig

type

Runtime unit config — SSOT shape for settings.units.

type UnitConfig
// = {
    length: LengthUnit;
    area?: AreaUnit;
    volume?: VolumeUnit;
    mass?: MassUnit;
}

View

type

A view is a CHAIN: optional typed HEAD + stackable filter/style steps. Family is the head method (VIEW_FAMILY_OF map in model's views.ts) — never inferred from camera fields. Config-fold evaluation turns operations into a projection definition consumed by resolveView's HLR math.

Heads (must be first when present; at most one): viewPlan / viewSection / viewElevation / view3d Steps (any count, any order among themselves after the head): viewFilter (rules json) / viewStyle (name) Headless (filter/style only, or empty ops) = pure filter/style preset, family "3d", never placeable on paper.

type View
// = {
    key: string;
    label?: string;
    operations: (Operation | Container)[];
}

VolumeUnit

type

type VolumeUnit
// = "mm3" | "cm3" | "m3" | "in3" | "ft3" | "L"
Last updated: 📖 6 min readEdit on GitHub