Model - Render diff
API reference for src/core/renderDiff, src/schema/renderDiff on @huukhanhnguyen/model, reachable from ..
See the Guide for the ModelJSON / evaluate pipeline.
API reference
Signatures are generated from the live package .d.ts - not hand-written.
contentKey
function
Geometry content key. For multi-face wraps, compose FNV over per-face serializations (not one whole-brep stringify) so editing one face of a body does not re-key the entire interned table blob budget-unfriendly.
contentKey(entity: Entity): stringdiffEntityFrame
function
Multiset reconcile of the previous frame's cells against the next frame's entities. Order-independent: cells bucket by content key; each next entity pops a matching cell (kept/updated) or lands in added; unclaimed cells are removed. O(previous + next).
diffEntityFrame(previous: RenderCell<T>[], next: Entity[]): FrameDiff<T>fireFrameHooks
function
Fire the crossing-into-the-scene points over one frame transition. The diff already computed the transition — added IS a mount, updated IS a props update, removed IS an unmount, kept is nothing at all — so this only brackets it; see SceneHooks (schema/renderDiff.ts) for what the six points mean, why kept has none, and why there is no insert.
PAIRING IS THE LAW, the same law the node tier states: every before gets exactly one after, including when a handler throws. A throwing handler also may not blank the rest of the frame — observers are per entity, one bad one must not stop the others — but it is not swallowed either: every error is collected and rethrown as one AggregateError AFTER the whole frame has fired, by which point the caller already holds the diff it passed in.
fireFrameHooks(host: HookHost, diff: FrameDiff<T>): voidFrameDiff
type
type FrameDiff
// = {
/** Same geometry, same props — host keeps the object untouched. */
kept: {
cell: RenderCell<T>;
entity: Entity;
}[];
/** Same geometry, different props — host updates material/layer/name only. */
updated: {
cell: RenderCell<T>;
entity: Entity;
}[];
/** New geometry — host builds (tessellates) a fresh object. */
added: Entity[];
/** Gone — host removes + disposes. */
removed: RenderCell<T>[];
}RenderCell
type
One mounted render object in the previous frame: its content key, the props it was rendered with, and the host payload (a Three Object3D, SU entity ids, …) the host needs back on keep/update/remove.
type RenderCell
// = {
key: string;
/** The entity this cell was mounted FROM. The diff itself only needs
* `key` + the props, but an unmount hook is handed an ENTITY like every
* other scene point (see SceneHooks) — so the cell keeps the one it was
* built from, and the previous frame's entities stay reachable for
* exactly one frame. */
entity: Entity;
material?: string;
layer?: string;
name?: string;
data: T;
}SceneHooks
type
The moment a node's output CROSSES INTO THE SCENE, observed per entity over a frame transition (renderDiff.ts's fireFrameHooks).
These are NODE-SIDE points, not events of a renderer. The engine says "this entity entered / changed / left"; what a host does about it is the host's business. So do NOT complete the analogy with three.js — nothing here is shaped like onBeforeRender / onAfterRender / onBeforeShadow / onBeforeCompile, because those belong to ONE renderer and this kernel has to run in a viewer, an editor, headless, or under a renderer nobody has written yet.
Different words from the node tier (NodeHooks' beforeRemove/afterRemove) on purpose: a node leaving the GRAPH and an entity leaving the SCENE are different events, and one node can mount and unmount many times without ever being removed.
THREE PAIRS, one per transition the diff actually computes: added → beforeMount / afterMount props-updated → beforeUpdate / afterUpdate removed → beforeUnmount / afterUnmount kept → NO point at all kept gets nothing because nothing happened — the same test that keeps a cache point out of the node tier: a moment with no value gets no name. props-updated is its own transition and not a rebuild: contentKey excludes props deliberately, so "same shape, new paint" arrives here (with the NEW entity) instead of as an unmount+mount pair.
There is deliberately NO insert point. domphy separates Insert from Mount because a DOM subtree can be inserted while still detached — "in the tree" and "live" are two moments there. Here the frame IS the scene: an entity is in it or it is not, so mount is ONE moment and a second point would name nothing.
A hook OBSERVES — never DECIDES, exactly as at the node tier: the return value is discarded (callHook), so a handler cannot veto a mount or rewrite what the diff decided.
type SceneHooks
// = {
beforeMount?: (entity: Entity) => void;
afterMount?: (entity: Entity) => void;
/** Same geometry, new material/layer/name — handed the NEW entity. */
beforeUpdate?: (entity: Entity) => void;
afterUpdate?: (entity: Entity) => void;
beforeUnmount?: (entity: Entity) => void;
afterUnmount?: (entity: Entity) => void;
}toRenderCell
function
Build the RenderCell for an entity a host just mounted.
toRenderCell(entity: Entity, key: string, data: T): RenderCell<T>