@weasel-js/core 1.11.1 → 1.12.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 (297) hide show
  1. package/CHANGELOG.md +184 -0
  2. package/dist/WeaselProvider.js +1 -1
  3. package/dist/affordances/pathAnchors.js +10 -6
  4. package/dist/affordances/pathAnchors.js.map +1 -1
  5. package/dist/barrel/canvas.js +3 -1
  6. package/dist/barrel/paint.js +4 -4
  7. package/dist/barrel/paths.js +8 -4
  8. package/dist/barrel/renderer.js +3 -2
  9. package/dist/barrel/routing.js +4 -4
  10. package/dist/barrel/scene.js +2 -1
  11. package/dist/barrel/text.js +2 -2
  12. package/dist/barrel/viewport.js +6 -2
  13. package/dist/canvas/Canvas.js +2 -1
  14. package/dist/canvas/Canvas.js.map +1 -1
  15. package/dist/canvas/CanvasView.js +23 -34
  16. package/dist/canvas/CanvasView.js.map +1 -1
  17. package/dist/canvas/ModalityHud.js +1 -1
  18. package/dist/canvas/NodeShape.js +4 -1
  19. package/dist/canvas/NodeShape.js.map +1 -1
  20. package/dist/canvas/SceneCanvas/ActionsProviderIfRoot.js +1 -1
  21. package/dist/canvas/SceneCanvas/DepRegistryProviderIfRoot.js +1 -1
  22. package/dist/canvas/SceneCanvas/PointerProviderIfRoot.js +1 -1
  23. package/dist/canvas/SceneCanvas/SurfaceDeclarations.js +23 -0
  24. package/dist/canvas/SceneCanvas/SurfaceDeclarations.js.map +1 -0
  25. package/dist/canvas/SceneCanvas/canvasPassThrough.js +1 -0
  26. package/dist/canvas/SceneCanvas/canvasPassThrough.js.map +1 -1
  27. package/dist/canvas/SceneCanvas/props.js.map +1 -1
  28. package/dist/canvas/SceneCanvas/scenePointQuery.js +7 -3
  29. package/dist/canvas/SceneCanvas/scenePointQuery.js.map +1 -1
  30. package/dist/canvas/SceneCanvas/useActionTunings.js +6 -1
  31. package/dist/canvas/SceneCanvas/useActionTunings.js.map +1 -1
  32. package/dist/canvas/SceneCanvas/useCanvasDevtoolsRoot.js +1 -0
  33. package/dist/canvas/SceneCanvas/useCanvasDevtoolsRoot.js.map +1 -1
  34. package/dist/canvas/SceneCanvas/useCanvasTools.js +7 -2
  35. package/dist/canvas/SceneCanvas/useCanvasTools.js.map +1 -1
  36. package/dist/canvas/SceneCanvas/useSceneCanvasHandle.js +9 -3
  37. package/dist/canvas/SceneCanvas/useSceneCanvasHandle.js.map +1 -1
  38. package/dist/canvas/SceneCanvas/useSceneCanvasLayers.js +10 -2
  39. package/dist/canvas/SceneCanvas/useSceneCanvasLayers.js.map +1 -1
  40. package/dist/canvas/SceneCanvas/useSceneCanvasTools.js +1 -0
  41. package/dist/canvas/SceneCanvas/useSceneCanvasTools.js.map +1 -1
  42. package/dist/canvas/SceneCanvas/useSceneGestureDispatcher.js +19 -4
  43. package/dist/canvas/SceneCanvas/useSceneGestureDispatcher.js.map +1 -1
  44. package/dist/canvas/SceneCanvas/useSceneStandardActions.js +2 -1
  45. package/dist/canvas/SceneCanvas/useSceneStandardActions.js.map +1 -1
  46. package/dist/canvas/SceneCanvas.js +11 -10
  47. package/dist/canvas/SceneCanvas.js.map +1 -1
  48. package/dist/canvas/ViewMarker.js +35 -0
  49. package/dist/canvas/ViewMarker.js.map +1 -0
  50. package/dist/canvas/defaultDrawOne.js +2 -0
  51. package/dist/canvas/defaultDrawOne.js.map +1 -1
  52. package/dist/canvas/deps/booleans.js +2 -1
  53. package/dist/canvas/deps/booleans.js.map +1 -1
  54. package/dist/canvas/deps/hitTestArea.js +2 -2
  55. package/dist/canvas/deps/hitTestArea.js.map +1 -1
  56. package/dist/canvas/deps/insert.js +6 -2
  57. package/dist/canvas/deps/insert.js.map +1 -1
  58. package/dist/canvas/deps/pathEditCommits.js +3 -7
  59. package/dist/canvas/deps/pathEditCommits.js.map +1 -1
  60. package/dist/canvas/drawnBounds.js +11 -3
  61. package/dist/canvas/drawnBounds.js.map +1 -1
  62. package/dist/canvas/inheritedPaints.js +44 -0
  63. package/dist/canvas/inheritedPaints.js.map +1 -0
  64. package/dist/canvas/nodeShapes/derivedPainter.js +15 -16
  65. package/dist/canvas/nodeShapes/derivedPainter.js.map +1 -1
  66. package/dist/canvas/nodeShapes/nodePaint.js +13 -34
  67. package/dist/canvas/nodeShapes/nodePaint.js.map +1 -1
  68. package/dist/canvas/nodeShapes/pathPainter.js +9 -8
  69. package/dist/canvas/nodeShapes/pathPainter.js.map +1 -1
  70. package/dist/canvas/nodeShapes/rectFallbackPainter.js +4 -5
  71. package/dist/canvas/nodeShapes/rectFallbackPainter.js.map +1 -1
  72. package/dist/canvas/nodeShapes/shapePainter.js +11 -10
  73. package/dist/canvas/nodeShapes/shapePainter.js.map +1 -1
  74. package/dist/canvas/nodeShapes/textPainter.js +13 -12
  75. package/dist/canvas/nodeShapes/textPainter.js.map +1 -1
  76. package/dist/canvas/pickReach.js +3 -3
  77. package/dist/canvas/pickReach.js.map +1 -1
  78. package/dist/canvas/pickWalk.js +3 -1
  79. package/dist/canvas/pickWalk.js.map +1 -1
  80. package/dist/canvas/renderSceneToPixels.js +1 -1
  81. package/dist/canvas/renderSceneToPixels.js.map +1 -1
  82. package/dist/canvas/sceneSlotWiring.js +2 -1
  83. package/dist/canvas/sceneSlotWiring.js.map +1 -1
  84. package/dist/canvas/sceneViewRender.js +2 -1
  85. package/dist/canvas/sceneViewRender.js.map +1 -1
  86. package/dist/canvas/surfaceContribution.js +12 -8
  87. package/dist/canvas/surfaceContribution.js.map +1 -1
  88. package/dist/canvas/useCanvasView.js +4 -2
  89. package/dist/canvas/useCanvasView.js.map +1 -1
  90. package/dist/canvas/useLayerStack.js +6 -2
  91. package/dist/canvas/useLayerStack.js.map +1 -1
  92. package/dist/canvas/useViewAim.js +121 -0
  93. package/dist/canvas/useViewAim.js.map +1 -0
  94. package/dist/canvas/useViewNesting.js +3 -2
  95. package/dist/canvas/useViewNesting.js.map +1 -1
  96. package/dist/canvas/useViewRegistrySurface.js +4 -2
  97. package/dist/canvas/useViewRegistrySurface.js.map +1 -1
  98. package/dist/canvas/viewMarkers.js +63 -0
  99. package/dist/canvas/viewMarkers.js.map +1 -0
  100. package/dist/canvas/viewRegistry.js +27 -1
  101. package/dist/canvas/viewRegistry.js.map +1 -1
  102. package/dist/core/pages/pageEdits.js +2 -3
  103. package/dist/core/pages/pageEdits.js.map +1 -1
  104. package/dist/core/paint/meshKindLazy.js +59 -2
  105. package/dist/core/paint/meshKindLazy.js.map +1 -1
  106. package/dist/core/scene/clipNothing.js +13 -0
  107. package/dist/core/scene/clipNothing.js.map +1 -0
  108. package/dist/core/scene/depMemo.js +11 -3
  109. package/dist/core/scene/depMemo.js.map +1 -1
  110. package/dist/core/scene/dependents.js +7 -1
  111. package/dist/core/scene/dependents.js.map +1 -1
  112. package/dist/core/scene/derivedParts.js +10 -11
  113. package/dist/core/scene/derivedParts.js.map +1 -1
  114. package/dist/core/scene/effectivePose.js +7 -2
  115. package/dist/core/scene/effectivePose.js.map +1 -1
  116. package/dist/core/scene/fontReads.js +37 -0
  117. package/dist/core/scene/fontReads.js.map +1 -0
  118. package/dist/core/scene/inheritedPaint.js +75 -0
  119. package/dist/core/scene/inheritedPaint.js.map +1 -0
  120. package/dist/core/scene/kitRegistry.js +5 -0
  121. package/dist/core/scene/kitRegistry.js.map +1 -1
  122. package/dist/core/scene/nodeMemo.js +15 -7
  123. package/dist/core/scene/nodeMemo.js.map +1 -1
  124. package/dist/core/scene/pathEffects/blend.js.map +1 -1
  125. package/dist/core/scene/pathEffects/derive.js +7 -2
  126. package/dist/core/scene/pathEffects/derive.js.map +1 -1
  127. package/dist/core/scene/pathEffects/effects.js +3 -1
  128. package/dist/core/scene/pathEffects/effects.js.map +1 -1
  129. package/dist/core/scene/pathEffects/textAlong.js +95 -0
  130. package/dist/core/scene/pathEffects/textAlong.js.map +1 -0
  131. package/dist/{cornerHandles-BHuoMs1o.d.ts → cornerHandles-p8fDIAKt.d.ts} +2 -1
  132. package/dist/{depSchema-BpZNOv7q.d.ts → depSchema-mG19aEdD.d.ts} +61 -13
  133. package/dist/devtools/registry.js +4 -4
  134. package/dist/devtools/registry.js.map +1 -1
  135. package/dist/features/measure/measureContribution.js +4 -2
  136. package/dist/features/measure/measureContribution.js.map +1 -1
  137. package/dist/features/modes/modeTriggers.js +7 -3
  138. package/dist/features/modes/modeTriggers.js.map +1 -1
  139. package/dist/features/pathEffects/effectSources.js +60 -0
  140. package/dist/features/pathEffects/effectSources.js.map +1 -0
  141. package/dist/features/pathEffects/readSource.js +17 -1
  142. package/dist/features/pathEffects/readSource.js.map +1 -1
  143. package/dist/features/pathEffects/textItems.js +53 -0
  144. package/dist/features/pathEffects/textItems.js.map +1 -0
  145. package/dist/features/pathEffects/useEffectSourcesLayer.js +57 -0
  146. package/dist/features/pathEffects/useEffectSourcesLayer.js.map +1 -0
  147. package/dist/features/paths/pathEditState.js +5 -2
  148. package/dist/features/paths/pathEditState.js.map +1 -1
  149. package/dist/features/text/sceneTextDrafts.js +65 -0
  150. package/dist/features/text/sceneTextDrafts.js.map +1 -0
  151. package/dist/features/text/textHitAt.js +31 -2
  152. package/dist/features/text/textHitAt.js.map +1 -1
  153. package/dist/features/text/useSceneTextEdit.js +22 -18
  154. package/dist/features/text/useSceneTextEdit.js.map +1 -1
  155. package/dist/features/viewports/aim.js +105 -0
  156. package/dist/features/viewports/aim.js.map +1 -0
  157. package/dist/features/viewports/viewMarker.js +82 -0
  158. package/dist/features/viewports/viewMarker.js.map +1 -0
  159. package/dist/features/viewports/viewMarkerDrag.js +55 -0
  160. package/dist/features/viewports/viewMarkerDrag.js.map +1 -0
  161. package/dist/index.d.ts +500 -201
  162. package/dist/index.js +23 -12
  163. package/dist/interactions/actions/booleans/booleans.js +2 -1
  164. package/dist/interactions/actions/booleans/booleans.js.map +1 -1
  165. package/dist/interactions/actions/booleans/operands.js +6 -5
  166. package/dist/interactions/actions/booleans/operands.js.map +1 -1
  167. package/dist/interactions/actions/clipboard/action.js +27 -17
  168. package/dist/interactions/actions/clipboard/action.js.map +1 -1
  169. package/dist/interactions/actions/defaults/anchorEditing.js +44 -23
  170. package/dist/interactions/actions/defaults/anchorEditing.js.map +1 -1
  171. package/dist/interactions/actions/defaults/anchorJoin.js +12 -6
  172. package/dist/interactions/actions/defaults/anchorJoin.js.map +1 -1
  173. package/dist/interactions/actions/defaults/bendSegment.js +7 -4
  174. package/dist/interactions/actions/defaults/bendSegment.js.map +1 -1
  175. package/dist/interactions/actions/defaults/cancelGesture.js +11 -8
  176. package/dist/interactions/actions/defaults/cancelGesture.js.map +1 -1
  177. package/dist/interactions/actions/defaults/contextMenu.js +17 -8
  178. package/dist/interactions/actions/defaults/contextMenu.js.map +1 -1
  179. package/dist/interactions/actions/defaults/createPaintAction.js +1 -1
  180. package/dist/interactions/actions/defaults/createPaintAction.js.map +1 -1
  181. package/dist/interactions/actions/defaults/delete.js +28 -14
  182. package/dist/interactions/actions/defaults/delete.js.map +1 -1
  183. package/dist/interactions/actions/defaults/duplicate.js +11 -8
  184. package/dist/interactions/actions/defaults/duplicate.js.map +1 -1
  185. package/dist/interactions/actions/defaults/enterPathEdit.js +19 -9
  186. package/dist/interactions/actions/defaults/enterPathEdit.js.map +1 -1
  187. package/dist/interactions/actions/defaults/escape.js +11 -8
  188. package/dist/interactions/actions/defaults/escape.js.map +1 -1
  189. package/dist/interactions/actions/defaults/exitPathEdit.js +7 -4
  190. package/dist/interactions/actions/defaults/exitPathEdit.js.map +1 -1
  191. package/dist/interactions/actions/defaults/group.js +19 -13
  192. package/dist/interactions/actions/defaults/group.js.map +1 -1
  193. package/dist/interactions/actions/defaults/ingest.js +10 -4
  194. package/dist/interactions/actions/defaults/ingest.js.map +1 -1
  195. package/dist/interactions/actions/defaults/insertPathAnchor.js +7 -4
  196. package/dist/interactions/actions/defaults/insertPathAnchor.js.map +1 -1
  197. package/dist/interactions/actions/defaults/nudge.js +8 -2
  198. package/dist/interactions/actions/defaults/nudge.js.map +1 -1
  199. package/dist/interactions/actions/defaults/pathEditTools.js +11 -8
  200. package/dist/interactions/actions/defaults/pathEditTools.js.map +1 -1
  201. package/dist/interactions/actions/defaults/pinchZoom.js +7 -4
  202. package/dist/interactions/actions/defaults/pinchZoom.js.map +1 -1
  203. package/dist/interactions/actions/defaults/reorder.js +24 -6
  204. package/dist/interactions/actions/defaults/reorder.js.map +1 -1
  205. package/dist/interactions/actions/defaults/selectAll.js +8 -5
  206. package/dist/interactions/actions/defaults/selectAll.js.map +1 -1
  207. package/dist/interactions/actions/defaults/setFill.js +7 -3
  208. package/dist/interactions/actions/defaults/setFill.js.map +1 -1
  209. package/dist/interactions/actions/defaults/setFillOpacity.js +1 -1
  210. package/dist/interactions/actions/defaults/setStroke.js +12 -11
  211. package/dist/interactions/actions/defaults/setStroke.js.map +1 -1
  212. package/dist/interactions/actions/defaults/setStrokeOpacity.js +1 -1
  213. package/dist/interactions/actions/defaults/toolActivate.js +7 -1
  214. package/dist/interactions/actions/defaults/toolActivate.js.map +1 -1
  215. package/dist/interactions/actions/defaults/toolResetToDefault.js +7 -4
  216. package/dist/interactions/actions/defaults/toolResetToDefault.js.map +1 -1
  217. package/dist/interactions/actions/defaults/undoRedo.js +19 -13
  218. package/dist/interactions/actions/defaults/undoRedo.js.map +1 -1
  219. package/dist/interactions/actions/defaults/viewportWheelPan.js +16 -7
  220. package/dist/interactions/actions/defaults/viewportWheelPan.js.map +1 -1
  221. package/dist/interactions/actions/defaults/viewportZoom.js +24 -6
  222. package/dist/interactions/actions/defaults/viewportZoom.js.map +1 -1
  223. package/dist/interactions/actions/edit-anchors/action.js +7 -4
  224. package/dist/interactions/actions/edit-anchors/action.js.map +1 -1
  225. package/dist/interactions/actions/flip/action.js +8 -2
  226. package/dist/interactions/actions/flip/action.js.map +1 -1
  227. package/dist/interactions/actions/insert/repositionAction.js +2 -1
  228. package/dist/interactions/actions/insert/repositionAction.js.map +1 -1
  229. package/dist/interactions/actions/move/action.js +7 -4
  230. package/dist/interactions/actions/move/action.js.map +1 -1
  231. package/dist/interactions/actions/outlines/createOutlinesAction.js +8 -5
  232. package/dist/interactions/actions/outlines/createOutlinesAction.js.map +1 -1
  233. package/dist/interactions/actions/outlines/outlineStroke.js +5 -3
  234. package/dist/interactions/actions/outlines/outlineStroke.js.map +1 -1
  235. package/dist/interactions/actions/pathCommands/cutJoin.js +8 -5
  236. package/dist/interactions/actions/pathCommands/cutJoin.js.map +1 -1
  237. package/dist/interactions/actions/resize/cornerHandles.js +2 -1
  238. package/dist/interactions/actions/resize/cornerHandles.js.map +1 -1
  239. package/dist/interactions/actions/selectionSubtree.js +12 -7
  240. package/dist/interactions/actions/selectionSubtree.js.map +1 -1
  241. package/dist/interactions/actions/transform-again/action.js +8 -5
  242. package/dist/interactions/actions/transform-again/action.js.map +1 -1
  243. package/dist/json.d.ts +4 -0
  244. package/dist/json.js +3 -0
  245. package/dist/kitSettingsSchema.js +8 -1
  246. package/dist/kitSettingsSchema.js.map +1 -1
  247. package/dist/{math-CrJULaZI.d.ts → math-B56zxVs5.d.ts} +10 -1
  248. package/dist/math.d.ts +1 -1
  249. package/dist/math.js +2 -1
  250. package/dist/resize.d.ts +1 -1
  251. package/dist/test-seams.d.ts +2 -2
  252. package/dist/tools/builtin/ellipse/useEllipseTool.js +1 -0
  253. package/dist/tools/builtin/ellipse/useEllipseTool.js.map +1 -1
  254. package/dist/tools/builtin/eyedropper/useEyedropperTool.js +2 -1
  255. package/dist/tools/builtin/eyedropper/useEyedropperTool.js.map +1 -1
  256. package/dist/tools/builtin/hand/dragPanContribution.js +4 -1
  257. package/dist/tools/builtin/hand/dragPanContribution.js.map +1 -1
  258. package/dist/tools/builtin/hand/useHandTool.js +7 -4
  259. package/dist/tools/builtin/hand/useHandTool.js.map +1 -1
  260. package/dist/tools/builtin/image/useImageTool.js +1 -0
  261. package/dist/tools/builtin/image/useImageTool.js.map +1 -1
  262. package/dist/tools/builtin/lasso/useLassoTool.js +1 -0
  263. package/dist/tools/builtin/lasso/useLassoTool.js.map +1 -1
  264. package/dist/tools/builtin/line/useLineTool.js +1 -0
  265. package/dist/tools/builtin/line/useLineTool.js.map +1 -1
  266. package/dist/tools/builtin/measure/useMeasureTool.js +12 -5
  267. package/dist/tools/builtin/measure/useMeasureTool.js.map +1 -1
  268. package/dist/tools/builtin/pen/usePenTool.js +14 -6
  269. package/dist/tools/builtin/pen/usePenTool.js.map +1 -1
  270. package/dist/tools/builtin/pencil/usePencilTool.js +1 -0
  271. package/dist/tools/builtin/pencil/usePencilTool.js.map +1 -1
  272. package/dist/tools/builtin/polygon/usePolygonTool.js +13 -4
  273. package/dist/tools/builtin/polygon/usePolygonTool.js.map +1 -1
  274. package/dist/tools/builtin/rect/useRectTool.js +1 -0
  275. package/dist/tools/builtin/rect/useRectTool.js.map +1 -1
  276. package/dist/tools/builtin/select/adapterPointQuery.js +3 -2
  277. package/dist/tools/builtin/select/adapterPointQuery.js.map +1 -1
  278. package/dist/tools/builtin/select/pressDecision.js +32 -0
  279. package/dist/tools/builtin/select/pressDecision.js.map +1 -0
  280. package/dist/tools/builtin/select/selectionContributions.js +22 -13
  281. package/dist/tools/builtin/select/selectionContributions.js.map +1 -1
  282. package/dist/tools/builtin/select/useSelectTool.js +13 -10
  283. package/dist/tools/builtin/select/useSelectTool.js.map +1 -1
  284. package/dist/tools/builtin/slice/useSliceTool.js +22 -8
  285. package/dist/tools/builtin/slice/useSliceTool.js.map +1 -1
  286. package/dist/tools/builtin/star/useStarTool.js +13 -4
  287. package/dist/tools/builtin/star/useStarTool.js.map +1 -1
  288. package/dist/tools/builtin/sticky/stickyNode.js +4 -1
  289. package/dist/tools/builtin/sticky/stickyNode.js.map +1 -1
  290. package/dist/tools/builtin/sticky/useStickyTool.js +10 -5
  291. package/dist/tools/builtin/sticky/useStickyTool.js.map +1 -1
  292. package/dist/tools/builtin/text/useTextTool.js +3 -1
  293. package/dist/tools/builtin/text/useTextTool.js.map +1 -1
  294. package/dist/util/paint.js +1 -3
  295. package/dist/util/paint.js.map +1 -1
  296. package/dist/version.js +1 -1
  297. package/package.json +28 -24
