@oh-just-another/scene 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 (129) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENSE +21 -0
  3. package/README.md +72 -0
  4. package/dist/.tsbuildinfo +1 -0
  5. package/dist/a11y.d.ts +23 -0
  6. package/dist/a11y.d.ts.map +1 -0
  7. package/dist/a11y.js +37 -0
  8. package/dist/a11y.js.map +1 -0
  9. package/dist/anchors.d.ts +120 -0
  10. package/dist/anchors.d.ts.map +1 -0
  11. package/dist/anchors.js +322 -0
  12. package/dist/anchors.js.map +1 -0
  13. package/dist/annotation-geometry.d.ts +11 -0
  14. package/dist/annotation-geometry.d.ts.map +1 -0
  15. package/dist/annotation-geometry.js +19 -0
  16. package/dist/annotation-geometry.js.map +1 -0
  17. package/dist/annotation.d.ts +32 -0
  18. package/dist/annotation.d.ts.map +1 -0
  19. package/dist/annotation.js +2 -0
  20. package/dist/annotation.js.map +1 -0
  21. package/dist/constants.d.ts +186 -0
  22. package/dist/constants.d.ts.map +1 -0
  23. package/dist/constants.js +186 -0
  24. package/dist/constants.js.map +1 -0
  25. package/dist/container.d.ts +112 -0
  26. package/dist/container.d.ts.map +1 -0
  27. package/dist/container.js +227 -0
  28. package/dist/container.js.map +1 -0
  29. package/dist/diff.d.ts +22 -0
  30. package/dist/diff.d.ts.map +1 -0
  31. package/dist/diff.js +18 -0
  32. package/dist/diff.js.map +1 -0
  33. package/dist/edge-curve.d.ts +37 -0
  34. package/dist/edge-curve.d.ts.map +1 -0
  35. package/dist/edge-curve.js +72 -0
  36. package/dist/edge-curve.js.map +1 -0
  37. package/dist/edge-geometry.d.ts +96 -0
  38. package/dist/edge-geometry.d.ts.map +1 -0
  39. package/dist/edge-geometry.js +467 -0
  40. package/dist/edge-geometry.js.map +1 -0
  41. package/dist/edge.d.ts +206 -0
  42. package/dist/edge.d.ts.map +1 -0
  43. package/dist/edge.js +2 -0
  44. package/dist/edge.js.map +1 -0
  45. package/dist/elbow-link.d.ts +23 -0
  46. package/dist/elbow-link.d.ts.map +1 -0
  47. package/dist/elbow-link.js +419 -0
  48. package/dist/elbow-link.js.map +1 -0
  49. package/dist/elbow-router.d.ts +20 -0
  50. package/dist/elbow-router.d.ts.map +1 -0
  51. package/dist/elbow-router.js +218 -0
  52. package/dist/elbow-router.js.map +1 -0
  53. package/dist/file.d.ts +41 -0
  54. package/dist/file.d.ts.map +1 -0
  55. package/dist/file.js +13 -0
  56. package/dist/file.js.map +1 -0
  57. package/dist/heading.d.ts +42 -0
  58. package/dist/heading.d.ts.map +1 -0
  59. package/dist/heading.js +77 -0
  60. package/dist/heading.js.map +1 -0
  61. package/dist/index.d.ts +47 -0
  62. package/dist/index.d.ts.map +1 -0
  63. package/dist/index.js +40 -0
  64. package/dist/index.js.map +1 -0
  65. package/dist/layer.d.ts +18 -0
  66. package/dist/layer.d.ts.map +1 -0
  67. package/dist/layer.js +2 -0
  68. package/dist/layer.js.map +1 -0
  69. package/dist/layout-registry.d.ts +35 -0
  70. package/dist/layout-registry.d.ts.map +1 -0
  71. package/dist/layout-registry.js +13 -0
  72. package/dist/layout-registry.js.map +1 -0
  73. package/dist/layout.d.ts +128 -0
  74. package/dist/layout.d.ts.map +1 -0
  75. package/dist/layout.js +453 -0
  76. package/dist/layout.js.map +1 -0
  77. package/dist/operations.d.ts +32 -0
  78. package/dist/operations.d.ts.map +1 -0
  79. package/dist/operations.js +105 -0
  80. package/dist/operations.js.map +1 -0
  81. package/dist/outline.d.ts +36 -0
  82. package/dist/outline.d.ts.map +1 -0
  83. package/dist/outline.js +129 -0
  84. package/dist/outline.js.map +1 -0
  85. package/dist/patch.d.ts +57 -0
  86. package/dist/patch.d.ts.map +1 -0
  87. package/dist/patch.js +47 -0
  88. package/dist/patch.js.map +1 -0
  89. package/dist/queries.d.ts +115 -0
  90. package/dist/queries.d.ts.map +1 -0
  91. package/dist/queries.js +403 -0
  92. package/dist/queries.js.map +1 -0
  93. package/dist/render-bounds.d.ts +36 -0
  94. package/dist/render-bounds.d.ts.map +1 -0
  95. package/dist/render-bounds.js +36 -0
  96. package/dist/render-bounds.js.map +1 -0
  97. package/dist/scene.d.ts +80 -0
  98. package/dist/scene.d.ts.map +1 -0
  99. package/dist/scene.js +129 -0
  100. package/dist/scene.js.map +1 -0
  101. package/dist/shape.d.ts +306 -0
  102. package/dist/shape.d.ts.map +1 -0
  103. package/dist/shape.js +192 -0
  104. package/dist/shape.js.map +1 -0
  105. package/dist/snap.d.ts +88 -0
  106. package/dist/snap.d.ts.map +1 -0
  107. package/dist/snap.js +128 -0
  108. package/dist/snap.js.map +1 -0
  109. package/dist/spatial.d.ts +38 -0
  110. package/dist/spatial.d.ts.map +1 -0
  111. package/dist/spatial.js +95 -0
  112. package/dist/spatial.js.map +1 -0
  113. package/dist/style.d.ts +87 -0
  114. package/dist/style.d.ts.map +1 -0
  115. package/dist/style.js +43 -0
  116. package/dist/style.js.map +1 -0
  117. package/dist/text-measure.d.ts +29 -0
  118. package/dist/text-measure.d.ts.map +1 -0
  119. package/dist/text-measure.js +8 -0
  120. package/dist/text-measure.js.map +1 -0
  121. package/dist/three-way-merge.d.ts +59 -0
  122. package/dist/three-way-merge.d.ts.map +1 -0
  123. package/dist/three-way-merge.js +108 -0
  124. package/dist/three-way-merge.js.map +1 -0
  125. package/dist/viewport.d.ts +78 -0
  126. package/dist/viewport.d.ts.map +1 -0
  127. package/dist/viewport.js +62 -0
  128. package/dist/viewport.js.map +1 -0
  129. package/package.json +54 -0
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Tunable thresholds for the scene-level helpers (snap engine, hit-test
3
+ * cheap-cull). Keep magic numbers here so hosts can re-tune the engine
4
+ * without touching the algorithm code.
5
+ */
6
+ /**
7
+ * Half-side of the bounding box used by `isProbeNearElement` to cheap-cull
8
+ * snap candidates. Shapes farther than this from the probe (plus the
9
+ * snap threshold cushion) are skipped without the full anchor walk.
10
+ *
11
+ * The default of 1000 world units covers any typical editor shape; bump
12
+ * it if hosts work with very large diagrams where the cheap-cull starts
13
+ * to over-prune real candidates.
14
+ */
15
+ export const SNAP_PROBE_CULL_RADIUS = 1000;
16
+ /**
17
+ * Fallback grid spacing (world units) used by snap-to-grid when the
18
+ * viewport has no `gridSize` (grid hidden) but snapping is still on.
19
+ * Lets the user keep clean grid-aligned geometry without a visible
20
+ * grid. Matches the app's default `gridSize` of 20. Range: 4–64.
21
+ */
22
+ export const DEFAULT_GRID_SPACING = 20;
23
+ /**
24
+ * Padding (world units) the elbow router inflates obstacle bboxes
25
+ * by before searching. Larger values keep edges visibly clear of
26
+ * shapes; smaller values let the router squeeze through tight
27
+ * spaces. 20 px matches the gridSize default for diagrams that
28
+ * snap to a 20-unit grid.
29
+ */
30
+ export const ELBOW_OBSTACLE_MARGIN = 20;
31
+ /**
32
+ * Epsilon used to decide whether an axis-aligned segment runs
33
+ * *along* an obstacle boundary (allowed) or *through* it (blocked).
34
+ * A degenerate small value catches floating-point fuzz from
35
+ * `inflate` arithmetic without admitting real crossings.
36
+ */
37
+ export const ELBOW_OBSTACLE_INTERIOR_EPSILON = 0.5;
38
+ /**
39
+ * Per-turn cost added in the elbow A* so the router minimises BENDS first,
40
+ * distance second (lexicographic — far larger than any plausible canvas
41
+ * distance). Keeps routes stable: small shape moves no longer flip between
42
+ * equal-distance alternatives, and the path takes the fewest corners.
43
+ */
44
+ export const ELBOW_BEND_PENALTY = 100000;
45
+ /**
46
+ * Hysteresis band (world px) for the C-wrap side choice in `wrapRoute`.
47
+ * When the connector must wrap around the union of its two bound shapes,
48
+ * the over/under (or left/right) side is picked from the endpoints'
49
+ * midpoint vs the union centre. Within this band of the centre the
50
+ * previous side is kept (read from `edge.routedPoints`), so a small
51
+ * back-and-forth drag doesn't thrash; the side only switches once the
52
+ * midpoint moves this far past the centre. Larger = stickier.
53
+ * Range: 8–48.
54
+ */
55
+ export const ELBOW_WRAP_HYSTERESIS = 24;
56
+ /**
57
+ * Perf cap for "avoid obstacles" routing. The A* grid scales with the
58
+ * number of obstacle corners, and the route is recomputed every frame
59
+ * while a shape is dragged, so above this many scene shapes we skip
60
+ * whole-scene avoidance and fall back to the cheap two-box elbow (the
61
+ * link still keeps clear of its own ends). Range: 60–300.
62
+ */
63
+ export const ELBOW_AVOID_MAX_OBSTACLES = 150;
64
+ /**
65
+ * Length (world px) of the fixed, non-movable terminal segment an elbow
66
+ * connector always leaves at each end before its first bend — the endpoint
67
+ * is pushed out this far along its exit heading so the connector departs/
68
+ * arrives perpendicular to the edge and there's buffer room to draw the
69
+ * arrowhead. Must stay ≥ ELBOW_OBSTACLE_MARGIN so the pushed-out point
70
+ * sits outside the inflated obstacle the A* router avoids. Larger → more
71
+ * breathing room before the first bend. Range: 16–40.
72
+ */
73
+ export const ELBOW_TERMINAL_BUFFER = 30;
74
+ /**
75
+ * --- Self-loop connectors (a link whose both ends bind to the SAME element) ---
76
+ *
77
+ * A self-loop is routed OUTSIDE the element so it reads as a loop/arc instead of
78
+ * a flat line on (or across) the shape.
79
+ *
80
+ * - `SELF_LOOP_SIZE` — how far (world px) the loop bows out past the element's
81
+ * edge. Fixed for every element. Range: 24–80.
82
+ * - `SELF_LOOP_SPREAD` — when both ends resolve to the SAME point (centre /
83
+ * floating / same anchor) the two exit points are spread this far apart along
84
+ * the edge so the loop has width. Clamped to a third of the side. Range: 12–40.
85
+ * - `SELF_LOOP_CURVE_ARM_FACTOR` — control-arm length for a curved self-loop as a
86
+ * multiple of `SELF_LOOP_SIZE`; larger = rounder, more pronounced arc. 2.2
87
+ * gives a clean teardrop. Range: 1.5–3.
88
+ */
89
+ export const SELF_LOOP_SIZE = 40;
90
+ export const SELF_LOOP_SPREAD = 24;
91
+ export const SELF_LOOP_CURVE_ARM_FACTOR = 2.2;
92
+ /**
93
+ * Clearance (world px) a candidate centred path (the "thread"/mid-S or the
94
+ * C-wrap) must keep from a bound shape's interior before it counts as CROSSING
95
+ * it. This is the threshold that decides thread-vs-wrap-vs-A*: a larger value
96
+ * makes the router bail off the direct/centred path sooner (route stays further
97
+ * from shapes), a smaller value lets it skim closer to an edge before detouring.
98
+ * At 1 px it only rejects genuine interior crossings, allowing edge-grazing.
99
+ * Range: 1–8.
100
+ */
101
+ export const ELBOW_OBSTACLE_CLEARANCE = 1;
102
+ /**
103
+ * Parametric step used to sample each segment of a candidate path when testing
104
+ * whether it crosses a shape (`pathCrossesObstacle`). Smaller = finer (catches
105
+ * a narrow shape a coarse sampling would skip) at more cost; 0.1 samples 11
106
+ * points per segment, enough for typical shape sizes. Range: 0.02–0.2.
107
+ */
108
+ export const ELBOW_CROSS_SAMPLE_STEP = 0.1;
109
+ /**
110
+ * --- Curved (bezier) link geometry ---
111
+ *
112
+ * Shared by the renderer (draws cubic beziers), hit-testing and bounds
113
+ * (flatten the same curve) so the visible curve and the clickable curve
114
+ * agree. Lives in scene so lower layers own the geometry; renderer-core
115
+ * imports it.
116
+ *
117
+ * - `CURVE_CATMULL_TENSION` — divisor for the Catmull-Rom tangents in the
118
+ * spline→bezier conversion (waypointed curves). 6 is canonical uniform
119
+ * Catmull-Rom (control point = P + (Pnext − Pprev) / 6). Larger → tighter;
120
+ * smaller → looser. Range: 4–8.
121
+ * - `CURVE_END_TANGENT_RATIO` — for a no-waypoint span the cubic's control
122
+ * arms leave/enter the endpoints along their edge normals with length =
123
+ * this fraction of the endpoint distance, so the connector exits/enters
124
+ * perpendicular to the element edge (flowchart look). Larger → rounder /
125
+ * more pronounced. Range: 0.25–0.6.
126
+ * - `CURVE_END_TANGENT_MAX_PX` — caps that control-arm length (world px) so a
127
+ * long link doesn't over-bow. Range: 60–160.
128
+ * - `CURVE_FLATTEN_SEGMENTS` — samples per cubic when flattening the curve
129
+ * for hit-testing / bounds. Higher = closer to the drawn curve. Range:
130
+ * 8–24.
131
+ */
132
+ export const CURVE_CATMULL_TENSION = 6;
133
+ export const CURVE_END_TANGENT_RATIO = 0.8;
134
+ export const CURVE_END_TANGENT_MAX_PX = 240;
135
+ export const CURVE_FLATTEN_SEGMENTS = 16;
136
+ /**
137
+ * --- Roundness (Style.roundness) ---
138
+ *
139
+ * Adaptive radius: pick a fixed radius for shapes bigger than the cutoff,
140
+ * scale proportionally for smaller ones so the corner doesn't dominate.
141
+ * 32 px / 0.25 looks rounded without becoming a capsule across the
142
+ * realistic shape-size range.
143
+ */
144
+ /** Fixed pixel radius used by adaptive rounding for shapes ≥ cutoff. */
145
+ export const ADAPTIVE_CORNER_RADIUS = 32;
146
+ /**
147
+ * Proportional radius (0..1 of the smaller side) used by adaptive
148
+ * rounding for shapes below the cutoff, and the fall-through when
149
+ * `Roundness.value` is omitted but the type is `round`.
150
+ */
151
+ export const PROPORTIONAL_CORNER_RADIUS = 0.25;
152
+ /**
153
+ * --- Text bounds estimation ---
154
+ *
155
+ * The text bounder has no layout engine, so it approximates the box.
156
+ * Renderers compute the precise layout (via `measureText`) during
157
+ * draw / caret positioning; these factors only drive selection bbox
158
+ * and resize-handle placement, where a rough estimate is fine.
159
+ *
160
+ * - `TEXT_APPROX_CHAR_WIDTH_FACTOR` — average glyph advance as a
161
+ * fraction of font size (~0.6 for proportional Latin text).
162
+ * - `TEXT_LINE_HEIGHT_FACTOR` — line height as a multiple of font
163
+ * size. Must match the renderer's `DEFAULT_LINE_HEIGHT_FACTOR`.
164
+ */
165
+ export const TEXT_APPROX_CHAR_WIDTH_FACTOR = 0.6;
166
+ export const TEXT_LINE_HEIGHT_FACTOR = 1.2;
167
+ /**
168
+ * --- Frame header (label strip) geometry ---
169
+ *
170
+ * The frame's name is drawn in a strip ABOVE the frame body (local y in
171
+ * `[-FRAME_HEADER_HEIGHT, 0]`). Shared by the renderer (draws it), the
172
+ * editor (header double-click → rename hit zone + render overflow) and
173
+ * react-ui (positions the inline name editor) so all three agree.
174
+ *
175
+ * - `FRAME_HEADER_HEIGHT` — strip height (world px).
176
+ * - `FRAME_HEADER_PADDING_X` — horizontal text inset, each side (world px).
177
+ * - `FRAME_HEADER_FONT_SIZE` — label font size (world px).
178
+ *
179
+ * The strip width is dynamic: it hugs the label width but is capped at the
180
+ * frame's own width (a too-long name is ellipsised) — computed in the
181
+ * renderer, which can measure text.
182
+ */
183
+ export const FRAME_HEADER_HEIGHT = 24;
184
+ export const FRAME_HEADER_PADDING_X = 8;
185
+ export const FRAME_HEADER_FONT_SIZE = 12;
186
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.js","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,IAAI,CAAC;AAE3C;;;;;GAKG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,EAAE,CAAC;AAEvC;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,EAAE,CAAC;AAExC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,+BAA+B,GAAG,GAAG,CAAC;AAEnD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,MAAM,CAAC;AAEzC;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,EAAE,CAAC;AAExC;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,GAAG,CAAC;AAE7C;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,EAAE,CAAC;AAExC;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,EAAE,CAAC;AACjC,MAAM,CAAC,MAAM,gBAAgB,GAAG,EAAE,CAAC;AACnC,MAAM,CAAC,MAAM,0BAA0B,GAAG,GAAG,CAAC;AAE9C;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,CAAC;AAE1C;;;;;GAKG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,GAAG,CAAC;AAE3C;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC;AACvC,MAAM,CAAC,MAAM,uBAAuB,GAAG,GAAG,CAAC;AAC3C,MAAM,CAAC,MAAM,wBAAwB,GAAG,GAAG,CAAC;AAC5C,MAAM,CAAC,MAAM,sBAAsB,GAAG,EAAE,CAAC;AAEzC;;;;;;;GAOG;AAEH,wEAAwE;AACxE,MAAM,CAAC,MAAM,sBAAsB,GAAG,EAAE,CAAC;AAEzC;;;;GAIG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,IAAI,CAAC;AAE/C;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,6BAA6B,GAAG,GAAG,CAAC;AACjD,MAAM,CAAC,MAAM,uBAAuB,GAAG,GAAG,CAAC;AAE3C;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,EAAE,CAAC;AACtC,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC;AACxC,MAAM,CAAC,MAAM,sBAAsB,GAAG,EAAE,CAAC"}
@@ -0,0 +1,112 @@
1
+ import type { Bounds, ElementId, Vec2 } from "@oh-just-another/types";
2
+ import type { Scene } from "./scene.js";
3
+ import type { Element, ElementBase } from "./shape.js";
4
+ /**
5
+ * Container behaviour spec. Carried on a shape via
6
+ * `shape.metadata.container`. Any shape marked with this object becomes
7
+ * a container: a dragged child landing in its drop-zone is attached via
8
+ * `parentId`, moving the container moves children in lockstep, and a
9
+ * drop expands the container if the child goes past the zone.
10
+ *
11
+ * `dropZone` is stored in local coordinates (relative to
12
+ * `shape.position`). The helpers below convert to world and back.
13
+ *
14
+ * `padding` is the inset for auto-grow expansion: when the zone grows,
15
+ * the new edge is added with this margin.
16
+ */
17
+ export interface ContainerSpec {
18
+ readonly dropZone: Bounds;
19
+ readonly padding?: number;
20
+ }
21
+ /**
22
+ * Live resolver that computes the current `ContainerSpec` for a shape
23
+ * — typically by re-running the template's layout against its current
24
+ * `width` / `height` so the drop-zone stays in sync after the user
25
+ * resizes the container. Returns `null` when the resolver doesn't
26
+ * handle this shape; the next resolver in the chain (or the static
27
+ * `metadata.container` fallback) is tried.
28
+ *
29
+ * Registered by `@templates` so kernel packages stay UI-agnostic.
30
+ */
31
+ export type ContainerResolver = (shape: ElementBase) => ContainerSpec | null;
32
+ /**
33
+ * Plug in a resolver. Called in registration order — first non-null
34
+ * wins. Idempotent: a duplicate function reference is ignored so
35
+ * hot-reload doesn't double-register.
36
+ */
37
+ export declare const registerContainerResolver: (resolver: ContainerResolver) => void;
38
+ /** True when a shape is declared a container — via resolver or metadata. */
39
+ export declare const isContainer: (s: ElementBase) => boolean;
40
+ /**
41
+ * Resolve the current `ContainerSpec` for `s`. Tries every registered
42
+ * resolver first (live, layout-aware path for rich templates); then
43
+ * synthesises a live drop-zone for any shape carrying
44
+ * `metadata.autoLayout` + a `width`/`height` field — its zone is
45
+ * always `(padding, padding, width - 2*padding, height - 2*padding)`,
46
+ * which keeps the area in sync with the shape after a manual resize
47
+ * (no stale stored offsets); finally falls back to a fully static
48
+ * `metadata.container` spec for hand-authored containers. Returns
49
+ * `null` when the shape is not a container.
50
+ */
51
+ export declare const getContainerSpec: (s: ElementBase) => ContainerSpec | null;
52
+ /**
53
+ * World-coord drop-zone rect for the given container shape. Returns
54
+ * `null` when the shape is not a container.
55
+ */
56
+ export declare const getDropZoneWorld: (s: ElementBase) => Bounds | null;
57
+ /**
58
+ * Resolver returning EVERY drop-zone of a shape in local coords (relative
59
+ * to `shape.position`) — e.g. each lane of a multi-lane swim-lane. Distinct
60
+ * from `ContainerResolver`, which collapses to the single attach zone. Return
61
+ * `null` to defer to the next resolver. Registered by `@templates`.
62
+ */
63
+ export type ContainerZonesResolver = (shape: ElementBase) => readonly Bounds[] | null;
64
+ /** Plug in a multi-zone resolver. First non-null wins; idempotent. */
65
+ export declare const registerContainerZonesResolver: (resolver: ContainerZonesResolver) => void;
66
+ /**
67
+ * ALL drop-zones of `s` in world coords — every lane of a multi-lane
68
+ * container, not just the single attach zone. Tries the multi-zone resolvers
69
+ * first; falls back to the single `getDropZoneWorld` (wrapped) so non-template
70
+ * containers still report their one zone. Returns `[]` when the shape is not a
71
+ * container. Introspection only — the attach protocol uses the single-zone
72
+ * path (`getDropZoneWorld` / `findContainerAt`).
73
+ */
74
+ export declare const getDropZonesWorld: (s: ElementBase) => readonly Bounds[];
75
+ /**
76
+ * Topmost container shape whose drop-zone contains `worldPoint`.
77
+ * Walks layers top-down + shapes within layer top-down so the visually
78
+ * topmost container wins. `exclude` lets the caller skip shapes being
79
+ * dragged (a container can't drop onto itself / its own children).
80
+ *
81
+ * Nested containers: returns the innermost match — the deepest
82
+ * container whose dropZone contains the point.
83
+ */
84
+ export declare const findContainerAt: (scene: Scene, worldPoint: Vec2, exclude?: ReadonlySet<ElementId>) => Element | null;
85
+ /**
86
+ * Compute the new local-space drop-zone bounds that fit `childWorld`.
87
+ * Returns `null` when the child is already internal — no change needed.
88
+ *
89
+ * The returned rect is in local coordinates (relative to
90
+ * `shape.position`), suitable to write back into `metadata.container`.
91
+ * Padding inflates the new edge but is never subtracted from existing
92
+ * edges — the zone only grows, never shrinks.
93
+ */
94
+ export declare const expandDropZoneToFit: (container: ElementBase, childWorld: Bounds) => Bounds | null;
95
+ /**
96
+ * Required shape-level `width` / `height` (when present) that match
97
+ * the new local drop-zone. Used by the editor after `expandDropZoneToFit`
98
+ * to sync the container's visual with the expanded zone.
99
+ * Preserves the existing dropZone offset within the shape — if the
100
+ * dropZone didn't start at (0,0), the shape grows by exactly the amount
101
+ * added to the dropZone, without shifting the offset.
102
+ */
103
+ export declare const containerSizeForZone: (current: {
104
+ width: number;
105
+ height: number;
106
+ spec: ContainerSpec;
107
+ }, nextZone: Bounds) => {
108
+ width: number;
109
+ height: number;
110
+ positionOffset: Vec2;
111
+ };
112
+ //# sourceMappingURL=container.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"container.d.ts","sourceRoot":"","sources":["../src/container.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAEtE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAGvD;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;;;GASG;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,KAAK,EAAE,WAAW,KAAK,aAAa,GAAG,IAAI,CAAC;AAI7E;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,GAAI,UAAU,iBAAiB,KAAG,IAEvE,CAAC;AAEF,4EAA4E;AAC5E,eAAO,MAAM,WAAW,GAAI,GAAG,WAAW,KAAG,OAAuC,CAAC;AAErF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,gBAAgB,GAAI,GAAG,WAAW,KAAG,aAAa,GAAG,IA4CjE,CAAC;AAYF;;;GAGG;AACH,eAAO,MAAM,gBAAgB,GAAI,GAAG,WAAW,KAAG,MAAM,GAAG,IAS1D,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,KAAK,EAAE,WAAW,KAAK,SAAS,MAAM,EAAE,GAAG,IAAI,CAAC;AAItF,sEAAsE;AACtE,eAAO,MAAM,8BAA8B,GAAI,UAAU,sBAAsB,KAAG,IAEjF,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,GAAI,GAAG,WAAW,KAAG,SAAS,MAAM,EAcjE,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe,GAC1B,OAAO,KAAK,EACZ,YAAY,IAAI,EAChB,UAAS,WAAW,CAAC,SAAS,CAAa,KAC1C,OAAO,GAAG,IAgBZ,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,GAC9B,WAAW,WAAW,EACtB,YAAY,MAAM,KACjB,MAAM,GAAG,IAuCX,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAC/B,SAAS;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,aAAa,CAAA;CAAE,EAC/D,UAAU,MAAM,KACf;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,IAAI,CAAA;CAcvD,CAAC"}
@@ -0,0 +1,227 @@
1
+ import { bounds as B } from "@oh-just-another/math";
2
+ import { getLayersInOrder, getElementsInLayer } from "./queries.js";
3
+ const resolvers = [];
4
+ /**
5
+ * Plug in a resolver. Called in registration order — first non-null
6
+ * wins. Idempotent: a duplicate function reference is ignored so
7
+ * hot-reload doesn't double-register.
8
+ */
9
+ export const registerContainerResolver = (resolver) => {
10
+ if (!resolvers.includes(resolver))
11
+ resolvers.push(resolver);
12
+ };
13
+ /** True when a shape is declared a container — via resolver or metadata. */
14
+ export const isContainer = (s) => getContainerSpec(s) !== null;
15
+ /**
16
+ * Resolve the current `ContainerSpec` for `s`. Tries every registered
17
+ * resolver first (live, layout-aware path for rich templates); then
18
+ * synthesises a live drop-zone for any shape carrying
19
+ * `metadata.autoLayout` + a `width`/`height` field — its zone is
20
+ * always `(padding, padding, width - 2*padding, height - 2*padding)`,
21
+ * which keeps the area in sync with the shape after a manual resize
22
+ * (no stale stored offsets); finally falls back to a fully static
23
+ * `metadata.container` spec for hand-authored containers. Returns
24
+ * `null` when the shape is not a container.
25
+ */
26
+ export const getContainerSpec = (s) => {
27
+ for (const resolver of resolvers) {
28
+ const spec = resolver(s);
29
+ if (spec)
30
+ return spec;
31
+ }
32
+ // Synthesise a live drop-zone for auto-layout shapes (layout.auto-grid,
33
+ // layout.auto-stack, etc.). Source of truth is shape.width/height +
34
+ // padding — `metadata.container.dropZone`, if present, is treated as
35
+ // initial/serialised fallback only. Otherwise a resize of the frame
36
+ // wouldn't grow the drop-area (stored zone stays e.g. 296×176 even
37
+ // after the rectangle becomes 500×300).
38
+ const meta = s.metadata;
39
+ // ElementBase doesn't carry width/height (text / path / freedraw
40
+ // shapes lack them); detect through a type-erased probe rather
41
+ // than narrowing through TS overloads.
42
+ const sized = s;
43
+ if (meta?.autoLayout != null &&
44
+ typeof sized.width === "number" &&
45
+ typeof sized.height === "number") {
46
+ const padding = meta.container?.padding ?? AUTO_LAYOUT_DEFAULT_PADDING;
47
+ const w = sized.width;
48
+ const h = sized.height;
49
+ return {
50
+ dropZone: {
51
+ x: padding,
52
+ y: padding,
53
+ width: Math.max(0, w - 2 * padding),
54
+ height: Math.max(0, h - 2 * padding),
55
+ },
56
+ padding,
57
+ };
58
+ }
59
+ const m = s.metadata?.container;
60
+ if (!m || typeof m !== "object")
61
+ return null;
62
+ const obj = m;
63
+ if (!obj.dropZone)
64
+ return null;
65
+ const spec = obj.padding !== undefined
66
+ ? { dropZone: obj.dropZone, padding: obj.padding }
67
+ : { dropZone: obj.dropZone };
68
+ return spec;
69
+ };
70
+ /**
71
+ * Default symmetric inset (in world units) used by the auto-layout
72
+ * drop-zone synthesiser when the shape doesn't carry an explicit
73
+ * `metadata.container.padding`. Zero by default — a "raw" auto-layout
74
+ * shape places its first child at its own top-left corner. Templates
75
+ * (layout.auto-grid / layout.auto-stack) opt into a visible inset via
76
+ * `metadata.container.padding`.
77
+ */
78
+ const AUTO_LAYOUT_DEFAULT_PADDING = 0;
79
+ /**
80
+ * World-coord drop-zone rect for the given container shape. Returns
81
+ * `null` when the shape is not a container.
82
+ */
83
+ export const getDropZoneWorld = (s) => {
84
+ const spec = getContainerSpec(s);
85
+ if (!spec)
86
+ return null;
87
+ return {
88
+ x: s.position.x + spec.dropZone.x,
89
+ y: s.position.y + spec.dropZone.y,
90
+ width: spec.dropZone.width,
91
+ height: spec.dropZone.height,
92
+ };
93
+ };
94
+ const zonesResolvers = [];
95
+ /** Plug in a multi-zone resolver. First non-null wins; idempotent. */
96
+ export const registerContainerZonesResolver = (resolver) => {
97
+ if (!zonesResolvers.includes(resolver))
98
+ zonesResolvers.push(resolver);
99
+ };
100
+ /**
101
+ * ALL drop-zones of `s` in world coords — every lane of a multi-lane
102
+ * container, not just the single attach zone. Tries the multi-zone resolvers
103
+ * first; falls back to the single `getDropZoneWorld` (wrapped) so non-template
104
+ * containers still report their one zone. Returns `[]` when the shape is not a
105
+ * container. Introspection only — the attach protocol uses the single-zone
106
+ * path (`getDropZoneWorld` / `findContainerAt`).
107
+ */
108
+ export const getDropZonesWorld = (s) => {
109
+ for (const resolver of zonesResolvers) {
110
+ const local = resolver(s);
111
+ if (local) {
112
+ return local.map((z) => ({
113
+ x: s.position.x + z.x,
114
+ y: s.position.y + z.y,
115
+ width: z.width,
116
+ height: z.height,
117
+ }));
118
+ }
119
+ }
120
+ const single = getDropZoneWorld(s);
121
+ return single ? [single] : [];
122
+ };
123
+ /**
124
+ * Topmost container shape whose drop-zone contains `worldPoint`.
125
+ * Walks layers top-down + shapes within layer top-down so the visually
126
+ * topmost container wins. `exclude` lets the caller skip shapes being
127
+ * dragged (a container can't drop onto itself / its own children).
128
+ *
129
+ * Nested containers: returns the innermost match — the deepest
130
+ * container whose dropZone contains the point.
131
+ */
132
+ export const findContainerAt = (scene, worldPoint, exclude = new Set()) => {
133
+ const layers = getLayersInOrder(scene);
134
+ for (let i = layers.length - 1; i >= 0; i--) {
135
+ const layer = layers[i];
136
+ if (!layer?.visible)
137
+ continue;
138
+ const shapes = getElementsInLayer(scene, layer.id);
139
+ for (let j = shapes.length - 1; j >= 0; j--) {
140
+ const s = shapes[j];
141
+ if (s === undefined)
142
+ continue;
143
+ if (exclude.has(s.id))
144
+ continue;
145
+ const zone = getDropZoneWorld(s);
146
+ if (!zone)
147
+ continue;
148
+ if (B.contains(zone, worldPoint))
149
+ return s;
150
+ }
151
+ }
152
+ return null;
153
+ };
154
+ /**
155
+ * Compute the new local-space drop-zone bounds that fit `childWorld`.
156
+ * Returns `null` when the child is already internal — no change needed.
157
+ *
158
+ * The returned rect is in local coordinates (relative to
159
+ * `shape.position`), suitable to write back into `metadata.container`.
160
+ * Padding inflates the new edge but is never subtracted from existing
161
+ * edges — the zone only grows, never shrinks.
162
+ */
163
+ export const expandDropZoneToFit = (container, childWorld) => {
164
+ const spec = getContainerSpec(container);
165
+ if (!spec)
166
+ return null;
167
+ const padding = spec.padding ?? 0;
168
+ // Convert child world to container-local.
169
+ const childLocal = {
170
+ x: childWorld.x - container.position.x,
171
+ y: childWorld.y - container.position.y,
172
+ width: childWorld.width,
173
+ height: childWorld.height,
174
+ };
175
+ const z = spec.dropZone;
176
+ const cl = childLocal.x;
177
+ const ct = childLocal.y;
178
+ const cr = childLocal.x + childLocal.width;
179
+ const cb = childLocal.y + childLocal.height;
180
+ const zl = z.x;
181
+ const zt = z.y;
182
+ const zr = z.x + z.width;
183
+ const zb = z.y + z.height;
184
+ // Padding inflates ONLY the edge that the child crossed. A child
185
+ // resting exactly on an existing edge (`cl === zl`, common because
186
+ // `runAutoLayout` places the first child at the drop-zone's
187
+ // top-left corner) doesn't trigger expansion — without this guard
188
+ // every layout pass would shift the zone outward by `padding`,
189
+ // moving the container on every add and on every page reload.
190
+ const newMinX = cl < zl ? cl - padding : zl;
191
+ const newMinY = ct < zt ? ct - padding : zt;
192
+ const newMaxX = cr > zr ? cr + padding : zr;
193
+ const newMaxY = cb > zb ? cb + padding : zb;
194
+ if (newMinX === zl && newMinY === zt && newMaxX === zr && newMaxY === zb) {
195
+ return null;
196
+ }
197
+ return {
198
+ x: newMinX,
199
+ y: newMinY,
200
+ width: newMaxX - newMinX,
201
+ height: newMaxY - newMinY,
202
+ };
203
+ };
204
+ /**
205
+ * Required shape-level `width` / `height` (when present) that match
206
+ * the new local drop-zone. Used by the editor after `expandDropZoneToFit`
207
+ * to sync the container's visual with the expanded zone.
208
+ * Preserves the existing dropZone offset within the shape — if the
209
+ * dropZone didn't start at (0,0), the shape grows by exactly the amount
210
+ * added to the dropZone, without shifting the offset.
211
+ */
212
+ export const containerSizeForZone = (current, nextZone) => {
213
+ const z = current.spec.dropZone;
214
+ // The amount added on the left/top shifts the container's position,
215
+ // and the dropZone moves back by the same amount so the shape-to-zone
216
+ // offset stays the same.
217
+ const dxLeft = z.x - nextZone.x;
218
+ const dyTop = z.y - nextZone.y;
219
+ const dxRight = nextZone.x + nextZone.width - (z.x + z.width);
220
+ const dyBottom = nextZone.y + nextZone.height - (z.y + z.height);
221
+ return {
222
+ width: current.width + dxLeft + dxRight,
223
+ height: current.height + dyTop + dyBottom,
224
+ positionOffset: { x: -dxLeft, y: -dyTop },
225
+ };
226
+ };
227
+ //# sourceMappingURL=container.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"container.js","sourceRoot":"","sources":["../src/container.ts"],"names":[],"mappings":"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"}
package/dist/diff.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ import type { ElementId } from "@oh-just-another/types";
2
+ import type { Scene } from "./scene.js";
3
+ /**
4
+ * Element-level diff between two scenes. Categorises every shape id
5
+ * present in either scene as:
6
+ *
7
+ * - `added`: in `next` only.
8
+ * - `removed`: in `prev` only.
9
+ * - `modified`: in both, but the shape reference differs (immutable
10
+ * patches replace the object).
11
+ *
12
+ * Reference equality is sufficient — scene mutations always go
13
+ * through `apply(scene, patch)` which produces fresh shape objects
14
+ * for every change. Pure read paths don't allocate.
15
+ */
16
+ export interface SceneElementDiff {
17
+ readonly added: readonly ElementId[];
18
+ readonly removed: readonly ElementId[];
19
+ readonly modified: readonly ElementId[];
20
+ }
21
+ export declare const diffSceneElements: (prev: Scene, next: Scene) => SceneElementDiff;
22
+ //# sourceMappingURL=diff.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diff.d.ts","sourceRoot":"","sources":["../src/diff.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAExC;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,EAAE,SAAS,SAAS,EAAE,CAAC;IACrC,QAAQ,CAAC,OAAO,EAAE,SAAS,SAAS,EAAE,CAAC;IACvC,QAAQ,CAAC,QAAQ,EAAE,SAAS,SAAS,EAAE,CAAC;CACzC;AAED,eAAO,MAAM,iBAAiB,GAAI,MAAM,KAAK,EAAE,MAAM,KAAK,KAAG,gBAa5D,CAAC"}
package/dist/diff.js ADDED
@@ -0,0 +1,18 @@
1
+ export const diffSceneElements = (prev, next) => {
2
+ const added = [];
3
+ const removed = [];
4
+ const modified = [];
5
+ for (const [id, shape] of next.elements) {
6
+ const before = prev.elements.get(id);
7
+ if (before === undefined)
8
+ added.push(id);
9
+ else if (before !== shape)
10
+ modified.push(id);
11
+ }
12
+ for (const id of prev.elements.keys()) {
13
+ if (!next.elements.has(id))
14
+ removed.push(id);
15
+ }
16
+ return { added, removed, modified };
17
+ };
18
+ //# sourceMappingURL=diff.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diff.js","sourceRoot":"","sources":["../src/diff.ts"],"names":[],"mappings":"AAsBA,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,IAAW,EAAE,IAAW,EAAoB,EAAE;IAC9E,MAAM,KAAK,GAAgB,EAAE,CAAC;IAC9B,MAAM,OAAO,GAAgB,EAAE,CAAC;IAChC,MAAM,QAAQ,GAAgB,EAAE,CAAC;IACjC,KAAK,MAAM,CAAC,EAAE,EAAE,KAAK,CAAC,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;QACxC,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACrC,IAAI,MAAM,KAAK,SAAS;YAAE,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;aACpC,IAAI,MAAM,KAAK,KAAK;YAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC/C,CAAC;IACD,KAAK,MAAM,EAAE,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC;QACtC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;YAAE,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC/C,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC;AACtC,CAAC,CAAC"}
@@ -0,0 +1,37 @@
1
+ import type { Vec2 } from "@oh-just-another/types";
2
+ /**
3
+ * Curved (bezier) link geometry — the single source of truth for the curve
4
+ * shape so the renderer (draws cubic beziers) and hit-testing / bounds
5
+ * (flatten the same curve) never disagree. A curved link bows away from the
6
+ * straight polyline; testing clicks against the polyline would miss the
7
+ * visible arc, so callers flatten the curve here instead.
8
+ */
9
+ /** One cubic bezier segment: control points + end (start is implicit/prev). */
10
+ export interface BezierSegment {
11
+ readonly c1: Vec2;
12
+ readonly c2: Vec2;
13
+ readonly to: Vec2;
14
+ }
15
+ /**
16
+ * Cubic bezier from `from` to `to` that LEAVES `from` along `dirFrom` and
17
+ * ENTERS `to` along `-dirTo` (both unit outward directions). This makes a
18
+ * connector exit/enter perpendicular to an element's edge (flowchart look),
19
+ * instead of always bowing to one fixed side. Control-arm length scales with
20
+ * the endpoint distance (capped).
21
+ */
22
+ export declare const cubicWithEndTangents: (from: Vec2, to: Vec2, dirFrom: Vec2, dirTo: Vec2) => BezierSegment;
23
+ /**
24
+ * Catmull-Rom spline through `pts` as cubic bezier segments. Each segment
25
+ * Pi→Pi+1 uses tangents from the neighbouring points; endpoints duplicate
26
+ * themselves. The curve passes through every point with no corners. Used for
27
+ * a waypointed curve (the bends define the shape).
28
+ */
29
+ export declare const catmullRomBeziers: (pts: readonly Vec2[]) => BezierSegment[];
30
+ /** Point on a cubic bezier at parameter `t` (0..1). de Casteljau / Bernstein. */
31
+ export declare const cubicAt: (p0: Vec2, c1: Vec2, c2: Vec2, p1: Vec2, t: number) => Vec2;
32
+ /**
33
+ * Flatten a `start` + cubic-segment list into a dense point list (for
34
+ * hit-testing / bounds). Includes `start` then `perSegment` samples per cubic.
35
+ */
36
+ export declare const flattenSegments: (start: Vec2, segments: readonly BezierSegment[], perSegment?: number) => Vec2[];
37
+ //# sourceMappingURL=edge-curve.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"edge-curve.d.ts","sourceRoot":"","sources":["../src/edge-curve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAanD;;;;;;GAMG;AAEH,+EAA+E;AAC/E,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;CACnB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAC/B,MAAM,IAAI,EACV,IAAI,IAAI,EACR,SAAS,IAAI,EACb,OAAO,IAAI,KACV,aAQF,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,GAAI,KAAK,SAAS,IAAI,EAAE,KAAG,aAAa,EAerE,CAAC;AAEF,iFAAiF;AACjF,eAAO,MAAM,OAAO,GAAI,IAAI,IAAI,EAAE,IAAI,IAAI,EAAE,IAAI,IAAI,EAAE,IAAI,IAAI,EAAE,GAAG,MAAM,KAAG,IAU3E,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,eAAe,GAC1B,OAAO,IAAI,EACX,UAAU,SAAS,aAAa,EAAE,EAClC,mBAAmC,KAClC,IAAI,EAUN,CAAC"}