Skip to content
Shapemetry

@huukhanhnguyen/types / model/modelDocument

model/modelDocument

AreaUnit

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


Argument

Argument = { input: string; key: string; label?: string; max?: string; min?: string; options?: Option[]; step?: string; }

input

input: 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().

key

key: string

label?

optional label?: string

max?

optional max?: string

min?

optional min?: string

Constraint fields — parameter args (numeric methods) only; ignored elsewhere.

options?

optional options?: Option[]

step?

optional step?: string


Chain

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

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.

key

key: string

label?

optional label?: string

operations

operations: Operation[]

parameters?

optional parameters?: (Operation | Group)[]


Component

Component = { animations?: Chain[]; entities?: unknown[]; flat?: boolean; key: string; label?: string; opening?: (DataStep | Group)[]; operations: (DataStep | Group)[]; parameters?: (Operation | Group)[]; }

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), NEVER inferred from content. A declared-flat component enforces the flat contract over its whole body (FLAT_LANE_METHODS, point-arg z=0, force-flatten created documents) so a slider cannot silently un-flatten it.

animations?

optional animations?: Chain[]

Animation chains scoped to this component — track nodes fold to AnimationClip + Track exactly like ModelJSON.animations, and materialize per placement. An animation importer would author track NODES here.

entities?

optional entities?: unknown[]

Baked static geometry (import/AI) — plain ObjectData, 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.

flat?

optional flat?: boolean

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).

key

key: string

label?

optional label?: string

Editor-chrome display name (not printed on the artifact — see model's title-vs-label law).

opening?

optional opening?: (DataStep | Group)[]

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 documents 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).

operations

operations: (DataStep | Group)[]

The component's own body — a data-streaming chain. Lane "component" when undeclared; when flat: true, the whole body is flat-enforced. Undeclared means 3D — flatness is never inferred from content.

parameters?

optional parameters?: (Operation | Group)[]

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/…).


DataStep

DataStep = { args?: Argument[]; enabled?: string; key?: string; label?: string; method: string; }

ONE step in a data-streaming chain: it folds a data stream to a data 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 PLACEMENT — BaseInstance.locked, stamped by applyLocked — because later steps skip a locked object, not a node), and no children (a chain is a flat pipeline; grouping is a Group, whose own operations is the nested list).

args?

optional args?: Argument[]

enabled?

optional enabled?: string

Optional enable expression — falsy = this step is bypassed / emits nothing.

key?

optional key?: string

label?

optional label?: string

method

method: 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.


Group

Group = { enabled?: string; instances?: NodeInstance[]; key?: string; label?: string; operations: (DataStep | Group)[]; properties?: PropertyNode[]; }

