@weasel-js/guides 1.7.0 → 1.7.2

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
@@ -3,8 +3,8 @@ export { gridSnapStrategy, pointToGridCell } from './chunk-5BCCIJY3.js';
3
3
  export { pointSnapToGrid } from './chunk-B25KFI3I.js';
4
4
  export { DEFAULT_GUIDE_TOLERANCE_PX, guideSnapStrategy } from './chunk-GEDAPTOM.js';
5
5
  import { useState, useRef, useCallback, useEffect } from 'react';
6
- import { useLatest, resolveUnit, pxExtent, AUTO_POSE_DESCRIPTOR, visualBoundsViaDescriptor, translatePoseViaDescriptor, unionBounds, fixedCornerOf, createTransformOp, resolveStrokeWidth, screenToWorld, screenTolerance } from '@weasel-js/core';
7
- import { PATH_M, PATH_L } from '@weasel-js/geom';
6
+ import { useLatest, resolveUnit, pxExtent, textCommandFromRuns, AUTO_POSE_DESCRIPTOR, visualBoundsViaDescriptor, translatePoseViaDescriptor, unionBounds, fixedCornerOf, createTransformOp, resolveStrokeWidth, screenToWorld, screenTolerance } from '@weasel-js/core';
7
+ import { rectPath, PATH_M, PATH_L } from '@weasel-js/geom';
8
8
 
9
9
  // src/grid/roundToCell.ts
10
10
  function roundToCell(value, cellSize) {
@@ -188,44 +188,74 @@ function useGuides(initial = []) {
188
188
  return { guides, getGuides, addGuide, removeGuide, clearGuides };
189
189
  }
190
190
  var DEFAULT_COLOR = "#3aa0ff";
191
+ var LABEL_FONT = { fontSize: 10 };
192
+ var LABEL_CHAR_W = 6;
193
+ var LABEL_H = 14;
194
+ var LABEL_PAD_X = 4;
195
+ function tickedSegment(x0, y0, x1, y1, t) {
196
+ const h = t / 2;
197
+ const vertical = x0 === x1;
198
+ 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];
199
+ const coords = [x0, y0, x1, y1, ...ends];
200
+ const commands = [];
201
+ for (let i = 0; i < coords.length; i += 4) commands.push(PATH_M, PATH_L);
202
+ return {
203
+ kind: "polygon",
204
+ commands: new Uint8Array(commands),
205
+ coords: new Float32Array(coords),
206
+ fillRule: "nonzero"
207
+ };
208
+ }
191
209
  function createGuidesLayer(opts) {
192
210
  const lineWidth = opts.lineWidth ?? 1;
193
211
  const color = opts.color ?? DEFAULT_COLOR;
194
- const stroke = {
195
- paint: { fill: "solid", color },
196
- width: lineWidth
197
- };
212
+ const gapColor = opts.gapColor ?? color;
213
+ const ticks = opts.ticks ?? 6;
214
+ const spanned = (opts.extent ?? "span") === "span";
215
+ const format = opts.gapLabels === false ? null : typeof opts.gapLabels === "function" ? opts.gapLabels : (n) => String(Math.round(n));
216
+ const stroke = { paint: { fill: "solid", color }, width: lineWidth };
217
+ const gapStroke = { paint: { fill: "solid", color: gapColor }, width: lineWidth };
198
218
  return {
199
219
  id: opts.id ?? "guides",
200
220
  label: opts.label ?? "Guides",
201
221
  space: "screen",
202
222
  draw: (_data, view, dims) => {
203
223
  const guides = opts.getGuides();
204
- if (guides.length === 0) return [];
224
+ const gaps = opts.getGaps?.() ?? [];
225
+ if (guides.length === 0 && gaps.length === 0) return [];
226
+ const sx = (x) => (x - view.x) * view.scale.x;
227
+ const sy = (y) => (y - view.y) * view.scale.y;
205
228
  const out = [];
206
229
  for (const g of guides) {
230
+ const span = spanned ? g.span : void 0;
207
231
  if (g.axis === "x") {
208
- const sx = (g.offset - view.x) * view.scale.x;
209
- if (sx < -lineWidth || sx > dims.width + lineWidth) continue;
210
- const path = {
211
- kind: "polygon",
212
- commands: new Uint8Array([PATH_M, PATH_L]),
213
- coords: new Float32Array([sx, 0, sx, dims.height]),
214
- fillRule: "nonzero"
215
- };
232
+ const x = sx(g.offset);
233
+ if (x < -lineWidth || x > dims.width + lineWidth) continue;
234
+ const path = span ? tickedSegment(x, sy(span.min), x, sy(span.max), ticks) : tickedSegment(x, 0, x, dims.height, 0);
216
235
  out.push({ kind: "path", path, stroke });
217
236
  } else {
218
- const sy = (g.offset - view.y) * view.scale.y;
219
- if (sy < -lineWidth || sy > dims.height + lineWidth) continue;
220
- const path = {
221
- kind: "polygon",
222
- commands: new Uint8Array([PATH_M, PATH_L]),
223
- coords: new Float32Array([0, sy, dims.width, sy]),
224
- fillRule: "nonzero"
225
- };
237
+ const y = sy(g.offset);
238
+ if (y < -lineWidth || y > dims.height + lineWidth) continue;
239
+ const path = span ? tickedSegment(sx(span.min), y, sx(span.max), y, ticks) : tickedSegment(0, y, dims.width, y, 0);
226
240
  out.push({ kind: "path", path, stroke });
227
241
  }
228
242
  }
243
+ for (const g of gaps) {
244
+ 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)];
245
+ out.push({ kind: "path", path: tickedSegment(x0, y0, x1, y1, ticks), stroke: gapStroke });
246
+ if (format === null) continue;
247
+ const text = format(g.max - g.min);
248
+ const w = text.length * LABEL_CHAR_W + LABEL_PAD_X * 2;
249
+ const left = (x0 + x1) / 2 - w / 2;
250
+ const top = (y0 + y1) / 2 - LABEL_H / 2;
251
+ out.push({ kind: "path", path: rectPath(left, top, w, LABEL_H), fill: { fill: "solid", color: gapColor } });
252
+ out.push(textCommandFromRuns(
253
+ left + LABEL_PAD_X,
254
+ top + 2,
255
+ [{ text, fill: { fill: "solid", color: "#fff" } }],
256
+ LABEL_FONT
257
+ ));
258
+ }
229
259
  return out;
230
260
  }
