@weasel-js/core 1.15.1 → 1.15.2

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 (91) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/dist/animation/poseTarget.js +3 -3
  3. package/dist/animation/poseTarget.js.map +1 -1
  4. package/dist/animation/reflow.js +7 -7
  5. package/dist/animation/reflow.js.map +1 -1
  6. package/dist/animation/rig/rigNode.js +2 -2
  7. package/dist/animation/rig/rigNode.js.map +1 -1
  8. package/dist/barrel/input.js +2 -1
  9. package/dist/barrel/paint.js +2 -2
  10. package/dist/barrel/viewport.js +3 -3
  11. package/dist/canvas/SceneCanvas/PointerProviderIfRoot.js +17 -64
  12. package/dist/canvas/SceneCanvas/PointerProviderIfRoot.js.map +1 -1
  13. package/dist/canvas/accentColor.js +3 -3
  14. package/dist/canvas/accentColor.js.map +1 -1
  15. package/dist/canvas/deps/enteredGroup.js +5 -4
  16. package/dist/canvas/deps/enteredGroup.js.map +1 -1
  17. package/dist/canvas/deps/selectionSnapshot.js +7 -2
  18. package/dist/canvas/deps/selectionSnapshot.js.map +1 -1
  19. package/dist/canvas/slopsDebugLayer.js +8 -2
  20. package/dist/canvas/slopsDebugLayer.js.map +1 -1
  21. package/dist/canvas/useViewRegistrySurface.js +6 -3
  22. package/dist/canvas/useViewRegistrySurface.js.map +1 -1
  23. package/dist/canvas/viewRegistry.js +5 -5
  24. package/dist/canvas/viewRegistry.js.map +1 -1
  25. package/dist/core/ops/reorder/index.js +2 -6
  26. package/dist/core/ops/reorder/index.js.map +1 -1
  27. package/dist/core/pages/fitViewToPage.js +2 -7
  28. package/dist/core/pages/fitViewToPage.js.map +1 -1
  29. package/dist/core/pages/pages.js +3 -9
  30. package/dist/core/pages/pages.js.map +1 -1
  31. package/dist/core/paint/meshKindLazy.js +9 -5
  32. package/dist/core/paint/meshKindLazy.js.map +1 -1
  33. package/dist/core/scene/pathEffects/blend.js +2 -2
  34. package/dist/core/scene/pathEffects/blend.js.map +1 -1
  35. package/dist/core/scene/pathEffects/textAlong.js +5 -4
  36. package/dist/core/scene/pathEffects/textAlong.js.map +1 -1
  37. package/dist/core/scene/scene.js +12 -1
  38. package/dist/core/scene/scene.js.map +1 -1
  39. package/dist/core/viewport/clientToWorld.js +7 -1
  40. package/dist/core/viewport/clientToWorld.js.map +1 -1
  41. package/dist/core/viewport/zoomAt.js +6 -6
  42. package/dist/core/viewport/zoomAt.js.map +1 -1
  43. package/dist/devtools/root.js +11 -4
  44. package/dist/devtools/root.js.map +1 -1
  45. package/dist/devtools/transport.js +12 -16
  46. package/dist/devtools/transport.js.map +1 -1
  47. package/dist/devtools-protocol.d.ts +0 -11
  48. package/dist/domains/measure/measureHit.js +2 -8
  49. package/dist/domains/measure/measureHit.js.map +1 -1
  50. package/dist/domains/paths/anchorMarkers.js +5 -11
  51. package/dist/domains/paths/anchorMarkers.js.map +1 -1
  52. package/dist/domains/paths/pathEditingOverlayLayer.js +5 -9
  53. package/dist/domains/paths/pathEditingOverlayLayer.js.map +1 -1
  54. package/dist/domains/paths/penPreviewLayer.js +9 -12
  55. package/dist/domains/paths/penPreviewLayer.js.map +1 -1
  56. package/dist/domains/paths/shapeGenerator.js +1 -2
  57. package/dist/domains/paths/shapeGenerator.js.map +1 -1
  58. package/dist/domains/snap/angle2d.js +2 -1
  59. package/dist/domains/snap/angle2d.js.map +1 -1
  60. package/dist/domains/snap/outcomeStore.js +5 -8
  61. package/dist/domains/snap/outcomeStore.js.map +1 -1
  62. package/dist/domains/text/useSceneTextEdit.js +9 -4
  63. package/dist/domains/text/useSceneTextEdit.js.map +1 -1
  64. package/dist/domains/viewports/aim.js +3 -8
  65. package/dist/domains/viewports/aim.js.map +1 -1
  66. package/dist/domains/viewports/viewResolver.js +5 -3
  67. package/dist/domains/viewports/viewResolver.js.map +1 -1
  68. package/dist/index.d.ts +20 -21
  69. package/dist/index.js +5 -4
  70. package/dist/input/usePublishPointer.js +59 -0
  71. package/dist/input/usePublishPointer.js.map +1 -0
  72. package/dist/interactions/actions/defaults/containerScroll.js +9 -3
  73. package/dist/interactions/actions/defaults/containerScroll.js.map +1 -1
  74. package/dist/interactions/actions/defaults/enterGroup.js +3 -3
  75. package/dist/interactions/actions/defaults/enterGroup.js.map +1 -1
  76. package/dist/interactions/actions/defaults/pathEditMarks.js +2 -5
  77. package/dist/interactions/actions/defaults/pathEditMarks.js.map +1 -1
  78. package/dist/interactions/actions/edit-anchors/handleDrag.js +4 -7
  79. package/dist/interactions/actions/edit-anchors/handleDrag.js.map +1 -1
  80. package/dist/interactions/actions/move/behaviors/clampMove.js +42 -0
  81. package/dist/interactions/actions/move/behaviors/clampMove.js.map +1 -0
  82. package/dist/interactions/actions/move/layout/container.js +3 -6
  83. package/dist/interactions/actions/move/layout/container.js.map +1 -1
  84. package/dist/interactions/actions/move/layout/exits.js +2 -2
  85. package/dist/interactions/actions/move/layout/exits.js.map +1 -1
  86. package/dist/interactions/actions/transform-again/memory.js +2 -2
  87. package/dist/interactions/actions/transform-again/memory.js.map +1 -1
  88. package/dist/move.d.ts +18 -3
  89. package/dist/move.js +2 -1
  90. package/dist/version.js +1 -1
  91. package/package.json +25 -25
@@ -1 +1 @@
1
- {"version":3,"file":"PointerProviderIfRoot.js","names":["_Fragment"],"sources":["../../../src/canvas/SceneCanvas/PointerProviderIfRoot.tsx"],"sourcesContent":["/**\n * Conditional `<PointerContextProvider>` wrapper. Mounts a provider only\n * when no parent provider is in scope — otherwise renders children unwrapped\n * so SceneCanvas defers to the host's existing scope.\n *\n * Pairs with `usePointerPublisher`, which attaches native pointermove/leave\n * listeners to the canvas element and writes world-space coords into the\n * surrounding context's `pointerRef`.\n */\nimport { useEffect, type MutableRefObject, type ReactNode, type RefObject } from 'react';\nimport { heldPointers } from '@weasel-js/gestures';\nimport { PointerContextProvider, usePointerContext } from '@weasel-js/routing/react';\nimport type { CanvasExtensionApi } from '../canvasExtension';\nimport { useOptionalViewRegistry } from '../viewRegistry';\nimport { worldPointAt } from './worldPointAt';\n\n/** Mount a `<PointerContextProvider>` only when none is already in scope. */\nexport function PointerProviderIfRoot({ children }: { children: ReactNode }) {\n const parent = usePointerContext();\n if (parent) return <>{children}</>;\n return <PointerContextProvider>{children}</PointerContextProvider>;\n}\n\n/**\n * Attaches native `pointermove` / `pointerleave` listeners to the supplied\n * canvas element and publishes the world-space pointer coordinates into the\n * surrounding `<PointerContextProvider>`. No-op when no provider is in scope\n * (matches \"publish if anyone is listening\").\n *\n * Called inside the provider scope so it sees both consumer-mounted providers\n * and SceneCanvas's auto-mounted one.\n */\nexport function usePointerPublisher(\n canvasRef: MutableRefObject<HTMLElement | null>,\n canvasApiRef: RefObject<CanvasExtensionApi | null>,\n): void {\n const ctx = usePointerContext();\n const registry = useOptionalViewRegistry();\n useEffect(() => {\n if (!ctx) return;\n const el = canvasRef.current;\n if (!el) return;\n // The surface's one resolver answers which camera a client point belongs\n // to, so paste-at-pointer lands in the panel the cursor is over. With no\n // views declared it answers the canvas's own camera and rect.\n const publish = (pointerId: number | null, clientX: number, clientY: number): void => {\n const api = canvasApiRef.current;\n if (!api) return;\n const target = registry?.resolver.at(pointerId, clientX, clientY);\n const [worldX, worldY] = worldPointAt(api, target, clientX, clientY);\n ctx.set({ world: { x: worldX, y: worldY }, viewId: target?.id ?? null });\n };\n const onMove = (e: PointerEvent): void => publish(e.pointerId, e.clientX, e.clientY);\n const held = heldPointers(el);\n const onLeave = (e: PointerEvent): void => {\n // A pointer held on the canvas is still working it once it leaves the\n // rect, and what it drives needs the latest world position.\n if (held.count() > 0) {\n publish(e.pointerId, e.clientX, e.clientY);\n return;\n }\n ctx.set(null);\n };\n // A drag that began on the canvas keeps reporting once it is off it; the\n // canvas's own listener has the moves that still target it.\n const onDocMove = (e: PointerEvent): void => {\n if (e.target === el) return;\n if (held.count() === 0) return;\n publish(e.pointerId, e.clientX, e.clientY);\n };\n el.addEventListener('pointermove', onMove);\n el.addEventListener('pointerleave', onLeave);\n document.addEventListener('pointermove', onDocMove);\n return () => {\n el.removeEventListener('pointermove', onMove);\n el.removeEventListener('pointerleave', onLeave);\n document.removeEventListener('pointermove', onDocMove);\n held.release();\n };\n // canvasRef + registry + ctx identity are all stable across the lifetime\n // of the surrounding provider; effect runs once per mount.\n }, [ctx, canvasRef, canvasApiRef, registry]);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAiBA,SAAgB,sBAAsB,EAAE,YAAqC;CAE3E,IADe,kBACX,GAAQ,OAAO,oBAAAA,UAAA,EAAG,SAAW,CAAA;CACjC,OAAO,oBAAC,wBAAD,EAAyB,SAAiC,CAAA;AACnE;;;;;;;;;;AAWA,SAAgB,oBACd,WACA,cACM;CACN,MAAM,MAAM,kBAAkB;CAC9B,MAAM,WAAW,wBAAwB;CACzC,gBAAgB;EACd,IAAI,CAAC,KAAK;EACV,MAAM,KAAK,UAAU;EACrB,IAAI,CAAC,IAAI;EAIT,MAAM,WAAW,WAA0B,SAAiB,YAA0B;GACpF,MAAM,MAAM,aAAa;GACzB,IAAI,CAAC,KAAK;GACV,MAAM,SAAS,UAAU,SAAS,GAAG,WAAW,SAAS,OAAO;GAChE,MAAM,CAAC,QAAQ,UAAU,aAAa,KAAK,QAAQ,SAAS,OAAO;GACnE,IAAI,IAAI;IAAE,OAAO;KAAE,GAAG;KAAQ,GAAG;IAAO;IAAG,QAAQ,QAAQ,MAAM;GAAK,CAAC;EACzE;EACA,MAAM,UAAU,MAA0B,QAAQ,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO;EACnF,MAAM,OAAO,aAAa,EAAE;EAC5B,MAAM,WAAW,MAA0B;GAGzC,IAAI,KAAK,MAAM,IAAI,GAAG;IACpB,QAAQ,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO;IACzC;GACF;GACA,IAAI,IAAI,IAAI;EACd;EAGA,MAAM,aAAa,MAA0B;GAC3C,IAAI,EAAE,WAAW,IAAI;GACrB,IAAI,KAAK,MAAM,MAAM,GAAG;GACxB,QAAQ,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO;EAC3C;EACA,GAAG,iBAAiB,eAAe,MAAM;EACzC,GAAG,iBAAiB,gBAAgB,OAAO;EAC3C,SAAS,iBAAiB,eAAe,SAAS;EAClD,aAAa;GACX,GAAG,oBAAoB,eAAe,MAAM;GAC5C,GAAG,oBAAoB,gBAAgB,OAAO;GAC9C,SAAS,oBAAoB,eAAe,SAAS;GACrD,KAAK,QAAQ;EACf;CAGF,GAAG;EAAC;EAAK;EAAW;EAAc;CAAQ,CAAC;AAC7C"}
1
+ {"version":3,"file":"PointerProviderIfRoot.js","names":["_Fragment"],"sources":["../../../src/canvas/SceneCanvas/PointerProviderIfRoot.tsx"],"sourcesContent":["/**\n * Conditional `<PointerContextProvider>` wrapper. Mounts a provider only\n * when no parent provider is in scope — otherwise renders children unwrapped\n * so SceneCanvas defers to the host's existing scope.\n *\n * Pairs with `usePointerPublisher`, which publishes the canvas's pointer\n * into the surrounding context.\n */\nimport type { MutableRefObject, ReactNode, RefObject } from 'react';\nimport { PointerContextProvider, usePointerContext } from '@weasel-js/routing/react';\nimport type { CanvasExtensionApi } from '../canvasExtension';\nimport { useOptionalViewRegistry } from '../viewRegistry';\nimport { worldPointAt } from './worldPointAt';\nimport { usePublishPointer } from '../../input/usePublishPointer';\n\n/** Mount a `<PointerContextProvider>` only when none is already in scope. */\nexport function PointerProviderIfRoot({ children }: { children: ReactNode }) {\n const parent = usePointerContext();\n if (parent) return <>{children}</>;\n return <PointerContextProvider>{children}</PointerContextProvider>;\n}\n\n/**\n * Publishes the canvas's pointer into the surrounding `<PointerContextProvider>`\n * through {@link usePublishPointer}, placing it with the surface's view\n * resolver so paste-at-pointer lands in the panel the cursor is over. With no\n * views declared it answers the canvas's own camera and rect.\n */\nexport function usePointerPublisher(\n canvasRef: MutableRefObject<HTMLElement | null>,\n canvasApiRef: RefObject<CanvasExtensionApi | null>,\n): void {\n const registry = useOptionalViewRegistry();\n usePublishPointer(canvasRef, (pointerId, clientX, clientY) => {\n const api = canvasApiRef.current;\n if (!api) return null;\n const target = registry?.resolver.at(pointerId, clientX, clientY);\n const [x, y] = worldPointAt(api, target, clientX, clientY);\n return { world: { x, y }, viewId: target?.id ?? null };\n });\n}\n"],"mappings":";;;;;;;;AAgBA,SAAgB,sBAAsB,EAAE,YAAqC;CAE3E,IADe,kBACX,GAAQ,OAAO,oBAAAA,UAAA,EAAG,SAAW,CAAA;CACjC,OAAO,oBAAC,wBAAD,EAAyB,SAAiC,CAAA;AACnE;;;;;;;AAQA,SAAgB,oBACd,WACA,cACM;CACN,MAAM,WAAW,wBAAwB;CACzC,kBAAkB,YAAY,WAAW,SAAS,YAAY;EAC5D,MAAM,MAAM,aAAa;EACzB,IAAI,CAAC,KAAK,OAAO;EACjB,MAAM,SAAS,UAAU,SAAS,GAAG,WAAW,SAAS,OAAO;EAChE,MAAM,CAAC,GAAG,KAAK,aAAa,KAAK,QAAQ,SAAS,OAAO;EACzD,OAAO;GAAE,OAAO;IAAE;IAAG;GAAE;GAAG,QAAQ,QAAQ,MAAM;EAAK;CACvD,CAAC;AACH"}
@@ -1,4 +1,5 @@
1
1
  import { useMemo } from "react";
2
+ import { watchColorScheme } from "@weasel-js/react";
2
3
 
3
4
  // src/canvas/accentColor.ts
4
5
  /** The accent canvas chrome paints in when the element sits under no theme. */
@@ -24,11 +25,10 @@ function watchThemeOf(el, fn) {
24
25
  if (records.some((r) => r.attributeName !== null && THEME_ATTRIBUTE.test(r.attributeName))) fn();
25
26
  });
26
27
  for (let node = el() ?? document.documentElement; node; node = node.parentElement) observer.observe(node, { attributes: true });
