@weasel-js/core 1.11.1 → 1.13.0-rc.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 (488) hide show
  1. package/CHANGELOG.md +275 -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/animation/poseTarget.js +46 -15
  6. package/dist/animation/poseTarget.js.map +1 -1
  7. package/dist/animation/poseTransition.js +3 -3
  8. package/dist/animation/poseTransition.js.map +1 -1
  9. package/dist/animation/reflow.js +3 -6
  10. package/dist/animation/reflow.js.map +1 -1
  11. package/dist/animation/rig/bindRig.js +2 -2
  12. package/dist/animation/rig/bindRig.js.map +1 -1
  13. package/dist/animation/rig/skinBinding.js +2 -2
  14. package/dist/animation/rig/skinBinding.js.map +1 -1
  15. package/dist/barrel/canvas.js +5 -1
  16. package/dist/barrel/chrome.js +3 -1
  17. package/dist/barrel/paint.js +4 -4
  18. package/dist/barrel/paths.js +9 -4
  19. package/dist/barrel/renderer.js +3 -2
  20. package/dist/barrel/routing.js +4 -4
  21. package/dist/barrel/scene.js +7 -1
  22. package/dist/barrel/text.js +2 -2
  23. package/dist/barrel/viewport.js +6 -2
  24. package/dist/canvas/Canvas.js +2 -1
  25. package/dist/canvas/Canvas.js.map +1 -1
  26. package/dist/canvas/CanvasView.js +23 -34
  27. package/dist/canvas/CanvasView.js.map +1 -1
  28. package/dist/canvas/ModalityHud.js +1 -1
  29. package/dist/canvas/NodeShape.js +5 -1
  30. package/dist/canvas/NodeShape.js.map +1 -1
  31. package/dist/canvas/SceneCanvas/ActionsProviderIfRoot.js +1 -1
  32. package/dist/canvas/SceneCanvas/DepRegistryProviderIfRoot.js +1 -1
  33. package/dist/canvas/SceneCanvas/PointerProviderIfRoot.js +1 -1
  34. package/dist/canvas/SceneCanvas/SceneCanvasInput.js +2 -0
  35. package/dist/canvas/SceneCanvas/SceneCanvasInput.js.map +1 -1
  36. package/dist/canvas/SceneCanvas/SurfaceDeclarations.js +23 -0
  37. package/dist/canvas/SceneCanvas/SurfaceDeclarations.js.map +1 -0
  38. package/dist/canvas/SceneCanvas/canvasPassThrough.js +2 -1
  39. package/dist/canvas/SceneCanvas/canvasPassThrough.js.map +1 -1
  40. package/dist/canvas/SceneCanvas/defaultNodeRouting.js +2 -2
  41. package/dist/canvas/SceneCanvas/defaultNodeRouting.js.map +1 -1
  42. package/dist/canvas/SceneCanvas/props.js.map +1 -1
  43. package/dist/canvas/SceneCanvas/scenePointQuery.js +27 -5
  44. package/dist/canvas/SceneCanvas/scenePointQuery.js.map +1 -1
  45. package/dist/canvas/SceneCanvas/useActionTunings.js +6 -1
  46. package/dist/canvas/SceneCanvas/useActionTunings.js.map +1 -1
  47. package/dist/canvas/SceneCanvas/useCanvasDevtoolsRoot.js +1 -0
  48. package/dist/canvas/SceneCanvas/useCanvasDevtoolsRoot.js.map +1 -1
  49. package/dist/canvas/SceneCanvas/useCanvasTools.js +10 -2
  50. package/dist/canvas/SceneCanvas/useCanvasTools.js.map +1 -1
  51. package/dist/canvas/SceneCanvas/usePreviewGhostLayer.js +25 -19
  52. package/dist/canvas/SceneCanvas/usePreviewGhostLayer.js.map +1 -1
  53. package/dist/canvas/SceneCanvas/useSceneCanvasHandle.js +9 -3
  54. package/dist/canvas/SceneCanvas/useSceneCanvasHandle.js.map +1 -1
  55. package/dist/canvas/SceneCanvas/useSceneCanvasLayers.js +17 -2
  56. package/dist/canvas/SceneCanvas/useSceneCanvasLayers.js.map +1 -1
  57. package/dist/canvas/SceneCanvas/useSceneCanvasTools.js +2 -0
  58. package/dist/canvas/SceneCanvas/useSceneCanvasTools.js.map +1 -1
  59. package/dist/canvas/SceneCanvas/useSceneGestureDispatcher.js +19 -4
  60. package/dist/canvas/SceneCanvas/useSceneGestureDispatcher.js.map +1 -1
  61. package/dist/canvas/SceneCanvas/useScenePicking.js +31 -6
  62. package/dist/canvas/SceneCanvas/useScenePicking.js.map +1 -1
  63. package/dist/canvas/SceneCanvas/useSceneSelectTool.js +4 -5
  64. package/dist/canvas/SceneCanvas/useSceneSelectTool.js.map +1 -1
  65. package/dist/canvas/SceneCanvas/useSceneStandardActions.js +7 -4
  66. package/dist/canvas/SceneCanvas/useSceneStandardActions.js.map +1 -1
  67. package/dist/canvas/SceneCanvas.js +30 -12
  68. package/dist/canvas/SceneCanvas.js.map +1 -1
  69. package/dist/canvas/ViewMarker.js +35 -0
  70. package/dist/canvas/ViewMarker.js.map +1 -0
  71. package/dist/canvas/accentColor.js +26 -3
  72. package/dist/canvas/accentColor.js.map +1 -1
  73. package/dist/canvas/buildSceneTree.js +14 -20
  74. package/dist/canvas/buildSceneTree.js.map +1 -1
  75. package/dist/canvas/copiesAbove.js +1 -1
  76. package/dist/canvas/copiesAbove.js.map +1 -1
  77. package/dist/canvas/defaultDrawOne.js +11 -6
  78. package/dist/canvas/defaultDrawOne.js.map +1 -1
  79. package/dist/canvas/deps/areaSelect.js +5 -2
  80. package/dist/canvas/deps/areaSelect.js.map +1 -1
  81. package/dist/canvas/deps/booleans.js +3 -2
  82. package/dist/canvas/deps/booleans.js.map +1 -1
  83. package/dist/canvas/deps/enteredGroup.js +81 -0
  84. package/dist/canvas/deps/enteredGroup.js.map +1 -0
  85. package/dist/canvas/deps/hitTestArea.js +28 -16
  86. package/dist/canvas/deps/hitTestArea.js.map +1 -1
  87. package/dist/canvas/deps/insert.js +10 -38
  88. package/dist/canvas/deps/insert.js.map +1 -1
  89. package/dist/canvas/deps/insertSite.js +39 -0
  90. package/dist/canvas/deps/insertSite.js.map +1 -0
  91. package/dist/canvas/deps/lassoSelect.js +6 -3
  92. package/dist/canvas/deps/lassoSelect.js.map +1 -1
  93. package/dist/canvas/deps/pathEditCommits.js +3 -7
  94. package/dist/canvas/deps/pathEditCommits.js.map +1 -1
  95. package/dist/canvas/deps/pathLeaf.js +1 -1
  96. package/dist/canvas/deps/pathLeaf.js.map +1 -1
  97. package/dist/canvas/deps/slice.js +2 -1
  98. package/dist/canvas/deps/slice.js.map +1 -1
  99. package/dist/canvas/derivedPath.js +14 -6
  100. package/dist/canvas/derivedPath.js.map +1 -1
  101. package/dist/canvas/doubleClickTarget.js +9 -11
  102. package/dist/canvas/doubleClickTarget.js.map +1 -1
  103. package/dist/canvas/drawnBounds.js +26 -9
  104. package/dist/canvas/drawnBounds.js.map +1 -1
  105. package/dist/canvas/inheritedPaints.js +55 -0
  106. package/dist/canvas/inheritedPaints.js.map +1 -0
  107. package/dist/canvas/nodeShapes/derivedPainter.js +15 -16
  108. package/dist/canvas/nodeShapes/derivedPainter.js.map +1 -1
  109. package/dist/canvas/nodeShapes/nodePaint.js +7 -34
  110. package/dist/canvas/nodeShapes/nodePaint.js.map +1 -1
  111. package/dist/canvas/nodeShapes/pathPainter.js +7 -7
  112. package/dist/canvas/nodeShapes/pathPainter.js.map +1 -1
  113. package/dist/canvas/nodeShapes/rectFallbackPainter.js +4 -5
  114. package/dist/canvas/nodeShapes/rectFallbackPainter.js.map +1 -1
  115. package/dist/canvas/nodeShapes/shapePainter.js +9 -9
  116. package/dist/canvas/nodeShapes/shapePainter.js.map +1 -1
  117. package/dist/canvas/nodeShapes/textPainter.js +13 -12
  118. package/dist/canvas/nodeShapes/textPainter.js.map +1 -1
  119. package/dist/canvas/ownClip.js +24 -0
  120. package/dist/canvas/ownClip.js.map +1 -0
  121. package/dist/canvas/paintCull.js.map +1 -1
  122. package/dist/canvas/pickIndex/nodeBroadBox.js +86 -0
  123. package/dist/canvas/pickIndex/nodeBroadBox.js.map +1 -0
  124. package/dist/canvas/pickIndex/scenePickIndex.js +208 -0
  125. package/dist/canvas/pickIndex/scenePickIndex.js.map +1 -0
  126. package/dist/canvas/pickReach.js +3 -3
  127. package/dist/canvas/pickReach.js.map +1 -1
  128. package/dist/canvas/pickWalk.js +17 -91
  129. package/dist/canvas/pickWalk.js.map +1 -1
  130. package/dist/canvas/poseCascade.js +1 -1
  131. package/dist/canvas/poseCascade.js.map +1 -1
  132. package/dist/canvas/publishPreviews.js +97 -0
  133. package/dist/canvas/publishPreviews.js.map +1 -0
  134. package/dist/canvas/renderSceneToPixels.js +1 -1
  135. package/dist/canvas/renderSceneToPixels.js.map +1 -1
  136. package/dist/canvas/sceneAdapter.js +7 -5
  137. package/dist/canvas/sceneAdapter.js.map +1 -1
  138. package/dist/canvas/sceneClipboard.js +17 -10
  139. package/dist/canvas/sceneClipboard.js.map +1 -1
  140. package/dist/canvas/scenePickReads.js +37 -0
  141. package/dist/canvas/scenePickReads.js.map +1 -0
  142. package/dist/canvas/scenePickSource.js +74 -0
  143. package/dist/canvas/scenePickSource.js.map +1 -0
  144. package/dist/canvas/sceneSlotWiring.js +9 -3
  145. package/dist/canvas/sceneSlotWiring.js.map +1 -1
  146. package/dist/canvas/sceneViewRender.js +9 -4
  147. package/dist/canvas/sceneViewRender.js.map +1 -1
  148. package/dist/canvas/shownNodes.js +15 -0
  149. package/dist/canvas/shownNodes.js.map +1 -0
  150. package/dist/canvas/surfaceContribution.js +12 -8
  151. package/dist/canvas/surfaceContribution.js.map +1 -1
  152. package/dist/canvas/useCanvasView.js +4 -2
  153. package/dist/canvas/useCanvasView.js.map +1 -1
  154. package/dist/canvas/useLayerStack.js +6 -2
  155. package/dist/canvas/useLayerStack.js.map +1 -1
  156. package/dist/canvas/useSceneAdapter.js +5 -3
  157. package/dist/canvas/useSceneAdapter.js.map +1 -1
  158. package/dist/canvas/useViewAim.js +121 -0
  159. package/dist/canvas/useViewAim.js.map +1 -0
  160. package/dist/canvas/useViewNesting.js +3 -2
  161. package/dist/canvas/useViewNesting.js.map +1 -1
  162. package/dist/canvas/useViewRegistrySurface.js +4 -2
  163. package/dist/canvas/useViewRegistrySurface.js.map +1 -1
  164. package/dist/canvas/viewMarkers.js +63 -0
  165. package/dist/canvas/viewMarkers.js.map +1 -0
  166. package/dist/canvas/viewRegistry.js +27 -1
  167. package/dist/canvas/viewRegistry.js.map +1 -1
  168. package/dist/clipboard.d.ts +2 -2
  169. package/dist/{clone-C3e0OTuF.d.ts → clone-qb2e1F5p.d.ts} +2 -2
  170. package/dist/clone.d.ts +3 -3
  171. package/dist/core/isPlainObject.js +2 -2
  172. package/dist/core/layers/layerGroup.js.map +1 -1
  173. package/dist/core/pages/pageEdits.js +2 -3
  174. package/dist/core/pages/pageEdits.js.map +1 -1
  175. package/dist/core/paint/meshKindLazy.js +59 -2
  176. package/dist/core/paint/meshKindLazy.js.map +1 -1
  177. package/dist/core/scene/NodeRouting.js.map +1 -1
  178. package/dist/core/scene/arrivalWindow.js +1 -1
  179. package/dist/core/scene/arrivalWindow.js.map +1 -1
  180. package/dist/core/scene/clipNothing.js +10 -0
  181. package/dist/core/scene/clipNothing.js.map +1 -0
  182. package/dist/core/scene/declaredFieldSetters.js +44 -0
  183. package/dist/core/scene/declaredFieldSetters.js.map +1 -0
  184. package/dist/core/scene/depMemo.js +14 -3
  185. package/dist/core/scene/depMemo.js.map +1 -1
  186. package/dist/core/scene/dependents.js +9 -1
  187. package/dist/core/scene/dependents.js.map +1 -1
  188. package/dist/core/scene/derivedParts.js +10 -11
  189. package/dist/core/scene/derivedParts.js.map +1 -1
  190. package/dist/core/scene/documentOf.js +43 -0
  191. package/dist/core/scene/documentOf.js.map +1 -0
  192. package/dist/core/scene/effectivePose.js +44 -15
  193. package/dist/core/scene/effectivePose.js.map +1 -1
  194. package/dist/core/scene/fieldEdits.js +17 -14
  195. package/dist/core/scene/fieldEdits.js.map +1 -1
  196. package/dist/core/scene/fontReads.js +37 -0
  197. package/dist/core/scene/fontReads.js.map +1 -0
  198. package/dist/core/scene/historyForm.js +4 -4
  199. package/dist/core/scene/historyForm.js.map +1 -1
  200. package/dist/core/scene/inheritedPaint.js +97 -0
  201. package/dist/core/scene/inheritedPaint.js.map +1 -0
  202. package/dist/core/scene/kitOps.js +3 -23
  203. package/dist/core/scene/kitOps.js.map +1 -1
  204. package/dist/core/scene/kitRegistry.js +5 -0
  205. package/dist/core/scene/kitRegistry.js.map +1 -1
  206. package/dist/core/scene/layoutPass.js +1 -2
  207. package/dist/core/scene/layoutPass.js.map +1 -1
  208. package/dist/core/scene/nodeAsShown.js +72 -0
  209. package/dist/core/scene/nodeAsShown.js.map +1 -0
  210. package/dist/core/scene/nodeFnFields.js +2 -10
  211. package/dist/core/scene/nodeFnFields.js.map +1 -1
  212. package/dist/core/scene/nodeMemo.js +21 -8
  213. package/dist/core/scene/nodeMemo.js.map +1 -1
  214. package/dist/core/scene/nodeStore.js +2 -4
  215. package/dist/core/scene/nodeStore.js.map +1 -1
  216. package/dist/core/scene/pathEffects/blend.js.map +1 -1
  217. package/dist/core/scene/pathEffects/derive.js +10 -4
  218. package/dist/core/scene/pathEffects/derive.js.map +1 -1
  219. package/dist/core/scene/pathEffects/effects.js +3 -1
  220. package/dist/core/scene/pathEffects/effects.js.map +1 -1
  221. package/dist/core/scene/pathEffects/foundHidden.js +35 -0
  222. package/dist/core/scene/pathEffects/foundHidden.js.map +1 -0
  223. package/dist/core/scene/pathEffects/textAlong.js +95 -0
  224. package/dist/core/scene/pathEffects/textAlong.js.map +1 -0
  225. package/dist/core/scene/plyStacks.js +89 -0
  226. package/dist/core/scene/plyStacks.js.map +1 -0
  227. package/dist/core/scene/poseHold.js +42 -24
  228. package/dist/core/scene/poseHold.js.map +1 -1
  229. package/dist/core/scene/poseOverrides.js +35 -38
  230. package/dist/core/scene/poseOverrides.js.map +1 -1
  231. package/dist/core/scene/posePlies.js +59 -0
  232. package/dist/core/scene/posePlies.js.map +1 -0
  233. package/dist/core/scene/reconcileSpecs.js +6 -15
  234. package/dist/core/scene/reconcileSpecs.js.map +1 -1
  235. package/dist/core/scene/renderOrder.js +10 -4
  236. package/dist/core/scene/renderOrder.js.map +1 -1
  237. package/dist/core/scene/sameValue.js +17 -0
  238. package/dist/core/scene/sameValue.js.map +1 -0
  239. package/dist/core/scene/scene.js +3 -3
  240. package/dist/core/scene/scene.js.map +1 -1
  241. package/dist/core/scene/sceneEdits.js +8 -0
  242. package/dist/core/scene/sceneEdits.js.map +1 -1
  243. package/dist/core/scene/sceneState.js +1 -2
  244. package/dist/core/scene/sceneState.js.map +1 -1
  245. package/dist/core/scene/serialization.js +45 -9
  246. package/dist/core/scene/serialization.js.map +1 -1
  247. package/dist/core/scene/staleReplay.js +5 -2
  248. package/dist/core/scene/staleReplay.js.map +1 -1
  249. package/dist/core/scene/statedFields.js +4 -2
  250. package/dist/core/scene/statedFields.js.map +1 -1
  251. package/dist/core/scene/treeEdits.js +2 -4
  252. package/dist/core/scene/treeEdits.js.map +1 -1
  253. package/dist/core/scene/treeOrder.js +3 -1
  254. package/dist/core/scene/treeOrder.js.map +1 -1
  255. package/dist/core/scene/treeReads.js +1 -2
  256. package/dist/core/scene/treeReads.js.map +1 -1
  257. package/dist/core/scene/treeShape.js +13 -0
  258. package/dist/core/scene/treeShape.js.map +1 -0
  259. package/dist/core/scene/types.js.map +1 -1
  260. package/dist/core/scene/useScene.js +1 -1
  261. package/dist/core/scene/useScene.js.map +1 -1
  262. package/dist/{cornerHandles-BHuoMs1o.d.ts → cornerHandles-CNCb7aTY.d.ts} +3 -2
  263. package/dist/{depSchema-BpZNOv7q.d.ts → depSchema-B1RS-IJx.d.ts} +39 -18
  264. package/dist/devtools/handlers.js +1 -1
  265. package/dist/devtools/handlers.js.map +1 -1
  266. package/dist/devtools/protocol.js.map +1 -1
  267. package/dist/devtools/registry.js +4 -4
  268. package/dist/devtools/registry.js.map +1 -1
  269. package/dist/devtools-protocol.d.ts +4 -3
  270. package/dist/devtools.d.ts +3 -2
  271. package/dist/features/measure/measureContribution.js +4 -2
  272. package/dist/features/measure/measureContribution.js.map +1 -1
  273. package/dist/features/measure/measureHit.js +5 -3
  274. package/dist/features/measure/measureHit.js.map +1 -1
  275. package/dist/features/measure/measurement.js +1 -1
  276. package/dist/features/measure/measurement.js.map +1 -1
  277. package/dist/features/modes/modeLayers.js +14 -7
  278. package/dist/features/modes/modeLayers.js.map +1 -1
  279. package/dist/features/modes/modeTriggers.js +10 -6
  280. package/dist/features/modes/modeTriggers.js.map +1 -1
  281. package/dist/features/pathEffects/effectSources.js +69 -0
  282. package/dist/features/pathEffects/effectSources.js.map +1 -0
  283. package/dist/features/pathEffects/readSource.js +18 -2
  284. package/dist/features/pathEffects/readSource.js.map +1 -1
  285. package/dist/features/pathEffects/textItems.js +53 -0
  286. package/dist/features/pathEffects/textItems.js.map +1 -0
  287. package/dist/features/pathEffects/useEffectSourcesLayer.js +53 -0
  288. package/dist/features/pathEffects/useEffectSourcesLayer.js.map +1 -0
  289. package/dist/features/paths/editablePath.js +1 -1
  290. package/dist/features/paths/editablePath.js.map +1 -1
  291. package/dist/features/paths/pathEditState.js +5 -2
  292. package/dist/features/paths/pathEditState.js.map +1 -1
  293. package/dist/features/paths/pathSnap/sceneIndex.js +1 -1
  294. package/dist/features/paths/pathSnap/sceneIndex.js.map +1 -1
  295. package/dist/features/selection/enteredGroupFrame.js +65 -0
  296. package/dist/features/selection/enteredGroupFrame.js.map +1 -0
  297. package/dist/features/selection/enteredGroupScope.js +44 -0
  298. package/dist/features/selection/enteredGroupScope.js.map +1 -0
  299. package/dist/features/text/sceneTextDrafts.js +64 -0
  300. package/dist/features/text/sceneTextDrafts.js.map +1 -0
  301. package/dist/features/text/textHitAt.js +34 -3
  302. package/dist/features/text/textHitAt.js.map +1 -1
  303. package/dist/features/text/useSceneTextEdit.js +23 -19
  304. package/dist/features/text/useSceneTextEdit.js.map +1 -1
  305. package/dist/features/viewports/aim.js +105 -0
  306. package/dist/features/viewports/aim.js.map +1 -0
  307. package/dist/features/viewports/viewMarker.js +82 -0
  308. package/dist/features/viewports/viewMarker.js.map +1 -0
  309. package/dist/features/viewports/viewMarkerDrag.js +55 -0
  310. package/dist/features/viewports/viewMarkerDrag.js.map +1 -0
  311. package/dist/index.d.ts +709 -258
  312. package/dist/index.js +33 -12
  313. package/dist/interactions/actions/admitsChild.js +11 -0
  314. package/dist/interactions/actions/admitsChild.js.map +1 -0
  315. package/dist/interactions/actions/booleans/booleans.js +2 -1
  316. package/dist/interactions/actions/booleans/booleans.js.map +1 -1
  317. package/dist/interactions/actions/booleans/operands.js +6 -5
  318. package/dist/interactions/actions/booleans/operands.js.map +1 -1
  319. package/dist/interactions/actions/clipboard/action.js +27 -17
  320. package/dist/interactions/actions/clipboard/action.js.map +1 -1
  321. package/dist/interactions/actions/defaults/anchorEditing.js +44 -23
  322. package/dist/interactions/actions/defaults/anchorEditing.js.map +1 -1
  323. package/dist/interactions/actions/defaults/anchorJoin.js +12 -6
  324. package/dist/interactions/actions/defaults/anchorJoin.js.map +1 -1
  325. package/dist/interactions/actions/defaults/bendSegment.js +7 -4
  326. package/dist/interactions/actions/defaults/bendSegment.js.map +1 -1
  327. package/dist/interactions/actions/defaults/cancelGesture.js +11 -8
  328. package/dist/interactions/actions/defaults/cancelGesture.js.map +1 -1
  329. package/dist/interactions/actions/defaults/contextMenu.js +33 -11
  330. package/dist/interactions/actions/defaults/contextMenu.js.map +1 -1
  331. package/dist/interactions/actions/defaults/createPaintAction.js +5 -2
  332. package/dist/interactions/actions/defaults/createPaintAction.js.map +1 -1
  333. package/dist/interactions/actions/defaults/delete.js +28 -14
  334. package/dist/interactions/actions/defaults/delete.js.map +1 -1
  335. package/dist/interactions/actions/defaults/duplicate.js +11 -8
  336. package/dist/interactions/actions/defaults/duplicate.js.map +1 -1
  337. package/dist/interactions/actions/defaults/enterGroup.js +88 -0
  338. package/dist/interactions/actions/defaults/enterGroup.js.map +1 -0
  339. package/dist/interactions/actions/defaults/enterPathEdit.js +20 -10
  340. package/dist/interactions/actions/defaults/enterPathEdit.js.map +1 -1
  341. package/dist/interactions/actions/defaults/enterTextEdit.js +1 -1
  342. package/dist/interactions/actions/defaults/enterTextEdit.js.map +1 -1
  343. package/dist/interactions/actions/defaults/escape.js +11 -8
  344. package/dist/interactions/actions/defaults/escape.js.map +1 -1
  345. package/dist/interactions/actions/defaults/exitPathEdit.js +7 -4
  346. package/dist/interactions/actions/defaults/exitPathEdit.js.map +1 -1
  347. package/dist/interactions/actions/defaults/group.js +26 -20
  348. package/dist/interactions/actions/defaults/group.js.map +1 -1
  349. package/dist/interactions/actions/defaults/ingest.js +10 -4
  350. package/dist/interactions/actions/defaults/ingest.js.map +1 -1
  351. package/dist/interactions/actions/defaults/insertPathAnchor.js +7 -4
  352. package/dist/interactions/actions/defaults/insertPathAnchor.js.map +1 -1
  353. package/dist/interactions/actions/defaults/nudge.js +8 -2
  354. package/dist/interactions/actions/defaults/nudge.js.map +1 -1
  355. package/dist/interactions/actions/defaults/pathEditTools.js +11 -8
  356. package/dist/interactions/actions/defaults/pathEditTools.js.map +1 -1
  357. package/dist/interactions/actions/defaults/pinchZoom.js +7 -4
  358. package/dist/interactions/actions/defaults/pinchZoom.js.map +1 -1
  359. package/dist/interactions/actions/defaults/reorder.js +24 -6
  360. package/dist/interactions/actions/defaults/reorder.js.map +1 -1
  361. package/dist/interactions/actions/defaults/selectAll.js +15 -8
  362. package/dist/interactions/actions/defaults/selectAll.js.map +1 -1
  363. package/dist/interactions/actions/defaults/selectDeep.js +35 -0
  364. package/dist/interactions/actions/defaults/selectDeep.js.map +1 -0
  365. package/dist/interactions/actions/defaults/setFill.js +7 -3
  366. package/dist/interactions/actions/defaults/setFill.js.map +1 -1
  367. package/dist/interactions/actions/defaults/setFillOpacity.js +1 -1
  368. package/dist/interactions/actions/defaults/setStroke.js +12 -11
  369. package/dist/interactions/actions/defaults/setStroke.js.map +1 -1
  370. package/dist/interactions/actions/defaults/setStrokeOpacity.js +1 -1
  371. package/dist/interactions/actions/defaults/toolActivate.js +7 -1
  372. package/dist/interactions/actions/defaults/toolActivate.js.map +1 -1
  373. package/dist/interactions/actions/defaults/toolResetToDefault.js +7 -4
  374. package/dist/interactions/actions/defaults/toolResetToDefault.js.map +1 -1
  375. package/dist/interactions/actions/defaults/undoRedo.js +19 -13
  376. package/dist/interactions/actions/defaults/undoRedo.js.map +1 -1
  377. package/dist/interactions/actions/defaults/viewportWheelPan.js +16 -7
  378. package/dist/interactions/actions/defaults/viewportWheelPan.js.map +1 -1
  379. package/dist/interactions/actions/defaults/viewportZoom.js +24 -6
  380. package/dist/interactions/actions/defaults/viewportZoom.js.map +1 -1
  381. package/dist/interactions/actions/drawnBoxes.js +2 -1
  382. package/dist/interactions/actions/drawnBoxes.js.map +1 -1
  383. package/dist/interactions/actions/edit-anchors/action.js +7 -4
  384. package/dist/interactions/actions/edit-anchors/action.js.map +1 -1
  385. package/dist/interactions/actions/flip/action.js +8 -2
  386. package/dist/interactions/actions/flip/action.js.map +1 -1
  387. package/dist/interactions/actions/insert/action.js +1 -0
  388. package/dist/interactions/actions/insert/action.js.map +1 -1
  389. package/dist/interactions/actions/insert/parent.js +7 -5
  390. package/dist/interactions/actions/insert/parent.js.map +1 -1
  391. package/dist/interactions/actions/insert/placeAction.js +3 -2
  392. package/dist/interactions/actions/insert/placeAction.js.map +1 -1
  393. package/dist/interactions/actions/insert/repositionAction.js +2 -1
  394. package/dist/interactions/actions/insert/repositionAction.js.map +1 -1
  395. package/dist/interactions/actions/insert/session.js +2 -2
  396. package/dist/interactions/actions/insert/session.js.map +1 -1
  397. package/dist/interactions/actions/move/action.js +7 -4
  398. package/dist/interactions/actions/move/action.js.map +1 -1
  399. package/dist/interactions/actions/move/commit/reparent.js +1 -1
  400. package/dist/interactions/actions/move/commit/reparent.js.map +1 -1
  401. package/dist/interactions/actions/move/gestureAdapter.js +6 -7
  402. package/dist/interactions/actions/move/gestureAdapter.js.map +1 -1
  403. package/dist/interactions/actions/move/layout/exits.js +1 -1
  404. package/dist/interactions/actions/move/layout/exits.js.map +1 -1
  405. package/dist/interactions/actions/move/session.js +4 -4
  406. package/dist/interactions/actions/move/session.js.map +1 -1
  407. package/dist/interactions/actions/outlines/createOutlinesAction.js +8 -5
  408. package/dist/interactions/actions/outlines/createOutlinesAction.js.map +1 -1
  409. package/dist/interactions/actions/outlines/outlineStroke.js +5 -3
  410. package/dist/interactions/actions/outlines/outlineStroke.js.map +1 -1
  411. package/dist/interactions/actions/pathCommands/cutJoin.js +8 -5
  412. package/dist/interactions/actions/pathCommands/cutJoin.js.map +1 -1
  413. package/dist/interactions/actions/pathCommands/pathNodes.js +14 -5
  414. package/dist/interactions/actions/pathCommands/pathNodes.js.map +1 -1
  415. package/dist/interactions/actions/resize/cornerHandles.js +2 -1
  416. package/dist/interactions/actions/resize/cornerHandles.js.map +1 -1
  417. package/dist/interactions/actions/selectionSubtree.js +12 -7
  418. package/dist/interactions/actions/selectionSubtree.js.map +1 -1
  419. package/dist/interactions/actions/transform-again/action.js +8 -5
  420. package/dist/interactions/actions/transform-again/action.js.map +1 -1
  421. package/dist/interactions/gestures/thresholdDrag/thresholdDrag.js +14 -2
  422. package/dist/interactions/gestures/thresholdDrag/thresholdDrag.js.map +1 -1
  423. package/dist/json.d.ts +4 -0
  424. package/dist/json.js +3 -0
  425. package/dist/kitSettingsSchema.js +8 -1
  426. package/dist/kitSettingsSchema.js.map +1 -1
  427. package/dist/{math-CrJULaZI.d.ts → math-C_elShP_.d.ts} +7 -1
  428. package/dist/math.d.ts +1 -1
  429. package/dist/math.js +2 -1
  430. package/dist/move.d.ts +4 -4
  431. package/dist/{options-DBxEryYq.d.ts → options-ClEm3SEl.d.ts} +1 -1
  432. package/dist/resize.d.ts +4 -4
  433. package/dist/test-seams.d.ts +6 -6
  434. package/dist/tools/builtin/ellipse/useEllipseTool.js +1 -0
  435. package/dist/tools/builtin/ellipse/useEllipseTool.js.map +1 -1
  436. package/dist/tools/builtin/eyedropper/useEyedropperTool.js +2 -1
  437. package/dist/tools/builtin/eyedropper/useEyedropperTool.js.map +1 -1
  438. package/dist/tools/builtin/hand/dragPanContribution.js +4 -1
  439. package/dist/tools/builtin/hand/dragPanContribution.js.map +1 -1
  440. package/dist/tools/builtin/hand/useHandTool.js +7 -4
  441. package/dist/tools/builtin/hand/useHandTool.js.map +1 -1
  442. package/dist/tools/builtin/image/useImageTool.js +1 -0
  443. package/dist/tools/builtin/image/useImageTool.js.map +1 -1
  444. package/dist/tools/builtin/lasso/useLassoTool.js +1 -0
  445. package/dist/tools/builtin/lasso/useLassoTool.js.map +1 -1
  446. package/dist/tools/builtin/line/useLineTool.js +1 -0
  447. package/dist/tools/builtin/line/useLineTool.js.map +1 -1
  448. package/dist/tools/builtin/measure/useMeasureTool.js +12 -5
  449. package/dist/tools/builtin/measure/useMeasureTool.js.map +1 -1
  450. package/dist/tools/builtin/pen/usePenTool.js +14 -6
  451. package/dist/tools/builtin/pen/usePenTool.js.map +1 -1
  452. package/dist/tools/builtin/pencil/usePencilTool.js +1 -0
  453. package/dist/tools/builtin/pencil/usePencilTool.js.map +1 -1
  454. package/dist/tools/builtin/polygon/usePolygonTool.js +13 -4
  455. package/dist/tools/builtin/polygon/usePolygonTool.js.map +1 -1
  456. package/dist/tools/builtin/pressTarget.js +22 -0
  457. package/dist/tools/builtin/pressTarget.js.map +1 -0
  458. package/dist/tools/builtin/rect/useRectTool.js +1 -0
  459. package/dist/tools/builtin/rect/useRectTool.js.map +1 -1
  460. package/dist/tools/builtin/select/adapterPointQuery.js +6 -5
  461. package/dist/tools/builtin/select/adapterPointQuery.js.map +1 -1
  462. package/dist/tools/builtin/select/collapseDeferred.js +40 -0
  463. package/dist/tools/builtin/select/collapseDeferred.js.map +1 -0
  464. package/dist/tools/builtin/select/groupEntry.js +82 -0
  465. package/dist/tools/builtin/select/groupEntry.js.map +1 -0
  466. package/dist/tools/builtin/select/pressDecision.js +32 -0
  467. package/dist/tools/builtin/select/pressDecision.js.map +1 -0
  468. package/dist/tools/builtin/select/selectionContributions.js +22 -13
  469. package/dist/tools/builtin/select/selectionContributions.js.map +1 -1
  470. package/dist/tools/builtin/select/useSelectTool.js +74 -55
  471. package/dist/tools/builtin/select/useSelectTool.js.map +1 -1
  472. package/dist/tools/builtin/slice/useSliceTool.js +22 -8
  473. package/dist/tools/builtin/slice/useSliceTool.js.map +1 -1
  474. package/dist/tools/builtin/star/useStarTool.js +13 -4
  475. package/dist/tools/builtin/star/useStarTool.js.map +1 -1
  476. package/dist/tools/builtin/sticky/stickyNode.js +4 -1
  477. package/dist/tools/builtin/sticky/stickyNode.js.map +1 -1
  478. package/dist/tools/builtin/sticky/useStickyTool.js +10 -5
  479. package/dist/tools/builtin/sticky/useStickyTool.js.map +1 -1
  480. package/dist/tools/builtin/text/useTextTool.js +3 -1
  481. package/dist/tools/builtin/text/useTextTool.js.map +1 -1
  482. package/dist/{types-7puAuVD2.d.ts → types-49uYJN3j.d.ts} +1 -1
  483. package/dist/{types-CniVctmk.d.ts → types-BEdRIhNc.d.ts} +272 -109
  484. package/dist/{types-oFeQ5VjV.d.ts → types-p2alu-R1.d.ts} +1 -1
  485. package/dist/util/paint.js +1 -3
  486. package/dist/util/paint.js.map +1 -1
  487. package/dist/version.js +1 -1
  488. package/package.json +29 -25
