@weasel-js/core 1.7.1 → 1.7.3

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.
Files changed (41) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +7 -0
  3. package/dist/{autoPoseDescriptor-BT_vp9FO.d.ts → autoPoseDescriptor-vCdt55xm.d.ts} +1 -1
  4. package/dist/{builtins-BSUfH8nU.d.ts → builtins-C-gRyVRx.d.ts} +7 -3
  5. package/dist/canvas/CursorCoordsHud.js +56 -29
  6. package/dist/canvas/CursorCoordsHud.js.map +1 -1
  7. package/dist/canvas/hud.module.js +3 -3
  8. package/dist/canvas/hud.module.js.map +1 -1
  9. package/dist/clipboard.d.ts +1 -1
  10. package/dist/clone.d.ts +2 -2
  11. package/dist/core/geometry/poseDescriptor.js +10 -15
  12. package/dist/core/geometry/poseDescriptor.js.map +1 -1
  13. package/dist/{cornerHandles-CDcZOV8h.d.ts → cornerHandles-BCrDp_mH.d.ts} +1 -1
  14. package/dist/features/paths/poseDescriptor.js +6 -11
  15. package/dist/features/paths/poseDescriptor.js.map +1 -1
  16. package/dist/index.d.ts +22 -17
  17. package/dist/interactions/actions/defaults/clone.js +64 -53
  18. package/dist/interactions/actions/defaults/clone.js.map +1 -1
  19. package/dist/interactions/actions/defaults/move.js +426 -322
  20. package/dist/interactions/actions/defaults/move.js.map +1 -1
  21. package/dist/interactions/actions/defaults/resize.js +75 -72
  22. package/dist/interactions/actions/defaults/resize.js.map +1 -1
  23. package/dist/interactions/actions/defaults/rotate.js +88 -85
  24. package/dist/interactions/actions/defaults/rotate.js.map +1 -1
  25. package/dist/interactions/actions/planeInput.js +82 -69
  26. package/dist/interactions/actions/planeInput.js.map +1 -1
  27. package/dist/interactions/actions/resize/autoPoseDescriptor.js +0 -1
  28. package/dist/interactions/actions/resize/autoPoseDescriptor.js.map +1 -1
  29. package/dist/{math-CUMxmZzn.d.ts → math-Drrw4ewZ.d.ts} +2 -2
  30. package/dist/math.d.ts +3 -3
  31. package/dist/move.d.ts +4 -4
  32. package/dist/{originProjection-BSn4xiEy.d.ts → originProjection-DEklpDc_.d.ts} +1 -1
  33. package/dist/{poseDescriptor-CjSXMf8X.d.ts → poseDescriptor-D-OH-tu5.d.ts} +0 -4
  34. package/dist/renderer.d.ts +4 -4
  35. package/dist/resize.d.ts +5 -5
  36. package/dist/style.css +2 -0
  37. package/dist/{types-CA54-mAZ.d.ts → types-A37hYkeR.d.ts} +4 -2
  38. package/dist/{types-DSjd6W3n.d.ts → types-B_9g6hMM.d.ts} +1 -1
  39. package/dist/version.js +1 -1
  40. package/package.json +16 -14
  41. package/dist/index.css +0 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,52 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.7.3
4
+
5
+ ### Patch Changes
6
+
7
+ - @weasel-js/cursor@1.7.3
8
+ - @weasel-js/font@1.7.3
9
+ - @weasel-js/geom@1.7.3
10
+ - @weasel-js/gestures@1.7.3
11
+ - @weasel-js/history@1.7.3
12
+ - @weasel-js/modes@1.7.3
13
+ - @weasel-js/paint@1.7.3
14
+ - @weasel-js/quantity@1.7.3
15
+ - @weasel-js/react@1.7.3
16
+ - @weasel-js/registry@1.7.3
17
+ - @weasel-js/routing@1.7.3
18
+ - @weasel-js/text@1.7.3
19
+
20
+ ## 1.7.2
21
+
22
+ ### Patch Changes
23
+
24
+ - 06ee1e6: Core's stylesheet is now published as `@weasel-js/core/style.css`. Through 1.7.1 it shipped in the tarball as `dist/index.css` with no export and no import, so an installed core drew its debug HUDs with class names and no rules behind them — unpositioned, unbackgrounded text. Import it once if you turn on `cursorCoordsHud`, `pickHud` or `modalityHud`. labkit's `styles.css` now includes it.
25
+
26
+ `cursorCoordsHud` lays its readings out as a label column and a value column, set in the numeric face, instead of padding a monospace line with spaces. World coordinates write negatives with U+2212. The face is built into core's stylesheet, so installing core still brings no theme dependency. The pick and modality HUDs list ids and stay monospace.
27
+ - 88e298e: Remove `PoseDescriptor.intersectsRect`. Nothing in the kit has read it since marquee and lasso began testing a node's drawn outline, so the built-in descriptors (`RECT_POSE_DESCRIPTOR`, `ROTATED_POSE_DESCRIPTOR`, `pathPoseDescriptor`, the auto descriptor, and kernel3d's) no longer implement it. This is a breaking removal of a public field for anyone implementing a custom `PoseDescriptor`: an object literal typed as `PoseDescriptor` that still declares `intersectsRect` now fails the excess-property check. Delete the method; nothing called it.
28
+ - 0756a82: A child dragged out of its layout container and released where no container
29
+ takes it now leaves that container, landing under the plain container beneath
30
+ the drop or at the top level, at the position it was dropped. It used to stay
31
+ the container's child and draw clipped to it, out of sight. A container whose
32
+ `releaseDrop` returns ops still decides the release, and a drop back inside the
33
+ container keeps the child where it is.
34
+ - b18ef4a: Editing a selection that spans parallax planes lands every node where it was drawn. Move, resize, rotate and clone used to measure the whole selection in one plane — the handle's target's, else the first selected node's — so a node on a plane that scales differently drifted off the pointer. Each node is now carried into that plane as the gesture starts and back into its own as it lands. A move that reparents a node onto another plane's layer, or drops it into a layout container there, carries its pose across, so it lands where it was drawn.
35
+
36
+ `ViewApi` gains an optional `planeOf(layer)`, which `inPlane` sets: how the world an invocation edits in maps into the world a layer's nodes are stored in. `inPlane` now wraps an action whenever the scene has any parallax layer, not only when the edited layer has one.
37
+ - @weasel-js/cursor@1.7.2
38
+ - @weasel-js/font@1.7.2
39
+ - @weasel-js/geom@1.7.2
40
+ - @weasel-js/gestures@1.7.2
41
+ - @weasel-js/history@1.7.2
42
+ - @weasel-js/modes@1.7.2
43
+ - @weasel-js/paint@1.7.2
44
+ - @weasel-js/quantity@1.7.2
45
+ - @weasel-js/react@1.7.2
46
+ - @weasel-js/registry@1.7.2
47
+ - @weasel-js/routing@1.7.2
48
+ - @weasel-js/text@1.7.2
49
+
3
50
  ## 1.7.1
4
51
 
5
52
  ### Patch Changes
package/README.md CHANGED
@@ -24,6 +24,13 @@ npm install @weasel-js/core react
24
24
 
25
25
  `react` is a peer dependency (>=18).
26
26
 
27
+ The canvas's debug overlays (`cursorCoordsHud`, `pickHud`, `modalityHud`) are
28
+ styled by core's stylesheet. Import it once if you turn any of them on:
29
+
30
+ ```ts
31
+ import '@weasel-js/core/style.css';
32
+ ```
33
+
27
34
  ## How it fits together
28
35
 
29
36
  `useScene` holds the scene: a tree of leaf and container nodes, each carrying your data and a pose. Every change to it is an **op**, which is what makes it undoable. `<SceneCanvas>` renders a scene and routes input to it.
@@ -1,5 +1,5 @@
1
1
  import { Path } from '@weasel-js/geom';
2
- import { P as PoseDescriptor } from './poseDescriptor-CjSXMf8X.js';
2
+ import { P as PoseDescriptor } from './poseDescriptor-D-OH-tu5.js';
3
3
 
4
4
  /** True for Path-shaped poses (`{kind: 'polygon' | 'rect'}`). Useful for
5
5
  * callers that need to fork between `pathPoseDescriptor` and
@@ -6,12 +6,12 @@ import { Path, EasingSpec, SpringPresetName, PolygonPath, Rect, Mat3 } from '@we
6
6
  import { ReactNode } from 'react';
7
7
  import * as _weasel_js_history from '@weasel-js/history';
8
8
  import { Op, History, SerializedHistory } from '@weasel-js/history';
9
- import { L as LayoutStrategy, a as ParallaxOpts, C as ContainerBounds, b as SceneAdapter, P as PlaneMap, R as RegionPickView, c as LayoutDropTargetMode, d as ReflowTransition, I as InsertAdapter } from './types-CA54-mAZ.js';
9
+ import { L as LayoutStrategy, a as ParallaxOpts, C as ContainerBounds, b as SceneAdapter, P as PlaneMap, R as RegionPickView, c as LayoutDropTargetMode, d as ReflowTransition, I as InsertAdapter } from './types-A37hYkeR.js';
10
10
  import { FontStyle } from '@weasel-js/font';
11
11
  import { ResolvedRun, TextStyle, TextVerticalAlign, StyledRun } from '@weasel-js/text';
12
- import { P as PoseDescriptor } from './poseDescriptor-CjSXMf8X.js';
12
+ import { P as PoseDescriptor } from './poseDescriptor-D-OH-tu5.js';
13
13
  import { ActiveToolContextValue, ToolsApi as ToolsApi$1, UseToolsOptions as UseToolsOptions$1 } from '@weasel-js/routing/react';
14
- import { B as BoundsConstraint, P as PointSnapBehavior } from './types-DSjd6W3n.js';
14
+ import { B as BoundsConstraint, P as PointSnapBehavior } from './types-B_9g6hMM.js';
15
15
  import { CursorSpec } from '@weasel-js/cursor';
16
16
  import { ClaimableGesture } from '@weasel-js/gestures';
17
17
 
@@ -3593,6 +3593,10 @@ interface ViewApi {
3593
3593
  * when that is a parallax plane — set by `inPlane` on the view it hands a
3594
3594
  * wrapped action. Absent or null: the camera's own world. */
3595
3595
  plane?(): PlaneMap | null;