27
- const scheme = typeof matchMedia === "function" ? matchMedia("(prefers-color-scheme: dark)") : null;
28
- scheme?.addEventListener?.("change", fn);
28
+ const stopScheme = watchColorScheme(fn);
29
29
  return () => {
30
30
  observer.disconnect();
31
- scheme?.removeEventListener?.("change", fn);
31
+ stopScheme();
32
32
  };
33
33
  }
34
34
  /** The color `name` on the element `ref` holds, read when asked, and how to hear it may have changed. */
@@ -1 +1 @@
1
- {"version":3,"file":"accentColor.js","names":[],"sources":["../../src/canvas/accentColor.ts"],"sourcesContent":["import { useMemo, type RefObject } from 'react';\n\n/** The accent canvas chrome paints in when the element sits under no theme. */\nexport const FALLBACK_ACCENT = '#4c8dff';\n\n/** Read the color custom property `name` off `el`, where the theme cascade lands on a canvas. */\nexport function chromeColorOf(el: Element | null | undefined, name: `--${string}`, fallback: string): string {\n if (!el || typeof getComputedStyle !== 'function') return fallback;\n return getComputedStyle(el).getPropertyValue(name).trim() || fallback;\n}\n\n/** Read `--wzl-accent` off `el`, where the theme cascade lands on a canvas. */\nexport function accentColorOf(el: Element | null | undefined): string {\n return chromeColorOf(el, '--wzl-accent', FALLBACK_ACCENT);\n}\n\nconst THEME_ATTRIBUTE = /^(data-wzl-|class$)/;\n\n/**\n * Hear that the colors read off an element may have changed: a theme, mode or class attribute\n * changing on it or anything above it, or the system color scheme flipping. `el` is read when\n * this is called; with none yet, the document root is watched. Returns the stop.\n */\nexport function watchThemeOf(el: () => Element | null | undefined, fn: () => void): () => void {\n if (typeof MutationObserver !== 'function' || typeof document === 'undefined') return () => {};\n const observer = new MutationObserver((records) => {\n if (records.some((r) => r.attributeName !== null && THEME_ATTRIBUTE.test(r.attributeName))) fn();\n });\n for (let node: Element | null = el() ?? document.documentElement; node; node = node.parentElement) {\n observer.observe(node, { attributes: true });\n }\n const scheme = typeof matchMedia === 'function' ? matchMedia('(prefers-color-scheme: dark)') : null;\n scheme?.addEventListener?.('change', fn);\n return () => {\n observer.disconnect();\n scheme?.removeEventListener?.('change', fn);\n };\n}\n\n/** The color `name` on the element `ref` holds, read when asked, and how to hear it may have changed. */\nexport function useChromeColor(\n ref: RefObject<Element | null>, name: `--${string}`, fallback: string,\n): { read: () => string; changed: (fn: () => void) => () => void } {\n return useMemo(() => ({\n read: () => chromeColorOf(ref.current, name, fallback),\n changed: (fn: () => void) => watchThemeOf(() => ref.current, fn),\n }), [ref, name, fallback]);\n}\n"],"mappings":";;;;AAGA,IAAa,kBAAkB;;AAG/B,SAAgB,cAAc,IAAgC,MAAqB,UAA0B;CAC3G,IAAI,CAAC,MAAM,OAAO,qBAAqB,YAAY,OAAO;CAC1D,OAAO,iBAAiB,EAAE,CAAC,CAAC,iBAAiB,IAAI,CAAC,CAAC,KAAK,KAAK;AAC/D;;AAGA,SAAgB,cAAc,IAAwC;CACpE,OAAO,cAAc,IAAI,gBAAgB,eAAe;AAC1D;AAEA,IAAM,kBAAkB;;;;;;AAOxB,SAAgB,aAAa,IAAsC,IAA4B;CAC7F,IAAI,OAAO,qBAAqB,cAAc,OAAO,aAAa,aAAa,aAAa,CAAC;CAC7F,MAAM,WAAW,IAAI,kBAAkB,YAAY;EACjD,IAAI,QAAQ,MAAM,MAAM,EAAE,kBAAkB,QAAQ,gBAAgB,KAAK,EAAE,aAAa,CAAC,GAAG,GAAG;CACjG,CAAC;CACD,KAAK,IAAI,OAAuB,GAAG,KAAK,SAAS,iBAAiB,MAAM,OAAO,KAAK,eAClF,SAAS,QAAQ,MAAM,EAAE,YAAY,KAAK,CAAC;CAE7C,MAAM,SAAS,OAAO,eAAe,aAAa,WAAW,8BAA8B,IAAI;CAC/F,QAAQ,mBAAmB,UAAU,EAAE;CACvC,aAAa;EACX,SAAS,WAAW;EACpB,QAAQ,sBAAsB,UAAU,EAAE;CAC5C;AACF;;AAGA,SAAgB,eACd,KAAgC,MAAqB,UACY;CACjE,OAAO,eAAe;EACpB,YAAY,cAAc,IAAI,SAAS,MAAM,QAAQ;EACrD,UAAU,OAAmB,mBAAmB,IAAI,SAAS,EAAE;CACjE,IAAI;EAAC;EAAK;EAAM;CAAQ,CAAC;AAC3B"}
1
+ {"version":3,"file":"accentColor.js","names":[],"sources":["../../src/canvas/accentColor.ts"],"sourcesContent":["import { watchColorScheme } from '@weasel-js/react';\nimport { useMemo, type RefObject } from 'react';\n\n/** The accent canvas chrome paints in when the element sits under no theme. */\nexport const FALLBACK_ACCENT = '#4c8dff';\n\n/** Read the color custom property `name` off `el`, where the theme cascade lands on a canvas. */\nexport function chromeColorOf(el: Element | null | undefined, name: `--${string}`, fallback: string): string {\n if (!el || typeof getComputedStyle !== 'function') return fallback;\n return getComputedStyle(el).getPropertyValue(name).trim() || fallback;\n}\n\n/** Read `--wzl-accent` off `el`, where the theme cascade lands on a canvas. */\nexport function accentColorOf(el: Element | null | undefined): string {\n return chromeColorOf(el, '--wzl-accent', FALLBACK_ACCENT);\n}\n\nconst THEME_ATTRIBUTE = /^(data-wzl-|class$)/;\n\n/**\n * Hear that the colors read off an element may have changed: a theme, mode or class attribute\n * changing on it or anything above it, or the system color scheme flipping. `el` is read when\n * this is called; with none yet, the document root is watched. Returns the stop.\n */\nexport function watchThemeOf(el: () => Element | null | undefined, fn: () => void): () => void {\n if (typeof MutationObserver !== 'function' || typeof document === 'undefined') return () => {};\n const observer = new MutationObserver((records) => {\n if (records.some((r) => r.attributeName !== null && THEME_ATTRIBUTE.test(r.attributeName))) fn();\n });\n for (let node: Element | null = el() ?? document.documentElement; node; node = node.parentElement) {\n observer.observe(node, { attributes: true });\n }\n const stopScheme = watchColorScheme(fn);\n return () => {\n observer.disconnect();\n stopScheme();\n };\n}\n\n/** The color `name` on the element `ref` holds, read when asked, and how to hear it may have changed. */\nexport function useChromeColor(\n ref: RefObject<Element | null>, name: `--${string}`, fallback: string,\n): { read: () => string; changed: (fn: () => void) => () => void } {\n return useMemo(() => ({\n read: () => chromeColorOf(ref.current, name, fallback),\n changed: (fn: () => void) => watchThemeOf(() => ref.current, fn),\n }), [ref, name, fallback]);\n}\n"],"mappings":";;;;;AAIA,IAAa,kBAAkB;;AAG/B,SAAgB,cAAc,IAAgC,MAAqB,UAA0B;CAC3G,IAAI,CAAC,MAAM,OAAO,qBAAqB,YAAY,OAAO;CAC1D,OAAO,iBAAiB,EAAE,CAAC,CAAC,iBAAiB,IAAI,CAAC,CAAC,KAAK,KAAK;AAC/D;;AAGA,SAAgB,cAAc,IAAwC;CACpE,OAAO,cAAc,IAAI,gBAAgB,eAAe;AAC1D;AAEA,IAAM,kBAAkB;;;;;;AAOxB,SAAgB,aAAa,IAAsC,IAA4B;CAC7F,IAAI,OAAO,qBAAqB,cAAc,OAAO,aAAa,aAAa,aAAa,CAAC;CAC7F,MAAM,WAAW,IAAI,kBAAkB,YAAY;EACjD,IAAI,QAAQ,MAAM,MAAM,EAAE,kBAAkB,QAAQ,gBAAgB,KAAK,EAAE,aAAa,CAAC,GAAG,GAAG;CACjG,CAAC;CACD,KAAK,IAAI,OAAuB,GAAG,KAAK,SAAS,iBAAiB,MAAM,OAAO,KAAK,eAClF,SAAS,QAAQ,MAAM,EAAE,YAAY,KAAK,CAAC;CAE7C,MAAM,aAAa,iBAAiB,EAAE;CACtC,aAAa;EACX,SAAS,WAAW;EACpB,WAAW;CACb;AACF;;AAGA,SAAgB,eACd,KAAgC,MAAqB,UACY;CACjE,OAAO,eAAe;EACpB,YAAY,cAAc,IAAI,SAAS,MAAM,QAAQ;EACrD,UAAU,OAAmB,mBAAmB,IAAI,SAAS,EAAE;CACjE,IAAI;EAAC;EAAK;EAAM;CAAQ,CAAC;AAC3B"}
@@ -1,3 +1,4 @@
1
+ import { createEmitter } from "@weasel-js/registry";
1
2
  import { useEffect, useMemo } from "react";
2
3
  import { useDepRegistry, useDepSource } from "@weasel-js/routing/react";
3
4
 
