Skip to content

feat(graph)!: connection lines, geometries and WebGL effects - #451

Draft
draedful wants to merge 47 commits into
v2from
claude/webgl-connections-detached
Draft

draedful wants to merge 47 commits into
v2from
claude/webgl-connections-detached

Conversation

@draedful

@draedful draedful commented Nov 6, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Connection lines get a public API with two renderers. A connection class picks the renderer of the whole graph with its line class: Path2DLine keeps Canvas2D, WebGLLine draws every line with WebGL2. The look of a line, its shape and its effects are separate, typed pieces:

  • static line on BlockConnection: a ConnectionLine subclass, one object per connection. It owns the stroke (getStroke(state), getArrowStroke(state)), the batch items (line and arrow), hit testing and, for Path2DLine, any Canvas2D shape and style (createPath, style, getGroup).
  • static geometry on BlockConnection: 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, plus lineSegment, bezierSegment, roundedPolyline for custom ones. A geometry may pick a shape per connection from its data.
  • Effects for WebGLLine: defineConnectionEffect({ fragment, uniforms, animated, extraWidth, fallback, encode }) returns an object; a stroke uses it as effect: glow.use(params). Uniform values are signals on the effect (glow.uniforms.intensity.value = 0.5 redraws without touching buffers), clone() gives another graph its own values with the same program. Library effects dashFlow (running dash, comets in either direction) and pulse (brightness wave), and graph_dashPhase / graph_comet / graph_wave GLSL 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 on webglcontextrestored.

MultipointConnection and BezierMultipointConnection stay as ready classes that only set static geometry.

Rendering

  • Canvas is unchanged: the default graph draws the same pixels as v2 (see Numbers). Path2D has no animation loop; a still dash is allowed and documented as slow on large graphs.
  • WebGL draws on its own canvas right below the graph layer's canvas, through the same BatchScene with 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.
  • Shader contract: an effect is GLSL ES 3.00 vec4 effect(ConnectionFragment f, vec4 color); ConnectionFragment (along, length, t, across, halfWidth, extraWidth, coverage, scale, world, time, params0/1) is versioned by GRAPH_CONNECTION_FRAGMENT; every internal name has the graph_ prefix, so u_<name> belongs to the effect's uniforms. extraWidth draws glows outside the stroke and widens the culling rectangle instead of the bounds.
  • Tessellation keeps curves within 0.25 CSS px of the true curve at every zoom: pieces are counted per power-of-two scale bucket, straight segments use a 4-vertex template and curves a 16-chord one, piece lengths come from Gauss–Legendre quadrature; chunks of another bucket are re-encoded over a few frames.
  • WebGL stops drawing and leaves the document while the graph layer is detached (graph.stop(), graph.detach(), GraphCanvas unmount), 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. ConnectionLine has a protected withInteraction(stroke, state): a selected line takes the selected color (styles.selectedBackground or the theme's), a hovered or selected one is 2 units wider, and the dash and the effect stay; the default getStroke() is built on it, so a custom look is written as return this.withInteraction({ color, width: 6, effect: flow.use(input) }, state). One getStroke() gives the same selection look in Path2DLine and WebGLLine.

A line with an effect keeps its own colors and the effect decides how hover and selection look: ConnectionFragment has hovered, selected and selectionColor. dashFlow fills the gaps between its dashes with the selection color and keeps the comets' color, pulse turns 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. The ConnectionFragment contract is unreleased, so GRAPH_CONNECTION_FRAGMENT stays 1.

Hit testing

One rule for both line kinds: a point hits within stroke.width / 2 + THRESHOLD_LINE_HIT world 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. Path2DLine uses isPointInStroke on its own path under an identity transform; WebGLLine measures 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)

BlockConnection no longer has renderer methods. Overrides of createPath, createArrowPath, style, styleArrow, setRenderStyles, getClassName, getStrokeColor, getStroke, collectBBoxPoints and 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 protected geometry field 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" and packages/graph/docs/connections/webgl-connections.md.

Numbers

Apple M3 Pro, Chromium (ANGLE Metal), DPR 2, scripts/bench/culling.

Canvas, default graph, base=origin/v2 vs 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:

Effects Calls at 0.01 WebGL draw at 0.01 Update at 0.01 Calls at 0.5
1 261 0.20 ms 6.2 ms 42
4 642 0.24 ms 5.8 ms 104
16 2,081 0.62 ms 6.1 ms 345

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

  • Unit: geometries, bounds and Path2D builders, lines and the update protocol, hover/selection parity of both line kinds, mixed shapes per connection, analytic distance, the WebGL backend on a fake WebGL2 context (layout, programs per source, uniforms without buffer writes, effect grouping and draw order, fills, culling margin, tessellation buckets, loss and restore, suspend while detached), renderer choice and fallback, development warnings. pnpm run typecheck, pnpm run test:unit, pnpm run lint pass.
  • E2E (285 passed): clicking a WebGL connection, the fallback without WebGL2, selection-colored dashFlow gaps in pixels, Path2DLine vs WebGLLine hit-test parity at scales 1 and 0.25, dashFlow and pulse compiling without warnings.
  • Storybook: Canvas/WebGLConnections (GraphStress, NirvanaEffects with pipeline, comets, signal wave, particles, gradient, glow and still dash, RunningDash, PerConnectionStyle, and EtlPipeline — 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 through invalidateLine()) checked by eye, including uniform sliders, selection looks and the fallback.
  • Benchmark: --renderers canvas,webgl picks the line class, --effect effects-1|4|16.