3596
+ /** How the world the invocation edits in maps into the world `layer`'s
3597
+ * nodes are stored in, read now — set by `inPlane` alongside `plane`.
3598
+ * Absent or null: the same world. */
3599
+ planeOf?(layer: string): PlaneMap | null;
3596
3600
  /** Optional recenter callback. When wired, `viewportZoomAction`'s `reset`
3597
3601
  * branch (Cmd-0) calls this instead of resetting to identity — letting
3598
3602
  * consumers re-fit the page (or other reference bounds) into the workspace.
@@ -3,52 +3,53 @@ import { clientToWorld as t } from "../core/viewport/clientToWorld.js";
3
3
  import n from "./hud.module.js";
4
4
  import { useHostAnchor as r } from "./useHostAnchor.js";
5
5
  import { useEffect as i, useRef as a, useState as o } from "react";
6
- import { jsxs as s } from "react/jsx-runtime";
6
+ import { jsx as s, jsxs as c } from "react/jsx-runtime";
7
+ import { decimal as l, qty as u } from "@weasel-js/quantity";
7
8
  //#region src/canvas/CursorCoordsHud.tsx
8
- function c({ canvasRef: c, anchorRef: l, viewRef: u, offset: d }) {
9
- let [f, p] = o({
9
+ function d({ canvasRef: l, anchorRef: u, viewRef: d, offset: p }) {
10
+ let [m, h] = o({
10
11
  client: {
11
12
  x: 0,
12
13
  y: 0
13
14
  },
14
15
  world: null,
15
16
  inCanvas: !1
16
- }), [m, h] = o(0), { ref: g, style: _ } = r(() => l?.current ?? c.current?.parentElement ?? c.current, {
17
+ }), [g, _] = o(0), { ref: v, style: y } = r(() => u?.current ?? l.current?.parentElement ?? l.current, {
17
18
  align: {
18
19
  x: "end",
19
20
  y: "start"
20
21
  },
21
22
  offset: {
22
- x: d?.right ?? 8,
23
- y: d?.top ?? 8
23
+ x: p?.right ?? 8,
24
+ y: p?.top ?? 8
24
25
  }
25
- }), v = a(0), y = a(0), b = e(() => {
26
- v.current++;
27
- let e = performance.now(), t = e - y.current;
28
- t >= 1e3 && (h(Math.round(v.current * 1e3 / t)), v.current = 0, y.current = e), b.request();
26
+ }), b = a(0), x = a(0), S = e(() => {
27
+ b.current++;
28
+ let e = performance.now(), t = e - x.current;
29
+ t >= 1e3 && (_(Math.round(b.current * 1e3 / t)), b.current = 0, x.current = e), S.request();
29
30
  }, { onResume: () => {
30
- v.current = 0, y.current = performance.now();
31
+ b.current = 0, x.current = performance.now();
31
32
  } });
32
- return i(() => (y.current = performance.now(), b.request(), () => b.cancel()), [b]), i(() => {
33
+ return i(() => (x.current = performance.now(), S.request(), () => S.cancel()), [S]), i(() => {
33
34
  let e = (e) => {
34
- let n = c.current, r = u.current, i = {
35
+ let n = l.current, r = d.current, i = {
35
36
  x: e.clientX,
36
37
  y: e.clientY
37
38
  };
38
39
  if (!n || !r) {
39
- p({
40
+ h({
40
41
  client: i,
41
42
  world: null,
42
43
  inCanvas: !1
43
44
  });
44
45
  return;
45
46
  }
46
- let a = n.getBoundingClientRect(), o = e.clientX >= a.left && e.clientX <= a.right && e.clientY >= a.top && e.clientY <= a.bottom, [s, l] = t(e.clientX, e.clientY, a, r);
47
- p({
47
+ let a = n.getBoundingClientRect(), o = e.clientX >= a.left && e.clientX <= a.right && e.clientY >= a.top && e.clientY <= a.bottom, [s, c] = t(e.clientX, e.clientY, a, r);
48
+ h({
48
49
  client: i,
49
50
  world: {
50
51
  x: s,
51
- y: l
52
+ y: c
52
53
  },
53
54
  inCanvas: o
54
55
  });
@@ -56,20 +57,46 @@ function c({ canvasRef: c, anchorRef: l, viewRef: u, offset: d }) {
56
57
  return document.addEventListener("pointermove", e), () => {
57
58
  document.removeEventListener("pointermove", e);
58
59
  };
59
- }, [c, u]), _ ? /* @__PURE__ */ s("div", {
60
- ref: g,
61
- className: `${n.hud} ${n.pre}`,
62
- style: _,
63
- children: [
64
- `fps ${m.toString().padStart(3)}`,
65
- "\n",
66
- `client (${f.client.x.toFixed(0)}, ${f.client.y.toFixed(0)})`,
67
- "\n",
68
- f.world ? `world (${f.world.x.toFixed(1)}, ${f.world.y.toFixed(1)})${f.inCanvas ? "" : " (off-canvas)"}` : "world — (no canvas/view yet)"
69
- ]
60
+ }, [l, d]), y ? /* @__PURE__ */ s("div", {
61
+ ref: v,
62
+ className: n.hud,
63
+ style: y,
64
+ children: /* @__PURE__ */ c("dl", {
65
+ className: n.readings,
66
+ children: [
67
+ /* @__PURE__ */ s("dt", {
68
+ className: n.muted,
69
+ children: "fps"
70
+ }),
71
+ /* @__PURE__ */ s("dd", { children: g }),
72
+ /* @__PURE__ */ s("dt", {
73
+ className: n.muted,
74
+ children: "client"
75
+ }),
76
+ /* @__PURE__ */ s("dd", { children: f(m.client, 0) }),
77
+ /* @__PURE__ */ s("dt", {
78
+ className: n.muted,
79
+ children: "world"
80
+ }),
81
+ m.world ? /* @__PURE__ */ c("dd", { children: [f(m.world, 1), m.inCanvas ? null : /* @__PURE__ */ s("span", {
82
+ className: n.absent,
83
+ children: " off-canvas"
84
+ })] }) : /* @__PURE__ */ s("dd", {
85
+ className: n.absent,
86
+ children: "— no canvas or view yet"
87
+ })
88
+ ]
89
+ })
70
90
  }) : null;
71
91
  }
92
+ function f({ x: e, y: t }, n) {
93
+ let r = l({
94
+ places: n,
95
+ grouping: !1
96
+ });
97
+ return `(${u(e, r).text}, ${u(t, r).text})`;
98
+ }
72
99
  //#endregion
73
- export { c as CursorCoordsHud };
100
+ export { d as CursorCoordsHud };
74
101
 
