@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/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, resolveAxes, pxExtent, textCommandFromRuns, AUTO_POSE_DESCRIPTOR, visualBoundsViaDescriptor, rectFromPlane, translatePoseViaDescriptor, unionBounds, fixedCornerOf, createTransformOp, resolveStrokeWidth, screenToWorld, rectToPlane, screenTolerance } from '@weasel-js/core';
8
- import { rectPath, PATH_M, PATH_L } from '@weasel-js/geom';
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 across2 = sx === ex ? px.x : px.y;
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) * across2 }
101
+ stroke: { ...s, width: resolveStrokeWidth(s.width ?? 1, 1) * across }
97
102
  });
98
103
  };
99
- if (subdivisions && subdivisions > 1) {
100
- const stepX = spacing.x / subdivisions;
101
- for (let x = x0; x <= x1 + 1e-9; x += stepX) {
102
- const k = Math.round((x - x0) / stepX);
103
- if (k % subdivisions === 0) continue;
104
- pushSegment(x, y0, x, y1, sub);
105
- }
106
- const stepY = spacing.y / subdivisions;
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
- let idx = 0;
114
- for (let x = x0; x <= x1 + 1e-9; x += spacing.x) {
115
- const isAccent = !!accentEvery && accentEvery > 0 && idx % accentEvery === 0;
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 span = spanned ? g.span : void 0;
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 = span ? tickedSegment(x, sy(span.min), x, sy(span.max), ticks) : tickedSegment(x, 0, x, dims.height, 0);
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 = span ? tickedSegment(sx(span.min), y, sx(span.max), y, ticks) : tickedSegment(0, y, dims.width, y, 0);
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({ kind: "path", path: tickedSegment(x0, y0, x1, y1, ticks), stroke: gapStroke });
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, span) {
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) < span[1] - EPS2 && hi(b, c) > span[0] + EPS2;
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 measure(targets, axis).map((m) => m.gap);
309
+ return measureGapSpans(targets, axis).map((m) => m.gap);
377
310
  }
