Skip to content
Shapemetry

@huukhanhnguyen/model / schema/nodes

schema/nodes

ArgumentNodeJSON

ArgumentNodeJSON = ExpressionNodeJSON & { argName: string; }


ChildNode

ChildNode = DataNodeHost | ContainerNodeHost | ParameterNodeHost | ExpressionNodeHost | BaseNodeHost


DirtyTarget

DirtyTarget = { _dependents?: Iterable<DirtyTarget>; _epoch?: number; _hooks?: NodeHooks; isDirty?: boolean; parent?: DirtyTarget; }

_dependents?

optional _dependents?: Iterable<DirtyTarget>

_epoch?

optional _epoch?: number

_hooks?

optional _hooks?: NodeHooks

isDirty?

optional isDirty?: boolean

parent?

optional parent?: DirtyTarget


EntryHeader

EntryHeader = Record<string, unknown> & { key: string; }

What a lane entry exposes without evaluating anything: its identity plus the plain header fields an index/schedule column can print.


EvaluateOutcome

EvaluateOutcome = { value: unknown; } | { error: unknown; } | { superseded: true; }

How an evaluation attempt ended. Discriminated STRUCTURALLY ("error" in outcome) — there is deliberately no type tag, the same convention document/dataKind.ts states for the *Data stream: the kind is sniffed from the SHAPE. error carries the raw thrown value, not the message string BaseNode.error stringifies it into.

superseded is the third way an attempt ends: a newer dirty bumped the node's _epoch while an async compute was in flight, so the settle discards itself (core/maybeAsync.ts, DataNode's step epoch check). It produced no value and no error — but it ENDED, and an observer that started a timer on entry has to be able to stop it.


EvaluateSettle

EvaluateSettle = (outcome, isAsync) => void

Closes one evaluation attempt — see beginEvaluate (nodes/hooks.ts). Optional wherever it is threaded: a caller that did not open an attempt passes nothing, and then no afterEvaluate fires, because there was no beforeEvaluate to pair with.

Parameters

outcome

EvaluateOutcome

isAsync

boolean

Returns

void


ExpressionNodeJSON

ExpressionNodeJSON = BaseObjectJSON & { expression: string; }


HookHost

HookHost = { _hooks?: Hooks; }

Anything carrying a hook bag: a LIVE node, a plain DECLARATION object (an DataDefinition) hooks were attached to before any node existed, or the scene-tier host a renderer fires its frame transitions through.

_hooks?

optional _hooks?: Hooks


Hooks

Hooks = NodeHooks & SceneHooks

Every point a hook bag can hold: the node-tier lifecycle (NodeHooks) plus the scene-tier mount pair (SceneHooks, schema/renderDiff.ts). One bag, two tiers — the key sets are disjoint, and addHook/callHook serve both.


KeyedNode

KeyedNode = BaseNodeHost & { enabled: ArgumentNodeHost | null; key: string; label: ArgumentNodeHost; }

The shared surface both node kinds carry (see DataNode/ContainerNode). label is ALWAYS a live ArgumentNode (never null) — same "always constructed, default expression ''" contract ParameterNode.label uses — so the UI can always bind an editable field to it; hasLabelOf is the content-based check for "was this actually authored".


KeyedNodeEntry

KeyedNodeEntry = { key: string; node: BaseNodeHost; useReferenceValue: boolean; }

One key reachable from an expression context, with the resolution strategy ExpressionNode._buildContext historically applied at that position: a top-level node always unwraps single-cardinality (resolveNodeValue); a nested operation resolves to its raw baked step output (referenceValue) when available, else falls back the same way. See NodeCollection.keyedNodes.

key

key: string

node

node: BaseNodeHost

useReferenceValue

useReferenceValue: boolean


NodeHooks

NodeHooks = { afterDirty?: (node) => void; afterEvaluate?: (node, outcome, isAsync) => void; afterRemove?: (node) => void; beforeDirty?: (node, source) => void; beforeEvaluate?: (node) => void; beforeRemove?: (node) => void; init?: (node) => void; }

Node lifecycle observation points (see nodes/hooks.ts).

