@weasel-js/guides 1.10.1 → 1.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,13 +1,13 @@
1
1
  # @weasel-js/guides
2
2
 
3
- Placement aids for weasel: a visible grid and snapping to it, guide lines and
4
- alignment to sibling edges, and the layout strategies a container arranges its
5
- children with.
6
-
7
- Everything here plugs into a seam `@weasel-js/core` already has. A snap
8
- strategy is a `SnapStrategy`, the select tool's `snap` option takes one, and
9
- core's `snap()` turns one into a move behavior. The move, resize and insert
10
- behaviors go in the actions' `behaviors` lists. The grid and guide overlays are
3
+ Placement aids for weasel: a visible grid, ruler guides, the snap sources that
4
+ snap to them and to sibling edges and gaps, and the layout strategies a
5
+ container arranges its children with.
6
+
7
+ Everything here plugs into a seam `@weasel-js/core` already has. A snap source
8
+ goes in `<SceneCanvas snap={{ sources }}>` or a preset's `snapSources`, and
9
+ competes with every other source under core's one ranking rule; see
10
+ [Snapping](https://github.com/orochi235/weasel/blob/main/docs/snapping.md). The grid and guide overlays are
11
11
  `RenderLayer`s for a canvas layer slot, and a layout strategy implements core's
12
12
  `LayoutStrategy`.
13
13
 
@@ -26,34 +26,42 @@ npm install @weasel-js/guides
26
26
  A grid you can see and snap to:
27
27
 
28
28
  ```tsx
29
- import { SceneCanvas, type RectPose } from '@weasel-js/core';
30
- import { createGridLayer, gridSnapStrategy } from '@weasel-js/guides';
29
+ import { SceneCanvas } from '@weasel-js/core';
30
+ import { createGridLayer, gridSnapSource } from '@weasel-js/guides';
31
31
 
32
32
  const grid = createGridLayer({ spacing: 20, bounds: () => ({ x: 0, y: 0, width: 800, height: 600 }) });
33
+ const snap = { sources: [gridSnapSource({ spacing: 20 })] };
33
34
 
34
35
  <SceneCanvas
35
36
  /* … */
36
- selectTool={{ snap: gridSnapStrategy<RectPose>(20) }}
37
+ snap={snap}
37
38
  layers={{ grid: { layer: grid } }}
38
39
  />;
39
40
  ```
40
41
 
41
- `snapToGrid` and `snapToGuides` exist for move, resize and insert, and each
42
- has the shape its action expects, so they are imported from a per-action
43
- subpath:
44
-
45
- ```ts
46
- import { snapToGrid } from '@weasel-js/guides/move';
47
- import { snapToGrid, pointSnapToGrid } from '@weasel-js/guides/resize';
48
- import { snapToGrid } from '@weasel-js/guides/insert';
49
- ```
42
+ The grid source snaps every snapping gesture — move, resize, insert, the pen —
43
+ and, being a fallback, gives way on any axis a real target in reach claims.
50
44
 
51
45
  | Area | Exports |
52
46
  |---|---|
53
- | Grid | `createGridLayer`, `createCellHighlightLayer`, `useGridCellHover`, `gridSnapStrategy`, `pointToGridCell`, `pointSnapToGrid`, `roundToCell` |
54
- | Guides | `useGuides`, `createGuidesLayer`, `guideSnapStrategy`, `deriveAlignmentGuides`, `matchAlignment`, `alignMoveBehavior`, `alignResizeBehavior`, `alignInsertBehavior` |
47
+ | Grid | `createGridLayer`, `createCellHighlightLayer`, `useGridCellHover`, `pointToGridCell`, `roundToCell` |
48
+ | Guides | `useGuides`, `createGuidesLayer`, `deriveAlignmentGuides`, `matchSpacing`, `measureGaps`, `siblingBounds`, `worldBoundsOf`, `worldUnionOf` |
49
+ | Snap sources | `gridSnapSource`, `guideSnapSource`, `alignmentSnapSource`, `spacingSnapSource`, for `<SceneCanvas snap={{ sources }}>` or a preset's `snapSources` |
50
+ | Spacing hints | `createSpacingHintsPreset` (below), `distanceReadout`, `measureDistances` |
55
51
  | Layout | `freeform`, `tileGrid`, `snapPoint`, `slots` (with the `rowSlots`, `gridSlots`, and `ringSlots` arrangements), `pack`, `quadtree`, `perContainer` for a strategy that keeps state, and the drop-target pickers `none`, `nearest`, `nearestWithin`, `containedThenNearest`, `cellAt` |
56
52
 
53
+ Spacing hints are one `<SceneCanvas features>` preset: its snap sources move
54
+ the selection to the spacing between its siblings (and, with `align`, to their
55
+ edges and centers), the canvas's snap readout marks the equal gaps, and holding
56
+ Alt over a shape measures its distance from the selection. It lives in
57
+ `src/spacingHints/`.
58
+
59
+ ```tsx
60
+ const spacingHints = useMemo(() => createSpacingHintsPreset({ color: '#e0397f' }), []);
61
+
62
+ <SceneCanvas /* … */ features={['draw', spacingHints]} />;
63
+ ```
64
+
57
65
  ## License
58
66
 
59
67
  MIT
package/dist/index.d.ts CHANGED
@@ -1,7 +1,5 @@
1
1
  import { RefObject } from 'react';
2
- import { ViewTransform, PlaneValue, PlaneUnits, CommandStroke, RenderLayer, CommandFill, SnapStrategy, DebugSink, OriginProjection, ModifierState, Bounds, PlaneMap, PoseDescriptor, InsertBehavior, MoveBehavior, BoundsConstraint, LayoutSnap, LayoutStrategy, LayoutContainer, Vec2, ContainerBounds, LayoutChild } from '@weasel-js/core';
3
- export { p as pointSnapToGrid } from './pointSnapToGrid-BaVEjV23.js';
4
- import { G as Guide, S as SpacingGap } from './types-DRSPiXfk.js';
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';
5
3
  import { Rect } from '@weasel-js/geom';
6
4
 
7
5
  /** Round `value` to the nearest multiple of `cellSize`. Returns 0 when the result would be -0. */
@@ -79,8 +77,13 @@ interface GridLayerOpts {
79
77
  spacing: PlaneValue;
80
78
  /** Optional unit system for resolving tagged `spacing` values. */
81
79
  unitSystem?: PlaneUnits;
82
- /** Bounds of the area to cover, in world units. */
83
- bounds: () => {
80
+ /**
81
+ * The area to cover, in world units, read every paint with that paint's
82
+ * view and canvas size. Lines start at its own `x`/`y`. Omitted, the grid
83
+ * covers whatever the canvas shows, with its lines on world multiples of
84
+ * `spacing` so they stay put as the view pans.
85
+ */
86
+ bounds?: (view: View, dims: Dims) => {
84
87
  x: number;
85
88
  y: number;
86
89
  width: number;
@@ -128,28 +131,10 @@ interface CellHighlightLayerOpts {
128
131
  /** Build a `RenderLayer` that fills a single grid cell — typically a snap-target preview. */
129
132
  declare function createCellHighlightLayer(opts: CellHighlightLayerOpts): RenderLayer<unknown>;
130
133
 
131
- /** Snap-strategy that rounds the pose's origin to the nearest multiple of
132
- * `spacing` (resolved through `unitSystem`). Either may be per axis —
133
- * `{ x, y }` — for a grid whose axes have different pitches or units. For
134
- * non-rect TPose pass an `OriginProjection` so the strategy knows how to
135
- * read/write the origin. */
136
- declare function gridSnapStrategy<TPose>(spacing: PlaneValue, unitSystem?: PlaneUnits): SnapStrategy<TPose>;
137
- /** As above, additionally reporting each candidate to a debug sink. */
138
- declare function gridSnapStrategy<TPose>(spacing: PlaneValue, opts: {
139
- unitSystem?: PlaneUnits;
140
- debug?: DebugSink;
141
- }): SnapStrategy<TPose>;
142
- /** As above, for a `TPose` that is not a rect: `origin` tells the strategy how
143
- * to read and write the pose's origin. */
144
- declare function gridSnapStrategy<TPose>(spacing: PlaneValue, opts: {
145
- unitSystem?: PlaneUnits;
146
- origin: OriginProjection<TPose>;
147
- debug?: DebugSink;
148
- }): SnapStrategy<TPose>;
149
134
  /**
150
135
  * The integer cell `{col, row}` that contains `point`. Pair with
151
136
  * `createCellHighlightLayer` (its `getCell` callback) to draw a snap-target
152
- * preview that uses the same spacing as `gridSnapStrategy`.
137
+ * preview that uses the same spacing as `gridSnapSource`.
153
138
  */
154
139
  declare function pointToGridCell(point: {
155
140
  x: number;
@@ -162,12 +147,67 @@ declare function pointToGridCell(point: {
162
147
  row: number;
163
148
  };
164
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
+
168
+ /**
169
+ * Guide — a horizontal or vertical world-space line used as a snap target
170
+ * and an optional rendered overlay.
171
+ *
172
+ * `axis === 'x'` means a vertical line at world `x = offset` (snaps the X
173
+ * coordinate); `axis === 'y'` means a horizontal line at world `y = offset`
174
+ * (snaps the Y coordinate). This matches "the X axis snaps along X."
175
+ */
176
+ interface Guide {
177
+ /** Stable, caller-supplied id. Used by removeGuide and as a render key. */
178
+ id: string;
179
+ /** Which world axis this guide constrains. */
180
+ axis: 'x' | 'y';
181
+ /** World-space offset along the constrained axis. */
182
+ offset: number;
183
+ /** The stretch of the line worth drawing, in world units on the other
184
+ * axis: an `'x'` guide's y range. Derived alignment guides carry the
185
+ * extent of the boxes that produced them. Absent, the line has no extent
186
+ * of its own and `createGuidesLayer` draws it across the canvas. */
187
+ span?: {
188
+ min: number;
189
+ max: number;
190
+ };
191
+ }
192
+ /**
193
+ * SpacingGap — one measured gap between two boxes, drawn as an equal-spacing
194
+ * marker. `axis === 'x'` is a horizontal gap from world `x = min` to
195
+ * `x = max`, drawn at `y = at`; `axis === 'y'` is the vertical counterpart.
196
+ */
197
+ interface SpacingGap {
198
+ axis: 'x' | 'y';
199
+ min: number;
200
+ max: number;
201
+ /** World position on the other axis the marker is drawn at. */
202
+ at: number;
203
+ }
204
+
165
205
  /** Return shape for `useGuides`. */
166
206
  interface UseGuidesReturn {
167
207
  /** Live list (state). Re-renders when guides change. */
168
208
  guides: Guide[];
169
209
  /** Stable getter; reads the latest list without forcing a re-render.
170
- * Pass to `guideSnapStrategy` and `createGuidesLayer`. */
210
+ * Pass to `guideSnapSource` and `createGuidesLayer`. */
171
211
  getGuides: () => Guide[];
172
212
  /** Add a guide. If a guide with the same id already exists it is replaced. */
173
213
  addGuide: (guide: Guide) => void;
@@ -178,7 +218,7 @@ interface UseGuidesReturn {
178
218
  }
179
219
  /**
180
220
  * In-memory guide storage. Holds a list of horizontal/vertical world-space
181
- * lines that callers can pass to `guideSnapStrategy` (snap behavior) and
221
+ * lines that callers can pass to `guideSnapSource` (snap source) and
182
222
  * `createGuidesLayer` (render layer).
183
223
  *
184
224
  * v1 is intentionally minimal: no undo integration, no persistence, no
@@ -195,7 +235,7 @@ declare function useGuides(initial?: Guide[]): UseGuidesReturn;
195
235
  * (`SpacingGap`) as ticked segments labeled with their size.
196
236
  *
197
237
  * Guides are not scene objects; they live outside the scene and are managed
198
- * via `useGuides`. Pair with `guideSnapStrategy` (snap behavior) to keep
238
+ * via `useGuides`. Pair with `guideSnapSource` (snap source) to keep
199
239
  * the visual guide and the snap target in lockstep.
200
240
  */
201
241
 
@@ -203,8 +243,8 @@ declare function useGuides(initial?: Guide[]): UseGuidesReturn;
203
243
  interface GuidesLayerOpts {
204
244
  /** Stable getter for the live guide list — typically `useGuides().getGuides`. */
205
245
  getGuides: () => readonly Guide[];
206
- /** Stable getter for the equal-spacing gaps to draw — typically what an
207
- * alignment behavior's `setActiveGaps` last published. */
246
+ /** Stable getter for the equal-spacing gaps to draw, such as
247
+ * `matchSpacing`'s. */
208
248
  getGaps?: () => readonly SpacingGap[];
209
249
  /** `'span'` (default) draws a guide that carries a `span` as a segment over
210
250
  * it; `'full'` draws every guide across the whole canvas. */
@@ -234,18 +274,6 @@ interface GuidesLayerOpts {
234
274
  * space, projected from world coordinates via the active view. */
235
275
  declare function createGuidesLayer(opts: GuidesLayerOpts): RenderLayer<unknown>;
236
276
 
237
- /** Which feature of a box to test against candidates, per axis.
238
- * 'min' = left/top edge, 'center' = centerline, 'max' = right/bottom edge. */
239
- type AlignAnchor = 'min' | 'center' | 'max';
240
- /** The correction an alignment match asks for: the translation that lands the
241
- * dragged bounds on the matched guides, and the guides themselves so they can
242
- * be drawn. */
243
- interface AlignMatchResult {
244
- dx: number;
245
- dy: number;
246
- activeX: Guide | null;
247
- activeY: Guide | null;
248
- }
249
277
  /** Which edges of a box move on one axis during a spacing match: `'both'`
250
278
  * for a translate, `'min'`/`'max'` for a resize dragging that edge. */
251
279
  type SpacingEdge = 'both' | 'min' | 'max';
@@ -272,74 +300,22 @@ interface DeriveAlignmentGuidesOptions<TPose = Bounds> {
272
300
  edges?: boolean;
273
301
  /** Emit centerX (x) and centerY (y) guides. Default true. */
274
302
  centers?: boolean;
275
- /** How to read each target. Pass the same descriptor `alignMoveBehavior`
276
- * gets, or the two sides disagree. Default `AUTO_POSE_DESCRIPTOR`. */
303
+ /** How to read each target. Default `AUTO_POSE_DESCRIPTOR`. */
277
304
  poseDescriptor?: PoseDescriptor<TPose>;
278
305
  }
279
- /** Common options shared by the three alignment behavior factories.
280
- *
281
- * Everything crossing this interface is in the camera's world, whatever
282
- * layer the gesture edits: the candidate lines and spacing boxes handed in,
283
- * and the lines and gaps published, which a layer the camera draws shows.
284
- * A gesture on a `parallax` layer carries them into its plane to match. */
285
- interface AlignmentBehaviorBase {
286
- /** Live candidate lines — consumer derives from current siblings + page.
287
- * A sibling on a parallax layer goes through `deriveAlignmentGuides`'
288
- * `planeOf`, or its lines land where its plane's numbers say, not where
289
- * it is drawn. */
290
- getCandidates: () => readonly Guide[];
291
- /** Publish the currently-matched line(s). Called every onMove; cleared
292
- * (`[]`) on a miss and on onEnd. */
293
- setActiveGuides: (guides: readonly Guide[]) => void;
294
- /** Tolerance in screen px, read through the gesture's view. Default 6. */
295
- tolerance?: number;
296
- /** Modifier key that bypasses snapping while held. */
297
- bypassKey?: keyof ModifierState;
298
- /** Boxes to measure gaps between for equal-spacing snaps (`matchSpacing`):
299
- * the siblings' visual bounds, without the dragged ones. Omit to snap to
300
- * alignment lines only. On each axis the nearer of the two snaps wins.
301
- * Move and unrotated resize use it; insert does not. */
302
- getSpacingTargets?: () => readonly Bounds[];
303
- /** Publish the equal-gap markers. Called with the guides; cleared (`[]`) on
304
- * a miss and on onEnd. */
305
- setActiveGaps?: (gaps: readonly SpacingGap[]) => void;
306
- }
307
306
 
308
307
  /** Derive candidate alignment lines from a set of sibling poses plus an
309
308
  * optional page box. Each box contributes up to 3 guides per axis: the two
310
309
  * edges and the center. Overlapping offsets collapse to one candidate, whose
311
310
  * `span` covers every box that produced it.
312
- * Poses go through the same descriptor `alignMoveBehavior` matches with, so
313
- * a rotated sibling advertises its ink edges rather than its stored box.
311
+ * Poses go through `opts.poseDescriptor`, so a rotated sibling advertises
312
+ * its ink edges rather than its stored box.
313
+ * The moving selection is measured with its stroke (`drawnBoundsOf`), so
314
+ * pass each sibling's `drawnAABBOf` box as its target for the two to agree.
314
315
  * The lines are in the camera's world: a sibling on a parallax layer needs
315
316
  * `planeOf` to say which plane its pose is in. */
316
317
  declare function deriveAlignmentGuides<TPose = Bounds>(targets: readonly TPose[], opts?: DeriveAlignmentGuidesOptions<TPose>): Guide[];
317
318
 
318
- /** Move/insert test all three features per axis. */
319
- declare const MOVE_ANCHORS: {
320
- x: readonly AlignAnchor[];
321
- y: readonly AlignAnchor[];
322
- };
323
- /**
324
- * Match a moving box's selected edge/center features against candidate guide
325
- * lines. Returns the per-axis snap delta and the matched candidate line(s).
326
- * The two axes resolve independently; on each axis the closest in-tolerance
327
- * (feature, candidate) pair wins. A matched guide that carries a `span` comes
328
- * back with it stretched to cover the snapped box, so the drawn segment runs
329
- * from the aligned siblings to the box being dragged.
330
- *
331
- * `worldTolerance` is per axis because a screen-pixel tolerance is not one
332
- * world distance under non-uniform zoom. Pass the same number twice for a
333
- * world-space tolerance.
334
- */
335
- declare function matchAlignment(bounds: Bounds, candidates: readonly Guide[], worldTolerance: {
336
- x: number;
337
- y: number;
338
- }, anchors: {
339
- x: readonly AlignAnchor[];
340
- y: readonly AlignAnchor[];
341
- }): AlignMatchResult;
342
-
343
319
  /**
344
320
  * The visible gaps between neighboring boxes along `axis`: for `'x'`, each box
345
321
  * and the nearest box to its right that shares some of its vertical range, as
@@ -368,64 +344,173 @@ declare function matchSpacing(bounds: Bounds, targets: readonly Bounds[], worldT
368
344
  y: SpacingEdge | null;
369
345
  }): SpacingMatchResult;
370
346
 
371
- /** Options for move and resize — adds the pose descriptor for non-rect poses. */
372
- interface AlignMoveArgs<TPose> extends AlignmentBehaviorBase {
373
- poseDescriptor?: PoseDescriptor<TPose>;
374
- }
375
- /** Move behavior: snap the dragged selection's union box (edges + center) to
376
- * candidates, shaping the proposed translate. The gesture applies the
377
- * transform uniformly to every dragged id, so the selection shifts together
378
- * and stays rigid. Single-select is the degenerate one-box union. Publishes
379
- * the matched line(s); clears on miss, end and cancel. */
380
- declare function alignMoveBehavior<TPose>(given: AlignMoveArgs<TPose>): MoveBehavior<TPose>;
381
- /** Insert behavior: snap the live `current` point to candidates (treating it
382
- * as a zero-size box). Publishes the matched line(s). */
383
- declare function alignInsertBehavior<TPose>(given: AlignmentBehaviorBase): InsertBehavior<TPose>;
384
- /** Resize constraint: snap the corner(s) the drag moves, as drawn, to
385
- * candidates; the pinned corner stays fixed. On an unrotated pose that is
386
- * the moving edge(s). On a rotated one the node's own axes set how the
387
- * corner can travel: a corner handle snaps each world axis, an edge handle
388
- * slides its edge until the nearer of its two corners meets a line.
389
- * `poseDescriptor` reads the origin pose's box and rotation, and must match
390
- * the resize action's. Publishes the matched line(s). */
391
- declare function alignResizeBehavior<TPose extends Bounds>(given: AlignMoveArgs<TPose>): BoundsConstraint<TPose>;
392
-
393
- /** Default snap tolerance (screen pixels). */
394
- declare const DEFAULT_GUIDE_TOLERANCE_PX = 6;
395
- /** Options for `guideSnapStrategy`. */
396
- interface GuideSnapOptions<TPose> {
397
- /**
398
- * Tolerance in screen pixels, divided by the gesture view's scale so the
399
- * trigger zone stays the same on screen at every zoom. Defaults to
400
- * `DEFAULT_GUIDE_TOLERANCE_PX`.
401
- */
402
- tolerance?: number;
403
- /**
404
- * Projection used when `TPose` doesn't expose `{x, y}` directly (Path,
405
- * polygon, etc.) — same contract as `gridSnapStrategy`. Defaults to the
406
- * rect-pose identity projection.
407
- */
408
- origin?: OriginProjection<TPose>;
347
+ /** Options shared by the scene readers below. */
348
+ interface SceneBoundsOptions {
349
+ /** How to read a pose's box. Pass the canvas's, so these agree with what it
350
+ * draws. Default `AUTO_POSE_DESCRIPTOR`. */
351
+ poseDescriptor?: PoseDescriptor<unknown>;
409
352
  }
353
+ /** The world box each id is drawn in, rotation included, skipping ids the
354
+ * scene no longer holds. Read through the scene's composition. */
355
+ declare function worldBoundsOf(scene: Scene<unknown, string, unknown>, ids: Iterable<NodeId>, opts?: SceneBoundsOptions): Bounds[];
356
+ /** The union of `ids`' world boxes, or null when none is in the scene. */
357
+ declare function worldUnionOf(scene: Scene<unknown, string, unknown>, ids: Iterable<NodeId>, opts?: SceneBoundsOptions): Bounds | null;
410
358
  /**
411
- * Snap-strategy that snaps the pose's origin to the nearest guide line on
412
- * each axis when within tolerance. Each axis is considered independently:
413
- * a vertical guide (`axis: 'x'`) constrains X, a horizontal guide
414
- * (`axis: 'y'`) constrains Y. When multiple guides are within range on the
415
- * same axis, the closest wins.
416
- *
417
- * Pair with `useGuides` for storage and `createGuidesLayer` for rendering.
418
- * Combining with `gridSnapStrategy` is left to the consumer.
359
+ * The world boxes of `ids`' siblings ({@link siblingIds}). What an
360
+ * equal-spacing snap measures gaps between while `ids` move.
419
361
  */
420
- declare function guideSnapStrategy<TPose extends {
362
+ declare function siblingBounds(scene: Scene<unknown, string, unknown>, ids: readonly NodeId[], opts?: SceneBoundsOptions): Bounds[];
363
+
364
+ type Point = {
421
365
  x: number;
422
366
  y: number;
423
- }>(getGuides: () => readonly Guide[], options?: Omit<GuideSnapOptions<TPose>, 'origin'>): SnapStrategy<TPose>;
424
- /** As above, for a `TPose` that is not a rect: `origin` tells the strategy how
425
- * to read and write the pose's origin. */
426
- declare function guideSnapStrategy<TPose>(getGuides: () => readonly Guide[], options: GuideSnapOptions<TPose> & {
427
- origin: OriginProjection<TPose>;
428
- }): SnapStrategy<TPose>;
367
+ };
368
+ /** Options for `gridSnapSource`. */
369
+ interface GridSnapSourceOptions {
370
+ /** The grid's pitch, or one per axis, in `unitSystem`'s units. */
371
+ spacing: PlaneValue;
372
+ /** A point the lattice passes through. Default `(0, 0)`. */
373
+ origin?: Point;
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;
380
+ }
381
+ /**
382
+ * A grid as snap targets: for each origin or pointer probe, the grid line on
383
+ * either side of it on each axis. Its reach is unlimited and it is a
384
+ * fallback, so it holds every axis no target claims and never displaces one.
385
+ * Its lines are the camera's, carried into the plane a gesture runs in.
386
+ */
387
+ declare function gridSnapSource(opts: GridSnapSourceOptions): SnapSource;
388
+
389
+ /**
390
+ * Ruler guides as snap targets: each guide within reach of an origin or
391
+ * pointer probe is a line it may land on. Reads `getGuides` every frame, so
392
+ * a guide added mid-drag counts at once.
393
+ */
394
+ declare function guideSnapSource(getGuides: () => readonly Guide[]): SnapSource;
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
+
399
+ /** Options for `alignmentSnapSource`. */
400
+ interface AlignmentSnapSourceOptions {
401
+ /** A page box whose edges and center are lines too, in the camera's world. */
402
+ page?: Bounds | (() => Bounds | null | undefined);
403
+ /** The lines to align to, in place of the edges and centers of the
404
+ * subject's siblings (of the scene's top-level nodes, for a point gesture). */
405
+ getCandidates?: () => readonly Guide[];
406
+ /** Which nodes offer lines. Narrows the default siblings; not `getCandidates`. */
407
+ filter?: TargetFilter;
408
+ /** The color of the lines it marks. Default: the readout layer's. */
409
+ color?: string;
410
+ /** Overrides the canvas's reach for these targets, in screen px. */
411
+ reachPx?: number;
412
+ }
413
+ /**
414
+ * The edges and centers of the subject's siblings, and optionally a page, as
415
+ * snap targets: each is a line a box or pointer probe may land on. A move's
416
+ * left, center and right (top, center, bottom) all try every line, as Figma's
417
+ * smart guides do. When the result meets a line, its mark runs from the
418
+ * subject's box to the farthest sibling on that line.
419
+ */
420
+ declare function alignmentSnapSource(opts?: AlignmentSnapSourceOptions): SnapSource;
421
+
422
+ /** Options for `spacingSnapSource`. */
423
+ interface SpacingSnapSourceOptions {
424
+ /** The boxes to measure gaps between, in place of the subject's siblings. */
425
+ getTargets?: () => readonly Bounds[];
426
+ /** Which nodes offer gaps. Narrows the default siblings; not `getTargets`. */
427
+ filter?: TargetFilter;
428
+ /** The color of the gaps it marks. Default: the readout layer's. */
429
+ color?: string;
430
+ /** Overrides the canvas's reach for these targets, in screen px. */
431
+ reachPx?: number;
432
+ }
433
+ /**
434
+ * Equal spacing as snap targets, Figma's: on each axis, the positions of the
435
+ * subject's box that make its gap to a neighbor equal one already between
436
+ * two siblings, or center it between its two neighbors. A resize moves only
437
+ * the edges it drags. When the result meets one, it marks every gap of that
438
+ * size, the box's own included.
439
+ */
440
+ declare function spacingSnapSource(opts?: SpacingSnapSourceOptions): SnapSource;
441
+
442
+ /** The distances `measureDistances` found, ready for `createGuidesLayer`:
443
+ * `gaps` as its `getGaps`, `leaders` as its `getGuides`. */
444
+ interface DistanceReadout {
445
+ /** One measured distance per gap, labeled with its length when drawn. */
446
+ gaps: SpacingGap[];
447
+ /** Where a gap runs past the target, the line from the gap's end along the
448
+ * target's edge to the target, so the number reads as reaching it. */
449
+ leaders: Guide[];
450
+ }
451
+ /**
452
+ * The distances between two boxes, on each axis where they separate: for
453
+ * `from` left of `to`, the gap from `from`'s right edge to `to`'s left. A gap
454
+ * is drawn through the range the boxes share on the other axis, or through
455
+ * `from`'s center when they share none, with a leader to `to`. When one box
456
+ * holds the other on an axis and they overlap on the other, the inner box's
457
+ * distances to the outer's two edges on that axis instead. An axis where the
458
+ * boxes partly overlap, or meet flush, has no distance.
459
+ */
460
+ declare function measureDistances(from: Bounds, to: Bounds): DistanceReadout;
461
+
462
+ /** Options for `distanceReadout`. */
463
+ interface DistanceReadoutOptions {
464
+ /** The key held to show the readout. Default `'alt'`. */
465
+ modifier?: 'alt' | 'ctrl' | 'meta' | 'shift';
466
+ /** The color of the marks it writes. Default: the readout layer's. */
467
+ color?: string;
468
+ /** Contribution id, and the prefix of its action's. Default `'distanceReadout'`. */
469
+ id?: string;
470
+ }
471
+ /** What `distanceReadout` returns: a surface contribution, plus a read of
472
+ * what it is showing. */
473
+ interface DistanceReadoutContribution extends SurfaceContribution {
474
+ /** The distances on screen now; empty while hidden. */
475
+ read(): DistanceReadout;
476
+ }
477
+ /**
478
+ * Distance readouts on hover: while `modifier` is held over a shape, the
479
+ * distances from the selection's box to that shape's, on each axis where they
480
+ * separate, measured by `measureDistances` and drawn as labeled gaps. Pass it
481
+ * in `<SceneCanvas ambient>`. It runs on `hover` bindings, so it claims no
482
+ * input, and it reads the canvas's `selection`, `scene`, `nodeAtPoint`,
483
+ * `poseDescriptor`, and `snapReadout` deps. A press hides it.
484
+ */
485
+ declare function distanceReadout(opts?: DistanceReadoutOptions): DistanceReadoutContribution;
486
+
487
+ /** Options for `createSpacingHintsPreset`. */
488
+ interface SpacingHintsOptions {
489
+ /** Preset id. Default `'spacingHints'`. */
490
+ id?: string;
491
+ /** Snap a move so its gap to a neighbor equals one already between its
492
+ * siblings, and mark the equal gaps. Default true. */
493
+ spacing?: boolean;
494
+ /** Snap to the siblings' edges and centers too, and mark the line met.
495
+ * Default false. */
496
+ align?: boolean;
497
+ /** Snap distance in screen px. Default the canvas's reach. */
498
+ tolerance?: number;
499
+ /** Which nodes the snap sources take as siblings to line up or space against. */
500
+ filter?: TargetFilter;
501
+ /** The Alt-hover distance readout, configured, or `false` for none. Default
502
+ * on, with the preset's `color`. */
503
+ readout?: false | Omit<DistanceReadoutOptions, 'color'>;
504
+ /** The color of the marks and the readout. */
505
+ color?: string;
506
+ }
507
+ /**
508
+ * Spacing hints as one `<SceneCanvas features>` preset: snap sources for
509
+ * equal spacing between siblings and, if asked, alignment to their edges
510
+ * and centers, whose marks the canvas's snap readout draws; and holding Alt
511
+ * over a shape measures its distance from the selection.
512
+ */
513
+ declare function createSpacingHintsPreset(opts?: SpacingHintsOptions): FeaturePreset;
429
514
 
430
515
  /** Snap policy that never picks a target — every drop is rejected, which is
431
516
  * what a free-form container wants. */
@@ -756,4 +841,4 @@ interface Sequencer<TPose> {
756
841
  */
757
842
  declare function sequenceLayout<TPose>(seq: Sequencer<TPose>): LayoutStrategy<TPose>;
758
843
 
759
- export { type AlignAnchor, type AlignMatchResult, type AlignMoveArgs, type AlignmentBehaviorBase, type Arrangement, type CellHighlightLayerOpts, DEFAULT_GUIDE_TOLERANCE_PX, type DeriveAlignmentGuidesOptions, type FreeformOptions, type GridLayerOpts, type GridSlotsOptions, Guide, type GuideSnapOptions, type GuidesLayerOpts, MOVE_ANCHORS, type PackOptions, type PackOverflow, type PerContainerOptions, type PerContainerStrategy, type QuadtreeCell, type QuadtreeOptions, type QuadtreeStrategy, type RingSlotsOptions, type RowSlotsOptions, type Sequencer, type Slot, type SlotsOptions, type SnapPattern, type SnapPointOptions, type SpacingEdge, SpacingGap, type SpacingMatchResult, type TileGridOptions, type TileGridOverflow, type UseGridCellHoverOptions, type UseGridCellHoverReturn, type UseGuidesReturn, alignInsertBehavior, alignMoveBehavior, alignResizeBehavior, cellAt, containedThenNearest, createCellHighlightLayer, createGridLayer, createGuidesLayer, deriveAlignmentGuides, freeform, gridSlots, gridSnapStrategy, guideSnapStrategy, matchAlignment, matchSpacing, measureGaps, nearest, nearestWithin, none, pack, perContainer, pointToGridCell, quadtree, ringSlots, roundToCell, rowSlots, sequenceLayout, slots, snapPoint, tileGrid, useGridCellHover, useGuides };
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 };