@weasel-js/guides 1.9.3 → 1.9.4

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.
@@ -1,6 +1,6 @@
1
- import { AUTO_ORIGIN_PROJECTION, resolveUnit, fromPlane, toPlane } from '@weasel-js/core';
1
+ import { roundToGridOn } from './chunk-NIEZTECL.js';
2
+ import { AUTO_ORIGIN_PROJECTION, resolveUnit } from '@weasel-js/core';
2
3
 
3
- // src/strategies/grid.ts
4
4
  function gridSnapStrategy(spacing, arg) {
5
5
  const isOpts = typeof arg === "object" && arg !== null && ("origin" in arg || "debug" in arg || "unitSystem" in arg) && !("base" in arg);
6
6
  const optsArg = isOpts ? arg : null;
@@ -11,10 +11,8 @@ function gridSnapStrategy(spacing, arg) {
11
11
  return {
12
12
  snap(pose, ctx) {
13
13
  const o = proj.getOrigin(pose);
14
- const m = ctx?.plane ?? null;
15
- const w = m ? fromPlane(m, o) : o;
16
- const cell = { x: Math.round(w.x / c) * c, y: Math.round(w.y / c) * c };
17
- const { x: sx, y: sy } = m ? toPlane(m, cell) : cell;
14
+ const sx = roundToGridOn(o.x, "x", c, ctx?.plane);
15
+ const sy = roundToGridOn(o.y, "y", c, ctx?.plane);
18
16
  debug?.recordSnapCandidate({ x: sx, y: sy }, true);
19
17
  return proj.translate(pose, sx - o.x, sy - o.y);
20
18
  }
@@ -29,5 +27,5 @@ function pointToGridCell(point, spacing, unitSystem, origin = { x: 0, y: 0 }) {
29
27
  }
30
28
 
31
29
  export { gridSnapStrategy, pointToGridCell };
32
- //# sourceMappingURL=chunk-5BCCIJY3.js.map
33
- //# sourceMappingURL=chunk-5BCCIJY3.js.map
30
+ //# sourceMappingURL=chunk-JETW4ZA3.js.map
31
+ //# sourceMappingURL=chunk-JETW4ZA3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/strategies/grid.ts"],"names":[],"mappings":";;;AAgCO,SAAS,gBAAA,CACd,SACA,GAAA,EACqB;AACrB,EAAA,MAAM,MAAA,GACJ,OAAO,GAAA,KAAQ,QAAA,IACf,GAAA,KAAQ,IAAA,KACP,QAAA,IAAY,GAAA,IAAO,OAAA,IAAW,GAAA,IAAO,YAAA,IAAgB,GAAA,CAAA,IACtD,EAAE,MAAA,IAAU,GAAA,CAAA;AACd,EAAA,MAAM,OAAA,GAAU,SACX,GAAA,GACD,IAAA;AACJ,EAAA,MAAM,UAAA,GAAa,OAAA,GAAU,OAAA,CAAQ,UAAA,GAAc,GAAA;AACnD,EAAA,MAAM,IAAA,GAAgC,OAAA,IAAW,OAAA,CAAQ,MAAA,GACrD,QAAQ,MAAA,GACP,sBAAA;AACL,EAAA,MAAM,KAAA,GAA+B,OAAA,GAAU,OAAA,CAAQ,KAAA,GAAQ,MAAA;AAC/D,EAAA,MAAM,CAAA,GAAI,WAAA,CAAY,OAAA,EAAS,UAAU,CAAA;AACzC,EAAA,OAAO;AAAA,IACL,IAAA,CAAK,MAAM,GAAA,EAAK;AACd,MAAA,MAAM,CAAA,GAAI,IAAA,CAAK,SAAA,CAAU,IAAI,CAAA;AAC7B,MAAA,MAAM,KAAK,aAAA,CAAc,CAAA,CAAE,GAAG,GAAA,EAAK,CAAA,EAAG,KAAK,KAAK,CAAA;AAChD,MAAA,MAAM,KAAK,aAAA,CAAc,CAAA,CAAE,GAAG,GAAA,EAAK,CAAA,EAAG,KAAK,KAAK,CAAA;AAChD,MAAA,KAAA,EAAO,oBAAoB,EAAE,CAAA,EAAG,IAAI,CAAA,EAAG,EAAA,IAAM,IAAI,CAAA;AACjD,MAAA,OAAO,IAAA,CAAK,UAAU,IAAA,EAAM,EAAA,GAAK,EAAE,CAAA,EAAG,EAAA,GAAK,EAAE,CAAC,CAAA;AAAA,IAChD;AAAA,GACF;AACF;AAmBO,SAAS,eAAA,CACd,KAAA,EACA,OAAA,EACA,UAAA,EACA,MAAA,GAAmC,EAAE,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAE,EAClB;AAC9B,EAAA,MAAM,CAAA,GAAI,WAAA,CAAY,OAAA,EAAS,UAAU,CAAA;AACzC,EAAA,OAAO;AAAA,IACL,KAAK,IAAA,CAAK,KAAA,CAAA,CAAO,MAAM,CAAA,GAAI,MAAA,CAAO,KAAK,CAAC,CAAA;AAAA,IACxC,KAAK,IAAA,CAAK,KAAA,CAAA,CAAO,MAAM,CAAA,GAAI,MAAA,CAAO,KAAK,CAAC;AAAA,GAC1C;AACF","file":"chunk-JETW4ZA3.js","sourcesContent":["import {\n AUTO_ORIGIN_PROJECTION,\n resolveUnit,\n type DebugSink,\n type OriginProjection,\n type SnapStrategy,\n type UnitSystem,\n type UnitValue,\n} from '@weasel-js/core';\nimport { roundToGridOn } from './onPlane';\n\n/** Snap-strategy that rounds the pose's origin to the nearest multiple of\n * `spacing` (resolved through `unitSystem`). For non-rect TPose pass an\n * `OriginProjection` so the strategy knows how to read/write the origin. */\nexport function gridSnapStrategy<TPose>(\n spacing: UnitValue,\n unitSystem?: UnitSystem,\n): SnapStrategy<TPose>;\n/** As above, additionally reporting each candidate to a debug sink. */\nexport function gridSnapStrategy<TPose>(\n spacing: UnitValue,\n opts: { unitSystem?: UnitSystem; debug?: DebugSink },\n): SnapStrategy<TPose>;\n/** As above, for a `TPose` that is not a rect: `origin` tells the strategy how\n * to read and write the pose's origin. */\nexport function gridSnapStrategy<TPose>(\n spacing: UnitValue,\n opts: { unitSystem?: UnitSystem; origin: OriginProjection<TPose>; debug?: DebugSink },\n): SnapStrategy<TPose>;\n/** Snap-strategy that rounds the pose's origin to the nearest multiple of\n * `spacing` (resolved through `unitSystem`). For non-rect TPose pass an\n * `OriginProjection` so the strategy knows how to read/write the origin. */\nexport function gridSnapStrategy<TPose>(\n spacing: UnitValue,\n arg?: UnitSystem | { unitSystem?: UnitSystem; origin?: OriginProjection<TPose>; debug?: DebugSink },\n): SnapStrategy<TPose> {\n const isOpts =\n typeof arg === 'object' &&\n arg !== null &&\n ('origin' in arg || 'debug' in arg || 'unitSystem' in arg) &&\n !('base' in arg);\n const optsArg = isOpts\n ? (arg as { unitSystem?: UnitSystem; origin?: OriginProjection<TPose>; debug?: DebugSink })\n : null;\n const unitSystem = optsArg ? optsArg.unitSystem : (arg as UnitSystem | undefined);\n const proj: OriginProjection<TPose> = optsArg && optsArg.origin\n ? optsArg.origin\n : (AUTO_ORIGIN_PROJECTION as unknown as OriginProjection<TPose>);\n const debug: DebugSink | undefined = optsArg ? optsArg.debug : undefined;\n const c = resolveUnit(spacing, unitSystem);\n return {\n snap(pose, ctx) {\n const o = proj.getOrigin(pose);\n const sx = roundToGridOn(o.x, 'x', c, ctx?.plane);\n const sy = roundToGridOn(o.y, 'y', c, ctx?.plane);\n debug?.recordSnapCandidate({ x: sx, y: sy }, true);\n return proj.translate(pose, sx - o.x, sy - o.y);\n },\n };\n}\n\n/**\n * Compute the integer cell `{col, row}` that contains `point`, given a grid\n * `spacing` and optional `origin` and `unitSystem`. Pair with\n * `createCellHighlightLayer` (its `getCell` callback) to draw a snap-target\n * preview that uses the same spacing as `gridSnapStrategy` — so the visual\n * and behavioral grids stay in lockstep:\n *\n * const SPACING = 20;\n * const snap = gridSnapStrategy(SPACING);\n * // visual grid:\n * createGridLayer({ spacing: SPACING, bounds: () => ... });\n * // hover-preview overlay:\n * createCellHighlightLayer({\n * spacing: SPACING,\n * getCell: () => hoverPoint && pointToGridCell(hoverPoint, SPACING),\n * });\n */\nexport function pointToGridCell(\n point: { x: number; y: number },\n spacing: UnitValue,\n unitSystem?: UnitSystem,\n origin: { x: number; y: number } = { x: 0, y: 0 },\n): { col: number; row: number } {\n const s = resolveUnit(spacing, unitSystem);\n return {\n col: Math.floor((point.x - origin.x) / s),\n row: Math.floor((point.y - origin.y) / s),\n };\n}\n"]}
@@ -0,0 +1,45 @@
1
+ import { AUTO_ORIGIN_PROJECTION, screenTolerance } from '@weasel-js/core';
2
+
3
+ // src/strategies/guides.ts
4
+
5
+ // src/strategies/onPlane.ts
6
+ function guideOffsetOn(g, plane) {
7
+ return plane ? g.offset * plane.scale[g.axis] + plane.offset[g.axis] : g.offset;
8
+ }
9
+ function nearestGuideDelta(guides, axis, value, tol, plane) {
10
+ let best = null;
11
+ for (const g of guides) {
12
+ if (g.axis !== axis) continue;
13
+ const d = guideOffsetOn(g, plane) - value;
14
+ if (Math.abs(d) <= tol && (best === null || Math.abs(d) < Math.abs(best))) best = d;
15
+ }
16
+ return best;
17
+ }
18
+ function roundToGridOn(value, axis, spacing, plane) {
19
+ if (!plane) return Math.round(value / spacing) * spacing;
20
+ const camera = (value - plane.offset[axis]) / plane.scale[axis];
21
+ return Math.round(camera / spacing) * spacing * plane.scale[axis] + plane.offset[axis];
22
+ }
23
+
24
+ // src/strategies/guides.ts
25
+ var DEFAULT_GUIDE_TOLERANCE_PX = 6;
26
+ function guideSnapStrategy(getGuides, options = {}) {
27
+ const tolerance = options.tolerance ?? DEFAULT_GUIDE_TOLERANCE_PX;
28
+ const proj = options.origin ?? AUTO_ORIGIN_PROJECTION;
29
+ return {
30
+ snap(pose, ctx) {
31
+ const guides = getGuides();
32
+ if (guides.length === 0) return null;
33
+ const tol = screenTolerance(tolerance, ctx);
34
+ const o = proj.getOrigin(pose);
35
+ const dx = nearestGuideDelta(guides, "x", o.x, tol.x, ctx.plane) ?? 0;
36
+ const dy = nearestGuideDelta(guides, "y", o.y, tol.y, ctx.plane) ?? 0;
37
+ if (dx === 0 && dy === 0) return null;
38
+ return proj.translate(pose, dx, dy);
39
+ }
40
+ };
41
+ }
42
+
43
+ export { DEFAULT_GUIDE_TOLERANCE_PX, guideOffsetOn, guideSnapStrategy, nearestGuideDelta, roundToGridOn };
44
+ //# sourceMappingURL=chunk-NIEZTECL.js.map
45
+ //# sourceMappingURL=chunk-NIEZTECL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/strategies/onPlane.ts","../src/strategies/guides.ts"],"names":[],"mappings":";;;;;AAQO,SAAS,aAAA,CAAc,GAAU,KAAA,EAAsB;AAC5D,EAAA,OAAO,KAAA,GAAQ,CAAA,CAAE,MAAA,GAAS,KAAA,CAAM,KAAA,CAAM,CAAA,CAAE,IAAI,CAAA,GAAI,KAAA,CAAM,MAAA,CAAO,CAAA,CAAE,IAAI,IAAI,CAAA,CAAE,MAAA;AAC3E;AAGO,SAAS,iBAAA,CACd,MAAA,EACA,IAAA,EACA,KAAA,EACA,KACA,KAAA,EACe;AACf,EAAA,IAAI,IAAA,GAAsB,IAAA;AAC1B,EAAA,KAAA,MAAW,KAAK,MAAA,EAAQ;AACtB,IAAA,IAAI,CAAA,CAAE,SAAS,IAAA,EAAM;AACrB,IAAA,MAAM,CAAA,GAAI,aAAA,CAAc,CAAA,EAAG,KAAK,CAAA,GAAI,KAAA;AACpC,IAAA,IAAI,IAAA,CAAK,GAAA,CAAI,CAAC,CAAA,IAAK,QAAQ,IAAA,KAAS,IAAA,IAAQ,IAAA,CAAK,GAAA,CAAI,CAAC,CAAA,GAAI,IAAA,CAAK,GAAA,CAAI,IAAI,IAAI,IAAA,GAAO,CAAA;AAAA,EACpF;AACA,EAAA,OAAO,IAAA;AACT;AAGO,SAAS,aAAA,CAAc,KAAA,EAAe,IAAA,EAAY,OAAA,EAAiB,KAAA,EAAsB;AAC9F,EAAA,IAAI,CAAC,KAAA,EAAO,OAAO,KAAK,KAAA,CAAM,KAAA,GAAQ,OAAO,CAAA,GAAI,OAAA;AACjD,EAAA,MAAM,MAAA,GAAA,CAAU,QAAQ,KAAA,CAAM,MAAA,CAAO,IAAI,CAAA,IAAK,KAAA,CAAM,MAAM,IAAI,CAAA;AAC9D,EAAA,OAAO,IAAA,CAAK,KAAA,CAAM,MAAA,GAAS,OAAO,CAAA,GAAI,OAAA,GAAU,KAAA,CAAM,KAAA,CAAM,IAAI,CAAA,GAAI,KAAA,CAAM,MAAA,CAAO,IAAI,CAAA;AACvF;;;ACxBO,IAAM,0BAAA,GAA6B;AA0CnC,SAAS,iBAAA,CACd,SAAA,EACA,OAAA,GAAmC,EAAC,EACf;AACrB,EAAA,MAAM,SAAA,GAAY,QAAQ,SAAA,IAAa,0BAAA;AACvC,EAAA,MAAM,IAAA,GACJ,QAAQ,MAAA,IAAW,sBAAA;AAErB,EAAA,OAAO;AAAA,IACL,IAAA,CAAK,MAAM,GAAA,EAAK;AACd,MAAA,MAAM,SAAS,SAAA,EAAU;AACzB,MAAA,IAAI,MAAA,CAAO,MAAA,KAAW,CAAA,EAAG,OAAO,IAAA;AAIhC,MAAA,MAAM,GAAA,GAAM,eAAA,CAAgB,SAAA,EAAW,GAAG,CAAA;AAE1C,MAAA,MAAM,CAAA,GAAI,IAAA,CAAK,SAAA,CAAU,IAAI,CAAA;AAC7B,MAAA,MAAM,EAAA,GAAK,iBAAA,CAAkB,MAAA,EAAQ,GAAA,EAAK,CAAA,CAAE,GAAG,GAAA,CAAI,CAAA,EAAG,GAAA,CAAI,KAAK,CAAA,IAAK,CAAA;AACpE,MAAA,MAAM,EAAA,GAAK,iBAAA,CAAkB,MAAA,EAAQ,GAAA,EAAK,CAAA,CAAE,GAAG,GAAA,CAAI,CAAA,EAAG,GAAA,CAAI,KAAK,CAAA,IAAK,CAAA;AACpE,MAAA,IAAI,EAAA,KAAO,CAAA,IAAK,EAAA,KAAO,CAAA,EAAG,OAAO,IAAA;AACjC,MAAA,OAAO,IAAA,CAAK,SAAA,CAAU,IAAA,EAAM,EAAA,EAAI,EAAE,CAAA;AAAA,IACpC;AAAA,GACF;AACF","file":"chunk-NIEZTECL.js","sourcesContent":["import type { GestureContext } from '@weasel-js/core';\nimport type { Guide } from '../guides/types';\n\n/** A gesture's `plane`: how the camera's world maps into the world its poses are in. */\ntype Plane = GestureContext<unknown>['plane'];\ntype Axis = 'x' | 'y';\n\n/** Where a guide the camera draws lies along its own axis in the world a gesture's poses are in. */\nexport function guideOffsetOn(g: Guide, plane: Plane): number {\n return plane ? g.offset * plane.scale[g.axis] + plane.offset[g.axis] : g.offset;\n}\n\n/** The signed distance from `value` to the nearest guide on `axis` within `tol`, or null when none is. */\nexport function nearestGuideDelta(\n guides: readonly Guide[],\n axis: Axis,\n value: number,\n tol: number,\n plane: Plane,\n): number | null {\n let best: number | null = null;\n for (const g of guides) {\n if (g.axis !== axis) continue;\n const d = guideOffsetOn(g, plane) - value;\n if (Math.abs(d) <= tol && (best === null || Math.abs(d) < Math.abs(best))) best = d;\n }\n return best;\n}\n\n/** `value` on `axis` moved to the nearest line of the camera's grid, in the world a gesture's poses are in. */\nexport function roundToGridOn(value: number, axis: Axis, spacing: number, plane: Plane): number {\n if (!plane) return Math.round(value / spacing) * spacing;\n const camera = (value - plane.offset[axis]) / plane.scale[axis];\n return Math.round(camera / spacing) * spacing * plane.scale[axis] + plane.offset[axis];\n}\n","import {\n type SnapStrategy,\n screenTolerance,\n type OriginProjection,\n AUTO_ORIGIN_PROJECTION,\n} from '@weasel-js/core';\nimport type { Guide } from '../guides/types';\nimport { nearestGuideDelta } from './onPlane';\n\n/** Default snap tolerance (screen pixels). */\nexport const DEFAULT_GUIDE_TOLERANCE_PX = 6;\n\n/** Options for `guideSnapStrategy`. */\nexport interface GuideSnapOptions<TPose> {\n /**\n * Tolerance in screen pixels, divided by the gesture view's scale so the\n * trigger zone stays the same on screen at every zoom. Defaults to\n * `DEFAULT_GUIDE_TOLERANCE_PX`.\n */\n tolerance?: number;\n /**\n * Projection used when `TPose` doesn't expose `{x, y}` directly (Path,\n * polygon, etc.) — same contract as `gridSnapStrategy`. Defaults to the\n * rect-pose identity projection.\n */\n origin?: OriginProjection<TPose>;\n}\n\n/**\n * Snap-strategy that snaps the pose's origin to the nearest guide line on\n * each axis when within tolerance. Each axis is considered independently:\n * a vertical guide (`axis: 'x'`) constrains X, a horizontal guide\n * (`axis: 'y'`) constrains Y. When multiple guides are within range on the\n * same axis, the closest wins.\n *\n * Pair with `useGuides` for storage and `createGuidesLayer` for rendering.\n *\n * **Composition:** combining with `gridSnapStrategy` is left to the\n * consumer in v1 — typically by picking one based on a modifier key, or\n * by stacking two move/resize/insert behaviors in priority order.\n */\nexport function guideSnapStrategy<TPose extends { x: number; y: number }>(\n getGuides: () => readonly Guide[],\n options?: Omit<GuideSnapOptions<TPose>, 'origin'>,\n): SnapStrategy<TPose>;\n/** As above, for a `TPose` that is not a rect: `origin` tells the strategy how\n * to read and write the pose's origin. */\nexport function guideSnapStrategy<TPose>(\n getGuides: () => readonly Guide[],\n options: GuideSnapOptions<TPose> & { origin: OriginProjection<TPose> },\n): SnapStrategy<TPose>;\n/** Snap a pose's origin to the nearest guide line within tolerance, per axis. */\nexport function guideSnapStrategy<TPose>(\n getGuides: () => readonly Guide[],\n options: GuideSnapOptions<TPose> = {},\n): SnapStrategy<TPose> {\n const tolerance = options.tolerance ?? DEFAULT_GUIDE_TOLERANCE_PX;\n const proj: OriginProjection<TPose> =\n options.origin ?? (AUTO_ORIGIN_PROJECTION as unknown as OriginProjection<TPose>);\n\n return {\n snap(pose, ctx) {\n const guides = getGuides();\n if (guides.length === 0) return null;\n\n // Per axis: a guide on the x axis is matched by a horizontal distance,\n // so it answers to `scale.x` alone.\n const tol = screenTolerance(tolerance, ctx);\n\n const o = proj.getOrigin(pose);\n const dx = nearestGuideDelta(guides, 'x', o.x, tol.x, ctx.plane) ?? 0;\n const dy = nearestGuideDelta(guides, 'y', o.y, tol.y, ctx.plane) ?? 0;\n if (dx === 0 && dy === 0) return null;\n return proj.translate(pose, dx, dy);\n },\n };\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { RefObject } from 'react';
2
- import { ViewTransform, UnitValue, UnitSystem, Stroke, RenderLayer, FillStyle, SnapStrategy, DebugSink, OriginProjection, ModifierState, Bounds, PoseDescriptor, InsertBehavior, MoveBehavior, BoundsConstraint, LayoutSnap, LayoutStrategy, Vec2 } from '@weasel-js/core';
2
+ import { ViewTransform, UnitValue, UnitSystem, CommandStroke, RenderLayer, CommandFill, SnapStrategy, DebugSink, OriginProjection, ModifierState, Bounds, PlaneMap, PoseDescriptor, InsertBehavior, MoveBehavior, BoundsConstraint, LayoutSnap, LayoutStrategy, Vec2 } from '@weasel-js/core';
3
3
  export { p as pointSnapToGrid } from './pointSnapToGrid-BaVEjV23.js';
4
4
  import { G as Guide, S as SpacingGap } from './types-DRSPiXfk.js';
5
5
 
@@ -97,9 +97,9 @@ interface GridLayerOpts {
97
97
  subdivisions?: number;
98
98
  /** Per-band stroke styles. Each is a FillStyle+width (etc.) — see `Stroke`. */
99
99
  style?: {
100
- line?: Stroke;
101
- accent?: Stroke;
102
- sub?: Stroke;
100
+ line?: CommandStroke;
101
+ accent?: CommandStroke;
102
+ sub?: CommandStroke;
103
103
  };
104
104
  }
105
105
  /** Build a `RenderLayer` that draws a world-space grid with optional accent lines and subdivisions. */
@@ -128,7 +128,7 @@ interface CellHighlightLayerOpts {
128
128
  row: number;
129
129
  } | null;
130
130
  /** FillStyle strategy for the filled cell. Defaults to a soft green. */
131
- fill?: FillStyle;
131
+ fill?: CommandFill;
132
132
  }
133
133
  /** Build a `RenderLayer` that fills a single grid cell — typically a snap-target preview. */
134
134
  declare function createCellHighlightLayer(opts: CellHighlightLayerOpts): RenderLayer<unknown>;
@@ -275,8 +275,14 @@ interface SpacingMatchResult {
275
275
  /** Which candidate lines to derive from a set of poses — edges, centers, or
276
276
  * both, and whether the page box contributes its own. */
277
277
  interface DeriveAlignmentGuidesOptions<TPose = Bounds> {
278
- /** Include the document/page box's edges + center as candidates. */
278
+ /** Include the document/page box's edges + center as candidates. It is in
279
+ * the camera's world, as every line derived is. */
279
280
  page?: Bounds;
281
+ /** The parallax plane a target's pose is stored in, for a sibling on a
282
+ * `parallax` layer (`view.planeOf(layer)`). Its box is carried out to the
283
+ * camera's world, which is the world alignment candidates are in. Omit it,
284
+ * or answer null, for a target already there. */
285
+ planeOf?: (target: TPose, index: number) => PlaneMap | null | undefined;
280
286
  /** Emit left/right (x) and top/bottom (y) edge guides. Default true. */
281
287
  edges?: boolean;
282
288
  /** Emit centerX (x) and centerY (y) guides. Default true. */
@@ -285,9 +291,17 @@ interface DeriveAlignmentGuidesOptions<TPose = Bounds> {
285
291
  * gets, or the two sides disagree. Default `AUTO_POSE_DESCRIPTOR`. */
286
292
  poseDescriptor?: PoseDescriptor<TPose>;
287
293
  }
288
- /** Common options shared by the three alignment behavior factories. */
294
+ /** Common options shared by the three alignment behavior factories.
295
+ *
296
+ * Everything crossing this interface is in the camera's world, whatever
297
+ * layer the gesture edits: the candidate lines and spacing boxes handed in,
298
+ * and the lines and gaps published, which a layer the camera draws shows.
299
+ * A gesture on a `parallax` layer carries them into its plane to match. */
289
300
  interface AlignmentBehaviorBase {
290
- /** Live candidate lines — consumer derives from current siblings + page. */
301
+ /** Live candidate lines — consumer derives from current siblings + page.
302
+ * A sibling on a parallax layer goes through `deriveAlignmentGuides`'
303
+ * `planeOf`, or its lines land where its plane's numbers say, not where
304
+ * it is drawn. */
291
305
  getCandidates: () => readonly Guide[];
292
306
  /** Publish the currently-matched line(s). Called every onMove; cleared
293
307
  * (`[]`) on a miss and on onEnd. */
@@ -311,7 +325,9 @@ interface AlignmentBehaviorBase {
311
325
  * edges and the center. Overlapping offsets collapse to one candidate, whose
312
326
  * `span` covers every box that produced it.
313
327
  * Poses go through the same descriptor `alignMoveBehavior` matches with, so
314
- * a rotated sibling advertises its ink edges rather than its stored box. */
328
+ * a rotated sibling advertises its ink edges rather than its stored box.
329
+ * The lines are in the camera's world: a sibling on a parallax layer needs
330
+ * `planeOf` to say which plane its pose is in. */
315
331
  declare function deriveAlignmentGuides<TPose = Bounds>(targets: readonly TPose[], opts?: DeriveAlignmentGuidesOptions<TPose>): Guide[];
316
332
 
317
333
  /** Move/insert test all three features per axis. */
@@ -378,10 +394,10 @@ interface AlignMoveArgs<TPose> extends AlignmentBehaviorBase {
378
394
  * transform uniformly to every dragged id, so the selection shifts together
379
395
  * and stays rigid. Single-select is the degenerate one-box union. Publishes
380
396
  * the matched line(s); clears on miss, end and cancel. */
381
- declare function alignMoveBehavior<TPose>(args: AlignMoveArgs<TPose>): MoveBehavior<TPose>;
397
+ declare function alignMoveBehavior<TPose>(given: AlignMoveArgs<TPose>): MoveBehavior<TPose>;
382
398
  /** Insert behavior: snap the live `current` point to candidates (treating it
383
399
  * as a zero-size box). Publishes the matched line(s). */
384
- declare function alignInsertBehavior<TPose>(args: AlignmentBehaviorBase): InsertBehavior<TPose>;
400
+ declare function alignInsertBehavior<TPose>(given: AlignmentBehaviorBase): InsertBehavior<TPose>;
385
401
  /** Resize constraint: snap the corner(s) the drag moves, as drawn, to
386
402
  * candidates; the pinned corner stays fixed. On an unrotated pose that is
387
403
  * the moving edge(s). On a rotated one the node's own axes set how the
@@ -389,7 +405,7 @@ declare function alignInsertBehavior<TPose>(args: AlignmentBehaviorBase): Insert
389
405
  * slides its edge until the nearer of its two corners meets a line.
390
406
  * `poseDescriptor` reads the origin pose's box and rotation, and must match
391
407
  * the resize action's. Publishes the matched line(s). */
392
- declare function alignResizeBehavior<TPose extends Bounds>(args: AlignMoveArgs<TPose>): BoundsConstraint<TPose>;
408
+ declare function alignResizeBehavior<TPose extends Bounds>(given: AlignMoveArgs<TPose>): BoundsConstraint<TPose>;
393
409
 
394
410
  /** Default snap tolerance (screen pixels). */
395
411
  declare const DEFAULT_GUIDE_TOLERANCE_PX = 6;
package/dist/index.js CHANGED
@@ -1,9 +1,10 @@
1
- import { pointToGridCell } from './chunk-5BCCIJY3.js';
2
- export { gridSnapStrategy, pointToGridCell } from './chunk-5BCCIJY3.js';
1
+ import { pointToGridCell } from './chunk-JETW4ZA3.js';
2
+ export { gridSnapStrategy, pointToGridCell } from './chunk-JETW4ZA3.js';
3
3
  export { pointSnapToGrid } from './chunk-B25KFI3I.js';
4
- export { DEFAULT_GUIDE_TOLERANCE_PX, guideSnapStrategy } from './chunk-GEDAPTOM.js';
4
+ import { guideOffsetOn } from './chunk-NIEZTECL.js';
5
+ export { DEFAULT_GUIDE_TOLERANCE_PX, guideSnapStrategy } from './chunk-NIEZTECL.js';
5
6
  import { useState, useRef, useCallback, useEffect } from 'react';
6
- import { useLatest, resolveUnit, pxExtent, textCommandFromRuns, AUTO_POSE_DESCRIPTOR, visualBoundsViaDescriptor, translatePoseViaDescriptor, unionBounds, fixedCornerOf, createTransformOp, resolveStrokeWidth, screenToWorld, screenTolerance } from '@weasel-js/core';
7
+ import { useLatest, resolveUnit, pxExtent, textCommandFromRuns, AUTO_POSE_DESCRIPTOR, visualBoundsViaDescriptor, rectFromPlane, translatePoseViaDescriptor, unionBounds, fixedCornerOf, createTransformOp, resolveStrokeWidth, screenToWorld, rectToPlane, screenTolerance } from '@weasel-js/core';
7
8
  import { rectPath, PATH_M, PATH_L } from '@weasel-js/geom';
8
9
 
9
10
  // src/grid/roundToCell.ts
@@ -88,11 +89,11 @@ function createGridLayer(opts) {
88
89
  coords: new Float32Array([sx, sy, ex, ey]),
89
90
  fillRule: "nonzero"
90
91
  };
91
- const across = sx === ex ? px.x : px.y;
92
+ const across2 = sx === ex ? px.x : px.y;
92
93
  children.push({
93
94
  kind: "path",
94
95
  path,
95
- stroke: { ...s, width: resolveStrokeWidth(s.width ?? 1, 1) * across }
96
+ stroke: { ...s, width: resolveStrokeWidth(s.width ?? 1, 1) * across2 }
96
97
  });
97
98
  };
98
99
  if (subdivisions && subdivisions > 1) {
@@ -291,7 +292,11 @@ function deriveAlignmentGuides(targets, opts = {}) {
291
292
  add("y", b.y + b.height / 2, b);
292
293
  }
293
294
  };
294
- for (const t of targets) emit(visualBoundsViaDescriptor(t, d));
295
+ targets.forEach((t, i) => {
296
+ const box = visualBoundsViaDescriptor(t, d);
297
+ const plane = opts.planeOf?.(t, i);
298
+ emit(plane ? rectFromPlane(plane, box) : box);
299
+ });
295
300
  if (opts.page) emit(opts.page);
296
301
  return [...seenX.values(), ...seenY.values()];
297
302
  }
@@ -460,6 +465,39 @@ function applyEdges(b, dx, dy, moving) {
460
465
  } else if (moving.y === "max") height += dy;
461
466
  return { x, y, width, height };
462
467
  }
468
+ var across = (axis) => axis === "x" ? "y" : "x";
469
+ var into = (m, axis, v) => v * m.scale[axis] + m.offset[axis];
470
+ var outOf = (m, axis, v) => (v - m.offset[axis]) / m.scale[axis];
471
+ function range(r, map) {
472
+ const a = map(r.min);
473
+ const b = map(r.max);
474
+ return { ...r, min: Math.min(a, b), max: Math.max(a, b) };
475
+ }
476
+ function guideToPlane(g, m) {
477
+ const span = g.span && range(g.span, (v) => into(m, across(g.axis), v));
478
+ return { ...g, offset: guideOffsetOn(g, m), ...span ? { span } : {} };
479
+ }
480
+ function guideFromPlane(g, m) {
481
+ const span = g.span && range(g.span, (v) => outOf(m, across(g.axis), v));
482
+ return { ...g, offset: outOf(m, g.axis, g.offset), ...span ? { span } : {} };
483
+ }
484
+ function gapFromPlane(g, m) {
485
+ return { ...range(g, (v) => outOf(m, g.axis, v)), at: outOf(m, across(g.axis), g.at) };
486
+ }
487
+ function alignmentOnPlane(args, plane) {
488
+ if (!plane) return args;
489
+ const spacing = args.getSpacingTargets;
490
+ const setGaps = args.setActiveGaps;
491
+ return {
492
+ ...args,
493
+ getCandidates: () => args.getCandidates().map((g) => guideToPlane(g, plane)),
494
+ setActiveGuides: (guides) => args.setActiveGuides(guides.map((g) => guideFromPlane(g, plane))),
495
+ ...spacing ? { getSpacingTargets: () => spacing().map((b) => rectToPlane(plane, b)) } : {},
496
+ ...setGaps ? { setActiveGaps: (gaps) => setGaps(gaps.map((g) => gapFromPlane(g, plane))) } : {}
497
+ };
498
+ }
499
+
500
+ // src/guides/alignment/behaviors.ts
463
501
  var activeList = (m) => [m.activeX, m.activeY].filter((g) => g !== null);
464
502
  var EXACT = { x: 1e-6, y: 1e-6 };
465
503
  function resolve(b, args, tol, anchors, edges) {
@@ -491,10 +529,11 @@ function publish(args, r) {
491
529
  function worldTol(base, ctx) {
492
530
  return screenTolerance(base.tolerance ?? 6, ctx);
493
531
  }
494
- function alignMoveBehavior(args) {
495
- const d = args.poseDescriptor ?? AUTO_POSE_DESCRIPTOR;
532
+ function alignMoveBehavior(given) {
533
+ const d = given.poseDescriptor ?? AUTO_POSE_DESCRIPTOR;
496
534
  return {
497
535
  onMove(ctx, transform) {
536
+ const args = alignmentOnPlane(given, ctx.plane);
498
537
  if (args.bypassKey && ctx.modifiers[args.bypassKey]) {
499
538
  publish(args, null);
500
539
  return;
@@ -517,17 +556,18 @@ function alignMoveBehavior(args) {
517
556
  return { transform: { kind: "translate", dx: transform.dx + r.dx, dy: transform.dy + r.dy } };
518
557
  },
519
558
  onEnd() {
520
- publish(args, null);
559
+ publish(given, null);
521
560
  },
522
561
  onCancel() {
523
- publish(args, null);
562
+ publish(given, null);
524
563
  }
525
564
  };
526
565
  }
527
- function alignInsertBehavior(args) {
566
+ function alignInsertBehavior(given) {
528
567
  const pointAnchors = { x: ["min"], y: ["min"] };
529
568
  return {
530
569
  onMove(ctx, { current }) {
570
+ const args = alignmentOnPlane(given, ctx.plane);
531
571
  if (args.bypassKey && ctx.modifiers[args.bypassKey]) {
532
572
  args.setActiveGuides([]);
533
573
  return;
@@ -542,17 +582,18 @@ function alignInsertBehavior(args) {
542
582
  return { current: { x: current.x + m.dx, y: current.y + m.dy } };
543
583
  },
544
584
  onEnd() {
545
- args.setActiveGuides([]);
585
+ given.setActiveGuides([]);
546
586
  },
547
587
  onCancel() {
548
- args.setActiveGuides([]);
588
+ given.setActiveGuides([]);
549
589
  }
550
590
  };
551
591
  }
552
- function alignResizeBehavior(args) {
553
- const d = args.poseDescriptor ?? AUTO_POSE_DESCRIPTOR;
592
+ function alignResizeBehavior(given) {
593
+ const d = given.poseDescriptor ?? AUTO_POSE_DESCRIPTOR;
554
594
  return {
555
595
  onMove(ctx, { pose, anchor }) {
596
+ const args = alignmentOnPlane(given, ctx.plane);
556
597
  if (args.bypassKey && ctx.modifiers[args.bypassKey]) {
557
598
  publish(args, null);
558
599
  return;
@@ -579,10 +620,10 @@ function alignResizeBehavior(args) {
579
620
  return { pose: { ...pose, x, y, width, height } };
580
621
  },
581
622
  onEnd() {
582
- publish(args, null);
623
+ publish(given, null);
583
624
  },
584
625
  onCancel() {
585
- publish(args, null);
626
+ publish(given, null);
586
627
  }
587
628
  };
588
629
  }
@@ -627,8 +668,8 @@ function matchRotatedCorners(pose, anchor, originBox, rotation, candidates, tol)
627
668
  const dir = along === "x" ? toWorld(1, 0) : toWorld(0, 1);
628
669
  const edge = movingEdge(along)[0];
629
670
  let best = null;
630
- for (const across2 of movingEdge(along === "x" ? "y" : "x")) {
631
- const c = along === "x" ? drawn(edge, across2) : drawn(across2, edge);
671
+ for (const across3 of movingEdge(along === "x" ? "y" : "x")) {
672
+ const c = along === "x" ? drawn(edge, across3) : drawn(across3, edge);
632
673
  for (const g of candidates) {
633
674
  const reach = g.axis === "x" ? dir.x : dir.y;
634
675
  if (Math.abs(reach) < 1e-9) continue;
@@ -638,8 +679,8 @@ function matchRotatedCorners(pose, anchor, originBox, rotation, candidates, tol)
638
679
  }
639
680
  }
640
681
  if (best === null) return null;
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)];
682
+ const across2 = best.guide.axis === "x" ? best.at.y + best.t * dir.y : best.at.x + best.t * dir.x;
683
+ const guides = [stretchSpan(best.guide, across2, across2)];
643
684
  return along === "x" ? { lx: best.t, ly: 0, guides, gaps: [] } : { lx: 0, ly: best.t, guides, gaps: [] };
644
685
  }
645
686
 
@@ -798,7 +839,7 @@ function tileGrid(opts) {
798
839
  if (fromIdx === null || fromIdx === idx) return null;
799
840
  return { occupant: occupant.id, from: occupant.pose, newPose: cellToPose(grid.rectAt(fromIdx), occupant.pose) };
800
841
  }
801
- function pack(from, into, children) {
842
+ function pack(from, into2, children) {
802
843
  const ordered = children.map((c) => ({ c, cell: from.indexOf(c.pose) })).sort((a, b) => {
803
844
  if (a.cell !== b.cell) {
804
845
  if (a.cell === null) return 1;
@@ -809,7 +850,7 @@ function tileGrid(opts) {
809
850
  });
810
851
  const out = /* @__PURE__ */ new Map();
811
852
  for (let i = 0; i < ordered.length; i++) {
812
- out.set(ordered[i].c.id, cellToPose(into.rectAt(i), ordered[i].c.pose));
853
+ out.set(ordered[i].c.id, cellToPose(into2.rectAt(i), ordered[i].c.pose));
813
854
  }
814
855
  return out;
815
856
  }