@weasel-js/core 1.8.0 → 1.9.0

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 (139) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/README.md +9 -5
  3. package/dist/animation/useAnimator.js +20 -13
  4. package/dist/animation/useAnimator.js.map +1 -1
  5. package/dist/{builtins-BW32dOXy.d.ts → builtins-CQcvcLXh.d.ts} +105 -31
  6. package/dist/canvas/DrawCanvas.js +33 -33
  7. package/dist/canvas/DrawCanvas.js.map +1 -1
  8. package/dist/canvas/MinimapCanvas.js +29 -29
  9. package/dist/canvas/MinimapCanvas.js.map +1 -1
  10. package/dist/canvas/NodeShape.js +63 -54
  11. package/dist/canvas/NodeShape.js.map +1 -1
  12. package/dist/canvas/SceneCanvas/features.js +26 -5
  13. package/dist/canvas/SceneCanvas/features.js.map +1 -1
  14. package/dist/canvas/SceneCanvas/useSceneSelectTool.js +3 -2
  15. package/dist/canvas/SceneCanvas/useSceneSelectTool.js.map +1 -1
  16. package/dist/canvas/SceneCanvas.js +526 -529
  17. package/dist/canvas/SceneCanvas.js.map +1 -1
  18. package/dist/canvas/SceneViewCanvas.js +35 -35
  19. package/dist/canvas/SceneViewCanvas.js.map +1 -1
  20. package/dist/canvas/defaultDrawOne.js +6 -1
  21. package/dist/canvas/defaultDrawOne.js.map +1 -1
  22. package/dist/canvas/pickWalk.js.map +1 -1
  23. package/dist/canvas/sceneAdapter.js.map +1 -1
  24. package/dist/canvas/useHitObserver.js +23 -0
  25. package/dist/canvas/useHitObserver.js.map +1 -0
  26. package/dist/canvas/useHostAnchor.js.map +1 -1
  27. package/dist/core/device/useDeviceProfile.js.map +1 -1
  28. package/dist/core/scene/derivedPath.js.map +1 -1
  29. package/dist/core/scene/effectivePose.js.map +1 -1
  30. package/dist/core/scene/kitRegistry.js.map +1 -1
  31. package/dist/core/scene/scene.js +9 -0
  32. package/dist/core/scene/scene.js.map +1 -1
  33. package/dist/core/scene/types.js.map +1 -1
  34. package/dist/core/scene/useScene.js.map +1 -1
  35. package/dist/core/selection/useSelection.js +27 -33
  36. package/dist/core/selection/useSelection.js.map +1 -1
  37. package/dist/core/stableStringify.js +8 -0
  38. package/dist/core/stableStringify.js.map +1 -0
  39. package/dist/core/viewport/fitViewToBounds.js +3 -3
  40. package/dist/core/viewport/fitViewToBounds.js.map +1 -1
  41. package/dist/core/viewport/screenSlop.js +8 -2
  42. package/dist/core/viewport/screenSlop.js.map +1 -1
  43. package/dist/features/minimap/actions.js.map +1 -1
  44. package/dist/features/paths/markerCommands.js +95 -63
  45. package/dist/features/paths/markerCommands.js.map +1 -1
  46. package/dist/features/paths/tessellate/dash.js +52 -0
  47. package/dist/features/paths/tessellate/dash.js.map +1 -0
  48. package/dist/features/paths/tessellate/insets.js +32 -0
  49. package/dist/features/paths/tessellate/insets.js.map +1 -0
  50. package/dist/features/paths/tessellate/metric.js +72 -0
  51. package/dist/features/paths/tessellate/metric.js.map +1 -0
  52. package/dist/features/paths/tessellate/stroke.js +74 -134
  53. package/dist/features/paths/tessellate/stroke.js.map +1 -1
  54. package/dist/features/paths/tessellate/vertexWidths.js +32 -0
  55. package/dist/features/paths/tessellate/vertexWidths.js.map +1 -0
  56. package/dist/features/pointer/PointerContext.js +2 -2
  57. package/dist/features/pointer/PointerContext.js.map +1 -1
  58. package/dist/features/poseRun/usePoseRun.js.map +1 -1
  59. package/dist/features/text/useTextEdit.js +86 -84
  60. package/dist/features/text/useTextEdit.js.map +1 -1
  61. package/dist/index.d.ts +84 -45
  62. package/dist/index.js +269 -268
  63. package/dist/interactions/actions/defaults/align.js.map +1 -1
  64. package/dist/interactions/actions/defaults/anchorEditing.js.map +1 -1
  65. package/dist/interactions/actions/defaults/areaSelect.js.map +1 -1
  66. package/dist/interactions/actions/defaults/booleans.js.map +1 -1
  67. package/dist/interactions/actions/defaults/cancelGesture.js.map +1 -1
  68. package/dist/interactions/actions/defaults/clearSelection.js.map +1 -1
  69. package/dist/interactions/actions/defaults/clipboard.js.map +1 -1
  70. package/dist/interactions/actions/defaults/clone.js.map +1 -1
  71. package/dist/interactions/actions/defaults/createOutlines.js +1 -1
  72. package/dist/interactions/actions/defaults/createOutlines.js.map +1 -1
  73. package/dist/interactions/actions/defaults/createPaintAction.js.map +1 -1
  74. package/dist/interactions/actions/defaults/delete.js.map +1 -1
  75. package/dist/interactions/actions/defaults/distribute.js.map +1 -1
  76. package/dist/interactions/actions/defaults/duplicate.js.map +1 -1
  77. package/dist/interactions/actions/defaults/editAnchors.js.map +1 -1
  78. package/dist/interactions/actions/defaults/enterPathEdit.js.map +1 -1
  79. package/dist/interactions/actions/defaults/enterTextEdit.js.map +1 -1
  80. package/dist/interactions/actions/defaults/escape.js +1 -1
  81. package/dist/interactions/actions/defaults/escape.js.map +1 -1
  82. package/dist/interactions/actions/defaults/exitPathEdit.js.map +1 -1
  83. package/dist/interactions/actions/defaults/flip.js.map +1 -1
  84. package/dist/interactions/actions/defaults/group.js.map +1 -1
  85. package/dist/interactions/actions/defaults/ingest.js.map +1 -1
  86. package/dist/interactions/actions/defaults/insert.js.map +1 -1
  87. package/dist/interactions/actions/defaults/insertPathAnchor.js +1 -1
  88. package/dist/interactions/actions/defaults/insertPathAnchor.js.map +1 -1
  89. package/dist/interactions/actions/defaults/lassoSelect.js.map +1 -1
  90. package/dist/interactions/actions/defaults/move.js.map +1 -1
  91. package/dist/interactions/actions/defaults/nudge.js.map +1 -1
  92. package/dist/interactions/actions/defaults/pinchZoom.js.map +1 -1
  93. package/dist/interactions/actions/defaults/reorder.js.map +1 -1
  94. package/dist/interactions/actions/defaults/requiresSelection.js.map +1 -1
  95. package/dist/interactions/actions/defaults/resize.js.map +1 -1
  96. package/dist/interactions/actions/defaults/rotate.js.map +1 -1
  97. package/dist/interactions/actions/defaults/selectAll.js.map +1 -1
  98. package/dist/interactions/actions/defaults/slice.js.map +1 -1
  99. package/dist/interactions/actions/defaults/toolActivate.js.map +1 -1
  100. package/dist/interactions/actions/defaults/toolResetToDefault.js.map +1 -1
  101. package/dist/interactions/actions/defaults/undoRedo.js +1 -1
  102. package/dist/interactions/actions/defaults/undoRedo.js.map +1 -1
  103. package/dist/interactions/actions/defaults/viewportDragPan.js.map +1 -1
  104. package/dist/interactions/actions/defaults/viewportWheelPan.js.map +1 -1
  105. package/dist/interactions/actions/defaults/viewportZoom.js.map +1 -1
  106. package/dist/interactions/actions/planeInput.js.map +1 -1
  107. package/dist/interactions/gestures/shared/screenTolerance.js.map +1 -1
  108. package/dist/{registerProgram-ateHDGjw.d.ts → registerProgram-BMZRDgrh.d.ts} +4 -0
  109. package/dist/renderer/cache/markerCommandCache.js +46 -42
  110. package/dist/renderer/cache/markerCommandCache.js.map +1 -1
  111. package/dist/renderer/cache/outlineStrokeMeshCache.js +30 -25
  112. package/dist/renderer/cache/outlineStrokeMeshCache.js.map +1 -1
  113. package/dist/renderer/cache/strokeMeshCache.js +42 -31
  114. package/dist/renderer/cache/strokeMeshCache.js.map +1 -1
  115. package/dist/renderer/cullDrawCommands.js +32 -31
  116. package/dist/renderer/cullDrawCommands.js.map +1 -1
  117. package/dist/renderer/draw.js +315 -362
  118. package/dist/renderer/draw.js.map +1 -1
  119. package/dist/renderer/drawBatch.js.map +1 -1
  120. package/dist/renderer/glyphOutlineMesh.js +67 -0
  121. package/dist/renderer/glyphOutlineMesh.js.map +1 -0
  122. package/dist/renderer/math/mat3.js +8 -3
  123. package/dist/renderer/math/mat3.js.map +1 -1
  124. package/dist/renderer/shaders/batchFill.js +3 -2
  125. package/dist/renderer/shaders/batchFill.js.map +1 -1
  126. package/dist/renderer/strokeLengths.js +50 -0
  127. package/dist/renderer/strokeLengths.js.map +1 -0
  128. package/dist/renderer.d.ts +2 -2
  129. package/dist/{test-seams-fYf6m5pu.d.ts → test-seams-CuIxe0xY.d.ts} +1 -1
  130. package/dist/test-seams.d.ts +2 -2
  131. package/dist/tools/builtin/eyedropper/useEyedropperTool.js.map +1 -1
  132. package/dist/tools/builtin/pen/usePenTool.js +1 -1
  133. package/dist/tools/builtin/pen/usePenTool.js.map +1 -1
  134. package/dist/tools/builtin/select/selectionContributions.js +43 -23
  135. package/dist/tools/builtin/select/selectionContributions.js.map +1 -1
  136. package/dist/tools/builtin/select/useSelectTool.js.map +1 -1
  137. package/dist/tools/builtin/slice/useSliceTool.js.map +1 -1
  138. package/dist/version.js +1 -1
  139. package/package.json +17 -16
@@ -4,51 +4,51 @@ import { useFrameLoop as n } from "./useFrameLoop.js";
4
4
  import { useCanvasRenderer as r } from "./useCanvasRenderer.js";
5
5
  import { sceneViewFrame as i } from "./sceneViewRender.js";
6
6
  import { useCallback as a, useEffect as o, useLayoutEffect as s, useRef as c, useSyncExternalStore as l } from "react";
7
- import { useLatest as u } from "@weasel-js/react";
8
- import { jsx as d } from "react/jsx-runtime";
7
+ import { assignRef as u, useLatest as d } from "@weasel-js/react";
8
+ import { jsx as f } from "react/jsx-runtime";
9
9
  //#region src/canvas/SceneViewCanvas.tsx
