@huukhanhnguyen/types / objects/mesh
objects/mesh
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.
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[]
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