Display grouping inside an object or component body, a sheet/template, and parameters[]. Not a scene object — the root is SceneNode[]. key is optional because a nested group needs none. The fold of a group is its own empty-stream scope (applyAttribute sees only the group's steps); the parent concatenates the result.

enabled?

optional enabled?: string

instances?

optional instances?: NodeInstance[]

Authored placements (object-tier). Absent === one identity.

key?

optional key?: string

label?

optional label?: string

operations

operations: (DataStep | Group)[]

properties?

optional properties?: PropertyNode[]

Steps that write instances[] (object-tier). Carried here for the same reason instances is: objectDocuments() hands an ObjectNode to the runtime as this shape, so the field has to survive the trip.


LaneNode

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

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 Parameters

M

M extends string = string

args?

optional args?: Argument[]

enabled?

optional enabled?: string

key

key: string

label?

optional label?: string

method

method: M


LengthUnit

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


MassUnit

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


ModelJSON

ModelJSON<CameraM> = { animations?: Chain[]; annotationStyles?: Operation[]; cameras?: LaneNode<CameraM>[]; components?: Component[]; flat?: boolean; id?: string; layers?: Operation[]; materials?: Operation[]; objects?: SceneNode[]; parameters?: (Operation | Group)[]; patterns?: Operation[]; settings?: ModelSettings; sheets?: Sheet[]; styles?: Operation[]; tables?: TableChain[]; templates?: Template[]; textStyles?: Operation[]; textures?: Chain[]; title: string; version?: number; views?: View[]; }

Type Parameters

CameraM

CameraM extends string = string

animations?

optional animations?: Chain[]

Animation chains — track nodes fold to AnimationClip (expression args). Baked import clips stay on Component.animations.

annotationStyles?

optional annotationStyles?: Operation[]

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.

cameras?

optional cameras?: LaneNode<CameraM>[]

Named camera poses (camera method nodes). A view3d head may reference by key. NOT product: never in the data stream. A luminaire is an ordinary object in objects[] whose data is LightData.

components?

optional components?: Component[]

Named reusable definitions — 2D or 3D, whichever they evaluate to; see Component's doc for the full contract (isolated scope, DECLARED 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.

flat?

optional flat?: boolean

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 documents force-flattened. Same field as Component.flat — model's installModel copies it onto the installed component. Empty body + flat is trivially valid.

id?

optional id?: string

layers?

optional layers?: Operation[]

Layer nodes (method "layer", expression args name/color/lineWidth/ lineStyle). name = by-name identity (applyLayer); key = optional graph id only. Resolved: evaluateLayers() → Layer.

materials?

optional materials?: Operation[]

Material nodes (method "material", expression args for every Material field). name = the record's identity, referenced by name from faceMaterial; key = optional node graph id (editor) use only, never a lookup key). Resolved: evaluateMaterials() → Material.

objects?

optional objects?: SceneNode[]

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 Group / migrateRootObjects).

parameters?

optional parameters?: (Operation | Group)[]

patterns?

optional patterns?: Operation[]

Pattern nodes (method "pattern", expression args name/lines/ lengthSpace). name = by-name identity (a hatch's pattern arg, and a material's cut/surface pattern). Resolved: evaluatePatterns() → Pattern. A hatch names one of these and never inlines its line families: two hatches drawn with concrete must BE the same concrete, and every interchange format references a pattern by name.

settings?

optional settings?: ModelSettings

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.

sheets?

optional sheets?: Sheet[]

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; a drawing-set writer renders one page per sheet plus a drawing index.

styles?

optional styles?: Operation[]

Style nodes (method "style", expression args name/background/ shading/exposure). name = by-name identity (viewStyle); key = optional graph id only. Resolved: evaluateStyles() → Style.

tables?

optional tables?: TableChain[]

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 data graph.

templates?

optional templates?: Template[]

Shared paper + furniture. Sheets name one by Sheet.template.

textStyles?

optional textStyles?: Operation[]

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.

textures?

optional textures?: Chain[]

Texture lane: math->image chains in the operations grammar, but a fully separate stream (image buffers, never data documents) 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.

title

title: string

Content title printed on the artifact (title-vs-label law).

version?

optional version?: number

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).

views?

optional views?: View[]

Named views: CHAIN lane — key + operations of typed heads (viewPlan/viewSection/viewElevation/view3d) + viewFilter/viewStyle steps. The head names how the drawing's camera is set up. Consumed by resolveSheet / evaluateView.


ModelLane

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

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 — data methods only, restricted to FLAT_LANE_METHODS (no solid/3D).


ModelSettings

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

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 owns its own separate legitimate knob.

  • 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.

activeView?

optional activeView?: string

cameraTour?

optional cameraTour?: { legs: { duration: number; view: string; }[]; }

legs

legs: { duration: number; view: string; }[]

environment?

optional environment?: Environment

units?

optional units?: UnitConfig


Operation

Operation = { animation?: AnimationBinding; args?: Argument[]; enabled?: string; key?: string; label?: string; method: string; }

animation?

optional animation?: AnimationBinding

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 a component key / 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 AnimationBinding.

args?

optional args?: Argument[]

enabled?

optional enabled?: string

Optional enable expression — falsy = this node is bypassed / emits nothing.

key?

optional key?: string

label?

optional label?: string

method

method: 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.


Option

Option = { input: string; label: string; }

input

input: string