@@ -28,13 +29,13 @@ function reachableInGroup(scene, group, id) {
28
29
  function createEnteredGroup(scene) {
29
30
  let chain = [];
30
31
  let announced = null;
31
- const listeners = /* @__PURE__ */ new Set();
32
+ const listeners = createEmitter();
32
33
  const isGroup = (id) => isEnterable(scene.get(id));
33
34
  const get = () => chain.find(isGroup) ?? null;
34
35
  const groupsFrom = (id) => [id, ...scene.ancestorsOf(id)].filter(isGroup);
35
36
  const announce = () => {
36
37
  announced = get();
37
- for (const fn of [...listeners]) fn();
38
+ listeners.emit();
38
39
  };
39
40
  let stopScene = null;
40
41
  const watchScene = () => scene.subscribe(() => {
@@ -63,9 +64,9 @@ function createEnteredGroup(scene) {
63
64
  announced = get();
64
65
  stopScene = watchScene();
65
66
  }
66
- listeners.add(fn);
67
+ const off = listeners.subscribe(fn);
67
68
  return () => {
68
- listeners.delete(fn);
69
+ off();
69
70
  if (listeners.size === 0) {
70
71
  stopScene?.();
71
72
  stopScene = null;
@@ -1 +1 @@
1
- {"version":3,"file":"enteredGroup.js","names":[],"sources":["../../../src/canvas/deps/enteredGroup.ts"],"sourcesContent":["/**\n * The entered group: the group a press resolves inside of. Session state beside\n * the selection, not scene data, so it is never saved and never undone.\n *\n * It remembers the entered group and the groups above it. `get()` answers the\n * innermost of those still in the scene, so deleting the group lands on its\n * parent, and an undo that brings it back lands on it again.\n */\nimport { useEffect, useMemo } from 'react';\nimport { useDepRegistry, useDepSource } from '@weasel-js/routing/react';\nimport type { NodeId, Scene } from 'core/scene/types';\n\n/** The group a press resolves inside of, held beside the selection; `<SceneCanvas enteredGroup>` takes one. */\nexport interface EnteredGroupApi {\n get(): NodeId | null;\n enter(id: NodeId): void;\n /** Moves to the nearest group above the current one, or to none. */\n leave(): void;\n /** Leaves every entered group at once. */\n leaveAll(): void;\n subscribe(fn: () => void): () => void;\n}\n\n/** Whether a node is entered as a group is: a group, or a node that consumes its children. */\nexport function isEnterable(node: { kind?: string; consumesChildren?: boolean } | undefined): boolean {\n return node?.kind === 'group' || node?.consumesChildren === true;\n}\n\n/** Whether `id` is reachable while `group` is entered: inside it, and not the group itself, and\n * not consumed by a node inside it (`SceneNode.consumesChildren`). With nothing entered every node\n * no node consumes is. Picking, region selects and select-all all ask this. */\nexport function reachableInGroup(\n scene: Pick<Scene<unknown, string, unknown>, 'ancestorsOf' | 'get'>,\n group: NodeId | null,\n id: string,\n): boolean {\n for (const above of scene.ancestorsOf(id as NodeId)) {\n if (above === group) return true;\n if (scene.get(above)?.consumesChildren === true) return false;\n }\n return group === null;\n}\n\ntype SceneReads = Pick<Scene<unknown, string, unknown>, 'get' | 'ancestorsOf' | 'subscribe'>;\n\n/** An entered-group store over `scene`, with nothing entered. */\nexport function createEnteredGroup(scene: SceneReads): EnteredGroupApi {\n let chain: NodeId[] = [];\n let announced: NodeId | null = null;\n const listeners = new Set<() => void>();\n\n const isGroup = (id: NodeId) => isEnterable(scene.get(id));\n const get = (): NodeId | null => chain.find(isGroup) ?? null;\n const groupsFrom = (id: NodeId): NodeId[] => [id, ...scene.ancestorsOf(id)].filter(isGroup);\n\n const announce = () => {\n announced = get();\n for (const fn of [...listeners]) fn();\n };\n\n let stopScene: (() => void) | null = null;\n const watchScene = () => scene.subscribe(() => {\n if (get() !== announced) announce();\n });\n\n return {\n get,\n enter(id) {\n if (!isGroup(id)) return;\n chain = groupsFrom(id);\n announce();\n },\n leave() {\n const here = get();\n if (here === null) return;\n chain = groupsFrom(here).slice(1);\n announce();\n },\n leaveAll() {\n if (get() === null) return;\n chain = [];\n announce();\n },\n subscribe(fn) {\n if (listeners.size === 0) {\n announced = get();\n stopScene = watchScene();\n }\n listeners.add(fn);\n return () => {\n listeners.delete(fn);\n if (listeners.size === 0) { stopScene?.(); stopScene = null; }\n };\n },\n };\n}\n\n/** One entered-group store for the life of the scene. */\nexport function useEnteredGroup(scene: SceneReads): EnteredGroupApi {\n return useMemo(() => createEnteredGroup(scene), [scene]);\n}\n\nexport function useEnteredGroupApiSource(dep: EnteredGroupApi): void {\n useDepSource('enteredGroup', () => dep);\n // What is drawn or pasted while a group is entered lands in it, unless a source\n // registered under this one, such as a symmetry group, names a container.\n useDepSource('insertInto', (below) => ({ get: () => below()?.get() ?? dep.get() }));\n const registry = useDepRegistry();\n useEffect(() => dep.subscribe(() => registry.changed?.()), [dep, registry]);\n}\n"],"mappings":";;;;;;;;;;;;;AAwBA,SAAgB,YAAY,MAA0E;CACpG,OAAO,MAAM,SAAS,WAAW,MAAM,qBAAqB;AAC9D;;;;AAKA,SAAgB,iBACd,OACA,OACA,IACS;CACT,KAAK,MAAM,SAAS,MAAM,YAAY,EAAY,GAAG;EACnD,IAAI,UAAU,OAAO,OAAO;EAC5B,IAAI,MAAM,IAAI,KAAK,CAAC,EAAE,qBAAqB,MAAM,OAAO;CAC1D;CACA,OAAO,UAAU;AACnB;;AAKA,SAAgB,mBAAmB,OAAoC;CACrE,IAAI,QAAkB,CAAC;CACvB,IAAI,YAA2B;CAC/B,MAAM,4BAAY,IAAI,IAAgB;CAEtC,MAAM,WAAW,OAAe,YAAY,MAAM,IAAI,EAAE,CAAC;CACzD,MAAM,YAA2B,MAAM,KAAK,OAAO,KAAK;CACxD,MAAM,cAAc,OAAyB,CAAC,IAAI,GAAG,MAAM,YAAY,EAAE,CAAC,CAAC,CAAC,OAAO,OAAO;CAE1F,MAAM,iBAAiB;EACrB,YAAY,IAAI;EAChB,KAAK,MAAM,MAAM,CAAC,GAAG,SAAS,GAAG,GAAG;CACtC;CAEA,IAAI,YAAiC;CACrC,MAAM,mBAAmB,MAAM,gBAAgB;EAC7C,IAAI,IAAI,MAAM,WAAW,SAAS;CACpC,CAAC;CAED,OAAO;EACL;EACA,MAAM,IAAI;GACR,IAAI,CAAC,QAAQ,EAAE,GAAG;GAClB,QAAQ,WAAW,EAAE;GACrB,SAAS;EACX;EACA,QAAQ;GACN,MAAM,OAAO,IAAI;GACjB,IAAI,SAAS,MAAM;GACnB,QAAQ,WAAW,IAAI,CAAC,CAAC,MAAM,CAAC;GAChC,SAAS;EACX;EACA,WAAW;GACT,IAAI,IAAI,MAAM,MAAM;GACpB,QAAQ,CAAC;GACT,SAAS;EACX;EACA,UAAU,IAAI;GACZ,IAAI,UAAU,SAAS,GAAG;IACxB,YAAY,IAAI;IAChB,YAAY,WAAW;GACzB;GACA,UAAU,IAAI,EAAE;GAChB,aAAa;IACX,UAAU,OAAO,EAAE;IACnB,IAAI,UAAU,SAAS,GAAG;KAAE,YAAY;KAAG,YAAY;IAAM;GAC/D;EACF;CACF;AACF;;AAGA,SAAgB,gBAAgB,OAAoC;CAClE,OAAO,cAAc,mBAAmB,KAAK,GAAG,CAAC,KAAK,CAAC;AACzD;AAEA,SAAgB,yBAAyB,KAA4B;CACnE,aAAa,sBAAsB,GAAG;CAGtC,aAAa,eAAe,WAAW,EAAE,WAAW,MAAM,CAAC,EAAE,IAAI,KAAK,IAAI,IAAI,EAAE,EAAE;CAClF,MAAM,WAAW,eAAe;CAChC,gBAAgB,IAAI,gBAAgB,SAAS,UAAU,CAAC,GAAG,CAAC,KAAK,QAAQ,CAAC;AAC5E"}
1
+ {"version":3,"file":"enteredGroup.js","names":[],"sources":["../../../src/canvas/deps/enteredGroup.ts"],"sourcesContent":["/**\n * The entered group: the group a press resolves inside of. Session state beside\n * the selection, not scene data, so it is never saved and never undone.\n *\n * It remembers the entered group and the groups above it. `get()` answers the\n * innermost of those still in the scene, so deleting the group lands on its\n * parent, and an undo that brings it back lands on it again.\n */\nimport { useEffect, useMemo } from 'react';\nimport { createEmitter } from '@weasel-js/registry';\nimport { useDepRegistry, useDepSource } from '@weasel-js/routing/react';\nimport type { NodeId, Scene } from 'core/scene/types';\n\n/** The group a press resolves inside of, held beside the selection; `<SceneCanvas enteredGroup>` takes one. */\nexport interface EnteredGroupApi {\n get(): NodeId | null;\n enter(id: NodeId): void;\n /** Moves to the nearest group above the current one, or to none. */\n leave(): void;\n /** Leaves every entered group at once. */\n leaveAll(): void;\n subscribe(fn: () => void): () => void;\n}\n\n/** Whether a node is entered as a group is: a group, or a node that consumes its children. */\nexport function isEnterable(node: { kind?: string; consumesChildren?: boolean } | undefined): boolean {\n return node?.kind === 'group' || node?.consumesChildren === true;\n}\n\n/** Whether `id` is reachable while `group` is entered: inside it, and not the group itself, and\n * not consumed by a node inside it (`SceneNode.consumesChildren`). With nothing entered every node\n * no node consumes is. Picking, region selects and select-all all ask this. */\nexport function reachableInGroup(\n scene: Pick<Scene<unknown, string, unknown>, 'ancestorsOf' | 'get'>,\n group: NodeId | null,\n id: string,\n): boolean {\n for (const above of scene.ancestorsOf(id as NodeId)) {\n if (above === group) return true;\n if (scene.get(above)?.consumesChildren === true) return false;\n }\n return group === null;\n}\n\ntype SceneReads = Pick<Scene<unknown, string, unknown>, 'get' | 'ancestorsOf' | 'subscribe'>;\n\n/** An entered-group store over `scene`, with nothing entered. */\nexport function createEnteredGroup(scene: SceneReads): EnteredGroupApi {\n let chain: NodeId[] = [];\n let announced: NodeId | null = null;\n const listeners = createEmitter();\n\n const isGroup = (id: NodeId) => isEnterable(scene.get(id));\n const get = (): NodeId | null => chain.find(isGroup) ?? null;\n const groupsFrom = (id: NodeId): NodeId[] => [id, ...scene.ancestorsOf(id)].filter(isGroup);\n\n const announce = () => {\n announced = get();\n listeners.emit();\n };\n\n let stopScene: (() => void) | null = null;\n const watchScene = () => scene.subscribe(() => {\n if (get() !== announced) announce();\n });\n\n return {\n get,\n enter(id) {\n if (!isGroup(id)) return;\n chain = groupsFrom(id);\n announce();\n },\n leave() {\n const here = get();\n if (here === null) return;\n chain = groupsFrom(here).slice(1);\n announce();\n },\n leaveAll() {\n if (get() === null) return;\n chain = [];\n announce();\n },\n subscribe(fn) {\n if (listeners.size === 0) {\n announced = get();\n stopScene = watchScene();\n }\n const off = listeners.subscribe(fn);\n return () => {\n off();\n if (listeners.size === 0) { stopScene?.(); stopScene = null; }\n };\n },\n };\n}\n\n/** One entered-group store for the life of the scene. */\nexport function useEnteredGroup(scene: SceneReads): EnteredGroupApi {\n return useMemo(() => createEnteredGroup(scene), [scene]);\n}\n\nexport function useEnteredGroupApiSource(dep: EnteredGroupApi): void {\n useDepSource('enteredGroup', () => dep);\n // What is drawn or pasted while a group is entered lands in it, unless a source\n // registered under this one, such as a symmetry group, names a container.\n useDepSource('insertInto', (below) => ({ get: () => below()?.get() ?? dep.get() }));\n const registry = useDepRegistry();\n useEffect(() => dep.subscribe(() => registry.changed?.()), [dep, registry]);\n}\n"],"mappings":";;;;;;;;;;;;;;AAyBA,SAAgB,YAAY,MAA0E;CACpG,OAAO,MAAM,SAAS,WAAW,MAAM,qBAAqB;AAC9D;;;;AAKA,SAAgB,iBACd,OACA,OACA,IACS;CACT,KAAK,MAAM,SAAS,MAAM,YAAY,EAAY,GAAG;EACnD,IAAI,UAAU,OAAO,OAAO;EAC5B,IAAI,MAAM,IAAI,KAAK,CAAC,EAAE,qBAAqB,MAAM,OAAO;CAC1D;CACA,OAAO,UAAU;AACnB;;AAKA,SAAgB,mBAAmB,OAAoC;CACrE,IAAI,QAAkB,CAAC;CACvB,IAAI,YAA2B;CAC/B,MAAM,YAAY,cAAc;CAEhC,MAAM,WAAW,OAAe,YAAY,MAAM,IAAI,EAAE,CAAC;CACzD,MAAM,YAA2B,MAAM,KAAK,OAAO,KAAK;CACxD,MAAM,cAAc,OAAyB,CAAC,IAAI,GAAG,MAAM,YAAY,EAAE,CAAC,CAAC,CAAC,OAAO,OAAO;CAE1F,MAAM,iBAAiB;EACrB,YAAY,IAAI;EAChB,UAAU,KAAK;CACjB;CAEA,IAAI,YAAiC;CACrC,MAAM,mBAAmB,MAAM,gBAAgB;EAC7C,IAAI,IAAI,MAAM,WAAW,SAAS;CACpC,CAAC;CAED,OAAO;EACL;EACA,MAAM,IAAI;GACR,IAAI,CAAC,QAAQ,EAAE,GAAG;GAClB,QAAQ,WAAW,EAAE;GACrB,SAAS;EACX;EACA,QAAQ;GACN,MAAM,OAAO,IAAI;GACjB,IAAI,SAAS,MAAM;GACnB,QAAQ,WAAW,IAAI,CAAC,CAAC,MAAM,CAAC;GAChC,SAAS;EACX;EACA,WAAW;GACT,IAAI,IAAI,MAAM,MAAM;GACpB,QAAQ,CAAC;GACT,SAAS;EACX;EACA,UAAU,IAAI;GACZ,IAAI,UAAU,SAAS,GAAG;IACxB,YAAY,IAAI;IAChB,YAAY,WAAW;GACzB;GACA,MAAM,MAAM,UAAU,UAAU,EAAE;GAClC,aAAa;IACX,IAAI;IACJ,IAAI,UAAU,SAAS,GAAG;KAAE,YAAY;KAAG,YAAY;IAAM;GAC/D;EACF;CACF;AACF;;AAGA,SAAgB,gBAAgB,OAAoC;CAClE,OAAO,cAAc,mBAAmB,KAAK,GAAG,CAAC,KAAK,CAAC;AACzD;AAEA,SAAgB,yBAAyB,KAA4B;CACnE,aAAa,sBAAsB,GAAG;CAGtC,aAAa,eAAe,WAAW,EAAE,WAAW,MAAM,CAAC,EAAE,IAAI,KAAK,IAAI,IAAI,EAAE,EAAE;CAClF,MAAM,WAAW,eAAe;CAChC,gBAAgB,IAAI,gBAAgB,SAAS,UAAU,CAAC,GAAG,CAAC,KAAK,QAAQ,CAAC;AAC5E"}
@@ -1,7 +1,12 @@
1
+ import { sameList } from "@weasel-js/registry";
1
2
  import { useDepRegistry, useDepSource } from "@weasel-js/routing/react";
2
3
 
3
4
  // src/canvas/deps/selectionSnapshot.ts
4
- var sameIds = (a, b) => a.length === b.length && a.every((id, i) => id === b[i]);
5
+ /**
6
+ * The `selectionSnapshot` dep: the node selection and, in path edit, the anchor
7
+ * selection, captured together, so the last operation puts both back before it
8
+ * reruns an action that changed them.
9
+ */
5
10
  function useSelectionSnapshotDepSource() {
6
11
  const registry = useDepRegistry();
7
12
  useDepSource("selectionSnapshot", () => ({ take() {
@@ -11,7 +16,7 @@ function useSelectionSnapshotDepSource() {
11
16
  const anchors = edit ? [...edit.selectedAnchors] : [];
12
17
  return () => {
13
18
  const selection = registry.get("selection");
14
- if (ids && selection && !sameIds(selection.get(), ids)) selection.set([...ids]);
19
+ if (ids && selection && !sameList(selection.get(), ids)) selection.set([...ids]);
15
20
  const now = registry.get("editAnchors");
16
21
  if (editing && now?.editingId === editing) now.setSelectedAnchors(anchors);
17
22
  };
@@ -1 +1 @@
1
- {"version":3,"file":"selectionSnapshot.js","names":[],"sources":["../../../src/canvas/deps/selectionSnapshot.ts"],"sourcesContent":["/**\n * The `selectionSnapshot` dep: the node selection and, in path edit, the anchor\n * selection, captured together, so the last operation puts both back before it\n * reruns an action that changed them.\n */\nimport type { SelectionSnapshotDep } from '@weasel-js/routing';\nimport { useDepRegistry, useDepSource } from '@weasel-js/routing/react';\nimport type {} from '../../interactions/actions/depSchema';\n\nconst sameIds = (a: readonly string[], b: readonly string[]): boolean =>\n a.length === b.length && a.every((id, i) => id === b[i]);\n\nexport function useSelectionSnapshotDepSource(): void {\n const registry = useDepRegistry();\n useDepSource('selectionSnapshot', (): SelectionSnapshotDep => ({\n take() {\n const ids = registry.get('selection')?.get() ?? null;\n const edit = registry.get('editAnchors');\n const editing = edit?.editingId ?? '';\n const anchors = edit ? [...edit.selectedAnchors] : [];\n return () => {\n const selection = registry.get('selection');\n if (ids && selection && !sameIds(selection.get(), ids)) selection.set([...ids] as never);\n const now = registry.get('editAnchors');\n if (editing && now?.editingId === editing) now.setSelectedAnchors(anchors);\n };\n },\n }));\n}\n"],"mappings":";;;AASA,IAAM,WAAW,GAAsB,MACrC,EAAE,WAAW,EAAE,UAAU,EAAE,OAAO,IAAI,MAAM,OAAO,EAAE,EAAE;AAEzD,SAAgB,gCAAsC;CACpD,MAAM,WAAW,eAAe;CAChC,aAAa,4BAAkD,EAC7D,OAAO;EACL,MAAM,MAAM,SAAS,IAAI,WAAW,CAAC,EAAE,IAAI,KAAK;EAChD,MAAM,OAAO,SAAS,IAAI,aAAa;EACvC,MAAM,UAAU,MAAM,aAAa;EACnC,MAAM,UAAU,OAAO,CAAC,GAAG,KAAK,eAAe,IAAI,CAAC;EACpD,aAAa;GACX,MAAM,YAAY,SAAS,IAAI,WAAW;GAC1C,IAAI,OAAO,aAAa,CAAC,QAAQ,UAAU,IAAI,GAAG,GAAG,GAAG,UAAU,IAAI,CAAC,GAAG,GAAG,CAAU;GACvF,MAAM,MAAM,SAAS,IAAI,aAAa;GACtC,IAAI,WAAW,KAAK,cAAc,SAAS,IAAI,mBAAmB,OAAO;EAC3E;CACF,EACF,EAAE;AACJ"}
1
+ {"version":3,"file":"selectionSnapshot.js","names":[],"sources":["../../../src/canvas/deps/selectionSnapshot.ts"],"sourcesContent":["/**\n * The `selectionSnapshot` dep: the node selection and, in path edit, the anchor\n * selection, captured together, so the last operation puts both back before it\n * reruns an action that changed them.\n */\nimport { sameList } from '@weasel-js/registry';\nimport type { SelectionSnapshotDep } from '@weasel-js/routing';\nimport { useDepRegistry, useDepSource } from '@weasel-js/routing/react';\nimport type {} from '../../interactions/actions/depSchema';\n\nexport function useSelectionSnapshotDepSource(): void {\n const registry = useDepRegistry();\n useDepSource('selectionSnapshot', (): SelectionSnapshotDep => ({\n take() {\n const ids = registry.get('selection')?.get() ?? null;\n const edit = registry.get('editAnchors');\n const editing = edit?.editingId ?? '';\n const anchors = edit ? [...edit.selectedAnchors] : [];\n return () => {\n const selection = registry.get('selection');\n if (ids && selection && !sameList(selection.get(), ids)) selection.set([...ids] as never);\n const now = registry.get('editAnchors');\n if (editing && now?.editingId === editing) now.setSelectedAnchors(anchors);\n };\n },\n }));\n}\n"],"mappings":";;;;;;;;;AAUA,SAAgB,gCAAsC;CACpD,MAAM,WAAW,eAAe;CAChC,aAAa,4BAAkD,EAC7D,OAAO;EACL,MAAM,MAAM,SAAS,IAAI,WAAW,CAAC,EAAE,IAAI,KAAK;EAChD,MAAM,OAAO,SAAS,IAAI,aAAa;EACvC,MAAM,UAAU,MAAM,aAAa;EACnC,MAAM,UAAU,OAAO,CAAC,GAAG,KAAK,eAAe,IAAI,CAAC;EACpD,aAAa;GACX,MAAM,YAAY,SAAS,IAAI,WAAW;GAC1C,IAAI,OAAO,aAAa,CAAC,SAAS,UAAU,IAAI,GAAG,GAAG,GAAG,UAAU,IAAI,CAAC,GAAG,GAAG,CAAU;GACxF,MAAM,MAAM,SAAS,IAAI,aAAa;GACtC,IAAI,WAAW,KAAK,cAAc,SAAS,IAAI,mBAAmB,OAAO;EAC3E;CACF,EACF,EAAE;AACJ"}
@@ -1,6 +1,6 @@
1
1
  import { pointRegionLive, pointRegionScreenQuad, transformOf } from "../affordances/hitAffordanceRegions.js";
2
2
  import { chromeStateFrom, isVisibleFrom } from "./drawEnvelope.js";
3
- import { PATH_L, PATH_M, PATH_Z } from "@weasel-js/geom";
3
+ import { PATH_L, PATH_M, PATH_Z, viewWorldToScreen } from "@weasel-js/geom";
4
4
 
5
5
  // src/canvas/slopsDebugLayer.ts
6
6
  var SLOP_FILL = "rgba(255, 80, 140, 0.18)";
@@ -14,7 +14,13 @@ function createSlopsDebugLayer(opts) {
14
14
  const chrome = chromeStateFrom(data);
15
15
  if (!chrome) return [];
16
16
  const isVisible = isVisibleFrom(data);
17
- const toScreen = (x, y) => [(x - view.x) * view.scale.x, (y - view.y) * view.scale.y];
17
+ const toScreen = (x, y) => {
18
+ const p = viewWorldToScreen(view, {
19
+ x,
20
+ y
21
+ });
22
+ return [p.x, p.y];
23
+ };
18
24
  const out = [];
19
25
  for (const a of opts.getAffordances()) {
20
26
  if (isVisible && !isVisible(a.id)) continue;
@@ -1 +1 @@
1
- {"version":3,"file":"slopsDebugLayer.js","names":[],"sources":["../../src/canvas/slopsDebugLayer.ts"],"sourcesContent":["/**\n * Screen-space debug overlay rendering the kit's hit-test \"slops\" — the\n * forgiveness regions around each point affordance (corner resize handles,\n * the rotate badge, anchor / control-handle markers). Off by default; gated\n * by SceneCanvas's `debug.slops` prop.\n *\n * It walks the same affordances the hit-test does, and draws each point\n * region's square from the same frame the hit-test reads, so a halo shows\n * exactly the pixels that grab.\n */\n\nimport type { DrawCommand } from '@weasel-js/renderer';\nimport type { RenderLayer } from 'core/layers/render';\nimport type { Affordance } from 'affordances/types';\nimport { pointRegionLive, pointRegionScreenQuad, transformOf } from 'affordances/hitAffordanceRegions';\nimport { PATH_L, PATH_M, PATH_Z } from '@weasel-js/geom';\nimport { chromeStateFrom, isVisibleFrom } from './drawEnvelope';\n\nexport interface CreateSlopsDebugLayerOptions {\n /** The affordances the canvas hit-tests — `chromeAffordances`, the same\n * list handed to `buildAffordanceAt`. */\n getAffordances: () => readonly Affordance[];\n}\n\nconst SLOP_FILL = 'rgba(255, 80, 140, 0.18)';\nconst SLOP_STROKE = 'rgba(255, 80, 140, 0.55)';\n\nexport function createSlopsDebugLayer(\n opts: CreateSlopsDebugLayerOptions,\n): RenderLayer<unknown> {\n return {\n id: 'slops-debug',\n label: 'Slops (debug)',\n space: 'screen',\n draw: (data, view) => {\n // Selection, bounds and visibility come off the envelope: the halos\n // have to sit on the chrome the drawing view actually painted.\n const chrome = chromeStateFrom(data);\n if (!chrome) return [];\n const isVisible = isVisibleFrom(data);\n const toScreen = (x: number, y: number): [number, number] =>\n [(x - view.x) * view.scale.x, (y - view.y) * view.scale.y];\n const out: DrawCommand[] = [];\n for (const a of opts.getAffordances()) {\n if (isVisible && !isVisible(a.id)) continue;\n for (const region of a.regions(chrome)) {\n if (region.shape.kind !== 'point') continue;\n const xf = transformOf(chrome, region.targetId);\n if (!pointRegionLive(region.shape, xf, view)) continue;\n const q = pointRegionScreenQuad(region.shape, xf, view, toScreen);\n out.push({\n kind: 'path',\n path: {\n kind: 'polygon',\n commands: new Uint8Array([PATH_M, PATH_L, PATH_L, PATH_L, PATH_Z]),\n coords: new Float32Array([q[0].x, q[0].y, q[1].x, q[1].y, q[2].x, q[2].y, q[3].x, q[3].y]),\n fillRule: 'nonzero',\n },\n fill: { paint: { kind: 'solid', color: SLOP_FILL } },\n stroke: { paint: { kind: 'solid', color: SLOP_STROKE }, width: 1 },\n });\n }\n }\n return out;\n },\n };\n}\n"],"mappings":";;;;;AAwBA,IAAM,YAAY;AAClB,IAAM,cAAc;AAEpB,SAAgB,sBACd,MACsB;CACtB,OAAO;EACL,IAAI;EACJ,OAAO;EACP,OAAO;EACP,OAAO,MAAM,SAAS;GAGpB,MAAM,SAAS,gBAAgB,IAAI;GACnC,IAAI,CAAC,QAAQ,OAAO,CAAC;GACrB,MAAM,YAAY,cAAc,IAAI;GACpC,MAAM,YAAY,GAAW,MAC3B,EAAE,IAAI,KAAK,KAAK,KAAK,MAAM,IAAI,IAAI,KAAK,KAAK,KAAK,MAAM,CAAC;GAC3D,MAAM,MAAqB,CAAC;GAC5B,KAAK,MAAM,KAAK,KAAK,eAAe,GAAG;IACrC,IAAI,aAAa,CAAC,UAAU,EAAE,EAAE,GAAG;IACnC,KAAK,MAAM,UAAU,EAAE,QAAQ,MAAM,GAAG;KACtC,IAAI,OAAO,MAAM,SAAS,SAAS;KACnC,MAAM,KAAK,YAAY,QAAQ,OAAO,QAAQ;KAC9C,IAAI,CAAC,gBAAgB,OAAO,OAAO,IAAI,IAAI,GAAG;KAC9C,MAAM,IAAI,sBAAsB,OAAO,OAAO,IAAI,MAAM,QAAQ;KAChE,IAAI,KAAK;MACP,MAAM;MACN,MAAM;OACJ,MAAM;OACN,UAAU,IAAI,WAAW;QAAC;QAAQ;QAAQ;QAAQ;QAAQ;OAAM,CAAC;OACjE,QAAQ,IAAI,aAAa;QAAC,EAAE,EAAE,CAAC;QAAG,EAAE,EAAE,CAAC;QAAG,EAAE,EAAE,CAAC;QAAG,EAAE,EAAE,CAAC;QAAG,EAAE,EAAE,CAAC;QAAG,EAAE,EAAE,CAAC;QAAG,EAAE,EAAE,CAAC;QAAG,EAAE,EAAE,CAAC;OAAC,CAAC;OACzF,UAAU;MACZ;MACA,MAAM,EAAE,OAAO;OAAE,MAAM;OAAS,OAAO;MAAU,EAAE;MACnD,QAAQ;OAAE,OAAO;QAAE,MAAM;QAAS,OAAO;OAAY;OAAG,OAAO;MAAE;KACnE,CAAC;IACH;GACF;GACA,OAAO;EACT;CACF;AACF"}
1
+ {"version":3,"file":"slopsDebugLayer.js","names":[],"sources":["../../src/canvas/slopsDebugLayer.ts"],"sourcesContent":["/**\n * Screen-space debug overlay rendering the kit's hit-test \"slops\" — the\n * forgiveness regions around each point affordance (corner resize handles,\n * the rotate badge, anchor / control-handle markers). Off by default; gated\n * by SceneCanvas's `debug.slops` prop.\n *\n * It walks the same affordances the hit-test does, and draws each point\n * region's square from the same frame the hit-test reads, so a halo shows\n * exactly the pixels that grab.\n */\n\nimport type { DrawCommand } from '@weasel-js/renderer';\nimport type { RenderLayer } from 'core/layers/render';\nimport type { Affordance } from 'affordances/types';\nimport { pointRegionLive, pointRegionScreenQuad, transformOf } from 'affordances/hitAffordanceRegions';\nimport { PATH_L, PATH_M, PATH_Z, viewWorldToScreen } from '@weasel-js/geom';\nimport { chromeStateFrom, isVisibleFrom } from './drawEnvelope';\n\nexport interface CreateSlopsDebugLayerOptions {\n /** The affordances the canvas hit-tests — `chromeAffordances`, the same\n * list handed to `buildAffordanceAt`. */\n getAffordances: () => readonly Affordance[];\n}\n\nconst SLOP_FILL = 'rgba(255, 80, 140, 0.18)';\nconst SLOP_STROKE = 'rgba(255, 80, 140, 0.55)';\n\nexport function createSlopsDebugLayer(\n opts: CreateSlopsDebugLayerOptions,\n): RenderLayer<unknown> {\n return {\n id: 'slops-debug',\n label: 'Slops (debug)',\n space: 'screen',\n draw: (data, view) => {\n // Selection, bounds and visibility come off the envelope: the halos\n // have to sit on the chrome the drawing view actually painted.\n const chrome = chromeStateFrom(data);\n if (!chrome) return [];\n const isVisible = isVisibleFrom(data);\n const toScreen = (x: number, y: number): [number, number] => {\n const p = viewWorldToScreen(view, { x, y });\n return [p.x, p.y];\n };\n const out: DrawCommand[] = [];\n for (const a of opts.getAffordances()) {\n if (isVisible && !isVisible(a.id)) continue;\n for (const region of a.regions(chrome)) {\n if (region.shape.kind !== 'point') continue;\n const xf = transformOf(chrome, region.targetId);\n if (!pointRegionLive(region.shape, xf, view)) continue;\n const q = pointRegionScreenQuad(region.shape, xf, view, toScreen);\n out.push({\n kind: 'path',\n path: {\n kind: 'polygon',\n commands: new Uint8Array([PATH_M, PATH_L, PATH_L, PATH_L, PATH_Z]),\n coords: new Float32Array([q[0].x, q[0].y, q[1].x, q[1].y, q[2].x, q[2].y, q[3].x, q[3].y]),\n fillRule: 'nonzero',\n },\n fill: { paint: { kind: 'solid', color: SLOP_FILL } },\n stroke: { paint: { kind: 'solid', color: SLOP_STROKE }, width: 1 },\n });\n }\n }\n return out;\n },\n };\n}\n"],"mappings":";;;;;AAwBA,IAAM,YAAY;AAClB,IAAM,cAAc;AAEpB,SAAgB,sBACd,MACsB;CACtB,OAAO;EACL,IAAI;EACJ,OAAO;EACP,OAAO;EACP,OAAO,MAAM,SAAS;GAGpB,MAAM,SAAS,gBAAgB,IAAI;GACnC,IAAI,CAAC,QAAQ,OAAO,CAAC;GACrB,MAAM,YAAY,cAAc,IAAI;GACpC,MAAM,YAAY,GAAW,MAAgC;IAC3D,MAAM,IAAI,kBAAkB,MAAM;KAAE;KAAG;IAAE,CAAC;IAC1C,OAAO,CAAC,EAAE,GAAG,EAAE,CAAC;GAClB;GACA,MAAM,MAAqB,CAAC;GAC5B,KAAK,MAAM,KAAK,KAAK,eAAe,GAAG;IACrC,IAAI,aAAa,CAAC,UAAU,EAAE,EAAE,GAAG;IACnC,KAAK,MAAM,UAAU,EAAE,QAAQ,MAAM,GAAG;KACtC,IAAI,OAAO,MAAM,SAAS,SAAS;KACnC,MAAM,KAAK,YAAY,QAAQ,OAAO,QAAQ;KAC9C,IAAI,CAAC,gBAAgB,OAAO,OAAO,IAAI,IAAI,GAAG;KAC9C,MAAM,IAAI,sBAAsB,OAAO,OAAO,IAAI,MAAM,QAAQ;KAChE,IAAI,KAAK;MACP,MAAM;MACN,MAAM;OACJ,MAAM;OACN,UAAU,IAAI,WAAW;QAAC;QAAQ;QAAQ;QAAQ;QAAQ;OAAM,CAAC;OACjE,QAAQ,IAAI,aAAa;QAAC,EAAE,EAAE,CAAC;QAAG,EAAE,EAAE,CAAC;QAAG,EAAE,EAAE,CAAC;QAAG,EAAE,EAAE,CAAC;QAAG,EAAE,EAAE,CAAC;QAAG,EAAE,EAAE,CAAC;QAAG,EAAE,EAAE,CAAC;QAAG,EAAE,EAAE,CAAC;OAAC,CAAC;OACzF,UAAU;MACZ;MACA,MAAM,EAAE,OAAO;OAAE,MAAM;OAAS,OAAO;MAAU,EAAE;MACnD,QAAQ;OAAE,OAAO;QAAE,MAAM;QAAS,OAAO;OAAY;OAAG,OAAO;MAAE;KACnE,CAAC;IACH;GACF;GACA,OAAO;EACT;CACF;AACF"}
@@ -1,3 +1,4 @@
1
+ import { viewScreenToWorld } from "@weasel-js/geom";
1
2
  import { useEffect } from "react";
2
3
  import { useLatest } from "@weasel-js/react";
3
4
 
@@ -23,9 +24,11 @@ function useViewRegistrySurface(viewRegistry, layers, frame, services) {
23
24
  setView,
24
25
  claimsAbove: (x, y) => {
25
26
  const view = viewRef.current;
26
- const worldX = x / view.scale.x + view.x;
27
- const worldY = y / view.scale.y + view.y;
28
- return hitTestExtrasIn(worldX, worldY, view, dimsRef.current, helpersRef.current) !== null;
27
+ const world = viewScreenToWorld(view, {
28
+ x,
29
+ y
30
+ });
31
+ return hitTestExtrasIn(world.x, world.y, view, dimsRef.current, helpersRef.current) !== null;
29
32
  }
30
33
  });
31
34
  }, [
@@ -1 +1 @@
1
- {"version":3,"file":"useViewRegistrySurface.js","names":[],"sources":["../../src/canvas/useViewRegistrySurface.ts"],"sourcesContent":["import { useEffect } from 'react';\nimport { useLatest } from '@weasel-js/react';\nimport type { View } from 'core/viewport/view';\nimport type { Dims, RenderLayer } from 'core/layers/render';\nimport type { CanvasHelpers } from './useViewHelpers';\nimport type { SurfaceHandle, ViewRegistry } from './viewRegistry';\n\n/** What the canvas reads at draw and hit time that changes outside a render. */\nexport interface SurfaceFrame {\n viewRef: { readonly current: View };\n dimsRef: { readonly current: Dims };\n helpersRef: { readonly current: Pick<CanvasHelpers<unknown>, 'getChromeState'> | null };\n}\n\n/** What the canvas does for a view that asks. */\nexport interface SurfaceServices {\n requestRedraw: () => void;\n hitTestExtrasIn: SurfaceHandle['hitTestExtras'];\n clientToWorld: SurfaceHandle['clientToWorld'];\n setView: NonNullable<SurfaceHandle['setView']>;\n}\n\n/**\n * Attach this canvas to the view registry as its surface. The stack a view paints through its\n * own camera is this canvas's `layers`, before the views themselves are folded in — otherwise\n * a view would contain itself. Published by reference and read at draw time.\n */\nexport function useViewRegistrySurface(\n viewRegistry: ViewRegistry | null,\n layers: readonly RenderLayer<unknown>[],\n frame: SurfaceFrame,\n services: SurfaceServices,\n): void {\n const surfaceLayersRef = useLatest<readonly RenderLayer<unknown>[]>(layers);\n const { viewRef, dimsRef, helpersRef } = frame;\n const { requestRedraw, hitTestExtrasIn, clientToWorld, setView } = services;\n useEffect(() => {\n return viewRegistry?.attachSurface({\n view: () => viewRef.current,\n dims: () => dimsRef.current,\n layers: () => surfaceLayersRef.current,\n requestRedraw,\n chromeState: () => helpersRef.current!.getChromeState(),\n hitTestExtras: hitTestExtrasIn,\n clientToWorld,\n setView,\n claimsAbove: (x, y) => {\n const view = viewRef.current;\n const worldX = x / view.scale.x + view.x;\n const worldY = y / view.scale.y + view.y;\n return hitTestExtrasIn(worldX, worldY, view, dimsRef.current, helpersRef.current) !== null;\n },\n });\n }, [viewRegistry, requestRedraw, hitTestExtrasIn, dimsRef, surfaceLayersRef, viewRef, helpersRef, clientToWorld, setView]);\n}\n"],"mappings":";;;;;;;;;AA2BA,SAAgB,uBACd,cACA,QACA,OACA,UACM;CACN,MAAM,mBAAmB,UAA2C,MAAM;CAC1E,MAAM,EAAE,SAAS,SAAS,eAAe;CACzC,MAAM,EAAE,eAAe,iBAAiB,eAAe,YAAY;CACnE,gBAAgB;EACd,OAAO,cAAc,cAAc;GACjC,YAAY,QAAQ;GACpB,YAAY,QAAQ;GACpB,cAAc,iBAAiB;GAC/B;GACA,mBAAmB,WAAW,QAAS,eAAe;GACtD,eAAe;GACf;GACA;GACA,cAAc,GAAG,MAAM;IACrB,MAAM,OAAO,QAAQ;IACrB,MAAM,SAAS,IAAI,KAAK,MAAM,IAAI,KAAK;IACvC,MAAM,SAAS,IAAI,KAAK,MAAM,IAAI,KAAK;IACvC,OAAO,gBAAgB,QAAQ,QAAQ,MAAM,QAAQ,SAAS,WAAW,OAAO,MAAM;GACxF;EACF,CAAC;CACH,GAAG;EAAC;EAAc;EAAe;EAAiB;EAAS;EAAkB;EAAS;EAAY;EAAe;CAAO,CAAC;AAC3H"}
1
+ {"version":3,"file":"useViewRegistrySurface.js","names":[],"sources":["../../src/canvas/useViewRegistrySurface.ts"],"sourcesContent":["import { useEffect } from 'react';\nimport { useLatest } from '@weasel-js/react';\nimport { viewScreenToWorld } from '@weasel-js/geom';\nimport type { View } from 'core/viewport/view';\nimport type { Dims, RenderLayer } from 'core/layers/render';\nimport type { CanvasHelpers } from './useViewHelpers';\nimport type { SurfaceHandle, ViewRegistry } from './viewRegistry';\n\n/** What the canvas reads at draw and hit time that changes outside a render. */\nexport interface SurfaceFrame {\n viewRef: { readonly current: View };\n dimsRef: { readonly current: Dims };\n helpersRef: { readonly current: Pick<CanvasHelpers<unknown>, 'getChromeState'> | null };\n}\n\n/** What the canvas does for a view that asks. */\nexport interface SurfaceServices {\n requestRedraw: () => void;\n hitTestExtrasIn: SurfaceHandle['hitTestExtras'];\n clientToWorld: SurfaceHandle['clientToWorld'];\n setView: NonNullable<SurfaceHandle['setView']>;\n}\n\n/**\n * Attach this canvas to the view registry as its surface. The stack a view paints through its\n * own camera is this canvas's `layers`, before the views themselves are folded in — otherwise\n * a view would contain itself. Published by reference and read at draw time.\n */\nexport function useViewRegistrySurface(\n viewRegistry: ViewRegistry | null,\n layers: readonly RenderLayer<unknown>[],\n frame: SurfaceFrame,\n services: SurfaceServices,\n): void {\n const surfaceLayersRef = useLatest<readonly RenderLayer<unknown>[]>(layers);\n const { viewRef, dimsRef, helpersRef } = frame;\n const { requestRedraw, hitTestExtrasIn, clientToWorld, setView } = services;\n useEffect(() => {\n return viewRegistry?.attachSurface({\n view: () => viewRef.current,\n dims: () => dimsRef.current,\n layers: () => surfaceLayersRef.current,\n requestRedraw,\n chromeState: () => helpersRef.current!.getChromeState(),\n hitTestExtras: hitTestExtrasIn,\n clientToWorld,\n setView,\n claimsAbove: (x, y) => {\n const view = viewRef.current;\n const world = viewScreenToWorld(view, { x, y });\n return hitTestExtrasIn(world.x, world.y, view, dimsRef.current, helpersRef.current) !== null;\n },\n });\n }, [viewRegistry, requestRedraw, hitTestExtrasIn, dimsRef, surfaceLayersRef, viewRef, helpersRef, clientToWorld, setView]);\n}\n"],"mappings":";;;;;;;;;;AA4BA,SAAgB,uBACd,cACA,QACA,OACA,UACM;CACN,MAAM,mBAAmB,UAA2C,MAAM;CAC1E,MAAM,EAAE,SAAS,SAAS,eAAe;CACzC,MAAM,EAAE,eAAe,iBAAiB,eAAe,YAAY;CACnE,gBAAgB;EACd,OAAO,cAAc,cAAc;GACjC,YAAY,QAAQ;GACpB,YAAY,QAAQ;GACpB,cAAc,iBAAiB;GAC/B;GACA,mBAAmB,WAAW,QAAS,eAAe;GACtD,eAAe;GACf;GACA;GACA,cAAc,GAAG,MAAM;IACrB,MAAM,OAAO,QAAQ;IACrB,MAAM,QAAQ,kBAAkB,MAAM;KAAE;KAAG;IAAE,CAAC;IAC9C,OAAO,gBAAgB,MAAM,GAAG,MAAM,GAAG,MAAM,QAAQ,SAAS,WAAW,OAAO,MAAM;GAC1F;EACF,CAAC;CACH,GAAG;EAAC;EAAc;EAAe;EAAiB;EAAS;EAAkB;EAAS;EAAY;EAAe;CAAO,CAAC;AAC3H"}
@@ -2,6 +2,7 @@ import { createViewResolver, viewContains } from "../domains/viewports/viewResol
2
2
  import { isRectAim, rectAim, viewForAim } from "../domains/viewports/aim.js";
3
3
  import { viewportWorldRect } from "../core/viewport/viewportWorldRect.js";
4
4
  import { createViewMarkerRegistry } from "./viewMarkers.js";
5
+ import { viewWorldToScreen } from "@weasel-js/geom";
5
6
  import { createEmitter } from "@weasel-js/registry";
6
7
  import { createContext, useContext, useMemo, useRef } from "react";
7
8
  import { jsx } from "react/jsx-runtime";
@@ -29,11 +30,10 @@ function surfaceLocal(surface, clientX, clientY) {
29
30
  y: clientY
30
31
  };
31
32
  const [worldX, worldY] = surface.clientToWorld(clientX, clientY);
32
- const view = surface.view();
33
- return {
34
- x: (worldX - view.x) * view.scale.x,
35
- y: (worldY - view.y) * view.scale.y
36
- };
33
+ return viewWorldToScreen(surface.view(), {
34
+ x: worldX,
35
+ y: worldY
36
+ });
37
37
  }
38
38
  var ViewRegistryContext = createContext(null);
39
39
  /** Mounts the registry for one surface. `<SceneCanvas>` mounts one; a view
@@ -1 +1 @@
1
- {"version":3,"file":"viewRegistry.js","names":[],"sources":["../../src/canvas/viewRegistry.tsx"],"sourcesContent":["/**\n * The seam between a canvas surface and the views drawn on it.\n *\n * Traffic runs both ways: the surface attaches a handle onto its frame and its\n * layer stack, and each view registers what it contributes back — the viewport\n * node it paints into and the record the gesture dispatcher routes its events\n * to. Both sides are thunks, read at draw and dispatch time, so neither can\n * serve the other a frame that has already moved.\n */\nimport { createContext, useContext, useMemo, useRef, type ReactNode } from 'react';\nimport type { Dims, RenderLayer } from 'core/layers/render';\nimport type { LayerHit } from 'affordances/types';\nimport type { ChromeState } from 'core/selection/chromeState';\nimport type { SelectionApi } from 'core/selection/useSelection';\nimport type { View } from 'core/viewport/view';\nimport type { ViewportLayer } from 'domains/viewports/viewportLayer';\nimport type { DispatcherViewTarget } from '@weasel-js/routing/internal';\nimport { createViewResolver, viewContains, type ViewResolver } from 'domains/viewports/viewResolver';\nimport { createEmitter } from '@weasel-js/registry';\nimport type { ViewAt } from './canvasExtension';\nimport { isRectAim, rectAim, viewForAim, type ViewAim } from 'domains/viewports/aim';\nimport { viewportWorldRect } from 'core/viewport/viewportWorldRect';\nimport type { ViewMarkerProps } from 'domains/viewports/viewMarker';\nimport { createViewMarkerRegistry, type ViewMarkerRegistry } from './viewMarkers';\n\n/**\n * What a view needs from the surface hosting it.\n *\n * @internal Attached by the surface, read by `<CanvasView>`.\n */\nexport interface SurfaceHandle {\n /** A client point in the surface's own world — the conversion the surface\n * itself makes, its `clientToWorld` override included. */\n clientToWorld(clientX: number, clientY: number): [number, number];\n /** The outer camera. */\n view(): View;\n /** The canvas's CSS-pixel size. */\n dims(): Dims;\n /** The stack a view paints through its own camera, unless it narrows it. */\n layers(): readonly RenderLayer<unknown>[];\n requestRedraw(): void;\n /** The canvas's own view chrome — its selection, its overlay-aware bounds.\n * What a `<CanvasView>` answers for itself, answered for view zero. */\n chromeState(): ChromeState;\n /** Hit-test the externally registered layers at a world point, against the\n * frame and draw envelope of whichever view is asking. The public\n * `hitTestExtras` is this with the canvas's own frame and envelope. */\n hitTestExtras(\n worldX: number,\n worldY: number,\n view: View,\n dims: Dims,\n data: unknown,\n /** Consider only these layers — the ones the asking view paints. A layer\n * asked on a frame it was not drawn in answers for the wrong pixels. */\n only?: readonly RenderLayer<unknown>[],\n ): { layerId: string; hit: LayerHit } | null;\n /** Whether a registered layer claims a canvas-local point on the surface's\n * own frame. Registered layers paint over every view, so such a point is\n * theirs even inside a view's rect. */\n claimsAbove(x: number, y: number): boolean;\n /** The root camera's input write: clamped, forwarded when controlled, and ending a glide. */\n setView?(next: View): void;\n}\n\n/**\n * A client point in the surface's CSS pixels, from its top-left: the surface's\n * own world point put back through its camera, so a `clientToWorld` override\n * reaches every view declared on it.\n *\n * @internal\n */\nexport function surfaceLocal(\n surface: Pick<SurfaceHandle, 'clientToWorld' | 'view'> | null,\n clientX: number,\n clientY: number,\n): { x: number; y: number } {\n if (!surface) return { x: clientX, y: clientY };\n const [worldX, worldY] = surface.clientToWorld(clientX, clientY);\n const view = surface.view();\n return { x: (worldX - view.x) * view.scale.x, y: (worldY - view.y) * view.scale.y };\n}\n\n/**\n * What one view contributes to the surface hosting it.\n *\n * @internal Written by `<CanvasView>`, read by the surface. Consumers declare\n * a view, not a registration.\n */\nexport interface ViewRegistration {\n id: string;\n /** The view this one is nested in: listed, painted and hit after it, and\n * its `layer` measures itself from that view's rect and camera. */\n parent?: string;\n /**\n * Paint and hit order, low to high. Views declared as props take their array\n * index; a view mounted as a child takes `Infinity`, so it lands after them\n * however React happens to order the mount effects.\n */\n order: number;\n /** The viewport node this view paints into. Its `resolvable` is what routes\n * input here. */\n layer: ViewportLayer<unknown>;\n /** `false` keeps input over the view on the canvas beneath it. */\n interactive: boolean;\n /** `false` leaves `layer` for a host to draw — a HUD window's interior — so\n * the surface does not paint it too. */\n paint: boolean;\n /** Everything about dispatching to this view except its id. */\n target: Omit<DispatcherViewTarget, 'id'>;\n /** Whether a scene layer reaches the screen in this view. For input that\n * reaches a view without a dispatch — hover — so it judges this view's\n * paint rather than the surface's. */\n layerIsPainted(layerId: string): boolean;\n /** The layers this view draws, in order — what a view nested in it paints\n * from. */\n drawnLayers(): readonly RenderLayer<unknown>[];\n /** The selection the view owns, or the one the view it follows owns;\n * `undefined` while it shares the surface's. */\n selection(): SelectionApi | undefined;\n /** Whether the view keeps its camera or its aim; the other is derived. */\n held: 'camera' | 'aim';\n /** What the view shows: the aim it holds, or its camera's. */\n aim(): ViewAim;\n /** `false` for an aim the view cannot read, or one with nowhere to go. */\n setAim(aim: ViewAim): boolean;\n}\n\n\n/** @internal */\nexport interface ViewRegistry extends ViewMarkerRegistry {\n /** Returns an unregister. */\n register(reg: ViewRegistration): () => void;\n /** Live registrations in paint order, each nested view after the view it\n * is nested in. */\n list(): readonly ViewRegistration[];\n /** The live registration with `id`, if any. */\n get(id: string): ViewRegistration | undefined;\n /** The innermost view painted under a canvas-local point, whether or not it\n * takes input, passing over `except` and the views nested in it. */\n viewAt(at: { x: number; y: number }, opts?: { except?: string }): ViewAt;\n /** What view `id` shows (`null`: the surface's root), or `null` while there is no such view. */\n aimOf(id: string | null): ViewAim | null;\n /** Point view `id` at `aim`; `false` when it cannot. */\n setAim(id: string | null, aim: ViewAim): boolean;\n /** Bumped whenever the list changes. Pair with {@link ViewRegistry.subscribe}\n * for `useSyncExternalStore`. */\n getVersion(): number;\n subscribe(fn: () => void): () => void;\n /** Which view a client point belongs to, sticky for a captured pointer.\n * One per surface: every caller that routes input — the dispatcher, pinch,\n * hover — must agree about where a point landed. */\n readonly resolver: ViewResolver;\n /** The hosting surface, or `null` before one attaches. */\n surface(): SurfaceHandle | null;\n /** Returns a detach, which leaves a handle attached since alone. */\n attachSurface(handle: SurfaceHandle): () => void;\n}\n\nconst ViewRegistryContext = createContext<ViewRegistry | null>(null);\n\n/** Mounts the registry for one surface. `<SceneCanvas>` mounts one; a view\n * declaring itself as a child must be inside it. */\nexport function ViewRegistryProvider({ children }: { children: ReactNode }) {\n const entriesRef = useRef(new Map<ViewRegistration, number>());\n const seqRef = useRef(0);\n const versionRef = useRef(0);\n const surfaceRef = useRef<SurfaceHandle | null>(null);\n const markersRef = useRef(new Map<() => ViewMarkerProps, RenderLayer<unknown>>());\n\n const registry = useMemo<ViewRegistry>(() => {\n const listeners = createEmitter();\n const changed = (): void => {\n versionRef.current++;\n listeners.emit();\n };\n const ordered = (): ViewRegistration[] => nestedOrder([...entriesRef.current.keys()].sort((a, b) => (\n a.order - b.order || entriesRef.current.get(a)! - entriesRef.current.get(b)!\n )));\n const get = (id: string): ViewRegistration | undefined => {\n for (const r of entriesRef.current.keys()) if (r.id === id) return r;\n return undefined;\n };\n /** Whether `r` is `id` or nested in it, however deep. */\n const within = (r: ViewRegistration, id: string): boolean => {\n const seen = new Set<ViewRegistration>();\n for (let at: ViewRegistration | undefined = r; at && !seen.has(at); at = at.parent === undefined ? undefined : get(at.parent)) {\n if (at.id === id) return true;\n seen.add(at);\n }\n return false;\n };\n const resolver = createViewResolver({\n views: () => {\n const surface = surfaceRef.current;\n const view = surface?.view() ?? IDENTITY_VIEW;\n const dims = surface?.dims() ?? UNMEASURED_DIMS;\n return ordered().filter((r) => r.interactive).map((r) => r.layer.resolvable(view, dims));\n },\n root: () => surfaceRef.current?.view() ?? IDENTITY_VIEW,\n toLocal: (clientX, clientY) => surfaceLocal(surfaceRef.current, clientX, clientY),\n occluded: (x, y) => surfaceRef.current?.claimsAbove(x, y) ?? false,\n });\n const registry: ViewRegistry = {\n resolver,\n register: (reg) => {\n entriesRef.current.set(reg, seqRef.current++);\n changed();\n return () => { entriesRef.current.delete(reg); changed(); };\n },\n list: ordered,\n get,\n viewAt: ({ x, y }, opts) => {\n const surface = surfaceRef.current;\n const view = surface?.view() ?? IDENTITY_VIEW;\n const dims = surface?.dims() ?? UNMEASURED_DIMS;\n const except = opts?.except;\n const list = ordered();\n for (let i = list.length - 1; i >= 0; i--) {\n const r = list[i]!;\n if (except !== undefined && within(r, except)) continue;\n const v = r.layer.resolvable(view, dims);\n if (viewContains(v, x, y)) return { id: v.id, view: v.view, rect: v.rect };\n }\n return { id: null, view, rect: { x: 0, y: 0, w: dims.width, h: dims.height } };\n },\n aimOf: (id) => {\n if (id !== null) return get(id)?.aim() ?? null;\n const surface = surfaceRef.current;\n return surface ? rectAim(null, viewportWorldRect(surface.view(), surface.dims())) : null;\n },\n setAim: (id, aim) => {\n if (id !== null) return get(id)?.setAim(aim) ?? false;\n const surface = surfaceRef.current;\n if (!isRectAim(aim) || !surface?.setView) return false;\n surface.setView(viewForAim(aim.extent, surface.dims(), 'exact'));\n return true;\n },\n ...createViewMarkerRegistry(markersRef.current, {\n host: { aimOf: (id) => registry.aimOf(id), setAim: (id, aim) => registry.setAim(id, aim) },\n surface: () => surfaceRef.current,\n resolve: (id, outer, dims) => get(id)?.layer.resolvable(outer, dims),\n changed,\n }),\n getVersion: () => versionRef.current,\n subscribe: listeners.subscribe,\n surface: () => surfaceRef.current,\n attachSurface: (handle) => {\n surfaceRef.current = handle;\n return () => { if (surfaceRef.current === handle) surfaceRef.current = null; };\n },\n };\n return registry;\n }, []);\n\n return <ViewRegistryContext.Provider value={registry}>{children}</ViewRegistryContext.Provider>;\n}\n\n/** The surface's view registry, or `null` outside one. */\nexport function useOptionalViewRegistry(): ViewRegistry | null {\n return useContext(ViewRegistryContext);\n}\n\n/** Each view followed by the views nested in it, in the order given. A view\n * whose parent is not registered keeps its place; one caught in a cycle\n * goes last. */\nfunction nestedOrder(sorted: readonly ViewRegistration[]): ViewRegistration[] {\n const ids = new Set(sorted.map((r) => r.id));\n const out: ViewRegistration[] = [];\n const placed = new Set<ViewRegistration>();\n const place = (r: ViewRegistration): void => {\n if (placed.has(r)) return;\n placed.add(r);\n out.push(r);\n for (const child of sorted) if (child.parent === r.id) place(child);\n };\n for (const r of sorted) if (r.parent === undefined || !ids.has(r.parent)) place(r);\n for (const r of sorted) place(r);\n return out;\n}\n\n/** The `Dims` a surface reports before it has measured itself. */\nexport const UNMEASURED_DIMS: Dims = { width: 0, height: 0 };\n\n/** The camera a view resolves against before its surface attaches. */\nexport const IDENTITY_VIEW: View = { x: 0, y: 0, scale: { x: 1, y: 1 } };\n"],"mappings":"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"}
1
+ {"version":3,"file":"viewRegistry.js","names":[],"sources":["../../src/canvas/viewRegistry.tsx"],"sourcesContent":["/**\n * The seam between a canvas surface and the views drawn on it.\n *\n * Traffic runs both ways: the surface attaches a handle onto its frame and its\n * layer stack, and each view registers what it contributes back — the viewport\n * node it paints into and the record the gesture dispatcher routes its events\n * to. Both sides are thunks, read at draw and dispatch time, so neither can\n * serve the other a frame that has already moved.\n */\nimport { createContext, useContext, useMemo, useRef, type ReactNode } from 'react';\nimport type { Dims, RenderLayer } from 'core/layers/render';\nimport type { LayerHit } from 'affordances/types';\nimport type { ChromeState } from 'core/selection/chromeState';\nimport type { SelectionApi } from 'core/selection/useSelection';\nimport type { View } from 'core/viewport/view';\nimport type { ViewportLayer } from 'domains/viewports/viewportLayer';\nimport type { DispatcherViewTarget } from '@weasel-js/routing/internal';\nimport { createViewResolver, viewContains, type ViewResolver } from 'domains/viewports/viewResolver';\nimport { createEmitter } from '@weasel-js/registry';\nimport { viewWorldToScreen } from '@weasel-js/geom';\nimport type { ViewAt } from './canvasExtension';\nimport { isRectAim, rectAim, viewForAim, type ViewAim } from 'domains/viewports/aim';\nimport { viewportWorldRect } from 'core/viewport/viewportWorldRect';\nimport type { ViewMarkerProps } from 'domains/viewports/viewMarker';\nimport { createViewMarkerRegistry, type ViewMarkerRegistry } from './viewMarkers';\n\n/**\n * What a view needs from the surface hosting it.\n *\n * @internal Attached by the surface, read by `<CanvasView>`.\n */\nexport interface SurfaceHandle {\n /** A client point in the surface's own world — the conversion the surface\n * itself makes, its `clientToWorld` override included. */\n clientToWorld(clientX: number, clientY: number): [number, number];\n /** The outer camera. */\n view(): View;\n /** The canvas's CSS-pixel size. */\n dims(): Dims;\n /** The stack a view paints through its own camera, unless it narrows it. */\n layers(): readonly RenderLayer<unknown>[];\n requestRedraw(): void;\n /** The canvas's own view chrome — its selection, its overlay-aware bounds.\n * What a `<CanvasView>` answers for itself, answered for view zero. */\n chromeState(): ChromeState;\n /** Hit-test the externally registered layers at a world point, against the\n * frame and draw envelope of whichever view is asking. The public\n * `hitTestExtras` is this with the canvas's own frame and envelope. */\n hitTestExtras(\n worldX: number,\n worldY: number,\n view: View,\n dims: Dims,\n data: unknown,\n /** Consider only these layers — the ones the asking view paints. A layer\n * asked on a frame it was not drawn in answers for the wrong pixels. */\n only?: readonly RenderLayer<unknown>[],\n ): { layerId: string; hit: LayerHit } | null;\n /** Whether a registered layer claims a canvas-local point on the surface's\n * own frame. Registered layers paint over every view, so such a point is\n * theirs even inside a view's rect. */\n claimsAbove(x: number, y: number): boolean;\n /** The root camera's input write: clamped, forwarded when controlled, and ending a glide. */\n setView?(next: View): void;\n}\n\n/**\n * A client point in the surface's CSS pixels, from its top-left: the surface's\n * own world point put back through its camera, so a `clientToWorld` override\n * reaches every view declared on it.\n *\n * @internal\n */\nexport function surfaceLocal(\n surface: Pick<SurfaceHandle, 'clientToWorld' | 'view'> | null,\n clientX: number,\n clientY: number,\n): { x: number; y: number } {\n if (!surface) return { x: clientX, y: clientY };\n const [worldX, worldY] = surface.clientToWorld(clientX, clientY);\n return viewWorldToScreen(surface.view(), { x: worldX, y: worldY });\n}\n\n/**\n * What one view contributes to the surface hosting it.\n *\n * @internal Written by `<CanvasView>`, read by the surface. Consumers declare\n * a view, not a registration.\n */\nexport interface ViewRegistration {\n id: string;\n /** The view this one is nested in: listed, painted and hit after it, and\n * its `layer` measures itself from that view's rect and camera. */\n parent?: string;\n /**\n * Paint and hit order, low to high. Views declared as props take their array\n * index; a view mounted as a child takes `Infinity`, so it lands after them\n * however React happens to order the mount effects.\n */\n order: number;\n /** The viewport node this view paints into. Its `resolvable` is what routes\n * input here. */\n layer: ViewportLayer<unknown>;\n /** `false` keeps input over the view on the canvas beneath it. */\n interactive: boolean;\n /** `false` leaves `layer` for a host to draw — a HUD window's interior — so\n * the surface does not paint it too. */\n paint: boolean;\n /** Everything about dispatching to this view except its id. */\n target: Omit<DispatcherViewTarget, 'id'>;\n /** Whether a scene layer reaches the screen in this view. For input that\n * reaches a view without a dispatch — hover — so it judges this view's\n * paint rather than the surface's. */\n layerIsPainted(layerId: string): boolean;\n /** The layers this view draws, in order — what a view nested in it paints\n * from. */\n drawnLayers(): readonly RenderLayer<unknown>[];\n /** The selection the view owns, or the one the view it follows owns;\n * `undefined` while it shares the surface's. */\n selection(): SelectionApi | undefined;\n /** Whether the view keeps its camera or its aim; the other is derived. */\n held: 'camera' | 'aim';\n /** What the view shows: the aim it holds, or its camera's. */\n aim(): ViewAim;\n /** `false` for an aim the view cannot read, or one with nowhere to go. */\n setAim(aim: ViewAim): boolean;\n}\n\n\n/** @internal */\nexport interface ViewRegistry extends ViewMarkerRegistry {\n /** Returns an unregister. */\n register(reg: ViewRegistration): () => void;\n /** Live registrations in paint order, each nested view after the view it\n * is nested in. */\n list(): readonly ViewRegistration[];\n /** The live registration with `id`, if any. */\n get(id: string): ViewRegistration | undefined;\n /** The innermost view painted under a canvas-local point, whether or not it\n * takes input, passing over `except` and the views nested in it. */\n viewAt(at: { x: number; y: number }, opts?: { except?: string }): ViewAt;\n /** What view `id` shows (`null`: the surface's root), or `null` while there is no such view. */\n aimOf(id: string | null): ViewAim | null;\n /** Point view `id` at `aim`; `false` when it cannot. */\n setAim(id: string | null, aim: ViewAim): boolean;\n /** Bumped whenever the list changes. Pair with {@link ViewRegistry.subscribe}\n * for `useSyncExternalStore`. */\n getVersion(): number;\n subscribe(fn: () => void): () => void;\n /** Which view a client point belongs to, sticky for a captured pointer.\n * One per surface: every caller that routes input — the dispatcher, pinch,\n * hover — must agree about where a point landed. */\n readonly resolver: ViewResolver;\n /** The hosting surface, or `null` before one attaches. */\n surface(): SurfaceHandle | null;\n /** Returns a detach, which leaves a handle attached since alone. */\n attachSurface(handle: SurfaceHandle): () => void;\n}\n\nconst ViewRegistryContext = createContext<ViewRegistry | null>(null);\n\n/** Mounts the registry for one surface. `<SceneCanvas>` mounts one; a view\n * declaring itself as a child must be inside it. */\nexport function ViewRegistryProvider({ children }: { children: ReactNode }) {\n const entriesRef = useRef(new Map<ViewRegistration, number>());\n const seqRef = useRef(0);\n const versionRef = useRef(0);\n const surfaceRef = useRef<SurfaceHandle | null>(null);\n const markersRef = useRef(new Map<() => ViewMarkerProps, RenderLayer<unknown>>());\n\n const registry = useMemo<ViewRegistry>(() => {\n const listeners = createEmitter();\n const changed = (): void => {\n versionRef.current++;\n listeners.emit();\n };\n const ordered = (): ViewRegistration[] => nestedOrder([...entriesRef.current.keys()].sort((a, b) => (\n a.order - b.order || entriesRef.current.get(a)! - entriesRef.current.get(b)!\n )));\n const get = (id: string): ViewRegistration | undefined => {\n for (const r of entriesRef.current.keys()) if (r.id === id) return r;\n return undefined;\n };\n /** Whether `r` is `id` or nested in it, however deep. */\n const within = (r: ViewRegistration, id: string): boolean => {\n const seen = new Set<ViewRegistration>();\n for (let at: ViewRegistration | undefined = r; at && !seen.has(at); at = at.parent === undefined ? undefined : get(at.parent)) {\n if (at.id === id) return true;\n seen.add(at);\n }\n return false;\n };\n const resolver = createViewResolver({\n views: () => {\n const surface = surfaceRef.current;\n const view = surface?.view() ?? IDENTITY_VIEW;\n const dims = surface?.dims() ?? UNMEASURED_DIMS;\n return ordered().filter((r) => r.interactive).map((r) => r.layer.resolvable(view, dims));\n },\n root: () => surfaceRef.current?.view() ?? IDENTITY_VIEW,\n toLocal: (clientX, clientY) => surfaceLocal(surfaceRef.current, clientX, clientY),\n occluded: (x, y) => surfaceRef.current?.claimsAbove(x, y) ?? false,\n });\n const registry: ViewRegistry = {\n resolver,\n register: (reg) => {\n entriesRef.current.set(reg, seqRef.current++);\n changed();\n return () => { entriesRef.current.delete(reg); changed(); };\n },\n list: ordered,\n get,\n viewAt: ({ x, y }, opts) => {\n const surface = surfaceRef.current;\n const view = surface?.view() ?? IDENTITY_VIEW;\n const dims = surface?.dims() ?? UNMEASURED_DIMS;\n const except = opts?.except;\n const list = ordered();\n for (let i = list.length - 1; i >= 0; i--) {\n const r = list[i]!;\n if (except !== undefined && within(r, except)) continue;\n const v = r.layer.resolvable(view, dims);\n if (viewContains(v, x, y)) return { id: v.id, view: v.view, rect: v.rect };\n }\n return { id: null, view, rect: { x: 0, y: 0, w: dims.width, h: dims.height } };\n },\n aimOf: (id) => {\n if (id !== null) return get(id)?.aim() ?? null;\n const surface = surfaceRef.current;\n return surface ? rectAim(null, viewportWorldRect(surface.view(), surface.dims())) : null;\n },\n setAim: (id, aim) => {\n if (id !== null) return get(id)?.setAim(aim) ?? false;\n const surface = surfaceRef.current;\n if (!isRectAim(aim) || !surface?.setView) return false;\n surface.setView(viewForAim(aim.extent, surface.dims(), 'exact'));\n return true;\n },\n ...createViewMarkerRegistry(markersRef.current, {\n host: { aimOf: (id) => registry.aimOf(id), setAim: (id, aim) => registry.setAim(id, aim) },\n surface: () => surfaceRef.current,\n resolve: (id, outer, dims) => get(id)?.layer.resolvable(outer, dims),\n changed,\n }),\n getVersion: () => versionRef.current,\n subscribe: listeners.subscribe,\n surface: () => surfaceRef.current,\n attachSurface: (handle) => {\n surfaceRef.current = handle;\n return () => { if (surfaceRef.current === handle) surfaceRef.current = null; };\n },\n };\n return registry;\n }, []);\n\n return <ViewRegistryContext.Provider value={registry}>{children}</ViewRegistryContext.Provider>;\n}\n\n/** The surface's view registry, or `null` outside one. */\nexport function useOptionalViewRegistry(): ViewRegistry | null {\n return useContext(ViewRegistryContext);\n}\n\n/** Each view followed by the views nested in it, in the order given. A view\n * whose parent is not registered keeps its place; one caught in a cycle\n * goes last. */\nfunction nestedOrder(sorted: readonly ViewRegistration[]): ViewRegistration[] {\n const ids = new Set(sorted.map((r) => r.id));\n const out: ViewRegistration[] = [];\n const placed = new Set<ViewRegistration>();\n const place = (r: ViewRegistration): void => {\n if (placed.has(r)) return;\n placed.add(r);\n out.push(r);\n for (const child of sorted) if (child.parent === r.id) place(child);\n };\n for (const r of sorted) if (r.parent === undefined || !ids.has(r.parent)) place(r);\n for (const r of sorted) place(r);\n return out;\n}\n\n/** The `Dims` a surface reports before it has measured itself. */\nexport const UNMEASURED_DIMS: Dims = { width: 0, height: 0 };\n\n/** The camera a view resolves against before its surface attaches. */\nexport const IDENTITY_VIEW: View = { x: 0, y: 0, scale: { x: 1, y: 1 } };\n"],"mappings":"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"}
@@ -1,13 +1,9 @@
1
1
  import { registerOpFactory } from "../registry.js";
2
2
  import { bringForward, bringToFront, canBringForward, canSendBackward, moveToIndex, sendBackward, sendToBack } from "./algorithms.js";
3
3
  import { createSetChildOrderOp } from "./setChildOrder.js";
4
+ import { sameList } from "@weasel-js/registry";
4
5
 
5
6
  // src/core/ops/reorder/index.ts
6
- function arraysEqual(a, b) {
7
- if (a.length !== b.length) return false;
8
- for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
9
- return true;
10
- }
11
7
  /**
12
8
  * Build an op that, on apply, partitions `ids` by their current parent,
13
9
  * runs `fn(currentChildren, idsForParent)` per parent, and writes the
@@ -52,7 +48,7 @@ function createPartitionedReorderOp(args) {
52
48
  for (const [parentId, parentIds] of byParent) {
53
49
  const before = a.getChildren(parentId);
54
50
  const after = fn(before, parentIds);
55
- if (arraysEqual(before, after)) continue;
51
+ if (sameList(before, after)) continue;
56
52
  snapshots.push({
57
53
  parentId,
58
54
  before: before.slice()
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../../../src/core/ops/reorder/index.ts"],"sourcesContent":["import type { Op } from '../types';\nimport { registerOpFactory } from '../registry';\nimport {\n bringForward,\n sendBackward,\n bringToFront,\n sendToBack,\n moveToIndex,\n} from './algorithms';\n\nimport { createSetChildOrderOp } from './setChildOrder';\n\nexport { canBringForward, canSendBackward } from './algorithms';\nexport { createSetChildOrderOp, type ChildOrder, type SetChildOrderArgs } from './setChildOrder';\n\ninterface ReorderAdapter {\n getParent(id: string): string | null;\n getChildren(parentId: string | null): string[];\n setChildOrder(parentId: string | null, ids: string[]): void;\n}\n\ntype ReorderFn = (list: string[], ids: string[]) => string[];\n\nfunction arraysEqual(a: readonly string[], b: readonly string[]): boolean {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;\n return true;\n}\n\n/** One parent's child order before a reorder, kept so undo can restore it. */\nexport interface ReorderRestoreEntry {\n parentId: string | null;\n before: string[];\n}\n\n/** Arguments to {@link createReorderOp}: move `ids` in paint order, one step or to the front/back. */\nexport interface ReorderArgs {\n ids: string[];\n direction: ReorderDirection;\n label?: string;\n /** Pre-mutation child order per touched parent. Filled in on first apply;\n * arrives populated when the op is rebuilt from a snapshot. */\n prevOrders?: ReorderRestoreEntry[];\n}\n\n/**\n * Build an op that, on apply, partitions `ids` by their current parent,\n * runs `fn(currentChildren, idsForParent)` per parent, and writes the\n * result back via setChildOrder. Records before-state per parent so invert\n * is exact.\n *\n * `prevOrders` is that before-state, mirrored into the op's serialized args\n * on first apply. A deserialized op has never run its own `apply`, so its\n * invert has nothing to capture — reading it back from the args is what lets\n * a reorder survive a reload, the same trick `createMoveToIndexOp` uses.\n */\nfunction createPartitionedReorderOp(args: {\n ids: string[];\n direction: ReorderDirection;\n fn: ReorderFn;\n label?: string;\n prevOrders?: ReorderRestoreEntry[];\n}): Op {\n const { ids, direction, fn, label, prevOrders } = args;\n const argsForSerial: ReorderArgs = {\n ids,\n direction,\n label,\n prevOrders: prevOrders ? prevOrders.map((e) => ({ ...e, before: e.before.slice() })) : undefined,\n };\n let restore: ReorderRestoreEntry[] | null =\n prevOrders ? prevOrders.map((e) => ({ ...e, before: e.before.slice() })) : null;\n\n return {\n name: 'reorder',\n args: argsForSerial,\n label,\n apply(adapter) {\n const a = adapter as ReorderAdapter;\n // Partition ids by current parent.\n const byParent = new Map<string | null, string[]>();\n for (const id of ids) {\n const parent = a.getParent(id);\n const list = byParent.get(parent) ?? [];\n list.push(id);\n byParent.set(parent, list);\n }\n const snapshots: ReorderRestoreEntry[] = [];\n let mutated = false;\n for (const [parentId, parentIds] of byParent) {\n const before = a.getChildren(parentId);\n const after = fn(before, parentIds);\n if (arraysEqual(before, after)) continue; // already at the desired order\n snapshots.push({ parentId, before: before.slice() });\n a.setChildOrder(parentId, after);\n mutated = true;\n }\n restore = snapshots;\n argsForSerial.prevOrders = snapshots.map((e) => ({ ...e, before: e.before.slice() }));\n return mutated;\n },\n invert() {\n const captured = restore;\n return {\n ...createSetChildOrderOp({\n orders: (captured ?? []).map((e) => ({ parentId: e.parentId, ids: e.before })),\n label,\n }),\n invert: () =>\n createPartitionedReorderOp({\n ids, direction, fn, label, prevOrders: captured ?? undefined,\n }),\n };\n },\n };\n}\n\n/** Direction of a sibling z-order reorder. */\nexport type ReorderDirection = 'forward' | 'backward' | 'front' | 'back';\n\nconst REORDER_DIRECTIONS: Record<ReorderDirection, { fn: ReorderFn; defaultLabel: string }> = {\n forward: { fn: bringForward, defaultLabel: 'Bring forward' },\n backward: { fn: sendBackward, defaultLabel: 'Send backward' },\n front: { fn: bringToFront, defaultLabel: 'Bring to front' },\n back: { fn: sendToBack, defaultLabel: 'Send to back' },\n};\n\n/**\n * Op: shift each id within its parent's child order per `direction`:\n * - `'forward'` — bump one step toward the top\n * - `'backward'` — bump one step toward the bottom\n * - `'front'` — move to the top, preserving relative order\n * - `'back'` — move to the bottom, preserving relative order\n */\nexport function createReorderOp(args: ReorderArgs): Op {\n const { ids, direction, label, prevOrders } = args;\n const { fn, defaultLabel } = REORDER_DIRECTIONS[direction];\n return createPartitionedReorderOp({\n ids, direction, fn, label: label ?? defaultLabel, prevOrders,\n });\n}\n\nregisterOpFactory<ReorderArgs>('reorder', (args) => createReorderOp(args));\n\n/** Arguments to {@link createMoveToIndexOp}: move `ids` to `index` under `parentId`. */\nexport interface MoveToIndexArgs {\n ids: string[];\n parentId: string | null;\n index: number;\n label?: string;\n /** Optional pre-mutation child order for the parent. When present, the op\n * uses it as the invert target instead of capturing at apply time — this\n * is how a deserialized op (whose original `apply` already ran in a prior\n * session) restores its invert behavior across an IDB round-trip. */\n prevPositions?: string[];\n}\n\n/** Op: move all `ids` to a contiguous block starting at `index` within `parentId`'s child order. */\nexport function createMoveToIndexOp(args: MoveToIndexArgs): Op {\n const { ids, parentId, index, label } = args;\n // Outer args record we'll mutate so the captured `before` lands in the\n // serialized form. `prevPositions` arrives populated when reconstructed\n // from a snapshot; otherwise it's filled in on first apply.\n const argsForSerial: MoveToIndexArgs = {\n ids,\n parentId,\n index,\n label,\n prevPositions: args.prevPositions ? args.prevPositions.slice() : undefined,\n };\n let before: string[] | null = args.prevPositions ? args.prevPositions.slice() : null;\n\n return {\n name: 'moveToIndex',\n args: argsForSerial,\n label: label ?? 'Move to index',\n apply(adapter) {\n const a = adapter as ReorderAdapter;\n // Filter to ids whose current parent matches target parent.\n const eligible = ids.filter((id) => a.getParent(id) === parentId);\n const current = a.getChildren(parentId);\n // Only overwrite `before` the first time we apply; redo (a second\n // apply) must not clobber the originally-captured order, otherwise\n // invert would replay the post-move state and undo nothing.\n if (before === null) {\n before = current.slice();\n argsForSerial.prevPositions = before.slice();\n }\n const after = moveToIndex(current, eligible, index);\n a.setChildOrder(parentId, after);\n },\n invert() {\n const captured = before;\n return {\n ...createSetChildOrderOp({ orders: captured ? [{ parentId, ids: captured }] : [], label }),\n invert: () =>\n createMoveToIndexOp({\n ids, parentId, index, label, prevPositions: captured ?? undefined,\n }),\n };\n },\n };\n}\n\nregisterOpFactory<MoveToIndexArgs>('moveToIndex', (args) => createMoveToIndexOp(args));\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../../src/core/ops/reorder/index.ts"],"sourcesContent":["import { sameList } from '@weasel-js/registry';\nimport type { Op } from '../types';\nimport { registerOpFactory } from '../registry';\nimport {\n bringForward,\n sendBackward,\n bringToFront,\n sendToBack,\n moveToIndex,\n} from './algorithms';\n\nimport { createSetChildOrderOp } from './setChildOrder';\n\nexport { canBringForward, canSendBackward } from './algorithms';\nexport { createSetChildOrderOp, type ChildOrder, type SetChildOrderArgs } from './setChildOrder';\n\ninterface ReorderAdapter {\n getParent(id: string): string | null;\n getChildren(parentId: string | null): string[];\n setChildOrder(parentId: string | null, ids: string[]): void;\n}\n\ntype ReorderFn = (list: string[], ids: string[]) => string[];\n\n/** One parent's child order before a reorder, kept so undo can restore it. */\nexport interface ReorderRestoreEntry {\n parentId: string | null;\n before: string[];\n}\n\n/** Arguments to {@link createReorderOp}: move `ids` in paint order, one step or to the front/back. */\nexport interface ReorderArgs {\n ids: string[];\n direction: ReorderDirection;\n label?: string;\n /** Pre-mutation child order per touched parent. Filled in on first apply;\n * arrives populated when the op is rebuilt from a snapshot. */\n prevOrders?: ReorderRestoreEntry[];\n}\n\n/**\n * Build an op that, on apply, partitions `ids` by their current parent,\n * runs `fn(currentChildren, idsForParent)` per parent, and writes the\n * result back via setChildOrder. Records before-state per parent so invert\n * is exact.\n *\n * `prevOrders` is that before-state, mirrored into the op's serialized args\n * on first apply. A deserialized op has never run its own `apply`, so its\n * invert has nothing to capture — reading it back from the args is what lets\n * a reorder survive a reload, the same trick `createMoveToIndexOp` uses.\n */\nfunction createPartitionedReorderOp(args: {\n ids: string[];\n direction: ReorderDirection;\n fn: ReorderFn;\n label?: string;\n prevOrders?: ReorderRestoreEntry[];\n}): Op {\n const { ids, direction, fn, label, prevOrders } = args;\n const argsForSerial: ReorderArgs = {\n ids,\n direction,\n label,\n prevOrders: prevOrders ? prevOrders.map((e) => ({ ...e, before: e.before.slice() })) : undefined,\n };\n let restore: ReorderRestoreEntry[] | null =\n prevOrders ? prevOrders.map((e) => ({ ...e, before: e.before.slice() })) : null;\n\n return {\n name: 'reorder',\n args: argsForSerial,\n label,\n apply(adapter) {\n const a = adapter as ReorderAdapter;\n // Partition ids by current parent.\n const byParent = new Map<string | null, string[]>();\n for (const id of ids) {\n const parent = a.getParent(id);\n const list = byParent.get(parent) ?? [];\n list.push(id);\n byParent.set(parent, list);\n }\n const snapshots: ReorderRestoreEntry[] = [];\n let mutated = false;\n for (const [parentId, parentIds] of byParent) {\n const before = a.getChildren(parentId);\n const after = fn(before, parentIds);\n if (sameList(before, after)) continue; // already at the desired order\n snapshots.push({ parentId, before: before.slice() });\n a.setChildOrder(parentId, after);\n mutated = true;\n }\n restore = snapshots;\n argsForSerial.prevOrders = snapshots.map((e) => ({ ...e, before: e.before.slice() }));\n return mutated;\n },\n invert() {\n const captured = restore;\n return {\n ...createSetChildOrderOp({\n orders: (captured ?? []).map((e) => ({ parentId: e.parentId, ids: e.before })),\n label,\n }),\n invert: () =>\n createPartitionedReorderOp({\n ids, direction, fn, label, prevOrders: captured ?? undefined,\n }),\n };\n },\n };\n}\n\n/** Direction of a sibling z-order reorder. */\nexport type ReorderDirection = 'forward' | 'backward' | 'front' | 'back';\n\nconst REORDER_DIRECTIONS: Record<ReorderDirection, { fn: ReorderFn; defaultLabel: string }> = {\n forward: { fn: bringForward, defaultLabel: 'Bring forward' },\n backward: { fn: sendBackward, defaultLabel: 'Send backward' },\n front: { fn: bringToFront, defaultLabel: 'Bring to front' },\n back: { fn: sendToBack, defaultLabel: 'Send to back' },\n};\n\n/**\n * Op: shift each id within its parent's child order per `direction`:\n * - `'forward'` — bump one step toward the top\n * - `'backward'` — bump one step toward the bottom\n * - `'front'` — move to the top, preserving relative order\n * - `'back'` — move to the bottom, preserving relative order\n */\nexport function createReorderOp(args: ReorderArgs): Op {\n const { ids, direction, label, prevOrders } = args;\n const { fn, defaultLabel } = REORDER_DIRECTIONS[direction];\n return createPartitionedReorderOp({\n ids, direction, fn, label: label ?? defaultLabel, prevOrders,\n });\n}\n\nregisterOpFactory<ReorderArgs>('reorder', (args) => createReorderOp(args));\n\n/** Arguments to {@link createMoveToIndexOp}: move `ids` to `index` under `parentId`. */\nexport interface MoveToIndexArgs {\n ids: string[];\n parentId: string | null;\n index: number;\n label?: string;\n /** Optional pre-mutation child order for the parent. When present, the op\n * uses it as the invert target instead of capturing at apply time — this\n * is how a deserialized op (whose original `apply` already ran in a prior\n * session) restores its invert behavior across an IDB round-trip. */\n prevPositions?: string[];\n}\n\n/** Op: move all `ids` to a contiguous block starting at `index` within `parentId`'s child order. */\nexport function createMoveToIndexOp(args: MoveToIndexArgs): Op {\n const { ids, parentId, index, label } = args;\n // Outer args record we'll mutate so the captured `before` lands in the\n // serialized form. `prevPositions` arrives populated when reconstructed\n // from a snapshot; otherwise it's filled in on first apply.\n const argsForSerial: MoveToIndexArgs = {\n ids,\n parentId,\n index,\n label,\n prevPositions: args.prevPositions ? args.prevPositions.slice() : undefined,\n };\n let before: string[] | null = args.prevPositions ? args.prevPositions.slice() : null;\n\n return {\n name: 'moveToIndex',\n args: argsForSerial,\n label: label ?? 'Move to index',\n apply(adapter) {\n const a = adapter as ReorderAdapter;\n // Filter to ids whose current parent matches target parent.\n const eligible = ids.filter((id) => a.getParent(id) === parentId);\n const current = a.getChildren(parentId);\n // Only overwrite `before` the first time we apply; redo (a second\n // apply) must not clobber the originally-captured order, otherwise\n // invert would replay the post-move state and undo nothing.\n if (before === null) {\n before = current.slice();\n argsForSerial.prevPositions = before.slice();\n }\n const after = moveToIndex(current, eligible, index);\n a.setChildOrder(parentId, after);\n },\n invert() {\n const captured = before;\n return {\n ...createSetChildOrderOp({ orders: captured ? [{ parentId, ids: captured }] : [], label }),\n invert: () =>\n createMoveToIndexOp({\n ids, parentId, index, label, prevPositions: captured ?? undefined,\n }),\n };\n },\n };\n}\n\nregisterOpFactory<MoveToIndexArgs>('moveToIndex', (args) => createMoveToIndexOp(args));\n"],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import { fitViewToBounds } from "../viewport/fitViewToBounds.js";
2
+ import { viewportWorldRect } from "../viewport/viewportWorldRect.js";
2
3
  import { pageOfBounds } from "./pages.js";
3
4
 
4
5
  // src/core/pages/fitViewToPage.ts
@@ -14,13 +15,7 @@ function fitViewToPage(page, viewportDims, currentView, options) {
14
15
  }
15
16
  /** The page the view shows most of: the one overlapping the visible world rect most, or `null` when none shows. */
16
17
  function pageInView(pages, view, viewportDims) {
17
- const visible = {
18
- x: view.x,
19
- y: view.y,
20
- width: viewportDims.width / view.scale.x,
21
- height: viewportDims.height / view.scale.y
22
- };
23
- return pageOfBounds(pages, visible);
18
+ return pageOfBounds(pages, viewportWorldRect(view, viewportDims));
24
19
  }
25
20
 
26
21
  export { fitViewToPage, pageInView };
@@ -1 +1 @@
1
- {"version":3,"file":"fitViewToPage.js","names":[],"sources":["../../../src/core/pages/fitViewToPage.ts"],"sourcesContent":["import {\n fitViewToBounds,\n type FitViewToBoundsOptions,\n type ViewportDims,\n} from '../viewport/fitViewToBounds';\nimport type { View } from '../viewport/view';\nimport { pageOfBounds, type PlacedPage } from './pages';\n\n/** The view that shows one placed page whole: `fitViewToBounds` on its rect. */\nexport function fitViewToPage(\n page: PlacedPage,\n viewportDims: ViewportDims,\n currentView: View,\n options?: FitViewToBoundsOptions,\n): View {\n const { x, y, width, height } = page;\n return fitViewToBounds({ x, y, width, height }, viewportDims, currentView, options);\n}\n\n/** The page the view shows most of: the one overlapping the visible world rect most, or `null` when none shows. */\nexport function pageInView(\n pages: readonly PlacedPage[],\n view: View,\n viewportDims: ViewportDims,\n): PlacedPage | null {\n const visible = { x: view.x, y: view.y, width: viewportDims.width / view.scale.x, height: viewportDims.height / view.scale.y };\n return pageOfBounds(pages, visible);\n}\n"],"mappings":";;;;;AASA,SAAgB,cACd,MACA,cACA,aACA,SACM;CACN,MAAM,EAAE,GAAG,GAAG,OAAO,WAAW;CAChC,OAAO,gBAAgB;EAAE;EAAG;EAAG;EAAO;CAAO,GAAG,cAAc,aAAa,OAAO;AACpF;;AAGA,SAAgB,WACd,OACA,MACA,cACmB;CACnB,MAAM,UAAU;EAAE,GAAG,KAAK;EAAG,GAAG,KAAK;EAAG,OAAO,aAAa,QAAQ,KAAK,MAAM;EAAG,QAAQ,aAAa,SAAS,KAAK,MAAM;CAAE;CAC7H,OAAO,aAAa,OAAO,OAAO;AACpC"}
1
+ {"version":3,"file":"fitViewToPage.js","names":[],"sources":["../../../src/core/pages/fitViewToPage.ts"],"sourcesContent":["import {\n fitViewToBounds,\n type FitViewToBoundsOptions,\n type ViewportDims,\n} from '../viewport/fitViewToBounds';\nimport type { View } from '../viewport/view';\nimport { viewportWorldRect } from '../viewport/viewportWorldRect';\nimport { pageOfBounds, type PlacedPage } from './pages';\n\n/** The view that shows one placed page whole: `fitViewToBounds` on its rect. */\nexport function fitViewToPage(\n page: PlacedPage,\n viewportDims: ViewportDims,\n currentView: View,\n options?: FitViewToBoundsOptions,\n): View {\n const { x, y, width, height } = page;\n return fitViewToBounds({ x, y, width, height }, viewportDims, currentView, options);\n}\n\n/** The page the view shows most of: the one overlapping the visible world rect most, or `null` when none shows. */\nexport function pageInView(\n pages: readonly PlacedPage[],\n view: View,\n viewportDims: ViewportDims,\n): PlacedPage | null {\n return pageOfBounds(pages, viewportWorldRect(view, viewportDims));\n}\n"],"mappings":";;;;;;AAUA,SAAgB,cACd,MACA,cACA,aACA,SACM;CACN,MAAM,EAAE,GAAG,GAAG,OAAO,WAAW;CAChC,OAAO,gBAAgB;EAAE;EAAG;EAAG;EAAO;CAAO,GAAG,cAAc,aAAa,OAAO;AACpF;;AAGA,SAAgB,WACd,OACA,MACA,cACmB;CACnB,OAAO,aAAa,OAAO,kBAAkB,MAAM,YAAY,CAAC;AAClE"}
@@ -1,3 +1,5 @@
1
+ import { unionRects } from "@weasel-js/geom";
2
+
1
3
  // src/core/pages/pages.ts
2
4
  var ALIGN = {
3
5
  start: 0,
@@ -109,15 +111,7 @@ function adjacentPage(pages, current, step, options = {}) {
109
111
  }
110
112
  /** The box around every page, or `null` for a document with none. */
111
113
  function pagesBounds(pages) {
112
- if (pages.length === 0) return null;
113
- const x = Math.min(...pages.map((p) => p.x));
114
- const y = Math.min(...pages.map((p) => p.y));
115
- return {
116
- x,
117
- y,
118
- width: Math.max(...pages.map((p) => p.x + p.width)) - x,
119
- height: Math.max(...pages.map((p) => p.y + p.height)) - y
120
- };
114
+ return unionRects(pages);
121
115
  }
122
116
 
123
117
  export { adjacentPage, layoutPages, pageAt, pageHolding, pageName, pageOfBounds, pagesBounds };
@@ -1 +1 @@
1
- {"version":3,"file":"pages.js","names":[],"sources":["../../../src/core/pages/pages.ts"],"sourcesContent":["import type { Bounds } from '@weasel-js/routing';\n\n/** One page of a document: a sheet of a fixed size, in world units. */\nexport interface Page {\n readonly id: string;\n readonly name?: string;\n readonly width: number;\n readonly height: number;\n /** What other packages keep on the page, each under its owner's key (`'diagram:graph'`). Every page edit\n * carries it, and the page's placement copies it. */\n readonly meta?: Readonly<Record<string, unknown>>;\n}\n\n/** A page and where it sits in world space. */\nexport interface PlacedPage extends Page {\n readonly x: number;\n readonly y: number;\n /** Its place in the document's page order. */\n readonly index: number;\n}\n\n/** Options for {@link layoutPages}. */\nexport interface PageLayoutOptions {\n /** Which way the pages run. Default `'row'`. */\n direction?: 'row' | 'column';\n /** World units between neighbors. Default 40. */\n gap?: number;\n /** Where a page sits across the run, against the largest page. Default `'start'`. */\n align?: 'start' | 'center' | 'end';\n /** Where the first page's run starts. Default the world origin. */\n origin?: { readonly x: number; readonly y: number };\n}\n\nconst ALIGN = { start: 0, center: 0.5, end: 1 } as const;\n\n/**\n * Place a document's pages in one world space, in order, so a single scene,\n * view, and history serve all of them. With the defaults the first page's\n * top-left corner is the world origin, where a one-page document's page is.\n * Throws on two pages with one id.\n */\nexport function layoutPages(pages: readonly Page[], options: PageLayoutOptions = {}): PlacedPage[] {\n const { direction = 'row', gap = 40, align = 'start', origin = { x: 0, y: 0 } } = options;\n const row = direction === 'row';\n const across = Math.max(0, ...pages.map((p) => (row ? p.height : p.width)));\n const seen = new Set<string>();\n let along = 0;\n return pages.map((page, index) => {\n if (seen.has(page.id)) throw new Error(`layoutPages: two pages have the id \"${page.id}\"`);\n seen.add(page.id);\n const offset = (across - (row ? page.height : page.width)) * ALIGN[align];\n const placed = {\n ...page,\n x: origin.x + (row ? along : offset),\n y: origin.y + (row ? offset : along),\n index,\n };\n along += (row ? page.width : page.height) + gap;\n return placed;\n });\n}\n\n/** The name page `index` of the page order shows: its own, else \"Page\" and its place. */\nexport function pageName(page: { readonly name?: string | undefined }, index: number): string {\n return page.name ?? `Page ${index + 1}`;\n}\n\n/** The page under a world point, or `null` between and outside them. A point on an edge is on the page. */\nexport function pageAt(\n pages: readonly PlacedPage[],\n point: { readonly x: number; readonly y: number },\n): PlacedPage | null {\n return pageOfBounds(pages, { x: point.x, y: point.y, width: 0, height: 0 });\n}\n\nfunction overlap(page: PlacedPage, b: Bounds): number | null {\n const w = Math.min(page.x + page.width, b.x + b.width) - Math.max(page.x, b.x);\n const h = Math.min(page.y + page.height, b.y + b.height) - Math.max(page.y, b.y);\n return w < 0 || h < 0 ? null : w * h;\n}\n\n/**\n * The page a world-space box belongs to: the one it overlaps most, the earlier\n * of two it overlaps equally, and `null` when it touches none. This is the\n * rule that says which page an object is on.\n */\nexport function pageOfBounds(pages: readonly PlacedPage[], bounds: Bounds): PlacedPage | null {\n let best: PlacedPage | null = null;\n let most = -1;\n for (const page of pages) {\n const area = overlap(page, bounds);\n if (area !== null && area > most) {\n best = page;\n most = area;\n }\n }\n return best;\n}\n\nfunction gap(page: PlacedPage, b: Bounds): number {\n const dx = Math.max(page.x - (b.x + b.width), b.x - (page.x + page.width), 0);\n const dy = Math.max(page.y - (b.y + b.height), b.y - (page.y + page.height), 0);\n return Math.hypot(dx, dy);\n}\n\n/**\n * The page a world-space box is held by: the one it is on (`pageOfBounds`),\n * else the one nearest it by the box-to-box gap, the earlier of two equally\n * near. `null` only with no pages. Used where every object must land on some\n * page, such as an export; `pageOfBounds` stays the rule for what is on one.\n */\nexport function pageHolding(pages: readonly PlacedPage[], bounds: Bounds): PlacedPage | null {\n const on = pageOfBounds(pages, bounds);\n if (on !== null) return on;\n let best: PlacedPage | null = null;\n let least = Infinity;\n for (const page of pages) {\n const d = gap(page, bounds);\n if (d < least) {\n best = page;\n least = d;\n }\n }\n return best;\n}\n\n/**\n * The page `step` places along the page order from the one with id `current`:\n * the next slide is `1`, the one before is `-1`. `null` past either end, unless\n * `wrap`. With no current page in the list, a step forward starts at the first\n * page and a step back at the last.\n */\nexport function adjacentPage(\n pages: readonly PlacedPage[],\n current: string | null,\n step: number,\n options: { wrap?: boolean } = {},\n): PlacedPage | null {\n const n = pages.length;\n if (n === 0) return null;\n const at = pages.findIndex((p) => p.id === current);\n if (at < 0) return pages[step < 0 ? n - 1 : 0] ?? null;\n const to = at + step;\n return pages[options.wrap ? ((to % n) + n) % n : to] ?? null;\n}\n\n/** The box around every page, or `null` for a document with none. */\nexport function pagesBounds(pages: readonly PlacedPage[]): Bounds | null {\n if (pages.length === 0) return null;\n const x = Math.min(...pages.map((p) => p.x));\n const y = Math.min(...pages.map((p) => p.y));\n return {\n x,\n y,\n width: Math.max(...pages.map((p) => p.x + p.width)) - x,\n height: Math.max(...pages.map((p) => p.y + p.height)) - y,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"pages.js","names":[],"sources":["../../../src/core/pages/pages.ts"],"sourcesContent":["import { unionRects } from '@weasel-js/geom';\nimport type { Bounds } from '@weasel-js/routing';\n\n/** One page of a document: a sheet of a fixed size, in world units. */\nexport interface Page {\n readonly id: string;\n readonly name?: string;\n readonly width: number;\n readonly height: number;\n /** What other packages keep on the page, each under its owner's key (`'diagram:graph'`). Every page edit\n * carries it, and the page's placement copies it. */\n readonly meta?: Readonly<Record<string, unknown>>;\n}\n\n/** A page and where it sits in world space. */\nexport interface PlacedPage extends Page {\n readonly x: number;\n readonly y: number;\n /** Its place in the document's page order. */\n readonly index: number;\n}\n\n/** Options for {@link layoutPages}. */\nexport interface PageLayoutOptions {\n /** Which way the pages run. Default `'row'`. */\n direction?: 'row' | 'column';\n /** World units between neighbors. Default 40. */\n gap?: number;\n /** Where a page sits across the run, against the largest page. Default `'start'`. */\n align?: 'start' | 'center' | 'end';\n /** Where the first page's run starts. Default the world origin. */\n origin?: { readonly x: number; readonly y: number };\n}\n\nconst ALIGN = { start: 0, center: 0.5, end: 1 } as const;\n\n/**\n * Place a document's pages in one world space, in order, so a single scene,\n * view, and history serve all of them. With the defaults the first page's\n * top-left corner is the world origin, where a one-page document's page is.\n * Throws on two pages with one id.\n */\nexport function layoutPages(pages: readonly Page[], options: PageLayoutOptions = {}): PlacedPage[] {\n const { direction = 'row', gap = 40, align = 'start', origin = { x: 0, y: 0 } } = options;\n const row = direction === 'row';\n const across = Math.max(0, ...pages.map((p) => (row ? p.height : p.width)));\n const seen = new Set<string>();\n let along = 0;\n return pages.map((page, index) => {\n if (seen.has(page.id)) throw new Error(`layoutPages: two pages have the id \"${page.id}\"`);\n seen.add(page.id);\n const offset = (across - (row ? page.height : page.width)) * ALIGN[align];\n const placed = {\n ...page,\n x: origin.x + (row ? along : offset),\n y: origin.y + (row ? offset : along),\n index,\n };\n along += (row ? page.width : page.height) + gap;\n return placed;\n });\n}\n\n/** The name page `index` of the page order shows: its own, else \"Page\" and its place. */\nexport function pageName(page: { readonly name?: string | undefined }, index: number): string {\n return page.name ?? `Page ${index + 1}`;\n}\n\n/** The page under a world point, or `null` between and outside them. A point on an edge is on the page. */\nexport function pageAt(\n pages: readonly PlacedPage[],\n point: { readonly x: number; readonly y: number },\n): PlacedPage | null {\n return pageOfBounds(pages, { x: point.x, y: point.y, width: 0, height: 0 });\n}\n\nfunction overlap(page: PlacedPage, b: Bounds): number | null {\n const w = Math.min(page.x + page.width, b.x + b.width) - Math.max(page.x, b.x);\n const h = Math.min(page.y + page.height, b.y + b.height) - Math.max(page.y, b.y);\n return w < 0 || h < 0 ? null : w * h;\n}\n\n/**\n * The page a world-space box belongs to: the one it overlaps most, the earlier\n * of two it overlaps equally, and `null` when it touches none. This is the\n * rule that says which page an object is on.\n */\nexport function pageOfBounds(pages: readonly PlacedPage[], bounds: Bounds): PlacedPage | null {\n let best: PlacedPage | null = null;\n let most = -1;\n for (const page of pages) {\n const area = overlap(page, bounds);\n if (area !== null && area > most) {\n best = page;\n most = area;\n }\n }\n return best;\n}\n\nfunction gap(page: PlacedPage, b: Bounds): number {\n const dx = Math.max(page.x - (b.x + b.width), b.x - (page.x + page.width), 0);\n const dy = Math.max(page.y - (b.y + b.height), b.y - (page.y + page.height), 0);\n return Math.hypot(dx, dy);\n}\n\n/**\n * The page a world-space box is held by: the one it is on (`pageOfBounds`),\n * else the one nearest it by the box-to-box gap, the earlier of two equally\n * near. `null` only with no pages. Used where every object must land on some\n * page, such as an export; `pageOfBounds` stays the rule for what is on one.\n */\nexport function pageHolding(pages: readonly PlacedPage[], bounds: Bounds): PlacedPage | null {\n const on = pageOfBounds(pages, bounds);\n if (on !== null) return on;\n let best: PlacedPage | null = null;\n let least = Infinity;\n for (const page of pages) {\n const d = gap(page, bounds);\n if (d < least) {\n best = page;\n least = d;\n }\n }\n return best;\n}\n\n/**\n * The page `step` places along the page order from the one with id `current`:\n * the next slide is `1`, the one before is `-1`. `null` past either end, unless\n * `wrap`. With no current page in the list, a step forward starts at the first\n * page and a step back at the last.\n */\nexport function adjacentPage(\n pages: readonly PlacedPage[],\n current: string | null,\n step: number,\n options: { wrap?: boolean } = {},\n): PlacedPage | null {\n const n = pages.length;\n if (n === 0) return null;\n const at = pages.findIndex((p) => p.id === current);\n if (at < 0) return pages[step < 0 ? n - 1 : 0] ?? null;\n const to = at + step;\n return pages[options.wrap ? ((to % n) + n) % n : to] ?? null;\n}\n\n/** The box around every page, or `null` for a document with none. */\nexport function pagesBounds(pages: readonly PlacedPage[]): Bounds | null {\n return unionRects(pages);\n}\n"],"mappings":"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"}
@@ -17,6 +17,14 @@ function parseHexBytes(input) {
17
17
  long.length === 8 ? byte(6) : 255
18
18
  ];
19
19
  }
20
+ // ../paint/src/shortArc.ts
21
+ const TWO_PI = Math.PI * 2;
22
+ /** `dh` radians as the shorter arc, in [-π, π]: a half turn keeps its sign. NaN for NaN and ±∞.
23
+ * geom's `wrapAngle(dh, 'sign')`, kept here because paint does not depend on geom. */
24
+ function shortArc(dh) {
25
+ const r = dh % TWO_PI;
26
+ return r > Math.PI ? r - TWO_PI : r < -Math.PI ? r + TWO_PI : r;
27
+ }
20
28
  // ../paint/src/colorSpaces.ts
21
29
  const SRGB_TO_LINEAR = (() => {
22
30
  const lut = /* @__PURE__ */ new Float64Array(256);
@@ -147,11 +155,7 @@ function lerpOklch(from, to, t) {
147
155
  const [L2, C2, h2Raw] = to;
148
156
  const C_EPS = 1e-4;
149
157
  const h1 = C1 < C_EPS ? h2Raw : h1Raw;
150
- const h2 = C2 < C_EPS ? h1Raw : h2Raw;
151
- const TWO_PI = Math.PI * 2;
152
- let dh = h2 - h1;
153
- while (dh > Math.PI) dh -= TWO_PI;
154
- while (dh < -Math.PI) dh += TWO_PI;
158
+ const dh = shortArc((C2 < C_EPS ? h1Raw : h2Raw) - h1);
155
159
  return [
156
160
  L1 + (L2 - L1) * t,
157
161
  C1 + (C2 - C1) * t,