A hook OBSERVES — never DECIDES. Its return value is discarded: it cannot veto an evaluate, cannot rewrite the outcome, cannot stop dirty propagation. Everything that decides a node's VALUE stays a subclass override with exactly one owner (compute, postCompute, errorValue on BaseNode); if a composable hook could change a value, two systems would both have a claim on it and neither would be the answer.

PAIRING IS THE LAW: every before gets exactly one after, on every path, with no exception. That is why a FAILED evaluate reports through afterEvaluate carrying { error } rather than through a separate error point, and why an attempt discarded mid-flight still reports { superseded: true } — one attempt, one notification, whichever way it ended. beginEvaluate (nodes/hooks.ts) is what enforces it structurally.

The dirty pair is typed on DirtyTarget, not on a node: the dirty walk traverses an open structural shape and not every hop up the owner chain is a full BaseNode (see BaseNode.ts's markDirty).

init is the exception to the pairing law, and the only one: it has no before because there is no assembled node to hand over yet. It is also the only point with more than one firing site — assembly completes in several places (see BaseNode.finishInit), so it is guarded to fire once rather than pretending one line covers every class.

afterDirty?

optional afterDirty?: (node) => void

"This node is now dirty" — NOT "the cascade is complete". Both dirty points fire inside setDirty, so a markDirty cascade shows up as N pairs, one per node it touched. Firing after once at the end of the walk instead would leave every bare setDirty call (ContainerNode's child listeners) with a before and no after.

Parameters

node

DirtyTarget

Returns

void

afterEvaluate?

optional afterEvaluate?: (node, outcome, isAsync) => void

isAsync distinguishes the deferred settle from the inline one.

Parameters

node

BaseNodeHost

outcome

EvaluateOutcome

isAsync

boolean

Returns

void

afterRemove?

optional afterRemove?: (node) => void

Parameters

node

BaseNodeHost

Returns

void

beforeDirty?

optional beforeDirty?: (node, source) => void

source is the node the dirty walk STARTED from — the same node when something dirtied it directly. The value is CAUSAL: it answers "this node is dirty BECAUSE that node changed".

Parameters

node

DirtyTarget

source

DirtyTarget

Returns

void

beforeEvaluate?

optional beforeEvaluate?: (node) => void

Parameters

node

BaseNodeHost

Returns

void

beforeRemove?

optional beforeRemove?: (node) => void

Parameters

node

BaseNodeHost

Returns

void

init?

optional init?: (node) => void

The node has finished being ASSEMBLED — it has its full structure and its children. NOT "the constructor returned": for DataNode, ContainerNode and ParameterNode the constructor leaves assembly unfinished (args pushed, children attached, constraints applied afterwards), so each assembly path calls finishInit() at the point wiring is genuinely done. Fires at most once per node.

No before counterpart, and this is the one point that cannot have one: there is no assembled node to pass.

Parameters

node

BaseNodeHost

Returns

void


ObjectChannel

ObjectChannel = "content" | "transform" | "props"

Which half of an object moved — a renderer re-tessellates for "content", writes one matrix for "transform", repaints for "props".


ObjectChannelListener

ObjectChannelListener = (key, channel) => void

Parameters

key

string

channel

ObjectChannel

Returns

void


ObjectInstance

ObjectInstance = { label?: string; layer?: string; material?: string; transformation: Transformation | null; }

The lifted placement of one scene object — what its whole content sits under, compiled from the object chain's TAIL instead of baked into geometry.

transformation is the product of the trailing move/rotate steps (null when the tail has none). The props are PLACEMENT DEFAULTS: the consumer applies each to a content document that carries none of its own — a document stamped inside the body always wins.

label?

optional label?: string

layer?

optional layer?: string

material?

optional material?: string

transformation

transformation: Transformation | null


ObjectSignature

ObjectSignature = { content: string; props: string; transform: string; }

The three comparable halves of a resolved object — Model diffs these to pick a channel, and skips the emit entirely when all three match.

content

content: string

props

props: string

transform

transform: string


OptionNodeJSON

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

input

input: string

label

label: string


ParameterNodeJSON

ParameterNodeJSON = BaseObjectJSON & { animation?: AnimationBinding; args?: Argument[]; flat?: boolean; key: string; label?: string; method: string; }

Last updated: 📖 8 min readEdit on GitHub