Skip to content

feat(joint-router-avoid): new avoid router package - #3457

Open
kumilingus wants to merge 55 commits into
clientIO:masterfrom
kumilingus:router-avoid-api-cleanup
Open

feat(joint-router-avoid): new avoid router package#3457
kumilingus wants to merge 55 commits into
clientIO:masterfrom
kumilingus:router-avoid-api-cleanup

Conversation

@kumilingus

@kumilingus kumilingus commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Supersedes #3443.

Summary

  • Adds @joint/router-avoid, a new package that routes JointJS links via libavoid (WASM, through libavoid-js), keeping a graph's links obstacle-avoiding and orthogonally routed - no router: attribute needed once a link connects two elements. Ships both a main-thread provider and a Web-Worker-backed provider (worker: true) for larger graphs.
  • Extracts the rightAngle router's path-finding into a new public @joint/core alg.rightAnglePath function, shared between the built-in rightAngle router and @joint/router-avoid's fallback route (used while avoid computes a link's real route, or when a link can't be routed by avoid at all). Fixes stale flat-property references left over from the bbox-based refactor that were breaking most of the rightAngle router's own test suite.
  • Adds useModelGeometry to the rightAngle router's options.
  • Adds examples/avoid-router-ts, a TypeScript demo app (simple graph, two independent RouterService instances on one dia.Graph, large graph routed via a Web Worker).
  • Full TSDoc across the public and internal API.

API surface

  • initAvoidRouter(graph, options) / loadAvoidRouter(filePath?) - entry points (also on the UMD global as joint.routers.avoid.*). The returned RouterService is not started: start()/stop() for continuous routing, routeAll()/routeSubgraph(cells) for one-shot passes (e.g. routing each container's content independently), isStarted, destroy().
  • One-shot passes resolve with a RoutingResult - { status: 'done' | 'cancelled' }. destroy() settles in-flight and queued passes as 'cancelled' instead of rejecting (no unhandled rejections for fire-and-forget callers); provider errors unrelated to destruction still reject. Overlapping passes are queued and run one after another. The result object is additive-extensible (e.g. per-status data later) without a breaking signature change.
  • Positive filter callbacks trackLink/trackElement select what is routed/tracked as an obstacle.
  • interceptUnroutableLink gives the consumer first refusal on a link avoid can't route, with a reason: 'unconnected' (loose end), 'untracked' (end element excluded via trackElement), or 'unsupported' (link-to-link connection).
  • setRouteAttributes overrides how computed routes are applied (e.g. through a command manager), receiving origin: 'avoid' | 'fallback' and the provisional/final state; changeFlag names the opt flag marking the router's own writes.
  • worker: boolean | { debounceTime?: number } runs libavoid inside a Web Worker; debounceTime controls how long the Worker batches incoming updates before processing them in one transaction.
  • Events: link:routing, link:routed ({ origin, reason }), link:routing:cancelled, idle.

Test plan

  • yarn test for @joint/router-avoid passes (28/28)
  • @joint/core's test suite passes, including the previously-broken rightAngle router suite
  • examples/avoid-router-ts type-checks (tsc --noEmit)
  • Manual review of the libavoid integration (main-thread and Worker providers) against a real app

🤖 Generated with Claude Code

Geliogabalus and others added 10 commits August 14, 2026 15:37
- rename unroutable reason 'untracked-element' to 'untracked',
  matching the trackElement option it derives from
- replace useWorker/workerUpdateDebounceTime with nested
  worker: boolean | { debounceTime } so the debounce option
  cannot be set without the worker it belongs to
- align provider-level default values with initAvoidRouter
  (shapeBufferDistance 10, idealNudgingDistance 5)
- export public option/callback types under their real names
  (index.mts still aliased the old Skip* names)
- document why WorkerProvider.sync may resolve on an
  uncorrelated 'processed' response
- update READMEs and examples to the current API

Note: test/index.js still asserts 'untracked-element' and needs
updating by the package author.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@kumilingus kumilingus changed the title refactor(router-avoid): public API cleanup on top of #3443 feat(joint-router-avoid): new avoid router package Aug 17, 2026
kumilingus and others added 3 commits August 17, 2026 22:28
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
routeAll()/routeSubgraph() resolve with { status: 'done' | 'cancelled' }
instead of void. destroy() settles in-flight and queued passes as
'cancelled' rather than rejecting, so fire-and-forget callers get no
unhandled rejections; provider errors unrelated to destruction still
reject. Passes are serialized - each replaces the provider's entire
content, so overlapping calls now queue instead of racing.

The result object is additive-extensible (e.g. per-status data later)
without breaking the signature.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a new JointJS workspace package, @joint/router-avoid, which integrates the libavoid WASM router to keep dia.Graph links orthogonally routed and obstacle-avoiding (optionally via a Web Worker), while also extracting the built-in rightAngle path-finding logic into a reusable @joint/core algorithm export (alg.rightAnglePath). It also introduces a TypeScript demo app showcasing usage and multiple routing modes.

Changes:

  • Adds @joint/router-avoid package: RouterService, main-thread and Worker providers, build/test setup (Rollup + Karma/QUnit), and full docs.
  • Refactors @joint/core rightAngle router to delegate path-finding to alg.rightAnglePath and adds useModelGeometry option (plus typings).
  • Adds examples/avoid-router-ts demo workspace and updates monorepo packaging/lockfile accordingly.

Reviewed changes

Copilot reviewed 45 out of 50 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
yarn.lock Adds workspace entries/deps for the new router package and the new demo workspace.
package.json Updates pack-all to include @joint/router-avoid.
packages/joint-router-avoid/package.json Defines the new published router package (exports, scripts, deps).
packages/joint-router-avoid/README.md Public documentation for API, events, caveats, and licensing notes.
packages/joint-router-avoid/SECURITY.md Package security policy.
packages/joint-router-avoid/LICENSE MPL-2.0 license file for the package.
packages/joint-router-avoid/.gitignore Ignores dist/node_modules/coverage outputs for the new workspace.
packages/joint-router-avoid/eslint.config.mjs ESLint flat-config wiring for the new workspace (incl. test globals).
packages/joint-router-avoid/tsconfig.json Base TS compiler configuration for the new workspace.
packages/joint-router-avoid/tsconfig.esm.json ESM build TS config for emitting dist/esm.
packages/joint-router-avoid/tsconfig.cjs.json CJS build TS config for emitting dist/cjs.
packages/joint-router-avoid/rollup.config.mjs UMD bundling configuration (banner + minified output).
packages/joint-router-avoid/karma.conf.js Browser test runner configuration for the router package (WASM proxying, coverage).
packages/joint-router-avoid/src/index.mts Public entrypoint exports for the package.
packages/joint-router-avoid/src/init.mts Entry points to load libavoid and initialize a RouterService (main-thread vs Worker).
packages/joint-router-avoid/src/RouterService.mts Core graph-listening service applying avoid/fallback routes and emitting routing lifecycle events.
packages/joint-router-avoid/src/providers/Provider.mts Provider abstraction for main-thread vs Worker-based avoid execution.
packages/joint-router-avoid/src/providers/MainThreadProvider.mts Main-thread provider implementation that drives avoid synchronously.
packages/joint-router-avoid/src/providers/WorkerProvider.mts Worker-backed provider implementation with message-based batching and sync serialization.
packages/joint-router-avoid/src/providers/Worker.mts Worker script that hosts the avoid router and debounced message processing.
packages/joint-router-avoid/test/index.html Test runner page for browser-based QUnit tests.
packages/joint-router-avoid/test/libavoid-loader.mjs Sets up the libavoidJs global for the UMD build tests.
packages/joint-router-avoid/test/index.js End-to-end QUnit tests for RouterService behavior/events (main-thread provider).
packages/joint-core/src/core.mjs Exposes alg namespace from @joint/core runtime exports.
packages/joint-core/src/alg/index.mjs Adds alg barrel export for the extracted algorithm(s).
packages/joint-core/src/alg/rightAnglePath.mjs New extracted path-finding implementation used by rightAngle and external consumers.
packages/joint-core/src/routers/rightAngle.mjs Refactors router to use rightAnglePath and adds useModelGeometry option handling.
packages/joint-core/types/index.d.ts Exposes alg namespace from @joint/core type exports.
packages/joint-core/types/alg.d.ts Declares the new alg.rightAnglePath public API in types.
packages/joint-core/types/routers.d.ts Adds useModelGeometry to RightAngleRouterArguments typings.
examples/avoid-router-ts/package.json Adds demo workspace package definition and deps.
examples/avoid-router-ts/README.md Demo README / setup and licensing notes.
examples/avoid-router-ts/.gitignore Ignores build/dist/node_modules for the demo.
examples/avoid-router-ts/tsconfig.json Demo TS compiler config.
examples/avoid-router-ts/webpack.config.js Demo webpack dev/build configuration (incl. wasm copy).
examples/avoid-router-ts/index.html Demo HTML shell with tabbed canvases.
examples/avoid-router-ts/styles.scss Demo shared styling (tabs/canvas layout).
examples/avoid-router-ts/src/index.ts Demo entrypoint wiring tab switching and lazy initialization.
examples/avoid-router-ts/src/common.ts Shared demo helpers (paper creation, zoom, link interactions).
examples/avoid-router-ts/src/simple-graph/shapes.ts Demo simple-graph custom shapes/ports.
examples/avoid-router-ts/src/simple-graph/resize-tool.ts Demo resize tool implementation.
examples/avoid-router-ts/src/simple-graph/example.ts Demo “simple graph” example using initAvoidRouter.
examples/avoid-router-ts/src/simple-graph-extra/shapes.ts Demo “simple extra” custom shapes with subgraph grouping.
examples/avoid-router-ts/src/simple-graph-extra/resize-tool.ts Demo resize tool for “simple extra”.
examples/avoid-router-ts/src/simple-graph-extra/example.ts Demo with two independent RouterService instances on one graph.
examples/avoid-router-ts/src/large-graph/shapes.ts Demo large-graph shapes/ports for worker-backed routing.
examples/avoid-router-ts/src/large-graph/example.ts Demo large-graph example using worker: true routing.
examples/avoid-router-ts/src/containers/shapes.ts Demo container and container-link shapes for isolated routing passes.
examples/avoid-router-ts/src/containers/example.ts Demo using routeSubgraph() for container-isolated one-shot routing.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread packages/joint-router-avoid/src/RouterService.mts Outdated
Comment thread packages/joint-router-avoid/src/init.mts
Comment thread packages/joint-router-avoid/src/providers/WorkerProvider.mts Outdated
- reject WorkerProvider.init() instead of hanging forever when the
  Worker fails to load or its avoid initialization fails (new 'error'
  worker response, worker.onerror/onmessageerror wiring)
- swallow the destroy()-induced rejection of the background sync
  started by start()/graph reset, so a normal teardown does not surface
  as an unhandled promise rejection
- skip the main-thread WASM load when the Worker provider is used - the
  Worker loads its own copy, the main-thread one was pure waste
- drop the unused _collection parameter from the reset handler

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 45 out of 50 changed files in this pull request and generated 2 comments.

Suppressed comments (2)

packages/joint-router-avoid/src/providers/MainThreadProvider.mts:83

  • setShape() recomputes shape.pins in the caller, but when the shape already exists this branch only calls moveShape() and returns, so pin definitions can never be added/updated after first creation. This makes the behavior depend on ports being fully known up-front; if ports are added/changed later, the provider will still have the old pin set.
    packages/joint-router-avoid/src/providers/Worker.mts:217
  • When a shape already exists, handleUpdateShape() only moves the shape rect and returns, ignoring shape.pins. As a result, pins are effectively immutable after first registration, which can cause mismatches if an element’s ports (and thus pins) are added/changed after the initial sync.

Comment thread packages/joint-router-avoid/src/RouterService.mts Outdated
Comment thread packages/joint-router-avoid/src/RouterService.mts Outdated
Applying route attributes - the consumer's setRouteAttributes callback,
or consumer change listeners reacting to link.set() - can throw. Both
apply paths now run through a shared applySafely() that closes any open
routing cycle via link:routing:cancelled before the error propagates,
so a buggy consumer cannot break the routing/routed pairing or the
idle event's accounting.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 45 out of 50 changed files in this pull request and generated no new comments.

Suppressed comments (6)

packages/joint-router-avoid/src/providers/Worker.mts:297

  • Typo in the @ts-expect-error explanation comment: "do not defined" → "not defined".
    packages/joint-router-avoid/src/providers/MainThreadProvider.mts:199
  • Typo in the @ts-expect-error explanation comment: "do not defined" → "not defined".
    packages/joint-router-avoid/src/RouterService.mts:614
  • x: x / width and y: y / height will produce Infinity/NaN when an element has a zero width/height (which can happen with programmatic models), yielding invalid pin coordinates and potentially breaking avoid routing. Consider guarding the normalization to avoid division by zero.
    packages/joint-router-avoid/src/RouterService.mts:745
  • When using setRouteAttributes, the callback never receives routing: false for final avoid-computed routes (it’s omitted entirely). Since SetRouteAttributesCallbackParameters documents routing as true for provisional and false for final, it would be clearer/safer to pass routing: false here.
    packages/joint-router-avoid/src/providers/Worker.mts:247
  • Typo in the @ts-expect-error explanation comment: "do not defined" → "not defined".

This issue also appears on line 296 of the same file.
packages/joint-router-avoid/src/providers/MainThreadProvider.mts:159

  • Typo in the @ts-expect-error explanation comment: "do not defined" → "not defined".

This issue also appears on line 198 of the same file.

- guard pin normalization against zero-size elements - dividing by a
  zero dimension fed NaN/Infinity pin coordinates into the avoid WASM
  module
- pass explicit routing: false to setRouteAttributes for final avoid
  routes, matching the documented contract
- fix '@ts-expect-error do not defined' typos

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 45 out of 50 changed files in this pull request and generated no new comments.

Suppressed comments (3)

packages/joint-core/src/alg/rightAnglePath.mjs:324

  • In the right→left S-shaped overlap branch, the fallback route starts at middleX instead of routing outward from the source (sOffsetX). This can make the first segment go into the source element rather than away from it, leading to intersecting/invalid paths.
                const middleX = (sOffsetX + tOffsetX) / 2;
                return [
                    { x: middleX, y: sOffsetY },
                    { x: middleX, y: middleY },
                    { x: middleX, y: middleY },

packages/joint-router-avoid/src/RouterService.mts:672

  • setRouting() emits link:routing every time it's called, even if the link is already in pendingLinks. During rapid changes (e.g. dragging), this can produce multiple link:routing events for a single open routing cycle, but only one link:routed to close it, which breaks the “cycle” semantics described in the doc comment and can confuse event consumers.
    packages/joint-core/src/alg/rightAnglePath.mjs:245
  • In the left→right S-shaped overlap branch, the fallback route starts at middleX instead of routing outward from the source (sOffsetX). This makes the first segment go into the source bbox (from the left side anchor toward the interior) and can produce paths that intersect the element.

This issue also appears on line 320 of the same file.

                const middleX = (sOffsetX + tOffsetX) / 2;
                return [
                    { x: middleX, y: sOffsetY },
                    { x: middleX, y: middleY },
                    { x: middleX, y: middleY },

Repeated changes while a link's routing cycle is already open (e.g.
dragging an element) re-emitted link:routing without a closing
link:routed, breaking the documented one-to-one pairing.

Co-Authored-By: Claude Fable 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.

3 participants