Skip to content

Specialized layers

Beyond the plain Layer, insomni ships a handful of purpose-built drawables and pure helpers. Each one is exported from the package barrel and is composed into the same renderer.render([...]) call as ordinary layers (or, for the pure helpers, used to feed it).

Minimap (mirror layer) A corner minimap mirrors the whole scene at small scale and tracks the camera's viewport as you pan and zoom.

mirrorLayer(primary, options?) (class MirrorLayer) re-renders another layer’s packed shapes through a different viewport / clip rect — minimaps, overview panes, picture-in-picture. By default the mirror shares the primary’s pack by reference, so any push to the primary appears in the mirror next frame at zero CPU cost. The mirror is read-only: its push* / clear methods throw — mutate the primary instead.

Pass simplify to trade detail for upload/draw-call savings at overview scale. The mirror then owns a derived pack built by filtering the primary’s draw commands; call mirror.refresh() after mutating the primary to rebuild (it no-ops when the primary’s version is unchanged).

MirrorLayerOptions:

OptionTypeDefaultNotes
clipFrameRectCSS-pixel scissor for the mirror.
simplifyMirrorSimplify|trueLOD filtering. true = a sensible preset.

MirrorSimplify (a bare true means { skipTriangles: true, skipSprites: true }):

FieldTypeDefaultNotes
skipTrianglesbooleanfalseDrop polygon fills / polyline tessellations / filled lines.
skipSpritesbooleanfalseDrop sprite (and MSDF-glyph) commands.
stripStrokesbooleanfalseRender SDF shapes fill-only; outline-only shapes are kept.
outlineBoostnumber1Multiply preserved outline-only stroke widths (readability).
import { createLayer, mirrorLayer } from "insomni";
const main = createLayer();
// … push content into `main` …
const minimap = mirrorLayer(main, {
clip: { x: 0, y: 0, width: 160, height: 120 },
simplify: { skipTriangles: true, stripStrokes: true },
});
renderer.render([main, minimap]);

createNavigator(options) builds a minimap/overview with an indicator rectangle that tracks the source camera’s visible region, plus optional drag/click interaction. It returns a Navigator handle:

  • layers — the layers to include in your render([...]) call, in z-order.
  • interactingtrue while the user is dragging the indicator.
  • refresh() — re-derive content + indicator (auto-runs on viewport changes; call manually when the data your render callback iterates changes).
  • dispose() — tear down listeners and the interaction node.

NavigatorOptions:

OptionTypeNotes
sourceCameraViewportThe camera the user navigates in the main view.
overviewCameraViewportHosts the navigator; positioned via its frame.
contentLayer | readonly Layer[]Layers to mirror through overview (cheap re-emit).
render(target, project) => voidAlt. content path: populate a navigator-owned UI layer; project maps world→CSS px.
indicatorNavigatorIndicatorOptionsIndicator styling.
interactionNavigatorInteractionOptionsDrag/click behavior.
contentSimplifyMirrorSimplify | trueSimplify mirrored content (requires the content path).
cacheNavigatorCacheOptionsRetain the render-callback overview in a stable GPU buffer.

Provide either content (mirror existing layers) or render (emit a fixed-weight schematic) — supplying neither throws.

  • NavigatorIndicatorOptionsfill, stroke, strokeWidth (default 1), cornerRadius (default 0).
  • NavigatorInteractionOptionsmanager (an InteractionManager), drag (default true), click ("center" | false, default "center"), bounds, zIndex (default 50), and onMove.
  • NavigatorCacheOptionsrenderer (a NavigatorRetainHost exposing retainLayer / unretainLayer) and spatialIndex (default false). Only the render-callback content path supports caching.

createLodLayer(levels) (class LodLayer) picks one of several pre-built layer sets based on the camera zoom — swap coarse/medium/fine representations without rebuilding per frame. It is a pure picker; resolve the active level in your frame loop and pass the result to the renderer.

A LodLevel is { minZoom, maxZoom, layers }. The active band is the first whose half-open range [minZoom, maxZoom) contains the zoom (use 0 / Infinity to open-end). resolve(zoom) returns that band’s layers, or [] when none match; selectLevel(zoom) returns the band (or null) for debugging.

import { createLodLayer } from "insomni";
const lod = createLodLayer([
{ minZoom: 0, maxZoom: 1, layers: [coarse] },
{ minZoom: 1, maxZoom: Infinity, layers: [fine, labels] },
]);
// per frame:
renderer.render(lod.resolve(camera.zoom));

LabelCuller is a greedy 2D overlap culler backed by a uniform hash grid. Feed AABBs in priority order (most-important first); each place(aabb) returns true and records the box when it does not overlap a previously placed one, or false (leaving the grid unchanged) when it does. Use it to thin a large label set down to a non-overlapping subset in O(N) expected time.

