@oh-just-another/renderer-core 0.57.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 (93) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/LICENSE +21 -0
  3. package/README.md +40 -0
  4. package/dist/.tsbuildinfo +1 -0
  5. package/dist/animation-adapter.d.ts +91 -0
  6. package/dist/animation-adapter.d.ts.map +1 -0
  7. package/dist/animation-adapter.js +90 -0
  8. package/dist/animation-adapter.js.map +1 -0
  9. package/dist/built-in-renderers.d.ts +6 -0
  10. package/dist/built-in-renderers.d.ts.map +1 -0
  11. package/dist/built-in-renderers.js +517 -0
  12. package/dist/built-in-renderers.js.map +1 -0
  13. package/dist/constants.d.ts +97 -0
  14. package/dist/constants.d.ts.map +1 -0
  15. package/dist/constants.js +115 -0
  16. package/dist/constants.js.map +1 -0
  17. package/dist/edge-cache-bitmap.d.ts +34 -0
  18. package/dist/edge-cache-bitmap.d.ts.map +1 -0
  19. package/dist/edge-cache-bitmap.js +42 -0
  20. package/dist/edge-cache-bitmap.js.map +1 -0
  21. package/dist/edge-cache.d.ts +32 -0
  22. package/dist/edge-cache.d.ts.map +1 -0
  23. package/dist/edge-cache.js +90 -0
  24. package/dist/edge-cache.js.map +1 -0
  25. package/dist/edge-renderer.d.ts +73 -0
  26. package/dist/edge-renderer.d.ts.map +1 -0
  27. package/dist/edge-renderer.js +554 -0
  28. package/dist/edge-renderer.js.map +1 -0
  29. package/dist/grid-renderer.d.ts +55 -0
  30. package/dist/grid-renderer.d.ts.map +1 -0
  31. package/dist/grid-renderer.js +173 -0
  32. package/dist/grid-renderer.js.map +1 -0
  33. package/dist/index.d.ts +37 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +24 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/js-rasterizer.d.ts +8 -0
  38. package/dist/js-rasterizer.d.ts.map +1 -0
  39. package/dist/js-rasterizer.js +86 -0
  40. package/dist/js-rasterizer.js.map +1 -0
  41. package/dist/layer-cache-composite.d.ts +39 -0
  42. package/dist/layer-cache-composite.d.ts.map +1 -0
  43. package/dist/layer-cache-composite.js +47 -0
  44. package/dist/layer-cache-composite.js.map +1 -0
  45. package/dist/layers.d.ts +14 -0
  46. package/dist/layers.d.ts.map +1 -0
  47. package/dist/layers.js +2 -0
  48. package/dist/layers.js.map +1 -0
  49. package/dist/rasterizer.d.ts +27 -0
  50. package/dist/rasterizer.d.ts.map +1 -0
  51. package/dist/rasterizer.js +10 -0
  52. package/dist/rasterizer.js.map +1 -0
  53. package/dist/render-target.d.ts +94 -0
  54. package/dist/render-target.d.ts.map +1 -0
  55. package/dist/render-target.js +2 -0
  56. package/dist/render-target.js.map +1 -0
  57. package/dist/scene-renderer.d.ts +126 -0
  58. package/dist/scene-renderer.d.ts.map +1 -0
  59. package/dist/scene-renderer.js +149 -0
  60. package/dist/scene-renderer.js.map +1 -0
  61. package/dist/shape-cache-bitmap.d.ts +42 -0
  62. package/dist/shape-cache-bitmap.d.ts.map +1 -0
  63. package/dist/shape-cache-bitmap.js +61 -0
  64. package/dist/shape-cache-bitmap.js.map +1 -0
  65. package/dist/shape-cache.d.ts +40 -0
  66. package/dist/shape-cache.d.ts.map +1 -0
  67. package/dist/shape-cache.js +67 -0
  68. package/dist/shape-cache.js.map +1 -0
  69. package/dist/shape-renderer.d.ts +36 -0
  70. package/dist/shape-renderer.d.ts.map +1 -0
  71. package/dist/shape-renderer.js +15 -0
  72. package/dist/shape-renderer.js.map +1 -0
  73. package/dist/text-editing.d.ts +81 -0
  74. package/dist/text-editing.d.ts.map +1 -0
  75. package/dist/text-editing.js +191 -0
  76. package/dist/text-editing.js.map +1 -0
  77. package/dist/text-layout.d.ts +44 -0
  78. package/dist/text-layout.d.ts.map +1 -0
  79. package/dist/text-layout.js +57 -0
  80. package/dist/text-layout.js.map +1 -0
  81. package/dist/text-shaper.d.ts +53 -0
  82. package/dist/text-shaper.d.ts.map +1 -0
  83. package/dist/text-shaper.js +19 -0
  84. package/dist/text-shaper.js.map +1 -0
  85. package/dist/tile-renderer.d.ts +126 -0
  86. package/dist/tile-renderer.d.ts.map +1 -0
  87. package/dist/tile-renderer.js +192 -0
  88. package/dist/tile-renderer.js.map +1 -0
  89. package/dist/worker-render.d.ts +54 -0
  90. package/dist/worker-render.d.ts.map +1 -0
  91. package/dist/worker-render.js +8 -0
  92. package/dist/worker-render.js.map +1 -0
  93. package/package.json +56 -0
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Tunable constants for the renderer core. All "magic numbers" used by
3
+ * `renderScene` / `renderLinks` / `renderGrid` live here so there is one
4
+ * place to tweak performance / visual behaviour.
5
+ */
6
+ import { GRID_COLOR, GRID_DOT_COLOR } from "@oh-just-another/tokens";
7
+ /**
8
+ * Default zoom thresholds for the level-of-detail pipeline. Tuned for
9
+ * a typical 1920×1080 viewport viewing a 10k-shape scene.
10
+ *
11
+ * - `placeholder: 0.15` — below 15% zoom (one screen ≈ 6.7× world),
12
+ * shapes degrade to flat AABB fills. Saves ~10× renderer cost per
13
+ * shape.
14
+ * - `hideText: 0.4` — below 40% zoom text glyphs are too small to read
15
+ * anyway; skipping the `wrapText` + `measureText` calls saves the
16
+ * bulk of text rendering cost.
17
+ *
18
+ * Hosts override per-render by passing `RenderSceneOptions.lod`.
19
+ */
20
+ export const DEFAULT_LOD = {
21
+ placeholder: 0.15,
22
+ hideText: 0.4,
23
+ };
24
+ /**
25
+ * Grey colour used for placeholder fills when LOD switches to the
26
+ * cheapest path. A mid-tone neutral that blends with most scene
27
+ * palettes; override via `RenderSceneOptions.placeholderFill`.
28
+ */
29
+ export const DEFAULT_PLACEHOLDER_FILL = "#bbb";
30
+ /**
31
+ * Viewport-rect inflation factor applied by hosts when computing the
32
+ * world-space culling rect. 0.05 = 5% padding on each side — enough
33
+ * to avoid flicker during a one-frame pan without keeping much
34
+ * off-screen geometry alive in the renderer.
35
+ */
36
+ export const VIEWPORT_CULL_PADDING_RATIO = 0.05;
37
+ /**
38
+ * Text-decoration geometry (underline / strikethrough), as fractions of
39
+ * font size, measured from the line's top (the renderer draws text with
40
+ * a top baseline).
41
+ *
42
+ * - `TEXT_DECORATION_THICKNESS` — line thickness ≈ 6% of font size
43
+ * (clamped to ≥1 px in the renderer).
44
+ * - `TEXT_UNDERLINE_OFFSET` — underline top, ~92% down (just below the
45
+ * glyph baseline).
46
+ * - `TEXT_STRIKETHROUGH_OFFSET` — strikethrough centre, ~50% (x-height).
47
+ */
48
+ export const TEXT_DECORATION_THICKNESS = 0.06;
49
+ export const TEXT_UNDERLINE_OFFSET = 0.92;
50
+ export const TEXT_STRIKETHROUGH_OFFSET = 0.5;
51
+ /**
52
+ * Corner radius (world px) for the rounded bends of an elbow (orthogonal)
53
+ * connector and of a straight connector broken by user waypoints. Each
54
+ * corner is replaced by a quadratic arc of this radius, clamped to half the
55
+ * shorter adjacent segment so short segments don't overshoot. 0 disables
56
+ * rounding (sharp corners). Range: 0–16.
57
+ */
58
+ export const LINK_CORNER_RADIUS = 10;
59
+ // --- Grid -------------------------------------------------------------------
60
+ //
61
+ // Lines and dots are tuned independently: a ruled line covers far more
62
+ // pixels than a lone dot, so the dot grid needs a darker colour, a
63
+ // slightly fatter mark, and a denser ladder to read as clearly as the
64
+ // line grid at the same zoom.
65
+ /** Stroke colour for the ruled (`"lines"`) grid. Neutral step-6 gray. */
66
+ export const GRID_LINE_COLOR = GRID_COLOR;
67
+ /** Fill colour for the dotted (`"dots"`) grid — darker step-9 gray so the dots stay legible on a gray canvas. */
68
+ export const GRID_DOT_FILL = GRID_DOT_COLOR;
69
+ /** On-screen stroke width (px) of a grid line. Divided by zoom at the use site so the line stays 1 px regardless of view scale. */
70
+ export const GRID_LINE_WIDTH_PX = 1.0;
71
+ /**
72
+ * Dot radius (screen px) for `gridStyle === "dots"`. Constant across
73
+ * zoom (divided by `zoom` at the use site). Reads as a crisp anchor on
74
+ * a gray surface. Range: 1.0–2.0.
75
+ */
76
+ export const GRID_DOT_RADIUS_PX = 1;
77
+ /**
78
+ * Below this on-screen spacing (px) a grid level paints nothing —
79
+ * denser rendering reads as a flat haze. Only used by the fixed-ladder
80
+ * path (`options.levels`); the default dynamic ladder uses the fade
81
+ * bands below instead.
82
+ */
83
+ export const GRID_MIN_SCREEN_SPACING_PX = 4;
84
+ // --- Dynamic (infinite) grid ladder -----------------------------------------
85
+ //
86
+ // The default grid is a SELF-SIMILAR, zoom-relative ladder: instead of a
87
+ // fixed set of world steps it renders a handful of rungs anchored to the
88
+ // current zoom, each rung `GRID_LEVEL_SUBDIV`× the previous. As you zoom
89
+ // the rungs slide — a finer rung fades in and a coarser one fades out —
90
+ // so new lines / dots keep appearing at EVERY zoom, not just at the
91
+ // hand-picked thresholds of a fixed ladder. Rungs finer than `gridSize`
92
+ // are purely visual (snap-to-grid still rounds to `gridSize`).
93
+ /** Ratio between adjacent rungs. 4 keeps the 64/16/4/1 cadence. */
94
+ export const GRID_LEVEL_SUBDIV = 4;
95
+ /**
96
+ * How many self-similar rungs to paint at once (finest first). 3 keeps a
97
+ * stable fully-opaque coarse tier while the finest rung fades in/out.
98
+ */
99
+ export const GRID_LEVEL_RUNGS = 3;
100
+ /**
101
+ * Line grid fade band (on-screen px). A rung is invisible at/below
102
+ * `FROM`, ramps to full opacity by `FULL`, and stays full above. Tuned
103
+ * so at 100 % (gridSize 20) the 20 px rung reads faint and the 80 px rung
104
+ * is solid, while subdividing forever.
105
+ */
106
+ export const GRID_LINE_FADE_FROM_PX = 12;
107
+ export const GRID_LINE_FADE_FULL_PX = 56;
108
+ /**
109
+ * Dot grid fade band. Lower / tighter than lines so the base `gridSize`
110
+ * dot lattice is fully solid at 100 % (the denser dot field) yet still
111
+ * subdivides on zoom-in.
112
+ */
113
+ export const GRID_DOT_FADE_FROM_PX = 10;
114
+ export const GRID_DOT_FADE_FULL_PX = 20;
115
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.js","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAGrE;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,WAAW,GAAe;IACrC,WAAW,EAAE,IAAI;IACjB,QAAQ,EAAE,GAAG;CACd,CAAC;AAEF;;;;GAIG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,MAAM,CAAC;AAE/C;;;;;GAKG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG,IAAI,CAAC;AAEhD;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,IAAI,CAAC;AAC9C,MAAM,CAAC,MAAM,qBAAqB,GAAG,IAAI,CAAC;AAC1C,MAAM,CAAC,MAAM,yBAAyB,GAAG,GAAG,CAAC;AAE7C;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,EAAE,CAAC;AAErC,+EAA+E;AAC/E,EAAE;AACF,uEAAuE;AACvE,mEAAmE;AACnE,sEAAsE;AACtE,8BAA8B;AAE9B,yEAAyE;AACzE,MAAM,CAAC,MAAM,eAAe,GAAG,UAAU,CAAC;AAE1C,iHAAiH;AACjH,MAAM,CAAC,MAAM,aAAa,GAAG,cAAc,CAAC;AAE5C,mIAAmI;AACnI,MAAM,CAAC,MAAM,kBAAkB,GAAG,GAAG,CAAC;AAEtC;;;;GAIG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC;AAEpC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,CAAC,CAAC;AAE5C,+EAA+E;AAC/E,EAAE;AACF,yEAAyE;AACzE,yEAAyE;AACzE,yEAAyE;AACzE,wEAAwE;AACxE,oEAAoE;AACpE,wEAAwE;AACxE,+DAA+D;AAE/D,mEAAmE;AACnE,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC;AAEnC;;;GAGG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC;AAElC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,EAAE,CAAC;AACzC,MAAM,CAAC,MAAM,sBAAsB,GAAG,EAAE,CAAC;AAEzC;;;;GAIG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,EAAE,CAAC;AACxC,MAAM,CAAC,MAAM,qBAAqB,GAAG,EAAE,CAAC"}
@@ -0,0 +1,34 @@
1
+ import type { Link } from "@oh-just-another/scene";
2
+ /**
3
+ * Per-edge rasterised bitmap cache. Mirror of `ElementBitmapCache` but
4
+ * keyed on `Link` identity. Long-path edges (orthogonal routes around
5
+ * obstacles, beziers with many waypoints) cost real CPU per frame;
6
+ * caching the rasterised stroke as an `ImageBitmap` lets pan / camera
7
+ * moves draw with one `drawImage` call instead of a fresh
8
+ * `path → stroke` pass.
9
+ *
10
+ * Invalidation: every scene mutation produces a fresh edge object via
11
+ * `apply(scene, patch)`, so reference inequality on the cached
12
+ * `edgeRef` signals "stale". No version field needed.
13
+ *
14
+ * Hosts that don't want bitmaps in memory just don't instantiate one —
15
+ * `renderLinks` ignores the cache when absent.
16
+ */
17
+ export interface LinkBitmapCache<V = unknown> {
18
+ get(edge: Link, zoomBucket: number): V | undefined;
19
+ set(edge: Link, zoomBucket: number, value: V): void;
20
+ delete(edge: Link, zoomBucket: number): void;
21
+ clear(): void;
22
+ readonly size: number;
23
+ }
24
+ export declare class InMemoryLinkBitmapCache<V> implements LinkBitmapCache<V> {
25
+ private readonly entries;
26
+ private readonly cap;
27
+ constructor(cap?: number);
28
+ get size(): number;
29
+ get(edge: Link, zoomBucket: number): V | undefined;
30
+ set(edge: Link, zoomBucket: number, value: V): void;
31
+ delete(edge: Link, zoomBucket: number): void;
32
+ clear(): void;
33
+ }
34
+ //# sourceMappingURL=edge-cache-bitmap.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"edge-cache-bitmap.d.ts","sourceRoot":"","sources":["../src/edge-cache-bitmap.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAEnD;;;;;;;;;;;;;;GAcG;AAEH,MAAM,WAAW,eAAe,CAAC,CAAC,GAAG,OAAO;IAC1C,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS,CAAC;IACnD,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC;IACpD,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7C,KAAK,IAAI,IAAI,CAAC;IACd,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AASD,qBAAa,uBAAuB,CAAC,CAAC,CAAE,YAAW,eAAe,CAAC,CAAC,CAAC;IACnE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA+B;IACvD,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;gBAEjB,GAAG,SAAM;IAIrB,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IAalD,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI;IAUnD,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI;IAI5C,KAAK,IAAI,IAAI;CAGd"}
@@ -0,0 +1,42 @@
1
+ const keyFor = (edge, zoomBucket) => `${edge.id}@${zoomBucket}`;
2
+ export class InMemoryLinkBitmapCache {
3
+ entries = new Map();
4
+ cap;
5
+ constructor(cap = 256) {
6
+ this.cap = cap;
7
+ }
8
+ get size() {
9
+ return this.entries.size;
10
+ }
11
+ get(edge, zoomBucket) {
12
+ const key = keyFor(edge, zoomBucket);
13
+ const e = this.entries.get(key);
14
+ if (!e)
15
+ return undefined;
16
+ if (e.edgeRef !== edge) {
17
+ this.entries.delete(key);
18
+ return undefined;
19
+ }
20
+ this.entries.delete(key);
21
+ this.entries.set(key, e);
22
+ return e.value;
23
+ }
24
+ set(edge, zoomBucket, value) {
25
+ const key = keyFor(edge, zoomBucket);
26
+ if (this.entries.has(key))
27
+ this.entries.delete(key);
28
+ this.entries.set(key, { edgeRef: edge, value });
29
+ if (this.entries.size > this.cap) {
30
+ const oldest = this.entries.keys().next().value;
31
+ if (oldest !== undefined)
32
+ this.entries.delete(oldest);
33
+ }
34
+ }
35
+ delete(edge, zoomBucket) {
36
+ this.entries.delete(keyFor(edge, zoomBucket));
37
+ }
38
+ clear() {
39
+ this.entries.clear();
40
+ }
41
+ }
42
+ //# sourceMappingURL=edge-cache-bitmap.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"edge-cache-bitmap.js","sourceRoot":"","sources":["../src/edge-cache-bitmap.ts"],"names":[],"mappings":"AA+BA,MAAM,MAAM,GAAG,CAAC,IAAU,EAAE,UAAkB,EAAU,EAAE,CAAC,GAAG,IAAI,CAAC,EAAE,IAAI,UAAU,EAAE,CAAC;AAEtF,MAAM,OAAO,uBAAuB;IACjB,OAAO,GAAG,IAAI,GAAG,EAAoB,CAAC;IACtC,GAAG,CAAS;IAE7B,YAAY,GAAG,GAAG,GAAG;QACnB,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;IACjB,CAAC;IAED,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC;IAC3B,CAAC;IAED,GAAG,CAAC,IAAU,EAAE,UAAkB;QAChC,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;QACrC,MAAM,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAChC,IAAI,CAAC,CAAC;YAAE,OAAO,SAAS,CAAC;QACzB,IAAI,CAAC,CAAC,OAAO,KAAK,IAAI,EAAE,CAAC;YACvB,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACzB,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACzB,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QACzB,OAAO,CAAC,CAAC,KAAK,CAAC;IACjB,CAAC;IAED,GAAG,CAAC,IAAU,EAAE,UAAkB,EAAE,KAAQ;QAC1C,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;QACrC,IAAI,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACpD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QAChD,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YACjC,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC;YAChD,IAAI,MAAM,KAAK,SAAS;gBAAE,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACxD,CAAC;IACH,CAAC;IAED,MAAM,CAAC,IAAU,EAAE,UAAkB;QACnC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC;IAChD,CAAC;IAED,KAAK;QACH,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;IACvB,CAAC;CACF"}
@@ -0,0 +1,32 @@
1
+ import type { Bounds, LinkId } from "@oh-just-another/types";
2
+ import { type Link, type Scene } from "@oh-just-another/scene";
3
+ /**
4
+ * Per-edge memo invalidated by identity of either the `Link` ref or the
5
+ * `Scene` ref. Links' world AABB depends on endpoint shapes' positions —
6
+ * a shape move replaces the shape ref but leaves the edge ref intact, so
7
+ * a plain by-edge cache would go stale. Patches always replace the
8
+ * `Scene` ref, so `(edge, scene)` together form a sound key.
9
+ */
10
+ export declare class LinkBoundsCache {
11
+ private readonly entries;
12
+ get(scene: Scene, edge: Link): Bounds | null | undefined;
13
+ set(scene: Scene, edge: Link, value: Bounds | null): Bounds | null;
14
+ getOrCompute(scene: Scene, edge: Link): Bounds | null;
15
+ invalidate(id: LinkId): void;
16
+ clear(): void;
17
+ prune(scene: Scene): void;
18
+ get size(): number;
19
+ }
20
+ /**
21
+ * AABB of the edge's polyline in world space. Returns `null` when the
22
+ * path is unresolvable (missing endpoint shape). Pure with respect to
23
+ * the `(scene, edge)` pair.
24
+ */
25
+ export declare const computeLinkWorldBounds: (scene: Scene, edge: Link) => Bounds | null;
26
+ /**
27
+ * Shared module-level cache. Survives across `renderLinks` calls — the
28
+ * `(edge, scene)` identity key ensures stale entries fall out automatically
29
+ * once a patch produces a new scene reference.
30
+ */
31
+ export declare const sharedLinkBoundsCache: LinkBoundsCache;
32
+ //# sourceMappingURL=edge-cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"edge-cache.d.ts","sourceRoot":"","sources":["../src/edge-cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,EAAsB,KAAK,IAAI,EAAE,KAAK,KAAK,EAAE,MAAM,wBAAwB,CAAC;AAEnF;;;;;;GAMG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAGpB;IAEJ,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS;IAUxD,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI;IAKlE,YAAY,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI;IAMrD,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAI5B,KAAK,IAAI,IAAI;IAIb,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI;IAMzB,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF;AAED;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,GAAI,OAAO,KAAK,EAAE,MAAM,IAAI,KAAG,MAAM,GAAG,IA2B1E,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,EAAE,eAAuC,CAAC"}
@@ -0,0 +1,90 @@
1
+ import { getLinkCurvePoints } from "@oh-just-another/scene";
2
+ /**
3
+ * Per-edge memo invalidated by identity of either the `Link` ref or the
4
+ * `Scene` ref. Links' world AABB depends on endpoint shapes' positions —
5
+ * a shape move replaces the shape ref but leaves the edge ref intact, so
6
+ * a plain by-edge cache would go stale. Patches always replace the
7
+ * `Scene` ref, so `(edge, scene)` together form a sound key.
8
+ */
9
+ export class LinkBoundsCache {
10
+ entries = new Map();
11
+ get(scene, edge) {
12
+ const entry = this.entries.get(edge.id);
13
+ if (!entry)
14
+ return undefined;
15
+ if (entry.edge !== edge || entry.scene !== scene) {
16
+ this.entries.delete(edge.id);
17
+ return undefined;
18
+ }
19
+ return entry.value;
20
+ }
21
+ set(scene, edge, value) {
22
+ this.entries.set(edge.id, { edge, scene, value });
23
+ return value;
24
+ }
25
+ getOrCompute(scene, edge) {
26
+ const cached = this.get(scene, edge);
27
+ if (cached !== undefined)
28
+ return cached;
29
+ return this.set(scene, edge, computeLinkWorldBounds(scene, edge));
30
+ }
31
+ invalidate(id) {
32
+ this.entries.delete(id);
33
+ }
34
+ clear() {
35
+ this.entries.clear();
36
+ }
37
+ prune(scene) {
38
+ for (const id of this.entries.keys()) {
39
+ if (!scene.links.has(id))
40
+ this.entries.delete(id);
41
+ }
42
+ }
43
+ get size() {
44
+ return this.entries.size;
45
+ }
46
+ }
47
+ /**
48
+ * AABB of the edge's polyline in world space. Returns `null` when the
49
+ * path is unresolvable (missing endpoint shape). Pure with respect to
50
+ * the `(scene, edge)` pair.
51
+ */
52
+ export const computeLinkWorldBounds = (scene, edge) => {
53
+ // Curved (bezier) links bow out from the straight chord, so the AABB is
54
+ // taken over the FLATTENED curve (same geometry the renderer draws and
55
+ // hit-testing uses) — otherwise the arc paints outside the dirty rect /
56
+ // viewport cull and gets clipped. Non-curved edges return their polyline.
57
+ const path = getLinkCurvePoints(scene, edge);
58
+ if (!path || path.length === 0)
59
+ return null;
60
+ let minX = Infinity;
61
+ let minY = Infinity;
62
+ let maxX = -Infinity;
63
+ let maxY = -Infinity;
64
+ for (const p of path) {
65
+ if (p.x < minX)
66
+ minX = p.x;
67
+ if (p.y < minY)
68
+ minY = p.y;
69
+ if (p.x > maxX)
70
+ maxX = p.x;
71
+ if (p.y > maxY)
72
+ maxY = p.y;
73
+ }
74
+ // Inflate by stroke half-width + arrowhead reach. This keeps an
75
+ // axis-aligned link (a horizontal/vertical line, otherwise a zero-area AABB)
76
+ // non-degenerate, so dirty-rect union / viewport culling don't drop it. Also
77
+ // covers arrowhead / thick-stroke overhang past the path.
78
+ const heads = edge.arrowheads;
79
+ const hasHead = (heads?.from !== undefined && heads.from !== "none") ||
80
+ (heads?.to !== undefined && heads.to !== "none");
81
+ const pad = Math.max(2, (edge.style.strokeWidth ?? 1) / 2 + (hasHead ? (heads.size ?? 10) : 0));
82
+ return { x: minX - pad, y: minY - pad, width: maxX - minX + pad * 2, height: maxY - minY + pad * 2 };
83
+ };
84
+ /**
85
+ * Shared module-level cache. Survives across `renderLinks` calls — the
86
+ * `(edge, scene)` identity key ensures stale entries fall out automatically
87
+ * once a patch produces a new scene reference.
88
+ */
89
+ export const sharedLinkBoundsCache = new LinkBoundsCache();
90
+ //# sourceMappingURL=edge-cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"edge-cache.js","sourceRoot":"","sources":["../src/edge-cache.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAyB,MAAM,wBAAwB,CAAC;AAEnF;;;;;;GAMG;AACH,MAAM,OAAO,eAAe;IACT,OAAO,GAAG,IAAI,GAAG,EAG/B,CAAC;IAEJ,GAAG,CAAC,KAAY,EAAE,IAAU;QAC1B,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACxC,IAAI,CAAC,KAAK;YAAE,OAAO,SAAS,CAAC;QAC7B,IAAI,KAAK,CAAC,IAAI,KAAK,IAAI,IAAI,KAAK,CAAC,KAAK,KAAK,KAAK,EAAE,CAAC;YACjD,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YAC7B,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,OAAO,KAAK,CAAC,KAAK,CAAC;IACrB,CAAC;IAED,GAAG,CAAC,KAAY,EAAE,IAAU,EAAE,KAAoB;QAChD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;QAClD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,YAAY,CAAC,KAAY,EAAE,IAAU;QACnC,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QACrC,IAAI,MAAM,KAAK,SAAS;YAAE,OAAO,MAAM,CAAC;QACxC,OAAO,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,EAAE,sBAAsB,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC;IACpE,CAAC;IAED,UAAU,CAAC,EAAU;QACnB,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAC1B,CAAC;IAED,KAAK;QACH,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;IACvB,CAAC;IAED,KAAK,CAAC,KAAY;QAChB,KAAK,MAAM,EAAE,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC;YACrC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;gBAAE,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QACpD,CAAC;IACH,CAAC;IAED,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC;IAC3B,CAAC;CACF;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,KAAY,EAAE,IAAU,EAAiB,EAAE;IAChF,wEAAwE;IACxE,uEAAuE;IACvE,wEAAwE;IACxE,0EAA0E;IAC1E,MAAM,IAAI,GAAG,kBAAkB,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IAC7C,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAC5C,IAAI,IAAI,GAAG,QAAQ,CAAC;IACpB,IAAI,IAAI,GAAG,QAAQ,CAAC;IACpB,IAAI,IAAI,GAAG,CAAC,QAAQ,CAAC;IACrB,IAAI,IAAI,GAAG,CAAC,QAAQ,CAAC;IACrB,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;QACrB,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI;YAAE,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC;QAC3B,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI;YAAE,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC;QAC3B,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI;YAAE,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC;QAC3B,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI;YAAE,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC;IAC7B,CAAC;IACD,gEAAgE;IAChE,6EAA6E;IAC7E,6EAA6E;IAC7E,0DAA0D;IAC1D,MAAM,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC;IAC9B,MAAM,OAAO,GACX,CAAC,KAAK,EAAE,IAAI,KAAK,SAAS,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,CAAC;QACpD,CAAC,KAAK,EAAE,EAAE,KAAK,SAAS,IAAI,KAAK,CAAC,EAAE,KAAK,MAAM,CAAC,CAAC;IACnD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,WAAW,IAAI,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAChG,OAAO,EAAE,CAAC,EAAE,IAAI,GAAG,GAAG,EAAE,CAAC,EAAE,IAAI,GAAG,GAAG,EAAE,KAAK,EAAE,IAAI,GAAG,IAAI,GAAG,GAAG,GAAG,CAAC,EAAE,MAAM,EAAE,IAAI,GAAG,IAAI,GAAG,GAAG,GAAG,CAAC,EAAE,CAAC;AACvG,CAAC,CAAC;AAEF;;;;GAIG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAoB,IAAI,eAAe,EAAE,CAAC"}
@@ -0,0 +1,73 @@
1
+ import { type Link, type Scene } from "@oh-just-another/scene";
2
+ import type { Bounds, Vec2 } from "@oh-just-another/types";
3
+ import type { RenderTarget } from "./render-target.js";
4
+ import { type LinkBoundsCache } from "./edge-cache.js";
5
+ import type { LinkBitmapCache } from "./edge-cache-bitmap.js";
6
+ export interface RenderLinksOptions {
7
+ /**
8
+ * Called for edges whose endpoints can't be resolved (e.g. anchor
9
+ * endpoint references a missing shape). The default is to silently
10
+ * skip them.
11
+ */
12
+ readonly onMissingEndpoint?: (edge: Link) => void;
13
+ /**
14
+ * World-space rect — only edges whose AABB intersects it are drawn.
15
+ * Omit to draw every edge. Match `RenderSceneOptions.viewportWorld`
16
+ * for consistent culling between shape and edge passes.
17
+ */
18
+ readonly viewportWorld?: Bounds;
19
+ /**
20
+ * Cache used to memoize per-edge world AABBs. Defaults to the shared
21
+ * module-level cache so per-frame edge-bounds work is amortized across
22
+ * paint passes.
23
+ */
24
+ readonly edgeBoundsCache?: LinkBoundsCache;
25
+ /**
26
+ * Optional dirty rectangle (world coords). Links whose AABB does
27
+ * not intersect this rect are skipped — mirrors the same dirty-rect
28
+ * filter `renderScene` applies to shapes. Caller is responsible for
29
+ * having cleared the corresponding screen region.
30
+ */
31
+ readonly dirtyWorld?: Bounds;
32
+ /**
33
+ * Per-edge bitmap cache. When supplied along with `rasteriseLink`,
34
+ * edges whose object reference hasn't changed AND whose zoom bucket
35
+ * matches are drawn from the cache via `drawImage` instead of
36
+ * re-stroking the path. Pass `undefined` to opt out — the rest of
37
+ * renderLinks works unchanged.
38
+ */
39
+ readonly edgeBitmapCache?: LinkBitmapCache;
40
+ /**
41
+ * Host-side rasteriser: receives the edge, its world bbox, the
42
+ * scene reference, and the active zoom bucket; returns the
43
+ * image source to cache (`ImageBitmap`, `OffscreenCanvas`,
44
+ * `HTMLCanvasElement` — anything the host's `RenderTarget.draw
45
+ * Image` accepts). Returning `null` skips the cache for that
46
+ * edge (e.g. an edge so small the bitmap isn't worth it).
47
+ */
48
+ readonly rasteriseLink?: (edge: Link, bounds: Bounds, scene: Scene, zoomBucket: number) => unknown;
49
+ /**
50
+ * Active zoom (defaults to `scene.viewport.zoom`). Cache lookups
51
+ * round this through `zoomBucket()` so small camera adjustments
52
+ * still hit the cache.
53
+ */
54
+ readonly zoom?: number;
55
+ }
56
+ /**
57
+ * Draws every edge in the scene, in layer-then-z order. Resolves endpoints
58
+ * through `getLinkPath` (which honours anchor refs and explicit waypoints)
59
+ * and emits the appropriate primitive sequence for the edge's routing.
60
+ *
61
+ * Call this *after* `renderScene` so edges sit on top of shapes. Both
62
+ * functions write to the same `RenderTarget` under the same scene-level
63
+ * viewport transform, so coordinates align.
64
+ */
65
+ export declare const renderLinks: (scene: Scene, target: RenderTarget, options?: RenderLinksOptions) => void;
66
+ /**
67
+ * Stroke a polyline with rounded corners: each interior vertex is replaced
68
+ * by a quadratic arc of `radius` (clamped to half the shorter adjacent
69
+ * segment). Works for 90° elbow bends and arbitrary-angle waypoint bends.
70
+ * Caller has already `beginPath()`.
71
+ */
72
+ export declare const strokeRoundedPolyline: (target: RenderTarget, raw: readonly Vec2[], radius: number) => void;
73
+ //# sourceMappingURL=edge-renderer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"edge-renderer.d.ts","sourceRoot":"","sources":["../src/edge-renderer.ts"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,IAAI,EAGT,KAAK,KAAK,EACX,MAAM,wBAAwB,CAAC;AAEhC,OAAO,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAAyB,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAC9E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAS9D,MAAM,WAAW,kBAAkB;IACjC;;;;OAIG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAC;IAClD;;;;OAIG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC;;;;OAIG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,eAAe,CAAC;IAC3C;;;;;OAKG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B;;;;;;OAMG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,eAAe,CAAC;IAC3C;;;;;;;OAOG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,CACvB,IAAI,EAAE,IAAI,EACV,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,KAAK,EACZ,UAAU,EAAE,MAAM,KACf,OAAO,CAAC;IACb;;;;OAIG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,WAAW,GACtB,OAAO,KAAK,EACZ,QAAQ,YAAY,EACpB,UAAS,kBAAuB,KAC/B,IAmDF,CAAC;AAyNF;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,GAChC,QAAQ,YAAY,EACpB,KAAK,SAAS,IAAI,EAAE,EACpB,QAAQ,MAAM,KACb,IAqCF,CAAC"}