Repository navigation
Conversation
Reviewer's GuideData structuresDefine the declarative connection stroke and appearance data, and add traversal of registered batch items with their parameters for renderer switching.
Add graph settings and public types for selecting the connection renderer and registering custom WebGL effects.
APIExpose stroke and appearance methods on connections, and export the new batch, appearance, effect, and renderer-stat APIs.
Uses of the APIExtend the culling benchmark to compare renderers and stroke effects, measure frame intervals and WebGL draw statistics, and support idle and drag modes.
ImplementationImplement WebGL batching, curve encoding, shader effects, context handling, fallback, and animation alongside Path2D redraw tracking and batch switching.
Export the shared CSS color parser for use when encoding stroke colors into WebGL instance data.
UIAdd interactive Storybook examples for animated dashes, per-connection styling, graph stress, and custom shader effects.
DocumentationDocument the benchmark renderer options, effect modes, frame metrics, and WebGL statistics.
TestsTest stroke appearances, animation scheduling, batch migration and fallback, WebGL encoding and effect compilation, and z-level grouping.
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
Preview is ready. |
1b2bc13 to
cb685b8
Compare
60296e4 to
e0f64eb
Compare
…arance() A connection's look becomes data that any batch backend can draw, so a second renderer does not need the imperative style(ctx) path. - getStroke() returns the line's color, width (world units) and dash; a dash can run with `speed` (world units per second, negative runs from target to source). Path2D styles, group keys and class names derive from it, so one override restyles every backend. - getAppearance() returns the line as cubic segments with that stroke, and the arrow as two straight segments; classes that draw their own createPath() or createArrowPath() return undefined. - Dash `fade`, `pulse` and `effect` are part of the stroke description but only the experimental WebGL renderer draws them; Path2D draws plain dashes and a steady line. - isAnimated(): Path2D chunks remember whether the last render drew an animated item (`drewAnimated`). - BatchScene.forEachItem() visits every item with its params, for switching batches. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rer setting Experimental second backend of the connection batch scene, to judge WebGL as a native renderer rather than a replacement for connection components (#426). Connection components, hit testing and the label pass stay; WebGL draws lines and arrows on its own canvas right below the graph layer's canvas. - `connectionsRenderer: "canvas" | "webgl"` picks one renderer for the whole graph. Switching moves every item to the new batch with forEachItem(); without WebGL2, after a failed shader or a lost context the graph stays on or returns to Path2D. Items without an appearance (own createPath()) are not drawn in WebGL mode. - A chunk is one instance buffer (one instance per cubic piece; curves are split on the CPU within 0.25 world units) drawn with one instanced call. WebGL chunks group by z-level only: color, width and dash are instance attributes. - Running dashes, comet dashes (`dash.fade`) and pulses (`pulse`) animate in WebGL's own requestAnimationFrame loop while such a chunk is drawn, without graph layer updates. Path2D requests layer frames while it draws an animated chunk. - `connectionEffects` (experimental): custom GLSL effects that recolor the line of the connections whose getStroke() names them, with up to 8 params per connection. One program per effect, compiled on first use; instances of a chunk are grouped by effect and drawn with one call per group. A broken or unknown effect is reported once and drawn as the plain stroke. - getWebGLConnectionsStats() counts draws, calls and instances for the benchmark. Measured with the culling benchmark (Apple M3 Pro, Chromium, after the connection components left the frame walk): plain lines cost the same main-thread time as Path2D (WebGL 0.1-2 ms more when zoomed out, cause not found); a still or running dash takes 65 ms to 1.6 s per frame with Path2D and 8.3 ms (120 Hz) with WebGL; per-connection styles are 1.5-2.8x faster on the main thread. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- --renderers canvas,webgl: a column per variant and renderer. - --effect none|dash|static-dash|style: a connection class with a running dash, the same dash standing still, or color and width from the connection id. - Report frame intervals (median and p95) next to the update time, WebGL draw time, calls, instances and items, and timeouts of steps without an update. - --modes idle (camera still, effects animating) and drag (moves the block with the most connections). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Canvas/WebGLConnections: a running dash and per-connection styles on generated graphs, the GraphStressTest graph (111k blocks, 109k connections) with plain, dashed and styled connections, and the NirvanaMaxGraphTest graph with comets, a signal wave and two custom shader effects (particles, gradient) defined in the story and registered with `connectionEffects`. Every story switches between the canvas and WebGL renderers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
e0f64eb to
04f1f31
Compare
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tore Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… geometry BlockConnection draws through `static line` (a ConnectionLine class, default Path2DLine) and `static geometry` (a pure function, default blockGeometry). The line owns the look and the line and arrow batch items; the component recomputes the geometry, bounds and culling bounds and hit-tests the line at the world center of the hit box. MultipointConnection and BezierMultipointConnection only set their geometry; multipoint labels move to getAfterRenderBounds() and hitTestLabel(). invalidateLine() re-reads looks that depend on data outside the store. BREAKING CHANGE: the renderer methods of BlockConnection (createPath, createArrowPath, style, styleArrow, getStroke, getClassName, getStrokeColor, getAppearance, collectBBoxPoints overrides and others), ConnectionArrow, the appearance types and the connectionsRenderer and connectionEffects settings are removed; connections are drawn with Path2D. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… work TConnectionLineClass takes `never` as the connection parameter, so a line typed for a connection subclass (`Path2DLine<MyConnection>`) is accepted as `static line`; the connection passes itself when it creates the line. Mount does less per connection with the same output: a straight cubic is bounded by its endpoints without solving for extrema, cubic bounds are computed without allocations, a line update reads getStroke() once and the arrow stroke only while arrows are shown, the arrow item is a small class instead of five closures, straight and Bezier geometries write their segments directly, and the first $hidden value no longer repeats the update mounting has just done. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The WebGL backend now reads `IConnectionBatchItem.getWebGLShape()` and skips items without a shape. Instances shrink to 26 floats (stroke, still dash, connection length, distances, 8 effect params at zero); the built-in running dash, comet and pulse are gone. Shader attributes, varyings, outputs and uniforms take the `graph_` prefix, the fragment prelude defines `GRAPH_CONNECTION_FRAGMENT 1` and `ConnectionFragment` with `extraWidth` and `coverage`, and the plain program uses the identity `effect()`. Closed arrows get a fill buffer per chunk (a triangle fan as TRIANGLES) drawn with a fill program before the chunk's strokes. `ConnectionPrograms` compiles the line and fill programs up front and frees what it created when one fails; `WebGLConnections.create` frees the context then. `setEffects` and the prototype effects module are removed. Tests run on `fakeGL.ts`, a recording WebGL2 context shared with later work. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eDash The instance grows to 28 floats: width, connection length, piece start and piece length, then four dash numbers, then the effect params. A dash pattern is normalized like canvas setLineDash (an odd-length list repeats to even length) and keeps two on/off pairs; a longer pattern draws its first four numbers and reports one development warning per pattern. A 2-number pattern leaves the second pair at zero and discards the same fragments as before. `f.world` is the fragment's world position: the varying is offset from the center line by normal * across / scale. The attribute tables move next to the layouts they describe (INSTANCE_ATTRIBUTES, FILL_ATTRIBUTES), and a test checks them against the vertex shaders' declarations. A fill allocation that gets only one of its buffer and vertex array frees the other. The fake WebGL2 context drops uniforms tests mark inactive, rejects draws past the end of a bound buffer, and unbinds deleted buffers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
BlockConnections reads `(settings.connection ?? BlockConnection).line.renderer` through a computed: a WebGL line class draws with WebGL2, created on the first frame with the graph layer's canvas in the document and before children compute geometry. Without WebGL2 or with failing programs, Path2D draws the WebGL lines until the requested renderer changes; a lost context falls back to Path2D and the lost renderer waits for webglcontextrestored, then is disposed and a fresh one is created on the next frame. Each fallback logs one warning with its reason. GraphLayer.$connectionsRenderer reports "canvas", "webgl" or "webgl-fallback"; BlockConnections is its only writer. TConnectionsRendererState is exported from the package index. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… failures A change of the connection class clears the failed-creation flag, so the next frame tries WebGL2 again, a WebGL to WebGL class change included; a working WebGL renderer stays as it is. Shader compile and link failures are reported as "shaders failed: <log>" (WebGLProgramError), other setup errors as "WebGL setup failed: <message>". Unmounting the connections resets $connectionsRenderer to "canvas". One helper computes the chunk size and cell size. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Effect programs are compiled lazily, one per program key, and shared by clones; a broken source reports one console error with the compiler log and its connections draw with the plain program. Chunks group instances by effect object; each z-level draws its fills, the plain stroke, then each effect once over all its chunks, so program switches per z-level equal the number of effects. Uniform values are applied once per frame per program (and again for another effect object of the same program) with peek(); a wrong length reports one development error and is not applied. The renderer subscribes to the uniforms of effects in its built chunks, refcounted by chunk; a change redraws once in its own frame without touching buffers. extraWidth widens the quad and the culling viewport by max(extraWidth) / scale; animated effects keep the loop on. Params are packed into instance floats 20-27. BatchScene calls the new optional IBatchBackend.endGroup() after each group that drew a chunk. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Curves are split so that 16-chord pieces stay within 0.25 CSS px at the camera's power-of-two scale bucket instead of 0.25 world units, so a long bezier is one piece at scale 0.01. Straight segments use a 4-vertex template and curve pieces a 16-chord one: per chunk and effect group a line range and a curve range, drawn after one program bind. Piece lengths come from 5-point Gauss-Legendre quadrature, so the dash phase stays put across buckets. Each chunk remembers its bucket. A drawn chunk of another bucket is brought to the frame's one in place within 2 ms a frame (at least one chunk), and only re-encoded when a curve gets another number of pieces; the rest are drawn as they are and WebGLConnections asks for another frame of its own. Stats gain lineInstances and curveInstances. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ries Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ffects Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New page docs/connections/webgl-connections.md: choosing a renderer with static line, when WebGL pays off, Path2DLine and WebGLLine, geometries and per-connection shapes, effects (defineConnectionEffect, three levels of control, clone(), extraWidth, fallback, errors), the shader contract, library effects and GLSL helpers, the fallback and $connectionsRenderer, hit testing, draw calls and order, known limits and the experimental stats. The v2 migration guide gains "Connection lines and geometry": the removed BlockConnection renderer methods and where they move, custom shapes, styles from data, multipoint classes, bounds, the new hit-test rule and the development warning. Connection system, batch rendering, component and settings docs no longer refer to removed methods; the READMEs list the page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The hit test reaches stroke.width / 2 + THRESHOLD_LINE_HIT from the line, but the hit box was padded by THRESHOLD_LINE_HIT only and culling by a fixed 1 (2 hovered or selected) world unit, so wide strokes were cut off by the broad phase and clipped by culling. - ConnectionLine splits update() into readStrokes() and updateItems() and reports the half widths of the strokes it read; BlockConnection reads the strokes before its bounds, so no extra getStroke() call per update. - Culling pads the line by half its stroke width and the arrow points by half the arrow width times the miter limit: default widths give the same 1 / 2 as before, so canvas pixels are unchanged. - The hit box covers the arrow points while arrows are shown and pads by THRESHOLD_LINE_HIT plus half the wider of the line and arrow strokes (BaseConnection.getHitBoxPadding()). - invalidateLine() updates the bounds when a stroke width changed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The connection docs describe the hit box and culling bounds padded by half the stroke width, invalidateLine() following width changes, getStroke on the component among the removed methods and the development-only dash warning. The migration guide no longer names settings that never existed in v1 or v2, and the connection system page no longer says arrows depend on zoom. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
graph.stop(), graph.detach() and the GraphCanvas unmount left the WebGL canvas in the root and an animated effect kept drawing every animation frame. BlockConnections now follows the graph layer's $attached: on detach the WebGL renderer cancels its frame, ignores uniform changes and takes its canvas out of the document, keeping its GL objects; the next frame after the attach puts the canvas back and draws. A lost renderer keeps its restore listener while detached. The renderer's own frame stops when the graph layer's canvas is no longer in the document. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the first frame The culling margin read only the effects of chunks already built, before this frame's builds retained theirs: a glow near the viewport's edge was missing in a static view until the next redraw, and a glow chunk just outside the viewport with no glow chunk inside was never drawn. Items now report their effect when they are added or updated (getWebGLEffect, internal), and the WebGL batch keeps the widest extraWidth of the items' and the built chunks' effects, recomputed only when the widest one leaves. A chunk built with an effect wider than the frame's margin (an item that does not report it) asks for one more frame of its own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…roken effect A lost context answers the compile and link status with null. compile() and link() took that for a failure: programOf logged a false console.error and cached the source as broken in the module-level report set, hiding a later real error, and the same in the renderer's setup made BlockConnections fall back for good with "shaders failed: shader" and no restore listener. Compile and link now check isContextLost(); programOf neither reports nor caches while the context is lost, and WebGLConnections.create() returns a LostWebGLContext for a context lost during the setup: BlockConnections falls back with "context lost" and creates a fresh renderer after the restore. The fake WebGL context answers status queries with null while lost, like a real one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nection A renderer switch (context loss, restore, late WebGL creation) gave every connection a new context with the new batch, so BlockConnection.checkData() marked every geometry dirty: 100k geometry and bounds recomputations in one frame plus a 100k setContext walk. Connections now hold one batch for the life of BlockConnections that forwards to the current renderer's batch; switchBatch() moves the items behind it and swaps the target, and no connection is touched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…in step The public invalidate() of a line only read its strokes again, skipping the bounds, hit box and label sync that BlockConnection.invalidateLine() does: a line subclass that called this.invalidate() after a width change kept stale bounds. invalidate() now runs the connection's invalidateLine(), which reads the strokes with update(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ConnectionLine's readStrokes, updateItems, getLineHalfWidth, getArrowHalfWidth, getWebGLShape, getArrowWebGLShape and getWebGLEffect, and IConnectionBatchItem's getWebGLShape and getWebGLEffect (which return the internal TWebGLShape and ConnectionEffect) are the protocol between the component, the line and the batches, not API. The build does not strip @internal; the tag is the documentation contract. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The development warning for more than 64 effect objects with one fragment counted every object ever created, so an app that remounts graphs and clones an effect per graph, as the docs advise, got a false warning after 64 mounts. A FinalizationRegistry counts collected objects down; where it is missing, objects keep counting as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… culling "Any change of the connection class tries WebGL again" did not hold while a lost context waits for its restore: a change to another WebGL class does not retry then, and a change to a Path2D class frees the lost context. The fallback section now says so (pinned by a test), names a context lost during the setup, and the renderer section says what happens while the graph is stopped or detached. The glow culling margin covers the effects the connections use, not only the ones drawn. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…raction() Hover and selection change the color and the width of a line, never its dash or effect. ConnectionLine gets the protected withInteraction(stroke, state): the selected color (styles.selectedBackground or the theme's) and 2 more units of width on top of any stroke. The default getStroke() is built on it, so its output does not change. Stories, code samples and docs build custom looks with it instead of returning the default stroke for hovered and selected connections. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A connection effect decides how hover and selection look. ConnectionFragment gets hovered, selected and
selectionColor (styles.selectedBackground or the theme's, whatever the state); the instance carries them in one
more vec4, 32 floats: the selection rgb, and hovered + 2 selected + 4 round(alpha * 255), decoded in the vertex
shader.
A WebGLLine applies the selection color when it draws the line, not in the stroke: withInteraction() only makes a
hovered or selected stroke 2 units wider, so { ...super.getStroke(state), effect } keeps the connection's color. A
selected line without an effect is drawn in the selection color, as before; one with an effect keeps its color and
the effect shows the selection. The arrow and the Path2D fallback of a selected WebGL line take the selection
color. dashFlow fills its gaps with the selection color and pulse dims to it. Path2DLine is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…effects Selected, the trace of the particles takes the selection color, the gradient gets a border in the selection color along both edges (its tint parameter is gone) and the halo of the glow takes the selection color at a stronger intensity; the code samples show the same GLSL. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… it is
A WebGLLine subclass that picks its own selected color in getStroke() was drawn in the theme's selection color.
withInteraction() now marks the stroke it returns with a module-private symbol key, which a spread keeps, so
{ ...super.getStroke(state), effect } stays marked. Only a marked stroke gets the selection color when it is drawn:
in WebGL without an effect, on its arrow and in the Path2D fallback. An unmarked stroke is drawn as it is returned,
and its effect still gets f.hovered, f.selected and f.selectionColor.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WebGLLine.withInteraction() recolors like ConnectionLine: the selection color and 2 units wider. A recolored stroke
records its own color and the applied selection color in a module-private symbol key that a spread keeps. When the
line is drawn, a stroke with an effect whose color is still the applied selection color gives the effect its own
color back, and the effect shows the selection. Every other stroke is drawn as it is returned, its arrow and its
Path2D fallback included. So a color set over super.getStroke() wins, picked or copied strokes keep the selection
color, and { ...super.getStroke(state), effect } runs the effect over the connection's color. The arrow and fallback
recolor branches and the getArrowStrokeOf() hook are gone.
Bit 10 of the packed interaction marks a selection the effect shows. The plain program's identity effect draws it in
the selection color, so the connections of an effect that does not compile still show the selection.
The docs glow and particles examples show the selection look; there is one particles definition, with its fallback.
The selection statements follow the new rule. An e2e probe reads the pixels of a selected still dashFlow line: the
selection color in its gaps, its own color in its dashes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ETL story draws its stages as cards on every zoom level: far away a card tinted with the status color, at the schematic level the kind, the name and the status, zoomed in a React card with metrics, Run / Pause / Retry and an inline log. The flows take their status from their stages; an action updates the stage and the flows touching it, and the lines follow the connection state. The stages can be dragged, and a renderer switch keeps their positions and statuses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Summary
Connection lines get a public API with two renderers. A connection class picks the renderer of the whole graph with its line class:
Path2DLinekeeps Canvas2D,WebGLLinedraws every line with WebGL2. The look of a line, its shape and its effects are separate, typed pieces:static lineonBlockConnection: aConnectionLinesubclass, one object per connection. It owns the stroke (getStroke(state),getArrowStroke(state)), the batch items (line and arrow), hit testing and, forPath2DLine, any Canvas2D shape and style (createPath,style,getGroup).static geometryonBlockConnection: a pure function from plain input (endpoints,points, Bézier settings, connection data) to cubic segments and an arrow. Library geometries:straightGeometry,bezierGeometry,blockGeometry(default),multipointGeometry,bezierMultipointGeometry, pluslineSegment,bezierSegment,roundedPolylinefor custom ones. A geometry may pick a shape per connection from its data.WebGLLine:defineConnectionEffect({ fragment, uniforms, animated, extraWidth, fallback, encode })returns an object; a stroke uses it aseffect: glow.use(params). Uniform values are signals on the effect (glow.uniforms.intensity.value = 0.5redraws without touching buffers),clone()gives another graph its own values with the same program. Library effectsdashFlow(running dash, comets in either direction) andpulse(brightness wave), andgraph_dashPhase/graph_comet/graph_waveGLSL helpers.graph.getGraphLayer().$connectionsRenderer:"canvas" | "webgl" | "webgl-fallback". Without WebGL2, with a failed line shader or after a context loss, WebGL lines are drawn with Path2D from the same geometry and each effect's fallback stroke, with one warning; WebGL comes back onwebglcontextrestored.MultipointConnectionandBezierMultipointConnectionstay as ready classes that only setstatic geometry.Rendering
v2(see Numbers). Path2D has no animation loop; a still dash is allowed and documented as slow on large graphs.BatchScenewith grid-cell chunks, one group per z-level. A chunk holds one instance buffer; instances are grouped by effect object; per z-level the loop runs effects outside and chunks inside, so program switches equal the number of effects present. Closed arrows (multipoint triangles) get one fill call per chunk.vec4 effect(ConnectionFragment f, vec4 color);ConnectionFragment(along,length,t,across,halfWidth,extraWidth,coverage,scale,world,time,params0/1) is versioned byGRAPH_CONNECTION_FRAGMENT; every internal name has thegraph_prefix, sou_<name>belongs to the effect's uniforms.extraWidthdraws glows outside the stroke and widens the culling rectangle instead of the bounds.graph.stop(),graph.detach(),GraphCanvasunmount), and a renderer switch never touches connection components.Hover and selection
Hover and selection change the color and the width of a line, never its style.
ConnectionLinehas a protectedwithInteraction(stroke, state): a selected line takes the selected color (styles.selectedBackgroundor the theme's), a hovered or selected one is 2 units wider, and the dash and the effect stay; the defaultgetStroke()is built on it, so a custom look is written asreturn this.withInteraction({ color, width: 6, effect: flow.use(input) }, state). OnegetStroke()gives the same selection look inPath2DLineandWebGLLine.A line with an effect keeps its own colors and the effect decides how hover and selection look:
ConnectionFragmenthashovered,selectedandselectionColor.dashFlowfills the gaps between its dashes with the selection color and keeps the comets' color,pulseturns the dim part of the wave into the selection color; the stories show particles with a selection-colored trace, a gradient with a selection-colored border and a glow with a selection-colored halo. An effect that does not read the flags shows selection by width; a broken effect program still draws the selection color. TheConnectionFragmentcontract is unreleased, soGRAPH_CONNECTION_FRAGMENTstays 1.Hit testing
One rule for both line kinds: a point hits within
stroke.width / 2 + THRESHOLD_LINE_HITworld units of the line (9 for a 2-unit line, 10 hovered or selected), and the arrow is part of the hit area while arrows are shown.Path2DLineusesisPointInStrokeon its own path under an identity transform;WebGLLinemeasures the distance to the geometry analytically and holds no Path2D. Bounds and hit boxes are padded by half the stroke width, so thick strokes are culled and hit correctly. Label hits are unchanged.Breaking changes (v2)
BlockConnectionno longer has renderer methods. Overrides ofcreatePath,createArrowPath,style,styleArrow,setRenderStyles,getClassName,getStrokeColor,getStroke,collectBBoxPointsand the other removed hooks move to a line class or a geometry; a development build warns once per class that still overrides one, and the method is not called. The protectedgeometryfield is gone (connectionPoints/getGeometry()). The multipoint triangle is stroked with the line's width, and a multipoint connection without resolved ports has no geometry. See the migration guide section "Connection lines and geometry" andpackages/graph/docs/connections/webgl-connections.md.Numbers
Apple M3 Pro, Chromium (ANGLE Metal), DPR 2,
scripts/bench/culling.Canvas, default graph,
base=origin/v2vs this branch: pixels identical in all 8 views (Nirvana and Playground at scales 0.01–0.1); update times within noise (Nirvana pan at 0.01: 4.92 vs 4.95 ms; Playground pan at 0.01: 17.75 vs 18.13 ms).WebGL, Nirvana (13,916 blocks, 21,654 connections), effects spread over connections:
With 16 effects zoomed out the frame becomes GPU-bound (9.8 ms frame interval instead of 8.3 ms).
Tessellation, Nirvana, instances drawn: world-unit tolerance (0.25 world units, 32 chords) vs screen-pixel tolerance (0.25 CSS px, 16 chords): scale 0.01: 167,922 → 76,700; 0.05: 116,131 → 71,171; 0.2: 44,935 → 44,935; 1: 39,787 → 72,136 (twice the instances at half the vertices each).
Mount (first two frames), Nirvana: canvas 411 ms, WebGL 479 ms; Playground: canvas 372 ms, WebGL 407 ms. WebGL lines build no Path2D per connection.
Plain lines are not faster with WebGL (Nirvana pan at scale 1: 0.25 ms canvas, 0.6–1.0 ms WebGL); WebGL pays off for effects, dashes and per-connection styles on large graphs, as the docs say.
Testing
pnpm run typecheck,pnpm run test:unit,pnpm run lintpass.dashFlowgaps in pixels,Path2DLinevsWebGLLinehit-test parity at scales 1 and 0.25,dashFlowandpulsecompiling without warnings.Canvas/WebGLConnections(GraphStress,NirvanaEffectswith pipeline, comets, signal wave, particles, gradient, glow and still dash,RunningDash,PerConnectionStyle, andEtlPipeline— a typical ETL flow with stage blocks drawn on canvas and as React cards (status, metrics, Run / Pause / Retry, logs), draggable stages, flows whose status follows their stages, and an effect picked in a selector and swapped at runtime throughinvalidateLine()) checked by eye, including uniform sliders, selection looks and the fallback.--renderers canvas,webglpicks the line class,--effect effects-1|4|16.🤖 Generated with Claude Code