@weasel-js/guides 1.10.1 → 1.11.1
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/README.md +30 -22
- package/dist/index.d.ts +237 -152
- package/dist/index.js +464 -347
- package/dist/index.js.map +1 -1
- package/package.json +4 -16
- package/dist/chunk-B25KFI3I.js +0 -18
- package/dist/chunk-B25KFI3I.js.map +0 -1
- package/dist/chunk-LOFNN7EQ.js +0 -45
- package/dist/chunk-LOFNN7EQ.js.map +0 -1
- package/dist/chunk-TLFMAZ6M.js +0 -31
- package/dist/chunk-TLFMAZ6M.js.map +0 -1
- package/dist/insert.d.ts +0 -30
- package/dist/insert.js +0 -58
- package/dist/insert.js.map +0 -1
- package/dist/move.d.ts +0 -42
- package/dist/move.js +0 -18
- package/dist/move.js.map +0 -1
- package/dist/pointSnapToGrid-BaVEjV23.d.ts +0 -13
- package/dist/resize.d.ts +0 -38
- package/dist/resize.js +0 -89
- package/dist/resize.js.map +0 -1
- package/dist/types-DRSPiXfk.d.ts +0 -38
package/dist/index.js
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
|
-
import { pointToGridCell } from './chunk-TLFMAZ6M.js';
|
|
2
|
-
export { gridSnapStrategy, pointToGridCell } from './chunk-TLFMAZ6M.js';
|
|
3
|
-
export { pointSnapToGrid } from './chunk-B25KFI3I.js';
|
|
4
|
-
import { guideOffsetOn } from './chunk-LOFNN7EQ.js';
|
|
5
|
-
export { DEFAULT_GUIDE_TOLERANCE_PX, guideSnapStrategy } from './chunk-LOFNN7EQ.js';
|
|
6
1
|
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
7
|
-
import { useLatest,
|
|
8
|
-
import {
|
|
2
|
+
import { resolveAxes, useLatest, viewportWorldRect, pxExtent, tickedSegment, measuredSegmentCommands, AUTO_POSE_DESCRIPTOR, visualBoundsViaDescriptor, rectFromPlane, scenePoseFrame, unionAABB, createTransformOp, sceneSharedUnitScopes, asNodeId, sceneUnitScopes, screenToWorld, nodeGates, resolveStrokeWidth, resolveScoped } from '@weasel-js/core';
|
|
3
|
+
import { PATH_M, PATH_L } from '@weasel-js/geom';
|
|
9
4
|
|
|
10
5
|
// src/grid/roundToCell.ts
|
|
11
6
|
function roundToCell(value, cellSize) {
|
|
12
7
|
return Math.round(value / cellSize) * cellSize || 0;
|
|
13
8
|
}
|
|
9
|
+
function pointToGridCell(point, spacing, unitSystem, origin = { x: 0, y: 0 }) {
|
|
10
|
+
const s = resolveAxes(spacing, unitSystem);
|
|
11
|
+
return {
|
|
12
|
+
col: Math.floor((point.x - origin.x) / s.x),
|
|
13
|
+
row: Math.floor((point.y - origin.y) / s.y)
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// src/grid/useGridCellHover.ts
|
|
14
18
|
function cellsEqual(a, b) {
|
|
15
19
|
if (a === b) return true;
|
|
16
20
|
if (a === null || b === null) return false;
|
|
@@ -64,6 +68,7 @@ var DEFAULT_SUB = {
|
|
|
64
68
|
paint: { kind: "solid", color: "rgba(255, 255, 255, 0.04)" },
|
|
65
69
|
width: 1
|
|
66
70
|
};
|
|
71
|
+
var MIN_SCREEN_SPACING = 4;
|
|
67
72
|
function createGridLayer(opts) {
|
|
68
73
|
const line = opts.style?.line ?? DEFAULT_LINE;
|
|
69
74
|
const accent = opts.style?.accent ?? DEFAULT_ACCENT;
|
|
@@ -71,11 +76,11 @@ function createGridLayer(opts) {
|
|
|
71
76
|
return {
|
|
72
77
|
id: "grid",
|
|
73
78
|
label: "Grid",
|
|
74
|
-
draw: (_data, view) => {
|
|
75
|
-
const b = opts.bounds();
|
|
76
|
-
if (b.width <= 0 || b.height <= 0) return [];
|
|
79
|
+
draw: (_data, view, dims) => {
|
|
77
80
|
const { accentEvery, subdivisions } = opts;
|
|
78
81
|
const spacing = resolveAxes(opts.spacing, opts.unitSystem);
|
|
82
|
+
const b = opts.bounds ? opts.bounds(view, dims) : onLattice(viewportWorldRect(view, dims), spacing, accentEvery && accentEvery > 0 ? accentEvery : 1);
|
|
83
|
+
if (!(b.width > 0 && b.height > 0 && Number.isFinite(b.width) && Number.isFinite(b.height))) return [];
|
|
79
84
|
const x0 = b.x;
|
|
80
85
|
const y0 = b.y;
|
|
81
86
|
const x1 = b.x + b.width;
|
|
@@ -89,55 +94,47 @@ function createGridLayer(opts) {
|
|
|
89
94
|
coords: new Float32Array([sx, sy, ex, ey]),
|
|
90
95
|
fillRule: "nonzero"
|
|
91
96
|
};
|
|
92
|
-
const
|
|
97
|
+
const across = sx === ex ? px.x : px.y;
|
|
93
98
|
children.push({
|
|
94
99
|
kind: "path",
|
|
95
100
|
path,
|
|
96
|
-
stroke: { ...s, width: resolveStrokeWidth(s.width ?? 1, 1) *
|
|
101
|
+
stroke: { ...s, width: resolveStrokeWidth(s.width ?? 1, 1) * across }
|
|
97
102
|
});
|
|
98
103
|
};
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (k %
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
for (let y = y0; y <= y1 + 1e-9; y += stepY) {
|
|
108
|
-
const k = Math.round((y - y0) / stepY);
|
|
109
|
-
if (k % subdivisions === 0) continue;
|
|
110
|
-
pushSegment(x0, y, x1, y, sub);
|
|
104
|
+
const family = (axis, step, skipEvery, s) => {
|
|
105
|
+
if (!(step * Math.abs(view.scale[axis]) >= MIN_SCREEN_SPACING)) return;
|
|
106
|
+
const [from, to] = axis === "x" ? [x0, x1] : [y0, y1];
|
|
107
|
+
for (let k = 0; from + k * step <= to + 1e-9; k++) {
|
|
108
|
+
if (skipEvery > 0 && k % skipEvery === 0) continue;
|
|
109
|
+
const at = from + k * step;
|
|
110
|
+
if (axis === "x") pushSegment(at, y0, at, y1, s);
|
|
111
|
+
else pushSegment(x0, at, x1, at, s);
|
|
111
112
|
}
|
|
113
|
+
};
|
|
114
|
+
const accentOn = accentEvery !== void 0 && accentEvery > 0;
|
|
115
|
+
for (const axis of ["x", "y"]) {
|
|
116
|
+
if (subdivisions && subdivisions > 1) family(axis, spacing[axis] / subdivisions, subdivisions, sub);
|
|
112
117
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
if (!isAccent) pushSegment(x, y0, x, y1, line);
|
|
117
|
-
idx++;
|
|
118
|
-
}
|
|
119
|
-
idx = 0;
|
|
120
|
-
for (let y = y0; y <= y1 + 1e-9; y += spacing.y) {
|
|
121
|
-
const isAccent = !!accentEvery && accentEvery > 0 && idx % accentEvery === 0;
|
|
122
|
-
if (!isAccent) pushSegment(x0, y, x1, y, line);
|
|
123
|
-
idx++;
|
|
124
|
-
}
|
|
125
|
-
if (accentEvery && accentEvery > 0) {
|
|
126
|
-
idx = 0;
|
|
127
|
-
for (let x = x0; x <= x1 + 1e-9; x += spacing.x) {
|
|
128
|
-
if (idx % accentEvery === 0) pushSegment(x, y0, x, y1, accent);
|
|
129
|
-
idx++;
|
|
130
|
-
}
|
|
131
|
-
idx = 0;
|
|
132
|
-
for (let y = y0; y <= y1 + 1e-9; y += spacing.y) {
|
|
133
|
-
if (idx % accentEvery === 0) pushSegment(x0, y, x1, y, accent);
|
|
134
|
-
idx++;
|
|
135
|
-
}
|
|
118
|
+
for (const axis of ["x", "y"]) family(axis, spacing[axis], accentOn ? accentEvery : 0, line);
|
|
119
|
+
if (accentOn) {
|
|
120
|
+
for (const axis of ["x", "y"]) family(axis, spacing[axis] * accentEvery, 0, accent);
|
|
136
121
|
}
|
|
137
122
|
return children;
|
|
138
123
|
}
|
|
139
124
|
};
|
|
140
125
|
}
|
|
126
|
+
function onLattice(r, spacing, accentEvery) {
|
|
127
|
+
const px = spacing.x * accentEvery;
|
|
128
|
+
const py = spacing.y * accentEvery;
|
|
129
|
+
const x = Math.floor(r.x / px) * px;
|
|
130
|
+
const y = Math.floor(r.y / py) * py;
|
|
131
|
+
return {
|
|
132
|
+
x,
|
|
133
|
+
y,
|
|
134
|
+
width: Math.ceil((r.x + r.width) / px) * px - x,
|
|
135
|
+
height: Math.ceil((r.y + r.height) / py) * py - y
|
|
136
|
+
};
|
|
137
|
+
}
|
|
141
138
|
var DEFAULT_FILL = { paint: { kind: "solid", color: "rgba(127,176,105,0.15)" } };
|
|
142
139
|
function createCellHighlightLayer(opts) {
|
|
143
140
|
const fill = opts.fill ?? DEFAULT_FILL;
|
|
@@ -162,6 +159,23 @@ function createCellHighlightLayer(opts) {
|
|
|
162
159
|
}
|
|
163
160
|
};
|
|
164
161
|
}
|
|
162
|
+
function span(v, chain) {
|
|
163
|
+
const zero = typeof v === "number" ? 0 : { ...v, value: 0 };
|
|
164
|
+
return resolveScoped(v, chain) - resolveScoped(zero, chain);
|
|
165
|
+
}
|
|
166
|
+
function scopedGridPitch(spacing, scopes, chain) {
|
|
167
|
+
const inner = chain.length > 1 ? chain[chain.length - 1] : null;
|
|
168
|
+
const v = inner && scopes.spacing ? scopes.spacing(inner) : spacing;
|
|
169
|
+
const on = (axis) => {
|
|
170
|
+
const a = typeof v === "object" && "x" in v ? v[axis] : v;
|
|
171
|
+
try {
|
|
172
|
+
return span(a, chain);
|
|
173
|
+
} catch (e) {
|
|
174
|
+
throw new Error(`${axis} axis: ${e instanceof Error ? e.message : String(e)}`);
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
return { x: on("x"), y: on("y") };
|
|
178
|
+
}
|
|
165
179
|
function useGuides(initial = []) {
|
|
166
180
|
const [guides, setGuides] = useState(initial);
|
|
167
181
|
const ref = useLatest(guides);
|
|
@@ -190,24 +204,6 @@ function useGuides(initial = []) {
|
|
|
190
204
|
return { guides, getGuides, addGuide, removeGuide, clearGuides };
|
|
191
205
|
}
|
|
192
206
|
var DEFAULT_COLOR = "#3aa0ff";
|
|
193
|
-
var LABEL_FONT = { fontSize: 10 };
|
|
194
|
-
var LABEL_CHAR_W = 6;
|
|
195
|
-
var LABEL_H = 14;
|
|
196
|
-
var LABEL_PAD_X = 4;
|
|
197
|
-
function tickedSegment(x0, y0, x1, y1, t) {
|
|
198
|
-
const h = t / 2;
|
|
199
|
-
const vertical = x0 === x1;
|
|
200
|
-
const ends = t <= 0 ? [] : vertical ? [x0 - h, y0, x0 + h, y0, x1 - h, y1, x1 + h, y1] : [x0, y0 - h, x0, y0 + h, x1, y1 - h, x1, y1 + h];
|
|
201
|
-
const coords = [x0, y0, x1, y1, ...ends];
|
|
202
|
-
const commands = [];
|
|
203
|
-
for (let i = 0; i < coords.length; i += 4) commands.push(PATH_M, PATH_L);
|
|
204
|
-
return {
|
|
205
|
-
kind: "polygon",
|
|
206
|
-
commands: new Uint8Array(commands),
|
|
207
|
-
coords: new Float32Array(coords),
|
|
208
|
-
fillRule: "nonzero"
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
207
|
function createGuidesLayer(opts) {
|
|
212
208
|
const lineWidth = opts.lineWidth ?? 1;
|
|
213
209
|
const color = opts.color ?? DEFAULT_COLOR;
|
|
@@ -229,34 +225,22 @@ function createGuidesLayer(opts) {
|
|
|
229
225
|
const sy = (y) => (y - view.y) * view.scale.y;
|
|
230
226
|
const out = [];
|
|
231
227
|
for (const g of guides) {
|
|
232
|
-
const
|
|
228
|
+
const span2 = spanned ? g.span : void 0;
|
|
233
229
|
if (g.axis === "x") {
|
|
234
230
|
const x = sx(g.offset);
|
|
235
231
|
if (x < -lineWidth || x > dims.width + lineWidth) continue;
|
|
236
|
-
const path =
|
|
232
|
+
const path = span2 ? tickedSegment(x, sy(span2.min), x, sy(span2.max), ticks) : tickedSegment(x, 0, x, dims.height, 0);
|
|
237
233
|
out.push({ kind: "path", path, stroke });
|
|
238
234
|
} else {
|
|
239
235
|
const y = sy(g.offset);
|
|
240
236
|
if (y < -lineWidth || y > dims.height + lineWidth) continue;
|
|
241
|
-
const path =
|
|
237
|
+
const path = span2 ? tickedSegment(sx(span2.min), y, sx(span2.max), y, ticks) : tickedSegment(0, y, dims.width, y, 0);
|
|
242
238
|
out.push({ kind: "path", path, stroke });
|
|
243
239
|
}
|
|
244
240
|
}
|
|
245
241
|
for (const g of gaps) {
|
|
246
242
|
const [x0, y0, x1, y1] = g.axis === "x" ? [sx(g.min), sy(g.at), sx(g.max), sy(g.at)] : [sx(g.at), sy(g.min), sx(g.at), sy(g.max)];
|
|
247
|
-
out.push(
|
|
248
|
-
if (format === null) continue;
|
|
249
|
-
const text = format(g.max - g.min);
|
|
250
|
-
const w = text.length * LABEL_CHAR_W + LABEL_PAD_X * 2;
|
|
251
|
-
const left = (x0 + x1) / 2 - w / 2;
|
|
252
|
-
const top = (y0 + y1) / 2 - LABEL_H / 2;
|
|
253
|
-
out.push({ kind: "path", path: rectPath(left, top, w, LABEL_H), fill: { paint: { kind: "solid", color: gapColor } } });
|
|
254
|
-
out.push(textCommandFromRuns(
|
|
255
|
-
left + LABEL_PAD_X,
|
|
256
|
-
top + 2,
|
|
257
|
-
[{ text, fill: { paint: { kind: "solid", color: "#fff" } } }],
|
|
258
|
-
LABEL_FONT
|
|
259
|
-
));
|
|
243
|
+
out.push(...measuredSegmentCommands(x0, y0, x1, y1, ticks, gapStroke, format?.(g.max - g.min) ?? null, gapColor));
|
|
260
244
|
}
|
|
261
245
|
return out;
|
|
262
246
|
}
|
|
@@ -302,57 +286,6 @@ function deriveAlignmentGuides(targets, opts = {}) {
|
|
|
302
286
|
return [...seenX.values(), ...seenY.values()];
|
|
303
287
|
}
|
|
304
288
|
|
|
305
|
-
// src/guides/alignment/match.ts
|
|
306
|
-
var MOVE_ANCHORS = {
|
|
307
|
-
x: ["min", "center", "max"],
|
|
308
|
-
y: ["min", "center", "max"]
|
|
309
|
-
};
|
|
310
|
-
function featureOffset(b, axis, anchor) {
|
|
311
|
-
if (axis === "x") {
|
|
312
|
-
if (anchor === "min") return b.x;
|
|
313
|
-
if (anchor === "center") return b.x + b.width / 2;
|
|
314
|
-
return b.x + b.width;
|
|
315
|
-
}
|
|
316
|
-
if (anchor === "min") return b.y;
|
|
317
|
-
if (anchor === "center") return b.y + b.height / 2;
|
|
318
|
-
return b.y + b.height;
|
|
319
|
-
}
|
|
320
|
-
function bestAxis(b, axis, anchors, candidates, worldTolerance) {
|
|
321
|
-
let bestAbs = Infinity;
|
|
322
|
-
let bestDelta = 0;
|
|
323
|
-
let bestGuide = null;
|
|
324
|
-
for (const anchor of anchors) {
|
|
325
|
-
const o = featureOffset(b, axis, anchor);
|
|
326
|
-
for (const g of candidates) {
|
|
327
|
-
if (g.axis !== axis) continue;
|
|
328
|
-
const d = g.offset - o;
|
|
329
|
-
const ad = Math.abs(d);
|
|
330
|
-
if (ad <= worldTolerance && ad < bestAbs) {
|
|
331
|
-
bestAbs = ad;
|
|
332
|
-
bestDelta = d;
|
|
333
|
-
bestGuide = g;
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
return { delta: bestGuide ? bestDelta : 0, guide: bestGuide };
|
|
338
|
-
}
|
|
339
|
-
function matchAlignment(bounds, candidates, worldTolerance, anchors) {
|
|
340
|
-
const x = bestAxis(bounds, "x", anchors.x, candidates, worldTolerance.x);
|
|
341
|
-
const y = bestAxis(bounds, "y", anchors.y, candidates, worldTolerance.y);
|
|
342
|
-
const top = bounds.y + y.delta;
|
|
343
|
-
const left = bounds.x + x.delta;
|
|
344
|
-
return {
|
|
345
|
-
dx: x.delta,
|
|
346
|
-
dy: y.delta,
|
|
347
|
-
activeX: stretchSpan(x.guide, top, top + bounds.height),
|
|
348
|
-
activeY: stretchSpan(y.guide, left, left + bounds.width)
|
|
349
|
-
};
|
|
350
|
-
}
|
|
351
|
-
function stretchSpan(g, min, max) {
|
|
352
|
-
if (g === null || g.span === void 0) return g;
|
|
353
|
-
return { ...g, span: { min: Math.min(g.span.min, min), max: Math.max(g.span.max, max) } };
|
|
354
|
-
}
|
|
355
|
-
|
|
356
289
|
// src/guides/alignment/spacing.ts
|
|
357
290
|
var EPS2 = 1e-6;
|
|
358
291
|
var lo = (b, a) => a === "x" ? b.x : b.y;
|
|
@@ -368,14 +301,14 @@ function gapBetween(a, b, axis) {
|
|
|
368
301
|
if (shared === null) return null;
|
|
369
302
|
return { axis, min: hi(a, axis), max: lo(b, axis), at: (shared[0] + shared[1]) / 2 };
|
|
370
303
|
}
|
|
371
|
-
function hitsStrip(g, b,
|
|
304
|
+
function hitsStrip(g, b, span2) {
|
|
372
305
|
const c = cross(g.axis);
|
|
373
|
-
return lo(b, g.axis) < g.max - EPS2 && hi(b, g.axis) > g.min + EPS2 && lo(b, c) <
|
|
306
|
+
return lo(b, g.axis) < g.max - EPS2 && hi(b, g.axis) > g.min + EPS2 && lo(b, c) < span2[1] - EPS2 && hi(b, c) > span2[0] + EPS2;
|
|
374
307
|
}
|
|
375
308
|
function measureGaps(targets, axis) {
|
|
376
|
-
return
|
|
309
|
+
return measureGapSpans(targets, axis).map((m) => m.gap);
|
|
377
310
|
}
|
|
378
|
-
function
|
|
311
|
+
function measureGapSpans(targets, axis) {
|
|
379
312
|
const out = [];
|
|
380
313
|
for (const a of targets) {
|
|
381
314
|
let next = null;
|
|
@@ -386,13 +319,13 @@ function measure(targets, axis) {
|
|
|
386
319
|
}
|
|
387
320
|
if (next === null) continue;
|
|
388
321
|
const gap = gapBetween(a, next, axis);
|
|
389
|
-
const
|
|
390
|
-
if (targets.some((t) => t !== a && t !== next && hitsStrip(gap, t,
|
|
391
|
-
out.push({ gap, span });
|
|
322
|
+
const span2 = overlap(a, next, cross(axis));
|
|
323
|
+
if (targets.some((t) => t !== a && t !== next && hitsStrip(gap, t, span2))) continue;
|
|
324
|
+
out.push({ gap, span: span2 });
|
|
392
325
|
}
|
|
393
326
|
return out.sort((p, q) => p.gap.min - q.gap.min || p.gap.at - q.gap.at);
|
|
394
327
|
}
|
|
395
|
-
function
|
|
328
|
+
function spacingOffers(b, axis, edge, targets, refs) {
|
|
396
329
|
const bLo = lo(b, axis);
|
|
397
330
|
const bHi = hi(b, axis);
|
|
398
331
|
const size = bHi - bLo;
|
|
@@ -405,10 +338,9 @@ function bestAxis2(b, axis, edge, targets, refs, tol) {
|
|
|
405
338
|
if (lo(t, axis) >= center && (right === null || lo(t, axis) < lo(right, axis))) right = t;
|
|
406
339
|
}
|
|
407
340
|
const sizes = refs.map((g) => g.max - g.min);
|
|
408
|
-
|
|
341
|
+
const out = [];
|
|
409
342
|
const offer = (delta, gap) => {
|
|
410
|
-
if (gap
|
|
411
|
-
if (best === null || Math.abs(delta) < Math.abs(best.delta)) best = { delta, size: gap };
|
|
343
|
+
if (gap > EPS2) out.push({ delta, size: gap });
|
|
412
344
|
};
|
|
413
345
|
const L = left === null ? null : hi(left, axis);
|
|
414
346
|
const R = right === null ? null : lo(right, axis);
|
|
@@ -428,28 +360,39 @@ function bestAxis2(b, axis, edge, targets, refs, tol) {
|
|
|
428
360
|
const own = R !== null ? [R - bHi] : [];
|
|
429
361
|
for (const g of [...sizes, ...own]) if (L + g < bHi - EPS2) offer(L + g - bLo, g);
|
|
430
362
|
}
|
|
363
|
+
return out;
|
|
364
|
+
}
|
|
365
|
+
function bestAxis(b, axis, edge, targets, refs, tol) {
|
|
366
|
+
let best = null;
|
|
367
|
+
for (const o of spacingOffers(b, axis, edge, targets, refs)) {
|
|
368
|
+
if (Math.abs(o.delta) > tol) continue;
|
|
369
|
+
if (best === null || Math.abs(o.delta) < Math.abs(best.delta)) best = o;
|
|
370
|
+
}
|
|
431
371
|
return best;
|
|
432
372
|
}
|
|
433
|
-
function
|
|
373
|
+
function equalGaps(b, axis, size, targets, refs) {
|
|
434
374
|
const eq = (g) => Math.abs(g.max - g.min - size) < 1e-3;
|
|
435
375
|
const out = refs.filter(eq);
|
|
436
|
-
const
|
|
376
|
+
const near = targets.filter((t) => hi(t, axis) > lo(b, axis) - size - EPS2 && lo(t, axis) < hi(b, axis) + size + EPS2);
|
|
377
|
+
const own = measureGaps([...near, b], axis).filter((g) => (Math.abs(g.max - lo(b, axis)) < EPS2 || Math.abs(g.min - hi(b, axis)) < EPS2) && eq(g));
|
|
437
378
|
return [...out, ...own];
|
|
438
379
|
}
|
|
380
|
+
function referenceGaps(bounds, measured) {
|
|
381
|
+
return measured.filter((m) => !hitsStrip(m.gap, bounds, m.span)).map((m) => m.gap);
|
|
382
|
+
}
|
|
439
383
|
function matchSpacing(bounds, targets, worldTolerance, moving = { x: "both", y: "both" }) {
|
|
440
|
-
const
|
|
441
|
-
const
|
|
442
|
-
const
|
|
443
|
-
const
|
|
444
|
-
const y = moving.y === null ? null : bestAxis2(bounds, "y", moving.y, targets, refsY, worldTolerance.y);
|
|
384
|
+
const refsX = referenceGaps(bounds, measureGapSpans(targets, "x"));
|
|
385
|
+
const refsY = referenceGaps(bounds, measureGapSpans(targets, "y"));
|
|
386
|
+
const x = moving.x === null ? null : bestAxis(bounds, "x", moving.x, targets, refsX, worldTolerance.x);
|
|
387
|
+
const y = moving.y === null ? null : bestAxis(bounds, "y", moving.y, targets, refsY, worldTolerance.y);
|
|
445
388
|
const dx = x?.delta ?? 0;
|
|
446
389
|
const dy = y?.delta ?? 0;
|
|
447
390
|
const final = applyEdges(bounds, dx, dy, moving);
|
|
448
391
|
return {
|
|
449
392
|
dx,
|
|
450
393
|
dy,
|
|
451
|
-
gapsX: x === null ? [] :
|
|
452
|
-
gapsY: y === null ? [] :
|
|
394
|
+
gapsX: x === null ? [] : equalGaps(final, "x", x.size, targets, refsX),
|
|
395
|
+
gapsY: y === null ? [] : equalGaps(final, "y", y.size, targets, refsY)
|
|
453
396
|
};
|
|
454
397
|
}
|
|
455
398
|
function applyEdges(b, dx, dy, moving) {
|
|
@@ -466,223 +409,397 @@ function applyEdges(b, dx, dy, moving) {
|
|
|
466
409
|
} else if (moving.y === "max") height += dy;
|
|
467
410
|
return { x, y, width, height };
|
|
468
411
|
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
const
|
|
412
|
+
function worldBoundsOf(scene, ids, opts = {}) {
|
|
413
|
+
const d = opts.poseDescriptor ?? AUTO_POSE_DESCRIPTOR;
|
|
414
|
+
const frame = scenePoseFrame(scene);
|
|
415
|
+
const out = [];
|
|
416
|
+
for (const id of ids) {
|
|
417
|
+
if (scene.get(id) === void 0) continue;
|
|
418
|
+
out.push(visualBoundsViaDescriptor(frame.world(id), d));
|
|
419
|
+
}
|
|
420
|
+
return out;
|
|
421
|
+
}
|
|
422
|
+
function worldUnionOf(scene, ids, opts = {}) {
|
|
423
|
+
const boxes = worldBoundsOf(scene, ids, opts);
|
|
424
|
+
return boxes.length === 0 ? null : unionAABB(boxes);
|
|
425
|
+
}
|
|
426
|
+
function siblingIds(scene, ids) {
|
|
427
|
+
const moving = new Set(ids);
|
|
428
|
+
const gates = nodeGates(scene);
|
|
429
|
+
const parents = /* @__PURE__ */ new Set();
|
|
430
|
+
for (const id of ids) {
|
|
431
|
+
const node = scene.get(id);
|
|
432
|
+
if (node !== void 0) parents.add(node.parent);
|
|
433
|
+
}
|
|
434
|
+
const siblings = [];
|
|
435
|
+
for (const parent of parents) {
|
|
436
|
+
for (const id of parent === null ? scene.roots : scene.childrenOf(parent)) {
|
|
437
|
+
const node = scene.get(id);
|
|
438
|
+
if (moving.has(id) || node === void 0 || gates.hidden(id)) continue;
|
|
439
|
+
siblings.push(id);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return siblings;
|
|
443
|
+
}
|
|
444
|
+
function siblingBounds(scene, ids, opts = {}) {
|
|
445
|
+
return worldBoundsOf(scene, siblingIds(scene, ids), opts);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// src/strategies/onPlane.ts
|
|
449
|
+
function guideOffsetOn(g, plane) {
|
|
450
|
+
return plane ? g.offset * plane.scale[g.axis] + plane.offset[g.axis] : g.offset;
|
|
451
|
+
}
|
|
452
|
+
function roundToGridOn(value, axis, spacing, plane) {
|
|
453
|
+
if (!plane) return Math.round(value / spacing) * spacing;
|
|
454
|
+
const camera = (value - plane.offset[axis]) / plane.scale[axis];
|
|
455
|
+
return Math.round(camera / spacing) * spacing * plane.scale[axis] + plane.offset[axis];
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// src/sources/lines.ts
|
|
459
|
+
function axisLine(axis, offset, near) {
|
|
460
|
+
return axis === "x" ? { at: { x: offset, y: near.y }, dirs: [{ x: 0, y: 1 }] } : { at: { x: near.x, y: offset }, dirs: [{ x: 1, y: 0 }] };
|
|
461
|
+
}
|
|
462
|
+
function landedShift(q, delta) {
|
|
463
|
+
return { x: delta.x - q.delta0.x, y: delta.y - q.delta0.y };
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// src/sources/grid.ts
|
|
467
|
+
function gridSnapSource(opts) {
|
|
468
|
+
const { scopes } = opts;
|
|
469
|
+
const rootPitch = scopes ? scopedGridPitch(opts.spacing, scopes, [scopes.root]) : resolveAxes(opts.spacing, opts.unitSystem);
|
|
470
|
+
const pitchFor = (q) => {
|
|
471
|
+
if (!scopes || !q.scene) return rootPitch;
|
|
472
|
+
if (q.ids?.length) return scopedGridPitch(opts.spacing, scopes, sceneSharedUnitScopes(q.scene, q.ids.map(asNodeId), scopes.root));
|
|
473
|
+
if (q.parent === void 0) return rootPitch;
|
|
474
|
+
return scopedGridPitch(opts.spacing, scopes, sceneUnitScopes(q.scene, asNodeId(q.parent), scopes.root, { inclusive: true }));
|
|
475
|
+
};
|
|
476
|
+
const o = opts.origin ?? { x: 0, y: 0 };
|
|
477
|
+
const lines = (v, axis, pitch, plane) => {
|
|
478
|
+
const s = plane ? plane.scale[axis] : 1;
|
|
479
|
+
const near = roundToGridOn(v - o[axis] * s, axis, pitch[axis], plane) + o[axis] * s;
|
|
480
|
+
const step = pitch[axis] * s;
|
|
481
|
+
return [near, near + (v >= near ? step : -step)];
|
|
482
|
+
};
|
|
492
483
|
return {
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
484
|
+
id: "grid",
|
|
485
|
+
tier: "fallback",
|
|
486
|
+
reachPx: Infinity,
|
|
487
|
+
roles: ["origin", "pointer"],
|
|
488
|
+
*collect(q) {
|
|
489
|
+
const pitch = pitchFor(q);
|
|
490
|
+
for (const p of q.probes) {
|
|
491
|
+
for (const axis of ["x", "y"]) {
|
|
492
|
+
for (const at of lines(p.at[axis], axis, pitch, q.plane ?? null)) yield { probe: p.id, locus: axisLine(axis, at, p.at) };
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
498
496
|
};
|
|
499
497
|
}
|
|
500
498
|
|
|
501
|
-
// src/guides
|
|
502
|
-
|
|
503
|
-
var EXACT = { x: 1e-6, y: 1e-6 };
|
|
504
|
-
function resolve(b, args, tol, anchors, edges) {
|
|
505
|
-
const candidates = args.getCandidates();
|
|
506
|
-
const m = matchAlignment(b, candidates, tol, anchors);
|
|
507
|
-
const targets = args.getSpacingTargets?.();
|
|
508
|
-
if (targets === void 0) {
|
|
509
|
-
if (m.activeX === null && m.activeY === null) return null;
|
|
510
|
-
return { dx: m.dx, dy: m.dy, guides: activeList(m), gaps: [] };
|
|
511
|
-
}
|
|
512
|
-
const s = matchSpacing(b, targets, tol, edges);
|
|
513
|
-
const pick = (hit, d, spaced, sd) => hit && (!spaced || Math.abs(d) <= Math.abs(sd)) ? d : spaced ? sd : null;
|
|
514
|
-
const dx = pick(m.activeX !== null, m.dx, s.gapsX.length > 0, s.dx);
|
|
515
|
-
const dy = pick(m.activeY !== null, m.dy, s.gapsY.length > 0, s.dy);
|
|
516
|
-
if (dx === null && dy === null) return null;
|
|
517
|
-
const settled = applyEdges(b, dx ?? 0, dy ?? 0, edges);
|
|
518
|
-
const shown = matchSpacing(settled, targets, EXACT, edges);
|
|
499
|
+
// src/sources/guides.ts
|
|
500
|
+
function guideSnapSource(getGuides) {
|
|
519
501
|
return {
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
502
|
+
id: "guides",
|
|
503
|
+
roles: ["origin", "pointer"],
|
|
504
|
+
*collect(q) {
|
|
505
|
+
const guides = getGuides();
|
|
506
|
+
if (guides.length === 0) return;
|
|
507
|
+
const reach = q.reachWorld(q.reachPx);
|
|
508
|
+
for (const p of q.probes) {
|
|
509
|
+
for (const g of guides) {
|
|
510
|
+
const offset = guideOffsetOn(g, q.plane ?? null);
|
|
511
|
+
if (Math.abs(p.at[g.axis] - offset) > reach) continue;
|
|
512
|
+
yield { probe: p.id, locus: axisLine(g.axis, offset, p.at) };
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
524
516
|
};
|
|
525
517
|
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
518
|
+
|
|
519
|
+
// src/guides/alignment/match.ts
|
|
520
|
+
function stretchSpan(g, min, max) {
|
|
521
|
+
if (g === null || g.span === void 0) return g;
|
|
522
|
+
return { ...g, span: { min: Math.min(g.span.min, min), max: Math.max(g.span.max, max) } };
|
|
529
523
|
}
|
|
530
|
-
function
|
|
531
|
-
|
|
524
|
+
function visibleRoots(scene) {
|
|
525
|
+
const gates = nodeGates(scene);
|
|
526
|
+
return scene.roots.filter((id) => !gates.hidden(id));
|
|
532
527
|
}
|
|
533
|
-
function
|
|
534
|
-
const
|
|
535
|
-
return
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
if (r === null) return;
|
|
557
|
-
return { transform: { kind: "translate", dx: transform.dx + r.dx, dy: transform.dy + r.dy } };
|
|
558
|
-
},
|
|
559
|
-
onEnd() {
|
|
560
|
-
publish(given, null);
|
|
561
|
-
},
|
|
562
|
-
onCancel() {
|
|
563
|
-
publish(given, null);
|
|
528
|
+
function targetIds(scene, exclude, filter) {
|
|
529
|
+
const ids = exclude.length > 0 ? siblingIds(scene, exclude) : visibleRoots(scene);
|
|
530
|
+
if (!filter) return ids;
|
|
531
|
+
return ids.filter((id) => {
|
|
532
|
+
const node = scene.get(id);
|
|
533
|
+
return node !== void 0 && filter(node);
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
function targetBoxes(q, scene, filter) {
|
|
537
|
+
const drawn = q.drawnBox;
|
|
538
|
+
return drawn ? targetIds(scene, q.exclude, filter).flatMap((id) => drawn(id) ?? []) : [];
|
|
539
|
+
}
|
|
540
|
+
function cachedPerScene(derive, filter) {
|
|
541
|
+
let key = null;
|
|
542
|
+
let value = null;
|
|
543
|
+
return (q) => {
|
|
544
|
+
const scene = q.scene;
|
|
545
|
+
if (!scene) return null;
|
|
546
|
+
const version = scene.getVersion();
|
|
547
|
+
const exclude = q.exclude.join("\n");
|
|
548
|
+
if (key === null || key.scene !== scene || key.version !== version || key.exclude !== exclude) {
|
|
549
|
+
value = derive(targetBoxes(q, scene, filter));
|
|
550
|
+
key = { scene, version, exclude };
|
|
564
551
|
}
|
|
552
|
+
return value;
|
|
565
553
|
};
|
|
566
554
|
}
|
|
567
|
-
|
|
568
|
-
|
|
555
|
+
|
|
556
|
+
// src/sources/alignment.ts
|
|
557
|
+
function indexLines(lines) {
|
|
558
|
+
const sorted = [...lines].sort((a, b) => a.offset - b.offset);
|
|
559
|
+
return { x: sorted.filter((g) => g.axis === "x"), y: sorted.filter((g) => g.axis === "y") };
|
|
560
|
+
}
|
|
561
|
+
function* inReach(lines, v, reach) {
|
|
562
|
+
let lo3 = 0;
|
|
563
|
+
let hi3 = lines.length;
|
|
564
|
+
while (lo3 < hi3) {
|
|
565
|
+
const mid2 = lo3 + hi3 >> 1;
|
|
566
|
+
if (lines[mid2].offset < v - reach) lo3 = mid2 + 1;
|
|
567
|
+
else hi3 = mid2;
|
|
568
|
+
}
|
|
569
|
+
for (let i = lo3; i < lines.length && lines[i].offset <= v + reach; i++) yield lines[i];
|
|
570
|
+
}
|
|
571
|
+
var cross2 = (a) => a === "x" ? "y" : "x";
|
|
572
|
+
function lineMark(g, extent, color) {
|
|
573
|
+
const span2 = stretchSpan(g, extent[0], extent[1])?.span ?? { min: extent[0], max: extent[1] };
|
|
574
|
+
const ends = g.axis === "x" ? { from: { x: g.offset, y: span2.min }, to: { x: g.offset, y: span2.max } } : { from: { x: span2.min, y: g.offset }, to: { x: span2.max, y: g.offset } };
|
|
575
|
+
return { kind: "line", ...ends, ...color === void 0 ? {} : { color } };
|
|
576
|
+
}
|
|
577
|
+
function alignmentSnapSource(opts = {}) {
|
|
578
|
+
const page = () => (typeof opts.page === "function" ? opts.page() : opts.page) ?? void 0;
|
|
579
|
+
const fromScene = cachedPerScene((boxes) => indexLines(deriveAlignmentGuides(boxes)), opts.filter);
|
|
580
|
+
const linesFor = (q) => {
|
|
581
|
+
const pageBox = page();
|
|
582
|
+
const own = opts.getCandidates ? indexLines(opts.getCandidates()) : fromScene(q);
|
|
583
|
+
return [...own ? [own] : [], ...pageBox ? [indexLines(deriveAlignmentGuides([], { page: pageBox }))] : []];
|
|
584
|
+
};
|
|
569
585
|
return {
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
const
|
|
577
|
-
const
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
586
|
+
id: "alignment",
|
|
587
|
+
...opts.reachPx === void 0 ? {} : { reachPx: opts.reachPx },
|
|
588
|
+
roles: ["box", "pointer"],
|
|
589
|
+
*collect(q) {
|
|
590
|
+
const indexes = linesFor(q);
|
|
591
|
+
if (indexes.length === 0) return;
|
|
592
|
+
const reach = q.reachWorld(q.reachPx);
|
|
593
|
+
const box = q.box ?? null;
|
|
594
|
+
const probes = q.probes.some((p) => p.role === "box") ? q.probes.filter((p) => p.role === "box") : q.probes;
|
|
595
|
+
for (const p of probes) {
|
|
596
|
+
for (const axis of ["x", "y"]) {
|
|
597
|
+
for (const index of indexes) {
|
|
598
|
+
for (const g of inReach(index[axis], p.at[axis], reach)) {
|
|
599
|
+
yield {
|
|
600
|
+
probe: p.id,
|
|
601
|
+
locus: axisLine(axis, g.offset, p.at),
|
|
602
|
+
mark: (delta) => lineMark(g, extentAcross(p, box, axis, landedShift(q, delta), q.edges), opts.color)
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
581
607
|
}
|
|
582
|
-
args.setActiveGuides(activeList(m));
|
|
583
|
-
return { current: { x: current.x + m.dx, y: current.y + m.dy } };
|
|
584
|
-
},
|
|
585
|
-
onEnd() {
|
|
586
|
-
given.setActiveGuides([]);
|
|
587
|
-
},
|
|
588
|
-
onCancel() {
|
|
589
|
-
given.setActiveGuides([]);
|
|
590
608
|
}
|
|
591
609
|
};
|
|
592
610
|
}
|
|
593
|
-
function
|
|
594
|
-
const
|
|
611
|
+
function extentAcross(p, box, axis, shift, edges = { x: "both", y: "both" }) {
|
|
612
|
+
const c = cross2(axis);
|
|
613
|
+
if (p.role !== "box" || box === null) return [p.at[c] + shift[c], p.at[c] + shift[c]];
|
|
614
|
+
const landed = applyEdges(box, shift.x, shift.y, edges);
|
|
615
|
+
const lo3 = c === "x" ? landed.x : landed.y;
|
|
616
|
+
return [lo3, lo3 + (c === "x" ? landed.width : landed.height)];
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// src/sources/spacing.ts
|
|
620
|
+
var measureAll = (boxes) => ({ boxes, x: measureGapSpans(boxes, "x"), y: measureGapSpans(boxes, "y") });
|
|
621
|
+
function gapMark(g, color) {
|
|
622
|
+
const ends = g.axis === "x" ? { from: { x: g.min, y: g.at }, to: { x: g.max, y: g.at } } : { from: { x: g.at, y: g.min }, to: { x: g.at, y: g.max } };
|
|
623
|
+
return { kind: "gap", ...ends, value: g.max - g.min, ...color === void 0 ? {} : { color } };
|
|
624
|
+
}
|
|
625
|
+
function spacingSnapSource(opts = {}) {
|
|
626
|
+
const fromScene = cachedPerScene(measureAll, opts.filter);
|
|
595
627
|
return {
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
const
|
|
603
|
-
const
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
628
|
+
id: "spacing",
|
|
629
|
+
...opts.reachPx === void 0 ? {} : { reachPx: opts.reachPx },
|
|
630
|
+
roles: ["box"],
|
|
631
|
+
gestures: ["move", "resize"],
|
|
632
|
+
*collect(q) {
|
|
633
|
+
const box = q.box;
|
|
634
|
+
const probe = q.probes.find((p) => p.role === "box");
|
|
635
|
+
const measured = opts.getTargets ? measureAll(opts.getTargets()) : fromScene(q);
|
|
636
|
+
if (!box || !probe || !measured) return;
|
|
637
|
+
const reach = q.reachWorld(q.reachPx);
|
|
638
|
+
const edges = q.edges ?? { x: "both", y: "both" };
|
|
639
|
+
for (const axis of ["x", "y"]) {
|
|
640
|
+
const edge = edges[axis];
|
|
641
|
+
if (edge === null) continue;
|
|
642
|
+
const refs = referenceGaps(box, measured[axis]);
|
|
643
|
+
const seen = /* @__PURE__ */ new Set();
|
|
644
|
+
for (const o of spacingOffers(box, axis, edge, measured.boxes, refs)) {
|
|
645
|
+
const key = `${o.delta.toFixed(6)}:${o.size.toFixed(6)}`;
|
|
646
|
+
if (Math.abs(o.delta) > reach || seen.has(key)) continue;
|
|
647
|
+
seen.add(key);
|
|
648
|
+
yield {
|
|
649
|
+
probe: probe.id,
|
|
650
|
+
locus: axisLine(axis, probe.at[axis] + o.delta, probe.at),
|
|
651
|
+
mark: (delta) => {
|
|
652
|
+
const shift = landedShift(q, delta);
|
|
653
|
+
const landed = applyEdges(box, shift.x, shift.y, edges);
|
|
654
|
+
return equalGaps(landed, axis, o.size, measured.boxes, refs).map((g) => gapMark(g, opts.color));
|
|
655
|
+
}
|
|
656
|
+
};
|
|
657
|
+
}
|
|
619
658
|
}
|
|
620
|
-
publish(args, { guides: m.guides, gaps: m.gaps });
|
|
621
|
-
return { pose: { ...pose, x, y, width, height } };
|
|
622
|
-
},
|
|
623
|
-
onEnd() {
|
|
624
|
-
publish(given, null);
|
|
625
|
-
},
|
|
626
|
-
onCancel() {
|
|
627
|
-
publish(given, null);
|
|
628
659
|
}
|
|
629
660
|
};
|
|
630
661
|
}
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
const
|
|
645
|
-
const
|
|
646
|
-
const
|
|
647
|
-
const
|
|
648
|
-
const
|
|
649
|
-
|
|
650
|
-
const o = toWorld(lx - fixedLocal.x, ly - fixedLocal.y);
|
|
651
|
-
return { x: fixedWorld.x + o.x, y: fixedWorld.y + o.y };
|
|
652
|
-
};
|
|
653
|
-
const movingEdge = (axis) => {
|
|
654
|
-
const lo2 = axis === "x" ? pose.x : pose.y;
|
|
655
|
-
const hi2 = lo2 + (axis === "x" ? pose.width : pose.height);
|
|
656
|
-
const a = anchor[axis];
|
|
657
|
-
return a === "min" ? [hi2] : a === "max" ? [lo2] : [lo2, hi2];
|
|
662
|
+
|
|
663
|
+
// src/spacingHints/distance.ts
|
|
664
|
+
var EPS3 = 1e-6;
|
|
665
|
+
var lo2 = (b, a) => a === "x" ? b.x : b.y;
|
|
666
|
+
var hi2 = (b, a) => a === "x" ? b.x + b.width : b.y + b.height;
|
|
667
|
+
var mid = (b, a) => (lo2(b, a) + hi2(b, a)) / 2;
|
|
668
|
+
var cross3 = (a) => a === "x" ? "y" : "x";
|
|
669
|
+
function measureDistances(from, to) {
|
|
670
|
+
const out = { gaps: [], leaders: [] };
|
|
671
|
+
for (const axis of ["x", "y"]) measureAxis(from, to, axis, out);
|
|
672
|
+
return out;
|
|
673
|
+
}
|
|
674
|
+
function measureAxis(from, to, axis, out) {
|
|
675
|
+
const c = cross3(axis);
|
|
676
|
+
const sharedMin = Math.max(lo2(from, c), lo2(to, c));
|
|
677
|
+
const sharedMax = Math.min(hi2(from, c), hi2(to, c));
|
|
678
|
+
const shares = sharedMax - sharedMin > EPS3;
|
|
679
|
+
const push = (min, max, at2) => {
|
|
680
|
+
if (max - min > EPS3) out.gaps.push({ axis, min, max, at: at2 });
|
|
658
681
|
};
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
const
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
const edge = movingEdge(along)[0];
|
|
671
|
-
let best = null;
|
|
672
|
-
for (const across3 of movingEdge(along === "x" ? "y" : "x")) {
|
|
673
|
-
const c = along === "x" ? drawn(edge, across3) : drawn(across3, edge);
|
|
674
|
-
for (const g of candidates) {
|
|
675
|
-
const reach = g.axis === "x" ? dir.x : dir.y;
|
|
676
|
-
if (Math.abs(reach) < 1e-9) continue;
|
|
677
|
-
const t = (g.offset - (g.axis === "x" ? c.x : c.y)) / reach;
|
|
678
|
-
if (Math.abs(t * dir.x) > tol.x || Math.abs(t * dir.y) > tol.y) continue;
|
|
679
|
-
if (best === null || Math.abs(t) < Math.abs(best.t)) best = { t, guide: g, at: c };
|
|
682
|
+
const before = hi2(from, axis) <= lo2(to, axis) + EPS3;
|
|
683
|
+
const after = hi2(to, axis) <= lo2(from, axis) + EPS3;
|
|
684
|
+
if (before || after) {
|
|
685
|
+
const [min, max] = before ? [hi2(from, axis), lo2(to, axis)] : [hi2(to, axis), lo2(from, axis)];
|
|
686
|
+
if (max - min <= EPS3) return;
|
|
687
|
+
const at2 = shares ? (sharedMin + sharedMax) / 2 : mid(from, c);
|
|
688
|
+
push(min, max, at2);
|
|
689
|
+
if (!shares) {
|
|
690
|
+
const edge = before ? lo2(to, axis) : hi2(to, axis);
|
|
691
|
+
const span2 = at2 < lo2(to, c) ? { min: at2, max: lo2(to, c) } : { min: hi2(to, c), max: at2 };
|
|
692
|
+
out.leaders.push({ id: `distance:${axis}`, axis, offset: edge, span: span2 });
|
|
680
693
|
}
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
if (!shares) return;
|
|
697
|
+
const [inner, outer] = lo2(from, axis) >= lo2(to, axis) - EPS3 && hi2(from, axis) <= hi2(to, axis) + EPS3 ? [from, to] : lo2(to, axis) >= lo2(from, axis) - EPS3 && hi2(to, axis) <= hi2(from, axis) + EPS3 ? [to, from] : [null, null];
|
|
698
|
+
if (inner === null || outer === null) return;
|
|
699
|
+
const at = mid(inner, c);
|
|
700
|
+
push(lo2(outer, axis), lo2(inner, axis), at);
|
|
701
|
+
push(hi2(inner, axis), hi2(outer, axis), at);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// src/spacingHints/distanceReadout.ts
|
|
705
|
+
function marksOf(r, color) {
|
|
706
|
+
const c = color === void 0 ? {} : { color };
|
|
707
|
+
return [
|
|
708
|
+
...r.gaps.map((g) => g.axis === "x" ? { kind: "distance", from: { x: g.min, y: g.at }, to: { x: g.max, y: g.at }, value: g.max - g.min, ...c } : { kind: "distance", from: { x: g.at, y: g.min }, to: { x: g.at, y: g.max }, value: g.max - g.min, ...c }),
|
|
709
|
+
...r.leaders.flatMap((l) => {
|
|
710
|
+
if (l.span === void 0) return [];
|
|
711
|
+
return [l.axis === "x" ? { kind: "line", from: { x: l.offset, y: l.span.min }, to: { x: l.offset, y: l.span.max }, ...c } : { kind: "line", from: { x: l.span.min, y: l.offset }, to: { x: l.span.max, y: l.offset }, ...c }];
|
|
712
|
+
})
|
|
713
|
+
];
|
|
714
|
+
}
|
|
715
|
+
var EMPTY = { gaps: [], leaders: [] };
|
|
716
|
+
var MODIFIERS = ["alt", "ctrl", "meta", "shift"];
|
|
717
|
+
function subtreeOf(scene, ids) {
|
|
718
|
+
const out = /* @__PURE__ */ new Set();
|
|
719
|
+
const stack = [...ids];
|
|
720
|
+
while (stack.length > 0) {
|
|
721
|
+
const id = stack.pop();
|
|
722
|
+
if (out.has(id)) continue;
|
|
723
|
+
out.add(id);
|
|
724
|
+
stack.push(...scene.childrenOf(id));
|
|
681
725
|
}
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
726
|
+
return out;
|
|
727
|
+
}
|
|
728
|
+
function distanceReadout(opts = {}) {
|
|
729
|
+
const id = opts.id ?? "distanceReadout";
|
|
730
|
+
const modifier = opts.modifier ?? "alt";
|
|
731
|
+
let shown = EMPTY;
|
|
732
|
+
let store = null;
|
|
733
|
+
const show = (next) => {
|
|
734
|
+
if (next === shown || next.gaps.length === 0 && shown.gaps.length === 0) return;
|
|
735
|
+
shown = next;
|
|
736
|
+
store?.set(marksOf(next, opts.color));
|
|
737
|
+
};
|
|
738
|
+
const action = {
|
|
739
|
+
id: `${id}.measure`,
|
|
740
|
+
label: "Measure distance to the shape under the pointer",
|
|
741
|
+
requires: ["selection", "scene", "nodeAtPoint", "poseDescriptor", "snapReadout"],
|
|
742
|
+
invoker: {
|
|
743
|
+
timing: "immediate",
|
|
744
|
+
run: (deps, params) => {
|
|
745
|
+
const p = params;
|
|
746
|
+
const { selection, scene, nodeAtPoint, poseDescriptor, snapReadout } = deps;
|
|
747
|
+
if (snapReadout !== void 0) store = snapReadout.store;
|
|
748
|
+
const ids = selection?.get() ?? [];
|
|
749
|
+
if (p?.show !== true || p.worldX === void 0 || p.worldY === void 0 || scene === void 0 || nodeAtPoint === void 0 || ids.length === 0) {
|
|
750
|
+
show(EMPTY);
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
const hit = nodeAtPoint({ x: p.worldX, y: p.worldY }, subtreeOf(scene, ids));
|
|
754
|
+
const from = worldUnionOf(scene, ids, { poseDescriptor });
|
|
755
|
+
const [to] = hit === null ? [] : worldBoundsOf(scene, [hit], { poseDescriptor });
|
|
756
|
+
show(from === null || to === void 0 ? EMPTY : measureDistances(from, to));
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
};
|
|
760
|
+
const mods = (held) => Object.fromEntries(
|
|
761
|
+
MODIFIERS.map((m) => [m, m === modifier ? held : "optional"])
|
|
762
|
+
);
|
|
763
|
+
const bindings = [
|
|
764
|
+
{ spec: { kind: "hover", mods: mods(true) }, actionId: action.id, opts: { params: { show: true } } },
|
|
765
|
+
{ spec: { kind: "hover", mods: mods(false) }, actionId: action.id, opts: { params: { show: false } } },
|
|
766
|
+
{ spec: { kind: "hover", motion: "leave" }, actionId: action.id, opts: { params: { show: false } } }
|
|
767
|
+
];
|
|
768
|
+
return {
|
|
769
|
+
id,
|
|
770
|
+
eligibility: { always: true },
|
|
771
|
+
actions: [action],
|
|
772
|
+
bindings,
|
|
773
|
+
attach: (api) => {
|
|
774
|
+
const el = api.element;
|
|
775
|
+
const hide = () => show(EMPTY);
|
|
776
|
+
el?.addEventListener("pointerdown", hide, true);
|
|
777
|
+
return () => {
|
|
778
|
+
el?.removeEventListener("pointerdown", hide, true);
|
|
779
|
+
show(EMPTY);
|
|
780
|
+
store = null;
|
|
781
|
+
};
|
|
782
|
+
},
|
|
783
|
+
read: () => shown
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// src/spacingHints/preset.ts
|
|
788
|
+
function createSpacingHintsPreset(opts = {}) {
|
|
789
|
+
const id = opts.id ?? "spacingHints";
|
|
790
|
+
const color = opts.color === void 0 ? {} : { color: opts.color };
|
|
791
|
+
const own = {
|
|
792
|
+
...color,
|
|
793
|
+
...opts.filter === void 0 ? {} : { filter: opts.filter },
|
|
794
|
+
...opts.tolerance === void 0 ? {} : { reachPx: opts.tolerance }
|
|
795
|
+
};
|
|
796
|
+
const snapSources = [
|
|
797
|
+
...opts.spacing ?? true ? [spacingSnapSource(own)] : [],
|
|
798
|
+
...opts.align ?? false ? [alignmentSnapSource(own)] : []
|
|
799
|
+
];
|
|
800
|
+
const contributions = [];
|
|
801
|
+
if (opts.readout !== false) contributions.push(distanceReadout({ ...opts.readout, ...color }));
|
|
802
|
+
return { id, snapSources, contributions };
|
|
686
803
|
}
|
|
687
804
|
|
|
688
805
|
// src/layout/snaps.ts
|
|
@@ -875,7 +992,7 @@ function tileGrid(opts) {
|
|
|
875
992
|
if (fromIdx === null || fromIdx === idx) return null;
|
|
876
993
|
return { occupant: occupant.id, from: occupant.pose, newPose: cellToPose(grid.rectAt(fromIdx), occupant.pose) };
|
|
877
994
|
}
|
|
878
|
-
function pack2(from,
|
|
995
|
+
function pack2(from, into, children) {
|
|
879
996
|
const ordered = children.map((c) => ({ c, cell: from.indexOf(c.pose) })).sort((a, b) => {
|
|
880
997
|
if (a.cell !== b.cell) {
|
|
881
998
|
if (a.cell === null) return 1;
|
|
@@ -886,7 +1003,7 @@ function tileGrid(opts) {
|
|
|
886
1003
|
});
|
|
887
1004
|
const out = /* @__PURE__ */ new Map();
|
|
888
1005
|
for (let i = 0; i < ordered.length; i++) {
|
|
889
|
-
out.set(ordered[i].c.id, cellToPose(
|
|
1006
|
+
out.set(ordered[i].c.id, cellToPose(into.rectAt(i), ordered[i].c.pose));
|
|
890
1007
|
}
|
|
891
1008
|
return out;
|
|
892
1009
|
}
|
|
@@ -1457,6 +1574,6 @@ function ringSlots(opts = {}) {
|
|
|
1457
1574
|
};
|
|
1458
1575
|
}
|
|
1459
1576
|
|
|
1460
|
-
export {
|
|
1577
|
+
export { alignmentSnapSource, cellAt, containedThenNearest, createCellHighlightLayer, createGridLayer, createGuidesLayer, createSpacingHintsPreset, deriveAlignmentGuides, distanceReadout, freeform, gridSlots, gridSnapSource, guideSnapSource, matchSpacing, measureDistances, measureGaps, nearest, nearestWithin, none, pack, perContainer, pointToGridCell, quadtree, ringSlots, roundToCell, rowSlots, scopedGridPitch, sequenceLayout, siblingBounds, slots, snapPoint, spacingSnapSource, tileGrid, useGridCellHover, useGuides, worldBoundsOf, worldUnionOf };
|
|
1461
1578
|
//# sourceMappingURL=index.js.map
|
|
1462
1579
|
//# sourceMappingURL=index.js.map
|