LabelCullerOptions: cellSize (default 64 — pick a value near the typical label size). The AABB type is { minX, minY, maxX, maxY }. Units are opaque: pass screen-space AABBs to cull per frame, or world-space AABBs to pre-cull at a fixed zoom. Call reset() between frames; placeRect(x, y, w, h) is a corner convenience.

import { LabelCuller } from "insomni";
const culler = new LabelCuller({ cellSize: 48 });
culler.reset();
for (const label of labelsByPriority) {
if (culler.placeRect(label.x, label.y, label.w, label.h)) {
layer.pushText(label.shape);
}
}

createBufferLayer(options) (class BufferLayer) draws data that lives entirely in a GPU storage buffer — e.g. the output of a compute pass — with no CPU round-trip. It is the “bring-your-own-GPU-buffer” seam.

BufferLayerInstancesOptions (kind: "instances") draws v3 uber instances — SDF rects/circles/ellipses decoded with the live per-kind geometry + SDF. The buffer holds the 64 B InstanceStruct (schema/instance.ts), the SAME record the live render() concat packs. A compute kernel emits one with the writeRectInstance WGSL helper (on insomni/advanced); allocate the buffer as root.createBuffer(d.arrayOf(INSTANCE_LAYOUT.tgpuStruct, N)).$usage("storage").

OptionTypeDefaultNotes
bufferTgpuBuffer (storage)Array of 64 B InstanceStruct records.
countnumberInstances to draw this frame (mutable).
groupGroupTransform applied to all instances.
spaceLayerSpace"world"Coordinate space.
clipRectFrameRectCSS-pixel scissor.
opaquebooleanfalseOpaque pass; caller guarantees alpha = 1.

An instance buffer stacks ABOVE all live geometry + bakes by submission order (no z-interleave) and carries a constant per-buffer near-z. There is no textured variant — instances are SDF primitives, not textured quads. To draw textured quads from a compute texture, use a normal layer’s pushSprite instead (see the heatmap geom in insomni-plot).

createCustomDrawable(space, record, options?) is the escape hatch: a raw render-pass hook for bringing your own pipelines, bind groups, and draw calls into the renderer’s frame. The renderer hands your record an active GPURenderPassEncoder mid-pass — do not begin or end passes.

record(pass, ctx) receives a CustomDrawableContext: device, encoder, scissor (the applied device-pixel rect, or null), space, cameraBindGroup (bind at @group(0)), the frame’s colorView / depthView / viewport, and depth (the reconciled in-layer depth when pushed via layer.pushCustom; 0 for a top-level render() node).

CustomDrawableOptions: clipRect (device-pixel scissor), placement ("underlay" | "overlay", default overlay), opaque, and onDestroy. isCustomDrawable(node) is the runtime brand-tag guard.

Custom drawables are phase hooks, not painter-ordered layers. All underlays share one isolated pre-scene pass; overlays retain the late phase. opaque only changes overlay routing under OIT—placement wins for underlays. Any custom drawable demotes partial redraw for that frame. To interleave a custom shader at a submission point inside a layer instead of a fixed phase, push it into the layer with layer.pushCustom; a guaranteed topmost HUD belongs in the separate UIOverlay canvas.

import { createCustomDrawable } from "insomni";
const overlay = createCustomDrawable("ui", (pass, ctx) => {
pass.setBindGroup(0, ctx.cameraBindGroup);
pass.setPipeline(myPipeline);
pass.draw(6);
});
renderer.render([content, overlay]);

These lower-level pieces back partial-redraw and overzoom caching. Most apps never touch them directly, but they are exported for advanced integrations.

  • RetainedTier — manages stable, WeakMap-keyed GPU instance buffers for packs whose paint/data damage is clear, so an undamaged layer is never re-uploaded. has(pack) / get(pack) / build(pack) / tickShrink(pack) drive the per-frame lifecycle; RetainedRecord holds the buffer + shrink bookkeeping.
  • TileGrid + dirtyTileSet — a row-major grid of command buckets over screen device-px space for tile-bucketed replay. new TileGrid(w, h, cellSize) builds the grid; dirtyTileSet(grid, regions, dpr) maps damage regions to the tile-id set to replay, returning null (fall back to a full frame) when the dirty fraction exceeds FULL_FRAME_TILE_FRACTION (0.5). ScreenAABB is the projected-pixel AABB shape.
  • buildSpatialGrid + queryRanges — a cell-binned index over a retained pack’s per-instance AABBs (the overzoom-cache spatial index). buildSpatialGrid(aabbs, count, options?) returns a SpatialGridResult (index + a newOrder permutation) or null when not worth building; queryRanges(index, view, out) returns the contiguous [offset, count) slot ranges intersecting the view. SpatialGridOptions: cellsPerAxis (default 32) and minItems (default 1024).