@weasel-js/core 1.11.1 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (297) hide show
  1. package/CHANGELOG.md +184 -0
  2. package/dist/WeaselProvider.js +1 -1
  3. package/dist/affordances/pathAnchors.js +10 -6
  4. package/dist/affordances/pathAnchors.js.map +1 -1
  5. package/dist/barrel/canvas.js +3 -1
  6. package/dist/barrel/paint.js +4 -4
  7. package/dist/barrel/paths.js +8 -4
  8. package/dist/barrel/renderer.js +3 -2
  9. package/dist/barrel/routing.js +4 -4
  10. package/dist/barrel/scene.js +2 -1
  11. package/dist/barrel/text.js +2 -2
  12. package/dist/barrel/viewport.js +6 -2
  13. package/dist/canvas/Canvas.js +2 -1
  14. package/dist/canvas/Canvas.js.map +1 -1
  15. package/dist/canvas/CanvasView.js +23 -34
  16. package/dist/canvas/CanvasView.js.map +1 -1
  17. package/dist/canvas/ModalityHud.js +1 -1
  18. package/dist/canvas/NodeShape.js +4 -1
  19. package/dist/canvas/NodeShape.js.map +1 -1
  20. package/dist/canvas/SceneCanvas/ActionsProviderIfRoot.js +1 -1
  21. package/dist/canvas/SceneCanvas/DepRegistryProviderIfRoot.js +1 -1
  22. package/dist/canvas/SceneCanvas/PointerProviderIfRoot.js +1 -1
  23. package/dist/canvas/SceneCanvas/SurfaceDeclarations.js +23 -0
  24. package/dist/canvas/SceneCanvas/SurfaceDeclarations.js.map +1 -0
  25. package/dist/canvas/SceneCanvas/canvasPassThrough.js +1 -0
  26. package/dist/canvas/SceneCanvas/canvasPassThrough.js.map +1 -1
  27. package/dist/canvas/SceneCanvas/props.js.map +1 -1
  28. package/dist/canvas/SceneCanvas/scenePointQuery.js +7 -3
  29. package/dist/canvas/SceneCanvas/scenePointQuery.js.map +1 -1
  30. package/dist/canvas/SceneCanvas/useActionTunings.js +6 -1
  31. package/dist/canvas/SceneCanvas/useActionTunings.js.map +1 -1
  32. package/dist/canvas/SceneCanvas/useCanvasDevtoolsRoot.js +1 -0
  33. package/dist/canvas/SceneCanvas/useCanvasDevtoolsRoot.js.map +1 -1
  34. package/dist/canvas/SceneCanvas/useCanvasTools.js +7 -2
  35. package/dist/canvas/SceneCanvas/useCanvasTools.js.map +1 -1
  36. package/dist/canvas/SceneCanvas/useSceneCanvasHandle.js +9 -3
  37. package/dist/canvas/SceneCanvas/useSceneCanvasHandle.js.map +1 -1
  38. package/dist/canvas/SceneCanvas/useSceneCanvasLayers.js +10 -2
  39. package/dist/canvas/SceneCanvas/useSceneCanvasLayers.js.map +1 -1
  40. package/dist/canvas/SceneCanvas/useSceneCanvasTools.js +1 -0
  41. package/dist/canvas/SceneCanvas/useSceneCanvasTools.js.map +1 -1
  42. package/dist/canvas/SceneCanvas/useSceneGestureDispatcher.js +19 -4
  43. package/dist/canvas/SceneCanvas/useSceneGestureDispatcher.js.map +1 -1
  44. package/dist/canvas/SceneCanvas/useSceneStandardActions.js +2 -1
  45. package/dist/canvas/SceneCanvas/useSceneStandardActions.js.map +1 -1
  46. package/dist/canvas/SceneCanvas.js +11 -10
  47. package/dist/canvas/SceneCanvas.js.map +1 -1
  48. package/dist/canvas/ViewMarker.js +35 -0
  49. package/dist/canvas/ViewMarker.js.map +1 -0
  50. package/dist/canvas/defaultDrawOne.js +2 -0
  51. package/dist/canvas/defaultDrawOne.js.map +1 -1
  52. package/dist/canvas/deps/booleans.js +2 -1
  53. package/dist/canvas/deps/booleans.js.map +1 -1
  54. package/dist/canvas/deps/hitTestArea.js +2 -2
  55. package/dist/canvas/deps/hitTestArea.js.map +1 -1
  56. package/dist/canvas/deps/insert.js +6 -2
  57. package/dist/canvas/deps/insert.js.map +1 -1
  58. package/dist/canvas/deps/pathEditCommits.js +3 -7
  59. package/dist/canvas/deps/pathEditCommits.js.map +1 -1
  60. package/dist/canvas/drawnBounds.js +11 -3
  61. package/dist/canvas/drawnBounds.js.map +1 -1
  62. package/dist/canvas/inheritedPaints.js +44 -0
  63. package/dist/canvas/inheritedPaints.js.map +1 -0
  64. package/dist/canvas/nodeShapes/derivedPainter.js +15 -16
  65. package/dist/canvas/nodeShapes/derivedPainter.js.map +1 -1
  66. package/dist/canvas/nodeShapes/nodePaint.js +13 -34
  67. package/dist/canvas/nodeShapes/nodePaint.js.map +1 -1
  68. package/dist/canvas/nodeShapes/pathPainter.js +9 -8
  69. package/dist/canvas/nodeShapes/pathPainter.js.map +1 -1
  70. package/dist/canvas/nodeShapes/rectFallbackPainter.js +4 -5
  71. package/dist/canvas/nodeShapes/rectFallbackPainter.js.map +1 -1
  72. package/dist/canvas/nodeShapes/shapePainter.js +11 -10
  73. package/dist/canvas/nodeShapes/shapePainter.js.map +1 -1
  74. package/dist/canvas/nodeShapes/textPainter.js +13 -12
  75. package/dist/canvas/nodeShapes/textPainter.js.map +1 -1
  76. package/dist/canvas/pickReach.js +3 -3
  77. package/dist/canvas/pickReach.js.map +1 -1
  78. package/dist/canvas/pickWalk.js +3 -1
  79. package/dist/canvas/pickWalk.js.map +1 -1
  80. package/dist/canvas/renderSceneToPixels.js +1 -1
  81. package/dist/canvas/renderSceneToPixels.js.map +1 -1
  82. package/dist/canvas/sceneSlotWiring.js +2 -1
  83. package/dist/canvas/sceneSlotWiring.js.map +1 -1
  84. package/dist/canvas/sceneViewRender.js +2 -1
  85. package/dist/canvas/sceneViewRender.js.map +1 -1
  86. package/dist/canvas/surfaceContribution.js +12 -8
  87. package/dist/canvas/surfaceContribution.js.map +1 -1
  88. package/dist/canvas/useCanvasView.js +4 -2
  89. package/dist/canvas/useCanvasView.js.map +1 -1
  90. package/dist/canvas/useLayerStack.js +6 -2
  91. package/dist/canvas/useLayerStack.js.map +1 -1
  92. package/dist/canvas/useViewAim.js +121 -0
  93. package/dist/canvas/useViewAim.js.map +1 -0
  94. package/dist/canvas/useViewNesting.js +3 -2
  95. package/dist/canvas/useViewNesting.js.map +1 -1
  96. package/dist/canvas/useViewRegistrySurface.js +4 -2
  97. package/dist/canvas/useViewRegistrySurface.js.map +1 -1
  98. package/dist/canvas/viewMarkers.js +63 -0
  99. package/dist/canvas/viewMarkers.js.map +1 -0
  100. package/dist/canvas/viewRegistry.js +27 -1
  101. package/dist/canvas/viewRegistry.js.map +1 -1
  102. package/dist/core/pages/pageEdits.js +2 -3
  103. package/dist/core/pages/pageEdits.js.map +1 -1
  104. package/dist/core/paint/meshKindLazy.js +59 -2
  105. package/dist/core/paint/meshKindLazy.js.map +1 -1
  106. package/dist/core/scene/clipNothing.js +13 -0
  107. package/dist/core/scene/clipNothing.js.map +1 -0
  108. package/dist/core/scene/depMemo.js +11 -3
  109. package/dist/core/scene/depMemo.js.map +1 -1
  110. package/dist/core/scene/dependents.js +7 -1
  111. package/dist/core/scene/dependents.js.map +1 -1
  112. package/dist/core/scene/derivedParts.js +10 -11
  113. package/dist/core/scene/derivedParts.js.map +1 -1
  114. package/dist/core/scene/effectivePose.js +7 -2
  115. package/dist/core/scene/effectivePose.js.map +1 -1
  116. package/dist/core/scene/fontReads.js +37 -0
  117. package/dist/core/scene/fontReads.js.map +1 -0
  118. package/dist/core/scene/inheritedPaint.js +75 -0
  119. package/dist/core/scene/inheritedPaint.js.map +1 -0
  120. package/dist/core/scene/kitRegistry.js +5 -0
  121. package/dist/core/scene/kitRegistry.js.map +1 -1
  122. package/dist/core/scene/nodeMemo.js +15 -7
  123. package/dist/core/scene/nodeMemo.js.map +1 -1
  124. package/dist/core/scene/pathEffects/blend.js.map +1 -1
  125. package/dist/core/scene/pathEffects/derive.js +7 -2
  126. package/dist/core/scene/pathEffects/derive.js.map +1 -1
  127. package/dist/core/scene/pathEffects/effects.js +3 -1
  128. package/dist/core/scene/pathEffects/effects.js.map +1 -1
  129. package/dist/core/scene/pathEffects/textAlong.js +95 -0
  130. package/dist/core/scene/pathEffects/textAlong.js.map +1 -0
  131. package/dist/{cornerHandles-BHuoMs1o.d.ts → cornerHandles-p8fDIAKt.d.ts} +2 -1
  132. package/dist/{depSchema-BpZNOv7q.d.ts → depSchema-mG19aEdD.d.ts} +61 -13
  133. package/dist/devtools/registry.js +4 -4
  134. package/dist/devtools/registry.js.map +1 -1
  135. package/dist/features/measure/measureContribution.js +4 -2
  136. package/dist/features/measure/measureContribution.js.map +1 -1
  137. package/dist/features/modes/modeTriggers.js +7 -3
  138. package/dist/features/modes/modeTriggers.js.map +1 -1
  139. package/dist/features/pathEffects/effectSources.js +60 -0
  140. package/dist/features/pathEffects/effectSources.js.map +1 -0
  141. package/dist/features/pathEffects/readSource.js +17 -1
  142. package/dist/features/pathEffects/readSource.js.map +1 -1
  143. package/dist/features/pathEffects/textItems.js +53 -0
  144. package/dist/features/pathEffects/textItems.js.map +1 -0
  145. package/dist/features/pathEffects/useEffectSourcesLayer.js +57 -0
  146. package/dist/features/pathEffects/useEffectSourcesLayer.js.map +1 -0
  147. package/dist/features/paths/pathEditState.js +5 -2
  148. package/dist/features/paths/pathEditState.js.map +1 -1
  149. package/dist/features/text/sceneTextDrafts.js +65 -0
  150. package/dist/features/text/sceneTextDrafts.js.map +1 -0
  151. package/dist/features/text/textHitAt.js +31 -2
  152. package/dist/features/text/textHitAt.js.map +1 -1
  153. package/dist/features/text/useSceneTextEdit.js +22 -18
  154. package/dist/features/text/useSceneTextEdit.js.map +1 -1
  155. package/dist/features/viewports/aim.js +105 -0
  156. package/dist/features/viewports/aim.js.map +1 -0
  157. package/dist/features/viewports/viewMarker.js +82 -0
  158. package/dist/features/viewports/viewMarker.js.map +1 -0
  159. package/dist/features/viewports/viewMarkerDrag.js +55 -0
  160. package/dist/features/viewports/viewMarkerDrag.js.map +1 -0
  161. package/dist/index.d.ts +500 -201
  162. package/dist/index.js +23 -12
  163. package/dist/interactions/actions/booleans/booleans.js +2 -1
  164. package/dist/interactions/actions/booleans/booleans.js.map +1 -1
  165. package/dist/interactions/actions/booleans/operands.js +6 -5
  166. package/dist/interactions/actions/booleans/operands.js.map +1 -1
  167. package/dist/interactions/actions/clipboard/action.js +27 -17
  168. package/dist/interactions/actions/clipboard/action.js.map +1 -1
  169. package/dist/interactions/actions/defaults/anchorEditing.js +44 -23
  170. package/dist/interactions/actions/defaults/anchorEditing.js.map +1 -1
  171. package/dist/interactions/actions/defaults/anchorJoin.js +12 -6
  172. package/dist/interactions/actions/defaults/anchorJoin.js.map +1 -1
  173. package/dist/interactions/actions/defaults/bendSegment.js +7 -4
  174. package/dist/interactions/actions/defaults/bendSegment.js.map +1 -1
  175. package/dist/interactions/actions/defaults/cancelGesture.js +11 -8
  176. package/dist/interactions/actions/defaults/cancelGesture.js.map +1 -1
  177. package/dist/interactions/actions/defaults/contextMenu.js +17 -8
  178. package/dist/interactions/actions/defaults/contextMenu.js.map +1 -1
  179. package/dist/interactions/actions/defaults/createPaintAction.js +1 -1
  180. package/dist/interactions/actions/defaults/createPaintAction.js.map +1 -1
  181. package/dist/interactions/actions/defaults/delete.js +28 -14
  182. package/dist/interactions/actions/defaults/delete.js.map +1 -1
  183. package/dist/interactions/actions/defaults/duplicate.js +11 -8
  184. package/dist/interactions/actions/defaults/duplicate.js.map +1 -1
  185. package/dist/interactions/actions/defaults/enterPathEdit.js +19 -9
  186. package/dist/interactions/actions/defaults/enterPathEdit.js.map +1 -1
  187. package/dist/interactions/actions/defaults/escape.js +11 -8
  188. package/dist/interactions/actions/defaults/escape.js.map +1 -1
  189. package/dist/interactions/actions/defaults/exitPathEdit.js +7 -4
  190. package/dist/interactions/actions/defaults/exitPathEdit.js.map +1 -1
  191. package/dist/interactions/actions/defaults/group.js +19 -13
  192. package/dist/interactions/actions/defaults/group.js.map +1 -1
  193. package/dist/interactions/actions/defaults/ingest.js +10 -4
  194. package/dist/interactions/actions/defaults/ingest.js.map +1 -1
  195. package/dist/interactions/actions/defaults/insertPathAnchor.js +7 -4
  196. package/dist/interactions/actions/defaults/insertPathAnchor.js.map +1 -1
  197. package/dist/interactions/actions/defaults/nudge.js +8 -2
  198. package/dist/interactions/actions/defaults/nudge.js.map +1 -1
  199. package/dist/interactions/actions/defaults/pathEditTools.js +11 -8
  200. package/dist/interactions/actions/defaults/pathEditTools.js.map +1 -1
  201. package/dist/interactions/actions/defaults/pinchZoom.js +7 -4
  202. package/dist/interactions/actions/defaults/pinchZoom.js.map +1 -1
  203. package/dist/interactions/actions/defaults/reorder.js +24 -6
  204. package/dist/interactions/actions/defaults/reorder.js.map +1 -1
  205. package/dist/interactions/actions/defaults/selectAll.js +8 -5
  206. package/dist/interactions/actions/defaults/selectAll.js.map +1 -1
  207. package/dist/interactions/actions/defaults/setFill.js +7 -3
  208. package/dist/interactions/actions/defaults/setFill.js.map +1 -1
  209. package/dist/interactions/actions/defaults/setFillOpacity.js +1 -1
  210. package/dist/interactions/actions/defaults/setStroke.js +12 -11
  211. package/dist/interactions/actions/defaults/setStroke.js.map +1 -1
  212. package/dist/interactions/actions/defaults/setStrokeOpacity.js +1 -1
  213. package/dist/interactions/actions/defaults/toolActivate.js +7 -1
  214. package/dist/interactions/actions/defaults/toolActivate.js.map +1 -1
  215. package/dist/interactions/actions/defaults/toolResetToDefault.js +7 -4
  216. package/dist/interactions/actions/defaults/toolResetToDefault.js.map +1 -1
  217. package/dist/interactions/actions/defaults/undoRedo.js +19 -13
  218. package/dist/interactions/actions/defaults/undoRedo.js.map +1 -1
  219. package/dist/interactions/actions/defaults/viewportWheelPan.js +16 -7
  220. package/dist/interactions/actions/defaults/viewportWheelPan.js.map +1 -1
  221. package/dist/interactions/actions/defaults/viewportZoom.js +24 -6
  222. package/dist/interactions/actions/defaults/viewportZoom.js.map +1 -1
  223. package/dist/interactions/actions/edit-anchors/action.js +7 -4
  224. package/dist/interactions/actions/edit-anchors/action.js.map +1 -1
  225. package/dist/interactions/actions/flip/action.js +8 -2
  226. package/dist/interactions/actions/flip/action.js.map +1 -1
  227. package/dist/interactions/actions/insert/repositionAction.js +2 -1
  228. package/dist/interactions/actions/insert/repositionAction.js.map +1 -1
  229. package/dist/interactions/actions/move/action.js +7 -4
  230. package/dist/interactions/actions/move/action.js.map +1 -1
  231. package/dist/interactions/actions/outlines/createOutlinesAction.js +8 -5
  232. package/dist/interactions/actions/outlines/createOutlinesAction.js.map +1 -1
  233. package/dist/interactions/actions/outlines/outlineStroke.js +5 -3
  234. package/dist/interactions/actions/outlines/outlineStroke.js.map +1 -1
  235. package/dist/interactions/actions/pathCommands/cutJoin.js +8 -5
  236. package/dist/interactions/actions/pathCommands/cutJoin.js.map +1 -1
  237. package/dist/interactions/actions/resize/cornerHandles.js +2 -1
  238. package/dist/interactions/actions/resize/cornerHandles.js.map +1 -1
  239. package/dist/interactions/actions/selectionSubtree.js +12 -7
  240. package/dist/interactions/actions/selectionSubtree.js.map +1 -1
  241. package/dist/interactions/actions/transform-again/action.js +8 -5
  242. package/dist/interactions/actions/transform-again/action.js.map +1 -1
  243. package/dist/json.d.ts +4 -0
  244. package/dist/json.js +3 -0
  245. package/dist/kitSettingsSchema.js +8 -1
  246. package/dist/kitSettingsSchema.js.map +1 -1
  247. package/dist/{math-CrJULaZI.d.ts → math-B56zxVs5.d.ts} +10 -1
  248. package/dist/math.d.ts +1 -1
  249. package/dist/math.js +2 -1
  250. package/dist/resize.d.ts +1 -1
  251. package/dist/test-seams.d.ts +2 -2
  252. package/dist/tools/builtin/ellipse/useEllipseTool.js +1 -0
  253. package/dist/tools/builtin/ellipse/useEllipseTool.js.map +1 -1
  254. package/dist/tools/builtin/eyedropper/useEyedropperTool.js +2 -1
  255. package/dist/tools/builtin/eyedropper/useEyedropperTool.js.map +1 -1
  256. package/dist/tools/builtin/hand/dragPanContribution.js +4 -1
  257. package/dist/tools/builtin/hand/dragPanContribution.js.map +1 -1
  258. package/dist/tools/builtin/hand/useHandTool.js +7 -4
  259. package/dist/tools/builtin/hand/useHandTool.js.map +1 -1
  260. package/dist/tools/builtin/image/useImageTool.js +1 -0
  261. package/dist/tools/builtin/image/useImageTool.js.map +1 -1
  262. package/dist/tools/builtin/lasso/useLassoTool.js +1 -0
  263. package/dist/tools/builtin/lasso/useLassoTool.js.map +1 -1
  264. package/dist/tools/builtin/line/useLineTool.js +1 -0
  265. package/dist/tools/builtin/line/useLineTool.js.map +1 -1
  266. package/dist/tools/builtin/measure/useMeasureTool.js +12 -5
  267. package/dist/tools/builtin/measure/useMeasureTool.js.map +1 -1
  268. package/dist/tools/builtin/pen/usePenTool.js +14 -6
  269. package/dist/tools/builtin/pen/usePenTool.js.map +1 -1
  270. package/dist/tools/builtin/pencil/usePencilTool.js +1 -0
  271. package/dist/tools/builtin/pencil/usePencilTool.js.map +1 -1
  272. package/dist/tools/builtin/polygon/usePolygonTool.js +13 -4
  273. package/dist/tools/builtin/polygon/usePolygonTool.js.map +1 -1
  274. package/dist/tools/builtin/rect/useRectTool.js +1 -0
  275. package/dist/tools/builtin/rect/useRectTool.js.map +1 -1
  276. package/dist/tools/builtin/select/adapterPointQuery.js +3 -2
  277. package/dist/tools/builtin/select/adapterPointQuery.js.map +1 -1
  278. package/dist/tools/builtin/select/pressDecision.js +32 -0
  279. package/dist/tools/builtin/select/pressDecision.js.map +1 -0
  280. package/dist/tools/builtin/select/selectionContributions.js +22 -13
  281. package/dist/tools/builtin/select/selectionContributions.js.map +1 -1
  282. package/dist/tools/builtin/select/useSelectTool.js +13 -10
  283. package/dist/tools/builtin/select/useSelectTool.js.map +1 -1
  284. package/dist/tools/builtin/slice/useSliceTool.js +22 -8
  285. package/dist/tools/builtin/slice/useSliceTool.js.map +1 -1
  286. package/dist/tools/builtin/star/useStarTool.js +13 -4
  287. package/dist/tools/builtin/star/useStarTool.js.map +1 -1
  288. package/dist/tools/builtin/sticky/stickyNode.js +4 -1
  289. package/dist/tools/builtin/sticky/stickyNode.js.map +1 -1
  290. package/dist/tools/builtin/sticky/useStickyTool.js +10 -5
  291. package/dist/tools/builtin/sticky/useStickyTool.js.map +1 -1
  292. package/dist/tools/builtin/text/useTextTool.js +3 -1
  293. package/dist/tools/builtin/text/useTextTool.js.map +1 -1
  294. package/dist/util/paint.js +1 -3
  295. package/dist/util/paint.js.map +1 -1
  296. package/dist/version.js +1 -1
  297. package/package.json +28 -24
