@erd-studio/renderer 0.1.4 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/README.md +13 -9
  3. package/dist/{chunk-DUQ2TTQH.js → chunk-64BVOHOR.js} +4 -3
  4. package/dist/chunk-7VFJNN4P.js +10461 -0
  5. package/dist/{chunk-WOTSQZCQ.js → chunk-YLXBIRPE.js} +132 -2
  6. package/dist/editor.js +51 -11
  7. package/dist/index.js +16 -5
  8. package/dist/sizing.js +2 -2
  9. package/dist/store.js +1 -1
  10. package/dist/styles.css +901 -292
  11. package/dist/types/ErdCanvas.d.ts +33 -5
  12. package/dist/types/canvas/CanvasBackdrop.d.ts +3 -2
  13. package/dist/types/canvas/EdgeRouting.d.ts +56 -0
  14. package/dist/types/canvas/useCanvasGraph.d.ts +11 -5
  15. package/dist/types/canvas/useHoverFocus.d.ts +64 -0
  16. package/dist/types/components/DetailPanel/AliasEditor.d.ts +16 -2
  17. package/dist/types/components/DetailPanel/ColumnEditor.d.ts +20 -4
  18. package/dist/types/components/DetailPanel/DescriptionEditor.d.ts +1 -1
  19. package/dist/types/components/DetailPanel/DetailPanel.d.ts +31 -2
  20. package/dist/types/components/DetailPanel/FieldsSection.d.ts +68 -0
  21. package/dist/types/components/Graph/ColumnTooltip.d.ts +1 -1
  22. package/dist/types/components/Graph/EdgeFlow.d.ts +55 -0
  23. package/dist/types/components/Graph/EdgeLabels.d.ts +18 -0
  24. package/dist/types/components/Graph/FkEdge.d.ts +8 -1
  25. package/dist/types/components/Graph/ModelNode.d.ts +4 -4
  26. package/dist/types/components/Legend/Legend.d.ts +35 -4
  27. package/dist/types/components/StatusBar/StatusBar.d.ts +45 -0
  28. package/dist/types/components/common/ColumnRowEditor.d.ts +20 -4
  29. package/dist/types/components/common/KeyBadgeGroup.d.ts +5 -1
  30. package/dist/types/editor.d.ts +10 -4
  31. package/dist/types/hooks/useColumnReorder.d.ts +1 -1
  32. package/dist/types/hooks/usePrefersReducedMotion.d.ts +13 -0
  33. package/dist/types/index.d.ts +5 -4
  34. package/dist/types/lib/badgeLabels.d.ts +7 -1
  35. package/dist/types/lib/canvasContext.d.ts +20 -0
  36. package/dist/types/lib/canvasFocus.d.ts +66 -0
  37. package/dist/types/lib/dragHandleSides.d.ts +2 -2
  38. package/dist/types/lib/edgeFlow.d.ts +84 -0
  39. package/dist/types/lib/edgeRouting/index.d.ts +146 -0
  40. package/dist/types/lib/edgeRouting/input.d.ts +35 -0
  41. package/dist/types/lib/edgeRouting/lineStyle.d.ts +16 -0
  42. package/dist/types/lib/edgeRouting/render/corners.d.ts +52 -0
  43. package/dist/types/lib/edgeRouting/render/crossings.d.ts +27 -0
  44. package/dist/types/lib/edgeRouting/render/curveCrossings.d.ts +35 -0
  45. package/dist/types/lib/edgeRouting/render/pathRender.d.ts +74 -0
  46. package/dist/types/lib/edgeRouting/render/tunnels.d.ts +49 -0
  47. package/dist/types/lib/edgeRouting/routeAll.d.ts +20 -0
  48. package/dist/types/lib/edgeRouting/routers/orthogonal/geom.d.ts +58 -0
  49. package/dist/types/lib/edgeRouting/routers/orthogonal/grid.d.ts +77 -0
  50. package/dist/types/lib/edgeRouting/routers/orthogonal/nudge.d.ts +62 -0
  51. package/dist/types/lib/edgeRouting/routers/orthogonal/params.d.ts +152 -0
  52. package/dist/types/lib/edgeRouting/routers/orthogonal/router.d.ts +51 -0
  53. package/dist/types/lib/edgeRouting/routers/orthogonal/search.d.ts +81 -0
  54. package/dist/types/lib/edgeRouting/routers/orthogonalAvoid.d.ts +11 -0
  55. package/dist/types/lib/edgeRouting/types.d.ts +113 -0
  56. package/dist/types/lib/edgeRouting/worker/client.d.ts +23 -0
  57. package/dist/types/lib/edgeRouting/worker/entry.d.ts +1 -0
  58. package/dist/types/lib/edgeRouting/worker/protocol.d.ts +23 -0
  59. package/dist/types/lib/graphTransformer.d.ts +7 -7
  60. package/dist/types/lib/hoverIntent.d.ts +44 -0
  61. package/dist/types/lib/layerCues.d.ts +26 -0
  62. package/dist/types/lib/modelLabels.d.ts +3 -3
  63. package/dist/types/lib/nodeOverlays.d.ts +10 -4
  64. package/dist/types/lib/nodeSizing.d.ts +6 -6
  65. package/dist/types/lib/pathGeometry.d.ts +45 -0
  66. package/dist/types/lib/reuseUnchanged.d.ts +28 -0
  67. package/dist/types/lib/roleOptions.d.ts +13 -0
  68. package/dist/types/store/editorStore.d.ts +65 -8
  69. package/dist/types/types/graph.d.ts +38 -14
  70. package/package.json +3 -2
  71. package/dist/chunk-IAFK7ZZV.js +0 -5576
  72. package/dist/types/components/DetailPanel/GrainEditor.d.ts +0 -16
  73. package/dist/types/components/DetailPanel/MetaEditor.d.ts +0 -21
  74. package/dist/types/components/DetailPanel/ModelRationale.d.ts +0 -27
  75. package/dist/types/components/DetailPanel/RationaleField.d.ts +0 -28
  76. package/dist/types/components/DetailPanel/RoleEditor.d.ts +0 -13
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Edge routing — how much of each segment a corner eats.
3
+ *
4
+ * Every interior vertex of a simplified polyline becomes a corner that
5
+ * starts `rIn` px before the vertex (along the incoming segment) and ends
6
+ * `rOut` px after it (along the outgoing one). Between corners, what is left
7
+ * of a segment is drawn straight; hops and cuts may only sit there.
8
+ *
9
+ * sharp 0
10
+ * rounded up to 8 px, a quadratic (today's smooth-step look)
11
+ * curved up to CURVE_RADIUS px each way, an elliptical quarter (cubic).
12
+ * An interior segment is shared by its two corners (half each);
13
+ * the first and last segments (from the card stubs) go wholly to
14
+ * their one corner, so a short step becomes one soft S. The
15
+ * curve stays inside the corner's triangle, so it only ever cuts
16
+ * the corner, never swings out; given the cards, a corner that
17
+ * would cut into one is shrunk until it clears.
18
+ */
19
+ import type { CornerMode, Point, Rect } from '../types.js';
20
+ export declare const ROUNDED_RADIUS = 8;
21
+ /** Largest radius of a curved corner. */
22
+ export declare const CURVE_RADIUS = 40;
23
+ /** Cubic control-point factor for a quarter circle (and ellipse). */
24
+ export declare const KAPPA = 0.5523;
25
+ export interface CornerRadii {
26
+ /** Length of each segment `i` (pts[i] → pts[i+1]). */
27
+ segLen: Float64Array;
28
+ /** Per vertex: px trimmed from the incoming segment (0 at the two ends). */
29
+ rIn: Float64Array;
30
+ /** Per vertex: px trimmed from the outgoing segment (0 at the two ends). */
31
+ rOut: Float64Array;
32
+ }
33
+ export declare function cornerRadii(pts: readonly Point[], mode: CornerMode, obstacles?: readonly Rect[]): CornerRadii;
34
+ /** The corner's start / end points and cubic control points. */
35
+ export declare function cornerCurve(prev: Point, cur: Point, next: Point, rIn: number, rOut: number): {
36
+ a: {
37
+ x: number;
38
+ y: number;
39
+ };
40
+ c1: {
41
+ x: number;
42
+ y: number;
43
+ };
44
+ c2: {
45
+ x: number;
46
+ y: number;
47
+ };
48
+ b: {
49
+ x: number;
50
+ y: number;
51
+ };
52
+ };
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Edge routing — where routed lines cross each other.
3
+ *
4
+ * Works on the simplified polylines (one segment per straight run). A
5
+ * crossing is *proper*: the two segments cut through each other strictly
6
+ * inside both of them. Touching ends, a corner sitting on another line,
7
+ * and collinear overlaps (a shared channel) are not crossings.
8
+ *
9
+ * A sweep over segments sorted by left edge keeps this well under a
10
+ * millisecond for a few hundred segments.
11
+ */
12
+ import type { Point } from '../types.js';
13
+ /** One end of a crossing: which line, which segment, how far along it (px). */
14
+ export interface CrossingEnd {
15
+ line: number;
16
+ seg: number;
17
+ /** Distance from the segment's first point. */
18
+ d: number;
19
+ }
20
+ export interface Crossing {
21
+ x: number;
22
+ y: number;
23
+ a: CrossingEnd;
24
+ b: CrossingEnd;
25
+ }
26
+ /** Every proper crossing between segments of different lines. */
27
+ export declare function findCrossings(polys: readonly Point[][]): Crossing[];
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Edge routing — where a drawn corner curve crosses another line's
3
+ * straight stretch.
4
+ *
5
+ * `findCrossings` works on the routed polylines, which is exact only where
6
+ * both lines are drawn straight. A rounded or curved corner cuts inside its
7
+ * vertex, so where a corner is involved the polyline crossing is in the wrong
8
+ * place (or the drawn lines do not cross at all, or cross where the
9
+ * polylines do not). With curved corners that is most crossings. This finds
10
+ * them on the drawn geometry: each corner curve flattened into chords,
11
+ * against every other line's straight stretches (the part of each segment no
12
+ * corner eats).
13
+ *
14
+ * Curve against curve is left out: there is no straight line to break.
15
+ */
16
+ import type { CornerMode, Point } from '../types.js';
17
+ import { type CornerRadii } from './corners.js';
18
+ import type { CrossingEnd } from './crossings.js';
19
+ export interface CurveCrossing {
20
+ x: number;
21
+ y: number;
22
+ /** The straight line's end, as `findCrossings` reports it (distance from the segment's first point). */
23
+ straight: CrossingEnd;
24
+ /** The curving line, its corner vertex, and how far through the corner (0..1). */
25
+ curve: {
26
+ line: number;
27
+ vertex: number;
28
+ t: number;
29
+ };
30
+ }
31
+ /**
32
+ * Every crossing between one line's corner curve and another line's straight
33
+ * stretch. `radii[i]` must be the radii the paths are drawn with.
34
+ */
35
+ export declare function findCurveCrossings(polys: readonly Point[][], radii: readonly CornerRadii[], mode: CornerMode): CurveCrossing[];
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Edge routing — turn routed polylines into SVG paths.
3
+ *
4
+ * Owns the corner style (rounded / curved / sharp) and the crossing
5
+ * treatment (hops / halo). Crossings need every line at once, so this
6
+ * takes the whole map.
7
+ *
8
+ * Who goes over at a crossing: **the more horizontal segment** (the later
9
+ * line id on a tie), as on a circuit schematic. One fixed rule reads at a
10
+ * glance — every bridge is a bump on a horizontal run, every vertical run
11
+ * passes under — where "whichever line is later" scatters bumps in both
12
+ * directions with no visible logic. Horizontal runs are also the long,
13
+ * calm stretches between cards, so a bump there disturbs the least. When the
14
+ * over line has no room at that spot (a corner or its end is too close), the
15
+ * other line takes the treatment; when neither has room, the crossing is
16
+ * drawn plain.
17
+ *
18
+ * hops the over line jumps the under one with a semicircle of
19
+ * HOP_RADIUS, drawn as two cubics (EdgeFlow's `measurePath` reads
20
+ * M L H V Q C only, so no `A`). Crossings closer than
21
+ * 2 × HOP_RADIUS + MERGE_SLACK merge into one flat-topped bridge.
22
+ * halo `halo: true`: FkEdge draws a canvas-coloured casing under every
23
+ * line. Left to paint order, the casing would let whichever line
24
+ * React Flow draws LATER win each crossing, a different rule from
25
+ * hops and a random-looking one. So the under line is also cut
26
+ * (`strokeD`, HALO_CUT_HALF either side) and FkEdge draws both its
27
+ * casing and its stroke from that: an under line has no casing at
28
+ * the crossing, so it can never paint over the line above it. `d`
29
+ * stays ONE continuous subpath (EdgeFlow and the hit area need one).
30
+ *
31
+ * Paths use absolute M / L / Q / C only, one subpath (`strokeD` aside), and
32
+ * numbers rounded to 0.01 px.
33
+ */
34
+ import type { CornerMode, CrossingMode, Point, Rect, RoutedLine } from '../types.js';
35
+ export interface RenderOptions {
36
+ crossings: CrossingMode;
37
+ corners: CornerMode;
38
+ /**
39
+ * The cards (optional). With them, a curved corner that would cut into a
40
+ * card is tightened until it clears.
41
+ */
42
+ obstacles?: readonly Rect[];
43
+ /**
44
+ * The canvas zoom (default 1). Hops are sized in screen px, so they stay
45
+ * readable as bridges when the diagram is zoomed out (see `hopRadiusFor`).
46
+ */
47
+ zoom?: number;
48
+ }
49
+ export interface RenderedPath {
50
+ /** One continuous subpath: draw the halo and the hit area with it; EdgeFlow follows it. */
51
+ d: string;
52
+ mid: Point;
53
+ halo?: boolean;
54
+ /**
55
+ * crossings = 'halo', on a line that passes under another: `d`
56
+ * with the breaks cut out (several `M` subpaths). Draw the visible stroke
57
+ * (and the halo casing) with this when present; keep `d` for EdgeFlow and
58
+ * the hit area.
59
+ */
60
+ strokeD?: string;
61
+ }
62
+ /** Radius (px) of a hop's semicircle at zoom 1. */
63
+ export declare const HOP_RADIUS = 6;
64
+ /** Hop radius in flow px for a canvas zoom: HOP_SCREEN_RADIUS on screen, between HOP_RADIUS and HOP_MAX_RADIUS. */
65
+ export declare function hopRadiusFor(zoom: number | undefined): number;
66
+ /** Half the cut (px) in an under line in halo mode: the casing's half width (3.5) plus a hair. */
67
+ export declare const HALO_CUT_HALF = 4.5;
68
+ /** Drop repeated points and points in the middle of a straight run. */
69
+ export declare function simplify(points: readonly Point[]): Point[];
70
+ /** A polyline with each corner rounded by up to `radius`, no crossings treatment. */
71
+ export declare function roundedPath(points: readonly Point[], radius?: number): string;
72
+ /** The point halfway along a polyline, by length. */
73
+ export declare function polylineMidpoint(points: readonly Point[]): Point;
74
+ export declare function renderPaths(lines: Map<string, RoutedLine>, opts: RenderOptions): Map<string, RenderedPath>;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Edge routing — "x-ray": the stretches of a line that pass under a card.
3
+ *
4
+ * Cards are drawn above the lines, so any stretch of a line inside a card's
5
+ * box is hidden. `computeTunnels` clips the line's polyline against every
6
+ * box (tables and sticky notes alike, the line's own two ends included: a
7
+ * line that doubles back across its own table is hidden there too) and
8
+ * returns each hidden stretch as a polyline. `EdgeOverlay` draws them above
9
+ * the cards, with a "portal" where the line dives under a border and where it
10
+ * comes back out.
11
+ *
12
+ * - Boxes are shrunk by `EDGE_TOLERANCE` first, so a line that grazes a
13
+ * corner is not "under" anything; a straight stretch parallel to a border
14
+ * and within `BORDER_HUG` of it runs "along" the border, not under the card.
15
+ * - Overlapping or nearly touching boxes (a gap under `MERGE_GAP`) give one
16
+ * tunnel, not a row of portals 4 px apart.
17
+ * - Stretches shorter than `MIN_TUNNEL` (a rounded corner clipping a box
18
+ * corner) are dropped.
19
+ * - A stretch at the line's own end that is only under the line's own two
20
+ * cards is dropped: when two related cards nearly touch, the 20 px stub
21
+ * starts inside the other card and the line hooks round inside the pair.
22
+ * Showing that hook says nothing the reader does not know (the line joins
23
+ * those two cards) and puts dashes in two headers.
24
+ *
25
+ * Pure and allocation-light: it runs for every line on every frame of a drag.
26
+ * A box is tested against a segment only when their bounding boxes meet.
27
+ */
28
+ import type { EdgeSpec, Point, Rect, RoutedLine } from '../types.js';
29
+ /** A line closer than this (px) to a border counts as outside the box. */
30
+ export declare const EDGE_TOLERANCE = 1;
31
+ /**
32
+ * A straight stretch running parallel to a border and closer to it than this
33
+ * (px) is "along the border", not under the card: drawn, its ghost reads as
34
+ * a second, dashed border a few px inside the real one (seen on 'big': 127 px
35
+ * of it 3 px inside dim_supplier's bottom). Like every threshold it flips
36
+ * somewhere during a drag; pops stay rare (~1 in 1500 nudges on the scenarios).
37
+ */
38
+ export declare const BORDER_HUG = 6;
39
+ /** Hidden stretches closer together than this (px) along the line become one tunnel. */
40
+ export declare const MERGE_GAP = 12;
41
+ /** Hidden stretches shorter than this (px) are not worth a tunnel. */
42
+ export declare const MIN_TUNNEL = 6;
43
+ /**
44
+ * The hidden stretches of `line` as polylines: hidden by `obstacles` (every
45
+ * card box). Each polyline starts where the line dives under a border and ends
46
+ * where it comes back out (or where the line itself starts / ends, if an end
47
+ * sits inside another card).
48
+ */
49
+ export declare function computeTunnels(line: RoutedLine, spec: EdgeSpec | undefined, obstacles: Rect[]): Point[][];
@@ -0,0 +1,20 @@
1
+ /**
2
+ * `routeAll`: route and draw every line of one diagram in one pass. Its own
3
+ * module (no React Flow, no React) so the line-routing worker can bundle it
4
+ * without the canvas code around it.
5
+ */
6
+ import type { LineRouter, LineStyleOptions, RouteResult, RoutingInput } from './types.js';
7
+ /** A router that throws must never take the canvas down: fall back. */
8
+ export declare function safely<T>(fn: () => T, fallback: T, onError: (err: unknown) => void): T;
9
+ export interface RouteAllOptions {
10
+ /** The router (and its memory of the last pass). Default: a fresh route-around router, so the call keeps no state. */
11
+ router?: LineRouter;
12
+ /** Clock for `millis` (default: none, `millis` is 0). */
13
+ now?: () => number;
14
+ /** The canvas zoom: hops are sized for it (`hopRadiusFor`). Default 1. */
15
+ zoom?: number;
16
+ /** Called with a router or renderer error (the pass still returns, with fewer or no routes). */
17
+ onError?: (err: unknown) => void;
18
+ }
19
+ /** Route and draw every line of `input` in one pass. Never throws. */
20
+ export declare function routeAll(input: RoutingInput, options: LineStyleOptions, ctx?: RouteAllOptions): RouteResult;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Route-around router — small geometry helpers shared by the grid, the search
3
+ * and the nudging pass. Orthogonal only: every segment is horizontal or vertical.
4
+ */
5
+ import { type Anchor, type Point, type Rect, type Side } from "../../types.js";
6
+ /** Leg orientation: 0 = horizontal, 1 = vertical. */
7
+ export type Orient = 0 | 1;
8
+ /** The sides tried for a port, in a fixed order (determinism). */
9
+ export declare const SIDES: readonly Side[];
10
+ /** Orientation of the straight leg that leaves (or enters) a side. */
11
+ export declare function legOrient(side: Side): Orient;
12
+ /** Direction of travel along that leg when leaving the card (+1 = towards +x / +y). */
13
+ export declare function outDir(side: Side): 1 | -1;
14
+ /** Where a side lies across its leg: x of a left/right side, y of a top/bottom side. */
15
+ export declare function sideLine(r: Rect, side: Side): number;
16
+ /** The span of a side along itself: [y0, y1] for left/right, [x0, x1] for top/bottom. */
17
+ export declare function sideSpan(r: Rect, side: Side): [number, number];
18
+ /** Where anchors may sit along a side: the span minus a corner inset (the centre for a short side). */
19
+ export declare function anchorRange(r: Rect, side: Side): [number, number];
20
+ export declare function sideMid(r: Rect, side: Side): number;
21
+ /** The port: the point `c` along the side, pushed PATH_GAP out (where the drawn line starts). */
22
+ export declare function portPoint(r: Rect, side: Side, c: number): Point;
23
+ /** The anchor on the card border behind a port. */
24
+ export declare function anchorBehind(port: Point, side: Side): Anchor;
25
+ export declare function pad(r: Rect, by: number): {
26
+ x0: number;
27
+ y0: number;
28
+ x1: number;
29
+ y1: number;
30
+ };
31
+ /** Do two rects (each grown by its own amount) overlap with positive area? */
32
+ export declare function rectsOverlap(a: Rect, growA: number, b: Rect, growB: number): boolean;
33
+ /** Drop repeated points and the middle point of any straight run. */
34
+ export declare function simplifyOrthogonal(points: Point[]): Point[];
35
+ /**
36
+ * Does the axis-aligned segment a→b pass through the inside of `r` (shrunk by
37
+ * `tol` on every side)? Touching the border does not count.
38
+ */
39
+ export declare function segmentEntersRect(a: Point, b: Point, r: Rect, tol: number): boolean;
40
+ /**
41
+ * A uniform bucket grid over the cards, for "which cards are near this
42
+ * segment" queries in the nudging and validation passes.
43
+ */
44
+ export declare class Buckets {
45
+ readonly rects: readonly Rect[];
46
+ private readonly x0;
47
+ private readonly y0;
48
+ private readonly cols;
49
+ private readonly rows;
50
+ private readonly cells;
51
+ private readonly seen;
52
+ private stamp;
53
+ private readonly cell;
54
+ constructor(rects: readonly Rect[], cell?: number);
55
+ private cellOf;
56
+ /** Indices of the rects whose bucket cells meet the box (a superset of the rects that meet it). */
57
+ query(x0: number, y0: number, x1: number, y1: number, out?: number[]): number[];
58
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Route-around router — the routing grid.
3
+ *
4
+ * A sparse orthogonal grid (Wybrow–Marriott–Stuckey style): its lines are the
5
+ * padded edges of every card, channel midlines between neighbouring cards,
6
+ * the centre of every card side, and the line where two related cards'
7
+ * sides overlap (so a straight line between them is on offer). A node is
8
+ * every crossing of an x-line and a y-line. Every padded card's edges are
9
+ * grid lines, so each elementary grid edge is either wholly inside a padded
10
+ * card or wholly outside it; `hBlk` / `vBlk` count the padded cards each one
11
+ * runs through, so a card can be lifted out again (`markRect(…, -1)`) for a
12
+ * relaxed retry.
13
+ *
14
+ * Node index n = j * nx + i (column i, row j).
15
+ * hBlk[n] the edge from (i, j) to (i + 1, j)
16
+ * vBlk[n] the edge from (i, j) to (i, j + 1)
17
+ * hUse / vUse how many lines routed so far run along that edge.
18
+ */
19
+ export interface Grid {
20
+ xs: Float64Array;
21
+ ys: Float64Array;
22
+ nx: number;
23
+ ny: number;
24
+ hBlk: Uint16Array;
25
+ vBlk: Uint16Array;
26
+ hUse: Uint8Array;
27
+ vUse: Uint8Array;
28
+ /** Anchor zones: the band in front of a card side where lines attach (closed to other lines). */
29
+ hZone: Uint8Array;
30
+ vZone: Uint8Array;
31
+ }
32
+ /** Sort and merge values closer than MERGE_EPS (each cluster keeps its smallest value). */
33
+ export declare function uniqueSorted(values: number[]): Float64Array;
34
+ /**
35
+ * The grid lines: every `hard` value (a padded card edge, which the blocking
36
+ * needs exactly) plus each `soft` value (side centres, midlines, straight-line
37
+ * options) that is more than `softEps` from every line kept so far. A cluster
38
+ * of lines a few px apart adds nodes but no real routing choice, and A* pays
39
+ * for every node.
40
+ */
41
+ export declare function gridLines(hard: number[], soft: number[], softEps: number): Float64Array;
42
+ /** The largest index i with arr[i] <= v (+ a hair), or -1. */
43
+ export declare function floorIndex(arr: Float64Array, v: number): number;
44
+ /** The smallest index i with arr[i] >= v (- a hair), or arr.length. */
45
+ export declare function ceilIndex(arr: Float64Array, v: number): number;
46
+ export declare function makeGrid(xs: Float64Array, ys: Float64Array): Grid;
47
+ /**
48
+ * Add (delta +1) or remove (-1) a padded card: every grid edge strictly
49
+ * inside the box. The box's edges must be grid lines (they snap to the line
50
+ * they were merged into).
51
+ */
52
+ export declare function markRect(g: Grid, x0: number, y0: number, x1: number, y1: number, delta: 1 | -1): void;
53
+ /** Count a routed line's grid edges (consecutive nodes of a search result). */
54
+ export declare function markUse(g: Grid, nodes: readonly number[]): void;
55
+ /**
56
+ * Close (delta +1) or reopen (-1) a box that is not grid-aligned: every grid
57
+ * edge that runs inside it, or crosses into it, is marked. Used for the
58
+ * anchor zone in front of a card side.
59
+ */
60
+ export declare function markZone(g: Grid, x0: number, y0: number, x1: number, y1: number, delta: 1 | -1): void;
61
+ /**
62
+ * Block (+1) or unblock (-1) a box that is not grid-aligned, like `markZone`
63
+ * but in the hard block counts (used for the band round a line's own cards).
64
+ */
65
+ export declare function markBand(g: Grid, x0: number, y0: number, x1: number, y1: number, delta: 1 | -1): void;
66
+ /** The index of the value within `eps` of `v` (the nearest one), or -1. */
67
+ export declare function nearIndex(arr: Float64Array, v: number, eps: number): number;
68
+ /**
69
+ * Count a polyline's grid edges as used, from its geometry (a route kept from
70
+ * an earlier pass, whose grid has changed since, or one routed on another
71
+ * line's local grid). Segments not within `eps` of a grid line are skipped:
72
+ * usage is only a hint to the lines routed after it.
73
+ */
74
+ export declare function markPathUse(g: Grid, pts: readonly {
75
+ x: number;
76
+ y: number;
77
+ }[], eps?: number): void;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Route-around router — after every line has a route, make the routes read
3
+ * as separate wires.
4
+ *
5
+ * For one dimension at a time (vertical segments, then horizontal):
6
+ *
7
+ * 1. Centre. A middle segment with both neighbours heading the same way (a
8
+ * "Z") can slide anywhere between them without changing the length or the
9
+ * bends; slide it to the middle of the free space (between the cards
10
+ * around it, and between its neighbours' far ends). Lines then run down
11
+ * the middle of a channel, and a step between two cards turns half-way,
12
+ * as today's smooth step does.
13
+ *
14
+ * 2. Nudge. Segments that share a channel (parallel, overlapping, within a
15
+ * few px) and the end segments that meet one card side are put in an
16
+ * order that does not make them cross, then spread apart: LINE_SEP in a
17
+ * channel, ANCHOR_SEP between anchors on a card side (moving an end
18
+ * segment slides its anchor along the side). The order comes from where
19
+ * the lines go at the ends of the shared stretch (Wybrow, Marriott &
20
+ * Stuckey's "ordering paths in a shared channel"): if one line turns off
21
+ * first, the other must sit on the far side of that turn. Where two lines
22
+ * turn together, the comparison follows them round the corner.
23
+ *
24
+ * Every move is bounded so a segment never comes closer than CLEAR to a
25
+ * card (sweep-free), anchors stay off the corners, and the first / last bend
26
+ * stays MIN_LEG past the stub.
27
+ */
28
+ import type { Point, Rect, Side } from "../../types.js";
29
+ import { type Buckets } from "./geom.js";
30
+ export interface NudgePath {
31
+ /** From the source port to the target port; mutated in place. */
32
+ pts: Point[];
33
+ src: Rect;
34
+ srcSide: Side;
35
+ tgt: Rect;
36
+ tgtSide: Side;
37
+ /** Tie-break order (by edge id). */
38
+ rank: number;
39
+ /** Indices of cards this line may pass under (ends buried under another card). */
40
+ ignore: ReadonlySet<number> | null;
41
+ /** Indices of its own two cards: kept OWN_CLEAR away, except along the stubs. */
42
+ own: readonly [number, number];
43
+ }
44
+ export interface NudgeContext {
45
+ /** Cards, self-loops, then (from `zoneFrom` on) anchor zones. */
46
+ rects: readonly Rect[];
47
+ buckets: Buckets;
48
+ /** Index of the first anchor zone in `rects`: zones need no extra clearance. */
49
+ zoneFrom: number;
50
+ }
51
+ /**
52
+ * Where a line steps sideways (segment k between two parallel segments that
53
+ * head the same way, a "Z"), slide one of the two onto the other's line:
54
+ * two bends fewer and never longer. Allowed when the area the segment
55
+ * sweeps is clear of cards, loops and anchor zones (by MARGIN), an end
56
+ * segment's anchor stays on its side, the neighbour beyond keeps its
57
+ * direction and length (a stub keeps MIN_LEG), and the line crosses no more
58
+ * of the others than before. Weighted A* and the grid both leave such jogs.
59
+ */
60
+ export declare function straightenPass(paths: NudgePath[], ctx: NudgeContext): void;
61
+ export declare function centrePass(paths: NudgePath[], vert: boolean, ctx: NudgeContext): void;
62
+ export declare function nudgePass(paths: NudgePath[], vert: boolean, ctx: NudgeContext): void;
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Route-around router — tuning knobs, in flow px (or px-equivalent costs).
3
+ *
4
+ * card ┃<- PATH_GAP 20 ->● stub end ── MIN_LEG 10 ──┐ first bend (≥ 30 out)
5
+ * ┃<- MARGIN 16 -> routes keep this far from every card
6
+ * ┃<- ZONE 42 -> no other line runs in front of a side with stubs
7
+ *
8
+ * The values were tuned against hand-placed scenarios
9
+ * (`test/fixtures/edgeRouting/scenarios.ts`): screenshots, crossings / bends /
10
+ * length, and A* expansions.
11
+ */
12
+ /** Routes keep at least this far from every card (the routing grid pads each card by it). */
13
+ export declare const MARGIN = 16;
14
+ /** Nudging parallel lines apart never brings a line closer than this to a card. */
15
+ export declare const CLEAR = 16;
16
+ /** Gap between parallel lines that share a channel (fit-to-view often draws at ~0.7×). */
17
+ export declare const LINE_SEP = 12;
18
+ /** Gap between twins (two lines joining the same two cards): their role labels must clear each other. */
19
+ export declare const TWIN_SEP = 30;
20
+ /** Gap between two anchors on one card side: room for the `*` / `1` of each. */
21
+ export declare const ANCHOR_SEP = 24;
22
+ /** Anchors keep this far from a card's corners. */
23
+ export declare const ANCHOR_INSET = 20;
24
+ /** Straight run past the stub before a line may bend. */
25
+ export declare const MIN_LEG = 10;
26
+ /** Shortest run the centring pass leaves next to a bend. */
27
+ export declare const MIN_RUN = 10;
28
+ /**
29
+ * A line keeps this far from its own two cards everywhere but along its two
30
+ * stubs (PATH_GAP + MIN_LEG - 1): it never runs between a card and its stubs.
31
+ */
32
+ export declare const OWN_CLEAR = 29;
33
+ /**
34
+ * Depth of the anchor zone in front of a card side that has lines attached:
35
+ * the stubs, their `*` / `1` and their first bends live there, and a line
36
+ * running past inside it reads as joined to them. Other lines pay ZONE_COST
37
+ * to run there; a line's own two cards' zones are open to it.
38
+ */
39
+ export declare const ZONE = 42;
40
+ /**
41
+ * Extra cost per px of running inside another card side's anchor zone (< 0:
42
+ * zones are closed outright). Closed zones send lines in a crowded diagram on
43
+ * long detours round the whole thing; at 2 a line cuts through only when the
44
+ * way round is about three times as long.
45
+ */
46
+ export declare const ZONE_COST = 2;
47
+ /** Cost of one bend, in px of length. */
48
+ export declare const BEND = 40;
49
+ /**
50
+ * Cost of crossing a line routed earlier: a crossing is worth a two-bend
51
+ * detour (at 40 the tangle scenario had twice the crossings).
52
+ */
53
+ export declare const CROSS = 80;
54
+ /**
55
+ * A crossing on another line's stub (inside an anchor zone, or on this line's
56
+ * own leg) costs this many times CROSS: right next to a `*` / `1` it reads as
57
+ * a T junction, as if the lines joined there.
58
+ */
59
+ export declare const STUB_CROSS = 3;
60
+ /** Extra cost per px of grid edge earlier lines already use (spreads lines over the channels). */
61
+ export declare const SHARE = 0.25;
62
+ /**
63
+ * Cost of an anchor slot right on top of another line's anchor on the same
64
+ * side (falling to 0 at ANCHOR_SEP away): a side has only so much room, so a
65
+ * later line takes a free slot or another side. Twins are exempt (they pair up).
66
+ */
67
+ export declare const SLOT = 400;
68
+ /**
69
+ * Hysteresis for a line routed again (its card moved, or a card came near it):
70
+ * taking a side other than the one it had costs STICK_SIDE (more than a bend,
71
+ * less than two), and a slot on its old side costs STICK_SLOT per px away from
72
+ * where its anchor was (carried along with the card). Without these a drag
73
+ * re-plans every line on the moved card from scratch each frame, and a 1 px
74
+ * move can swap the grid line a slot sits on: anchors jumped 20-50 px and
75
+ * lines flipped sides on ~10% of drag frames in the tangle and 40-card scenarios.
76
+ */
77
+ export declare const STICK_SIDE = 100;
78
+ export declare const STICK_SLOT = 2;
79
+ /** …applied only when at most this many boxes changed since the last pass (`changed` holds each moved box twice: old and new). */
80
+ export declare const STICK_MAX_CHANGED = 8;
81
+ /** Cost per px an anchor sits off the centre of its side: a straight line is worth a shift… */
82
+ export declare const SHIFT = 1.1;
83
+ /** …plus this per px², so an aligned straight line settles in the middle of the overlap. */
84
+ export declare const SHIFT_SQ = 0.002;
85
+ /** Grid lines closer than this are one line. */
86
+ export declare const MERGE_EPS = 1;
87
+ /**
88
+ * A soft grid line (side centre, midline, straight-line option) within this
89
+ * of another line is dropped: a cluster of near-identical lines adds A* nodes,
90
+ * not routing choices (on the 40-card scenario this alone cut expansions by 3×).
91
+ */
92
+ export declare const SOFT_EPS = 12;
93
+ /** Parallel segments closer than this share a channel: they are ordered by where they go. */
94
+ export declare const NEAR = 3;
95
+ /** Segments this close that a group's spreading swapped are re-solved together. */
96
+ export declare const RELATE_WINDOW = 40;
97
+ /** The furthest the nudging pass moves a segment. */
98
+ export declare const MAX_SHIFT = 120;
99
+ /**
100
+ * Weighted A*: the heuristic is scaled by this. Crossing and sharing costs
101
+ * make the length + bends estimate loose, and an unweighted search floods
102
+ * the channels; 1.1 keeps the routes a designer would draw (1.2 already
103
+ * sends the odd long line the long way round).
104
+ */
105
+ export declare const HEURISTIC_WEIGHT = 1.1;
106
+ /**
107
+ * Lines spanning more than LONG_FROM px (centre to centre, Manhattan) get up
108
+ * to LONG_EXTRA more weight, reached LONG_RAMP px further on: on the 40-card
109
+ * scenario a quarter fewer expansions for a few more crossings, while every
110
+ * line of a normal diagram (the star's longest spans 1,600 px) stays at 1.1.
111
+ */
112
+ export declare const LONG_FROM = 1600;
113
+ export declare const LONG_RAMP = 1200;
114
+ export declare const LONG_EXTRA = 0.3;
115
+ /**
116
+ * …and their crossings cost up to this much less: across a whole diagram
117
+ * most are unavoidable, and pricing them in full doubles the search.
118
+ */
119
+ export declare const LONG_CROSS_CUT = 0.7;
120
+ /** A* gives up (and the line falls back) after this many expansions. */
121
+ export declare const MAX_POPS = 40000;
122
+ /**
123
+ * Diagrams with more cards (and self-loops) than this route each line on its
124
+ * own local grid (router.ts, `localGrid`): one grid over the whole diagram
125
+ * grows with the square of its size (every card's edges cross every other
126
+ * card's), to millions of nodes on an uneven 1,000-card layout.
127
+ */
128
+ export declare const LOCAL_FROM_CARDS = 150;
129
+ /** A local grid starts this far (px) round its line's two cards: about one card and a gap. */
130
+ export declare const LOCAL_MARGIN = 400;
131
+ /** …and its margin grows by this factor each time the route runs along its edge. */
132
+ export declare const LOCAL_GROWTH = 2.5;
133
+ /** A local grid larger than this (nodes) is not built: the line keeps what it found, or falls back. */
134
+ export declare const LOCAL_MAX_NODES = 1500000;
135
+ /**
136
+ * On local grids (large diagrams), long lines are greedier still: from
137
+ * LOCAL_LONG_FROM px they get up to LOCAL_LONG_EXTRA more heuristic weight
138
+ * and crossings up to LOCAL_LONG_CROSS_CUT cheaper. A line across a 2,000-card
139
+ * layered diagram otherwise runs out of MAX_POPS among the other lines and
140
+ * falls back (161 of 2,732 did); with these none do, for ~2.5% more crossings.
141
+ */
142
+ export declare const LOCAL_LONG_FROM = 1200;
143
+ export declare const LOCAL_LONG_EXTRA = 0.9;
144
+ export declare const LOCAL_LONG_CROSS_CUT = 0.85;
145
+ /**
146
+ * On local grids, a line whose search runs out of MAX_POPS gets one greedy
147
+ * retry before it falls back: this heuristic weight, and crossings at this
148
+ * fraction of CROSS. A few more crossings beat a line drawn through every card
149
+ * between its two ends (lines between neighbouring columns 5,000 px apart).
150
+ */
151
+ export declare const RESCUE_WEIGHT = 3;
152
+ export declare const RESCUE_CROSS = 0.1;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Route-around router — the whole pass.
3
+ *
4
+ * 1. Grid: the padded cards' edges, channel midlines, side centres, and per
5
+ * line the middle of the gap between its two cards and the straight-line
6
+ * option where their sides overlap (grid.ts). Self-loops are left to the
7
+ * canvas, but the space their loop takes is an obstacle.
8
+ * 2. Route every line, shortest first, with A* from any free anchor slot on
9
+ * any side of its source to any on its target (search.ts). The router
10
+ * picks the side: a line may leave the side the transformer chose when
11
+ * another is clearly better. Lines routed later pay to cross or run
12
+ * along earlier ones, so they spread over the free channels, and once a
13
+ * card side has a line attached, the zone in front of it (stubs and
14
+ * `*` / `1` labels) is closed to every other line.
15
+ * Before the first line is routed, the zones in front of the sides the
16
+ * canvas picked for every line are marked too, and a crossing on a stub
17
+ * (between a card and its `*` / `1`, or on the leg out of it) costs
18
+ * STUB_CROSS times more: it reads as a T junction.
19
+ * In a drag, lines away from the moved card keep their routes; a line
20
+ * routed again prefers the sides and slots it had (hysteresis), so it
21
+ * does not jump about as its card moves a pixel at a time.
22
+ * 3. Nudge: centre free segments, then order and spread lines that share a
23
+ * channel or a card side, so they read as separate wires and never
24
+ * cross right at a card (nudge.ts).
25
+ * 4. Check: no segment may pass through a card other than one the line had
26
+ * to ignore. A line that can't be routed at all (its end is buried under
27
+ * another card) falls back to today's simple three-segment path.
28
+ */
29
+ import { type RoutedLine, type RoutingInput } from "../../types.js";
30
+ export interface AvoidResult {
31
+ lines: Map<string, RoutedLine>;
32
+ /** Lines with no route around the cards: they got today's three-segment path. */
33
+ fallbacks: Set<string>;
34
+ /** Lines routed only by letting them pass under cards that crowd their ends. */
35
+ relaxed: Map<string, Set<string>>;
36
+ /** Grid size and A* expansions, for tests and the perf readout. */
37
+ stats: {
38
+ gridX: number;
39
+ gridY: number;
40
+ pops: number;
41
+ reused: number;
42
+ };
43
+ }
44
+ /** A route-around router with its own memory of the last pass. */
45
+ export interface AvoidRouter {
46
+ /** Route every line; lines away from what changed since the last pass keep their routes. */
47
+ route(input: RoutingInput): AvoidResult;
48
+ /** Drop the last pass: the next `route` routes every line from scratch. */
49
+ reset(): void;
50
+ }
51
+ export declare function createAvoidRouter(): AvoidRouter;