@@ -0,0 +1,53 @@
1
+ import { sceneDepLookup } from "../../canvas/derivedPath.js";
2
+ import { sourcesHiddenBy } from "./effectSources.js";
3
+ import { PATH_OVERLAY_ROLES } from "../../canvas/pathOverlay.js";
4
+ import { chromeStateFrom } from "../../canvas/drawEnvelope.js";
5
+ import { readEffectSource } from "./readSource.js";
6
+ import { useMemo } from "react";
7
+ import { useLatest } from "@weasel-js/react";
8
+
9
+ //#region src/features/pathEffects/useEffectSourcesLayer.ts
10
+ /**
11
+ * The layer that shows what a selected path effect was made from: the outlines
12
+ * of the sources it hides, drawn faintly under the selection chrome.
13
+ */
14
+ var FAINT = .4;
15
+ /** For each selected path-effect node, the outlines of the sources it holds hidden (`sourcesHiddenBy`) as the effect
16
+ * reads them, in a one-screen-pixel stroke of the `'chrome'` overlay color at 40% opacity. The selection comes off the draw envelope. */
17
+ function useEffectSourcesLayer(scene) {
18
+ const sceneRef = useLatest(scene);
19
+ return useMemo(() => ({
20
+ id: "effect-sources",
21
+ label: "Hidden effect sources",
22
+ space: "world",
23
+ draw: (data) => {
24
+ const selected = chromeStateFrom(data)?.selection ?? [];
25
+ const sc = sceneRef.current;
26
+ const stroke = PATH_OVERLAY_ROLES["chrome"].stroke;
27
+ const depOf = sceneDepLookup(sc);
28
+ const lines = [];
29
+ for (const id of selected) for (const source of sourcesHiddenBy(sc, id)) {
30
+ if (!sc.isHidden(source)) continue;
31
+ const dep = depOf(source);
32
+ const path = dep === void 0 ? null : readEffectSource(dep)?.world ?? null;
33
+ if (path !== null) lines.push({
34
+ kind: "path",
35
+ path,
36
+ stroke: {
37
+ paint: stroke,
38
+ width: { px: 1 }
39
+ }
40
+ });
41
+ }
42
+ return lines.length === 0 ? [] : [{
43
+ kind: "group",
44
+ alpha: FAINT,
45
+ children: lines
46
+ }];
47
+ }
48
+ }), [sceneRef]);
49
+ }
50
+
51
+ //#endregion
52
+ export { useEffectSourcesLayer };
53
+ //# sourceMappingURL=useEffectSourcesLayer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useEffectSourcesLayer.js","names":[],"sources":["../../../src/features/pathEffects/useEffectSourcesLayer.ts"],"sourcesContent":["/**\n * The layer that shows what a selected path effect was made from: the outlines\n * of the sources it hides, drawn faintly under the selection chrome.\n */\nimport { useMemo } from 'react';\nimport { useLatest } from '@weasel-js/react';\nimport type { DrawCommand } from '@weasel-js/renderer';\nimport { PATH_OVERLAY_ROLES } from 'canvas/pathOverlay';\nimport { sceneDepLookup } from 'canvas/derivedPath';\nimport { chromeStateFrom } from 'canvas/drawEnvelope';\nimport type { RenderLayer } from 'core/layers/render';\nimport { sourcesHiddenBy } from './effectSources';\nimport type { NodeId, Scene } from 'core/scene/types';\nimport { readEffectSource } from './readSource';\n\nconst FAINT = 0.4;\n\n/** For each selected path-effect node, the outlines of the sources it holds hidden (`sourcesHiddenBy`) as the effect\n * reads them, in a one-screen-pixel stroke of the `'chrome'` overlay color at 40% opacity. The selection comes off the draw envelope. */\nexport function useEffectSourcesLayer<TData, TLayer extends string, TPose>(\n scene: Scene<TData, TLayer, TPose>,\n): RenderLayer<unknown> {\n const sceneRef = useLatest(scene);\n return useMemo<RenderLayer<unknown>>(() => ({\n id: 'effect-sources',\n label: 'Hidden effect sources',\n space: 'world',\n draw: (data) => {\n const selected = chromeStateFrom(data)?.selection ?? [];\n const sc = sceneRef.current;\n const stroke = PATH_OVERLAY_ROLES['chrome']!.stroke!;\n const depOf = sceneDepLookup(sc);\n const lines: DrawCommand[] = [];\n for (const id of selected) {\n for (const source of sourcesHiddenBy(sc, id as NodeId)) {\n if (!sc.isHidden(source)) continue;\n const dep = depOf(source);\n const path = dep === undefined ? null : readEffectSource(dep as never)?.world ?? null;\n if (path !== null) lines.push({ kind: 'path', path, stroke: { paint: stroke, width: { px: 1 } } });\n }\n }\n return lines.length === 0 ? [] : [{ kind: 'group', alpha: FAINT, children: lines }];\n },\n }), [sceneRef]);\n}\n"],"mappings":";;;;;;;;;;;;;AAeA,IAAM,QAAQ;;;AAId,SAAgB,sBACd,OACsB;CACtB,MAAM,WAAW,UAAU,KAAK;CAChC,OAAO,eAAqC;EAC1C,IAAI;EACJ,OAAO;EACP,OAAO;EACP,OAAO,SAAS;GACd,MAAM,WAAW,gBAAgB,IAAI,CAAC,EAAE,aAAa,CAAC;GACtD,MAAM,KAAK,SAAS;GACpB,MAAM,SAAS,mBAAmB,SAAS,CAAE;GAC7C,MAAM,QAAQ,eAAe,EAAE;GAC/B,MAAM,QAAuB,CAAC;GAC9B,KAAK,MAAM,MAAM,UACf,KAAK,MAAM,UAAU,gBAAgB,IAAI,EAAY,GAAG;IACtD,IAAI,CAAC,GAAG,SAAS,MAAM,GAAG;IAC1B,MAAM,MAAM,MAAM,MAAM;IACxB,MAAM,OAAO,QAAQ,SAAY,OAAO,iBAAiB,GAAY,CAAC,EAAE,SAAS;IACjF,IAAI,SAAS,MAAM,MAAM,KAAK;KAAE,MAAM;KAAQ;KAAM,QAAQ;MAAE,OAAO;MAAQ,OAAO,EAAE,IAAI,EAAE;KAAE;IAAE,CAAC;GACnG;GAEF,OAAO,MAAM,WAAW,IAAI,CAAC,IAAI,CAAC;IAAE,MAAM;IAAS,OAAO;IAAO,UAAU;GAAM,CAAC;EACpF;CACF,IAAI,CAAC,QAAQ,CAAC;AAChB"}
@@ -17,7 +17,7 @@ import { asPolygonPath, pathInWorld, rectPath, worldEditToStorage } from "@wease
17
17
  /** Whether `node` is a leaf whose only geometry is its rect pose, drawn by
18
18
  * the kit's rect fallback. */