@@ -1 +1 @@
1
- {"version":3,"file":"derive.js","names":[],"sources":["../../../../src/core/scene/pathEffects/derive.ts"],"sourcesContent":["/**\n * A path effect as a scene node: a leaf whose `dependsOn` names its sources,\n * whose `data.pathEffect` says what to make of them, and whose path and pose\n * are both derived — the path to paint and pick, the pose as that path's\n * bounds, so selection chrome and marquee see where it is.\n *\n * Reading a source's outline and paint is its painter's business, which sits\n * above the scene, so the derivations are made from a reader:\n * `createPathEffectDerivations(read)`. The canvas layer makes the kit's, and\n * a scene that holds path effects registers them under {@link PATH_EFFECT}.\n */\nimport { boundsOfPath, type Path } from '@weasel-js/geom';\nimport { derivedPartsOf, pathFromParts, type DerivedPart } from '../derivedParts';\nimport { samePoseValue, snapshotPose } from '../poseSnapshot';\nimport type { DerivedDep, NodeId } from '../types';\nimport { effectParts, type PathEffect } from './effects';\nimport type { EffectSourceReader } from './source';\n\n/** The registry key both derivations go by, for `derivePath` and `derivePose`. */\nexport const PATH_EFFECT = 'kit:pathEffect';\n\n/** A node's path effect, or `null` when its data holds none. */\nexport function pathEffectOf(node: { data?: unknown } | undefined): PathEffect | null {\n const effect = (node?.data as { pathEffect?: PathEffect } | null | undefined)?.pathEffect;\n return effect && typeof effect === 'object' && typeof effect.kind === 'string' ? effect : null;\n}\n\n/** A path-effect node's two derivations. */\nexport interface PathEffectDerivations {\n /** What it paints and picks by. */\n derivePath(node: { data?: unknown }, deps: readonly (DerivedDep<unknown> | undefined)[]): Path | null;\n /** The bounds of what it paints, or `null` — its authored pose — while it\n * paints nothing. */\n derivePose(node: { data?: unknown }, deps: readonly (DerivedDep<unknown> | undefined)[]): { x: number; y: number; width: number; height: number } | null;\n}\n\ninterface Derived {\n data: unknown;\n nodes: unknown[];\n depData: unknown[];\n poses: unknown[];\n paths: (Path | null)[];\n path: Path | null;\n}\n\n/** The derivations of a path effect whose sources `read` reads. */\nexport function createPathEffectDerivations(read: EffectSourceReader): PathEffectDerivations {\n /** One result per effect node, shared by its two derivations: the pose is\n * the path's bounds, and working the effect out twice a frame would double\n * its cost. Checked against everything it read, by value for poses — a\n * drag's override mutates its pose in place. */\n const results = new WeakMap<object, Derived>();\n const derive = (node: { data?: unknown }, deps: readonly (DerivedDep<unknown> | undefined)[]): Derived => {\n const held = results.get(node);\n if (held !== undefined && current(held, node, deps)) return held;\n const effect = pathEffectOf(node);\n const parts = effect === null\n ? null\n : effectParts(effect, deps.map((d) => (d === undefined ? null : read(d))));\n const result: Derived = {\n data: node.data,\n nodes: deps.map((d) => d?.node),\n depData: deps.map((d) => d?.node.data),\n poses: deps.map((d) => (d === undefined ? undefined : snapshotPose(d.pose))),\n paths: deps.map((d) => d?.path ?? null),\n path: parts === null ? null : pathFromParts(parts),\n };\n results.set(node, result);\n return result;\n };\n return {\n derivePath: (node, deps) => derive(node, deps).path,\n derivePose: (node, deps) => {\n const path = derive(node, deps).path;\n if (path === null) return null;\n const { x, y, width, height } = boundsOfPath(path);\n return { x, y, width, height };\n },\n };\n}\n\nfunction current(held: Derived, node: { data?: unknown }, deps: readonly (DerivedDep<unknown> | undefined)[]): boolean {\n if (held.data !== node.data || held.nodes.length !== deps.length) return false;\n for (let i = 0; i < deps.length; i++) {\n const d = deps[i];\n if (held.nodes[i] !== d?.node) return false;\n if (d === undefined) continue;\n if (held.depData[i] !== d.node.data || !samePoseValue(held.poses[i], d.pose)) return false;\n if (held.paths[i] !== d.path) return false;\n }\n return true;\n}\n\n/** The parts a path-effect node paints, as its sources stand, or `null` when\n * it has no derivation or nothing to paint. */\nexport function pathEffectParts(\n node: { data?: unknown; derivePath?: unknown },\n deps: readonly (DerivedDep<unknown> | undefined)[],\n): readonly DerivedPart[] | null {\n if (typeof node.derivePath !== 'function') return null;\n return derivedPartsOf((node.derivePath as PathEffectDerivations['derivePath'])(node, deps)) ?? null;\n}\n\n/** What to hand `Scene.add` (or an insert op) for a path-effect node over\n * `sources`, in the order its kind reads them, deriving through\n * `derivations` — the ones the scene registers under {@link PATH_EFFECT}. */\nexport function pathEffectNode<TLayer extends string>(\n effect: PathEffect,\n sources: readonly NodeId[],\n layer: TLayer,\n derivations: PathEffectDerivations,\n): {\n kind: 'leaf';\n layer: TLayer;\n pose: { x: number; y: number; width: number; height: number };\n data: { pathEffect: PathEffect };\n dependsOn: readonly NodeId[];\n derivePath: PathEffectDerivations['derivePath'];\n derivePose: PathEffectDerivations['derivePose'];\n} {\n return {\n kind: 'leaf',\n layer,\n pose: { x: 0, y: 0, width: 0, height: 0 },\n data: { pathEffect: effect },\n dependsOn: [...sources],\n derivePath: derivations.derivePath,\n derivePose: derivations.derivePose,\n };\n}\n\n/** The path-effect derivations `registry` holds under {@link PATH_EFFECT},\n * or `null` when it holds none. */\nexport function registeredPathEffects(registry: {\n derivePath?: Readonly<Record<string, unknown>>;\n derivePose?: Readonly<Record<string, unknown>>;\n}): PathEffectDerivations | null {\n const derivePath = registry.derivePath?.[PATH_EFFECT];\n const derivePose = registry.derivePose?.[PATH_EFFECT];\n return typeof derivePath === 'function' && typeof derivePose === 'function'\n ? { derivePath, derivePose } as PathEffectDerivations\n : null;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAmBA,IAAa,cAAc;;AAG3B,SAAgB,aAAa,MAAyD;CACpF,MAAM,UAAU,MAAM,KAAA,EAAyD;CAC/E,OAAO,UAAU,OAAO,WAAW,YAAY,OAAO,OAAO,SAAS,WAAW,SAAS;AAC5F;;AAqBA,SAAgB,4BAA4B,MAAiD;;;;;CAK3F,MAAM,0BAAU,IAAI,QAAyB;CAC7C,MAAM,UAAU,MAA0B,SAAgE;EACxG,MAAM,OAAO,QAAQ,IAAI,IAAI;EAC7B,IAAI,SAAS,UAAa,QAAQ,MAAM,MAAM,IAAI,GAAG,OAAO;EAC5D,MAAM,SAAS,aAAa,IAAI;EAChC,MAAM,QAAQ,WAAW,OACrB,OACA,YAAY,QAAQ,KAAK,KAAK,MAAO,MAAM,SAAY,OAAO,KAAK,CAAC,CAAE,CAAC;EAC3E,MAAM,SAAkB;GACtB,MAAM,KAAK;GACX,OAAO,KAAK,KAAK,MAAM,GAAG,IAAI;GAC9B,SAAS,KAAK,KAAK,MAAM,GAAG,KAAK,IAAI;GACrC,OAAO,KAAK,KAAK,MAAO,MAAM,SAAY,SAAY,aAAa,EAAE,IAAI,CAAE;GAC3E,OAAO,KAAK,KAAK,MAAM,GAAG,QAAQ,IAAI;GACtC,MAAM,UAAU,OAAO,OAAO,cAAc,KAAK;EACnD;EACA,QAAQ,IAAI,MAAM,MAAM;EACxB,OAAO;CACT;CACA,OAAO;EACL,aAAa,MAAM,SAAS,OAAO,MAAM,IAAI,CAAC,CAAC;EAC/C,aAAa,MAAM,SAAS;GAC1B,MAAM,OAAO,OAAO,MAAM,IAAI,CAAC,CAAC;GAChC,IAAI,SAAS,MAAM,OAAO;GAC1B,MAAM,EAAE,GAAG,GAAG,OAAO,WAAW,aAAa,IAAI;GACjD,OAAO;IAAE;IAAG;IAAG;IAAO;GAAO;EAC/B;CACF;AACF;AAEA,SAAS,QAAQ,MAAe,MAA0B,MAA6D;CACrH,IAAI,KAAK,SAAS,KAAK,QAAQ,KAAK,MAAM,WAAW,KAAK,QAAQ,OAAO;CACzE,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;EACpC,MAAM,IAAI,KAAK;EACf,IAAI,KAAK,MAAM,OAAO,GAAG,MAAM,OAAO;EACtC,IAAI,MAAM,QAAW;EACrB,IAAI,KAAK,QAAQ,OAAO,EAAE,KAAK,QAAQ,CAAC,cAAc,KAAK,MAAM,IAAI,EAAE,IAAI,GAAG,OAAO;EACrF,IAAI,KAAK,MAAM,OAAO,EAAE,MAAM,OAAO;CACvC;CACA,OAAO;AACT;;;AAIA,SAAgB,gBACd,MACA,MAC+B;CAC/B,IAAI,OAAO,KAAK,eAAe,YAAY,OAAO;CAClD,OAAO,eAAgB,KAAK,WAAmD,MAAM,IAAI,CAAC,KAAK;AACjG;;;;AAKA,SAAgB,eACd,QACA,SACA,OACA,aASA;CACA,OAAO;EACL,MAAM;EACN;EACA,MAAM;GAAE,GAAG;GAAG,GAAG;GAAG,OAAO;GAAG,QAAQ;EAAE;EACxC,MAAM,EAAE,YAAY,OAAO;EAC3B,WAAW,CAAC,GAAG,OAAO;EACtB,YAAY,YAAY;EACxB,YAAY,YAAY;CAC1B;AACF;;;AAIA,SAAgB,sBAAsB,UAGL;CAC/B,MAAM,aAAa,SAAS,aAAa;CACzC,MAAM,aAAa,SAAS,aAAa;CACzC,OAAO,OAAO,eAAe,cAAc,OAAO,eAAe,aAC7D;EAAE;EAAY;CAAW,IACzB;AACN"}
1
+ {"version":3,"file":"derive.js","names":[],"sources":["../../../../src/core/scene/pathEffects/derive.ts"],"sourcesContent":["/**\n * A path effect as a scene node: a leaf whose `dependsOn` names its sources,\n * whose `data.pathEffect` says what to make of them, and whose path and pose\n * are both derived — the path to paint and pick, the pose as that path's\n * bounds, so selection chrome and marquee see where it is.\n *\n * Reading a source's outline and paint is its painter's business, which sits\n * above the scene, so the derivations are made from a reader:\n * `createPathEffectDerivations(read)`. The canvas layer makes the kit's, and\n * a scene that holds path effects registers them under {@link PATH_EFFECT}.\n */\nimport { boundsOfPath, type Path } from '@weasel-js/geom';\nimport { derivedPartsOf, pathFromParts, type DerivedPart } from '../derivedParts';\nimport { fontGeneration, noteFontRead, withFontReads } from '../fontReads';\nimport { samePoseValue, snapshotPose } from '../poseSnapshot';\nimport type { DerivedDep, NodeId } from '../types';\nimport { effectParts, type PathEffect } from './effects';\nimport type { EffectSourceReader } from './source';\n\n/** The registry key both derivations go by, for `derivePath` and `derivePose`. */\nexport const PATH_EFFECT = 'kit:pathEffect';\n\n/** A node's path effect, or `null` when its data holds none. */\nexport function pathEffectOf(node: { data?: unknown } | undefined): PathEffect | null {\n const effect = (node?.data as { pathEffect?: PathEffect } | null | undefined)?.pathEffect;\n return effect && typeof effect === 'object' && typeof effect.kind === 'string' ? effect : null;\n}\n\n/** A path-effect node's two derivations. */\nexport interface PathEffectDerivations {\n /** What it paints and picks by. */\n derivePath(node: { data?: unknown }, deps: readonly (DerivedDep<unknown> | undefined)[]): Path | null;\n /** The bounds of what it paints, or `null` — its authored pose — while it\n * paints nothing. */\n derivePose(node: { data?: unknown }, deps: readonly (DerivedDep<unknown> | undefined)[]): { x: number; y: number; width: number; height: number } | null;\n}\n\ninterface Derived {\n data: unknown;\n nodes: unknown[];\n depData: unknown[];\n poses: unknown[];\n paths: (Path | null)[];\n path: Path | null;\n /** The face generation it was worked out under, or `null` when it read no\n * glyph outlines. */\n faces: number | null;\n}\n\n/** The derivations of a path effect whose sources `read` reads. */\nexport function createPathEffectDerivations(read: EffectSourceReader): PathEffectDerivations {\n /** One result per effect node, shared by its two derivations: the pose is\n * the path's bounds, and working the effect out twice a frame would double\n * its cost. Checked against everything it read, by value for poses — a\n * drag's override mutates its pose in place. */\n const results = new WeakMap<object, Derived>();\n const derive = (node: { data?: unknown }, deps: readonly (DerivedDep<unknown> | undefined)[]): Derived => {\n const held = results.get(node);\n if (held !== undefined && current(held, node, deps)) return held;\n const effect = pathEffectOf(node);\n const generation = fontGeneration();\n const { value: parts, fonts } = withFontReads(() => (effect === null\n ? null\n : effectParts(effect, deps.map((d) => (d === undefined ? null : read(d))))));\n const result: Derived = {\n data: node.data,\n nodes: deps.map((d) => d?.node),\n depData: deps.map((d) => d?.node.data),\n poses: deps.map((d) => (d === undefined ? undefined : snapshotPose(d.pose))),\n paths: deps.map((d) => d?.path ?? null),\n path: parts === null ? null : pathFromParts(parts),\n faces: fonts ? generation : null,\n };\n results.set(node, result);\n return result;\n };\n return {\n derivePath: (node, deps) => derive(node, deps).path,\n derivePose: (node, deps) => {\n const path = derive(node, deps).path;\n if (path === null) return null;\n const { x, y, width, height } = boundsOfPath(path);\n return { x, y, width, height };\n },\n };\n}\n\nfunction current(held: Derived, node: { data?: unknown }, deps: readonly (DerivedDep<unknown> | undefined)[]): boolean {\n if (held.data !== node.data || held.nodes.length !== deps.length) return false;\n if (held.faces !== null && held.faces !== fontGeneration()) return false;\n for (let i = 0; i < deps.length; i++) {\n const d = deps[i];\n if (held.nodes[i] !== d?.node) return false;\n if (d === undefined) continue;\n if (held.depData[i] !== d.node.data || !samePoseValue(held.poses[i], d.pose)) return false;\n if (held.paths[i] !== d.path) return false;\n }\n // A hit is still a read, for the scene memo enclosing this one.\n if (held.faces !== null) noteFontRead();\n return true;\n}\n\n/** The parts a path-effect node paints, as its sources stand, or `null` when\n * it has no derivation or nothing to paint. */\nexport function pathEffectParts(\n node: { data?: unknown; derivePath?: unknown },\n deps: readonly (DerivedDep<unknown> | undefined)[],\n): readonly DerivedPart[] | null {\n if (typeof node.derivePath !== 'function') return null;\n return derivedPartsOf((node.derivePath as PathEffectDerivations['derivePath'])(node, deps)) ?? null;\n}\n\n/** What to hand `Scene.add` (or an insert op) for a path-effect node over\n * `sources`, in the order its kind reads them, deriving through\n * `derivations` — the ones the scene registers under {@link PATH_EFFECT}. */\nexport function pathEffectNode<TLayer extends string>(\n effect: PathEffect,\n sources: readonly NodeId[],\n layer: TLayer,\n derivations: PathEffectDerivations,\n): {\n kind: 'leaf';\n layer: TLayer;\n pose: { x: number; y: number; width: number; height: number };\n data: { pathEffect: PathEffect };\n dependsOn: readonly NodeId[];\n derivePath: PathEffectDerivations['derivePath'];\n derivePose: PathEffectDerivations['derivePose'];\n} {\n return {\n kind: 'leaf',\n layer,\n pose: { x: 0, y: 0, width: 0, height: 0 },\n data: { pathEffect: effect },\n dependsOn: [...sources],\n derivePath: derivations.derivePath,\n derivePose: derivations.derivePose,\n };\n}\n\n/** The path-effect derivations `registry` holds under {@link PATH_EFFECT},\n * or `null` when it holds none. */\nexport function registeredPathEffects(registry: {\n derivePath?: Readonly<Record<string, unknown>>;\n derivePose?: Readonly<Record<string, unknown>>;\n}): PathEffectDerivations | null {\n const derivePath = registry.derivePath?.[PATH_EFFECT];\n const derivePose = registry.derivePose?.[PATH_EFFECT];\n return typeof derivePath === 'function' && typeof derivePose === 'function'\n ? { derivePath, derivePose } as PathEffectDerivations\n : null;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAoBA,IAAa,cAAc;;AAG3B,SAAgB,aAAa,MAAyD;CACpF,MAAM,UAAU,MAAM,KAAA,EAAyD;CAC/E,OAAO,UAAU,OAAO,WAAW,YAAY,OAAO,OAAO,SAAS,WAAW,SAAS;AAC5F;;AAwBA,SAAgB,4BAA4B,MAAiD;;;;;CAK3F,MAAM,0BAAU,IAAI,QAAyB;CAC7C,MAAM,UAAU,MAA0B,SAAgE;EACxG,MAAM,OAAO,QAAQ,IAAI,IAAI;EAC7B,IAAI,SAAS,UAAa,QAAQ,MAAM,MAAM,IAAI,GAAG,OAAO;EAC5D,MAAM,SAAS,aAAa,IAAI;EAChC,MAAM,aAAa,eAAe;EAClC,MAAM,EAAE,OAAO,OAAO,UAAU,oBAAqB,WAAW,OAC5D,OACA,YAAY,QAAQ,KAAK,KAAK,MAAO,MAAM,SAAY,OAAO,KAAK,CAAC,CAAE,CAAC,CAAE;EAC7E,MAAM,SAAkB;GACtB,MAAM,KAAK;GACX,OAAO,KAAK,KAAK,MAAM,GAAG,IAAI;GAC9B,SAAS,KAAK,KAAK,MAAM,GAAG,KAAK,IAAI;GACrC,OAAO,KAAK,KAAK,MAAO,MAAM,SAAY,SAAY,aAAa,EAAE,IAAI,CAAE;GAC3E,OAAO,KAAK,KAAK,MAAM,GAAG,QAAQ,IAAI;GACtC,MAAM,UAAU,OAAO,OAAO,cAAc,KAAK;GACjD,OAAO,QAAQ,aAAa;EAC9B;EACA,QAAQ,IAAI,MAAM,MAAM;EACxB,OAAO;CACT;CACA,OAAO;EACL,aAAa,MAAM,SAAS,OAAO,MAAM,IAAI,CAAC,CAAC;EAC/C,aAAa,MAAM,SAAS;GAC1B,MAAM,OAAO,OAAO,MAAM,IAAI,CAAC,CAAC;GAChC,IAAI,SAAS,MAAM,OAAO;GAC1B,MAAM,EAAE,GAAG,GAAG,OAAO,WAAW,aAAa,IAAI;GACjD,OAAO;IAAE;IAAG;IAAG;IAAO;GAAO;EAC/B;CACF;AACF;AAEA,SAAS,QAAQ,MAAe,MAA0B,MAA6D;CACrH,IAAI,KAAK,SAAS,KAAK,QAAQ,KAAK,MAAM,WAAW,KAAK,QAAQ,OAAO;CACzE,IAAI,KAAK,UAAU,QAAQ,KAAK,UAAU,eAAe,GAAG,OAAO;CACnE,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;EACpC,MAAM,IAAI,KAAK;EACf,IAAI,KAAK,MAAM,OAAO,GAAG,MAAM,OAAO;EACtC,IAAI,MAAM,QAAW;EACrB,IAAI,KAAK,QAAQ,OAAO,EAAE,KAAK,QAAQ,CAAC,cAAc,KAAK,MAAM,IAAI,EAAE,IAAI,GAAG,OAAO;EACrF,IAAI,KAAK,MAAM,OAAO,EAAE,MAAM,OAAO;CACvC;CAEA,IAAI,KAAK,UAAU,MAAM,aAAa;CACtC,OAAO;AACT;;;AAIA,SAAgB,gBACd,MACA,MAC+B;CAC/B,IAAI,OAAO,KAAK,eAAe,YAAY,OAAO;CAClD,OAAO,eAAgB,KAAK,WAAmD,MAAM,IAAI,CAAC,KAAK;AACjG;;;;AAKA,SAAgB,eACd,QACA,SACA,OACA,aASA;CACA,OAAO;EACL,MAAM;EACN;EACA,MAAM;GAAE,GAAG;GAAG,GAAG;GAAG,OAAO;GAAG,QAAQ;EAAE;EACxC,MAAM,EAAE,YAAY,OAAO;EAC3B,WAAW,CAAC,GAAG,OAAO;EACtB,YAAY,YAAY;EACxB,YAAY,YAAY;CAC1B;AACF;;;AAIA,SAAgB,sBAAsB,UAGL;CAC/B,MAAM,aAAa,SAAS,aAAa;CACzC,MAAM,aAAa,SAAS,aAAa;CACzC,OAAO,OAAO,eAAe,cAAc,OAAO,eAAe,aAC7D;EAAE;EAAY;CAAW,IACzB;AACN"}
@@ -1,11 +1,12 @@
1
1
  import { blendParts } from "./blend.js";
2
+ import { textAlongParts, wholeItem } from "./textAlong.js";
2
3
  import { bendAlongPath, repeatAlongPath } from "@weasel-js/geom";
3
4
 
4
5
  //#region src/core/scene/pathEffects/effects.ts
5
6
  /**
6
7
  * The path effects, each as the parts it paints from its sources: a blend's
7
8
  * steps between two shapes, a shape bent along a spine, copies of a shape
8
- * along a path.
9
+ * along a path, text laid along a path.
9
10
  */
10
11
  /** The parts `effect` paints from `sources`, in its sources' order, or
11
12
  * `null` when a source it needs is missing — a blend's spine among them,
@@ -32,6 +33,7 @@ function effectParts(effect, sources) {
32
33
  fill: a.fill,
33
34
  stroke: a.stroke
34
35
  }));
36
+ case "textAlong": return textAlongParts(effect, a.items?.() ?? [wholeItem(a)], b.world);
35
37
  }
36
38
  }
37
39
 
@@ -1 +1 @@
1
- {"version":3,"file":"effects.js","names":[],"sources":["../../../../src/core/scene/pathEffects/effects.ts"],"sourcesContent":["/**\n * The path effects, each as the parts it paints from its sources: a blend's\n * steps between two shapes, a shape bent along a spine, copies of a shape\n * along a path.\n */\nimport {\n bendAlongPath, repeatAlongPath, type BendAlongPathOptions, type RepeatAlongPathOptions,\n} from '@weasel-js/geom';\nimport type { DerivedPart } from '../derivedParts';\nimport { blendParts, type BlendEffect } from './blend';\nimport type { EffectSource } from './source';\n\nexport type { BlendEffect };\n\n/** A shape bent along a spine. Its sources are `[shape, spine]`. */\nexport interface BendAlongEffect extends BendAlongPathOptions {\n kind: 'bendAlong';\n}\n\n/** Copies of a shape along a path. Its sources are `[shape, path]`. */\nexport interface RepeatAlongEffect extends Omit<RepeatAlongPathOptions, 'flattenTolerance'> {\n kind: 'repeatAlong';\n}\n\n/** A path effect, as a node's `data.pathEffect` holds it. */\nexport type PathEffect = BlendEffect | BendAlongEffect | RepeatAlongEffect;\n\n/** The parts `effect` paints from `sources`, in its sources' order, or\n * `null` when a source it needs is missing — a blend's spine among them,\n * where it names one. */\nexport function effectParts(effect: PathEffect, sources: readonly (EffectSource | null)[]): DerivedPart[] | null {\n const [a, b] = sources;\n if (!a || !b) return null;\n switch (effect.kind) {\n case 'blend': {\n if (sources.length < 3) return blendParts(effect, a, b);\n const spine = sources[2];\n return spine ? blendParts(effect, a, b, spine.world) : null;\n }\n case 'bendAlong': {\n const path = bendAlongPath(a.world, b.world, effect);\n return path === null ? null : [{ path, fill: a.fill, stroke: a.stroke }];\n }\n case 'repeatAlong':\n return repeatAlongPath(a.world, b.world, effect).map((path) => ({ path, fill: a.fill, stroke: a.stroke }));\n }\n}\n"],"mappings":";;;;;;;;;;;;AA8BA,SAAgB,YAAY,QAAoB,SAAiE;CAC/G,MAAM,CAAC,GAAG,KAAK;CACf,IAAI,CAAC,KAAK,CAAC,GAAG,OAAO;CACrB,QAAQ,OAAO,MAAf;EACE,KAAK,SAAS;GACZ,IAAI,QAAQ,SAAS,GAAG,OAAO,WAAW,QAAQ,GAAG,CAAC;GACtD,MAAM,QAAQ,QAAQ;GACtB,OAAO,QAAQ,WAAW,QAAQ,GAAG,GAAG,MAAM,KAAK,IAAI;EACzD;EACA,KAAK,aAAa;GAChB,MAAM,OAAO,cAAc,EAAE,OAAO,EAAE,OAAO,MAAM;GACnD,OAAO,SAAS,OAAO,OAAO,CAAC;IAAE;IAAM,MAAM,EAAE;IAAM,QAAQ,EAAE;GAAO,CAAC;EACzE;EACA,KAAK,eACH,OAAO,gBAAgB,EAAE,OAAO,EAAE,OAAO,MAAM,CAAC,CAAC,KAAK,UAAU;GAAE;GAAM,MAAM,EAAE;GAAM,QAAQ,EAAE;EAAO,EAAE;CAC7G;AACF"}
1
+ {"version":3,"file":"effects.js","names":[],"sources":["../../../../src/core/scene/pathEffects/effects.ts"],"sourcesContent":["/**\n * The path effects, each as the parts it paints from its sources: a blend's\n * steps between two shapes, a shape bent along a spine, copies of a shape\n * along a path, text laid along a path.\n */\nimport {\n bendAlongPath, repeatAlongPath, type BendAlongPathOptions, type RepeatAlongPathOptions,\n} from '@weasel-js/geom';\nimport type { DerivedPart } from '../derivedParts';\nimport { blendParts, type BlendEffect } from './blend';\nimport type { EffectSource, PathEffectOptions } from './source';\nimport { textAlongParts, wholeItem, type TextAlongEffect } from './textAlong';\n\nexport type { BlendEffect, PathEffectOptions, TextAlongEffect };\n\n/** A shape bent along a spine. Its sources are `[shape, spine]`. */\nexport interface BendAlongEffect extends BendAlongPathOptions, PathEffectOptions {\n kind: 'bendAlong';\n}\n\n/** Copies of a shape along a path. Its sources are `[shape, path]`. */\nexport interface RepeatAlongEffect extends Omit<RepeatAlongPathOptions, 'flattenTolerance'>, PathEffectOptions {\n kind: 'repeatAlong';\n}\n\n/** A path effect, as a node's `data.pathEffect` holds it. */\nexport type PathEffect = BlendEffect | BendAlongEffect | RepeatAlongEffect | TextAlongEffect;\n\n/** The parts `effect` paints from `sources`, in its sources' order, or\n * `null` when a source it needs is missing — a blend's spine among them,\n * where it names one. */\nexport function effectParts(effect: PathEffect, sources: readonly (EffectSource | null)[]): DerivedPart[] | null {\n const [a, b] = sources;\n if (!a || !b) return null;\n switch (effect.kind) {\n case 'blend': {\n if (sources.length < 3) return blendParts(effect, a, b);\n const spine = sources[2];\n return spine ? blendParts(effect, a, b, spine.world) : null;\n }\n case 'bendAlong': {\n const path = bendAlongPath(a.world, b.world, effect);\n return path === null ? null : [{ path, fill: a.fill, stroke: a.stroke }];\n }\n case 'repeatAlong':\n return repeatAlongPath(a.world, b.world, effect).map((path) => ({ path, fill: a.fill, stroke: a.stroke }));\n case 'textAlong':\n return textAlongParts(effect, a.items?.() ?? [wholeItem(a)], b.world);\n }\n}\n"],"mappings":";;;;;;;;;;;;;AA+BA,SAAgB,YAAY,QAAoB,SAAiE;CAC/G,MAAM,CAAC,GAAG,KAAK;CACf,IAAI,CAAC,KAAK,CAAC,GAAG,OAAO;CACrB,QAAQ,OAAO,MAAf;EACE,KAAK,SAAS;GACZ,IAAI,QAAQ,SAAS,GAAG,OAAO,WAAW,QAAQ,GAAG,CAAC;GACtD,MAAM,QAAQ,QAAQ;GACtB,OAAO,QAAQ,WAAW,QAAQ,GAAG,GAAG,MAAM,KAAK,IAAI;EACzD;EACA,KAAK,aAAa;GAChB,MAAM,OAAO,cAAc,EAAE,OAAO,EAAE,OAAO,MAAM;GACnD,OAAO,SAAS,OAAO,OAAO,CAAC;IAAE;IAAM,MAAM,EAAE;IAAM,QAAQ,EAAE;GAAO,CAAC;EACzE;EACA,KAAK,eACH,OAAO,gBAAgB,EAAE,OAAO,EAAE,OAAO,MAAM,CAAC,CAAC,KAAK,UAAU;GAAE;GAAM,MAAM,EAAE;GAAM,QAAQ,EAAE;EAAO,EAAE;EAC3G,KAAK,aACH,OAAO,eAAe,QAAQ,EAAE,QAAQ,KAAK,CAAC,UAAU,CAAC,CAAC,GAAG,EAAE,KAAK;CACxE;AACF"}
@@ -0,0 +1,95 @@
1
+ import { boundsOfPath, multiply, pathShape, placeAlongRun, rotate, runLength, trackRun, transformPath, translate } from "@weasel-js/geom";
2
+
3
+ //#region src/core/scene/pathEffects/textAlong.ts
4
+ /**
5
+ * Text along a path: a source's items — a text node's grapheme clusters, or a
6
+ * whole shape as one item — laid end to end along a path, each turned to it.
7
+ */
8
+ var LEAD = {
9
+ start: 0,
10
+ center: .5,
11
+ end: 1
12
+ };
13
+ /** The parts `items` paint laid along `path` as `effect` says; `[]` when there is nothing to lay or nowhere to lay it. */
14
+ function textAlongParts(effect, items, path) {
15
+ const shape = pathShape(path);
16
+ if (shape === null || !(shape.length > 0) || items.length === 0) return [];
17
+ const L = shape.length;
18
+ const closed = shape.closed;
19
+ const lengths = items.map((it) => it.length);
20
+ const k = LEAD[effect.align ?? "start"];
21
+ const at = clamp01(effect.start ?? 0) * L;
22
+ const gap = effect.overflow === "fit" ? fitGap(lengths, roomAt(at, L, k, closed)) : 0;
23
+ const total = runLength(lengths, gap);
24
+ const c = at + (.5 - k) * total;
25
+ const about = closed ? (c / L % 1 + 1) % 1 : clamp01(c / L);
26
+ const run = trackRun(shape, 0, 1, {
27
+ side: effect.side ?? "auto",
28
+ about
29
+ });
30
+ const places = placeAlongRun(run, lengths, {
31
+ gap,
32
+ align: "center",
33
+ at: run.reversed ? L - c : c,
34
+ shift: finiteOr(effect.shift, 0),
35
+ wrap: closed
36
+ });
37
+ const center = effect.anchor === "center";
38
+ const out = [];
39
+ const firstStart = places[0].center - lengths[0] / 2;
40
+ places.forEach((place, i) => {
41
+ if (dropped(place, closed ? firstStart : null, L)) return;
42
+ const item = items[i];
43
+ const m = itemFrame(place, item.length, center ? item.middle : 0);
44
+ for (const part of item.parts) out.push({
45
+ ...part,
46
+ path: transformPath(part.path, m)
47
+ });
48
+ });
49
+ return out;
50
+ }
51
+ /** `source`'s whole outline as one item: its bounds' left at x = 0 and their vertical middle on the anchor line. */
52
+ function wholeItem(source) {
53
+ const b = boundsOfPath(source.world);
54
+ return {
55
+ length: b.width,
56
+ middle: 0,
57
+ parts: [{
58
+ path: transformPath(source.world, translate(-b.x, -(b.y + b.height / 2))),
59
+ fill: source.fill,
60
+ stroke: source.stroke
61
+ }]
62
+ };
63
+ }
64
+ /** SVG `textPath`'s rule: an item is not drawn once its middle is past an open path's ends, or on a closed path a whole
65
+ * lap or more on from the first item's start. Judged within a hair of rounding. */
66
+ function dropped(place, firstStart, L) {
67
+ const hair = 1e-9 * Math.max(1, L);
68
+ if (firstStart !== null) return place.center - firstStart >= L - hair;
69
+ return place.center < -hair || place.center > L + hair;
70
+ }
71
+ /** How much path the text has before it runs off, from `at`, with `k` of it laid back behind `at`. */
72
+ function roomAt(at, L, k, closed) {
73
+ if (closed) return L;
74
+ return k === 0 ? L - at : k === 1 ? at : 2 * Math.min(at, L - at);
75
+ }
76
+ /** The gap that makes `lengths` fill `room`, or 0 when they fit already. Never so negative that an item's step back
77
+ * from the one before it (its length plus the gap) goes below zero, so items keep their order in too short a room. */
78
+ function fitGap(lengths, room) {
79
+ const total = runLength(lengths, 0);
80
+ if (!(total > room) || lengths.length < 2) return 0;
81
+ let floor = -Infinity;
82
+ for (let i = 1; i < lengths.length; i++) floor = Math.max(floor, -(lengths[i - 1] + lengths[i]) / 2);
83
+ return Math.max((room - total) / (lengths.length - 1), floor);
84
+ }
85
+ /** Item frame to world: its middle on `place`, turned to it, its anchor line `lift` above y = 0 brought onto the path. */
86
+ function itemFrame(place, length, lift) {
87
+ const turn = multiply(rotate(place.angle * Math.PI / 180), translate(-length / 2, lift));
88
+ return multiply(translate(place.point.x, place.point.y), turn);
89
+ }
90
+ var clamp01 = (v) => v > 1 ? 1 : v > 0 ? v : 0;
91
+ var finiteOr = (v, d) => v !== void 0 && Number.isFinite(v) ? v : d;
92
+
93
+ //#endregion
94
+ export { textAlongParts, wholeItem };
95
+ //# sourceMappingURL=textAlong.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"textAlong.js","names":[],"sources":["../../../../src/core/scene/pathEffects/textAlong.ts"],"sourcesContent":["/**\n * Text along a path: a source's items — a text node's grapheme clusters, or a\n * whole shape as one item — laid end to end along a path, each turned to it.\n */\nimport {\n boundsOfPath, multiply, pathShape, placeAlongRun, rotate, runLength, trackRun, transformPath, translate,\n type Affine, type Path, type RunAlign, type RunPlace, type TrackSide,\n} from '@weasel-js/geom';\nimport type { DerivedPart } from '../derivedParts';\nimport type { EffectItem, EffectSource, PathEffectOptions } from './source';\n\n/**\n * Text (or any shape) laid along a path, item by item. Its sources are `[text, path]`. Only the text's first laid line\n * follows the path; a color glyph keeps its advance and paints nothing; underlines and other decorations are not drawn.\n * On an open path an item whose middle falls past either end is not drawn; on a closed one the text wraps past the\n * seam, and an item a whole lap on from the first item's start is not drawn.\n */\nexport interface TextAlongEffect extends PathEffectOptions {\n kind: 'textAlong';\n /** Which part of the text sits at `start`. Default `'start'`. */\n align?: RunAlign;\n /** Where the text sits, as a fraction 0..1 of the path's length from its first point. Default 0. */\n start?: number;\n /** How far the anchor line sits off the path, toward the glyphs' up, in world units. Default 0. */\n shift?: number;\n /** What sits on the path: the glyphs' baseline, or (`'center'`) their x-height middle. Default `'baseline'`. */\n anchor?: 'baseline' | 'center';\n /** Text longer than its room is cut at the path's end (`'clip'`), or has the gaps between items squeezed until it\n * fits (`'fit'`). Default `'clip'`. */\n overflow?: 'clip' | 'fit';\n /** Which side of the path the glyphs stand on; see `TrackSide`. Default `'auto'`, whichever reads upright. Turned over,\n * the text keeps the stretch of path it had, and on an open path drops whichever letters fall off it. */\n side?: TrackSide;\n}\n\nconst LEAD: Record<RunAlign, number> = { start: 0, center: 0.5, end: 1 };\n\n/** The parts `items` paint laid along `path` as `effect` says; `[]` when there is nothing to lay or nowhere to lay it. */\nexport function textAlongParts(effect: TextAlongEffect, items: readonly EffectItem[], path: Path): DerivedPart[] {\n const shape = pathShape(path);\n if (shape === null || !(shape.length > 0) || items.length === 0) return [];\n const L = shape.length;\n const closed = shape.closed;\n const lengths = items.map((it) => it.length);\n const k = LEAD[effect.align ?? 'start'];\n const at = clamp01(effect.start ?? 0) * L;\n const gap = effect.overflow === 'fit' ? fitGap(lengths, roomAt(at, L, k, closed)) : 0;\n const total = runLength(lengths, gap);\n const c = at + (0.5 - k) * total;\n const about = closed ? ((c / L) % 1 + 1) % 1 : clamp01(c / L);\n const run = trackRun(shape, 0, 1, { side: effect.side ?? 'auto', about });\n const places = placeAlongRun(run, lengths, {\n gap, align: 'center', at: run.reversed ? L - c : c, shift: finiteOr(effect.shift, 0), wrap: closed,\n });\n const center = effect.anchor === 'center';\n const out: DerivedPart[] = [];\n const firstStart = places[0]!.center - lengths[0]! / 2;\n places.forEach((place, i) => {\n // By center (SVG textPath's rule), not by RunPlace.clipped, which judges by extent.\n if (dropped(place, closed ? firstStart : null, L)) return;\n const item = items[i]!;\n const m = itemFrame(place, item.length, center ? item.middle : 0);\n for (const part of item.parts) out.push({ ...part, path: transformPath(part.path, m) });\n });\n return out;\n}\n\n/** `source`'s whole outline as one item: its bounds' left at x = 0 and their vertical middle on the anchor line. */\nexport function wholeItem(source: EffectSource): EffectItem {\n const b = boundsOfPath(source.world);\n return {\n length: b.width,\n middle: 0,\n parts: [{\n path: transformPath(source.world, translate(-b.x, -(b.y + b.height / 2))),\n fill: source.fill,\n stroke: source.stroke,\n }],\n };\n}\n\n/** SVG `textPath`'s rule: an item is not drawn once its middle is past an open path's ends, or on a closed path a whole\n * lap or more on from the first item's start. Judged within a hair of rounding. */\nfunction dropped(place: RunPlace, firstStart: number | null, L: number): boolean {\n const hair = 1e-9 * Math.max(1, L);\n if (firstStart !== null) return place.center - firstStart >= L - hair;\n return place.center < -hair || place.center > L + hair;\n}\n\n/** How much path the text has before it runs off, from `at`, with `k` of it laid back behind `at`. */\nfunction roomAt(at: number, L: number, k: number, closed: boolean): number {\n if (closed) return L;\n return k === 0 ? L - at : k === 1 ? at : 2 * Math.min(at, L - at);\n}\n\n/** The gap that makes `lengths` fill `room`, or 0 when they fit already. Never so negative that an item's step back\n * from the one before it (its length plus the gap) goes below zero, so items keep their order in too short a room. */\nfunction fitGap(lengths: readonly number[], room: number): number {\n const total = runLength(lengths, 0);\n if (!(total > room) || lengths.length < 2) return 0;\n let floor = -Infinity;\n for (let i = 1; i < lengths.length; i++) floor = Math.max(floor, -(lengths[i - 1]! + lengths[i]!) / 2);\n return Math.max((room - total) / (lengths.length - 1), floor);\n}\n\n/** Item frame to world: its middle on `place`, turned to it, its anchor line `lift` above y = 0 brought onto the path. */\nfunction itemFrame(place: RunPlace, length: number, lift: number): Affine {\n const turn = multiply(rotate((place.angle * Math.PI) / 180), translate(-length / 2, lift));\n return multiply(translate(place.point.x, place.point.y), turn);\n}\n\nconst clamp01 = (v: number): number => (v > 1 ? 1 : v > 0 ? v : 0);\nconst finiteOr = (v: number | undefined, d: number): number => (v !== undefined && Number.isFinite(v) ? v : d);\n"],"mappings":";;;;;;;AAmCA,IAAM,OAAiC;CAAE,OAAO;CAAG,QAAQ;CAAK,KAAK;AAAE;;AAGvE,SAAgB,eAAe,QAAyB,OAA8B,MAA2B;CAC/G,MAAM,QAAQ,UAAU,IAAI;CAC5B,IAAI,UAAU,QAAQ,EAAE,MAAM,SAAS,MAAM,MAAM,WAAW,GAAG,OAAO,CAAC;CACzE,MAAM,IAAI,MAAM;CAChB,MAAM,SAAS,MAAM;CACrB,MAAM,UAAU,MAAM,KAAK,OAAO,GAAG,MAAM;CAC3C,MAAM,IAAI,KAAK,OAAO,SAAS;CAC/B,MAAM,KAAK,QAAQ,OAAO,SAAS,CAAC,IAAI;CACxC,MAAM,MAAM,OAAO,aAAa,QAAQ,OAAO,SAAS,OAAO,IAAI,GAAG,GAAG,MAAM,CAAC,IAAI;CACpF,MAAM,QAAQ,UAAU,SAAS,GAAG;CACpC,MAAM,IAAI,MAAM,KAAM,KAAK;CAC3B,MAAM,QAAQ,UAAW,IAAI,IAAK,IAAI,KAAK,IAAI,QAAQ,IAAI,CAAC;CAC5D,MAAM,MAAM,SAAS,OAAO,GAAG,GAAG;EAAE,MAAM,OAAO,QAAQ;EAAQ;CAAM,CAAC;CACxE,MAAM,SAAS,cAAc,KAAK,SAAS;EACzC;EAAK,OAAO;EAAU,IAAI,IAAI,WAAW,IAAI,IAAI;EAAG,OAAO,SAAS,OAAO,OAAO,CAAC;EAAG,MAAM;CAC9F,CAAC;CACD,MAAM,SAAS,OAAO,WAAW;CACjC,MAAM,MAAqB,CAAC;CAC5B,MAAM,aAAa,OAAO,EAAE,CAAE,SAAS,QAAQ,KAAM;CACrD,OAAO,SAAS,OAAO,MAAM;EAE3B,IAAI,QAAQ,OAAO,SAAS,aAAa,MAAM,CAAC,GAAG;EACnD,MAAM,OAAO,MAAM;EACnB,MAAM,IAAI,UAAU,OAAO,KAAK,QAAQ,SAAS,KAAK,SAAS,CAAC;EAChE,KAAK,MAAM,QAAQ,KAAK,OAAO,IAAI,KAAK;GAAE,GAAG;GAAM,MAAM,cAAc,KAAK,MAAM,CAAC;EAAE,CAAC;CACxF,CAAC;CACD,OAAO;AACT;;AAGA,SAAgB,UAAU,QAAkC;CAC1D,MAAM,IAAI,aAAa,OAAO,KAAK;CACnC,OAAO;EACL,QAAQ,EAAE;EACV,QAAQ;EACR,OAAO,CAAC;GACN,MAAM,cAAc,OAAO,OAAO,UAAU,CAAC,EAAE,GAAG,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;GACxE,MAAM,OAAO;GACb,QAAQ,OAAO;EACjB,CAAC;CACH;AACF;;;AAIA,SAAS,QAAQ,OAAiB,YAA2B,GAAoB;CAC/E,MAAM,OAAO,OAAO,KAAK,IAAI,GAAG,CAAC;CACjC,IAAI,eAAe,MAAM,OAAO,MAAM,SAAS,cAAc,IAAI;CACjE,OAAO,MAAM,SAAS,CAAC,QAAQ,MAAM,SAAS,IAAI;AACpD;;AAGA,SAAS,OAAO,IAAY,GAAW,GAAW,QAAyB;CACzE,IAAI,QAAQ,OAAO;CACnB,OAAO,MAAM,IAAI,IAAI,KAAK,MAAM,IAAI,KAAK,IAAI,KAAK,IAAI,IAAI,IAAI,EAAE;AAClE;;;AAIA,SAAS,OAAO,SAA4B,MAAsB;CAChE,MAAM,QAAQ,UAAU,SAAS,CAAC;CAClC,IAAI,EAAE,QAAQ,SAAS,QAAQ,SAAS,GAAG,OAAO;CAClD,IAAI,QAAQ;CACZ,KAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK,QAAQ,KAAK,IAAI,OAAO,EAAE,QAAQ,IAAI,KAAM,QAAQ,MAAO,CAAC;CACrG,OAAO,KAAK,KAAK,OAAO,UAAU,QAAQ,SAAS,IAAI,KAAK;AAC9D;;AAGA,SAAS,UAAU,OAAiB,QAAgB,MAAsB;CACxE,MAAM,OAAO,SAAS,OAAQ,MAAM,QAAQ,KAAK,KAAM,GAAG,GAAG,UAAU,CAAC,SAAS,GAAG,IAAI,CAAC;CACzF,OAAO,SAAS,UAAU,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,GAAG,IAAI;AAC/D;AAEA,IAAM,WAAW,MAAuB,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI;AAChE,IAAM,YAAY,GAAuB,MAAuB,MAAM,UAAa,OAAO,SAAS,CAAC,IAAI,IAAI"}
@@ -139,7 +139,8 @@ declare function handlePoint(bounds: FramedBox, entry: Pick<ResizeHandleAnchor,
139
139
  x: number;
140
140
  y: number;
141
141
  };
142
- /** @deprecated Use {@link handlePoint}, which also places edge handles. */
142
+ /** A corner handle's position for the given bounds, in the bounds' frame.
143
+ * @deprecated Use {@link handlePoint}, which also places edge handles. */
143
144
  declare const cornerPoint: (bounds: FramedBox, entry: Pick<CornerAnchor, 'xEdge' | 'yEdge'>) => {
144
145
  x: number;
145
146
  y: number;
@@ -1,4 +1,5 @@
1
- import { TileKind, Paint, GradStop, TilePaint, Fill, Stroke, CommandFill, CommandStroke, CommandPaint } from '@weasel-js/paint';
1
+ import * as _weasel_js_paint from '@weasel-js/paint';
2
+ import { TileKind, Paint, GradStop, TilePaint, PaintedFill, PaintedStroke, Inherit, Fill, Stroke, CommandFill, CommandStroke, CommandPaint } from '@weasel-js/paint';
2
3
  import { Reflection } from '@weasel-js/registry';
3
4
  import { ComponentType } from 'react';
4
5
  import { IconRef } from '@weasel-js/icons';
@@ -200,6 +201,53 @@ declare function paintKindOf(fill: Paint): PaintKindEntry | undefined;
200
201
  /** @internal Test helper — do not call from product code. */
201
202
  declare function _resetPaintKindsForTests(): void;
202
203
 
204
+ /**
205
+ * What a node's `'inherit'` fill and stroke resolve to: its nearest enclosing
206
+ * container's, `null` for none. A field the node does not inherit is `null`
207
+ * here whatever its containers hold.
208
+ */
209
+ interface InheritedPaint {
210
+ readonly fill: PaintedFill | null;
211
+ readonly stroke: PaintedStroke | null;
212
+ }
213
+ /** Whether `node`'s fill or stroke is {@link INHERIT}, so painting it needs
214
+ * its containers' paint. */
215
+ declare function inheritsPaint(node: {
216
+ data?: unknown;
217
+ }): boolean;
218
+ /**
219
+ * {@link InheritedPaint} for `node`, given its ancestors innermost first. Only
220
+ * containers are sources; one whose own field is `'inherit'` passes the
221
+ * question on to the next, and running out of containers is none.
222
+ *
223
+ * Hands back the object it returned last time for `node` while both values
224
+ * are the same references, so a memo can key on it.
225
+ */
226
+ declare function inheritedPaintOf(node: {
227
+ data?: unknown;
228
+ }, ancestors: Iterable<{
229
+ kind?: string;
230
+ data?: unknown;
231
+ }>): InheritedPaint;
232
+ /** The fields {@link withInheritedPaint} resolves. */
233
+ type ResolvedPaintFields<T> = Omit<T, 'fill' | 'stroke'> & {
234
+ fill?: Exclude<T extends {
235
+ fill?: infer F;
236
+ } ? F : never, Inherit>;
237
+ stroke?: Exclude<T extends {
238
+ stroke?: infer S;
239
+ } ? S : never, Inherit>;
240
+ };
241
+ /**
242
+ * `data` with an `'inherit'` fill or stroke replaced by what `inherited` says
243
+ * it resolves to, for a reader that wants the paint a node shows rather than
244
+ * the one it declares. Hands `data` back as is when nothing inherits.
245
+ */
246
+ declare function withInheritedPaint<T extends {
247
+ fill?: unknown;
248
+ stroke?: unknown;
249
+ }>(data: T, inherited: InheritedPaint | undefined): ResolvedPaintFields<T>;
250
+
203
251
  /** The paint a path node wears, resolved: what an action that turns ink into
204
252
  * geometry reads from a node and tells a consumer to put on a result. */
205
253
  interface PathPaint {
@@ -209,13 +257,12 @@ interface PathPaint {
209
257
  stroke?: Stroke;
210
258
  }
211
259
  /** {@link PathPaint} of a node that keeps its paint in `data.fill` and
212
- * `data.stroke`, as the built-in path painter draws it: a stroke with no
213
- * `paint` is none, and an undeclared fill is the default shape fill unless
214
- * the node is stroked. */
260
+ * `data.stroke`, as the built-in path painter draws it: a stroke or a fill
261
+ * with no `paint` is none, and an `'inherit'` one is `inherited`'s. */
215
262
  declare function pathPaintOf(data: {
216
- fill?: Fill | null;
217
- stroke?: Stroke | null;
218
- }): PathPaint;
263
+ fill?: Fill | Inherit | null;
264
+ stroke?: Stroke | Inherit | null;
265
+ }, inherited?: InheritedPaint): PathPaint;
219
266
 
220
267
  /**
221
268
  * Create Outlines: replace each selected text node with path nodes carrying
@@ -1450,7 +1497,7 @@ type InsertNodeFactory = (bounds: {
1450
1497
  /**
1451
1498
  * The paint a kit-built shape is inserted with. `kind` is the shape being
1452
1499
  * built and `seq` counts this canvas's inserts from 1. A key left out stays
1453
- * off the node, so its painter's default applies; `null` is "none". A pencil
1500
+ * off the node, which paints none of it, the same as `null`. A pencil
1454
1501
  * trail too short to fit is built as a rect and asks as one. A line and an
1455
1502
  * open pen path are inserted unfilled: a fill given to either is dropped.
1456
1503
  * `fillRule` is the rule a path that carries one is built with, where the
@@ -1474,8 +1521,8 @@ interface TextNodeData {
1474
1521
  text: string;
1475
1522
  style?: TextStyle;
1476
1523
  runs?: readonly StyledRun[];
1477
- fill?: Fill | null;
1478
- stroke?: Stroke | null;
1524
+ fill?: Fill | Inherit | null;
1525
+ stroke?: Stroke | Inherit | null;
1479
1526
  verticalAlign?: TextVerticalAlign;
1480
1527
  /** Paint for the whole pose box, drawn under the glyphs. `fill` is the
1481
1528
  * glyphs' own paint, so a box behind them needs a key of its own. */
@@ -1508,11 +1555,11 @@ interface MinSize {
1508
1555
  declare function minSizeOf(data: unknown): MinSize | null;
1509
1556
 
1510
1557
  /** How a sticky note looks as it is made, named as the text node it becomes
1511
- * names them. Every field but `fill` has a default, in {@link STICKY_DEFAULTS}. */
1558
+ * names them. Every field has a default, in {@link STICKY_DEFAULTS}. */
1512
1559
  interface StickyStyle {
1513
1560
  /** The note itself, painted over its whole box. */
1514
1561
  background?: Fill;
1515
- /** The text's paint. Absent, the text style's default. */
1562
+ /** The text's paint. */
1516
1563
  fill?: Fill;
1517
1564
  /** World units from the note's edge to its text, on every side. */
1518
1565
  padding?: number;
@@ -1538,6 +1585,7 @@ declare const STICKY_DEFAULTS: {
1538
1585
  readonly color: "#fff1a6ff";
1539
1586
  };
1540
1587
  };
1588
+ readonly fill: _weasel_js_paint.PaintedFill;
1541
1589
  readonly padding: 12;
1542
1590
  readonly style: {
1543
1591
  readonly fontSize: 18;
@@ -2800,4 +2848,4 @@ declare module '@weasel-js/routing' {
2800
2848
  }
2801
2849
  }
2802
2850
 
2803
- export { type SnapCollect as $, type AnchorMarkerShape as A, type PathEditTarget as B, type ContentHandlerEntry as C, type Dims as D, type EditAnchorsDep as E, type FitViewToBoundsOptions as F, type GestureLifecycleOptions as G, type HandleLocks as H, type InertiaConfig as I, type FinishedEditFrame as J, type PathSymmetryDep as K, type LayerHit as L, type BooleanOpOptions as M, type BooleansAdapter as N, type BooleanOp as O, type PathEditMark as P, type PathPaint as Q, type RenderLayer as R, type SnapSource as S, type TransformFollower as T, type BooleanOpResult as U, type ViewAnimationOptions as V, type BooleanStrokes as W, type SnapMods as X, type SourceCandidate as Y, type SnapProbeAt as Z, _resetPaintKindsForTests as _, type PathEditExtension as a, type SnapEdges as a$, type SnapMark as a0, type SnapDep as a1, type SnapOutcome as a2, type SnapReadoutStyle as a3, type AngleCandidate as a4, type AngleCollect as a5, type AnimateToBoundsOptions as a6, type AreaSelectDep as a7, type BooleanOpPreview as a8, type BooleanPreviewPiece as a9, type NodeAtPointDep as aA, type OutlinePathSpec as aB, PATH_OVERLAY_ROLES as aC, type PaintKind as aD, type PaintKindEditorProps as aE, type PaintKindFamily as aF, type PaintKindLoader as aG, type PanBounds as aH, type PathEditDrag as aI, type PathEditDragStart as aJ, type PathEditFinishCtx as aK, type PathEditHandleMove as aL, type PathEditMarksCtx as aM, type PathEditOther as aN, type PathEditPart as aO, type PathEditStore as aP, type PathForm as aQ, type PathOverlay as aR, type PathOverlayOptions as aS, type PathOverlayStyle as aT, type PathRepresentation as aU, type PathTransformFinishCtx as aV, type PreviewPathsDep as aW, type ResizePolicy as aX, STICKY_DEFAULTS as aY, type SnapBegin as aZ, type SnapEdge as a_, type CanDropIntoDep as aa, type ClipboardDep as ab, type ClipboardIngestCtx as ac, type CreateOutlinesResult as ad, type CustomPaintContext as ae, DEFAULT_LAYER_RETAIN as af, type DecayLoopConfig as ag, type FinishedPathEdit as ah, GEOMETRY_BOUND_DATA as ai, type HandleLock as aj, type HitTestView as ak, type IngestCtx as al, type IngestionDep as am, type InsertDep as an, type InsertEnds as ao, type InsertExtras as ap, type InsertIntoDep as aq, type InsertLive as ar, type LassoSelectDep as as, type LayerCommandCache as at, type LayerDrawFailure as au, type LayerDrawSpan as av, type LayerRetainPolicy as aw, type LayoutDep as ax, type Locus as ay, type MinSize as az, type SnapGesture as b, type SnapMarks as b0, type SnapReadoutDep as b1, type SvgUnpacker as b2, type TextEditDep as b3, type TextNodeData as b4, type TransformPreviewNode as b5, type TransientPath as b6, VIEW_ANIMATION_KEY as b7, type ViewAnimationApi as b8, type ViewApi as b9, registerPaintKindLoader as bA, seedTile as bB, stickyData as bC, stickyNodeFactory as bD, subscribeToSources as bE, switchGradientKind as bF, textBoxOf as bG, useDecayLoop as bH, useViewAnimation as bI, useViewAnimationOn as bJ, viewAxes as bK, withoutGeometryBoundData as bL, type ViewChannel as ba, applyBooleanOp as bb, applyCreateOutlines as bc, asPaint as bd, createPathOverlay as be, createSnapOutcomeStore as bf, depsUnchanged as bg, drawLayers as bh, drawOneLayer as bi, fitViewToBounds as bj, getPaintKind as bk, isLayerPainted as bl, isLayerVisible as bm, listGradientKinds as bn, listPaintKinds as bo, minSizeOf as bp, paintKindOf as bq, paintKindRegistry as br, paintedLayers as bs, paintsLayer as bt, paletteInsertPaint as bu, pathPaintOf as bv, patternFamily as bw, previewBooleanOp as bx, registerContentHandler as by, registerPaintKind as bz, type SnapConstrain as c, type SnapResizeBox as d, type SnapGeometry as e, type SnapOutcomeStore as f, type SnapSession as g, type ViewportDims as h, type RedrawSource as i, type EditAnchorsParams as j, type SliceDep as k, type StickyStyle as l, type ChromeState as m, type LayerGroup as n, type GeometryProjection as o, type InsertNodeFactory as p, type InsertPaint as q, type SvgIngestOptions as r, type SceneCanvasSnap as s, type Affordance as t, type AffordanceRegion as u, type AffordanceBinding as v, type CommonAffordanceScratch as w, type TextOverflow as x, type CreateOutlinesAdapter as y, type PaintKindEntry as z };
2851
+ export { type SnapProbeAt as $, type AnchorMarkerShape as A, type PaintKindEntry as B, type ContentHandlerEntry as C, type Dims as D, type EditAnchorsDep as E, type FitViewToBoundsOptions as F, type GestureLifecycleOptions as G, type HandleLocks as H, type InheritedPaint as I, type PathEditTarget as J, type FinishedEditFrame as K, type LayerHit as L, type PathSymmetryDep as M, type BooleanOpOptions as N, type BooleansAdapter as O, type PathEditMark as P, type BooleanOp as Q, type RenderLayer as R, type SnapSource as S, type TransformFollower as T, type PathPaint as U, type ViewAnimationOptions as V, type BooleanOpResult as W, type BooleanStrokes as X, type SnapMods as Y, type SourceCandidate as Z, _resetPaintKindsForTests as _, type PathEditExtension as a, type SnapEdge as a$, type SnapCollect as a0, type SnapMark as a1, type SnapDep as a2, type SnapOutcome as a3, type SnapReadoutStyle as a4, type AngleCandidate as a5, type AngleCollect as a6, type AnimateToBoundsOptions as a7, type AreaSelectDep as a8, type BooleanOpPreview as a9, type MinSize as aA, type NodeAtPointDep as aB, type OutlinePathSpec as aC, PATH_OVERLAY_ROLES as aD, type PaintKind as aE, type PaintKindEditorProps as aF, type PaintKindFamily as aG, type PaintKindLoader as aH, type PanBounds as aI, type PathEditDrag as aJ, type PathEditDragStart as aK, type PathEditFinishCtx as aL, type PathEditHandleMove as aM, type PathEditMarksCtx as aN, type PathEditOther as aO, type PathEditPart as aP, type PathEditStore as aQ, type PathForm as aR, type PathOverlay as aS, type PathOverlayOptions as aT, type PathOverlayStyle as aU, type PathRepresentation as aV, type PathTransformFinishCtx as aW, type PreviewPathsDep as aX, type ResizePolicy as aY, STICKY_DEFAULTS as aZ, type SnapBegin as a_, type BooleanPreviewPiece as aa, type CanDropIntoDep as ab, type ClipboardDep as ac, type ClipboardIngestCtx as ad, type CreateOutlinesResult as ae, type CustomPaintContext as af, DEFAULT_LAYER_RETAIN as ag, type DecayLoopConfig as ah, type FinishedPathEdit as ai, GEOMETRY_BOUND_DATA as aj, type HandleLock as ak, type HitTestView as al, type IngestCtx as am, type IngestionDep as an, type InsertDep as ao, type InsertEnds as ap, type InsertExtras as aq, type InsertIntoDep as ar, type InsertLive as as, type LassoSelectDep as at, type LayerCommandCache as au, type LayerDrawFailure as av, type LayerDrawSpan as aw, type LayerRetainPolicy as ax, type LayoutDep as ay, type Locus as az, type SnapGesture as b, type SnapEdges as b0, type SnapMarks as b1, type SnapReadoutDep as b2, type SvgUnpacker as b3, type TextEditDep as b4, type TextNodeData as b5, type TransformPreviewNode as b6, type TransientPath as b7, VIEW_ANIMATION_KEY as b8, type ViewAnimationApi as b9, previewBooleanOp as bA, registerContentHandler as bB, registerPaintKind as bC, registerPaintKindLoader as bD, seedTile as bE, stickyData as bF, stickyNodeFactory as bG, subscribeToSources as bH, switchGradientKind as bI, textBoxOf as bJ, useDecayLoop as bK, useViewAnimation as bL, useViewAnimationOn as bM, viewAxes as bN, withInheritedPaint as bO, withoutGeometryBoundData as bP, type ViewApi as ba, type ViewChannel as bb, applyBooleanOp as bc, applyCreateOutlines as bd, asPaint as be, createPathOverlay as bf, createSnapOutcomeStore as bg, depsUnchanged as bh, drawLayers as bi, drawOneLayer as bj, fitViewToBounds as bk, getPaintKind as bl, inheritedPaintOf as bm, inheritsPaint as bn, isLayerPainted as bo, isLayerVisible as bp, listGradientKinds as bq, listPaintKinds as br, minSizeOf as bs, paintKindOf as bt, paintKindRegistry as bu, paintedLayers as bv, paintsLayer as bw, paletteInsertPaint as bx, pathPaintOf as by, patternFamily as bz, type SnapConstrain as c, type SnapResizeBox as d, type SnapGeometry as e, type SnapOutcomeStore as f, type SnapSession as g, type ViewportDims as h, type RedrawSource as i, type EditAnchorsParams as j, type InertiaConfig as k, type SliceDep as l, type StickyStyle as m, type ChromeState as n, type LayerGroup as o, type GeometryProjection as p, type InsertNodeFactory as q, type InsertPaint as r, type SvgIngestOptions as s, type SceneCanvasSnap as t, type Affordance as u, type AffordanceRegion as v, type AffordanceBinding as w, type CommonAffordanceScratch as x, type TextOverflow as y, type CreateOutlinesAdapter as z };
@@ -1,5 +1,5 @@
1
1
  import { serialize } from "./serialize.js";
2
- import { actionBindings, buildDepsFromRequires, evaluateActive, evaluateEnabled, routesForSpec } from "@weasel-js/routing";
2
+ import { actionBindings, buildDepsFromRequires, evaluateActive, evaluateEnabled, overriddenBindings, routesForSpec } from "@weasel-js/routing";
3
3
 
4
4
  //#region src/devtools/registry.ts
5
5
  /** What one canvas has registered — its tools, actions, deps and node kinds — as plain data for `get-registry`. */
@@ -18,7 +18,7 @@ function bindingOf(binding) {
18
18
  opts: serialize(binding.opts)
19
19
  };
20
20
  }
21
- function toolOf(entry, slot) {
21
+ function toolOf(entry, slot, overrides) {
22
22
  const p = entry.presentation;
23
23
  return {
24
24
  id: entry.id,
@@ -31,7 +31,7 @@ function toolOf(entry, slot) {
31
31
  hidden: p?.hide === true,
32
32
  cursor: serialize(entry.cursor),
33
33
  eligibility: serialize(entry.eligibility),
34
- bindings: (entry.bindings ?? []).map(bindingOf),
34
+ bindings: overriddenBindings(entry.bindings ?? [], entry.id, overrides).map(bindingOf),
35
35
  actions: (entry.actions ?? []).map((a) => a.id),
36
36
  deps: Object.keys(entry.deps ?? {}),
37
37
  hasOverlay: entry.overlay !== void 0
@@ -71,7 +71,7 @@ function describeRegistry(inputs) {
71
71
  for (const e of entries) {
72
72
  if (seen.has(e.id)) continue;
73
73
  seen.add(e.id);
74
- tools.push(toolOf(e, slot));
74
+ tools.push(toolOf(e, slot, inputs.overrides));
75
75
  }
76
76
  };
77
77
  add(Object.values(inputs.tools.registry), "tool");
@@ -1 +1 @@
1
- {"version":3,"file":"registry.js","names":[],"sources":["../../src/devtools/registry.ts"],"sourcesContent":["/** What one canvas has registered — its tools, actions, deps and node kinds — as plain data for `get-registry`. */\nimport {\n actionBindings, buildDepsFromRequires, evaluateActive, evaluateEnabled, routesForSpec,\n type Action, type ActionSource, type Contribution, type DepRegistry, type GestureBinding,\n} from '@weasel-js/routing';\nimport type {\n DevtoolsActionEntry, DevtoolsBinding, DevtoolsRegistry, DevtoolsToolEntry,\n} from './protocol';\nimport { serialize } from './serialize';\n\ntype Entry = Contribution<unknown> & { cursor?: unknown };\n\n/** The live registries {@link describeRegistry} reads. */\nexport interface RegistryInputs {\n tools: { registry: Readonly<Record<string, Entry>>; ambient: readonly Entry[] };\n /** The entries a `features` preset brought beside the tools. */\n contributions: readonly Entry[];\n actions: ActionSource | null;\n deps: DepRegistry | null;\n routingKinds: readonly string[];\n}\n\nfunction routesOf(binding: GestureBinding): string[] {\n try {\n return [...routesForSpec(binding.spec)];\n } catch {\n return [];\n }\n}\n\nfunction bindingOf(binding: GestureBinding): DevtoolsBinding {\n return {\n actionId: binding.actionId,\n spec: serialize(binding.spec),\n routes: routesOf(binding),\n opts: serialize(binding.opts),\n };\n}\n\nfunction toolOf(entry: Entry, slot: DevtoolsToolEntry['slot']): DevtoolsToolEntry {\n const p = entry.presentation;\n return {\n id: entry.id,\n slot,\n label: p?.label ?? entry.id,\n icon: p?.icon ?? null,\n group: p?.group ?? null,\n shortcut: p?.shortcut ?? null,\n hotkey: entry.eligibility?.offhand ?? null,\n hidden: p?.hide === true,\n cursor: serialize(entry.cursor),\n eligibility: serialize(entry.eligibility),\n bindings: (entry.bindings ?? []).map(bindingOf),\n actions: (entry.actions ?? []).map((a) => a.id),\n deps: Object.keys(entry.deps ?? {}),\n hasOverlay: entry.overlay !== undefined,\n };\n}\n\nfunction actionOf(action: Action, deps: DepRegistry | null): DevtoolsActionEntry {\n const bag = deps ? buildDepsFromRequires(action, deps) : undefined;\n const enabled = evaluateEnabled(action, bag);\n const rule: unknown = action.eligible;\n return {\n id: action.id,\n label: action.label ?? action.id,\n icon: action.icon ?? null,\n group: action.group ?? null,\n tags: [...(action.tags ?? [])],\n shortcut: action.shortcut ?? null,\n scope: action.scope === 'hotkey' ? 'hotkey' : 'ambient',\n invoker: action.invoker?.timing ?? null,\n requires: [...(action.requires ?? [])],\n variants: (action.variants ?? []).map((v) => ({\n key: v.key, label: v.label, icon: v.icon ?? null, params: serialize(v.params),\n })),\n enabled: enabled.enabled ? true : (enabled.reason ?? 'disabled'),\n active: evaluateActive(action, bag) ?? null,\n hasPreview: action.preview !== undefined,\n // A fluent condition is callable and carries its rule tree on `.rule`.\n eligible: serialize(typeof rule === 'function' ? (rule as { rule?: unknown }).rule : (rule ?? null)),\n bindings: actionBindings(action).map(bindingOf),\n };\n}\n\nexport function describeRegistry(inputs: RegistryInputs): DevtoolsRegistry {\n const seen = new Set<string>();\n const tools: DevtoolsToolEntry[] = [];\n const add = (entries: readonly Entry[], slot: DevtoolsToolEntry['slot']): void => {\n for (const e of entries) {\n if (seen.has(e.id)) continue;\n seen.add(e.id);\n tools.push(toolOf(e, slot));\n }\n };\n add(Object.values(inputs.tools.registry), 'tool');\n add(inputs.tools.ambient, 'ambient');\n add(inputs.contributions, 'contribution');\n return {\n tools,\n actions: (inputs.actions?.list() ?? []).map((a) => actionOf(a, inputs.deps)),\n deps: (inputs.deps?.list?.() ?? []).map((d) => ({ ...d })),\n routingKinds: [...inputs.routingKinds],\n };\n}\n"],"mappings":";;;;;AAsBA,SAAS,SAAS,SAAmC;CACnD,IAAI;EACF,OAAO,CAAC,GAAG,cAAc,QAAQ,IAAI,CAAC;CACxC,QAAQ;EACN,OAAO,CAAC;CACV;AACF;AAEA,SAAS,UAAU,SAA0C;CAC3D,OAAO;EACL,UAAU,QAAQ;EAClB,MAAM,UAAU,QAAQ,IAAI;EAC5B,QAAQ,SAAS,OAAO;EACxB,MAAM,UAAU,QAAQ,IAAI;CAC9B;AACF;AAEA,SAAS,OAAO,OAAc,MAAoD;CAChF,MAAM,IAAI,MAAM;CAChB,OAAO;EACL,IAAI,MAAM;EACV;EACA,OAAO,GAAG,SAAS,MAAM;EACzB,MAAM,GAAG,QAAQ;EACjB,OAAO,GAAG,SAAS;EACnB,UAAU,GAAG,YAAY;EACzB,QAAQ,MAAM,aAAa,WAAW;EACtC,QAAQ,GAAG,SAAS;EACpB,QAAQ,UAAU,MAAM,MAAM;EAC9B,aAAa,UAAU,MAAM,WAAW;EACxC,WAAW,MAAM,YAAY,CAAC,EAAA,CAAG,IAAI,SAAS;EAC9C,UAAU,MAAM,WAAW,CAAC,EAAA,CAAG,KAAK,MAAM,EAAE,EAAE;EAC9C,MAAM,OAAO,KAAK,MAAM,QAAQ,CAAC,CAAC;EAClC,YAAY,MAAM,YAAY;CAChC;AACF;AAEA,SAAS,SAAS,QAAgB,MAA+C;CAC/E,MAAM,MAAM,OAAO,sBAAsB,QAAQ,IAAI,IAAI;CACzD,MAAM,UAAU,gBAAgB,QAAQ,GAAG;CAC3C,MAAM,OAAgB,OAAO;CAC7B,OAAO;EACL,IAAI,OAAO;EACX,OAAO,OAAO,SAAS,OAAO;EAC9B,MAAM,OAAO,QAAQ;EACrB,OAAO,OAAO,SAAS;EACvB,MAAM,CAAC,GAAI,OAAO,QAAQ,CAAC,CAAE;EAC7B,UAAU,OAAO,YAAY;EAC7B,OAAO,OAAO,UAAU,WAAW,WAAW;EAC9C,SAAS,OAAO,SAAS,UAAU;EACnC,UAAU,CAAC,GAAI,OAAO,YAAY,CAAC,CAAE;EACrC,WAAW,OAAO,YAAY,CAAC,EAAA,CAAG,KAAK,OAAO;GAC5C,KAAK,EAAE;GAAK,OAAO,EAAE;GAAO,MAAM,EAAE,QAAQ;GAAM,QAAQ,UAAU,EAAE,MAAM;EAC9E,EAAE;EACF,SAAS,QAAQ,UAAU,OAAQ,QAAQ,UAAU;EACrD,QAAQ,eAAe,QAAQ,GAAG,KAAK;EACvC,YAAY,OAAO,YAAY;EAE/B,UAAU,UAAU,OAAO,SAAS,aAAc,KAA4B,OAAQ,QAAQ,IAAK;EACnG,UAAU,eAAe,MAAM,CAAC,CAAC,IAAI,SAAS;CAChD;AACF;AAEA,SAAgB,iBAAiB,QAA0C;CACzE,MAAM,uBAAO,IAAI,IAAY;CAC7B,MAAM,QAA6B,CAAC;CACpC,MAAM,OAAO,SAA2B,SAA0C;EAChF,KAAK,MAAM,KAAK,SAAS;GACvB,IAAI,KAAK,IAAI,EAAE,EAAE,GAAG;GACpB,KAAK,IAAI,EAAE,EAAE;GACb,MAAM,KAAK,OAAO,GAAG,IAAI,CAAC;EAC5B;CACF;CACA,IAAI,OAAO,OAAO,OAAO,MAAM,QAAQ,GAAG,MAAM;CAChD,IAAI,OAAO,MAAM,SAAS,SAAS;CACnC,IAAI,OAAO,eAAe,cAAc;CACxC,OAAO;EACL;EACA,UAAU,OAAO,SAAS,KAAK,KAAK,CAAC,EAAA,CAAG,KAAK,MAAM,SAAS,GAAG,OAAO,IAAI,CAAC;EAC3E,OAAO,OAAO,MAAM,OAAO,KAAK,CAAC,EAAA,CAAG,KAAK,OAAO,EAAE,GAAG,EAAE,EAAE;EACzD,cAAc,CAAC,GAAG,OAAO,YAAY;CACvC;AACF"}
1
+ {"version":3,"file":"registry.js","names":[],"sources":["../../src/devtools/registry.ts"],"sourcesContent":["/** What one canvas has registered — its tools, actions, deps and node kinds — as plain data for `get-registry`. */\nimport {\n actionBindings, buildDepsFromRequires, evaluateActive, evaluateEnabled, overriddenBindings, routesForSpec,\n type Action, type ActionSource, type Contribution, type DepRegistry, type GestureBinding, type ResolvedBindingOverrides,\n} from '@weasel-js/routing';\nimport type {\n DevtoolsActionEntry, DevtoolsBinding, DevtoolsRegistry, DevtoolsToolEntry,\n} from './protocol';\nimport { serialize } from './serialize';\n\ntype Entry = Contribution<unknown> & { cursor?: unknown };\n\n/** The live registries {@link describeRegistry} reads. */\nexport interface RegistryInputs {\n tools: { registry: Readonly<Record<string, Entry>>; ambient: readonly Entry[] };\n /** The entries a `features` preset brought beside the tools. */\n contributions: readonly Entry[];\n actions: ActionSource | null;\n /** Binding overrides in force, applied to the entries' bindings; `actions`\n * lists its own already overridden. */\n overrides?: ResolvedBindingOverrides;\n deps: DepRegistry | null;\n routingKinds: readonly string[];\n}\n\nfunction routesOf(binding: GestureBinding): string[] {\n try {\n return [...routesForSpec(binding.spec)];\n } catch {\n return [];\n }\n}\n\nfunction bindingOf(binding: GestureBinding): DevtoolsBinding {\n return {\n actionId: binding.actionId,\n spec: serialize(binding.spec),\n routes: routesOf(binding),\n opts: serialize(binding.opts),\n };\n}\n\nfunction toolOf(entry: Entry, slot: DevtoolsToolEntry['slot'], overrides: ResolvedBindingOverrides | undefined): DevtoolsToolEntry {\n const p = entry.presentation;\n return {\n id: entry.id,\n slot,\n label: p?.label ?? entry.id,\n icon: p?.icon ?? null,\n group: p?.group ?? null,\n shortcut: p?.shortcut ?? null,\n hotkey: entry.eligibility?.offhand ?? null,\n hidden: p?.hide === true,\n cursor: serialize(entry.cursor),\n eligibility: serialize(entry.eligibility),\n bindings: overriddenBindings(entry.bindings ?? [], entry.id, overrides).map(bindingOf),\n actions: (entry.actions ?? []).map((a) => a.id),\n deps: Object.keys(entry.deps ?? {}),\n hasOverlay: entry.overlay !== undefined,\n };\n}\n\nfunction actionOf(action: Action, deps: DepRegistry | null): DevtoolsActionEntry {\n const bag = deps ? buildDepsFromRequires(action, deps) : undefined;\n const enabled = evaluateEnabled(action, bag);\n const rule: unknown = action.eligible;\n return {\n id: action.id,\n label: action.label ?? action.id,\n icon: action.icon ?? null,\n group: action.group ?? null,\n tags: [...(action.tags ?? [])],\n shortcut: action.shortcut ?? null,\n scope: action.scope === 'hotkey' ? 'hotkey' : 'ambient',\n invoker: action.invoker?.timing ?? null,\n requires: [...(action.requires ?? [])],\n variants: (action.variants ?? []).map((v) => ({\n key: v.key, label: v.label, icon: v.icon ?? null, params: serialize(v.params),\n })),\n enabled: enabled.enabled ? true : (enabled.reason ?? 'disabled'),\n active: evaluateActive(action, bag) ?? null,\n hasPreview: action.preview !== undefined,\n // A fluent condition is callable and carries its rule tree on `.rule`.\n eligible: serialize(typeof rule === 'function' ? (rule as { rule?: unknown }).rule : (rule ?? null)),\n bindings: actionBindings(action).map(bindingOf),\n };\n}\n\nexport function describeRegistry(inputs: RegistryInputs): DevtoolsRegistry {\n const seen = new Set<string>();\n const tools: DevtoolsToolEntry[] = [];\n const add = (entries: readonly Entry[], slot: DevtoolsToolEntry['slot']): void => {\n for (const e of entries) {\n if (seen.has(e.id)) continue;\n seen.add(e.id);\n tools.push(toolOf(e, slot, inputs.overrides));\n }\n };\n add(Object.values(inputs.tools.registry), 'tool');\n add(inputs.tools.ambient, 'ambient');\n add(inputs.contributions, 'contribution');\n return {\n tools,\n actions: (inputs.actions?.list() ?? []).map((a) => actionOf(a, inputs.deps)),\n deps: (inputs.deps?.list?.() ?? []).map((d) => ({ ...d })),\n routingKinds: [...inputs.routingKinds],\n };\n}\n"],"mappings":";;;;;AAyBA,SAAS,SAAS,SAAmC;CACnD,IAAI;EACF,OAAO,CAAC,GAAG,cAAc,QAAQ,IAAI,CAAC;CACxC,QAAQ;EACN,OAAO,CAAC;CACV;AACF;AAEA,SAAS,UAAU,SAA0C;CAC3D,OAAO;EACL,UAAU,QAAQ;EAClB,MAAM,UAAU,QAAQ,IAAI;EAC5B,QAAQ,SAAS,OAAO;EACxB,MAAM,UAAU,QAAQ,IAAI;CAC9B;AACF;AAEA,SAAS,OAAO,OAAc,MAAiC,WAAoE;CACjI,MAAM,IAAI,MAAM;CAChB,OAAO;EACL,IAAI,MAAM;EACV;EACA,OAAO,GAAG,SAAS,MAAM;EACzB,MAAM,GAAG,QAAQ;EACjB,OAAO,GAAG,SAAS;EACnB,UAAU,GAAG,YAAY;EACzB,QAAQ,MAAM,aAAa,WAAW;EACtC,QAAQ,GAAG,SAAS;EACpB,QAAQ,UAAU,MAAM,MAAM;EAC9B,aAAa,UAAU,MAAM,WAAW;EACxC,UAAU,mBAAmB,MAAM,YAAY,CAAC,GAAG,MAAM,IAAI,SAAS,CAAC,CAAC,IAAI,SAAS;EACrF,UAAU,MAAM,WAAW,CAAC,EAAA,CAAG,KAAK,MAAM,EAAE,EAAE;EAC9C,MAAM,OAAO,KAAK,MAAM,QAAQ,CAAC,CAAC;EAClC,YAAY,MAAM,YAAY;CAChC;AACF;AAEA,SAAS,SAAS,QAAgB,MAA+C;CAC/E,MAAM,MAAM,OAAO,sBAAsB,QAAQ,IAAI,IAAI;CACzD,MAAM,UAAU,gBAAgB,QAAQ,GAAG;CAC3C,MAAM,OAAgB,OAAO;CAC7B,OAAO;EACL,IAAI,OAAO;EACX,OAAO,OAAO,SAAS,OAAO;EAC9B,MAAM,OAAO,QAAQ;EACrB,OAAO,OAAO,SAAS;EACvB,MAAM,CAAC,GAAI,OAAO,QAAQ,CAAC,CAAE;EAC7B,UAAU,OAAO,YAAY;EAC7B,OAAO,OAAO,UAAU,WAAW,WAAW;EAC9C,SAAS,OAAO,SAAS,UAAU;EACnC,UAAU,CAAC,GAAI,OAAO,YAAY,CAAC,CAAE;EACrC,WAAW,OAAO,YAAY,CAAC,EAAA,CAAG,KAAK,OAAO;GAC5C,KAAK,EAAE;GAAK,OAAO,EAAE;GAAO,MAAM,EAAE,QAAQ;GAAM,QAAQ,UAAU,EAAE,MAAM;EAC9E,EAAE;EACF,SAAS,QAAQ,UAAU,OAAQ,QAAQ,UAAU;EACrD,QAAQ,eAAe,QAAQ,GAAG,KAAK;EACvC,YAAY,OAAO,YAAY;EAE/B,UAAU,UAAU,OAAO,SAAS,aAAc,KAA4B,OAAQ,QAAQ,IAAK;EACnG,UAAU,eAAe,MAAM,CAAC,CAAC,IAAI,SAAS;CAChD;AACF;AAEA,SAAgB,iBAAiB,QAA0C;CACzE,MAAM,uBAAO,IAAI,IAAY;CAC7B,MAAM,QAA6B,CAAC;CACpC,MAAM,OAAO,SAA2B,SAA0C;EAChF,KAAK,MAAM,KAAK,SAAS;GACvB,IAAI,KAAK,IAAI,EAAE,EAAE,GAAG;GACpB,KAAK,IAAI,EAAE,EAAE;GACb,MAAM,KAAK,OAAO,GAAG,MAAM,OAAO,SAAS,CAAC;EAC9C;CACF;CACA,IAAI,OAAO,OAAO,OAAO,MAAM,QAAQ,GAAG,MAAM;CAChD,IAAI,OAAO,MAAM,SAAS,SAAS;CACnC,IAAI,OAAO,eAAe,cAAc;CACxC,OAAO;EACL;EACA,UAAU,OAAO,SAAS,KAAK,KAAK,CAAC,EAAA,CAAG,KAAK,MAAM,SAAS,GAAG,OAAO,IAAI,CAAC;EAC3E,OAAO,OAAO,MAAM,OAAO,KAAK,CAAC,EAAA,CAAG,KAAK,OAAO,EAAE,GAAG,EAAE,EAAE;EACzD,cAAc,CAAC,GAAG,OAAO,YAAY;CACvC;AACF"}
@@ -40,14 +40,16 @@ function measurementsContribution(scene, ms) {
40
40
  target: { kindOf: onMeasurement },
41
41
  mods: MODS_ANY
42
42
  },
43
- actionId: MEASURE_ACTION_IDS.pick
43
+ actionId: MEASURE_ACTION_IDS.pick,
44
+ opts: { id: "weasel-measure.click" }
44
45
  }, {
45
46
  spec: {
46
47
  kind: "drag",
47
48
  target: { kindOf: onMeasurement },
48
49
  mods: MODS_ANY
49
50
  },
50
- actionId: MEASURE_ACTION_IDS.drag
51
+ actionId: MEASURE_ACTION_IDS.drag,
52
+ opts: { id: "weasel-measure.drag" }
51
53
  }]
52
54
  };
53
55
  }
@@ -1 +1 @@
1
- {"version":3,"file":"measureContribution.js","names":[],"sources":["../../../src/features/measure/measureContribution.ts"],"sourcesContent":["import { hitPredicate } from '@weasel-js/gestures';\nimport type { Scene } from 'core/scene/types';\nimport type { SurfaceContribution } from 'canvas/surfaceContribution';\nimport type { Measurements } from './measurements';\nimport { measureCommands } from './measureCommands';\nimport { measureDragAction, measurePickAction } from './measureGestures';\nimport { MEASURE_AFFORDANCE_KIND, measurementsLayer } from './measureLayer';\nimport { MEASURE_ACTION_IDS } from './actionIds';\n\nconst onMeasurement = hitPredicate((hit) => (hit as { kind?: string } | null | undefined)?.kind === MEASURE_AFFORDANCE_KIND);\n\nconst MODS_ANY = { shift: 'optional', alt: 'optional', meta: 'optional', ctrl: 'optional' } as const;\n\n/**\n * A scene's measurements on a canvas, whatever tool is active: the layer\n * that shows the picked one and claims presses on measurements, a click on\n * one to pick it and a drag to edit it, and the commands — Move by, Rotate\n * by, Delete, Clear — a readout binds buttons to. Install it with\n * `<SceneCanvas ambient={[…]}>`; `useMeasureTool` adds the measuring itself.\n */\nexport function measurementsContribution(\n scene: Scene<unknown, string, unknown>,\n ms: Measurements,\n): SurfaceContribution {\n const layer = measurementsLayer(scene, ms);\n return {\n id: 'weasel-measure',\n eligibility: { claimed: true },\n attach: (api) => {\n const off = api.registerLayer(layer);\n api.requestRedraw();\n return off;\n },\n actions: [...measureCommands(ms), measurePickAction(ms), measureDragAction(ms)],\n bindings: [\n { spec: { kind: 'click', target: { kindOf: onMeasurement }, mods: MODS_ANY }, actionId: MEASURE_ACTION_IDS.pick },\n { spec: { kind: 'drag', target: { kindOf: onMeasurement }, mods: MODS_ANY }, actionId: MEASURE_ACTION_IDS.drag },\n ],\n };\n}\n"],"mappings":";;;;;;;AASA,IAAM,gBAAgB,cAAc,QAAS,KAA8C,SAAS,uBAAuB;AAE3H,IAAM,WAAW;CAAE,OAAO;CAAY,KAAK;CAAY,MAAM;CAAY,MAAM;AAAW;;;;;;;;AAS1F,SAAgB,yBACd,OACA,IACqB;CACrB,MAAM,QAAQ,kBAAkB,OAAO,EAAE;CACzC,OAAO;EACL,IAAI;EACJ,aAAa,EAAE,SAAS,KAAK;EAC7B,SAAS,QAAQ;GACf,MAAM,MAAM,IAAI,cAAc,KAAK;GACnC,IAAI,cAAc;GAClB,OAAO;EACT;EACA,SAAS;GAAC,GAAG,gBAAgB,EAAE;GAAG,kBAAkB,EAAE;GAAG,kBAAkB,EAAE;EAAC;EAC9E,UAAU,CACR;GAAE,MAAM;IAAE,MAAM;IAAS,QAAQ,EAAE,QAAQ,cAAc;IAAG,MAAM;GAAS;GAAG,UAAU,mBAAmB;EAAK,GAChH;GAAE,MAAM;IAAE,MAAM;IAAQ,QAAQ,EAAE,QAAQ,cAAc;IAAG,MAAM;GAAS;GAAG,UAAU,mBAAmB;EAAK,CACjH;CACF;AACF"}
1
+ {"version":3,"file":"measureContribution.js","names":[],"sources":["../../../src/features/measure/measureContribution.ts"],"sourcesContent":["import { hitPredicate } from '@weasel-js/gestures';\nimport type { Scene } from 'core/scene/types';\nimport type { SurfaceContribution } from 'canvas/surfaceContribution';\nimport type { Measurements } from './measurements';\nimport { measureCommands } from './measureCommands';\nimport { measureDragAction, measurePickAction } from './measureGestures';\nimport { MEASURE_AFFORDANCE_KIND, measurementsLayer } from './measureLayer';\nimport { MEASURE_ACTION_IDS } from './actionIds';\n\nconst onMeasurement = hitPredicate((hit) => (hit as { kind?: string } | null | undefined)?.kind === MEASURE_AFFORDANCE_KIND);\n\nconst MODS_ANY = { shift: 'optional', alt: 'optional', meta: 'optional', ctrl: 'optional' } as const;\n\n/**\n * A scene's measurements on a canvas, whatever tool is active: the layer\n * that shows the picked one and claims presses on measurements, a click on\n * one to pick it and a drag to edit it, and the commands — Move by, Rotate\n * by, Delete, Clear — a readout binds buttons to. Install it with\n * `<SceneCanvas ambient={[…]}>`; `useMeasureTool` adds the measuring itself.\n */\nexport function measurementsContribution(\n scene: Scene<unknown, string, unknown>,\n ms: Measurements,\n): SurfaceContribution {\n const layer = measurementsLayer(scene, ms);\n return {\n id: 'weasel-measure',\n eligibility: { claimed: true },\n attach: (api) => {\n const off = api.registerLayer(layer);\n api.requestRedraw();\n return off;\n },\n actions: [...measureCommands(ms), measurePickAction(ms), measureDragAction(ms)],\n bindings: [\n { spec: { kind: 'click', target: { kindOf: onMeasurement }, mods: MODS_ANY }, actionId: MEASURE_ACTION_IDS.pick, opts: { id: 'weasel-measure.click' } },\n { spec: { kind: 'drag', target: { kindOf: onMeasurement }, mods: MODS_ANY }, actionId: MEASURE_ACTION_IDS.drag, opts: { id: 'weasel-measure.drag' } },\n ],\n };\n}\n"],"mappings":";;;;;;;AASA,IAAM,gBAAgB,cAAc,QAAS,KAA8C,SAAS,uBAAuB;AAE3H,IAAM,WAAW;CAAE,OAAO;CAAY,KAAK;CAAY,MAAM;CAAY,MAAM;AAAW;;;;;;;;AAS1F,SAAgB,yBACd,OACA,IACqB;CACrB,MAAM,QAAQ,kBAAkB,OAAO,EAAE;CACzC,OAAO;EACL,IAAI;EACJ,aAAa,EAAE,SAAS,KAAK;EAC7B,SAAS,QAAQ;GACf,MAAM,MAAM,IAAI,cAAc,KAAK;GACnC,IAAI,cAAc;GAClB,OAAO;EACT;EACA,SAAS;GAAC,GAAG,gBAAgB,EAAE;GAAG,kBAAkB,EAAE;GAAG,kBAAkB,EAAE;EAAC;EAC9E,UAAU,CACR;GAAE,MAAM;IAAE,MAAM;IAAS,QAAQ,EAAE,QAAQ,cAAc;IAAG,MAAM;GAAS;GAAG,UAAU,mBAAmB;GAAM,MAAM,EAAE,IAAI,uBAAuB;EAAE,GACtJ;GAAE,MAAM;IAAE,MAAM;IAAQ,QAAQ,EAAE,QAAQ,cAAc;IAAG,MAAM;GAAS;GAAG,UAAU,mBAAmB;GAAM,MAAM,EAAE,IAAI,sBAAsB;EAAE,CACtJ;CACF;AACF"}
@@ -38,12 +38,16 @@ function triggerAction(mode, targets, enter, canEnter) {
38
38
  if (id === void 0) return null;
39
39
  return canEnter?.(mode.id, id) === false ? null : id;
40
40
  };
41
+ const id = `mode.${mode.id}.trigger`;
41
42
  return {
42
- id: `mode.${mode.id}.trigger`,
43
+ id,
43
44
  label: `Enter ${modeLabel(mode)}`,
44
45
  defaultBinding: targets.map((kind) => ({
45
- kind: "doubleClick",
46
- target: `kind:${kind}`
46
+ spec: {
47
+ kind: "doubleClick",
48
+ target: `kind:${kind}`
49
+ },
50
+ opts: { id: `${id}.doubleClick.${kind}` }
47
51
  })),
48
52
  requires: ["hitAt"],
49
53
  enabled: (deps, at) => targetAt(deps, at) !== null ? true : ActionDisabledReason.NotApplicable,
@@ -1 +1 @@
1
- {"version":3,"file":"modeTriggers.js","names":[],"sources":["../../../src/features/modes/modeTriggers.ts"],"sourcesContent":["import { modeLabel, type ModeDefinition, type ModeRegistry } from '@weasel-js/modes';\nimport { ActionDisabledReason, type Action, type ActionDeps, type Contribution } from '@weasel-js/routing';\n\n/** What a mode's double-click trigger does. */\nexport interface ModeTriggerHandlers {\n /** Enter mode `id` on the node `targetId`. Without it nothing is bound. */\n enter?: (id: string, targetId: string) => void;\n /**\n * Whether mode `id` can be entered on `targetId` now. Where it says no the double-click falls\n * through to the next binding on it, so a canvas action that already enters the mode on some\n * of those nodes keeps them: `enterPathEdit` on a path with anchors, say. Default yes.\n */\n canEnter?: (id: string, targetId: string) => boolean;\n}\n\n/**\n * The registry's double-click entries as an always-on contribution: for each mode whose\n * `entry.trigger` is `'double-click-target'`, one action bound to a double-click on each routing\n * kind its `entry.targets` names, entering the mode on the node double-clicked. Install it with\n * `<SceneCanvas ambient>`, beside `modeShortcuts`.\n *\n * The target is the node the dispatcher classified the double-click on (the `hitAt` dep's\n * `doubleClick`, else its hit), the one the binding's `kind:` target matched: a double-click on\n * a grouped member names its outermost group inside `<SceneCanvas isolatedGroup>`. The action is live in every mode, the target mode\n * included, so a double-click on another node moves a mode onto it; `canEnter` and `enter`\n * decide what a switch from the current mode means.\n *\n * The bindings ride the ambient tier: a tool that claims the double-click itself, the pen\n * finishing a stroke, keeps it.\n */\nexport function modeTriggers(registry: ModeRegistry, handlers: ModeTriggerHandlers): Contribution<never> {\n const actions: Action[] = [];\n const { enter } = handlers;\n if (enter) {\n for (const mode of registry.list()) {\n const targets = mode.entry?.trigger === 'double-click-target' ? mode.entry.targets ?? [] : [];\n if (targets.length > 0) actions.push(triggerAction(mode, targets, enter, handlers.canEnter));\n }\n }\n return { id: 'modes.triggers', eligibility: { always: true }, actions };\n}\n\nfunction triggerAction(\n mode: ModeDefinition,\n targets: readonly string[],\n enter: (id: string, targetId: string) => void,\n canEnter: ModeTriggerHandlers['canEnter'],\n): Action {\n const targetAt = (deps: ActionDeps | undefined, at: { x: number; y: number } | undefined): string | null => {\n if (!at) return null;\n const hit = deps?.hitAt?.(at);\n const id = (hit?.doubleClick ?? hit)?.id;\n if (id === undefined) return null;\n return canEnter?.(mode.id, id) === false ? null : id;\n };\n return {\n id: `mode.${mode.id}.trigger`,\n label: `Enter ${modeLabel(mode)}`,\n defaultBinding: targets.map((kind) => ({ kind: 'doubleClick' as const, target: `kind:${kind}` as const })),\n requires: ['hitAt'],\n enabled: (deps, at) => (targetAt(deps, at) !== null ? true : ActionDisabledReason.NotApplicable),\n invoker: {\n timing: 'immediate',\n run: (deps, params) => {\n const x = params?.worldX, y = params?.worldY;\n const id = typeof x === 'number' && typeof y === 'number' ? targetAt(deps, { x, y }) : null;\n if (id !== null) enter(mode.id, id);\n },\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AA8BA,SAAgB,aAAa,UAAwB,UAAoD;CACvG,MAAM,UAAoB,CAAC;CAC3B,MAAM,EAAE,UAAU;CAClB,IAAI,OACF,KAAK,MAAM,QAAQ,SAAS,KAAK,GAAG;EAClC,MAAM,UAAU,KAAK,OAAO,YAAY,wBAAwB,KAAK,MAAM,WAAW,CAAC,IAAI,CAAC;EAC5F,IAAI,QAAQ,SAAS,GAAG,QAAQ,KAAK,cAAc,MAAM,SAAS,OAAO,SAAS,QAAQ,CAAC;CAC7F;CAEF,OAAO;EAAE,IAAI;EAAkB,aAAa,EAAE,QAAQ,KAAK;EAAG;CAAQ;AACxE;AAEA,SAAS,cACP,MACA,SACA,OACA,UACQ;CACR,MAAM,YAAY,MAA8B,OAA4D;EAC1G,IAAI,CAAC,IAAI,OAAO;EAChB,MAAM,MAAM,MAAM,QAAQ,EAAE;EAC5B,MAAM,MAAM,KAAK,eAAe,IAAA,EAAM;EACtC,IAAI,OAAO,QAAW,OAAO;EAC7B,OAAO,WAAW,KAAK,IAAI,EAAE,MAAM,QAAQ,OAAO;CACpD;CACA,OAAO;EACL,IAAI,QAAQ,KAAK,GAAG;EACpB,OAAO,SAAS,UAAU,IAAI;EAC9B,gBAAgB,QAAQ,KAAK,UAAU;GAAE,MAAM;GAAwB,QAAQ,QAAQ;EAAgB,EAAE;EACzG,UAAU,CAAC,OAAO;EAClB,UAAU,MAAM,OAAQ,SAAS,MAAM,EAAE,MAAM,OAAO,OAAO,qBAAqB;EAClF,SAAS;GACP,QAAQ;GACR,MAAM,MAAM,WAAW;IACrB,MAAM,IAAI,QAAQ,QAAQ,IAAI,QAAQ;IACtC,MAAM,KAAK,OAAO,MAAM,YAAY,OAAO,MAAM,WAAW,SAAS,MAAM;KAAE;KAAG;IAAE,CAAC,IAAI;IACvF,IAAI,OAAO,MAAM,MAAM,KAAK,IAAI,EAAE;GACpC;EACF;CACF;AACF"}
1
+ {"version":3,"file":"modeTriggers.js","names":[],"sources":["../../../src/features/modes/modeTriggers.ts"],"sourcesContent":["import { modeLabel, type ModeDefinition, type ModeRegistry } from '@weasel-js/modes';\nimport { ActionDisabledReason, type Action, type ActionDeps, type Contribution } from '@weasel-js/routing';\n\n/** What a mode's double-click trigger does. */\nexport interface ModeTriggerHandlers {\n /** Enter mode `id` on the node `targetId`. Without it nothing is bound. */\n enter?: (id: string, targetId: string) => void;\n /**\n * Whether mode `id` can be entered on `targetId` now. Where it says no the double-click falls\n * through to the next binding on it, so a canvas action that already enters the mode on some\n * of those nodes keeps them: `enterPathEdit` on a path with anchors, say. Default yes.\n */\n canEnter?: (id: string, targetId: string) => boolean;\n}\n\n/**\n * The registry's double-click entries as an always-on contribution: for each mode whose\n * `entry.trigger` is `'double-click-target'`, one action bound to a double-click on each routing\n * kind its `entry.targets` names, entering the mode on the node double-clicked. Install it with\n * `<SceneCanvas ambient>`, beside `modeShortcuts`.\n *\n * The target is the node the dispatcher classified the double-click on (the `hitAt` dep's\n * `doubleClick`, else its hit), the one the binding's `kind:` target matched: a double-click on\n * a grouped member names its outermost group inside `<SceneCanvas isolatedGroup>`. The action is live in every mode, the target mode\n * included, so a double-click on another node moves a mode onto it; `canEnter` and `enter`\n * decide what a switch from the current mode means.\n *\n * The bindings ride the ambient tier: a tool that claims the double-click itself, the pen\n * finishing a stroke, keeps it.\n */\nexport function modeTriggers(registry: ModeRegistry, handlers: ModeTriggerHandlers): Contribution<never> {\n const actions: Action[] = [];\n const { enter } = handlers;\n if (enter) {\n for (const mode of registry.list()) {\n const targets = mode.entry?.trigger === 'double-click-target' ? mode.entry.targets ?? [] : [];\n if (targets.length > 0) actions.push(triggerAction(mode, targets, enter, handlers.canEnter));\n }\n }\n return { id: 'modes.triggers', eligibility: { always: true }, actions };\n}\n\nfunction triggerAction(\n mode: ModeDefinition,\n targets: readonly string[],\n enter: (id: string, targetId: string) => void,\n canEnter: ModeTriggerHandlers['canEnter'],\n): Action {\n const targetAt = (deps: ActionDeps | undefined, at: { x: number; y: number } | undefined): string | null => {\n if (!at) return null;\n const hit = deps?.hitAt?.(at);\n const id = (hit?.doubleClick ?? hit)?.id;\n if (id === undefined) return null;\n return canEnter?.(mode.id, id) === false ? null : id;\n };\n const id = `mode.${mode.id}.trigger`;\n return {\n id,\n label: `Enter ${modeLabel(mode)}`,\n defaultBinding: targets.map((kind) => ({\n spec: { kind: 'doubleClick' as const, target: `kind:${kind}` as const },\n opts: { id: `${id}.doubleClick.${kind}` },\n })),\n requires: ['hitAt'],\n enabled: (deps, at) => (targetAt(deps, at) !== null ? true : ActionDisabledReason.NotApplicable),\n invoker: {\n timing: 'immediate',\n run: (deps, params) => {\n const x = params?.worldX, y = params?.worldY;\n const id = typeof x === 'number' && typeof y === 'number' ? targetAt(deps, { x, y }) : null;\n if (id !== null) enter(mode.id, id);\n },\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AA8BA,SAAgB,aAAa,UAAwB,UAAoD;CACvG,MAAM,UAAoB,CAAC;CAC3B,MAAM,EAAE,UAAU;CAClB,IAAI,OACF,KAAK,MAAM,QAAQ,SAAS,KAAK,GAAG;EAClC,MAAM,UAAU,KAAK,OAAO,YAAY,wBAAwB,KAAK,MAAM,WAAW,CAAC,IAAI,CAAC;EAC5F,IAAI,QAAQ,SAAS,GAAG,QAAQ,KAAK,cAAc,MAAM,SAAS,OAAO,SAAS,QAAQ,CAAC;CAC7F;CAEF,OAAO;EAAE,IAAI;EAAkB,aAAa,EAAE,QAAQ,KAAK;EAAG;CAAQ;AACxE;AAEA,SAAS,cACP,MACA,SACA,OACA,UACQ;CACR,MAAM,YAAY,MAA8B,OAA4D;EAC1G,IAAI,CAAC,IAAI,OAAO;EAChB,MAAM,MAAM,MAAM,QAAQ,EAAE;EAC5B,MAAM,MAAM,KAAK,eAAe,IAAA,EAAM;EACtC,IAAI,OAAO,QAAW,OAAO;EAC7B,OAAO,WAAW,KAAK,IAAI,EAAE,MAAM,QAAQ,OAAO;CACpD;CACA,MAAM,KAAK,QAAQ,KAAK,GAAG;CAC3B,OAAO;EACL;EACA,OAAO,SAAS,UAAU,IAAI;EAC9B,gBAAgB,QAAQ,KAAK,UAAU;GACrC,MAAM;IAAE,MAAM;IAAwB,QAAQ,QAAQ;GAAgB;GACtE,MAAM,EAAE,IAAI,GAAG,GAAG,eAAe,OAAO;EAC1C,EAAE;EACF,UAAU,CAAC,OAAO;EAClB,UAAU,MAAM,OAAQ,SAAS,MAAM,EAAE,MAAM,OAAO,OAAO,qBAAqB;EAClF,SAAS;GACP,QAAQ;GACR,MAAM,MAAM,WAAW;IACrB,MAAM,IAAI,QAAQ,QAAQ,IAAI,QAAQ;IACtC,MAAM,KAAK,OAAO,MAAM,YAAY,OAAO,MAAM,WAAW,SAAS,MAAM;KAAE;KAAG;IAAE,CAAC,IAAI;IACvF,IAAI,OAAO,MAAM,MAAM,KAAK,IAAI,EAAE;GACpC;EACF;CACF;AACF"}
@@ -0,0 +1,60 @@
1
+ import { pathEffectOf } from "../../core/scene/pathEffects/derive.js";
2
+ import { isTextData } from "@weasel-js/text";
3
+
4
+ //#region src/features/pathEffects/effectSources.ts
5
+ /**
6
+ * A path effect that hides its sources is edited through itself: its text and
7
+ * its path are reached from the effect node.
8
+ */
9
+ /**
10
+ * For a path-effect node with `hideSources`, the source an edit of `role`
11
+ * goes to: `'text'`, its first source when that is a text node; `'path'`, the
12
+ * path it lays along — the second source of an along kind, a blend's spine.
13
+ * `null` otherwise. The one answer every edit route asks.
14
+ */
15
+ function effectSourceFor(scene, id, role) {
16
+ const node = scene.get(id);
17
+ const effect = pathEffectOf(node);
18
+ const sources = node?.dependsOn;
19
+ if (effect === null || effect.hideSources !== true || !Array.isArray(sources)) return null;
20
+ const source = sources[role === "text" ? 0 : effect.kind === "blend" ? 2 : 1];
21
+ const found = source === void 0 ? void 0 : scene.get(source);
22
+ if (found === void 0) return null;
23
+ return role === "text" && !isTextData(found.data) ? null : source;
24
+ }
25
+ /**
26
+ * The sources a removal of `ids` takes with it: those that a path effect with
27
+ * `hideSources` in the removal hid, and that nothing staying still derives
28
+ * from. A source another effect still reads stays, and so does one shown
29
+ * again since or named in `keep`. Transitive, so an effect's hidden effect
30
+ * takes its own too.
31
+ */
32
+ function hiddenSourcesTakenWith(scene, ids, keep = /* @__PURE__ */ new Set()) {
33
+ const going = new Set(scene.removalClosure(ids));
34
+ const taken = [];
35
+ const within = (node, root) => {
36
+ for (let at = node; at != null; at = scene.get(at)?.parent) if (at === root) return true;
37
+ return false;
38
+ };
39
+ let grew = true;
40
+ while (grew) {
41
+ grew = false;
42
+ for (const id of [...going]) {
43
+ const node = scene.get(id);
44
+ if (pathEffectOf(node)?.hideSources !== true || !Array.isArray(node?.dependsOn)) continue;
45
+ for (const source of node.dependsOn) {
46
+ if (going.has(source) || keep.has(source) || scene.get(source)?.hidden !== true) continue;
47
+ const closure = scene.removalClosure([source]);
48
+ if (!closure.every((n) => going.has(n) || within(n, source))) continue;
49
+ for (const n of closure) going.add(n);
50
+ taken.push(source);
51
+ grew = true;
52
+ }
53
+ }
54
+ }
55
+ return taken;
56
+ }
57
+
58
+ //#endregion
59
+ export { effectSourceFor, hiddenSourcesTakenWith };
60
+ //# sourceMappingURL=effectSources.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"effectSources.js","names":[],"sources":["../../../src/features/pathEffects/effectSources.ts"],"sourcesContent":["/**\n * A path effect that hides its sources is edited through itself: its text and\n * its path are reached from the effect node.\n */\nimport { isTextData } from '@weasel-js/text';\nimport { pathEffectOf } from 'core/scene/pathEffects/derive';\nimport type { NodeId, Scene } from 'core/scene/types';\n\n/**\n * For a path-effect node with `hideSources`, the source an edit of `role`\n * goes to: `'text'`, its first source when that is a text node; `'path'`, the\n * path it lays along — the second source of an along kind, a blend's spine.\n * `null` otherwise. The one answer every edit route asks.\n */\nexport function effectSourceFor<TData, TLayer extends string, TPose>(\n scene: Pick<Scene<TData, TLayer, TPose>, 'get'>,\n id: NodeId,\n role: 'text' | 'path',\n): NodeId | null {\n const node = scene.get(id);\n const effect = pathEffectOf(node);\n const sources = node?.dependsOn;\n if (effect === null || effect.hideSources !== true || !Array.isArray(sources)) return null;\n const at = role === 'text' ? 0 : effect.kind === 'blend' ? 2 : 1;\n const source = (sources as readonly NodeId[])[at];\n const found = source === undefined ? undefined : scene.get(source);\n if (found === undefined) return null;\n return role === 'text' && !isTextData(found.data) ? null : source!;\n}\n\n/**\n * The sources a removal of `ids` takes with it: those that a path effect with\n * `hideSources` in the removal hid, and that nothing staying still derives\n * from. A source another effect still reads stays, and so does one shown\n * again since or named in `keep`. Transitive, so an effect's hidden effect\n * takes its own too.\n */\nexport function hiddenSourcesTakenWith<TData, TLayer extends string, TPose>(\n scene: Pick<Scene<TData, TLayer, TPose>, 'get' | 'removalClosure'>,\n ids: readonly NodeId[],\n keep: ReadonlySet<NodeId> = new Set(),\n): NodeId[] {\n const going = new Set<NodeId>(scene.removalClosure(ids));\n const taken: NodeId[] = [];\n const within = (node: NodeId, root: NodeId): boolean => {\n for (let at: NodeId | null | undefined = node; at != null; at = scene.get(at)?.parent) if (at === root) return true;\n return false;\n };\n let grew = true;\n while (grew) {\n grew = false;\n for (const id of [...going]) {\n const node = scene.get(id);\n if (pathEffectOf(node)?.hideSources !== true || !Array.isArray(node?.dependsOn)) continue;\n for (const source of node.dependsOn as readonly NodeId[]) {\n if (going.has(source) || keep.has(source) || scene.get(source)?.hidden !== true) continue;\n const closure = scene.removalClosure([source]);\n if (!closure.every((n) => going.has(n) || within(n, source))) continue;\n for (const n of closure) going.add(n);\n taken.push(source);\n grew = true;\n }\n }\n }\n return taken;\n}\n"],"mappings":";;;;;;;;;;;;;;AAcA,SAAgB,gBACd,OACA,IACA,MACe;CACf,MAAM,OAAO,MAAM,IAAI,EAAE;CACzB,MAAM,SAAS,aAAa,IAAI;CAChC,MAAM,UAAU,MAAM;CACtB,IAAI,WAAW,QAAQ,OAAO,gBAAgB,QAAQ,CAAC,MAAM,QAAQ,OAAO,GAAG,OAAO;CAEtF,MAAM,SAAU,QADL,SAAS,SAAS,IAAI,OAAO,SAAS,UAAU,IAAI;CAE/D,MAAM,QAAQ,WAAW,SAAY,SAAY,MAAM,IAAI,MAAM;CACjE,IAAI,UAAU,QAAW,OAAO;CAChC,OAAO,SAAS,UAAU,CAAC,WAAW,MAAM,IAAI,IAAI,OAAO;AAC7D;;;;;;;;AASA,SAAgB,uBACd,OACA,KACA,uBAA4B,IAAI,IAAI,GAC1B;CACV,MAAM,QAAQ,IAAI,IAAY,MAAM,eAAe,GAAG,CAAC;CACvD,MAAM,QAAkB,CAAC;CACzB,MAAM,UAAU,MAAc,SAA0B;EACtD,KAAK,IAAI,KAAgC,MAAM,MAAM,MAAM,KAAK,MAAM,IAAI,EAAE,CAAC,EAAE,QAAQ,IAAI,OAAO,MAAM,OAAO;EAC/G,OAAO;CACT;CACA,IAAI,OAAO;CACX,OAAO,MAAM;EACX,OAAO;EACP,KAAK,MAAM,MAAM,CAAC,GAAG,KAAK,GAAG;GAC3B,MAAM,OAAO,MAAM,IAAI,EAAE;GACzB,IAAI,aAAa,IAAI,CAAC,EAAE,gBAAgB,QAAQ,CAAC,MAAM,QAAQ,MAAM,SAAS,GAAG;GACjF,KAAK,MAAM,UAAU,KAAK,WAAgC;IACxD,IAAI,MAAM,IAAI,MAAM,KAAK,KAAK,IAAI,MAAM,KAAK,MAAM,IAAI,MAAM,CAAC,EAAE,WAAW,MAAM;IACjF,MAAM,UAAU,MAAM,eAAe,CAAC,MAAM,CAAC;IAC7C,IAAI,CAAC,QAAQ,OAAO,MAAM,MAAM,IAAI,CAAC,KAAK,OAAO,GAAG,MAAM,CAAC,GAAG;IAC9D,KAAK,MAAM,KAAK,SAAS,MAAM,IAAI,CAAC;IACpC,MAAM,KAAK,MAAM;IACjB,OAAO;GACT;EACF;CACF;CACA,OAAO;AACT"}
@@ -1,15 +1,31 @@
1
+ import { noteFontRead } from "../../core/scene/fontReads.js";
1
2
  import { isRectPose } from "../../core/geometry/autoPoseDescriptor.js";
2
3
  import { pathPaintOf } from "../../canvas/nodeShapes/nodePaint.js";
3
4
  import { findNodeShape } from "../../canvas/NodeShape.js";
5
+ import { textEffectItems } from "./textItems.js";
4
6
  import { pathThroughPoseFrame, poseLinearOf } from "@weasel-js/geom";
7
+ import { isTextData } from "@weasel-js/text";
5
8
 
6
9
  //#region src/features/pathEffects/readSource.ts
7
10
  /**
8
11
  * How the kit's path effects read a source node: its outline from its
9
12
  * painter's silhouette, its paint as `kit:path` resolves it.
10
13
  */
11
- /** `dep` as a path effect's source, or `null` when it has no outline. */
14
+ /** `dep` as a path effect's source, or `null` when it has no outline. A text
15
+ * node's source also carries its grapheme clusters as items. */
12
16
  function readEffectSource(dep) {
17
+ const data = dep.node.data;
18
+ const text = isTextData(data);
19
+ if (text) noteFontRead();
20
+ const source = readShape(dep);
21
+ const pose = dep.pose;
22
+ if (source === null || !text || !isRectPose(pose)) return source;
23
+ return {
24
+ ...source,
25
+ items: () => textEffectItems(data, pose)
26
+ };
27
+ }
28
+ function readShape(dep) {
13
29
  const data = dep.node.data ?? {};
14
30
  const { fill, stroke } = pathPaintOf(data);
15
31
  const paint = {