An expression string (model's exprSchema).

label

label: string


RawModel

RawModel<CameraM> = Partial<ModelJSON<CameraM>> & { title: string; unit?: UnitConfig; }

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).


Sheet

Sheet = { key: string; operations: (Operation | Group)[]; parameters?: (Operation | Group)[]; template?: string; title?: string; }

A drawing sheet — an OBJECT whose space is BOUNDED. Its content is a data chain like any other; what makes it a sheet is that the chain composes into a finite rectangle of paper instead of unbounded world space. Resolving one produces Page.

Record spine is Chain with title instead of label — a deliberate divergence, kept explicit (see Chain, and TableChain which diverges the same way with columns). The reason is the title-vs-label law: a sheet's name is PRINTED, in the ISO 7200 title block's "Title" cell, so it is content, not editor chrome, and it is an expression. A second editor-chrome label beside it would be a second name for the same thing, so there is none.

key

key: string

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 entry key.

operations

operations: (Operation | Group)[]

This sheet's content — lane "flat" (see model's FLAT_LANE_METHODS). A top-level viewObject node with no at arg auto-tiles into a scaled row (resolveSheet extracts these before running the rest); every other node (a positioned viewObject, 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.

Required, like every other chain's body. A sheet with no operations is a blank page — frame, zone grid and title block and nothing else — which is a legal (if empty) drawing, and it is spelled []; undefined was a second spelling of the same nothing (the zod schema defaults it, so a document that omits the field still loads).

parameters?

optional parameters?: (Operation | Group)[]

Free params (per-sheet title-block args: date, revision, …). Paper size is on the template — leftover paperWidth/paperHeight fail loud.

template?

optional template?: string

Key of a Template. Absent = the engine's ISO 5457 default.

title?

optional title?: 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.


TableChain

TableChain = { columns?: Operation[]; key: string; label?: string; operations: Operation[]; }

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).

columns?

optional columns?: Operation[]

HEADER — one column node per column (key on the node).

key

key: string

label?

optional label?: string

operations

operations: Operation[]

BODY — row producers + verb transforms (tableFilter/Group/Sort/…).


Template

Template = { height: number; key: string; margins: number | [number, number, number, number]; operations?: (Operation | Group)[]; parameters?: (Operation | Group)[]; width: number; }

Shared paper + furniture for a family of sheets. Sheets reference a template by key (AnimationClip.tracks, FreeCAD DrawPage.Template, ArchiCAD Master Layout) — the template does not contain them.

No orientation field: 420×297 versus 297×420 already says it. margins is one number if uniform, or [left, bottom, right, top] mm.

height

height: number

key

key: string

margins

margins: number | [number, number, number, number]

operations?

optional operations?: (Operation | Group)[]

Drawn furniture (frame, zone grid, title-block placeComponent). Nested groups keep a polyline with its applyAttribute stamps.

parameters?

optional parameters?: (Operation | Group)[]

Shared furniture args (project name, client) — value methods.

width

width: number


UnitConfig

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

Runtime unit config — SSOT shape for settings.units.

area?

optional area?: AreaUnit

length

length: LengthUnit

mass?

optional mass?: MassUnit

volume?

optional volume?: VolumeUnit


View

View = { key: string; label?: string; operations: Operation[]; }

A view is a CHAIN: optional typed HEAD + stackable filter/style steps. A fold of it is one of three shapes (FlatView / SectionView / ProjectionView), sniffed by the one extra field. Config-fold evaluation turns operations into that record; resolveView / projectView / drawView consume it.

Heads (must be first when present; at most one): viewPlan / viewSection / viewElevation / view3d / viewDetail Steps (any count, any order among themselves after the head): viewFilter (rules json) / viewStyle (name) Headless (filter/style only, or empty ops) = a ViewBase with no extra field.

key

key: string

label?

optional label?: string

operations

operations: Operation[]


VolumeUnit

VolumeUnit = "mm3" | "cm3" | "m3" | "in3" | "ft3" | "L"

Last updated: 📖 19 min readEdit on GitHub