Import it from @grafloria/renderer.
tsclass SVGRenderer implements IRenderer
Properties
| Name | Type | Default | Description |
|---|---|---|---|
mode | Renderer mode | ||
capabilities | RendererCapabilities | What this renderer can actually do — so callers can ask instead of assuming. supportsExport is now TRUE (see export() below); hit-testing and text measurement still are not implemented here, and saying so is the point. |
Methods
getRegistry(): DiagramRegistry— Register shapes / styles / markers / link-pipeline stages for THIS renderer alone. The module-levelregisterShape()& friends remain the process-wide registry, and this one shadows it — seeext/diagram-registry.ts.constructor( private engine: DiagramEngine, config: SVGRendererConfig = {}, theme?: Theme )render(viewport: Rectangle, zoom: number): VNode— Render diagram to VNode tree.
THE REGISTRY SCOPE IS ACTIVATED HERE, around the whole pass, and this is the
only place it needs to be for the picture to be right. Every contribution
registry the frame consults — shapes, named styles, link/label templates,
markers, anchors, connection points, connectors — resolves this diagram's
own table first and the process-global one second, including from the read
sites that are pure free functions three frames down (shapeStrategy in
port-layout, resolveNodeStyle in the style cascade) and could not have been
handed an instance without rewriting their signatures. See
ext/registry-scope.ts.
A diagram that contributed nothing activates nothing, so the single-diagram path is byte-identical to what it was.
getQualityState(): { tier: LODLevel; governor?: GovernorState }— The tier actually rendered last frame, and the governor's reasoning for it.
Exposed because an invisible governor is indistinguishable from a bug: if the picture silently simplifies, the only honest thing to do is be able to say WHY. The perf HUD reads this; so can an application that wants to tell the user "this diagram is being drawn at reduced detail to stay responsive".
invalidateFrame(): void— Drop the cached frame. Call from anything whose effect on the picture the mutation epoch cannot see — a topology event, a style/theme invalidation, a registry swap. Cheap and idempotent: the cost of calling it when you did not need to is one rebuilt frame; the cost of NOT calling it when you did is a stale picture, so when in doubt, call it.getInvalidationEpoch(): number— How many times this renderer has been told "the picture you have is no longer the picture you would draw".
A HOST'S idle-skip must consult this, not just the model's mutation epoch. The two are not the same, and the gap between them is a real bug: when the
off-thread route solver answers, the MODEL has not changed — the epoch does
not move — but the renderer's own answer about it has improved. A scheduler
keyed only on the model would drop that repaint before render() was ever
called, and the refined routes would never reach the screen. The renderer's
internal gate cannot save you there; it never gets asked.
So: model epoch says "did the world change", this says "did MY picture of it change". Skip a frame only when both say no.
getFrameCoverage(): FrameCoverage | null— The {@link FrameCoverage} of the most recentrender()pass.
CAPTURE IT IMMEDIATELY after the render() call whose frame you patched into
the DOM — do not hold the renderer and ask later. Exports run through the
same render pass with their own viewport (see exportSvg callers), so this
field describes whatever rendered LAST, which is not necessarily what is on
screen. createDiagram's paint() takes its own copy for exactly this
reason.
getFrameStats(): { built: number; skipped: number }— The incrementality is only real if these move.framesSkippedcounts frames served from the previous root (zero DOM work);framesBuiltcounts frames actually walked. An idle canvas should build ONE.getTheme(): Theme— Get current themesetHighlightConnected(value: boolean | HighlightConnectedOptions | undefined): void— SwitchhighlightConnectedlive:falseturns it off,truetakes the defaults, an object tunes it. The next frame redraws every line.getHighlightConnected(): boolean | HighlightConnectedOptionsgetLineOverlay(): VNode | null— The lifted lines, as an<svg>in WORLD coordinates for the overlay the instance keeps at the end of the HTML layer — above SVG nodes and HTML custom nodes alike, moved by the same camera transform.nullwhenhighlightConnectedis off; an empty<svg>when nothing is lifted.setTheme(theme: Theme): voidapplyThemeVariables(theme: Theme): void— THE HOT-SWAP. Re-theme by rewriting this instance's--grafloria-*variables.
In CSS mode every value the built-in stylesheet paints resolves through those
variables, and every themeRef-bound property that lands in an inline CSS
style string is emitted as var(--grafloria-…). So for all of them, this one
string write IS the re-theme: no VNode is rebuilt, no element is touched,
the browser simply recomputes.
What it cannot cover, it does not pretend to: the entities recorded in
themeBoundNodes / themeBoundLinks (see the field docs) baked a theme
literal into their VNode, so they — and only they — are marked dirty and
re-resolve on the next frame. An idle diagram re-themes with zero restyles;
a diagram with three selected nodes restyles three nodes.
Programmatic (Canvas) mode has no stylesheet at all, so there is nothing to rebind and the full invalidation is the only correct answer.
getColorMode(): ColorMode | undefined— The colorMode in force, or undefined when the host never asked for one.setColorMode(mode: ColorMode, themes?: ThemeSet): void— Switch colour mode at runtime.'system'starts following the OS; the other two pin it. Creates the media-query subscription on first use, so a host can opt in after construction.setTokenBridge(bridge: TokenBridge | null | undefined): void— Point this diagram's variables at the host design system's tokens (shadcnBridge(),muiBridge(),tailwindBridge(), or a hand-written map).nullremoves the bridge.
Pure CSS: the values are the host's own var(--…) expressions, so when the
host app flips ITS theme, this diagram follows with no code at all — and no
VNode is rebuilt here either, for exactly the reason the hot-swap works.
getTokenBridge(): TokenBridge | undefined— The bridge currently applied, if any.getInstanceId(): string— This renderer's instance id (grafloria-3). It is the value of thedata-grafloria-instanceattribute on the root<svg>, the scope of this diagram's CSS variables, and the suffix of its<style>element id.getStyleElementId(): string— Id of the<style>element holding THIS renderer's theme variables.getOverrideElementId(): string— Id of the<style>element holding THIS renderer's bridge + a11y overrides.getStyleSheet(): string— The complete stylesheet this renderer would inject into<head>: the shared theme-independent rules, the animation rules, and THIS instance's--grafloria-*variable block.
Needed by the SSR path. In CSS mode the theme lives entirely in CSS
variables, so the emitted SVG is theme-INDEPENDENT — which is exactly what
makes hydration cheap, but it also means a server-rendered diagram is
unstyled until some stylesheet arrives. renderToStaticSVG() returns this
string so the server can ship it in a <style> tag; the client renderer then
re-injects byte-identical content under the same ids, so hydration is still a
no-op visually.
applyInstanceScope(element: Element | null | undefined): void— Put this diagram's scope on a host element.
The root <svg> carries it automatically, and foreignObject content
inherits from it (it lives inside the SVG). Nodes rendered on an HTML LAYER
(metadata.useHTMLLayer) do NOT — they are siblings of the SVG. Call this
with the element that wraps BOTH layers (the canvas host) so those nodes
inherit the --grafloria-* variables and match the scoped rules too.
setViewLifecycle(lifecycle: ViewLifecycle | null): void— Install the freeze / lazy-mount gate.
With no lifecycle installed the renderer behaves exactly as it always has — every entity culling admits gets a view, on the frame it is admitted. That is the default, and it stays the default: laziness is something a host asks for.
getViewLifecycle(): ViewLifecycle | nullgetDeferredEntities(): ReadonlyArray<readonly [LazyEntityKind, string]>— What culling admitted on the last frame and the gate held back.
This is the progressive mounter's work queue, and it comes from the renderer rather than being recomputed by the mounter because the viewport→viewBox→cull maths (zoom, link margin) lives here and must not be reimplemented anywhere it could drift.
-
getThemeBoundEntityCount(): number— How many entities the NEXT theme swap would have to restyle. Zero means the swap is a pure variable rebind. Exposed because "no restyle of every VNode" is a claim that should be measurable, not taken on trust — the tests assert on it, and so can a host. -
getPerformanceMetrics(): PerformanceMetrics— Get performance metrics -
async export(format: ExportFormat = 'svg', options: ExportOptions = {}): Promise<string>— Export the diagram. -
'svg'→ a STANDALONE, styles-inlined SVG document. Pure, deterministic, and DOM-free: it runs in plain Node (an SSR pass, a thumbnail worker). -
'png' | 'jpeg' | 'webp'→ adata:URL. Needs an SVG rasterizer: a canvas one is used automatically in a browser/worker; in bare Node you must passoptions.rasterBackend(and you get a clear error if you don't).
Defaults to the whole diagram (content bounds + 20px), not the current viewport — a thumbnail of the visible slice is rarely what anyone means.
exportSvgString(options: ExportOptions = {}): SvgExportResult— The synchronous, fully headless SVG path — whatexport('svg')returns, plus the fidelitywarnings(foreignObject, unresolved theme vars) that the string-onlyIRenderer.exportsignature has nowhere to put.exportPdf(options: ExportOptions = {}): PdfExportResult— A TRUE VECTOR PDF: paths stay paths and text stays text, so it is selectable, searchable and scales without pixelation.
Painted straight from the VNode tree — the same tree the screen gets — so there is no
SVG→DOM→PDF round trip and no second rendering path. See export/pdf/ for the
dependency decision (we do NOT use svg2pdf.js + jsPDF, and why) and for the honest
list of what a base-14-font PDF cannot do.
exportPages(pagination: PaginationOptions, options: ExportOptions = {}): PagedSvgResult— Slice the diagram into pages.
Returns one standalone SVG per page — a tile grid you can print, or lay out as a poster. Breaks are snapped so they do not cut a node in half; see export/pagination.ts.
exportPaginatedPdf(pagination: PaginationOptions, options: ExportOptions = {}): PdfExportResult— A multi-page PDF of a diagram too big for one sheet.
The paginator supplies the page grid; the PDF writer honours each page's clip, so a
break pulled back to spare a node leaves white space rather than half a box.
collectExportImageUrls(options: ExportOptions = {}): string[]— Every EXTERNAL image URL the exported tree will reference — a panel node's avatar/logo/icon (<image href="https://…">painted by the renderer itself), or any registered shape that emits one. Widget (HTML-layer) captures are NOT in this tree; their URLs are collected from the captures by the instance layer.
This is the LOOK-BEFORE-YOU-EXPORT half of the async image pass: await export(…)
calls this first, fetches the URLs through the tiers, and hands the resolved map
back down as {@link ExportOptions.resolvedAssets} for the pure sync substitution. The tree is enumerated from the SAME render() the export will serialize (VNode
caching makes the second call cheap), so what is collected and what is substituted
cannot drift. Synchronous and network-free.
getRoutingStats(): { routed: number; reused: number; cached: number }— How many links this frame actually had to route, and how many were served from the previous frame.
Public because a cache that silently stops hitting is indistinguishable from no cache at all — it just gets slow again, and nothing says so. Tests assert on this; the benchmark harness prints it.
dispose(): void— Dispose renderer and clean up resourcesgetRouteSolverStats(): RouteSolverStats | null— What the off-thread solver has been doing.setAccessibleFocus(target: { type: 'node' | 'link' | 'comment'; id: string } | null): void— Tell the renderer which entity the keyboard controller has focused, so the next frame emitstabindex=0on it (and-1on everything else).
Marks only the OLD and NEW focus targets dirty — moving focus must not invalidate the whole diagram.
getAccessibleFocus(): { type: 'node' | 'link' | 'comment'; id: string } | nullsetCommentSource(source: CommentSource | null): void— Attach (or detach) the source of comment pins.
Installing a source CHANGES THE PICTURE while moving nothing the frame gate watches — not the model epoch, not the viewport. Without the explicit invalidation the gate would keep serving back the last frame, which has no pins in it, forever. This is the trap this wave was warned about, and it is one line.
getCommentSource(): CommentSource | nullgetContainerId(nodeId: string): string | undefined— Get container ID for a node (if it uses foreignObject)isUsingForeignObject(nodeId: string): boolean— Check if a node uses foreignObject renderinggetAnimationService(): AnimationService— The animation service — global animation enable/speed, reduced-motion and battery-saver policy. Public because these are HOST decisions: the service existed with a full config surface and no accessor, so nothing outside this class could e.g. opt out of the battery auto-toggle (respectBatteryStatus: false) — a laptop under 20% silently killed every edge animation with no way back.
Was this page helpful?