@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
@@ -1 +1 @@
1
- {"version":3,"file":"useSceneTextEdit.js","names":[],"sources":["../../../src/domains/text/useSceneTextEdit.ts"],"sourcesContent":["/**\n * `useSceneTextEdit` — scene-aware wrapper around `useTextEdit`: scene\n * lookups for text / style / runs / screen-pose, and\n * `scene.update(id, { data })` for commits. The overlay and a double-click\n * both read the pose a node is painted at, so text inside a container whose\n * pose is a frame is edited where it is drawn.\n *\n * Defaults assume the node's `data` carries `text` / `style` / `runs`\n * directly. For custom data shapes, override any of the optional projections.\n * Consumers with non-rect poses should drop down to raw `useTextEdit` and\n * supply their own `getScreenPose`.\n */\nimport { useCallback, useEffect, useMemo, useRef } from 'react';\nimport { useLatest } from '@weasel-js/react';\nimport { asNodeId } from '../../core/scene/types';\nimport { scenePaintedPoseOf } from '../../canvas/derivedPath';\nimport type { Scene } from '../../core/scene/types';\nimport { drawOneEditing, type EditingDrawOne } from './editingDrawOne';\nimport { textHitAt } from './textHitAt';\nimport { registerHostTextEditor } from './hostTextEditors';\nimport { editedData, sceneTextDrafts } from './sceneTextDrafts';\nimport type { Stroke, Fill, Inherit } from '@weasel-js/paint';\nimport { resolveScreenLength } from '@weasel-js/paint';\nimport { inheritedPaintIn } from '../../canvas/inheritedPaints';\nimport { withInheritedPaint } from '../../core/scene/inheritedPaint';\nimport { clientToCanvas } from '@weasel-js/routing';\nimport { findMountedCanvas, type MountedCanvas } from '../../canvas/mountedCanvases';\nimport type { View } from '../../core/viewport/view';\nimport type { RectPose } from 'core/geometry/unionBounds';\nimport { poseFrameOf } from '@weasel-js/geom';\nimport { isTextData, type StyledRun, type TextPaint, type TextStyle, type TextVerticalAlign } from '@weasel-js/text';\nimport { textBoxOf, type TextOverflow } from '../../canvas/nodeShapes/textPainter';\nimport { useTextEdit, type TextEditClipRect, type TextEditScreenPose, type UseTextEditReturn } from '@weasel-js/text-dom';\n\n/** Shape the default projections expect `data` to satisfy. All fields\n * optional so any object satisfies the constraint — projections fill in\n * the gaps for non-default shapes. */\nexport interface DefaultTextData {\n text?: string;\n style?: TextStyle;\n runs?: readonly StyledRun[];\n fill?: Fill | Inherit | null;\n stroke?: Stroke | Inherit | null;\n verticalAlign?: TextVerticalAlign;\n padding?: number;\n overflow?: TextOverflow;\n}\n\n/** All-optional projections + fontSize fallback. */\nexport interface UseSceneTextEditOptions<TData> {\n /**\n * Is this node text? A double-click opens an edit only on a node that\n * answers yes, and passes through any other to the text under it. Default:\n * `isTextData`, the test `inferredNodeRouting` classifies a text node by. Pass one when `getText` reads some other shape.\n */\n isText?: (data: TData) => boolean;\n /** Read plain text from `data`. Default: `data.text ?? ''`. */\n getText?: (data: TData) => string;\n /** Read style from `data`. Default: `data.style`. */\n getStyle?: (data: TData) => TextStyle | undefined;\n /** Read the node's paint from `data`. Default: `data.fill` / `data.stroke`. */\n getPaint?: (data: TData) => TextPaint | undefined;\n /** Read rich-text runs from `data`. Default: `data.runs`. */\n getRuns?: (data: TData) => readonly StyledRun[] | undefined;\n /** Read the box vertical alignment from `data`. Default: `data.verticalAlign`. */\n getVerticalAlign?: (data: TData) => TextVerticalAlign | undefined;\n /** Read the inset from the node's box to its text from `data`. Default: `data.padding`. */\n getPadding?: (data: TData) => number | undefined;\n /** Read whether the node cuts its text at its box from `data`, which the\n * overlay then does too. Default: `data.overflow`. */\n getOverflow?: (data: TData) => TextOverflow | undefined;\n /** Produce updated data with new text. Default: `{ ...data, text }`. */\n setText?: (data: TData, text: string) => TData;\n /** Produce updated data with new runs. Default: `{ ...data, runs }`. */\n setRuns?: (data: TData, runs: StyledRun[]) => TData;\n /**\n * Produce updated data with a new node-wide style. Default:\n * `{ ...data, style }`. Written only when a range toggle has to *clear* a\n * flag the node itself sets — the run algebra is additive, so that edit\n * lowers the node flag and raises it on the runs outside the range. See\n * `useTextEdit`'s `setStyle`.\n *\n * An edit's text, runs, and style are folded through these three and\n * written as one `scene.update`, so a commit is one undo step.\n */\n setStyle?: (data: TData, style: TextStyle) => TData;\n /** Fallback fontSize when `style.fontSize` is unset. Default `16`. */\n defaultFontSize?: number;\n /**\n * The view to project through. Omit it and the view is read live from the\n * weasel canvas mounted inside `container` (the one a double-click landed\n * on, when there are several). With no canvas there, the node's world box\n * is passed through as screen pixels.\n *\n * A thunk is re-read on every projection; a plain `View` is the value from\n * the render that supplied it.\n */\n view?: View | (() => View);\n /**\n * Forwarded to `useTextEdit`: is `el` part of the editor's own chrome?\n * Focus moving into it does not end the edit. Wire it to whatever renders\n * the character controls.\n */\n isEditorChrome?: (el: Element) => boolean;\n /** Forwarded to `useTextEdit`: what Escape does to the edit. */\n escape?: 'cancel' | 'commit';\n /** Forwarded to `useTextEdit`: how long an edit waits for its face. */\n fontHold?: number;\n /** Forwarded to `useTextEdit`: called as an edit of the node `id` opens,\n * whichever way it was started. */\n onStart?: (id: string) => void;\n /** Forwarded to `useTextEdit`: called as the edit of `id` closes, after\n * any commit has been written to the scene. */\n onEnd?: (id: string) => void;\n}\n\nfunction resolveView(view: View | (() => View) | undefined): View | undefined {\n return typeof view === 'function' ? view() : view;\n}\n\n/** What `onDoubleClick` reads of the event: a React or a DOM `MouseEvent`. */\nexport type TextEditDoubleClick = Pick<MouseEvent, 'target' | 'clientX' | 'clientY'>;\n\n/** Return shape extends `UseTextEditReturn` with an `onDoubleClick`\n * binding for the canvas container. Double-clicking inside a text\n * node enters edit mode with the caret seeded at the clicked glyph. */\nexport interface UseSceneTextEditReturn extends UseTextEditReturn {\n /** Bind to the element wrapping `<SceneCanvas>` (the same element\n * passed as `container`, or any ancestor that receives the dblclick\n * bubble). Starts an edit on the top-most text node under the cursor. */\n onDoubleClick: (e: TextEditDoubleClick) => void;\n /** The `isText` option asked of the node with this id: what\n * `TextEditDep.isTextNode` wants, so both ways into an edit agree. */\n isTextNode: (id: string) => boolean;\n /**\n * `defaultDrawOne`, less the glyphs of the node being edited: the overlay\n * stands in for its text, and the rest of the node, a `background` among\n * it, goes on painting. Pass it as `layers.scene.drawOne`, with\n * `paintBounds: defaultPaintBounds`, in place of an `alphaFor` that hides\n * the whole node.\n */\n drawOne: EditingDrawOne;\n}\n\n/** How each field is read from a node's `data`: the option's projection, else the default shape. */\nfunction projectionsOf<TData extends DefaultTextData>(opts: { readonly current: UseSceneTextEditOptions<TData> }) {\n return {\n isText: (data: TData): boolean =>\n opts.current.isText ? opts.current.isText(data) : isTextData(data),\n text: (data: TData): string =>\n opts.current.getText ? opts.current.getText(data) : (data.text ?? ''),\n style: (data: TData): TextStyle | undefined =>\n opts.current.getStyle ? opts.current.getStyle(data) : data.style,\n runs: (data: TData): readonly StyledRun[] | undefined =>\n opts.current.getRuns ? opts.current.getRuns(data) : data.runs,\n verticalAlign: (data: TData): TextVerticalAlign | undefined =>\n opts.current.getVerticalAlign ? opts.current.getVerticalAlign(data) : data.verticalAlign,\n padding: (data: TData): number | undefined =>\n opts.current.getPadding ? opts.current.getPadding(data) : data.padding,\n overflow: (data: TData): TextOverflow | undefined =>\n opts.current.getOverflow ? opts.current.getOverflow(data) : data.overflow,\n };\n}\n\n/** Wire double-click-to-edit text on a scene: mounts a DOM editing overlay\n * over the node being edited and writes the result back as an undoable op.\n * `<SceneCanvas>`'s own editor stands down for any canvas inside `container`. */\nexport function useSceneTextEdit<\n TData extends DefaultTextData,\n TLayer extends string,\n TPose extends RectPose = RectPose,\n>(\n scene: Scene<TData, TLayer, TPose>,\n container: HTMLElement | null,\n options: UseSceneTextEditOptions<TData> = {},\n): UseSceneTextEditReturn {\n useEffect(() => (container ? registerHostTextEditor(container) : undefined), [container]);\n return useSceneTextEditor(scene, container, options);\n}\n\n/** `useSceneTextEdit` without announcing itself as a host's editor: what\n * `<SceneCanvas>` mounts as its own. */\nexport function useSceneTextEditor<\n TData extends DefaultTextData,\n TLayer extends string,\n TPose extends RectPose = RectPose,\n>(\n scene: Scene<TData, TLayer, TPose>,\n container: HTMLElement | null,\n options: UseSceneTextEditOptions<TData> = {},\n): UseSceneTextEditReturn {\n // Refs, so the useTextEdit callbacks always see the current values\n // without re-binding.\n const sceneRef = useLatest(scene);\n const optsRef = useLatest(options);\n const containerRef = useLatest(container);\n // The last double-click's target, so the overlay projects through the\n // canvas the edit was opened on.\n const openedOnRef = useRef<EventTarget | null>(null);\n const drafts = useMemo(() => sceneTextDrafts(sceneRef, optsRef), [sceneRef, optsRef]);\n useEffect(() => () => drafts.close(), [drafts]);\n\n // The canvas's box in container pixels is both the origin its view projects\n // from and the region its glyphs are clipped to.\n const surface = (): { canvas: MountedCanvas; box: TextEditClipRect } | null => {\n const root = containerRef.current;\n const canvas = root ? findMountedCanvas(root, openedOnRef.current) : null;\n if (!root || !canvas) return null;\n const a = canvas.element.getBoundingClientRect();\n const b = root.getBoundingClientRect();\n return {\n canvas,\n box: {\n x: a.left - b.left - root.clientLeft + root.scrollLeft,\n y: a.top - b.top - root.clientTop + root.scrollTop,\n width: a.width,\n height: a.height,\n },\n };\n };\n\n const read = useMemo(() => projectionsOf(optsRef), [optsRef]);\n\n const edit = useTextEdit({\n container,\n isEditorChrome: (el) => optsRef.current.isEditorChrome?.(el) ?? false,\n escape: options.escape,\n fontHold: options.fontHold,\n onStart: (id) => optsRef.current.onStart?.(id),\n onEnd: (id) => {\n drafts.end(id);\n optsRef.current.onEnd?.(id);\n },\n onDraft: drafts.draft,\n getText: (id) => {\n const node = sceneRef.current.get(asNodeId(id));\n return node ? read.text(node.data) : '';\n },\n getStyle: (id) => {\n const node = sceneRef.current.get(asNodeId(id));\n return node ? read.style(node.data) : undefined;\n },\n getPaint: (id) => {\n const node = sceneRef.current.get(asNodeId(id));\n if (!node) return undefined;\n const get = optsRef.current.getPaint;\n return get ? get(node.data) : shownTextPaint(sceneRef.current, node);\n },\n getScreenPose: (id) => {\n const scene = sceneRef.current;\n const node = scene.get(asNodeId(id));\n if (!node) return null;\n const at = surface();\n const view = resolveView(optsRef.current.view) ?? at?.canvas.getView();\n return screenPoseOf(node.data, scenePaintedPoseOf(scene)(node), read, view, at?.box, optsRef.current.defaultFontSize);\n },\n getClipRect: () => surface()?.box ?? null,\n getRuns: (id) => {\n const node = sceneRef.current.get(asNodeId(id));\n return node ? read.runs(node.data) : undefined;\n },\n commit: (id, { text, runs, style }) => {\n const nid = asNodeId(id);\n const node = sceneRef.current.get(nid);\n if (!node) return;\n drafts.commit(id, editedData(node.data, { text, runs, style }, optsRef.current));\n },\n });\n\n const onDoubleClick = useCallback((e: TextEditDoubleClick) => {\n // The dblclick bubbles from the canvas up to any wrapping container; the\n // element it started on is what the click coordinates are relative to.\n const canvas = e.target instanceof HTMLElement ? e.target : null;\n if (!canvas) return;\n // Inside the editor a double-click selects a word, and its target is a run\n // span whose box the click would be measured against as if it were a canvas.\n if (canvas.closest('[contenteditable=\"true\"]')) return;\n\n // The click is un-projected here; `textHitAt` takes it back through the\n // frame the node is drawn in.\n const [canvasX, canvasY] = clientToCanvas(canvas, e.clientX, e.clientY);\n const root = containerRef.current;\n const view = resolveView(optsRef.current.view)\n ?? (root ? findMountedCanvas(root, canvas)?.getView() : undefined);\n const cx = view ? canvasX / view.scale.x + view.x : canvasX;\n const cy = view ? canvasY / view.scale.y + view.y : canvasY;\n const hit = textHitAt(sceneRef.current, read, cx, cy);\n if (!hit) return;\n openedOnRef.current = canvas;\n edit.startEdit(hit.id, { caret: hit.caret });\n }, [edit, containerRef, optsRef, read, sceneRef]);\n\n const isTextNode = useCallback((id: string): boolean => {\n const node = sceneRef.current.get(asNodeId(id));\n return node !== undefined && read.isText(node.data);\n }, [read, sceneRef]);\n\n const drawOne = useMemo(() => drawOneEditing(edit.editingId), [edit.editingId]);\n return { ...edit, onDoubleClick, isTextNode, drawOne };\n}\n\n/** The overlay's pose for a node painted at `painted`: its text box, inset\n * about the box center, which is all the overlay's frame needs, projected\n * through `view`. */\nfunction screenPoseOf<TData extends DefaultTextData>(\n d: TData,\n painted: RectPose,\n read: ReturnType<typeof projectionsOf<TData>>,\n view: View | undefined,\n origin: TextEditClipRect | undefined,\n defaultFontSize = 16,\n): TextEditScreenPose {\n const inset = textBoxOf({ padding: read.padding(d) }, painted);\n const box = screenBoxOf({ ...painted, ...inset }, view, origin);\n return {\n ...box,\n // Against `zoom`, the same factor the overlay scales back up by, so a\n // `{ px }` size arrives on screen at the pixel count it names.\n fontSize: resolveScreenLength(read.style(d)?.fontSize ?? defaultFontSize, box.zoom),\n verticalAlign: read.verticalAlign(d),\n ...(read.overflow(d) === 'clip' ? { clip: clipToBox(inset, painted) } : {}),\n };\n}\n\n/** How far `box` reaches past `inner` on each side: the overlay's cut at\n * `box` when its own pose is `inner`. */\nfunction clipToBox(inner: RectPose, box: RectPose): NonNullable<TextEditScreenPose['clip']> {\n return {\n top: inner.y - box.y,\n left: inner.x - box.x,\n bottom: box.y + box.height - (inner.y + inner.height),\n right: box.x + box.width - (inner.x + inner.width),\n };\n}\n\n/**\n * `pose`'s box as the overlay takes it, `origin` being the canvas's corner in\n * container pixels. Only the box center is projected, since the overlay's\n * frame is about it. Width and height stay in world units and reach the\n * screen through the overlay's own `scale(zoom)` — see\n * `TextEditScreenPose.zoom`.\n */\nfunction screenBoxOf(\n pose: RectPose,\n view: View | undefined,\n origin: { x: number; y: number } | undefined,\n): Pick<TextEditScreenPose, 'x' | 'y' | 'width' | 'height' | 'frame'> & { zoom: number } {\n const zoom = view ? view.scale.x : 1;\n const zoomY = view ? view.scale.y : 1;\n const cx = (origin?.x ?? 0) + (pose.x + pose.width / 2 - (view?.x ?? 0)) * zoom;\n const cy = (origin?.y ?? 0) + (pose.y + pose.height / 2 - (view?.y ?? 0)) * zoomY;\n return {\n x: cx - (pose.width * zoom) / 2,\n y: cy - (pose.height * zoom) / 2,\n width: pose.width,\n height: pose.height,\n zoom,\n frame: frameLinearOf(pose, zoomY / zoom),\n };\n}\n\n/** The linear part of the frame `pose` is drawn in, as the overlay takes it.\n * `stretch` is the view's y scale over its x scale: the overlay scales both\n * axes by the x scale, so the frame carries what is left of y. */\nfunction frameLinearOf(pose: unknown, stretch: number): TextEditScreenPose['frame'] {\n const m = poseFrameOf(pose);\n if (!m && stretch === 1) return undefined;\n const [a, b, c, d] = m ?? [1, 0, 0, 1];\n return [a, b * stretch, c, d * stretch];\n}\n\n/** A text node's paint as the canvas paints it: an inherited one is its container's. */\nfunction shownTextPaint<TData extends DefaultTextData>(\n scene: Scene<TData, string, unknown>,\n node: NonNullable<ReturnType<Scene<TData, string, unknown>['get']>>,\n): TextPaint {\n const { fill, stroke } = withInheritedPaint(node.data, inheritedPaintIn(scene, node));\n return { fill, stroke };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"useSceneTextEdit.js","names":[],"sources":["../../../src/domains/text/useSceneTextEdit.ts"],"sourcesContent":["/**\n * `useSceneTextEdit` — scene-aware wrapper around `useTextEdit`: scene\n * lookups for text / style / runs / screen-pose, and\n * `scene.update(id, { data })` for commits. The overlay and a double-click\n * both read the pose a node is painted at, so text inside a container whose\n * pose is a frame is edited where it is drawn.\n *\n * Defaults assume the node's `data` carries `text` / `style` / `runs`\n * directly. For custom data shapes, override any of the optional projections.\n * Consumers with non-rect poses should drop down to raw `useTextEdit` and\n * supply their own `getScreenPose`.\n */\nimport { useCallback, useEffect, useMemo, useRef } from 'react';\nimport { useLatest } from '@weasel-js/react';\nimport { asNodeId } from '../../core/scene/types';\nimport { scenePaintedPoseOf } from '../../canvas/derivedPath';\nimport type { Scene } from '../../core/scene/types';\nimport { drawOneEditing, type EditingDrawOne } from './editingDrawOne';\nimport { textHitAt } from './textHitAt';\nimport { registerHostTextEditor } from './hostTextEditors';\nimport { editedData, sceneTextDrafts } from './sceneTextDrafts';\nimport type { Stroke, Fill, Inherit } from '@weasel-js/paint';\nimport { resolveScreenLength } from '@weasel-js/paint';\nimport { inheritedPaintIn } from '../../canvas/inheritedPaints';\nimport { withInheritedPaint } from '../../core/scene/inheritedPaint';\nimport { clientToCanvas } from '@weasel-js/routing';\nimport { findMountedCanvas, type MountedCanvas } from '../../canvas/mountedCanvases';\nimport type { View } from '../../core/viewport/view';\nimport type { RectPose } from 'core/geometry/unionBounds';\nimport { poseFrameOf, viewScreenToWorld } from '@weasel-js/geom';\nimport { isTextData, type StyledRun, type TextPaint, type TextStyle, type TextVerticalAlign } from '@weasel-js/text';\nimport { textBoxOf, type TextOverflow } from '../../canvas/nodeShapes/textPainter';\nimport { useTextEdit, type TextEditClipRect, type TextEditScreenPose, type UseTextEditReturn } from '@weasel-js/text-dom';\n\n/** Shape the default projections expect `data` to satisfy. All fields\n * optional so any object satisfies the constraint — projections fill in\n * the gaps for non-default shapes. */\nexport interface DefaultTextData {\n text?: string;\n style?: TextStyle;\n runs?: readonly StyledRun[];\n fill?: Fill | Inherit | null;\n stroke?: Stroke | Inherit | null;\n verticalAlign?: TextVerticalAlign;\n padding?: number;\n overflow?: TextOverflow;\n}\n\n/** All-optional projections + fontSize fallback. */\nexport interface UseSceneTextEditOptions<TData> {\n /**\n * Is this node text? A double-click opens an edit only on a node that\n * answers yes, and passes through any other to the text under it. Default:\n * `isTextData`, the test `inferredNodeRouting` classifies a text node by. Pass one when `getText` reads some other shape.\n */\n isText?: (data: TData) => boolean;\n /** Read plain text from `data`. Default: `data.text ?? ''`. */\n getText?: (data: TData) => string;\n /** Read style from `data`. Default: `data.style`. */\n getStyle?: (data: TData) => TextStyle | undefined;\n /** Read the node's paint from `data`. Default: `data.fill` / `data.stroke`. */\n getPaint?: (data: TData) => TextPaint | undefined;\n /** Read rich-text runs from `data`. Default: `data.runs`. */\n getRuns?: (data: TData) => readonly StyledRun[] | undefined;\n /** Read the box vertical alignment from `data`. Default: `data.verticalAlign`. */\n getVerticalAlign?: (data: TData) => TextVerticalAlign | undefined;\n /** Read the inset from the node's box to its text from `data`. Default: `data.padding`. */\n getPadding?: (data: TData) => number | undefined;\n /** Read whether the node cuts its text at its box from `data`, which the\n * overlay then does too. Default: `data.overflow`. */\n getOverflow?: (data: TData) => TextOverflow | undefined;\n /** Produce updated data with new text. Default: `{ ...data, text }`. */\n setText?: (data: TData, text: string) => TData;\n /** Produce updated data with new runs. Default: `{ ...data, runs }`. */\n setRuns?: (data: TData, runs: StyledRun[]) => TData;\n /**\n * Produce updated data with a new node-wide style. Default:\n * `{ ...data, style }`. Written only when a range toggle has to *clear* a\n * flag the node itself sets — the run algebra is additive, so that edit\n * lowers the node flag and raises it on the runs outside the range. See\n * `useTextEdit`'s `setStyle`.\n *\n * An edit's text, runs, and style are folded through these three and\n * written as one `scene.update`, so a commit is one undo step.\n */\n setStyle?: (data: TData, style: TextStyle) => TData;\n /** Fallback fontSize when `style.fontSize` is unset. Default `16`. */\n defaultFontSize?: number;\n /**\n * The view to project through. Omit it and the view is read live from the\n * weasel canvas mounted inside `container` (the one a double-click landed\n * on, when there are several). With no canvas there, the node's world box\n * is passed through as screen pixels.\n *\n * A thunk is re-read on every projection; a plain `View` is the value from\n * the render that supplied it.\n */\n view?: View | (() => View);\n /**\n * Forwarded to `useTextEdit`: is `el` part of the editor's own chrome?\n * Focus moving into it does not end the edit. Wire it to whatever renders\n * the character controls.\n */\n isEditorChrome?: (el: Element) => boolean;\n /** Forwarded to `useTextEdit`: what Escape does to the edit. */\n escape?: 'cancel' | 'commit';\n /** Forwarded to `useTextEdit`: how long an edit waits for its face. */\n fontHold?: number;\n /** Forwarded to `useTextEdit`: called as an edit of the node `id` opens,\n * whichever way it was started. */\n onStart?: (id: string) => void;\n /** Forwarded to `useTextEdit`: called as the edit of `id` closes, after\n * any commit has been written to the scene. */\n onEnd?: (id: string) => void;\n}\n\nfunction resolveView(view: View | (() => View) | undefined): View | undefined {\n return typeof view === 'function' ? view() : view;\n}\n\n/** What `onDoubleClick` reads of the event: a React or a DOM `MouseEvent`. */\nexport type TextEditDoubleClick = Pick<MouseEvent, 'target' | 'clientX' | 'clientY'>;\n\n/** Return shape extends `UseTextEditReturn` with an `onDoubleClick`\n * binding for the canvas container. Double-clicking inside a text\n * node enters edit mode with the caret seeded at the clicked glyph. */\nexport interface UseSceneTextEditReturn extends UseTextEditReturn {\n /** Bind to the element wrapping `<SceneCanvas>` (the same element\n * passed as `container`, or any ancestor that receives the dblclick\n * bubble). Starts an edit on the top-most text node under the cursor. */\n onDoubleClick: (e: TextEditDoubleClick) => void;\n /** The `isText` option asked of the node with this id: what\n * `TextEditDep.isTextNode` wants, so both ways into an edit agree. */\n isTextNode: (id: string) => boolean;\n /**\n * `defaultDrawOne`, less the glyphs of the node being edited: the overlay\n * stands in for its text, and the rest of the node, a `background` among\n * it, goes on painting. Pass it as `layers.scene.drawOne`, with\n * `paintBounds: defaultPaintBounds`, in place of an `alphaFor` that hides\n * the whole node.\n */\n drawOne: EditingDrawOne;\n}\n\n/** How each field is read from a node's `data`: the option's projection, else the default shape. */\nfunction projectionsOf<TData extends DefaultTextData>(opts: { readonly current: UseSceneTextEditOptions<TData> }) {\n return {\n isText: (data: TData): boolean =>\n opts.current.isText ? opts.current.isText(data) : isTextData(data),\n text: (data: TData): string =>\n opts.current.getText ? opts.current.getText(data) : (data.text ?? ''),\n style: (data: TData): TextStyle | undefined =>\n opts.current.getStyle ? opts.current.getStyle(data) : data.style,\n runs: (data: TData): readonly StyledRun[] | undefined =>\n opts.current.getRuns ? opts.current.getRuns(data) : data.runs,\n verticalAlign: (data: TData): TextVerticalAlign | undefined =>\n opts.current.getVerticalAlign ? opts.current.getVerticalAlign(data) : data.verticalAlign,\n padding: (data: TData): number | undefined =>\n opts.current.getPadding ? opts.current.getPadding(data) : data.padding,\n overflow: (data: TData): TextOverflow | undefined =>\n opts.current.getOverflow ? opts.current.getOverflow(data) : data.overflow,\n };\n}\n\n/** Wire double-click-to-edit text on a scene: mounts a DOM editing overlay\n * over the node being edited and writes the result back as an undoable op.\n * `<SceneCanvas>`'s own editor stands down for any canvas inside `container`. */\nexport function useSceneTextEdit<\n TData extends DefaultTextData,\n TLayer extends string,\n TPose extends RectPose = RectPose,\n>(\n scene: Scene<TData, TLayer, TPose>,\n container: HTMLElement | null,\n options: UseSceneTextEditOptions<TData> = {},\n): UseSceneTextEditReturn {\n useEffect(() => (container ? registerHostTextEditor(container) : undefined), [container]);\n return useSceneTextEditor(scene, container, options);\n}\n\n/** `useSceneTextEdit` without announcing itself as a host's editor: what\n * `<SceneCanvas>` mounts as its own. */\nexport function useSceneTextEditor<\n TData extends DefaultTextData,\n TLayer extends string,\n TPose extends RectPose = RectPose,\n>(\n scene: Scene<TData, TLayer, TPose>,\n container: HTMLElement | null,\n options: UseSceneTextEditOptions<TData> = {},\n): UseSceneTextEditReturn {\n // Refs, so the useTextEdit callbacks always see the current values\n // without re-binding.\n const sceneRef = useLatest(scene);\n const optsRef = useLatest(options);\n const containerRef = useLatest(container);\n // The last double-click's target, so the overlay projects through the\n // canvas the edit was opened on.\n const openedOnRef = useRef<EventTarget | null>(null);\n const drafts = useMemo(() => sceneTextDrafts(sceneRef, optsRef), [sceneRef, optsRef]);\n useEffect(() => () => drafts.close(), [drafts]);\n\n // The canvas's box in container pixels is both the origin its view projects\n // from and the region its glyphs are clipped to.\n const surface = (): { canvas: MountedCanvas; box: TextEditClipRect } | null => {\n const root = containerRef.current;\n const canvas = root ? findMountedCanvas(root, openedOnRef.current) : null;\n if (!root || !canvas) return null;\n const a = canvas.element.getBoundingClientRect();\n const b = root.getBoundingClientRect();\n return {\n canvas,\n box: {\n x: a.left - b.left - root.clientLeft + root.scrollLeft,\n y: a.top - b.top - root.clientTop + root.scrollTop,\n width: a.width,\n height: a.height,\n },\n };\n };\n\n const read = useMemo(() => projectionsOf(optsRef), [optsRef]);\n\n const edit = useTextEdit({\n container,\n isEditorChrome: (el) => optsRef.current.isEditorChrome?.(el) ?? false,\n escape: options.escape,\n fontHold: options.fontHold,\n onStart: (id) => optsRef.current.onStart?.(id),\n onEnd: (id) => {\n drafts.end(id);\n optsRef.current.onEnd?.(id);\n },\n onDraft: drafts.draft,\n getText: (id) => {\n const node = sceneRef.current.get(asNodeId(id));\n return node ? read.text(node.data) : '';\n },\n getStyle: (id) => {\n const node = sceneRef.current.get(asNodeId(id));\n return node ? read.style(node.data) : undefined;\n },\n getPaint: (id) => {\n const node = sceneRef.current.get(asNodeId(id));\n if (!node) return undefined;\n const get = optsRef.current.getPaint;\n return get ? get(node.data) : shownTextPaint(sceneRef.current, node);\n },\n getScreenPose: (id) => {\n const scene = sceneRef.current;\n const node = scene.get(asNodeId(id));\n if (!node) return null;\n const at = surface();\n const view = resolveView(optsRef.current.view) ?? at?.canvas.getView();\n return screenPoseOf(node.data, scenePaintedPoseOf(scene)(node), read, view, at?.box, optsRef.current.defaultFontSize);\n },\n getClipRect: () => surface()?.box ?? null,\n getRuns: (id) => {\n const node = sceneRef.current.get(asNodeId(id));\n return node ? read.runs(node.data) : undefined;\n },\n commit: (id, { text, runs, style }) => {\n const nid = asNodeId(id);\n const node = sceneRef.current.get(nid);\n if (!node) return;\n drafts.commit(id, editedData(node.data, { text, runs, style }, optsRef.current));\n },\n });\n\n const onDoubleClick = useCallback((e: TextEditDoubleClick) => {\n // The dblclick bubbles from the canvas up to any wrapping container; the\n // element it started on is what the click coordinates are relative to.\n const canvas = e.target instanceof HTMLElement ? e.target : null;\n if (!canvas) return;\n // Inside the editor a double-click selects a word, and its target is a run\n // span whose box the click would be measured against as if it were a canvas.\n if (canvas.closest('[contenteditable=\"true\"]')) return;\n\n // The click is un-projected here; `textHitAt` takes it back through the\n // frame the node is drawn in.\n const [canvasX, canvasY] = clientToCanvas(canvas, e.clientX, e.clientY);\n const root = containerRef.current;\n const view = resolveView(optsRef.current.view)\n ?? (root ? findMountedCanvas(root, canvas)?.getView() : undefined);\n const at = view ? viewScreenToWorld(view, { x: canvasX, y: canvasY }) : { x: canvasX, y: canvasY };\n const hit = textHitAt(sceneRef.current, read, at.x, at.y);\n if (!hit) return;\n openedOnRef.current = canvas;\n edit.startEdit(hit.id, { caret: hit.caret });\n }, [edit, containerRef, optsRef, read, sceneRef]);\n\n const isTextNode = useCallback((id: string): boolean => {\n const node = sceneRef.current.get(asNodeId(id));\n return node !== undefined && read.isText(node.data);\n }, [read, sceneRef]);\n\n const drawOne = useMemo(() => drawOneEditing(edit.editingId), [edit.editingId]);\n return { ...edit, onDoubleClick, isTextNode, drawOne };\n}\n\n/** The overlay's pose for a node painted at `painted`: its text box, inset\n * about the box center, which is all the overlay's frame needs, projected\n * through `view`. */\nfunction screenPoseOf<TData extends DefaultTextData>(\n d: TData,\n painted: RectPose,\n read: ReturnType<typeof projectionsOf<TData>>,\n view: View | undefined,\n origin: TextEditClipRect | undefined,\n defaultFontSize = 16,\n): TextEditScreenPose {\n const inset = textBoxOf({ padding: read.padding(d) }, painted);\n const box = screenBoxOf({ ...painted, ...inset }, view, origin);\n return {\n ...box,\n // Against `zoom`, the same factor the overlay scales back up by, so a\n // `{ px }` size arrives on screen at the pixel count it names.\n fontSize: resolveScreenLength(read.style(d)?.fontSize ?? defaultFontSize, box.zoom),\n verticalAlign: read.verticalAlign(d),\n ...(read.overflow(d) === 'clip' ? { clip: clipToBox(inset, painted) } : {}),\n };\n}\n\n/** How far `box` reaches past `inner` on each side: the overlay's cut at\n * `box` when its own pose is `inner`. */\nfunction clipToBox(inner: RectPose, box: RectPose): NonNullable<TextEditScreenPose['clip']> {\n return {\n top: inner.y - box.y,\n left: inner.x - box.x,\n bottom: box.y + box.height - (inner.y + inner.height),\n right: box.x + box.width - (inner.x + inner.width),\n };\n}\n\n/**\n * `pose`'s box as the overlay takes it, `origin` being the canvas's corner in\n * container pixels. Only the box center is projected, since the overlay's\n * frame is about it. Width and height stay in world units and reach the\n * screen through the overlay's own `scale(zoom)` — see\n * `TextEditScreenPose.zoom`.\n */\nfunction screenBoxOf(\n pose: RectPose,\n view: View | undefined,\n origin: { x: number; y: number } | undefined,\n): Pick<TextEditScreenPose, 'x' | 'y' | 'width' | 'height' | 'frame'> & { zoom: number } {\n const zoom = view ? view.scale.x : 1;\n const zoomY = view ? view.scale.y : 1;\n const cx = (origin?.x ?? 0) + (pose.x + pose.width / 2 - (view?.x ?? 0)) * zoom;\n const cy = (origin?.y ?? 0) + (pose.y + pose.height / 2 - (view?.y ?? 0)) * zoomY;\n return {\n x: cx - (pose.width * zoom) / 2,\n y: cy - (pose.height * zoom) / 2,\n width: pose.width,\n height: pose.height,\n zoom,\n frame: frameLinearOf(pose, zoomY / zoom),\n };\n}\n\n/** The linear part of the frame `pose` is drawn in, as the overlay takes it.\n * `stretch` is the view's y scale over its x scale: the overlay scales both\n * axes by the x scale, so the frame carries what is left of y. */\nfunction frameLinearOf(pose: unknown, stretch: number): TextEditScreenPose['frame'] {\n const m = poseFrameOf(pose);\n if (!m && stretch === 1) return undefined;\n const [a, b, c, d] = m ?? [1, 0, 0, 1];\n return [a, b * stretch, c, d * stretch];\n}\n\n/** A text node's paint as the canvas paints it: an inherited one is its container's. */\nfunction shownTextPaint<TData extends DefaultTextData>(\n scene: Scene<TData, string, unknown>,\n node: NonNullable<ReturnType<Scene<TData, string, unknown>['get']>>,\n): TextPaint {\n const { fill, stroke } = withInheritedPaint(node.data, inheritedPaintIn(scene, node));\n return { fill, stroke };\n}\n"],"mappings":"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"}
@@ -1,6 +1,7 @@
1
1
  import { fitViewToBounds } from "../../core/viewport/fitViewToBounds.js";
2
2
  import { normalizeView } from "../../core/viewport/view.js";
3
3
  import { ZOOM_FLOOR } from "../../core/viewport/zoomBounds.js";
4
+ import { viewScreenToWorld, viewWorldToScreen } from "@weasel-js/geom";
4
5
 
5
6
  // src/domains/viewports/aim.ts
6
7
  /** The aim of view `viewId` (`null`: a surface's root) showing world rect `rect`. */
@@ -61,14 +62,8 @@ function aimSpace2d(viewId, view, size) {
61
62
  width: size.width,
62
63
  height: size.height
63
64
  },
64
- project: (p) => ({
65
- x: (p.x - view.x) * view.scale.x,
66
- y: (p.y - view.y) * view.scale.y
67
- }),
68
- unproject: (px) => ({
69
- x: px.x / view.scale.x + view.x,
70
- y: px.y / view.scale.y + view.y
71
- })
65
+ project: (p) => viewWorldToScreen(view, p),
66
+ unproject: (px) => viewScreenToWorld(view, px)
72
67
  };
73
68
  }
74
69
  var UNUSED_CURRENT = {
@@ -1 +1 @@
1
- {"version":3,"file":"aim.js","names":[],"sources":["../../../src/domains/viewports/aim.ts"],"sourcesContent":["import { fitViewToBounds, type Bounds } from 'core/viewport/fitViewToBounds';\nimport type { Dims } from 'core/layers/render';\nimport { normalizeView, type View } from 'core/viewport/view';\nimport { ZOOM_FLOOR } from 'core/viewport/zoomBounds';\n\n/** A point in a view's world. A 2D world is the plane z = 0, so `z` is absent there. */\nexport interface AimPoint {\n x: number;\n y: number;\n z?: number;\n}\n\n/** A closed outline in a frame's CSS px, from the frame's top-left; at least 3 points. */\nexport type AimOutline = readonly { x: number; y: number }[];\n\n/** A target view, as an aim is drawn into it: its world, and its frame. */\nexport interface AimSpace {\n /** The view whose frame this is; `null` for a surface's root view. */\n readonly viewId: string | null;\n /** `'2d'` for core's views; any other value is its owner's (kernel3d: `'3d'`). */\n readonly kind: string;\n readonly size: { width: number; height: number };\n /** World point to frame px; `null` where the point does not draw (behind a 3D camera). */\n project(p: AimPoint): { x: number; y: number } | null;\n /** Frame px to the world point under it; `null` where there is none (a 3D ray missing its plane). */\n unproject(px: { x: number; y: number }): AimPoint | null;\n}\n\n/** What a viewport shows, read in the world of the view it names. */\nexport interface ViewAim<TExtent = unknown> {\n readonly viewId: string | null;\n /** Discriminates `extent`: `'rect'` for core's 2D views. */\n readonly kind: string;\n /** Where the aim lies in its world, in its owner's own terms. */\n readonly extent: TExtent;\n /** The aim's outline in `space`'s frame, or `null` when it cannot be drawn there. */\n outlineIn(space: AimSpace): AimOutline | null;\n /** This aim moved so the world point under frame px `from` in `space` ends up under `to`; `null` when either misses. */\n pannedIn(space: AimSpace, from: { x: number; y: number }, to: { x: number; y: number }): ViewAim<TExtent> | null;\n}\n\n/** Core's 2D aim: a world rect a view shows. */\nexport type RectAim = ViewAim<Bounds> & { readonly kind: 'rect' };\n\n/** The aim of view `viewId` (`null`: a surface's root) showing world rect `rect`. */\nexport function rectAim(viewId: string | null, rect: Bounds): RectAim {\n return {\n viewId,\n kind: 'rect',\n extent: rect,\n outlineIn(space) {\n const { x, y, width, height } = rect;\n const out: { x: number; y: number }[] = [];\n for (const corner of [\n { x, y },\n { x: x + width, y },\n { x: x + width, y: y + height },\n { x, y: y + height },\n ]) {\n const p = space.project(corner);\n if (!p) return null;\n out.push(p);\n }\n return out;\n },\n pannedIn(space, from, to) {\n const a = space.unproject(from);\n const b = space.unproject(to);\n if (!a || !b) return null;\n return rectAim(viewId, { ...rect, x: rect.x + (b.x - a.x), y: rect.y + (b.y - a.y) });\n },\n };\n}\n\n/** Whether `aim` is core's 2D rect aim, so its `extent` is a world rect. */\nexport function isRectAim(aim: ViewAim): aim is RectAim {\n return aim.kind === 'rect';\n}\n\n/** A 2D view's frame: `view` is its camera and `size` its frame; frame px are measured from the frame's top-left. */\nexport function aimSpace2d(viewId: string | null, view: View, size: Dims): AimSpace {\n return {\n viewId,\n kind: '2d',\n size: { width: size.width, height: size.height },\n project: (p) => ({ x: (p.x - view.x) * view.scale.x, y: (p.y - view.y) * view.scale.y }),\n unproject: (px) => ({ x: px.x / view.scale.x + view.x, y: px.y / view.scale.y + view.y }),\n };\n}\n\nconst UNUSED_CURRENT: View = { x: 0, y: 0, scale: { x: 1, y: 1 } };\n\n/**\n * The camera that shows `rect` in a frame of `size`: `'exact'` per axis (the inverse of `viewportWorldRect`),\n * `'contain'` uniform and centered. Not clamped to a zoom range, and finite for an empty rect or frame. Normalized.\n */\nexport function viewForAim(rect: Bounds, size: Dims, fit: 'exact' | 'contain'): View {\n if (!(rect.width > 0 && rect.height > 0 && size.width > 0 && size.height > 0)) {\n return normalizeView({ x: rect.x, y: rect.y, scale: { x: ZOOM_FLOOR, y: ZOOM_FLOOR } });\n }\n return normalizeView(fitViewToBounds(rect, size, UNUSED_CURRENT, {\n mode: fit === 'exact' ? 'stretch' : 'contain', padding: 0, minScale: 0, maxScale: Infinity,\n }));\n}\n"],"mappings":";;;;;;AA6CA,SAAgB,QAAQ,QAAuB,MAAuB;CACpE,OAAO;EACL;EACA,MAAM;EACN,QAAQ;EACR,UAAU,OAAO;GACf,MAAM,EAAE,GAAG,GAAG,OAAO,WAAW;GAChC,MAAM,MAAkC,CAAC;GACzC,KAAK,MAAM,UAAU;IACnB;KAAE;KAAG;IAAE;IACP;KAAE,GAAG,IAAI;KAAO;IAAE;IAClB;KAAE,GAAG,IAAI;KAAO,GAAG,IAAI;IAAO;IAC9B;KAAE;KAAG,GAAG,IAAI;IAAO;GACrB,GAAG;IACD,MAAM,IAAI,MAAM,QAAQ,MAAM;IAC9B,IAAI,CAAC,GAAG,OAAO;IACf,IAAI,KAAK,CAAC;GACZ;GACA,OAAO;EACT;EACA,SAAS,OAAO,MAAM,IAAI;GACxB,MAAM,IAAI,MAAM,UAAU,IAAI;GAC9B,MAAM,IAAI,MAAM,UAAU,EAAE;GAC5B,IAAI,CAAC,KAAK,CAAC,GAAG,OAAO;GACrB,OAAO,QAAQ,QAAQ;IAAE,GAAG;IAAM,GAAG,KAAK,KAAK,EAAE,IAAI,EAAE;IAAI,GAAG,KAAK,KAAK,EAAE,IAAI,EAAE;GAAG,CAAC;EACtF;CACF;AACF;;AAGA,SAAgB,UAAU,KAA8B;CACtD,OAAO,IAAI,SAAS;AACtB;;AAGA,SAAgB,WAAW,QAAuB,MAAY,MAAsB;CAClF,OAAO;EACL;EACA,MAAM;EACN,MAAM;GAAE,OAAO,KAAK;GAAO,QAAQ,KAAK;EAAO;EAC/C,UAAU,OAAO;GAAE,IAAI,EAAE,IAAI,KAAK,KAAK,KAAK,MAAM;GAAG,IAAI,EAAE,IAAI,KAAK,KAAK,KAAK,MAAM;EAAE;EACtF,YAAY,QAAQ;GAAE,GAAG,GAAG,IAAI,KAAK,MAAM,IAAI,KAAK;GAAG,GAAG,GAAG,IAAI,KAAK,MAAM,IAAI,KAAK;EAAE;CACzF;AACF;AAEA,IAAM,iBAAuB;CAAE,GAAG;CAAG,GAAG;CAAG,OAAO;EAAE,GAAG;EAAG,GAAG;CAAE;AAAE;;;;;AAMjE,SAAgB,WAAW,MAAc,MAAY,KAAgC;CACnF,IAAI,EAAE,KAAK,QAAQ,KAAK,KAAK,SAAS,KAAK,KAAK,QAAQ,KAAK,KAAK,SAAS,IACzE,OAAO,cAAc;EAAE,GAAG,KAAK;EAAG,GAAG,KAAK;EAAG,OAAO;GAAE,GAAG;GAAY,GAAG;EAAW;CAAE,CAAC;CAExF,OAAO,cAAc,gBAAgB,MAAM,MAAM,gBAAgB;EAC/D,MAAM,QAAQ,UAAU,YAAY;EAAW,SAAS;EAAG,UAAU;EAAG,UAAU;CACpF,CAAC,CAAC;AACJ"}
1
+ {"version":3,"file":"aim.js","names":[],"sources":["../../../src/domains/viewports/aim.ts"],"sourcesContent":["import { fitViewToBounds, type Bounds } from 'core/viewport/fitViewToBounds';\nimport type { Dims } from 'core/layers/render';\nimport { normalizeView, type View } from 'core/viewport/view';\nimport { ZOOM_FLOOR } from 'core/viewport/zoomBounds';\nimport { viewScreenToWorld, viewWorldToScreen } from '@weasel-js/geom';\n\n/** A point in a view's world. A 2D world is the plane z = 0, so `z` is absent there. */\nexport interface AimPoint {\n x: number;\n y: number;\n z?: number;\n}\n\n/** A closed outline in a frame's CSS px, from the frame's top-left; at least 3 points. */\nexport type AimOutline = readonly { x: number; y: number }[];\n\n/** A target view, as an aim is drawn into it: its world, and its frame. */\nexport interface AimSpace {\n /** The view whose frame this is; `null` for a surface's root view. */\n readonly viewId: string | null;\n /** `'2d'` for core's views; any other value is its owner's (kernel3d: `'3d'`). */\n readonly kind: string;\n readonly size: { width: number; height: number };\n /** World point to frame px; `null` where the point does not draw (behind a 3D camera). */\n project(p: AimPoint): { x: number; y: number } | null;\n /** Frame px to the world point under it; `null` where there is none (a 3D ray missing its plane). */\n unproject(px: { x: number; y: number }): AimPoint | null;\n}\n\n/** What a viewport shows, read in the world of the view it names. */\nexport interface ViewAim<TExtent = unknown> {\n readonly viewId: string | null;\n /** Discriminates `extent`: `'rect'` for core's 2D views. */\n readonly kind: string;\n /** Where the aim lies in its world, in its owner's own terms. */\n readonly extent: TExtent;\n /** The aim's outline in `space`'s frame, or `null` when it cannot be drawn there. */\n outlineIn(space: AimSpace): AimOutline | null;\n /** This aim moved so the world point under frame px `from` in `space` ends up under `to`; `null` when either misses. */\n pannedIn(space: AimSpace, from: { x: number; y: number }, to: { x: number; y: number }): ViewAim<TExtent> | null;\n}\n\n/** Core's 2D aim: a world rect a view shows. */\nexport type RectAim = ViewAim<Bounds> & { readonly kind: 'rect' };\n\n/** The aim of view `viewId` (`null`: a surface's root) showing world rect `rect`. */\nexport function rectAim(viewId: string | null, rect: Bounds): RectAim {\n return {\n viewId,\n kind: 'rect',\n extent: rect,\n outlineIn(space) {\n const { x, y, width, height } = rect;\n const out: { x: number; y: number }[] = [];\n for (const corner of [\n { x, y },\n { x: x + width, y },\n { x: x + width, y: y + height },\n { x, y: y + height },\n ]) {\n const p = space.project(corner);\n if (!p) return null;\n out.push(p);\n }\n return out;\n },\n pannedIn(space, from, to) {\n const a = space.unproject(from);\n const b = space.unproject(to);\n if (!a || !b) return null;\n return rectAim(viewId, { ...rect, x: rect.x + (b.x - a.x), y: rect.y + (b.y - a.y) });\n },\n };\n}\n\n/** Whether `aim` is core's 2D rect aim, so its `extent` is a world rect. */\nexport function isRectAim(aim: ViewAim): aim is RectAim {\n return aim.kind === 'rect';\n}\n\n/** A 2D view's frame: `view` is its camera and `size` its frame; frame px are measured from the frame's top-left. */\nexport function aimSpace2d(viewId: string | null, view: View, size: Dims): AimSpace {\n return {\n viewId,\n kind: '2d',\n size: { width: size.width, height: size.height },\n project: (p) => viewWorldToScreen(view, p),\n unproject: (px) => viewScreenToWorld(view, px),\n };\n}\n\nconst UNUSED_CURRENT: View = { x: 0, y: 0, scale: { x: 1, y: 1 } };\n\n/**\n * The camera that shows `rect` in a frame of `size`: `'exact'` per axis (the inverse of `viewportWorldRect`),\n * `'contain'` uniform and centered. Not clamped to a zoom range, and finite for an empty rect or frame. Normalized.\n */\nexport function viewForAim(rect: Bounds, size: Dims, fit: 'exact' | 'contain'): View {\n if (!(rect.width > 0 && rect.height > 0 && size.width > 0 && size.height > 0)) {\n return normalizeView({ x: rect.x, y: rect.y, scale: { x: ZOOM_FLOOR, y: ZOOM_FLOOR } });\n }\n return normalizeView(fitViewToBounds(rect, size, UNUSED_CURRENT, {\n mode: fit === 'exact' ? 'stretch' : 'contain', padding: 0, minScale: 0, maxScale: Infinity,\n }));\n}\n"],"mappings":";;;;;;;AA8CA,SAAgB,QAAQ,QAAuB,MAAuB;CACpE,OAAO;EACL;EACA,MAAM;EACN,QAAQ;EACR,UAAU,OAAO;GACf,MAAM,EAAE,GAAG,GAAG,OAAO,WAAW;GAChC,MAAM,MAAkC,CAAC;GACzC,KAAK,MAAM,UAAU;IACnB;KAAE;KAAG;IAAE;IACP;KAAE,GAAG,IAAI;KAAO;IAAE;IAClB;KAAE,GAAG,IAAI;KAAO,GAAG,IAAI;IAAO;IAC9B;KAAE;KAAG,GAAG,IAAI;IAAO;GACrB,GAAG;IACD,MAAM,IAAI,MAAM,QAAQ,MAAM;IAC9B,IAAI,CAAC,GAAG,OAAO;IACf,IAAI,KAAK,CAAC;GACZ;GACA,OAAO;EACT;EACA,SAAS,OAAO,MAAM,IAAI;GACxB,MAAM,IAAI,MAAM,UAAU,IAAI;GAC9B,MAAM,IAAI,MAAM,UAAU,EAAE;GAC5B,IAAI,CAAC,KAAK,CAAC,GAAG,OAAO;GACrB,OAAO,QAAQ,QAAQ;IAAE,GAAG;IAAM,GAAG,KAAK,KAAK,EAAE,IAAI,EAAE;IAAI,GAAG,KAAK,KAAK,EAAE,IAAI,EAAE;GAAG,CAAC;EACtF;CACF;AACF;;AAGA,SAAgB,UAAU,KAA8B;CACtD,OAAO,IAAI,SAAS;AACtB;;AAGA,SAAgB,WAAW,QAAuB,MAAY,MAAsB;CAClF,OAAO;EACL;EACA,MAAM;EACN,MAAM;GAAE,OAAO,KAAK;GAAO,QAAQ,KAAK;EAAO;EAC/C,UAAU,MAAM,kBAAkB,MAAM,CAAC;EACzC,YAAY,OAAO,kBAAkB,MAAM,EAAE;CAC/C;AACF;AAEA,IAAM,iBAAuB;CAAE,GAAG;CAAG,GAAG;CAAG,OAAO;EAAE,GAAG;EAAG,GAAG;CAAE;AAAE;;;;;AAMjE,SAAgB,WAAW,MAAc,MAAY,KAAgC;CACnF,IAAI,EAAE,KAAK,QAAQ,KAAK,KAAK,SAAS,KAAK,KAAK,QAAQ,KAAK,KAAK,SAAS,IACzE,OAAO,cAAc;EAAE,GAAG,KAAK;EAAG,GAAG,KAAK;EAAG,OAAO;GAAE,GAAG;GAAY,GAAG;EAAW;CAAE,CAAC;CAExF,OAAO,cAAc,gBAAgB,MAAM,MAAM,gBAAgB;EAC/D,MAAM,QAAQ,UAAU,YAAY;EAAW,SAAS;EAAG,UAAU;EAAG,UAAU;CACpF,CAAC,CAAC;AACJ"}
@@ -1,3 +1,5 @@
1
+ import { viewScreenToWorld } from "@weasel-js/geom";
2
+
1
3
  // src/domains/viewports/viewResolver.ts
2
4
  /** Right and bottom edges are exclusive, so neighbouring views never both
3
5
  * claim a pixel. */
@@ -11,9 +13,9 @@ function createViewResolver(opts) {
11
13
  const { views, root, toLocal, occluded } = opts;
12
14
  const pinned = /* @__PURE__ */ new Map();
13
15
  /** `at`, a canvas point `from` its surface's top-left, through `view`. */
14
- const worldOf = (at, from, view) => ({
15
- x: (at.x - from.x) / view.scale.x + view.x,
16
- y: (at.y - from.y) / view.scale.y + view.y
16
+ const worldOf = (at, from, view) => viewScreenToWorld(view, {
17
+ x: at.x - from.x,
18
+ y: at.y - from.y
17
19
  });
18
20
  function rootTarget(at) {
19
21
  const view = root();
@@ -1 +1 @@
1
- {"version":3,"file":"viewResolver.js","names":[],"sources":["../../../src/domains/viewports/viewResolver.ts"],"sourcesContent":["import type { View } from 'core/viewport/view';\n\n/**\n * A view the resolver can route input to: a camera and the rect it paints\n * into, in canvas CSS-pixel space. A viewport node supplies one of these per\n * frame; so could any other per-view surface.\n */\nexport interface ResolvableView {\n id: string;\n view: View;\n rect: { x: number; y: number; w: number; h: number };\n /** The part of `rect` the view is seen through, in the same space — a view\n * nested in another is cut to it. Omitted, the whole rect. */\n clip?: { x: number; y: number; w: number; h: number };\n}\n\ntype Rect = { x: number; y: number; w: number; h: number };\n\n/** Right and bottom edges are exclusive, so neighbouring views never both\n * claim a pixel. */\nconst inside = (r: Rect, x: number, y: number): boolean =>\n x >= r.x && x < r.x + r.w && y >= r.y && y < r.y + r.h;\n\n/** Whether `v` takes a canvas-local point: inside its rect and its clip. */\nexport function viewContains(v: ResolvableView, x: number, y: number): boolean {\n return inside(v.rect, x, y) && (v.clip === undefined || inside(v.clip, x, y));\n}\n\n/**\n * The view an input event belongs to. `id` is `null` for the root view —\n * the whole canvas, when no viewport contains the point.\n */\nexport interface ViewTarget {\n id: string | null;\n view: View;\n /** The point asked about, in that view's world. */\n world: { x: number; y: number };\n}\n\n/**\n * Routes client points to views, and holds a captured pointer on the view its\n * gesture started in.\n *\n * Stickiness is the point. A drag that leaves its view's rect — over a\n * neighbouring view, or off the canvas — must keep reporting coordinates in\n * the space it began in, or a marquee crossing a panel edge silently starts\n * measuring against the wrong camera.\n */\nexport interface ViewResolver {\n /** Resolve and pin `pointerId`. Call on pointerdown. */\n begin(pointerId: number, clientX: number, clientY: number): ViewTarget;\n /**\n * The view for a point. A pinned `pointerId` gets its pinned view, looked\n * up fresh so a rect that moved mid-gesture is honored. Pass `null` for\n * input with no pointer to capture — wheel, hover, keys.\n */\n at(pointerId: number | null, clientX: number, clientY: number): ViewTarget;\n /** Release `pointerId`. Call on pointerup and pointercancel. */\n end(pointerId: number): void;\n /** Release every pin. */\n clear(): void;\n}\n\n/**\n * Options for `createViewResolver`. Each field is a function called afresh on\n * every query, so views and the canvas may move between events.\n */\nexport interface CreateViewResolverOpts {\n /** Candidate views in paint order — the last one containing a point wins,\n * so a nested view listed after the one around it is the one a point\n * inside both reaches. */\n views: () => readonly ResolvableView[];\n /** The camera for points no view claims. */\n root: () => View;\n /** A client point in the canvas's own CSS pixels, from its top-left. */\n toLocal: (clientX: number, clientY: number) => { x: number; y: number };\n /**\n * Whether something painted over every view claims a canvas-local point —\n * chrome on the surface's own frame. Such a point resolves to the root even\n * inside a view's rect. Asked only where a view would otherwise win.\n */\n occluded?: (x: number, y: number) => boolean;\n}\n\n/** Build a `ViewResolver` over `opts.views`, falling back to `opts.root`. */\nexport function createViewResolver(opts: CreateViewResolverOpts): ViewResolver {\n const { views, root, toLocal, occluded } = opts;\n const pinned = new Map<number, string | null>();\n\n type Local = { x: number; y: number };\n /** `at`, a canvas point `from` its surface's top-left, through `view`. */\n const worldOf = (at: Local, from: Local, view: View): Local => ({\n x: (at.x - from.x) / view.scale.x + view.x,\n y: (at.y - from.y) / view.scale.y + view.y,\n });\n\n function rootTarget(at: Local): ViewTarget {\n const view = root();\n return { id: null, view, world: worldOf(at, { x: 0, y: 0 }, view) };\n }\n\n function target(v: ResolvableView, at: Local): ViewTarget {\n return { id: v.id, view: v.view, world: worldOf(at, v.rect, v.view) };\n }\n\n function hit({ x, y }: Local): ResolvableView | undefined {\n const list = views();\n for (let i = list.length - 1; i >= 0; i--) {\n if (viewContains(list[i]!, x, y)) {\n return occluded?.(x, y) ? undefined : list[i];\n }\n }\n return undefined;\n }\n\n function fresh(at: Local): ViewTarget {\n const v = hit(at);\n return v ? target(v, at) : rootTarget(at);\n }\n\n return {\n begin(pointerId, clientX, clientY) {\n const t = fresh(toLocal(clientX, clientY));\n pinned.set(pointerId, t.id);\n return t;\n },\n at(pointerId, clientX, clientY) {\n const at = toLocal(clientX, clientY);\n if (pointerId !== null && pinned.has(pointerId)) {\n const id = pinned.get(pointerId)!;\n if (id === null) return rootTarget(at);\n const v = views().find((c) => c.id === id);\n if (v) return target(v, at);\n pinned.delete(pointerId);\n }\n return fresh(at);\n },\n end(pointerId) {\n pinned.delete(pointerId);\n },\n clear() {\n pinned.clear();\n },\n };\n}\n"],"mappings":";;;AAoBA,IAAM,UAAU,GAAS,GAAW,MAClC,KAAK,EAAE,KAAK,IAAI,EAAE,IAAI,EAAE,KAAK,KAAK,EAAE,KAAK,IAAI,EAAE,IAAI,EAAE;;AAGvD,SAAgB,aAAa,GAAmB,GAAW,GAAoB;CAC7E,OAAO,OAAO,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,SAAS,UAAa,OAAO,EAAE,MAAM,GAAG,CAAC;AAC7E;;AA2DA,SAAgB,mBAAmB,MAA4C;CAC7E,MAAM,EAAE,OAAO,MAAM,SAAS,aAAa;CAC3C,MAAM,yBAAS,IAAI,IAA2B;;CAI9C,MAAM,WAAW,IAAW,MAAa,UAAuB;EAC9D,IAAI,GAAG,IAAI,KAAK,KAAK,KAAK,MAAM,IAAI,KAAK;EACzC,IAAI,GAAG,IAAI,KAAK,KAAK,KAAK,MAAM,IAAI,KAAK;CAC3C;CAEA,SAAS,WAAW,IAAuB;EACzC,MAAM,OAAO,KAAK;EAClB,OAAO;GAAE,IAAI;GAAM;GAAM,OAAO,QAAQ,IAAI;IAAE,GAAG;IAAG,GAAG;GAAE,GAAG,IAAI;EAAE;CACpE;CAEA,SAAS,OAAO,GAAmB,IAAuB;EACxD,OAAO;GAAE,IAAI,EAAE;GAAI,MAAM,EAAE;GAAM,OAAO,QAAQ,IAAI,EAAE,MAAM,EAAE,IAAI;EAAE;CACtE;CAEA,SAAS,IAAI,EAAE,GAAG,KAAwC;EACxD,MAAM,OAAO,MAAM;EACnB,KAAK,IAAI,IAAI,KAAK,SAAS,GAAG,KAAK,GAAG,KACpC,IAAI,aAAa,KAAK,IAAK,GAAG,CAAC,GAC7B,OAAO,WAAW,GAAG,CAAC,IAAI,SAAY,KAAK;CAIjD;CAEA,SAAS,MAAM,IAAuB;EACpC,MAAM,IAAI,IAAI,EAAE;EAChB,OAAO,IAAI,OAAO,GAAG,EAAE,IAAI,WAAW,EAAE;CAC1C;CAEA,OAAO;EACL,MAAM,WAAW,SAAS,SAAS;GACjC,MAAM,IAAI,MAAM,QAAQ,SAAS,OAAO,CAAC;GACzC,OAAO,IAAI,WAAW,EAAE,EAAE;GAC1B,OAAO;EACT;EACA,GAAG,WAAW,SAAS,SAAS;GAC9B,MAAM,KAAK,QAAQ,SAAS,OAAO;GACnC,IAAI,cAAc,QAAQ,OAAO,IAAI,SAAS,GAAG;IAC/C,MAAM,KAAK,OAAO,IAAI,SAAS;IAC/B,IAAI,OAAO,MAAM,OAAO,WAAW,EAAE;IACrC,MAAM,IAAI,MAAM,CAAC,CAAC,MAAM,MAAM,EAAE,OAAO,EAAE;IACzC,IAAI,GAAG,OAAO,OAAO,GAAG,EAAE;IAC1B,OAAO,OAAO,SAAS;GACzB;GACA,OAAO,MAAM,EAAE;EACjB;EACA,IAAI,WAAW;GACb,OAAO,OAAO,SAAS;EACzB;EACA,QAAQ;GACN,OAAO,MAAM;EACf;CACF;AACF"}
1
+ {"version":3,"file":"viewResolver.js","names":[],"sources":["../../../src/domains/viewports/viewResolver.ts"],"sourcesContent":["import type { View } from 'core/viewport/view';\nimport { viewScreenToWorld } from '@weasel-js/geom';\n\n/**\n * A view the resolver can route input to: a camera and the rect it paints\n * into, in canvas CSS-pixel space. A viewport node supplies one of these per\n * frame; so could any other per-view surface.\n */\nexport interface ResolvableView {\n id: string;\n view: View;\n rect: { x: number; y: number; w: number; h: number };\n /** The part of `rect` the view is seen through, in the same space — a view\n * nested in another is cut to it. Omitted, the whole rect. */\n clip?: { x: number; y: number; w: number; h: number };\n}\n\ntype Rect = { x: number; y: number; w: number; h: number };\n\n/** Right and bottom edges are exclusive, so neighbouring views never both\n * claim a pixel. */\nconst inside = (r: Rect, x: number, y: number): boolean =>\n x >= r.x && x < r.x + r.w && y >= r.y && y < r.y + r.h;\n\n/** Whether `v` takes a canvas-local point: inside its rect and its clip. */\nexport function viewContains(v: ResolvableView, x: number, y: number): boolean {\n return inside(v.rect, x, y) && (v.clip === undefined || inside(v.clip, x, y));\n}\n\n/**\n * The view an input event belongs to. `id` is `null` for the root view —\n * the whole canvas, when no viewport contains the point.\n */\nexport interface ViewTarget {\n id: string | null;\n view: View;\n /** The point asked about, in that view's world. */\n world: { x: number; y: number };\n}\n\n/**\n * Routes client points to views, and holds a captured pointer on the view its\n * gesture started in.\n *\n * Stickiness is the point. A drag that leaves its view's rect — over a\n * neighbouring view, or off the canvas — must keep reporting coordinates in\n * the space it began in, or a marquee crossing a panel edge silently starts\n * measuring against the wrong camera.\n */\nexport interface ViewResolver {\n /** Resolve and pin `pointerId`. Call on pointerdown. */\n begin(pointerId: number, clientX: number, clientY: number): ViewTarget;\n /**\n * The view for a point. A pinned `pointerId` gets its pinned view, looked\n * up fresh so a rect that moved mid-gesture is honored. Pass `null` for\n * input with no pointer to capture — wheel, hover, keys.\n */\n at(pointerId: number | null, clientX: number, clientY: number): ViewTarget;\n /** Release `pointerId`. Call on pointerup and pointercancel. */\n end(pointerId: number): void;\n /** Release every pin. */\n clear(): void;\n}\n\n/**\n * Options for `createViewResolver`. Each field is a function called afresh on\n * every query, so views and the canvas may move between events.\n */\nexport interface CreateViewResolverOpts {\n /** Candidate views in paint order — the last one containing a point wins,\n * so a nested view listed after the one around it is the one a point\n * inside both reaches. */\n views: () => readonly ResolvableView[];\n /** The camera for points no view claims. */\n root: () => View;\n /** A client point in the canvas's own CSS pixels, from its top-left. */\n toLocal: (clientX: number, clientY: number) => { x: number; y: number };\n /**\n * Whether something painted over every view claims a canvas-local point —\n * chrome on the surface's own frame. Such a point resolves to the root even\n * inside a view's rect. Asked only where a view would otherwise win.\n */\n occluded?: (x: number, y: number) => boolean;\n}\n\n/** Build a `ViewResolver` over `opts.views`, falling back to `opts.root`. */\nexport function createViewResolver(opts: CreateViewResolverOpts): ViewResolver {\n const { views, root, toLocal, occluded } = opts;\n const pinned = new Map<number, string | null>();\n\n type Local = { x: number; y: number };\n /** `at`, a canvas point `from` its surface's top-left, through `view`. */\n const worldOf = (at: Local, from: Local, view: View): Local =>\n viewScreenToWorld(view, { x: at.x - from.x, y: at.y - from.y });\n\n function rootTarget(at: Local): ViewTarget {\n const view = root();\n return { id: null, view, world: worldOf(at, { x: 0, y: 0 }, view) };\n }\n\n function target(v: ResolvableView, at: Local): ViewTarget {\n return { id: v.id, view: v.view, world: worldOf(at, v.rect, v.view) };\n }\n\n function hit({ x, y }: Local): ResolvableView | undefined {\n const list = views();\n for (let i = list.length - 1; i >= 0; i--) {\n if (viewContains(list[i]!, x, y)) {\n return occluded?.(x, y) ? undefined : list[i];\n }\n }\n return undefined;\n }\n\n function fresh(at: Local): ViewTarget {\n const v = hit(at);\n return v ? target(v, at) : rootTarget(at);\n }\n\n return {\n begin(pointerId, clientX, clientY) {\n const t = fresh(toLocal(clientX, clientY));\n pinned.set(pointerId, t.id);\n return t;\n },\n at(pointerId, clientX, clientY) {\n const at = toLocal(clientX, clientY);\n if (pointerId !== null && pinned.has(pointerId)) {\n const id = pinned.get(pointerId)!;\n if (id === null) return rootTarget(at);\n const v = views().find((c) => c.id === id);\n if (v) return target(v, at);\n pinned.delete(pointerId);\n }\n return fresh(at);\n },\n end(pointerId) {\n pinned.delete(pointerId);\n },\n clear() {\n pinned.clear();\n },\n };\n}\n"],"mappings":";;;;;AAqBA,IAAM,UAAU,GAAS,GAAW,MAClC,KAAK,EAAE,KAAK,IAAI,EAAE,IAAI,EAAE,KAAK,KAAK,EAAE,KAAK,IAAI,EAAE,IAAI,EAAE;;AAGvD,SAAgB,aAAa,GAAmB,GAAW,GAAoB;CAC7E,OAAO,OAAO,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,SAAS,UAAa,OAAO,EAAE,MAAM,GAAG,CAAC;AAC7E;;AA2DA,SAAgB,mBAAmB,MAA4C;CAC7E,MAAM,EAAE,OAAO,MAAM,SAAS,aAAa;CAC3C,MAAM,yBAAS,IAAI,IAA2B;;CAI9C,MAAM,WAAW,IAAW,MAAa,SACvC,kBAAkB,MAAM;EAAE,GAAG,GAAG,IAAI,KAAK;EAAG,GAAG,GAAG,IAAI,KAAK;CAAE,CAAC;CAEhE,SAAS,WAAW,IAAuB;EACzC,MAAM,OAAO,KAAK;EAClB,OAAO;GAAE,IAAI;GAAM;GAAM,OAAO,QAAQ,IAAI;IAAE,GAAG;IAAG,GAAG;GAAE,GAAG,IAAI;EAAE;CACpE;CAEA,SAAS,OAAO,GAAmB,IAAuB;EACxD,OAAO;GAAE,IAAI,EAAE;GAAI,MAAM,EAAE;GAAM,OAAO,QAAQ,IAAI,EAAE,MAAM,EAAE,IAAI;EAAE;CACtE;CAEA,SAAS,IAAI,EAAE,GAAG,KAAwC;EACxD,MAAM,OAAO,MAAM;EACnB,KAAK,IAAI,IAAI,KAAK,SAAS,GAAG,KAAK,GAAG,KACpC,IAAI,aAAa,KAAK,IAAK,GAAG,CAAC,GAC7B,OAAO,WAAW,GAAG,CAAC,IAAI,SAAY,KAAK;CAIjD;CAEA,SAAS,MAAM,IAAuB;EACpC,MAAM,IAAI,IAAI,EAAE;EAChB,OAAO,IAAI,OAAO,GAAG,EAAE,IAAI,WAAW,EAAE;CAC1C;CAEA,OAAO;EACL,MAAM,WAAW,SAAS,SAAS;GACjC,MAAM,IAAI,MAAM,QAAQ,SAAS,OAAO,CAAC;GACzC,OAAO,IAAI,WAAW,EAAE,EAAE;GAC1B,OAAO;EACT;EACA,GAAG,WAAW,SAAS,SAAS;GAC9B,MAAM,KAAK,QAAQ,SAAS,OAAO;GACnC,IAAI,cAAc,QAAQ,OAAO,IAAI,SAAS,GAAG;IAC/C,MAAM,KAAK,OAAO,IAAI,SAAS;IAC/B,IAAI,OAAO,MAAM,OAAO,WAAW,EAAE;IACrC,MAAM,IAAI,MAAM,CAAC,CAAC,MAAM,MAAM,EAAE,OAAO,EAAE;IACzC,IAAI,GAAG,OAAO,OAAO,GAAG,EAAE;IAC1B,OAAO,OAAO,SAAS;GACzB;GACA,OAAO,MAAM,EAAE;EACjB;EACA,IAAI,WAAW;GACb,OAAO,OAAO,SAAS;EACzB;EACA,QAAQ;GACN,OAAO,MAAM;EACf;CACF;AACF"}