🤖 Generated with Claude Code

@sourcery-ai

sourcery-ai Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

Data structures

Define the declarative connection stroke and appearance data, and add traversal of registered batch items with their parameters for renderer switching.

  • packages/graph/src/components/canvas/connections/appearance.ts L1-48
  • packages/graph/src/components/canvas/connections/batch.ts L1-20
  • packages/graph/src/lib/batch/BatchScene.ts L162-166

Add graph settings and public types for selecting the connection renderer and registering custom WebGL effects.

  • packages/graph/src/store/settings.ts L57-66 (+3 small edits)

API

Expose stroke and appearance methods on connections, and export the new batch, appearance, effect, and renderer-stat APIs.

  • packages/graph/src/components/canvas/connections/Arrow/index.ts L4, L24-31
  • packages/graph/src/components/canvas/connections/BlockConnection.ts L272-275, L284-290, L534-573 (+2 small edits)
  • packages/graph/src/components/canvas/connections/index.ts L8-16
  • packages/graph/src/components/canvas/connections/webgl/effects.ts L1-36
  • packages/graph/src/components/canvas/connections/webgl/stats.ts L1-40

Uses of the API

Extend the culling benchmark to compare renderers and stroke effects, measure frame intervals and WebGL draw statistics, and support idle and drag modes.

  • scripts/bench/culling/entry.ts L186-230, L327-345, L412-429 (+8 small edits)
  • scripts/bench/culling/run.mjs L181-206, L229-241, L258-294 (+6 small edits)

Implementation

Implement WebGL batching, curve encoding, shader effects, context handling, fallback, and animation alongside Path2D redraw tracking and batch switching.

  • packages/graph/src/components/canvas/connections/BatchPath2D/index.ts L34-73, L82-97 (+2 small edits)
  • packages/graph/src/components/canvas/connections/BlockConnections.ts L33-42, L82-141, L204-215 (+5 small edits)
  • packages/graph/src/components/canvas/connections/webgl/WebGLBatchBackend.ts L1-146
  • packages/graph/src/components/canvas/connections/webgl/WebGLConnections.ts L1-194
  • packages/graph/src/components/canvas/connections/webgl/ZLevelBatch.ts L1-52
  • packages/graph/src/components/canvas/connections/webgl/encode.ts L1-166
  • packages/graph/src/components/canvas/connections/webgl/programs.ts L1-196
  • packages/graph/src/components/canvas/connections/webgl/shaders.ts L1-162
  • packages/graph/src/lib/batch/BatchScene.test.ts L219-233

Export the shared CSS color parser for use when encoding stroke colors into WebGL instance data.

  • packages/graph/src/utils/functions/color.ts L38

UI

Add interactive Storybook examples for animated dashes, per-connection styling, graph stress, and custom shader effects.

  • apps/storybook/src/stories/canvas/webglConnections/webglConnections.stories.tsx L1-420

Documentation

Document the benchmark renderer options, effect modes, frame metrics, and WebGL statistics.

  • scripts/bench/culling/README.md L23, L29-46

Tests

Test stroke appearances, animation scheduling, batch migration and fallback, WebGL encoding and effect compilation, and z-level grouping.

  • packages/graph/src/components/canvas/connections/BatchPath2D/index.test.ts L224-233
  • packages/graph/src/components/canvas/connections/BlockConnections.test.ts L1-171
  • packages/graph/src/components/canvas/connections/appearance.test.ts L1-139
  • packages/graph/src/components/canvas/connections/webgl/ZLevelBatch.test.ts L1-68
  • packages/graph/src/components/canvas/connections/webgl/effects.test.ts L1-54
  • packages/graph/src/components/canvas/connections/webgl/encode.test.ts L1-84
  • packages/graph/src/components/canvas/connections/webgl/programs.test.ts L1-106

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@gravity-ui-bot

Copy link
Copy Markdown
Contributor

Preview is ready.

@draedful
draedful force-pushed the claude/batch-spatial-chunks branch from 1b2bc13 to cb685b8 Compare November 8, 2026 12:51
@draedful
draedful force-pushed the claude/webgl-connections-detached branch from 60296e4 to e0f64eb Compare November 8, 2026 12:52
draedful and others added 4 commits November 8, 2026 16:08
…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>
@draedful
draedful force-pushed the claude/webgl-connections-detached branch from e0f64eb to 04f1f31 Compare November 8, 2026 13:08
@draedful
draedful changed the base branch from claude/batch-spatial-chunks to v2 November 8, 2026 13:08
@draedful
draedful changed the base branch from v2 to claude/batch-spatial-chunks November 8, 2026 13:14
@draedful
draedful changed the base branch from claude/batch-spatial-chunks to v2 November 8, 2026 13:14
draedful and others added 18 commits November 8, 2026 16:55
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>
draedful and others added 17 commits November 8, 2026 22:17
…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>
@draedful draedful changed the title feat(graph): experimental WebGL connections renderer with custom effects feat(graph)!: connection lines, geometries and WebGL effects Nov 8, 2026
draedful and others added 8 commits November 9, 2026 00:29
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants