Skip to content

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

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

feat(joint-router-avoid): new avoid router package#3457
kumilingus wants to merge 51 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 18 commits August 13, 2026 14:06
# Conflicts:
#	packages/joint-core/src/routers/rightAngle.mjs
- 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 on lines +311 to +316
reset: (_collection: unknown) => this.sync(this.graph.getCells()),
});

this.graphListener = listener;

this.sync(this.graph.getCells());
Comment on lines +77 to +85
export async function initAvoidRouter(graph: dia.Graph, options: InitAvoidOptions = {}): Promise<RouterService> {
if (loadAvoidPromise) {
await loadAvoidPromise;
} else if (!AvoidLib.avoidLib) {
await loadAvoidRouter(options.libavoidFilePath);
}

const provider = options.worker ? new WorkerProvider() : new MainThreadProvider();

Comment on lines +60 to +82
const ready = new Promise<void>((resolve) => {
worker.onmessage = (evt: MessageEvent<WorkerResponse>) => {
const message = evt.data;
switch (message.type) {
case 'ready': {
resolve();
break;
}
case 'connectorChanged': {
this.trigger(
'connector:changed',
message.connectorId,
message.points.map((point) => new g.Point(point))
);
break;
}
case 'processed': {
this.trigger('processed');
break;
}
}
};
});
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