75
102
  //# sourceMappingURL=CursorCoordsHud.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"CursorCoordsHud.js","names":["s","hud_module_default","CursorCoordsHud","state","setState","useState","fps","setFps","anchorStyle","useHostAnchor","anchorRef","canvasRef","offset","framesRef","useRef","lastFlushRef","fpsLoop","useVisibleRaf","now","dt","useEffect","onMove","canvas","view","viewRef","client","rect","inCanvas","wx","wy","clientToWorld"],"sources":["../../src/canvas/CursorCoordsHud.tsx"],"sourcesContent":["/**\n * Fixed-position dev HUD that shows the cursor's current viewport\n * (client) coords next to the equivalent canvas (world) coords. Mounts\n * top-left of the viewport; listens to `document` pointermove so it\n * reads the cursor even when it leaves the canvas.\n *\n * Mount inside a `<SceneCanvas>` subtree (or anywhere with access to a\n * canvas + view ref) for quick coord-reconciliation during gesture\n * debugging.\n */\nimport { useEffect, useRef, useState } from 'react';\nimport { useVisibleRaf } from '../scheduling/useVisibleRaf';\nimport s from './hud.module.css';\nimport { useHostAnchor } from './useHostAnchor';\nimport type { View } from 'core/viewport/view';\nimport { clientToWorld } from 'core/viewport/clientToWorld';\n\n/** Props for `<CursorCoordsHud>`. */\nexport interface CursorCoordsHudProps {\n canvasRef: React.RefObject<HTMLElement | null>;\n /** Element the HUD pins its corner to. Defaults to the canvas's parent — the\n * wrapper a bare `<canvas>` sits in. A detached canvas passes its own input\n * box, whose parent is the shared surface every pane sits in. */\n anchorRef?: React.RefObject<HTMLElement | null>;\n viewRef: React.RefObject<View>;\n /** Inset from the canvas's top-right corner, in px. Default 8 on both axes. */\n offset?: { top?: number; right?: number };\n}\n\ninterface HudState {\n client: { x: number; y: number };\n world: { x: number; y: number } | null;\n inCanvas: boolean;\n}\n\n/** Debug overlay showing the pointer's client and world coordinates and the\n * current frame rate, pinned to the canvas's top-right corner. */\nexport function CursorCoordsHud({ canvasRef, anchorRef, viewRef, offset }: CursorCoordsHudProps) {\n const [state, setState] = useState<HudState>({\n client: { x: 0, y: 0 }, world: null, inCanvas: false,\n });\n const [fps, setFps] = useState<number>(0);\n const { ref, style: anchorStyle } = useHostAnchor(\n () => anchorRef?.current ?? canvasRef.current?.parentElement ?? canvasRef.current,\n {\n align: { x: 'end', y: 'start' },\n offset: { x: offset?.right ?? 8, y: offset?.top ?? 8 },\n },\n );\n\n // FPS counter: tally frames per rAF tick; flush once a second.\n const framesRef = useRef(0);\n const lastFlushRef = useRef(0);\n const fpsLoop = useVisibleRaf(\n () => {\n framesRef.current++;\n const now = performance.now();\n const dt = now - lastFlushRef.current;\n if (dt >= 1000) {\n setFps(Math.round((framesRef.current * 1000) / dt));\n framesRef.current = 0;\n lastFlushRef.current = now;\n }\n fpsLoop.request();\n },\n // Frames not drawn while hidden are not frames that were slow to draw:\n // without this the readout would flush a near-zero rate on resume.\n {\n onResume: () => {\n framesRef.current = 0;\n lastFlushRef.current = performance.now();\n },\n },\n );\n\n useEffect(() => {\n lastFlushRef.current = performance.now();\n fpsLoop.request();\n return () => fpsLoop.cancel();\n }, [fpsLoop]);\n\n useEffect(() => {\n const onMove = (e: PointerEvent) => {\n const canvas = canvasRef.current;\n const view = viewRef.current;\n const client = { x: e.clientX, y: e.clientY };\n if (!canvas || !view) {\n setState({ client, world: null, inCanvas: false });\n return;\n }\n const rect = canvas.getBoundingClientRect();\n const inCanvas =\n e.clientX >= rect.left && e.clientX <= rect.right &&\n e.clientY >= rect.top && e.clientY <= rect.bottom;\n const [wx, wy] = clientToWorld(e.clientX, e.clientY, rect, view);\n const world = { x: wx, y: wy };\n setState({ client, world, inCanvas });\n };\n\n document.addEventListener('pointermove', onMove);\n\n return () => {\n document.removeEventListener('pointermove', onMove);\n };\n }, [canvasRef, viewRef]);\n\n if (!anchorStyle) return null;\n\n return (\n <div\n ref={ref}\n className={`${s.hud} ${s.pre}`}\n style={anchorStyle}\n >\n {`fps ${fps.toString().padStart(3)}`}\n {'\\n'}\n {`client (${state.client.x.toFixed(0)}, ${state.client.y.toFixed(0)})`}\n {'\\n'}\n {state.world\n ? `world (${state.world.x.toFixed(1)}, ${state.world.y.toFixed(1)})${state.inCanvas ? '' : ' (off-canvas)'}`\n : 'world — (no canvas/view yet)'}\n </div>\n );\n}\n"],"mappings":";;;;;;;AAqCA,SAAgBE,EAAgB,EAAE,cAAW,cAAW,YAAS,aAAgC;CAC/F,IAAM,CAACC,GAAOC,KAAYC,EAAmB;EAC3C,QAAQ;GAAE,GAAG;GAAG,GAAG;EAAE;EAAG,OAAO;EAAM,UAAU;CACjD,CAAC,GACK,CAACC,GAAKC,KAAUF,EAAiB,CAAC,GAClC,EAAE,QAAK,OAAOG,MAAgBC,QAC5BC,GAAW,WAAWC,EAAU,SAAS,iBAAiBA,EAAU,SAC1E;EACE,OAAO;GAAE,GAAG;GAAO,GAAG;EAAQ;EAC9B,QAAQ;GAAE,GAAGC,GAAQ,SAAS;GAAG,GAAGA,GAAQ,OAAO;EAAE;CACvD,CACF,GAGMC,IAAYC,EAAO,CAAC,GACpBC,IAAeD,EAAO,CAAC,GACvBE,IAAUC,QACR;EACJ,EAAU;EACV,IAAMC,IAAM,YAAY,IAAI,GACtBC,IAAKD,IAAMH,EAAa;EAM9B,AALII,KAAM,QACRZ,EAAO,KAAK,MAAOM,EAAU,UAAU,MAAQM,CAAE,CAAC,GAClD,EAAU,UAAU,GACpB,EAAa,UAAUD,IAEzBF,EAAQ,QAAQ;CAClB,GAGA,EACE,gBAAgB;EAEd,AADA,EAAU,UAAU,GACpB,EAAa,UAAU,YAAY,IAAI;CACzC,EACF,CACF;CAmCA,OAjCAI,SACE,EAAa,UAAU,YAAY,IAAI,GACvCJ,EAAQ,QAAQ,SACHA,EAAQ,OAAO,IAC3B,CAACA,CAAO,CAAC,GAEZI,QAAgB;EACd,IAAMC,KAAU,MAAoB;GAClC,IAAMC,IAASX,EAAU,SACnBY,IAAOC,EAAQ,SACfC,IAAS;IAAE,GAAG,EAAE;IAAS,GAAG,EAAE;GAAQ;GAC5C,IAAI,CAACH,KAAU,CAACC,GAAM;IACpB,EAAS;KAAE;KAAQ,OAAO;KAAM,UAAU;IAAM,CAAC;IACjD;GACF;GACA,IAAMG,IAAOJ,EAAO,sBAAsB,GACpCK,IACJ,EAAE,WAAWD,EAAK,QAAQ,EAAE,WAAWA,EAAK,SAC5C,EAAE,WAAWA,EAAK,OAAQ,EAAE,WAAWA,EAAK,QACxC,CAACE,GAAIC,KAAMC,EAAc,EAAE,SAAS,EAAE,SAASJ,GAAMH,CAAI;GAE/D,EAAS;IAAE;IAAQ,OAAA;KADH,GAAGK;KAAI,GAAGC;IACP;IAAO;GAAS,CAAC;EACtC;EAIA,OAFA,SAAS,iBAAiB,eAAeR,CAAM,SAElC;GACX,SAAS,oBAAoB,eAAeA,CAAM;EACpD;CACF,GAAG,CAACV,GAAWa,CAAO,CAAC,GAElBhB,IAGH,kBAAC,OAAD;EACO;EACL,WAAW,GAAGR,EAAE,IAAI,GAAGA,EAAE;EACzB,OAAOQ;EAHT,UAAA;GAKG,UAAUF,EAAI,SAAS,CAAC,CAAC,SAAS,CAAC;GACnC;GACA,WAAWH,EAAM,OAAO,EAAE,QAAQ,CAAC,EAAE,IAAIA,EAAM,OAAO,EAAE,QAAQ,CAAC,EAAE;GACnE;GACAA,EAAM,QACH,WAAWA,EAAM,MAAM,EAAE,QAAQ,CAAC,EAAE,IAAIA,EAAM,MAAM,EAAE,QAAQ,CAAC,EAAE,GAAGA,EAAM,WAAW,KAAK,qBAC1F;EACD;MAfkB;AAiB3B"}
1
+ {"version":3,"file":"CursorCoordsHud.js","names":["s","hud_module_default","CursorCoordsHud","state","setState","useState","fps","setFps","anchorStyle","useHostAnchor","anchorRef","canvasRef","offset","framesRef","useRef","lastFlushRef","fpsLoop","useVisibleRaf","now","dt","useEffect","onMove","canvas","view","viewRef","client","rect","inCanvas","wx","wy","clientToWorld","point","at","decimal","qty","x","y"],"sources":["../../src/canvas/CursorCoordsHud.tsx"],"sourcesContent":["/**\n * Fixed-position dev HUD that shows the cursor's current viewport\n * (client) coords next to the equivalent canvas (world) coords. Mounts\n * top-left of the viewport; listens to `document` pointermove so it\n * reads the cursor even when it leaves the canvas.\n *\n * Mount inside a `<SceneCanvas>` subtree (or anywhere with access to a\n * canvas + view ref) for quick coord-reconciliation during gesture\n * debugging.\n */\nimport { useEffect, useRef, useState } from 'react';\nimport { decimal, qty } from '@weasel-js/quantity';\nimport { useVisibleRaf } from '../scheduling/useVisibleRaf';\nimport s from './hud.module.css';\nimport { useHostAnchor } from './useHostAnchor';\nimport type { View } from 'core/viewport/view';\nimport { clientToWorld } from 'core/viewport/clientToWorld';\n\n/** Props for `<CursorCoordsHud>`. */\nexport interface CursorCoordsHudProps {\n canvasRef: React.RefObject<HTMLElement | null>;\n /** Element the HUD pins its corner to. Defaults to the canvas's parent — the\n * wrapper a bare `<canvas>` sits in. A detached canvas passes its own input\n * box, whose parent is the shared surface every pane sits in. */\n anchorRef?: React.RefObject<HTMLElement | null>;\n viewRef: React.RefObject<View>;\n /** Inset from the canvas's top-right corner, in px. Default 8 on both axes. */\n offset?: { top?: number; right?: number };\n}\n\ninterface HudState {\n client: { x: number; y: number };\n world: { x: number; y: number } | null;\n inCanvas: boolean;\n}\n\n/** Debug overlay showing the pointer's client and world coordinates and the\n * current frame rate, pinned to the canvas's top-right corner. */\nexport function CursorCoordsHud({ canvasRef, anchorRef, viewRef, offset }: CursorCoordsHudProps) {\n const [state, setState] = useState<HudState>({\n client: { x: 0, y: 0 }, world: null, inCanvas: false,\n });\n const [fps, setFps] = useState<number>(0);\n const { ref, style: anchorStyle } = useHostAnchor(\n () => anchorRef?.current ?? canvasRef.current?.parentElement ?? canvasRef.current,\n {\n align: { x: 'end', y: 'start' },\n offset: { x: offset?.right ?? 8, y: offset?.top ?? 8 },\n },\n );\n\n // FPS counter: tally frames per rAF tick; flush once a second.\n const framesRef = useRef(0);\n const lastFlushRef = useRef(0);\n const fpsLoop = useVisibleRaf(\n () => {\n framesRef.current++;\n const now = performance.now();\n const dt = now - lastFlushRef.current;\n if (dt >= 1000) {\n setFps(Math.round((framesRef.current * 1000) / dt));\n framesRef.current = 0;\n lastFlushRef.current = now;\n }\n fpsLoop.request();\n },\n // Frames not drawn while hidden are not frames that were slow to draw:\n // without this the readout would flush a near-zero rate on resume.\n {\n onResume: () => {\n framesRef.current = 0;\n lastFlushRef.current = performance.now();\n },\n },\n );\n\n useEffect(() => {\n lastFlushRef.current = performance.now();\n fpsLoop.request();\n return () => fpsLoop.cancel();\n }, [fpsLoop]);\n\n useEffect(() => {\n const onMove = (e: PointerEvent) => {\n const canvas = canvasRef.current;\n const view = viewRef.current;\n const client = { x: e.clientX, y: e.clientY };\n if (!canvas || !view) {\n setState({ client, world: null, inCanvas: false });\n return;\n }\n const rect = canvas.getBoundingClientRect();\n const inCanvas =\n e.clientX >= rect.left && e.clientX <= rect.right &&\n e.clientY >= rect.top && e.clientY <= rect.bottom;\n const [wx, wy] = clientToWorld(e.clientX, e.clientY, rect, view);\n const world = { x: wx, y: wy };\n setState({ client, world, inCanvas });\n };\n\n document.addEventListener('pointermove', onMove);\n\n return () => {\n document.removeEventListener('pointermove', onMove);\n };\n }, [canvasRef, viewRef]);\n\n if (!anchorStyle) return null;\n\n return (\n <div ref={ref} className={s.hud} style={anchorStyle}>\n <dl className={s.readings}>\n <dt className={s.muted}>fps</dt>\n <dd>{fps}</dd>\n <dt className={s.muted}>client</dt>\n <dd>{point(state.client, 0)}</dd>\n <dt className={s.muted}>world</dt>\n {state.world ? (\n <dd>\n {point(state.world, 1)}\n {state.inCanvas ? null : <span className={s.absent}> off-canvas</span>}\n </dd>\n ) : (\n <dd className={s.absent}>— no canvas or view yet</dd>\n )}\n </dl>\n </div>\n );\n}\n\n/** `(x, y)` at `places` decimals, its negatives written with U+2212. */\nfunction point({ x, y }: { x: number; y: number }, places: number): string {\n const at = decimal({ places, grouping: false });\n return `(${qty(x, at).text}, ${qty(y, at).text})`;\n}\n"],"mappings":"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"}
@@ -1,8 +1,8 @@
1
1
  //#region src/canvas/hud.module.css
