@weasel-js/guides 1.11.0 → 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.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { RefObject } from 'react';
2
- import { ViewTransform, PlaneValue, PlaneUnits, View, Dims, CommandStroke, RenderLayer, CommandFill, Bounds, PlaneMap, PoseDescriptor, Scene, NodeId, SnapSource, SurfaceContribution, FeaturePreset, LayoutSnap, LayoutStrategy, LayoutContainer, Vec2, ContainerBounds, LayoutChild } from '@weasel-js/core';
2
+ import { ViewTransform, PlaneValue, PlaneUnits, View, Dims, CommandStroke, RenderLayer, CommandFill, UnitScope, PerAxis, Bounds, PlaneMap, PoseDescriptor, Scene, NodeId, SnapSource, SceneNode, SurfaceContribution, FeaturePreset, LayoutSnap, LayoutStrategy, LayoutContainer, Vec2, ContainerBounds, LayoutChild } from '@weasel-js/core';
3
3
  import { Rect } from '@weasel-js/geom';
4
4
 
5
5
  /** Round `value` to the nearest multiple of `cellSize`. Returns 0 when the result would be -0. */
@@ -147,6 +147,24 @@ declare function pointToGridCell(point: {
147
147
  row: number;
148
148
  };
149
149
 
150
+ /** How a grid reads the unit scopes a scene's containers declare
151
+ * (`ContainerNode.units`). */
152
+ interface GridScopes {
153
+ /** The scene's outermost scope, as `sceneUnitScopes` is handed it. The grid's
154
+ * `spacing` is read in its system, in place of `unitSystem`. */
155
+ root: UnitScope;
156
+ /** The pitch inside a container's scope, written in that scope's units.
157
+ * Default: the grid's `spacing`, read there, so a bare 10 is ten of its
158
+ * base units. */
159
+ spacing?: (scope: UnitScope) => PlaneValue;
160
+ }
161
+ /**
162
+ * A grid's pitch on each axis, in world units, where `chain` (outermost
163
+ * first, rooted at `scopes.root`) encloses it: `spacing` in the root, and
164
+ * `scopes.spacing` of the innermost scope inside a container's.
165
+ */
166
+ declare function scopedGridPitch(spacing: PlaneValue, scopes: GridScopes, chain: readonly UnitScope[]): PerAxis<number>;
167
+
150
168
  /**
151
169
  * Guide — a horizontal or vertical world-space line used as a snap target
152
170
  * and an optional rendered overlay.
@@ -354,6 +372,11 @@ interface GridSnapSourceOptions {
354
372
  /** A point the lattice passes through. Default `(0, 0)`. */
355
373
  origin?: Point;
356
374
  unitSystem?: PlaneUnits;
375
+ /** Read the unit scope the subject sits in: a node inside a millimeter
376
+ * sub-assembly snaps on that scope's pitch. The subject's scope is the one
377
+ * all its nodes share; for a gesture with none, the scope of the
378
+ * container it draws into (`SnapCollect.parent`), else the root. */
379
+ scopes?: GridScopes;
357
380
  }
358
381
  /**
359
382
  * A grid as snap targets: for each origin or pointer probe, the grid line on
@@ -370,6 +393,9 @@ declare function gridSnapSource(opts: GridSnapSourceOptions): SnapSource;
370
393
  */
371
394
  declare function guideSnapSource(getGuides: () => readonly Guide[]): SnapSource;
372
395
 
396
+ /** Which nodes may be targets; one that fails offers no lines or gaps. */
397
+ type TargetFilter = (node: SceneNode<unknown, string, unknown>) => boolean;
398
+
373
399
  /** Options for `alignmentSnapSource`. */
