@huukhanhnguyen/model / graph
graph
AngularDimension
AngularDimension = {
arcPoint:Point;center:Point;references:Point[]; }
An angle measured at center, between the arms running out to references.
references is the same CHAIN as linear measure: N+1 arms give N measured angles, and every element has the same role.
arcPoint does two jobs that a bare radius number cannot: it sets the dimension arc's radius (its distance from center), and it picks WHICH of the four angles two crossing arms subtend is the one meant.
arcPoint
arcPoint:
Point
center
center:
Point
references
references:
Point[]
AngularDimensionData
AngularDimensionData = {
dimensions:AngularDimensionEntity[]; }
Object-tier angular-dimension datablock.
dimensions
dimensions:
AngularDimensionEntity[]
AngularDimensionEntity
AngularDimensionEntity =
AngularDimension& {attributes?:Record<string,unknown> |null;curves:PolyCurve[];label?:Text;style:string;text?:string;textPoint?:Point; }
One angular dimension in a AngularDimensionData bag.
AngularDimensionInstance
AngularDimensionInstance =
BaseInstance& {material?:string; }
AngularDimensionObject
AngularDimensionObject =
BaseObject& {data:AngularDimensionData;instances:AngularDimensionInstance[];type:"angularDimension"; }
An object whose data is angular dimensions and nothing else.
An angle has a vertex and two rays — the third arity.
AnimationClip
AnimationClip = {
name:string;tracks:string[]; }
A named GROUP of tracks — the export/selection unit ("Walk", "Run", "Idle"). Every format that round-trips a real animation clip requires one: Blender's Action, Unity's AnimationClip, Unreal's AnimSequence, FBX's AnimStack, glTF/USD's named animation — measured 2026-08-14: a clip with no shared anchor for its member tracks is a real, filed interop bug (github.com/KhronosGroup/glTF#569 — mismatched per-track length with no clip-level tie, ~1.79s desync per loop between two implementations' guesses). Membership is BY REFERENCE (track keys), not containment — a track stays independently addressable (a parameter can point at one directly) whether or not it is also a clip member.
name
name:
string
tracks
tracks:
string[]
Member track keys, in authoring order.
Annotation
Annotation =
LinearDimension|RadialDimension|AngularDimension|Text|Leader|Image
Union of the drafting geometries.
AnnotationStyle
AnnotationStyle = {
arrowSize?:number;baselineSpacing?:number;centerMarkSize?:number;decimals?:number;extensionOffset?:number;extensionOvershoot?:number;lineOvershoot?:number;name:string;rounding?:number;scale?:number;textStyle:string;textSuffix?:string;tickSize?:number; }
Produced by collectAnnotationStyleNodes() from an authored annotationStyle node's expression args; consumed BY NAME.
arrowSize?
optionalarrowSize?:number
Arrowhead length, model units (DXF DIMASZ).
baselineSpacing?
optionalbaselineSpacing?:number
Distance between stacked baseline dimensions (DXF DIMDLI).
centerMarkSize?
optionalcenterMarkSize?:number
Size of the cross drawn at a circle's centre. A centre mark MEASURES NOTHING, so it is this NUMBER and not a dimension type — DXF DIMCEN.
decimals?
optionaldecimals?:number
Decimal places in the measurement string (DXF DIMDEC).
extensionOffset?
optionalextensionOffset?:number
Gap between the measured point and where its extension line starts (DXF DIMEXO).
extensionOvershoot?
optionalextensionOvershoot?:number
How far an extension line runs PAST the dimension line (DXF DIMEXE).
lineOvershoot?
optionallineOvershoot?:number
How far the dimension line runs past the extension lines (DXF DIMDLE).
name
name:
string
rounding?
optionalrounding?:number
Round the measurement to this increment before formatting (DIMRND).
scale?
optionalscale?:number
Multiplier applied to every length above, so one style set draws at a second paper scale (DXF DIMSCALE).
textStyle
textStyle:
string
Name of the textStyles record the measurement string is drawn with.
textSuffix?
optionaltextSuffix?:string
Appended to the measurement string — a unit, "mm" (DIMPOST).
tickSize?
optionaltickSize?:number
Size of the oblique tick drawn instead of an arrowhead when the architectural tick is used (DXF DIMTSZ; 0/absent = arrowheads).
AnnotationStyles
AnnotationStyles = {
annotation:AnnotationStyle;text:TextStyle; }
The two style records a drafting mark needs at once: how the MEASUREMENT is drawn and how its TEXT is drawn. They travel together because a mark's drawn form needs both in one pass, and resolving them separately at each call site is where a mark ends up drawn with one style's arrows and another's text.
annotation
annotation:
AnnotationStyle
text
text:
TextStyle
AssetFile
AssetFile = {
name:string;url:string; }
One entry of a scope's COMPUTED asset views (ModelScope.fonts / ModelScope.images): a url-method parameter, reduced to the pair every consumer of it needs. name is the parameter's KEY — what a reference names (a textCurve font arg, a sheet's imageEntity, a pattern chain's image source); url is the parameter's literal url (https:/data:), where the Store pre-loads the bytes from in the async phase. Neither view is a document lane: both are derived from the parameters lane by assetTypeOf, so both are always current.
One { name, url } type for both views. What distinguishes a font entry from an image entry is which view it came out of.
name
name:
string
url
url:
string
BaseInstance
BaseInstance = {
attributes?:Record<string,unknown>;layer?:string;locked?:boolean;name?:string;transformation:Transformation;visible?:boolean; }
Placement fields every instance carries, whatever the object's kind.
Kind-specific extras (material, behaviors, …) live on that kind's *Instance, not here. behaviors in particular is a block instruction and must not appear on a mesh or a curve placement.
Its index is STABLE — instances mirror an authored list one-for-one, so an expression may address instances[2] and keep meaning the same placement across re-evaluation.
Coordinates are LOCAL to the object; whatever contains the object composes its own transform on top. An instance therefore never names its parent.
attributes?
optionalattributes?:Record<string,unknown>
Free-form classification read by a consumer (schedule, IFC export, plugin), never by the engine. Per PLACEMENT, so one door out of forty can carry a fire rating the other thirty-nine do not.
layer?
optionallayer?:string
Layer key.
locked?
optionallocked?:boolean
Authoring-safety marker lifted from the chain at assembly. After assembly it shields the placement (draw3d refuses gesture edits). Absent === unlocked.
name?
optionalname?:string
What THIS placement is in the world, distinct from BaseObject.label — a different TIER, not a synonym.
label describes the DEFINITION ("this is a Door") and is shared by every placement of it. name identifies the real, concrete thing this one placement IS — "Door-01" vs "Door-02" for two placements of one Door object — the way a schedule/BOM row needs a unique tag per physical unit, not per definition. A name is a fact about the placement, not about the formula that produced it, which is exactly why it sits beside transformation and not on the object.
Absent means the reader falls back to the definition's label. Not attributes: that bag is opaque and a reader must know a key to interpret it, whereas an identity is named on sight.
transformation
transformation:
Transformation
The placement, LOCAL to the object, as one composed matrix.
A matrix HERE and translation/rotation/scale on the authoring node — the two tiers want opposite things and this is where they part:
- The node stores them apart because a human authors "move it up 100" and an animation track binds to a single CHANNEL. A matrix has no channels. (Blender:
loc/rot/scale, DNA_object_types.h; three.js:position/rotation/scale, Object3D.js.) - This value stores them composed because nobody ever asks it whether POSITION changed — the diff asks whether the placement changed at all, and a renderer wants the matrix anyway. (Rhino stores exactly this for a block reference:
ON_InstanceRef::m_xform, one 4x4, no TRS.)
Required, not optional: this is COMPUTED, so the producer always knows it, and an absent-means-identity convention would put a branch in every consumer to buy back sixteen numbers.
visible?
optionalvisible?:boolean
Absent === visible.
BlockBehaviors
BlockBehaviors = {
opening?:BrepData[]; }
System attributes of a block — things that automatically happen once the block is in the scene. Presence means enabled.
opening?
optionalopening?:BrepData[]
Cut volume, in the nest's OWN local space (each placement transforms it by its own transformation).
BlockData
BlockData = {
objects:SceneObject[];sourceComponent?: {args?:Record<string,string>;key:string; }; }
Mixed nest of scene objects — the payload of BlockObject.
objects
objects:
SceneObject[]
sourceComponent?
optionalsourceComponent?: {args?:Record<string,string>;key:string; }
Set ONLY for a nest created by placeComponent — the components[] key + args signature this nest renders.
args?
optionalargs?:Record<string,string>
key
key:
string
BlockInstance
BlockInstance =
BaseInstance& {behaviors?:BlockBehaviors;material?:string; }
BlockObject
BlockObject =
BaseObject& {data:BlockData;instances:BlockInstance[];type:"block"; }
The only kind that nests. Mixed content is a list of scene objects, never a second geometry kind beside the others.
Bone
One bone's GEOMETRY: a SEGMENT from head to tail, twisted by roll about its own axis. Blender's form (DNA_armature_types.h head[3] / tail[3] / roll). Length is |tail − head|.
head / tail / roll are the REST pose, relative to the parent.
head
head:
Point
roll
roll:
number
tail
tail:
Point
BoneEntity
BoneEntity =
Bone& {attributes?:Record<string,unknown> |null;name:string;parent?:string;positionTrack?:string;rotationTrack?:string;scaleTrack?:string; }
One bone as an entry of a SkeletonData: the segment plus what the DOCUMENT says about it — its identity, its place in the hierarchy, the animated channels it plays, and its opaque claims.
parent rides here and not on Bone because it names another bone's name: a reference cannot live on a tier that has no identity to reference.
positionTrack / rotationTrack / scaleTrack are optional Track.key references — this bone's OWN animated channel. Consumer-holds-the- reference.
BrepData
BrepData = {
edges:BrepEdge[];faces:BrepFace[];loops:BrepLoop[];vertices:BrepVertex[]; }
Shared topology tables of one body. The geometry may contain several topological shells' faces; the partition is derived (classifyShells). Same stem as the BrepData ops namespace.
edges
edges:
BrepEdge[]
faces
faces:
BrepFace[]
loops
loops:
BrepLoop[]
vertices
vertices:
BrepVertex[]
BrepEdge
BrepEdge = {
attributes?:Record<string,unknown>;curve:Segment;end:number;start:number;tolerance?:number; }
A curve bounded by two vertices, given as indices into the document's vertex table. The parameter window is the carrier's own natural range.
attributes?
optionalattributes?:Record<string,unknown>
curve
curve:
Segment
end
end:
number
start
start:
number
tolerance?
optionaltolerance?:number
BrepFace
BrepFace = {
attributes?:Record<string,unknown>;innerLoops:number[];layer?:string;material?:string;outerLoop:number;reversed:boolean;surface:Surface;tolerance?:number;uvCurves:BrepFaceUvCurve[];uvMatrix?:Transformation; }
A bounded patch of a surface: one outer loop, plus inner loops that cut holes.
The geometry kernel derives the chart window from surface + outer trim (Store::natural_bounds).
reversed — whether the face's outward normal opposes its surface's natural normal. Surfaces are shared via Rc across faces, so orientation cannot be folded into the carrier.
uvCurves — one PolyCurve2d per boundary edge. Seam-unwrap anchors are baked into that chain on write.
A seam edge is walked TWICE by one loop, once per parameter column, and the second image is not stored: the kernel computes it by translating the first one period along the periodic direction (Store::uv_curve_on_use).
attributes?
optionalattributes?:Record<string,unknown>
innerLoops
innerLoops:
number[]
layer?
optionallayer?:string
Layer key for THIS face, overriding its placement's Instance.layer. Absent === inherit from the instance.
Per-face for the reason material already is: an IMPORTED document can carry N different layers across its faces before it is ever wrapped into one Object — DXF especially, which assigns layer per ENTITY and never per block — and collapsing that to one Instance-level layer on arrival loses what the file actually said.
material?
optionalmaterial?:string
Material key for THIS face, overriding its placement's Instance.material. Absent === inherit from the instance.
The oldest per-face lane reference and the reason layer is one too: a boolean fragment, an OBJ usemtl run or a glTF primitive names a material per FACE, and one document holds many faces, so the claim cannot ride the placement.
outerLoop
outerLoop:
number
reversed
reversed:
boolean
surface
surface:
Surface
tolerance?
optionaltolerance?:number
Surface-fit tolerance; omitted when at the 1e-7 default.
uvCurves
uvCurves:
BrepFaceUvCurve[]
uvMatrix?
optionaluvMatrix?:Transformation
Authored row-major 4×4 plane→world UV frame for THIS face (world = uvMatrix · [u,v,0,1]). Absent ⇒ derived from the face's own surface on read. Per-face, never per-shell.
BrepFaceUvCurve
BrepFaceUvCurve = {
edge:number;uvCurve:PolyCurve2d; }
A uv image recorded on a face for one of its boundary edges: one PolyCurve2d in the face's (u, v). Reverse, a partial window and a 3D-projected image are baked into that chain on write.
edge
edge:
number
uvCurve
uvCurve:
PolyCurve2d
BrepInstance
BrepInstance =
BaseInstance& {material?:string; }
BrepLoop
BrepLoop = {
orientedEdges:OrientedEdge[]; }
An ordered, closed chain of oriented edges bounding a region of a face.
No type parameter, no tolerance, no attributes: a loop carries no geometry, only the order its edges are walked in.
It does NOT say whether it is the outer boundary or a hole — the FACE declares which of its loops is outer. OpenNURBS carries the claim on both, where they can disagree; one source cannot.
orientedEdges
orientedEdges:
OrientedEdge[]
BrepObject
BrepObject =
BaseObject& {data:BrepData;instances:BrepInstance[];type:"brep"; }
An object whose data is one kernel brep — one body.
A boolean that returns a body with cavities stays one object; the shell partition is derived inside the kernel.
BrepVertex
BrepVertex = {
attributes?:Record<string,unknown>;position:Point;tolerance?:number; }
A point in the topology, plus the ball inside which two coincident vertices are the same vertex.
tolerance ABSENT means 0 means EXACT — the convention every optional tolerance in this package keeps, so the lean document is also the exact one.
KNOWN GAP: no producer in this repo satisfies that convention yet. Measured 2026-08-09 across brep_cylinder, brep_sphere, brep_box and box − cylinder: every vertex and every edge carries the same stamped constant 1e-7, and nothing carries 0. So "exact" is an empty category today, and tolerance cannot be used to tell an exact element from a fitted one — a rule of the form "do X only when tolerance > 0" fires on everything. The convention above states the INTENT; treat a 1e-7 you read as "unset", not as a measured deviation. This matters beyond bookkeeping: BrepVertex.tolerance is read both as an identity ball and as a closure band, and a stamped constant makes those bands identical regardless of how the point was produced.
attributes?
optionalattributes?:Record<string,unknown>
position
position:
Point
tolerance?
optionaltolerance?:number
CameraEntity
CameraEntity =
CameraData& {gizmoSize?:number; }
A camera's Entity tier: its projection (CameraData) plus what only a VIEWPORT needs to draw it.
There is NO target here, and its absence is a decision rather than an omission. A camera's pose needs 3 DOF — including roll — so a single aim point cannot round-trip one: a look-at gesture computes the Euler and writes it to the instance's transformation (see the note above), and the aim point is not kept. LightEntity DOES store a target for exactly the reason this one cannot: a light aims with a bare Vector that carries no roll, so a point expresses it losslessly.
CameraInstance
CameraInstance =
BaseInstance& { }
A camera has no surfaces; material is not on this kind.
CameraObject
CameraObject =
BaseObject& {data:CameraEntity;instances:CameraInstance[];type:"camera"; }
A camera in the scene. Projection is CameraData; placement is instances[i].transformation.
Color
Color = [
number,number,number]
RGB triple in 0..255. Channels outside the range are not clamped here — a consumer interprets as it sees fit. Not a spatial Point: it shares the layout and nothing else, which is why it is its own name.
ColumnMeta
ColumnMeta = {
computed?:boolean;format?:string;key:string;label?:string;type?:string; }
Table — what evaluateTableChain produces. The authoring tables[] lane (TableChain) stays in the model document schema.
computed?
optionalcomputed?:boolean
True when header value expression is set (computed column).
format?
optionalformat?:string
key
key:
string
label?
optionallabel?:string
type?
optionaltype?:string
ComponentBody
ComponentBody = {
data:ObjectData;flat:boolean;isFlat:boolean;opening:BrepData[]; }
data
data:
ObjectData
flat
flat:
boolean
DECLARED flatness — Component.flat. Wins over isFlat, and when set the WHOLE body (data) is the 2D representation.
isFlat
isFlat:
boolean
INFERRED flatness — the resolved body's bbox has no z extent at the current parameter values. A fallback: it can flip under a slider.
opening
opening:
BrepData[]
CurveData
CurveData = {
edges:CurveEdge[]; }
Object-tier curve datablock — N chains, each with its own claims.
Distinct from the kernel Curve document (geometry/curve.ts), which may carry transformation for geometry ops; object placement lives on instances, so CurveData does not have that field.
Array-of-structs, matching Blender's curve datablock: a Blender Curve holds a ListBase of Nurb splines and each spline carries its own mat_nr / resolu / flags directly (DNA_curve_types.h), rather than parallel per-spline arrays hanging off the datablock.
edges
edges:
CurveEdge[]
CurveEdge
CurveEdge = {
attributes?:Record<string,unknown> |null;color?:string;curve:PolyCurve;layer?:string;lineWidth?:number;material?:string; }
One authored chain in a curve document, carrying its own claims.
A CurveEdge is a lone unshared chain — no vertices, no loops, no sharing with a neighbour. The {Kind}{ElementKind} prefix is what lets "edge" mean that here and a shared topology edge on BrepData without collision (ARCHITECTURE.md Entity). Blender's matching type is Nurb/Spline in a Curve datablock; that spelling is not the type name here.
attributes?
optionalattributes?:Record<string,unknown> |null
Opaque claims for this chain — absent or null = no claims. Written by object assembly or curve* ops, never hand-authored.
color?
optionalcolor?:string
Stroke colour for THIS chain, as a CSS colour string, overriding the placement's material. Absent === inherit.
Colour, width and dash are what a line HAS — the drafting trio every linework format states its own way (SVG stroke / stroke-width / stroke-dasharray, DXF layer colour / lineweight / linetype) — so they sit on the chain rather than in attributes.
curve
curve:
PolyCurve
layer?
optionallayer?:string
Layer key for THIS chain, overriding its placement's Instance.layer. Absent === inherit from the instance.
Per-chain for the reason material already is, and a curve document is the case that forces it: DXF assigns layer per ENTITY and never per block, so one imported drawing legitimately carries N layers across its chains before it is ever wrapped into one Object.
lineWidth?
optionallineWidth?:number
Stroke width in millimetres, world units and not pixels. Absent === the renderer's hairline.
material?
optionalmaterial?:string
Material key for THIS chain, overriding its placement's Instance.material. Absent === inherit from the instance.
Per-chain for the same reason layer is: one document holds many chains — curveFromFace gives each ring the material of the face it came off — so the claim cannot ride the placement.
CurveInstance
CurveInstance =
BaseInstance& {material?:string; }
CurveObject
CurveObject =
BaseObject& {data:CurveData;instances:CurveInstance[];type:"curve"; }
An object whose data is one curve document — N PolyCurve chains.
EdgeMark
EdgeMark = {
attributes?:Record<string,unknown> |null;sharp?:boolean;v1:number;v2:number; }
One marked mesh edge — vertex-index pair plus its own claims. Named EdgeMark, not BrepEdge (BrepData's shared TOPOLOGY edge, other entries reference it by index; this is a discrete vertex-index pair with no sharing — the mesh analogue of CurveData's CurveEdge) and not MeshEdge (already taken: geometry/triangleMesh.ts's MeshEdge is the DISPLAY tessellation's rendered edge polyline, a different tier).
attributes?
optionalattributes?:Record<string,unknown> |null
Opaque claims for this edge — absent or null = no claims. Written by object assembly or edge* ops, never hand-authored.
sharp?
optionalsharp?:boolean
Hard edge (split normal) — Blender sharp_edge, the FBX smoothing-group boundary. Absent === smooth.
v1
v1:
number
Vertex index into vertices.
v2
v2:
number
Vertex index into vertices.
Environment
Environment = {
groundColor?:string;hdri?:EnvironmentHdri;moment?:SunMoment;site?:Site;skyColor?:string;skyLux?:number;sunKelvin?:number;sunLux?:number; }
settings.environment — the surroundings the model is lit by. Every field optional: a document with no environment is lit by its own emitting objects and nothing else.
groundColor?
optionalgroundColor?:string
Colour bounced from below (−Z) — the ground half. Equal to skyColor gives the flat uniform fill a renderer calls "ambient".
hdri?
optionalhdri?:EnvironmentHdri
moment?
optionalmoment?:SunMoment
Which moment — the sun's second input. Without both, there is no sun.
site?
optionalsite?:Site
Where on Earth — the sun's first input.
skyColor?
optionalskyColor?:string
Colour arriving from above (+Z) — the sky half of the outdoor bounce.
skyLux?
optionalskyLux?:number
Illuminance (lux) of that sky/ground fill.
sunKelvin?
optionalsunKelvin?:number
Sun colour temperature (K).
sunLux?
optionalsunLux?:number
Direct-sun illuminance (lux) at the model. Absent = a bright clear midday (the host's documented default).
FlatView
FlatView =
ViewBase& {source:string; }
Space = a flat component; its content is already 2D.
GeometryPlacement
GeometryPlacement = {
attributes?:Record<string,unknown>;layer?:string;material?:string;name?:string;transformation:Transformation;visible?:boolean; }
Placement matrix + props for one block placement in a render frame.
attributes?
optionalattributes?:Record<string,unknown>
layer?
optionallayer?:string
material?
optionalmaterial?:string
name?
optionalname?:string
transformation
transformation:
Transformation
visible?
optionalvisible?:boolean
HatchData
HatchData = {
hatches:HatchEntity[]; }
Object-tier hatch datablock — the bag of hatches.
hatches
hatches:
HatchEntity[]
HatchEntity
HatchEntity = {
angle?:number;attributes?:Record<string,unknown> |null;color?:string;fill?:HatchFill;origin?: [number,number];pattern:string;region:Curve2d;scale?:number; }
One hatch: a region, the line layers filling it, and where those layers sit.
HATCH SPACE is the 2D frame the lines are laid out in — millimetres on the region's own plane, NEVER a surface's uv. A parameterization is not arc-length, so a uv-laid pattern stretches on a revolved face, and two neighbouring faces have unrelated uv so their hatching breaks at the shared edge. Every surveyed system lays out on a plane in drawing units instead.
angle?
optionalangle?:number
Rotation of every layer about HatchEntity.origin, in degrees. Absent === 0.
attributes?
optionalattributes?:Record<string,unknown> |null
Opaque per-hatch claims. Absent or null === no claims.
color?
optionalcolor?:string
One colour for every layer, overriding each layer's own. Absent === each layer keeps the colour its pattern gave it.
A drawing standard asks for the same pattern in another colour far more often than it asks for a different pattern, and duplicating the record to change a colour is how one pattern becomes six. The override is a single colour on purpose: a per-layer override would be the pattern again, written twice.
fill?
optionalfill?:HatchFill
Paint behind every layer, over the whole region. Absent === the region is not painted and only the lines are drawn.
origin?
optionalorigin?: [number,number]
Where the pattern is anchored, in hatch space. Absent === [0, 0] of hatch space — NOT the region's own extent, and that difference is the whole point.
Two regions sharing a plane share hatch space, so an anchor of [0, 0] is the same world point for both and their hatching lines up across the edge they share; anchoring to each region's own extent would give them two different anchors and break exactly that. It is also why the pattern stays put when the region is edited. Rhino calls this m_basepoint; Archicad exposes the same choice as linking to the project origin versus linking to the fill's own origin.
pattern
pattern:
string
The patterns[] record whose line families fill this region, BY NAME.
By name and never inline: two hatches drawn with concrete must be the same concrete, one edit must reach both, and every interchange format references a pattern by name rather than embedding it. A pattern that draws nothing is how a region gets HatchEntity.fill and no linework over it — the empty case lives in the RECORD, so there is still exactly one way to say it.
region
region:
Curve2d
The region being filled: closed rings, with transformation placing their plane in the world. Rings that nest punch holes by the even-odd rule — no ring declares itself an island, the same way BrepLoop does not, because two ends of one claim can disagree.
Rings, not a BrepData: a hatch region is planar by definition, and carrying a trimmed-surface record for it stored the plane twice (once as Hatch.plane, once inside the face's surface) and let the two disagree.
scale?
optionalscale?:number
Uniform scale about HatchEntity.origin, multiplying each layer's shift, spacing, dashes and origin. Absent === 1.
HatchFill
HatchFill = {
color:string;type:"solid"; } | {end: [number,number];repeat?:number;start: [number,number];stops:ColorStop[];type:"linearGradient"; } | {end: [number,number];repeat?:number;start: [number,number];stops:ColorStop[];type:"radialGradient"; }
Paint laid over the whole region, behind the lines. Archicad carries the same thing as a fill's background pen; DXF as its solid-fill flag.
PDF is the system that got the shape right — one object discriminated by a type tag. Rhino instead put an enum on the pattern and had to abandon its gradient arm (the enum value is commented out in opennurbs_hatch.h), then bolt gradient back on as four loose fields of ON_Hatch.
ONE tag, never a tag inside a tag: the two gradient arms are spelled out in full and named after SVG's own elements, linearGradient and radialGradient.
Union Members
Type Literal
{ color: string; type: "solid"; }
color
color:
string
CSS colour string.
type
type:
"solid"
Type Literal
{ end: [number, number]; repeat?: number; start: [number, number]; stops: ColorStop[]; type: "linearGradient"; }
end
end: [
number,number]
repeat?
optionalrepeat?:number
Ramp repetitions across start→end. Absent === 1.
start
start: [
number,number]
Ramp endpoints in hatch space.
stops
stops:
ColorStop[]
Ramp stops, the shape the textures lane already normalizes to.
type
type:
"linearGradient"
Type Literal
{ end: [number, number]; repeat?: number; start: [number, number]; stops: ColorStop[]; type: "radialGradient"; }
end
end: [
number,number]
repeat?
optionalrepeat?:number
Ramp repetitions across start→end. Absent === 1.
start
start: [
number,number]
Centre, and a point on the outer ring, in hatch space.
stops
stops:
ColorStop[]
Ramp stops, the shape the textures lane already normalizes to.
type
type:
"radialGradient"
HatchInstance
HatchInstance =
BaseInstance& {material?:string; }
HatchLine
HatchLine = {
angle:number;dashes?:number[];origin: [number,number];shift:number;spacing:number; }
One family of parallel infinite lines carrying a repeating pen cycle — the whole mathematics of a hatch, and nothing else. One of these is exactly one data line of an AutoCAD .pat file, so a pattern definition copies straight in and straight back out.
Measured 2026-08-22 against the AutoCAD 2023 acad.pat shipped on this machine (72 patterns, 266 data lines): the format has EXACTLY this one mechanism. There is no finite-shape primitive. *AR-CONC's stones are three families of short dashes at three angles whose marks cross — the triangle is a visual effect, not an object, and the three cycles (9.0, 7.2, 7.6488) are deliberately incommensurate so the result never repeats.
A closed repeating figure IS constructible with this mechanism: give the families a common lattice and the dashes meet at their endpoints. What the mechanism cannot do is FILL such a figure — lines bound nothing on their own, which is what HatchEntity.fill is for.
angle
angle:
number
Direction of the family, in degrees.
dashes?
optionaldashes?:number[]
Repeating pen cycle measured along the line, in the SVG spelling: all values POSITIVE, alternating, EVEN indices draw and ODD indices lift. An odd-length list repeats once to make it even, as SVG's stroke-dasharray does.
.pat instead signs the values (negative = lift). The signs are the one thing that makes that format hard to read, and they are recovered on export by alternating them back.
A 0 in a DRAW slot is a dot, painted at HatchLineEntity.dotSize. Absent === a solid, unbroken line.
origin
origin: [
number,number]
A point the first line passes through, in hatch space.
shift
shift:
number
Slide ALONG the line direction between one line and the next.
Sliding a solid line along itself returns the same line, so this only becomes visible once HatchLine.dashes breaks it up: it staggers the marks of successive lines. A value that does not divide the dash cycle never comes back into phase, which is how a pattern reads as random while staying fully determined.
spacing
spacing:
number
Distance ACROSS, between one line and the next.
HatchLineEntity
HatchLineEntity = {
color?:string;dotSize?:number;line:HatchLine;lineWidth?:number; }
One drawn layer of a hatch: how the line looks, plus where the lines are.
The split is the point. line is pure mathematics and is exactly a .pat data line, so it round-trips to DXF untouched and an author (or a generator) can paste one in. Everything beside it is appearance, which .pat has no room for and which is simply dropped on export to that format.
Layers paint in array order, so a later layer draws over an earlier one.
color?
optionalcolor?:string
CSS colour string, the spelling Material.baseColor already uses. Absent === inherit from the placement's material.
dotSize?
optionaldotSize?:number
Diameter painted for a zero-length dash. Absent === HatchLineEntity.lineWidth.
line
line:
HatchLine
Where the lines are.
lineWidth?
optionallineWidth?:number
Stroke width in millimetres. Absent === the renderer's hairline.
HatchObject
HatchObject =
BaseObject& {data:HatchData;instances:HatchInstance[];type:"hatch"; }
An object whose data is hatches and nothing else.
Image
Image = {
anchors:Point[];image:string; }
Raster image rectangle: a url string plus its placement anchors.
anchors
anchors:
Point[]
image
image:
string
ImageData
ImageData = {
images:ImageEntity[]; }
Object-tier image datablock.
images
images:
ImageEntity[]
ImageEntity
ImageEntity =
Image& {attributes?:Record<string,unknown> |null; }
One image of an image object.
ImageInstance
ImageInstance =
BaseInstance& {material?:string; }
ImageObject
ImageObject =
BaseObject& {data:ImageData;instances:ImageInstance[];type:"image"; }
An object whose data is images and nothing else.
A raster placed in the scene is not a mesh (no topology) and not a hatch (not a UV pattern of drawn content).
Instance
Instance =
CurveInstance|BrepInstance|MeshInstance|PointCloudInstance|BlockInstance|LightInstance|SectionInstance|HatchInstance|SkeletonInstance|LinearDimensionInstance|RadialDimensionInstance|AngularDimensionInstance|LeaderInstance|TextInstance|ImageInstance|ViewInstance|CameraInstance
Evaluated placement union. Generic walks (fold, applyAuthoredInstances) use this; a kind's own instances field is the matching member.
Layer
Layer = {
color?:Color;lineStyle?:LineStyle;lineWidth?:number;name:string; }
Resolved linework-appearance record. Layer is pure CLASSIFICATION, never a rendering fallback: color, lineWidth and lineStyle live on the Entity directly and take NO Layer-level default (layer/material resolve exactly two tiers, Entity override → Instance default). Materials stay the SURFACE record — a face's fill is its material's baseColor. Renderers honor what they can and ignore the rest (three ignores all three today). The three fields are the portable trio every linework format states its own way — SVG as stroke/stroke-width/stroke-dasharray, DXF as layer color/lineweight/linetype.
color?
optionalcolor?:Color
Linework color, RGB 0–1.
lineStyle?
optionallineStyle?:LineStyle
lineWidth?
optionallineWidth?:number
Linework width in WORLD units (mm) — not pixels.
name
name:
string
LeaderData
LeaderData = {
leaders:LeaderEntity[]; }
Object-tier leader datablock.
leaders
leaders:
LeaderEntity[]
LeaderEntity
LeaderEntity =
Leader& {attributes?:Record<string,unknown> |null;curves:PolyCurve[];label?:Text;style:string; }
One leader in a LeaderData document. The text it points WITH is label, never a sibling field on the object.
LeaderInstance
LeaderInstance =
BaseInstance& {material?:string; }
LeaderObject
LeaderObject =
BaseObject& {data:LeaderData;instances:LeaderInstance[];type:"leader"; }
An object whose data is leaders and nothing else.
A pointer is not a dimension (no measured value) and not a text (the string lives on label inside each LeaderEntity).
LightData
LightData =
LightShape& {color?:string;coneAngle?:number;direction?:Vector;ies?:string;kelvin?:number;lumens:number;penumbra?:number;position:Point; }
Object-tier light datablock: ONE emitter — where it is, what shape emits, and what it radiates. There is no separate Light beside it, because there is only one shape and the law allows it only one name.
A light has no INTERNAL plurality, so it carries no Data-tier array (being an INDEPENDENT kind is a different question — point is independent yet its entries are a list). N is instances[]'s job — a rig of 12 identical bulbs is one LightData placed 12 times, never 12 entries in a list.
Photometric units, so an author states what a manufacturer prints on the box and a renderer converts to its own.
LightEntity
LightEntity =
LightData& {gizmoSize?:number;target?:Point; }
A light's Entity tier: its emitting core (LightData) plus what only a VIEWPORT needs to draw and aim it. A light has no internal plurality, so this is exactly ONE Entity and not an array (unlike PointEntity, which is a point of a cloud); N lights is instances[].
The two fields here are exactly what the functional core cannot say: lumens does not become a gizmo by living on the same document, and a photometric field is not made display data by being read by a renderer.
LightInstance
LightInstance =
BaseInstance& { }
A light has no surfaces; material is rejected on this kind.
LightObject
LightObject =
BaseObject& {data:LightEntity;instances:LightInstance[];type:"light"; }
An object whose data is lights and nothing else.
A light is an emitter, not a tessellatable body. Grouping emitters as their own kind gives a lighting consumer a DOMAIN instead of sniffing the stream for lumens. The housing is ordinary solid geometry on a different object.
LightShape
LightShape =
PointLight|SphereLight|DiskLight|RectangleLight|CylinderLight
The emitting shape. A tagged union because these are genuinely different geometry — a disk has a radius and a rectangle has two sides — not because a renderer happens to have a class per kind.
There is no spot arm, and its absence is the point: a spot is any shape with a coneAngle set, so narrowing stays ORTHOGONAL to what emits, and a disk-with-a-cone (a real downlight) is expressible. three.js's SpotLight class fuses the two and cannot say it.
LinearDimension
LinearDimension = {
direction?:Vector;linePoint:Point;references:Point[]; }
A distance measured between marks, drawn on a dimension line.
references is a CHAIN, not a bag: N+1 marks give N measured segments, so a single dimension is a chain of one and needs no separate type, and a running / chained dimension needs no second type either. Every element has the SAME role.
linePoint is a point the dimension LINE passes through: the offset from the references is geometry, not a number to guess a sign for.
direction absent means measure ALONG the references (the ordinary case). Present, it measures the projection onto that direction.
direction?
optionaldirection?:Vector
linePoint
linePoint:
Point
references
references:
Point[]
LinearDimensionData
LinearDimensionData = {
dimensions:LinearDimensionEntity[]; }
Object-tier linear-dimension datablock.
dimensions
dimensions:
LinearDimensionEntity[]
LinearDimensionEntity
LinearDimensionEntity =
LinearDimension& {attributes?:Record<string,unknown> |null;curves:PolyCurve[];label?:Text;style:string;text?:string;textPoint?:Point; }
One linear dimension in a LinearDimensionData bag: the measured geometry plus the style it is drawn with, the drawn form, and the two overrides a drafter reaches for. Never a stored value — the measurement is a function of the geometry's own points.
LinearDimensionInstance
LinearDimensionInstance =
BaseInstance& {material?:string; }
LinearDimensionObject
LinearDimensionObject =
BaseObject& {data:LinearDimensionData;instances:LinearDimensionInstance[];type:"linearDimension"; }
An object whose data is linear dimensions and nothing else.
Linear measure has two anchors and a direction. The three dimension kinds are different mathematical arities and cannot share one object kind.
Material
Material = {
alphaMode?:"OPAQUE"|"MASK"|"BLEND";alphaTest?:number;aoTexture?:string;attenuationColor?:string;attenuationDistance?:number;attributes?:Record<string,unknown>;baseColor?:string;clearcoat?:number;clearcoatRoughness?:number;colorFactor?:number;cutPattern?:string;displacementScale?:number;displacementTexture?:string;doubleSided?:boolean;emissive?:string;emissiveIntensity?:number;emissiveTexture?:string;ior?:number;metallic?:number;metallicTexture?:string;name:string;normalTexture?:string;opacity?:number;roughness?:number;roughnessTexture?:string;sheenColor?:string;sheenRoughness?:number;surfacePattern?:string;texture?:string;thickness?:number;tileHeight?:number;tileWidth?:number;transmission?:number; }
Runtime value contract — what evaluation produces (data stream, resolved records).
THE VOCABULARY ITSELF LIVES IN @huukhanhnguyen/types. This file re-exports ObjectData + the geometry-shape guards, the resolved asset records (material/layer/font/image), skeleton/animation, emission/camera, style description, and the declarative sheets/layouts/saved-views vocabulary so every consumer keeps importing through the @huukhanhnguyen/model barrel (src/index.ts export * from "./graph/index.js").
What stays in this package: the authoring node lane, the expression/evaluate engines, and the evaluated LANE shapes in the sibling modules re-exported at the bottom of this file (table/texture/view/sheet/ component/frame/spaces).
Import law: nothing outside graph/ — never product packages or apps/ (any other npm package is fine, per the graph-isolation boundary rule).
alphaMode?
optionalalphaMode?:"OPAQUE"|"MASK"|"BLEND"
glTF alphaMode — the EXPLICIT statement, which wins over the inference when present: "MASK" reads alphaTest (default threshold 0.5), "BLEND" reads opacity, "OPAQUE" ignores both. Absent = inferred from the two fields above (alphaTest set → cutout; opacity < 1 → blend).
alphaTest?
optionalalphaTest?:number
Alpha-cutout threshold (0..1) for a billboard/foliage-style texture — pixels below this alpha are fully discarded (glTF alphaMode "MASK"), not blended. Unset = no cutout (opaque, or opacity-blended).
aoTexture?
optionalaoTexture?:string
Ambient-occlusion map (URI or texture key) — grayscale multiplier on ambient/indirect light; shares tileWidth/tileHeight.
attenuationColor?
optionalattenuationColor?:string
KHR_materials_volume attenuation color, hex.
attenuationDistance?
optionalattenuationDistance?:number
KHR_materials_volume attenuation distance (mm).
attributes?
optionalattributes?:Record<string,unknown>
Domain attribute bag (classify-by-data). Documented keys:
- kFactor?: number — sheet-metal bend K-factor (ANSI)
- density?: number — kg/mm³ material mass density Core never grows domain fields as first-class Material props.
baseColor?
optionalbaseColor?:string
clearcoat?
optionalclearcoat?:number
KHR_materials_clearcoat — varnish / gloss paint, 0..1.
clearcoatRoughness?
optionalclearcoatRoughness?:number
colorFactor?
optionalcolorFactor?:number
0 = white tint (texture shows through fully), 1 = solid baseColor. Default 1.
cutPattern?
optionalcutPattern?:string
The patterns[] record drawn where a SECTION CUTS this material, BY NAME. The region comes from the cut itself, at stamp time.
A name and not a hatch-shaped object: everything a hatch carries beside the name says WHERE the pattern sits, and a material knows no region to sit in. Carrying the placement knobs here made a second, half-usable spelling of a hatch that nothing could author.
displacementScale?
optionaldisplacementScale?:number
Displacement amplitude in mm (three displacementScale). Default 1.
displacementTexture?
optionaldisplacementTexture?:string
Displacement/height map (URI or texture key) — grayscale, displaces vertices along their normals. Only visible on tessellated/dense geometry (a 2-triangle face has no vertices to move — use the displace geometry op for real relief there).
doubleSided?
optionaldoubleSided?:boolean
Render both faces of a surface (glTF doubleSided) — thin imported geometry (a leaf, a curtain) has no solid thickness to close it.
emissive?
optionalemissive?:string
Emissive (self-lit) color, hex — glTF emissiveFactor.
emissiveIntensity?
optionalemissiveIntensity?:number
Emissive strength multiplier (glTF KHR_materials_emissive_strength). Default 1.
emissiveTexture?
optionalemissiveTexture?:string
Emissive map (URI or texture key, same convention as texture).
ior?
optionalior?:number
KHR_materials_ior — glass ≈ 1.5.
metallic?
optionalmetallic?:number
metallicTexture?
optionalmetallicTexture?:string
Metallic map (URI or texture key) — grayscale, blue channel read per glTF convention; shares tileWidth/tileHeight.
name
name:
string
normalTexture?
optionalnormalTexture?:string
Normal map (URI or texture key) — real light-responsive relief. Sampled linear (never sRGB); shares tileWidth/tileHeight.
opacity?
optionalopacity?:number
roughness?
optionalroughness?:number
roughnessTexture?
optionalroughnessTexture?:string
Roughness map (URI or texture key) — grayscale, green channel read per glTF convention; shares tileWidth/tileHeight.
sheenColor?
optionalsheenColor?:string
KHR_materials_sheen — fabric/upholstery color, hex.
sheenRoughness?
optionalsheenRoughness?:number
surfacePattern?
optionalsurfacePattern?:string
The patterns[] record drawn where a face is seen UNCUT — the elevation hatch. Same rule as Material.cutPattern.
texture?
optionaltexture?:string
Texture image as a URI (https:// or data:) or a processors key — images have no lane of their own; the string itself is the reference.
thickness?
optionalthickness?:number
KHR_materials_volume thickness in mm (optical path length).
tileHeight?
optionaltileHeight?:number
tileWidth?
optionaltileWidth?:number
Physical size (mm) of ONE seamless texture tile — face UVs are plane millimetres, so the renderer shows the texture at REAL size regardless of geometry dimensions (repeat = 1/tile). FALLBACK only: a textures[] chain declares its physical size via its builtin lengthX/lengthY parameters (the tile's size is intrinsic to the tile), which ride TextureData and win over these; declare here for plain URI textures, which have no record of their own. Default 1000.
transmission?
optionaltransmission?:number
KHR_materials_transmission — 0..1. Glass walls need this (opacity alone is frosted plastic).
MaterialData
MaterialData =
Material& {aoTextureData?:TextureData;displacementTextureData?:TextureData;emissiveTextureData?:TextureData;metallicTextureData?:TextureData;normalTextureData?:TextureData;roughnessTextureData?:TextureData;textureData?:TextureData;textureFlipY?:boolean; }
A Material with its texture chains RESOLVED to pixels — what an evaluation hands a consumer that needs pixels. The *TextureData fields are attached at evaluate time when the matching texture / normalTexture / roughnessTexture / … names a texture key.
MeshData
MeshData = {
edges?:EdgeMark[];faces:MeshFace[];morphs?:MorphTarget[];skin?:SkinBinding;vertices:MeshVertex[];weightsTrack?:string; }
Opaque n-gon polygon mesh — design topology stored as faces, triangles only at the door (render tessellation). Per-vertex claims live on each MeshVertex (NOTE the difference from shell tables: here vertex i's uv is vertices[i].uv, its normal vertices[i].normal, its skin influences vertices[i].skin). The object's kind is explicit in the evaluated value (MeshObject.type); no reader sniffs a geometry field to recognise it. Closed BY TYPE (the boundary law): only the mesh→mesh op family consumes it; boolean/BOM/tableSource/nesting/watertight filter it out by type.
Kept DISTINCT from the tessellation TriangleMesh wire type (see triangleMesh.ts) on purpose: the stream shape carries per-vertex MeshVertex.skin (indices/weights) while the tessellation expands it to skinIndices/skinWeights arrays (+ skeleton), and the tessellation adds the groups bake-time field a stream document never has. Tessellation always carries triangle indices.
edges?
optionaledges?:EdgeMark[]
Sparse marked edges for interaction/shading — one entry per marked edge, array-of-structs — the same shape CurveData.edges keeps: an array of CurveEdge entries instead of a parallel curveAttributes. Runtime builds the FULL edge table from faces; this array only ever lists edges that carry a mark.
faces
faces:
MeshFace[]
N-gon faces, array-of-structs — see MeshFace.
morphs?
optionalmorphs?:MorphTarget[]
Morph targets — see MorphTarget (offsets are per-VERTEX position deltas, flat and parallel to vertices by index — already the tessellation layout, no remap at render).
skin?
optionalskin?:SkinBinding
This mesh's skeleton binding — see SkinBinding. Absent === not skinned. The per-vertex influences live on MeshVertex.skin; this holds only what is true of the whole document (which skeleton, and the bind pose it was attached at), which is why a writer reads the name and the inverse-bind matrices here and the weights per vertex.
vertices
vertices:
MeshVertex[]
Vertices — see MeshVertex.
weightsTrack?
optionalweightsTrack?:string
Track.key reference (2026-08-14) — this mesh's OWN weights channel. One track covers every entry in morphs at once: its values[i].length must equal morphs.length, in the SAME order — glTF packs one node's whole weights array into a single channel (confirmed against the spec: node.weights count MUST match the referenced mesh's morph target count), so one track already covers it — no per-target track to re-merge at export. Consumer-holds-the- reference, same as BoneEntity's *Track fields. Absent = this mesh's morph weights never animate (still usable as a static pose via each target's own authored weight elsewhere, out of scope here).
MeshFace
MeshFace = {
attributes?:Record<string,unknown> |null;indices:number[];layer?:string;material?:string;sharp?:boolean; }
One n-gon face — vertex-index loop plus its own claims. Array-of-structs: faces, faceAttributes and faceSharp used to be three arrays a caller had to keep index-locked by hand — the same burden edges/edgeAttributes carried before folding into EdgeMark, for the identical reason: three parallel arrays cost an index-sync bug surface and bought nothing.
attributes?
optionalattributes?:Record<string,unknown> |null
Opaque claims for this face — absent or null = no claims. Written by object assembly (merge) or face* ops, never hand-authored.
indices
indices:
number[]
CCW outer loop of vertex indices into vertices, length ≥ 3. Non-planar faces allowed. A triangle is a 3-gon.
layer?
optionallayer?:string
Layer key for THIS face, overriding its placement's Instance.layer. Absent === inherit from the instance.
Per-face for the reason material already is: an IMPORTED document can carry N different layers across its faces before it is ever wrapped into one Object — DXF especially, which assigns layer per ENTITY and never per block — and collapsing that to one Instance-level layer on arrival loses what the file actually said.
material?
optionalmaterial?:string
Material key for THIS face — a materials[] record name — overriding its placement's Instance.material. Absent === inherit from the instance.
The oldest per-face lane reference and the reason layer is one too: a glTF primitive, an OBJ usemtl run or a boolean fragment names a material per FACE, and one document holds many faces, so the claim cannot ride the placement.
sharp?
optionalsharp?:boolean
Flat/hard shading — absent or false means smooth shaded. Blender sharp_face.
MeshInstance
MeshInstance =
BaseInstance& {material?:string; }
MeshObject
MeshObject =
BaseObject& {data:MeshData;instances:MeshInstance[];type:"mesh"; }
An object whose data is one mesh buffer.
One object IS one buffer. Disconnected pieces ("islands") stay derived. Turning those islands into separately named objects is looseToBlocks.
MeshVertex
MeshVertex = {
attributes?:Record<string,unknown> |null;normal?: [number,number,number];position: [number,number,number];skin?: {indices: [number,number,number,number];weights: [number,number,number,number]; };uv?: [number,number];uv2?: [number,number]; }
One mesh vertex — position plus its own per-vertex claims. Array-of- structs replacing MeshData's old positions/uvs/uvs2/normals/skin(top)/ vertexAttributes parallel arrays, for the same reason faces/edges were already folded: N parallel arrays cost an index-sync bug surface and buy nothing.
attributes?
optionalattributes?:Record<string,unknown> |null
Opaque claims for this vertex — absent or null = no claims.
normal?
optionalnormal?: [number,number,number]
Absent = a renderer derives it (area-weighted vertex normal).
position
position: [
number,number,number]
skin?
optionalskin?: {indices: [number,number,number,number];weights: [number,number,number,number]; }
Skeletal binding of THIS vertex — 4 entries, glTF SKIN semantics. The other half is MeshData.skin, which names the skeleton and the bind pose once for the whole document.
indices
indices: [
number,number,number,number]
weights
weights: [
number,number,number,number]
uv?
optionaluv?: [number,number]
glTF TEXCOORD_0 equivalent.
uv2?
optionaluv2?: [number,number]
glTF TEXCOORD_1 equivalent — AO/lightmaps.
MorphTarget
MorphTarget = {
name?:string;offsets:number[]; }
One morph target: per-vertex POSITION DELTAS, flat [dx,dy,dz] per vertex, parallel to MeshData.vertices by index (deliberate: a morph is a shape DELTA the engine reads directly, not a per-vertex claim). Static shape data — never animated itself; MeshData.weightsTrack is what varies over time.
name?
optionalname?:string
offsets
offsets:
number[]
ObjectData
ObjectData =
CurveData|BrepData|MeshData|PointCloudData|BlockData|LightData|SectionData|HatchData|SkeletonData|LinearDimensionData|RadialDimensionData|AngularDimensionData|LeaderData|TextData|ImageData|ViewData|CameraData
Mid-chain stream — the payload of a SceneObject, without identity or placement. A chain produces exactly one kind; the value is that kind's *Data. Object-level claims live on BaseObject / BaseInstance after assembly; per-region claims live on each entry (MeshFace.attributes, CurveEdge.attributes, BrepFace.attributes).
OrientedEdge
OrientedEdge = {
edge:number;forward:boolean; }
An edge taken in a traversal direction. forward walks start → end.
edge
edge:
number
forward
forward:
boolean
Page
Page = {
height:number;objects:SceneObject[];title:string;width:number; }
One composed sheet: content inside the page that BOUNDS it.
A sheet is an OBJECT whose space is finite, so this is an object's content plus the bound, and nothing else.
Model.evaluateSheet returns it inside the shared envelope — Resolved<Page> (frame.ts).
The bound is a SIZE, not a box, and that is the one place a sheet differs from a view: a view's crop floats in the view plane (both corners carry information, so it is a BoundingBox2d), while the paper's corner IS the origin — every composed document lands in [0,width] × [0,height]. Storing minX/minY for paper would be storing two constant zeros.
height
height:
number
objects
objects:
SceneObject[]
Paper objects — the same SceneObject union the scene uses. A viewObject placement is a ViewObject; template furniture and ad-hoc drawing are the geometry kinds. No second envelope.
title
title:
string
The evaluated title-block name, e.g. "GROUND FLOOR PLAN" — CONTENT printed on the paper (title-vs-label law), which is why it rides the VALUE and not the envelope's editor-chrome label. Sheet.title is an expression (titleValue law); this is what it evaluated to. "" when the sheet declares none.
width
width:
number
The bound — paper millimetres.
Pattern
Pattern = {
lengthSpace?:"model"|"paper";lines:HatchLineEntity[];name:string; }
Resolved pattern record — the drawing side of appearance, as Material is the surface side and Layer the classification side.
Produced by evaluatePatterns() in the model package from the authored pattern-method nodes; a resolved record is never written back to the document.
A pattern is the line families and NOTHING about where they are drawn. That split is what makes the record shareable at all: anything region-dependent would be a different pattern per region, leaving nothing to name. It is also what every interchange format requires — DXF names a pattern at group code 2, SVG references one by id from defs, PDF as a named resource — so a pattern that cannot be named cannot be written out at all.
lengthSpace?
optionallengthSpace?:"model"|"paper"
Which space this pattern's lengths are measured in. Absent === "model".
"model" — spacing, shift, dashes and lineWidth are millimetres of the MODEL, so the pattern shrinks with the drawing as the view scale falls. A material's own texture-like pattern wants this.
"paper" — the same numbers are millimetres of the SHEET, so a 2mm spacing prints as 2mm whether the view is at 1 or 1. Every drafting standard specifies its hatches this way, and getting it wrong is not a small error: at 1 a model-space 2mm hatch prints at 0.02mm and reads as solid black.
Precedent for the choice being a property of the PATTERN rather than of each hatch: Archicad carries it on the fill attribute (its help calls the two "scale with plan" and "scale independent"); AutoCAD reaches the same end through annotative scaling.
TODAY, "paper" is authorable and expandable but has no in-repo caller: renderLeaf takes a sheetScale and both of its callers fold MODEL space, where no sheet exists. Expanding a paper pattern without one THROWS, on purpose. A host that places a view on a sheet is the party that knows the number.
lines
lines:
HatchLineEntity[]
The drawn layers, painted in order. EMPTY is legitimate: a pattern that draws nothing is how a region gets a solid fill and no linework over it.
name
name:
string
Point
Point = [
number,number,number]
3D point (default space). Same stem as geometry Point namespace.
PointCloudData
PointCloudData = {
points:PointEntity[]; }
Geometry-tier point-cloud data document — the bag of points, a sibling of instances[] under the object.
points
points:
PointEntity[]
PointCloudInstance
PointCloudInstance =
BaseInstance& {material?:string; }
PointCloudObject
PointCloudObject =
BaseObject& {data:PointCloudData;instances:PointCloudInstance[];type:"pointCloud"; }
An object whose data is points and nothing else.
The name is the industry's (Blender OB_POINTCLOUD, Rhino ON_PointCloud). A point intersects nothing and cuts nothing, so a cloud never needs the boolean/offset/trim machinery the other content kinds do.
PointEntity
PointEntity = {
attributes?:Record<string,unknown> |null;position:Point;size?:number; }
One point of a cloud. An IMPORTED cloud (LIDAR, photogrammetry) carries per-point claims straight from its source format, so the point is a record rather than a bare position.
attributes?
optionalattributes?:Record<string,unknown> |null
Opaque per-point claims (colour, classification, intensity, …). Absent or null === no claims.
position
position:
Point
size?
optionalsize?:number
Display size/radius for this point. Absent === renderer default. Named directly rather than buried in attributes because it is near-universal: USD UsdGeomPoints.widths, Houdini pscale, Blender's point-cloud radius.
PolyCurve
PolyCurve = {
segments:Segment[]; }
Ordered chain of 3D curve segments (endpoint-welded).
An ordered chain only — segments meet end to start, never branch. Closest OCCT idea: CompositeCurve / a polyline of heterogeneous carriers. Wire: { segments: Segment[] }.
This is the geometry CurveData carries. Anything the user draws (one line, one arc, a polyline, a heterogeneous chain) is a PolyCurve — a single segment is a valid one-element chain. Bare NurbsCurve / bare LineCurve are kind carriers inside segments, not document payloads.
segments
segments:
Segment[]
ProjectionView
ProjectionView =
ViewBase& {camera:string; }
Axis is a free human choice, so it comes from a CameraObject. camera is that object's key — placement from its instance, projection from its camera field. A CameraData has no axis after the pose/projection split (objects/camera.ts). A drawing still takes its paper frame from the layout; fov/height are ignored there.
Quaternion
Quaternion = [
number,number,number,number]
Unit rotation [x, y, z, w].
RadialDimension
RadialDimension = {
angle:number;arc:ArcCurve; }
A radius (or diameter) measured on a circle or circular arc.
The measured subject is an ArcCurve because that type ALREADY carries centre, radius, plane normal and both axes — a centre plus a rim point cannot determine a circle in space.
angle (radians, in the arc's own xAxis/yAxis frame) locates the leader ON the arc.
angle
angle:
number
arc
arc:
ArcCurve
RadialDimensionData
RadialDimensionData = {
dimensions:RadialDimensionEntity[]; }
Object-tier radial-dimension datablock.
dimensions
dimensions:
RadialDimensionEntity[]
RadialDimensionEntity
RadialDimensionEntity =
RadialDimension& {attributes?:Record<string,unknown> |null;curves:PolyCurve[];label?:Text;style:string;text?:string;textPoint?:Point; }
One radial dimension in a RadialDimensionData bag.
RadialDimensionInstance
RadialDimensionInstance =
BaseInstance& {material?:string; }
RadialDimensionObject
RadialDimensionObject =
BaseObject& {data:RadialDimensionData;instances:RadialDimensionInstance[];type:"radialDimension"; }
An object whose data is radial dimensions and nothing else.
Radius and diameter have one centre and one radius point — a different arity from linear.
RenderFrameBlock
RenderFrameBlock = {
definition?:string;leaves:ObjectData[];placement:GeometryPlacement; }
definition?
optionaldefinition?:string
leaves
leaves:
ObjectData[]
The placement's shared content, LOCAL to it — the consumer composes placement.transformation itself. N placements of one object hand back the SAME array by reference, so a diff host tessellates it once.
placement
placement:
GeometryPlacement
RenderFrameSlice
RenderFrameSlice = {
blocks:RenderFrameBlock[];id:string;key:string;leaves:ObjectData[];object?:SceneObject; }
blocks
blocks:
RenderFrameBlock[]
id
id:
string
key
key:
string
leaves
leaves:
ObjectData[]
World-space leaves, PRE-BAKE: nested block placements are exploded and their matrices baked into the geometry, but nothing is tessellated — a curve is still a CurveData, a solid still a BrepData. That is what an exact-B-rep consumer needs — a tessellation cannot be un-tessellated — and what keeps attributes readable by a table. A consumer that wants triangles calls render()/tessellateScene() itself.
EVERY kind rides here — curve, brep, mesh, point cloud, text, dimension, hatch, section, light, skeleton. Narrowing this to one kind silently drops the rest of the scene out of the frame.
object?
optionalobject?:SceneObject
Typed object for this slice when the model has an objects[] entry. Leaves stay the flat snapshot; this is the scene-stream handle.
Resolved
Resolved<
T> = {key:string;label?:string;value:T; }
Common envelope for every Model.evaluate* result.
Type Parameters
T
T
key
key:
string
label?
optionallabel?:string
value
value:
T
SceneObject
SceneObject =
CurveObject|BrepObject|MeshObject|PointCloudObject|BlockObject|LightObject|SectionObject|HatchObject|SkeletonObject|LinearDimensionObject|RadialDimensionObject|AngularDimensionObject|LeaderObject|TextObject|ImageObject|ViewObject|CameraObject
The evaluated-object union. One name, because a bare Object shadows the TypeScript global and reads ambiguously at every call site.
Seventeen kinds. Every member is { type, instances, data }.
ScenePull
ScenePull = {
frame:RenderFrameSlice[];slices:Map<string,ObjectData[]> |null; }
Fold slices + render frame — ONE SceneNode resolve produces both. A slice holds the root fold's own value, unbaked: frameFromSlices and modelCombinationFromSlices each derive their own tier from it.
frame
frame:
RenderFrameSlice[]
slices
slices:
Map<string,ObjectData[]> |null
Section
Section = {
section: {normal:Vector;point:Point; };size?:number; }
A cutting plane (point + normal). Discriminant: presence of section.
section
Discriminant — presence of this field makes a payload a section plane.
normal
normal:
Vector
point
point:
Point
size?
optionalsize?:number
Display half-size of the plane gizmo (mm); cutting is unbounded.
SectionData
SectionData = {
sections:SectionEntity[]; }
Object-tier section datablock — the bag of cutting planes.
sections
sections:
SectionEntity[]
SectionEntity
SectionEntity =
Section& {attributes?:Record<string,unknown> |null; }
One cutting plane of a section object.
SectionInstance
SectionInstance =
BaseInstance& { }
A section has no surfaces; material is rejected on this kind.
SectionObject
SectionObject =
BaseObject& {data:SectionData;instances:SectionInstance[];type:"section"; }
An object whose data is section planes and nothing else.
A section is a cutting plane, not a body. The cut is an operation ON other content; this kind holds the plane that does the cutting.
SectionView
Space is cut by a plane; the plane gives the axis. Section up is the first world axis not parallel to the normal, projected onto the plane, tried Z then X (puts world X up the sheet on a plan — the common convention is Y / north up).
SkeletonData
SkeletonData = {
bones:BoneEntity[]; }
Object-tier skeleton datablock — the bone hierarchy itself. One object IS one skeleton; two skeletons are two objects. bones order is the skin index space — not an implementation detail. Authored as that order.
bones
bones:
BoneEntity[]
SkeletonInstance
SkeletonInstance =
BaseInstance& { }
A skeleton has no surfaces; material is rejected on this kind.
SkeletonObject
SkeletonObject =
BaseObject& {data:SkeletonData;instances:SkeletonInstance[];type:"skeleton"; }
An object whose data is one bone hierarchy and nothing else.
The skeleton's identity is this object's KEY — there is no name inside the data. faceSkeleton names that key; lookup has to fail loud when the key is not a skeleton.
SkinBinding
SkinBinding = {
inverseBind:number[];skeleton:string; }
A mesh's DOCUMENT-level skin facts (skeletal deformation) — glTF SKIN semantics. skeleton names the SkeletonObject's object key in the same model or component scope. Lookup of that key must resolve to a SkeletonObject or the engine throws. inverseBind is world_rest(B)⁻¹ at the moment THIS mesh was attached — a fact about the bind, not about a bone. Flat 16 × bones.length, column-major. Written by faceSkeleton — never hand-authored.
The per-POINT half (4 influences each) is MeshVertex.skin, not here: one document names one skeleton and one bind, while indices and weights vary per vertex. Holding both halves in one type would restate the per-vertex arrays the vertices already own.
inverseBind
inverseBind:
number[]
skeleton
skeleton:
string
Style
Style = {
background?:string;exposure?:number;name:string;shading?:"shaded"|"wireframe"|"realistic"|"matcap"; }
Resolved styles[] record — the RESOLVED shape evaluateStyles() / collectStyleNodes() produce from an authored style node's expression args, consumed BY NAME via a view's viewStyle step.
background?
optionalbackground?:string
Viewport clear color, any CSS hex ("#1a1a1a").
exposure?
optionalexposure?:number
EV compensation on the 3D viewport's auto-exposure meter, realistic mode only — same as a camera's +/- exposure dial: 1 (default) trusts the meter as-is, 2 doubles the metered brightness, 0.5 halves it.
name
name:
string
shading?
optionalshading?:"shaded"|"wireframe"|"realistic"|"matcap"
Viewport display mode — SSOT applied by Viewport.applyStyle on every host (edit/embed/model). "realistic" switches to PBR tone mapping and is the ONLY mode that materializes emitting light data and the document's settings.environment into real renderer lights. "matcap" is self-lit form-reading (MeshMatcapMaterial). "shaded" is the CAD headlight rig. Absent = host may fill via applyStyle({ defaultShading }) only; never overridden when authored.
StyleCapabilities
StyleCapabilities = {
exposure:boolean;lights:boolean;materials:boolean; }
What a style's shading STYLE can show — the SSOT both the model's validation (which style fields a shading permits — exposure requires "realistic") and packages/three's Viewport read, instead of scattered shading === "realistic" string comparisons. NOT a "view kind" concept — an orthographic (drawing) view referencing a "realistic" style is legal (a lit axonometric in the viewport, Revit precedent); the 2D paper pipeline structurally never reads a style at all, so plans stay immune with no extra rule needed.
exposure
exposure:
boolean
lights
lights:
boolean
Emitting LightData objects and the document's settings.environment materialize into real renderer lights.
materials
materials:
boolean
MeshData materials render (their own color/texture) vs. flat line work only.
Sun
Sun = {
direction:Vector;kelvin?:number;lux:number; }
The sun as a lit direction — COMPUTED from Environment.site + moment, never stored. Absent from a ResolvedEnvironment whenever either input is missing; present with direction[2] < 0 when the site's sun is below the horizon at that moment, which is a night study, not an error.
direction
direction:
Vector
Unit vector TOWARD the sun, Z-up world space.
kelvin?
optionalkelvin?:number
Colour temperature (K).
lux
lux:
number
Direct illuminance (lux) at the model.
Surface
Surface = {
type:"plane"; } &PlaneSurface| {type:"revolve"; } &RevolveSurface| {formAbsent?:boolean;type:"nurbs";uPeriodic?:boolean;vPeriodic?:boolean; } &NurbsSurface
The document's surface carrier: the three kinds of the PlaneSurface / RevolveSurface / NurbsSurface union (geometry/surface.ts) wearing their type tags, plus carrier-only fields on the nurbs arm that are document concerns rather than geometry.
Table
Table = {
columns:ColumnMeta[];rows:Record<string,unknown>[]; }
columns
columns:
ColumnMeta[]
rows
rows:
Record<string,unknown>[]
Text
Text = {
normal:Vector;point:Point;text:string;xAxis:Vector; }
A string placed and ORIENTED in space.
normal + xAxis make it self-determining in 3D: the plane it is written on and the reading direction inside that plane.
Font, height and alignment live on the named TextStyle.
normal
normal:
Vector
point
point:
Point
text
text:
string
xAxis
xAxis:
Vector
TextData
TextData = {
texts:TextEntity[]; }
Object-tier text datablock.
texts
texts:
TextEntity[]
TextEntity
TextEntity =
Text& {attributes?:Record<string,unknown>;style:string; }
One string in a TextData bag. style names a textStyles record (font, height, width factor, oblique) — the text itself carries none of that.
TextInstance
TextInstance =
BaseInstance& {material?:string; }
TextObject
TextObject =
BaseObject& {data:TextData;instances:TextInstance[];type:"text"; }
An object whose data is texts and nothing else.
A drawn string is not a dimension (no measured value) and not a leader (no pointer).
TextStyle
TextStyle = {
font?:string;height:number;name:string;oblique?:number;widthFactor?:number; }
Produced by collectTextStyleNodes() from an authored textStyle node's expression args; consumed BY NAME (identity: "name", matching materials / layers / styles).
font?
optionalfont?:string
A url-method parameter key naming an attached font file (the asset mechanism assetTypeOf already resolves), OR a family name the reader looks up itself. Absent = the reader's own default face.
height
height:
number
Cap height in MODEL units — the size a drafter states.
name
name:
string
oblique?
optionaloblique?:number
Slant in degrees, 0 = upright (DXF group code 50).
widthFactor?
optionalwidthFactor?:number
Horizontal stretch, 1 = natural (DXF group code 41).
TextureData
TextureData = {
data:Uint8ClampedArray;filter?:"linear"|"nearest";height:number;tileHeight?:number;tileWidth?:number;uvSet?:0|1;width:number;wrap?:"repeat"|"clamp"|"mirror"; }
Resolved texture pixels + sampling metadata. The texture-chain ENGINE (which PRODUCES these) stays in the model package — this is the inert output bag a pixel consumer takes, and a Material alone cannot carry it (its texture / normalTexture / … fields are URI strings).
data
data:
Uint8ClampedArray
filter?
optionalfilter?:"linear"|"nearest"
Sampler filter from the chain's declared filter builtin. Absent = linear.
height
height:
number
tileHeight?
optionaltileHeight?:number
tileWidth?
optionaltileWidth?:number
Physical mm of one tile — renderers/exporters prefer this over material tile*.
uvSet?
optionaluvSet?:0|1
Which UV set this texture samples (0 = TEXCOORD_0, 1 = TEXCOORD_1). Declared on the CHAIN, not per material slot.
width
width:
number
wrap?
optionalwrap?:"repeat"|"clamp"|"mirror"
Sampler wrap mode from the chain's declared wrap builtin. Absent = repeat.
Track
Track = {
interpolation?:"LINEAR"|"STEP";key:string;times:number[];values:number[][]; }
One keyframe track — pure value-over-time data, nothing else. key is unique across the WHOLE model (not just within its enclosing AnimationClip, if any) — a reference to a track never needs to know which clip (if any) it lives in.
values[i] is the value at times[i] — an array whose OWN length is however many numbers that keyframe carries (3 for a position, 4 for a rotation quaternion, N for a mesh's whole morph-weights array at once — glTF packs every morph target of one node into a single weights channel, so one track already covers all of them).
interpolation?
optionalinterpolation?:"LINEAR"|"STEP"
LINEAR when absent. STEP holds the previous key's value.
key
key:
string
times
times:
number[]
values
values:
number[][]
Transformation
Transformation =
number[]
A flat 4×4 affine matrix, 16 numbers.
Major order depends on the USE and the two are NOT interchangeable, so every field carrying this type states its own convention: placement transforms are COLUMN-major (Three.Matrix4 / SketchUp Geom::Transformation, translation at 12,13,14), while a face's uvMatrix is ROW-major (plane→world frame, origin at 3,7,11).
Vector
Vector = [
number,number,number]
Free 3D vector (direction / axis). Same layout as Point; field role differs.
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?
optionallabel?:string
operations
operations:
Operation[]
ViewBase
ViewBase = {
filter?:ViewFilterRule[];key:string;label?:string;style?:string; }
Runtime value contract — what evaluation produces (data stream, resolved records).
THE VOCABULARY ITSELF LIVES IN @huukhanhnguyen/types. This file re-exports ObjectData + the geometry-shape guards, the resolved asset records (material/layer/font/image), skeleton/animation, emission/camera, style description, and the declarative sheets/layouts/saved-views vocabulary so every consumer keeps importing through the @huukhanhnguyen/model barrel (src/index.ts export * from "./graph/index.js").
What stays in this package: the authoring node lane, the expression/evaluate engines, and the evaluated LANE shapes in the sibling modules re-exported at the bottom of this file (table/texture/view/sheet/ component/frame/spaces).
Import law: nothing outside graph/ — never product packages or apps/ (any other npm package is fine, per the graph-isolation boundary rule).
filter?
optionalfilter?:ViewFilterRule[]
key
key:
string
label?
optionallabel?:string
style?
optionalstyle?:string
ViewData
ViewData = {
crop?:BoundingBox2d;scale:number|"auto";source:string; }
What a ViewObject places: which saved view, at what scale, optionally cropped. Not the drawing itself — that is re-derived on demand and never stored. Placement itself is instances.
crop?
optionalcrop?:BoundingBox2d
Fractions of the uncropped drawing. Omit = full drawing.
scale
scale:
number|"auto"
Drawing scale — a number (0.02 = 1) or "auto".
source
source:
string
Key of a views[] entry.
ViewInstance
ViewInstance =
BaseInstance& { }
A view placement has no surfaces; material is not on this kind.
ViewObject
ViewObject =
BaseObject& {data:ViewData;instances:ViewInstance[];type:"view"; }
Placement of a saved view on paper — the object tier of ARCHITECTURE.md's View section. Two-tier: data is a REFERENCE (a views[] key), so there is no document tier. Still carries instances — no kind is structurally singular.
annotationPoints
Re-exports annotationPoints
asSheetSpaceData
Re-exports asSheetSpaceData
asViewSpaceData
Re-exports asViewSpaceData
asWorldSpaceData
Re-exports asWorldSpaceData
dataTypeOf
Re-exports dataTypeOf
draftingTextOf
Re-exports draftingTextOf
indexByName
Re-exports indexByName
isAngularDimension
Re-exports isAngularDimension
isAnnotation
Re-exports isAnnotation
isBlock
Re-exports isBlock
isCameraJSON
Re-exports isCameraJSON
isCameraPoseJSON
Re-exports isCameraPoseJSON
isCurve
Re-exports isCurve
isDimension
Re-exports isDimension
isFlatViewJSON
Re-exports isFlatViewJSON
isHatch
Re-exports isHatch
isImage
Re-exports isImage
isLeader
Re-exports isLeader
isLight
Re-exports isLight
isLinearDimension
Re-exports isLinearDimension
isMesh
Re-exports isMesh
isPoint
Re-exports isPoint
isPointCloud
Re-exports isPointCloud
isPolyCurve
Re-exports isPolyCurve
isProjectionViewJSON
Re-exports isProjectionViewJSON
isRadialDimension
Re-exports isRadialDimension
isSection
Re-exports isSection
isSectionViewJSON
Re-exports isSectionViewJSON
isShell
Re-exports isShell
isSkeleton
Re-exports isSkeleton
isText
Re-exports isText
isViewFilterOverride
Re-exports isViewFilterOverride
isViewJSON
Re-exports isViewJSON
kindOfData
Re-exports kindOfData
mapAnnotationGeometry
Re-exports mapAnnotationGeometry
SheetSpaceData
Re-exports SheetSpaceData
STYLE_CAPABILITIES
Re-exports STYLE_CAPABILITIES
ViewSpaceData
Re-exports ViewSpaceData
WorldSpaceData
Re-exports WorldSpaceData