@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.
- package/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +72 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/a11y.d.ts +23 -0
- package/dist/a11y.d.ts.map +1 -0
- package/dist/a11y.js +37 -0
- package/dist/a11y.js.map +1 -0
- package/dist/anchors.d.ts +120 -0
- package/dist/anchors.d.ts.map +1 -0
- package/dist/anchors.js +322 -0
- package/dist/anchors.js.map +1 -0
- package/dist/annotation-geometry.d.ts +11 -0
- package/dist/annotation-geometry.d.ts.map +1 -0
- package/dist/annotation-geometry.js +19 -0
- package/dist/annotation-geometry.js.map +1 -0
- package/dist/annotation.d.ts +32 -0
- package/dist/annotation.d.ts.map +1 -0
- package/dist/annotation.js +2 -0
- package/dist/annotation.js.map +1 -0
- package/dist/constants.d.ts +186 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +186 -0
- package/dist/constants.js.map +1 -0
- package/dist/container.d.ts +112 -0
- package/dist/container.d.ts.map +1 -0
- package/dist/container.js +227 -0
- package/dist/container.js.map +1 -0
- package/dist/diff.d.ts +22 -0
- package/dist/diff.d.ts.map +1 -0
- package/dist/diff.js +18 -0
- package/dist/diff.js.map +1 -0
- package/dist/edge-curve.d.ts +37 -0
- package/dist/edge-curve.d.ts.map +1 -0
- package/dist/edge-curve.js +72 -0
- package/dist/edge-curve.js.map +1 -0
- package/dist/edge-geometry.d.ts +96 -0
- package/dist/edge-geometry.d.ts.map +1 -0
- package/dist/edge-geometry.js +467 -0
- package/dist/edge-geometry.js.map +1 -0
- package/dist/edge.d.ts +206 -0
- package/dist/edge.d.ts.map +1 -0
- package/dist/edge.js +2 -0
- package/dist/edge.js.map +1 -0
- package/dist/elbow-link.d.ts +23 -0
- package/dist/elbow-link.d.ts.map +1 -0
- package/dist/elbow-link.js +419 -0
- package/dist/elbow-link.js.map +1 -0
- package/dist/elbow-router.d.ts +20 -0
- package/dist/elbow-router.d.ts.map +1 -0
- package/dist/elbow-router.js +218 -0
- package/dist/elbow-router.js.map +1 -0
- package/dist/file.d.ts +41 -0
- package/dist/file.d.ts.map +1 -0
- package/dist/file.js +13 -0
- package/dist/file.js.map +1 -0
- package/dist/heading.d.ts +42 -0
- package/dist/heading.d.ts.map +1 -0
- package/dist/heading.js +77 -0
- package/dist/heading.js.map +1 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/dist/layer.d.ts +18 -0
- package/dist/layer.d.ts.map +1 -0
- package/dist/layer.js +2 -0
- package/dist/layer.js.map +1 -0
- package/dist/layout-registry.d.ts +35 -0
- package/dist/layout-registry.d.ts.map +1 -0
- package/dist/layout-registry.js +13 -0
- package/dist/layout-registry.js.map +1 -0
- package/dist/layout.d.ts +128 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/layout.js +453 -0
- package/dist/layout.js.map +1 -0
- package/dist/operations.d.ts +32 -0
- package/dist/operations.d.ts.map +1 -0
- package/dist/operations.js +105 -0
- package/dist/operations.js.map +1 -0
- package/dist/outline.d.ts +36 -0
- package/dist/outline.d.ts.map +1 -0
- package/dist/outline.js +129 -0
- package/dist/outline.js.map +1 -0
- package/dist/patch.d.ts +57 -0
- package/dist/patch.d.ts.map +1 -0
- package/dist/patch.js +47 -0
- package/dist/patch.js.map +1 -0
- package/dist/queries.d.ts +115 -0
- package/dist/queries.d.ts.map +1 -0
- package/dist/queries.js +403 -0
- package/dist/queries.js.map +1 -0
- package/dist/render-bounds.d.ts +36 -0
- package/dist/render-bounds.d.ts.map +1 -0
- package/dist/render-bounds.js +36 -0
- package/dist/render-bounds.js.map +1 -0
- package/dist/scene.d.ts +80 -0
- package/dist/scene.d.ts.map +1 -0
- package/dist/scene.js +129 -0
- package/dist/scene.js.map +1 -0
- package/dist/shape.d.ts +306 -0
- package/dist/shape.d.ts.map +1 -0
- package/dist/shape.js +192 -0
- package/dist/shape.js.map +1 -0
- package/dist/snap.d.ts +88 -0
- package/dist/snap.d.ts.map +1 -0
- package/dist/snap.js +128 -0
- package/dist/snap.js.map +1 -0
- package/dist/spatial.d.ts +38 -0
- package/dist/spatial.d.ts.map +1 -0
- package/dist/spatial.js +95 -0
- package/dist/spatial.js.map +1 -0
- package/dist/style.d.ts +87 -0
- package/dist/style.d.ts.map +1 -0
- package/dist/style.js +43 -0
- package/dist/style.js.map +1 -0
- package/dist/text-measure.d.ts +29 -0
- package/dist/text-measure.d.ts.map +1 -0
- package/dist/text-measure.js +8 -0
- package/dist/text-measure.js.map +1 -0
- package/dist/three-way-merge.d.ts +59 -0
- package/dist/three-way-merge.d.ts.map +1 -0
- package/dist/three-way-merge.js +108 -0
- package/dist/three-way-merge.js.map +1 -0
- package/dist/viewport.d.ts +78 -0
- package/dist/viewport.d.ts.map +1 -0
- package/dist/viewport.js +62 -0
- package/dist/viewport.js.map +1 -0
- 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":"AACA,OAAO,EAAE,MAAM,IAAI,CAAC,EAAE,MAAM,uBAAuB,CAAC;AAGpD,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAgCpE,MAAM,SAAS,GAAwB,EAAE,CAAC;AAE1C;;;;GAIG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,QAA2B,EAAQ,EAAE;IAC7E,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC;QAAE,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;AAC9D,CAAC,CAAC;AAEF,4EAA4E;AAC5E,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAc,EAAW,EAAE,CAAC,gBAAgB,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC;AAErF;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAc,EAAwB,EAAE;IACvE,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;QACjC,MAAM,IAAI,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;QACzB,IAAI,IAAI;YAAE,OAAO,IAAI,CAAC;IACxB,CAAC;IACD,wEAAwE;IACxE,oEAAoE;IACpE,qEAAqE;IACrE,oEAAoE;IACpE,mEAAmE;IACnE,wCAAwC;IACxC,MAAM,IAAI,GAAG,CAAC,CAAC,QAEF,CAAC;IACd,iEAAiE;IACjE,+DAA+D;IAC/D,uCAAuC;IACvC,MAAM,KAAK,GAAG,CAAqD,CAAC;IACpE,IACE,IAAI,EAAE,UAAU,IAAI,IAAI;QACxB,OAAO,KAAK,CAAC,KAAK,KAAK,QAAQ;QAC/B,OAAO,KAAK,CAAC,MAAM,KAAK,QAAQ,EAChC,CAAC;QACD,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,EAAE,OAAO,IAAI,2BAA2B,CAAC;QACvE,MAAM,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC;QACtB,MAAM,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC;QACvB,OAAO;YACL,QAAQ,EAAE;gBACR,CAAC,EAAE,OAAO;gBACV,CAAC,EAAE,OAAO;gBACV,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC;gBACnC,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC;aACrC;YACD,OAAO;SACR,CAAC;IACJ,CAAC;IACD,MAAM,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,SAAS,CAAC;IAChC,IAAI,CAAC,CAAC,IAAI,OAAO,CAAC,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC7C,MAAM,GAAG,GAAG,CAA4C,CAAC;IACzD,IAAI,CAAC,GAAG,CAAC,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC/B,MAAM,IAAI,GAAkB,GAAG,CAAC,OAAO,KAAK,SAAS;QACnD,CAAC,CAAC,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE;QAClD,CAAC,CAAC,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,EAAE,CAAC;IAC/B,OAAO,IAAI,CAAC;AACd,CAAC,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,2BAA2B,GAAG,CAAC,CAAC;AAEtC;;;GAGG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAc,EAAiB,EAAE;IAChE,MAAM,IAAI,GAAG,gBAAgB,CAAC,CAAC,CAAC,CAAC;IACjC,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IACvB,OAAO;QACL,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC;QACjC,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC;QACjC,KAAK,EAAE,IAAI,CAAC,QAAQ,CAAC,KAAK;QAC1B,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,MAAM;KAC7B,CAAC;AACJ,CAAC,CAAC;AAUF,MAAM,cAAc,GAA6B,EAAE,CAAC;AAEpD,sEAAsE;AACtE,MAAM,CAAC,MAAM,8BAA8B,GAAG,CAAC,QAAgC,EAAQ,EAAE;IACvF,IAAI,CAAC,cAAc,CAAC,QAAQ,CAAC,QAAQ,CAAC;QAAE,cAAc,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;AACxE,CAAC,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAc,EAAqB,EAAE;IACrE,KAAK,MAAM,QAAQ,IAAI,cAAc,EAAE,CAAC;QACtC,MAAM,KAAK,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;QAC1B,IAAI,KAAK,EAAE,CAAC;YACV,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;gBACvB,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;gBACrB,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;gBACrB,KAAK,EAAE,CAAC,CAAC,KAAK;gBACd,MAAM,EAAE,CAAC,CAAC,MAAM;aACjB,CAAC,CAAC,CAAC;QACN,CAAC;IACH,CAAC;IACD,MAAM,MAAM,GAAG,gBAAgB,CAAC,CAAC,CAAC,CAAC;IACnC,OAAO,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AAChC,CAAC,CAAC;AAEF;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAC7B,KAAY,EACZ,UAAgB,EAChB,UAAkC,IAAI,GAAG,EAAE,EAC3B,EAAE;IAClB,MAAM,MAAM,GAAG,gBAAgB,CAAC,KAAK,CAAC,CAAC;IACvC,KAAK,IAAI,CAAC,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC5C,MAAM,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;QACxB,IAAI,CAAC,KAAK,EAAE,OAAO;YAAE,SAAS;QAC9B,MAAM,MAAM,GAAG,kBAAkB,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC;QACnD,KAAK,IAAI,CAAC,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;YAC5C,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;YACpB,IAAI,CAAC,KAAK,SAAS;gBAAE,SAAS;YAC9B,IAAI,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;gBAAE,SAAS;YAChC,MAAM,IAAI,GAAG,gBAAgB,CAAC,CAAC,CAAC,CAAC;YACjC,IAAI,CAAC,IAAI;gBAAE,SAAS;YACpB,IAAI,CAAC,CAAC,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;gBAAE,OAAO,CAAC,CAAC;QAC7C,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC,CAAC;AAEF;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CACjC,SAAsB,EACtB,UAAkB,EACH,EAAE;IACjB,MAAM,IAAI,GAAG,gBAAgB,CAAC,SAAS,CAAC,CAAC;IACzC,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IACvB,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,CAAC,CAAC;IAClC,0CAA0C;IAC1C,MAAM,UAAU,GAAW;QACzB,CAAC,EAAE,UAAU,CAAC,CAAC,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;QACtC,CAAC,EAAE,UAAU,CAAC,CAAC,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;QACtC,KAAK,EAAE,UAAU,CAAC,KAAK;QACvB,MAAM,EAAE,UAAU,CAAC,MAAM;KAC1B,CAAC;IACF,MAAM,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC;IACxB,MAAM,EAAE,GAAG,UAAU,CAAC,CAAC,CAAC;IACxB,MAAM,EAAE,GAAG,UAAU,CAAC,CAAC,CAAC;IACxB,MAAM,EAAE,GAAG,UAAU,CAAC,CAAC,GAAG,UAAU,CAAC,KAAK,CAAC;IAC3C,MAAM,EAAE,GAAG,UAAU,CAAC,CAAC,GAAG,UAAU,CAAC,MAAM,CAAC;IAC5C,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;IACf,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;IACf,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC;IACzB,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC;IAC1B,iEAAiE;IACjE,mEAAmE;IACnE,4DAA4D;IAC5D,kEAAkE;IAClE,+DAA+D;IAC/D,8DAA8D;IAC9D,MAAM,OAAO,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;IAC5C,MAAM,OAAO,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;IAC5C,MAAM,OAAO,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;IAC5C,MAAM,OAAO,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;IAC5C,IAAI,OAAO,KAAK,EAAE,IAAI,OAAO,KAAK,EAAE,IAAI,OAAO,KAAK,EAAE,IAAI,OAAO,KAAK,EAAE,EAAE,CAAC;QACzE,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO;QACL,CAAC,EAAE,OAAO;QACV,CAAC,EAAE,OAAO;QACV,KAAK,EAAE,OAAO,GAAG,OAAO;QACxB,MAAM,EAAE,OAAO,GAAG,OAAO;KAC1B,CAAC;AACJ,CAAC,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAClC,OAA+D,EAC/D,QAAgB,EACyC,EAAE;IAC3D,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC;IAChC,oEAAoE;IACpE,sEAAsE;IACtE,yBAAyB;IACzB,MAAM,MAAM,GAAG,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC;IAChC,MAAM,KAAK,GAAG,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC;IAC/B,MAAM,OAAO,GAAG,QAAQ,CAAC,CAAC,GAAG,QAAQ,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC;IAC9D,MAAM,QAAQ,GAAG,QAAQ,CAAC,CAAC,GAAG,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC;IACjE,OAAO;QACL,KAAK,EAAE,OAAO,CAAC,KAAK,GAAG,MAAM,GAAG,OAAO;QACvC,MAAM,EAAE,OAAO,CAAC,MAAM,GAAG,KAAK,GAAG,QAAQ;QACzC,cAAc,EAAE,EAAE,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC,KAAK,EAAE;KAC1C,CAAC;AACJ,CAAC,CAAC"}
|
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
|
package/dist/diff.js.map
ADDED
|
@@ -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"}
|