19
19
  function drawsPoseAlone(node) {
20
- if (node.kind !== "leaf" || node.data?.path != null) return false;
20
+ if (node.kind !== "mark" || node.data?.path != null) return false;
21
21
  return findNodeShape(node) === RECT_FALLBACK_PAINTER;
22
22
  }
23
23
  /** Resolve where a node's editable polygon lives. Returns null when the
@@ -1 +1 @@
1
- {"version":3,"file":"editablePath.js","names":[],"sources":["../../../src/features/paths/editablePath.ts"],"sourcesContent":["/**\n * A node's editable polygon. It lives in one of two places: the pose itself\n * (`pose.kind === 'polygon'`), or `data.path` with a rect pose, stored pose-local.\n * A rect `data.path` is edited as its four corners and is stored as a polygon\n * from the first edit on; reading it changes nothing. A leaf drawn as its rect\n * pose alone reads as a rect `data.path` the size of its pose, and gains one\n * on the first edit.\n */\nimport { asPolygonPath, pathInWorld, rectPath, worldEditToStorage, type Path, type PolygonPath } from '@weasel-js/geom';\nimport type { GesturePreviewSource } from 'canvas/gestureBounds';\nimport { findNodeShape } from 'canvas/NodeShape';\nimport { RECT_FALLBACK_PAINTER } from 'canvas/nodeShapes/rectFallbackPainter';\nimport { pathFromPlane } from 'canvas/planeClips';\nimport { isRectPose } from 'core/geometry/autoPoseDescriptor';\nimport type { RectPose } from 'core/scene/types';\nimport type { PlaneMap } from 'core/viewport/parallax';\nimport type { PathEditExtension } from './pathEditExtension';\nimport { pathFormOf, representationOf, withoutPathForm, type AnchorMarkerShape } from './pathRepresentation';\n\n/** The fields of a node the editable path is read from. A node's `kind`\n * says whether a rect pose with no path is a leaf drawn as that rect. */\nexport interface EditableShape {\n pose: unknown;\n data: unknown;\n kind?: string;\n}\n\n/** Whether `node` is a leaf whose only geometry is its rect pose, drawn by\n * the kit's rect fallback. */\nfunction drawsPoseAlone(node: EditableShape): boolean {\n if (node.kind !== 'leaf' || (node.data as { path?: unknown } | null)?.path != null) return false;\n return findNodeShape(node as never) === RECT_FALLBACK_PAINTER;\n}\n\n/** Resolve where a node's editable polygon lives. Returns null when the\n * node has neither a polygon pose nor a polygon or rect on `data.path`.\n * A rect comes back as the polygon it draws. */\nexport function classifyPathStorage(\n node: EditableShape | undefined | null,\n): { kind: 'pose' } | { kind: 'data'; pose: RectPose; data: { path: PolygonPath } } | null {\n if (!node) return null;\n const pose = node.pose as { kind?: string } | undefined;\n if (pose?.kind === 'polygon') return { kind: 'pose' };\n if (isRectPose(node.pose) && drawsPoseAlone(node)) {\n const { width, height } = node.pose;\n return { kind: 'data', pose: node.pose, data: { ...(node.data as object), path: asPolygonPath(rectPath(0, 0, width, height)) } };\n }\n const data = node.data as { path?: Path } | null;\n const kind = (data?.path as { kind?: string } | undefined)?.kind;\n if (!data?.path || (kind !== 'polygon' && kind !== 'rect') || !isRectPose(node.pose)) return null;\n const path = asPolygonPath(data.path);\n return {\n kind: 'data',\n pose: node.pose,\n data: (path === data.path ? data : { ...data, path }) as { path: PolygonPath },\n };\n}\n\n/** Whether `node` has a path that path edit can show anchors for. */\nexport function hasEditablePath(node: EditableShape | undefined | null): boolean {\n return classifyPathStorage(node) !== null;\n}\n\n/** A node's editable polygon in world coords. Does NOT consult any preview state —\n * callers that want live state read in-flight handles themselves. */\nexport function resolveEditablePathOf(\n node: EditableShape | undefined | null,\n): PolygonPath | null {\n if (!node) return null;\n const storage = classifyPathStorage(node);\n if (!storage) return null;\n // Pose-as-polygon: the pose IS the polygon (polygon poses can't carry\n // rotation), so it's already world.\n if (storage.kind === 'pose') return (node as { pose: PolygonPath }).pose;\n // data.path: translate to the pose origin AND bake `pose.rotation`, so anchors render on\n // the rotated contour the renderer draws.\n return pathInWorld(storage.data.path, storage.pose) as PolygonPath;\n}\n\n/**\n * What path edit edits on a node, in world coords: the edit path of the form\n * the node's path was drawn from, where one of `extensions` reads it\n * (`PathRepresentation`), and otherwise its editable polygon. Null when the\n * node has no editable polygon.\n */\nexport function pathEditPathOf(\n node: EditableShape | undefined | null,\n extensions: readonly PathEditExtension[] = [],\n): PolygonPath | null {\n const path = resolveEditablePathOf(node);\n const held = path ? representationOf(extensions, node!.data) : null;\n return (held && held.representation.editPath(path!, held.form)) ?? path;\n}\n\n/**\n * The pose and data that store `worldPath` where `node` stores its editable\n * polygon — the inverse of {@link resolveEditablePathOf}, so a `data.path`\n * node keeps its rotation. On a node whose path was drawn from a form one of\n * `extensions` reads, `worldPath` is an edit of what {@link pathEditPathOf}\n * gave, and the representation turns it into the drawn path; a form none\n * reads, or that no longer describes the path, is dropped. Null when `node`\n * has no editable polygon.\n */\nexport function storeEditablePath(\n node: EditableShape | undefined | null,\n worldPath: PolygonPath,\n extensions: readonly PathEditExtension[] = [],\n): EditableShape | null {\n const storage = classifyPathStorage(node);\n if (!storage) return null;\n let path = worldPath;\n let data = node!.data;\n if (pathFormOf(data)) {\n const held = representationOf(extensions, data);\n const solved = held ? held.representation.store(resolveEditablePathOf(node)!, held.form, worldPath) : null;\n path = solved?.path ?? worldPath;\n data = solved ? { ...(data as object), representation: solved.form } : withoutPathForm(data);\n }\n if (storage.kind === 'pose') return { pose: path, data };\n const stored = worldEditToStorage(storage.pose, path);\n return { pose: stored.pose, data: { ...(data as object), path: stored.path } };\n}\n\n/** `node` as the in-flight gestures in `previews` would leave it: a handle\n * previewing `id` contributes its pose and its data. */\nfunction previewedShape(\n id: string,\n node: EditableShape,\n previews: readonly GesturePreviewSource[],\n): EditableShape {\n let pose = node.pose;\n let data = node.data;\n let touched = false;\n const kind = node.kind;\n for (const source of previews) {\n const previewIds = source.previewIds?.();\n if (!previewIds) continue;\n let owns = false;\n for (const pid of previewIds) {\n if (pid === id) { owns = true; break; }\n }\n if (!owns) continue;\n const p = source.previewPose?.(id);\n if (p != null && !touched) { pose = p; touched = true; }\n const d = source.previewData?.(id);\n if (d != null) { data = d; touched = true; }\n }\n return { pose, data, kind };\n}\n\n/**\n * What path edit shows of node `id` right now ({@link pathEditPathOf}): following any\n * in-flight gesture in `previews`, and in the world of the camera whose\n * `plane` this is — a path on a parallax plane is stored in the plane's.\n */\nexport function liveEditablePath(\n id: string,\n node: EditableShape | undefined | null,\n previews: readonly GesturePreviewSource[],\n plane: PlaneMap | null,\n extensions: readonly PathEditExtension[] = [],\n): PolygonPath | null {\n if (!node) return null;\n return pathFromPlane(pathEditPathOf(previewedShape(id, node, previews), extensions), plane);\n}\n\n/** The marker of each anchor {@link liveEditablePath} shows for node `id`, as\n * the representation reading its form draws them; null for a node it reads\n * none of. */\nexport function liveAnchorMarkers(\n id: string,\n node: EditableShape | undefined | null,\n previews: readonly GesturePreviewSource[],\n extensions: readonly PathEditExtension[],\n): readonly (AnchorMarkerShape | null)[] | null {\n if (!node) return null;\n const held = representationOf(extensions, previewedShape(id, node, previews).data);\n return held?.representation.anchorMarkers?.(held.form) ?? null;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"editablePath.js","names":[],"sources":["../../../src/features/paths/editablePath.ts"],"sourcesContent":["/**\n * A node's editable polygon. It lives in one of two places: the pose itself\n * (`pose.kind === 'polygon'`), or `data.path` with a rect pose, stored pose-local.\n * A rect `data.path` is edited as its four corners and is stored as a polygon\n * from the first edit on; reading it changes nothing. A leaf drawn as its rect\n * pose alone reads as a rect `data.path` the size of its pose, and gains one\n * on the first edit.\n */\nimport { asPolygonPath, pathInWorld, rectPath, worldEditToStorage, type Path, type PolygonPath } from '@weasel-js/geom';\nimport type { GesturePreviewSource } from 'canvas/gestureBounds';\nimport { findNodeShape } from 'canvas/NodeShape';\nimport { RECT_FALLBACK_PAINTER } from 'canvas/nodeShapes/rectFallbackPainter';\nimport { pathFromPlane } from 'canvas/planeClips';\nimport { isRectPose } from 'core/geometry/autoPoseDescriptor';\nimport type { RectPose } from 'core/scene/types';\nimport type { PlaneMap } from 'core/viewport/parallax';\nimport type { PathEditExtension } from './pathEditExtension';\nimport { pathFormOf, representationOf, withoutPathForm, type AnchorMarkerShape } from './pathRepresentation';\n\n/** The fields of a node the editable path is read from. A node's `kind`\n * says whether a rect pose with no path is a leaf drawn as that rect. */\nexport interface EditableShape {\n pose: unknown;\n data: unknown;\n kind?: string;\n}\n\n/** Whether `node` is a leaf whose only geometry is its rect pose, drawn by\n * the kit's rect fallback. */\nfunction drawsPoseAlone(node: EditableShape): boolean {\n if (node.kind !== 'mark' || (node.data as { path?: unknown } | null)?.path != null) return false;\n return findNodeShape(node as never) === RECT_FALLBACK_PAINTER;\n}\n\n/** Resolve where a node's editable polygon lives. Returns null when the\n * node has neither a polygon pose nor a polygon or rect on `data.path`.\n * A rect comes back as the polygon it draws. */\nexport function classifyPathStorage(\n node: EditableShape | undefined | null,\n): { kind: 'pose' } | { kind: 'data'; pose: RectPose; data: { path: PolygonPath } } | null {\n if (!node) return null;\n const pose = node.pose as { kind?: string } | undefined;\n if (pose?.kind === 'polygon') return { kind: 'pose' };\n if (isRectPose(node.pose) && drawsPoseAlone(node)) {\n const { width, height } = node.pose;\n return { kind: 'data', pose: node.pose, data: { ...(node.data as object), path: asPolygonPath(rectPath(0, 0, width, height)) } };\n }\n const data = node.data as { path?: Path } | null;\n const kind = (data?.path as { kind?: string } | undefined)?.kind;\n if (!data?.path || (kind !== 'polygon' && kind !== 'rect') || !isRectPose(node.pose)) return null;\n const path = asPolygonPath(data.path);\n return {\n kind: 'data',\n pose: node.pose,\n data: (path === data.path ? data : { ...data, path }) as { path: PolygonPath },\n };\n}\n\n/** Whether `node` has a path that path edit can show anchors for. */\nexport function hasEditablePath(node: EditableShape | undefined | null): boolean {\n return classifyPathStorage(node) !== null;\n}\n\n/** A node's editable polygon in world coords. Does NOT consult any preview state —\n * callers that want live state read in-flight handles themselves. */\nexport function resolveEditablePathOf(\n node: EditableShape | undefined | null,\n): PolygonPath | null {\n if (!node) return null;\n const storage = classifyPathStorage(node);\n if (!storage) return null;\n // Pose-as-polygon: the pose IS the polygon (polygon poses can't carry\n // rotation), so it's already world.\n if (storage.kind === 'pose') return (node as { pose: PolygonPath }).pose;\n // data.path: translate to the pose origin AND bake `pose.rotation`, so anchors render on\n // the rotated contour the renderer draws.\n return pathInWorld(storage.data.path, storage.pose) as PolygonPath;\n}\n\n/**\n * What path edit edits on a node, in world coords: the edit path of the form\n * the node's path was drawn from, where one of `extensions` reads it\n * (`PathRepresentation`), and otherwise its editable polygon. Null when the\n * node has no editable polygon.\n */\nexport function pathEditPathOf(\n node: EditableShape | undefined | null,\n extensions: readonly PathEditExtension[] = [],\n): PolygonPath | null {\n const path = resolveEditablePathOf(node);\n const held = path ? representationOf(extensions, node!.data) : null;\n return (held && held.representation.editPath(path!, held.form)) ?? path;\n}\n\n/**\n * The pose and data that store `worldPath` where `node` stores its editable\n * polygon — the inverse of {@link resolveEditablePathOf}, so a `data.path`\n * node keeps its rotation. On a node whose path was drawn from a form one of\n * `extensions` reads, `worldPath` is an edit of what {@link pathEditPathOf}\n * gave, and the representation turns it into the drawn path; a form none\n * reads, or that no longer describes the path, is dropped. Null when `node`\n * has no editable polygon.\n */\nexport function storeEditablePath(\n node: EditableShape | undefined | null,\n worldPath: PolygonPath,\n extensions: readonly PathEditExtension[] = [],\n): EditableShape | null {\n const storage = classifyPathStorage(node);\n if (!storage) return null;\n let path = worldPath;\n let data = node!.data;\n if (pathFormOf(data)) {\n const held = representationOf(extensions, data);\n const solved = held ? held.representation.store(resolveEditablePathOf(node)!, held.form, worldPath) : null;\n path = solved?.path ?? worldPath;\n data = solved ? { ...(data as object), representation: solved.form } : withoutPathForm(data);\n }\n if (storage.kind === 'pose') return { pose: path, data };\n const stored = worldEditToStorage(storage.pose, path);\n return { pose: stored.pose, data: { ...(data as object), path: stored.path } };\n}\n\n/** `node` as the in-flight gestures in `previews` would leave it: a handle\n * previewing `id` contributes its pose and its data. */\nfunction previewedShape(\n id: string,\n node: EditableShape,\n previews: readonly GesturePreviewSource[],\n): EditableShape {\n let pose = node.pose;\n let data = node.data;\n let touched = false;\n const kind = node.kind;\n for (const source of previews) {\n const previewIds = source.previewIds?.();\n if (!previewIds) continue;\n let owns = false;\n for (const pid of previewIds) {\n if (pid === id) { owns = true; break; }\n }\n if (!owns) continue;\n const p = source.previewPose?.(id);\n if (p != null && !touched) { pose = p; touched = true; }\n const d = source.previewData?.(id);\n if (d != null) { data = d; touched = true; }\n }\n return { pose, data, kind };\n}\n\n/**\n * What path edit shows of node `id` right now ({@link pathEditPathOf}): following any\n * in-flight gesture in `previews`, and in the world of the camera whose\n * `plane` this is — a path on a parallax plane is stored in the plane's.\n */\nexport function liveEditablePath(\n id: string,\n node: EditableShape | undefined | null,\n previews: readonly GesturePreviewSource[],\n plane: PlaneMap | null,\n extensions: readonly PathEditExtension[] = [],\n): PolygonPath | null {\n if (!node) return null;\n return pathFromPlane(pathEditPathOf(previewedShape(id, node, previews), extensions), plane);\n}\n\n/** The marker of each anchor {@link liveEditablePath} shows for node `id`, as\n * the representation reading its form draws them; null for a node it reads\n * none of. */\nexport function liveAnchorMarkers(\n id: string,\n node: EditableShape | undefined | null,\n previews: readonly GesturePreviewSource[],\n extensions: readonly PathEditExtension[],\n): readonly (AnchorMarkerShape | null)[] | null {\n if (!node) return null;\n const held = representationOf(extensions, previewedShape(id, node, previews).data);\n return held?.representation.anchorMarkers?.(held.form) ?? null;\n}\n"],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { effectSourceFor } from "../pathEffects/effectSources.js";
1
2
  import { createEmitter } from "@weasel-js/registry";
2
3
 
3
4
  //#region src/features/paths/pathEditState.ts
@@ -10,9 +11,11 @@ import { createEmitter } from "@weasel-js/registry";
10
11
  var NO_IDS = /* @__PURE__ */ new Set();
11
12
  var NO_EXTENSIONS = [];
12
13
  var NO_ANCHORS = /* @__PURE__ */ new Set();
13
- /** Whether `id` names a node `scene` holds and `selection` includes. */
14
+ /** Whether `id` names a node `scene` holds and `selection` includes, or the path a selected effect hides and is edited
15
+ * through. */
14
16
  function isSelectedNode(scene, selection, id) {
15
- return scene.get(id) != null && selection.current.includes(id);
17
+ if (scene.get(id) == null) return false;
18
+ return selection.current.includes(id) || selection.current.some((sel) => effectSourceFor(scene, sel, "path") === id);
16
19
  }
17
20
  function createPathEditState(source = {}) {
18
21
  let ownId = "";
@@ -1 +1 @@
1
- {"version":3,"file":"pathEditState.js","names":[],"sources":["../../../src/features/paths/pathEditState.ts"],"sourcesContent":["/**\n * Which path is being anchor-edited, which of its anchors are selected, and\n * the anchor marquee in flight: one object every reader shares — the\n * `editAnchors` dep, the anchor overlay, the selection overlay's suppressed\n * set, the chrome-caps rule context, and the select tool's shift-click lock.\n */\nimport { createEmitter } from '@weasel-js/registry';\nimport type { PathEditHost } from 'canvas/SceneCanvas/pathEditHost';\nimport type { HandleLocks } from 'interactions/actions/edit-anchors/params';\nimport type { PathEditExtension, PathEditMark } from './pathEditExtension';\n\n/** A world-coord rect. */\nexport interface AnchorMarquee {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\n/**\n * What decides whether a stored edit target is still one: its node exists and\n * is selected, and the mode permits anchor editing. Each is read at the\n * moment of the question.\n */\nexport interface PathEditSource {\n /** Whether `id` names a node that exists and is selected. Absent means\n * every id does. */\n isSelectedNode?(id: string): boolean;\n /** A host that owns the id; the state then keeps none of its own. */\n host?(): PathEditHost | undefined;\n /**\n * Whether anchor editing is permitted right now — whether the active mode\n * allows the `edits-anchors` capability.\n *\n * **Omit this when no mode registry is wired.** Absent means \"no modality,\n * so nothing revokes edit mode\"; a predicate built from an empty capability\n * set would revoke edit mode instantly.\n */\n isAllowed?(): boolean;\n /** The extensions it starts with; `setExtensions` replaces them. */\n extensions?: readonly PathEditExtension[];\n}\n\nexport interface PathEditState {\n /**\n * The node whose anchors are editable now, or `''`: the stored id, or the\n * host's, while `PathEditSource` still lets it stand. Masked rather than\n * cleared: a host's mode machine can leave path-edit mode without running\n * `exitPathEdit`, and nothing re-renders when it does, so there is no\n * moment to write the id back.\n */\n getEditingId(): string;\n /** `getEditingId()` as the set the selection overlay skips. */\n getEditingIds(): ReadonlySet<string>;\n setEditingId(id: string | null): void;\n /** Selected anchors (flat indices) of the path being edited. */\n getSelectedAnchors(): ReadonlySet<number>;\n setSelectedAnchors(next: ReadonlySet<number>): void;\n /** The in-flight anchor-marquee rect, in the edited path's world. */\n getMarquee(): AnchorMarquee | null;\n setMarquee(rect: AnchorMarquee | null): void;\n /** The extension mark selected in place of anchors, or null. */\n getSelectedMark(): PathEditMark | null;\n setSelectedMark(mark: PathEditMark | null): void;\n /** The canvas's path-edit extensions, in order. */\n getExtensions(): readonly PathEditExtension[];\n /** Replace the extensions; the overlay repaints when they change. */\n setExtensions(next: readonly PathEditExtension[]): void;\n /** The handle locks the `handleLock.*` toggles set; null until one is\n * pressed. Not tied to the edited path, so they outlast a change of it. */\n getHandleLocks(): HandleLocks | null;\n setHandleLocks(next: HandleLocks): void;\n /**\n * The path-edit tool in use, or null for plain anchor editing. Path edit has\n * a set of named tools, one active at a time, each changing what a plain\n * drag does: `'simplifyBrush'` brushes the path simpler. A tool belongs to\n * the open path; with none open, or a different one, there is none.\n */\n getTool(): string | null;\n /** Switch tool, or pass null to go back to plain anchor editing. Ignored\n * with no path open. */\n setTool(id: string | null): void;\n /** Notified when a write changes what the anchor overlay paints. A host\n * that owns the id announces its own changes. */\n subscribe(listener: () => void): () => void;\n}\n\nconst NO_IDS: ReadonlySet<string> = new Set();\nconst NO_EXTENSIONS: readonly PathEditExtension[] = [];\nconst NO_ANCHORS: ReadonlySet<number> = new Set();\n\n/** Whether `id` names a node `scene` holds and `selection` includes. */\nexport function isSelectedNode(\n scene: { get(id: never): unknown },\n selection: { readonly current: readonly string[] },\n id: string,\n): boolean {\n return scene.get(id as never) != null && selection.current.includes(id);\n}\n\nexport function createPathEditState(source: PathEditSource = {}): PathEditState {\n let ownId = '';\n let selectedAnchors = NO_ANCHORS;\n let marquee: AnchorMarquee | null = null;\n let handleLocks: HandleLocks | null = null;\n let ownTool: { id: string; on: string } | null = null;\n let idSet = NO_IDS;\n let selectedMark: PathEditMark | null = null;\n let extensions = source.extensions ?? NO_EXTENSIONS;\n const changed = createEmitter();\n const notify = changed.emit;\n\n const next = (id: string | null): string => id ?? '';\n const getEditingId = (): string => {\n const host = source.host?.();\n const id = host ? host.editingId() ?? '' : ownId;\n if (!id) return '';\n if (source.isAllowed && !source.isAllowed()) return '';\n if (source.isSelectedNode && !source.isSelectedNode(id)) return '';\n return id;\n };\n\n return {\n getEditingId,\n getEditingIds() {\n const id = getEditingId();\n if (!id) return NO_IDS;\n if (!idSet.has(id)) idSet = new Set([id]);\n return idSet;\n },\n setEditingId(id) {\n const host = source.host?.();\n if (next(id) !== getEditingId()) ownTool = null;\n if (host) { host.setEditingId(id); return; }\n if (next(id) === ownId) return;\n ownId = next(id);\n notify();\n },\n getSelectedAnchors: () => {\n const host = source.host?.();\n return host?.selectedAnchors && host.setSelectedAnchors ? host.selectedAnchors() : selectedAnchors;\n },\n setSelectedAnchors(next) {\n const host = source.host?.();\n if (host?.selectedAnchors && host.setSelectedAnchors) { host.setSelectedAnchors(next); return; }\n selectedAnchors = next;\n notify();\n },\n getMarquee: () => marquee,\n setMarquee(rect) {\n marquee = rect;\n notify();\n },\n getSelectedMark: () => selectedMark,\n setSelectedMark(mark) {\n if (mark?.owner === selectedMark?.owner && mark?.index === selectedMark?.index) return;\n selectedMark = mark;\n notify();\n },\n getExtensions: () => extensions,\n setExtensions(next) {\n if (next === extensions) return;\n extensions = next;\n notify();\n },\n getHandleLocks: () => handleLocks,\n setHandleLocks(next) {\n handleLocks = { angle: next.angle, length: next.length };\n notify();\n },\n getTool() {\n const on = getEditingId();\n const host = source.host?.();\n if (host?.tool) return on ? host.tool() : null;\n // Dropped on the read that finds its path gone: a host can close the\n // path without telling this state.\n if (ownTool && ownTool.on !== on) ownTool = null;\n return ownTool?.id ?? null;\n },\n setTool(id) {\n const host = source.host?.();\n if (host?.tool && host.setTool) { host.setTool(id); return; }\n const on = getEditingId();\n const tool = id && on ? { id, on } : null;\n if (tool?.id === ownTool?.id && tool?.on === ownTool?.on) return;\n ownTool = tool;\n notify();\n },\n subscribe: changed.subscribe,\n };\n}\n"],"mappings":";;;;;;;;;AAuFA,IAAM,yBAA8B,IAAI,IAAI;AAC5C,IAAM,gBAA8C,CAAC;AACrD,IAAM,6BAAkC,IAAI,IAAI;;AAGhD,SAAgB,eACd,OACA,WACA,IACS;CACT,OAAO,MAAM,IAAI,EAAW,KAAK,QAAQ,UAAU,QAAQ,SAAS,EAAE;AACxE;AAEA,SAAgB,oBAAoB,SAAyB,CAAC,GAAkB;CAC9E,IAAI,QAAQ;CACZ,IAAI,kBAAkB;CACtB,IAAI,UAAgC;CACpC,IAAI,cAAkC;CACtC,IAAI,UAA6C;CACjD,IAAI,QAAQ;CACZ,IAAI,eAAoC;CACxC,IAAI,aAAa,OAAO,cAAc;CACtC,MAAM,UAAU,cAAc;CAC9B,MAAM,SAAS,QAAQ;CAEvB,MAAM,QAAQ,OAA8B,MAAM;CAClD,MAAM,qBAA6B;EACjC,MAAM,OAAO,OAAO,OAAO;EAC3B,MAAM,KAAK,OAAO,KAAK,UAAU,KAAK,KAAK;EAC3C,IAAI,CAAC,IAAI,OAAO;EAChB,IAAI,OAAO,aAAa,CAAC,OAAO,UAAU,GAAG,OAAO;EACpD,IAAI,OAAO,kBAAkB,CAAC,OAAO,eAAe,EAAE,GAAG,OAAO;EAChE,OAAO;CACT;CAEA,OAAO;EACL;EACA,gBAAgB;GACd,MAAM,KAAK,aAAa;GACxB,IAAI,CAAC,IAAI,OAAO;GAChB,IAAI,CAAC,MAAM,IAAI,EAAE,GAAG,wBAAQ,IAAI,IAAI,CAAC,EAAE,CAAC;GACxC,OAAO;EACT;EACA,aAAa,IAAI;GACf,MAAM,OAAO,OAAO,OAAO;GAC3B,IAAI,KAAK,EAAE,MAAM,aAAa,GAAG,UAAU;GAC3C,IAAI,MAAM;IAAE,KAAK,aAAa,EAAE;IAAG;GAAQ;GAC3C,IAAI,KAAK,EAAE,MAAM,OAAO;GACxB,QAAQ,KAAK,EAAE;GACf,OAAO;EACT;EACA,0BAA0B;GACxB,MAAM,OAAO,OAAO,OAAO;GAC3B,OAAO,MAAM,mBAAmB,KAAK,qBAAqB,KAAK,gBAAgB,IAAI;EACrF;EACA,mBAAmB,MAAM;GACvB,MAAM,OAAO,OAAO,OAAO;GAC3B,IAAI,MAAM,mBAAmB,KAAK,oBAAoB;IAAE,KAAK,mBAAmB,IAAI;IAAG;GAAQ;GAC/F,kBAAkB;GAClB,OAAO;EACT;EACA,kBAAkB;EAClB,WAAW,MAAM;GACf,UAAU;GACV,OAAO;EACT;EACA,uBAAuB;EACvB,gBAAgB,MAAM;GACpB,IAAI,MAAM,UAAU,cAAc,SAAS,MAAM,UAAU,cAAc,OAAO;GAChF,eAAe;GACf,OAAO;EACT;EACA,qBAAqB;EACrB,cAAc,MAAM;GAClB,IAAI,SAAS,YAAY;GACzB,aAAa;GACb,OAAO;EACT;EACA,sBAAsB;EACtB,eAAe,MAAM;GACnB,cAAc;IAAE,OAAO,KAAK;IAAO,QAAQ,KAAK;GAAO;GACvD,OAAO;EACT;EACA,UAAU;GACR,MAAM,KAAK,aAAa;GACxB,MAAM,OAAO,OAAO,OAAO;GAC3B,IAAI,MAAM,MAAM,OAAO,KAAK,KAAK,KAAK,IAAI;GAG1C,IAAI,WAAW,QAAQ,OAAO,IAAI,UAAU;GAC5C,OAAO,SAAS,MAAM;EACxB;EACA,QAAQ,IAAI;GACV,MAAM,OAAO,OAAO,OAAO;GAC3B,IAAI,MAAM,QAAQ,KAAK,SAAS;IAAE,KAAK,QAAQ,EAAE;IAAG;GAAQ;GAC5D,MAAM,KAAK,aAAa;GACxB,MAAM,OAAO,MAAM,KAAK;IAAE;IAAI;GAAG,IAAI;GACrC,IAAI,MAAM,OAAO,SAAS,MAAM,MAAM,OAAO,SAAS,IAAI;GAC1D,UAAU;GACV,OAAO;EACT;EACA,WAAW,QAAQ;CACrB;AACF"}
1
+ {"version":3,"file":"pathEditState.js","names":[],"sources":["../../../src/features/paths/pathEditState.ts"],"sourcesContent":["/**\n * Which path is being anchor-edited, which of its anchors are selected, and\n * the anchor marquee in flight: one object every reader shares — the\n * `editAnchors` dep, the anchor overlay, the selection overlay's suppressed\n * set, the chrome-caps rule context, and the select tool's shift-click lock.\n */\nimport { createEmitter } from '@weasel-js/registry';\nimport type { PathEditHost } from 'canvas/SceneCanvas/pathEditHost';\nimport type { HandleLocks } from 'interactions/actions/edit-anchors/params';\nimport type { PathEditExtension, PathEditMark } from './pathEditExtension';\nimport type { NodeId, Scene } from 'core/scene/types';\nimport { effectSourceFor } from 'features/pathEffects/effectSources';\n\n/** A world-coord rect. */\nexport interface AnchorMarquee {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\n/**\n * What decides whether a stored edit target is still one: its node exists and\n * is selected, and the mode permits anchor editing. Each is read at the\n * moment of the question.\n */\nexport interface PathEditSource {\n /** Whether `id` names a node that exists and is selected. Absent means\n * every id does. */\n isSelectedNode?(id: string): boolean;\n /** A host that owns the id; the state then keeps none of its own. */\n host?(): PathEditHost | undefined;\n /**\n * Whether anchor editing is permitted right now — whether the active mode\n * allows the `edits-anchors` capability.\n *\n * **Omit this when no mode registry is wired.** Absent means \"no modality,\n * so nothing revokes edit mode\"; a predicate built from an empty capability\n * set would revoke edit mode instantly.\n */\n isAllowed?(): boolean;\n /** The extensions it starts with; `setExtensions` replaces them. */\n extensions?: readonly PathEditExtension[];\n}\n\nexport interface PathEditState {\n /**\n * The node whose anchors are editable now, or `''`: the stored id, or the\n * host's, while `PathEditSource` still lets it stand. Masked rather than\n * cleared: a host's mode machine can leave path-edit mode without running\n * `exitPathEdit`, and nothing re-renders when it does, so there is no\n * moment to write the id back.\n */\n getEditingId(): string;\n /** `getEditingId()` as the set the selection overlay skips. */\n getEditingIds(): ReadonlySet<string>;\n setEditingId(id: string | null): void;\n /** Selected anchors (flat indices) of the path being edited. */\n getSelectedAnchors(): ReadonlySet<number>;\n setSelectedAnchors(next: ReadonlySet<number>): void;\n /** The in-flight anchor-marquee rect, in the edited path's world. */\n getMarquee(): AnchorMarquee | null;\n setMarquee(rect: AnchorMarquee | null): void;\n /** The extension mark selected in place of anchors, or null. */\n getSelectedMark(): PathEditMark | null;\n setSelectedMark(mark: PathEditMark | null): void;\n /** The canvas's path-edit extensions, in order. */\n getExtensions(): readonly PathEditExtension[];\n /** Replace the extensions; the overlay repaints when they change. */\n setExtensions(next: readonly PathEditExtension[]): void;\n /** The handle locks the `handleLock.*` toggles set; null until one is\n * pressed. Not tied to the edited path, so they outlast a change of it. */\n getHandleLocks(): HandleLocks | null;\n setHandleLocks(next: HandleLocks): void;\n /**\n * The path-edit tool in use, or null for plain anchor editing. Path edit has\n * a set of named tools, one active at a time, each changing what a plain\n * drag does: `'simplifyBrush'` brushes the path simpler. A tool belongs to\n * the open path; with none open, or a different one, there is none.\n */\n getTool(): string | null;\n /** Switch tool, or pass null to go back to plain anchor editing. Ignored\n * with no path open. */\n setTool(id: string | null): void;\n /** Notified when a write changes what the anchor overlay paints. A host\n * that owns the id announces its own changes. */\n subscribe(listener: () => void): () => void;\n}\n\nconst NO_IDS: ReadonlySet<string> = new Set();\nconst NO_EXTENSIONS: readonly PathEditExtension[] = [];\nconst NO_ANCHORS: ReadonlySet<number> = new Set();\n\n/** Whether `id` names a node `scene` holds and `selection` includes, or the path a selected effect hides and is edited\n * through. */\nexport function isSelectedNode(\n scene: Pick<Scene<unknown, string, unknown>, 'get'>,\n selection: { readonly current: readonly string[] },\n id: string,\n): boolean {\n if (scene.get(id as NodeId) == null) return false;\n return selection.current.includes(id)\n || selection.current.some((sel) => effectSourceFor(scene, sel as NodeId, 'path') === id);\n}\n\nexport function createPathEditState(source: PathEditSource = {}): PathEditState {\n let ownId = '';\n let selectedAnchors = NO_ANCHORS;\n let marquee: AnchorMarquee | null = null;\n let handleLocks: HandleLocks | null = null;\n let ownTool: { id: string; on: string } | null = null;\n let idSet = NO_IDS;\n let selectedMark: PathEditMark | null = null;\n let extensions = source.extensions ?? NO_EXTENSIONS;\n const changed = createEmitter();\n const notify = changed.emit;\n\n const next = (id: string | null): string => id ?? '';\n const getEditingId = (): string => {\n const host = source.host?.();\n const id = host ? host.editingId() ?? '' : ownId;\n if (!id) return '';\n if (source.isAllowed && !source.isAllowed()) return '';\n if (source.isSelectedNode && !source.isSelectedNode(id)) return '';\n return id;\n };\n\n return {\n getEditingId,\n getEditingIds() {\n const id = getEditingId();\n if (!id) return NO_IDS;\n if (!idSet.has(id)) idSet = new Set([id]);\n return idSet;\n },\n setEditingId(id) {\n const host = source.host?.();\n if (next(id) !== getEditingId()) ownTool = null;\n if (host) { host.setEditingId(id); return; }\n if (next(id) === ownId) return;\n ownId = next(id);\n notify();\n },\n getSelectedAnchors: () => {\n const host = source.host?.();\n return host?.selectedAnchors && host.setSelectedAnchors ? host.selectedAnchors() : selectedAnchors;\n },\n setSelectedAnchors(next) {\n const host = source.host?.();\n if (host?.selectedAnchors && host.setSelectedAnchors) { host.setSelectedAnchors(next); return; }\n selectedAnchors = next;\n notify();\n },\n getMarquee: () => marquee,\n setMarquee(rect) {\n marquee = rect;\n notify();\n },\n getSelectedMark: () => selectedMark,\n setSelectedMark(mark) {\n if (mark?.owner === selectedMark?.owner && mark?.index === selectedMark?.index) return;\n selectedMark = mark;\n notify();\n },\n getExtensions: () => extensions,\n setExtensions(next) {\n if (next === extensions) return;\n extensions = next;\n notify();\n },\n getHandleLocks: () => handleLocks,\n setHandleLocks(next) {\n handleLocks = { angle: next.angle, length: next.length };\n notify();\n },\n getTool() {\n const on = getEditingId();\n const host = source.host?.();\n if (host?.tool) return on ? host.tool() : null;\n // Dropped on the read that finds its path gone: a host can close the\n // path without telling this state.\n if (ownTool && ownTool.on !== on) ownTool = null;\n return ownTool?.id ?? null;\n },\n setTool(id) {\n const host = source.host?.();\n if (host?.tool && host.setTool) { host.setTool(id); return; }\n const on = getEditingId();\n const tool = id && on ? { id, on } : null;\n if (tool?.id === ownTool?.id && tool?.on === ownTool?.on) return;\n ownTool = tool;\n notify();\n },\n subscribe: changed.subscribe,\n };\n}\n"],"mappings":"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"}
@@ -29,7 +29,7 @@ function createSceneSnapIndexer(scene) {
29
29
  const paths = [];
30
30
  const extra = [];
31
31
  for (const node of scene.nodes.values()) {
32
- if (node.kind !== "leaf" || notes.has(node.layer) || gates.hidden(node.id)) continue;
32
+ if (node.kind !== "mark" || notes.has(node.layer) || gates.hidden(node.id)) continue;
33
33
  const g = nodeSnapGeometry(node);
34
34
  if (!g) continue;
35
35
  for (const p of g.points) extra.push({
@@ -1 +1 @@
1
- {"version":3,"file":"sceneIndex.js","names":[],"sources":["../../../../src/features/paths/pathSnap/sceneIndex.ts"],"sourcesContent":["import {\n createPathSnapIndex, type Path, type PathSnapIndex, type PathSnapTarget,\n} from '@weasel-js/geom';\nimport type { Scene } from 'core/scene/types';\nimport { isAnnotationLayer } from 'core/scene/annotation';\nimport { nodeGates } from 'core/scene/nodeGates';\nimport type { SnapGeometry } from 'interactions/actions/snapDep';\nimport { nodeSnapGeometry } from './snapGeometry';\n\ntype SnapScene = Pick<Scene<unknown, string, unknown>, 'nodes' | 'layers' | 'getVersion'>;\n\n/** A scene's paths indexed for snapping; `ids[i]` is the node path `i` came\n * from, or null for a path of the query's own (`also`). */\nexport interface SceneSnapIndex {\n ids: (string | null)[];\n index: PathSnapIndex;\n}\n\n/** Indexes every leaf of `scene` with geometry (`nodeSnapGeometry`), but\n * hidden ones (`nodeGates`) and those on an annotation layer, once per scene\n * version, and again when a query brings geometry of its own. The scene\n * alone and the scene with the last `also` are cached apart, so a query\n * without geometry does not evict a gesture's. */\nexport function createSceneSnapIndexer(scene: SnapScene): (also?: SnapGeometry) => SceneSnapIndex {\n type Slot = { version: number; layers: unknown; also: SnapGeometry | undefined; built: SceneSnapIndex };\n const slots: { bare: Slot | null; also: Slot | null } = { bare: null, also: null };\n return (also) => {\n const version = scene.getVersion();\n const layers = scene.layers;\n const key = also ? 'also' : 'bare';\n const cached = slots[key];\n if (cached?.version === version && cached.layers === layers && cached.also === also) return cached.built;\n const notes = new Set(layers.filter(isAnnotationLayer).map((l) => l.id));\n const gates = nodeGates({ layers, get: (id) => scene.nodes.get(id) });\n const ids: (string | null)[] = [];\n const paths: Path[] = [];\n const extra: PathSnapTarget[] = [];\n for (const node of scene.nodes.values()) {\n if (node.kind !== 'leaf' || notes.has(node.layer) || gates.hidden(node.id)) continue;\n const g = nodeSnapGeometry(node);\n if (!g) continue;\n for (const p of g.points) extra.push({ ...p, paths: [paths.length] });\n ids.push(node.id);\n paths.push(g.path);\n }\n for (const path of also?.paths ?? []) { ids.push(null); paths.push(path); }\n if (also?.anchors) {\n ids.push(null);\n for (const p of also.anchors) extra.push({ kind: 'anchor', x: p.x, y: p.y, paths: [paths.length] });\n paths.push({ kind: 'polygon', commands: new Uint8Array(), coords: new Float32Array(), fillRule: 'nonzero' });\n }\n const built = { ids, index: createPathSnapIndex(paths, extra) };\n slots[key] = { version, layers, also, built };\n return built;\n };\n}\n\nconst indexers = new WeakMap<object, ReturnType<typeof createSceneSnapIndexer>>();\n\n/** {@link createSceneSnapIndexer}, one per scene, so every source over a\n * scene shares its index. */\nexport function sceneSnapIndex(scene: SnapScene, also?: SnapGeometry): SceneSnapIndex {\n let indexer = indexers.get(scene);\n if (!indexer) indexers.set(scene, (indexer = createSceneSnapIndexer(scene)));\n return indexer(also);\n}\n"],"mappings":";;;;;;;;;;;AAuBA,SAAgB,uBAAuB,OAA2D;CAEhG,MAAM,QAAkD;EAAE,MAAM;EAAM,MAAM;CAAK;CACjF,QAAQ,SAAS;EACf,MAAM,UAAU,MAAM,WAAW;EACjC,MAAM,SAAS,MAAM;EACrB,MAAM,MAAM,OAAO,SAAS;EAC5B,MAAM,SAAS,MAAM;EACrB,IAAI,QAAQ,YAAY,WAAW,OAAO,WAAW,UAAU,OAAO,SAAS,MAAM,OAAO,OAAO;EACnG,MAAM,QAAQ,IAAI,IAAI,OAAO,OAAO,iBAAiB,CAAC,CAAC,KAAK,MAAM,EAAE,EAAE,CAAC;EACvE,MAAM,QAAQ,UAAU;GAAE;GAAQ,MAAM,OAAO,MAAM,MAAM,IAAI,EAAE;EAAE,CAAC;EACpE,MAAM,MAAyB,CAAC;EAChC,MAAM,QAAgB,CAAC;EACvB,MAAM,QAA0B,CAAC;EACjC,KAAK,MAAM,QAAQ,MAAM,MAAM,OAAO,GAAG;GACvC,IAAI,KAAK,SAAS,UAAU,MAAM,IAAI,KAAK,KAAK,KAAK,MAAM,OAAO,KAAK,EAAE,GAAG;GAC5E,MAAM,IAAI,iBAAiB,IAAI;GAC/B,IAAI,CAAC,GAAG;GACR,KAAK,MAAM,KAAK,EAAE,QAAQ,MAAM,KAAK;IAAE,GAAG;IAAG,OAAO,CAAC,MAAM,MAAM;GAAE,CAAC;GACpE,IAAI,KAAK,KAAK,EAAE;GAChB,MAAM,KAAK,EAAE,IAAI;EACnB;EACA,KAAK,MAAM,QAAQ,MAAM,SAAS,CAAC,GAAG;GAAE,IAAI,KAAK,IAAI;GAAG,MAAM,KAAK,IAAI;EAAG;EAC1E,IAAI,MAAM,SAAS;GACjB,IAAI,KAAK,IAAI;GACb,KAAK,MAAM,KAAK,KAAK,SAAS,MAAM,KAAK;IAAE,MAAM;IAAU,GAAG,EAAE;IAAG,GAAG,EAAE;IAAG,OAAO,CAAC,MAAM,MAAM;GAAE,CAAC;GAClG,MAAM,KAAK;IAAE,MAAM;IAAW,0BAAU,IAAI,WAAW;IAAG,wBAAQ,IAAI,aAAa;IAAG,UAAU;GAAU,CAAC;EAC7G;EACA,MAAM,QAAQ;GAAE;GAAK,OAAO,oBAAoB,OAAO,KAAK;EAAE;EAC9D,MAAM,OAAO;GAAE;GAAS;GAAQ;GAAM;EAAM;EAC5C,OAAO;CACT;AACF;AAEA,IAAM,2BAAW,IAAI,QAA2D;;;AAIhF,SAAgB,eAAe,OAAkB,MAAqC;CACpF,IAAI,UAAU,SAAS,IAAI,KAAK;CAChC,IAAI,CAAC,SAAS,SAAS,IAAI,OAAQ,UAAU,uBAAuB,KAAK,CAAE;CAC3E,OAAO,QAAQ,IAAI;AACrB"}
1
+ {"version":3,"file":"sceneIndex.js","names":[],"sources":["../../../../src/features/paths/pathSnap/sceneIndex.ts"],"sourcesContent":["import {\n createPathSnapIndex, type Path, type PathSnapIndex, type PathSnapTarget,\n} from '@weasel-js/geom';\nimport type { Scene } from 'core/scene/types';\nimport { isAnnotationLayer } from 'core/scene/annotation';\nimport { nodeGates } from 'core/scene/nodeGates';\nimport type { SnapGeometry } from 'interactions/actions/snapDep';\nimport { nodeSnapGeometry } from './snapGeometry';\n\ntype SnapScene = Pick<Scene<unknown, string, unknown>, 'nodes' | 'layers' | 'getVersion'>;\n\n/** A scene's paths indexed for snapping; `ids[i]` is the node path `i` came\n * from, or null for a path of the query's own (`also`). */\nexport interface SceneSnapIndex {\n ids: (string | null)[];\n index: PathSnapIndex;\n}\n\n/** Indexes every leaf of `scene` with geometry (`nodeSnapGeometry`), but\n * hidden ones (`nodeGates`) and those on an annotation layer, once per scene\n * version, and again when a query brings geometry of its own. The scene\n * alone and the scene with the last `also` are cached apart, so a query\n * without geometry does not evict a gesture's. */\nexport function createSceneSnapIndexer(scene: SnapScene): (also?: SnapGeometry) => SceneSnapIndex {\n type Slot = { version: number; layers: unknown; also: SnapGeometry | undefined; built: SceneSnapIndex };\n const slots: { bare: Slot | null; also: Slot | null } = { bare: null, also: null };\n return (also) => {\n const version = scene.getVersion();\n const layers = scene.layers;\n const key = also ? 'also' : 'bare';\n const cached = slots[key];\n if (cached?.version === version && cached.layers === layers && cached.also === also) return cached.built;\n const notes = new Set(layers.filter(isAnnotationLayer).map((l) => l.id));\n const gates = nodeGates({ layers, get: (id) => scene.nodes.get(id) });\n const ids: (string | null)[] = [];\n const paths: Path[] = [];\n const extra: PathSnapTarget[] = [];\n for (const node of scene.nodes.values()) {\n if (node.kind !== 'mark' || notes.has(node.layer) || gates.hidden(node.id)) continue;\n const g = nodeSnapGeometry(node);\n if (!g) continue;\n for (const p of g.points) extra.push({ ...p, paths: [paths.length] });\n ids.push(node.id);\n paths.push(g.path);\n }\n for (const path of also?.paths ?? []) { ids.push(null); paths.push(path); }\n if (also?.anchors) {\n ids.push(null);\n for (const p of also.anchors) extra.push({ kind: 'anchor', x: p.x, y: p.y, paths: [paths.length] });\n paths.push({ kind: 'polygon', commands: new Uint8Array(), coords: new Float32Array(), fillRule: 'nonzero' });\n }\n const built = { ids, index: createPathSnapIndex(paths, extra) };\n slots[key] = { version, layers, also, built };\n return built;\n };\n}\n\nconst indexers = new WeakMap<object, ReturnType<typeof createSceneSnapIndexer>>();\n\n/** {@link createSceneSnapIndexer}, one per scene, so every source over a\n * scene shares its index. */\nexport function sceneSnapIndex(scene: SnapScene, also?: SnapGeometry): SceneSnapIndex {\n let indexer = indexers.get(scene);\n if (!indexer) indexers.set(scene, (indexer = createSceneSnapIndexer(scene)));\n return indexer(also);\n}\n"],"mappings":";;;;;;;;;;;AAuBA,SAAgB,uBAAuB,OAA2D;CAEhG,MAAM,QAAkD;EAAE,MAAM;EAAM,MAAM;CAAK;CACjF,QAAQ,SAAS;EACf,MAAM,UAAU,MAAM,WAAW;EACjC,MAAM,SAAS,MAAM;EACrB,MAAM,MAAM,OAAO,SAAS;EAC5B,MAAM,SAAS,MAAM;EACrB,IAAI,QAAQ,YAAY,WAAW,OAAO,WAAW,UAAU,OAAO,SAAS,MAAM,OAAO,OAAO;EACnG,MAAM,QAAQ,IAAI,IAAI,OAAO,OAAO,iBAAiB,CAAC,CAAC,KAAK,MAAM,EAAE,EAAE,CAAC;EACvE,MAAM,QAAQ,UAAU;GAAE;GAAQ,MAAM,OAAO,MAAM,MAAM,IAAI,EAAE;EAAE,CAAC;EACpE,MAAM,MAAyB,CAAC;EAChC,MAAM,QAAgB,CAAC;EACvB,MAAM,QAA0B,CAAC;EACjC,KAAK,MAAM,QAAQ,MAAM,MAAM,OAAO,GAAG;GACvC,IAAI,KAAK,SAAS,UAAU,MAAM,IAAI,KAAK,KAAK,KAAK,MAAM,OAAO,KAAK,EAAE,GAAG;GAC5E,MAAM,IAAI,iBAAiB,IAAI;GAC/B,IAAI,CAAC,GAAG;GACR,KAAK,MAAM,KAAK,EAAE,QAAQ,MAAM,KAAK;IAAE,GAAG;IAAG,OAAO,CAAC,MAAM,MAAM;GAAE,CAAC;GACpE,IAAI,KAAK,KAAK,EAAE;GAChB,MAAM,KAAK,EAAE,IAAI;EACnB;EACA,KAAK,MAAM,QAAQ,MAAM,SAAS,CAAC,GAAG;GAAE,IAAI,KAAK,IAAI;GAAG,MAAM,KAAK,IAAI;EAAG;EAC1E,IAAI,MAAM,SAAS;GACjB,IAAI,KAAK,IAAI;GACb,KAAK,MAAM,KAAK,KAAK,SAAS,MAAM,KAAK;IAAE,MAAM;IAAU,GAAG,EAAE;IAAG,GAAG,EAAE;IAAG,OAAO,CAAC,MAAM,MAAM;GAAE,CAAC;GAClG,MAAM,KAAK;IAAE,MAAM;IAAW,0BAAU,IAAI,WAAW;IAAG,wBAAQ,IAAI,aAAa;IAAG,UAAU;GAAU,CAAC;EAC7G;EACA,MAAM,QAAQ;GAAE;GAAK,OAAO,oBAAoB,OAAO,KAAK;EAAE;EAC9D,MAAM,OAAO;GAAE;GAAS;GAAQ;GAAM;EAAM;EAC5C,OAAO;CACT;AACF;AAEA,IAAM,2BAAW,IAAI,QAA2D;;;AAIhF,SAAgB,eAAe,OAAkB,MAAqC;CACpF,IAAI,UAAU,SAAS,IAAI,KAAK;CAChC,IAAI,CAAC,SAAS,SAAS,IAAI,OAAQ,UAAU,uBAAuB,KAAK,CAAE;CAC3E,OAAO,QAAQ,IAAI;AACrB"}
@@ -0,0 +1,65 @@
1
+ import { viewToTransform } from "../../core/viewport/view.js";
2
+ import { worldToScreen } from "../../core/viewport/viewTransform.js";
3
+ import { rectPathFor } from "./handleCommands.js";
4
+ import { useMemo } from "react";
5
+
6
+ //#region src/features/selection/enteredGroupFrame.ts
7
+ /** The theme token the frame reads its color from, set on the canvas or any element above it. */
8
+ var ENTERED_FRAME_TOKEN = "--wzl-entered-group";
9
+ /** The frame's color where nothing sets {@link ENTERED_FRAME_TOKEN}. */
10
+ var ENTERED_FRAME_COLOR = "#a48bd4";
11
+ var FRAME_WIDTH = 1;
12
+ var FRAME_DASH = [6, 4];
13
+ /**
14
+ * The frame around the group a canvas is working inside: one dashed outline on the group's
15
+ * box, in the selection chrome's own screen space, repainting when the entered group changes.
16
+ */
17
+ function createEnteredGroupFrameLayer(opts) {
18
+ return {
19
+ id: "entered-group-frame",
20
+ label: "Entered group",
21
+ space: "screen",
22
+ subscribe: (repaint) => {
23
+ const offGroup = opts.entered.subscribe(repaint);
24
+ const offColor = opts.colorChanged?.(repaint);
25
+ return () => {
26
+ offGroup();
27
+ offColor?.();
28
+ };
29
+ },
30
+ draw: (_data, view) => {
31
+ const id = opts.entered.get();
32
+ const b = id === null ? null : opts.boundsOf(id);
33
+ if (b === null) return [];
34
+ const [x, y] = worldToScreen(b.x, b.y, viewToTransform(view));
35
+ return [{
36
+ kind: "path",
37
+ path: rectPathFor(x, y, b.width * view.scale.x, b.height * view.scale.y),
38
+ stroke: {
39
+ paint: {
40
+ kind: "solid",
41
+ color: opts.color?.() ?? ENTERED_FRAME_COLOR
42
+ },
43
+ width: FRAME_WIDTH,
44
+ dash: FRAME_DASH
45
+ }
46
+ }];
47
+ }
48
+ };
49
+ }
50
+ function useEnteredGroupFrameLayer(entered, boundsOf, color) {
51
+ return useMemo(() => createEnteredGroupFrameLayer({
52
+ entered,
53
+ boundsOf,
54
+ color: color.read,
55
+ colorChanged: color.changed
56
+ }), [
57
+ entered,
58
+ boundsOf,
59
+ color
60
+ ]);
61
+ }
62
+
63
+ //#endregion
64
+ export { ENTERED_FRAME_COLOR, ENTERED_FRAME_TOKEN, createEnteredGroupFrameLayer, useEnteredGroupFrameLayer };
65
+ //# sourceMappingURL=enteredGroupFrame.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"enteredGroupFrame.js","names":[],"sources":["../../../src/features/selection/enteredGroupFrame.ts"],"sourcesContent":["import { useMemo } from 'react';\nimport type { RenderLayer } from 'core/layers/render';\nimport type { Bounds } from 'core/viewport/fitViewToBounds';\nimport { viewToTransform } from 'core/viewport/view';\nimport { worldToScreen } from 'core/viewport/viewTransform';\nimport type { EnteredGroupApi } from 'canvas/deps/enteredGroup';\nimport { rectPathFor } from './handleCommands';\n\n/** The theme token the frame reads its color from, set on the canvas or any element above it. */\nexport const ENTERED_FRAME_TOKEN = '--wzl-entered-group';\n/** The frame's color where nothing sets {@link ENTERED_FRAME_TOKEN}. */\nexport const ENTERED_FRAME_COLOR = '#a48bd4';\nconst FRAME_WIDTH = 1;\nconst FRAME_DASH = [6, 4];\n\n/**\n * The frame around the group a canvas is working inside: one dashed outline on the group's\n * box, in the selection chrome's own screen space, repainting when the entered group changes.\n */\nexport function createEnteredGroupFrameLayer(opts: {\n entered: EnteredGroupApi;\n /** The box to frame, in world units. */\n boundsOf: (id: string) => Bounds | null;\n /** The frame's color, read at each paint. Default {@link ENTERED_FRAME_COLOR}. */\n color?: () => string;\n /** Hears that `color` may answer differently: a theme or mode switch. */\n colorChanged?: (fn: () => void) => () => void;\n}): RenderLayer<unknown> {\n return {\n id: 'entered-group-frame',\n label: 'Entered group',\n space: 'screen',\n subscribe: (repaint) => {\n const offGroup = opts.entered.subscribe(repaint);\n const offColor = opts.colorChanged?.(repaint);\n return () => { offGroup(); offColor?.(); };\n },\n draw: (_data, view) => {\n const id = opts.entered.get();\n const b = id === null ? null : opts.boundsOf(id);\n if (b === null) return [];\n const [x, y] = worldToScreen(b.x, b.y, viewToTransform(view));\n return [{\n kind: 'path',\n path: rectPathFor(x, y, b.width * view.scale.x, b.height * view.scale.y),\n stroke: {\n paint: { kind: 'solid', color: opts.color?.() ?? ENTERED_FRAME_COLOR },\n width: FRAME_WIDTH,\n dash: FRAME_DASH,\n },\n }];\n },\n };\n}\n\nexport function useEnteredGroupFrameLayer(\n entered: EnteredGroupApi,\n boundsOf: (id: string) => Bounds | null,\n color: { read: () => string; changed: (fn: () => void) => () => void },\n): RenderLayer<unknown> {\n return useMemo(\n () => createEnteredGroupFrameLayer({ entered, boundsOf, color: color.read, colorChanged: color.changed }),\n [entered, boundsOf, color],\n );\n}\n"],"mappings":";;;;;;;AASA,IAAa,sBAAsB;;AAEnC,IAAa,sBAAsB;AACnC,IAAM,cAAc;AACpB,IAAM,aAAa,CAAC,GAAG,CAAC;;;;;AAMxB,SAAgB,6BAA6B,MAQpB;CACvB,OAAO;EACL,IAAI;EACJ,OAAO;EACP,OAAO;EACP,YAAY,YAAY;GACtB,MAAM,WAAW,KAAK,QAAQ,UAAU,OAAO;GAC/C,MAAM,WAAW,KAAK,eAAe,OAAO;GAC5C,aAAa;IAAE,SAAS;IAAG,WAAW;GAAG;EAC3C;EACA,OAAO,OAAO,SAAS;GACrB,MAAM,KAAK,KAAK,QAAQ,IAAI;GAC5B,MAAM,IAAI,OAAO,OAAO,OAAO,KAAK,SAAS,EAAE;GAC/C,IAAI,MAAM,MAAM,OAAO,CAAC;GACxB,MAAM,CAAC,GAAG,KAAK,cAAc,EAAE,GAAG,EAAE,GAAG,gBAAgB,IAAI,CAAC;GAC5D,OAAO,CAAC;IACN,MAAM;IACN,MAAM,YAAY,GAAG,GAAG,EAAE,QAAQ,KAAK,MAAM,GAAG,EAAE,SAAS,KAAK,MAAM,CAAC;IACvE,QAAQ;KACN,OAAO;MAAE,MAAM;MAAS,OAAO,KAAK,QAAQ,KAAK;KAAoB;KACrE,OAAO;KACP,MAAM;IACR;GACF,CAAC;EACH;CACF;AACF;AAEA,SAAgB,0BACd,SACA,UACA,OACsB;CACtB,OAAO,cACC,6BAA6B;EAAE;EAAS;EAAU,OAAO,MAAM;EAAM,cAAc,MAAM;CAAQ,CAAC,GACxG;EAAC;EAAS;EAAU;CAAK,CAC3B;AACF"}
@@ -0,0 +1,44 @@
1
+ import { reachableInGroup } from "../../canvas/deps/enteredGroup.js";
2
+ import { createEmitter } from "@weasel-js/registry";
3
+ import { OUT_OF_SCOPE_ALPHA } from "@weasel-js/modes";
4
+
5
+ //#region src/features/selection/enteredGroupScope.ts
6
+ /**
7
+ * The entered group as a scope: everything outside its subtree dims and stops taking hits, and
8
+ * so does the group itself, whose empty box is empty canvas while it is entered. With nothing
9
+ * entered every node is lit and live. Compose `alphaFor` into `<SceneCanvas alphaFor>` and the
10
+ * scope into `redrawOn` to show it.
11
+ */
12
+ function createEnteredGroupScope(opts) {
13
+ const { entered, scene } = opts;
14
+ const dim = opts.dimAlpha ?? OUT_OF_SCOPE_ALPHA;
15
+ const listeners = createEmitter();
16
+ let version = 0;
17
+ let stop = null;
18
+ return {
19
+ alphaFor: (id) => {
20
+ const group = entered.get();
21
+ return id === group || reachableInGroup(scene, group, id) ? 1 : dim;
22
+ },
23
+ isPointerInteractive: (id) => reachableInGroup(scene, entered.get(), id),
24
+ getVersion: () => version,
25
+ subscribe(fn) {
26
+ if (stop === null) stop = entered.subscribe(() => {
27
+ version++;
28
+ listeners.emit();
29
+ });
30
+ const off = listeners.subscribe(fn);
31
+ return () => {
32
+ off();
33
+ if (listeners.size === 0) {
34
+ stop?.();
35
+ stop = null;
36
+ }
37
+ };
38
+ }
39
+ };
40
+ }
41
+
42
+ //#endregion
43
+ export { createEnteredGroupScope };
44
+ //# sourceMappingURL=enteredGroupScope.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"enteredGroupScope.js","names":[],"sources":["../../../src/features/selection/enteredGroupScope.ts"],"sourcesContent":["import { OUT_OF_SCOPE_ALPHA, type NodeScope } from '@weasel-js/modes';\nimport { createEmitter } from '@weasel-js/registry';\nimport { reachableInGroup, type EnteredGroupApi } from 'canvas/deps/enteredGroup';\nimport type { Scene } from 'core/scene/types';\n\n/**\n * The entered group as a scope: everything outside its subtree dims and stops taking hits, and\n * so does the group itself, whose empty box is empty canvas while it is entered. With nothing\n * entered every node is lit and live. Compose `alphaFor` into `<SceneCanvas alphaFor>` and the\n * scope into `redrawOn` to show it.\n */\nexport function createEnteredGroupScope(opts: {\n entered: EnteredGroupApi;\n scene: Pick<Scene<unknown, string, unknown>, 'ancestorsOf'>;\n /** Alpha for a node outside the entered group. Default `OUT_OF_SCOPE_ALPHA`. */\n dimAlpha?: number;\n}): NodeScope {\n const { entered, scene } = opts;\n const dim = opts.dimAlpha ?? OUT_OF_SCOPE_ALPHA;\n const listeners = createEmitter();\n let version = 0;\n let stop: (() => void) | null = null;\n\n return {\n alphaFor: (id) => {\n const group = entered.get();\n return id === group || reachableInGroup(scene, group, id) ? 1 : dim;\n },\n isPointerInteractive: (id) => reachableInGroup(scene, entered.get(), id),\n getVersion: () => version,\n subscribe(fn) {\n if (stop === null) stop = entered.subscribe(() => { version++; listeners.emit(); });\n const off = listeners.subscribe(fn);\n return () => {\n off();\n if (listeners.size === 0) { stop?.(); stop = null; }\n };\n },\n };\n}\n"],"mappings":";;;;;;;;;;;AAWA,SAAgB,wBAAwB,MAK1B;CACZ,MAAM,EAAE,SAAS,UAAU;CAC3B,MAAM,MAAM,KAAK,YAAY;CAC7B,MAAM,YAAY,cAAc;CAChC,IAAI,UAAU;CACd,IAAI,OAA4B;CAEhC,OAAO;EACL,WAAW,OAAO;GAChB,MAAM,QAAQ,QAAQ,IAAI;GAC1B,OAAO,OAAO,SAAS,iBAAiB,OAAO,OAAO,EAAE,IAAI,IAAI;EAClE;EACA,uBAAuB,OAAO,iBAAiB,OAAO,QAAQ,IAAI,GAAG,EAAE;EACvE,kBAAkB;EAClB,UAAU,IAAI;GACZ,IAAI,SAAS,MAAM,OAAO,QAAQ,gBAAgB;IAAE;IAAW,UAAU,KAAK;GAAG,CAAC;GAClF,MAAM,MAAM,UAAU,UAAU,EAAE;GAClC,aAAa;IACX,IAAI;IACJ,IAAI,UAAU,SAAS,GAAG;KAAE,OAAO;KAAG,OAAO;IAAM;GACrD;EACF;CACF;AACF"}
@@ -0,0 +1,64 @@
1
+ import { asNodeId } from "../../core/scene/types.js";
2
+
3
+ //#region src/features/text/sceneTextDrafts.ts
4
+ /**
5
+ * A text edit shown to the scene keystroke by keystroke, for the readers that
6
+ * see a node somewhere besides the overlay: text something derives from, such
7
+ * as the hidden text a path effect lays along a path, or a second view.
8
+ */
9
+ /** `data` with an edit's text, runs and style folded in through the options' setters, for one `scene.update`. */
10
+ function editedData(data, edit, { setText, setRuns, setStyle }) {
11
+ let next = setText ? setText(data, edit.text) : {
12
+ ...data,
13
+ text: edit.text
14
+ };
15
+ if (edit.runs) next = setRuns ? setRuns(next, [...edit.runs]) : {
16
+ ...next,
17
+ runs: edit.runs
18
+ };
19
+ if (edit.style) next = setStyle ? setStyle(next, edit.style) : {
20
+ ...next,
21
+ style: edit.style
22
+ };
23
+ return next;
24
+ }
25
+ /** Where an edit's writes go. Each `draft` is shown as a data override folding the draft into whatever the node stores,
26
+ * so a recorded write made meanwhile shows through it and the document holds no draft; the `commit` records the
27
+ * result as one undo entry and then lets the override go; `end` lets it go, so a canceled edit leaves no entry and
28
+ * the redo stack as it was. */
29
+ function sceneTextDrafts(sceneRef, optsRef) {
30
+ /** The open edit's claim on the scene's overrides, with the scene it was made on. */
31
+ let held = null;
32
+ const release = (id) => {
33
+ if (held?.id !== id) return;
34
+ held.hold.drop();
35
+ held = null;
36
+ };
37
+ return {
38
+ draft: (id, draft) => {
39
+ const scene = sceneRef.current;
40
+ if (scene.get(asNodeId(id)) === void 0) return;
41
+ if (held !== null && (held.id !== id || held.scene !== scene)) release(held.id);
42
+ held ??= {
43
+ id,
44
+ scene,
45
+ hold: scene.overrides.hold()
46
+ };
47
+ const opts = optsRef.current;
48
+ held.hold.show([[asNodeId(id), { data: (stored) => editedData(stored, draft, opts) }]]);
49
+ },
50
+ commit: (id, data) => {
51
+ const scene = sceneRef.current;
52
+ scene.batch("Edit text", () => scene.update(asNodeId(id), { data }));
53
+ release(id);
54
+ },
55
+ end: release,
56
+ close: () => {
57
+ if (held !== null) release(held.id);
58
+ }
59
+ };
60
+ }
61
+
62
+ //#endregion
63
+ export { editedData, sceneTextDrafts };
64
+ //# sourceMappingURL=sceneTextDrafts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sceneTextDrafts.js","names":[],"sources":["../../../src/features/text/sceneTextDrafts.ts"],"sourcesContent":["/**\n * A text edit shown to the scene keystroke by keystroke, for the readers that\n * see a node somewhere besides the overlay: text something derives from, such\n * as the hidden text a path effect lays along a path, or a second view.\n */\nimport { asNodeId, type PoseHold, type Scene } from '../../core/scene/types';\nimport type { StyledRun, TextStyle } from '@weasel-js/text';\nimport type { TextEditDraft } from '@weasel-js/text-dom';\nimport type { DefaultTextData, UseSceneTextEditOptions } from './useSceneTextEdit';\n\n/** `data` with an edit's text, runs and style folded in through the options' setters, for one `scene.update`. */\nexport function editedData<TData extends DefaultTextData>(\n data: TData,\n edit: { text: string; runs?: readonly StyledRun[]; style?: TextStyle },\n { setText, setRuns, setStyle }: UseSceneTextEditOptions<TData>,\n): TData {\n let next = setText ? setText(data, edit.text) : { ...data, text: edit.text };\n if (edit.runs) next = setRuns ? setRuns(next, [...edit.runs]) : { ...next, runs: edit.runs };\n if (edit.style) next = setStyle ? setStyle(next, edit.style) : { ...next, style: edit.style };\n return next;\n}\n\n/** Where an edit's writes go. Each `draft` is shown as a data override folding the draft into whatever the node stores,\n * so a recorded write made meanwhile shows through it and the document holds no draft; the `commit` records the\n * result as one undo entry and then lets the override go; `end` lets it go, so a canceled edit leaves no entry and\n * the redo stack as it was. */\nexport function sceneTextDrafts<TData extends DefaultTextData, TLayer extends string, TPose>(\n sceneRef: { readonly current: Scene<TData, TLayer, TPose> },\n optsRef: { readonly current: UseSceneTextEditOptions<TData> },\n): {\n draft: (id: string, draft: TextEditDraft) => void;\n commit: (id: string, data: TData) => void;\n end: (id: string) => void;\n /** Let any open edit's draft go, as an unmount must: the overlay goes without an `end`. */\n close: () => void;\n} {\n /** The open edit's claim on the scene's overrides, with the scene it was made on. */\n let held: { id: string; scene: Scene<TData, TLayer, TPose>; hold: PoseHold<TPose, TData> } | null = null;\n const release = (id: string): void => {\n if (held?.id !== id) return;\n held.hold.drop();\n held = null;\n };\n return {\n draft: (id, draft) => {\n const scene = sceneRef.current;\n if (scene.get(asNodeId(id)) === undefined) return;\n if (held !== null && (held.id !== id || held.scene !== scene)) release(held.id);\n held ??= { id, scene, hold: scene.overrides.hold() };\n const opts = optsRef.current;\n held.hold.show([[asNodeId(id), { data: (stored) => editedData(stored, draft, opts) }]]);\n },\n commit: (id, data) => {\n const scene = sceneRef.current;\n scene.batch('Edit text', () => scene.update(asNodeId(id), { data }));\n // After the write, so no frame shows the text from before the edit in between.\n release(id);\n },\n end: release,\n close: () => {\n if (held !== null) release(held.id);\n },\n };\n}\n"],"mappings":";;;;;;;;;AAWA,SAAgB,WACd,MACA,MACA,EAAE,SAAS,SAAS,YACb;CACP,IAAI,OAAO,UAAU,QAAQ,MAAM,KAAK,IAAI,IAAI;EAAE,GAAG;EAAM,MAAM,KAAK;CAAK;CAC3E,IAAI,KAAK,MAAM,OAAO,UAAU,QAAQ,MAAM,CAAC,GAAG,KAAK,IAAI,CAAC,IAAI;EAAE,GAAG;EAAM,MAAM,KAAK;CAAK;CAC3F,IAAI,KAAK,OAAO,OAAO,WAAW,SAAS,MAAM,KAAK,KAAK,IAAI;EAAE,GAAG;EAAM,OAAO,KAAK;CAAM;CAC5F,OAAO;AACT;;;;;AAMA,SAAgB,gBACd,UACA,SAOA;;CAEA,IAAI,OAAgG;CACpG,MAAM,WAAW,OAAqB;EACpC,IAAI,MAAM,OAAO,IAAI;EACrB,KAAK,KAAK,KAAK;EACf,OAAO;CACT;CACA,OAAO;EACL,QAAQ,IAAI,UAAU;GACpB,MAAM,QAAQ,SAAS;GACvB,IAAI,MAAM,IAAI,SAAS,EAAE,CAAC,MAAM,QAAW;GAC3C,IAAI,SAAS,SAAS,KAAK,OAAO,MAAM,KAAK,UAAU,QAAQ,QAAQ,KAAK,EAAE;GAC9E,SAAS;IAAE;IAAI;IAAO,MAAM,MAAM,UAAU,KAAK;GAAE;GACnD,MAAM,OAAO,QAAQ;GACrB,KAAK,KAAK,KAAK,CAAC,CAAC,SAAS,EAAE,GAAG,EAAE,OAAO,WAAW,WAAW,QAAQ,OAAO,IAAI,EAAE,CAAC,CAAC,CAAC;EACxF;EACA,SAAS,IAAI,SAAS;GACpB,MAAM,QAAQ,SAAS;GACvB,MAAM,MAAM,mBAAmB,MAAM,OAAO,SAAS,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;GAEnE,QAAQ,EAAE;EACZ;EACA,KAAK;EACL,aAAa;GACX,IAAI,SAAS,MAAM,QAAQ,KAAK,EAAE;EACpC;CACF;AACF"}
@@ -1,6 +1,10 @@
1
+ import { derivedPartsOf } from "../../core/scene/derivedParts.js";
2
+ import { nodesAsShown, shownLookup } from "../../core/scene/nodeAsShown.js";
1
3
  import { targetTransformOf, worldToLocal } from "../../core/geometry/targetFrame.js";
2
4
  import { textBoxOf } from "../../canvas/nodeShapes/textPainter.js";
3
- import { scenePaintedPoseOf } from "../../canvas/derivedPath.js";
5
+ import { sceneDerivedPathOf, scenePaintedPoseOf } from "../../canvas/derivedPath.js";
6
+ import { effectSourceFor } from "../pathEffects/effectSources.js";
7
+ import { boundsOfPath, pathContainsPoint } from "@weasel-js/geom";
4
8
  import { caretIndexAt, pointInTextPose } from "@weasel-js/text";
5
9
 
6
10
  //#region src/features/text/textHitAt.ts
@@ -10,14 +14,31 @@ import { caretIndexAt, pointInTextPose } from "@weasel-js/text";
10
14
  * `pointInTextPose` and `caretIndexAt` work in the node's own box, so the
11
15
  * point is taken back through the frame the node is drawn in. The whole box
12
16
  * takes the point, padding included; the caret is placed in the inset one.
17
+ *
18
+ * A path effect hiding a text source answers for it within the bounds of any
19
+ * glyph it lays, counters included, with the caret at the end of the text.
20
+ * A curved glyph takes a double-click anywhere in its axis-aligned box, wider
21
+ * than the ink selection picks.
13
22
  */
14
23
  function textHitAt(scene, read, cx, cy) {
15
- const order = scene.renderOrderNodes();
24
+ const order = nodesAsShown(scene.overrides, scene.renderOrderNodes());
25
+ const shown = shownLookup(scene);
16
26
  const paintedPoseOf = scenePaintedPoseOf(scene);
27
+ const derivedPathOf = sceneDerivedPathOf(scene);
17
28
  for (let i = order.length - 1; i >= 0; i--) {
18
29
  const node = order[i];
19
30
  if (scene.isLocked(node.id) || scene.isHidden(node.id)) continue;
20
- if (!read.isText(node.data)) continue;
31
+ if (!read.isText(node.data)) {
32
+ const source = effectSourceFor(scene, node.id, "text");
33
+ const text = source === null || scene.isLocked(source) ? void 0 : shown.get(source);
34
+ if (text === void 0 || !read.isText(text.data)) continue;
35
+ const path = derivedPathOf(node);
36
+ if (path !== null && onAGlyph(path, cx, cy)) return {
37
+ id: String(source),
38
+ caret: read.text(text.data).length
39
+ };
40
+ continue;
41
+ }
21
42
  const at = paintedPoseOf(node);
22
43
  const p = worldToLocal(targetTransformOf(at), cx, cy);
23
44
  if (!p) continue;
@@ -45,6 +66,16 @@ function textHitAt(scene, read, cx, cy) {
45
66
  }
46
67
  return null;
47
68
  }
69
+ /** Whether `(x, y)` is within the bounds of one of `path`'s parts: a glyph's box, so a stem too thin to land on and the
70
+ * hole in an o still take the click. */
71
+ function onAGlyph(path, x, y) {
72
+ const parts = derivedPartsOf(path);
73
+ if (parts === void 0) return pathContainsPoint(path, x, y);
74
+ return parts.some(({ path: part }) => {
75
+ const b = boundsOfPath(part);
76
+ return x >= b.x && x <= b.x + b.width && y >= b.y && y <= b.y + b.height;
77
+ });
78
+ }
48
79
 
49
80
  //#endregion
50
81
  export { textHitAt };
@@ -1 +1 @@
1
- {"version":3,"file":"textHitAt.js","names":[],"sources":["../../../src/features/text/textHitAt.ts"],"sourcesContent":["import type { Scene } from '../../core/scene/types';\nimport type { RectPose } from 'core/geometry/unionBounds';\nimport { scenePaintedPoseOf } from '../../canvas/derivedPath';\nimport { targetTransformOf, worldToLocal } from 'core/geometry/targetFrame';\nimport { caretIndexAt, pointInTextPose, type StyledRun, type TextStyle, type TextVerticalAlign } from '@weasel-js/text';\nimport { textBoxOf } from '../../canvas/nodeShapes/textPainter';\n\n/** How a node's text fields are read from its `data`. */\nexport interface TextReads<TData> {\n isText(data: TData): boolean;\n text(data: TData): string;\n style(data: TData): TextStyle | undefined;\n runs(data: TData): readonly StyledRun[] | undefined;\n verticalAlign(data: TData): TextVerticalAlign | undefined;\n padding(data: TData): number | undefined;\n}\n\n/**\n * The top-most unlocked text node at world `(cx, cy)`, and the caret there.\n *\n * `pointInTextPose` and `caretIndexAt` work in the node's own box, so the\n * point is taken back through the frame the node is drawn in. The whole box\n * takes the point, padding included; the caret is placed in the inset one.\n */\nexport function textHitAt<TData, TPose extends RectPose>(\n scene: Scene<TData, string, TPose>,\n read: TextReads<TData>,\n cx: number,\n cy: number,\n): { id: string; caret: number } | null {\n // renderOrder() is back-to-front, so iterate in reverse and stop on the first hit.\n const order = scene.renderOrderNodes();\n const paintedPoseOf = scenePaintedPoseOf(scene);\n for (let i = order.length - 1; i >= 0; i--) {\n const node = order[i];\n if (scene.isLocked(node.id) || scene.isHidden(node.id)) continue;\n if (!read.isText(node.data)) continue;\n const at = paintedPoseOf(node);\n const p = worldToLocal(targetTransformOf(at), cx, cy);\n if (!p) continue;\n const pose = {\n ...textBoxOf({ padding: read.padding(node.data) }, at),\n text: read.text(node.data),\n runs: read.runs(node.data) as StyledRun[] | undefined,\n style: read.style(node.data),\n verticalAlign: read.verticalAlign(node.data),\n };\n const box = { x: at.x, y: at.y, width: at.width, height: at.height };\n if (!pointInTextPose(p.x, p.y, { ...pose, ...box })) continue;\n return { id: String(node.id), caret: caretIndexAt(p.x, p.y, pose) };\n }\n return null;\n}\n"],"mappings":";;;;;;;;;;;;;AAwBA,SAAgB,UACd,OACA,MACA,IACA,IACsC;CAEtC,MAAM,QAAQ,MAAM,iBAAiB;CACrC,MAAM,gBAAgB,mBAAmB,KAAK;CAC9C,KAAK,IAAI,IAAI,MAAM,SAAS,GAAG,KAAK,GAAG,KAAK;EAC1C,MAAM,OAAO,MAAM;EACnB,IAAI,MAAM,SAAS,KAAK,EAAE,KAAK,MAAM,SAAS,KAAK,EAAE,GAAG;EACxD,IAAI,CAAC,KAAK,OAAO,KAAK,IAAI,GAAG;EAC7B,MAAM,KAAK,cAAc,IAAI;EAC7B,MAAM,IAAI,aAAa,kBAAkB,EAAE,GAAG,IAAI,EAAE;EACpD,IAAI,CAAC,GAAG;EACR,MAAM,OAAO;GACX,GAAG,UAAU,EAAE,SAAS,KAAK,QAAQ,KAAK,IAAI,EAAE,GAAG,EAAE;GACrD,MAAM,KAAK,KAAK,KAAK,IAAI;GACzB,MAAM,KAAK,KAAK,KAAK,IAAI;GACzB,OAAO,KAAK,MAAM,KAAK,IAAI;GAC3B,eAAe,KAAK,cAAc,KAAK,IAAI;EAC7C;EACA,MAAM,MAAM;GAAE,GAAG,GAAG;GAAG,GAAG,GAAG;GAAG,OAAO,GAAG;GAAO,QAAQ,GAAG;EAAO;EACnE,IAAI,CAAC,gBAAgB,EAAE,GAAG,EAAE,GAAG;GAAE,GAAG;GAAM,GAAG;EAAI,CAAC,GAAG;EACrD,OAAO;GAAE,IAAI,OAAO,KAAK,EAAE;GAAG,OAAO,aAAa,EAAE,GAAG,EAAE,GAAG,IAAI;EAAE;CACpE;CACA,OAAO;AACT"}
1
+ {"version":3,"file":"textHitAt.js","names":[],"sources":["../../../src/features/text/textHitAt.ts"],"sourcesContent":["import type { Scene } from '../../core/scene/types';\nimport type { RectPose } from 'core/geometry/unionBounds';\nimport { boundsOfPath, pathContainsPoint, type Path } from '@weasel-js/geom';\nimport { sceneDerivedPathOf, scenePaintedPoseOf } from '../../canvas/derivedPath';\nimport { derivedPartsOf } from '../../core/scene/derivedParts';\nimport { nodesAsShown, shownLookup } from '../../core/scene/nodeAsShown';\nimport { effectSourceFor } from '../pathEffects/effectSources';\nimport { targetTransformOf, worldToLocal } from 'core/geometry/targetFrame';\nimport { caretIndexAt, pointInTextPose, type StyledRun, type TextStyle, type TextVerticalAlign } from '@weasel-js/text';\nimport { textBoxOf } from '../../canvas/nodeShapes/textPainter';\n\n/** How a node's text fields are read from its `data`. */\nexport interface TextReads<TData> {\n isText(data: TData): boolean;\n text(data: TData): string;\n style(data: TData): TextStyle | undefined;\n runs(data: TData): readonly StyledRun[] | undefined;\n verticalAlign(data: TData): TextVerticalAlign | undefined;\n padding(data: TData): number | undefined;\n}\n\n/**\n * The top-most unlocked text node at world `(cx, cy)`, and the caret there.\n *\n * `pointInTextPose` and `caretIndexAt` work in the node's own box, so the\n * point is taken back through the frame the node is drawn in. The whole box\n * takes the point, padding included; the caret is placed in the inset one.\n *\n * A path effect hiding a text source answers for it within the bounds of any\n * glyph it lays, counters included, with the caret at the end of the text.\n * A curved glyph takes a double-click anywhere in its axis-aligned box, wider\n * than the ink selection picks.\n */\nexport function textHitAt<TData, TPose extends RectPose>(\n scene: Scene<TData, string, TPose>,\n read: TextReads<TData>,\n cx: number,\n cy: number,\n): { id: string; caret: number } | null {\n // renderOrder() is back-to-front, so iterate in reverse and stop on the first hit.\n // Hit as painted: an open edit's draft is what the glyphs and the caret are laid out from.\n const order = nodesAsShown(scene.overrides, scene.renderOrderNodes());\n const shown = shownLookup(scene);\n const paintedPoseOf = scenePaintedPoseOf(scene);\n const derivedPathOf = sceneDerivedPathOf(scene);\n for (let i = order.length - 1; i >= 0; i--) {\n const node = order[i];\n if (scene.isLocked(node.id) || scene.isHidden(node.id)) continue;\n if (!read.isText(node.data)) {\n const source = effectSourceFor(scene, node.id, 'text');\n const text = source === null || scene.isLocked(source) ? undefined : shown.get(source);\n if (text === undefined || !read.isText(text.data)) continue;\n const path = derivedPathOf(node);\n if (path !== null && onAGlyph(path, cx, cy)) return { id: String(source), caret: read.text(text.data).length };\n continue;\n }\n const at = paintedPoseOf(node);\n const p = worldToLocal(targetTransformOf(at), cx, cy);\n if (!p) continue;\n const pose = {\n ...textBoxOf({ padding: read.padding(node.data) }, at),\n text: read.text(node.data),\n runs: read.runs(node.data) as StyledRun[] | undefined,\n style: read.style(node.data),\n verticalAlign: read.verticalAlign(node.data),\n };\n const box = { x: at.x, y: at.y, width: at.width, height: at.height };\n if (!pointInTextPose(p.x, p.y, { ...pose, ...box })) continue;\n return { id: String(node.id), caret: caretIndexAt(p.x, p.y, pose) };\n }\n return null;\n}\n\n/** Whether `(x, y)` is within the bounds of one of `path`'s parts: a glyph's box, so a stem too thin to land on and the\n * hole in an o still take the click. */\nfunction onAGlyph(path: Path, x: number, y: number): boolean {\n const parts = derivedPartsOf(path);\n if (parts === undefined) return pathContainsPoint(path, x, y);\n return parts.some(({ path: part }) => {\n const b = boundsOfPath(part);\n return x >= b.x && x <= b.x + b.width && y >= b.y && y <= b.y + b.height;\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAiCA,SAAgB,UACd,OACA,MACA,IACA,IACsC;CAGtC,MAAM,QAAQ,aAAa,MAAM,WAAW,MAAM,iBAAiB,CAAC;CACpE,MAAM,QAAQ,YAAY,KAAK;CAC/B,MAAM,gBAAgB,mBAAmB,KAAK;CAC9C,MAAM,gBAAgB,mBAAmB,KAAK;CAC9C,KAAK,IAAI,IAAI,MAAM,SAAS,GAAG,KAAK,GAAG,KAAK;EAC1C,MAAM,OAAO,MAAM;EACnB,IAAI,MAAM,SAAS,KAAK,EAAE,KAAK,MAAM,SAAS,KAAK,EAAE,GAAG;EACxD,IAAI,CAAC,KAAK,OAAO,KAAK,IAAI,GAAG;GAC3B,MAAM,SAAS,gBAAgB,OAAO,KAAK,IAAI,MAAM;GACrD,MAAM,OAAO,WAAW,QAAQ,MAAM,SAAS,MAAM,IAAI,SAAY,MAAM,IAAI,MAAM;GACrF,IAAI,SAAS,UAAa,CAAC,KAAK,OAAO,KAAK,IAAI,GAAG;GACnD,MAAM,OAAO,cAAc,IAAI;GAC/B,IAAI,SAAS,QAAQ,SAAS,MAAM,IAAI,EAAE,GAAG,OAAO;IAAE,IAAI,OAAO,MAAM;IAAG,OAAO,KAAK,KAAK,KAAK,IAAI,CAAC,CAAC;GAAO;GAC7G;EACF;EACA,MAAM,KAAK,cAAc,IAAI;EAC7B,MAAM,IAAI,aAAa,kBAAkB,EAAE,GAAG,IAAI,EAAE;EACpD,IAAI,CAAC,GAAG;EACR,MAAM,OAAO;GACX,GAAG,UAAU,EAAE,SAAS,KAAK,QAAQ,KAAK,IAAI,EAAE,GAAG,EAAE;GACrD,MAAM,KAAK,KAAK,KAAK,IAAI;GACzB,MAAM,KAAK,KAAK,KAAK,IAAI;GACzB,OAAO,KAAK,MAAM,KAAK,IAAI;GAC3B,eAAe,KAAK,cAAc,KAAK,IAAI;EAC7C;EACA,MAAM,MAAM;GAAE,GAAG,GAAG;GAAG,GAAG,GAAG;GAAG,OAAO,GAAG;GAAO,QAAQ,GAAG;EAAO;EACnE,IAAI,CAAC,gBAAgB,EAAE,GAAG,EAAE,GAAG;GAAE,GAAG;GAAM,GAAG;EAAI,CAAC,GAAG;EACrD,OAAO;GAAE,IAAI,OAAO,KAAK,EAAE;GAAG,OAAO,aAAa,EAAE,GAAG,EAAE,GAAG,IAAI;EAAE;CACpE;CACA,OAAO;AACT;;;AAIA,SAAS,SAAS,MAAY,GAAW,GAAoB;CAC3D,MAAM,QAAQ,eAAe,IAAI;CACjC,IAAI,UAAU,QAAW,OAAO,kBAAkB,MAAM,GAAG,CAAC;CAC5D,OAAO,MAAM,MAAM,EAAE,MAAM,WAAW;EACpC,MAAM,IAAI,aAAa,IAAI;EAC3B,OAAO,KAAK,EAAE,KAAK,KAAK,EAAE,IAAI,EAAE,SAAS,KAAK,EAAE,KAAK,KAAK,EAAE,IAAI,EAAE;CACpE,CAAC;AACH"}
@@ -1,11 +1,13 @@
1
+ import { inheritedPaintIn, withInheritedPaint } from "../../core/scene/inheritedPaint.js";
1
2
  import { textBoxOf } from "../../canvas/nodeShapes/textPainter.js";
2
3
  import { asNodeId } from "../../core/scene/types.js";
3
4
  import { scenePaintedPoseOf } from "../../canvas/derivedPath.js";
4
5
  import { findMountedCanvas } from "../../canvas/mountedCanvases.js";
5
6
  import { drawOneEditing } from "./editingDrawOne.js";
6
7
  import { textHitAt } from "./textHitAt.js";
8
+ import { editedData, sceneTextDrafts } from "./sceneTextDrafts.js";
7
9
  import { poseFrameOf } from "@weasel-js/geom";
8
- import { useCallback, useMemo, useRef } from "react";
10
+ import { useCallback, useEffect, useMemo, useRef } from "react";
9
11
  import { useLatest } from "@weasel-js/react";
10
12
  import { clientToCanvas } from "@weasel-js/routing";
11
13
  import { resolveScreenLength } from "@weasel-js/paint";
@@ -47,6 +49,8 @@ function useSceneTextEdit(scene, container, options = {}) {
47
49
  const optsRef = useLatest(options);
48
50
  const containerRef = useLatest(container);
49
51
  const openedOnRef = useRef(null);
52
+ const drafts = useMemo(() => sceneTextDrafts(sceneRef, optsRef), [sceneRef, optsRef]);
53
+ useEffect(() => () => drafts.close(), [drafts]);
50
54
  const surface = () => {
51
55
  const root = containerRef.current;
52
56
  const canvas = root ? findMountedCanvas(root, openedOnRef.current) : null;
@@ -70,7 +74,11 @@ function useSceneTextEdit(scene, container, options = {}) {
70
74
  escape: options.escape,
71
75
  fontHold: options.fontHold,
72
76
  onStart: (id) => optsRef.current.onStart?.(id),
73
- onEnd: (id) => optsRef.current.onEnd?.(id),
77
+ onEnd: (id) => {
78
+ drafts.end(id);
79
+ optsRef.current.onEnd?.(id);
80
+ },
81
+ onDraft: drafts.draft,
74
82
  getText: (id) => {
75
83
  const node = sceneRef.current.get(asNodeId(id));
76
84
  return node ? read.text(node.data) : "";
@@ -83,10 +91,7 @@ function useSceneTextEdit(scene, container, options = {}) {
83
91
  const node = sceneRef.current.get(asNodeId(id));
84
92
  if (!node) return void 0;
85
93
  const get = optsRef.current.getPaint;
86
- return get ? get(node.data) : {
87
- fill: node.data.fill,
88
- stroke: node.data.stroke
89
- };
94
+ return get ? get(node.data) : shownTextPaint(sceneRef.current, node);
90
95
  },
91
96
  getScreenPose: (id) => {
92
97
  const scene = sceneRef.current;
@@ -105,20 +110,11 @@ function useSceneTextEdit(scene, container, options = {}) {
105
110
  const nid = asNodeId(id);
106
111
  const node = sceneRef.current.get(nid);
107
112
  if (!node) return;
108
- const { setText, setRuns, setStyle } = optsRef.current;
109
- let data = setText ? setText(node.data, text) : {
110
- ...node.data,
111
- text
112
- };
113
- if (runs) data = setRuns ? setRuns(data, runs) : {
114
- ...data,
115
- runs
116
- };
117
- if (style) data = setStyle ? setStyle(data, style) : {
118
- ...data,
113
+ drafts.commit(id, editedData(node.data, {
114
+ text,
115
+ runs,
119
116
  style
120
- };
121
- sceneRef.current.update(nid, { data });
117
+ }, optsRef.current));
122
118
  }
123
119
  });
124
120
  const onDoubleClick = useCallback((e) => {
@@ -219,6 +215,14 @@ function frameLinearOf(pose, stretch) {
219
215
  d * stretch
220
216
  ];
221
217
  }
218
+ /** A text node's paint as the canvas paints it: an inherited one is its container's. */
219
+ function shownTextPaint(scene, node) {
220
+ const { fill, stroke } = withInheritedPaint(node.data, inheritedPaintIn(scene, node));
221
+ return {
222
+ fill,
223
+ stroke
224
+ };
225
+ }
222
226
 
223
227
  //#endregion
224
228
  export { useSceneTextEdit };