@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 @@
1
+ {"version":3,"file":"render-target.d.ts","sourceRoot":"","sources":["../src/render-target.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=render-target.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render-target.js","sourceRoot":"","sources":["../src/render-target.ts"],"names":[],"mappings":""}
@@ -0,0 +1,126 @@
1
+ import { type Scene, type ElementBase, type SpatialGrid } from "@oh-just-another/scene";
2
+ import type { Bounds, LayerId, ElementId } from "@oh-just-another/types";
3
+ import type { RenderTarget } from "./render-target.js";
4
+ import { ElementCache } from "./shape-cache.js";
5
+ import type { LayerCompositeCache } from "./layer-cache-composite.js";
6
+ /**
7
+ * Zoom-based level-of-detail thresholds. Each threshold turns on a
8
+ * cheaper render path when the scene zoom drops below it:
9
+ *
10
+ * - **placeholder** → draw a flat fill at the shape's world AABB and
11
+ * skip the registered renderer entirely. Highest savings, biggest
12
+ * visual fidelity loss; reserve for very-zoomed-out overviews.
13
+ * - **hideText** → skip text shapes (their wrap+measure cost is high).
14
+ *
15
+ * Thresholds compare against `scene.viewport.zoom` (1.0 = 1:1 pixels).
16
+ * Omit a threshold to disable that level.
17
+ */
18
+ export interface LodOptions {
19
+ readonly placeholder?: number;
20
+ readonly hideText?: number;
21
+ }
22
+ export interface RenderSceneOptions {
23
+ /** Skip clearing the target before drawing. Default: false. */
24
+ readonly skipClear?: boolean;
25
+ /** Called for shapes whose `type` has no registered renderer. Default: ignore. */
26
+ readonly onUnknownElement?: (shape: ElementBase) => void;
27
+ /**
28
+ * World-space viewport bounds. When provided, shapes whose AABB does
29
+ * not intersect this rectangle are skipped (viewport culling). Pass
30
+ * a slightly inflated rect to avoid pop-in during pan.
31
+ */
32
+ readonly viewport?: Bounds;
33
+ /**
34
+ * Persistent bounds cache. When omitted a fresh per-render cache is
35
+ * created — fine for hot paths because lookups inside one frame still
36
+ * amortize. Pass a long-lived cache from `Editor` to share work across
37
+ * frames, hit-test, and overlay.
38
+ */
39
+ readonly boundsCache?: ElementCache<Bounds>;
40
+ /**
41
+ * Pre-built spatial index. When provided together with `viewport`, the
42
+ * renderer picks candidate shapes from the index and skips full layer
43
+ * scans — pays off around ~10k shapes.
44
+ */
45
+ readonly spatialIndex?: SpatialGrid;
46
+ /**
47
+ * Zoom thresholds for cheaper render paths. See {@link LodOptions}.
48
+ */
49
+ readonly lod?: LodOptions;
50
+ /**
51
+ * Placeholder fill colour. Defaults to `#bbb`. Pick something close
52
+ * to the average shape colour so the transition is unobtrusive.
53
+ */
54
+ readonly placeholderFill?: string;
55
+ /**
56
+ * Optional dirty rectangle in **world** coords. When set:
57
+ * • the renderer clears only the corresponding screen region;
58
+ * • shapes whose world AABB doesn't intersect the dirty rect are
59
+ * skipped entirely.
60
+ * Combined with shape-identity tracking by the host this drops most
61
+ * of the per-frame work for "single shape moves on otherwise static
62
+ * scene".
63
+ */
64
+ readonly dirtyWorld?: Bounds;
65
+ /**
66
+ * Shapes to render with reduced alpha (modern-style group isolation).
67
+ * For each shape whose `id` appears in this set, the renderer sets
68
+ * `globalAlpha = dimOpacity` for the per-shape draw pass before
69
+ * dispatching to the registered renderer.
70
+ *
71
+ * Caveat: shapes whose own `style.opacity` is explicitly set will
72
+ * have their renderer call `setOpacity` again and override the
73
+ * dim — the dim affects only the common case where shapes don't
74
+ * carry an explicit opacity. Acceptable for the isolation UX
75
+ * because outsiders are usually plain opaque shapes.
76
+ */
77
+ readonly dimElements?: ReadonlySet<ElementId>;
78
+ /**
79
+ * Alpha to use for `dimElements`. Default 1 (no-op). Hosts using the
80
+ * isolation feature should pass their `ISOLATION_DIM_OPACITY`
81
+ * constant.
82
+ */
83
+ readonly dimOpacity?: number;
84
+ /**
85
+ * Element ids that should NOT render this pass. Used by hosts that
86
+ * implement per-shape / group hide propagation: the editor walks
87
+ * the parent chain to compute which shapes are effectively hidden
88
+ * and forwards the set here. Hidden shapes are also skipped in
89
+ * hit-test on the editor side, so they read as "absent" entirely.
90
+ */
91
+ readonly hideElements?: ReadonlySet<ElementId>;
92
+ /**
93
+ * Per-layer composite bitmap cache. When supplied along with
94
+ * `compositeLayerBitmap`, unchanged layers (i.e. not present in
95
+ * `dirtyLayerIds`) are drawn from a single cached `drawImage` call
96
+ * instead of walking every shape.
97
+ *
98
+ * Pass `dirtyLayerIds` so the renderer knows which layers to
99
+ * re-rasterise. Without it the cache is treated as cold every
100
+ * frame (defensive — better stale work than a stale visual).
101
+ */
102
+ readonly layerCompositeCache?: LayerCompositeCache;
103
+ readonly dirtyLayerIds?: ReadonlySet<LayerId>;
104
+ /**
105
+ * Host-side layer rasteriser. Receives the layer id, the active
106
+ * zoom bucket, and the scene; returns the bitmap to cache or
107
+ * `null` to opt out. The kernel doesn't ship one — `Offscreen
108
+ * Canvas` import lives in `@renderer-canvas`.
109
+ */
110
+ readonly compositeLayerBitmap?: (layerId: LayerId, zoomBucket: number, scene: Scene) => unknown;
111
+ }
112
+ /**
113
+ * Renders the `main` z-stack of a scene onto a single target.
114
+ *
115
+ * Order of operations:
116
+ * 1. Optionally clear the surface.
117
+ * 2. Apply the scene's world-to-screen transform.
118
+ * 3. For each visible layer (bottom → top): for each shape (bottom → top):
119
+ * save state, push the shape's local TRS, invoke its registered renderer.
120
+ *
121
+ * This function does not draw edges, selection handles, or grids — those
122
+ * either live on different layers (`background` / `overlay`) or are added by
123
+ * higher-level packages.
124
+ */
125
+ export declare const renderScene: (scene: Scene, target: RenderTarget, options?: RenderSceneOptions) => void;
126
+ //# sourceMappingURL=scene-renderer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scene-renderer.d.ts","sourceRoot":"","sources":["../src/scene-renderer.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,KAAK,EACV,KAAK,WAAW,EAChB,KAAK,WAAW,EACjB,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAEzE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD,OAAO,EAAqB,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAEnE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAGtE;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,kBAAkB;IACjC,+DAA+D;IAC/D,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAC7B,kFAAkF;IAClF,QAAQ,CAAC,gBAAgB,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC;IACzD;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,YAAY,CAAC,MAAM,CAAC,CAAC;IAC5C;;;;OAIG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,WAAW,CAAC;IACpC;;OAEG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,UAAU,CAAC;IAC1B;;;OAGG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC;;;;;;;;OAQG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,WAAW,CAAC,SAAS,CAAC,CAAC;IAC9C;;;;OAIG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B;;;;;;OAMG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,WAAW,CAAC,SAAS,CAAC,CAAC;IAC/C;;;;;;;;;OASG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,mBAAmB,CAAC;IACnD,QAAQ,CAAC,aAAa,CAAC,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IAC9C;;;;;OAKG;IACH,QAAQ,CAAC,oBAAoB,CAAC,EAAE,CAC9B,OAAO,EAAE,OAAO,EAChB,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,KAAK,KACT,OAAO,CAAC;CACd;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,WAAW,GACtB,OAAO,KAAK,EACZ,QAAQ,YAAY,EACpB,UAAS,kBAAuB,KAC/B,IAkIF,CAAC"}
@@ -0,0 +1,149 @@
1
+ import { getLayersInOrder, getElementsInLayer, getWorldToScreen, } from "@oh-just-another/scene";
2
+ import { bounds as B, matrix } from "@oh-just-another/math";
3
+ import { getElementRenderer } from "./shape-renderer.js";
4
+ import { cachedWorldBounds, ElementCache } from "./shape-cache.js";
5
+ import { DEFAULT_PLACEHOLDER_FILL } from "./constants.js";
6
+ import { zoomBucket as bucketFor } from "./shape-cache-bitmap.js";
7
+ /**
8
+ * Renders the `main` z-stack of a scene onto a single target.
9
+ *
10
+ * Order of operations:
11
+ * 1. Optionally clear the surface.
12
+ * 2. Apply the scene's world-to-screen transform.
13
+ * 3. For each visible layer (bottom → top): for each shape (bottom → top):
14
+ * save state, push the shape's local TRS, invoke its registered renderer.
15
+ *
16
+ * This function does not draw edges, selection handles, or grids — those
17
+ * either live on different layers (`background` / `overlay`) or are added by
18
+ * higher-level packages.
19
+ */
20
+ export const renderScene = (scene, target, options = {}) => {
21
+ const w2s = getWorldToScreen(scene.viewport);
22
+ const dirtyWorld = options.dirtyWorld;
23
+ if (!options.skipClear) {
24
+ if (dirtyWorld) {
25
+ // Project the dirty rect to screen pixels, inflate by a few
26
+ // pixels to cover anti-aliased stroke fuzz.
27
+ const corners = [
28
+ matrix.applyToPoint(w2s, { x: dirtyWorld.x, y: dirtyWorld.y }),
29
+ matrix.applyToPoint(w2s, {
30
+ x: dirtyWorld.x + dirtyWorld.width,
31
+ y: dirtyWorld.y + dirtyWorld.height,
32
+ }),
33
+ ];
34
+ const screen = B.expand(B.fromPoints(corners), 2);
35
+ target.clear(screen);
36
+ }
37
+ else {
38
+ target.clear();
39
+ }
40
+ }
41
+ target.save();
42
+ target.setTransform(w2s);
43
+ const boundsCache = options.boundsCache ?? new ElementCache();
44
+ const viewport = options.viewport;
45
+ // Spatial-index candidate set: when present, restricts the per-layer
46
+ // walk to shapes the index considers possibly-visible. Without it the
47
+ // per-shape AABB check on a cached bounds is still cheap (~50ns), so
48
+ // the index is only worth the build cost for very large scenes.
49
+ let candidates = null;
50
+ if (viewport && options.spatialIndex) {
51
+ candidates = options.spatialIndex.query(viewport);
52
+ }
53
+ const zoom = scene.viewport.zoom;
54
+ const lod = options.lod;
55
+ const usePlaceholder = lod?.placeholder !== undefined && zoom < lod.placeholder;
56
+ const dropText = lod?.hideText !== undefined && zoom < lod.hideText;
57
+ const placeholderFill = options.placeholderFill ?? DEFAULT_PLACEHOLDER_FILL;
58
+ const layerCache = options.layerCompositeCache;
59
+ const dirtyLayers = options.dirtyLayerIds;
60
+ const compositeLayerBitmap = options.compositeLayerBitmap;
61
+ const zoomBucket = bucketFor(zoom);
62
+ const layerBoundsFor = (layerId) => {
63
+ let acc = null;
64
+ for (const shape of getElementsInLayer(scene, layerId)) {
65
+ const bb = cachedWorldBounds(boundsCache, shape);
66
+ acc = acc ? B.union(acc, bb) : bb;
67
+ }
68
+ return acc;
69
+ };
70
+ for (const layer of getLayersInOrder(scene)) {
71
+ if (!layer.visible)
72
+ continue;
73
+ // Per-layer composite cache fast path. Only fires when the
74
+ // host plugged a cache + a layer rasteriser; the kernel
75
+ // doesn't ship a default rasteriser because OffscreenCanvas
76
+ // creation lives in renderer-canvas. Drop dirty layers + ones
77
+ // touched by a dirty rect from the cache so the bitmap isn't
78
+ // re-used after a mutation.
79
+ if (layerCache && compositeLayerBitmap) {
80
+ if (dirtyLayers?.has(layer.id))
81
+ layerCache.invalidateLayer(layer.id);
82
+ let bitmap = layerCache.get(layer.id, zoomBucket);
83
+ if (bitmap === undefined) {
84
+ const fresh = compositeLayerBitmap(layer.id, zoomBucket, scene);
85
+ if (fresh !== null) {
86
+ layerCache.set(layer.id, zoomBucket, fresh);
87
+ bitmap = fresh;
88
+ }
89
+ }
90
+ if (bitmap !== undefined) {
91
+ const bb = layerBoundsFor(layer.id);
92
+ if (bb)
93
+ target.drawImage(bitmap, bb.x, bb.y, bb.width, bb.height);
94
+ continue;
95
+ }
96
+ }
97
+ for (const shape of getElementsInLayer(scene, layer.id)) {
98
+ if (options.hideElements?.has(shape.id))
99
+ continue;
100
+ if (candidates && !candidates.has(shape.id))
101
+ continue;
102
+ if (viewport) {
103
+ const bb = cachedWorldBounds(boundsCache, shape);
104
+ if (!B.intersects(bb, viewport))
105
+ continue;
106
+ }
107
+ if (dirtyWorld) {
108
+ const bb = cachedWorldBounds(boundsCache, shape);
109
+ if (!B.intersects(bb, dirtyWorld))
110
+ continue;
111
+ }
112
+ if (dropText && shape.type === "text")
113
+ continue;
114
+ if (usePlaceholder) {
115
+ // Draw the AABB directly in world coords — skip the renderer
116
+ // entirely. The shape's TRS is folded into the cached bounds.
117
+ const bb = cachedWorldBounds(boundsCache, shape);
118
+ target.setFill(placeholderFill);
119
+ target.setStrokeWidth(0);
120
+ target.beginPath();
121
+ target.rect(bb.x, bb.y, bb.width, bb.height);
122
+ target.fill();
123
+ continue;
124
+ }
125
+ const renderer = getElementRenderer(shape.type);
126
+ if (!renderer) {
127
+ options.onUnknownElement?.(shape);
128
+ continue;
129
+ }
130
+ target.save();
131
+ // Isolation dim — set BEFORE the renderer runs so any
132
+ // shape-style-specific setOpacity inside the renderer can
133
+ // override (see RenderSceneOptions.dimElements).
134
+ if (options.dimElements?.has(shape.id) && options.dimOpacity !== undefined) {
135
+ target.setOpacity(options.dimOpacity);
136
+ }
137
+ target.translate(shape.position.x, shape.position.y);
138
+ if (shape.rotation !== 0)
139
+ target.rotate(shape.rotation);
140
+ if (shape.scale.x !== 1 || shape.scale.y !== 1) {
141
+ target.scale(shape.scale.x, shape.scale.y);
142
+ }
143
+ renderer(shape, target, { zoom });
144
+ target.restore();
145
+ }
146
+ }
147
+ target.restore();
148
+ };
149
+ //# sourceMappingURL=scene-renderer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scene-renderer.js","sourceRoot":"","sources":["../src/scene-renderer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ import type { ElementBase } from "@oh-just-another/scene";
2
+ /**
3
+ * Per-shape rasterised cache. Keyed by the shape's identity reference —
4
+ * since scene mutations always replace the shape object (`apply(scene,
5
+ * patch)` produces fresh references), a cache hit is guaranteed to reflect
6
+ * the exact rendered output of the cached version. Pan / zoom invalidation
7
+ * is the host's job: keep zoom in a small "bucket" (e.g. round to 0.1) and
8
+ * include it in the key.
9
+ *
10
+ * LRU-by-insertion-order with a count cap. Hosts can replace with their own
11
+ * cache by implementing the same `get` / `set` / `delete` surface.
12
+ */
13
+ export interface ElementBitmapCache<V = unknown> {
14
+ get(shape: ElementBase, zoomBucket: number): V | undefined;
15
+ set(shape: ElementBase, zoomBucket: number, value: V): void;
16
+ delete(shape: ElementBase, zoomBucket: number): void;
17
+ clear(): void;
18
+ readonly size: number;
19
+ }
20
+ /**
21
+ * In-memory LRU cache. Operates on shape identity (reference) —
22
+ * a stale shape reference for the same id is a miss because the
23
+ * cached entry's `shapeRef !== shape`. That is the invalidation
24
+ * mechanism — no version field needed.
25
+ */
26
+ export declare class InMemoryElementBitmapCache<V> implements ElementBitmapCache<V> {
27
+ private readonly entries;
28
+ private readonly cap;
29
+ constructor(cap?: number);
30
+ get size(): number;
31
+ get(shape: ElementBase, zoomBucket: number): V | undefined;
32
+ set(shape: ElementBase, zoomBucket: number, value: V): void;
33
+ delete(shape: ElementBase, zoomBucket: number): void;
34
+ clear(): void;
35
+ }
36
+ /**
37
+ * Quantise a continuous zoom value to a bucket. Buckets within a
38
+ * power-of-two range share a cache entry so small zoom adjustments
39
+ * don't blow the cache. `bucket = 2 ^ round(log2(zoom))`.
40
+ */
41
+ export declare const zoomBucket: (zoom: number) => number;
42
+ //# sourceMappingURL=shape-cache-bitmap.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shape-cache-bitmap.d.ts","sourceRoot":"","sources":["../src/shape-cache-bitmap.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAE1D;;;;;;;;;;GAUG;AAEH,MAAM,WAAW,kBAAkB,CAAC,CAAC,GAAG,OAAO;IAC7C,GAAG,CAAC,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS,CAAC;IAC3D,GAAG,CAAC,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC;IAC5D,MAAM,CAAC,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IACrD,KAAK,IAAI,IAAI,CAAC;IACd,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAUD;;;;;GAKG;AACH,qBAAa,0BAA0B,CAAC,CAAC,CAAE,YAAW,kBAAkB,CAAC,CAAC,CAAC;IACzE,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,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IAe1D,GAAG,CAAC,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI;IAW3D,MAAM,CAAC,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI;IAIpD,KAAK,IAAI,IAAI;CAGd;AAED;;;;GAIG;AACH,eAAO,MAAM,UAAU,GAAI,MAAM,MAAM,KAAG,MAGzC,CAAC"}
@@ -0,0 +1,61 @@
1
+ const keyFor = (shape, zoomBucket) => `${shape.id}@${zoomBucket}`;
2
+ /**
3
+ * In-memory LRU cache. Operates on shape identity (reference) —
4
+ * a stale shape reference for the same id is a miss because the
5
+ * cached entry's `shapeRef !== shape`. That is the invalidation
6
+ * mechanism — no version field needed.
7
+ */
8
+ export class InMemoryElementBitmapCache {
9
+ entries = new Map();
10
+ cap;
11
+ constructor(cap = 512) {
12
+ this.cap = cap;
13
+ }
14
+ get size() {
15
+ return this.entries.size;
16
+ }
17
+ get(shape, zoomBucket) {
18
+ const key = keyFor(shape, zoomBucket);
19
+ const e = this.entries.get(key);
20
+ if (!e)
21
+ return undefined;
22
+ if (e.shapeRef !== shape) {
23
+ // Reference changed → stale; evict so the slot is free.
24
+ this.entries.delete(key);
25
+ return undefined;
26
+ }
27
+ // Re-insert to mark as recently-used (Map preserves insert order).
28
+ this.entries.delete(key);
29
+ this.entries.set(key, e);
30
+ return e.value;
31
+ }
32
+ set(shape, zoomBucket, value) {
33
+ const key = keyFor(shape, zoomBucket);
34
+ if (this.entries.has(key))
35
+ this.entries.delete(key);
36
+ this.entries.set(key, { shapeRef: shape, value });
37
+ if (this.entries.size > this.cap) {
38
+ // Evict the oldest entry (first key in insertion order).
39
+ const oldest = this.entries.keys().next().value;
40
+ if (oldest !== undefined)
41
+ this.entries.delete(oldest);
42
+ }
43
+ }
44
+ delete(shape, zoomBucket) {
45
+ this.entries.delete(keyFor(shape, zoomBucket));
46
+ }
47
+ clear() {
48
+ this.entries.clear();
49
+ }
50
+ }
51
+ /**
52
+ * Quantise a continuous zoom value to a bucket. Buckets within a
53
+ * power-of-two range share a cache entry so small zoom adjustments
54
+ * don't blow the cache. `bucket = 2 ^ round(log2(zoom))`.
55
+ */
56
+ export const zoomBucket = (zoom) => {
57
+ if (zoom <= 0)
58
+ return 1;
59
+ return 2 ** Math.round(Math.log2(zoom));
60
+ };
61
+ //# sourceMappingURL=shape-cache-bitmap.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shape-cache-bitmap.js","sourceRoot":"","sources":["../src/shape-cache-bitmap.ts"],"names":[],"mappings":"AAsBA,MAAM,MAAM,GAAG,CAAC,KAAkB,EAAE,UAAkB,EAAU,EAAE,CAChE,GAAG,KAAK,CAAC,EAAE,IAAI,UAAU,EAAE,CAAC;AAO9B;;;;;GAKG;AACH,MAAM,OAAO,0BAA0B;IACpB,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,KAAkB,EAAE,UAAkB;QACxC,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC;QACtC,MAAM,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAChC,IAAI,CAAC,CAAC;YAAE,OAAO,SAAS,CAAC;QACzB,IAAI,CAAC,CAAC,QAAQ,KAAK,KAAK,EAAE,CAAC;YACzB,wDAAwD;YACxD,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACzB,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,mEAAmE;QACnE,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,KAAkB,EAAE,UAAkB,EAAE,KAAQ;QAClD,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC;QACtC,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,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;QAClD,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YACjC,yDAAyD;YACzD,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,KAAkB,EAAE,UAAkB;QAC3C,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC;IACjD,CAAC;IAED,KAAK;QACH,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;IACvB,CAAC;CACF;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,IAAY,EAAU,EAAE;IACjD,IAAI,IAAI,IAAI,CAAC;QAAE,OAAO,CAAC,CAAC;IACxB,OAAO,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;AAC1C,CAAC,CAAC"}
@@ -0,0 +1,40 @@
1
+ import type { Bounds, ElementId } from "@oh-just-another/types";
2
+ import { type Scene, type ElementBase } from "@oh-just-another/scene";
3
+ /**
4
+ * Per-shape memo with object-identity invalidation. Cached value sticks
5
+ * until the underlying shape reference changes — and because every scene
6
+ * op (`updateElement` / `moveElement` / ...) returns a new shape object, the
7
+ * cache invalidates automatically without the caller threading versions
8
+ * through.
9
+ *
10
+ * Caches survive across frames; pair with `prune(scene)` after large
11
+ * deletions if memory matters. For ephemeral, single-render memos use
12
+ * a fresh `ElementCache` instance (cheap to construct).
13
+ */
14
+ export declare class ElementCache<T> {
15
+ private readonly entries;
16
+ get(shape: ElementBase): T | undefined;
17
+ set(shape: ElementBase, value: T): T;
18
+ /**
19
+ * Lazy memo. Returns the cached value if `shape` is the same reference
20
+ * as the one we cached against; otherwise runs `compute`, stores the
21
+ * result, and returns it.
22
+ */
23
+ getOrCompute(shape: ElementBase, compute: (s: ElementBase) => T): T;
24
+ invalidate(id: ElementId): void;
25
+ clear(): void;
26
+ /** Drop entries whose shape is no longer in the scene. */
27
+ prune(scene: Scene): void;
28
+ get size(): number;
29
+ }
30
+ /**
31
+ * Shared module-level cache for world-space bounds. Used by `renderScene`
32
+ * for viewport culling and reusable from outside (hit-test, overlay) so
33
+ * a single computation amortizes across passes.
34
+ *
35
+ * `getElementWorldBounds` is pure — same shape ref → same bounds — so a
36
+ * by-identity cache is sound.
37
+ */
38
+ export declare const sharedBoundsCache: ElementCache<Bounds>;
39
+ export declare const cachedWorldBounds: (cache: ElementCache<Bounds>, shape: ElementBase) => Bounds;
40
+ //# sourceMappingURL=shape-cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shape-cache.d.ts","sourceRoot":"","sources":["../src/shape-cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAChE,OAAO,EAAyB,KAAK,KAAK,EAAE,KAAK,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAE7F;;;;;;;;;;GAUG;AACH,qBAAa,YAAY,CAAC,CAAC;IACzB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAiE;IAEzF,GAAG,CAAC,KAAK,EAAE,WAAW,GAAG,CAAC,GAAG,SAAS;IAUtC,GAAG,CAAC,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC;IAKpC;;;;OAIG;IACH,YAAY,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,CAAC,CAAC,EAAE,WAAW,KAAK,CAAC,GAAG,CAAC;IAMnE,UAAU,CAAC,EAAE,EAAE,SAAS,GAAG,IAAI;IAI/B,KAAK,IAAI,IAAI;IAIb,0DAA0D;IAC1D,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI;IAMzB,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,EAAE,YAAY,CAAC,MAAM,CAA8B,CAAC;AAElF,eAAO,MAAM,iBAAiB,GAAI,OAAO,YAAY,CAAC,MAAM,CAAC,EAAE,OAAO,WAAW,KAAG,MAClC,CAAC"}
@@ -0,0 +1,67 @@
1
+ import { getElementWorldBounds } from "@oh-just-another/scene";
2
+ /**
3
+ * Per-shape memo with object-identity invalidation. Cached value sticks
4
+ * until the underlying shape reference changes — and because every scene
5
+ * op (`updateElement` / `moveElement` / ...) returns a new shape object, the
6
+ * cache invalidates automatically without the caller threading versions
7
+ * through.
8
+ *
9
+ * Caches survive across frames; pair with `prune(scene)` after large
10
+ * deletions if memory matters. For ephemeral, single-render memos use
11
+ * a fresh `ElementCache` instance (cheap to construct).
12
+ */
13
+ export class ElementCache {
14
+ entries = new Map();
15
+ get(shape) {
16
+ const entry = this.entries.get(shape.id);
17
+ if (!entry)
18
+ return undefined;
19
+ if (entry.ref !== shape) {
20
+ this.entries.delete(shape.id);
21
+ return undefined;
22
+ }
23
+ return entry.value;
24
+ }
25
+ set(shape, value) {
26
+ this.entries.set(shape.id, { ref: shape, value });
27
+ return value;
28
+ }
29
+ /**
30
+ * Lazy memo. Returns the cached value if `shape` is the same reference
31
+ * as the one we cached against; otherwise runs `compute`, stores the
32
+ * result, and returns it.
33
+ */
34
+ getOrCompute(shape, compute) {
35
+ const cached = this.get(shape);
36
+ if (cached !== undefined)
37
+ return cached;
38
+ return this.set(shape, compute(shape));
39
+ }
40
+ invalidate(id) {
41
+ this.entries.delete(id);
42
+ }
43
+ clear() {
44
+ this.entries.clear();
45
+ }
46
+ /** Drop entries whose shape is no longer in the scene. */
47
+ prune(scene) {
48
+ for (const id of this.entries.keys()) {
49
+ if (!scene.elements.has(id))
50
+ this.entries.delete(id);
51
+ }
52
+ }
53
+ get size() {
54
+ return this.entries.size;
55
+ }
56
+ }
57
+ /**
58
+ * Shared module-level cache for world-space bounds. Used by `renderScene`
59
+ * for viewport culling and reusable from outside (hit-test, overlay) so
60
+ * a single computation amortizes across passes.
61
+ *
62
+ * `getElementWorldBounds` is pure — same shape ref → same bounds — so a
63
+ * by-identity cache is sound.
64
+ */
65
+ export const sharedBoundsCache = new ElementCache();
66
+ export const cachedWorldBounds = (cache, shape) => cache.getOrCompute(shape, getElementWorldBounds);
67
+ //# sourceMappingURL=shape-cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shape-cache.js","sourceRoot":"","sources":["../src/shape-cache.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,qBAAqB,EAAgC,MAAM,wBAAwB,CAAC;AAE7F;;;;;;;;;;GAUG;AACH,MAAM,OAAO,YAAY;IACN,OAAO,GAAG,IAAI,GAAG,EAAsD,CAAC;IAEzF,GAAG,CAAC,KAAkB;QACpB,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACzC,IAAI,CAAC,KAAK;YAAE,OAAO,SAAS,CAAC;QAC7B,IAAI,KAAK,CAAC,GAAG,KAAK,KAAK,EAAE,CAAC;YACxB,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YAC9B,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,OAAO,KAAK,CAAC,KAAK,CAAC;IACrB,CAAC;IAED,GAAG,CAAC,KAAkB,EAAE,KAAQ;QAC9B,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;QAClD,OAAO,KAAK,CAAC;IACf,CAAC;IAED;;;;OAIG;IACH,YAAY,CAAC,KAAkB,EAAE,OAA8B;QAC7D,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QAC/B,IAAI,MAAM,KAAK,SAAS;YAAE,OAAO,MAAM,CAAC;QACxC,OAAO,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;IACzC,CAAC;IAED,UAAU,CAAC,EAAa;QACtB,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAC1B,CAAC;IAED,KAAK;QACH,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;IACvB,CAAC;IAED,0DAA0D;IAC1D,KAAK,CAAC,KAAY;QAChB,KAAK,MAAM,EAAE,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC;YACrC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;gBAAE,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QACvD,CAAC;IACH,CAAC;IAED,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC;IAC3B,CAAC;CACF;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAyB,IAAI,YAAY,EAAU,CAAC;AAElF,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,KAA2B,EAAE,KAAkB,EAAU,EAAE,CAC3F,KAAK,CAAC,YAAY,CAAC,KAAK,EAAE,qBAAqB,CAAC,CAAC"}
@@ -0,0 +1,36 @@
1
+ import type { ElementBase } from "@oh-just-another/scene";
2
+ import type { RenderTarget } from "./render-target.js";
3
+ /**
4
+ * Optional draw context passed to an {@link ElementRenderer}. Carries the
5
+ * current view `zoom` so a renderer can draw screen-constant features (e.g. a
6
+ * 1px hairline border that does NOT scale with zoom): a local stroke width of
7
+ * `1 / (zoom * shape.scale)` lands at one device pixel. Optional and additive —
8
+ * renderers that don't need it ignore the third argument, and callers that
9
+ * can't supply it (preview / export at 1:1) may omit it.
10
+ */
11
+ export interface ElementRenderContext {
12
+ /** Current view scale (1.0 = 1:1). `world × zoom = screen px`. */
13
+ readonly zoom: number;
14
+ }
15
+ /**
16
+ * Draws a single shape onto `target`. The shape's `position` / `rotation` /
17
+ * `scale` have already been applied to the target — implementations draw in
18
+ * the shape's *local* coordinate space.
19
+ *
20
+ * Implementations should also apply style (fill / stroke / etc.) themselves;
21
+ * the renderer-core does not push styles globally because some shapes (e.g.
22
+ * `text`) extend the base `Style` with overlays.
23
+ */
24
+ export type ElementRenderer<S extends ElementBase = ElementBase> = (shape: S, target: RenderTarget, ctx?: ElementRenderContext) => void;
25
+ /**
26
+ * Register a renderer for a shape type. Plugins call this at module load.
27
+ * The kernel ships renderers for every built-in shape from `@oh-just-another/scene`
28
+ * — they are installed by `@oh-just-another/renderer-canvas` (and any other
29
+ * backend) on import.
30
+ */
31
+ export declare const registerElementRenderer: <S extends ElementBase>(type: S["type"], renderer: ElementRenderer<S>) => void;
32
+ /** Look up a registered renderer. Returns `undefined` for unknown types. */
33
+ export declare const getElementRenderer: (type: string) => ElementRenderer | undefined;
34
+ /** True if a renderer is registered for `type`. */
35
+ export declare const hasElementRenderer: (type: string) => boolean;
36
+ //# sourceMappingURL=shape-renderer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shape-renderer.d.ts","sourceRoot":"","sources":["../src/shape-renderer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD;;;;;;;GAOG;AACH,MAAM,WAAW,oBAAoB;IACnC,kEAAkE;IAClE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,eAAe,CAAC,CAAC,SAAS,WAAW,GAAG,WAAW,IAAI,CACjE,KAAK,EAAE,CAAC,EACR,MAAM,EAAE,YAAY,EACpB,GAAG,CAAC,EAAE,oBAAoB,KACvB,IAAI,CAAC;AAIV;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,GAAI,CAAC,SAAS,WAAW,EAC3D,MAAM,CAAC,CAAC,MAAM,CAAC,EACf,UAAU,eAAe,CAAC,CAAC,CAAC,KAC3B,IAEF,CAAC;AAEF,4EAA4E;AAC5E,eAAO,MAAM,kBAAkB,GAAI,MAAM,MAAM,KAAG,eAAe,GAAG,SAA+B,CAAC;AAEpG,mDAAmD;AACnD,eAAO,MAAM,kBAAkB,GAAI,MAAM,MAAM,KAAG,OAA6B,CAAC"}
@@ -0,0 +1,15 @@
1
+ const registry = new Map();
2
+ /**
3
+ * Register a renderer for a shape type. Plugins call this at module load.
4
+ * The kernel ships renderers for every built-in shape from `@oh-just-another/scene`
5
+ * — they are installed by `@oh-just-another/renderer-canvas` (and any other
6
+ * backend) on import.
7
+ */
8
+ export const registerElementRenderer = (type, renderer) => {
9
+ registry.set(type, renderer);
10
+ };
11
+ /** Look up a registered renderer. Returns `undefined` for unknown types. */
12
+ export const getElementRenderer = (type) => registry.get(type);
13
+ /** True if a renderer is registered for `type`. */
14
+ export const hasElementRenderer = (type) => registry.has(type);
15
+ //# sourceMappingURL=shape-renderer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shape-renderer.js","sourceRoot":"","sources":["../src/shape-renderer.ts"],"names":[],"mappings":"AA+BA,MAAM,QAAQ,GAAG,IAAI,GAAG,EAA2B,CAAC;AAEpD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,CACrC,IAAe,EACf,QAA4B,EACtB,EAAE;IACR,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,QAA2B,CAAC,CAAC;AAClD,CAAC,CAAC;AAEF,4EAA4E;AAC5E,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,IAAY,EAA+B,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AAEpG,mDAAmD;AACnD,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,IAAY,EAAW,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC"}