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.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.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.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?