Import these from @grafloria/renderer.
Classes
HtmlHostCuller
The per-frame cull decision for HTML-layer node hosts.
Stateless with respect to the hosts themselves: admits() is told whether the host is
currently attached rather than remembering it. That is on purpose — a culler holding its
own attached-set is a second copy of a fact the DOM already owns, and the two desync the
first time a node is removed from the model mid-gesture. The DOM is the record; this is
only the policy.
culler.beginFrame(viewport.getViewBox(), viewport.getZoom(), heldByGesture); for (const node of customNodes) { if (culler.admits(node.id, bounds(node), host?.isConnected ?? false)) … }
tsclass HtmlHostCuller
Methods
constructor(options: HostCullOptions = {}, freeze: FreezeQuery | null = null)getMode(): HostCullModebeginFrame(visible: Rectangle, zoom: number, exempt: ReadonlySet<string>): void— Fix this frame's two rects and the set of nodes a live gesture owns.
visible is the WORLD rect actually on screen — the viewBox, not the raw camera rect. They diverge the moment zoom != 1, and culling against the camera rect drops hosts that
are on screen whenever the board is zoomed out (which fit-to-content always does). The
SVG side learned this the hard way; see svg-renderer.ts visibleRect.
admits(id: string, bounds: Rectangle, attached: boolean): boolean— Should this node's host be in the document on this frame?
ProgressiveMounter
tsclass ProgressiveMounter
Methods
constructor( engine: DiagramEngine, lifecycle: ViewLifecycle, frame: MountFrame, deferred: DeferredQuery )isRunning(): booleanmount(viewport: Rectangle, zoom: number, options: ProgressiveMountOptions = {}): Promise<MountStats>— Bring the scene up in rAF-yielded slices. Resolves when everything culling admits has a view (or when the mount is cancelled).cancel(): void— Stop mounting. Whatever is not yet mounted is mounted by the next normal render — the gate is handed back here, so a cancelled mount can never strand half a diagram on screen.dispose(): void
ViewLifecycle
tsclass ViewLifecycle implements MountGate
Methods
constructor(options: ViewLifecycleOptions = {})freeze(kind: EntityKind, id: string): void— Give up this entity's view. It keeps its model and its spatial-index entry; it stops being drawn and stops costing anything per frame.unfreeze(kind: EntityKind, id: string): void— Give it a view again. It is rebuilt on the next frame that can see it.isFrozen(kind: EntityKind, id: string): booleanisExplicitlyFrozen(kind: EntityKind, id: string): boolean— Explicitly frozen only — NOT the ones autoFreeze is holding off-screen.unfreezeAll(): voidsetAutoFreeze(on: boolean): voidisAutoFreeze(): booleanretainedCount(): number— The views currently retained — i.e. what the renderer is paying for.
This is the number autoFreeze exists to bound, so it is measurable rather than asserted.
frozenCount(): numberretainVisible(visible: ReadonlyArray<readonly [EntityKind, string]>): void— Called by the renderer each frame with what culling admitted, BEFORE the gate is applied. Anything that was on screen and no longer is gets its view dropped.
Not called at all when autoFreeze is off — the whole feature is one Set diff per frame, and a host that has not asked for it pays nothing.
beginDeferred(): void— Defer EVERYTHING. Nothing has a view untiladmit()says so.admit(kind: EntityKind, id: string): void— Let this entity's view be built from now on.admitAll(kind: EntityKind): void— Admit a whole KIND without naming its members. Slice 0 uses this for nodes: a node's view is cheap (no routing), and enumerating 10k ids to admit them one at a time would cost more than building the ~56 views culling actually keeps.endDeferred(): void— The mount is over (finished, cancelled, or pre-empted). Gate opens fully.admits(kind: EntityKind, id: string): boolean— May the renderer build (or refresh) this entity's view on this frame?isDeferring(): boolean— True while a progressive mount is running — the renderer's cue that the scene is being brought up in slices and is not yet whole.setChangeHook(hook: (() => void) | null): void— Tell the renderer that what it would draw has changed, even though the model has not. Set bySVGRenderer.setViewLifecycle; seechangeHook.
Interfaces
FreezeQuery
The sliver of ViewLifecycle that host culling honours.
isExplicitlyFrozen, NOT admits, and the distinction is load-bearing. admits is also
false for anything autoFreeze is holding off-screen, and autoFreeze decides that against
the bare viewport with no margin — routing custom hosts through it would silently
override the hysteresis band with a zero-width one. isExplicitlyFrozen is the signal
that a HOST deliberately said "this node has no view", which is a decision custom nodes
should obey.
tsinterface FreezeQuery
Members
isExplicitlyFrozen(kind: EntityKind, id: string): boolean
HostCullOptions
tsinterface HostCullOptions
Properties
| Name | Type | Default | Description |
|---|---|---|---|
margin? | number | How far beyond the viewport edge a host is still kept mounted, in CSS pixels. | |
hysteresis? | number | The extra distance, in CSS pixels, a host must travel BEYOND margin before it is culled. This is the hysteresis band, and it is the difference between culling and thrashing. | |
mode? | HostCullMode | What a cull does to the element. Default 'detach' — see {@link HostCullMode}. |
MountGate
The gate the renderer consults before instantiating an entity's VIEW.
A gate can only ever SUBTRACT from what culling already admitted — it never adds an off-screen entity back in. That asymmetry is deliberate: a gate bug can make something arrive late, never wrong.
tsinterface MountGate
Members
admits(kind: EntityKind, id: string): boolean— May the renderer build (or refresh) this entity's view on this frame?isDeferring?(): boolean— True while a progressive mount is running — the renderer's cue that the scene is being brought up in slices and is not yet whole.
MountStats
What one mount() actually did — the numbers the claim lives on.
tsinterface MountStats
Properties
| Name | Type | Default | Description |
|---|---|---|---|
firstPaintMs | number | ms from mount() to the first frame that reached the screen. | |
completeMs | number | Wall clock from mount() to the last entity mounted — INCLUDING the rAF waits. | |
cpuMs | number | CPU actually spent, summed over the slices — i.e. completeMs minus the time spent yielded to the browser. | |
slices | number | rAF slices used (1 = it all fitted in the first frame). | |
nodesMounted | number | Entities whose views were built. | |
linksMounted | number | ||
worstSliceMs | number | The worst single slice — the jank a user would actually feel. | |
aborted | boolean | True if the mount was cancelled or pre-empted by a model change. |
ProgressiveMountOptions
tsinterface ProgressiveMountOptions
Properties
| Name | Type | Default | Description |
|---|---|---|---|
sliceMs? | number | Target ms per slice. The chunk size adapts to hit it. Default 8 (half a 60fps frame). | |
initialChunk? | number | Links admitted by the FIRST link slice — the one slice with no measurement to adapt from. Default 4, deliberately timid: link costs are wildly skewed (a typical link routes in 3ms, a long one against 10k obstacles in 850ms), so a big opening chunk is a coin-flip on a multi-second stall. It ramps up fast from here when links are cheap. | |
maxSlices? | number | Hard cap on slices, so a pathological scene still terminates. Default 500. | |
onFirstPaint? | (stats: Readonly<MountStats>) => void | ||
onSlice? | (stats: Readonly<MountStats>) => void |
ViewLifecycleOptions
tsinterface ViewLifecycleOptions
Properties
| Name | Type | Default | Description |
|---|---|---|---|
autoFreeze? | boolean | Drop the view of any entity that leaves the viewport. Default false — the historical behaviour (views linger in the LRU until evicted by pressure). |
Types
DeferredQuery
What the renderer deferred on the last frame — culling admitted it, the gate did not.
tstype DeferredQuery = () => ReadonlyArray<readonly [EntityKind, string]>;
EntityKind
Also has every member of String, listed on its own entry.
The two things that have views.
tstype EntityKind = 'node' | 'link';
HostCullMode
Also has every member of String, listed on its own entry.
What a cull does to the host element.
'detach' — remove the element from the document and keep the reference. Re-entry
re-appends the SAME element: renderCustomNode is NOT called again, removeCustomNode
is NOT called on the cull, and everything inside the widget survives byte for byte.
'destroy' — fire removeCustomNode and drop the element. Re-entry re-creates the host
and re-runs renderCustomNode. Frees the widget's memory; costs a full re-init and every
piece of state the widget was holding.
'detach' is the default, and the reasoning is that the cost this feature exists to
remove is a cost of being ATTACHED. Layout, style recalc, paint, compositing and hit
testing are all charged per element IN THE DOCUMENT; a detached subtree is inert heap. Detaching therefore captures essentially the whole win while keeping the mount-once
guarantee that makes custom nodes usable at all. Measured on a 300-widget board with ~16
tiles on screen: 2720 DOM nodes under the canvas fall to 164, a 94% reduction, and a pan
away and back re-runs the painter exactly zero times.
What 'detach' does NOT bound is the RETAINED set. A host mounted once is kept for the
life of the instance, so panning a 10,000-widget board end to end eventually holds 10,000
detached elements — the same "cache versus leak" distinction ViewLifecycle's header
draws about autoFreeze, and the honest reason 'destroy' exists. (Same board, after a
sweep across and back: 20 attached, 78 retained off-screen.) Choose 'destroy' when the
widgets are individually huge — a WebGL scene, a 50k-row grid — and heap rather than
frame time is the binding constraint. It is opt-in inside an opt-in, because a host that
asks for it is accepting that its painter re-runs and its widget state is lost.
tstype HostCullMode = 'detach' | 'destroy';
MountFrame
Produce and PRESENT one frame. Deliberately not "the SVG renderer": the SVG patcher, the canvas backend and the tier-switching backend all satisfy this, so a progressive mount works on any of them.
tstype MountFrame = (viewport: Rectangle, zoom: number) => void;
Was this page helpful?