@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
package/dist/snap.d.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { Vec2 } from "@oh-just-another/types";
|
|
2
|
+
import type { Scene } from "./scene.js";
|
|
3
|
+
/**
|
|
4
|
+
* One snap target a contributor can offer for a probe point. `snapped` is
|
|
5
|
+
* the world-space position the probe should jump to; `kind` and `metadata`
|
|
6
|
+
* help downstream consumers (renderers, edge-endpoint builders) decide what
|
|
7
|
+
* to do with the snap.
|
|
8
|
+
*/
|
|
9
|
+
export interface SnapCandidate {
|
|
10
|
+
readonly snapped: Vec2;
|
|
11
|
+
/** Squared distance from probe to `snapped`. Smaller wins. */
|
|
12
|
+
readonly distance: number;
|
|
13
|
+
/** What kind of snap this is. Hosts use it to decide downstream. */
|
|
14
|
+
readonly kind: "grid" | "anchor" | "outline" | "guideline";
|
|
15
|
+
/** Optional payload — anchor ref, outline ratio, alignment line, etc. */
|
|
16
|
+
readonly metadata?: Readonly<Record<string, unknown>>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Per-call context handed to every contributor. Includes the probe point,
|
|
20
|
+
* the active scene, an optional set of shape ids to exclude from snap (e.g.
|
|
21
|
+
* the shape currently being dragged), and the snap threshold in world units.
|
|
22
|
+
*/
|
|
23
|
+
export interface SnapContext {
|
|
24
|
+
readonly scene: Scene;
|
|
25
|
+
readonly probe: Vec2;
|
|
26
|
+
readonly threshold: number;
|
|
27
|
+
readonly excludeElementIds?: ReadonlySet<string>;
|
|
28
|
+
/**
|
|
29
|
+
* What kind of gesture is asking for snap. Contributors can filter
|
|
30
|
+
* themselves out by gesture (e.g. anchor snap only applies during edge
|
|
31
|
+
* draw, not when dragging a shape).
|
|
32
|
+
*/
|
|
33
|
+
readonly gesture: "draw-edge" | "edit-edge-endpoint" | "move-shape" | "draw-shape";
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* A snap source. Plugins implement this to add a new snap rule (e.g. snap
|
|
37
|
+
* to ruler lines, page margins, ...). Built-ins: grid, anchor, outline,
|
|
38
|
+
* guideline.
|
|
39
|
+
*
|
|
40
|
+
* Contributors return zero or more candidates. The engine keeps the best
|
|
41
|
+
* (closest within threshold) per `kind` and exposes them all so renderers
|
|
42
|
+
* can paint guides for each.
|
|
43
|
+
*/
|
|
44
|
+
export interface SnapContributor {
|
|
45
|
+
readonly id: string;
|
|
46
|
+
contribute(ctx: SnapContext): readonly SnapCandidate[];
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Combines contributors. Stateless — instance is a thin container.
|
|
50
|
+
*
|
|
51
|
+
* ```ts
|
|
52
|
+
* const engine = new SnapEngine([gridSnapper, anchorSnapper, outlineSnapper]);
|
|
53
|
+
* const result = engine.snap({ scene, probe, threshold: 8, gesture: "draw-edge" });
|
|
54
|
+
* if (result.best) editor.useSnap(result.best);
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export declare class SnapEngine {
|
|
58
|
+
private readonly contributors;
|
|
59
|
+
constructor(contributors: readonly SnapContributor[]);
|
|
60
|
+
/**
|
|
61
|
+
* Run every contributor, collect their candidates within `threshold`,
|
|
62
|
+
* and return them sorted by distance. `best` is the closest overall.
|
|
63
|
+
*/
|
|
64
|
+
snap(ctx: SnapContext): {
|
|
65
|
+
readonly best: SnapCandidate | null;
|
|
66
|
+
readonly all: readonly SnapCandidate[];
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Snap the probe to the nearest grid intersection. No-op when the scene
|
|
71
|
+
* viewport has no `gridSize`.
|
|
72
|
+
*/
|
|
73
|
+
export declare const gridSnapper: SnapContributor;
|
|
74
|
+
/**
|
|
75
|
+
* Snap edge endpoints to the nearest port on the shape under the probe.
|
|
76
|
+
* Only contributes for edge-related gestures.
|
|
77
|
+
*/
|
|
78
|
+
export declare const anchorSnapper: SnapContributor;
|
|
79
|
+
/**
|
|
80
|
+
* Snap edge endpoints to the nearest point on a shape's outline. Useful
|
|
81
|
+
* as a fallback when the user wants the connector "on this edge" rather
|
|
82
|
+
* than at one of the 9 named ports. Combines well with `anchorSnapper`:
|
|
83
|
+
* since anchors live on the same bounding rectangle, the anchor snap
|
|
84
|
+
* usually wins when the pointer is close to a corner / midpoint, and
|
|
85
|
+
* outline snap takes over for arbitrary positions in between.
|
|
86
|
+
*/
|
|
87
|
+
export declare const outlineSnapper: SnapContributor;
|
|
88
|
+
//# sourceMappingURL=snap.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snap.d.ts","sourceRoot":"","sources":["../src/snap.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAGnD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAKxC;;;;;GAKG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC;IACvB,8DAA8D;IAC9D,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,oEAAoE;IACpE,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,GAAG,WAAW,CAAC;IAC3D,yEAAyE;IACzE,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACvD;AAED;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC;IACrB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,iBAAiB,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACjD;;;;OAIG;IACH,QAAQ,CAAC,OAAO,EAAE,WAAW,GAAG,oBAAoB,GAAG,YAAY,GAAG,YAAY,CAAC;CACpF;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,GAAG,EAAE,WAAW,GAAG,SAAS,aAAa,EAAE,CAAC;CACxD;AAED;;;;;;;;GAQG;AACH,qBAAa,UAAU;IACT,OAAO,CAAC,QAAQ,CAAC,YAAY;gBAAZ,YAAY,EAAE,SAAS,eAAe,EAAE;IAErE;;;OAGG;IACH,IAAI,CAAC,GAAG,EAAE,WAAW,GAAG;QACtB,QAAQ,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI,CAAC;QACpC,QAAQ,CAAC,GAAG,EAAE,SAAS,aAAa,EAAE,CAAC;KACxC;CAUF;AAID;;;GAGG;AACH,eAAO,MAAM,WAAW,EAAE,eAazB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,aAAa,EAAE,eAqB3B,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,EAAE,eAqB5B,CAAC"}
|
package/dist/snap.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { findNearestOutlinePoint } from "./outline.js";
|
|
2
|
+
import { findNearestAnchor, snapExcludedAnchors } from "./anchors.js";
|
|
3
|
+
import { SNAP_PROBE_CULL_RADIUS } from "./constants.js";
|
|
4
|
+
/**
|
|
5
|
+
* Combines contributors. Stateless — instance is a thin container.
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* const engine = new SnapEngine([gridSnapper, anchorSnapper, outlineSnapper]);
|
|
9
|
+
* const result = engine.snap({ scene, probe, threshold: 8, gesture: "draw-edge" });
|
|
10
|
+
* if (result.best) editor.useSnap(result.best);
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
export class SnapEngine {
|
|
14
|
+
contributors;
|
|
15
|
+
constructor(contributors) {
|
|
16
|
+
this.contributors = contributors;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Run every contributor, collect their candidates within `threshold`,
|
|
20
|
+
* and return them sorted by distance. `best` is the closest overall.
|
|
21
|
+
*/
|
|
22
|
+
snap(ctx) {
|
|
23
|
+
const candidates = [];
|
|
24
|
+
for (const c of this.contributors) {
|
|
25
|
+
for (const cand of c.contribute(ctx)) {
|
|
26
|
+
if (cand.distance <= ctx.threshold * ctx.threshold)
|
|
27
|
+
candidates.push(cand);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
candidates.sort((a, b) => a.distance - b.distance);
|
|
31
|
+
return { best: candidates[0] ?? null, all: candidates };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// --- Built-in contributors ---
|
|
35
|
+
/**
|
|
36
|
+
* Snap the probe to the nearest grid intersection. No-op when the scene
|
|
37
|
+
* viewport has no `gridSize`.
|
|
38
|
+
*/
|
|
39
|
+
export const gridSnapper = {
|
|
40
|
+
id: "grid",
|
|
41
|
+
contribute(ctx) {
|
|
42
|
+
const size = ctx.scene.viewport.gridSize;
|
|
43
|
+
if (!size || size <= 0)
|
|
44
|
+
return [];
|
|
45
|
+
const snapped = {
|
|
46
|
+
x: Math.round(ctx.probe.x / size) * size,
|
|
47
|
+
y: Math.round(ctx.probe.y / size) * size,
|
|
48
|
+
};
|
|
49
|
+
const dx = snapped.x - ctx.probe.x;
|
|
50
|
+
const dy = snapped.y - ctx.probe.y;
|
|
51
|
+
return [{ snapped, distance: dx * dx + dy * dy, kind: "grid" }];
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Snap edge endpoints to the nearest port on the shape under the probe.
|
|
56
|
+
* Only contributes for edge-related gestures.
|
|
57
|
+
*/
|
|
58
|
+
export const anchorSnapper = {
|
|
59
|
+
id: "anchor",
|
|
60
|
+
contribute(ctx) {
|
|
61
|
+
if (ctx.gesture !== "draw-edge" && ctx.gesture !== "edit-edge-endpoint")
|
|
62
|
+
return [];
|
|
63
|
+
const out = [];
|
|
64
|
+
for (const shape of ctx.scene.elements.values()) {
|
|
65
|
+
if (ctx.excludeElementIds?.has(shape.id))
|
|
66
|
+
continue;
|
|
67
|
+
// Cheap reject: skip shapes whose AABB is far from the probe.
|
|
68
|
+
if (!isProbeNearElement(shape, ctx.probe, ctx.threshold))
|
|
69
|
+
continue;
|
|
70
|
+
const nearest = findNearestAnchor(shape, ctx.probe, snapExcludedAnchors(shape));
|
|
71
|
+
const dx = nearest.world.x - ctx.probe.x;
|
|
72
|
+
const dy = nearest.world.y - ctx.probe.y;
|
|
73
|
+
out.push({
|
|
74
|
+
snapped: nearest.world,
|
|
75
|
+
distance: dx * dx + dy * dy,
|
|
76
|
+
kind: "anchor",
|
|
77
|
+
metadata: { elementId: shape.id, ref: nearest.ref },
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Snap edge endpoints to the nearest point on a shape's outline. Useful
|
|
85
|
+
* as a fallback when the user wants the connector "on this edge" rather
|
|
86
|
+
* than at one of the 9 named ports. Combines well with `anchorSnapper`:
|
|
87
|
+
* since anchors live on the same bounding rectangle, the anchor snap
|
|
88
|
+
* usually wins when the pointer is close to a corner / midpoint, and
|
|
89
|
+
* outline snap takes over for arbitrary positions in between.
|
|
90
|
+
*/
|
|
91
|
+
export const outlineSnapper = {
|
|
92
|
+
id: "outline",
|
|
93
|
+
contribute(ctx) {
|
|
94
|
+
if (ctx.gesture !== "draw-edge" && ctx.gesture !== "edit-edge-endpoint")
|
|
95
|
+
return [];
|
|
96
|
+
const out = [];
|
|
97
|
+
for (const shape of ctx.scene.elements.values()) {
|
|
98
|
+
if (ctx.excludeElementIds?.has(shape.id))
|
|
99
|
+
continue;
|
|
100
|
+
if (!isProbeNearElement(shape, ctx.probe, ctx.threshold))
|
|
101
|
+
continue;
|
|
102
|
+
const nearest = findNearestOutlinePoint(shape, ctx.probe);
|
|
103
|
+
if (!nearest)
|
|
104
|
+
continue;
|
|
105
|
+
const dx = nearest.world.x - ctx.probe.x;
|
|
106
|
+
const dy = nearest.world.y - ctx.probe.y;
|
|
107
|
+
out.push({
|
|
108
|
+
snapped: nearest.world,
|
|
109
|
+
distance: dx * dx + dy * dy,
|
|
110
|
+
kind: "outline",
|
|
111
|
+
metadata: { elementId: shape.id, ratio: nearest.ratio },
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
const isProbeNearElement = (shape, probe, threshold) => {
|
|
118
|
+
// Cheap AABB-around-position test. Hosts can register more precise
|
|
119
|
+
// contributors if their shape types deserve it.
|
|
120
|
+
const cushion = threshold;
|
|
121
|
+
const px = probe.x - shape.position.x;
|
|
122
|
+
const py = probe.y - shape.position.y;
|
|
123
|
+
// `SNAP_PROBE_CULL_RADIUS` is intentionally generous; the inner
|
|
124
|
+
// `findNearestAnchor` does the precise check.
|
|
125
|
+
return (Math.abs(px) <= SNAP_PROBE_CULL_RADIUS + cushion &&
|
|
126
|
+
Math.abs(py) <= SNAP_PROBE_CULL_RADIUS + cushion);
|
|
127
|
+
};
|
|
128
|
+
//# sourceMappingURL=snap.js.map
|
package/dist/snap.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snap.js","sourceRoot":"","sources":["../src/snap.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Bounds, ElementId } from "@oh-just-another/types";
|
|
2
|
+
/**
|
|
3
|
+
* Uniform-cell spatial index over shape world-AABBs.
|
|
4
|
+
*
|
|
5
|
+
* Cells are addressed by `(cellX, cellY) = (floor(x / cellSize), floor(y / cellSize))`,
|
|
6
|
+
* and a shape's AABB is registered in every cell it overlaps. Range queries
|
|
7
|
+
* iterate the overlapping cells and collect candidates; the caller filters
|
|
8
|
+
* against the precise AABB.
|
|
9
|
+
*
|
|
10
|
+
* Trade-off vs an R-tree: a grid is simpler, has predictable memory, and is
|
|
11
|
+
* very fast for editor-scale scenes (≤ ~10k shapes) where cell count is small
|
|
12
|
+
* and shapes are roughly uniform in size.
|
|
13
|
+
*/
|
|
14
|
+
export declare class SpatialGrid {
|
|
15
|
+
/** Cell side length in world units. */
|
|
16
|
+
readonly cellSize: number;
|
|
17
|
+
/** `cellKey(cellX, cellY)` → set of shape ids overlapping the cell. */
|
|
18
|
+
private readonly cells;
|
|
19
|
+
/** Reverse index: shape id → cached AABB, used on remove/update. */
|
|
20
|
+
private readonly bounds;
|
|
21
|
+
constructor(cellSize?: number);
|
|
22
|
+
/** Number of indexed shapes. */
|
|
23
|
+
get size(): number;
|
|
24
|
+
insert(id: ElementId, b: Bounds): void;
|
|
25
|
+
remove(id: ElementId): void;
|
|
26
|
+
update(id: ElementId, b: Bounds): void;
|
|
27
|
+
/**
|
|
28
|
+
* Ids whose AABB overlaps `range`. The returned set is a superset of the
|
|
29
|
+
* true answer — the caller must filter against precise bounds. Order is
|
|
30
|
+
* arbitrary.
|
|
31
|
+
*/
|
|
32
|
+
query(range: Bounds): ReadonlySet<ElementId>;
|
|
33
|
+
/** Drop all entries. */
|
|
34
|
+
clear(): void;
|
|
35
|
+
/** Iterate cell keys that overlap `b`. */
|
|
36
|
+
private eachCell;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=spatial.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spatial.d.ts","sourceRoot":"","sources":["../src/spatial.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAEhE;;;;;;;;;;;GAWG;AACH,qBAAa,WAAW;IACtB,uCAAuC;IACvC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAE1B,uEAAuE;IACvE,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqC;IAE3D,oEAAoE;IACpE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgC;gBAE3C,QAAQ,SAAM;IAK1B,gCAAgC;IAChC,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI;IAetC,MAAM,CAAC,EAAE,EAAE,SAAS,GAAG,IAAI;IAY3B,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI;IAKtC;;;;OAIG;IACH,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,CAAC,SAAS,CAAC;IAU5C,wBAAwB;IACxB,KAAK,IAAI,IAAI;IAKb,0CAA0C;IAC1C,OAAO,CAAC,QAAQ;CAWjB"}
|
package/dist/spatial.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Uniform-cell spatial index over shape world-AABBs.
|
|
3
|
+
*
|
|
4
|
+
* Cells are addressed by `(cellX, cellY) = (floor(x / cellSize), floor(y / cellSize))`,
|
|
5
|
+
* and a shape's AABB is registered in every cell it overlaps. Range queries
|
|
6
|
+
* iterate the overlapping cells and collect candidates; the caller filters
|
|
7
|
+
* against the precise AABB.
|
|
8
|
+
*
|
|
9
|
+
* Trade-off vs an R-tree: a grid is simpler, has predictable memory, and is
|
|
10
|
+
* very fast for editor-scale scenes (≤ ~10k shapes) where cell count is small
|
|
11
|
+
* and shapes are roughly uniform in size.
|
|
12
|
+
*/
|
|
13
|
+
export class SpatialGrid {
|
|
14
|
+
/** Cell side length in world units. */
|
|
15
|
+
cellSize;
|
|
16
|
+
/** `cellKey(cellX, cellY)` → set of shape ids overlapping the cell. */
|
|
17
|
+
cells = new Map();
|
|
18
|
+
/** Reverse index: shape id → cached AABB, used on remove/update. */
|
|
19
|
+
bounds = new Map();
|
|
20
|
+
constructor(cellSize = 256) {
|
|
21
|
+
if (cellSize <= 0)
|
|
22
|
+
throw new Error("cellSize must be positive");
|
|
23
|
+
this.cellSize = cellSize;
|
|
24
|
+
}
|
|
25
|
+
/** Number of indexed shapes. */
|
|
26
|
+
get size() {
|
|
27
|
+
return this.bounds.size;
|
|
28
|
+
}
|
|
29
|
+
insert(id, b) {
|
|
30
|
+
if (this.bounds.has(id)) {
|
|
31
|
+
throw new Error(`Element already indexed: ${id}`);
|
|
32
|
+
}
|
|
33
|
+
this.bounds.set(id, b);
|
|
34
|
+
this.eachCell(b, (key) => {
|
|
35
|
+
let cell = this.cells.get(key);
|
|
36
|
+
if (!cell) {
|
|
37
|
+
cell = new Set();
|
|
38
|
+
this.cells.set(key, cell);
|
|
39
|
+
}
|
|
40
|
+
cell.add(id);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
remove(id) {
|
|
44
|
+
const b = this.bounds.get(id);
|
|
45
|
+
if (!b)
|
|
46
|
+
return;
|
|
47
|
+
this.bounds.delete(id);
|
|
48
|
+
this.eachCell(b, (key) => {
|
|
49
|
+
const cell = this.cells.get(key);
|
|
50
|
+
if (!cell)
|
|
51
|
+
return;
|
|
52
|
+
cell.delete(id);
|
|
53
|
+
if (cell.size === 0)
|
|
54
|
+
this.cells.delete(key);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
update(id, b) {
|
|
58
|
+
this.remove(id);
|
|
59
|
+
this.insert(id, b);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Ids whose AABB overlaps `range`. The returned set is a superset of the
|
|
63
|
+
* true answer — the caller must filter against precise bounds. Order is
|
|
64
|
+
* arbitrary.
|
|
65
|
+
*/
|
|
66
|
+
query(range) {
|
|
67
|
+
const out = new Set();
|
|
68
|
+
this.eachCell(range, (key) => {
|
|
69
|
+
const cell = this.cells.get(key);
|
|
70
|
+
if (!cell)
|
|
71
|
+
return;
|
|
72
|
+
for (const id of cell)
|
|
73
|
+
out.add(id);
|
|
74
|
+
});
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
/** Drop all entries. */
|
|
78
|
+
clear() {
|
|
79
|
+
this.cells.clear();
|
|
80
|
+
this.bounds.clear();
|
|
81
|
+
}
|
|
82
|
+
/** Iterate cell keys that overlap `b`. */
|
|
83
|
+
eachCell(b, fn) {
|
|
84
|
+
const minCx = Math.floor(b.x / this.cellSize);
|
|
85
|
+
const minCy = Math.floor(b.y / this.cellSize);
|
|
86
|
+
const maxCx = Math.floor((b.x + b.width) / this.cellSize);
|
|
87
|
+
const maxCy = Math.floor((b.y + b.height) / this.cellSize);
|
|
88
|
+
for (let cy = minCy; cy <= maxCy; cy++) {
|
|
89
|
+
for (let cx = minCx; cx <= maxCx; cx++) {
|
|
90
|
+
fn(`${cx},${cy}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=spatial.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spatial.js","sourceRoot":"","sources":["../src/spatial.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;GAWG;AACH,MAAM,OAAO,WAAW;IACtB,uCAAuC;IAC9B,QAAQ,CAAS;IAE1B,uEAAuE;IACtD,KAAK,GAAG,IAAI,GAAG,EAA0B,CAAC;IAE3D,oEAAoE;IACnD,MAAM,GAAG,IAAI,GAAG,EAAqB,CAAC;IAEvD,YAAY,QAAQ,GAAG,GAAG;QACxB,IAAI,QAAQ,IAAI,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,2BAA2B,CAAC,CAAC;QAChE,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC3B,CAAC;IAED,gCAAgC;IAChC,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC;IAC1B,CAAC;IAED,MAAM,CAAC,EAAa,EAAE,CAAS;QAC7B,IAAI,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC;YACxB,MAAM,IAAI,KAAK,CAAC,4BAA4B,EAAE,EAAE,CAAC,CAAC;QACpD,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;QACvB,IAAI,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE;YACvB,IAAI,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YAC/B,IAAI,CAAC,IAAI,EAAE,CAAC;gBACV,IAAI,GAAG,IAAI,GAAG,EAAE,CAAC;gBACjB,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;YAC5B,CAAC;YACD,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACf,CAAC,CAAC,CAAC;IACL,CAAC;IAED,MAAM,CAAC,EAAa;QAClB,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC9B,IAAI,CAAC,CAAC;YAAE,OAAO;QACf,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QACvB,IAAI,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE;YACvB,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YACjC,IAAI,CAAC,IAAI;gBAAE,OAAO;YAClB,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YAChB,IAAI,IAAI,CAAC,IAAI,KAAK,CAAC;gBAAE,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC9C,CAAC,CAAC,CAAC;IACL,CAAC;IAED,MAAM,CAAC,EAAa,EAAE,CAAS;QAC7B,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAChB,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;IACrB,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,KAAa;QACjB,MAAM,GAAG,GAAG,IAAI,GAAG,EAAa,CAAC;QACjC,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC,GAAG,EAAE,EAAE;YAC3B,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YACjC,IAAI,CAAC,IAAI;gBAAE,OAAO;YAClB,KAAK,MAAM,EAAE,IAAI,IAAI;gBAAE,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACrC,CAAC,CAAC,CAAC;QACH,OAAO,GAAG,CAAC;IACb,CAAC;IAED,wBAAwB;IACxB,KAAK;QACH,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;QACnB,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;IACtB,CAAC;IAED,0CAA0C;IAClC,QAAQ,CAAC,CAAS,EAAE,EAAyB;QACnD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC9C,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC9C,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC1D,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC3D,KAAK,IAAI,EAAE,GAAG,KAAK,EAAE,EAAE,IAAI,KAAK,EAAE,EAAE,EAAE,EAAE,CAAC;YACvC,KAAK,IAAI,EAAE,GAAG,KAAK,EAAE,EAAE,IAAI,KAAK,EAAE,EAAE,EAAE,EAAE,CAAC;gBACvC,EAAE,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC;YACpB,CAAC;QACH,CAAC;IACH,CAAC;CACF"}
|
package/dist/style.d.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { Color } from "@oh-just-another/types";
|
|
2
|
+
export type LineCap = "butt" | "round" | "square";
|
|
3
|
+
export type LineJoin = "miter" | "round" | "bevel";
|
|
4
|
+
/**
|
|
5
|
+
* Where the stroke sits relative to the shape's path.
|
|
6
|
+
* `center` — half the stroke width inside the path, half outside.
|
|
7
|
+
* Canvas2D / SVG default.
|
|
8
|
+
* `inside` — stroke is fully inside the path (path = outer edge).
|
|
9
|
+
* Useful when shape bounds must match the fill region
|
|
10
|
+
* exactly (auto-layout / hit-tests).
|
|
11
|
+
* `outside` — stroke is fully outside (path = inner edge).
|
|
12
|
+
*/
|
|
13
|
+
export type StrokeAlign = "center" | "inside" | "outside";
|
|
14
|
+
/**
|
|
15
|
+
* Corner-rounding spec for shapes that support it (rectangle, container,
|
|
16
|
+
* box arrow, …). Borrowed from standard's adaptive model:
|
|
17
|
+
* `sharp` — no rounding (sharp corners). Equivalent to omitting the
|
|
18
|
+
* field; kept for explicit serialisation.
|
|
19
|
+
* `round` — rounded corners. Without `value`, falls back to the
|
|
20
|
+
* adaptive radius (fixed 32 px for big shapes, scales to
|
|
21
|
+
* 25 % of the smaller side for shapes < 128 px so they
|
|
22
|
+
* don't read as a capsule).
|
|
23
|
+
*/
|
|
24
|
+
export interface Roundness {
|
|
25
|
+
readonly type: "sharp" | "round";
|
|
26
|
+
/**
|
|
27
|
+
* Override the rounded-corner radius in world units. Ignored when
|
|
28
|
+
* `type === "sharp"`. When omitted on `round` shapes the renderer
|
|
29
|
+
* applies the adaptive default (see {@link Style}).
|
|
30
|
+
*/
|
|
31
|
+
readonly value?: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Visual style for shapes and edges. Every field is optional so that scenes,
|
|
35
|
+
* patches and partial updates stay compact; renderers fall back to library
|
|
36
|
+
* defaults when a field is omitted.
|
|
37
|
+
*/
|
|
38
|
+
export interface Style {
|
|
39
|
+
readonly fill?: Color;
|
|
40
|
+
readonly stroke?: Color;
|
|
41
|
+
readonly strokeWidth?: number;
|
|
42
|
+
readonly opacity?: number;
|
|
43
|
+
readonly dashArray?: readonly number[];
|
|
44
|
+
readonly lineCap?: LineCap;
|
|
45
|
+
readonly lineJoin?: LineJoin;
|
|
46
|
+
/** Stroke alignment relative to the path. Defaults to `center`. */
|
|
47
|
+
readonly strokeAlign?: StrokeAlign;
|
|
48
|
+
/** Corner-rounding spec. Omitted = sharp corners. */
|
|
49
|
+
readonly roundness?: Roundness;
|
|
50
|
+
}
|
|
51
|
+
export type TextAlign = "left" | "center" | "right";
|
|
52
|
+
export type TextBaseline = "top" | "middle" | "bottom";
|
|
53
|
+
export type FontWeight = "normal" | "bold";
|
|
54
|
+
export type FontStyle = "normal" | "italic";
|
|
55
|
+
/**
|
|
56
|
+
* Text decorations (underline / strikethrough). Both can be on at once.
|
|
57
|
+
* Rendered as thin line-rects under / through the text by the renderer,
|
|
58
|
+
* so they work identically on Canvas2D and WebGL2.
|
|
59
|
+
*/
|
|
60
|
+
export interface TextDecoration {
|
|
61
|
+
readonly underline?: boolean;
|
|
62
|
+
readonly strikethrough?: boolean;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Text-specific style overlay. Inherits all `Style` fields (fill = text color,
|
|
66
|
+
* stroke = outline). Layout metrics live on the `TextElement` itself, not here.
|
|
67
|
+
*/
|
|
68
|
+
export interface TextStyle extends Style {
|
|
69
|
+
readonly textAlign?: TextAlign;
|
|
70
|
+
readonly textBaseline?: TextBaseline;
|
|
71
|
+
/** Bold toggle. Default `"normal"`. */
|
|
72
|
+
readonly fontWeight?: FontWeight;
|
|
73
|
+
/** Italic toggle. Default `"normal"`. */
|
|
74
|
+
readonly fontStyle?: FontStyle;
|
|
75
|
+
/** Underline / strikethrough. Omitted = neither. */
|
|
76
|
+
readonly textDecoration?: TextDecoration;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* How far a shape's stroke extends OUTSIDE its geometric contour, in world
|
|
80
|
+
* units. Depends on stroke width and alignment: `outside` → the full width,
|
|
81
|
+
* `center` → half, `inside` → none. No stroke → 0. Used to place the
|
|
82
|
+
* selection halo a constant distance beyond the shape's VISIBLE outer edge
|
|
83
|
+
* (contour + this extent), regardless of border thickness / alignment.
|
|
84
|
+
*/
|
|
85
|
+
export declare const strokeOutsideExtent: (style: Style) => number;
|
|
86
|
+
export declare const getCornerRadius: (roundness: Roundness | undefined, width: number, height: number) => number;
|
|
87
|
+
//# sourceMappingURL=style.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"style.d.ts","sourceRoot":"","sources":["../src/style.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,wBAAwB,CAAC;AAMpD,MAAM,MAAM,OAAO,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;AAClD,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC;AAEnD;;;;;;;;GAQG;AACH,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE1D;;;;;;;;;GASG;AACH,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;IACjC;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;GAIG;AACH,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC;IACtB,QAAQ,CAAC,MAAM,CAAC,EAAE,KAAK,CAAC;IACxB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACvC,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC;IAC7B,mEAAmE;IACnE,QAAQ,CAAC,WAAW,CAAC,EAAE,WAAW,CAAC;IACnC,qDAAqD;IACrD,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC;CAChC;AAED,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAC;AACpD,MAAM,MAAM,YAAY,GAAG,KAAK,GAAG,QAAQ,GAAG,QAAQ,CAAC;AACvD,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,MAAM,CAAC;AAC3C,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAE5C;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;CAClC;AAED;;;GAGG;AACH,MAAM,WAAW,SAAU,SAAQ,KAAK;IACtC,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC;IAC/B,QAAQ,CAAC,YAAY,CAAC,EAAE,YAAY,CAAC;IACrC,uCAAuC;IACvC,QAAQ,CAAC,UAAU,CAAC,EAAE,UAAU,CAAC;IACjC,yCAAyC;IACzC,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC;IAC/B,oDAAoD;IACpD,QAAQ,CAAC,cAAc,CAAC,EAAE,cAAc,CAAC;CAC1C;AAED;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,GAAI,OAAO,KAAK,KAAG,MAalD,CAAC;AAEF,eAAO,MAAM,eAAe,GAC1B,WAAW,SAAS,GAAG,SAAS,EAChC,OAAO,MAAM,EACb,QAAQ,MAAM,KACb,MAcF,CAAC"}
|
package/dist/style.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ADAPTIVE_CORNER_RADIUS, PROPORTIONAL_CORNER_RADIUS, } from "./constants.js";
|
|
2
|
+
/**
|
|
3
|
+
* How far a shape's stroke extends OUTSIDE its geometric contour, in world
|
|
4
|
+
* units. Depends on stroke width and alignment: `outside` → the full width,
|
|
5
|
+
* `center` → half, `inside` → none. No stroke → 0. Used to place the
|
|
6
|
+
* selection halo a constant distance beyond the shape's VISIBLE outer edge
|
|
7
|
+
* (contour + this extent), regardless of border thickness / alignment.
|
|
8
|
+
*/
|
|
9
|
+
export const strokeOutsideExtent = (style) => {
|
|
10
|
+
const hasStroke = style.stroke !== undefined && style.stroke !== "transparent";
|
|
11
|
+
if (!hasStroke)
|
|
12
|
+
return 0;
|
|
13
|
+
const w = style.strokeWidth ?? 1;
|
|
14
|
+
if (w <= 0)
|
|
15
|
+
return 0;
|
|
16
|
+
switch (style.strokeAlign ?? "center") {
|
|
17
|
+
case "outside":
|
|
18
|
+
return w;
|
|
19
|
+
case "inside":
|
|
20
|
+
return 0;
|
|
21
|
+
default:
|
|
22
|
+
return w / 2;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
export const getCornerRadius = (roundness, width, height) => {
|
|
26
|
+
if (!roundness || roundness.type === "sharp")
|
|
27
|
+
return 0;
|
|
28
|
+
const smaller = Math.min(Math.abs(width), Math.abs(height));
|
|
29
|
+
if (smaller <= 0)
|
|
30
|
+
return 0;
|
|
31
|
+
if (roundness.value !== undefined) {
|
|
32
|
+
// Honour the override but clamp to half the smaller side so
|
|
33
|
+
// the corner radii can't overlap on narrow shapes (would
|
|
34
|
+
// produce a degenerate path).
|
|
35
|
+
return Math.max(0, Math.min(roundness.value, smaller / 2));
|
|
36
|
+
}
|
|
37
|
+
// Adaptive default: proportional below the cutoff, fixed above.
|
|
38
|
+
const cutoff = ADAPTIVE_CORNER_RADIUS / PROPORTIONAL_CORNER_RADIUS;
|
|
39
|
+
if (smaller <= cutoff)
|
|
40
|
+
return smaller * PROPORTIONAL_CORNER_RADIUS;
|
|
41
|
+
return ADAPTIVE_CORNER_RADIUS;
|
|
42
|
+
};
|
|
43
|
+
//# sourceMappingURL=style.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"style.js","sourceRoot":"","sources":["../src/style.ts"],"names":[],"mappings":"AACA,OAAO,EACL,sBAAsB,EACtB,0BAA0B,GAC3B,MAAM,gBAAgB,CAAC;AAqFxB;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,KAAY,EAAU,EAAE;IAC1D,MAAM,SAAS,GAAG,KAAK,CAAC,MAAM,KAAK,SAAS,IAAI,KAAK,CAAC,MAAM,KAAK,aAAa,CAAC;IAC/E,IAAI,CAAC,SAAS;QAAE,OAAO,CAAC,CAAC;IACzB,MAAM,CAAC,GAAG,KAAK,CAAC,WAAW,IAAI,CAAC,CAAC;IACjC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,CAAC,CAAC;IACrB,QAAQ,KAAK,CAAC,WAAW,IAAI,QAAQ,EAAE,CAAC;QACtC,KAAK,SAAS;YACZ,OAAO,CAAC,CAAC;QACX,KAAK,QAAQ;YACX,OAAO,CAAC,CAAC;QACX;YACE,OAAO,CAAC,GAAG,CAAC,CAAC;IACjB,CAAC;AACH,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,eAAe,GAAG,CAC7B,SAAgC,EAChC,KAAa,EACb,MAAc,EACN,EAAE;IACV,IAAI,CAAC,SAAS,IAAI,SAAS,CAAC,IAAI,KAAK,OAAO;QAAE,OAAO,CAAC,CAAC;IACvD,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;IAC5D,IAAI,OAAO,IAAI,CAAC;QAAE,OAAO,CAAC,CAAC;IAC3B,IAAI,SAAS,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;QAClC,4DAA4D;QAC5D,yDAAyD;QACzD,8BAA8B;QAC9B,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC;IAC7D,CAAC;IACD,gEAAgE;IAChE,MAAM,MAAM,GAAG,sBAAsB,GAAG,0BAA0B,CAAC;IACnE,IAAI,OAAO,IAAI,MAAM;QAAE,OAAO,OAAO,GAAG,0BAA0B,CAAC;IACnE,OAAO,sBAAsB,CAAC;AAChC,CAAC,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional host-provided text measurer. The text bounder
|
|
3
|
+
* (`getElementLocalBounds` for `TextElement`) is otherwise purely
|
|
4
|
+
* geometric (`chars × fontSize × factor`), which can diverge a lot
|
|
5
|
+
* from the actually-rendered width — the WebGL2 MSDF path draws with a
|
|
6
|
+
* WASM-baked font whose glyph advances differ from any geometric
|
|
7
|
+
* estimate, so the selection box would not hug the text.
|
|
8
|
+
*
|
|
9
|
+
* A host (the interaction layer) injects a measurer backed by the
|
|
10
|
+
* renderer's `measureText` (which itself matches the active text
|
|
11
|
+
* backend). When set, the bounder uses it for accurate width; when
|
|
12
|
+
* absent (headless / tests), it falls back to the geometric estimate.
|
|
13
|
+
*
|
|
14
|
+
* Returns the measured width in world px, or `null` to defer to the
|
|
15
|
+
* geometric estimate for that call. `opts` carries weight/style so the
|
|
16
|
+
* measurer matches the *rendered* width — bold/italic change glyph
|
|
17
|
+
* advances, so without it the bounds lag behind a bolded text and the
|
|
18
|
+
* glyphs overflow the element box.
|
|
19
|
+
*/
|
|
20
|
+
export interface TextMeasureOpts {
|
|
21
|
+
readonly bold?: boolean;
|
|
22
|
+
readonly italic?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export type TextMeasurer = (text: string, fontFamily: string, fontSize: number, opts?: TextMeasureOpts) => number | null;
|
|
25
|
+
/** Install (or clear, with `null`) the active text measurer. */
|
|
26
|
+
export declare const setTextMeasurer: (measurer: TextMeasurer | null) => void;
|
|
27
|
+
/** The active text measurer, or `null` when none is installed. */
|
|
28
|
+
export declare const getTextMeasurer: () => TextMeasurer | null;
|
|
29
|
+
//# sourceMappingURL=text-measure.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text-measure.d.ts","sourceRoot":"","sources":["../src/text-measure.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,MAAM,YAAY,GAAG,CACzB,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE,eAAe,KACnB,MAAM,GAAG,IAAI,CAAC;AAInB,gEAAgE;AAChE,eAAO,MAAM,eAAe,GAAI,UAAU,YAAY,GAAG,IAAI,KAAG,IAE/D,CAAC;AAEF,kEAAkE;AAClE,eAAO,MAAM,eAAe,QAAO,YAAY,GAAG,IAAsB,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
let activeMeasurer = null;
|
|
2
|
+
/** Install (or clear, with `null`) the active text measurer. */
|
|
3
|
+
export const setTextMeasurer = (measurer) => {
|
|
4
|
+
activeMeasurer = measurer;
|
|
5
|
+
};
|
|
6
|
+
/** The active text measurer, or `null` when none is installed. */
|
|
7
|
+
export const getTextMeasurer = () => activeMeasurer;
|
|
8
|
+
//# sourceMappingURL=text-measure.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text-measure.js","sourceRoot":"","sources":["../src/text-measure.ts"],"names":[],"mappings":"AA+BA,IAAI,cAAc,GAAwB,IAAI,CAAC;AAE/C,gEAAgE;AAChE,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,QAA6B,EAAQ,EAAE;IACrE,cAAc,GAAG,QAAQ,CAAC;AAC5B,CAAC,CAAC;AAEF,kEAAkE;AAClE,MAAM,CAAC,MAAM,eAAe,GAAG,GAAwB,EAAE,CAAC,cAAc,CAAC"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { ElementId } from "@oh-just-another/types";
|
|
2
|
+
import type { Scene } from "./scene.js";
|
|
3
|
+
import type { Element } from "./shape.js";
|
|
4
|
+
/**
|
|
5
|
+
* Pure three-way merge for scene shapes. Takes the common ancestor of two
|
|
6
|
+
* branches + the two heads, returns a merge report:
|
|
7
|
+
*
|
|
8
|
+
* - `autoMerged.elements` — every shape that could be auto-resolved.
|
|
9
|
+
* A change is auto-applied when at most one branch touched it
|
|
10
|
+
* vs the ancestor; both-touched-identically is also auto-applied
|
|
11
|
+
* (degenerate "no conflict").
|
|
12
|
+
* - `conflicts[]` — shapes where ancestor / source / target all
|
|
13
|
+
* diverge differently. Host UI (`<MergeDialog>`) presents these
|
|
14
|
+
* for user resolution; `applyConflictResolution` produces the
|
|
15
|
+
* final scene from a chosen `ours / theirs / both` per conflict.
|
|
16
|
+
*
|
|
17
|
+
* No history / patch generation here — caller composes the
|
|
18
|
+
* resolved Scene and pushes it through `Editor.loadScene` (or
|
|
19
|
+
* derives a patch via `diffSceneElements`).
|
|
20
|
+
*
|
|
21
|
+
* Equality semantics: scene mutations always produce fresh shape
|
|
22
|
+
* references via `apply(scene, patch)`. Reference equality is
|
|
23
|
+
* sufficient to detect "did this branch touch the shape?". For
|
|
24
|
+
* cross-process / cross-document scenarios where references differ
|
|
25
|
+
* naturally, callers can pass a custom `compareElements` predicate.
|
|
26
|
+
*/
|
|
27
|
+
export interface ThreeWayMergeReport {
|
|
28
|
+
/** Best-effort merged scene; conflicts retain the `target` version. */
|
|
29
|
+
readonly autoMerged: Scene;
|
|
30
|
+
readonly conflicts: readonly ThreeWayMergeConflict[];
|
|
31
|
+
}
|
|
32
|
+
export interface ThreeWayMergeConflict {
|
|
33
|
+
readonly elementId: ElementId;
|
|
34
|
+
/** Element as it stood in the common ancestor (or null when added in both branches). */
|
|
35
|
+
readonly base: Element | null;
|
|
36
|
+
readonly source: Element | null;
|
|
37
|
+
readonly target: Element | null;
|
|
38
|
+
}
|
|
39
|
+
export interface ThreeWayMergeOptions {
|
|
40
|
+
/**
|
|
41
|
+
* Custom equality predicate. Default = reference equality (the
|
|
42
|
+
* intended use; scene mutations always allocate new shape objects).
|
|
43
|
+
*/
|
|
44
|
+
readonly compareElements?: (a: Element, b: Element) => boolean;
|
|
45
|
+
}
|
|
46
|
+
export declare const mergeScenesThreeWay: (ancestor: Scene, source: Scene, target: Scene, options?: ThreeWayMergeOptions) => ThreeWayMergeReport;
|
|
47
|
+
/**
|
|
48
|
+
* Apply user-supplied resolutions to a merge report's conflicts and
|
|
49
|
+
* return the final scene. Each resolution picks `ours` (target),
|
|
50
|
+
* `theirs` (source), or `both` (target keeps original; source lands
|
|
51
|
+
* as a duplicate via the `cloneWithNewId` helper supplied by the
|
|
52
|
+
* caller — defaults to a "{id}-copy" suffix).
|
|
53
|
+
*/
|
|
54
|
+
export interface ConflictResolutionInput {
|
|
55
|
+
readonly elementId: ElementId;
|
|
56
|
+
readonly choice: "ours" | "theirs" | "both";
|
|
57
|
+
}
|
|
58
|
+
export declare const applyConflictResolutions: (report: ThreeWayMergeReport, resolutions: readonly ConflictResolutionInput[], cloneWithNewId?: (shape: Element) => Element) => Scene;
|
|
59
|
+
//# sourceMappingURL=three-way-merge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"three-way-merge.d.ts","sourceRoot":"","sources":["../src/three-way-merge.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAE1C;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,WAAW,mBAAmB;IAClC,uEAAuE;IACvE,QAAQ,CAAC,UAAU,EAAE,KAAK,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,SAAS,qBAAqB,EAAE,CAAC;CACtD;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAC9B,wFAAwF;IACxF,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC;IAChC,QAAQ,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC;CACjC;AAED,MAAM,WAAW,oBAAoB;IACnC;;;OAGG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC;CAChE;AAED,eAAO,MAAM,mBAAmB,GAC9B,UAAU,KAAK,EACf,QAAQ,KAAK,EACb,QAAQ,KAAK,EACb,UAAS,oBAAyB,KACjC,mBAgEF,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAC;CAC7C;AAED,eAAO,MAAM,wBAAwB,GACnC,QAAQ,mBAAmB,EAC3B,aAAa,SAAS,uBAAuB,EAAE,EAC/C,iBAAgB,CAAC,KAAK,EAAE,OAAO,KAAK,OAAsB,KACzD,KA4BF,CAAC"}
|