231
261
  };
@@ -237,22 +267,28 @@ function deriveAlignmentGuides(targets, opts = {}) {
237
267
  const centers = opts.centers ?? true;
238
268
  const seenX = /* @__PURE__ */ new Map();
239
269
  const seenY = /* @__PURE__ */ new Map();
240
- const add = (axis, offset) => {
270
+ const add = (axis, offset, b) => {
241
271
  const seen = axis === "x" ? seenX : seenY;
242
272
  const key = Math.round(offset / EPS);
243
- if (seen.has(key)) return;
244
- seen.set(key, { id: `align:${axis}:${offset.toFixed(3)}`, axis, offset });
273
+ const min = axis === "x" ? b.y : b.x;
274
+ const max = min + (axis === "x" ? b.height : b.width);
275
+ const prev = seen.get(key);
276
+ if (prev) {
277
+ prev.span = { min: Math.min(prev.span.min, min), max: Math.max(prev.span.max, max) };
278
+ return;
279
+ }
280
+ seen.set(key, { id: `align:${axis}:${offset.toFixed(3)}`, axis, offset, span: { min, max } });
245
281
  };
246
282
  const emit = (b) => {
247
283
  if (edges) {
248
- add("x", b.x);
249
- add("x", b.x + b.width);
250
- add("y", b.y);
251
- add("y", b.y + b.height);
284
+ add("x", b.x, b);
285
+ add("x", b.x + b.width, b);
286
+ add("y", b.y, b);
287
+ add("y", b.y + b.height, b);
252
288
  }
253
289
  if (centers) {
254
- add("x", b.x + b.width / 2);
255
- add("y", b.y + b.height / 2);
290
+ add("x", b.x + b.width / 2, b);
291
+ add("y", b.y + b.height / 2, b);
256
292
  }
257
293
  };
258
294
  for (const t of targets) emit(visualBoundsViaDescriptor(t, d));
@@ -297,9 +333,161 @@ function bestAxis(b, axis, anchors, candidates, worldTolerance) {
297
333
  function matchAlignment(bounds, candidates, worldTolerance, anchors) {
298
334
  const x = bestAxis(bounds, "x", anchors.x, candidates, worldTolerance.x);
299
335
  const y = bestAxis(bounds, "y", anchors.y, candidates, worldTolerance.y);
300
- return { dx: x.delta, dy: y.delta, activeX: x.guide, activeY: y.guide };
336
+ const top = bounds.y + y.delta;
337
+ const left = bounds.x + x.delta;
338
+ return {
339
+ dx: x.delta,
340
+ dy: y.delta,
341
+ activeX: stretchSpan(x.guide, top, top + bounds.height),
342
+ activeY: stretchSpan(y.guide, left, left + bounds.width)
343
+ };
344
+ }
345
+ function stretchSpan(g, min, max) {
346
+ if (g === null || g.span === void 0) return g;
347
+ return { ...g, span: { min: Math.min(g.span.min, min), max: Math.max(g.span.max, max) } };
348
+ }
349
+
350
+ // src/guides/alignment/spacing.ts
351
+ var EPS2 = 1e-6;
352
+ var lo = (b, a) => a === "x" ? b.x : b.y;
353
+ var hi = (b, a) => a === "x" ? b.x + b.width : b.y + b.height;
354
+ var cross = (a) => a === "x" ? "y" : "x";
355
+ function overlap(a, b, axis) {
356
+ const min = Math.max(lo(a, axis), lo(b, axis));
357
+ const max = Math.min(hi(a, axis), hi(b, axis));
358
+ return max - min > EPS2 ? [min, max] : null;
359
+ }
360
+ function gapBetween(a, b, axis) {
361
+ const shared = overlap(a, b, cross(axis));
362
+ if (shared === null) return null;
363
+ return { axis, min: hi(a, axis), max: lo(b, axis), at: (shared[0] + shared[1]) / 2 };
364
+ }
365
+ function hitsStrip(g, b, span) {
366
+ const c = cross(g.axis);
367
+ 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;
368
+ }
369
+ function measureGaps(targets, axis) {
370
+ return measure(targets, axis).map((m) => m.gap);
371
+ }
372
+ function measure(targets, axis) {
373
+ const out = [];
374
+ for (const a of targets) {
375
+ let next = null;
376
+ for (const c of targets) {
377
+ if (c === a || lo(c, axis) < hi(a, axis) + EPS2) continue;
378
+ if (overlap(a, c, cross(axis)) === null) continue;
379
+ if (next === null || lo(c, axis) < lo(next, axis)) next = c;
380
+ }
381
+ if (next === null) continue;
382
+ const gap = gapBetween(a, next, axis);
383
+ const span = overlap(a, next, cross(axis));
384
+ if (targets.some((t) => t !== a && t !== next && hitsStrip(gap, t, span))) continue;
385
+ out.push({ gap, span });
386
+ }
387
+ return out.sort((p, q) => p.gap.min - q.gap.min || p.gap.at - q.gap.at);
388
+ }
389
+ function bestAxis2(b, axis, edge, targets, refs, tol) {
390
+ const bLo = lo(b, axis);
391
+ const bHi = hi(b, axis);
392
+ const size = bHi - bLo;
393
+ const center = (bLo + bHi) / 2;
394
+ let left = null;
395
+ let right = null;
396
+ for (const t of targets) {
397
+ if (overlap(t, b, cross(axis)) === null) continue;
398
+ if (hi(t, axis) <= center && (left === null || hi(t, axis) > hi(left, axis))) left = t;
399
+ if (lo(t, axis) >= center && (right === null || lo(t, axis) < lo(right, axis))) right = t;
400
+ }
401
+ const sizes = refs.map((g) => g.max - g.min);
402
+ let best = null;
403
+ const offer = (delta, gap) => {
404
+ if (gap <= EPS2 || Math.abs(delta) > tol) return;
405
+ if (best === null || Math.abs(delta) < Math.abs(best.delta)) best = { delta, size: gap };
406
+ };
407
+ const L = left === null ? null : hi(left, axis);
408
+ const R = right === null ? null : lo(right, axis);
409
+ if (edge === "both") {
410
+ for (const g of sizes) {
411
+ if (L !== null && (R === null || L + g + size <= R + EPS2)) offer(L + g - bLo, g);
412
+ if (R !== null && (L === null || R - g - size >= L - EPS2)) offer(R - g - size - bLo, g);
413
+ }
414
+ if (L !== null && R !== null) {
415
+ const g = (R - L - size) / 2;
416
+ offer(L + g - bLo, g);
417
+ }
418
+ } else if (edge === "max" && R !== null) {
419
+ const own = L !== null ? [bLo - L] : [];
420
+ for (const g of [...sizes, ...own]) if (R - g > bLo + EPS2) offer(R - g - bHi, g);
421
+ } else if (edge === "min" && L !== null) {
422
+ const own = R !== null ? [R - bHi] : [];
423
+ for (const g of [...sizes, ...own]) if (L + g < bHi - EPS2) offer(L + g - bLo, g);
424
+ }
425
+ return best;
426
+ }
427
+ function markers(b, axis, size, targets, refs) {
428
+ const eq = (g) => Math.abs(g.max - g.min - size) < 1e-3;
429
+ const out = refs.filter(eq);
430
+ 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));
431
+ return [...out, ...own];
432
+ }
433
+ function matchSpacing(bounds, targets, worldTolerance, moving = { x: "both", y: "both" }) {
434
+ const refsFor = (axis) => measure(targets, axis).filter((m) => !hitsStrip(m.gap, bounds, m.span)).map((m) => m.gap);
435
+ const refsX = refsFor("x");
436
+ const refsY = refsFor("y");
437
+ const x = moving.x === null ? null : bestAxis2(bounds, "x", moving.x, targets, refsX, worldTolerance.x);
438
+ const y = moving.y === null ? null : bestAxis2(bounds, "y", moving.y, targets, refsY, worldTolerance.y);
439
+ const dx = x?.delta ?? 0;
440
+ const dy = y?.delta ?? 0;
441
+ const final = applyEdges(bounds, dx, dy, moving);
442
+ return {
443
+ dx,
444
+ dy,
445
+ gapsX: x === null ? [] : markers(final, "x", x.size, targets, refsX),
446
+ gapsY: y === null ? [] : markers(final, "y", y.size, targets, refsY)
447
+ };
448
+ }
449
+ function applyEdges(b, dx, dy, moving) {
450
+ let { x, y, width, height } = b;
451
+ if (moving.x === "both") x += dx;
452
+ else if (moving.x === "min") {
453
+ x += dx;
454
+ width -= dx;
455
+ } else if (moving.x === "max") width += dx;
456
+ if (moving.y === "both") y += dy;
457
+ else if (moving.y === "min") {
458
+ y += dy;
459
+ height -= dy;
460
+ } else if (moving.y === "max") height += dy;
461
+ return { x, y, width, height };
301
462
  }
302
463
  var activeList = (m) => [m.activeX, m.activeY].filter((g) => g !== null);
464
+ var EXACT = { x: 1e-6, y: 1e-6 };
465
+ function resolve(b, args, tol, anchors, edges) {
466
+ const candidates = args.getCandidates();
467
+ const m = matchAlignment(b, candidates, tol, anchors);
468
+ const targets = args.getSpacingTargets?.();
469
+ if (targets === void 0) {
470
+ if (m.activeX === null && m.activeY === null) return null;
471
+ return { dx: m.dx, dy: m.dy, guides: activeList(m), gaps: [] };
472
+ }
473
+ const s = matchSpacing(b, targets, tol, edges);
474
+ const pick = (hit, d, spaced, sd) => hit && (!spaced || Math.abs(d) <= Math.abs(sd)) ? d : spaced ? sd : null;
475
+ const dx = pick(m.activeX !== null, m.dx, s.gapsX.length > 0, s.dx);
476
+ const dy = pick(m.activeY !== null, m.dy, s.gapsY.length > 0, s.dy);
477
+ if (dx === null && dy === null) return null;
478
+ const settled = applyEdges(b, dx ?? 0, dy ?? 0, edges);
479
+ const shown = matchSpacing(settled, targets, EXACT, edges);
480
+ return {
481
+ dx: dx ?? 0,
482
+ dy: dy ?? 0,
483
+ guides: activeList(matchAlignment(settled, candidates, EXACT, anchors)),
484
+ gaps: [...shown.gapsX, ...shown.gapsY]
485
+ };
486
+ }
487
+ function publish(args, r) {
488
+ args.setActiveGuides(r?.guides ?? []);
489
+ args.setActiveGaps?.(r?.gaps ?? []);
490
+ }
303
491
  function worldTol(base, ctx) {
304
492
  return screenTolerance(base.tolerance ?? 6, ctx);
305
493
  }
@@ -308,7 +496,7 @@ function alignMoveBehavior(args) {
308
496
  return {
309
497
  onMove(ctx, transform) {
310
498
  if (args.bypassKey && ctx.modifiers[args.bypassKey]) {
311
- args.setActiveGuides([]);
499
+ publish(args, null);
312
500
  return;
313
501
  }
314
502
  if (transform.kind !== "translate") return;
@@ -323,19 +511,16 @@ function alignMoveBehavior(args) {
323
511
  }
324
512
  const union = unionBounds(boxes);
325
513
  if (union === null) return;
326
- const m = matchAlignment(union, args.getCandidates(), worldTol(args, ctx), MOVE_ANCHORS);
327
- if (m.activeX === null && m.activeY === null) {
328
- args.setActiveGuides([]);
329
- return;
330
- }
331
- args.setActiveGuides(activeList(m));
332
- return { transform: { kind: "translate", dx: transform.dx + m.dx, dy: transform.dy + m.dy } };
514
+ const r = resolve(union, args, worldTol(args, ctx), MOVE_ANCHORS, { x: "both", y: "both" });
515
+ publish(args, r);
516
+ if (r === null) return;
517
+ return { transform: { kind: "translate", dx: transform.dx + r.dx, dy: transform.dy + r.dy } };
333
518
  },
334
519
  onEnd() {
335
- args.setActiveGuides([]);
520
+ publish(args, null);
336
521
  },
337
522
  onCancel() {
338
- args.setActiveGuides([]);
523
+ publish(args, null);
339
524
  }
340
525
  };
341
526
  }
@@ -369,14 +554,14 @@ function alignResizeBehavior(args) {
369
554
  return {
370
555
  onMove(ctx, { pose, anchor }) {
371
556
  if (args.bypassKey && ctx.modifiers[args.bypassKey]) {
372
- args.setActiveGuides([]);
557
+ publish(args, null);
373
558
  return;
374
559
  }
375
560
  const origin = ctx.origin.get(ctx.draggedIds[0]);
376
561
  const rotation = origin === void 0 ? 0 : d.getRotation?.(origin) ?? 0;
377
- const m = rotation === 0 ? matchMovingEdges(pose, anchor, args.getCandidates(), worldTol(args, ctx)) : matchRotatedCorners(pose, anchor, d.getBounds(origin), rotation, args.getCandidates(), worldTol(args, ctx));
562
+ const m = rotation === 0 ? matchMovingEdges(pose, anchor, args, worldTol(args, ctx)) : matchRotatedCorners(pose, anchor, d.getBounds(origin), rotation, args.getCandidates(), worldTol(args, ctx));
378
563
  if (m === null) {
379
- args.setActiveGuides([]);
564
+ publish(args, null);
380
565
  return;
381
566
  }
382
567
  let { x, y, width, height } = pose;
@@ -390,23 +575,26 @@ function alignResizeBehavior(args) {
390
575
  y += m.ly;
391
576
  height -= m.ly;
392
577
  }
393
- args.setActiveGuides(m.guides);
578
+ publish(args, { guides: m.guides, gaps: m.gaps });
394
579
  return { pose: { ...pose, x, y, width, height } };
395
580
  },
396
581
  onEnd() {
397
- args.setActiveGuides([]);
582
+ publish(args, null);
398
583
  },
399
584
  onCancel() {
400
- args.setActiveGuides([]);
585
+ publish(args, null);
401
586
  }
402
587
  };
403
588
  }
404
- function matchMovingEdges(pose, anchor, candidates, tol) {
405
- const movingX = anchor.x === "min" ? ["max"] : anchor.x === "max" ? ["min"] : [];
406
- const movingY = anchor.y === "min" ? ["max"] : anchor.y === "max" ? ["min"] : [];
407
- const m = matchAlignment(pose, candidates, tol, { x: movingX, y: movingY });
408
- if (m.activeX === null && m.activeY === null) return null;
409
- return { lx: m.dx, ly: m.dy, guides: activeList(m) };
589
+ function matchMovingEdges(pose, anchor, args, tol) {
590
+ const moving = (a) => a === "min" ? "max" : a === "max" ? "min" : null;
591
+ const edges = { x: moving(anchor.x), y: moving(anchor.y) };
592
+ const anchors = {
593
+ x: edges.x === null ? [] : [edges.x],
594
+ y: edges.y === null ? [] : [edges.y]
595
+ };
596
+ const r = resolve(pose, args, tol, anchors, edges);
597
+ return r === null ? null : { lx: r.dx, ly: r.dy, guides: r.guides, gaps: r.gaps };
410
598
  }
411
599
  function matchRotatedCorners(pose, anchor, originBox, rotation, candidates, tol) {
412
600
  const cos = Math.cos(rotation);
@@ -421,10 +609,10 @@ function matchRotatedCorners(pose, anchor, originBox, rotation, candidates, tol)
421
609
  return { x: fixedWorld.x + o.x, y: fixedWorld.y + o.y };
422
610
  };
423
611
  const movingEdge = (axis) => {
424
- const lo = axis === "x" ? pose.x : pose.y;
425
- const hi = lo + (axis === "x" ? pose.width : pose.height);
612
+ const lo2 = axis === "x" ? pose.x : pose.y;
613
+ const hi2 = lo2 + (axis === "x" ? pose.width : pose.height);
426
614
  const a = anchor[axis];
427
- return a === "min" ? [hi] : a === "max" ? [lo] : [lo, hi];
615
+ return a === "min" ? [hi2] : a === "max" ? [lo2] : [lo2, hi2];
428
616
  };
429
617
  if (anchor.x !== "free" && anchor.y !== "free") {
430
618
  const c = drawn(movingEdge("x")[0], movingEdge("y")[0]);
@@ -432,25 +620,27 @@ function matchRotatedCorners(pose, anchor, originBox, rotation, candidates, tol)
432
620
  if (m.activeX === null && m.activeY === null) return null;
433
621
  const lx = m.dx * cos + m.dy * sin;
434
622
  const ly = -m.dx * sin + m.dy * cos;
435
- return { lx, ly, guides: activeList(m) };
623
+ return { lx, ly, guides: activeList(m), gaps: [] };
436
624
  }
437
625
  if (anchor.x === "free" && anchor.y === "free") return null;
438
626
  const along = anchor.x === "free" ? "y" : "x";
439
627
  const dir = along === "x" ? toWorld(1, 0) : toWorld(0, 1);
440
628
  const edge = movingEdge(along)[0];
441
629
  let best = null;
442
- for (const across of movingEdge(along === "x" ? "y" : "x")) {
443
- const c = along === "x" ? drawn(edge, across) : drawn(across, edge);
630
+ for (const across2 of movingEdge(along === "x" ? "y" : "x")) {
631
+ const c = along === "x" ? drawn(edge, across2) : drawn(across2, edge);
444
632
  for (const g of candidates) {
445
633
  const reach = g.axis === "x" ? dir.x : dir.y;
446
634
  if (Math.abs(reach) < 1e-9) continue;
447
635
  const t = (g.offset - (g.axis === "x" ? c.x : c.y)) / reach;
448
636
  if (Math.abs(t * dir.x) > tol.x || Math.abs(t * dir.y) > tol.y) continue;
449
- if (best === null || Math.abs(t) < Math.abs(best.t)) best = { t, guide: g };
637
+ if (best === null || Math.abs(t) < Math.abs(best.t)) best = { t, guide: g, at: c };
450
638
  }
451
639
  }
452
640
  if (best === null) return null;
453
- return along === "x" ? { lx: best.t, ly: 0, guides: [best.guide] } : { lx: 0, ly: best.t, guides: [best.guide] };
641
+ const across = best.guide.axis === "x" ? best.at.y + best.t * dir.y : best.at.x + best.t * dir.x;
642
+ const guides = [stretchSpan(best.guide, across, across)];
643
+ return along === "x" ? { lx: best.t, ly: 0, guides, gaps: [] } : { lx: 0, ly: best.t, guides, gaps: [] };
454
644
  }
455
645
 
456
646
  // src/layout/snaps.ts
@@ -546,103 +736,111 @@ function defaultCenterOf(pose) {
546
736
  const p = pose;
547
737
  return { x: (p.x ?? 0) + (p.width ?? 0) / 2, y: (p.y ?? 0) + (p.height ?? 0) / 2 };
548
738
  }
549
- function cellRectAt(bounds, cols, rows, gap, col, row) {
550
- const cw = (bounds.width - gap * (cols - 1)) / cols;
551
- const ch = (bounds.height - gap * (rows - 1)) / rows;
552
- return {
553
- x: bounds.x + col * (cw + gap),
554
- y: bounds.y + row * (ch + gap),
555
- width: cw,
556
- height: ch
557
- };
558
- }
559
739
  function tileGrid(opts) {
560
740
  const { cols, rows } = opts;
561
741
  const gap = opts.gap ?? 0;
742
+ const byColumn = opts.flow === "column";
743
+ const overflow = opts.overflow ?? "reject";
562
744
  const snap = opts.snap ?? cellAt();
563
745
  const capacity = cols * rows;
746
+ const perLine = byColumn ? rows : cols;
747
+ const minLines = byColumn ? cols : rows;
564
748
  const cellToPose = opts.cellToPose ?? ((cell, dragged) => {
565
749
  return { ...dragged, ...cell };
566
750
  });
567
751
  const centerOf = opts.centerOf ?? defaultCenterOf;
568
- function cellPose(bounds, col, row, basis) {
569
- return cellToPose(cellRectAt(bounds, cols, rows, gap, col, row), basis);
570
- }
571
- function cellIndexOf(bounds, pose) {
572
- const p = centerOf(pose);
573
- for (let row = 0; row < rows; row++) {
574
- for (let col = 0; col < cols; col++) {
575
- const r = cellRectAt(bounds, cols, rows, gap, col, row);
576
- if (p.x >= r.x && p.x < r.x + r.width && p.y >= r.y && p.y < r.y + r.height) {
577
- return row * cols + col;
578
- }
579
- }
752
+ const linesFor = (n) => Math.max(minLines, Math.ceil(n / perLine));
753
+ function gridOf(bounds, n) {
754
+ const lines = overflow === "grow" ? linesFor(n) : minLines;
755
+ const c = byColumn ? lines : cols;
756
+ const r = byColumn ? rows : lines;
757
+ const cw = (bounds.width - gap * (c - 1)) / c;
758
+ const ch = (bounds.height - gap * (r - 1)) / r;
759
+ function rectAt(index) {
760
+ const along = index % perLine;
761
+ const line = Math.floor(index / perLine);
762
+ const col = byColumn ? line : along;
763
+ const row = byColumn ? along : line;
764
+ return { x: bounds.x + col * (cw + gap), y: bounds.y + row * (ch + gap), width: cw, height: ch };
765
+ }
766
+ function indexOf(pose) {
767
+ const p = centerOf(pose);
768
+ const col = Math.floor((p.x - bounds.x) / (cw + gap));
769
+ const row = Math.floor((p.y - bounds.y) / (ch + gap));
770
+ if (col < 0 || row < 0) return null;
771
+ if (p.x - bounds.x - col * (cw + gap) >= cw) return null;
772
+ if (p.y - bounds.y - row * (ch + gap) >= ch) return null;
773
+ if (byColumn ? row >= rows : col >= cols) return null;
774
+ return byColumn ? col * rows + row : row * cols + col;
580
775
  }
581
- return null;
776
+ const cells = overflow === "reject" ? capacity : linesFor(n) * perLine;
777
+ function boundsFor(lineCount) {
778
+ return byColumn ? { ...bounds, width: lineCount * cw + gap * (lineCount - 1) } : { ...bounds, height: lineCount * ch + gap * (lineCount - 1) };
779
+ }
780
+ return { rectAt, indexOf, cells, boundsFor };
582
781
  }
782
+ const metaOf = (index, cellRect) => {
783
+ const along = index % perLine;
784
+ const line = Math.floor(index / perLine);
785
+ return byColumn ? { col: line, row: along, cellRect } : { col: along, row: line, cellRect };
786
+ };
787
+ const indexOfMeta = (m) => byColumn ? m.col * rows + m.row : m.row * cols + m.col;
788
+ const heldBy = (container, children, dragged) => dragged && dragged.sourceContainerId !== container.id ? children.filter((c) => c.id !== dragged.id).length : children.length;
583
789
  function computeSwap(container, children, dragged, target) {
584
790
  if (target === null) return null;
585
791
  if (dragged.sourceContainerId !== container.id) return null;
586
- const meta = target.meta;
587
- const idx = meta.row * cols + meta.col;
588
- const occupant = children.find(
589
- (c) => c.id !== dragged.id && cellIndexOf(container.bounds, c.pose) === idx
590
- );
792
+ const grid = gridOf(container.bounds, children.length);
793
+ const idx = indexOfMeta(target.meta);
794
+ const occupant = children.find((c) => c.id !== dragged.id && grid.indexOf(c.pose) === idx);
591
795
  if (occupant === void 0) return null;
592
796
  const self = children.find((c) => c.id === dragged.id);
593
- const fromIdx = cellIndexOf(container.bounds, self ? self.pose : dragged.originPose);
797
+ const fromIdx = grid.indexOf(self ? self.pose : dragged.originPose);
594
798
  if (fromIdx === null || fromIdx === idx) return null;
595
- const oldCellRect = cellRectAt(
596
- container.bounds,
597
- cols,
598
- rows,
599
- gap,
600
- fromIdx % cols,
601
- Math.floor(fromIdx / cols)
602
- );
603
- return { occupant: occupant.id, from: occupant.pose, newPose: cellToPose(oldCellRect, occupant.pose) };
799
+ return { occupant: occupant.id, from: occupant.pose, newPose: cellToPose(grid.rectAt(fromIdx), occupant.pose) };
800
+ }
801
+ function pack(from, into, children) {
802
+ const ordered = children.map((c) => ({ c, cell: from.indexOf(c.pose) })).sort((a, b) => {
803
+ if (a.cell !== b.cell) {
804
+ if (a.cell === null) return 1;
805
+ if (b.cell === null) return -1;
806
+ return a.cell - b.cell;
807
+ }
808
+ return a.c.id < b.c.id ? -1 : a.c.id > b.c.id ? 1 : 0;
809
+ });
810
+ const out = /* @__PURE__ */ new Map();
811
+ for (let i = 0; i < ordered.length; i++) {
812
+ out.set(ordered[i].c.id, cellToPose(into.rectAt(i), ordered[i].c.pose));
813
+ }
814
+ return out;
604
815
  }
605
816
  return {
606
817
  snap,
607
818
  childPoses(container, children) {
608
- const out = /* @__PURE__ */ new Map();
609
- const ordered = children.map((c) => ({ c, cell: cellIndexOf(container.bounds, c.pose) })).sort((a, b) => {
610
- if (a.cell !== b.cell) {
611
- if (a.cell === null) return 1;
612
- if (b.cell === null) return -1;
613
- return a.cell - b.cell;
614
- }
615
- return a.c.id < b.c.id ? -1 : a.c.id > b.c.id ? 1 : 0;
616
- });
617
- for (let i = 0; i < ordered.length && i < capacity; i++) {
618
- const col = i % cols;
619
- const row = Math.floor(i / cols);
620
- out.set(ordered[i].c.id, cellPose(container.bounds, col, row, ordered[i].c.pose));
621
- }
622
- return out;
819
+ const grid = gridOf(container.bounds, children.length);
820
+ return pack(grid, grid, children);
623
821
  },
624
822
  getDropTargets(container, children, dragged) {
823
+ const grid = gridOf(container.bounds, heldBy(container, children, dragged));
625
824
  const occupied = /* @__PURE__ */ new Set();
626
- if (dragged.sourceContainerId !== container.id) {
825
+ const fromOutside = dragged.sourceContainerId !== container.id;
826
+ if (fromOutside) {
627
827
  for (const c of children) {
628
828
  if (c.id === dragged.id) continue;
629
- const idx = cellIndexOf(container.bounds, c.pose);
829
+ const idx = grid.indexOf(c.pose);
630
830
  if (idx !== null) occupied.add(idx);
631
831
  }
632
832
  }
633
- const out = [];
634
- for (let row = 0; row < rows; row++) {
635
- for (let col = 0; col < cols; col++) {
636
- if (occupied.has(row * cols + col)) continue;
637
- const cellRect = cellRectAt(container.bounds, cols, rows, gap, col, row);
638
- out.push({
639
- pose: cellToPose(cellRect, dragged.pose),
640
- origin: { x: cellRect.x + cellRect.width / 2, y: cellRect.y + cellRect.height / 2 },
641
- meta: { col, row, cellRect }
642
- });
643
- }
644
- }
645
- return out;
833
+ const offered = [];
834
+ for (let i = 0; i < grid.cells; i++) if (!occupied.has(i)) offered.push(i);
835
+ if (fromOutside && offered.length === 0 && overflow !== "reject") offered.push(grid.cells);
836
+ return offered.map((i) => {
837
+ const cellRect = grid.rectAt(i);
838
+ return {
839
+ pose: cellToPose(cellRect, dragged.pose),
840
+ origin: { x: cellRect.x + cellRect.width / 2, y: cellRect.y + cellRect.height / 2 },
841
+ meta: metaOf(i, cellRect)
842
+ };
843
+ });
646
844
  },
647
845
  reflowPoses(container, children, dragged, target) {
648
846
  const out = /* @__PURE__ */ new Map();
@@ -681,6 +879,67 @@ function tileGrid(opts) {
681
879
  })
682
880
  );
683
881
  return ops;
882
+ },
883
+ arrive(container, children, arrivals) {
884
+ const n = children.length;
885
+ if (overflow === "reject" && n > capacity) return null;
886
+ const grid = gridOf(container.bounds, n - arrivals.size);
887
+ const slots = overflow === "reject" ? capacity : linesFor(n) * perLine;
888
+ const taken = /* @__PURE__ */ new Set();
889
+ for (const c of children) {
890
+ if (arrivals.has(c.id)) continue;
891
+ const idx = grid.indexOf(c.pose);
892
+ if (idx !== null) taken.add(idx);
893
+ }
894
+ const poses = /* @__PURE__ */ new Map();
895
+ for (const c of children) {
896
+ if (!arrivals.has(c.id)) continue;
897
+ const own = grid.indexOf(c.pose);
898
+ let idx;
899
+ if (own !== null && own < slots && !taken.has(own)) {
900
+ idx = own;
901
+ } else {
902
+ const at = centerOf(c.pose);
903
+ let best = -1;
904
+ let bestD = Infinity;
905
+ for (let i = 0; i < slots; i++) {
906
+ if (taken.has(i)) continue;
907
+ const r = grid.rectAt(i);
908
+ const dx = r.x + r.width / 2 - at.x;
909
+ const dy = r.y + r.height / 2 - at.y;
910
+ const d = dx * dx + dy * dy;
911
+ if (d < bestD) {
912
+ bestD = d;
913
+ best = i;
914
+ }
915
+ }
916
+ if (best < 0) return null;
917
+ idx = best;
918
+ }
919
+ taken.add(idx);
920
+ poses.set(c.id, cellToPose(grid.rectAt(idx), c.pose));
921
+ }
922
+ if (overflow !== "grow") return { poses };
923
+ const lines = linesFor(n);
924
+ if (lines === linesFor(n - arrivals.size)) return { poses };
925
+ return { poses, bounds: grid.boundsFor(lines) };
926
+ },
927
+ depart(container, children, departed) {
928
+ const n = children.length;
929
+ const before = gridOf(container.bounds, n + departed.size);
930
+ const lines = linesFor(n);
931
+ if (overflow !== "grow" || lines === linesFor(n + departed.size)) {
932
+ return { poses: pack(before, before, children) };
933
+ }
934
+ const bounds = before.boundsFor(lines);
935
+ return { poses: pack(before, gridOf(bounds, n), children), bounds };
936
+ },
937
+ contentExtent(container, children) {
938
+ const grid = gridOf(container.bounds, children.length);
939
+ const lines = Math.ceil(children.length / perLine);
940
+ if (lines <= minLines) return container.bounds;
941
+ const reach = grid.boundsFor(lines);
942
+ return overflow === "grow" ? container.bounds : { ...reach, width: Math.max(reach.width, container.bounds.width), height: Math.max(reach.height, container.bounds.height) };
684
943
  }
685
944
  };
686
945
  }
@@ -751,6 +1010,6 @@ function snapPoint(opts) {
751
1010
  };
752
1011
  }
753
1012
 
754
- export { MOVE_ANCHORS, alignInsertBehavior, alignMoveBehavior, alignResizeBehavior, cellAt, containedThenNearest, createCellHighlightLayer, createGridLayer, createGuidesLayer, deriveAlignmentGuides, freeform, matchAlignment, nearest, nearestWithin, none, roundToCell, snapPoint, tileGrid, useGridCellHover, useGuides };
1013
+ export { MOVE_ANCHORS, alignInsertBehavior, alignMoveBehavior, alignResizeBehavior, cellAt, containedThenNearest, createCellHighlightLayer, createGridLayer, createGuidesLayer, deriveAlignmentGuides, freeform, matchAlignment, matchSpacing, measureGaps, nearest, nearestWithin, none, roundToCell, snapPoint, tileGrid, useGridCellHover, useGuides };
755
1014
  //# sourceMappingURL=index.js.map
756
1015
  //# sourceMappingURL=index.js.map