2
- var e = "_hud_s26ij_2", t = "_sized_s26ij_15", n = "_pre_s26ij_20", r = "_muted_s26ij_24", i = "_absent_s26ij_29", a = "_header_s26ij_34", o = "_list_s26ij_38", s = "_item_s26ij_47", c = "_itemBest_s26ij_53", l = "_row_s26ij_57", u = "_stack_s26ij_64", d = {
2
+ var e = "_hud_1hv8c_3", t = "_sized_1hv8c_17", n = "_readings_1hv8c_22 _numeric_zns9n_4", r = "_muted_1hv8c_39", i = "_absent_1hv8c_44", a = "_header_1hv8c_49", o = "_list_1hv8c_53", s = "_item_1hv8c_62", c = "_itemBest_1hv8c_68", l = "_row_1hv8c_72", u = "_stack_1hv8c_79", d = {
3
3
  hud: e,
4
4
  sized: t,
5
- pre: n,
5
+ readings: n,
6
6
  muted: r,
7
7
  absent: i,
8
8
  header: a,
@@ -13,6 +13,6 @@ var e = "_hud_s26ij_2", t = "_sized_s26ij_15", n = "_pre_s26ij_20", r = "_muted_
13
13
  stack: u
14
14
  };
15
15
  //#endregion
16
- export { i as absent, d as default, a as header, e as hud, s as item, c as itemBest, o as list, r as muted, n as pre, l as row, t as sized, u as stack };
16
+ export { i as absent, d as default, a as header, e as hud, s as item, c as itemBest, o as list, r as muted, n as readings, l as row, t as sized, u as stack };
17
17
 
18
18
  //# sourceMappingURL=hud.module.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"hud.module.js","names":["hud","sized","pre","muted","absent","header","list","item","itemBest","row","stack","hud_module_default"],"sources":["../../src/canvas/hud.module.css"],"sourcesContent":["/* The debug HUDs <Canvas> can draw: CursorCoordsHud, PickHud, ModalityHud. */\n.hud {\n position: fixed;\n z-index: 10000;\n background: rgba(0, 0, 0, 0.7);\n color: #e8e8e8;\n font: 11px ui-monospace, SFMono-Regular, Menlo, monospace;\n padding: 6px 8px;\n border-radius: 4px;\n pointer-events: none;\n line-height: 1.4;\n text-align: right;\n}\n\n.sized {\n min-width: 120px;\n max-width: 260px;\n}\n\n.pre {\n white-space: pre;\n}\n\n.muted {\n color: #888;\n}\n\n/* A value that is not there: an em dash, \"off-canvas\". */\n.absent {\n color: #888;\n font-style: italic;\n}\n\n.header {\n margin-bottom: 2px;\n}\n\n.list {\n list-style: none;\n margin: 0;\n padding: 0;\n display: flex;\n flex-direction: column;\n gap: 1px;\n}\n\n.item {\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.itemBest {\n font-weight: 700;\n}\n\n.row {\n display: flex;\n gap: 8px;\n justify-content: flex-end;\n white-space: nowrap;\n}\n\n.stack {\n display: flex;\n flex-direction: column;\n align-items: flex-end;\n}\n"],"mappings":""}
1
+ {"version":3,"file":"hud.module.js","names":["hud","sized","readings","muted","absent","header","list","item","itemBest","row","stack","hud_module_default"],"sources":["../../src/canvas/hud.module.css"],"sourcesContent":["/* The debug HUDs <Canvas> can draw: CursorCoordsHud, PickHud, ModalityHud. The\n mono face is for the ids the pick and modality HUDs list; figures take the\n numeric face, on a child so `.hud`'s own font cannot override it. */\n.hud {\n position: fixed;\n z-index: 10000;\n background: rgba(0, 0, 0, 0.7);\n color: #e8e8e8;\n font: 11px ui-monospace, SFMono-Regular, Menlo, monospace;\n padding: 6px 8px;\n border-radius: 4px;\n pointer-events: none;\n line-height: 1.4;\n text-align: right;\n}\n\n.sized {\n min-width: 120px;\n max-width: 260px;\n}\n\n.readings {\n composes: numeric from '@weasel-js/theme/numeric.module.css';\n display: grid;\n grid-template-columns: auto auto;\n column-gap: 8px;\n margin: 0;\n}\n\n.readings dt {\n text-align: left;\n}\n\n.readings dd {\n margin: 0;\n white-space: nowrap;\n}\n\n.muted {\n color: #888;\n}\n\n/* A value that is not there: an em dash, \"off-canvas\". */\n.absent {\n color: #888;\n font-style: italic;\n}\n\n.header {\n margin-bottom: 2px;\n}\n\n.list {\n list-style: none;\n margin: 0;\n padding: 0;\n display: flex;\n flex-direction: column;\n gap: 1px;\n}\n\n.item {\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.itemBest {\n font-weight: 700;\n}\n\n.row {\n display: flex;\n gap: 8px;\n justify-content: flex-end;\n white-space: nowrap;\n}\n\n.stack {\n display: flex;\n flex-direction: column;\n align-items: flex-end;\n}\n"],"mappings":""}
@@ -1,5 +1,5 @@
1
1
  import { NodeId } from '@weasel-js/routing';
2
- import { j as ClipboardSnapshot, I as InsertAdapter } from './types-CA54-mAZ.js';
2
+ import { j as ClipboardSnapshot, I as InsertAdapter } from './types-A37hYkeR.js';
3
3
  import '@weasel-js/history';
4
4
  import '@weasel-js/geom';
5
5
 
package/dist/clone.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { C as CloneBehavior } from './types-DSjd6W3n.js';
2
- import './types-CA54-mAZ.js';
1
+ import { C as CloneBehavior } from './types-B_9g6hMM.js';
2
+ import './types-A37hYkeR.js';
3
3
  import '@weasel-js/history';
4
4
  import '@weasel-js/geom';
5
5
  import '@weasel-js/routing';
@@ -3,10 +3,7 @@ import { axisAlignedBounds as e } from "./unionBounds.js";
3
3
  function t(e, t) {
4
4
  return e.forNode?.(t) ?? e;
5
5
  }
6
- function n(e, t) {
7
- return e.x < t.x + t.width && e.x + e.width > t.x && e.y < t.y + t.height && e.y + e.height > t.y;
8
- }
9
- var r = {
6
+ var n = {
10
7
  getBounds: (e) => e,
11
8
  remapBounds: (e, t, n) => {
12
9
  let r = t.width === 0 ? 1 : n.width / t.width, i = t.height === 0 ? 1 : n.height / t.height;
@@ -29,7 +26,6 @@ var r = {
29
26
  x: e.x + t,
30
27
  y: e.y + n
31
28
  }),
32
- intersectsRect: (e, t) => n(e, t),
33
29
  lerp: (e, t, n) => ({
34
30
  ...e,
35
31
  x: e.x + (t.x - e.x) * n,
@@ -42,12 +38,11 @@ var r = {
42
38
  ...e,
43
39
  rotation: t
44
40
  })
45
- }, i = {
46
- getBounds: r.getBounds,
47
- remapBounds: r.remapBounds,
48
- translate: r.translate,
49
- intersectsRect: r.intersectsRect,
50
- lerp: r.lerp,
41
+ }, r = {
42
+ getBounds: n.getBounds,
43
+ remapBounds: n.remapBounds,
44
+ translate: n.translate,
45
+ lerp: n.lerp,
51
46
  fromBounds: (e) => ({
52
47
  x: e.x,
53
48
  y: e.y,
@@ -61,7 +56,7 @@ var r = {
61
56
  rotation: t
62
57
  })
63
58
  };
64
- function a(e, t, n) {
59
+ function i(e, t, n) {
65
60
  let r = t.width === 0 ? 1 : n.width / t.width, i = t.height === 0 ? 1 : n.height / t.height, a = e.rotation ?? 0, o = Math.cos(a), s = Math.sin(a), c = r * o, l = i * s, u = -r * s, d = i * o, f = e.width * Math.hypot(c, l), p = e.height * Math.hypot(u, d), m = Math.atan2(l, c), h = n.x + (e.x + e.width / 2 - t.x) * r, g = n.y + (e.y + e.height / 2 - t.y) * i;
66
61
  return {
67
62
  ...e,
@@ -72,7 +67,7 @@ function a(e, t, n) {
72
67
  rotation: m
73
68
  };
74
69
  }
75
- function o(t, n) {
70
+ function a(t, n) {
76
71
  let r = n.getBounds(t), i = n.getRotation?.(t) ?? r.rotation ?? 0;
77
72
  return e({
78
73
  x: r.x,
@@ -82,7 +77,7 @@ function o(t, n) {
82
77
  rotation: i
83
78
  });
84
79
  }
85
- function s(e, t, n, r) {
80
+ function o(e, t, n, r) {
86
81
  if (r.translate) return r.translate(e, t, n);
87
82
  let i = r.getBounds(e), a = {
88
83
  x: i.x + t,
@@ -93,6 +88,6 @@ function s(e, t, n, r) {
93
88
  return r.remapBounds(e, i, a);
94
89
  }
95
90
  //#endregion
96
- export { r as RECT_POSE_DESCRIPTOR, i as ROTATED_POSE_DESCRIPTOR, n as aabbIntersectsRect, t as poseDescriptorForNode, a as remapRotatedLeaf, s as translatePoseViaDescriptor, o as visualBoundsViaDescriptor };
91
+ export { n as RECT_POSE_DESCRIPTOR, r as ROTATED_POSE_DESCRIPTOR, t as poseDescriptorForNode, i as remapRotatedLeaf, o as translatePoseViaDescriptor, a as visualBoundsViaDescriptor };
97
92
 
98
93
  //# sourceMappingURL=poseDescriptor.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"poseDescriptor.js","names":["poseDescriptorForNode","descriptor","node","aabbIntersectsRect","b","r","RECT_POSE_DESCRIPTOR","p","sx","src","dst","sy","dx","dy","a","t","ROTATED_POSE_DESCRIPTOR","remapRotatedLeaf","theta","pose","cos","sin","ux","uy","vx","vy","width","height","rotation","cxNew","cyNew","visualBoundsViaDescriptor","geometry","axisAlignedBounds","translatePoseViaDescriptor"],"sources":["../../../src/core/geometry/poseDescriptor.ts"],"sourcesContent":["import type { Bounds } from 'core/viewport/fitViewToBounds';\nimport { axisAlignedBounds } from 'core/geometry/unionBounds';\n\n/** `Bounds` with a required rotation angle (radians). Pivot is the AABB\n * center of the unrotated `{x, y, width, height}`. */\nexport interface RotatedPose extends Bounds {\n rotation: number;\n}\n\n/**\n * Bridges arbitrary `TPose` shapes into the resize hook's bounds-driven math.\n * The hook reads bounds via `getBounds`, runs anchor-relative math on those\n * bounds, then asks `remapBounds` to project the result back into TPose.\n *\n * `remapBounds(pose, src, dst)` is a single operation that subsumes both\n * \"set my own AABB to dst\" (single-leaf resize) and \"scale me as a leaf\n * inside parent's src→dst rect\" (group resize) — they're the same affine\n * map. For rect-shaped poses the default geometry interprets the pose as\n * its own bounds; for Path or polygon poses the consumer supplies a\n * projection that knows how to read and rewrite the underlying geometry.\n */\nexport interface PoseDescriptor<TPose, TNode = unknown> {\n getBounds(pose: TPose): Bounds;\n remapBounds(pose: TPose, src: Bounds, dst: Bounds): TPose;\n /** A pose occupying `bounds`, of the same kind as `template`. Unlike\n * `remapBounds`, the result carries none of the template's shape. */\n fromBounds(bounds: Bounds, template: TPose): TPose;\n /** Translate the pose by (dx, dy). Optional — when omitted, callers fall\n * back to a translation derived from `remapBounds` (origin shifted, no\n * scale). Path-shaped poses should provide this for performance. */\n translate?(pose: TPose, dx: number, dy: number): TPose;\n /** True iff any portion of the pose's geometry intersects `rect`. Optional.\n * Marquee and lasso do not read it: both test the node's drawn outline\n * (`core/geometry/regionHit`). */\n intersectsRect?(pose: TPose, rect: Bounds): boolean;\n /** Interpolate between two poses. Optional — animation helpers fall back to\n * rect-shape lerp when omitted (which fails for non-rect poses). */\n lerp?(a: TPose, b: TPose, t: number): TPose;\n /** Read the pose's rotation in radians. Pivot is the AABB center\n * (`getBounds(pose)` center). Default 0 when omitted — descriptor\n * declares \"this pose has no rotation.\" When supplied and non-zero,\n * `useResize` projects the drag delta into the leaf's local frame,\n * runs anchor math there, and translates the resulting pose so the\n * diagonally opposite world-space corner is pinned. */\n getRotation?(pose: TPose): number;\n /** True iff this pose shape can carry a rotation. Consulted by the\n * rotation affordance to decide whether to render the rotate cursor /\n * drag-band over a selection. When omitted, the kit assumes `true` for\n * back-compat — descriptors whose poses lack `x/y/width/height/rotation`\n * fields (e.g. polygon Paths) should return `false` so the affordance\n * hides instead of exposing a non-functional rotate cursor. */\n supportsRotation?(pose: TPose): boolean;\n /** Write a rotation (radians) into the pose, bounds unchanged. Absent means\n * the pose cannot carry one, and the rotate action leaves it alone. */\n withRotation?(pose: TPose, rotation: number): TPose;\n /**\n * A descriptor specialized to one node. Every other method here sees only\n * the pose, so shapes sharing a pose type — a sphere and a box both posed by\n * position/rotation/scale — are indistinguishable to them. Implement this to\n * read `node.data` once and hand back a descriptor that knows which shape it\n * is describing.\n *\n * Callers reach it through {@link poseDescriptorForNode}, at whichever site\n * holds the node; the specialized descriptor then travels into the bounds and\n * hit-test helpers in place of this one. Sites with only an id keep using the\n * unspecialized descriptor, which is why this is optional on both ends.\n */\n forNode?(node: TNode): PoseDescriptor<TPose, TNode>;\n}\n\n/** The descriptor `node` should be read through: its own specialization when\n * it declares one, and otherwise the descriptor unchanged. */\nexport function poseDescriptorForNode<TPose, TNode>(\n descriptor: PoseDescriptor<TPose, TNode>,\n node: TNode,\n): PoseDescriptor<TPose, TNode> {\n return descriptor.forNode?.(node) ?? descriptor;\n}\n\n/** AABB-vs-AABB overlap. Exported for callers building a default\n * `intersectsRect` from `getBounds`. */\nexport function aabbIntersectsRect(b: Bounds, r: Bounds): boolean {\n return b.x < r.x + r.width && b.x + b.width > r.x && b.y < r.y + r.height && b.y + b.height > r.y;\n}\n\n/** Identity geometry for `TPose extends Bounds`. Treats the pose as its\n * own bounds and remaps via affine scale against `src`/`dst`. */\nexport const RECT_POSE_DESCRIPTOR: PoseDescriptor<Bounds> = {\n getBounds: (p) => p,\n remapBounds: (p, src, dst) => {\n const sx = src.width === 0 ? 1 : dst.width / src.width;\n const sy = src.height === 0 ? 1 : dst.height / src.height;\n return {\n ...p,\n x: dst.x + (p.x - src.x) * sx,\n y: dst.y + (p.y - src.y) * sy,\n width: p.width * sx,\n height: p.height * sy,\n };\n },\n fromBounds: (b) => ({ x: b.x, y: b.y, width: b.width, height: b.height }),\n translate: (p, dx, dy) => ({ ...p, x: p.x + dx, y: p.y + dy }),\n intersectsRect: (p, r) => aabbIntersectsRect(p, r),\n lerp: (a, b, t) => ({\n ...a,\n x: a.x + (b.x - a.x) * t,\n y: a.y + (b.y - a.y) * t,\n width: a.width + (b.width - a.width) * t,\n height: a.height + (b.height - a.height) * t,\n }),\n // `Bounds` carries an optional `rotation`, and `withRotation` writes into\n // it — so reading it back has to be the same channel, or a pose reports\n // itself unrotated to `useResize` and to port placement while painting\n // rotated.\n getRotation: (p) => p.rotation ?? 0,\n withRotation: (p, rotation) => ({ ...p, rotation }),\n};\n\n/** Identity geometry for `RotatedPose`. Inherits rect-shape projection\n * from `RECT_POSE_DESCRIPTOR` (the `RotatedPose extends Bounds`\n * subtype lets the rect descriptor's methods apply directly; `remapBounds`\n * preserves the `rotation` field via `...p` spread). Adds `getRotation` so\n * `useResize` knows to take the rotation-aware math path. */\nexport const ROTATED_POSE_DESCRIPTOR: PoseDescriptor<RotatedPose> = {\n getBounds: RECT_POSE_DESCRIPTOR.getBounds as PoseDescriptor<RotatedPose>['getBounds'],\n remapBounds: RECT_POSE_DESCRIPTOR.remapBounds as PoseDescriptor<RotatedPose>['remapBounds'],\n translate: RECT_POSE_DESCRIPTOR.translate as PoseDescriptor<RotatedPose>['translate'],\n intersectsRect: RECT_POSE_DESCRIPTOR.intersectsRect as PoseDescriptor<RotatedPose>['intersectsRect'],\n lerp: RECT_POSE_DESCRIPTOR.lerp as PoseDescriptor<RotatedPose>['lerp'],\n fromBounds: (b) => ({ x: b.x, y: b.y, width: b.width, height: b.height, rotation: 0 }),\n getRotation: (p) => p.rotation,\n withRotation: (p, rotation) => ({ ...p, rotation }),\n};\n\n/**\n * Remap a **rotated** leaf inside a group resize.\n *\n * The naive `remapBounds` scales a leaf's axis-aligned `width`/`height` by the\n * group's per-axis factors and carries `rotation` through untouched. That is\n * right when `src`/`dst` are already expressed in the leaf's own frame — the\n * single-leaf resize path, where the drag delta was projected into that frame\n * before the anchor math ran. In a group resize they are world-frame, so the\n * leaf's local axes do not line up with the axes being scaled: at 90° a\n * horizontal stretch should grow the leaf's `height`, and the naive map grows\n * its `width`.\n *\n * What this computes is the group affine applied to the leaf's local frame,\n * with the shear dropped — the pose model is `{x, y, width, height, rotation}`\n * and cannot represent the parallelogram a non-uniform scale of a rotated box\n * actually produces. The center moves exactly; each local axis takes the\n * length of its own image; the rotation follows the image of the local x-axis.\n *\n * Degenerates to the naive map at `rotation === 0`, and to a plain uniform\n * scale when `sx === sy`, both exactly.\n */\nexport function remapRotatedLeaf<TPose extends RotatedPose>(\n pose: TPose,\n src: Bounds,\n dst: Bounds,\n): TPose {\n const sx = src.width === 0 ? 1 : dst.width / src.width;\n const sy = src.height === 0 ? 1 : dst.height / src.height;\n const theta = pose.rotation ?? 0;\n const cos = Math.cos(theta);\n const sin = Math.sin(theta);\n\n // Images of the leaf's local unit axes under the group's diagonal scale.\n const ux = sx * cos, uy = sy * sin;\n const vx = -sx * sin, vy = sy * cos;\n\n const width = pose.width * Math.hypot(ux, uy);\n const height = pose.height * Math.hypot(vx, vy);\n const rotation = Math.atan2(uy, ux);\n\n // The center is a point, so it takes the group affine directly.\n const cxNew = dst.x + (pose.x + pose.width / 2 - src.x) * sx;\n const cyNew = dst.y + (pose.y + pose.height / 2 - src.y) * sy;\n\n return {\n ...pose,\n x: cxNew - width / 2,\n y: cyNew - height / 2,\n width,\n height,\n rotation,\n };\n}\n\n/** The pose's visual bounds: its frame expanded to cover the rotated\n * rectangle, so a turned shape reports the extent of its ink. */\nexport function visualBoundsViaDescriptor<TPose>(\n pose: TPose,\n geometry: PoseDescriptor<TPose>,\n): Bounds {\n const b = geometry.getBounds(pose);\n const rotation =\n geometry.getRotation?.(pose) ?? (b as { rotation?: number }).rotation ?? 0;\n return axisAlignedBounds({ x: b.x, y: b.y, width: b.width, height: b.height, rotation });\n}\n\n/** Translate through `geometry.translate`, or via `remapBounds` onto a shifted\n * copy of the pose's own bounds when the descriptor has none. */\nexport function translatePoseViaDescriptor<TPose>(\n pose: TPose,\n dx: number,\n dy: number,\n geometry: PoseDescriptor<TPose>,\n): TPose {\n if (geometry.translate) return geometry.translate(pose, dx, dy);\n const src = geometry.getBounds(pose);\n const dst = { x: src.x + dx, y: src.y + dy, width: src.width, height: src.height };\n return geometry.remapBounds(pose, src, dst);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"poseDescriptor.js","names":["poseDescriptorForNode","descriptor","node","RECT_POSE_DESCRIPTOR","p","sx","src","dst","sy","b","dx","dy","a","t","ROTATED_POSE_DESCRIPTOR","remapRotatedLeaf","theta","pose","cos","sin","ux","uy","vx","vy","width","height","rotation","cxNew","cyNew","visualBoundsViaDescriptor","geometry","axisAlignedBounds","translatePoseViaDescriptor"],"sources":["../../../src/core/geometry/poseDescriptor.ts"],"sourcesContent":["import type { Bounds } from 'core/viewport/fitViewToBounds';\nimport { axisAlignedBounds } from 'core/geometry/unionBounds';\n\n/** `Bounds` with a required rotation angle (radians). Pivot is the AABB\n * center of the unrotated `{x, y, width, height}`. */\nexport interface RotatedPose extends Bounds {\n rotation: number;\n}\n\n/**\n * Bridges arbitrary `TPose` shapes into the resize hook's bounds-driven math.\n * The hook reads bounds via `getBounds`, runs anchor-relative math on those\n * bounds, then asks `remapBounds` to project the result back into TPose.\n *\n * `remapBounds(pose, src, dst)` is a single operation that subsumes both\n * \"set my own AABB to dst\" (single-leaf resize) and \"scale me as a leaf\n * inside parent's src→dst rect\" (group resize) — they're the same affine\n * map. For rect-shaped poses the default geometry interprets the pose as\n * its own bounds; for Path or polygon poses the consumer supplies a\n * projection that knows how to read and rewrite the underlying geometry.\n */\nexport interface PoseDescriptor<TPose, TNode = unknown> {\n getBounds(pose: TPose): Bounds;\n remapBounds(pose: TPose, src: Bounds, dst: Bounds): TPose;\n /** A pose occupying `bounds`, of the same kind as `template`. Unlike\n * `remapBounds`, the result carries none of the template's shape. */\n fromBounds(bounds: Bounds, template: TPose): TPose;\n /** Translate the pose by (dx, dy). Optional — when omitted, callers fall\n * back to a translation derived from `remapBounds` (origin shifted, no\n * scale). Path-shaped poses should provide this for performance. */\n translate?(pose: TPose, dx: number, dy: number): TPose;\n /** Interpolate between two poses. Optional — animation helpers fall back to\n * rect-shape lerp when omitted (which fails for non-rect poses). */\n lerp?(a: TPose, b: TPose, t: number): TPose;\n /** Read the pose's rotation in radians. Pivot is the AABB center\n * (`getBounds(pose)` center). Default 0 when omitted — descriptor\n * declares \"this pose has no rotation.\" When supplied and non-zero,\n * `useResize` projects the drag delta into the leaf's local frame,\n * runs anchor math there, and translates the resulting pose so the\n * diagonally opposite world-space corner is pinned. */\n getRotation?(pose: TPose): number;\n /** True iff this pose shape can carry a rotation. Consulted by the\n * rotation affordance to decide whether to render the rotate cursor /\n * drag-band over a selection. When omitted, the kit assumes `true` for\n * back-compat — descriptors whose poses lack `x/y/width/height/rotation`\n * fields (e.g. polygon Paths) should return `false` so the affordance\n * hides instead of exposing a non-functional rotate cursor. */\n supportsRotation?(pose: TPose): boolean;\n /** Write a rotation (radians) into the pose, bounds unchanged. Absent means\n * the pose cannot carry one, and the rotate action leaves it alone. */\n withRotation?(pose: TPose, rotation: number): TPose;\n /**\n * A descriptor specialized to one node. Every other method here sees only\n * the pose, so shapes sharing a pose type — a sphere and a box both posed by\n * position/rotation/scale — are indistinguishable to them. Implement this to\n * read `node.data` once and hand back a descriptor that knows which shape it\n * is describing.\n *\n * Callers reach it through {@link poseDescriptorForNode}, at whichever site\n * holds the node; the specialized descriptor then travels into the bounds and\n * hit-test helpers in place of this one. Sites with only an id keep using the\n * unspecialized descriptor, which is why this is optional on both ends.\n */\n forNode?(node: TNode): PoseDescriptor<TPose, TNode>;\n}\n\n/** The descriptor `node` should be read through: its own specialization when\n * it declares one, and otherwise the descriptor unchanged. */\nexport function poseDescriptorForNode<TPose, TNode>(\n descriptor: PoseDescriptor<TPose, TNode>,\n node: TNode,\n): PoseDescriptor<TPose, TNode> {\n return descriptor.forNode?.(node) ?? descriptor;\n}\n\n/** Identity geometry for `TPose extends Bounds`. Treats the pose as its\n * own bounds and remaps via affine scale against `src`/`dst`. */\nexport const RECT_POSE_DESCRIPTOR: PoseDescriptor<Bounds> = {\n getBounds: (p) => p,\n remapBounds: (p, src, dst) => {\n const sx = src.width === 0 ? 1 : dst.width / src.width;\n const sy = src.height === 0 ? 1 : dst.height / src.height;\n return {\n ...p,\n x: dst.x + (p.x - src.x) * sx,\n y: dst.y + (p.y - src.y) * sy,\n width: p.width * sx,\n height: p.height * sy,\n };\n },\n fromBounds: (b) => ({ x: b.x, y: b.y, width: b.width, height: b.height }),\n translate: (p, dx, dy) => ({ ...p, x: p.x + dx, y: p.y + dy }),\n lerp: (a, b, t) => ({\n ...a,\n x: a.x + (b.x - a.x) * t,\n y: a.y + (b.y - a.y) * t,\n width: a.width + (b.width - a.width) * t,\n height: a.height + (b.height - a.height) * t,\n }),\n // `Bounds` carries an optional `rotation`, and `withRotation` writes into\n // it — so reading it back has to be the same channel, or a pose reports\n // itself unrotated to `useResize` and to port placement while painting\n // rotated.\n getRotation: (p) => p.rotation ?? 0,\n withRotation: (p, rotation) => ({ ...p, rotation }),\n};\n\n/** Identity geometry for `RotatedPose`. Inherits rect-shape projection\n * from `RECT_POSE_DESCRIPTOR` (the `RotatedPose extends Bounds`\n * subtype lets the rect descriptor's methods apply directly; `remapBounds`\n * preserves the `rotation` field via `...p` spread). Adds `getRotation` so\n * `useResize` knows to take the rotation-aware math path. */\nexport const ROTATED_POSE_DESCRIPTOR: PoseDescriptor<RotatedPose> = {\n getBounds: RECT_POSE_DESCRIPTOR.getBounds as PoseDescriptor<RotatedPose>['getBounds'],\n remapBounds: RECT_POSE_DESCRIPTOR.remapBounds as PoseDescriptor<RotatedPose>['remapBounds'],\n translate: RECT_POSE_DESCRIPTOR.translate as PoseDescriptor<RotatedPose>['translate'],\n lerp: RECT_POSE_DESCRIPTOR.lerp as PoseDescriptor<RotatedPose>['lerp'],\n fromBounds: (b) => ({ x: b.x, y: b.y, width: b.width, height: b.height, rotation: 0 }),\n getRotation: (p) => p.rotation,\n withRotation: (p, rotation) => ({ ...p, rotation }),\n};\n\n/**\n * Remap a **rotated** leaf inside a group resize.\n *\n * The naive `remapBounds` scales a leaf's axis-aligned `width`/`height` by the\n * group's per-axis factors and carries `rotation` through untouched. That is\n * right when `src`/`dst` are already expressed in the leaf's own frame — the\n * single-leaf resize path, where the drag delta was projected into that frame\n * before the anchor math ran. In a group resize they are world-frame, so the\n * leaf's local axes do not line up with the axes being scaled: at 90° a\n * horizontal stretch should grow the leaf's `height`, and the naive map grows\n * its `width`.\n *\n * What this computes is the group affine applied to the leaf's local frame,\n * with the shear dropped — the pose model is `{x, y, width, height, rotation}`\n * and cannot represent the parallelogram a non-uniform scale of a rotated box\n * actually produces. The center moves exactly; each local axis takes the\n * length of its own image; the rotation follows the image of the local x-axis.\n *\n * Degenerates to the naive map at `rotation === 0`, and to a plain uniform\n * scale when `sx === sy`, both exactly.\n */\nexport function remapRotatedLeaf<TPose extends RotatedPose>(\n pose: TPose,\n src: Bounds,\n dst: Bounds,\n): TPose {\n const sx = src.width === 0 ? 1 : dst.width / src.width;\n const sy = src.height === 0 ? 1 : dst.height / src.height;\n const theta = pose.rotation ?? 0;\n const cos = Math.cos(theta);\n const sin = Math.sin(theta);\n\n // Images of the leaf's local unit axes under the group's diagonal scale.\n const ux = sx * cos, uy = sy * sin;\n const vx = -sx * sin, vy = sy * cos;\n\n const width = pose.width * Math.hypot(ux, uy);\n const height = pose.height * Math.hypot(vx, vy);\n const rotation = Math.atan2(uy, ux);\n\n // The center is a point, so it takes the group affine directly.\n const cxNew = dst.x + (pose.x + pose.width / 2 - src.x) * sx;\n const cyNew = dst.y + (pose.y + pose.height / 2 - src.y) * sy;\n\n return {\n ...pose,\n x: cxNew - width / 2,\n y: cyNew - height / 2,\n width,\n height,\n rotation,\n };\n}\n\n/** The pose's visual bounds: its frame expanded to cover the rotated\n * rectangle, so a turned shape reports the extent of its ink. */\nexport function visualBoundsViaDescriptor<TPose>(\n pose: TPose,\n geometry: PoseDescriptor<TPose>,\n): Bounds {\n const b = geometry.getBounds(pose);\n const rotation =\n geometry.getRotation?.(pose) ?? (b as { rotation?: number }).rotation ?? 0;\n return axisAlignedBounds({ x: b.x, y: b.y, width: b.width, height: b.height, rotation });\n}\n\n/** Translate through `geometry.translate`, or via `remapBounds` onto a shifted\n * copy of the pose's own bounds when the descriptor has none. */\nexport function translatePoseViaDescriptor<TPose>(\n pose: TPose,\n dx: number,\n dy: number,\n geometry: PoseDescriptor<TPose>,\n): TPose {\n if (geometry.translate) return geometry.translate(pose, dx, dy);\n const src = geometry.getBounds(pose);\n const dst = { x: src.x + dx, y: src.y + dy, width: src.width, height: src.height };\n return geometry.remapBounds(pose, src, dst);\n}\n"],"mappings":";;AAoEA,SAAgBA,EACd,GACA,GAC8B;CAC9B,OAAOC,EAAW,UAAUC,CAAI,KAAKD;AACvC;AAIA,IAAaE,IAA+C;CAC1D,YAAY,MAAMC;CAClB,cAAc,GAAG,GAAK,MAAQ;EAC5B,IAAMC,IAAKC,EAAI,UAAU,IAAI,IAAIC,EAAI,QAAQD,EAAI,OAC3CE,IAAKF,EAAI,WAAW,IAAI,IAAIC,EAAI,SAASD,EAAI;EACnD,OAAO;GACL,GAAGF;GACH,GAAGG,EAAI,KAAKH,EAAE,IAAIE,EAAI,KAAKD;GAC3B,GAAGE,EAAI,KAAKH,EAAE,IAAIE,EAAI,KAAKE;GAC3B,OAAOJ,EAAE,QAAQC;GACjB,QAAQD,EAAE,SAASI;EACrB;CACF;CACA,aAAa,OAAO;EAAE,GAAGC,EAAE;EAAG,GAAGA,EAAE;EAAG,OAAOA,EAAE;EAAO,QAAQA,EAAE;CAAO;CACvE,YAAY,GAAG,GAAI,OAAQ;EAAE,GAAGL;EAAG,GAAGA,EAAE,IAAIM;EAAI,GAAGN,EAAE,IAAIO;CAAG;CAC5D,OAAO,GAAG,GAAG,OAAO;EAClB,GAAGC;EACH,GAAGA,EAAE,KAAKH,EAAE,IAAIG,EAAE,KAAKC;EACvB,GAAGD,EAAE,KAAKH,EAAE,IAAIG,EAAE,KAAKC;EACvB,OAAOD,EAAE,SAASH,EAAE,QAAQG,EAAE,SAASC;EACvC,QAAQD,EAAE,UAAUH,EAAE,SAASG,EAAE,UAAUC;CAC7C;CAKA,cAAc,MAAMT,EAAE,YAAY;CAClC,eAAe,GAAG,OAAc;EAAE,GAAGA;EAAG;CAAS;AACnD,GAOaU,IAAuD;CAClE,WAAWX,EAAqB;CAChC,aAAaA,EAAqB;CAClC,WAAWA,EAAqB;CAChC,MAAMA,EAAqB;CAC3B,aAAa,OAAO;EAAE,GAAGM,EAAE;EAAG,GAAGA,EAAE;EAAG,OAAOA,EAAE;EAAO,QAAQA,EAAE;EAAQ,UAAU;CAAE;CACpF,cAAc,MAAML,EAAE;CACtB,eAAe,GAAG,OAAc;EAAE,GAAGA;EAAG;CAAS;AACnD;AAuBA,SAAgBW,EACd,GACA,GACA,GACO;CACP,IAAMV,IAAKC,EAAI,UAAU,IAAI,IAAIC,EAAI,QAAQD,EAAI,OAC3CE,IAAKF,EAAI,WAAW,IAAI,IAAIC,EAAI,SAASD,EAAI,QAC7CU,IAAQC,EAAK,YAAY,GACzBC,IAAM,KAAK,IAAIF,CAAK,GACpBG,IAAM,KAAK,IAAIH,CAAK,GAGpBI,IAAKf,IAAKa,GAAKG,IAAKb,IAAKW,GACzBG,IAAK,CAACjB,IAAKc,GAAKI,IAAKf,IAAKU,GAE1BM,IAAQP,EAAK,QAAQ,KAAK,MAAMG,GAAIC,CAAE,GACtCI,IAASR,EAAK,SAAS,KAAK,MAAMK,GAAIC,CAAE,GACxCG,IAAW,KAAK,MAAML,GAAID,CAAE,GAG5BO,IAAQpB,EAAI,KAAKU,EAAK,IAAIA,EAAK,QAAQ,IAAIX,EAAI,KAAKD,GACpDuB,IAAQrB,EAAI,KAAKU,EAAK,IAAIA,EAAK,SAAS,IAAIX,EAAI,KAAKE;CAE3D,OAAO;EACL,GAAGS;EACH,GAAGU,IAAQH,IAAQ;EACnB,GAAGI,IAAQH,IAAS;EACpB;EACA;EACA;CACF;AACF;AAIA,SAAgBI,EACd,GACA,GACQ;CACR,IAAMpB,IAAIqB,EAAS,UAAUb,CAAI,GAC3BS,IACJI,EAAS,cAAcb,CAAI,KAAMR,EAA4B,YAAY;CAC3E,OAAOsB,EAAkB;EAAE,GAAGtB,EAAE;EAAG,GAAGA,EAAE;EAAG,OAAOA,EAAE;EAAO,QAAQA,EAAE;EAAQ;CAAS,CAAC;AACzF;AAIA,SAAgBuB,EACd,GACA,GACA,GACA,GACO;CACP,IAAIF,EAAS,WAAW,OAAOA,EAAS,UAAUb,GAAMP,GAAIC,CAAE;CAC9D,IAAML,IAAMwB,EAAS,UAAUb,CAAI,GAC7BV,IAAM;EAAE,GAAGD,EAAI,IAAII;EAAI,GAAGJ,EAAI,IAAIK;EAAI,OAAOL,EAAI;EAAO,QAAQA,EAAI;CAAO;CACjF,OAAOwB,EAAS,YAAYb,GAAMX,GAAKC,CAAG;AAC5C"}
@@ -1,4 +1,4 @@
1
- import { B as BoundsConstraint, P as PointSnapBehavior } from './types-DSjd6W3n.js';
1
+ import { B as BoundsConstraint, P as PointSnapBehavior } from './types-B_9g6hMM.js';
2
2
  import { Bounds, ResizeAnchor } from '@weasel-js/routing';
3
3
 
4
4
  /** Option surface for the `resize` action. */
@@ -1,9 +1,8 @@
1
- import { aabbIntersectsRect as e } from "../../core/geometry/poseDescriptor.js";
2
- import { boundsOfPath as t, boxToBox as n, pointInPath as r, transformPath as i, translatePath as a } from "@weasel-js/geom";
1
+ import { boundsOfPath as e, boxToBox as t, transformPath as n, translatePath as r } from "@weasel-js/geom";
3
2
  //#region src/features/paths/poseDescriptor.ts
4
- var o = {
5
- getBounds: (e) => t(e),
6
- remapBounds: (e, t, r) => i(e, n(t.x, t.y, t.width, t.height, r.x, r.y, r.width, r.height)),
3
+ var i = {
4
+ getBounds: (t) => e(t),
5
+ remapBounds: (e, r, i) => n(e, t(r.x, r.y, r.width, r.height, i.x, i.y, i.width, i.height)),
7
6
  fromBounds: (e) => ({
8
7
  kind: "rect",
9
8
  x: e.x,
@@ -11,11 +10,7 @@ var o = {
11
10
  width: e.width,
12
11
  height: e.height
13
12
  }),
14
- translate: (e, t, n) => a(e, t, n),
15
- intersectsRect: (n, i) => {
16
- let a = t(n);
17
- return e(a, i) ? (n.kind === "rect" || r(n, i.x, i.y) || r(n, i.x + i.width, i.y) || r(n, i.x, i.y + i.height) || r(n, i.x + i.width, i.y + i.height), !0) : !1;
18
- },
13
+ translate: (e, t, n) => r(e, t, n),
19
14
  lerp: (e, t, n) => {
20
15
  if (e.kind === "rect" && t.kind === "rect") return {
21
16
  kind: "rect",
@@ -39,6 +34,6 @@ var o = {
39
34
  supportsRotation: () => !1
40
35
  };
41
36
  //#endregion
42
- export { o as pathPoseDescriptor };
37
+ export { i as pathPoseDescriptor };
43
38
 
44
39
  //# sourceMappingURL=poseDescriptor.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"poseDescriptor.js","names":["pathPoseDescriptor","boundsOfPath","path","transformPath","boxToBox","src","dst","b","translatePath","dx","dy","aabbIntersectsRect","rect","pointInPath","a","t","next"],"sources":["../../../src/features/paths/poseDescriptor.ts"],"sourcesContent":["import {\n boxToBox,\n pointInPath,\n boundsOfPath,\n translatePath,\n transformPath,\n type Path,\n} from '@weasel-js/geom';\nimport { aabbIntersectsRect, type PoseDescriptor } from 'interactions/actions/resize/geometry';\n\n/**\n * `PoseDescriptor` for `Path` poses — wires `useResize` to operate\n * on `Path` directly. `getBounds` defers to the same `boundsOfPath` kernel\n * the rest of the kit uses.\n *\n * `remapBounds` mirrors `scalePathToBounds` but takes `src` explicitly: the\n * resize hook knows the group's origin AABB and uses it for every leaf,\n * instead of each leaf scaling against its own AABB (which would ignore\n * group context).\n */\nexport const pathPoseDescriptor: PoseDescriptor<Path> = {\n getBounds: (path) => boundsOfPath(path),\n remapBounds: (path, src, dst) => transformPath(\n path,\n boxToBox(src.x, src.y, src.width, src.height, dst.x, dst.y, dst.width, dst.height),\n ),\n fromBounds: (b) => ({ kind: 'rect', x: b.x, y: b.y, width: b.width, height: b.height }),\n translate: (path, dx, dy) => translatePath(path, dx, dy),\n // WHY: AABB pre-test is cheap; only fall through to per-corner pointInPath\n // when the rect is fully inside the AABB (silhouette test).\n intersectsRect: (path, rect) => {\n const b = boundsOfPath(path);\n if (!aabbIntersectsRect(b, rect)) return false;\n if (path.kind === 'rect') return true;\n // Sample the rect's four corners; any inside the polygon ⇒ overlap.\n if (\n pointInPath(path, rect.x, rect.y) ||\n pointInPath(path, rect.x + rect.width, rect.y) ||\n pointInPath(path, rect.x, rect.y + rect.height) ||\n pointInPath(path, rect.x + rect.width, rect.y + rect.height)\n ) return true;\n // Conservative fallback: AABB overlap counts. Tighter edge-vs-edge test\n // would be the next step; defer until a demo demands it.\n return true;\n },\n lerp: (a, b, t) => {\n if (a.kind === 'rect' && b.kind === 'rect') {\n return {\n kind: 'rect',\n x: a.x + (b.x - a.x) * t,\n y: a.y + (b.y - a.y) * t,\n width: a.width + (b.width - a.width) * t,\n height: a.height + (b.height - a.height) * t,\n };\n }\n if (a.kind === 'polygon' && b.kind === 'polygon' && a.coords.length === b.coords.length) {\n const next = new Float32Array(a.coords.length);\n for (let i = 0; i < a.coords.length; i++) {\n next[i] = a.coords[i] + (b.coords[i] - a.coords[i]) * t;\n }\n return { kind: 'polygon', commands: a.commands, coords: next, fillRule: a.fillRule };\n }\n throw new Error('pathPoseDescriptor.lerp: incompatible path shapes');\n },\n // Path poses carry no x/y/width/height/rotation fields — the kit's\n // `wrapWithPoseRotation` helper has nothing to bind a rotation to, and\n // `remapBounds` would drop a synthetic rotation field on every commit.\n // Returning false here hides the rotation affordance for Path consumers\n // so the rotate cursor doesn't appear without a working interaction.\n supportsRotation: () => false,\n};\n"],"mappings":";;;AAoBA,IAAaA,IAA2C;CACtD,YAAY,MAASC,EAAaC,CAAI;CACtC,cAAc,GAAM,GAAK,MAAQC,EAC/BD,GACAE,EAASC,EAAI,GAAGA,EAAI,GAAGA,EAAI,OAAOA,EAAI,QAAQC,EAAI,GAAGA,EAAI,GAAGA,EAAI,OAAOA,EAAI,MAAM,CACnF;CACA,aAAa,OAAO;EAAE,MAAM;EAAQ,GAAGC,EAAE;EAAG,GAAGA,EAAE;EAAG,OAAOA,EAAE;EAAO,QAAQA,EAAE;CAAO;CACrF,YAAY,GAAM,GAAI,MAAOC,EAAcN,GAAMO,GAAIC,CAAE;CAGvD,iBAAiB,GAAM,MAAS;EAC9B,IAAMH,IAAIN,EAAaC,CAAI;EAY3B,OAXKS,EAAmBJ,GAAGK,CAAI,KAC3BV,EAAK,SAAS,UAGhBW,EAAYX,GAAMU,EAAK,GAAGA,EAAK,CAAC,KAChCC,EAAYX,GAAMU,EAAK,IAAIA,EAAK,OAAOA,EAAK,CAAC,KAC7CC,EAAYX,GAAMU,EAAK,GAAGA,EAAK,IAAIA,EAAK,MAAM,KAC9CC,EAAYX,GAAMU,EAAK,IAAIA,EAAK,OAAOA,EAAK,IAAIA,EAAK,MAAM,GACpD,MARgC;CAY3C;CACA,OAAO,GAAG,GAAG,MAAM;EACjB,IAAIE,EAAE,SAAS,UAAUP,EAAE,SAAS,QAClC,OAAO;GACL,MAAM;GACN,GAAGO,EAAE,KAAKP,EAAE,IAAIO,EAAE,KAAKC;GACvB,GAAGD,EAAE,KAAKP,EAAE,IAAIO,EAAE,KAAKC;GACvB,OAAOD,EAAE,SAASP,EAAE,QAAQO,EAAE,SAASC;GACvC,QAAQD,EAAE,UAAUP,EAAE,SAASO,EAAE,UAAUC;EAC7C;EAEF,IAAID,EAAE,SAAS,aAAaP,EAAE,SAAS,aAAaO,EAAE,OAAO,WAAWP,EAAE,OAAO,QAAQ;GACvF,IAAMS,IAAO,IAAI,aAAaF,EAAE,OAAO,MAAM;GAC7C,KAAK,IAAI,IAAI,GAAG,IAAIA,EAAE,OAAO,QAAQ,KACnC,EAAK,KAAKA,EAAE,OAAO,MAAMP,EAAE,OAAO,KAAKO,EAAE,OAAO,MAAMC;GAExD,OAAO;IAAE,MAAM;IAAW,UAAUD,EAAE;IAAU,QAAQE;IAAM,UAAUF,EAAE;GAAS;EACrF;EACA,MAAU,MAAM,mDAAmD;CACrE;CAMA,wBAAwB;AAC1B"}
1
+ {"version":3,"file":"poseDescriptor.js","names":["pathPoseDescriptor","boundsOfPath","path","transformPath","boxToBox","src","dst","b","translatePath","dx","dy","a","t","next"],"sources":["../../../src/features/paths/poseDescriptor.ts"],"sourcesContent":["import {\n boxToBox,\n boundsOfPath,\n translatePath,\n transformPath,\n type Path,\n} from '@weasel-js/geom';\nimport type { PoseDescriptor } from 'interactions/actions/resize/geometry';\n\n/**\n * `PoseDescriptor` for `Path` poses — wires `useResize` to operate\n * on `Path` directly. `getBounds` defers to the same `boundsOfPath` kernel\n * the rest of the kit uses.\n *\n * `remapBounds` mirrors `scalePathToBounds` but takes `src` explicitly: the\n * resize hook knows the group's origin AABB and uses it for every leaf,\n * instead of each leaf scaling against its own AABB (which would ignore\n * group context).\n */\nexport const pathPoseDescriptor: PoseDescriptor<Path> = {\n getBounds: (path) => boundsOfPath(path),\n remapBounds: (path, src, dst) => transformPath(\n path,\n boxToBox(src.x, src.y, src.width, src.height, dst.x, dst.y, dst.width, dst.height),\n ),\n fromBounds: (b) => ({ kind: 'rect', x: b.x, y: b.y, width: b.width, height: b.height }),\n translate: (path, dx, dy) => translatePath(path, dx, dy),\n lerp: (a, b, t) => {\n if (a.kind === 'rect' && b.kind === 'rect') {\n return {\n kind: 'rect',\n x: a.x + (b.x - a.x) * t,\n y: a.y + (b.y - a.y) * t,\n width: a.width + (b.width - a.width) * t,\n height: a.height + (b.height - a.height) * t,\n };\n }\n if (a.kind === 'polygon' && b.kind === 'polygon' && a.coords.length === b.coords.length) {\n const next = new Float32Array(a.coords.length);\n for (let i = 0; i < a.coords.length; i++) {\n next[i] = a.coords[i] + (b.coords[i] - a.coords[i]) * t;\n }\n return { kind: 'polygon', commands: a.commands, coords: next, fillRule: a.fillRule };\n }\n throw new Error('pathPoseDescriptor.lerp: incompatible path shapes');\n },\n // Path poses carry no x/y/width/height/rotation fields — the kit's\n // `wrapWithPoseRotation` helper has nothing to bind a rotation to, and\n // `remapBounds` would drop a synthetic rotation field on every commit.\n // Returning false here hides the rotation affordance for Path consumers\n // so the rotate cursor doesn't appear without a working interaction.\n supportsRotation: () => false,\n};\n"],"mappings":";;AAmBA,IAAaA,IAA2C;CACtD,YAAY,MAASC,EAAaC,CAAI;CACtC,cAAc,GAAM,GAAK,MAAQC,EAC/BD,GACAE,EAASC,EAAI,GAAGA,EAAI,GAAGA,EAAI,OAAOA,EAAI,QAAQC,EAAI,GAAGA,EAAI,GAAGA,EAAI,OAAOA,EAAI,MAAM,CACnF;CACA,aAAa,OAAO;EAAE,MAAM;EAAQ,GAAGC,EAAE;EAAG,GAAGA,EAAE;EAAG,OAAOA,EAAE;EAAO,QAAQA,EAAE;CAAO;CACrF,YAAY,GAAM,GAAI,MAAOC,EAAcN,GAAMO,GAAIC,CAAE;CACvD,OAAO,GAAG,GAAG,MAAM;EACjB,IAAIC,EAAE,SAAS,UAAUJ,EAAE,SAAS,QAClC,OAAO;GACL,MAAM;GACN,GAAGI,EAAE,KAAKJ,EAAE,IAAII,EAAE,KAAKC;GACvB,GAAGD,EAAE,KAAKJ,EAAE,IAAII,EAAE,KAAKC;GACvB,OAAOD,EAAE,SAASJ,EAAE,QAAQI,EAAE,SAASC;GACvC,QAAQD,EAAE,UAAUJ,EAAE,SAASI,EAAE,UAAUC;EAC7C;EAEF,IAAID,EAAE,SAAS,aAAaJ,EAAE,SAAS,aAAaI,EAAE,OAAO,WAAWJ,EAAE,OAAO,QAAQ;GACvF,IAAMM,IAAO,IAAI,aAAaF,EAAE,OAAO,MAAM;GAC7C,KAAK,IAAI,IAAI,GAAG,IAAIA,EAAE,OAAO,QAAQ,KACnC,EAAK,KAAKA,EAAE,OAAO,MAAMJ,EAAE,OAAO,KAAKI,EAAE,OAAO,MAAMC;GAExD,OAAO;IAAE,MAAM;IAAW,UAAUD,EAAE;IAAU,QAAQE;IAAM,UAAUF,EAAE;GAAS;EACrF;EACA,MAAU,MAAM,mDAAmD;CACrE;CAMA,wBAAwB;AAC1B"}