Entities
A scene entity is `{ id, geometry, visible?, layer?, material?, label?, sourceKey? }` with NO `type` tag — its KIND is sniffed from the SHAPE of the entity. Every generator node returns an array of entities; a solid is a Face[] (a box = 6 faces).
Source: packages/parametric/src/schema/graph.ts
The 4 entity kinds
| Kind (namespace) | Guard | Shape | TS type |
|---|---|---|---|
curveEntity | isCurve | geometry = NurbsCurveJSON (has controlPoints) | CurveJSON |
faceEntity | isFace | geometry = { loops, uvMatrix? } (has loops) | FaceJSON |
pointEntity | isPoint | geometry = [x, y, z] (a bare array) | PointJSON |
groupEntity | isGroup | definition id + transform (places a GroupDefinition) | GroupEntity |
Common meta fields (EntityBase) on every entity: `id` (required), plus optional `visible`, `layer`, `material`, `label`, `sourceKey` (key of the container child that produced it). Groups follow SketchUp's component model: a GroupDefinition holds the shared child `entities` ONCE (stored in the model's definitions map, keyed by id) and is NOT a scene entity; a groupEntity is ONE placement of a definition — the scene entity — carrying its `definition` id + a transform + per-instance material/layer. Placing a definition N times = N groupEntity, one shared definition. This is the DEFAULT value of every generator node (a pull-based reference preserves the placements); add a `groupExplode` modifier to bake a node's placements flat.
JSON examples
curveEntity
{
"id": "c1",
"material": "Steel",
"layer": "Frame",
"geometry": { "degree": 1, "controlPoints": [[0,0,0],[100,0,0],[100,50,0]], "knots": [0,0,0.5,1,1], "weights": [1,1,1] }
}faceEntity
{
"id": "f1",
"material": "Oak",
"layer": "Carcass",
"geometry": { "loops": [[[0,0,0],[100,0,0],[100,50,0],[0,50,0]]], "uvMatrix": [1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1] }
}pointEntity
{
"id": "p1",
"layer": "Markers",
"geometry": [10, 20, 0]
}groupEntity — one placement of a definition
{
"id": "g1",
"definition": "cabinet",
"transformation": [1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1],
"material": "Oak",
"layer": "Carcass"
}GroupDefinition — shared content (in the model definitions map, NOT a scene entity)
{
"id": "cabinet",
"entities": [ /* CurveJSON | FaceJSON | PointJSON | GroupEntity */ ]
}TypeScript contract
Common entity base fields
//
// An entity is `{ geometry, …meta }` — there is NO `type` discriminant tag.
// An entity's KIND is sniffed from the SHAPE of its `geometry` field
// (see the guards isPoint / isCurve / isFace below). The entity schema is
// ParaShape's own; shapemetry is only the internal geometry kernel — we reuse
// its JSON shapes freely (NurbsCurveJSON) without coupling to a `type` field.
export type EntityBase = {
id: string
visible?: boolean
layer?: string
material?: string
label?: string
sourceKey?: string
}Geometry data (the entity's `geometry` field)
//
// - point geometry = Point3D = [x,y,z]
// - curve geometry = NurbsCurveJSON (reuse the shapemetry kernel shape)
// - face geometry = FaceGeometry: 3D WORLD loops (outer loop first, then
// holes) + an optional uvMatrix (a row-major 4×4 plane→world
// frame) used for UV and 2D projection. A face is a flat 3D
// polygon.
export type FaceGeometry = {
/** 3D world loops: outer loop first, then holes. Each loop is a polygon. */
loops: Point3D[][]
/** Row-major 4×4 plane→world frame (UV + 2D projection). */
uvMatrix?: Transformation
}Entity JSON shapes (3 scene-entity kinds, discriminated by `geometry`)
export type CurveJSON = EntityBase & {
geometry: NurbsCurveJSON
}
export type FaceJSON = EntityBase & {
geometry: FaceGeometry
}
// Point marker (render-only visualization entity — not a construction atom).
export type PointJSON = EntityBase & {
geometry: Point3D
}Group definition + group instance (SketchUp component model)
//
// The two are DISTINCT, mirroring SketchUp's ComponentDefinition / Instance:
//
// - GroupDefinition = the SHARED content: child entities stored ONCE. It lives in
// the model's runtime `definitions` map (keyed by `id`) and is
// NOT a scene entity itself.
// - groupEntity = ONE placement OF a definition, and the actual SCENE ENTITY
// (in the `EntityJSON` union). It carries `definition` (the id
// of the GroupDefinition it places) + a transform + per-
// instance overrides (material/layer/name/visible). Placing a
// definition N times = N groupEntity, one shared definition.
//
// `Instance` = the placement fields shared by the runtime Instance accumulator and
// a serialized groupEntity (a groupEntity is an Instance plus id + definition).
export type Instance = {
/** Column-major 4×4 placement transform (translation at indices 12,13,14). */
transformation: Transformation
material?: string
layer?: string
name?: string
visible?: boolean
}
export type GroupEntity = Instance & {
id: string
/** Id of the GroupDefinition this instance places (in the model definitions map). */
definition: string
}
export type GroupDefinition = {
id: string
/** Shared child entities (the definition's content), stored ONCE. */
entities: EntityJSON[]
}Entity union
export type EntityJSON =
| CurveJSON
| FaceJSON
| PointJSON
| GroupEntityGeometry-shape guards (TS type predicates)
// The KIND of an entity is its geometry's shape — there is no `type` tag.
// point geometry = an array ([x,y,z])
// curve geometry = an object with controlPoints
// face geometry = an object with points
function geometryOf(e: unknown): unknown {
return e != null && typeof e === "object" ? (e as { geometry?: unknown }).geometry : undefined
}
export function isPoint(e: unknown): e is PointJSON {
return Array.isArray(geometryOf(e))
}
export function isCurve(e: unknown): e is CurveJSON {
const g = geometryOf(e)
return !Array.isArray(g) && typeof g === "object" && g !== null && "controlPoints" in (g as object)
}
export function isFace(e: unknown): e is FaceJSON {
const g = geometryOf(e)
return !Array.isArray(g) && typeof g === "object" && g !== null && "loops" in (g as object)
}
// A groupEntity places a definition: it has a `definition` id (and a transform)
// instead of a `geometry` field. It is the group KIND in the scene-entity union.
export function isGroup(e: unknown): e is GroupEntity {
return e != null && typeof e === "object" && typeof (e as { definition?: unknown }).definition === "string"
}
/** The kind name for code that needs it (dispatch is via the guards above). */
export function entityKind(e: unknown): "curveEntity" | "faceEntity" | "pointEntity" | "groupEntity" {
if (isGroup(e)) return "groupEntity"
if (isPoint(e)) return "pointEntity"
if (isFace(e)) return "faceEntity"
return "curveEntity"
}