@weasel-js/core 1.15.1 → 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/CHANGELOG.md +170 -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/NodeShape.js +23 -10
  12. package/dist/canvas/NodeShape.js.map +1 -1
  13. package/dist/canvas/SceneCanvas/PointerProviderIfRoot.js +17 -64
  14. package/dist/canvas/SceneCanvas/PointerProviderIfRoot.js.map +1 -1
  15. package/dist/canvas/SceneCanvas/props.js.map +1 -1
  16. package/dist/canvas/SceneCanvas/useSceneCanvasLayers.js +1 -1
  17. package/dist/canvas/SceneCanvas/useSceneCanvasLayers.js.map +1 -1
  18. package/dist/canvas/SceneCanvas/useSceneRedrawSources.js +4 -1
  19. package/dist/canvas/SceneCanvas/useSceneRedrawSources.js.map +1 -1
  20. package/dist/canvas/SceneCanvas/useSceneSlotLayers.js +3 -3
  21. package/dist/canvas/SceneCanvas/useSceneSlotLayers.js.map +1 -1
  22. package/dist/canvas/SceneCanvas.js +1 -1
  23. package/dist/canvas/SceneCanvas.js.map +1 -1
  24. package/dist/canvas/SceneViewCanvas.js +4 -1
  25. package/dist/canvas/SceneViewCanvas.js.map +1 -1
  26. package/dist/canvas/accentColor.js +3 -3
  27. package/dist/canvas/accentColor.js.map +1 -1
  28. package/dist/canvas/deps/enteredGroup.js +5 -4
  29. package/dist/canvas/deps/enteredGroup.js.map +1 -1
  30. package/dist/canvas/deps/selectionSnapshot.js +7 -2
  31. package/dist/canvas/deps/selectionSnapshot.js.map +1 -1
  32. package/dist/canvas/nodeShapes/derivedPainter.js +57 -8
  33. package/dist/canvas/nodeShapes/derivedPainter.js.map +1 -1
  34. package/dist/canvas/nodeShapes/nodePaint.js +33 -2
  35. package/dist/canvas/nodeShapes/nodePaint.js.map +1 -1
  36. package/dist/canvas/nodeShapes/pathPainter.js +5 -4
  37. package/dist/canvas/nodeShapes/pathPainter.js.map +1 -1
  38. package/dist/canvas/nodeShapes/pickOutline.js +48 -0
  39. package/dist/canvas/nodeShapes/pickOutline.js.map +1 -0
  40. package/dist/canvas/nodeShapes/shapePainter.js +6 -4
  41. package/dist/canvas/nodeShapes/shapePainter.js.map +1 -1
  42. package/dist/canvas/paintOverrides.js +43 -0
  43. package/dist/canvas/paintOverrides.js.map +1 -0
  44. package/dist/canvas/renderSceneToPixels.js +1 -1
  45. package/dist/canvas/renderSceneToPixels.js.map +1 -1
  46. package/dist/canvas/sceneSlotWiring.js +3 -3
  47. package/dist/canvas/sceneSlotWiring.js.map +1 -1
  48. package/dist/canvas/sceneViewRender.js +11 -7
  49. package/dist/canvas/sceneViewRender.js.map +1 -1
  50. package/dist/canvas/slopsDebugLayer.js +8 -2
  51. package/dist/canvas/slopsDebugLayer.js.map +1 -1
  52. package/dist/canvas/trimOverrides.js +18 -0
  53. package/dist/canvas/trimOverrides.js.map +1 -0
  54. package/dist/canvas/useViewRegistrySurface.js +6 -3
  55. package/dist/canvas/useViewRegistrySurface.js.map +1 -1
  56. package/dist/canvas/viewRegistry.js +5 -5
  57. package/dist/canvas/viewRegistry.js.map +1 -1
  58. package/dist/core/ops/reorder/index.js +2 -6
  59. package/dist/core/ops/reorder/index.js.map +1 -1
  60. package/dist/core/pages/fitViewToPage.js +2 -7
  61. package/dist/core/pages/fitViewToPage.js.map +1 -1
  62. package/dist/core/pages/pages.js +3 -9
  63. package/dist/core/pages/pages.js.map +1 -1
  64. package/dist/core/paint/meshKindLazy.js +13 -6
  65. package/dist/core/paint/meshKindLazy.js.map +1 -1
  66. package/dist/core/scene/pathEffects/blend.js +2 -2
  67. package/dist/core/scene/pathEffects/blend.js.map +1 -1
  68. package/dist/core/scene/pathEffects/textAlong.js +6 -5
  69. package/dist/core/scene/pathEffects/textAlong.js.map +1 -1
  70. package/dist/core/scene/scene.js +12 -1
  71. package/dist/core/scene/scene.js.map +1 -1
  72. package/dist/core/viewport/clientToWorld.js +7 -1
  73. package/dist/core/viewport/clientToWorld.js.map +1 -1
  74. package/dist/core/viewport/zoomAt.js +6 -6
  75. package/dist/core/viewport/zoomAt.js.map +1 -1
  76. package/dist/devtools/root.js +11 -4
  77. package/dist/devtools/root.js.map +1 -1
  78. package/dist/devtools/transport.js +12 -16
  79. package/dist/devtools/transport.js.map +1 -1
  80. package/dist/devtools-protocol.d.ts +0 -11
  81. package/dist/domains/measure/measureHit.js +2 -8
  82. package/dist/domains/measure/measureHit.js.map +1 -1
  83. package/dist/domains/paths/anchorMarkers.js +5 -11
  84. package/dist/domains/paths/anchorMarkers.js.map +1 -1
  85. package/dist/domains/paths/pathEditingOverlayLayer.js +5 -9
  86. package/dist/domains/paths/pathEditingOverlayLayer.js.map +1 -1
  87. package/dist/domains/paths/pathLayer.js +20 -6
  88. package/dist/domains/paths/pathLayer.js.map +1 -1
  89. package/dist/domains/paths/penPreviewLayer.js +9 -12
  90. package/dist/domains/paths/penPreviewLayer.js.map +1 -1
  91. package/dist/domains/paths/shapeGenerator.js +1 -2
  92. package/dist/domains/paths/shapeGenerator.js.map +1 -1
  93. package/dist/domains/snap/angle2d.js +2 -1
  94. package/dist/domains/snap/angle2d.js.map +1 -1
  95. package/dist/domains/snap/outcomeStore.js +5 -8
  96. package/dist/domains/snap/outcomeStore.js.map +1 -1
  97. package/dist/domains/text/useSceneTextEdit.js +9 -4
  98. package/dist/domains/text/useSceneTextEdit.js.map +1 -1
  99. package/dist/domains/viewports/aim.js +3 -8
  100. package/dist/domains/viewports/aim.js.map +1 -1
  101. package/dist/domains/viewports/viewResolver.js +5 -3
  102. package/dist/domains/viewports/viewResolver.js.map +1 -1
  103. package/dist/index.d.ts +66 -50
  104. package/dist/index.js +5 -4
  105. package/dist/input/usePublishPointer.js +59 -0
  106. package/dist/input/usePublishPointer.js.map +1 -0
  107. package/dist/interactions/actions/defaults/containerScroll.js +9 -3
  108. package/dist/interactions/actions/defaults/containerScroll.js.map +1 -1
  109. package/dist/interactions/actions/defaults/enterGroup.js +3 -3
  110. package/dist/interactions/actions/defaults/enterGroup.js.map +1 -1
  111. package/dist/interactions/actions/defaults/pathEditMarks.js +2 -5
  112. package/dist/interactions/actions/defaults/pathEditMarks.js.map +1 -1
  113. package/dist/interactions/actions/edit-anchors/handleDrag.js +4 -7
  114. package/dist/interactions/actions/edit-anchors/handleDrag.js.map +1 -1
  115. package/dist/interactions/actions/move/behaviors/clampMove.js +42 -0
  116. package/dist/interactions/actions/move/behaviors/clampMove.js.map +1 -0
  117. package/dist/interactions/actions/move/layout/container.js +3 -6
  118. package/dist/interactions/actions/move/layout/container.js.map +1 -1
  119. package/dist/interactions/actions/move/layout/exits.js +2 -2
  120. package/dist/interactions/actions/move/layout/exits.js.map +1 -1
  121. package/dist/interactions/actions/transform-again/memory.js +2 -2
  122. package/dist/interactions/actions/transform-again/memory.js.map +1 -1
  123. package/dist/move.d.ts +18 -3
  124. package/dist/move.js +2 -1
  125. package/dist/version.js +1 -1
  126. package/package.json +25 -25
  127. package/dist/canvas/colorOverrides.js +0 -23
  128. package/dist/canvas/colorOverrides.js.map +0 -1
