ParaShape

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)GuardShapeTS type
curveEntityisCurvegeometry = NurbsCurveJSON (has controlPoints)CurveJSON
faceEntityisFacegeometry = { loops, uvMatrix? } (has loops)FaceJSON
pointEntityisPointgeometry = [x, y, z] (a bare array)PointJSON
groupEntityisGroupdefinition 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
    | GroupEntity

Geometry-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"
}