import { Bounds, type PathSink } from 'vega-scenegraph'; import { type ItemTransform } from '../path/geometryForItem.js'; import { BufferManager } from '../util/bufferManager.js'; import type { Point } from '../types/geometry.js'; import { type JoinEnd } from '../util/join.js'; import { VertexBufferManager } from '../util/vertexManager.js'; import { type ShaderKey } from '../shaders/index.js'; import type { ClipRect, GPUVegaCanvasContext, GPUVegaScene } from '../types/context.js'; import type { DrawCounts, QueueElement } from '../util/renderQueue.js'; import type { SceneGroupExt, SceneRectExt, SceneItem } from '../types/scene.js'; import { type RGBA } from '../util/color.js'; import { LruMap } from '../util/lru.js'; import type { SceneColor, SceneGradient } from '../types/scene.js'; import type WebGPURenderer from '../WebGPURenderer.js'; /** A mark renderer module, as registered in marks/index.ts. */ export interface MarkModule { draw: (this: WebGPURenderer, device: GPUDevice, ctx: GPUVegaCanvasContext, scene: GPUVegaScene, vb: Bounds, markTypes?: string[]) => void; } /** * Returns the GPU resources for a mark type, creating them on first use * or after a device change. Resources live on the canvas context, so each * renderer instance keeps its own set. */ export declare function getMarkResources(ctx: GPUVegaCanvasContext, markType: string, device: GPUDevice, vb: Bounds | undefined, create: () => T): T; /** Triangulated geometry in one colour, as [x, y, r, g, b, a] vertices. */ export declare function vertexData(triangles: Float32Array, count: number, color: RGBA): Float32Array; /** * vertexData for triangles a cache already holds, kept beside them while the * colour stays the same. An area or a trail that has not moved is the same * cached geometry frame after frame. */ export declare function heldVertexData(triangles: Float32Array, count: number, color: RGBA): Float32Array; /** * A box in the current group's coordinates, as a scissor rect in device * pixels. The group translation is already in `_tx`. */ export declare function deviceClip(ctx: GPUVegaCanvasContext, x: number, y: number, w: number, h: number): ClipRect; /** * The clip path's coverage for the mark being drawn, or the 1x1 placeholder * where there is no path clip. * * Every mark pipeline reads this binding, because every fragment entry calls * `clipCoverage`, so one has to be bound whether or not a clip path is in * force. The uniform flag beside it is what decides whether it is read. */ export declare function clipMaskView(ctx: GPUVegaCanvasContext, device: GPUDevice): GPUTextureView; /** A mark's group 0 bind group, with the clip mask in force. */ export declare function uniformBindGroup(ctx: GPUVegaCanvasContext, device: GPUDevice, label: string, pipeline: GPURenderPipeline, uniforms: GPUBuffer): GPUBindGroup; /** * Two scissor rects narrowed to what both cover, which is what canvas's * `context.clip()` does to whatever is already clipped. An empty result is * left with a zero extent and the render queue drops the draw. */ export declare function intersectClip(outer: ClipRect | undefined, inner: ClipRect): ClipRect; /** * Narrows the scissor to a clip path's box. The path itself is cut by its * coverage mask, which the renderer draws beside this. */ export declare function pushPathClip(ctx: GPUVegaCanvasContext, clip: (sink: PathSink) => unknown): void; /** * Narrows the clip to a group's rectangle, which canvas clips a clipping group * to, and a mark with `clip: true` inside one. canvas narrows whatever is * already clipped rather than replacing it, so a clip inside a clip is cut by * both. The rectangle is rounded where the group is, which a scissor cannot * express, so the corners are cut in the fragment stage. A group with no radius * of its own leaves an enclosing rounded clip cutting. */ export declare function pushGroupClip(ctx: GPUVegaCanvasContext, group: SceneGroupExt): void; /** * The box a rect or a group background spans its ramp over. * * vega's boundStroke grows an item's bounds by a whole stroke width on each * side, so a stroked rect fills its gradient over rather more than its own * box. A scenegraph that reached the renderer unbounded keeps the box. */ export declare function boxGradientBounds(item: SceneRectExt): [number, number, number, number]; /** * A rect's box with a negative extent flipped onto the other side of x or y, * which is how canvas's fillRect and strokeRect draw one. */ export declare function rectBox(item: SceneRectExt): [x: number, y: number, w: number, h: number]; /** Fill color for vertex data: white carrier with opacity when a gradient is used. */ export declare function whiteCarrier(opacity?: number, fillOpacity?: number): RGBA; /** A gradient and the box it spans, in the [x, y, w, h] the gradient shaders read. */ export interface Ramp { gradient: SceneGradient; bounds: [x: number, y: number, w: number, h: number]; } /** A fill or a stroke as it draws: the colour its vertices carry, and the ramp that replaces it. */ export interface Paint { colour: RGBA; ramp: Ramp | null; } /** * The ramp a paint draws from, or null for a flat colour. A gradient spans the * item's bounds, and with none it has nothing to span and draws flat. */ export declare function rampOf(value: SceneColor | null | undefined, bounds: Bounds | undefined): Ramp | null; /** The same for a rect or a group, which span their own box when they have no bounds. */ export declare function boxRampOf(value: SceneColor | null | undefined, item: SceneRectExt): Ramp | null; /** The colour a paint's vertices carry: white at its opacity under a ramp, its own colour otherwise. */ export declare function paintColour(value: SceneColor | null | undefined, opacity: number | undefined, paintOpacity: number | undefined, ramp: Ramp | null): RGBA; /** A fill or a stroke as it draws, for an item that spans its bounds. */ export declare function paintOf(value: SceneColor | null | undefined, opacity: number | undefined, paintOpacity: number | undefined, bounds: Bounds | undefined): Paint; /** Where a mark enqueues its fills or its outlines: a pipeline for each paint and the buffers. */ export interface DrawTarget { ctx: GPUVegaCanvasContext; device: GPUDevice; name: string; pipelineFor: (blend: string) => GPURenderPipeline; gradientPipelineFor: (blend: string) => GPURenderPipeline; bufferManager: BufferManager; uniformBuffer: GPUBuffer; } export declare function targetOf(ctx: GPUVegaCanvasContext, device: GPUDevice, name: string, pipelines: { pipelineFor: (blend: string) => GPURenderPipeline; gradientPipelineFor: (blend: string) => GPURenderPipeline; }, bufferManager: BufferManager, uniformBuffer: GPUBuffer): DrawTarget; export declare function instanceScratch(length: number): Float32Array; /** * A mark's items in the order canvas paints them. * * vega draws the items carrying no zindex in list order and the raised ones * after, and its canvas renderer gets that by routing every mark through * sceneVisit. This one did it for group alone, so a raised item was picked as * if it were on top and drawn as if it were not. * * The list is returned untouched unless vega has actually z-ordered the mark, * which is the usual case and costs nothing. area, line and trail draw all * their items as one shape, so they keep the list whatever it says. */ export declare function markItems(scene: GPUVegaScene): T[]; /** Queues one buffer of triangles, coloured by its vertices or from a ramp. */ export declare function enqueueFill(target: DrawTarget, data: Float32Array, ramp: Ramp | null, blend?: string): void; /** Queues a draw through the target's pipelines, sampling the ramp when there is one. */ export declare function enqueueDraw(target: DrawTarget, ramp: Ramp | null, blend: string, drawCounts: DrawCounts, vertexBuffers: GPUBuffer[]): void; /** * Builds a mark pipeline. The colour format and sample count must match the * frame's attachments, and getting either wrong silently breaks MSAA, so they * are filled in here rather than repeated at every call site. */ export declare function markPipeline(ctx: GPUVegaCanvasContext, device: GPUDevice, label: string, shaderKey: ShaderKey, vertexManager: VertexBufferManager, fragmentEntryPoint?: string, blend?: string): GPURenderPipeline; /** The dash pattern of an item, or null when its outline draws solid. */ export declare function dashPatternOf(item: { strokeDash?: number[] | null; }): number[] | null; /** * What a mark that triangulates its shape needs: a solid fill, the same taking * its colour from a ramp, and an outline drawn through the segment shader. * * arc, area, path, shape and trail all want exactly this. A mark adds its own * fields by spreading this into its record rather than inheriting from it, * since what they add has nothing in common. */ export interface FillResources { device: GPUDevice; bufferManager: BufferManager; vertexManager: VertexBufferManager; /** The fill, one pipeline per blend mode. */ pipelineFor: (blend: string) => GPURenderPipeline; /** The same, taking the colour from a ramp. */ gradientPipelineFor: (blend: string) => GPURenderPipeline; outline: OutlinePipelines; } export declare function fillResources(ctx: GPUVegaCanvasContext, device: GPUDevice, vb: Bounds, name: string, outlineName?: string): FillResources; /** * The segment shader pipelines a mark strokes an outline with, and the layout * they share. * * Every mark that walks its own contours wants the same pair, and since * `markPipeline` caches on the layout rather than the label they are the same * pipelines whoever asks. Ten marks each built them by hand, which is where the * routes that quietly dropped their blend mode were hiding. */ export interface OutlinePipelines { /** Labels the pipelines and the bind groups made from them. */ name: string; vertexManager: VertexBufferManager; /** Solid, one per blend mode. */ pipelineFor: (blend: string) => GPURenderPipeline; /** The same, taking the colour from a ramp. */ gradientPipelineFor: (blend: string) => GPURenderPipeline; } /** What a mark needs to draw an outline, from a ramp when one is set. */ export declare function outlinePipelines(ctx: GPUVegaCanvasContext, device: GPUDevice, name: string): OutlinePipelines; /** * One pipeline per blend mode, for one shader and layout. * * A blend is baked into the pipeline state, so a mark needs one of these for * every mode its items ask for. `markPipeline` keys its cache on a string built * per call, and only the blend and the backdrop vary here, so the pipelines are * held by blend and dropped when the backdrop flips. A sample count change * rebuilds the mark resources this lives in. */ export declare function blendPipelines(ctx: GPUVegaCanvasContext, device: GPUDevice, name: string, shader: ShaderKey, vertexManager: VertexBufferManager, fragmentEntryPoint?: string): (blend: string) => GPURenderPipeline; /** * Draws an outline as segments, taking its colour from a ramp when the stroke * is a gradient. * * A gradient stroke used to keep the extruded ribbon, since only that could * sample a ramp, and a ribbon carries no dash: a stroke that was both came out * solid with the dash silently dropped. The segment shader has a gradient entry * now, so both reach the same draw. */ export declare function enqueueOutline(target: DrawTarget, data: Float32Array, ramp: Ramp | null, blend: string): void; /** * Draws a clip path's coverage into a target of its own and returns it, for * the marks inside that clip to be cut by. * * vega parses `clip: {path}` and `clip: {sphere}` into a generator that draws * the path when it is given a context and returns the path when it is not, * which is the string this triangulates. Enqueued as a mask run, so the queue * lifts it into a pass ahead of everything that reads it. */ export declare function drawClipMask(device: GPUDevice, ctx: GPUVegaCanvasContext, clip: (context?: unknown) => unknown, vb: Bounds): GPUTextureView | undefined; /** Single channel coverage, which is all a mask holds. */ export declare const MASK_FORMAT: GPUTextureFormat; /** * The draw that folds a layer run back into the frame with its blend evaluated. * * Everything it needs is the two textures, so the queue can build it for any * mark without knowing anything about that mark. */ export declare function blendCompositeElement(ctx: GPUVegaCanvasContext, device: GPUDevice, blend: string, clip: ClipRect | undefined): QueueElement; /** How far past an end point a stroke of this item can reach. */ export declare function strokeReach(item: { strokeWidth?: number; strokeJoin?: string; strokeMiterLimit?: number; }): number; /** * Draws an outline into the coverage mask and composites it once. * * A stroke whose own bands overlap cannot be composited band by band. vega * writes one closed contour per trail segment and consecutive contours overlap, * so two antialiased fringes land on the same pixel, and compositing both * darkens every joint where canvas fills the union once. The mask keeps the * largest coverage each pixel receives, which is that union, and the composite * that follows paints it in one go. * * The quad covers the box the instances reach rather than the frame, so the * cost follows the mark and not the canvas. */ export declare function enqueueMaskedOutline(target: DrawTarget, data: Float32Array, blend: string, color: RGBA, reach: number): void; /** * Where strokeRuns draws contours, the way geometryForItem places a fill: scaled * first, so the stroke width stays uniform, then turned about the item origin, * then moved by dx, dy. */ export interface Placement extends Partial { dx: number; dy: number; } /** * The runs an item's stroke draws: the contours the stroke is extruded from, * placed, and cut into dashes when the item has a pattern. Contours that need * neither come back untouched. */ export declare function strokeRuns(lines: readonly (readonly Point[])[], item: { strokeDash?: number[] | null; strokeDashOffset?: number; strokeCap?: string; strokeWidth?: number; }, place?: Placement): readonly (readonly Point[])[]; /** * Floats per segment instance: start, end, colour, width, an end each, and how * far the neighbour at each end reaches. */ export declare const SEGMENT_STRIDE = 19; /** How the vertices between a run's ends are joined. */ export interface StrokeJoin { style: string; miterLimit: number; } /** How the runs of an outline end and join, as the segment writers want it. */ export interface StrokeEnds { caps: readonly [JoinEnd, JoinEnd]; join: StrokeJoin; /** A square cap, which the writer draws by lengthening the run. */ square: boolean; } /** The caps and join of an item. */ export declare function strokeEnds(item: { strokeCap?: string; strokeJoin?: string; strokeMiterLimit?: number; }): StrokeEnds; /** Packs every segment of every polyline, or null when there is nothing to draw. */ export declare function segmentInstances(runs: readonly (readonly Point[])[], color: RGBA, width: number, ends: StrokeEnds): Float32Array | null; /** Segments a set of polyline runs turns into. */ export declare function segmentCount(runs: readonly (readonly Point[])[]): number; /** * Writes runs as segment instances into `data` at `offset`, returning where it * stopped. Field by field rather than through a temporary array, since a * choropleth's borders run to hundreds of thousands of segments a frame. * * `caps` applies to a run's two outer ends, and every vertex between them gets * a join. A run that comes back to where it started has no outer end at all: * its seam is a vertex like any other, which is what draws the corner there. * * Two runs whose facing round caps reach each other, which is every gap of a * dash narrower than the stroke, have both cut back to the midpoint between * them. With equal radii each arc is the outer one on its own side, so the two * cuts trace the union's outline exactly and neither draws over the other. Left * whole they overlap, which an opaque stroke hides and a blend does not. * * Each join also carries how far its neighbour runs. A segment gives up the far * side of the bisector on the understanding that the other one draws it, which * needs the other one to reach up to a half width past the vertex. A dash cut * landing nearer than that leaves a stub that cannot, and the sliver was then * drawn by neither: the stroke came out cut off along the inside of the corner. * Past where the neighbour stops, the cut does not apply. */ export declare function writeSegments(data: Float32Array, offset: number, runs: readonly (readonly Point[])[], color: RGBA, width: number, { caps, join, square }: StrokeEnds): number; /** * vega nudges a group's border by half a pixel when the stroke is about one * pixel wide, so the hairline lands on one row of pixels instead of straddling * two. Only the background and border move, not the group's contents. */ export declare function withStrokeOffset(item: SceneGroupExt): SceneGroupExt; /** * Rect and group strokes are drawn analytically in the fragment shader, which * can express neither a dash pattern nor a ramp. For either the border is * walked as a closed polyline instead and emitted as line instances, dashed * when a pattern is set and whole when it is not. Returns null when the item * has no border this has to draw. */ export declare function borderInstances(ctx: GPUVegaCanvasContext, item: SceneRectExt, ramp: Ramp | null): Float32Array | null; /** * Per-item geometry cache, shared by the marks that triangulate. Rebuilding the * vertex data every frame is what makes a re-render cost multiples of canvas, * and the geometry only changes when the item's position, size or path does. */ export interface CacheableItem { x?: number; y?: number; bounds?: Bounds; strokeWidth?: number; path?: string; angle?: number; scaleX?: number; scaleY?: number; datum?: { id?: unknown; }; id?: unknown; } export type BoundsSnapshot = { x1: number; y1: number; x2: number; y2: number; }; export interface GeometryCacheEntry { fill: RGBA; x?: number; y?: number; bounds?: BoundsSnapshot; strokeWidth?: number; path?: string; angle?: number; scaleX?: number; scaleY?: number; data: Float32Array; /** What the caller keeps beside the data. */ extra: E; } export type GeometryCache = LruMap>; /** * A per-context geometry cache, bounded so a streaming session, where every * frame brings new datum ids, cannot grow one without limit. */ export declare function geometryCache(): GeometryCache; /** Identifies an item across frames: vega keeps tuple ids on a symbol. */ export declare function itemKey(item: CacheableItem): unknown; /** * Vega mutates a Bounds in place as the view pans or zooms, so comparing by * identity never sees a change. Snapshot the numbers and compare those. */ export declare function copyBounds(b?: Bounds): BoundsSnapshot | undefined; /** * A projection can send a shape outside its domain and leave NaN in its * bounds. Compared with `===` those items never match their own snapshot and * rebuild on every frame. */ export declare function sameBounds(b: Bounds | undefined, snap: BoundsSnapshot | undefined): boolean; export declare function sameColor(a: RGBA, b: RGBA): boolean; /** Copies positions from `source` and writes `color` into every vertex. */ export declare function recolor(data: Float32Array, source: Float32Array, color: RGBA): void; /** * The cache entry for an item whose geometry has not changed since it was * built. A colour-only change rewrites the colours over the cached positions * instead of triangulating again, and keeps them. */ export declare function cachedFill(cache: GeometryCache, key: unknown, item: CacheableItem, fill: RGBA): GeometryCacheEntry | undefined; /** Keeps an item's freshly built fill vertex data, for cachedFill to find. */ export declare function cacheFill(cache: GeometryCache, key: unknown, item: CacheableItem, fill: RGBA, data: Float32Array, extra: E): void; /** The item's fill vertex data, building it only when the geometry changed. */ export declare function cachedGeometryData(cache: GeometryCache, key: unknown, item: CacheableItem, fill: RGBA, build: () => Float32Array): Float32Array;