374
400
  interface AlignmentSnapSourceOptions {
375
401
  /** A page box whose edges and center are lines too, in the camera's world. */
@@ -377,6 +403,8 @@ interface AlignmentSnapSourceOptions {
377
403
  /** The lines to align to, in place of the edges and centers of the
378
404
  * subject's siblings (of the scene's top-level nodes, for a point gesture). */
379
405
  getCandidates?: () => readonly Guide[];
406
+ /** Which nodes offer lines. Narrows the default siblings; not `getCandidates`. */
407
+ filter?: TargetFilter;
380
408
  /** The color of the lines it marks. Default: the readout layer's. */
381
409
  color?: string;
382
410
  /** Overrides the canvas's reach for these targets, in screen px. */
@@ -395,6 +423,8 @@ declare function alignmentSnapSource(opts?: AlignmentSnapSourceOptions): SnapSou
395
423
  interface SpacingSnapSourceOptions {
396
424
  /** The boxes to measure gaps between, in place of the subject's siblings. */
397
425
  getTargets?: () => readonly Bounds[];
426
+ /** Which nodes offer gaps. Narrows the default siblings; not `getTargets`. */
427
+ filter?: TargetFilter;
398
428
  /** The color of the gaps it marks. Default: the readout layer's. */
399
429
  color?: string;
400
430
  /** Overrides the canvas's reach for these targets, in screen px. */
@@ -466,6 +496,8 @@ interface SpacingHintsOptions {
466
496
  align?: boolean;
467
497
  /** Snap distance in screen px. Default the canvas's reach. */
468
498
  tolerance?: number;
499
+ /** Which nodes the snap sources take as siblings to line up or space against. */
500
+ filter?: TargetFilter;
469
501
  /** The Alt-hover distance readout, configured, or `false` for none. Default
470
502
  * on, with the preset's `color`. */
471
503
  readout?: false | Omit<DistanceReadoutOptions, 'color'>;
@@ -809,4 +841,4 @@ interface Sequencer<TPose> {
809
841
  */
810
842
  declare function sequenceLayout<TPose>(seq: Sequencer<TPose>): LayoutStrategy<TPose>;
811
843
 
812
- export { type AlignmentSnapSourceOptions, type Arrangement, type CellHighlightLayerOpts, type DeriveAlignmentGuidesOptions, type DistanceReadout, type DistanceReadoutContribution, type DistanceReadoutOptions, type FreeformOptions, type GridLayerOpts, type GridSlotsOptions, type GridSnapSourceOptions, type Guide, type GuidesLayerOpts, type PackOptions, type PackOverflow, type PerContainerOptions, type PerContainerStrategy, type QuadtreeCell, type QuadtreeOptions, type QuadtreeStrategy, type RingSlotsOptions, type RowSlotsOptions, type SceneBoundsOptions, type Sequencer, type Slot, type SlotsOptions, type SnapPattern, type SnapPointOptions, type SpacingEdge, type SpacingGap, type SpacingHintsOptions, type SpacingMatchResult, type SpacingSnapSourceOptions, type TileGridOptions, type TileGridOverflow, type UseGridCellHoverOptions, type UseGridCellHoverReturn, type UseGuidesReturn, 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, sequenceLayout, siblingBounds, slots, snapPoint, spacingSnapSource, tileGrid, useGridCellHover, useGuides, worldBoundsOf, worldUnionOf };
844
+ export { type AlignmentSnapSourceOptions, type Arrangement, type CellHighlightLayerOpts, type DeriveAlignmentGuidesOptions, type DistanceReadout, type DistanceReadoutContribution, type DistanceReadoutOptions, type FreeformOptions, type GridLayerOpts, type GridScopes, type GridSlotsOptions, type GridSnapSourceOptions, type Guide, type GuidesLayerOpts, type PackOptions, type PackOverflow, type PerContainerOptions, type PerContainerStrategy, type QuadtreeCell, type QuadtreeOptions, type QuadtreeStrategy, type RingSlotsOptions, type RowSlotsOptions, type SceneBoundsOptions, type Sequencer, type Slot, type SlotsOptions, type SnapPattern, type SnapPointOptions, type SpacingEdge, type SpacingGap, type SpacingHintsOptions, type SpacingMatchResult, type SpacingSnapSourceOptions, type TargetFilter, type TileGridOptions, type TileGridOverflow, type UseGridCellHoverOptions, type UseGridCellHoverReturn, type UseGuidesReturn, 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 };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { useState, useRef, useCallback, useEffect } from 'react';
2
- import { resolveAxes, useLatest, viewportWorldRect, pxExtent, tickedSegment, measuredSegmentCommands, AUTO_POSE_DESCRIPTOR, visualBoundsViaDescriptor, rectFromPlane, scenePoseFrame, unionAABB, createTransformOp, screenToWorld, nodeGates, resolveStrokeWidth } from '@weasel-js/core';
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
3
  import { PATH_M, PATH_L } from '@weasel-js/geom';
4
4
 
5
5
  // src/grid/roundToCell.ts
@@ -159,6 +159,23 @@ function createCellHighlightLayer(opts) {
159
159
  }
160
160
  };
161
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
+ }
162
179
  function useGuides(initial = []) {
163
180
  const [guides, setGuides] = useState(initial);
164
181
  const ref = useLatest(guides);
@@ -208,16 +225,16 @@ function createGuidesLayer(opts) {
208
225
  const sy = (y) => (y - view.y) * view.scale.y;
209
226
  const out = [];
210
227
  for (const g of guides) {
211
- const span = spanned ? g.span : void 0;
228
+ const span2 = spanned ? g.span : void 0;
212
229
  if (g.axis === "x") {
213
230
  const x = sx(g.offset);
214
231
  if (x < -lineWidth || x > dims.width + lineWidth) continue;
215
- 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);
216
233
  out.push({ kind: "path", path, stroke });
217
234
  } else {
218
235
  const y = sy(g.offset);
219
236
  if (y < -lineWidth || y > dims.height + lineWidth) continue;
220
- 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);
221
238
  out.push({ kind: "path", path, stroke });
222
239
  }
223
240
  }
@@ -284,9 +301,9 @@ function gapBetween(a, b, axis) {
284
301
  if (shared === null) return null;
285
302
  return { axis, min: hi(a, axis), max: lo(b, axis), at: (shared[0] + shared[1]) / 2 };
286
303
  }
287
- function hitsStrip(g, b, span) {
304
+ function hitsStrip(g, b, span2) {
288
305
  const c = cross(g.axis);
289
- 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;
290
307
  }
291
308
  function measureGaps(targets, axis) {
292
309
  return measureGapSpans(targets, axis).map((m) => m.gap);
@@ -302,9 +319,9 @@ function measureGapSpans(targets, axis) {
302
319
  }
303
320
  if (next === null) continue;
304
321
  const gap = gapBetween(a, next, axis);
305
- const span = overlap(a, next, cross(axis));
306
- if (targets.some((t) => t !== a && t !== next && hitsStrip(gap, t, span))) continue;
307
- 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 });
308
325
  }
309
326
  return out.sort((p, q) => p.gap.min - q.gap.min || p.gap.at - q.gap.at);
310
327
  }
@@ -448,9 +465,16 @@ function landedShift(q, delta) {
448
465
 
449
466
  // src/sources/grid.ts
450
467
  function gridSnapSource(opts) {
451
- const pitch = resolveAxes(opts.spacing, opts.unitSystem);
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
+ };
452
476
  const o = opts.origin ?? { x: 0, y: 0 };
453
- const lines = (v, axis, plane) => {
477
+ const lines = (v, axis, pitch, plane) => {
454
478
  const s = plane ? plane.scale[axis] : 1;
455
479
  const near = roundToGridOn(v - o[axis] * s, axis, pitch[axis], plane) + o[axis] * s;
456
480
  const step = pitch[axis] * s;
@@ -462,9 +486,10 @@ function gridSnapSource(opts) {
462
486
  reachPx: Infinity,
463
487
  roles: ["origin", "pointer"],
464
488
  *collect(q) {
489
+ const pitch = pitchFor(q);
465
490
  for (const p of q.probes) {
466
491
  for (const axis of ["x", "y"]) {
467
- for (const at of lines(p.at[axis], axis, q.plane ?? null)) yield { probe: p.id, locus: axisLine(axis, at, p.at) };
492
+ for (const at of lines(p.at[axis], axis, pitch, q.plane ?? null)) yield { probe: p.id, locus: axisLine(axis, at, p.at) };
468
493
  }
469
494
  }
470
495
  }
@@ -496,16 +521,23 @@ function stretchSpan(g, min, max) {
496
521
  if (g === null || g.span === void 0) return g;
497
522
  return { ...g, span: { min: Math.min(g.span.min, min), max: Math.max(g.span.max, max) } };
498
523
  }
499
- function targetIds(scene, exclude) {
500
- if (exclude.length > 0) return siblingIds(scene, exclude);
524
+ function visibleRoots(scene) {
501
525
  const gates = nodeGates(scene);
502
526
  return scene.roots.filter((id) => !gates.hidden(id));
503
527
  }
504
- function targetBoxes(q, scene) {
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) {
505
537
  const drawn = q.drawnBox;
506
- return drawn ? targetIds(scene, q.exclude).flatMap((id) => drawn(id) ?? []) : [];
538
+ return drawn ? targetIds(scene, q.exclude, filter).flatMap((id) => drawn(id) ?? []) : [];
507
539
  }
508
- function cachedPerScene(derive) {
540
+ function cachedPerScene(derive, filter) {
509
541
  let key = null;
510
542
  let value = null;
511
543
  return (q) => {
@@ -514,7 +546,7 @@ function cachedPerScene(derive) {
514
546
  const version = scene.getVersion();
515
547
  const exclude = q.exclude.join("\n");
516
548
  if (key === null || key.scene !== scene || key.version !== version || key.exclude !== exclude) {
517
- value = derive(targetBoxes(q, scene));
549
+ value = derive(targetBoxes(q, scene, filter));
518
550
  key = { scene, version, exclude };
519
551
  }
520
552
  return value;
@@ -538,13 +570,13 @@ function* inReach(lines, v, reach) {
538
570
  }
539
571
  var cross2 = (a) => a === "x" ? "y" : "x";
540
572
  function lineMark(g, extent, color) {
541
- const span = stretchSpan(g, extent[0], extent[1])?.span ?? { min: extent[0], max: extent[1] };
542
- const ends = g.axis === "x" ? { from: { x: g.offset, y: span.min }, to: { x: g.offset, y: span.max } } : { from: { x: span.min, y: g.offset }, to: { x: span.max, y: g.offset } };
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 } };
543
575
  return { kind: "line", ...ends, ...color === void 0 ? {} : { color } };
544
576
  }
545
577
  function alignmentSnapSource(opts = {}) {
546
578
  const page = () => (typeof opts.page === "function" ? opts.page() : opts.page) ?? void 0;
547
- const fromScene = cachedPerScene((boxes) => indexLines(deriveAlignmentGuides(boxes)));
579
+ const fromScene = cachedPerScene((boxes) => indexLines(deriveAlignmentGuides(boxes)), opts.filter);
548
580
  const linesFor = (q) => {
549
581
  const pageBox = page();
550
582
  const own = opts.getCandidates ? indexLines(opts.getCandidates()) : fromScene(q);
@@ -591,7 +623,7 @@ function gapMark(g, color) {
591
623
  return { kind: "gap", ...ends, value: g.max - g.min, ...color === void 0 ? {} : { color } };
592
624
  }
593
625
  function spacingSnapSource(opts = {}) {
594
- const fromScene = cachedPerScene(measureAll);
626
+ const fromScene = cachedPerScene(measureAll, opts.filter);
595
627
  return {
596
628
  id: "spacing",
597
629
  ...opts.reachPx === void 0 ? {} : { reachPx: opts.reachPx },
@@ -656,8 +688,8 @@ function measureAxis(from, to, axis, out) {
656
688
  push(min, max, at2);
657
689
  if (!shares) {
658
690
  const edge = before ? lo2(to, axis) : hi2(to, axis);
659
- const span = at2 < lo2(to, c) ? { min: at2, max: lo2(to, c) } : { min: hi2(to, c), max: at2 };
660
- out.leaders.push({ id: `distance:${axis}`, axis, offset: edge, span });
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 });
661
693
  }
662
694
  return;
663
695
  }
@@ -756,7 +788,11 @@ function distanceReadout(opts = {}) {
756
788
  function createSpacingHintsPreset(opts = {}) {
757
789
  const id = opts.id ?? "spacingHints";
758
790
  const color = opts.color === void 0 ? {} : { color: opts.color };
759
- const own = { ...color, ...opts.tolerance === void 0 ? {} : { reachPx: opts.tolerance } };
791
+ const own = {
792
+ ...color,
793
+ ...opts.filter === void 0 ? {} : { filter: opts.filter },
794
+ ...opts.tolerance === void 0 ? {} : { reachPx: opts.tolerance }
795
+ };
760
796
  const snapSources = [
761
797
  ...opts.spacing ?? true ? [spacingSnapSource(own)] : [],
762
798
  ...opts.align ?? false ? [alignmentSnapSource(own)] : []
@@ -1538,6 +1574,6 @@ function ringSlots(opts = {}) {
1538
1574
  };
1539
1575
  }
1540
1576
 
1541
- 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, sequenceLayout, siblingBounds, slots, snapPoint, spacingSnapSource, tileGrid, useGridCellHover, useGuides, worldBoundsOf, worldUnionOf };
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 };
1542
1578
  //# sourceMappingURL=index.js.map
1543
1579
  //# sourceMappingURL=index.js.map