@@ -1 +1 @@
1
- {"version":3,"file":"sceneViewRender.js","names":["viewToMat3"],"sources":["../../src/canvas/sceneViewRender.ts"],"sourcesContent":["/**\n * `renderSceneToCanvas` — substrate for detached, read-only scene views\n * (`<SceneViewCanvas>` / `<MinimapCanvas>`). Given a `<canvas>`, a `Scene`,\n * a `View`, and a per-node `drawOne`, render one frame.\n */\n\nimport type { WeaselRenderer } from '@weasel-js/renderer';\nimport { cachedCanvasRenderer, paintCanvas } from './canvasRenderer';\nimport { viewToMat3 } from '@weasel-js/geom';\nimport type { DrawCommand } from '@weasel-js/renderer';\nimport type { View } from '../core/viewport/view';\nimport type { Node, NodeId, Scene } from '../core/scene/types';\nimport { wrapNodeOutput } from './wrapNodeOutput';\nimport { buildSceneTree, type HierarchicalAdapter } from './buildSceneTree';\nimport { sceneToAdapter } from './sceneAdapter';\nimport { withDerivedPaths, sceneDerivedPathOf } from './derivedPath';\nimport { withColorOverrides } from './colorOverrides';\nimport type { ColorOverrideRegistry } from '@weasel-js/animator';\nimport type { SceneViewDrawOne } from './NodeShape';\nimport { paintedSceneLayers } from './sceneLayerPaint';\nimport { cullDrawCommands } from '@weasel-js/renderer';\nimport { paintMissesView, type PaintBoundsFn } from './paintCull';\nimport { paintBoundsFor } from './defaultDrawOne';\n\nexport type { SceneViewDrawOne } from './NodeShape';\n\n/**\n * Which scene layers a detached view paints, on top of each layer's own\n * `visible` flag. Keyed the way `<SceneCanvas>` keys its scene layers —\n * `scene:<layerId>` — so the map a main canvas takes serves its minimap too.\n */\nexport interface SceneViewLayers {\n /** `false` hides that layer in this view. It cannot show one the scene hides. */\n layerVisibility?: Record<string, boolean>;\n /** Paint order, bottom first. A listed order is the whole list: a scene\n * layer left out of it is not painted. */\n layerOrder?: readonly string[];\n}\n\n/** The view size `buildSceneViewCommands` culls to, and how it bounds a\n * node's paint before painting it. */\nexport interface SceneViewCull<TData, TLayer extends string, TPose> {\n width: number;\n height: number;\n paintBounds?: PaintBoundsFn<Node<TData, TLayer, TPose>, TPose>;\n}\n\n/** What to draw into an existing canvas: the scene, the view, and the same\n * painting hooks `<SceneCanvas>` takes. */\nexport interface RenderSceneToCanvasArgs<TData, TLayer extends string, TPose> {\n /** The DOM canvas to paint into. The first call on a given canvas creates\n * its renderer and later calls reuse it;\n * `releaseCanvasRenderer(canvas)` frees it. */\n canvas: HTMLCanvasElement;\n scene: Scene<TData, TLayer, TPose>;\n /** Camera view applied to scene and `extraCommands` alike. */\n view: View;\n /** CSS-pixel size. The drawing buffer is set to `width × dpr` and\n * `height × dpr`; the element's CSS size is the caller's to set. */\n width: number;\n height: number;\n drawOne: SceneViewDrawOne<TData, TLayer, TPose>;\n /** Commands appended after the scene-node output, wrapped in the same view\n * transform as scene commands. */\n extraCommands?: DrawCommand[];\n /** Per-id alpha multiplier, mirroring `<SceneCanvas>`'s scene-slot\n * `alphaFor`. Defaults to `() => 1`. */\n alphaFor?: (id: string) => number;\n /** Per-view layer visibility. See {@link SceneViewLayers}. */\n layerVisibility?: SceneViewLayers['layerVisibility'];\n /** Per-view layer order. See {@link SceneViewLayers}. */\n layerOrder?: SceneViewLayers['layerOrder'];\n /** Animated vertex colors to paint, typically an animator's\n * `colorOverrides` — see `NodePaintCtx.vertexColors`. */\n colorOverrides?: ColorOverrideRegistry;\n /** Skip what cannot reach the `width × height` view: nodes `paintBounds`\n * places outside it are not painted, and scene commands that cannot reach\n * it are dropped before the renderer — the scene slot's `cull`. Off by\n * default. `extraCommands` are never culled. */\n cull?: boolean;\n /** Where `drawOne`'s output for a node can reach, as on the scene slot.\n * Defaults to `defaultPaintBounds` when `drawOne` is `defaultDrawOne`. */\n paintBounds?: PaintBoundsFn<Node<TData, TLayer, TPose>, TPose>;\n /** Device-pixel ratio. Defaults to `window.devicePixelRatio || 1` when\n * available, otherwise 1. */\n dpr?: number;\n}\n\n/** Test seam: the renderer `renderSceneToCanvas` created for a canvas. Not\n * re-exported from the barrel. */\nexport function __getCachedRendererForTest(\n canvas: HTMLCanvasElement,\n): WeaselRenderer | undefined {\n return cachedCanvasRenderer(canvas);\n}\n\n/**\n * Present a `Scene` as the reading surface `buildSceneTree` walks: the scene\n * adapter's own reads, under this view's layer list.\n */\nfunction sceneAsHierarchy<TData, TLayer extends string, TPose>(\n scene: Scene<TData, TLayer, TPose>,\n layers: SceneViewLayers = {},\n): HierarchicalAdapter<Node<TData, TLayer, TPose>, TPose> {\n const { getNode, getChildren, getPose, composePose } = sceneToAdapter(scene);\n return {\n getLayers: () =>\n paintedSceneLayers(scene.layers, layers)\n .map((l) => ({ id: l.id, visible: l.visible, parallax: l.parallax })),\n getNode,\n getChildren,\n getPose,\n ...(composePose ? { composePose } : {}),\n };\n}\n\n/**\n * Build the DrawCommand list that `renderSceneToCanvas` would dispatch, in\n * screen space. Pure — no GL, no DOM. Walks the scene through `buildSceneTree`,\n * the same walk `<SceneCanvas>` uses, which is what keeps hidden layers\n * unpainted and container clips applied here.\n *\n * Each node's commands go through `wrapNodeOutput` — pose rotation, then the\n * `alphaFor` multiplier — so a `drawOne` that rotates its own output rotates\n * twice; emit unrotated geometry and let the pose drive it.\n *\n * Output is one group per **visible** layer, in scene-layer order (or\n * `layers.layerOrder`), each holding one group per node on that layer.\n */\nexport function buildSceneViewCommands<TData, TLayer extends string, TPose>(\n scene: Scene<TData, TLayer, TPose>,\n view: View,\n drawOne: SceneViewDrawOne<TData, TLayer, TPose>,\n extraCommands?: ReadonlyArray<DrawCommand>,\n alphaFor?: (id: string) => number,\n /** This view's own layer visibility and order. */\n layers?: SceneViewLayers,\n /** Animated vertex colors to paint, typically an animator's `colorOverrides`. */\n colorOverrides?: ColorOverrideRegistry,\n /** Cull the scene to this CSS-pixel view size — see\n * `RenderSceneToCanvasArgs.cull`. Omit to paint every node. */\n cull?: SceneViewCull<TData, TLayer, TPose>,\n): DrawCommand[] {\n const derivedOnly = withDerivedPaths(scene, drawOne);\n const derived = colorOverrides ? withColorOverrides(colorOverrides, derivedOnly) : derivedOnly;\n const derivedPathOf = sceneDerivedPathOf(scene);\n const wrappedDrawOne = (\n node: Node<TData, TLayer, TPose>,\n pose: TPose,\n v: View,\n ): DrawCommand[] => {\n const overrideAlpha = scene.overrides.read(node.id as NodeId)?.alpha ?? 1;\n return wrapNodeOutput(\n derived(node, pose, v),\n pose,\n (alphaFor ? alphaFor(node.id) : 1) * overrideAlpha,\n );\n };\n\n const hierarchy = sceneAsHierarchy(scene, layers);\n\n const cullRect = cull ? { x: 0, y: 0, width: cull.width, height: cull.height } : undefined;\n const culled = cullRect && cull?.paintBounds ? paintMissesView(cull.paintBounds, view, cullRect) : undefined;\n const tree: DrawCommand[] = buildSceneTree(\n hierarchy as Parameters<typeof buildSceneTree>[0],\n wrappedDrawOne as unknown as Parameters<typeof buildSceneTree>[1],\n view,\n undefined,\n derivedPathOf as unknown as Parameters<typeof buildSceneTree>[4],\n culled as Parameters<typeof buildSceneTree>[5],\n );\n const children = cullRect ? cullDrawCommands(tree, viewToMat3(view), cullRect) : tree;\n if (extraCommands && extraCommands.length > 0) {\n for (const cmd of extraCommands) children.push(cmd);\n }\n return [{\n kind: 'group',\n transform: viewToMat3(view),\n children,\n }];\n}\n\n/**\n * The commands `renderSceneToCanvas` paints for one frame — everything in\n * `args` but the canvas. `<SceneViewCanvas>` paints these through its own\n * renderer lease.\n */\nexport function sceneViewFrame<TData, TLayer extends string, TPose>(\n args: Omit<RenderSceneToCanvasArgs<TData, TLayer, TPose>, 'canvas'>,\n): DrawCommand[] {\n const {\n scene, view, width, height, drawOne, extraCommands, alphaFor, layerVisibility, layerOrder,\n colorOverrides, cull,\n } = args;\n return buildSceneViewCommands(\n scene, view, drawOne, extraCommands, alphaFor, { layerVisibility, layerOrder }, colorOverrides,\n cull ? { width, height, paintBounds: args.paintBounds ?? paintBoundsFor(drawOne) } : undefined,\n );\n}\n\n/**\n * Render one frame of `scene` at `view` into `canvas`. Where no WebGL2\n * context is available (jsdom) the call paints nothing. The canvas's renderer\n * lives until `releaseCanvasRenderer(canvas)`.\n */\nexport function renderSceneToCanvas<TData, TLayer extends string, TPose>(\n args: RenderSceneToCanvasArgs<TData, TLayer, TPose>,\n): void {\n const { canvas, view, width, height, dpr } = args;\n paintCanvas(canvas, sceneViewFrame(args), view, { width, height, dpr });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAoGA,SAAS,iBACP,OACA,SAA0B,CAAC,GAC6B;CACxD,MAAM,EAAE,SAAS,aAAa,SAAS,gBAAgB,eAAe,KAAK;CAC3E,OAAO;EACL,iBACE,mBAAmB,MAAM,QAAQ,MAAM,CAAC,CACrC,KAAK,OAAO;GAAE,IAAI,EAAE;GAAI,SAAS,EAAE;GAAS,UAAU,EAAE;EAAS,EAAE;EACxE;EACA;EACA;EACA,GAAI,cAAc,EAAE,YAAY,IAAI,CAAC;CACvC;AACF;;;;;;;;;;;;;;AAeA,SAAgB,uBACd,OACA,MACA,SACA,eACA,UAEA,QAEA,gBAGA,MACe;CACf,MAAM,cAAc,iBAAiB,OAAO,OAAO;CACnD,MAAM,UAAU,iBAAiB,mBAAmB,gBAAgB,WAAW,IAAI;CACnF,MAAM,gBAAgB,mBAAmB,KAAK;CAC9C,MAAM,kBACJ,MACA,MACA,MACkB;EAClB,MAAM,gBAAgB,MAAM,UAAU,KAAK,KAAK,EAAY,CAAC,EAAE,SAAS;EACxE,OAAO,eACL,QAAQ,MAAM,MAAM,CAAC,GACrB,OACC,WAAW,SAAS,KAAK,EAAE,IAAI,KAAK,aACvC;CACF;CAEA,MAAM,YAAY,iBAAiB,OAAO,MAAM;CAEhD,MAAM,WAAW,OAAO;EAAE,GAAG;EAAG,GAAG;EAAG,OAAO,KAAK;EAAO,QAAQ,KAAK;CAAO,IAAI;CACjF,MAAM,SAAS,YAAY,MAAM,cAAc,gBAAgB,KAAK,aAAa,MAAM,QAAQ,IAAI;CACnG,MAAM,OAAsB,eAC1B,WACA,gBACA,MACA,QACA,eACA,MACF;CACA,MAAM,WAAW,WAAW,iBAAiB,MAAMA,aAAW,IAAI,GAAG,QAAQ,IAAI;CACjF,IAAI,iBAAiB,cAAc,SAAS,GAC1C,KAAK,MAAM,OAAO,eAAe,SAAS,KAAK,GAAG;CAEpD,OAAO,CAAC;EACN,MAAM;EACN,WAAWA,aAAW,IAAI;EAC1B;CACF,CAAC;AACH;;;;;;AAOA,SAAgB,eACd,MACe;CACf,MAAM,EACJ,OAAO,MAAM,OAAO,QAAQ,SAAS,eAAe,UAAU,iBAAiB,YAC/E,gBAAgB,SACd;CACJ,OAAO,uBACL,OAAO,MAAM,SAAS,eAAe,UAAU;EAAE;EAAiB;CAAW,GAAG,gBAChF,OAAO;EAAE;EAAO;EAAQ,aAAa,KAAK,eAAe,eAAe,OAAO;CAAE,IAAI,MACvF;AACF;;;;;;AAOA,SAAgB,oBACd,MACM;CACN,MAAM,EAAE,QAAQ,MAAM,OAAO,QAAQ,QAAQ;CAC7C,YAAY,QAAQ,eAAe,IAAI,GAAG,MAAM;EAAE;EAAO;EAAQ;CAAI,CAAC;AACxE"}
1
+ {"version":3,"file":"sceneViewRender.js","names":["viewToMat3"],"sources":["../../src/canvas/sceneViewRender.ts"],"sourcesContent":["/**\n * `renderSceneToCanvas` — substrate for detached, read-only scene views\n * (`<SceneViewCanvas>` / `<MinimapCanvas>`). Given a `<canvas>`, a `Scene`,\n * a `View`, and a per-node `drawOne`, render one frame.\n */\n\nimport type { WeaselRenderer } from '@weasel-js/renderer';\nimport { cachedCanvasRenderer, paintCanvas } from './canvasRenderer';\nimport { viewToMat3 } from '@weasel-js/geom';\nimport type { DrawCommand } from '@weasel-js/renderer';\nimport type { View } from '../core/viewport/view';\nimport type { Node, NodeId, Scene } from '../core/scene/types';\nimport { wrapNodeOutput } from './wrapNodeOutput';\nimport { buildSceneTree, type HierarchicalAdapter } from './buildSceneTree';\nimport { sceneToAdapter } from './sceneAdapter';\nimport { withInheritedPaints } from './inheritedPaints';\nimport { withDerivedPaths, sceneDerivedPathOf } from './derivedPath';\nimport { withColorOverrides } from './colorOverrides';\nimport type { ColorOverrideRegistry } from '@weasel-js/animator';\nimport type { SceneViewDrawOne } from './NodeShape';\nimport { paintedSceneLayers } from './sceneLayerPaint';\nimport { cullDrawCommands } from '@weasel-js/renderer';\nimport { paintMissesView, type PaintBoundsFn } from './paintCull';\nimport { paintBoundsFor } from './defaultDrawOne';\n\nexport type { SceneViewDrawOne } from './NodeShape';\n\n/**\n * Which scene layers a detached view paints, on top of each layer's own\n * `visible` flag. Keyed the way `<SceneCanvas>` keys its scene layers —\n * `scene:<layerId>` — so the map a main canvas takes serves its minimap too.\n */\nexport interface SceneViewLayers {\n /** `false` hides that layer in this view. It cannot show one the scene hides. */\n layerVisibility?: Record<string, boolean>;\n /** Paint order, bottom first. A listed order is the whole list: a scene\n * layer left out of it is not painted. */\n layerOrder?: readonly string[];\n}\n\n/** The view size `buildSceneViewCommands` culls to, and how it bounds a\n * node's paint before painting it. */\nexport interface SceneViewCull<TData, TLayer extends string, TPose> {\n width: number;\n height: number;\n paintBounds?: PaintBoundsFn<Node<TData, TLayer, TPose>, TPose>;\n}\n\n/** What to draw into an existing canvas: the scene, the view, and the same\n * painting hooks `<SceneCanvas>` takes. */\nexport interface RenderSceneToCanvasArgs<TData, TLayer extends string, TPose> {\n /** The DOM canvas to paint into. The first call on a given canvas creates\n * its renderer and later calls reuse it;\n * `releaseCanvasRenderer(canvas)` frees it. */\n canvas: HTMLCanvasElement;\n scene: Scene<TData, TLayer, TPose>;\n /** Camera view applied to scene and `extraCommands` alike. */\n view: View;\n /** CSS-pixel size. The drawing buffer is set to `width × dpr` and\n * `height × dpr`; the element's CSS size is the caller's to set. */\n width: number;\n height: number;\n drawOne: SceneViewDrawOne<TData, TLayer, TPose>;\n /** Commands appended after the scene-node output, wrapped in the same view\n * transform as scene commands. */\n extraCommands?: DrawCommand[];\n /** Per-id alpha multiplier, mirroring `<SceneCanvas>`'s scene-slot\n * `alphaFor`. Defaults to `() => 1`. */\n alphaFor?: (id: string) => number;\n /** Per-view layer visibility. See {@link SceneViewLayers}. */\n layerVisibility?: SceneViewLayers['layerVisibility'];\n /** Per-view layer order. See {@link SceneViewLayers}. */\n layerOrder?: SceneViewLayers['layerOrder'];\n /** Animated vertex colors to paint, typically an animator's\n * `colorOverrides` — see `NodePaintCtx.vertexColors`. */\n colorOverrides?: ColorOverrideRegistry;\n /** Skip what cannot reach the `width × height` view: nodes `paintBounds`\n * places outside it are not painted, and scene commands that cannot reach\n * it are dropped before the renderer — the scene slot's `cull`. Off by\n * default. `extraCommands` are never culled. */\n cull?: boolean;\n /** Where `drawOne`'s output for a node can reach, as on the scene slot.\n * Defaults to `defaultPaintBounds` when `drawOne` is `defaultDrawOne`. */\n paintBounds?: PaintBoundsFn<Node<TData, TLayer, TPose>, TPose>;\n /** Device-pixel ratio. Defaults to `window.devicePixelRatio || 1` when\n * available, otherwise 1. */\n dpr?: number;\n}\n\n/** Test seam: the renderer `renderSceneToCanvas` created for a canvas. Not\n * re-exported from the barrel. */\nexport function __getCachedRendererForTest(\n canvas: HTMLCanvasElement,\n): WeaselRenderer | undefined {\n return cachedCanvasRenderer(canvas);\n}\n\n/**\n * Present a `Scene` as the reading surface `buildSceneTree` walks: the scene\n * adapter's own reads, under this view's layer list.\n */\nfunction sceneAsHierarchy<TData, TLayer extends string, TPose>(\n scene: Scene<TData, TLayer, TPose>,\n layers: SceneViewLayers = {},\n): HierarchicalAdapter<Node<TData, TLayer, TPose>, TPose> {\n const { getNode, getChildren, getPose, composePose } = sceneToAdapter(scene);\n return {\n getLayers: () =>\n paintedSceneLayers(scene.layers, layers)\n .map((l) => ({ id: l.id, visible: l.visible, parallax: l.parallax })),\n getNode,\n getChildren,\n getPose,\n ...(composePose ? { composePose } : {}),\n };\n}\n\n/**\n * Build the DrawCommand list that `renderSceneToCanvas` would dispatch, in\n * screen space. Pure — no GL, no DOM. Walks the scene through `buildSceneTree`,\n * the same walk `<SceneCanvas>` uses, which is what keeps hidden layers\n * unpainted and container clips applied here.\n *\n * Each node's commands go through `wrapNodeOutput` — pose rotation, then the\n * `alphaFor` multiplier — so a `drawOne` that rotates its own output rotates\n * twice; emit unrotated geometry and let the pose drive it.\n *\n * Output is one group per **visible** layer, in scene-layer order (or\n * `layers.layerOrder`), each holding one group per node on that layer.\n */\nexport function buildSceneViewCommands<TData, TLayer extends string, TPose>(\n scene: Scene<TData, TLayer, TPose>,\n view: View,\n drawOne: SceneViewDrawOne<TData, TLayer, TPose>,\n extraCommands?: ReadonlyArray<DrawCommand>,\n alphaFor?: (id: string) => number,\n /** This view's own layer visibility and order. */\n layers?: SceneViewLayers,\n /** Animated vertex colors to paint, typically an animator's `colorOverrides`. */\n colorOverrides?: ColorOverrideRegistry,\n /** Cull the scene to this CSS-pixel view size — see\n * `RenderSceneToCanvasArgs.cull`. Omit to paint every node. */\n cull?: SceneViewCull<TData, TLayer, TPose>,\n): DrawCommand[] {\n const derivedOnly = withDerivedPaths(scene, withInheritedPaints(scene, drawOne));\n const derived = colorOverrides ? withColorOverrides(colorOverrides, derivedOnly) : derivedOnly;\n const derivedPathOf = sceneDerivedPathOf(scene);\n const wrappedDrawOne = (\n node: Node<TData, TLayer, TPose>,\n pose: TPose,\n v: View,\n ): DrawCommand[] => {\n const overrideAlpha = scene.overrides.read(node.id as NodeId)?.alpha ?? 1;\n return wrapNodeOutput(\n derived(node, pose, v),\n pose,\n (alphaFor ? alphaFor(node.id) : 1) * overrideAlpha,\n );\n };\n\n const hierarchy = sceneAsHierarchy(scene, layers);\n\n const cullRect = cull ? { x: 0, y: 0, width: cull.width, height: cull.height } : undefined;\n const culled = cullRect && cull?.paintBounds ? paintMissesView(cull.paintBounds, view, cullRect) : undefined;\n const tree: DrawCommand[] = buildSceneTree(\n hierarchy as Parameters<typeof buildSceneTree>[0],\n wrappedDrawOne as unknown as Parameters<typeof buildSceneTree>[1],\n view,\n undefined,\n derivedPathOf as unknown as Parameters<typeof buildSceneTree>[4],\n culled as Parameters<typeof buildSceneTree>[5],\n );\n const children = cullRect ? cullDrawCommands(tree, viewToMat3(view), cullRect) : tree;\n if (extraCommands && extraCommands.length > 0) {\n for (const cmd of extraCommands) children.push(cmd);\n }\n return [{\n kind: 'group',\n transform: viewToMat3(view),\n children,\n }];\n}\n\n/**\n * The commands `renderSceneToCanvas` paints for one frame — everything in\n * `args` but the canvas. `<SceneViewCanvas>` paints these through its own\n * renderer lease.\n */\nexport function sceneViewFrame<TData, TLayer extends string, TPose>(\n args: Omit<RenderSceneToCanvasArgs<TData, TLayer, TPose>, 'canvas'>,\n): DrawCommand[] {\n const {\n scene, view, width, height, drawOne, extraCommands, alphaFor, layerVisibility, layerOrder,\n colorOverrides, cull,\n } = args;\n return buildSceneViewCommands(\n scene, view, drawOne, extraCommands, alphaFor, { layerVisibility, layerOrder }, colorOverrides,\n cull ? { width, height, paintBounds: args.paintBounds ?? paintBoundsFor(drawOne) } : undefined,\n );\n}\n\n/**\n * Render one frame of `scene` at `view` into `canvas`. Where no WebGL2\n * context is available (jsdom) the call paints nothing. The canvas's renderer\n * lives until `releaseCanvasRenderer(canvas)`.\n */\nexport function renderSceneToCanvas<TData, TLayer extends string, TPose>(\n args: RenderSceneToCanvasArgs<TData, TLayer, TPose>,\n): void {\n const { canvas, view, width, height, dpr } = args;\n paintCanvas(canvas, sceneViewFrame(args), view, { width, height, dpr });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAqGA,SAAS,iBACP,OACA,SAA0B,CAAC,GAC6B;CACxD,MAAM,EAAE,SAAS,aAAa,SAAS,gBAAgB,eAAe,KAAK;CAC3E,OAAO;EACL,iBACE,mBAAmB,MAAM,QAAQ,MAAM,CAAC,CACrC,KAAK,OAAO;GAAE,IAAI,EAAE;GAAI,SAAS,EAAE;GAAS,UAAU,EAAE;EAAS,EAAE;EACxE;EACA;EACA;EACA,GAAI,cAAc,EAAE,YAAY,IAAI,CAAC;CACvC;AACF;;;;;;;;;;;;;;AAeA,SAAgB,uBACd,OACA,MACA,SACA,eACA,UAEA,QAEA,gBAGA,MACe;CACf,MAAM,cAAc,iBAAiB,OAAO,oBAAoB,OAAO,OAAO,CAAC;CAC/E,MAAM,UAAU,iBAAiB,mBAAmB,gBAAgB,WAAW,IAAI;CACnF,MAAM,gBAAgB,mBAAmB,KAAK;CAC9C,MAAM,kBACJ,MACA,MACA,MACkB;EAClB,MAAM,gBAAgB,MAAM,UAAU,KAAK,KAAK,EAAY,CAAC,EAAE,SAAS;EACxE,OAAO,eACL,QAAQ,MAAM,MAAM,CAAC,GACrB,OACC,WAAW,SAAS,KAAK,EAAE,IAAI,KAAK,aACvC;CACF;CAEA,MAAM,YAAY,iBAAiB,OAAO,MAAM;CAEhD,MAAM,WAAW,OAAO;EAAE,GAAG;EAAG,GAAG;EAAG,OAAO,KAAK;EAAO,QAAQ,KAAK;CAAO,IAAI;CACjF,MAAM,SAAS,YAAY,MAAM,cAAc,gBAAgB,KAAK,aAAa,MAAM,QAAQ,IAAI;CACnG,MAAM,OAAsB,eAC1B,WACA,gBACA,MACA,QACA,eACA,MACF;CACA,MAAM,WAAW,WAAW,iBAAiB,MAAMA,aAAW,IAAI,GAAG,QAAQ,IAAI;CACjF,IAAI,iBAAiB,cAAc,SAAS,GAC1C,KAAK,MAAM,OAAO,eAAe,SAAS,KAAK,GAAG;CAEpD,OAAO,CAAC;EACN,MAAM;EACN,WAAWA,aAAW,IAAI;EAC1B;CACF,CAAC;AACH;;;;;;AAOA,SAAgB,eACd,MACe;CACf,MAAM,EACJ,OAAO,MAAM,OAAO,QAAQ,SAAS,eAAe,UAAU,iBAAiB,YAC/E,gBAAgB,SACd;CACJ,OAAO,uBACL,OAAO,MAAM,SAAS,eAAe,UAAU;EAAE;EAAiB;CAAW,GAAG,gBAChF,OAAO;EAAE;EAAO;EAAQ,aAAa,KAAK,eAAe,eAAe,OAAO;CAAE,IAAI,MACvF;AACF;;;;;;AAOA,SAAgB,oBACd,MACM;CACN,MAAM,EAAE,QAAQ,MAAM,OAAO,QAAQ,QAAQ;CAC7C,YAAY,QAAQ,eAAe,IAAI,GAAG,MAAM;EAAE;EAAO;EAAQ;CAAI,CAAC;AACxE"}
@@ -8,19 +8,23 @@ import { mergeContributions } from "@weasel-js/routing";
8
8
  */
9
9
  /**
10
10
  * Concatenate contribution bundles into one registry, preserving order. Throws
11
- * on a duplicate entry id, dep name or view id — two features answering for
12
- * the same name is a bug in whichever was added second.
11
+ * on a duplicate entry id, dep name, view id or marker id — two features
12
+ * answering for the same name is a bug in whichever was added second.
13
13
  */
14
14
  function mergeContributions$1(...bundles) {
15
15
  const out = mergeContributions(...bundles);
16
- const viewOwner = /* @__PURE__ */ new Map();
17
- for (const entry of out) for (const view of entry.views ?? []) {
18
- const owner = viewOwner.get(view.id);
19
- if (owner !== void 0) throw new Error(`mergeContributions: view "${view.id}" is added by both "${owner}" and "${entry.id}". View ids are unique per surface; rename one.`);
20
- viewOwner.set(view.id, entry.id);
21
- }
16
+ claimIds(out, "view", (entry) => entry.views ?? []);
17
+ claimIds(out, "marker", (entry) => entry.markers ?? []);
22
18
  return out;
23
19
  }
20
+ function claimIds(entries, kind, of) {
21
+ const owners = /* @__PURE__ */ new Map();
22
+ for (const entry of entries) for (const { id } of of(entry)) {
23
+ const owner = owners.get(id);
24
+ if (owner !== void 0) throw new Error(`mergeContributions: ${kind} "${id}" is added by both "${owner}" and "${entry.id}". ${kind === "view" ? "View" : "Marker"} ids are unique per surface; rename one.`);
25
+ owners.set(id, entry.id);
26
+ }
27
+ }
24
28
 
25
29
  //#endregion
26
30
  export { mergeContributions$1 as mergeContributions };
@@ -1 +1 @@
1
- {"version":3,"file":"surfaceContribution.js","names":["mergeContributions","mergeRoutingContributions"],"sources":["../../src/canvas/surfaceContribution.ts"],"sourcesContent":["/**\n * The canvas's unit of extension: a routing `Contribution` plus the roles only\n * a surface can install — views on it, and whatever else needs the mounted\n * canvas.\n */\nimport { mergeContributions as mergeRoutingContributions } from '@weasel-js/routing';\nimport type { DepName, DepSchema } from 'interactions/actions/depSchema';\nimport type { Contribution } from '../tools/overlayBinding';\nimport type { CanvasExtensionApi } from './canvasExtension';\nimport type { CanvasViewProps } from './CanvasView';\nimport type { View } from '../core/viewport/view';\nimport type { KitRequirements } from './kitRequirements';\n\n/** Reads the surface's deps from inside `SurfaceContribution.attach`. */\nexport interface ContributionDepReader {\n get<K extends DepName>(name: K): DepSchema[K] | undefined;\n}\n\n/** What {@link SurfaceContribution.beforePaint} and `afterPaint` are handed. */\nexport interface ContributionFrameCtx {\n /** The frame's timestamp, on `performance.now()`'s clock. Both phases of one\n * frame see the same value. */\n readonly time: number;\n /** The surface camera this frame paints with. */\n readonly view: View;\n /** Ask for another frame. A hook animating something calls this every frame\n * it still has work for, and stops calling it to let the loop go idle. */\n requestFrame(): void;\n /** The surface's deps, as `attach` reads them. */\n readonly deps: ContributionDepReader;\n}\n\n/**\n * A feature a surface installs in one step. Every role is optional: `bindings`,\n * `actions`, `deps`, `overlay` and `presentation` from `Contribution`, plus\n * the ones below. Installing it installs every role it declares, and removing\n * it removes them.\n */\nexport interface SurfaceContribution extends Contribution {\n /** Views this entry adds to the surface, as `<SceneCanvas views>` takes. */\n views?: readonly CanvasViewProps[];\n /**\n * Runs once the surface mounts; returns its teardown. For what the roles\n * above cannot say — a registered (hit-testable) layer, a subscription.\n *\n * Keyed by the function's identity, so a contribution built once (in a\n * `useMemo`, or at module scope) attaches once however often the host\n * re-renders.\n */\n attach?: (api: CanvasExtensionApi, deps: ContributionDepReader) => () => void;\n /**\n * Runs on every frame the surface paints, before the paint — for state the\n * entry's layers read, advanced once per frame. Hooks run in the order the\n * surface lists its entries (registered tools, then `ambient` in array\n * order), and one that throws is reported and skipped without stopping the\n * frame.\n */\n beforePaint?: (ctx: ContributionFrameCtx) => void;\n /** Runs after a frame's pixels have landed, in the same order as\n * {@link beforePaint}. A frame that painted nothing runs no `afterPaint`. */\n afterPaint?: (ctx: ContributionFrameCtx) => void;\n /**\n * The kit versions this entry was written against, as semver ranges:\n * `{ core: '^1.7' }`. A mismatch warns in development, or throws where the\n * surface is set to `versionCheck=\"strict\"`.\n */\n requires?: KitRequirements;\n}\n\n/**\n * Concatenate contribution bundles into one registry, preserving order. Throws\n * on a duplicate entry id, dep name or view id — two features answering for\n * the same name is a bug in whichever was added second.\n */\nexport function mergeContributions<T extends SurfaceContribution>(\n ...bundles: readonly (readonly T[])[]\n): T[] {\n const out = mergeRoutingContributions<T>(...bundles);\n const viewOwner = new Map<string, string>();\n for (const entry of out) {\n for (const view of entry.views ?? []) {\n const owner = viewOwner.get(view.id);\n if (owner !== undefined) {\n throw new Error(\n `mergeContributions: view \"${view.id}\" is added by both \"${owner}\" and \"${entry.id}\". `\n + `View ids are unique per surface; rename one.`,\n );\n }\n viewOwner.set(view.id, entry.id);\n }\n }\n return out;\n}\n"],"mappings":";;;;;;;;;;;;;AA0EA,SAAgBA,qBACd,GAAG,SACE;CACL,MAAM,MAAMC,mBAA6B,GAAG,OAAO;CACnD,MAAM,4BAAY,IAAI,IAAoB;CAC1C,KAAK,MAAM,SAAS,KAClB,KAAK,MAAM,QAAQ,MAAM,SAAS,CAAC,GAAG;EACpC,MAAM,QAAQ,UAAU,IAAI,KAAK,EAAE;EACnC,IAAI,UAAU,QACZ,MAAM,IAAI,MACR,6BAA6B,KAAK,GAAG,sBAAsB,MAAM,SAAS,MAAM,GAAG,gDAErF;EAEF,UAAU,IAAI,KAAK,IAAI,MAAM,EAAE;CACjC;CAEF,OAAO;AACT"}
1
+ {"version":3,"file":"surfaceContribution.js","names":["mergeContributions","mergeRoutingContributions"],"sources":["../../src/canvas/surfaceContribution.ts"],"sourcesContent":["/**\n * The canvas's unit of extension: a routing `Contribution` plus the roles only\n * a surface can install — views on it, and whatever else needs the mounted\n * canvas.\n */\nimport { mergeContributions as mergeRoutingContributions } from '@weasel-js/routing';\nimport type { DepName, DepSchema } from 'interactions/actions/depSchema';\nimport type { Contribution } from '../tools/overlayBinding';\nimport type { CanvasExtensionApi } from './canvasExtension';\nimport type { CanvasViewProps } from './CanvasView';\nimport type { ViewMarkerProps } from '../features/viewports/viewMarker';\nimport type { View } from '../core/viewport/view';\nimport type { KitRequirements } from './kitRequirements';\n\n/** Reads the surface's deps from inside `SurfaceContribution.attach`. */\nexport interface ContributionDepReader {\n get<K extends DepName>(name: K): DepSchema[K] | undefined;\n}\n\n/** What {@link SurfaceContribution.beforePaint} and `afterPaint` are handed. */\nexport interface ContributionFrameCtx {\n /** The frame's timestamp, on `performance.now()`'s clock. Both phases of one\n * frame see the same value. */\n readonly time: number;\n /** The surface camera this frame paints with. */\n readonly view: View;\n /** Ask for another frame. A hook animating something calls this every frame\n * it still has work for, and stops calling it to let the loop go idle. */\n requestFrame(): void;\n /** The surface's deps, as `attach` reads them. */\n readonly deps: ContributionDepReader;\n}\n\n/**\n * A feature a surface installs in one step. Every role is optional: `bindings`,\n * `actions`, `deps`, `overlay` and `presentation` from `Contribution`, plus\n * the ones below. Installing it installs every role it declares, and removing\n * it removes them.\n */\nexport interface SurfaceContribution extends Contribution {\n /** Views this entry adds to the surface, as `<SceneCanvas views>` takes. */\n views?: readonly CanvasViewProps[];\n /** Markers this entry draws, as `<ViewMarker>` elements would. */\n markers?: readonly ViewMarkerProps[];\n /**\n * Runs once the surface mounts; returns its teardown. For what the roles\n * above cannot say — a registered (hit-testable) layer, a subscription.\n *\n * Keyed by the function's identity, so a contribution built once (in a\n * `useMemo`, or at module scope) attaches once however often the host\n * re-renders.\n */\n attach?: (api: CanvasExtensionApi, deps: ContributionDepReader) => () => void;\n /**\n * Runs on every frame the surface paints, before the paint — for state the\n * entry's layers read, advanced once per frame. Hooks run in the order the\n * surface lists its entries (registered tools, then `ambient` in array\n * order), and one that throws is reported and skipped without stopping the\n * frame.\n */\n beforePaint?: (ctx: ContributionFrameCtx) => void;\n /** Runs after a frame's pixels have landed, in the same order as\n * {@link beforePaint}. A frame that painted nothing runs no `afterPaint`. */\n afterPaint?: (ctx: ContributionFrameCtx) => void;\n /**\n * The kit versions this entry was written against, as semver ranges:\n * `{ core: '^1.7' }`. A mismatch warns in development, or throws where the\n * surface is set to `versionCheck=\"strict\"`.\n */\n requires?: KitRequirements;\n}\n\n/**\n * Concatenate contribution bundles into one registry, preserving order. Throws\n * on a duplicate entry id, dep name, view id or marker id — two features\n * answering for the same name is a bug in whichever was added second.\n */\nexport function mergeContributions<T extends SurfaceContribution>(\n ...bundles: readonly (readonly T[])[]\n): T[] {\n const out = mergeRoutingContributions<T>(...bundles);\n claimIds(out, 'view', (entry) => entry.views ?? []);\n claimIds(out, 'marker', (entry) => entry.markers ?? []);\n return out;\n}\n\nfunction claimIds<T extends SurfaceContribution>(\n entries: readonly T[], kind: 'view' | 'marker', of: (entry: T) => readonly { id: string }[],\n): void {\n const owners = new Map<string, string>();\n for (const entry of entries) {\n for (const { id } of of(entry)) {\n const owner = owners.get(id);\n if (owner !== undefined) {\n throw new Error(\n `mergeContributions: ${kind} \"${id}\" is added by both \"${owner}\" and \"${entry.id}\". `\n + `${kind === 'view' ? 'View' : 'Marker'} ids are unique per surface; rename one.`,\n );\n }\n owners.set(id, entry.id);\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;AA6EA,SAAgBA,qBACd,GAAG,SACE;CACL,MAAM,MAAMC,mBAA6B,GAAG,OAAO;CACnD,SAAS,KAAK,SAAS,UAAU,MAAM,SAAS,CAAC,CAAC;CAClD,SAAS,KAAK,WAAW,UAAU,MAAM,WAAW,CAAC,CAAC;CACtD,OAAO;AACT;AAEA,SAAS,SACP,SAAuB,MAAyB,IAC1C;CACN,MAAM,yBAAS,IAAI,IAAoB;CACvC,KAAK,MAAM,SAAS,SAClB,KAAK,MAAM,EAAE,QAAQ,GAAG,KAAK,GAAG;EAC9B,MAAM,QAAQ,OAAO,IAAI,EAAE;EAC3B,IAAI,UAAU,QACZ,MAAM,IAAI,MACR,uBAAuB,KAAK,IAAI,GAAG,sBAAsB,MAAM,SAAS,MAAM,GAAG,KAC5E,SAAS,SAAS,SAAS,SAAS,yCAC3C;EAEF,OAAO,IAAI,IAAI,MAAM,EAAE;CACzB;AAEJ"}
@@ -30,7 +30,8 @@ function useCanvasView(opts) {
30
30
  onViewChange,
31
31
  viewBounds,
32
32
  dims,
33
- requestRedraw
33
+ requestRedraw,
34
+ by: opts.controlledBy ?? "view"
34
35
  });
35
36
  const controlledWarnedRef = useRef(false);
36
37
  const getView = useCallback(() => {
@@ -44,7 +45,8 @@ function useCanvasView(opts) {
44
45
  if (controlled) {
45
46
  if (imperative && !controlledWarnedRef.current) {
46
47
  controlledWarnedRef.current = true;
47
- console.warn(forward ? "[weasel] setView not applied locally: this canvas is controlled by its `view` prop. The value was forwarded to `onViewChange` — update the prop from there, or drop it to take the imperative path." : "[weasel] setView ignored: this canvas is controlled by its `view` prop and has no `onViewChange`, so the value has nowhere to go. Update the prop, or drop it to take the imperative path.");
48
+ const [prop, cb] = live.current.by === "aim" ? ["aim", "onAimChange"] : ["view", "onViewChange"];
49
+ console.warn(forward ? `[weasel] setView not applied locally: this canvas is controlled by its \`${prop}\` prop. The value was forwarded to \`${cb}\` — update the prop from there, or drop it to take the imperative path.` : `[weasel] setView ignored: this canvas is controlled by its \`${prop}\` prop and has no \`${cb}\`, so the value has nowhere to go. Update the prop, or drop it to take the imperative path.`);
48
50
  }
49
51
  forward?.(clamped);
50
52
  if (derived) redraw();
@@ -1 +1 @@
1
- {"version":3,"file":"useCanvasView.js","names":[],"sources":["../../src/canvas/useCanvasView.ts"],"sourcesContent":["/**\n * A camera: held in a ref when uncontrolled, taken from the `view` option when\n * controlled, with one write path and one subscriber list over both.\n * `<SceneCanvas>` makes the one its `<Canvas>` looks through, and each\n * `<CanvasView>` makes its own.\n */\nimport { useCallback, useRef, type MutableRefObject } from 'react';\nimport { useLatest } from '@weasel-js/react';\nimport type { Dims } from 'core/layers/render';\nimport type { Bounds } from 'core/viewport/fitViewToBounds';\nimport { clampView } from 'core/viewport/clampView';\nimport { normalizeView, sameView, type View } from 'core/viewport/view';\n\n/** The view props a canvas takes for its camera. */\nexport interface CameraProps {\n /** Controlled viewport. When supplied, the canvas does not own the value —\n * the consumer must supply `onViewChange` and re-render with the new\n * view. See `View` JSDoc for the camera-position convention. */\n view?: View;\n /** Initial viewport for the uncontrolled path. Default `{x:0, y:0}`. */\n defaultView?: View;\n /** Fires whenever the viewport changes — in both controlled and\n * uncontrolled modes. */\n onViewChange?: (next: View) => void;\n /**\n * Optional world-space rect that constrains pan: every write to the view passes through\n * `clampView` first, keeping the visible rect inside `viewBounds`. An axis where the\n * visible rect is larger than the bounds is centered. Has no effect on `scale`.\n */\n viewBounds?: { x: number; y: number; width: number; height: number };\n}\n\n/** Inputs to {@link useCanvasView}: the camera's view props and what a write\n * needs from whatever the camera looks through. */\nexport interface CanvasViewOptions {\n /** Supplied, the camera is controlled. A thunk is a camera derived from\n * something else, read fresh at every {@link CanvasViewState.getView}. */\n view: View | (() => View) | undefined;\n defaultView: View | undefined;\n onViewChange: ((next: View) => void) | undefined;\n viewBounds: Bounds | undefined;\n /** The size the camera looks through, which `viewBounds` clamps against. */\n dims: () => Dims;\n requestRedraw: () => void;\n}\n\n/** What `useCanvasView` hands back. The owner decides when in a commit\n * the two controlled steps run, since they have to sit among its own effects. */\nexport interface CanvasViewState {\n /** The view in effect. Written by a write when uncontrolled and by\n * `adoptControlled` when controlled. A camera derived by a thunk is not\n * kept here: read `getView`. */\n viewRef: MutableRefObject<View>;\n /** This render's controlled view, absent when the camera is uncontrolled or\n * derived. A new object every render, so compare its numbers. */\n controlled: View | undefined;\n /** The imperative write. Controlled, it is forwarded to `onViewChange`\n * instead of applied, and says so once per mount. */\n setView: (next: View | ((current: View) => View)) => void;\n /** The write input makes. Controlled, forwarding it to `onViewChange` is how\n * the owner hears of a pan at all, so nothing is warned. */\n setViewFromInput: (next: View) => void;\n getView: () => View;\n subscribeView: (fn: (v: View) => void) => () => void;\n /** Take this render's controlled view into `viewRef`. Call from an insertion effect. */\n adoptControlled: () => void;\n /** Tell subscribers of a controlled view that moved since the last call.\n * A controlled view reaches the ref with no write, so this is where they\n * hear of it. Call from a layout effect. */\n announceControlled: () => void;\n /** Drop every subscriber, on unmount. */\n clearSubscribers: () => void;\n}\n\nconst IDENTITY: View = { x: 0, y: 0, scale: { x: 1, y: 1 } };\n\nexport function useCanvasView(opts: CanvasViewOptions): CanvasViewState {\n const { view: viewProp, defaultView, onViewChange, viewBounds, dims, requestRedraw } = opts;\n const derived = typeof viewProp === 'function' ? viewProp : undefined;\n const controlledView = viewProp === undefined || derived ? undefined : normalizeView(viewProp as View);\n // The uncontrolled view lives in a ref, not `useState`, so a camera moving\n // at 60 Hz costs no React render; DOM that mirrors it subscribes instead.\n const viewRef = useRef<View>(controlledView ?? normalizeView(defaultView ?? IDENTITY));\n const viewSubsRef = useRef<Set<(v: View) => void>>(new Set());\n const live = useLatest({\n controlled: viewProp !== undefined, derived, onViewChange, viewBounds, dims, requestRedraw,\n });\n const controlledWarnedRef = useRef(false);\n\n const getView = useCallback((): View => {\n const thunk = live.current.derived;\n return thunk ? normalizeView(thunk()) : viewRef.current;\n }, [live]);\n\n const write = useCallback((next: View, imperative: boolean): void => {\n const {\n controlled, derived, onViewChange: forward, viewBounds: bounds, dims: size, requestRedraw: redraw,\n } = live.current;\n const valid = normalizeView(next);\n const clamped = bounds ? clampView(valid, bounds, size()) : valid;\n if (controlled) {\n // The prop is the authority; writing the ref would put pixels and props\n // out of step with nothing to reconcile them.\n if (imperative && !controlledWarnedRef.current) {\n controlledWarnedRef.current = true;\n console.warn(forward\n ? '[weasel] setView not applied locally: this canvas is controlled by its `view` prop. The value was forwarded to `onViewChange` — update the prop from there, or drop it to take the imperative path.'\n : '[weasel] setView ignored: this canvas is controlled by its `view` prop and has no `onViewChange`, so the value has nowhere to go. Update the prop, or drop it to take the imperative path.');\n }\n forward?.(clamped);\n // A derived camera has no prop to change, so nothing else repaints it.\n if (derived) redraw();\n return;\n }\n viewRef.current = clamped;\n for (const fn of viewSubsRef.current) fn(clamped);\n forward?.(clamped);\n redraw();\n }, [live]);\n\n const setView = useCallback((next: View | ((current: View) => View)) => {\n write(typeof next === 'function' ? next(getView()) : next, true);\n }, [write, getView]);\n const setViewFromInput = useCallback((next: View) => { write(next, false); }, [write]);\n\n const subscribeView = useCallback((fn: (v: View) => void) => {\n viewSubsRef.current.add(fn);\n return () => { viewSubsRef.current.delete(fn); };\n }, []);\n\n const controlledMovedRef = useRef(false);\n // A closure over this render's prop, not a ref read: the owner calls it\n // from the same render's insertion effect.\n const adoptControlled = (): void => {\n if (!controlledView) return;\n controlledMovedRef.current ||= !sameView(viewRef.current, controlledView);\n viewRef.current = controlledView;\n };\n const announceControlled = useCallback(() => {\n if (!controlledMovedRef.current) return;\n controlledMovedRef.current = false;\n for (const fn of viewSubsRef.current) fn(viewRef.current);\n }, []);\n const clearSubscribers = useCallback(() => { viewSubsRef.current.clear(); }, []);\n\n return {\n viewRef, controlled: controlledView, setView, setViewFromInput, getView, subscribeView,\n adoptControlled, announceControlled, clearSubscribers,\n };\n}\n"],"mappings":";;;;;;;;;;;;AA0EA,IAAM,WAAiB;CAAE,GAAG;CAAG,GAAG;CAAG,OAAO;EAAE,GAAG;EAAG,GAAG;CAAE;AAAE;AAE3D,SAAgB,cAAc,MAA0C;CACtE,MAAM,EAAE,MAAM,UAAU,aAAa,cAAc,YAAY,MAAM,kBAAkB;CACvF,MAAM,UAAU,OAAO,aAAa,aAAa,WAAW;CAC5D,MAAM,iBAAiB,aAAa,UAAa,UAAU,SAAY,cAAc,QAAgB;CAGrG,MAAM,UAAU,OAAa,kBAAkB,cAAc,eAAe,QAAQ,CAAC;CACrF,MAAM,cAAc,uBAA+B,IAAI,IAAI,CAAC;CAC5D,MAAM,OAAO,UAAU;EACrB,YAAY,aAAa;EAAW;EAAS;EAAc;EAAY;EAAM;CAC/E,CAAC;CACD,MAAM,sBAAsB,OAAO,KAAK;CAExC,MAAM,UAAU,kBAAwB;EACtC,MAAM,QAAQ,KAAK,QAAQ;EAC3B,OAAO,QAAQ,cAAc,MAAM,CAAC,IAAI,QAAQ;CAClD,GAAG,CAAC,IAAI,CAAC;CAET,MAAM,QAAQ,aAAa,MAAY,eAA8B;EACnE,MAAM,EACJ,YAAY,SAAS,cAAc,SAAS,YAAY,QAAQ,MAAM,MAAM,eAAe,WACzF,KAAK;EACT,MAAM,QAAQ,cAAc,IAAI;EAChC,MAAM,UAAU,SAAS,UAAU,OAAO,QAAQ,KAAK,CAAC,IAAI;EAC5D,IAAI,YAAY;GAGd,IAAI,cAAc,CAAC,oBAAoB,SAAS;IAC9C,oBAAoB,UAAU;IAC9B,QAAQ,KAAK,UACT,wMACA,4LAA4L;GAClM;GACA,UAAU,OAAO;GAEjB,IAAI,SAAS,OAAO;GACpB;EACF;EACA,QAAQ,UAAU;EAClB,KAAK,MAAM,MAAM,YAAY,SAAS,GAAG,OAAO;EAChD,UAAU,OAAO;EACjB,OAAO;CACT,GAAG,CAAC,IAAI,CAAC;CAET,MAAM,UAAU,aAAa,SAA2C;EACtE,MAAM,OAAO,SAAS,aAAa,KAAK,QAAQ,CAAC,IAAI,MAAM,IAAI;CACjE,GAAG,CAAC,OAAO,OAAO,CAAC;CACnB,MAAM,mBAAmB,aAAa,SAAe;EAAE,MAAM,MAAM,KAAK;CAAG,GAAG,CAAC,KAAK,CAAC;CAErF,MAAM,gBAAgB,aAAa,OAA0B;EAC3D,YAAY,QAAQ,IAAI,EAAE;EAC1B,aAAa;GAAE,YAAY,QAAQ,OAAO,EAAE;EAAG;CACjD,GAAG,CAAC,CAAC;CAEL,MAAM,qBAAqB,OAAO,KAAK;CAGvC,MAAM,wBAA8B;EAClC,IAAI,CAAC,gBAAgB;EACrB,mBAAmB,YAAY,CAAC,SAAS,QAAQ,SAAS,cAAc;EACxE,QAAQ,UAAU;CACpB;CAQA,OAAO;EACL;EAAS,YAAY;EAAgB;EAAS;EAAkB;EAAS;EACzE;EAAiB,oBATQ,kBAAkB;GAC3C,IAAI,CAAC,mBAAmB,SAAS;GACjC,mBAAmB,UAAU;GAC7B,KAAK,MAAM,MAAM,YAAY,SAAS,GAAG,QAAQ,OAAO;EAC1D,GAAG,CAAC,CAKe;EAAoB,kBAJd,kBAAkB;GAAE,YAAY,QAAQ,MAAM;EAAG,GAAG,CAAC,CAIvC;CACvC;AACF"}
1
+ {"version":3,"file":"useCanvasView.js","names":[],"sources":["../../src/canvas/useCanvasView.ts"],"sourcesContent":["/**\n * A camera: held in a ref when uncontrolled, taken from the `view` option when\n * controlled, with one write path and one subscriber list over both.\n * `<SceneCanvas>` makes the one its `<Canvas>` looks through, and each\n * `<CanvasView>` makes its own.\n */\nimport { useCallback, useRef, type MutableRefObject } from 'react';\nimport { useLatest } from '@weasel-js/react';\nimport type { Dims } from 'core/layers/render';\nimport type { Bounds } from 'core/viewport/fitViewToBounds';\nimport { clampView } from 'core/viewport/clampView';\nimport { normalizeView, sameView, type View } from 'core/viewport/view';\n\n/** The view props a canvas takes for its camera. */\nexport interface CameraProps {\n /** Controlled viewport. When supplied, the canvas does not own the value —\n * the consumer must supply `onViewChange` and re-render with the new\n * view. See `View` JSDoc for the camera-position convention. */\n view?: View;\n /** Initial viewport for the uncontrolled path. Default `{x:0, y:0}`. */\n defaultView?: View;\n /** Fires whenever the viewport changes — in both controlled and\n * uncontrolled modes. */\n onViewChange?: (next: View) => void;\n /**\n * Optional world-space rect that constrains pan: every write to the view passes through\n * `clampView` first, keeping the visible rect inside `viewBounds`. An axis where the\n * visible rect is larger than the bounds is centered. Has no effect on `scale`.\n */\n viewBounds?: { x: number; y: number; width: number; height: number };\n}\n\n/** Inputs to {@link useCanvasView}: the camera's view props and what a write\n * needs from whatever the camera looks through. */\nexport interface CanvasViewOptions {\n /** Supplied, the camera is controlled. A thunk is a camera derived from\n * something else, read fresh at every {@link CanvasViewState.getView}. */\n view: View | (() => View) | undefined;\n defaultView: View | undefined;\n onViewChange: ((next: View) => void) | undefined;\n viewBounds: Bounds | undefined;\n /** The size the camera looks through, which `viewBounds` clamps against. */\n dims: () => Dims;\n requestRedraw: () => void;\n /** The prop a controlled camera answers to, which a warning names. Default `'view'`. */\n controlledBy?: 'view' | 'aim';\n}\n\n/** What `useCanvasView` hands back. The owner decides when in a commit\n * the two controlled steps run, since they have to sit among its own effects. */\nexport interface CanvasViewState {\n /** The view in effect. Written by a write when uncontrolled and by\n * `adoptControlled` when controlled. A camera derived by a thunk is not\n * kept here: read `getView`. */\n viewRef: MutableRefObject<View>;\n /** This render's controlled view, absent when the camera is uncontrolled or\n * derived. A new object every render, so compare its numbers. */\n controlled: View | undefined;\n /** The imperative write. Controlled, it is forwarded to `onViewChange`\n * instead of applied, and says so once per mount. */\n setView: (next: View | ((current: View) => View)) => void;\n /** The write input makes. Controlled, forwarding it to `onViewChange` is how\n * the owner hears of a pan at all, so nothing is warned. */\n setViewFromInput: (next: View) => void;\n getView: () => View;\n subscribeView: (fn: (v: View) => void) => () => void;\n /** Take this render's controlled view into `viewRef`. Call from an insertion effect. */\n adoptControlled: () => void;\n /** Tell subscribers of a controlled view that moved since the last call.\n * A controlled view reaches the ref with no write, so this is where they\n * hear of it. Call from a layout effect. */\n announceControlled: () => void;\n /** Drop every subscriber, on unmount. */\n clearSubscribers: () => void;\n}\n\nconst IDENTITY: View = { x: 0, y: 0, scale: { x: 1, y: 1 } };\n\nexport function useCanvasView(opts: CanvasViewOptions): CanvasViewState {\n const { view: viewProp, defaultView, onViewChange, viewBounds, dims, requestRedraw } = opts;\n const derived = typeof viewProp === 'function' ? viewProp : undefined;\n const controlledView = viewProp === undefined || derived ? undefined : normalizeView(viewProp as View);\n // The uncontrolled view lives in a ref, not `useState`, so a camera moving\n // at 60 Hz costs no React render; DOM that mirrors it subscribes instead.\n const viewRef = useRef<View>(controlledView ?? normalizeView(defaultView ?? IDENTITY));\n const viewSubsRef = useRef<Set<(v: View) => void>>(new Set());\n const live = useLatest({\n controlled: viewProp !== undefined, derived, onViewChange, viewBounds, dims, requestRedraw,\n by: opts.controlledBy ?? 'view',\n });\n const controlledWarnedRef = useRef(false);\n\n const getView = useCallback((): View => {\n const thunk = live.current.derived;\n return thunk ? normalizeView(thunk()) : viewRef.current;\n }, [live]);\n\n const write = useCallback((next: View, imperative: boolean): void => {\n const {\n controlled, derived, onViewChange: forward, viewBounds: bounds, dims: size, requestRedraw: redraw,\n } = live.current;\n const valid = normalizeView(next);\n const clamped = bounds ? clampView(valid, bounds, size()) : valid;\n if (controlled) {\n // The prop is the authority; writing the ref would put pixels and props\n // out of step with nothing to reconcile them.\n if (imperative && !controlledWarnedRef.current) {\n controlledWarnedRef.current = true;\n const [prop, cb] = live.current.by === 'aim' ? ['aim', 'onAimChange'] : ['view', 'onViewChange'];\n console.warn(forward\n ? `[weasel] setView not applied locally: this canvas is controlled by its \\`${prop}\\` prop. The value was forwarded to \\`${cb}\\` — update the prop from there, or drop it to take the imperative path.`\n : `[weasel] setView ignored: this canvas is controlled by its \\`${prop}\\` prop and has no \\`${cb}\\`, so the value has nowhere to go. Update the prop, or drop it to take the imperative path.`);\n }\n forward?.(clamped);\n // A derived camera has no prop to change, so nothing else repaints it.\n if (derived) redraw();\n return;\n }\n viewRef.current = clamped;\n for (const fn of viewSubsRef.current) fn(clamped);\n forward?.(clamped);\n redraw();\n }, [live]);\n\n const setView = useCallback((next: View | ((current: View) => View)) => {\n write(typeof next === 'function' ? next(getView()) : next, true);\n }, [write, getView]);\n const setViewFromInput = useCallback((next: View) => { write(next, false); }, [write]);\n\n const subscribeView = useCallback((fn: (v: View) => void) => {\n viewSubsRef.current.add(fn);\n return () => { viewSubsRef.current.delete(fn); };\n }, []);\n\n const controlledMovedRef = useRef(false);\n // A closure over this render's prop, not a ref read: the owner calls it\n // from the same render's insertion effect.\n const adoptControlled = (): void => {\n if (!controlledView) return;\n controlledMovedRef.current ||= !sameView(viewRef.current, controlledView);\n viewRef.current = controlledView;\n };\n const announceControlled = useCallback(() => {\n if (!controlledMovedRef.current) return;\n controlledMovedRef.current = false;\n for (const fn of viewSubsRef.current) fn(viewRef.current);\n }, []);\n const clearSubscribers = useCallback(() => { viewSubsRef.current.clear(); }, []);\n\n return {\n viewRef, controlled: controlledView, setView, setViewFromInput, getView, subscribeView,\n adoptControlled, announceControlled, clearSubscribers,\n };\n}\n"],"mappings":";;;;;;;;;;;;AA4EA,IAAM,WAAiB;CAAE,GAAG;CAAG,GAAG;CAAG,OAAO;EAAE,GAAG;EAAG,GAAG;CAAE;AAAE;AAE3D,SAAgB,cAAc,MAA0C;CACtE,MAAM,EAAE,MAAM,UAAU,aAAa,cAAc,YAAY,MAAM,kBAAkB;CACvF,MAAM,UAAU,OAAO,aAAa,aAAa,WAAW;CAC5D,MAAM,iBAAiB,aAAa,UAAa,UAAU,SAAY,cAAc,QAAgB;CAGrG,MAAM,UAAU,OAAa,kBAAkB,cAAc,eAAe,QAAQ,CAAC;CACrF,MAAM,cAAc,uBAA+B,IAAI,IAAI,CAAC;CAC5D,MAAM,OAAO,UAAU;EACrB,YAAY,aAAa;EAAW;EAAS;EAAc;EAAY;EAAM;EAC7E,IAAI,KAAK,gBAAgB;CAC3B,CAAC;CACD,MAAM,sBAAsB,OAAO,KAAK;CAExC,MAAM,UAAU,kBAAwB;EACtC,MAAM,QAAQ,KAAK,QAAQ;EAC3B,OAAO,QAAQ,cAAc,MAAM,CAAC,IAAI,QAAQ;CAClD,GAAG,CAAC,IAAI,CAAC;CAET,MAAM,QAAQ,aAAa,MAAY,eAA8B;EACnE,MAAM,EACJ,YAAY,SAAS,cAAc,SAAS,YAAY,QAAQ,MAAM,MAAM,eAAe,WACzF,KAAK;EACT,MAAM,QAAQ,cAAc,IAAI;EAChC,MAAM,UAAU,SAAS,UAAU,OAAO,QAAQ,KAAK,CAAC,IAAI;EAC5D,IAAI,YAAY;GAGd,IAAI,cAAc,CAAC,oBAAoB,SAAS;IAC9C,oBAAoB,UAAU;IAC9B,MAAM,CAAC,MAAM,MAAM,KAAK,QAAQ,OAAO,QAAQ,CAAC,OAAO,aAAa,IAAI,CAAC,QAAQ,cAAc;IAC/F,QAAQ,KAAK,UACT,4EAA4E,KAAK,wCAAwC,GAAG,4EAC5H,gEAAgE,KAAK,uBAAuB,GAAG,6FAA6F;GAClM;GACA,UAAU,OAAO;GAEjB,IAAI,SAAS,OAAO;GACpB;EACF;EACA,QAAQ,UAAU;EAClB,KAAK,MAAM,MAAM,YAAY,SAAS,GAAG,OAAO;EAChD,UAAU,OAAO;EACjB,OAAO;CACT,GAAG,CAAC,IAAI,CAAC;CAET,MAAM,UAAU,aAAa,SAA2C;EACtE,MAAM,OAAO,SAAS,aAAa,KAAK,QAAQ,CAAC,IAAI,MAAM,IAAI;CACjE,GAAG,CAAC,OAAO,OAAO,CAAC;CACnB,MAAM,mBAAmB,aAAa,SAAe;EAAE,MAAM,MAAM,KAAK;CAAG,GAAG,CAAC,KAAK,CAAC;CAErF,MAAM,gBAAgB,aAAa,OAA0B;EAC3D,YAAY,QAAQ,IAAI,EAAE;EAC1B,aAAa;GAAE,YAAY,QAAQ,OAAO,EAAE;EAAG;CACjD,GAAG,CAAC,CAAC;CAEL,MAAM,qBAAqB,OAAO,KAAK;CAGvC,MAAM,wBAA8B;EAClC,IAAI,CAAC,gBAAgB;EACrB,mBAAmB,YAAY,CAAC,SAAS,QAAQ,SAAS,cAAc;EACxE,QAAQ,UAAU;CACpB;CAQA,OAAO;EACL;EAAS,YAAY;EAAgB;EAAS;EAAkB;EAAS;EACzE;EAAiB,oBATQ,kBAAkB;GAC3C,IAAI,CAAC,mBAAmB,SAAS;GACjC,mBAAmB,UAAU;GAC7B,KAAK,MAAM,MAAM,YAAY,SAAS,GAAG,QAAQ,OAAO;EAC1D,GAAG,CAAC,CAKe;EAAoB,kBAJd,kBAAkB;GAAE,YAAY,QAAQ,MAAM;EAAG,GAAG,CAAC,CAIvC;CACvC;AACF"}
@@ -32,7 +32,7 @@ function useSlotLayers(layersMap, tools, backgroundFill, decorationLayer) {
32
32
  ]);
33
33
  }
34
34
  /**
35
- * The full stack the canvas paints: its slot layers, then registered views, the debug overlay,
35
+ * The full stack the canvas paints: its slot layers, then root view markers, registered views, the debug overlay,
36
36
  * externally registered layers, and the cursor. Every layer that can change on its own
37
37
  * repaints the canvas through `requestRedraw`.
38
38
  */
@@ -40,7 +40,11 @@ function useComposedLayers(layers, above, requestRedraw) {
40
40
  const { viewRegistry, debugOverlay, extrasRef, extrasVersion, cursorLayer } = above;
41
41
  const viewRegistryVersion = useSyncExternalStore(useCallback((cb) => viewRegistry?.subscribe(cb) ?? (() => {}), [viewRegistry]), () => viewRegistry?.getVersion() ?? 0, () => 0);
42
42
  const composed = useMemo(() => {
43
- const withViews = viewRegistry ? [...layers, ...viewRegistry.list().filter((r) => r.paint).map((r) => r.layer)] : layers;
43
+ const withViews = viewRegistry ? [
44
+ ...layers,
45
+ ...viewRegistry.markerLayers(null),
46
+ ...viewRegistry.list().filter((r) => r.paint).map((r) => r.layer)
47
+ ] : layers;
44
48
  return [
45
49
  ...debugOverlay ? [...withViews, debugOverlay] : withViews,
46
50
  ...extrasRef.current,
@@ -1 +1 @@
1
- {"version":3,"file":"useLayerStack.js","names":[],"sources":["../../src/canvas/useLayerStack.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useSyncExternalStore } from 'react';\nimport type { RenderLayer } from 'core/layers/render';\nimport type { Paint } from '@weasel-js/paint';\nimport { createBackgroundFillLayer } from 'core/layers/backgroundFill';\nimport { composeOrderedLayers, placeToolOverlays } from './layerOrder';\nimport { STANDARD_SLOTS, isCustomEntry } from './layerSlots';\nimport type { CanvasProps } from './canvasProps';\nimport type { ViewRegistry } from './viewRegistry';\n\n/** The `layers` prop's slots and custom entries in draw order, with the background fill beneath,\n * the decoration layer above, and the active tool's overlays placed among them. */\nexport function useSlotLayers(\n layersMap: CanvasProps<unknown>['layers'],\n tools: CanvasProps<unknown>['tools'],\n backgroundFill: Paint | undefined,\n decorationLayer: RenderLayer<unknown> | undefined,\n): RenderLayer<unknown>[] {\n const backgroundLayer = useMemo(\n () => (backgroundFill ? createBackgroundFillLayer(backgroundFill) : undefined),\n [backgroundFill],\n );\n\n return useMemo<RenderLayer<unknown>[]>(() => {\n const standardLayers: Partial<\n Record<(typeof STANDARD_SLOTS)[number], RenderLayer<unknown>>\n > = {};\n for (const slot of ['grid', 'cellHighlight', 'selectionOverlay'] as const) {\n const entry = layersMap[slot];\n if (isCustomEntry(entry)) standardLayers[slot] = entry.layer;\n }\n const out: RenderLayer<unknown>[] = composeOrderedLayers(\n decorationLayer ? { ...layersMap, decorationLayer: { layer: decorationLayer } } : layersMap,\n standardLayers, layersMap.scene?.layers, backgroundLayer,\n );\n if (tools) {\n placeToolOverlays(out, standardLayers.selectionOverlay, tools.getActiveOverlays);\n }\n return out;\n }, [layersMap, tools, backgroundLayer, decorationLayer]);\n}\n\n/** What the canvas paints on top of its slot layers. */\nexport interface StackAbove {\n viewRegistry: ViewRegistry | null;\n debugOverlay: RenderLayer<unknown> | null | undefined;\n /** Registered layers; re-read whenever `extrasVersion` changes. */\n extrasRef: { readonly current: ReadonlySet<RenderLayer<unknown>> };\n extrasVersion: number;\n cursorLayer: RenderLayer<unknown>;\n}\n\n/**\n * The full stack the canvas paints: its slot layers, then registered views, the debug overlay,\n * externally registered layers, and the cursor. Every layer that can change on its own\n * repaints the canvas through `requestRedraw`.\n */\nexport function useComposedLayers(\n layers: readonly RenderLayer<unknown>[],\n above: StackAbove,\n requestRedraw: () => void,\n): RenderLayer<unknown>[] {\n const { viewRegistry, debugOverlay, extrasRef, extrasVersion, cursorLayer } = above;\n const viewRegistryVersion = useSyncExternalStore(\n useCallback((cb: () => void) => viewRegistry?.subscribe(cb) ?? (() => {}), [viewRegistry]),\n () => viewRegistry?.getVersion() ?? 0,\n () => 0,\n );\n\n const composed = useMemo(() => {\n // Views paint through the surface's own stack, so they sit above it and\n // below both the debug overlay and externally-registered layers.\n const withViews = viewRegistry\n ? [...layers, ...viewRegistry.list().filter((r) => r.paint).map((r) => r.layer as RenderLayer<unknown>)]\n : layers;\n const base = debugOverlay ? [...withViews, debugOverlay] : withViews;\n // The cursor goes last: it is the one thing that is always in front of\n // everything, including a consumer's own registered layers.\n return [...base, ...extrasRef.current, cursorLayer];\n // extrasVersion drives re-reads of extrasRef when layers are registered/detached;\n // viewRegistryVersion does the same for registered views.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [layers, debugOverlay, extrasVersion, viewRegistry, viewRegistryVersion,\n cursorLayer]);\n\n useEffect(() => {\n const redraw = (): void => { requestRedraw(); };\n const offs: (() => void)[] = [];\n for (const layer of composed) if (layer.subscribe) offs.push(layer.subscribe(redraw));\n return () => { for (const off of offs) off(); };\n }, [composed, requestRedraw]);\n\n return composed;\n}\n"],"mappings":";;;;;;;;AAWA,SAAgB,cACd,WACA,OACA,gBACA,iBACwB;CACxB,MAAM,kBAAkB,cACf,iBAAiB,0BAA0B,cAAc,IAAI,QACpE,CAAC,cAAc,CACjB;CAEA,OAAO,cAAsC;EAC3C,MAAM,iBAEF,CAAC;EACL,KAAK,MAAM,QAAQ;GAAC;GAAQ;GAAiB;EAAkB,GAAY;GACzE,MAAM,QAAQ,UAAU;GACxB,IAAI,cAAc,KAAK,GAAG,eAAe,QAAQ,MAAM;EACzD;EACA,MAAM,MAA8B,qBAClC,kBAAkB;GAAE,GAAG;GAAW,iBAAiB,EAAE,OAAO,gBAAgB;EAAE,IAAI,WAClF,gBAAgB,UAAU,OAAO,QAAQ,eAC3C;EACA,IAAI,OACF,kBAAkB,KAAK,eAAe,kBAAkB,MAAM,iBAAiB;EAEjF,OAAO;CACT,GAAG;EAAC;EAAW;EAAO;EAAiB;CAAe,CAAC;AACzD;;;;;;AAiBA,SAAgB,kBACd,QACA,OACA,eACwB;CACxB,MAAM,EAAE,cAAc,cAAc,WAAW,eAAe,gBAAgB;CAC9E,MAAM,sBAAsB,qBAC1B,aAAa,OAAmB,cAAc,UAAU,EAAE,YAAY,CAAC,IAAI,CAAC,YAAY,CAAC,SACnF,cAAc,WAAW,KAAK,SAC9B,CACR;CAEA,MAAM,WAAW,cAAc;EAG7B,MAAM,YAAY,eACd,CAAC,GAAG,QAAQ,GAAG,aAAa,KAAK,CAAC,CAAC,QAAQ,MAAM,EAAE,KAAK,CAAC,CAAC,KAAK,MAAM,EAAE,KAA6B,CAAC,IACrG;EAIJ,OAAO;GAAC,GAHK,eAAe,CAAC,GAAG,WAAW,YAAY,IAAI;GAG1C,GAAG,UAAU;GAAS;EAAW;CAIpD,GAAG;EAAC;EAAQ;EAAc;EAAe;EAAc;EACnD;CAAW,CAAC;CAEhB,gBAAgB;EACd,MAAM,eAAqB;GAAE,cAAc;EAAG;EAC9C,MAAM,OAAuB,CAAC;EAC9B,KAAK,MAAM,SAAS,UAAU,IAAI,MAAM,WAAW,KAAK,KAAK,MAAM,UAAU,MAAM,CAAC;EACpF,aAAa;GAAE,KAAK,MAAM,OAAO,MAAM,IAAI;EAAG;CAChD,GAAG,CAAC,UAAU,aAAa,CAAC;CAE5B,OAAO;AACT"}
1
+ {"version":3,"file":"useLayerStack.js","names":[],"sources":["../../src/canvas/useLayerStack.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useSyncExternalStore } from 'react';\nimport type { RenderLayer } from 'core/layers/render';\nimport type { Paint } from '@weasel-js/paint';\nimport { createBackgroundFillLayer } from 'core/layers/backgroundFill';\nimport { composeOrderedLayers, placeToolOverlays } from './layerOrder';\nimport { STANDARD_SLOTS, isCustomEntry } from './layerSlots';\nimport type { CanvasProps } from './canvasProps';\nimport type { ViewRegistry } from './viewRegistry';\n\n/** The `layers` prop's slots and custom entries in draw order, with the background fill beneath,\n * the decoration layer above, and the active tool's overlays placed among them. */\nexport function useSlotLayers(\n layersMap: CanvasProps<unknown>['layers'],\n tools: CanvasProps<unknown>['tools'],\n backgroundFill: Paint | undefined,\n decorationLayer: RenderLayer<unknown> | undefined,\n): RenderLayer<unknown>[] {\n const backgroundLayer = useMemo(\n () => (backgroundFill ? createBackgroundFillLayer(backgroundFill) : undefined),\n [backgroundFill],\n );\n\n return useMemo<RenderLayer<unknown>[]>(() => {\n const standardLayers: Partial<\n Record<(typeof STANDARD_SLOTS)[number], RenderLayer<unknown>>\n > = {};\n for (const slot of ['grid', 'cellHighlight', 'selectionOverlay'] as const) {\n const entry = layersMap[slot];\n if (isCustomEntry(entry)) standardLayers[slot] = entry.layer;\n }\n const out: RenderLayer<unknown>[] = composeOrderedLayers(\n decorationLayer ? { ...layersMap, decorationLayer: { layer: decorationLayer } } : layersMap,\n standardLayers, layersMap.scene?.layers, backgroundLayer,\n );\n if (tools) {\n placeToolOverlays(out, standardLayers.selectionOverlay, tools.getActiveOverlays);\n }\n return out;\n }, [layersMap, tools, backgroundLayer, decorationLayer]);\n}\n\n/** What the canvas paints on top of its slot layers. */\nexport interface StackAbove {\n viewRegistry: ViewRegistry | null;\n debugOverlay: RenderLayer<unknown> | null | undefined;\n /** Registered layers; re-read whenever `extrasVersion` changes. */\n extrasRef: { readonly current: ReadonlySet<RenderLayer<unknown>> };\n extrasVersion: number;\n cursorLayer: RenderLayer<unknown>;\n}\n\n/**\n * The full stack the canvas paints: its slot layers, then root view markers, registered views, the debug overlay,\n * externally registered layers, and the cursor. Every layer that can change on its own\n * repaints the canvas through `requestRedraw`.\n */\nexport function useComposedLayers(\n layers: readonly RenderLayer<unknown>[],\n above: StackAbove,\n requestRedraw: () => void,\n): RenderLayer<unknown>[] {\n const { viewRegistry, debugOverlay, extrasRef, extrasVersion, cursorLayer } = above;\n const viewRegistryVersion = useSyncExternalStore(\n useCallback((cb: () => void) => viewRegistry?.subscribe(cb) ?? (() => {}), [viewRegistry]),\n () => viewRegistry?.getVersion() ?? 0,\n () => 0,\n );\n\n const composed = useMemo(() => {\n // Views paint through the surface's own stack, so they sit above it and\n // below both the debug overlay and externally-registered layers. Root\n // markers sit under the views, as a view's own markers sit under its nested ones.\n const withViews = viewRegistry\n ? [\n ...layers,\n ...viewRegistry.markerLayers(null),\n ...viewRegistry.list().filter((r) => r.paint).map((r) => r.layer as RenderLayer<unknown>),\n ]\n : layers;\n const base = debugOverlay ? [...withViews, debugOverlay] : withViews;\n // The cursor goes last: it is the one thing that is always in front of\n // everything, including a consumer's own registered layers.\n return [...base, ...extrasRef.current, cursorLayer];\n // extrasVersion drives re-reads of extrasRef when layers are registered/detached;\n // viewRegistryVersion does the same for registered views.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [layers, debugOverlay, extrasVersion, viewRegistry, viewRegistryVersion,\n cursorLayer]);\n\n useEffect(() => {\n const redraw = (): void => { requestRedraw(); };\n const offs: (() => void)[] = [];\n for (const layer of composed) if (layer.subscribe) offs.push(layer.subscribe(redraw));\n return () => { for (const off of offs) off(); };\n }, [composed, requestRedraw]);\n\n return composed;\n}\n"],"mappings":";;;;;;;;AAWA,SAAgB,cACd,WACA,OACA,gBACA,iBACwB;CACxB,MAAM,kBAAkB,cACf,iBAAiB,0BAA0B,cAAc,IAAI,QACpE,CAAC,cAAc,CACjB;CAEA,OAAO,cAAsC;EAC3C,MAAM,iBAEF,CAAC;EACL,KAAK,MAAM,QAAQ;GAAC;GAAQ;GAAiB;EAAkB,GAAY;GACzE,MAAM,QAAQ,UAAU;GACxB,IAAI,cAAc,KAAK,GAAG,eAAe,QAAQ,MAAM;EACzD;EACA,MAAM,MAA8B,qBAClC,kBAAkB;GAAE,GAAG;GAAW,iBAAiB,EAAE,OAAO,gBAAgB;EAAE,IAAI,WAClF,gBAAgB,UAAU,OAAO,QAAQ,eAC3C;EACA,IAAI,OACF,kBAAkB,KAAK,eAAe,kBAAkB,MAAM,iBAAiB;EAEjF,OAAO;CACT,GAAG;EAAC;EAAW;EAAO;EAAiB;CAAe,CAAC;AACzD;;;;;;AAiBA,SAAgB,kBACd,QACA,OACA,eACwB;CACxB,MAAM,EAAE,cAAc,cAAc,WAAW,eAAe,gBAAgB;CAC9E,MAAM,sBAAsB,qBAC1B,aAAa,OAAmB,cAAc,UAAU,EAAE,YAAY,CAAC,IAAI,CAAC,YAAY,CAAC,SACnF,cAAc,WAAW,KAAK,SAC9B,CACR;CAEA,MAAM,WAAW,cAAc;EAI7B,MAAM,YAAY,eACd;GACE,GAAG;GACH,GAAG,aAAa,aAAa,IAAI;GACjC,GAAG,aAAa,KAAK,CAAC,CAAC,QAAQ,MAAM,EAAE,KAAK,CAAC,CAAC,KAAK,MAAM,EAAE,KAA6B;EAC1F,IACA;EAIJ,OAAO;GAAC,GAHK,eAAe,CAAC,GAAG,WAAW,YAAY,IAAI;GAG1C,GAAG,UAAU;GAAS;EAAW;CAIpD,GAAG;EAAC;EAAQ;EAAc;EAAe;EAAc;EACnD;CAAW,CAAC;CAEhB,gBAAgB;EACd,MAAM,eAAqB;GAAE,cAAc;EAAG;EAC9C,MAAM,OAAuB,CAAC;EAC9B,KAAK,MAAM,SAAS,UAAU,IAAI,MAAM,WAAW,KAAK,KAAK,MAAM,UAAU,MAAM,CAAC;EACpF,aAAa;GAAE,KAAK,MAAM,OAAO,MAAM,IAAI;EAAG;CAChD,GAAG,CAAC,UAAU,aAAa,CAAC;CAE5B,OAAO;AACT"}
@@ -0,0 +1,121 @@
1
+ import { normalizeView } from "../core/viewport/view.js";
2
+ import { isRectAim, rectAim, viewForAim } from "../features/viewports/aim.js";
3
+ import { viewportWorldRect } from "../core/viewport/viewportWorldRect.js";
4
+ import { useCanvasView } from "./useCanvasView.js";
5
+ import { sizeOf } from "./useViewNesting.js";
6
+ import { IDENTITY_VIEW, UNMEASURED_DIMS } from "./viewRegistry.js";
7
+ import { useCallback } from "react";
8
+ import { useLatest } from "@weasel-js/react";
9
+
10
+ //#region src/canvas/useViewAim.ts
11
+ /**
12
+ * A `<CanvasView>`'s camera and its aim, whichever of the two the view holds.
13
+ * A camera-held view answers its aim from its camera; an aim-held one derives
14
+ * its camera from the aim, through the same thunk a derived `view` uses.
15
+ */
16
+ /** `held` carried by the camera move that turns the world rect `from` into `to`: panned and scaled
17
+ * with it, so the aim keeps its own shape rather than taking the frame's. */
18
+ function carried(held, from, to) {
19
+ const kx = from.width > 0 ? to.width / from.width : 1;
20
+ const ky = from.height > 0 ? to.height / from.height : 1;
21
+ return {
22
+ x: to.x + (held.x - from.x) * kx,
23
+ y: to.y + (held.y - from.y) * ky,
24
+ width: held.width * kx,
25
+ height: held.height * ky
26
+ };
27
+ }
28
+ function useViewAim(args) {
29
+ const { props, id, registry, derivedAt, rectAt, rectNow } = args;
30
+ const { view, defaultView, onViewChange, viewBounds, aim: aimProp, onAimChange } = props;
31
+ const held = aimProp !== void 0 ? "aim" : "camera";
32
+ const live = useLatest({
33
+ view,
34
+ onViewChange,
35
+ aimProp,
36
+ onAimChange
37
+ });
38
+ /** The rect an aim-held view holds, for a surface frame. */
39
+ const heldAt = useCallback((outer, dims) => {
40
+ const a = live.current.aimProp;
41
+ return typeof a === "function" ? derivedAt(a, outer, dims) : a;
42
+ }, [live, derivedAt]);
43
+ /** The camera derived for a surface frame, or `undefined` for a camera the view keeps. */
44
+ const derivedCamera = useCallback((outer, dims) => {
45
+ const rect = heldAt(outer, dims);
46
+ if (rect) return viewForAim(rect, sizeOf(rectAt(outer, dims)), "contain");
47
+ const v = live.current.view;
48
+ return typeof v === "function" ? normalizeView(derivedAt(v, outer, dims)) : void 0;
49
+ }, [
50
+ live,
51
+ heldAt,
52
+ rectAt,
53
+ derivedAt
54
+ ]);
55
+ const surfaceFrame = useCallback(() => {
56
+ const surface = registry?.surface();
57
+ return [surface?.view() ?? IDENTITY_VIEW, surface?.dims() ?? UNMEASURED_DIMS];
58
+ }, [registry]);
59
+ /** A camera write on an aim-held view, reported as the held rect it carries. */
60
+ const reportWrite = useCallback((next) => {
61
+ const frame = surfaceFrame();
62
+ const held = heldAt(...frame);
63
+ const current = derivedCamera(...frame);
64
+ if (!held || !current) return;
65
+ const size = sizeOf(rectAt(...frame));
66
+ live.current.onAimChange?.(carried(held, viewportWorldRect(current, size), viewportWorldRect(next, size)));
67
+ }, [
68
+ live,
69
+ surfaceFrame,
70
+ heldAt,
71
+ derivedCamera,
72
+ rectAt
73
+ ]);
74
+ const state = useCanvasView({
75
+ view: held === "aim" || typeof view === "function" ? () => derivedCamera(...surfaceFrame()) : view,
76
+ defaultView,
77
+ onViewChange: held === "aim" ? onAimChange && reportWrite : onViewChange,
78
+ viewBounds,
79
+ dims: () => sizeOf(rectNow()),
80
+ requestRedraw: () => registry?.surface()?.requestRedraw(),
81
+ controlledBy: held === "aim" ? "aim" : "view"
82
+ });
83
+ const { getView, setViewFromInput } = state;
84
+ const aim = useCallback(() => {
85
+ const rect = heldAt(...surfaceFrame());
86
+ return rectAim(id, rect ?? viewportWorldRect(getView(), sizeOf(rectNow())));
87
+ }, [
88
+ id,
89
+ heldAt,
90
+ surfaceFrame,
91
+ getView,
92
+ rectNow
93
+ ]);
94
+ const setAim = useCallback((next) => {
95
+ if (!isRectAim(next)) return false;
96
+ const { view: v, onViewChange: onView, aimProp: a, onAimChange: report } = live.current;
97
+ if (a === void 0) {
98
+ if (v !== void 0 && !onView) return false;
99
+ setViewFromInput(viewForAim(next.extent, sizeOf(rectNow()), "exact"));
100
+ return true;
101
+ }
102
+ report?.(next.extent);
103
+ return report !== void 0;
104
+ }, [
105
+ live,
106
+ setViewFromInput,
107
+ rectNow
108
+ ]);
109
+ const cameraAt = useCallback((outer, dims) => derivedCamera(outer, dims) ?? getView(), [derivedCamera, getView]);
110
+ return {
111
+ ...state,
112
+ held,
113
+ aim,
114
+ setAim,
115
+ cameraAt
116
+ };
117
+ }
118
+
119
+ //#endregion
120
+ export { useViewAim };
121
+ //# sourceMappingURL=useViewAim.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useViewAim.js","names":[],"sources":["../../src/canvas/useViewAim.ts"],"sourcesContent":["/**\n * A `<CanvasView>`'s camera and its aim, whichever of the two the view holds.\n * A camera-held view answers its aim from its camera; an aim-held one derives\n * its camera from the aim, through the same thunk a derived `view` uses.\n */\nimport { useCallback } from 'react';\nimport { useLatest } from '@weasel-js/react';\nimport type { Dims } from 'core/layers/render';\nimport { normalizeView, type View } from 'core/viewport/view';\nimport type { Bounds } from 'core/viewport/fitViewToBounds';\nimport { viewportWorldRect } from 'core/viewport/viewportWorldRect';\nimport { isRectAim, rectAim, viewForAim, type ViewAim } from 'features/viewports/aim';\nimport { useCanvasView, type CanvasViewState } from './useCanvasView';\nimport type { CanvasViewProps, ViewRect } from './canvasViewProps';\nimport { sizeOf, type useViewNesting } from './useViewNesting';\nimport { IDENTITY_VIEW, UNMEASURED_DIMS, type ViewRegistry } from './viewRegistry';\n\n/** `held` carried by the camera move that turns the world rect `from` into `to`: panned and scaled\n * with it, so the aim keeps its own shape rather than taking the frame's. */\nfunction carried(held: Bounds, from: Bounds, to: Bounds): Bounds {\n const kx = from.width > 0 ? to.width / from.width : 1;\n const ky = from.height > 0 ? to.height / from.height : 1;\n return {\n x: to.x + (held.x - from.x) * kx,\n y: to.y + (held.y - from.y) * ky,\n width: held.width * kx,\n height: held.height * ky,\n };\n}\n\ntype Nesting = ReturnType<typeof useViewNesting>;\n\nexport function useViewAim(args: {\n props: Pick<CanvasViewProps, 'view' | 'defaultView' | 'onViewChange' | 'viewBounds' | 'aim' | 'onAimChange'>;\n id: string;\n registry: ViewRegistry | null;\n derivedAt: Nesting['derivedAt'];\n rectAt: Nesting['rectAt'];\n rectNow: () => ViewRect;\n}): CanvasViewState & {\n held: 'camera' | 'aim';\n aim(): ViewAim;\n setAim(aim: ViewAim): boolean;\n /** The camera for a frame the caller names. A derived camera is asked for that frame; any other is\n * the same whatever the surface is doing. */\n cameraAt(outer: View, dims: Dims): View;\n} {\n const { props, id, registry, derivedAt, rectAt, rectNow } = args;\n const { view, defaultView, onViewChange, viewBounds, aim: aimProp, onAimChange } = props;\n const held = aimProp !== undefined ? 'aim' : 'camera';\n const live = useLatest({ view, onViewChange, aimProp, onAimChange });\n\n /** The rect an aim-held view holds, for a surface frame. */\n const heldAt = useCallback((outer: View, dims: Dims): Bounds | undefined => {\n const a = live.current.aimProp;\n return typeof a === 'function' ? derivedAt(a, outer, dims) : a;\n }, [live, derivedAt]);\n\n /** The camera derived for a surface frame, or `undefined` for a camera the view keeps. */\n const derivedCamera = useCallback((outer: View, dims: Dims): View | undefined => {\n const rect = heldAt(outer, dims);\n if (rect) return viewForAim(rect, sizeOf(rectAt(outer, dims)), 'contain');\n const v = live.current.view;\n return typeof v === 'function' ? normalizeView(derivedAt(v, outer, dims)) : undefined;\n }, [live, heldAt, rectAt, derivedAt]);\n\n const surfaceFrame = useCallback((): [View, Dims] => {\n const surface = registry?.surface();\n return [surface?.view() ?? IDENTITY_VIEW, surface?.dims() ?? UNMEASURED_DIMS];\n }, [registry]);\n\n /** A camera write on an aim-held view, reported as the held rect it carries. */\n const reportWrite = useCallback((next: View): void => {\n const frame = surfaceFrame();\n const held = heldAt(...frame);\n const current = derivedCamera(...frame);\n if (!held || !current) return;\n const size = sizeOf(rectAt(...frame));\n live.current.onAimChange?.(carried(held, viewportWorldRect(current, size), viewportWorldRect(next, size)));\n }, [live, surfaceFrame, heldAt, derivedCamera, rectAt]);\n\n const state = useCanvasView({\n view: held === 'aim' || typeof view === 'function' ? () => derivedCamera(...surfaceFrame())! : view,\n defaultView,\n onViewChange: held === 'aim'\n ? onAimChange && reportWrite\n : onViewChange,\n viewBounds,\n dims: () => sizeOf(rectNow()),\n requestRedraw: () => registry?.surface()?.requestRedraw(),\n controlledBy: held === 'aim' ? 'aim' : 'view',\n });\n const { getView, setViewFromInput } = state;\n\n const aim = useCallback((): ViewAim => {\n const rect = heldAt(...surfaceFrame());\n return rectAim(id, rect ?? viewportWorldRect(getView(), sizeOf(rectNow())));\n }, [id, heldAt, surfaceFrame, getView, rectNow]);\n\n const setAim = useCallback((next: ViewAim): boolean => {\n if (!isRectAim(next)) return false;\n const { view: v, onViewChange: onView, aimProp: a, onAimChange: report } = live.current;\n if (a === undefined) {\n if (v !== undefined && !onView) return false;\n setViewFromInput(viewForAim(next.extent, sizeOf(rectNow()), 'exact'));\n return true;\n }\n report?.(next.extent);\n return report !== undefined;\n }, [live, setViewFromInput, rectNow]);\n\n const cameraAt = useCallback(\n (outer: View, dims: Dims): View => derivedCamera(outer, dims) ?? getView(),\n [derivedCamera, getView],\n );\n\n return { ...state, held, aim, setAim, cameraAt };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAmBA,SAAS,QAAQ,MAAc,MAAc,IAAoB;CAC/D,MAAM,KAAK,KAAK,QAAQ,IAAI,GAAG,QAAQ,KAAK,QAAQ;CACpD,MAAM,KAAK,KAAK,SAAS,IAAI,GAAG,SAAS,KAAK,SAAS;CACvD,OAAO;EACL,GAAG,GAAG,KAAK,KAAK,IAAI,KAAK,KAAK;EAC9B,GAAG,GAAG,KAAK,KAAK,IAAI,KAAK,KAAK;EAC9B,OAAO,KAAK,QAAQ;EACpB,QAAQ,KAAK,SAAS;CACxB;AACF;AAIA,SAAgB,WAAW,MAczB;CACA,MAAM,EAAE,OAAO,IAAI,UAAU,WAAW,QAAQ,YAAY;CAC5D,MAAM,EAAE,MAAM,aAAa,cAAc,YAAY,KAAK,SAAS,gBAAgB;CACnF,MAAM,OAAO,YAAY,SAAY,QAAQ;CAC7C,MAAM,OAAO,UAAU;EAAE;EAAM;EAAc;EAAS;CAAY,CAAC;;CAGnE,MAAM,SAAS,aAAa,OAAa,SAAmC;EAC1E,MAAM,IAAI,KAAK,QAAQ;EACvB,OAAO,OAAO,MAAM,aAAa,UAAU,GAAG,OAAO,IAAI,IAAI;CAC/D,GAAG,CAAC,MAAM,SAAS,CAAC;;CAGpB,MAAM,gBAAgB,aAAa,OAAa,SAAiC;EAC/E,MAAM,OAAO,OAAO,OAAO,IAAI;EAC/B,IAAI,MAAM,OAAO,WAAW,MAAM,OAAO,OAAO,OAAO,IAAI,CAAC,GAAG,SAAS;EACxE,MAAM,IAAI,KAAK,QAAQ;EACvB,OAAO,OAAO,MAAM,aAAa,cAAc,UAAU,GAAG,OAAO,IAAI,CAAC,IAAI;CAC9E,GAAG;EAAC;EAAM;EAAQ;EAAQ;CAAS,CAAC;CAEpC,MAAM,eAAe,kBAAgC;EACnD,MAAM,UAAU,UAAU,QAAQ;EAClC,OAAO,CAAC,SAAS,KAAK,KAAK,eAAe,SAAS,KAAK,KAAK,eAAe;CAC9E,GAAG,CAAC,QAAQ,CAAC;;CAGb,MAAM,cAAc,aAAa,SAAqB;EACpD,MAAM,QAAQ,aAAa;EAC3B,MAAM,OAAO,OAAO,GAAG,KAAK;EAC5B,MAAM,UAAU,cAAc,GAAG,KAAK;EACtC,IAAI,CAAC,QAAQ,CAAC,SAAS;EACvB,MAAM,OAAO,OAAO,OAAO,GAAG,KAAK,CAAC;EACpC,KAAK,QAAQ,cAAc,QAAQ,MAAM,kBAAkB,SAAS,IAAI,GAAG,kBAAkB,MAAM,IAAI,CAAC,CAAC;CAC3G,GAAG;EAAC;EAAM;EAAc;EAAQ;EAAe;CAAM,CAAC;CAEtD,MAAM,QAAQ,cAAc;EAC1B,MAAM,SAAS,SAAS,OAAO,SAAS,mBAAmB,cAAc,GAAG,aAAa,CAAC,IAAK;EAC/F;EACA,cAAc,SAAS,QACnB,eAAe,cACf;EACJ;EACA,YAAY,OAAO,QAAQ,CAAC;EAC5B,qBAAqB,UAAU,QAAQ,CAAC,EAAE,cAAc;EACxD,cAAc,SAAS,QAAQ,QAAQ;CACzC,CAAC;CACD,MAAM,EAAE,SAAS,qBAAqB;CAEtC,MAAM,MAAM,kBAA2B;EACrC,MAAM,OAAO,OAAO,GAAG,aAAa,CAAC;EACrC,OAAO,QAAQ,IAAI,QAAQ,kBAAkB,QAAQ,GAAG,OAAO,QAAQ,CAAC,CAAC,CAAC;CAC5E,GAAG;EAAC;EAAI;EAAQ;EAAc;EAAS;CAAO,CAAC;CAE/C,MAAM,SAAS,aAAa,SAA2B;EACrD,IAAI,CAAC,UAAU,IAAI,GAAG,OAAO;EAC7B,MAAM,EAAE,MAAM,GAAG,cAAc,QAAQ,SAAS,GAAG,aAAa,WAAW,KAAK;EAChF,IAAI,MAAM,QAAW;GACnB,IAAI,MAAM,UAAa,CAAC,QAAQ,OAAO;GACvC,iBAAiB,WAAW,KAAK,QAAQ,OAAO,QAAQ,CAAC,GAAG,OAAO,CAAC;GACpE,OAAO;EACT;EACA,SAAS,KAAK,MAAM;EACpB,OAAO,WAAW;CACpB,GAAG;EAAC;EAAM;EAAkB;CAAO,CAAC;CAEpC,MAAM,WAAW,aACd,OAAa,SAAqB,cAAc,OAAO,IAAI,KAAK,QAAQ,GACzE,CAAC,eAAe,OAAO,CACzB;CAEA,OAAO;EAAE,GAAG;EAAO;EAAM;EAAK;EAAQ;CAAS;AACjD"}
@@ -13,6 +13,7 @@ var NO_RECT = {
13
13
  w: 0,
14
14
  h: 0
15
15
  };
16
+ /** A view rect's size, as the `Dims` its camera looks through. */
16
17
  var sizeOf = (rect) => ({
17
18
  width: rect.w,
18
19
  height: rect.h
@@ -60,7 +61,7 @@ function useViewNesting(registry, live) {
60
61
  y: r.y + frame.origin.y
61
62
  };
62
63
  }, [live, frameIn]);
63
- /** A derived camera, for a surface frame: asked of the frame it is measured in. */
64
+ /** A derived camera or aim, for a surface frame: asked of the frame it is measured in. */
64
65
  const derivedAt = useCallback((derive, outer, dims) => {
65
66
  const frame = frameIn(outer, dims);
66
67
  return frame ? derive(frame.view, frame.dims) : derive(outer, dims);
@@ -100,5 +101,5 @@ function useViewNesting(registry, live) {
100
101
  }
101
102
 
102
103
  //#endregion
103
- export { NO_RECT, useViewNesting };
104
+ export { NO_RECT, sizeOf, useViewNesting };
104
105
  //# sourceMappingURL=useViewNesting.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"useViewNesting.js","names":[],"sources":["../../src/canvas/useViewNesting.ts"],"sourcesContent":["/**\n * How a `<CanvasView>` sits in the view it is nested in: the frame its\n * `bounds` and derived camera are measured against, and the layers it may\n * paint. Without a `parent` the frame is the surface's.\n */\nimport { useCallback, useRef, useState } from 'react';\nimport type { Dims, RenderLayer } from 'core/layers/render';\nimport type { View } from 'core/viewport/view';\nimport type { CanvasViewProps, ViewRect } from './canvasViewProps';\nimport { createSceneLayerGate, viewPaintedLayers } from './sceneLayerPaint';\nimport type { ViewRegistration, ViewRegistry } from './viewRegistry';\n\nexport const NO_RECT: ViewRect = { x: 0, y: 0, w: 0, h: 0 };\n\n/** What a view's `bounds` and `view` are measured against: the surface, or\n * the view it is nested in. `origin` and `clip` are in canvas CSS px. */\nexport interface OuterFrame { view: View; dims: Dims; origin: { x: number; y: number }; clip?: ViewRect }\n\n/** The props the nesting reads, as of the last committed render. */\nexport type ViewNestingInputs = Pick<CanvasViewProps, 'parent' | 'bounds' | 'layers' | 'layerVisibility' | 'layerOrder'>;\n\nconst sizeOf = (rect: ViewRect): Dims => ({ width: rect.w, height: rect.h });\n\nexport function useViewNesting(\n registry: ViewRegistry | null,\n live: { readonly current: ViewNestingInputs },\n) {\n // Set while this view asks its parent for a frame, so a parent chain that\n // loops back finds no frame rather than recursing.\n const measuring = useRef(false);\n /** The frame this view is measured in, for a surface frame; `null` while\n * its parent is missing. */\n const frameIn = useCallback((outer: View, dims: Dims): OuterFrame | null => {\n const p = live.current.parent;\n if (p === undefined) return { view: outer, dims, origin: { x: 0, y: 0 } };\n const reg = registry?.get(p);\n if (!reg || measuring.current) return null;\n measuring.current = true;\n try {\n const r = reg.layer.resolvable(outer, dims);\n return {\n view: r.view, dims: sizeOf(r.rect), origin: { x: r.rect.x, y: r.rect.y }, clip: r.clip ?? r.rect,\n };\n } finally {\n measuring.current = false;\n }\n }, [live, registry]);\n\n const rectAt = useCallback((outer: View, dims: Dims): ViewRect => {\n const frame = frameIn(outer, dims);\n if (!frame) return NO_RECT;\n const b = live.current.bounds;\n const r = typeof b === 'function' ? b(frame.view, frame.dims) : b;\n return { ...r, x: r.x + frame.origin.x, y: r.y + frame.origin.y };\n }, [live, frameIn]);\n\n /** A derived camera, for a surface frame: asked of the frame it is measured in. */\n const derivedAt = useCallback((derive: (outer: View, dims: Dims) => View, outer: View, dims: Dims): View => {\n const frame = frameIn(outer, dims);\n return frame ? derive(frame.view, frame.dims) : derive(outer, dims);\n }, [frameIn]);\n\n /** The view this one is nested in, while it is registered. */\n const parentReg = useCallback((): ViewRegistration | undefined => {\n const p = live.current.parent;\n return p === undefined ? undefined : registry?.get(p);\n }, [live, registry]);\n\n /** The layers this view draws, in the order it draws them: of the surface's,\n * or of what the view it is nested in draws. */\n const drawnLayers = useCallback((): readonly RenderLayer<unknown>[] => {\n if (live.current.parent !== undefined) return viewPaintedLayers(parentReg()?.drawnLayers() ?? [], live.current);\n return viewPaintedLayers(registry?.surface()?.layers() ?? [], live.current);\n }, [registry, live, parentReg]);\n\n const [sceneLayerGate] = useState(createSceneLayerGate);\n /** What this view's picks, marquees and select-alls ask of a scene layer:\n * painted here, and in every view this one is nested in. */\n const layerIsPainted = useCallback((layerId: string): boolean => {\n if (!sceneLayerGate(registry?.surface()?.layers() ?? [], live.current)(layerId)) return false;\n if (live.current.parent === undefined) return true;\n return parentReg()?.layerIsPainted(layerId) ?? false;\n }, [registry, sceneLayerGate, live, parentReg]);\n return { frameIn, rectAt, derivedAt, drawnLayers, layerIsPainted };\n}\n"],"mappings":";;;;;;;;;AAYA,IAAa,UAAoB;CAAE,GAAG;CAAG,GAAG;CAAG,GAAG;CAAG,GAAG;AAAE;AAS1D,IAAM,UAAU,UAA0B;CAAE,OAAO,KAAK;CAAG,QAAQ,KAAK;AAAE;AAE1E,SAAgB,eACd,UACA,MACA;CAGA,MAAM,YAAY,OAAO,KAAK;;;CAG9B,MAAM,UAAU,aAAa,OAAa,SAAkC;EAC1E,MAAM,IAAI,KAAK,QAAQ;EACvB,IAAI,MAAM,QAAW,OAAO;GAAE,MAAM;GAAO;GAAM,QAAQ;IAAE,GAAG;IAAG,GAAG;GAAE;EAAE;EACxE,MAAM,MAAM,UAAU,IAAI,CAAC;EAC3B,IAAI,CAAC,OAAO,UAAU,SAAS,OAAO;EACtC,UAAU,UAAU;EACpB,IAAI;GACF,MAAM,IAAI,IAAI,MAAM,WAAW,OAAO,IAAI;GAC1C,OAAO;IACL,MAAM,EAAE;IAAM,MAAM,OAAO,EAAE,IAAI;IAAG,QAAQ;KAAE,GAAG,EAAE,KAAK;KAAG,GAAG,EAAE,KAAK;IAAE;IAAG,MAAM,EAAE,QAAQ,EAAE;GAC9F;EACF,UAAU;GACR,UAAU,UAAU;EACtB;CACF,GAAG,CAAC,MAAM,QAAQ,CAAC;CAEnB,MAAM,SAAS,aAAa,OAAa,SAAyB;EAChE,MAAM,QAAQ,QAAQ,OAAO,IAAI;EACjC,IAAI,CAAC,OAAO,OAAO;EACnB,MAAM,IAAI,KAAK,QAAQ;EACvB,MAAM,IAAI,OAAO,MAAM,aAAa,EAAE,MAAM,MAAM,MAAM,IAAI,IAAI;EAChE,OAAO;GAAE,GAAG;GAAG,GAAG,EAAE,IAAI,MAAM,OAAO;GAAG,GAAG,EAAE,IAAI,MAAM,OAAO;EAAE;CAClE,GAAG,CAAC,MAAM,OAAO,CAAC;;CAGlB,MAAM,YAAY,aAAa,QAA2C,OAAa,SAAqB;EAC1G,MAAM,QAAQ,QAAQ,OAAO,IAAI;EACjC,OAAO,QAAQ,OAAO,MAAM,MAAM,MAAM,IAAI,IAAI,OAAO,OAAO,IAAI;CACpE,GAAG,CAAC,OAAO,CAAC;;CAGZ,MAAM,YAAY,kBAAgD;EAChE,MAAM,IAAI,KAAK,QAAQ;EACvB,OAAO,MAAM,SAAY,SAAY,UAAU,IAAI,CAAC;CACtD,GAAG,CAAC,MAAM,QAAQ,CAAC;;;CAInB,MAAM,cAAc,kBAAmD;EACrE,IAAI,KAAK,QAAQ,WAAW,QAAW,OAAO,kBAAkB,UAAU,CAAC,EAAE,YAAY,KAAK,CAAC,GAAG,KAAK,OAAO;EAC9G,OAAO,kBAAkB,UAAU,QAAQ,CAAC,EAAE,OAAO,KAAK,CAAC,GAAG,KAAK,OAAO;CAC5E,GAAG;EAAC;EAAU;EAAM;CAAS,CAAC;CAE9B,MAAM,CAAC,kBAAkB,SAAS,oBAAoB;CAQtD,OAAO;EAAE;EAAS;EAAQ;EAAW;EAAa,gBAL3B,aAAa,YAA6B;GAC/D,IAAI,CAAC,eAAe,UAAU,QAAQ,CAAC,EAAE,OAAO,KAAK,CAAC,GAAG,KAAK,OAAO,CAAC,CAAC,OAAO,GAAG,OAAO;GACxF,IAAI,KAAK,QAAQ,WAAW,QAAW,OAAO;GAC9C,OAAO,UAAU,CAAC,EAAE,eAAe,OAAO,KAAK;EACjD,GAAG;GAAC;GAAU;GAAgB;GAAM;EAAS,CACK;CAAe;AACnE"}
1
+ {"version":3,"file":"useViewNesting.js","names":[],"sources":["../../src/canvas/useViewNesting.ts"],"sourcesContent":["/**\n * How a `<CanvasView>` sits in the view it is nested in: the frame its\n * `bounds` and derived camera are measured against, and the layers it may\n * paint. Without a `parent` the frame is the surface's.\n */\nimport { useCallback, useRef, useState } from 'react';\nimport type { Dims, RenderLayer } from 'core/layers/render';\nimport type { View } from 'core/viewport/view';\nimport type { CanvasViewProps, ViewRect } from './canvasViewProps';\nimport { createSceneLayerGate, viewPaintedLayers } from './sceneLayerPaint';\nimport type { ViewRegistration, ViewRegistry } from './viewRegistry';\n\nexport const NO_RECT: ViewRect = { x: 0, y: 0, w: 0, h: 0 };\n\n/** What a view's `bounds` and `view` are measured against: the surface, or\n * the view it is nested in. `origin` and `clip` are in canvas CSS px. */\nexport interface OuterFrame { view: View; dims: Dims; origin: { x: number; y: number }; clip?: ViewRect }\n\n/** The props the nesting reads, as of the last committed render. */\nexport type ViewNestingInputs = Pick<CanvasViewProps, 'parent' | 'bounds' | 'layers' | 'layerVisibility' | 'layerOrder'>;\n\n/** A view rect's size, as the `Dims` its camera looks through. */\nexport const sizeOf = (rect: ViewRect): Dims => ({ width: rect.w, height: rect.h });\n\nexport function useViewNesting(\n registry: ViewRegistry | null,\n live: { readonly current: ViewNestingInputs },\n) {\n // Set while this view asks its parent for a frame, so a parent chain that\n // loops back finds no frame rather than recursing.\n const measuring = useRef(false);\n /** The frame this view is measured in, for a surface frame; `null` while\n * its parent is missing. */\n const frameIn = useCallback((outer: View, dims: Dims): OuterFrame | null => {\n const p = live.current.parent;\n if (p === undefined) return { view: outer, dims, origin: { x: 0, y: 0 } };\n const reg = registry?.get(p);\n if (!reg || measuring.current) return null;\n measuring.current = true;\n try {\n const r = reg.layer.resolvable(outer, dims);\n return {\n view: r.view, dims: sizeOf(r.rect), origin: { x: r.rect.x, y: r.rect.y }, clip: r.clip ?? r.rect,\n };\n } finally {\n measuring.current = false;\n }\n }, [live, registry]);\n\n const rectAt = useCallback((outer: View, dims: Dims): ViewRect => {\n const frame = frameIn(outer, dims);\n if (!frame) return NO_RECT;\n const b = live.current.bounds;\n const r = typeof b === 'function' ? b(frame.view, frame.dims) : b;\n return { ...r, x: r.x + frame.origin.x, y: r.y + frame.origin.y };\n }, [live, frameIn]);\n\n /** A derived camera or aim, for a surface frame: asked of the frame it is measured in. */\n const derivedAt = useCallback(<T>(derive: (outer: View, dims: Dims) => T, outer: View, dims: Dims): T => {\n const frame = frameIn(outer, dims);\n return frame ? derive(frame.view, frame.dims) : derive(outer, dims);\n }, [frameIn]);\n\n /** The view this one is nested in, while it is registered. */\n const parentReg = useCallback((): ViewRegistration | undefined => {\n const p = live.current.parent;\n return p === undefined ? undefined : registry?.get(p);\n }, [live, registry]);\n\n /** The layers this view draws, in the order it draws them: of the surface's,\n * or of what the view it is nested in draws. */\n const drawnLayers = useCallback((): readonly RenderLayer<unknown>[] => {\n if (live.current.parent !== undefined) return viewPaintedLayers(parentReg()?.drawnLayers() ?? [], live.current);\n return viewPaintedLayers(registry?.surface()?.layers() ?? [], live.current);\n }, [registry, live, parentReg]);\n\n const [sceneLayerGate] = useState(createSceneLayerGate);\n /** What this view's picks, marquees and select-alls ask of a scene layer:\n * painted here, and in every view this one is nested in. */\n const layerIsPainted = useCallback((layerId: string): boolean => {\n if (!sceneLayerGate(registry?.surface()?.layers() ?? [], live.current)(layerId)) return false;\n if (live.current.parent === undefined) return true;\n return parentReg()?.layerIsPainted(layerId) ?? false;\n }, [registry, sceneLayerGate, live, parentReg]);\n return { frameIn, rectAt, derivedAt, drawnLayers, layerIsPainted };\n}\n"],"mappings":";;;;;;;;;AAYA,IAAa,UAAoB;CAAE,GAAG;CAAG,GAAG;CAAG,GAAG;CAAG,GAAG;AAAE;;AAU1D,IAAa,UAAU,UAA0B;CAAE,OAAO,KAAK;CAAG,QAAQ,KAAK;AAAE;AAEjF,SAAgB,eACd,UACA,MACA;CAGA,MAAM,YAAY,OAAO,KAAK;;;CAG9B,MAAM,UAAU,aAAa,OAAa,SAAkC;EAC1E,MAAM,IAAI,KAAK,QAAQ;EACvB,IAAI,MAAM,QAAW,OAAO;GAAE,MAAM;GAAO;GAAM,QAAQ;IAAE,GAAG;IAAG,GAAG;GAAE;EAAE;EACxE,MAAM,MAAM,UAAU,IAAI,CAAC;EAC3B,IAAI,CAAC,OAAO,UAAU,SAAS,OAAO;EACtC,UAAU,UAAU;EACpB,IAAI;GACF,MAAM,IAAI,IAAI,MAAM,WAAW,OAAO,IAAI;GAC1C,OAAO;IACL,MAAM,EAAE;IAAM,MAAM,OAAO,EAAE,IAAI;IAAG,QAAQ;KAAE,GAAG,EAAE,KAAK;KAAG,GAAG,EAAE,KAAK;IAAE;IAAG,MAAM,EAAE,QAAQ,EAAE;GAC9F;EACF,UAAU;GACR,UAAU,UAAU;EACtB;CACF,GAAG,CAAC,MAAM,QAAQ,CAAC;CAEnB,MAAM,SAAS,aAAa,OAAa,SAAyB;EAChE,MAAM,QAAQ,QAAQ,OAAO,IAAI;EACjC,IAAI,CAAC,OAAO,OAAO;EACnB,MAAM,IAAI,KAAK,QAAQ;EACvB,MAAM,IAAI,OAAO,MAAM,aAAa,EAAE,MAAM,MAAM,MAAM,IAAI,IAAI;EAChE,OAAO;GAAE,GAAG;GAAG,GAAG,EAAE,IAAI,MAAM,OAAO;GAAG,GAAG,EAAE,IAAI,MAAM,OAAO;EAAE;CAClE,GAAG,CAAC,MAAM,OAAO,CAAC;;CAGlB,MAAM,YAAY,aAAgB,QAAwC,OAAa,SAAkB;EACvG,MAAM,QAAQ,QAAQ,OAAO,IAAI;EACjC,OAAO,QAAQ,OAAO,MAAM,MAAM,MAAM,IAAI,IAAI,OAAO,OAAO,IAAI;CACpE,GAAG,CAAC,OAAO,CAAC;;CAGZ,MAAM,YAAY,kBAAgD;EAChE,MAAM,IAAI,KAAK,QAAQ;EACvB,OAAO,MAAM,SAAY,SAAY,UAAU,IAAI,CAAC;CACtD,GAAG,CAAC,MAAM,QAAQ,CAAC;;;CAInB,MAAM,cAAc,kBAAmD;EACrE,IAAI,KAAK,QAAQ,WAAW,QAAW,OAAO,kBAAkB,UAAU,CAAC,EAAE,YAAY,KAAK,CAAC,GAAG,KAAK,OAAO;EAC9G,OAAO,kBAAkB,UAAU,QAAQ,CAAC,EAAE,OAAO,KAAK,CAAC,GAAG,KAAK,OAAO;CAC5E,GAAG;EAAC;EAAU;EAAM;CAAS,CAAC;CAE9B,MAAM,CAAC,kBAAkB,SAAS,oBAAoB;CAQtD,OAAO;EAAE;EAAS;EAAQ;EAAW;EAAa,gBAL3B,aAAa,YAA6B;GAC/D,IAAI,CAAC,eAAe,UAAU,QAAQ,CAAC,EAAE,OAAO,KAAK,CAAC,GAAG,KAAK,OAAO,CAAC,CAAC,OAAO,GAAG,OAAO;GACxF,IAAI,KAAK,QAAQ,WAAW,QAAW,OAAO;GAC9C,OAAO,UAAU,CAAC,EAAE,eAAe,OAAO,KAAK;EACjD,GAAG;GAAC;GAAU;GAAgB;GAAM;EAAS,CACK;CAAe;AACnE"}
@@ -10,7 +10,7 @@ import { useLatest } from "@weasel-js/react";
10
10
  function useViewRegistrySurface(viewRegistry, layers, frame, services) {
11
11
  const surfaceLayersRef = useLatest(layers);
12
12
  const { viewRef, dimsRef, helpersRef } = frame;
13
- const { requestRedraw, hitTestExtrasIn, clientToWorld } = services;
13
+ const { requestRedraw, hitTestExtrasIn, clientToWorld, setView } = services;
14
14
  useEffect(() => {
15
15
  return viewRegistry?.attachSurface({
16
16
  view: () => viewRef.current,
@@ -20,6 +20,7 @@ function useViewRegistrySurface(viewRegistry, layers, frame, services) {
20
20
  chromeState: () => helpersRef.current.getChromeState(),
21
21
  hitTestExtras: hitTestExtrasIn,
22
22
  clientToWorld,
23
+ setView,
23
24
  claimsAbove: (x, y) => {
24
25
  const view = viewRef.current;
25
26
  const worldX = x / view.scale.x + view.x;
@@ -35,7 +36,8 @@ function useViewRegistrySurface(viewRegistry, layers, frame, services) {
35
36
  surfaceLayersRef,
36
37
  viewRef,
37
38
  helpersRef,
38
- clientToWorld
39
+ clientToWorld,
40
+ setView
39
41
  ]);
40
42
  }
41
43
 
@@ -1 +1 @@
1
- {"version":3,"file":"useViewRegistrySurface.js","names":[],"sources":["../../src/canvas/useViewRegistrySurface.ts"],"sourcesContent":["import { useEffect } from 'react';\nimport { useLatest } from '@weasel-js/react';\nimport type { View } from 'core/viewport/view';\nimport type { Dims, RenderLayer } from 'core/layers/render';\nimport type { CanvasHelpers } from './useViewHelpers';\nimport type { SurfaceHandle, ViewRegistry } from './viewRegistry';\n\n/** What the canvas reads at draw and hit time that changes outside a render. */\nexport interface SurfaceFrame {\n viewRef: { readonly current: View };\n dimsRef: { readonly current: Dims };\n helpersRef: { readonly current: Pick<CanvasHelpers<unknown>, 'getChromeState'> | null };\n}\n\n/** What the canvas does for a view that asks. */\nexport interface SurfaceServices {\n requestRedraw: () => void;\n hitTestExtrasIn: SurfaceHandle['hitTestExtras'];\n clientToWorld: SurfaceHandle['clientToWorld'];\n}\n\n/**\n * Attach this canvas to the view registry as its surface. The stack a view paints through its\n * own camera is this canvas's `layers`, before the views themselves are folded in — otherwise\n * a view would contain itself. Published by reference and read at draw time.\n */\nexport function useViewRegistrySurface(\n viewRegistry: ViewRegistry | null,\n layers: readonly RenderLayer<unknown>[],\n frame: SurfaceFrame,\n services: SurfaceServices,\n): void {\n const surfaceLayersRef = useLatest<readonly RenderLayer<unknown>[]>(layers);\n const { viewRef, dimsRef, helpersRef } = frame;\n const { requestRedraw, hitTestExtrasIn, clientToWorld } = services;\n useEffect(() => {\n return viewRegistry?.attachSurface({\n view: () => viewRef.current,\n dims: () => dimsRef.current,\n layers: () => surfaceLayersRef.current,\n requestRedraw,\n chromeState: () => helpersRef.current!.getChromeState(),\n hitTestExtras: hitTestExtrasIn,\n clientToWorld,\n claimsAbove: (x, y) => {\n const view = viewRef.current;\n const worldX = x / view.scale.x + view.x;\n const worldY = y / view.scale.y + view.y;\n return hitTestExtrasIn(worldX, worldY, view, dimsRef.current, helpersRef.current) !== null;\n },\n });\n }, [viewRegistry, requestRedraw, hitTestExtrasIn, dimsRef, surfaceLayersRef, viewRef, helpersRef, clientToWorld]);\n}\n"],"mappings":";;;;;;;;;AA0BA,SAAgB,uBACd,cACA,QACA,OACA,UACM;CACN,MAAM,mBAAmB,UAA2C,MAAM;CAC1E,MAAM,EAAE,SAAS,SAAS,eAAe;CACzC,MAAM,EAAE,eAAe,iBAAiB,kBAAkB;CAC1D,gBAAgB;EACd,OAAO,cAAc,cAAc;GACjC,YAAY,QAAQ;GACpB,YAAY,QAAQ;GACpB,cAAc,iBAAiB;GAC/B;GACA,mBAAmB,WAAW,QAAS,eAAe;GACtD,eAAe;GACf;GACA,cAAc,GAAG,MAAM;IACrB,MAAM,OAAO,QAAQ;IACrB,MAAM,SAAS,IAAI,KAAK,MAAM,IAAI,KAAK;IACvC,MAAM,SAAS,IAAI,KAAK,MAAM,IAAI,KAAK;IACvC,OAAO,gBAAgB,QAAQ,QAAQ,MAAM,QAAQ,SAAS,WAAW,OAAO,MAAM;GACxF;EACF,CAAC;CACH,GAAG;EAAC;EAAc;EAAe;EAAiB;EAAS;EAAkB;EAAS;EAAY;CAAa,CAAC;AAClH"}
1
+ {"version":3,"file":"useViewRegistrySurface.js","names":[],"sources":["../../src/canvas/useViewRegistrySurface.ts"],"sourcesContent":["import { useEffect } from 'react';\nimport { useLatest } from '@weasel-js/react';\nimport type { View } from 'core/viewport/view';\nimport type { Dims, RenderLayer } from 'core/layers/render';\nimport type { CanvasHelpers } from './useViewHelpers';\nimport type { SurfaceHandle, ViewRegistry } from './viewRegistry';\n\n/** What the canvas reads at draw and hit time that changes outside a render. */\nexport interface SurfaceFrame {\n viewRef: { readonly current: View };\n dimsRef: { readonly current: Dims };\n helpersRef: { readonly current: Pick<CanvasHelpers<unknown>, 'getChromeState'> | null };\n}\n\n/** What the canvas does for a view that asks. */\nexport interface SurfaceServices {\n requestRedraw: () => void;\n hitTestExtrasIn: SurfaceHandle['hitTestExtras'];\n clientToWorld: SurfaceHandle['clientToWorld'];\n setView: NonNullable<SurfaceHandle['setView']>;\n}\n\n/**\n * Attach this canvas to the view registry as its surface. The stack a view paints through its\n * own camera is this canvas's `layers`, before the views themselves are folded in — otherwise\n * a view would contain itself. Published by reference and read at draw time.\n */\nexport function useViewRegistrySurface(\n viewRegistry: ViewRegistry | null,\n layers: readonly RenderLayer<unknown>[],\n frame: SurfaceFrame,\n services: SurfaceServices,\n): void {\n const surfaceLayersRef = useLatest<readonly RenderLayer<unknown>[]>(layers);\n const { viewRef, dimsRef, helpersRef } = frame;\n const { requestRedraw, hitTestExtrasIn, clientToWorld, setView } = services;\n useEffect(() => {\n return viewRegistry?.attachSurface({\n view: () => viewRef.current,\n dims: () => dimsRef.current,\n layers: () => surfaceLayersRef.current,\n requestRedraw,\n chromeState: () => helpersRef.current!.getChromeState(),\n hitTestExtras: hitTestExtrasIn,\n clientToWorld,\n setView,\n claimsAbove: (x, y) => {\n const view = viewRef.current;\n const worldX = x / view.scale.x + view.x;\n const worldY = y / view.scale.y + view.y;\n return hitTestExtrasIn(worldX, worldY, view, dimsRef.current, helpersRef.current) !== null;\n },\n });\n }, [viewRegistry, requestRedraw, hitTestExtrasIn, dimsRef, surfaceLayersRef, viewRef, helpersRef, clientToWorld, setView]);\n}\n"],"mappings":";;;;;;;;;AA2BA,SAAgB,uBACd,cACA,QACA,OACA,UACM;CACN,MAAM,mBAAmB,UAA2C,MAAM;CAC1E,MAAM,EAAE,SAAS,SAAS,eAAe;CACzC,MAAM,EAAE,eAAe,iBAAiB,eAAe,YAAY;CACnE,gBAAgB;EACd,OAAO,cAAc,cAAc;GACjC,YAAY,QAAQ;GACpB,YAAY,QAAQ;GACpB,cAAc,iBAAiB;GAC/B;GACA,mBAAmB,WAAW,QAAS,eAAe;GACtD,eAAe;GACf;GACA;GACA,cAAc,GAAG,MAAM;IACrB,MAAM,OAAO,QAAQ;IACrB,MAAM,SAAS,IAAI,KAAK,MAAM,IAAI,KAAK;IACvC,MAAM,SAAS,IAAI,KAAK,MAAM,IAAI,KAAK;IACvC,OAAO,gBAAgB,QAAQ,QAAQ,MAAM,QAAQ,SAAS,WAAW,OAAO,MAAM;GACxF;EACF,CAAC;CACH,GAAG;EAAC;EAAc;EAAe;EAAiB;EAAS;EAAkB;EAAS;EAAY;EAAe;CAAO,CAAC;AAC3H"}
@@ -0,0 +1,63 @@
1
+ import { aimSpace2d } from "../features/viewports/aim.js";
2
+ import { createViewMarkerLayer, grabViewMarker } from "../features/viewports/viewMarker.js";
3
+ import { layerAffordance } from "./affordanceAt.js";
4
+
5
+ //#region src/canvas/viewMarkers.ts
6
+ /** @internal */
7
+ function createViewMarkerRegistry(markers, deps) {
8
+ let gate = null;
9
+ const targetSpace = (target) => {
10
+ const surface = deps.surface();
11
+ if (!surface) return null;
12
+ if (target === null) return aimSpace2d(null, surface.view(), surface.dims());
13
+ const v = deps.resolve(target, surface.view(), surface.dims());
14
+ return v ? aimSpace2d(target, v.view, {
15
+ width: v.rect.w,
16
+ height: v.rect.h
17
+ }) : null;
18
+ };
19
+ return {
20
+ registerMarker: (props) => {
21
+ const { id } = props();
22
+ for (const live of markers.keys()) if (live().id === id) {
23
+ console.error(`[weasel] registerMarker: marker "${id}" is already on this surface, so this one is skipped. Marker ids are unique per surface; rename one.`);
24
+ return () => {};
25
+ }
26
+ markers.set(props, createViewMarkerLayer(props, deps.host));
27
+ deps.changed();
28
+ return () => {
29
+ markers.delete(props);
30
+ deps.changed();
31
+ };
32
+ },
33
+ markerLayers: (target) => {
34
+ const out = [];
35
+ for (const [props, layer] of markers) if (props().target === target) out.push(layer);
36
+ return out;
37
+ },
38
+ markerAt: (target, world) => {
39
+ if (!gate?.()) return null;
40
+ const space = targetSpace(target);
41
+ const at = space?.project(world);
42
+ if (!space || !at) return null;
43
+ const live = [...markers.keys()];
44
+ for (let i = live.length - 1; i >= 0; i--) {
45
+ const props = live[i]();
46
+ if (props.target !== target) continue;
47
+ const grab = grabViewMarker(props, deps.host, space, at);
48
+ if (grab) return layerAffordance(`view-marker:${props.id}`, { initialScratch: grab });
49
+ }
50
+ return null;
51
+ },
52
+ gateMarkers: (open) => {
53
+ gate = open;
54
+ return () => {
55
+ if (gate === open) gate = null;
56
+ };
57
+ }
58
+ };
59
+ }
60
+
61
+ //#endregion
62
+ export { createViewMarkerRegistry };
63
+ //# sourceMappingURL=viewMarkers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"viewMarkers.js","names":[],"sources":["../../src/canvas/viewMarkers.ts"],"sourcesContent":["/**\n * The marker half of a surface's view registry: which markers are live, the\n * layers they draw, and which one a press lands on.\n */\nimport type { Dims, RenderLayer } from 'core/layers/render';\nimport type { AffordanceHit } from '@weasel-js/routing';\nimport { aimSpace2d, type AimSpace } from 'features/viewports/aim';\nimport type { ResolvableView } from 'features/viewports/viewResolver';\nimport type { View } from 'core/viewport/view';\nimport {\n createViewMarkerLayer, grabViewMarker, type ViewMarkerHost, type ViewMarkerProps,\n} from 'features/viewports/viewMarker';\nimport { layerAffordance } from './affordanceAt';\n\n/** @internal Composed into `ViewRegistry`. */\nexport interface ViewMarkerRegistry {\n /** Returns an unregister. `props` is read at every draw. An id already live\n * on the surface logs an error and is skipped, and its unregister does nothing. */\n registerMarker(props: () => ViewMarkerProps): () => void;\n /** The layers of the markers drawn in `target`'s frame, in mount order. */\n markerLayers(target: string | null): readonly RenderLayer<unknown>[];\n /** The topmost interactive marker drawn in `target` under `world` (a point\n * in `target`'s world), as the affordance a press there routes. */\n markerAt(target: string | null, world: { x: number; y: number }): AffordanceHit | null;\n /** Lets `markerAt` answer while `open()` says the surface binds a marker drag;\n * until then it answers `null`, so a press falls through. Returns an undo. */\n gateMarkers(open: () => boolean): () => void;\n}\n\n/** What the marker half reads off the rest of the registry. */\nexport interface ViewMarkerRegistryDeps {\n host: ViewMarkerHost;\n /** The surface's camera and size, or `null` before one attaches. */\n surface(): { view(): View; dims(): Dims } | null;\n /** View `id` resolved against the surface, or `undefined` while there is none. */\n resolve(id: string, outer: View, dims: Dims): ResolvableView | undefined;\n changed(): void;\n}\n\n/** @internal */\nexport function createViewMarkerRegistry(\n markers: Map<() => ViewMarkerProps, RenderLayer<unknown>>,\n deps: ViewMarkerRegistryDeps,\n): ViewMarkerRegistry {\n let gate: (() => boolean) | null = null;\n const targetSpace = (target: string | null): AimSpace | null => {\n const surface = deps.surface();\n if (!surface) return null;\n if (target === null) return aimSpace2d(null, surface.view(), surface.dims());\n const v = deps.resolve(target, surface.view(), surface.dims());\n return v ? aimSpace2d(target, v.view, { width: v.rect.w, height: v.rect.h }) : null;\n };\n return {\n registerMarker: (props) => {\n const { id } = props();\n for (const live of markers.keys()) {\n if (live().id === id) {\n console.error(`[weasel] registerMarker: marker \"${id}\" is already on this surface, so this one is skipped. Marker ids are unique per surface; rename one.`);\n return () => {};\n }\n }\n markers.set(props, createViewMarkerLayer(props, deps.host));\n deps.changed();\n return () => { markers.delete(props); deps.changed(); };\n },\n markerLayers: (target) => {\n const out: RenderLayer<unknown>[] = [];\n for (const [props, layer] of markers) if (props().target === target) out.push(layer);\n return out;\n },\n markerAt: (target, world) => {\n if (!gate?.()) return null;\n const space = targetSpace(target);\n const at = space?.project(world);\n if (!space || !at) return null;\n const live = [...markers.keys()];\n for (let i = live.length - 1; i >= 0; i--) {\n const props = live[i]!();\n if (props.target !== target) continue;\n const grab = grabViewMarker(props, deps.host, space, at);\n if (grab) return layerAffordance(`view-marker:${props.id}`, { initialScratch: grab });\n }\n return null;\n },\n gateMarkers: (open) => {\n gate = open;\n return () => { if (gate === open) gate = null; };\n },\n };\n}\n"],"mappings":";;;;;;AAwCA,SAAgB,yBACd,SACA,MACoB;CACpB,IAAI,OAA+B;CACnC,MAAM,eAAe,WAA2C;EAC9D,MAAM,UAAU,KAAK,QAAQ;EAC7B,IAAI,CAAC,SAAS,OAAO;EACrB,IAAI,WAAW,MAAM,OAAO,WAAW,MAAM,QAAQ,KAAK,GAAG,QAAQ,KAAK,CAAC;EAC3E,MAAM,IAAI,KAAK,QAAQ,QAAQ,QAAQ,KAAK,GAAG,QAAQ,KAAK,CAAC;EAC7D,OAAO,IAAI,WAAW,QAAQ,EAAE,MAAM;GAAE,OAAO,EAAE,KAAK;GAAG,QAAQ,EAAE,KAAK;EAAE,CAAC,IAAI;CACjF;CACA,OAAO;EACL,iBAAiB,UAAU;GACzB,MAAM,EAAE,OAAO,MAAM;GACrB,KAAK,MAAM,QAAQ,QAAQ,KAAK,GAC9B,IAAI,KAAK,CAAC,CAAC,OAAO,IAAI;IACpB,QAAQ,MAAM,oCAAoC,GAAG,qGAAqG;IAC1J,aAAa,CAAC;GAChB;GAEF,QAAQ,IAAI,OAAO,sBAAsB,OAAO,KAAK,IAAI,CAAC;GAC1D,KAAK,QAAQ;GACb,aAAa;IAAE,QAAQ,OAAO,KAAK;IAAG,KAAK,QAAQ;GAAG;EACxD;EACA,eAAe,WAAW;GACxB,MAAM,MAA8B,CAAC;GACrC,KAAK,MAAM,CAAC,OAAO,UAAU,SAAS,IAAI,MAAM,CAAC,CAAC,WAAW,QAAQ,IAAI,KAAK,KAAK;GACnF,OAAO;EACT;EACA,WAAW,QAAQ,UAAU;GAC3B,IAAI,CAAC,OAAO,GAAG,OAAO;GACtB,MAAM,QAAQ,YAAY,MAAM;GAChC,MAAM,KAAK,OAAO,QAAQ,KAAK;GAC/B,IAAI,CAAC,SAAS,CAAC,IAAI,OAAO;GAC1B,MAAM,OAAO,CAAC,GAAG,QAAQ,KAAK,CAAC;GAC/B,KAAK,IAAI,IAAI,KAAK,SAAS,GAAG,KAAK,GAAG,KAAK;IACzC,MAAM,QAAQ,KAAK,EAAE,CAAE;IACvB,IAAI,MAAM,WAAW,QAAQ;IAC7B,MAAM,OAAO,eAAe,OAAO,KAAK,MAAM,OAAO,EAAE;IACvD,IAAI,MAAM,OAAO,gBAAgB,eAAe,MAAM,MAAM,EAAE,gBAAgB,KAAK,CAAC;GACtF;GACA,OAAO;EACT;EACA,cAAc,SAAS;GACrB,OAAO;GACP,aAAa;IAAE,IAAI,SAAS,MAAM,OAAO;GAAM;EACjD;CACF;AACF"}
@@ -1,4 +1,7 @@
1
1
  import { createViewResolver, viewContains } from "../features/viewports/viewResolver.js";
2
+ import { isRectAim, rectAim, viewForAim } from "../features/viewports/aim.js";
3
+ import { viewportWorldRect } from "../core/viewport/viewportWorldRect.js";
4
+ import { createViewMarkerRegistry } from "./viewMarkers.js";
2
5
  import { createEmitter } from "@weasel-js/registry";
3
6
  import { createContext, useContext, useMemo, useRef } from "react";
4
7
  import { jsx } from "react/jsx-runtime";
@@ -40,6 +43,7 @@ function ViewRegistryProvider({ children }) {
40
43
  const seqRef = useRef(0);
41
44
  const versionRef = useRef(0);
42
45
  const surfaceRef = useRef(null);
46
+ const markersRef = useRef(/* @__PURE__ */ new Map());
43
47
  const registry = useMemo(() => {
44
48
  const listeners = createEmitter();
45
49
  const changed = () => {
@@ -59,7 +63,7 @@ function ViewRegistryProvider({ children }) {
59
63
  }
60
64
  return false;
61
65
  };
62
- return {
66
+ const registry = {
63
67
  resolver: createViewResolver({
64
68
  views: () => {
65
69
  const surface = surfaceRef.current;
@@ -108,6 +112,27 @@ function ViewRegistryProvider({ children }) {
108
112
  }
109
113
  };
110
114
  },
115
+ aimOf: (id) => {
116
+ if (id !== null) return get(id)?.aim() ?? null;
117
+ const surface = surfaceRef.current;
118
+ return surface ? rectAim(null, viewportWorldRect(surface.view(), surface.dims())) : null;
119
+ },
120
+ setAim: (id, aim) => {
121
+ if (id !== null) return get(id)?.setAim(aim) ?? false;
122
+ const surface = surfaceRef.current;
123
+ if (!isRectAim(aim) || !surface?.setView) return false;
124
+ surface.setView(viewForAim(aim.extent, surface.dims(), "exact"));
125
+ return true;
126
+ },
127
+ ...createViewMarkerRegistry(markersRef.current, {
128
+ host: {
129
+ aimOf: (id) => registry.aimOf(id),
130
+ setAim: (id, aim) => registry.setAim(id, aim)
131
+ },
132
+ surface: () => surfaceRef.current,
133
+ resolve: (id, outer, dims) => get(id)?.layer.resolvable(outer, dims),
134
+ changed
135
+ }),
111
136
  getVersion: () => versionRef.current,
112
137
  subscribe: listeners.subscribe,
113
138
  surface: () => surfaceRef.current,
@@ -118,6 +143,7 @@ function ViewRegistryProvider({ children }) {
118
143
  };
119
144
  }
120
145
  };
146
+ return registry;
121
147
  }, []);
122
148
  return /* @__PURE__ */ jsx(ViewRegistryContext.Provider, {
123
149
  value: registry,