@@ -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":";;;;;;;;;;;;;;;;;;;;;;;;;;AAyEA,SAAgB,aACd,SACA,SACA,SAC0B;CAC1B,IAAI,CAAC,SAAS,OAAO;EAAE,GAAG;EAAS,GAAG;CAAQ;CAC9C,MAAM,CAAC,QAAQ,UAAU,QAAQ,cAAc,SAAS,OAAO;CAC/D,OAAO,kBAAkB,QAAQ,KAAK,GAAG;EAAE,GAAG;EAAQ,GAAG;CAAO,CAAC;AACnE;AA8EA,IAAM,sBAAsB,cAAmC,IAAI;;;AAInE,SAAgB,qBAAqB,EAAE,YAAqC;CAC1E,MAAM,aAAa,uBAAO,IAAI,IAA8B,CAAC;CAC7D,MAAM,SAAS,OAAO,CAAC;CACvB,MAAM,aAAa,OAAO,CAAC;CAC3B,MAAM,aAAa,OAA6B,IAAI;CACpD,MAAM,aAAa,uBAAO,IAAI,IAAiD,CAAC;CAEhF,MAAM,WAAW,cAA4B;EAC3C,MAAM,YAAY,cAAc;EAChC,MAAM,gBAAsB;GAC1B,WAAW;GACX,UAAU,KAAK;EACjB;EACA,MAAM,gBAAoC,YAAY,CAAC,GAAG,WAAW,QAAQ,KAAK,CAAC,CAAC,CAAC,MAAM,GAAG,MAC5F,EAAE,QAAQ,EAAE,SAAS,WAAW,QAAQ,IAAI,CAAC,IAAK,WAAW,QAAQ,IAAI,CAAC,CAC3E,CAAC;EACF,MAAM,OAAO,OAA6C;GACxD,KAAK,MAAM,KAAK,WAAW,QAAQ,KAAK,GAAG,IAAI,EAAE,OAAO,IAAI,OAAO;EAErE;;EAEA,MAAM,UAAU,GAAqB,OAAwB;GAC3D,MAAM,uBAAO,IAAI,IAAsB;GACvC,KAAK,IAAI,KAAmC,GAAG,MAAM,CAAC,KAAK,IAAI,EAAE,GAAG,KAAK,GAAG,WAAW,SAAY,SAAY,IAAI,GAAG,MAAM,GAAG;IAC7H,IAAI,GAAG,OAAO,IAAI,OAAO;IACzB,KAAK,IAAI,EAAE;GACb;GACA,OAAO;EACT;EAYA,MAAM,WAAyB;GAC7B,UAZe,mBAAmB;IAClC,aAAa;KACX,MAAM,UAAU,WAAW;KAC3B,MAAM,OAAO,SAAS,KAAK,KAAK;KAChC,MAAM,OAAO,SAAS,KAAK,KAAK;KAChC,OAAO,QAAQ,CAAC,CAAC,QAAQ,MAAM,EAAE,WAAW,CAAC,CAAC,KAAK,MAAM,EAAE,MAAM,WAAW,MAAM,IAAI,CAAC;IACzF;IACA,YAAY,WAAW,SAAS,KAAK,KAAK;IAC1C,UAAU,SAAS,YAAY,aAAa,WAAW,SAAS,SAAS,OAAO;IAChF,WAAW,GAAG,MAAM,WAAW,SAAS,YAAY,GAAG,CAAC,KAAK;GAC/D,CAEE;GACA,WAAW,QAAQ;IACjB,WAAW,QAAQ,IAAI,KAAK,OAAO,SAAS;IAC5C,QAAQ;IACR,aAAa;KAAE,WAAW,QAAQ,OAAO,GAAG;KAAG,QAAQ;IAAG;GAC5D;GACA,MAAM;GACN;GACA,SAAS,EAAE,GAAG,KAAK,SAAS;IAC1B,MAAM,UAAU,WAAW;IAC3B,MAAM,OAAO,SAAS,KAAK,KAAK;IAChC,MAAM,OAAO,SAAS,KAAK,KAAK;IAChC,MAAM,SAAS,MAAM;IACrB,MAAM,OAAO,QAAQ;IACrB,KAAK,IAAI,IAAI,KAAK,SAAS,GAAG,KAAK,GAAG,KAAK;KACzC,MAAM,IAAI,KAAK;KACf,IAAI,WAAW,UAAa,OAAO,GAAG,MAAM,GAAG;KAC/C,MAAM,IAAI,EAAE,MAAM,WAAW,MAAM,IAAI;KACvC,IAAI,aAAa,GAAG,GAAG,CAAC,GAAG,OAAO;MAAE,IAAI,EAAE;MAAI,MAAM,EAAE;MAAM,MAAM,EAAE;KAAK;IAC3E;IACA,OAAO;KAAE,IAAI;KAAM;KAAM,MAAM;MAAE,GAAG;MAAG,GAAG;MAAG,GAAG,KAAK;MAAO,GAAG,KAAK;KAAO;IAAE;GAC/E;GACA,QAAQ,OAAO;IACb,IAAI,OAAO,MAAM,OAAO,IAAI,EAAE,CAAC,EAAE,IAAI,KAAK;IAC1C,MAAM,UAAU,WAAW;IAC3B,OAAO,UAAU,QAAQ,MAAM,kBAAkB,QAAQ,KAAK,GAAG,QAAQ,KAAK,CAAC,CAAC,IAAI;GACtF;GACA,SAAS,IAAI,QAAQ;IACnB,IAAI,OAAO,MAAM,OAAO,IAAI,EAAE,CAAC,EAAE,OAAO,GAAG,KAAK;IAChD,MAAM,UAAU,WAAW;IAC3B,IAAI,CAAC,UAAU,GAAG,KAAK,CAAC,SAAS,SAAS,OAAO;IACjD,QAAQ,QAAQ,WAAW,IAAI,QAAQ,QAAQ,KAAK,GAAG,OAAO,CAAC;IAC/D,OAAO;GACT;GACA,GAAG,yBAAyB,WAAW,SAAS;IAC9C,MAAM;KAAE,QAAQ,OAAO,SAAS,MAAM,EAAE;KAAG,SAAS,IAAI,QAAQ,SAAS,OAAO,IAAI,GAAG;IAAE;IACzF,eAAe,WAAW;IAC1B,UAAU,IAAI,OAAO,SAAS,IAAI,EAAE,CAAC,EAAE,MAAM,WAAW,OAAO,IAAI;IACnE;GACF,CAAC;GACD,kBAAkB,WAAW;GAC7B,WAAW,UAAU;GACrB,eAAe,WAAW;GAC1B,gBAAgB,WAAW;IACzB,WAAW,UAAU;IACrB,aAAa;KAAE,IAAI,WAAW,YAAY,QAAQ,WAAW,UAAU;IAAM;GAC/E;EACF;EACA,OAAO;CACT,GAAG,CAAC,CAAC;CAEL,OAAO,oBAAC,oBAAoB,UAArB;EAA8B,OAAO;EAAW;CAAuC,CAAA;AAChG;;AAGA,SAAgB,0BAA+C;CAC7D,OAAO,WAAW,mBAAmB;AACvC;;;;AAKA,SAAS,YAAY,QAAyD;CAC5E,MAAM,MAAM,IAAI,IAAI,OAAO,KAAK,MAAM,EAAE,EAAE,CAAC;CAC3C,MAAM,MAA0B,CAAC;CACjC,MAAM,yBAAS,IAAI,IAAsB;CACzC,MAAM,SAAS,MAA8B;EAC3C,IAAI,OAAO,IAAI,CAAC,GAAG;EACnB,OAAO,IAAI,CAAC;EACZ,IAAI,KAAK,CAAC;EACV,KAAK,MAAM,SAAS,QAAQ,IAAI,MAAM,WAAW,EAAE,IAAI,MAAM,KAAK;CACpE;CACA,KAAK,MAAM,KAAK,QAAQ,IAAI,EAAE,WAAW,UAAa,CAAC,IAAI,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;CACjF,KAAK,MAAM,KAAK,QAAQ,MAAM,CAAC;CAC/B,OAAO;AACT;;AAGA,IAAa,kBAAwB;CAAE,OAAO;CAAG,QAAQ;AAAE;;AAG3D,IAAa,gBAAsB;CAAE,GAAG;CAAG,GAAG;CAAG,OAAO;EAAE,GAAG;EAAG,GAAG;CAAE;AAAE"}
@@ -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":";;;;;;;;;;;;;;;;;AAmDA,SAAS,2BAA2B,MAM7B;CACL,MAAM,EAAE,KAAK,WAAW,IAAI,OAAO,eAAe;CAClD,MAAM,gBAA6B;EACjC;EACA;EACA;EACA,YAAY,aAAa,WAAW,KAAK,OAAO;GAAE,GAAG;GAAG,QAAQ,EAAE,OAAO,MAAM;EAAE,EAAE,IAAI;CACzF;CACA,IAAI,UACF,aAAa,WAAW,KAAK,OAAO;EAAE,GAAG;EAAG,QAAQ,EAAE,OAAO,MAAM;CAAE,EAAE,IAAI;CAE7E,OAAO;EACL,MAAM;EACN,MAAM;EACN;EACA,MAAM,SAAS;GACb,MAAM,IAAI;GAEV,MAAM,2BAAW,IAAI,IAA6B;GAClD,KAAK,MAAM,MAAM,KAAK;IACpB,MAAM,SAAS,EAAE,UAAU,EAAE;IAC7B,MAAM,OAAO,SAAS,IAAI,MAAM,KAAK,CAAC;IACtC,KAAK,KAAK,EAAE;IACZ,SAAS,IAAI,QAAQ,IAAI;GAC3B;GACA,MAAM,YAAmC,CAAC;GAC1C,IAAI,UAAU;GACd,KAAK,MAAM,CAAC,UAAU,cAAc,UAAU;IAC5C,MAAM,SAAS,EAAE,YAAY,QAAQ;IACrC,MAAM,QAAQ,GAAG,QAAQ,SAAS;IAClC,IAAI,SAAS,QAAQ,KAAK,GAAG;IAC7B,UAAU,KAAK;KAAE;KAAU,QAAQ,OAAO,MAAM;IAAE,CAAC;IACnD,EAAE,cAAc,UAAU,KAAK;IAC/B,UAAU;GACZ;GACA,UAAU;GACV,cAAc,aAAa,UAAU,KAAK,OAAO;IAAE,GAAG;IAAG,QAAQ,EAAE,OAAO,MAAM;GAAE,EAAE;GACpF,OAAO;EACT;EACA,SAAS;GACP,MAAM,WAAW;GACjB,OAAO;IACL,GAAG,sBAAsB;KACvB,SAAS,YAAY,CAAC,EAAA,CAAG,KAAK,OAAO;MAAE,UAAU,EAAE;MAAU,KAAK,EAAE;KAAO,EAAE;KAC7E;IACF,CAAC;IACD,cACE,2BAA2B;KACzB;KAAK;KAAW;KAAI;KAAO,YAAY,YAAY;IACrD,CAAC;GACL;EACF;CACF;AACF;AAKA,IAAM,qBAAwF;CAC5F,SAAU;EAAE,IAAI;EAAe,cAAc;CAAgB;CAC7D,UAAU;EAAE,IAAI;EAAe,cAAc;CAAgB;CAC7D,OAAU;EAAE,IAAI;EAAe,cAAc;CAAiB;CAC9D,MAAU;EAAE,IAAI;EAAe,cAAc;CAAe;AAC9D;;;;;;;;AASA,SAAgB,gBAAgB,MAAuB;CACrD,MAAM,EAAE,KAAK,WAAW,OAAO,eAAe;CAC9C,MAAM,EAAE,IAAI,iBAAiB,mBAAmB;CAChD,OAAO,2BAA2B;EAChC;EAAK;EAAW;EAAI,OAAO,SAAS;EAAc;CACpD,CAAC;AACH;AAEA,kBAA+B,YAAY,SAAS,gBAAgB,IAAI,CAAC;;AAgBzE,SAAgB,oBAAoB,MAA2B;CAC7D,MAAM,EAAE,KAAK,UAAU,OAAO,UAAU;CAIxC,MAAM,gBAAiC;EACrC;EACA;EACA;EACA;EACA,eAAe,KAAK,gBAAgB,KAAK,cAAc,MAAM,IAAI;CACnE;CACA,IAAI,SAA0B,KAAK,gBAAgB,KAAK,cAAc,MAAM,IAAI;CAEhF,OAAO;EACL,MAAM;EACN,MAAM;EACN,OAAO,SAAS;EAChB,MAAM,SAAS;GACb,MAAM,IAAI;GAEV,MAAM,WAAW,IAAI,QAAQ,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ;GAChE,MAAM,UAAU,EAAE,YAAY,QAAQ;GAItC,IAAI,WAAW,MAAM;IACnB,SAAS,QAAQ,MAAM;IACvB,cAAc,gBAAgB,OAAO,MAAM;GAC7C;GACA,MAAM,QAAQ,YAAY,SAAS,UAAU,KAAK;GAClD,EAAE,cAAc,UAAU,KAAK;EACjC;EACA,SAAS;GACP,MAAM,WAAW;GACjB,OAAO;IACL,GAAG,sBAAsB;KAAE,QAAQ,WAAW,CAAC;MAAE;MAAU,KAAK;KAAS,CAAC,IAAI,CAAC;KAAG;IAAM,CAAC;IACzF,cACE,oBAAoB;KAClB;KAAK;KAAU;KAAO;KAAO,eAAe,YAAY;IAC1D,CAAC;GACL;EACF;CACF;AACF;AAEA,kBAAmC,gBAAgB,SAAS,oBAAoB,IAAI,CAAC"}
@@ -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,
@@ -645,6 +649,9 @@ const NAMED_COLORS = (() => {
645
649
  }
646
650
  return out;
647
651
  })();
652
+ // ../geom/src/angle.ts
653
+ /** Multiply degrees by this to get radians. */
654
+ const DEG_TO_RAD = Math.PI / 180;
648
655
  // ../geom/src/affine.ts
649
656
  /**
650
657
  * 2D affine transforms in canvas/DOMMatrix order: [a, b, c, d, e, f].
@@ -1214,7 +1221,7 @@ function shifted(color, dL, cScale, dHueDeg) {
1214
1221
  const [r, g, b, a] = resolveColor(color);
1215
1222
  const [L, A, B] = srgbFloatToOklab(r, g, b);
1216
1223
  const [, C, h] = oklabToOklch(L, A, B);
1217
- const [nL, nA, nB] = oklchToOklab(Math.min(1, Math.max(0, L + dL)), Math.max(0, C * cScale), h + dHueDeg * Math.PI / 180);
1224
+ const [nL, nA, nB] = oklchToOklab(Math.min(1, Math.max(0, L + dL)), Math.max(0, C * cScale), h + dHueDeg * DEG_TO_RAD);
1218
1225
  const [nr, ng, nb] = oklabToSrgbU8(nL, nA, nB);
1219
1226
  return rgbaToHex([
1220
1227
  nr / 255,