10
- function f(f) {
11
- let { scene: p, width: m, height: h, drawOne: g, extraCommands: _, alphaFor: v, layerVisibility: y, layerOrder: b, animator: x, className: S, canvasRef: C, cull: w, paintBounds: T } = f, E = e(f.view);
12
- l(p.subscribe, p.getVersion, p.getVersion);
13
- let D = c(null), O = (e) => {
14
- D.current = e, typeof C == "function" ? C(e) : C && typeof C == "object" && (C.current = e);
15
- }, { paint: k } = r(), A = u(() => {
16
- let e = D.current;
10
+ function p(p) {
11
+ let { scene: m, width: h, height: g, drawOne: _, extraCommands: v, alphaFor: y, layerVisibility: b, layerOrder: x, animator: S, className: C, canvasRef: w, cull: T, paintBounds: E } = p, D = e(p.view);
12
+ l(m.subscribe, m.getVersion, m.getVersion);
13
+ let O = c(null), k = a((e) => {
14
+ O.current = e, u(w, e);
15
+ }, [w]), { paint: A } = r(), j = d(() => {
16
+ let e = O.current;
17
17
  if (!e) return !1;
18
18
  let t = i({
19
- scene: p,
20
- view: E,
21
- width: m,
22
- height: h,
23
- drawOne: g,
24
- extraCommands: _,
25
- alphaFor: v,
26
- layerVisibility: y,
27
- layerOrder: b,
28
- colorOverrides: x?.colorOverrides,
29
- cull: w,
30
- paintBounds: T
19
+ scene: m,
20
+ view: D,
21
+ width: h,
22
+ height: g,
23
+ drawOne: _,
24
+ extraCommands: v,
25
+ alphaFor: y,
26
+ layerVisibility: b,
27
+ layerOrder: x,
28
+ colorOverrides: S?.colorOverrides,
29
+ cull: T,
30
+ paintBounds: E
31
31
  });
32
- return k(e, t, E, {
33
- width: m,
34
- height: h
32
+ return A(e, t, D, {
33
+ width: h,
34
+ height: g
35
35
  }), !0;
36
- }), { requestRedraw: j } = n(a(() => A.current(), [A])), M = c(!1);
36
+ }), { requestRedraw: M } = n(a(() => j.current(), [j])), N = c(!1);
37
37
  return s(() => {
38
- if (M.current) {
39
- j();
38
+ if (N.current) {
39
+ M();
40
40
  return;
41
41
  }
42
- M.current = A.current(), M.current || j();
43
- }), o(() => p.overrides.subscribe(j), [p, j]), o(() => x?.onTick(j), [x, j]), t(j), /* @__PURE__ */ d("canvas", {
44
- ref: O,
45
- width: m,
46
- height: h,
47
- className: S
42
+ N.current = j.current(), N.current || M();
43
+ }), o(() => m.overrides.subscribe(M), [m, M]), o(() => S?.onTick(M), [S, M]), t(M), /* @__PURE__ */ f("canvas", {
44
+ ref: k,
45
+ width: h,
46
+ height: g,
47
+ className: C
48
48
  });
49
49
  }
50
- var p = f;
50
+ var m = p;
51
51
  //#endregion
52
- export { p as SceneViewCanvas };
52
+ export { m as SceneViewCanvas };
53
53
 
54
54
  //# sourceMappingURL=SceneViewCanvas.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"SceneViewCanvas.js","names":["SceneViewCanvasInner","props","view","normalizeView","scene","localRef","useRef","setCanvasRef","el","canvasRef","paintCanvas","useCanvasRenderer","paintRef","useLatest","canvas","commands","sceneViewFrame","animator","useFrameLoop","useCallback","paintedRef","useLayoutEffect","requestRedraw","useEffect","useLateContentRedraw","SceneViewCanvas"],"sources":["../../src/canvas/SceneViewCanvas.tsx"],"sourcesContent":["/**\n * `<SceneViewCanvas>` — pointer-inert, read-only render of a `Scene` at a\n * given `View` into its own `<canvas>` DOM element.\n *\n * The component:\n * - mounts its own `<canvas>`,\n * - subscribes to the scene via `useSyncExternalStore(scene.subscribe,\n * scene.getVersion)` so any scene mutation triggers a re-render,\n * - subscribes to `scene.overrides` as well, since override writes never\n * bump the scene version — without this the view would paint document\n * poses while `<SceneCanvas>` paints overridden ones,\n * - paints through `useFrameLoop` rather than from React, so both a render\n * (prop change or version bump) and an override commit just mark the\n * surface dirty and one `requestAnimationFrame` coalesces them. The mount\n * paint is synchronous so the first frame the user sees is the scene, not\n * a blank canvas,\n * - attaches **no** event listeners (consumers attach their own via\n * `canvasRef`),\n * - frees its renderer and every GL object it owns on unmount\n * (`useCanvasRenderer`).\n *\n * Sizing follows `<SceneCanvas>`'s convention: the consumer passes `width` /\n * `height` in CSS pixels. The HTML `width` / `height` attributes of the\n * `<canvas>` (drawing-buffer size) are set to `width × dpr` and `height × dpr`\n * by the canvas renderer (via the underlying renderer's `resize`). The CSS\n * size — what the consumer sees on screen — is managed via the `className`\n * prop; no inline styles are emitted (per project rules).\n */\nimport {\n useCallback,\n useEffect,\n useLayoutEffect,\n useRef,\n useSyncExternalStore,\n} from 'react';\nimport type { Ref } from 'react';\nimport { useLatest } from '@weasel-js/react';\nimport { useFrameLoop } from './useFrameLoop';\nimport { useLateContentRedraw } from './useLateContentRedraw';\nimport { useCanvasRenderer } from './useCanvasRenderer';\nimport { sceneViewFrame } from './sceneViewRender';\nimport type { RenderSceneToCanvasArgs, SceneViewDrawOne, SceneViewLayers } from './sceneViewRender';\nimport type { DrawCommand } from '../renderer/DrawCommand';\nimport { normalizeView, type View } from '../core/viewport/view';\nimport type { Scene } from '../core/scene/types';\nimport type { Animator } from '../animation/types';\n\n/** Props for `<SceneViewCanvas>`. */\nexport interface SceneViewCanvasProps<TData, TLayer extends string, TPose> {\n /** Scene to render. Must be the same `Scene` instance passed to whoever\n * is mutating it; the component subscribes to its version stream and\n * repaints when the version advances. */\n scene: Scene<TData, TLayer, TPose>;\n /** Camera. Applied to scene + `extraCommands` via the renderer's group\n * transform. */\n view: View;\n /** CSS-pixel width. Drives both layout (via the rendered drawing-buffer\n * size) and the renderer's coordinate space. */\n width: number;\n /** CSS-pixel height. */\n height: number;\n /** Per-node draw callback. See `SceneViewDrawOne`. The consumer is\n * responsible for what gets painted — there is no AABB-fill default. */\n drawOne: SceneViewDrawOne<TData, TLayer, TPose>;\n /** Optional extra commands appended after scene commands, inside the same\n * view transform. Used by `<MinimapCanvas>` for its visible-window\n * indicator; available to other consumers that want to overlay a\n * world-space annotation. */\n extraCommands?: ReadonlyArray<DrawCommand>;\n /** Optional per-id alpha multiplier, mirroring `<SceneCanvas>`'s scene-slot\n * `alphaFor`. Pass the same function the main canvas uses to keep a\n * scoping-dim treatment consistent across both. Defaults to `() => 1`. */\n alphaFor?: (id: string) => number;\n /** Hide scene layers in this view only, keyed `scene:<layerId>` as on\n * `<SceneCanvas>` — pass the main canvas's map to keep the two in step. */\n layerVisibility?: SceneViewLayers['layerVisibility'];\n /** Paint order for this view, keyed as `layerVisibility`. */\n layerOrder?: SceneViewLayers['layerOrder'];\n /** Skip nodes and commands that cannot reach this view — the scene slot's\n * `cull`. Off by default; worth it when the view shows a small part of a\n * large scene. */\n cull?: boolean;\n /** Where `drawOne`'s output for a node can reach — the scene slot's\n * `paintBounds`. Defaults to `defaultPaintBounds` when `drawOne` is\n * `defaultDrawOne`. */\n paintBounds?: RenderSceneToCanvasArgs<TData, TLayer, TPose>['paintBounds'];\n /** Optional animator, as on `<SceneCanvas>`: the view repaints on its ticks\n * and paints its `colorOverrides`. Pass the main canvas's animator so both\n * show the same colors. */\n animator?: Animator;\n /** Optional CSS class for sizing / positioning the `<canvas>`. The kit\n * does not emit inline styles for layout — use a class. */\n className?: string;\n /** Forwarded ref to the underlying `<canvas>`. Consumers attach their own\n * pointer listeners or call `toBlob` for snapshots through this. */\n canvasRef?: Ref<HTMLCanvasElement>;\n}\n\nfunction SceneViewCanvasInner<TData, TLayer extends string, TPose>(\n props: SceneViewCanvasProps<TData, TLayer, TPose>,\n) {\n const {\n scene, width, height, drawOne, extraCommands, alphaFor, layerVisibility, layerOrder,\n animator, className, canvasRef, cull, paintBounds,\n } = props;\n const view = normalizeView(props.view);\n\n // Subscribe to scene version. The snapshot value isn't used directly —\n // we only need React to re-render the component when the scene mutates.\n useSyncExternalStore(scene.subscribe, scene.getVersion, scene.getVersion);\n\n const localRef = useRef<HTMLCanvasElement | null>(null);\n\n // Forward the canvas element to the caller's ref. `useImperativeHandle`-on-\n // ref isn't quite right for a plain element; instead, we expose the same\n // element via a callback ref pattern by writing through to the consumer-\n // supplied ref each time React attaches the canvas.\n const setCanvasRef = (el: HTMLCanvasElement | null): void => {\n localRef.current = el;\n if (typeof canvasRef === 'function') {\n canvasRef(el);\n } else if (canvasRef && typeof canvasRef === 'object') {\n (canvasRef as { current: HTMLCanvasElement | null }).current = el;\n }\n };\n\n // The frame loop paints, not React — so an override commit can repaint\n // without a render. The thunk defers to `paintRef`, which every committed\n // render republishes from an insertion effect — ahead of the layout effect\n // below, and never from a render React abandoned.\n const { paint: paintCanvas } = useCanvasRenderer();\n const paint = (): boolean => {\n const canvas = localRef.current;\n if (!canvas) return false;\n const commands = sceneViewFrame({\n scene,\n view,\n width,\n height,\n drawOne,\n extraCommands: extraCommands as DrawCommand[] | undefined,\n alphaFor,\n layerVisibility,\n layerOrder,\n colorOverrides: animator?.colorOverrides,\n cull,\n paintBounds,\n });\n paintCanvas(canvas, commands, view, { width, height });\n return true;\n };\n const paintRef = useLatest(paint);\n\n const { requestRedraw } = useFrameLoop(useCallback(() => paintRef.current(), [paintRef]));\n\n // Every render — a prop change or a scene-version bump — marks the surface\n // dirty. No dependency array: the paint reads all of them.\n //\n // The mount paint is the exception: it runs here, in the caller's stack, so\n // the first frame the user sees is the scene rather than a blank canvas.\n const paintedRef = useRef(false);\n useLayoutEffect(() => {\n if (paintedRef.current) { requestRedraw(); return; }\n paintedRef.current = paintRef.current();\n if (!paintedRef.current) requestRedraw();\n });\n\n // Override writes never bump the scene version, so the version subscription\n // above cannot see them.\n useEffect(() => scene.overrides.subscribe(requestRedraw), [scene, requestRedraw]);\n useEffect(() => animator?.onTick(requestRedraw), [animator, requestRedraw]);\n useLateContentRedraw(requestRedraw);\n\n return (\n <canvas\n ref={setCanvasRef}\n width={width}\n height={height}\n className={className}\n />\n );\n}\n\n/**\n * Pointer-inert read-only scene view. See `SceneViewCanvasProps` for the\n * full surface and the module docstring for the rendering model.\n *\n * `forwardRef` is not used because the component is generic over `<TData,\n * TLayer, TPose>` and React's `forwardRef` erases generics. The\n * `canvasRef` prop is the plain-prop equivalent (the spec calls it out as\n * the public surface).\n */\nexport const SceneViewCanvas = SceneViewCanvasInner;\n"],"mappings":";;;;;;;;;AAkGA,SAASA,EACP,GACA;CACA,IAAM,EACJ,UAAO,UAAO,WAAQ,YAAS,kBAAe,aAAU,oBAAiB,eACzE,aAAU,cAAW,cAAW,SAAM,mBACpCC,GACEC,IAAOC,EAAcF,EAAM,IAAI;CAIrC,EAAqBG,EAAM,WAAWA,EAAM,YAAYA,EAAM,UAAU;CAExE,IAAMC,IAAWC,EAAiC,IAAI,GAMhDC,KAAgB,MAAuC;EAE3D,AADA,EAAS,UAAUC,GACf,OAAOC,KAAc,aACvBA,EAAUD,CAAE,IACHC,KAAa,OAAOA,KAAc,aAC3C,EAAqD,UAAUD;CAEnE,GAMM,EAAE,OAAOE,MAAgBC,EAAkB,GAqB3CC,IAAWC,QApBY;EAC3B,IAAMC,IAAST,EAAS;EACxB,IAAI,CAACS,GAAQ,OAAO;EACpB,IAAMC,IAAWC,EAAe;GAC9B;GACA;GACA;GACA;GACA;GACe;GACf;GACA;GACA;GACA,gBAAgBC,GAAU;GAC1B;GACA;EACF,CAAC;EAED,OADAP,EAAYI,GAAQC,GAAUb,GAAM;GAAE;GAAO;EAAO,CAAC,GAC9C;CACT,CACgC,GAE1B,EAAE,qBAAkBgB,EAAaC,QAAkBP,EAAS,QAAQ,GAAG,CAACA,CAAQ,CAAC,CAAC,GAOlFQ,IAAad,EAAO,EAAK;CAa/B,OAZAe,QAAsB;EACpB,IAAID,EAAW,SAAS;GAAE,EAAc;GAAG;EAAQ;EAEnD,AADA,EAAW,UAAUR,EAAS,QAAQ,GACjCQ,EAAW,WAASE,EAAc;CACzC,CAAC,GAIDC,QAAgBnB,EAAM,UAAU,UAAUkB,CAAa,GAAG,CAAClB,GAAOkB,CAAa,CAAC,GAChFC,QAAgBN,GAAU,OAAOK,CAAa,GAAG,CAACL,GAAUK,CAAa,CAAC,GAC1EE,EAAqBF,CAAa,GAGhC,kBAAC,UAAD;EACE,KAAKf;EACE;EACC;EACG;CACZ,CAAA;AAEL;AAWA,IAAakB,IAAkBzB"}
1
+ {"version":3,"file":"SceneViewCanvas.js","names":["SceneViewCanvasInner","props","view","normalizeView","scene","localRef","useRef","setCanvasRef","useCallback","el","assignRef","canvasRef","paintCanvas","useCanvasRenderer","paintRef","useLatest","canvas","commands","sceneViewFrame","animator","useFrameLoop","paintedRef","useLayoutEffect","requestRedraw","useEffect","useLateContentRedraw","SceneViewCanvas"],"sources":["../../src/canvas/SceneViewCanvas.tsx"],"sourcesContent":["/**\n * `<SceneViewCanvas>` — pointer-inert, read-only render of a `Scene` at a\n * given `View` into its own `<canvas>` DOM element.\n *\n * The component:\n * - mounts its own `<canvas>`,\n * - subscribes to the scene via `useSyncExternalStore(scene.subscribe,\n * scene.getVersion)` so any scene mutation triggers a re-render,\n * - subscribes to `scene.overrides` as well, since override writes never\n * bump the scene version — without this the view would paint document\n * poses while `<SceneCanvas>` paints overridden ones,\n * - paints through `useFrameLoop` rather than from React, so both a render\n * (prop change or version bump) and an override commit just mark the\n * surface dirty and one `requestAnimationFrame` coalesces them. The mount\n * paint is synchronous so the first frame the user sees is the scene, not\n * a blank canvas,\n * - attaches **no** event listeners (consumers attach their own via\n * `canvasRef`),\n * - frees its renderer and every GL object it owns on unmount\n * (`useCanvasRenderer`).\n *\n * Sizing follows `<SceneCanvas>`'s convention: the consumer passes `width` /\n * `height` in CSS pixels. The HTML `width` / `height` attributes of the\n * `<canvas>` (drawing-buffer size) are set to `width × dpr` and `height × dpr`\n * by the canvas renderer (via the underlying renderer's `resize`). The CSS\n * size — what the consumer sees on screen — is managed via the `className`\n * prop; no inline styles are emitted (per project rules).\n */\nimport {\n useCallback,\n useEffect,\n useLayoutEffect,\n useRef,\n useSyncExternalStore,\n} from 'react';\nimport type { Ref } from 'react';\nimport { assignRef, useLatest } from '@weasel-js/react';\nimport { useFrameLoop } from './useFrameLoop';\nimport { useLateContentRedraw } from './useLateContentRedraw';\nimport { useCanvasRenderer } from './useCanvasRenderer';\nimport { sceneViewFrame } from './sceneViewRender';\nimport type { RenderSceneToCanvasArgs, SceneViewDrawOne, SceneViewLayers } from './sceneViewRender';\nimport type { DrawCommand } from '../renderer/DrawCommand';\nimport { normalizeView, type View } from '../core/viewport/view';\nimport type { Scene } from '../core/scene/types';\nimport type { Animator } from '../animation/types';\n\n/** Props for `<SceneViewCanvas>`. */\nexport interface SceneViewCanvasProps<TData, TLayer extends string, TPose> {\n /** Scene to render. Must be the same `Scene` instance passed to whoever\n * is mutating it; the component subscribes to its version stream and\n * repaints when the version advances. */\n scene: Scene<TData, TLayer, TPose>;\n /** Camera. Applied to scene + `extraCommands` via the renderer's group\n * transform. */\n view: View;\n /** CSS-pixel width. Drives both layout (via the rendered drawing-buffer\n * size) and the renderer's coordinate space. */\n width: number;\n /** CSS-pixel height. */\n height: number;\n /** Per-node draw callback. See `SceneViewDrawOne`. The consumer is\n * responsible for what gets painted — there is no AABB-fill default. */\n drawOne: SceneViewDrawOne<TData, TLayer, TPose>;\n /** Optional extra commands appended after scene commands, inside the same\n * view transform. Used by `<MinimapCanvas>` for its visible-window\n * indicator; available to other consumers that want to overlay a\n * world-space annotation. */\n extraCommands?: ReadonlyArray<DrawCommand>;\n /** Optional per-id alpha multiplier, mirroring `<SceneCanvas>`'s scene-slot\n * `alphaFor`. Pass the same function the main canvas uses to keep a\n * scoping-dim treatment consistent across both. Defaults to `() => 1`. */\n alphaFor?: (id: string) => number;\n /** Hide scene layers in this view only, keyed `scene:<layerId>` as on\n * `<SceneCanvas>` — pass the main canvas's map to keep the two in step. */\n layerVisibility?: SceneViewLayers['layerVisibility'];\n /** Paint order for this view, keyed as `layerVisibility`. */\n layerOrder?: SceneViewLayers['layerOrder'];\n /** Skip nodes and commands that cannot reach this view — the scene slot's\n * `cull`. Off by default; worth it when the view shows a small part of a\n * large scene. */\n cull?: boolean;\n /** Where `drawOne`'s output for a node can reach — the scene slot's\n * `paintBounds`. Defaults to `defaultPaintBounds` when `drawOne` is\n * `defaultDrawOne`. */\n paintBounds?: RenderSceneToCanvasArgs<TData, TLayer, TPose>['paintBounds'];\n /** Optional animator, as on `<SceneCanvas>`: the view repaints on its ticks\n * and paints its `colorOverrides`. Pass the main canvas's animator so both\n * show the same colors. */\n animator?: Animator;\n /** Optional CSS class for sizing / positioning the `<canvas>`. The kit\n * does not emit inline styles for layout — use a class. */\n className?: string;\n /** Forwarded ref to the underlying `<canvas>`. Consumers attach their own\n * pointer listeners or call `toBlob` for snapshots through this. */\n canvasRef?: Ref<HTMLCanvasElement>;\n}\n\nfunction SceneViewCanvasInner<TData, TLayer extends string, TPose>(\n props: SceneViewCanvasProps<TData, TLayer, TPose>,\n) {\n const {\n scene, width, height, drawOne, extraCommands, alphaFor, layerVisibility, layerOrder,\n animator, className, canvasRef, cull, paintBounds,\n } = props;\n const view = normalizeView(props.view);\n\n // Subscribe to scene version. The snapshot value isn't used directly —\n // we only need React to re-render the component when the scene mutates.\n useSyncExternalStore(scene.subscribe, scene.getVersion, scene.getVersion);\n\n const localRef = useRef<HTMLCanvasElement | null>(null);\n\n const setCanvasRef = useCallback((el: HTMLCanvasElement | null): void => {\n localRef.current = el;\n assignRef(canvasRef, el);\n }, [canvasRef]);\n\n // The frame loop paints, not React — so an override commit can repaint\n // without a render. The thunk defers to `paintRef`, which every committed\n // render republishes from an insertion effect — ahead of the layout effect\n // below, and never from a render React abandoned.\n const { paint: paintCanvas } = useCanvasRenderer();\n const paint = (): boolean => {\n const canvas = localRef.current;\n if (!canvas) return false;\n const commands = sceneViewFrame({\n scene,\n view,\n width,\n height,\n drawOne,\n extraCommands: extraCommands as DrawCommand[] | undefined,\n alphaFor,\n layerVisibility,\n layerOrder,\n colorOverrides: animator?.colorOverrides,\n cull,\n paintBounds,\n });\n paintCanvas(canvas, commands, view, { width, height });\n return true;\n };\n const paintRef = useLatest(paint);\n\n const { requestRedraw } = useFrameLoop(useCallback(() => paintRef.current(), [paintRef]));\n\n // Every render — a prop change or a scene-version bump — marks the surface\n // dirty. No dependency array: the paint reads all of them.\n //\n // The mount paint is the exception: it runs here, in the caller's stack, so\n // the first frame the user sees is the scene rather than a blank canvas.\n const paintedRef = useRef(false);\n useLayoutEffect(() => {\n if (paintedRef.current) { requestRedraw(); return; }\n paintedRef.current = paintRef.current();\n if (!paintedRef.current) requestRedraw();\n });\n\n // Override writes never bump the scene version, so the version subscription\n // above cannot see them.\n useEffect(() => scene.overrides.subscribe(requestRedraw), [scene, requestRedraw]);\n useEffect(() => animator?.onTick(requestRedraw), [animator, requestRedraw]);\n useLateContentRedraw(requestRedraw);\n\n return (\n <canvas\n ref={setCanvasRef}\n width={width}\n height={height}\n className={className}\n />\n );\n}\n\n/**\n * Pointer-inert read-only scene view. See `SceneViewCanvasProps` for the\n * full surface and the module docstring for the rendering model.\n *\n * `forwardRef` is not used because the component is generic over `<TData,\n * TLayer, TPose>` and React's `forwardRef` erases generics. The\n * `canvasRef` prop is the plain-prop equivalent (the spec calls it out as\n * the public surface).\n */\nexport const SceneViewCanvas = SceneViewCanvasInner;\n"],"mappings":";;;;;;;;;AAkGA,SAASA,EACP,GACA;CACA,IAAM,EACJ,UAAO,UAAO,WAAQ,YAAS,kBAAe,aAAU,oBAAiB,eACzE,aAAU,cAAW,cAAW,SAAM,mBACpCC,GACEC,IAAOC,EAAcF,EAAM,IAAI;CAIrC,EAAqBG,EAAM,WAAWA,EAAM,YAAYA,EAAM,UAAU;CAExE,IAAMC,IAAWC,EAAiC,IAAI,GAEhDC,IAAeC,GAAa,MAAuC;EAEvE,AADA,EAAS,UAAUC,GACnBC,EAAUC,GAAWF,CAAE;CACzB,GAAG,CAACE,CAAS,CAAC,GAMR,EAAE,OAAOC,MAAgBC,EAAkB,GAqB3CC,IAAWC,QApBY;EAC3B,IAAMC,IAASX,EAAS;EACxB,IAAI,CAACW,GAAQ,OAAO;EACpB,IAAMC,IAAWC,EAAe;GAC9B;GACA;GACA;GACA;GACA;GACe;GACf;GACA;GACA;GACA,gBAAgBC,GAAU;GAC1B;GACA;EACF,CAAC;EAED,OADAP,EAAYI,GAAQC,GAAUf,GAAM;GAAE;GAAO;EAAO,CAAC,GAC9C;CACT,CACgC,GAE1B,EAAE,qBAAkBkB,EAAaZ,QAAkBM,EAAS,QAAQ,GAAG,CAACA,CAAQ,CAAC,CAAC,GAOlFO,IAAaf,EAAO,EAAK;CAa/B,OAZAgB,QAAsB;EACpB,IAAID,EAAW,SAAS;GAAE,EAAc;GAAG;EAAQ;EAEnD,AADA,EAAW,UAAUP,EAAS,QAAQ,GACjCO,EAAW,WAASE,EAAc;CACzC,CAAC,GAIDC,QAAgBpB,EAAM,UAAU,UAAUmB,CAAa,GAAG,CAACnB,GAAOmB,CAAa,CAAC,GAChFC,QAAgBL,GAAU,OAAOI,CAAa,GAAG,CAACJ,GAAUI,CAAa,CAAC,GAC1EE,EAAqBF,CAAa,GAGhC,kBAAC,UAAD;EACE,KAAKhB;EACE;EACC;EACG;CACZ,CAAA;AAEL;AAWA,IAAamB,IAAkB1B"}
@@ -32,7 +32,12 @@ function s(n, i, a, s) {
32
32
  }
33
33
  function c(e, t, r) {
34
34
  let i = e.data;
35
- return i?.label && i.text == null ? null : n(e, t, { scale: Math.sqrt(Math.abs(r.scale.x * r.scale.y)) });
35
+ if (i?.label && i.text == null) return null;
36
+ let a = Math.abs(r.scale.x), o = Math.abs(r.scale.y);
37
+ return n(e, t, {
38
+ scale: Math.sqrt(a * o),
39
+ leastScale: Math.min(a, o)
40
+ });
36
41
  }
37
42
  function l(e) {
38
43
  return e === s ? c : void 0;
@@ -1 +1 @@
1
- {"version":3,"file":"defaultDrawOne.js","names":["pixelScaleOf","dpr","view","lastView","lastViewCtx","viewOnlyCtx","pixelScale","defaultDrawOne","painter","findNodeShape","node","paintCtx","ctx","primary","pose","data","p","textCommandFromRuns","defaultPaintBounds","findShapeBounds","paintBoundsFor","drawOne"],"sources":["../../src/canvas/defaultDrawOne.ts"],"sourcesContent":["/**\n * `defaultDrawOne` — the kit's default per-node draw function.\n *\n * Extracted from `SceneCanvas.tsx` so non-React callers (headless\n * rasterization via `renderSceneToPixels`) can use it without importing\n * the React component tree.\n *\n * Default scene-slot `drawOne` — dispatches through the shape-painter\n * registry (`./NodeShape`). The kit registers built-in painters for\n * text (`kit:text`), paths (`kit:path`), and a rect-from-pose fallback\n * (`kit:rect-fallback`) at module load, so every shape it ships out of\n * the box (rect, ellipse, polygon, star, line, pen, pencil, text) paints\n * without consumer intervention.\n *\n * To teach the kit about a new kind of shape, register a painter — do\n * not override `drawOne`. See `registerNodeShape` for the API and\n * priority semantics. Override `drawOne` only for cross-cutting\n * decoration (post-process every node, mix in overlays from outside\n * the per-node data, etc.).\n *\n * This function also emits an optional `data.label` overlay (sans-serif\n * 11px, top-left) on every non-text painter's output — a convenience\n * for naming zones in demos. Nodes whose painter is `kit:text` skip the\n * overlay since their content already shows.\n */\nimport type { Node } from 'core/scene/types';\nimport type { View } from 'core/viewport/view';\nimport type { DrawCommand } from '../renderer';\nimport { textCommandFromRuns } from 'features/text/textCommand';\nimport { findNodeShape, findShapeBounds, type NodePaintCtx } from './NodeShape';\nimport type { Bounds } from 'core/viewport/fitViewToBounds';\n\nfunction pixelScaleOf(view: View): number {\n const dpr = (typeof window !== 'undefined' && window.devicePixelRatio) || 1;\n return Math.max(Math.abs(view.scale.x), Math.abs(view.scale.y)) * dpr;\n}\n\n// Shared across a frame's nodes, which all arrive with the same view.\nlet lastView: View | undefined;\nlet lastViewCtx: NodePaintCtx = {};\nfunction viewOnlyCtx(view: View): NodePaintCtx {\n const pixelScale = pixelScaleOf(view);\n if (view !== lastView || lastViewCtx.pixelScale !== pixelScale) {\n lastView = view;\n lastViewCtx = { pixelScale };\n }\n return lastViewCtx;\n}\n\n/** Paint one node the way the kit does by default: dispatch to the first\n * registered node-shape painter that matches, then draw its `data.label` on\n * top when it has one. Consumers replace or wrap this via `drawOne`. */\nexport function defaultDrawOne<TData, TLayer extends string, TPose>(\n node: Node<TData, TLayer, TPose>,\n pose: TPose,\n view?: View,\n ctx?: NodePaintCtx,\n): DrawCommand[] {\n const painter = findNodeShape(node);\n const paintCtx = !view || ctx?.pixelScale !== undefined\n ? ctx\n : ctx ? { ...ctx, pixelScale: pixelScaleOf(view) } : viewOnlyCtx(view);\n const primary = painter ? painter.paint(node, pose, paintCtx) : [];\n\n // Label overlay — skipped for text nodes (their content is the label).\n const data = node.data as { label?: string; text?: string } | null;\n if (data?.label && data.text == null) {\n const p = pose as unknown as { x: number; y: number };\n // Copy rather than `push`. A painter's return value is not ours to grow:\n // one that memoizes its command list — which is the point of caching\n // `paint` — would gain another label command on every frame, forever.\n return [...primary, textCommandFromRuns(\n p.x + 6,\n p.y + 14,\n [{ text: data.label, fill: { fill: 'solid', color: 'rgba(0,0,0,0.7)' } }],\n { fontFamily: 'sans-serif', fontSize: 11 },\n )];\n }\n\n // Rotation and per-node alpha are applied OUTSIDE this function, by\n // `wrapNodeOutput` in the scene walks (`buildSceneLayer`,\n // `buildSceneViewCommands`) and the preview-ghost layer, so that\n // consumer-supplied painters get them too. Doing it here would double-wrap.\n return primary;\n}\n\n/**\n * The box {@link defaultDrawOne}'s output for `node` fits in — the matched\n * painter's `bounds` — or `null` when it has none, or when a `data.label`\n * overlay adds text nothing bounds cheaply. The scene slot's `cull` reads it\n * to skip painting nodes the view cannot see.\n */\nexport function defaultPaintBounds<TData, TLayer extends string, TPose>(\n node: Node<TData, TLayer, TPose>,\n pose: TPose,\n view: View,\n): Bounds | null {\n const data = node.data as { label?: string; text?: string } | null;\n if (data?.label && data.text == null) return null;\n return findShapeBounds(node, pose, { scale: Math.sqrt(Math.abs(view.scale.x * view.scale.y)) });\n}\n\n/**\n * `defaultPaintBounds` when `drawOne` is `defaultDrawOne`, and nothing\n * otherwise: a painter's box says nothing about what a replacement\n * `drawOne` paints.\n */\nexport function paintBoundsFor<TData, TLayer extends string, TPose>(\n drawOne: unknown,\n): typeof defaultPaintBounds<TData, TLayer, TPose> | undefined {\n return drawOne === defaultDrawOne ? defaultPaintBounds : undefined;\n}\n"],"mappings":";;;AAgCA,SAASA,EAAa,GAAoB;CACxC,IAAMC,IAAO,OAAO,SAAW,OAAe,OAAO,oBAAqB;CAC1E,OAAO,KAAK,IAAI,KAAK,IAAIC,EAAK,MAAM,CAAC,GAAG,KAAK,IAAIA,EAAK,MAAM,CAAC,CAAC,IAAID;AACpE;AAGA,IAAIE,GACAC,IAA4B,CAAC;AACjC,SAASC,EAAY,GAA0B;CAC7C,IAAMC,IAAaN,EAAaE,CAAI;CAKpC,QAJIA,MAASC,KAAYC,EAAY,eAAeE,OAClD,IAAWJ,GACX,IAAc,EAAE,cAAW,IAEtBE;AACT;AAKA,SAAgBG,EACd,GACA,GACA,GACA,GACe;CACf,IAAMC,IAAUC,EAAcC,CAAI,GAC5BC,IAAW,CAACT,KAAQU,GAAK,eAAe,KAAA,IAC1CA,IACAA,IAAM;EAAE,GAAGA;EAAK,YAAYZ,EAAaE,CAAI;CAAE,IAAIG,EAAYH,CAAI,GACjEW,IAAUL,IAAUA,EAAQ,MAAME,GAAMI,GAAMH,CAAQ,IAAI,CAAC,GAG3DI,IAAOL,EAAK;CAClB,IAAIK,GAAM,SAASA,EAAK,QAAQ,MAAM;EACpC,IAAMC,IAAIF;EAIV,OAAO,CAAC,GAAGD,GAASI,EAClBD,EAAE,IAAI,GACNA,EAAE,IAAI,IACN,CAAC;GAAE,MAAMD,EAAK;GAAO,MAAM;IAAE,MAAM;IAAS,OAAO;GAAkB;EAAE,CAAC,GACxE;GAAE,YAAY;GAAc,UAAU;EAAG,CAC3C,CAAC;CACH;CAMA,OAAOF;AACT;AAQA,SAAgBK,EACd,GACA,GACA,GACe;CACf,IAAMH,IAAOL,EAAK;CAElB,OADIK,GAAM,SAASA,EAAK,QAAQ,OAAa,OACtCI,EAAgBT,GAAMI,GAAM,EAAE,OAAO,KAAK,KAAK,KAAK,IAAIZ,EAAK,MAAM,IAAIA,EAAK,MAAM,CAAC,CAAC,EAAE,CAAC;AAChG;AAOA,SAAgBkB,EACd,GAC6D;CAC7D,OAAOC,MAAYd,IAAiBW,IAAqB,KAAA;AAC3D"}
1
+ {"version":3,"file":"defaultDrawOne.js","names":["pixelScaleOf","dpr","view","lastView","lastViewCtx","viewOnlyCtx","pixelScale","defaultDrawOne","painter","findNodeShape","node","paintCtx","ctx","primary","pose","data","p","textCommandFromRuns","defaultPaintBounds","sx","sy","findShapeBounds","paintBoundsFor","drawOne"],"sources":["../../src/canvas/defaultDrawOne.ts"],"sourcesContent":["/**\n * `defaultDrawOne` — the kit's default per-node draw function.\n *\n * Extracted from `SceneCanvas.tsx` so non-React callers (headless\n * rasterization via `renderSceneToPixels`) can use it without importing\n * the React component tree.\n *\n * Default scene-slot `drawOne` — dispatches through the shape-painter\n * registry (`./NodeShape`). The kit registers built-in painters for\n * text (`kit:text`), paths (`kit:path`), and a rect-from-pose fallback\n * (`kit:rect-fallback`) at module load, so every shape it ships out of\n * the box (rect, ellipse, polygon, star, line, pen, pencil, text) paints\n * without consumer intervention.\n *\n * To teach the kit about a new kind of shape, register a painter — do\n * not override `drawOne`. See `registerNodeShape` for the API and\n * priority semantics. Override `drawOne` only for cross-cutting\n * decoration (post-process every node, mix in overlays from outside\n * the per-node data, etc.).\n *\n * This function also emits an optional `data.label` overlay (sans-serif\n * 11px, top-left) on every non-text painter's output — a convenience\n * for naming zones in demos. Nodes whose painter is `kit:text` skip the\n * overlay since their content already shows.\n */\nimport type { Node } from 'core/scene/types';\nimport type { View } from 'core/viewport/view';\nimport type { DrawCommand } from '../renderer';\nimport { textCommandFromRuns } from 'features/text/textCommand';\nimport { findNodeShape, findShapeBounds, type NodePaintCtx } from './NodeShape';\nimport type { Bounds } from 'core/viewport/fitViewToBounds';\n\nfunction pixelScaleOf(view: View): number {\n const dpr = (typeof window !== 'undefined' && window.devicePixelRatio) || 1;\n return Math.max(Math.abs(view.scale.x), Math.abs(view.scale.y)) * dpr;\n}\n\n// Shared across a frame's nodes, which all arrive with the same view.\nlet lastView: View | undefined;\nlet lastViewCtx: NodePaintCtx = {};\nfunction viewOnlyCtx(view: View): NodePaintCtx {\n const pixelScale = pixelScaleOf(view);\n if (view !== lastView || lastViewCtx.pixelScale !== pixelScale) {\n lastView = view;\n lastViewCtx = { pixelScale };\n }\n return lastViewCtx;\n}\n\n/** Paint one node the way the kit does by default: dispatch to the first\n * registered node-shape painter that matches, then draw its `data.label` on\n * top when it has one. Consumers replace or wrap this via `drawOne`. */\nexport function defaultDrawOne<TData, TLayer extends string, TPose>(\n node: Node<TData, TLayer, TPose>,\n pose: TPose,\n view?: View,\n ctx?: NodePaintCtx,\n): DrawCommand[] {\n const painter = findNodeShape(node);\n const paintCtx = !view || ctx?.pixelScale !== undefined\n ? ctx\n : ctx ? { ...ctx, pixelScale: pixelScaleOf(view) } : viewOnlyCtx(view);\n const primary = painter ? painter.paint(node, pose, paintCtx) : [];\n\n // Label overlay — skipped for text nodes (their content is the label).\n const data = node.data as { label?: string; text?: string } | null;\n if (data?.label && data.text == null) {\n const p = pose as unknown as { x: number; y: number };\n // Copy rather than `push`. A painter's return value is not ours to grow:\n // one that memoizes its command list — which is the point of caching\n // `paint` — would gain another label command on every frame, forever.\n return [...primary, textCommandFromRuns(\n p.x + 6,\n p.y + 14,\n [{ text: data.label, fill: { fill: 'solid', color: 'rgba(0,0,0,0.7)' } }],\n { fontFamily: 'sans-serif', fontSize: 11 },\n )];\n }\n\n // Rotation and per-node alpha are applied OUTSIDE this function, by\n // `wrapNodeOutput` in the scene walks (`buildSceneLayer`,\n // `buildSceneViewCommands`) and the preview-ghost layer, so that\n // consumer-supplied painters get them too. Doing it here would double-wrap.\n return primary;\n}\n\n/**\n * The box {@link defaultDrawOne}'s output for `node` fits in — the matched\n * painter's `bounds` — or `null` when it has none, or when a `data.label`\n * overlay adds text nothing bounds cheaply. The scene slot's `cull` reads it\n * to skip painting nodes the view cannot see.\n */\nexport function defaultPaintBounds<TData, TLayer extends string, TPose>(\n node: Node<TData, TLayer, TPose>,\n pose: TPose,\n view: View,\n): Bounds | null {\n const data = node.data as { label?: string; text?: string } | null;\n if (data?.label && data.text == null) return null;\n const sx = Math.abs(view.scale.x), sy = Math.abs(view.scale.y);\n return findShapeBounds(node, pose, { scale: Math.sqrt(sx * sy), leastScale: Math.min(sx, sy) });\n}\n\n/**\n * `defaultPaintBounds` when `drawOne` is `defaultDrawOne`, and nothing\n * otherwise: a painter's box says nothing about what a replacement\n * `drawOne` paints.\n */\nexport function paintBoundsFor<TData, TLayer extends string, TPose>(\n drawOne: unknown,\n): typeof defaultPaintBounds<TData, TLayer, TPose> | undefined {\n return drawOne === defaultDrawOne ? defaultPaintBounds : undefined;\n}\n"],"mappings":";;;AAgCA,SAASA,EAAa,GAAoB;CACxC,IAAMC,IAAO,OAAO,SAAW,OAAe,OAAO,oBAAqB;CAC1E,OAAO,KAAK,IAAI,KAAK,IAAIC,EAAK,MAAM,CAAC,GAAG,KAAK,IAAIA,EAAK,MAAM,CAAC,CAAC,IAAID;AACpE;AAGA,IAAIE,GACAC,IAA4B,CAAC;AACjC,SAASC,EAAY,GAA0B;CAC7C,IAAMC,IAAaN,EAAaE,CAAI;CAKpC,QAJIA,MAASC,KAAYC,EAAY,eAAeE,OAClD,IAAWJ,GACX,IAAc,EAAE,cAAW,IAEtBE;AACT;AAKA,SAAgBG,EACd,GACA,GACA,GACA,GACe;CACf,IAAMC,IAAUC,EAAcC,CAAI,GAC5BC,IAAW,CAACT,KAAQU,GAAK,eAAe,KAAA,IAC1CA,IACAA,IAAM;EAAE,GAAGA;EAAK,YAAYZ,EAAaE,CAAI;CAAE,IAAIG,EAAYH,CAAI,GACjEW,IAAUL,IAAUA,EAAQ,MAAME,GAAMI,GAAMH,CAAQ,IAAI,CAAC,GAG3DI,IAAOL,EAAK;CAClB,IAAIK,GAAM,SAASA,EAAK,QAAQ,MAAM;EACpC,IAAMC,IAAIF;EAIV,OAAO,CAAC,GAAGD,GAASI,EAClBD,EAAE,IAAI,GACNA,EAAE,IAAI,IACN,CAAC;GAAE,MAAMD,EAAK;GAAO,MAAM;IAAE,MAAM;IAAS,OAAO;GAAkB;EAAE,CAAC,GACxE;GAAE,YAAY;GAAc,UAAU;EAAG,CAC3C,CAAC;CACH;CAMA,OAAOF;AACT;AAQA,SAAgBK,EACd,GACA,GACA,GACe;CACf,IAAMH,IAAOL,EAAK;CAClB,IAAIK,GAAM,SAASA,EAAK,QAAQ,MAAM,OAAO;CAC7C,IAAMI,IAAK,KAAK,IAAIjB,EAAK,MAAM,CAAC,GAAGkB,IAAK,KAAK,IAAIlB,EAAK,MAAM,CAAC;CAC7D,OAAOmB,EAAgBX,GAAMI,GAAM;EAAE,OAAO,KAAK,KAAKK,IAAKC,CAAE;EAAG,YAAY,KAAK,IAAID,GAAIC,CAAE;CAAE,CAAC;AAChG;AAOA,SAAgBE,EACd,GAC6D;CAC7D,OAAOC,MAAYhB,IAAiBW,IAAqB,KAAA;AAC3D"}
@@ -1 +1 @@
1
- {"version":3,"file":"pickWalk.js","names":["EMPTY_PARENTS","ownClipOf","node","pose","findShapeSilhouette","derived","pickWalk","includeContainers","q","isLocked","src","out","clipPath","carry","clipCarrier","clipOf","cached","ancestor","resolved","planeQueries","queryFor","layer","found","map","query","clipped","clip","lockedNodeTest","locked","scene","lockedLayers","cur","asNodeId","id","seen","hiddenLayerIds","layers","scenePickSource","hidden","opts","faded","enumerable","alphaOf","a","readAlpha","composition","chain","effectivePose","definesFrame","poseOf","getPose","composeWorldPose","childrenOf","depOf","dep","resolveDerivedPath","derivedPathOf","n","planeOf","scenePlaneOf","parentId","parent","layerIsPainted","maps","l","planeMap","camera","adapterPickSource","hier","adapter","hierarchical","parents","worldPose","nodeOf","next","visit","childId","gates"],"sources":["../../src/canvas/pickWalk.ts"],"sourcesContent":["/**\n * The one hit-test walk.\n *\n * Four walks used to answer this question — two point queries, two area\n * queries — over the same tree with the same gates. They drifted three times\n * without a failing test, because nothing held them to each other. Everything\n * that decides whether a node *can* be hit lives here now; a query supplies\n * only the shape test and the clip predicate for its own region.\n *\n * The gates, in the order the walk applies them:\n * 1. containers, when the query does not want them\n * 2. the layer is painted at all — scene visibility, and the asking view's\n * `layerVisibility` / `layerOrder`\n * 3. painted alpha is above zero\n * 4. the node is not locked, unless the query asks for locked nodes\n * 5. the query's own shape test\n * 6. every ancestor clip admits the query region\n *\n * 2, 3 and 6 are all \"the renderer did not put this on screen\". A pick path\n * that skips one answers for a node the user cannot see. 4 is the other axis:\n * a locked node is on screen and still out of reach.\n */\n\nimport type { Path } from '@weasel-js/geom';\nimport { findShapeSilhouette } from 'canvas/NodeShape';\nimport type { DerivedDep, Node, NodeId, Scene } from 'core/scene/types';\nimport { asNodeId } from 'core/scene/types';\nimport { definesFrame, effectivePose } from 'core/scene/effectivePose';\nimport { resolveDerivedPath } from './derivedPath';\nimport { composeWorldPose, type PoseAdapter, type PoseComposition } from 'features/groups/composePose';\nimport { planeMap, type ParallaxOpts, type PlaneMap } from 'core/viewport/parallax';\nimport type { View } from 'core/viewport/view';\nimport { clipCarrier } from './planeClips';\n\n/** Shared, never mutated: most scenes are flat and every node returns it. */\nconst EMPTY_PARENTS: readonly never[] = [];\n\n/** The subset of a node the walk itself reads. Deliberately structural: a\n * bare adapter's node satisfies it as readily as a `SceneNode`. */\nexport interface PickCandidate<TPose> {\n id: string;\n kind?: string;\n layer?: string;\n pose: TPose;\n /** `false` makes the node transparent to the walk — see `SceneNode.pickable`. */\n pickable?: boolean;\n clipFromPose?: (pose: TPose) => Path | null;\n}\n\n/**\n * Everything the walk reads that a **view** owns.\n *\n * A layer must not hold state a view owns, so none of these are closed over\n * at construction — each is asked per walk, of the view being dispatched to.\n */\nexport interface PickSource<TPose> {\n /** Candidates in paint order, back-to-front. Containers included; the\n * query decides whether they can be hit. */\n order(): readonly PickCandidate<TPose>[];\n /** The pose the renderer draws — override-aware. Not `node.pose`, which is\n * how the render and pick paths came to disagree about where a node is. */\n poseOf(node: PickCandidate<TPose>): TPose;\n /** Ancestors, innermost first. Only walked for candidates that already\n * passed their own shape test. */\n parentsOf(node: PickCandidate<TPose>): readonly PickCandidate<TPose>[];\n /** Painted alpha for the asking view: the view's `alphaFor` times any\n * per-node override alpha. Omit when the source has no alpha concept. */\n alphaOf?(id: string): number;\n /** Whether this layer reaches the screen in the asking view. Omit for a\n * source with no layers. */\n layerIsPainted?(layer: string): boolean;\n /** Whether the node is locked — painted, but not something a pointer can\n * take. Omit for a source with no lock concept. */\n isLocked?(node: PickCandidate<TPose>): boolean;\n /** The path a node computes from its dependencies' poses, or null when it\n * derives nothing. Only a scene-backed source can answer — deriving needs\n * the dependencies' poses — so a bare adapter omits it, and every derived\n * node there keeps answering from its own (placeholder) pose. */\n derivedPathOf?(node: PickCandidate<TPose>): Path | null;\n /** How the asking view's world maps into a parallax layer's, or null for a\n * layer that moves with the camera. Omit for a source with no planes. */\n planeOf?(layer: string): PlaneMap | null;\n}\n\n/** One hit-test question: what region, and what counts as covering it. */\nexport interface PickQuery<TPose> {\n /** Default `true`. The area walks exclude containers so a marquee does not\n * return a container and its children both. */\n includeContainers?: boolean;\n /** Default `false`. Every selecting or editing query passes over locked\n * nodes; one that only samples what is painted can take them back. */\n includeLocked?: boolean;\n /** Does this node, drawn at this pose, cover the query region? `derived`\n * is the node's resolved path when it has one — a derived node's pose is a\n * placeholder, so a region test that reads only the pose answers for the\n * wrong box. */\n hits(node: PickCandidate<TPose>, pose: TPose, derived?: Path | null): boolean;\n /**\n * Does an ancestor's clip leave anything of `node` for this query to hit?\n *\n * A point query asks point-in-clip and ignores the node. An area query has\n * to ask about the node as well: the clip and the area can overlap where\n * the node is not, and the node and the clip can overlap where the area is\n * not, and only the two terms together reject both.\n */\n clipAdmits(\n clip: Path,\n node: PickCandidate<TPose>,\n pose: TPose,\n derived?: Path | null,\n ): boolean;\n /** The same question asked in a parallax plane's world: the region carried\n * through `map`, and any screen-pixel slop re-derived at the plane's scale.\n * A query without it cannot reach nodes on a plane — it would test them\n * where their poses sit rather than where they are painted. */\n inPlane?(map: PlaneMap): PickQuery<TPose>;\n}\n\n/** The clip a container imposes on its subtree, or null when it imposes none. */\nexport function ownClipOf<TPose>(\n node: PickCandidate<TPose>,\n pose: TPose,\n derived?: Path | null,\n): Path | null {\n if (node.kind !== 'container') return null;\n if (typeof node.clipFromPose === 'function') return node.clipFromPose(pose);\n return findShapeSilhouette(\n node as unknown as Node<unknown, string, TPose>,\n pose,\n { derivedPath: derived },\n );\n}\n\n/**\n * Collect every candidate the query hits, back-to-front — the last element is\n * the topmost, which is the order `pickTopMostHit` expects.\n */\nexport function pickWalk<TPose>(\n src: PickSource<TPose>,\n q: PickQuery<TPose>,\n): string[] {\n const includeContainers = q.includeContainers ?? true;\n const isLocked = q.includeLocked === true ? undefined : src.isLocked;\n const out: string[] = [];\n\n // Resolving a container's clip means `clipFromPose` or a silhouette build,\n // which is the expensive half; the predicate against it is cheap. So the\n // *path* is memoized per container for the walk and the predicate re-runs\n // per candidate — `clipAdmits` needs the candidate, and caching its answer\n // per container would answer the wrong question for an area query.\n const clipPath = new Map<string, Path | null>();\n // A clip is in its container's plane; the query is in the candidate's.\n const carry = clipCarrier(src.planeOf ?? null);\n\n const clipOf = (ancestor: PickCandidate<TPose>): Path | null => {\n const cached = clipPath.get(ancestor.id);\n if (cached !== undefined) return cached;\n const resolved = ownClipOf(\n ancestor, src.poseOf(ancestor), src.derivedPathOf?.(ancestor),\n );\n clipPath.set(ancestor.id, resolved);\n return resolved;\n };\n\n // One reprojected query per plane per walk; `null` marks a plane this\n // query cannot reach.\n const planeQueries = new Map<string, PickQuery<TPose> | null>();\n const queryFor = (layer: string | undefined): PickQuery<TPose> | null => {\n if (layer === undefined || src.planeOf === undefined) return q;\n let found = planeQueries.get(layer);\n if (found === undefined) {\n const map = src.planeOf(layer);\n found = map === null ? q : (q.inPlane?.(map) ?? null);\n planeQueries.set(layer, found);\n }\n return found;\n };\n\n for (const node of src.order()) {\n if (!includeContainers && node.kind === 'container') continue;\n // Transparent to picking: the click belongs to whatever is behind it,\n // which for a label inside a box is the box.\n if (node.pickable === false) continue;\n if (node.layer !== undefined && src.layerIsPainted?.(node.layer) === false) continue;\n if (src.alphaOf !== undefined && src.alphaOf(node.id) <= 0) continue;\n if (isLocked !== undefined && isLocked(node)) continue;\n\n const query = queryFor(node.layer);\n if (query === null) continue;\n const pose = src.poseOf(node);\n const derived = src.derivedPathOf?.(node);\n if (!query.hits(node, pose, derived)) continue;\n\n let clipped = false;\n for (const ancestor of src.parentsOf(node)) {\n const clip = clipOf(ancestor);\n if (clip === null) continue;\n if (!query.clipAdmits(carry(clip, ancestor.layer, node.layer), node, pose, derived)) {\n clipped = true;\n break;\n }\n }\n if (clipped) continue;\n\n out.push(node.id);\n }\n\n return out;\n}\n\n// ---------------------------------------------------------------------------\n// Sources\n// ---------------------------------------------------------------------------\n\n/** Answers `Scene.isLocked` from the layer records and parent links alone,\n * so a partial scene stand-in works too. Null when nothing is locked, which\n * is what keeps the gate off the walk in the common case. */\nfunction lockedNodeTest(\n scene: Pick<Scene<unknown, string, unknown>, 'layers' | 'get'>,\n): ((id: string) => boolean) | null {\n let locked: Set<string> | null = null;\n for (const layer of scene.layers ?? []) {\n if (layer.locked === true) (locked ??= new Set()).add(layer.id);\n }\n if (locked === null) return null;\n const lockedLayers = locked;\n return (id) => {\n let cur = scene.get(asNodeId(id));\n const seen = new Set<string>();\n while (cur !== undefined && !seen.has(cur.id)) {\n if (lockedLayers.has(cur.layer)) return true;\n seen.add(cur.id);\n cur = cur.parent === null ? undefined : scene.get(cur.parent);\n }\n return false;\n };\n}\n\n/** Layer ids the scene reports as hidden. Only an explicit `false` hides a\n * layer: a partial scene stand-in that omits the flag stays pickable. */\nexport function hiddenLayerIds(\n layers: readonly { id: string; visible?: boolean }[] | undefined,\n): Set<string> {\n const out = new Set<string>();\n for (const layer of layers ?? []) {\n if (layer.visible === false) out.add(layer.id);\n }\n return out;\n}\n\n/**\n * The two answers a pick needs from the view it is asked for rather than from\n * the data behind it: how that view paints a node, and whether it paints a\n * layer at all. A scene and a bare adapter take the same pair.\n */\nexport interface ViewPickGates {\n /** Painted alpha: the view's `alphaFor` times any per-node override alpha.\n * A node painted at alpha 0 cannot be hit. */\n alphaOf?: (id: string) => number;\n /** Whether a node's `layer` reaches the screen in the asking view. */\n layerIsPainted?: (layer: string) => boolean;\n}\n\n/** The gates a bare adapter takes from its view, plus how that view's camera\n * maps into each parallax layer — see `scenePlaneOf`. */\nexport interface AdapterPickGates extends ViewPickGates {\n planeOf?: (layer: string) => PlaneMap | null;\n}\n\nexport interface ScenePickSourceOptions<TPose> extends ViewPickGates {\n /** Pose lookup, when the caller holds an adapter that already resolves\n * overrides. Defaults to `effectivePose` against the scene's own. */\n getPose?: (id: string) => TPose;\n /** The asking view's composed alpha — `alphaFor(id)` times override alpha.\n * Defaults to the override alpha alone, which is all a bare scene knows. */\n alphaOf?: (id: string) => number;\n /** The asking view's answer to \"does this scene layer reach the screen\",\n * which is `layerVisibility` and `layerOrder` on top of the scene's own\n * `visible` flag. Consulted after the scene's flag, never instead of it. */\n layerIsPainted?: (layer: string) => boolean;\n /** How a child's stored pose folds into its parent's frame. Needed because\n * picking tests where a node is drawn, and under a composing strategy that\n * is not where its own pose says. Ignored when `getPose` already answers in\n * world coordinates. */\n poseComposition?: PoseComposition<TPose>;\n /** The asking view's camera. With it, a layer carrying `parallax` is picked\n * through its plane; without it, every layer is taken to move with the\n * camera. */\n camera?: View;\n}\n\nexport function scenePickSource<TData, TLayer extends string, TPose>(\n scene: Scene<TData, TLayer, TPose>,\n opts: ScenePickSourceOptions<TPose> = {},\n): PickSource<TPose> {\n const hidden = hiddenLayerIds(scene.layers);\n const locked = lockedNodeTest(scene as never);\n const { getPose, alphaOf, layerIsPainted } = opts;\n\n // Resolved once per walk rather than once per candidate: a scene with\n // nothing faded has nothing to say about alpha, and asking it per node is a\n // Map lookup on a linear scan that has no other reason to touch the\n // override table. Needs enumeration — a partial scene stand-in carrying\n // only `read` falls back to reading per node.\n let faded: Set<string> | null = null;\n const enumerable = typeof scene.overrides?.ids === 'function';\n if (!alphaOf && enumerable) {\n for (const id of scene.overrides.ids()) {\n const a = scene.overrides.read(id)?.alpha;\n if (a !== undefined && a <= 0) (faded ??= new Set()).add(id);\n }\n }\n const readAlpha = alphaOf\n ?? (enumerable\n ? (faded ? (id: string) => (faded.has(id) ? 0 : 1) : undefined)\n : (id: string) => scene.overrides.read(asNodeId(id))?.alpha ?? 1);\n\n // A composing strategy makes a node's own pose local, so picking has to fold\n // the parent chain the same way the render walk does or it tests where the\n // node is stored rather than where it is drawn.\n const composition = opts.poseComposition;\n const chain: PoseAdapter<TPose> | null =\n composition !== undefined && composition.closure !== 'identity'\n ? {\n getPose: (id: string) => {\n const n = scene.get(asNodeId(id));\n return n ? effectivePose(scene, n) : (undefined as unknown as TPose);\n },\n getParent: (id: string) => scene.get(asNodeId(id))?.parent ?? null,\n definesFrame: (id: string) => {\n const n = scene.get(asNodeId(id));\n return n === undefined || definesFrame(n);\n },\n }\n : null;\n\n const poseOf = getPose\n ? (node: PickCandidate<TPose>) => getPose(node.id)\n : chain\n ? (node: PickCandidate<TPose>) => composeWorldPose(chain, node.id, composition!.compose)\n : (node: PickCandidate<TPose>) => effectivePose(scene, node as never);\n\n // Through `poseOf`, not the document pose: a derived node has to be tested\n // where the renderer draws its dependencies, which is what an override says.\n const childrenOf = (id: NodeId): readonly NodeId[] => scene.childrenOf(id);\n const depOf = (id: NodeId): DerivedDep<TPose> | undefined => {\n const dep = scene.get(id);\n if (dep === undefined) return undefined;\n return {\n node: dep as Node<unknown, string, TPose>,\n pose: poseOf(dep as never),\n get path(): Path | null {\n return resolveDerivedPath(dep as never, depOf, childrenOf);\n },\n };\n };\n\n const derivedPathOf = (node: PickCandidate<TPose>): Path | null => {\n const n = node as unknown as Node<TData, TLayer, TPose>;\n if (n.dependsOn === undefined) return null;\n return resolveDerivedPath(n, depOf, childrenOf);\n };\n\n const planeOf = opts.camera ? scenePlaneOf(scene.layers, opts.camera) : null;\n\n return {\n ...(planeOf ? { planeOf } : {}),\n order: () => scene.renderOrderNodes() as unknown as readonly PickCandidate<TPose>[],\n derivedPathOf,\n poseOf,\n parentsOf(node) {\n let parentId = (node as { parent?: string | null }).parent ?? null;\n if (parentId === null) return EMPTY_PARENTS;\n const chain: PickCandidate<TPose>[] = [];\n const seen = new Set<string>();\n while (parentId !== null) {\n if (seen.has(parentId)) break;\n seen.add(parentId);\n const parent = scene.get(asNodeId(parentId));\n if (!parent) break;\n chain.push(parent as unknown as PickCandidate<TPose>);\n parentId = parent.parent ?? null;\n }\n return chain;\n },\n // Absent entirely when nothing is faded, so the walk skips the gate.\n ...(readAlpha ? { alphaOf: readAlpha } : {}),\n ...(locked ? { isLocked: (node: PickCandidate<TPose>) => locked(node.id) } : {}),\n ...(hidden.size > 0 || layerIsPainted\n ? {\n layerIsPainted: (layer: string) =>\n !hidden.has(layer) && (layerIsPainted === undefined || layerIsPainted(layer)),\n }\n : {}),\n };\n}\n\n/** Each parallax layer's plane map under `camera`, or null when no layer is a\n * plane — which keeps the per-layer lookup off the walk entirely. */\nexport function scenePlaneOf(\n layers: readonly { id: string; parallax?: ParallaxOpts }[] | undefined,\n camera: View,\n): ((layer: string) => PlaneMap | null) | null {\n let maps: Map<string, PlaneMap> | null = null;\n for (const l of layers ?? []) {\n if (l.parallax) (maps ??= new Map()).set(l.id, planeMap(camera, l.parallax));\n }\n if (maps === null) return null;\n const found = maps;\n return (layer) => found.get(layer) ?? null;\n}\n\n/** The adapter surface the walk can reach without a `Scene`. */\ninterface PickAdapter<TPose> {\n /** Composed world pose, when the adapter has a composition strategy.\n * Falls back to `getPose`, which is the same value under the\n * absolute-pose model. */\n getWorldPose?(id: string): TPose;\n getNodes(): readonly { id: string }[];\n getPose(id: string): TPose;\n getNode?: (id: string) => unknown;\n getChildren?: (parentId: string | null) => readonly string[];\n}\n\n/**\n * A source over a bare `SelectAdapter`, for consumers with no `Scene`.\n *\n * A hierarchical adapter is walked depth-first, parents before children —\n * the same back-to-front order `renderOrderNodes` produces for a single\n * layer. A flat adapter falls back to `getNodes()`, which carries no\n * parentage, so nothing clips.\n *\n * `gates` come from whichever view is picking, never from the adapter.\n */\nexport function adapterPickSource<TPose>(\n adapter: PickAdapter<TPose>,\n gates: AdapterPickGates = {},\n): PickSource<TPose> {\n const hier = adapter as Required<Pick<PickAdapter<TPose>, 'getNode' | 'getChildren'>>;\n const hierarchical =\n typeof adapter.getChildren === 'function' && typeof adapter.getNode === 'function';\n\n // Rebuilt per walk: the tree may have changed between queries, and a walk\n // is the only thing that reads it.\n let parents = new Map<string, PickCandidate<TPose>>();\n\n // The adapter composes if it has a strategy; `getPose` alone is the node's\n // own pose, which is not where a framed child is drawn.\n const worldPose = (id: string): TPose =>\n (typeof adapter.getWorldPose === 'function' ? adapter.getWorldPose(id) : adapter.getPose(id));\n\n const nodeOf = (id: string): PickCandidate<TPose> => {\n const raw = hier.getNode(id) as Partial<PickCandidate<TPose>>;\n return { ...raw, id, pose: worldPose(id) } as PickCandidate<TPose>;\n };\n\n return {\n order() {\n if (!hierarchical) {\n parents = new Map();\n return adapter.getNodes().map((n) => ({\n ...(n as object), id: n.id, pose: worldPose(n.id),\n })) as PickCandidate<TPose>[];\n }\n const out: PickCandidate<TPose>[] = [];\n const next = new Map<string, PickCandidate<TPose>>();\n const visit = (parentId: string | null, parent: PickCandidate<TPose> | null): void => {\n for (const childId of hier.getChildren(parentId)) {\n const node = nodeOf(childId);\n if (parent) next.set(childId, parent);\n out.push(node);\n visit(childId, node);\n }\n };\n visit(null, null);\n parents = next;\n return out;\n },\n ...(gates.alphaOf ? { alphaOf: gates.alphaOf } : {}),\n ...(gates.layerIsPainted ? { layerIsPainted: gates.layerIsPainted } : {}),\n ...(gates.planeOf ? { planeOf: gates.planeOf } : {}),\n poseOf: (node) => worldPose(node.id),\n parentsOf(node) {\n const chain: PickCandidate<TPose>[] = [];\n let cur = parents.get(node.id);\n const seen = new Set<string>();\n while (cur && !seen.has(cur.id)) {\n seen.add(cur.id);\n chain.push(cur);\n cur = parents.get(cur.id);\n }\n return chain;\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"pickWalk.js","names":["EMPTY_PARENTS","ownClipOf","node","pose","findShapeSilhouette","derived","pickWalk","includeContainers","q","isLocked","src","out","clipPath","carry","clipCarrier","clipOf","cached","ancestor","resolved","planeQueries","queryFor","layer","found","map","query","clipped","clip","lockedNodeTest","locked","scene","lockedLayers","cur","asNodeId","id","seen","hiddenLayerIds","layers","scenePickSource","hidden","opts","faded","enumerable","alphaOf","a","readAlpha","composition","chain","effectivePose","definesFrame","poseOf","getPose","composeWorldPose","childrenOf","depOf","dep","resolveDerivedPath","derivedPathOf","n","planeOf","scenePlaneOf","parentId","parent","layerIsPainted","maps","l","planeMap","camera","adapterPickSource","hier","adapter","hierarchical","parents","worldPose","nodeOf","next","visit","childId","gates"],"sources":["../../src/canvas/pickWalk.ts"],"sourcesContent":["/**\n * The one hit-test walk.\n *\n * Four walks used to answer this question — two point queries, two area\n * queries — over the same tree with the same gates. They drifted three times\n * without a failing test, because nothing held them to each other. Everything\n * that decides whether a node *can* be hit lives here now; a query supplies\n * only the shape test and the clip predicate for its own region.\n *\n * The gates, in the order the walk applies them:\n * 1. containers, when the query does not want them\n * 2. the layer is painted at all — scene visibility, and the asking view's\n * `layerVisibility` / `layerOrder`\n * 3. painted alpha is above zero\n * 4. the node is not locked, unless the query asks for locked nodes\n * 5. the query's own shape test\n * 6. every ancestor clip admits the query region\n *\n * 2, 3 and 6 are all \"the renderer did not put this on screen\". A pick path\n * that skips one answers for a node the user cannot see. 4 is the other axis:\n * a locked node is on screen and still out of reach.\n */\n\nimport type { Path } from '@weasel-js/geom';\nimport { findShapeSilhouette } from 'canvas/NodeShape';\nimport type { ClipFromPoseFn, DerivedDep, Node, NodeId, Scene } from 'core/scene/types';\nimport { asNodeId } from 'core/scene/types';\nimport { definesFrame, effectivePose } from 'core/scene/effectivePose';\nimport { resolveDerivedPath } from './derivedPath';\nimport { composeWorldPose, type PoseAdapter, type PoseComposition } from 'features/groups/composePose';\nimport { planeMap, type ParallaxOpts, type PlaneMap } from 'core/viewport/parallax';\nimport type { View } from 'core/viewport/view';\nimport { clipCarrier } from './planeClips';\n\n/** Shared, never mutated: most scenes are flat and every node returns it. */\nconst EMPTY_PARENTS: readonly never[] = [];\n\n/** The subset of a node the walk itself reads. Deliberately structural: a\n * bare adapter's node satisfies it as readily as a `SceneNode`. */\nexport interface PickCandidate<TPose> {\n id: string;\n kind?: string;\n layer?: string;\n pose: TPose;\n /** `false` makes the node transparent to the walk — see `SceneNode.pickable`. */\n pickable?: boolean;\n clipFromPose?: ClipFromPoseFn<TPose>;\n}\n\n/**\n * Everything the walk reads that a **view** owns.\n *\n * A layer must not hold state a view owns, so none of these are closed over\n * at construction — each is asked per walk, of the view being dispatched to.\n */\nexport interface PickSource<TPose> {\n /** Candidates in paint order, back-to-front. Containers included; the\n * query decides whether they can be hit. */\n order(): readonly PickCandidate<TPose>[];\n /** The pose the renderer draws — override-aware. Not `node.pose`, which is\n * how the render and pick paths came to disagree about where a node is. */\n poseOf(node: PickCandidate<TPose>): TPose;\n /** Ancestors, innermost first. Only walked for candidates that already\n * passed their own shape test. */\n parentsOf(node: PickCandidate<TPose>): readonly PickCandidate<TPose>[];\n /** Painted alpha for the asking view: the view's `alphaFor` times any\n * per-node override alpha. Omit when the source has no alpha concept. */\n alphaOf?(id: string): number;\n /** Whether this layer reaches the screen in the asking view. Omit for a\n * source with no layers. */\n layerIsPainted?(layer: string): boolean;\n /** Whether the node is locked — painted, but not something a pointer can\n * take. Omit for a source with no lock concept. */\n isLocked?(node: PickCandidate<TPose>): boolean;\n /** The path a node computes from its dependencies' poses, or null when it\n * derives nothing. Only a scene-backed source can answer — deriving needs\n * the dependencies' poses — so a bare adapter omits it, and every derived\n * node there keeps answering from its own (placeholder) pose. */\n derivedPathOf?(node: PickCandidate<TPose>): Path | null;\n /** How the asking view's world maps into a parallax layer's, or null for a\n * layer that moves with the camera. Omit for a source with no planes. */\n planeOf?(layer: string): PlaneMap | null;\n}\n\n/** One hit-test question: what region, and what counts as covering it. */\nexport interface PickQuery<TPose> {\n /** Default `true`. The area walks exclude containers so a marquee does not\n * return a container and its children both. */\n includeContainers?: boolean;\n /** Default `false`. Every selecting or editing query passes over locked\n * nodes; one that only samples what is painted can take them back. */\n includeLocked?: boolean;\n /** Does this node, drawn at this pose, cover the query region? `derived`\n * is the node's resolved path when it has one — a derived node's pose is a\n * placeholder, so a region test that reads only the pose answers for the\n * wrong box. */\n hits(node: PickCandidate<TPose>, pose: TPose, derived?: Path | null): boolean;\n /**\n * Does an ancestor's clip leave anything of `node` for this query to hit?\n *\n * A point query asks point-in-clip and ignores the node. An area query has\n * to ask about the node as well: the clip and the area can overlap where\n * the node is not, and the node and the clip can overlap where the area is\n * not, and only the two terms together reject both.\n */\n clipAdmits(\n clip: Path,\n node: PickCandidate<TPose>,\n pose: TPose,\n derived?: Path | null,\n ): boolean;\n /** The same question asked in a parallax plane's world: the region carried\n * through `map`, and any screen-pixel slop re-derived at the plane's scale.\n * A query without it cannot reach nodes on a plane — it would test them\n * where their poses sit rather than where they are painted. */\n inPlane?(map: PlaneMap): PickQuery<TPose>;\n}\n\n/** The clip a container imposes on its subtree, or null when it imposes none. */\nexport function ownClipOf<TPose>(\n node: PickCandidate<TPose>,\n pose: TPose,\n derived?: Path | null,\n): Path | null {\n if (node.kind !== 'container') return null;\n if (typeof node.clipFromPose === 'function') return node.clipFromPose(pose);\n return findShapeSilhouette(\n node as unknown as Node<unknown, string, TPose>,\n pose,\n { derivedPath: derived },\n );\n}\n\n/**\n * Collect every candidate the query hits, back-to-front — the last element is\n * the topmost, which is the order `pickTopMostHit` expects.\n */\nexport function pickWalk<TPose>(\n src: PickSource<TPose>,\n q: PickQuery<TPose>,\n): string[] {\n const includeContainers = q.includeContainers ?? true;\n const isLocked = q.includeLocked === true ? undefined : src.isLocked;\n const out: string[] = [];\n\n // Resolving a container's clip means `clipFromPose` or a silhouette build,\n // which is the expensive half; the predicate against it is cheap. So the\n // *path* is memoized per container for the walk and the predicate re-runs\n // per candidate — `clipAdmits` needs the candidate, and caching its answer\n // per container would answer the wrong question for an area query.\n const clipPath = new Map<string, Path | null>();\n // A clip is in its container's plane; the query is in the candidate's.\n const carry = clipCarrier(src.planeOf ?? null);\n\n const clipOf = (ancestor: PickCandidate<TPose>): Path | null => {\n const cached = clipPath.get(ancestor.id);\n if (cached !== undefined) return cached;\n const resolved = ownClipOf(\n ancestor, src.poseOf(ancestor), src.derivedPathOf?.(ancestor),\n );\n clipPath.set(ancestor.id, resolved);\n return resolved;\n };\n\n // One reprojected query per plane per walk; `null` marks a plane this\n // query cannot reach.\n const planeQueries = new Map<string, PickQuery<TPose> | null>();\n const queryFor = (layer: string | undefined): PickQuery<TPose> | null => {\n if (layer === undefined || src.planeOf === undefined) return q;\n let found = planeQueries.get(layer);\n if (found === undefined) {\n const map = src.planeOf(layer);\n found = map === null ? q : (q.inPlane?.(map) ?? null);\n planeQueries.set(layer, found);\n }\n return found;\n };\n\n for (const node of src.order()) {\n if (!includeContainers && node.kind === 'container') continue;\n // Transparent to picking: the click belongs to whatever is behind it,\n // which for a label inside a box is the box.\n if (node.pickable === false) continue;\n if (node.layer !== undefined && src.layerIsPainted?.(node.layer) === false) continue;\n if (src.alphaOf !== undefined && src.alphaOf(node.id) <= 0) continue;\n if (isLocked !== undefined && isLocked(node)) continue;\n\n const query = queryFor(node.layer);\n if (query === null) continue;\n const pose = src.poseOf(node);\n const derived = src.derivedPathOf?.(node);\n if (!query.hits(node, pose, derived)) continue;\n\n let clipped = false;\n for (const ancestor of src.parentsOf(node)) {\n const clip = clipOf(ancestor);\n if (clip === null) continue;\n if (!query.clipAdmits(carry(clip, ancestor.layer, node.layer), node, pose, derived)) {\n clipped = true;\n break;\n }\n }\n if (clipped) continue;\n\n out.push(node.id);\n }\n\n return out;\n}\n\n// ---------------------------------------------------------------------------\n// Sources\n// ---------------------------------------------------------------------------\n\n/** Answers `Scene.isLocked` from the layer records and parent links alone,\n * so a partial scene stand-in works too. Null when nothing is locked, which\n * is what keeps the gate off the walk in the common case. */\nfunction lockedNodeTest(\n scene: Pick<Scene<unknown, string, unknown>, 'layers' | 'get'>,\n): ((id: string) => boolean) | null {\n let locked: Set<string> | null = null;\n for (const layer of scene.layers ?? []) {\n if (layer.locked === true) (locked ??= new Set()).add(layer.id);\n }\n if (locked === null) return null;\n const lockedLayers = locked;\n return (id) => {\n let cur = scene.get(asNodeId(id));\n const seen = new Set<string>();\n while (cur !== undefined && !seen.has(cur.id)) {\n if (lockedLayers.has(cur.layer)) return true;\n seen.add(cur.id);\n cur = cur.parent === null ? undefined : scene.get(cur.parent);\n }\n return false;\n };\n}\n\n/** Layer ids the scene reports as hidden. Only an explicit `false` hides a\n * layer: a partial scene stand-in that omits the flag stays pickable. */\nexport function hiddenLayerIds(\n layers: readonly { id: string; visible?: boolean }[] | undefined,\n): Set<string> {\n const out = new Set<string>();\n for (const layer of layers ?? []) {\n if (layer.visible === false) out.add(layer.id);\n }\n return out;\n}\n\n/**\n * The two answers a pick needs from the view it is asked for rather than from\n * the data behind it: how that view paints a node, and whether it paints a\n * layer at all. A scene and a bare adapter take the same pair.\n */\nexport interface ViewPickGates {\n /** Painted alpha: the view's `alphaFor` times any per-node override alpha.\n * A node painted at alpha 0 cannot be hit. */\n alphaOf?: (id: string) => number;\n /** Whether a node's `layer` reaches the screen in the asking view. */\n layerIsPainted?: (layer: string) => boolean;\n}\n\n/** The gates a bare adapter takes from its view, plus how that view's camera\n * maps into each parallax layer — see `scenePlaneOf`. */\nexport interface AdapterPickGates extends ViewPickGates {\n planeOf?: (layer: string) => PlaneMap | null;\n}\n\nexport interface ScenePickSourceOptions<TPose> extends ViewPickGates {\n /** Pose lookup, when the caller holds an adapter that already resolves\n * overrides. Defaults to `effectivePose` against the scene's own. */\n getPose?: (id: string) => TPose;\n /** The asking view's composed alpha — `alphaFor(id)` times override alpha.\n * Defaults to the override alpha alone, which is all a bare scene knows. */\n alphaOf?: (id: string) => number;\n /** The asking view's answer to \"does this scene layer reach the screen\",\n * which is `layerVisibility` and `layerOrder` on top of the scene's own\n * `visible` flag. Consulted after the scene's flag, never instead of it. */\n layerIsPainted?: (layer: string) => boolean;\n /** How a child's stored pose folds into its parent's frame. Needed because\n * picking tests where a node is drawn, and under a composing strategy that\n * is not where its own pose says. Ignored when `getPose` already answers in\n * world coordinates. */\n poseComposition?: PoseComposition<TPose>;\n /** The asking view's camera. With it, a layer carrying `parallax` is picked\n * through its plane; without it, every layer is taken to move with the\n * camera. */\n camera?: View;\n}\n\nexport function scenePickSource<TData, TLayer extends string, TPose>(\n scene: Scene<TData, TLayer, TPose>,\n opts: ScenePickSourceOptions<TPose> = {},\n): PickSource<TPose> {\n const hidden = hiddenLayerIds(scene.layers);\n const locked = lockedNodeTest(scene as never);\n const { getPose, alphaOf, layerIsPainted } = opts;\n\n // Resolved once per walk rather than once per candidate: a scene with\n // nothing faded has nothing to say about alpha, and asking it per node is a\n // Map lookup on a linear scan that has no other reason to touch the\n // override table. Needs enumeration — a partial scene stand-in carrying\n // only `read` falls back to reading per node.\n let faded: Set<string> | null = null;\n const enumerable = typeof scene.overrides?.ids === 'function';\n if (!alphaOf && enumerable) {\n for (const id of scene.overrides.ids()) {\n const a = scene.overrides.read(id)?.alpha;\n if (a !== undefined && a <= 0) (faded ??= new Set()).add(id);\n }\n }\n const readAlpha = alphaOf\n ?? (enumerable\n ? (faded ? (id: string) => (faded.has(id) ? 0 : 1) : undefined)\n : (id: string) => scene.overrides.read(asNodeId(id))?.alpha ?? 1);\n\n // A composing strategy makes a node's own pose local, so picking has to fold\n // the parent chain the same way the render walk does or it tests where the\n // node is stored rather than where it is drawn.\n const composition = opts.poseComposition;\n const chain: PoseAdapter<TPose> | null =\n composition !== undefined && composition.closure !== 'identity'\n ? {\n getPose: (id: string) => {\n const n = scene.get(asNodeId(id));\n return n ? effectivePose(scene, n) : (undefined as unknown as TPose);\n },\n getParent: (id: string) => scene.get(asNodeId(id))?.parent ?? null,\n definesFrame: (id: string) => {\n const n = scene.get(asNodeId(id));\n return n === undefined || definesFrame(n);\n },\n }\n : null;\n\n const poseOf = getPose\n ? (node: PickCandidate<TPose>) => getPose(node.id)\n : chain\n ? (node: PickCandidate<TPose>) => composeWorldPose(chain, node.id, composition!.compose)\n : (node: PickCandidate<TPose>) => effectivePose(scene, node as never);\n\n // Through `poseOf`, not the document pose: a derived node has to be tested\n // where the renderer draws its dependencies, which is what an override says.\n const childrenOf = (id: NodeId): readonly NodeId[] => scene.childrenOf(id);\n const depOf = (id: NodeId): DerivedDep<TPose> | undefined => {\n const dep = scene.get(id);\n if (dep === undefined) return undefined;\n return {\n node: dep as Node<unknown, string, TPose>,\n pose: poseOf(dep as never),\n get path(): Path | null {\n return resolveDerivedPath(dep as never, depOf, childrenOf);\n },\n };\n };\n\n const derivedPathOf = (node: PickCandidate<TPose>): Path | null => {\n const n = node as unknown as Node<TData, TLayer, TPose>;\n if (n.dependsOn === undefined) return null;\n return resolveDerivedPath(n, depOf, childrenOf);\n };\n\n const planeOf = opts.camera ? scenePlaneOf(scene.layers, opts.camera) : null;\n\n return {\n ...(planeOf ? { planeOf } : {}),\n order: () => scene.renderOrderNodes() as unknown as readonly PickCandidate<TPose>[],\n derivedPathOf,\n poseOf,\n parentsOf(node) {\n let parentId = (node as { parent?: string | null }).parent ?? null;\n if (parentId === null) return EMPTY_PARENTS;\n const chain: PickCandidate<TPose>[] = [];\n const seen = new Set<string>();\n while (parentId !== null) {\n if (seen.has(parentId)) break;\n seen.add(parentId);\n const parent = scene.get(asNodeId(parentId));\n if (!parent) break;\n chain.push(parent as unknown as PickCandidate<TPose>);\n parentId = parent.parent ?? null;\n }\n return chain;\n },\n // Absent entirely when nothing is faded, so the walk skips the gate.\n ...(readAlpha ? { alphaOf: readAlpha } : {}),\n ...(locked ? { isLocked: (node: PickCandidate<TPose>) => locked(node.id) } : {}),\n ...(hidden.size > 0 || layerIsPainted\n ? {\n layerIsPainted: (layer: string) =>\n !hidden.has(layer) && (layerIsPainted === undefined || layerIsPainted(layer)),\n }\n : {}),\n };\n}\n\n/** Each parallax layer's plane map under `camera`, or null when no layer is a\n * plane — which keeps the per-layer lookup off the walk entirely. */\nexport function scenePlaneOf(\n layers: readonly { id: string; parallax?: ParallaxOpts }[] | undefined,\n camera: View,\n): ((layer: string) => PlaneMap | null) | null {\n let maps: Map<string, PlaneMap> | null = null;\n for (const l of layers ?? []) {\n if (l.parallax) (maps ??= new Map()).set(l.id, planeMap(camera, l.parallax));\n }\n if (maps === null) return null;\n const found = maps;\n return (layer) => found.get(layer) ?? null;\n}\n\n/** The adapter surface the walk can reach without a `Scene`. */\ninterface PickAdapter<TPose> {\n /** Composed world pose, when the adapter has a composition strategy.\n * Falls back to `getPose`, which is the same value under the\n * absolute-pose model. */\n getWorldPose?(id: string): TPose;\n getNodes(): readonly { id: string }[];\n getPose(id: string): TPose;\n getNode?: (id: string) => unknown;\n getChildren?: (parentId: string | null) => readonly string[];\n}\n\n/**\n * A source over a bare `SelectAdapter`, for consumers with no `Scene`.\n *\n * A hierarchical adapter is walked depth-first, parents before children —\n * the same back-to-front order `renderOrderNodes` produces for a single\n * layer. A flat adapter falls back to `getNodes()`, which carries no\n * parentage, so nothing clips.\n *\n * `gates` come from whichever view is picking, never from the adapter.\n */\nexport function adapterPickSource<TPose>(\n adapter: PickAdapter<TPose>,\n gates: AdapterPickGates = {},\n): PickSource<TPose> {\n const hier = adapter as Required<Pick<PickAdapter<TPose>, 'getNode' | 'getChildren'>>;\n const hierarchical =\n typeof adapter.getChildren === 'function' && typeof adapter.getNode === 'function';\n\n // Rebuilt per walk: the tree may have changed between queries, and a walk\n // is the only thing that reads it.\n let parents = new Map<string, PickCandidate<TPose>>();\n\n // The adapter composes if it has a strategy; `getPose` alone is the node's\n // own pose, which is not where a framed child is drawn.\n const worldPose = (id: string): TPose =>\n (typeof adapter.getWorldPose === 'function' ? adapter.getWorldPose(id) : adapter.getPose(id));\n\n const nodeOf = (id: string): PickCandidate<TPose> => {\n const raw = hier.getNode(id) as Partial<PickCandidate<TPose>>;\n return { ...raw, id, pose: worldPose(id) } as PickCandidate<TPose>;\n };\n\n return {\n order() {\n if (!hierarchical) {\n parents = new Map();\n return adapter.getNodes().map((n) => ({\n ...(n as object), id: n.id, pose: worldPose(n.id),\n })) as PickCandidate<TPose>[];\n }\n const out: PickCandidate<TPose>[] = [];\n const next = new Map<string, PickCandidate<TPose>>();\n const visit = (parentId: string | null, parent: PickCandidate<TPose> | null): void => {\n for (const childId of hier.getChildren(parentId)) {\n const node = nodeOf(childId);\n if (parent) next.set(childId, parent);\n out.push(node);\n visit(childId, node);\n }\n };\n visit(null, null);\n parents = next;\n return out;\n },\n ...(gates.alphaOf ? { alphaOf: gates.alphaOf } : {}),\n ...(gates.layerIsPainted ? { layerIsPainted: gates.layerIsPainted } : {}),\n ...(gates.planeOf ? { planeOf: gates.planeOf } : {}),\n poseOf: (node) => worldPose(node.id),\n parentsOf(node) {\n const chain: PickCandidate<TPose>[] = [];\n let cur = parents.get(node.id);\n const seen = new Set<string>();\n while (cur && !seen.has(cur.id)) {\n seen.add(cur.id);\n chain.push(cur);\n cur = parents.get(cur.id);\n }\n return chain;\n },\n };\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"sceneAdapter.js","names":["adapterFallbackIdCounter","adapterFallbackId","pasteIdCounter","pasteNodeId","copyField","value","sceneToAdapter","visibleLayers","out","layer","scene","sel","options","getSelection","setSelection","d","AUTO_POSE_DESCRIPTOR","poseBounds","visualBoundsViaDescriptor","p","translate","translatePoseViaDescriptor","dx","dy","node","poseDescriptorForNode","composition","composes","pickOpts","pickOptsFor","view","regionPickOptions","adapter","asNodeId","id","visible","nid","effectivePose","n","definesFrame","composeWorldPose","g","before","after","pose","descIds","collect","cid","rootId","cn","parentId","i","ids","l","declared","label","applyOpsTo","ops","index","fnFieldsOfNode","layout","hitTestArea","rect","hitTestLassoPolygon","polygon","mode","created","bounds","idSet","taken","items","poseFor","isRoot","frame","frameAtOrAbove","derivation","capture","a","src","clipboard","idMap","item","byId","ridesPastedFrame","offset","ctx","minX","minY","b","base","c","useSceneAdapter","useMemo","selection","commitInsert","insertLayer","layouts","poseDescriptor","cascadeContainerPose","poseComposition"],"sources":["../../src/canvas/sceneAdapter.ts"],"sourcesContent":["/**\n * Synthesize a `<Canvas>`-compatible adapter from a `Scene` primitive.\n *\n * The Scene owns nodes/poses/parenting and auto-records ops on every mutation.\n * `<SceneCanvas>` calls `sceneToAdapter(scene)` and forwards the result to\n * `<Canvas>`'s `adapter` prop. Selection and hit-testing are not part of the\n * Scene model — Canvas's internal `useSelection` and pose-derived pickEvery\n * cover those.\n *\n * Pose semantics match the rest of the kit: `getPose` / `setPose` return /\n * write **local** coordinates (relative to parent). The Scene stores pose on\n * each node directly; world composition (when needed) is the renderer's job\n * via `composeWorldPose`, not the adapter's.\n */\nimport { useMemo } from 'react';\nimport type {\n AreaSelectAdapter,\n ClipboardSnapshot,\n InsertAdapter,\n LassoHitMode,\n LassoSelectAdapter,\n RegionPickView,\n LayerEnumerableAdapter,\n MoveAdapter,\n ResizeAdapter,\n RotateAdapter,\n} from 'core/adapters/types';\nimport type { LayoutStrategy } from '../layout/types';\nimport type { Op } from 'core/ops/types';\nimport type { Node, NodeId, Scene } from 'core/scene/types';\nimport { definesFrame, effectivePose } from 'core/scene/effectivePose';\nimport { fnFieldsOfNode } from 'core/scene/nodeFnFields';\nimport { asNodeId } from 'core/scene/types';\nimport { applyOpsTo } from 'core/applyOps';\nimport { hitTestArea, hitTestLassoPolygon, regionPickOptions } from 'canvas/deps/hitTestArea';\nimport type { ScenePickSourceOptions } from 'canvas/pickWalk';\nimport {\n poseDescriptorForNode,\n translatePoseViaDescriptor,\n visualBoundsViaDescriptor,\n type PoseDescriptor,\n} from 'interactions/actions/resize/geometry';\nimport { AUTO_POSE_DESCRIPTOR } from 'interactions/actions/resize/autoPoseDescriptor';\nimport { composeWorldPose, frameAtOrAbove, type PoseComposition } from 'features/groups/composePose';\nimport type { Bounds } from '../core/viewport/fitViewToBounds';\n\n/** Minimal selection contract `sceneToAdapter` needs to wire `getSelection` /\n * `setSelection`. Matches `useSelection().adapterMethods` plus an imperative\n * read; pass `useSelection()` itself or `selection.adapterMethods`. */\nexport interface SceneAdapterSelection {\n get?(): string[];\n set?(ids: string[]): void;\n getSelection?(): string[];\n setSelection?(ids: string[]): void;\n}\n\n/** Sibling z-order surface used by reorder ops. Inlined here (rather than\n * pulled from `core/ops/reorder` to avoid import cycles); the `null` parent\n * channel addresses the root sibling list. */\n/** @internal */\ninterface ReorderAdapter {\n getChildren(parentId: string | null): string[];\n setChildOrder(parentId: string | null, ids: string[]): void;\n}\n\n/** Everything the interaction layer can ask of a scene, as one type: the\n * move, resize, rotate, selection, layer and reorder contracts combined.\n * `<SceneCanvas>` synthesizes one of these, which is why most consumers never\n * touch an adapter directly. */\nexport type SceneCanvasAdapter<TData, TLayer extends string, TPose> =\n & MoveAdapter<Node<TData, TLayer, TPose>, TPose>\n & ResizeAdapter<Node<TData, TLayer, TPose>, TPose>\n & RotateAdapter<Node<TData, TLayer, TPose>, TPose>\n & AreaSelectAdapter\n & LassoSelectAdapter\n & LayerEnumerableAdapter<TLayer>\n & ReorderAdapter\n & Partial<InsertAdapter<Node<TData, TLayer, TPose>>>\n // Tighten the parts that are optional on the underlying adapter contracts\n // but unconditionally provided by a scene-backed adapter. Anything that\n // routes scene mutations through this adapter (kit InsertOps, hierarchical\n // hooks like useNest, etc.) can rely on these being present.\n & {\n getParent(id: string): string | null;\n /** The node's pose with every ancestor's frame folded in. Equal to\n * `getPose` unless a `poseComposition` is configured. */\n getWorldPose(id: string): TPose;\n /** Whether `id`'s pose is a frame its children are expressed in — see\n * `definesFrame`. */\n definesFrame(id: string): boolean;\n /** Present only when a composing strategy is configured; the render\n * walk feature-detects it. */\n composePose?(parent: TPose, child: TPose): TPose;\n getSelection(): string[];\n setSelection(ids: string[]): void;\n insertNode(node: Node<TData, TLayer, TPose>, index?: number): void;\n removeNode(id: string): void;\n /** Replace a node's `data` — what a `setData` op writes through. */\n setData(id: string, data: TData): void;\n /** Swap a container's layout — what a `setLayout` op writes through. */\n setLayout(id: string, layout: LayoutStrategy<TPose> | string | null): void;\n getRemovalClosure(ids: readonly string[]): string[];\n applyOps(ops: Op[], label?: string): void;\n snapshotSelection(ids: string[]): ClipboardSnapshot;\n commitPaste(\n clipboard: ClipboardSnapshot,\n offset: { dx: number; dy: number },\n ctx?: { dropPoint?: { worldX: number; worldY: number } },\n ): Node<TData, TLayer, TPose>[];\n getPasteOffset(clipboard: ClipboardSnapshot): { dx: number; dy: number };\n };\n\n/** Optional extras for the synthesized adapter. Pass `commitInsert` to wire\n * the insert gesture into a Scene-owned canvas; the returned object becomes\n * a leaf on the layer named by `layer` (default `'default'`). */\nexport interface SceneToAdapterOptions<TData, TLayer extends string, TPose> {\n /** Factory for new objects. Returning `null` aborts the insert. */\n commitInsert?: (bounds: Bounds) => {\n pose: TPose;\n data: TData;\n id?: string;\n } | null;\n /** Layer to place inserted nodes on. Defaults to the trivial-form layer. */\n insertLayer?: TLayer;\n /** Selection source for `AreaSelectAdapter.getSelection` / `setSelection`.\n * Pass the result of `useSelection()` (or its `adapterMethods`). When\n * omitted, `getSelection` returns `[]` and `setSelection` is a noop —\n * fine for read-only or selection-less canvases, but the marquee gesture\n * won't update any external selection state. */\n selection?: SceneAdapterSelection;\n /** How to read and rewrite this scene's poses. Default `AUTO_POSE_DESCRIPTOR`. */\n poseDescriptor?: PoseDescriptor<TPose>;\n /** Layout strategies keyed by container node id, for containers whose\n * scene node declares none (`ContainerNode.layout` wins). `move` runs its\n * layout-aware pass on them; for inserts, deletes and resizes to reach\n * them too, install them with `useLayoutArrivals`. Pass either a static\n * map, or a `getLayout(id)` function for dynamic resolution. */\n layouts?:\n | Record<string, LayoutStrategy<TPose>>\n | ((containerId: string) => LayoutStrategy<TPose> | null);\n /** When true, `setPose` on a container translates every descendant by the\n * same delta, through the pose descriptor. Scene v1 stores absolute poses,\n * so without it children stay behind. */\n cascadeContainerPose?: boolean;\n /** How a child's stored pose folds into its parent's frame. Omit for the\n * absolute-pose model, where a parent contributes no transform. Supplying a\n * composing strategy makes a container's pose a **frame**: rotating the\n * container rotates its contents, and `setPose` on it must not also\n * translate them, so it cannot be combined with `cascadeContainerPose`. */\n poseComposition?: PoseComposition<TPose>;\n}\n\n// Fallback id generator for `commitInsert` when the consumer factory omits\n// `created.id`. Mirrors `scene.ts`'s `defaultGenerateId` shape; module-scoped\n// counter keeps ids unique across adapters in a session.\nlet adapterFallbackIdCounter = 0;\nfunction adapterFallbackId(): string {\n return `n${(adapterFallbackIdCounter++).toString(36)}-${Math.random().toString(36).slice(2, 8)}`;\n}\n\n// Fresh ids for pasted nodes. The scene's own `generateId` isn't reachable\n// from the adapter, so `commitPaste` mints ids in the same shape as\n// `defaultGenerateId` with a `paste-` prefix (module-scoped counter + random\n// suffix keeps them unique across adapters and repeated pastes). Accepted\n// risk: the counter is per-tab, so cross-tab paste (the OS-clipboard path)\n// leans on the random suffix alone for uniqueness — a collision makes\n// `scene.add` throw rather than silently overwrite.\nlet pasteIdCounter = 0;\nfunction pasteNodeId(): string {\n return `paste-${(pasteIdCounter++).toString(36)}-${Math.random().toString(36).slice(2, 8)}`;\n}\n\n// Copy one node field (pose / data / children) so clipboard snapshots don't\n// alias live scene state. Spread-level copy per the clipboard contract:\n// mutating a snapshot's pose/data/children must not touch the scene.\nfunction copyField<T>(value: T): T {\n if (Array.isArray(value)) return [...value] as T;\n if (value !== null && typeof value === 'object') return { ...(value as object) } as T;\n return value;\n}\n\n/** Derive the adapter for a scene. Reads go straight to the scene; writes are\n * ops applied through its history, so anything done through the adapter is\n * undoable. */\nexport function sceneToAdapter<TData, TLayer extends string, TPose>(\n scene: Scene<TData, TLayer, TPose>,\n options: SceneToAdapterOptions<TData, TLayer, TPose> = {},\n): SceneCanvasAdapter<TData, TLayer, TPose> {\n const visibleLayers = (): Set<TLayer> => {\n const out = new Set<TLayer>();\n for (const layer of scene.layers) {\n if (layer.visible) out.add(layer.id);\n }\n return out;\n };\n\n const sel = options.selection;\n const getSelection = sel?.getSelection ?? sel?.get ?? (() => [] as string[]);\n const setSelection = sel?.setSelection ?? sel?.set ?? (() => {});\n const d = (options.poseDescriptor ?? AUTO_POSE_DESCRIPTOR) as PoseDescriptor<TPose>;\n const poseBounds = (p: TPose): Bounds => visualBoundsViaDescriptor(p, d);\n const translate = (p: TPose, dx: number, dy: number, node?: unknown): TPose =>\n translatePoseViaDescriptor(\n p, dx, dy, node === undefined ? d : poseDescriptorForNode(d, node),\n );\n\n const composition = options.poseComposition;\n const composes = composition !== undefined && composition.closure !== 'identity';\n const pickOpts: ScenePickSourceOptions<unknown> = composition\n ? { poseComposition: composition as PoseComposition<unknown> }\n : {};\n // The asking view's layer gate and camera, over the adapter's own options.\n const pickOptsFor = (view: RegionPickView | undefined): ScenePickSourceOptions<unknown> =>\n (view ? { ...pickOpts, ...regionPickOptions(view, undefined, undefined) } : pickOpts);\n if (composes && options.cascadeContainerPose) {\n throw new Error(\n 'sceneToAdapter: poseComposition and cascadeContainerPose contradict each other. ' +\n 'Under a composing strategy a child\\'s pose is already relative to its parent, so ' +\n 'the cascade would move every descendant twice. Drop cascadeContainerPose.',\n );\n }\n\n const adapter: SceneCanvasAdapter<TData, TLayer, TPose> = {\n getNode(id) {\n return scene.get(asNodeId(id));\n },\n getNodes() {\n const visible = visibleLayers();\n const out: Node<TData, TLayer, TPose>[] = [];\n for (const n of scene.renderOrderNodes()) {\n if (visible.has(n.layer)) out.push(n);\n }\n return out;\n },\n getPose(id) {\n const nid = asNodeId(id);\n const n = scene.get(nid);\n if (!n) throw new Error(`sceneToAdapter: unknown node \"${id}\"`);\n return effectivePose(scene, n);\n },\n getParent(id) {\n const n = scene.get(asNodeId(id));\n return n?.parent ?? null;\n },\n definesFrame(id) {\n const n = scene.get(asNodeId(id));\n return n === undefined || definesFrame(n);\n },\n getWorldPose(id) {\n if (!composes) return adapter.getPose(id);\n return composeWorldPose(adapter, id, composition!.compose);\n },\n // Only present when it would do something: the render walk skips the fold\n // entirely when this is absent, which is the absolute-pose path.\n ...(composes ? { composePose: composition!.compose } : {}),\n setPose(id, pose) {\n // Container cascade (opt-in): under scene v1's absolute-pose semantics,\n // moving a container needs to translate every descendant by the same\n // delta so children visually stay attached. The whole cascade lands as\n // one scene.batch so undo collapses to a single step.\n if (options.cascadeContainerPose) {\n const node = scene.get(asNodeId(id));\n if (node && node.kind === 'container') {\n const g = poseDescriptorForNode(d, node);\n const before = g.getBounds(node.pose);\n const after = g.getBounds(pose);\n const dx = after.x - before.x;\n const dy = after.y - before.y;\n if (dx !== 0 || dy !== 0) {\n const descIds: string[] = [];\n const collect = (rootId: string): void => {\n for (const cid of scene.childrenOf(asNodeId(rootId))) {\n descIds.push(cid);\n collect(cid);\n }\n };\n collect(id);\n scene.batch('move container', () => {\n scene.setPose(asNodeId(id), pose);\n for (const cid of descIds) {\n const cn = scene.get(asNodeId(cid));\n if (!cn) continue;\n scene.setPose(asNodeId(cid), translate(cn.pose, dx, dy, cn));\n }\n });\n return;\n }\n }\n }\n scene.setPose(asNodeId(id), pose);\n },\n setParent(id, parentId) {\n scene.move(asNodeId(id), parentId === null ? null : asNodeId(parentId));\n },\n getChildren(id) {\n // Reorder ops call this with parentId === null to mean \"root siblings.\"\n // The kit's existing callers always pass a string, so this widening is\n // additive — falls through to scene.childrenOf for the typed path and\n // returns scene.roots for the null path.\n if (id === null) return [...scene.roots];\n return [...scene.childrenOf(asNodeId(id))];\n },\n setChildOrder(parentId, ids) {\n // Batched reorder: each scene.reorder call is its own kit:move log\n // entry, but scene.batch wraps them into one undo unit + one notify.\n scene.batch('Reorder', () => {\n for (let i = 0; i < ids.length; i++) {\n // Skip ids that are already in the target slot to avoid useless\n // log entries.\n const current = parentId === null\n ? scene.roots\n : scene.childrenOf(asNodeId(parentId));\n if (current[i] === ids[i]) continue;\n scene.reorder(asNodeId(ids[i]), i);\n }\n });\n },\n getLayers() {\n return scene.layers.map((l) => ({ id: l.id, visible: l.visible, parallax: l.parallax }));\n },\n getLayout(id) {\n const declared = scene.layoutOf(asNodeId(id));\n if (declared !== null || !options.layouts) return declared;\n return typeof options.layouts === 'function'\n ? options.layouts(id)\n : options.layouts[id] ?? null;\n },\n // Unified applyOps: when a label is supplied the ops are wrapped in a\n // batch checkpoint (history-aware path used by Move/Resize/Rotate/Insert/\n // Delete tools). When a journal is active (via scene's getActiveJournal\n // option), scene.applyBatch routes to the journal instead of recording a\n // new parent-history entry. Without a label the ops are applied transiently\n // via applyOpsTo (AreaSelectAdapter contract — no checkpoint).\n applyOps(ops: Op[], label?: string) {\n if (label !== undefined) {\n scene.applyBatch(ops, label, this);\n } else {\n applyOpsTo(this, ops);\n }\n },\n // Node-mutation surface used by kit InsertOps / DeleteOps (e.g.\n // useNest wrapping the selection in a new container node, or its\n // inverse). Re-adds via the full structural spec so the round-trip\n // survives undo/redo; removes by id.\n insertNode(node: Node<TData, TLayer, TPose>, index?: number) {\n scene.add({\n kind: node.kind,\n layer: node.layer,\n pose: node.pose,\n data: node.data,\n id: node.id,\n ...(index !== undefined ? { index } : {}),\n ...(node.parent !== null ? { parent: node.parent } : {}),\n // Derivation travels with the node, or undo of a delete brings an\n // edge back as a static path that never follows its endpoints again.\n ...(node.dependsOn !== undefined ? { dependsOn: node.dependsOn } : {}),\n ...fnFieldsOfNode(node),\n });\n },\n removeNode(id: string) {\n scene.remove(asNodeId(id));\n },\n setData(id: string, data: TData) {\n scene.update(asNodeId(id), { data });\n },\n setLayout(id: string, layout: LayoutStrategy<TPose> | string | null) {\n scene.setLayout(asNodeId(id), layout);\n },\n getRemovalClosure(ids: readonly string[]): string[] {\n return [...scene.removalClosure(ids.map(asNodeId))];\n },\n // AreaSelectAdapter surface — included unconditionally so plain\n // `useSelectTool(sceneToAdapter(scene, { selection }))` Just Works for the\n // marquee gesture. `applyOps` (above) dispatches transiently when called\n // without a label, matching the AreaSelectAdapter contract.\n getSelection,\n setSelection,\n // The live marquee's and lasso's own hit-tests. Containers come back:\n // this adapter's consumer has nothing to fold children into them.\n hitTestArea(rect: Bounds, view?: RegionPickView) {\n return hitTestArea(\n scene as unknown as Scene<unknown, string, unknown>,\n rect,\n pickOptsFor(view),\n d as PoseDescriptor<unknown>,\n { includeContainers: true },\n );\n },\n hitTestLasso(polygon, mode: LassoHitMode, view?: RegionPickView) {\n return hitTestLassoPolygon(\n scene as unknown as Scene<unknown, string, unknown>,\n polygon,\n mode,\n pickOptsFor(view),\n d as PoseDescriptor<unknown>,\n { includeContainers: true },\n );\n },\n // commitInsert (gesture-time leaf insert) is opt-in: present only when\n // `options.commitInsert` is. The full insertNode/removeNode mutators\n // above are always present so kit-side InsertOp / DeleteOp round-trip\n // through scene.add / scene.remove regardless.\n //\n // Contract: this is a pure factory — it constructs a Node spec but does\n // NOT touch the scene. `useInsert`'s onEnd dispatches an `InsertOp`\n // whose apply() calls `adapter.insertNode` → `scene.add`, which is the\n // sole insertion path. Calling `scene.add` here too would double-insert,\n // and the second add throws on id collision (silently dropping the\n // gesture's pointer-up cleanup and breaking the next drag).\n ...(options.commitInsert\n ? {\n commitInsert: (bounds: { x: number; y: number; width: number; height: number }) => {\n const created = options.commitInsert!(bounds);\n if (!created) return null;\n const layer = (options.insertLayer ?? ('default' as TLayer));\n const id = asNodeId(created.id ?? adapterFallbackId());\n return {\n kind: 'leaf',\n id,\n layer,\n pose: created.pose,\n data: created.data,\n parent: null,\n } as Node<TData, TLayer, TPose>;\n },\n }\n : {}),\n // Clipboard seam (`useClipboardOps` / the paste ingestion path).\n //\n // `snapshotSelection` captures adapter-shaped node copies; `commitPaste`\n // is a pure factory like `commitInsert` above — it mints fresh nodes but\n // NEVER touches the scene. Insertion happens via the hook's InsertOps\n // (one per created node, parents before children), which is what makes a\n // whole paste a single undo entry.\n snapshotSelection(ids: string[]): ClipboardSnapshot {\n const idSet = new Set(ids);\n const taken = new Set<string>();\n const items: Node<TData, TLayer, TPose>[] = [];\n // DFS from one snapshot root: push the node, then its subtree —\n // parents-before-children, so paste can re-insert in array order.\n // A stored pose is relative to its frame, and a node whose frame is not\n // copied with it lands somewhere else entirely on paste. Capture those in\n // world coordinates: every root, and under an envelope root, the nodes\n // sharing the frame the root left behind.\n const poseFor = (n: Node<TData, TLayer, TPose>, isRoot: boolean): TPose => {\n if (!composes) return n.pose;\n if (isRoot) return adapter.getWorldPose(n.id);\n const frame = frameAtOrAbove(adapter, n.parent);\n return frame !== null && taken.has(frame) ? n.pose : adapter.getWorldPose(n.id);\n };\n // Only a derivation over `'children'` survives the copy: an id list\n // would still name the originals.\n const derivation = (n: Node<TData, TLayer, TPose>) =>\n n.dependsOn === 'children' && n.derivePose\n ? { dependsOn: n.dependsOn, derivePose: n.derivePose }\n : {};\n\n const capture = (id: string, isRoot = false): void => {\n if (taken.has(id)) return;\n const n = scene.get(asNodeId(id));\n if (!n) return;\n taken.add(id);\n if (n.kind === 'container') {\n items.push({\n kind: 'container',\n id: n.id,\n layer: n.layer,\n parent: n.parent,\n pose: copyField(poseFor(n, isRoot)),\n data: copyField(n.data),\n children: [...n.children],\n ...(n.clipFromPose ? { clipFromPose: n.clipFromPose } : {}),\n ...derivation(n),\n });\n for (const cid of scene.childrenOf(asNodeId(id))) capture(cid);\n } else {\n items.push({\n kind: 'leaf',\n id: n.id,\n layer: n.layer,\n parent: n.parent,\n pose: copyField(poseFor(n, isRoot)),\n data: copyField(n.data),\n });\n }\n };\n for (const id of ids) {\n // Dedupe: an id whose ancestor is also selected is already covered\n // by that ancestor's subtree walk.\n if (scene.ancestorsOf(asNodeId(id)).some((a) => idSet.has(a))) continue;\n capture(id, true);\n }\n return { items };\n },\n commitPaste(\n clipboard: ClipboardSnapshot,\n offset: { dx: number; dy: number },\n ctx?: { dropPoint?: { worldX: number; worldY: number } },\n ): Node<TData, TLayer, TPose>[] {\n // Snapshot items are adapter-shaped node copies: what snapshotSelection\n // captures, and what commitPaste itself returns (`useClipboardOps`\n // feeds `created` back in as the next cascade source).\n const src = clipboard.items as Node<TData, TLayer, TPose>[];\n if (src.length === 0) return [];\n const idMap = new Map<string, NodeId>();\n for (const item of src) idMap.set(item.id, asNodeId(pasteNodeId()));\n // Items whose captured parent isn't part of the snapshot become roots\n // of the pasted cluster.\n const isRoot = (item: Node<TData, TLayer, TPose>): boolean =>\n item.parent === null || !idMap.has(item.parent);\n // Under a composing strategy a node inside a pasted frame rides it, and\n // offsetting it as well would move it twice.\n const byId = new Map(src.map((item) => [item.id, item]));\n const ridesPastedFrame = (item: Node<TData, TLayer, TPose>): boolean => {\n if (!composes) return false;\n let p = item.parent === null ? undefined : byId.get(item.parent);\n while (p !== undefined) {\n if (definesFrame(p)) return true;\n p = p.parent === null ? undefined : byId.get(p.parent);\n }\n return false;\n };\n // Scene v1 poses are absolute (see the cascade notes above), so the\n // delta applies to EVERY pasted node — the cluster translates rigidly,\n // descendants staying attached to their parents.\n let dx = offset.dx;\n let dy = offset.dy;\n if (ctx?.dropPoint) {\n // Drop-point policy: land the cluster origin (min corner of the root\n // items' bounds) on the drop point; `offset` is ignored.\n let minX = Infinity;\n let minY = Infinity;\n for (const item of src) {\n if (!isRoot(item)) continue;\n const b = poseBounds(item.pose);\n if (b.x < minX) minX = b.x;\n if (b.y < minY) minY = b.y;\n }\n if (Number.isFinite(minX) && Number.isFinite(minY)) {\n dx = ctx.dropPoint.worldX - minX;\n dy = ctx.dropPoint.worldY - minY;\n }\n }\n const out: Node<TData, TLayer, TPose>[] = [];\n for (const item of src) {\n const base = {\n id: idMap.get(item.id)!,\n layer: item.layer,\n parent: isRoot(item) ? null : idMap.get(item.parent as string)!,\n pose: ridesPastedFrame(item) ? copyField(item.pose) : translate(copyField(item.pose), dx, dy),\n data: copyField(item.data),\n };\n if (item.kind === 'container') {\n out.push({\n ...base,\n kind: 'container',\n children: item.children.filter((c) => idMap.has(c)).map((c) => idMap.get(c)!),\n ...(item.clipFromPose ? { clipFromPose: item.clipFromPose } : {}),\n ...(item.dependsOn === 'children' && item.derivePose\n ? { dependsOn: item.dependsOn, derivePose: item.derivePose }\n : {}),\n });\n } else {\n out.push({ ...base, kind: 'leaf' });\n }\n }\n // `src` is parents-before-children (snapshotSelection's DFS order),\n // and this loop preserves it — children re-attach to freshly-inserted\n // parents when the hook's InsertOps apply in order.\n return out;\n },\n // Constant cascade: the hook re-snapshots the just-created items, so\n // repeated pastes stack +12/+12 each time.\n getPasteOffset(): { dx: number; dy: number } {\n return { dx: 12, dy: 12 };\n },\n };\n\n return adapter;\n}\n\n/**\n * React-hook wrapper around `sceneToAdapter`. Memoizes on every option, so\n * the adapter keeps its identity across renders unless one of them changes.\n */\nexport function useSceneAdapter<TData, TLayer extends string, TPose>(\n scene: Scene<TData, TLayer, TPose>,\n options: SceneToAdapterOptions<TData, TLayer, TPose> = {},\n): SceneCanvasAdapter<TData, TLayer, TPose> {\n const {\n selection, commitInsert, insertLayer, layouts, poseDescriptor, cascadeContainerPose, poseComposition,\n } = options;\n return useMemo(\n () => sceneToAdapter(scene, {\n selection, commitInsert, insertLayer, layouts, poseDescriptor, cascadeContainerPose, poseComposition,\n }),\n [scene, selection, commitInsert, insertLayer, layouts, poseDescriptor, cascadeContainerPose, poseComposition],\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"sceneAdapter.js","names":["adapterFallbackIdCounter","adapterFallbackId","pasteIdCounter","pasteNodeId","copyField","value","sceneToAdapter","visibleLayers","out","layer","scene","sel","options","getSelection","setSelection","d","AUTO_POSE_DESCRIPTOR","poseBounds","visualBoundsViaDescriptor","p","translate","translatePoseViaDescriptor","dx","dy","node","poseDescriptorForNode","composition","composes","pickOpts","pickOptsFor","view","regionPickOptions","adapter","asNodeId","id","visible","nid","effectivePose","n","definesFrame","composeWorldPose","g","before","after","pose","descIds","collect","cid","rootId","cn","parentId","i","ids","l","declared","label","applyOpsTo","ops","index","fnFieldsOfNode","layout","hitTestArea","rect","hitTestLassoPolygon","polygon","mode","created","bounds","idSet","taken","items","poseFor","isRoot","frame","frameAtOrAbove","derivation","capture","a","src","clipboard","idMap","item","byId","ridesPastedFrame","offset","ctx","minX","minY","b","base","c","useSceneAdapter","useMemo","selection","commitInsert","insertLayer","layouts","poseDescriptor","cascadeContainerPose","poseComposition"],"sources":["../../src/canvas/sceneAdapter.ts"],"sourcesContent":["/**\n * Synthesize a `<Canvas>`-compatible adapter from a `Scene` primitive.\n *\n * The Scene owns nodes/poses/parenting and auto-records ops on every mutation.\n * `<SceneCanvas>` calls `sceneToAdapter(scene)` and forwards the result to\n * `<Canvas>`'s `adapter` prop. Selection and hit-testing are not part of the\n * Scene model — Canvas's internal `useSelection` and pose-derived pickEvery\n * cover those.\n *\n * Pose semantics match the rest of the kit: `getPose` / `setPose` return /\n * write **local** coordinates (relative to parent). The Scene stores pose on\n * each node directly; world composition (when needed) is the renderer's job\n * via `composeWorldPose`, not the adapter's.\n */\nimport { useMemo } from 'react';\nimport type {\n AreaSelectAdapter,\n ClipboardSnapshot,\n InsertAdapter,\n LassoHitMode,\n LassoSelectAdapter,\n RegionPickView,\n LayerEnumerableAdapter,\n MoveAdapter,\n ResizeAdapter,\n RotateAdapter,\n} from 'core/adapters/types';\nimport type { LayoutStrategy } from '../layout/types';\nimport type { Op } from 'core/ops/types';\nimport type { Node, NodeId, Scene } from 'core/scene/types';\nimport { definesFrame, effectivePose } from 'core/scene/effectivePose';\nimport { fnFieldsOfNode } from 'core/scene/nodeFnFields';\nimport { asNodeId } from 'core/scene/types';\nimport { applyOpsTo } from 'core/applyOps';\nimport { hitTestArea, hitTestLassoPolygon, regionPickOptions } from 'canvas/deps/hitTestArea';\nimport type { ScenePickSourceOptions } from 'canvas/pickWalk';\nimport {\n poseDescriptorForNode,\n translatePoseViaDescriptor,\n visualBoundsViaDescriptor,\n type PoseDescriptor,\n} from 'interactions/actions/resize/geometry';\nimport { AUTO_POSE_DESCRIPTOR } from 'interactions/actions/resize/autoPoseDescriptor';\nimport { composeWorldPose, frameAtOrAbove, type PoseComposition } from 'features/groups/composePose';\nimport type { Bounds } from '../core/viewport/fitViewToBounds';\n\n/** Minimal selection contract `sceneToAdapter` needs to wire `getSelection` /\n * `setSelection`. Matches `useSelection().adapterMethods` plus an imperative\n * read; pass `useSelection()` itself or `selection.adapterMethods`. */\nexport interface SceneAdapterSelection {\n get?(): string[];\n set?(ids: string[]): void;\n getSelection?(): string[];\n setSelection?(ids: string[]): void;\n}\n\n/** Sibling z-order surface used by reorder ops. Inlined here (rather than\n * pulled from `core/ops/reorder` to avoid import cycles); the `null` parent\n * channel addresses the root sibling list. */\n/** @internal */\ninterface ReorderAdapter {\n getChildren(parentId: string | null): string[];\n setChildOrder(parentId: string | null, ids: string[]): void;\n}\n\n/** Everything the interaction layer can ask of a scene, as one type: the\n * move, resize, rotate, selection, layer and reorder contracts combined.\n * `<SceneCanvas>` synthesizes one of these, which is why most consumers never\n * touch an adapter directly. */\nexport type SceneCanvasAdapter<TData, TLayer extends string, TPose> =\n & MoveAdapter<Node<TData, TLayer, TPose>, TPose>\n & ResizeAdapter<Node<TData, TLayer, TPose>, TPose>\n & RotateAdapter<Node<TData, TLayer, TPose>, TPose>\n & AreaSelectAdapter\n & LassoSelectAdapter\n & LayerEnumerableAdapter<TLayer>\n & ReorderAdapter\n & Partial<InsertAdapter<Node<TData, TLayer, TPose>>>\n // Tighten the parts that are optional on the underlying adapter contracts\n // but unconditionally provided by a scene-backed adapter. Anything that\n // routes scene mutations through this adapter (kit InsertOps, hierarchical\n // hooks like useNest, etc.) can rely on these being present.\n & {\n getParent(id: string): string | null;\n /** The node's pose with every ancestor's frame folded in. Equal to\n * `getPose` unless a `poseComposition` is configured. */\n getWorldPose(id: string): TPose;\n /** Whether `id`'s pose is a frame its children are expressed in — see\n * `definesFrame`. */\n definesFrame(id: string): boolean;\n /** Present only when a composing strategy is configured; the render\n * walk feature-detects it. */\n composePose?(parent: TPose, child: TPose): TPose;\n getSelection(): string[];\n setSelection(ids: string[]): void;\n insertNode(node: Node<TData, TLayer, TPose>, index?: number): void;\n removeNode(id: string): void;\n /** Replace a node's `data` — what a `setData` op writes through. */\n setData(id: string, data: TData): void;\n /** Swap a container's layout — what a `setLayout` op writes through. */\n setLayout(id: string, layout: LayoutStrategy<TPose> | string | null): void;\n getRemovalClosure(ids: readonly string[]): string[];\n applyOps(ops: Op[], label?: string): void;\n snapshotSelection(ids: string[]): ClipboardSnapshot;\n commitPaste(\n clipboard: ClipboardSnapshot,\n offset: { dx: number; dy: number },\n ctx?: { dropPoint?: { worldX: number; worldY: number } },\n ): Node<TData, TLayer, TPose>[];\n getPasteOffset(clipboard: ClipboardSnapshot): { dx: number; dy: number };\n };\n\n/** Optional extras for the synthesized adapter. Pass `commitInsert` to wire\n * the insert gesture into a Scene-owned canvas; the returned object becomes\n * a leaf on the layer named by `layer` (default `'default'`). */\nexport interface SceneToAdapterOptions<TData, TLayer extends string, TPose> {\n /** Factory for new objects. Returning `null` aborts the insert. */\n commitInsert?: (bounds: Bounds) => {\n pose: TPose;\n data: TData;\n id?: string;\n } | null;\n /** Layer to place inserted nodes on. Defaults to the trivial-form layer. */\n insertLayer?: TLayer;\n /** Selection source for `AreaSelectAdapter.getSelection` / `setSelection`.\n * Pass the result of `useSelection()` (or its `adapterMethods`). When\n * omitted, `getSelection` returns `[]` and `setSelection` is a noop —\n * fine for read-only or selection-less canvases, but the marquee gesture\n * won't update any external selection state. */\n selection?: SceneAdapterSelection;\n /** How to read and rewrite this scene's poses. Default `AUTO_POSE_DESCRIPTOR`. */\n poseDescriptor?: PoseDescriptor<TPose>;\n /** Layout strategies keyed by container node id, for containers whose\n * scene node declares none (`ContainerNode.layout` wins). `move` runs its\n * layout-aware pass on them; for inserts, deletes and resizes to reach\n * them too, install them with `useLayoutArrivals`. Pass either a static\n * map, or a `getLayout(id)` function for dynamic resolution. */\n layouts?:\n | Record<string, LayoutStrategy<TPose>>\n | ((containerId: string) => LayoutStrategy<TPose> | null);\n /** When true, `setPose` on a container translates every descendant by the\n * same delta, through the pose descriptor. Scene v1 stores absolute poses,\n * so without it children stay behind. */\n cascadeContainerPose?: boolean;\n /** How a child's stored pose folds into its parent's frame. Omit for the\n * absolute-pose model, where a parent contributes no transform. Supplying a\n * composing strategy makes a container's pose a **frame**: rotating the\n * container rotates its contents, and `setPose` on it must not also\n * translate them, so it cannot be combined with `cascadeContainerPose`. */\n poseComposition?: PoseComposition<TPose>;\n}\n\n// Fallback id generator for `commitInsert` when the consumer factory omits\n// `created.id`. Mirrors `scene.ts`'s `defaultGenerateId` shape; module-scoped\n// counter keeps ids unique across adapters in a session.\nlet adapterFallbackIdCounter = 0;\nfunction adapterFallbackId(): string {\n return `n${(adapterFallbackIdCounter++).toString(36)}-${Math.random().toString(36).slice(2, 8)}`;\n}\n\n// Fresh ids for pasted nodes. The scene's own `generateId` isn't reachable\n// from the adapter, so `commitPaste` mints ids in the same shape as\n// `defaultGenerateId` with a `paste-` prefix (module-scoped counter + random\n// suffix keeps them unique across adapters and repeated pastes). Accepted\n// risk: the counter is per-tab, so cross-tab paste (the OS-clipboard path)\n// leans on the random suffix alone for uniqueness — a collision makes\n// `scene.add` throw rather than silently overwrite.\nlet pasteIdCounter = 0;\nfunction pasteNodeId(): string {\n return `paste-${(pasteIdCounter++).toString(36)}-${Math.random().toString(36).slice(2, 8)}`;\n}\n\n// Copy one node field (pose / data / children) so clipboard snapshots don't\n// alias live scene state. Spread-level copy per the clipboard contract:\n// mutating a snapshot's pose/data/children must not touch the scene.\nfunction copyField<T>(value: T): T {\n if (Array.isArray(value)) return [...value] as T;\n if (value !== null && typeof value === 'object') return { ...(value as object) } as T;\n return value;\n}\n\n/** Derive the adapter for a scene. Reads go straight to the scene; writes are\n * ops applied through its history, so anything done through the adapter is\n * undoable. */\nexport function sceneToAdapter<TData, TLayer extends string, TPose>(\n scene: Scene<TData, TLayer, TPose>,\n options: SceneToAdapterOptions<TData, TLayer, TPose> = {},\n): SceneCanvasAdapter<TData, TLayer, TPose> {\n const visibleLayers = (): Set<TLayer> => {\n const out = new Set<TLayer>();\n for (const layer of scene.layers) {\n if (layer.visible) out.add(layer.id);\n }\n return out;\n };\n\n const sel = options.selection;\n const getSelection = sel?.getSelection ?? sel?.get ?? (() => [] as string[]);\n const setSelection = sel?.setSelection ?? sel?.set ?? (() => {});\n const d = (options.poseDescriptor ?? AUTO_POSE_DESCRIPTOR) as PoseDescriptor<TPose>;\n const poseBounds = (p: TPose): Bounds => visualBoundsViaDescriptor(p, d);\n const translate = (p: TPose, dx: number, dy: number, node?: unknown): TPose =>\n translatePoseViaDescriptor(\n p, dx, dy, node === undefined ? d : poseDescriptorForNode(d, node),\n );\n\n const composition = options.poseComposition;\n const composes = composition !== undefined && composition.closure !== 'identity';\n const pickOpts: ScenePickSourceOptions<unknown> = composition\n ? { poseComposition: composition as PoseComposition<unknown> }\n : {};\n // The asking view's layer gate and camera, over the adapter's own options.\n const pickOptsFor = (view: RegionPickView | undefined): ScenePickSourceOptions<unknown> =>\n (view ? { ...pickOpts, ...regionPickOptions(view, undefined, undefined) } : pickOpts);\n if (composes && options.cascadeContainerPose) {\n throw new Error(\n 'sceneToAdapter: poseComposition and cascadeContainerPose contradict each other. ' +\n 'Under a composing strategy a child\\'s pose is already relative to its parent, so ' +\n 'the cascade would move every descendant twice. Drop cascadeContainerPose.',\n );\n }\n\n const adapter: SceneCanvasAdapter<TData, TLayer, TPose> = {\n getNode(id) {\n return scene.get(asNodeId(id));\n },\n getNodes() {\n const visible = visibleLayers();\n const out: Node<TData, TLayer, TPose>[] = [];\n for (const n of scene.renderOrderNodes()) {\n if (visible.has(n.layer)) out.push(n);\n }\n return out;\n },\n getPose(id) {\n const nid = asNodeId(id);\n const n = scene.get(nid);\n if (!n) throw new Error(`sceneToAdapter: unknown node \"${id}\"`);\n return effectivePose(scene, n);\n },\n getParent(id) {\n const n = scene.get(asNodeId(id));\n return n?.parent ?? null;\n },\n definesFrame(id) {\n const n = scene.get(asNodeId(id));\n return n === undefined || definesFrame(n);\n },\n getWorldPose(id) {\n if (!composes) return adapter.getPose(id);\n return composeWorldPose(adapter, id, composition!.compose);\n },\n // Only present when it would do something: the render walk skips the fold\n // entirely when this is absent, which is the absolute-pose path.\n ...(composes ? { composePose: composition!.compose } : {}),\n setPose(id, pose) {\n // Container cascade (opt-in): under scene v1's absolute-pose semantics,\n // moving a container needs to translate every descendant by the same\n // delta so children visually stay attached. The whole cascade lands as\n // one scene.batch so undo collapses to a single step.\n if (options.cascadeContainerPose) {\n const node = scene.get(asNodeId(id));\n if (node && node.kind === 'container') {\n const g = poseDescriptorForNode(d, node);\n const before = g.getBounds(node.pose);\n const after = g.getBounds(pose);\n const dx = after.x - before.x;\n const dy = after.y - before.y;\n if (dx !== 0 || dy !== 0) {\n const descIds: string[] = [];\n const collect = (rootId: string): void => {\n for (const cid of scene.childrenOf(asNodeId(rootId))) {\n descIds.push(cid);\n collect(cid);\n }\n };\n collect(id);\n scene.batch('move container', () => {\n scene.setPose(asNodeId(id), pose);\n for (const cid of descIds) {\n const cn = scene.get(asNodeId(cid));\n if (!cn) continue;\n scene.setPose(asNodeId(cid), translate(cn.pose, dx, dy, cn));\n }\n });\n return;\n }\n }\n }\n scene.setPose(asNodeId(id), pose);\n },\n setParent(id, parentId) {\n scene.move(asNodeId(id), parentId === null ? null : asNodeId(parentId));\n },\n getChildren(id) {\n // Reorder ops call this with parentId === null to mean \"root siblings.\"\n // The kit's existing callers always pass a string, so this widening is\n // additive — falls through to scene.childrenOf for the typed path and\n // returns scene.roots for the null path.\n if (id === null) return [...scene.roots];\n return [...scene.childrenOf(asNodeId(id))];\n },\n setChildOrder(parentId, ids) {\n // Batched reorder: each scene.reorder call is its own kit:move log\n // entry, but scene.batch wraps them into one undo unit + one notify.\n scene.batch('Reorder', () => {\n for (let i = 0; i < ids.length; i++) {\n // Skip ids that are already in the target slot to avoid useless\n // log entries.\n const current = parentId === null\n ? scene.roots\n : scene.childrenOf(asNodeId(parentId));\n if (current[i] === ids[i]) continue;\n scene.reorder(asNodeId(ids[i]), i);\n }\n });\n },\n getLayers() {\n return scene.layers.map((l) => ({ id: l.id, visible: l.visible, parallax: l.parallax }));\n },\n getLayout(id) {\n const declared = scene.layoutOf(asNodeId(id));\n if (declared !== null || !options.layouts) return declared;\n return typeof options.layouts === 'function'\n ? options.layouts(id)\n : options.layouts[id] ?? null;\n },\n // Unified applyOps: when a label is supplied the ops are wrapped in a\n // batch checkpoint (history-aware path used by Move/Resize/Rotate/Insert/\n // Delete tools). When a journal is active (via scene's getActiveJournal\n // option), scene.applyBatch routes to the journal instead of recording a\n // new parent-history entry. Without a label the ops are applied transiently\n // via applyOpsTo (AreaSelectAdapter contract — no checkpoint).\n applyOps(ops: Op[], label?: string) {\n if (label !== undefined) {\n scene.applyBatch(ops, label, this);\n } else {\n applyOpsTo(this, ops);\n }\n },\n // Node-mutation surface used by kit InsertOps / DeleteOps (e.g.\n // useNest wrapping the selection in a new container node, or its\n // inverse). Re-adds via the full structural spec so the round-trip\n // survives undo/redo; removes by id.\n insertNode(node: Node<TData, TLayer, TPose>, index?: number) {\n scene.add({\n kind: node.kind,\n layer: node.layer,\n pose: node.pose,\n data: node.data,\n id: node.id,\n ...(index !== undefined ? { index } : {}),\n ...(node.parent !== null ? { parent: node.parent } : {}),\n // Derivation travels with the node, or undo of a delete brings an\n // edge back as a static path that never follows its endpoints again.\n ...(node.dependsOn !== undefined ? { dependsOn: node.dependsOn } : {}),\n ...fnFieldsOfNode(node),\n });\n },\n removeNode(id: string) {\n scene.remove(asNodeId(id));\n },\n setData(id: string, data: TData) {\n scene.update(asNodeId(id), { data });\n },\n setLayout(id: string, layout: LayoutStrategy<TPose> | string | null) {\n scene.setLayout(asNodeId(id), layout);\n },\n getRemovalClosure(ids: readonly string[]): string[] {\n return [...scene.removalClosure(ids.map(asNodeId))];\n },\n // AreaSelectAdapter surface — included unconditionally so plain\n // `useSelectTool(sceneToAdapter(scene, { selection }))` Just Works for the\n // marquee gesture. `applyOps` (above) dispatches transiently when called\n // without a label, matching the AreaSelectAdapter contract.\n getSelection,\n setSelection,\n // The live marquee's and lasso's own hit-tests. Containers come back:\n // this adapter's consumer has nothing to fold children into them.\n hitTestArea(rect: Bounds, view?: RegionPickView) {\n return hitTestArea(\n scene,\n rect,\n pickOptsFor(view),\n d as PoseDescriptor<unknown>,\n { includeContainers: true },\n );\n },\n hitTestLasso(polygon, mode: LassoHitMode, view?: RegionPickView) {\n return hitTestLassoPolygon(\n scene,\n polygon,\n mode,\n pickOptsFor(view),\n d as PoseDescriptor<unknown>,\n { includeContainers: true },\n );\n },\n // commitInsert (gesture-time leaf insert) is opt-in: present only when\n // `options.commitInsert` is. The full insertNode/removeNode mutators\n // above are always present so kit-side InsertOp / DeleteOp round-trip\n // through scene.add / scene.remove regardless.\n //\n // Contract: this is a pure factory — it constructs a Node spec but does\n // NOT touch the scene. `useInsert`'s onEnd dispatches an `InsertOp`\n // whose apply() calls `adapter.insertNode` → `scene.add`, which is the\n // sole insertion path. Calling `scene.add` here too would double-insert,\n // and the second add throws on id collision (silently dropping the\n // gesture's pointer-up cleanup and breaking the next drag).\n ...(options.commitInsert\n ? {\n commitInsert: (bounds: { x: number; y: number; width: number; height: number }) => {\n const created = options.commitInsert!(bounds);\n if (!created) return null;\n const layer = (options.insertLayer ?? ('default' as TLayer));\n const id = asNodeId(created.id ?? adapterFallbackId());\n return {\n kind: 'leaf',\n id,\n layer,\n pose: created.pose,\n data: created.data,\n parent: null,\n } as Node<TData, TLayer, TPose>;\n },\n }\n : {}),\n // Clipboard seam (`useClipboardOps` / the paste ingestion path).\n //\n // `snapshotSelection` captures adapter-shaped node copies; `commitPaste`\n // is a pure factory like `commitInsert` above — it mints fresh nodes but\n // NEVER touches the scene. Insertion happens via the hook's InsertOps\n // (one per created node, parents before children), which is what makes a\n // whole paste a single undo entry.\n snapshotSelection(ids: string[]): ClipboardSnapshot {\n const idSet = new Set(ids);\n const taken = new Set<string>();\n const items: Node<TData, TLayer, TPose>[] = [];\n // DFS from one snapshot root: push the node, then its subtree —\n // parents-before-children, so paste can re-insert in array order.\n // A stored pose is relative to its frame, and a node whose frame is not\n // copied with it lands somewhere else entirely on paste. Capture those in\n // world coordinates: every root, and under an envelope root, the nodes\n // sharing the frame the root left behind.\n const poseFor = (n: Node<TData, TLayer, TPose>, isRoot: boolean): TPose => {\n if (!composes) return n.pose;\n if (isRoot) return adapter.getWorldPose(n.id);\n const frame = frameAtOrAbove(adapter, n.parent);\n return frame !== null && taken.has(frame) ? n.pose : adapter.getWorldPose(n.id);\n };\n // Only a derivation over `'children'` survives the copy: an id list\n // would still name the originals.\n const derivation = (n: Node<TData, TLayer, TPose>) =>\n n.dependsOn === 'children' && n.derivePose\n ? { dependsOn: n.dependsOn, derivePose: n.derivePose }\n : {};\n\n const capture = (id: string, isRoot = false): void => {\n if (taken.has(id)) return;\n const n = scene.get(asNodeId(id));\n if (!n) return;\n taken.add(id);\n if (n.kind === 'container') {\n items.push({\n kind: 'container',\n id: n.id,\n layer: n.layer,\n parent: n.parent,\n pose: copyField(poseFor(n, isRoot)),\n data: copyField(n.data),\n children: [...n.children],\n ...(n.clipFromPose ? { clipFromPose: n.clipFromPose } : {}),\n ...derivation(n),\n });\n for (const cid of scene.childrenOf(asNodeId(id))) capture(cid);\n } else {\n items.push({\n kind: 'leaf',\n id: n.id,\n layer: n.layer,\n parent: n.parent,\n pose: copyField(poseFor(n, isRoot)),\n data: copyField(n.data),\n });\n }\n };\n for (const id of ids) {\n // Dedupe: an id whose ancestor is also selected is already covered\n // by that ancestor's subtree walk.\n if (scene.ancestorsOf(asNodeId(id)).some((a) => idSet.has(a))) continue;\n capture(id, true);\n }\n return { items };\n },\n commitPaste(\n clipboard: ClipboardSnapshot,\n offset: { dx: number; dy: number },\n ctx?: { dropPoint?: { worldX: number; worldY: number } },\n ): Node<TData, TLayer, TPose>[] {\n // Snapshot items are adapter-shaped node copies: what snapshotSelection\n // captures, and what commitPaste itself returns (`useClipboardOps`\n // feeds `created` back in as the next cascade source).\n const src = clipboard.items as Node<TData, TLayer, TPose>[];\n if (src.length === 0) return [];\n const idMap = new Map<string, NodeId>();\n for (const item of src) idMap.set(item.id, asNodeId(pasteNodeId()));\n // Items whose captured parent isn't part of the snapshot become roots\n // of the pasted cluster.\n const isRoot = (item: Node<TData, TLayer, TPose>): boolean =>\n item.parent === null || !idMap.has(item.parent);\n // Under a composing strategy a node inside a pasted frame rides it, and\n // offsetting it as well would move it twice.\n const byId = new Map(src.map((item) => [item.id, item]));\n const ridesPastedFrame = (item: Node<TData, TLayer, TPose>): boolean => {\n if (!composes) return false;\n let p = item.parent === null ? undefined : byId.get(item.parent);\n while (p !== undefined) {\n if (definesFrame(p)) return true;\n p = p.parent === null ? undefined : byId.get(p.parent);\n }\n return false;\n };\n // Scene v1 poses are absolute (see the cascade notes above), so the\n // delta applies to EVERY pasted node — the cluster translates rigidly,\n // descendants staying attached to their parents.\n let dx = offset.dx;\n let dy = offset.dy;\n if (ctx?.dropPoint) {\n // Drop-point policy: land the cluster origin (min corner of the root\n // items' bounds) on the drop point; `offset` is ignored.\n let minX = Infinity;\n let minY = Infinity;\n for (const item of src) {\n if (!isRoot(item)) continue;\n const b = poseBounds(item.pose);\n if (b.x < minX) minX = b.x;\n if (b.y < minY) minY = b.y;\n }\n if (Number.isFinite(minX) && Number.isFinite(minY)) {\n dx = ctx.dropPoint.worldX - minX;\n dy = ctx.dropPoint.worldY - minY;\n }\n }\n const out: Node<TData, TLayer, TPose>[] = [];\n for (const item of src) {\n const base = {\n id: idMap.get(item.id)!,\n layer: item.layer,\n parent: isRoot(item) ? null : idMap.get(item.parent as string)!,\n pose: ridesPastedFrame(item) ? copyField(item.pose) : translate(copyField(item.pose), dx, dy),\n data: copyField(item.data),\n };\n if (item.kind === 'container') {\n out.push({\n ...base,\n kind: 'container',\n children: item.children.filter((c) => idMap.has(c)).map((c) => idMap.get(c)!),\n ...(item.clipFromPose ? { clipFromPose: item.clipFromPose } : {}),\n ...(item.dependsOn === 'children' && item.derivePose\n ? { dependsOn: item.dependsOn, derivePose: item.derivePose }\n : {}),\n });\n } else {\n out.push({ ...base, kind: 'leaf' });\n }\n }\n // `src` is parents-before-children (snapshotSelection's DFS order),\n // and this loop preserves it — children re-attach to freshly-inserted\n // parents when the hook's InsertOps apply in order.\n return out;\n },\n // Constant cascade: the hook re-snapshots the just-created items, so\n // repeated pastes stack +12/+12 each time.\n getPasteOffset(): { dx: number; dy: number } {\n return { dx: 12, dy: 12 };\n },\n };\n\n return adapter;\n}\n\n/**\n * React-hook wrapper around `sceneToAdapter`. Memoizes on every option, so\n * the adapter keeps its identity across renders unless one of them changes.\n */\nexport function useSceneAdapter<TData, TLayer extends string, TPose>(\n scene: Scene<TData, TLayer, TPose>,\n options: SceneToAdapterOptions<TData, TLayer, TPose> = {},\n): SceneCanvasAdapter<TData, TLayer, TPose> {\n const {\n selection, commitInsert, insertLayer, layouts, poseDescriptor, cascadeContainerPose, poseComposition,\n } = options;\n return useMemo(\n () => sceneToAdapter(scene, {\n selection, commitInsert, insertLayer, layouts, poseDescriptor, cascadeContainerPose, poseComposition,\n }),\n [scene, selection, commitInsert, insertLayer, layouts, poseDescriptor, cascadeContainerPose, poseComposition],\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ import { useMemo as e } from "react";
2
+ import { useLatest as t } from "@weasel-js/react";
3
+ //#region src/canvas/useHitObserver.ts
4
+ function n(n, r) {
5
+ let i = t(n), a = !!n;
6
+ return e(() => {
7
+ if (a) return (e) => {
8
+ let t = r.current?.(e.x, e.y);
9
+ i.current?.(t ? {
10
+ id: t.id,
11
+ kind: t.kind
12
+ } : null);
13
+ };
14
+ }, [
15
+ a,
16
+ i,
17
+ r
18
+ ]);
19
+ }
20
+ //#endregion
21
+ export { n as useHitObserver };
22
+
23
+ //# sourceMappingURL=useHitObserver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useHitObserver.js","names":["useHitObserver","cbRef","useLatest","cb","wired","useMemo","result","pickerRef","world"],"sources":["../../src/canvas/useHitObserver.ts"],"sourcesContent":["import { useMemo, type MutableRefObject } from 'react';\nimport { useLatest } from '@weasel-js/react';\nimport type { SceneCanvasHit } from './SceneCanvas';\n\ntype Picker = ((x: number, y: number) => { id: string; kind: string } | null | undefined) | undefined;\n\n/** Turns a consumer's hit callback into the world-point observer the gesture\n * dispatcher takes, resolving the hit with the canvas's own picker. Identity\n * changes only between wired and not, so the dispatcher isn't rebuilt per\n * render. */\nexport function useHitObserver(\n cb: ((hit: SceneCanvasHit | null) => void) | undefined,\n pickerRef: MutableRefObject<Picker>,\n): ((world: { x: number; y: number }) => void) | undefined {\n const cbRef = useLatest(cb);\n const wired = Boolean(cb);\n return useMemo(() => {\n if (!wired) return undefined;\n return (world) => {\n const result = pickerRef.current?.(world.x, world.y);\n cbRef.current?.(result ? { id: result.id, kind: result.kind } : null);\n };\n }, [wired, cbRef, pickerRef]);\n}\n"],"mappings":";;;AAUA,SAAgBA,EACd,GACA,GACyD;CACzD,IAAMC,IAAQC,EAAUC,CAAE,GACpBC,IAAQ,EAAQD;CACtB,OAAOE,QAAc;EACnB,IAAKD,GACL,QAAQ,MAAU;GAChB,IAAME,IAASC,EAAU,UAAUC,EAAM,GAAGA,EAAM,CAAC;GACnD,EAAM,UAAUF,IAAS;IAAE,IAAIA,EAAO;IAAI,MAAMA,EAAO;GAAK,IAAI,IAAI;EACtE;CACF,GAAG;EAACF;EAAOH;EAAOM;CAAS,CAAC;AAC9B"}
@@ -1 +1 @@
1
- {"version":3,"file":"useHostAnchor.js","names":["useHostAnchor","panel","setPanel","useState","style","setStyle","ref","useCallback","el","resolve","useLatest","resolveHost","options","alignX","alignY","align","offsetX","offsetY","offset","recompute","hostEl","h","p","viewport","rect","hostAnchorRect","hostAnchorCss","padding","raf","useVisibleRaf","useEffect","schedule","observer"],"sources":["../../src/canvas/useHostAnchor.ts"],"sourcesContent":["import { useCallback, useEffect, useState } from 'react';\nimport { useLatest } from '@weasel-js/react';\nimport { useVisibleRaf } from '../scheduling/useVisibleRaf';\nimport {\n hostAnchorCss, hostAnchorRect,\n type HostAnchorAlign, type HostAnchorOffset,\n} from './hostAnchor';\n\n/** Options for {@link useHostAnchor}. */\nexport interface UseHostAnchorOptions {\n align: HostAnchorAlign;\n offset: HostAnchorOffset;\n /** Minimum gap kept between the panel and the viewport edge. Default 0. */\n padding?: number;\n}\n\n/** CSS the caller spreads onto the panel; which edges appear follows `align`. */\nexport type HostAnchorStyle = { top?: number; right?: number; bottom?: number; left?: number };\n\n/**\n * Hold a fixed-position panel against a host element's corner, kept inside the\n * viewport, following it through scrolls, resizes and reflows.\n *\n * `resolveHost` runs on every recompute, so a host that mounts late or is found\n * by selector works the same as one held in a ref. Put the returned `ref` on the\n * panel: until it is measured the panel counts as zero-sized, which resolves to\n * the bare corner, so the first paint lands where an unclamped anchor would and\n * measurement only ever pulls it back on screen.\n */\nexport function useHostAnchor(\n resolveHost: () => Element | null,\n options: UseHostAnchorOptions,\n): { ref: (el: HTMLElement | null) => void; style: HostAnchorStyle | null } {\n const [panel, setPanel] = useState<HTMLElement | null>(null);\n const [style, setStyle] = useState<HostAnchorStyle | null>(null);\n const ref = useCallback((el: HTMLElement | null) => setPanel(el), []);\n\n // Held in a ref so a caller passing an inline arrow doesn't re-subscribe\n // every render.\n const resolve = useLatest(resolveHost);\n\n const { align, offset, padding = 0 } = options;\n const { x: alignX, y: alignY } = align;\n const { x: offsetX, y: offsetY } = offset;\n\n const recompute = useCallback(() => {\n const hostEl = resolve.current();\n if (!hostEl) {\n setStyle(null);\n return;\n }\n const h = hostEl.getBoundingClientRect();\n const p = panel?.getBoundingClientRect();\n const viewport = { width: window.innerWidth, height: window.innerHeight };\n const align = { x: alignX, y: alignY };\n const rect = hostAnchorRect({\n host: { x: h.left, y: h.top, width: h.width, height: h.height },\n panel: { width: p?.width ?? 0, height: p?.height ?? 0 },\n viewport,\n align,\n offset: { x: offsetX, y: offsetY },\n padding,\n });\n setStyle(hostAnchorCss(rect, align, viewport));\n }, [resolve, panel, alignX, alignY, offsetX, offsetY, padding]);\n\n // Coalesces bursts of scroll/resize into one frame, and holds the work while\n // the tab is hidden rather than dropping it.\n const raf = useVisibleRaf(recompute);\n\n useEffect(() => {\n recompute();\n const schedule = () => raf.request();\n window.addEventListener('scroll', schedule, true);\n window.addEventListener('resize', schedule);\n const observer =\n typeof ResizeObserver !== 'undefined' ? new ResizeObserver(schedule) : null;\n if (panel) observer?.observe(panel);\n const hostEl = resolve.current();\n if (hostEl) observer?.observe(hostEl);\n\n return () => {\n window.removeEventListener('scroll', schedule, true);\n window.removeEventListener('resize', schedule);\n raf.cancel();\n observer?.disconnect();\n };\n }, [recompute, raf, panel, resolve]);\n\n return { ref, style };\n}\n"],"mappings":";;;;;AA6BA,SAAgBA,EACd,GACA,GAC0E;CAC1E,IAAM,CAACC,GAAOC,KAAYC,EAA6B,IAAI,GACrD,CAACC,GAAOC,KAAYF,EAAiC,IAAI,GACzDG,IAAMC,GAAa,MAA2BL,EAASM,CAAE,GAAG,CAAC,CAAC,GAI9DC,IAAUC,EAAUC,CAAW,GAE/B,EAAE,UAAO,WAAQ,aAAU,MAAMC,GACjC,EAAE,GAAGC,GAAWC,MAAWC,GAC3B,EAAE,GAAGC,GAAS,GAAGC,MAAYC,GAE7BC,IAAYZ,QAAkB;EAClC,IAAMa,IAASX,EAAQ,QAAQ;EAC/B,IAAI,CAACW,GAAQ;GACX,EAAS,IAAI;GACb;EACF;EACA,IAAMC,IAAID,EAAO,sBAAsB,GACjCE,IAAIrB,GAAO,sBAAsB,GACjCsB,IAAW;GAAE,OAAO,OAAO;GAAY,QAAQ,OAAO;EAAY,GAClER,IAAQ;GAAE,GAAGF;GAAWC;EAAO,GAC/BU,IAAOC,EAAe;GAC1B,MAAM;IAAE,GAAGJ,EAAE;IAAM,GAAGA,EAAE;IAAK,OAAOA,EAAE;IAAO,QAAQA,EAAE;GAAO;GAC9D,OAAO;IAAE,OAAOC,GAAG,SAAS;IAAG,QAAQA,GAAG,UAAU;GAAE;GACtD;GACA;GACA,QAAQ;IAAE,GAAGN;IAAS,GAAGC;GAAQ;GACjC;EACF,CAAC;EACD,EAASS,EAAcF,GAAMT,GAAOQ,CAAQ,CAAC;CAC/C,GAAG;EAACd;EAASR;EAAOY;EAAQC;EAAQE;EAASC;EAASU;CAAO,CAAC,GAIxDC,IAAMC,EAAcV,CAAS;CAqBnC,OAnBAW,QAAgB;EACd,EAAU;EACV,IAAMC,UAAiBH,EAAI,QAAQ;EAEnC,AADA,OAAO,iBAAiB,UAAUG,GAAU,EAAI,GAChD,OAAO,iBAAiB,UAAUA,CAAQ;EAC1C,IAAMC,IACJ,OAAO,iBAAmB,MAAc,IAAI,eAAeD,CAAQ,IAAI;EACzE,AAAI9B,KAAO+B,GAAU,QAAQ/B,CAAK;EAClC,IAAMmB,IAASX,EAAQ,QAAQ;EAG/B,OAFIW,KAAQY,GAAU,QAAQZ,CAAM,SAEvB;GAIX,AAHA,OAAO,oBAAoB,UAAUW,GAAU,EAAI,GACnD,OAAO,oBAAoB,UAAUA,CAAQ,GAC7CH,EAAI,OAAO,GACXI,GAAU,WAAW;EACvB;CACF,GAAG;EAACb;EAAWS;EAAK3B;EAAOQ;CAAO,CAAC,GAE5B;EAAE;EAAK;CAAM;AACtB"}
1
+ {"version":3,"file":"useHostAnchor.js","names":["useHostAnchor","panel","setPanel","useState","style","setStyle","ref","useCallback","el","resolve","useLatest","resolveHost","options","alignX","alignY","align","offsetX","offsetY","offset","recompute","hostEl","h","p","viewport","rect","hostAnchorRect","hostAnchorCss","padding","raf","useVisibleRaf","useEffect","schedule","observer"],"sources":["../../src/canvas/useHostAnchor.ts"],"sourcesContent":["import { useCallback, useEffect, useState } from 'react';\nimport { useLatest } from '@weasel-js/react';\nimport { useVisibleRaf } from '../scheduling/useVisibleRaf';\nimport {\n hostAnchorCss, hostAnchorRect,\n type HostAnchorAlign, type HostAnchorOffset,\n} from './hostAnchor';\n\n/** Options for {@link useHostAnchor}. */\nexport interface UseHostAnchorOptions {\n align: HostAnchorAlign;\n offset: HostAnchorOffset;\n /** Minimum gap kept between the panel and the viewport edge. Default 0. */\n padding?: number;\n}\n\n/** CSS the caller spreads onto the panel; which edges appear follows `align`. */\nexport type HostAnchorStyle = { top?: number; right?: number; bottom?: number; left?: number };\n\n/**\n * Hold a fixed-position panel against a host element's corner, kept inside the\n * viewport, following it through scrolls, resizes and reflows.\n *\n * `resolveHost` runs on every recompute, so a host that mounts late or is found\n * by selector works the same as one held in a ref. Put the returned `ref` on the\n * panel: until it is measured the panel counts as zero-sized, which resolves to\n * the bare corner, so the first paint lands where an unclamped anchor would and\n * measurement only ever pulls it back on screen.\n */\nexport function useHostAnchor(\n resolveHost: () => Element | null,\n options: UseHostAnchorOptions,\n): { ref: (el: HTMLElement | null) => void; style: HostAnchorStyle | null } {\n const [panel, setPanel] = useState<HTMLElement | null>(null);\n const [style, setStyle] = useState<HostAnchorStyle | null>(null);\n const ref = useCallback((el: HTMLElement | null) => setPanel(el), []);\n\n // Held in a ref so a caller passing an inline arrow doesn't re-subscribe\n // every render.\n const resolve = useLatest(resolveHost);\n\n const { align, offset, padding = 0 } = options;\n const { x: alignX, y: alignY } = align;\n const { x: offsetX, y: offsetY } = offset;\n\n const recompute = useCallback(() => {\n const hostEl = resolve.current();\n if (!hostEl) {\n setStyle(null);\n return;\n }\n const h = hostEl.getBoundingClientRect();\n const p = panel?.getBoundingClientRect();\n const viewport = { width: window.innerWidth, height: window.innerHeight };\n const align = { x: alignX, y: alignY };\n const rect = hostAnchorRect({\n host: { x: h.left, y: h.top, width: h.width, height: h.height },\n panel: { width: p?.width ?? 0, height: p?.height ?? 0 },\n viewport,\n align,\n offset: { x: offsetX, y: offsetY },\n padding,\n });\n setStyle(hostAnchorCss(rect, align, viewport));\n }, [resolve, panel, alignX, alignY, offsetX, offsetY, padding]);\n\n // Coalesces bursts of scroll/resize into one frame, and holds the work while\n // the tab is hidden rather than dropping it.\n const raf = useVisibleRaf(recompute);\n\n useEffect(() => {\n // eslint-disable-next-line react-hooks/set-state-in-effect -- measures layout\n recompute();\n const schedule = () => raf.request();\n window.addEventListener('scroll', schedule, true);\n window.addEventListener('resize', schedule);\n const observer =\n typeof ResizeObserver !== 'undefined' ? new ResizeObserver(schedule) : null;\n if (panel) observer?.observe(panel);\n const hostEl = resolve.current();\n if (hostEl) observer?.observe(hostEl);\n\n return () => {\n window.removeEventListener('scroll', schedule, true);\n window.removeEventListener('resize', schedule);\n raf.cancel();\n observer?.disconnect();\n };\n }, [recompute, raf, panel, resolve]);\n\n return { ref, style };\n}\n"],"mappings":";;;;;AA6BA,SAAgBA,EACd,GACA,GAC0E;CAC1E,IAAM,CAACC,GAAOC,KAAYC,EAA6B,IAAI,GACrD,CAACC,GAAOC,KAAYF,EAAiC,IAAI,GACzDG,IAAMC,GAAa,MAA2BL,EAASM,CAAE,GAAG,CAAC,CAAC,GAI9DC,IAAUC,EAAUC,CAAW,GAE/B,EAAE,UAAO,WAAQ,aAAU,MAAMC,GACjC,EAAE,GAAGC,GAAWC,MAAWC,GAC3B,EAAE,GAAGC,GAAS,GAAGC,MAAYC,GAE7BC,IAAYZ,QAAkB;EAClC,IAAMa,IAASX,EAAQ,QAAQ;EAC/B,IAAI,CAACW,GAAQ;GACX,EAAS,IAAI;GACb;EACF;EACA,IAAMC,IAAID,EAAO,sBAAsB,GACjCE,IAAIrB,GAAO,sBAAsB,GACjCsB,IAAW;GAAE,OAAO,OAAO;GAAY,QAAQ,OAAO;EAAY,GAClER,IAAQ;GAAE,GAAGF;GAAWC;EAAO,GAC/BU,IAAOC,EAAe;GAC1B,MAAM;IAAE,GAAGJ,EAAE;IAAM,GAAGA,EAAE;IAAK,OAAOA,EAAE;IAAO,QAAQA,EAAE;GAAO;GAC9D,OAAO;IAAE,OAAOC,GAAG,SAAS;IAAG,QAAQA,GAAG,UAAU;GAAE;GACtD;GACA;GACA,QAAQ;IAAE,GAAGN;IAAS,GAAGC;GAAQ;GACjC;EACF,CAAC;EACD,EAASS,EAAcF,GAAMT,GAAOQ,CAAQ,CAAC;CAC/C,GAAG;EAACd;EAASR;EAAOY;EAAQC;EAAQE;EAASC;EAASU;CAAO,CAAC,GAIxDC,IAAMC,EAAcV,CAAS;CAsBnC,OApBAW,QAAgB;EAEd,EAAU;EACV,IAAMC,UAAiBH,EAAI,QAAQ;EAEnC,AADA,OAAO,iBAAiB,UAAUG,GAAU,EAAI,GAChD,OAAO,iBAAiB,UAAUA,CAAQ;EAC1C,IAAMC,IACJ,OAAO,iBAAmB,MAAc,IAAI,eAAeD,CAAQ,IAAI;EACzE,AAAI9B,KAAO+B,GAAU,QAAQ/B,CAAK;EAClC,IAAMmB,IAASX,EAAQ,QAAQ;EAG/B,OAFIW,KAAQY,GAAU,QAAQZ,CAAM,SAEvB;GAIX,AAHA,OAAO,oBAAoB,UAAUW,GAAU,EAAI,GACnD,OAAO,oBAAoB,UAAUA,CAAQ,GAC7CH,EAAI,OAAO,GACXI,GAAU,WAAW;EACvB;CACF,GAAG;EAACb;EAAWS;EAAK3B;EAAOQ;CAAO,CAAC,GAE5B;EAAE;EAAK;CAAM;AACtB"}