378
- function measure(targets, axis) {
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 span = overlap(a, next, cross(axis));
390
- if (targets.some((t) => t !== a && t !== next && hitsStrip(gap, t, span))) continue;
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 bestAxis2(b, axis, edge, targets, refs, tol) {
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
- let best = null;
341
+ const out = [];
409
342
  const offer = (delta, gap) => {
410
- if (gap <= EPS2 || Math.abs(delta) > tol) return;
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 markers(b, axis, size, targets, refs) {
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 own = measureGaps([...targets, b], axis).filter((g) => (Math.abs(g.max - lo(b, axis)) < EPS2 || Math.abs(g.min - hi(b, axis)) < EPS2) && eq(g));
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 refsFor = (axis) => measure(targets, axis).filter((m) => !hitsStrip(m.gap, bounds, m.span)).map((m) => m.gap);
441
- const refsX = refsFor("x");
442
- const refsY = refsFor("y");
443
- const x = moving.x === null ? null : bestAxis2(bounds, "x", moving.x, targets, refsX, worldTolerance.x);
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 ? [] : markers(final, "x", x.size, targets, refsX),
452
- gapsY: y === null ? [] : markers(final, "y", y.size, targets, refsY)
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
- var across = (axis) => axis === "x" ? "y" : "x";
470
- var into = (m, axis, v) => v * m.scale[axis] + m.offset[axis];
471
- var outOf = (m, axis, v) => (v - m.offset[axis]) / m.scale[axis];
472
- function range(r, map) {
473
- const a = map(r.min);
474
- const b = map(r.max);
475
- return { ...r, min: Math.min(a, b), max: Math.max(a, b) };
476
- }
477
- function guideToPlane(g, m) {
478
- const span = g.span && range(g.span, (v) => into(m, across(g.axis), v));
479
- return { ...g, offset: guideOffsetOn(g, m), ...span ? { span } : {} };
480
- }
481
- function guideFromPlane(g, m) {
482
- const span = g.span && range(g.span, (v) => outOf(m, across(g.axis), v));
483
- return { ...g, offset: outOf(m, g.axis, g.offset), ...span ? { span } : {} };
484
- }
485
- function gapFromPlane(g, m) {
486
- return { ...range(g, (v) => outOf(m, g.axis, v)), at: outOf(m, across(g.axis), g.at) };
487
- }
488
- function alignmentOnPlane(args, plane) {
489
- if (!plane) return args;
490
- const spacing = args.getSpacingTargets;
491
- const setGaps = args.setActiveGaps;
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
- ...args,
494
- getCandidates: () => args.getCandidates().map((g) => guideToPlane(g, plane)),
495
- setActiveGuides: (guides) => args.setActiveGuides(guides.map((g) => guideFromPlane(g, plane))),
496
- ...spacing ? { getSpacingTargets: () => spacing().map((b) => rectToPlane(plane, b)) } : {},
497
- ...setGaps ? { setActiveGaps: (gaps) => setGaps(gaps.map((g) => gapFromPlane(g, plane))) } : {}
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/alignment/behaviors.ts
502
- var activeList = (m) => [m.activeX, m.activeY].filter((g) => g !== null);
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
- dx: dx ?? 0,
521
- dy: dy ?? 0,
522
- guides: activeList(matchAlignment(settled, candidates, EXACT, anchors)),
523
- gaps: [...shown.gapsX, ...shown.gapsY]
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
- function publish(args, r) {
527
- args.setActiveGuides(r?.guides ?? []);
528
- args.setActiveGaps?.(r?.gaps ?? []);
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 worldTol(base, ctx) {
531
- return screenTolerance(base.tolerance ?? 6, ctx);
524
+ function visibleRoots(scene) {
525
+ const gates = nodeGates(scene);
526
+ return scene.roots.filter((id) => !gates.hidden(id));
532
527
  }
533
- function alignMoveBehavior(given) {
534
- const d = given.poseDescriptor ?? AUTO_POSE_DESCRIPTOR;
535
- return {
536
- onMove(ctx, transform) {
537
- const args = alignmentOnPlane(given, ctx.plane);
538
- if (args.bypassKey && ctx.modifiers[args.bypassKey]) {
539
- publish(args, null);
540
- return;
541
- }
542
- if (transform.kind !== "translate") return;
543
- const boxes = [];
544
- for (const id of ctx.draggedIds) {
545
- const originPose = ctx.origin.get(id);
546
- if (originPose === void 0) continue;
547
- boxes.push(visualBoundsViaDescriptor(
548
- translatePoseViaDescriptor(originPose, transform.dx, transform.dy, d),
549
- d
550
- ));
551
- }
552
- const union = unionBounds(boxes);
553
- if (union === null) return;
554
- const r = resolve(union, args, worldTol(args, ctx), MOVE_ANCHORS, { x: "both", y: "both" });
555
- publish(args, r);
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
- function alignInsertBehavior(given) {
568
- const pointAnchors = { x: ["min"], y: ["min"] };
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
- onMove(ctx, { current }) {
571
- const args = alignmentOnPlane(given, ctx.plane);
572
- if (args.bypassKey && ctx.modifiers[args.bypassKey]) {
573
- args.setActiveGuides([]);
574
- return;
575
- }
576
- const box = { x: current.x, y: current.y, width: 0, height: 0 };
577
- const m = matchAlignment(box, args.getCandidates(), worldTol(args, ctx), pointAnchors);
578
- if (m.activeX === null && m.activeY === null) {
579
- args.setActiveGuides([]);
580
- return;
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 alignResizeBehavior(given) {
594
- const d = given.poseDescriptor ?? AUTO_POSE_DESCRIPTOR;
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
- onMove(ctx, { pose, anchor }) {
597
- const args = alignmentOnPlane(given, ctx.plane);
598
- if (args.bypassKey && ctx.modifiers[args.bypassKey]) {
599
- publish(args, null);
600
- return;
601
- }
602
- const origin = ctx.origin.get(ctx.draggedIds[0]);
603
- const rotation = origin === void 0 ? 0 : d.getRotation?.(origin) ?? 0;
604
- const m = rotation === 0 ? matchMovingEdges(pose, anchor, args, worldTol(args, ctx)) : matchRotatedCorners(pose, anchor, d.getBounds(origin), rotation, args.getCandidates(), worldTol(args, ctx));
605
- if (m === null) {
606
- publish(args, null);
607
- return;
608
- }
609
- let { x, y, width, height } = pose;
610
- if (anchor.x === "min") width += m.lx;
611
- else if (anchor.x === "max") {
612
- x += m.lx;
613
- width -= m.lx;
614
- }
615
- if (anchor.y === "min") height += m.ly;
616
- else if (anchor.y === "max") {
617
- y += m.ly;
618
- height -= m.ly;
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
- function matchMovingEdges(pose, anchor, args, tol) {
632
- const moving = (a) => a === "min" ? "max" : a === "max" ? "min" : null;
633
- const edges = { x: moving(anchor.x), y: moving(anchor.y) };
634
- const anchors = {
635
- x: edges.x === null ? [] : [edges.x],
636
- y: edges.y === null ? [] : [edges.y]
637
- };
638
- const r = resolve(pose, args, tol, anchors, edges);
639
- return r === null ? null : { lx: r.dx, ly: r.dy, guides: r.guides, gaps: r.gaps };
640
- }
641
- function matchRotatedCorners(pose, anchor, originBox, rotation, candidates, tol) {
642
- const cos = Math.cos(rotation);
643
- const sin = Math.sin(rotation);
644
- const toWorld = (lx, ly) => ({ x: lx * cos - ly * sin, y: lx * sin + ly * cos });
645
- const fixedLocal = fixedCornerOf(pose, anchor);
646
- const f0 = fixedCornerOf(originBox, anchor);
647
- const pivot = toWorld(f0.x - originBox.x - originBox.width / 2, f0.y - originBox.y - originBox.height / 2);
648
- const fixedWorld = { x: originBox.x + originBox.width / 2 + pivot.x, y: originBox.y + originBox.height / 2 + pivot.y };
649
- const drawn = (lx, ly) => {
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
- if (anchor.x !== "free" && anchor.y !== "free") {
660
- const c = drawn(movingEdge("x")[0], movingEdge("y")[0]);
661
- const m = matchAlignment({ x: c.x, y: c.y, width: 0, height: 0 }, candidates, tol, { x: ["min"], y: ["min"] });
662
- if (m.activeX === null && m.activeY === null) return null;
663
- const lx = m.dx * cos + m.dy * sin;
664
- const ly = -m.dx * sin + m.dy * cos;
665
- return { lx, ly, guides: activeList(m), gaps: [] };
666
- }
667
- if (anchor.x === "free" && anchor.y === "free") return null;
668
- const along = anchor.x === "free" ? "y" : "x";
669
- const dir = along === "x" ? toWorld(1, 0) : toWorld(0, 1);
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
- if (best === null) return null;
683
- const across2 = best.guide.axis === "x" ? best.at.y + best.t * dir.y : best.at.x + best.t * dir.x;
684
- const guides = [stretchSpan(best.guide, across2, across2)];
685
- return along === "x" ? { lx: best.t, ly: 0, guides, gaps: [] } : { lx: 0, ly: best.t, guides, gaps: [] };
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, into2, children) {
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(into2.rectAt(i), ordered[i].c.pose));
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 { MOVE_ANCHORS, alignInsertBehavior, alignMoveBehavior, alignResizeBehavior, cellAt, containedThenNearest, createCellHighlightLayer, createGridLayer, createGuidesLayer, deriveAlignmentGuides, freeform, gridSlots, matchAlignment, matchSpacing, measureGaps, nearest, nearestWithin, none, pack, perContainer, quadtree, ringSlots, roundToCell, rowSlots, sequenceLayout, slots, snapPoint, tileGrid, useGridCellHover, useGuides };
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