@weasel-js/core 1.12.0 → 1.13.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (292) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/dist/animation/poseTarget.js +46 -15
  3. package/dist/animation/poseTarget.js.map +1 -1
  4. package/dist/animation/poseTransition.js +3 -3
  5. package/dist/animation/poseTransition.js.map +1 -1
  6. package/dist/animation/reflow.js +3 -6
  7. package/dist/animation/reflow.js.map +1 -1
  8. package/dist/animation/rig/bindRig.js +2 -2
  9. package/dist/animation/rig/bindRig.js.map +1 -1
  10. package/dist/animation/rig/skinBinding.js +2 -2
  11. package/dist/animation/rig/skinBinding.js.map +1 -1
  12. package/dist/barrel/canvas.js +5 -3
  13. package/dist/barrel/chrome.js +3 -1
  14. package/dist/barrel/paths.js +3 -2
  15. package/dist/barrel/routing.js +2 -2
  16. package/dist/barrel/scene.js +6 -1
  17. package/dist/canvas/NodeShape.js +1 -0
  18. package/dist/canvas/NodeShape.js.map +1 -1
  19. package/dist/canvas/SceneCanvas/SceneCanvasInput.js +2 -0
  20. package/dist/canvas/SceneCanvas/SceneCanvasInput.js.map +1 -1
  21. package/dist/canvas/SceneCanvas/canvasPassThrough.js +1 -1
  22. package/dist/canvas/SceneCanvas/canvasPassThrough.js.map +1 -1
  23. package/dist/canvas/SceneCanvas/defaultNodeRouting.js +2 -2
  24. package/dist/canvas/SceneCanvas/defaultNodeRouting.js.map +1 -1
  25. package/dist/canvas/SceneCanvas/scenePointQuery.js +20 -2
  26. package/dist/canvas/SceneCanvas/scenePointQuery.js.map +1 -1
  27. package/dist/canvas/SceneCanvas/useCanvasTools.js +3 -0
  28. package/dist/canvas/SceneCanvas/useCanvasTools.js.map +1 -1
  29. package/dist/canvas/SceneCanvas/usePreviewGhostLayer.js +25 -19
  30. package/dist/canvas/SceneCanvas/usePreviewGhostLayer.js.map +1 -1
  31. package/dist/canvas/SceneCanvas/useSceneCanvasLayers.js +7 -0
  32. package/dist/canvas/SceneCanvas/useSceneCanvasLayers.js.map +1 -1
  33. package/dist/canvas/SceneCanvas/useSceneCanvasTools.js +1 -0
  34. package/dist/canvas/SceneCanvas/useSceneCanvasTools.js.map +1 -1
  35. package/dist/canvas/SceneCanvas/useScenePicking.js +31 -6
  36. package/dist/canvas/SceneCanvas/useScenePicking.js.map +1 -1
  37. package/dist/canvas/SceneCanvas/useSceneSelectTool.js +4 -5
  38. package/dist/canvas/SceneCanvas/useSceneSelectTool.js.map +1 -1
  39. package/dist/canvas/SceneCanvas/useSceneStandardActions.js +5 -3
  40. package/dist/canvas/SceneCanvas/useSceneStandardActions.js.map +1 -1
  41. package/dist/canvas/SceneCanvas.js +19 -2
  42. package/dist/canvas/SceneCanvas.js.map +1 -1
  43. package/dist/canvas/accentColor.js +26 -3
  44. package/dist/canvas/accentColor.js.map +1 -1
  45. package/dist/canvas/buildSceneTree.js +14 -20
  46. package/dist/canvas/buildSceneTree.js.map +1 -1
  47. package/dist/canvas/copiesAbove.js +1 -1
  48. package/dist/canvas/copiesAbove.js.map +1 -1
  49. package/dist/canvas/defaultDrawOne.js +10 -7
  50. package/dist/canvas/defaultDrawOne.js.map +1 -1
  51. package/dist/canvas/deps/areaSelect.js +5 -2
  52. package/dist/canvas/deps/areaSelect.js.map +1 -1
  53. package/dist/canvas/deps/booleans.js +1 -1
  54. package/dist/canvas/deps/booleans.js.map +1 -1
  55. package/dist/canvas/deps/enteredGroup.js +81 -0
  56. package/dist/canvas/deps/enteredGroup.js.map +1 -0
  57. package/dist/canvas/deps/hitTestArea.js +27 -15
  58. package/dist/canvas/deps/hitTestArea.js.map +1 -1
  59. package/dist/canvas/deps/insert.js +4 -36
  60. package/dist/canvas/deps/insert.js.map +1 -1
  61. package/dist/canvas/deps/insertSite.js +39 -0
  62. package/dist/canvas/deps/insertSite.js.map +1 -0
  63. package/dist/canvas/deps/lassoSelect.js +6 -3
  64. package/dist/canvas/deps/lassoSelect.js.map +1 -1
  65. package/dist/canvas/deps/pathLeaf.js +1 -1
  66. package/dist/canvas/deps/pathLeaf.js.map +1 -1
  67. package/dist/canvas/deps/slice.js +2 -1
  68. package/dist/canvas/deps/slice.js.map +1 -1
  69. package/dist/canvas/derivedPath.js +14 -6
  70. package/dist/canvas/derivedPath.js.map +1 -1
  71. package/dist/canvas/doubleClickTarget.js +9 -11
  72. package/dist/canvas/doubleClickTarget.js.map +1 -1
  73. package/dist/canvas/drawnBounds.js +16 -7
  74. package/dist/canvas/drawnBounds.js.map +1 -1
  75. package/dist/canvas/inheritedPaints.js +22 -11
  76. package/dist/canvas/inheritedPaints.js.map +1 -1
  77. package/dist/canvas/nodeShapes/nodePaint.js +1 -7
  78. package/dist/canvas/nodeShapes/nodePaint.js.map +1 -1
  79. package/dist/canvas/nodeShapes/pathPainter.js +2 -3
  80. package/dist/canvas/nodeShapes/pathPainter.js.map +1 -1
  81. package/dist/canvas/nodeShapes/rectFallbackPainter.js +1 -1
  82. package/dist/canvas/nodeShapes/rectFallbackPainter.js.map +1 -1
  83. package/dist/canvas/nodeShapes/shapePainter.js +2 -3
  84. package/dist/canvas/nodeShapes/shapePainter.js.map +1 -1
  85. package/dist/canvas/ownClip.js +24 -0
  86. package/dist/canvas/ownClip.js.map +1 -0
  87. package/dist/canvas/paintCull.js.map +1 -1
  88. package/dist/canvas/pickIndex/nodeBroadBox.js +86 -0
  89. package/dist/canvas/pickIndex/nodeBroadBox.js.map +1 -0
  90. package/dist/canvas/pickIndex/scenePickIndex.js +208 -0
  91. package/dist/canvas/pickIndex/scenePickIndex.js.map +1 -0
  92. package/dist/canvas/pickWalk.js +14 -90
  93. package/dist/canvas/pickWalk.js.map +1 -1
  94. package/dist/canvas/poseCascade.js +1 -1
  95. package/dist/canvas/poseCascade.js.map +1 -1
  96. package/dist/canvas/publishPreviews.js +97 -0
  97. package/dist/canvas/publishPreviews.js.map +1 -0
  98. package/dist/canvas/sceneAdapter.js +7 -5
  99. package/dist/canvas/sceneAdapter.js.map +1 -1
  100. package/dist/canvas/sceneClipboard.js +17 -10
  101. package/dist/canvas/sceneClipboard.js.map +1 -1
  102. package/dist/canvas/scenePickReads.js +37 -0
  103. package/dist/canvas/scenePickReads.js.map +1 -0
  104. package/dist/canvas/scenePickSource.js +74 -0
  105. package/dist/canvas/scenePickSource.js.map +1 -0
  106. package/dist/canvas/sceneSlotWiring.js +9 -4
  107. package/dist/canvas/sceneSlotWiring.js.map +1 -1
  108. package/dist/canvas/sceneViewRender.js +9 -5
  109. package/dist/canvas/sceneViewRender.js.map +1 -1
  110. package/dist/canvas/shownNodes.js +15 -0
  111. package/dist/canvas/shownNodes.js.map +1 -0
  112. package/dist/canvas/useSceneAdapter.js +5 -3
  113. package/dist/canvas/useSceneAdapter.js.map +1 -1
  114. package/dist/clipboard.d.ts +2 -2
  115. package/dist/{clone-C3e0OTuF.d.ts → clone-qb2e1F5p.d.ts} +2 -2
  116. package/dist/clone.d.ts +3 -3
  117. package/dist/core/isPlainObject.js +2 -2
  118. package/dist/core/layers/layerGroup.js.map +1 -1
  119. package/dist/core/scene/NodeRouting.js.map +1 -1
  120. package/dist/core/scene/arrivalWindow.js +1 -1
  121. package/dist/core/scene/arrivalWindow.js.map +1 -1
  122. package/dist/core/scene/clipNothing.js +2 -5
  123. package/dist/core/scene/clipNothing.js.map +1 -1
  124. package/dist/core/scene/declaredFieldSetters.js +44 -0
  125. package/dist/core/scene/declaredFieldSetters.js.map +1 -0
  126. package/dist/core/scene/depMemo.js +3 -0
  127. package/dist/core/scene/depMemo.js.map +1 -1
  128. package/dist/core/scene/dependents.js +3 -1
  129. package/dist/core/scene/dependents.js.map +1 -1
  130. package/dist/core/scene/documentOf.js +43 -0
  131. package/dist/core/scene/documentOf.js.map +1 -0
  132. package/dist/core/scene/effectivePose.js +37 -13
  133. package/dist/core/scene/effectivePose.js.map +1 -1
  134. package/dist/core/scene/fieldEdits.js +17 -14
  135. package/dist/core/scene/fieldEdits.js.map +1 -1
  136. package/dist/core/scene/historyForm.js +4 -4
  137. package/dist/core/scene/historyForm.js.map +1 -1
  138. package/dist/core/scene/inheritedPaint.js +27 -5
  139. package/dist/core/scene/inheritedPaint.js.map +1 -1
  140. package/dist/core/scene/kitOps.js +3 -23
  141. package/dist/core/scene/kitOps.js.map +1 -1
  142. package/dist/core/scene/layoutPass.js +1 -2
  143. package/dist/core/scene/layoutPass.js.map +1 -1
  144. package/dist/core/scene/nodeAsShown.js +72 -0
  145. package/dist/core/scene/nodeAsShown.js.map +1 -0
  146. package/dist/core/scene/nodeFnFields.js +2 -10
  147. package/dist/core/scene/nodeFnFields.js.map +1 -1
  148. package/dist/core/scene/nodeMemo.js +6 -1
  149. package/dist/core/scene/nodeMemo.js.map +1 -1
  150. package/dist/core/scene/nodeStore.js +2 -4
  151. package/dist/core/scene/nodeStore.js.map +1 -1
  152. package/dist/core/scene/pathEffects/derive.js +3 -2
  153. package/dist/core/scene/pathEffects/derive.js.map +1 -1
  154. package/dist/core/scene/pathEffects/foundHidden.js +35 -0
  155. package/dist/core/scene/pathEffects/foundHidden.js.map +1 -0
  156. package/dist/core/scene/plyStacks.js +89 -0
  157. package/dist/core/scene/plyStacks.js.map +1 -0
  158. package/dist/core/scene/poseHold.js +42 -24
  159. package/dist/core/scene/poseHold.js.map +1 -1
  160. package/dist/core/scene/poseOverrides.js +35 -38
  161. package/dist/core/scene/poseOverrides.js.map +1 -1
  162. package/dist/core/scene/posePlies.js +59 -0
  163. package/dist/core/scene/posePlies.js.map +1 -0
  164. package/dist/core/scene/reconcileSpecs.js +6 -15
  165. package/dist/core/scene/reconcileSpecs.js.map +1 -1
  166. package/dist/core/scene/renderOrder.js +10 -4
  167. package/dist/core/scene/renderOrder.js.map +1 -1
  168. package/dist/core/scene/sameValue.js +17 -0
  169. package/dist/core/scene/sameValue.js.map +1 -0
  170. package/dist/core/scene/scene.js +3 -3
  171. package/dist/core/scene/scene.js.map +1 -1
  172. package/dist/core/scene/sceneEdits.js +8 -0
  173. package/dist/core/scene/sceneEdits.js.map +1 -1
  174. package/dist/core/scene/sceneState.js +1 -2
  175. package/dist/core/scene/sceneState.js.map +1 -1
  176. package/dist/core/scene/serialization.js +45 -9
  177. package/dist/core/scene/serialization.js.map +1 -1
  178. package/dist/core/scene/staleReplay.js +5 -2
  179. package/dist/core/scene/staleReplay.js.map +1 -1
  180. package/dist/core/scene/statedFields.js +4 -2
  181. package/dist/core/scene/statedFields.js.map +1 -1
  182. package/dist/core/scene/treeEdits.js +2 -4
  183. package/dist/core/scene/treeEdits.js.map +1 -1
  184. package/dist/core/scene/treeOrder.js +3 -1
  185. package/dist/core/scene/treeOrder.js.map +1 -1
  186. package/dist/core/scene/treeReads.js +1 -2
  187. package/dist/core/scene/treeReads.js.map +1 -1
  188. package/dist/core/scene/treeShape.js +13 -0
  189. package/dist/core/scene/treeShape.js.map +1 -0
  190. package/dist/core/scene/types.js.map +1 -1
  191. package/dist/core/scene/useScene.js +1 -1
  192. package/dist/core/scene/useScene.js.map +1 -1
  193. package/dist/{cornerHandles-p8fDIAKt.d.ts → cornerHandles-CNCb7aTY.d.ts} +1 -1
  194. package/dist/{depSchema-mG19aEdD.d.ts → depSchema-B1RS-IJx.d.ts} +27 -54
  195. package/dist/devtools/handlers.js +1 -1
  196. package/dist/devtools/handlers.js.map +1 -1
  197. package/dist/devtools/protocol.js.map +1 -1
  198. package/dist/devtools-protocol.d.ts +4 -3
  199. package/dist/devtools.d.ts +3 -2
  200. package/dist/features/measure/measureHit.js +5 -3
  201. package/dist/features/measure/measureHit.js.map +1 -1
  202. package/dist/features/measure/measurement.js +1 -1
  203. package/dist/features/measure/measurement.js.map +1 -1
  204. package/dist/features/modes/modeLayers.js +14 -7
  205. package/dist/features/modes/modeLayers.js.map +1 -1
  206. package/dist/features/modes/modeTriggers.js +3 -3
  207. package/dist/features/modes/modeTriggers.js.map +1 -1
  208. package/dist/features/pathEffects/effectSources.js +25 -16
  209. package/dist/features/pathEffects/effectSources.js.map +1 -1
  210. package/dist/features/pathEffects/readSource.js +1 -1
  211. package/dist/features/pathEffects/readSource.js.map +1 -1
  212. package/dist/features/pathEffects/useEffectSourcesLayer.js +15 -19
  213. package/dist/features/pathEffects/useEffectSourcesLayer.js.map +1 -1
  214. package/dist/features/paths/editablePath.js +1 -1
  215. package/dist/features/paths/editablePath.js.map +1 -1
  216. package/dist/features/paths/pathSnap/sceneIndex.js +1 -1
  217. package/dist/features/paths/pathSnap/sceneIndex.js.map +1 -1
  218. package/dist/features/selection/enteredGroupFrame.js +65 -0
  219. package/dist/features/selection/enteredGroupFrame.js.map +1 -0
  220. package/dist/features/selection/enteredGroupScope.js +44 -0
  221. package/dist/features/selection/enteredGroupScope.js.map +1 -0
  222. package/dist/features/text/sceneTextDrafts.js +26 -27
  223. package/dist/features/text/sceneTextDrafts.js.map +1 -1
  224. package/dist/features/text/textHitAt.js +4 -2
  225. package/dist/features/text/textHitAt.js.map +1 -1
  226. package/dist/features/text/useSceneTextEdit.js +3 -3
  227. package/dist/features/text/useSceneTextEdit.js.map +1 -1
  228. package/dist/index.d.ts +225 -73
  229. package/dist/index.js +15 -5
  230. package/dist/interactions/actions/admitsChild.js +11 -0
  231. package/dist/interactions/actions/admitsChild.js.map +1 -0
  232. package/dist/interactions/actions/defaults/contextMenu.js +16 -3
  233. package/dist/interactions/actions/defaults/contextMenu.js.map +1 -1
  234. package/dist/interactions/actions/defaults/createPaintAction.js +4 -1
  235. package/dist/interactions/actions/defaults/createPaintAction.js.map +1 -1
  236. package/dist/interactions/actions/defaults/enterGroup.js +88 -0
  237. package/dist/interactions/actions/defaults/enterGroup.js.map +1 -0
  238. package/dist/interactions/actions/defaults/enterPathEdit.js +1 -1
  239. package/dist/interactions/actions/defaults/enterPathEdit.js.map +1 -1
  240. package/dist/interactions/actions/defaults/enterTextEdit.js +1 -1
  241. package/dist/interactions/actions/defaults/enterTextEdit.js.map +1 -1
  242. package/dist/interactions/actions/defaults/group.js +7 -7
  243. package/dist/interactions/actions/defaults/group.js.map +1 -1
  244. package/dist/interactions/actions/defaults/selectAll.js +7 -3
  245. package/dist/interactions/actions/defaults/selectAll.js.map +1 -1
  246. package/dist/interactions/actions/defaults/selectDeep.js +35 -0
  247. package/dist/interactions/actions/defaults/selectDeep.js.map +1 -0
  248. package/dist/interactions/actions/drawnBoxes.js +2 -1
  249. package/dist/interactions/actions/drawnBoxes.js.map +1 -1
  250. package/dist/interactions/actions/insert/action.js +1 -0
  251. package/dist/interactions/actions/insert/action.js.map +1 -1
  252. package/dist/interactions/actions/insert/parent.js +7 -5
  253. package/dist/interactions/actions/insert/parent.js.map +1 -1
  254. package/dist/interactions/actions/insert/placeAction.js +3 -2
  255. package/dist/interactions/actions/insert/placeAction.js.map +1 -1
  256. package/dist/interactions/actions/insert/session.js +2 -2
  257. package/dist/interactions/actions/insert/session.js.map +1 -1
  258. package/dist/interactions/actions/move/commit/reparent.js +1 -1
  259. package/dist/interactions/actions/move/commit/reparent.js.map +1 -1
  260. package/dist/interactions/actions/move/gestureAdapter.js +6 -7
  261. package/dist/interactions/actions/move/gestureAdapter.js.map +1 -1
  262. package/dist/interactions/actions/move/layout/exits.js +1 -1
  263. package/dist/interactions/actions/move/layout/exits.js.map +1 -1
  264. package/dist/interactions/actions/move/session.js +4 -4
  265. package/dist/interactions/actions/move/session.js.map +1 -1
  266. package/dist/interactions/actions/pathCommands/pathNodes.js +14 -5
  267. package/dist/interactions/actions/pathCommands/pathNodes.js.map +1 -1
  268. package/dist/interactions/gestures/thresholdDrag/thresholdDrag.js +14 -2
  269. package/dist/interactions/gestures/thresholdDrag/thresholdDrag.js.map +1 -1
  270. package/dist/{math-B56zxVs5.d.ts → math-C_elShP_.d.ts} +2 -5
  271. package/dist/math.d.ts +1 -1
  272. package/dist/move.d.ts +4 -4
  273. package/dist/{options-DBxEryYq.d.ts → options-ClEm3SEl.d.ts} +1 -1
  274. package/dist/resize.d.ts +4 -4
  275. package/dist/test-seams.d.ts +6 -6
  276. package/dist/tools/builtin/pressTarget.js +22 -0
  277. package/dist/tools/builtin/pressTarget.js.map +1 -0
  278. package/dist/tools/builtin/select/adapterPointQuery.js +3 -3
  279. package/dist/tools/builtin/select/adapterPointQuery.js.map +1 -1
  280. package/dist/tools/builtin/select/collapseDeferred.js +40 -0
  281. package/dist/tools/builtin/select/collapseDeferred.js.map +1 -0
  282. package/dist/tools/builtin/select/groupEntry.js +82 -0
  283. package/dist/tools/builtin/select/groupEntry.js.map +1 -0
  284. package/dist/tools/builtin/select/pressDecision.js +1 -1
  285. package/dist/tools/builtin/select/pressDecision.js.map +1 -1
  286. package/dist/tools/builtin/select/useSelectTool.js +61 -45
  287. package/dist/tools/builtin/select/useSelectTool.js.map +1 -1
  288. package/dist/{types-7puAuVD2.d.ts → types-49uYJN3j.d.ts} +1 -1
  289. package/dist/{types-CniVctmk.d.ts → types-BEdRIhNc.d.ts} +272 -109
  290. package/dist/{types-oFeQ5VjV.d.ts → types-p2alu-R1.d.ts} +1 -1
  291. package/dist/version.js +1 -1
  292. package/package.json +25 -25
@@ -1 +1 @@
1
- {"version":3,"file":"readSource.js","names":[],"sources":["../../../src/features/pathEffects/readSource.ts"],"sourcesContent":["/**\n * How the kit's path effects read a source node: its outline from its\n * painter's silhouette, its paint as `kit:path` resolves it.\n */\nimport { pathThroughPoseFrame, poseLinearOf, type PathInWorldPose } from '@weasel-js/geom';\nimport type { Fill, Stroke } from '@weasel-js/paint';\nimport { isTextData, type TextOutlineSource } from '@weasel-js/text';\nimport { findNodeShape } from 'canvas/NodeShape';\nimport { isRectPose } from 'core/geometry/autoPoseDescriptor';\nimport { noteFontRead } from 'core/scene/fontReads';\nimport type { EffectSource } from 'core/scene/pathEffects/source';\nimport type { DerivedDep } from 'core/scene/types';\nimport { pathPaintOf } from 'canvas/nodeShapes/nodePaint';\nimport { textEffectItems } from './textItems';\n\n/** `dep` as a path effect's source, or `null` when it has no outline. A text\n * node's source also carries its grapheme clusters as items. */\nexport function readEffectSource(dep: DerivedDep<unknown>): EffectSource | null {\n const data = dep.node.data;\n const text = isTextData(data);\n // A text node's outline is laid out in its face, which may not have landed: without this read, an effect that found\n // no outline while the face loads would never look again.\n if (text) noteFontRead();\n const source = readShape(dep);\n const pose = dep.pose;\n if (source === null || !text || !isRectPose(pose)) return source;\n return { ...source, items: () => textEffectItems(data as TextOutlineSource, pose as PathInWorldPose) };\n}\n\nfunction readShape(dep: DerivedDep<unknown>): EffectSource | null {\n const data = (dep.node.data ?? {}) as { fill?: Fill | null; stroke?: Stroke | null };\n const { fill, stroke } = pathPaintOf(data);\n const paint = { fill, stroke: stroke ?? null };\n const shown = dep.path;\n if (shown !== null) return { world: shown, ...paint };\n const local = findNodeShape(dep.node)?.silhouette?.(dep.node, dep.pose) ?? null;\n if (local === null) return null;\n const world = pathThroughPoseFrame(local, dep.pose);\n const pose = dep.pose;\n if (!isRectPose(pose) || poseLinearOf(pose) !== null) return { world, ...paint };\n return {\n world,\n framed: { path: local, cx: pose.x + pose.width / 2, cy: pose.y + pose.height / 2, rotation: pose.rotation ?? 0 },\n ...paint,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;AAiBA,SAAgB,iBAAiB,KAA+C;CAC9E,MAAM,OAAO,IAAI,KAAK;CACtB,MAAM,OAAO,WAAW,IAAI;CAG5B,IAAI,MAAM,aAAa;CACvB,MAAM,SAAS,UAAU,GAAG;CAC5B,MAAM,OAAO,IAAI;CACjB,IAAI,WAAW,QAAQ,CAAC,QAAQ,CAAC,WAAW,IAAI,GAAG,OAAO;CAC1D,OAAO;EAAE,GAAG;EAAQ,aAAa,gBAAgB,MAA2B,IAAuB;CAAE;AACvG;AAEA,SAAS,UAAU,KAA+C;CAChE,MAAM,OAAQ,IAAI,KAAK,QAAQ,CAAC;CAChC,MAAM,EAAE,MAAM,WAAW,YAAY,IAAI;CACzC,MAAM,QAAQ;EAAE;EAAM,QAAQ,UAAU;CAAK;CAC7C,MAAM,QAAQ,IAAI;CAClB,IAAI,UAAU,MAAM,OAAO;EAAE,OAAO;EAAO,GAAG;CAAM;CACpD,MAAM,QAAQ,cAAc,IAAI,IAAI,CAAC,EAAE,aAAa,IAAI,MAAM,IAAI,IAAI,KAAK;CAC3E,IAAI,UAAU,MAAM,OAAO;CAC3B,MAAM,QAAQ,qBAAqB,OAAO,IAAI,IAAI;CAClD,MAAM,OAAO,IAAI;CACjB,IAAI,CAAC,WAAW,IAAI,KAAK,aAAa,IAAI,MAAM,MAAM,OAAO;EAAE;EAAO,GAAG;CAAM;CAC/E,OAAO;EACL;EACA,QAAQ;GAAE,MAAM;GAAO,IAAI,KAAK,IAAI,KAAK,QAAQ;GAAG,IAAI,KAAK,IAAI,KAAK,SAAS;GAAG,UAAU,KAAK,YAAY;EAAE;EAC/G,GAAG;CACL;AACF"}
1
+ {"version":3,"file":"readSource.js","names":[],"sources":["../../../src/features/pathEffects/readSource.ts"],"sourcesContent":["/**\n * How the kit's path effects read a source node: its outline from its\n * painter's silhouette, its paint as `kit:path` resolves it.\n */\nimport { pathThroughPoseFrame, poseLinearOf, type PathInWorldPose } from '@weasel-js/geom';\nimport type { Fill, Stroke } from '@weasel-js/paint';\nimport { isTextData, type TextOutlineSource } from '@weasel-js/text';\nimport { findNodeShape } from 'canvas/NodeShape';\nimport { isRectPose } from 'core/geometry/autoPoseDescriptor';\nimport { noteFontRead } from 'core/scene/fontReads';\nimport type { EffectSource } from 'core/scene/pathEffects/source';\nimport type { DerivedDep } from 'core/scene/types';\nimport { pathPaintOf } from 'canvas/nodeShapes/nodePaint';\nimport { textEffectItems } from './textItems';\n\n/** `dep` as a path effect's source, or `null` when it has no outline. A text\n * node's source also carries its grapheme clusters as items. */\nexport function readEffectSource(dep: DerivedDep<unknown>): EffectSource | null {\n const data = dep.node.data;\n const text = isTextData(data);\n // A text node's outline is laid out in its face, which may not have landed: without this read, an effect that found\n // no outline while the face loads would never look again.\n if (text) noteFontRead();\n const source = readShape(dep);\n const pose = dep.pose;\n if (source === null || !text || !isRectPose(pose)) return source;\n return { ...source, items: () => textEffectItems(data as TextOutlineSource, pose as PathInWorldPose) };\n}\n\nfunction readShape(dep: DerivedDep<unknown>): EffectSource | null {\n const data = (dep.node.data ?? {}) as { fill?: Fill | null; stroke?: Stroke | null };\n const { fill, stroke } = pathPaintOf(data, dep.inherited);\n const paint = { fill, stroke: stroke ?? null };\n const shown = dep.path;\n if (shown !== null) return { world: shown, ...paint };\n const local = findNodeShape(dep.node)?.silhouette?.(dep.node, dep.pose) ?? null;\n if (local === null) return null;\n const world = pathThroughPoseFrame(local, dep.pose);\n const pose = dep.pose;\n if (!isRectPose(pose) || poseLinearOf(pose) !== null) return { world, ...paint };\n return {\n world,\n framed: { path: local, cx: pose.x + pose.width / 2, cy: pose.y + pose.height / 2, rotation: pose.rotation ?? 0 },\n ...paint,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;AAiBA,SAAgB,iBAAiB,KAA+C;CAC9E,MAAM,OAAO,IAAI,KAAK;CACtB,MAAM,OAAO,WAAW,IAAI;CAG5B,IAAI,MAAM,aAAa;CACvB,MAAM,SAAS,UAAU,GAAG;CAC5B,MAAM,OAAO,IAAI;CACjB,IAAI,WAAW,QAAQ,CAAC,QAAQ,CAAC,WAAW,IAAI,GAAG,OAAO;CAC1D,OAAO;EAAE,GAAG;EAAQ,aAAa,gBAAgB,MAA2B,IAAuB;CAAE;AACvG;AAEA,SAAS,UAAU,KAA+C;CAChE,MAAM,OAAQ,IAAI,KAAK,QAAQ,CAAC;CAChC,MAAM,EAAE,MAAM,WAAW,YAAY,MAAM,IAAI,SAAS;CACxD,MAAM,QAAQ;EAAE;EAAM,QAAQ,UAAU;CAAK;CAC7C,MAAM,QAAQ,IAAI;CAClB,IAAI,UAAU,MAAM,OAAO;EAAE,OAAO;EAAO,GAAG;CAAM;CACpD,MAAM,QAAQ,cAAc,IAAI,IAAI,CAAC,EAAE,aAAa,IAAI,MAAM,IAAI,IAAI,KAAK;CAC3E,IAAI,UAAU,MAAM,OAAO;CAC3B,MAAM,QAAQ,qBAAqB,OAAO,IAAI,IAAI;CAClD,MAAM,OAAO,IAAI;CACjB,IAAI,CAAC,WAAW,IAAI,KAAK,aAAa,IAAI,MAAM,MAAM,OAAO;EAAE;EAAO,GAAG;CAAM;CAC/E,OAAO;EACL;EACA,QAAQ;GAAE,MAAM;GAAO,IAAI,KAAK,IAAI,KAAK,QAAQ;GAAG,IAAI,KAAK,IAAI,KAAK,SAAS;GAAG,UAAU,KAAK,YAAY;EAAE;EAC/G,GAAG;CACL;AACF"}
@@ -1,5 +1,5 @@
1
1
  import { sceneDepLookup } from "../../canvas/derivedPath.js";
2
- import { pathEffectOf } from "../../core/scene/pathEffects/derive.js";
2
+ import { sourcesHiddenBy } from "./effectSources.js";
3
3
  import { PATH_OVERLAY_ROLES } from "../../canvas/pathOverlay.js";
4
4
  import { chromeStateFrom } from "../../canvas/drawEnvelope.js";
5
5
  import { readEffectSource } from "./readSource.js";
@@ -12,8 +12,8 @@ import { useLatest } from "@weasel-js/react";
12
12
  * of the sources it hides, drawn faintly under the selection chrome.
13
13
  */
14
14
  var FAINT = .4;
15
- /** For each selected path-effect node with `hideSources`, its hidden sources' outlines as the effect reads them, in a
16
- * one-screen-pixel stroke of the `'chrome'` overlay color at 40% opacity. The selection comes off the draw envelope. */
15
+ /** For each selected path-effect node, the outlines of the sources it holds hidden (`sourcesHiddenBy`) as the effect
16
+ * reads them, in a one-screen-pixel stroke of the `'chrome'` overlay color at 40% opacity. The selection comes off the draw envelope. */
17
17
  function useEffectSourcesLayer(scene) {
18
18
  const sceneRef = useLatest(scene);
19
19
  return useMemo(() => ({
@@ -26,22 +26,18 @@ function useEffectSourcesLayer(scene) {
26
26
  const stroke = PATH_OVERLAY_ROLES["chrome"].stroke;
27
27
  const depOf = sceneDepLookup(sc);
28
28
  const lines = [];
29
- for (const id of selected) {
30
- const node = sc.get(id);
31
- if (pathEffectOf(node)?.hideSources !== true || !Array.isArray(node.dependsOn)) continue;
32
- for (const source of node.dependsOn) {
33
- if (!sc.isHidden(source)) continue;
34
- const dep = depOf(source);
35
- const path = dep === void 0 ? null : readEffectSource(dep)?.world ?? null;
36
- if (path !== null) lines.push({
37
- kind: "path",
38
- path,
39
- stroke: {
40
- paint: stroke,
41
- width: { px: 1 }
42
- }
43
- });
44
- }
29
+ for (const id of selected) for (const source of sourcesHiddenBy(sc, id)) {
30
+ if (!sc.isHidden(source)) continue;
31
+ const dep = depOf(source);
32
+ const path = dep === void 0 ? null : readEffectSource(dep)?.world ?? null;
33
+ if (path !== null) lines.push({
34
+ kind: "path",
35
+ path,
36
+ stroke: {
37
+ paint: stroke,
38
+ width: { px: 1 }
39
+ }
40
+ });
45
41
  }
46
42
  return lines.length === 0 ? [] : [{
47
43
  kind: "group",
@@ -1 +1 @@
1
- {"version":3,"file":"useEffectSourcesLayer.js","names":[],"sources":["../../../src/features/pathEffects/useEffectSourcesLayer.ts"],"sourcesContent":["/**\n * The layer that shows what a selected path effect was made from: the outlines\n * of the sources it hides, drawn faintly under the selection chrome.\n */\nimport { useMemo } from 'react';\nimport { useLatest } from '@weasel-js/react';\nimport type { DrawCommand } from '@weasel-js/renderer';\nimport { PATH_OVERLAY_ROLES } from 'canvas/pathOverlay';\nimport { sceneDepLookup } from 'canvas/derivedPath';\nimport { chromeStateFrom } from 'canvas/drawEnvelope';\nimport type { RenderLayer } from 'core/layers/render';\nimport { pathEffectOf } from 'core/scene/pathEffects/derive';\nimport type { NodeId, Scene } from 'core/scene/types';\nimport { readEffectSource } from './readSource';\n\nconst FAINT = 0.4;\n\n/** For each selected path-effect node with `hideSources`, its hidden sources' outlines as the effect reads them, in a\n * one-screen-pixel stroke of the `'chrome'` overlay color at 40% opacity. The selection comes off the draw envelope. */\nexport function useEffectSourcesLayer<TData, TLayer extends string, TPose>(\n scene: Scene<TData, TLayer, TPose>,\n): RenderLayer<unknown> {\n const sceneRef = useLatest(scene);\n return useMemo<RenderLayer<unknown>>(() => ({\n id: 'effect-sources',\n label: 'Hidden effect sources',\n space: 'world',\n draw: (data) => {\n const selected = chromeStateFrom(data)?.selection ?? [];\n const sc = sceneRef.current;\n const stroke = PATH_OVERLAY_ROLES['chrome']!.stroke!;\n const depOf = sceneDepLookup(sc);\n const lines: DrawCommand[] = [];\n for (const id of selected) {\n const node = sc.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 (!sc.isHidden(source)) continue;\n const dep = depOf(source);\n const path = dep === undefined ? null : readEffectSource(dep as never)?.world ?? null;\n if (path !== null) lines.push({ kind: 'path', path, stroke: { paint: stroke, width: { px: 1 } } });\n }\n }\n return lines.length === 0 ? [] : [{ kind: 'group', alpha: FAINT, children: lines }];\n },\n }), [sceneRef]);\n}\n"],"mappings":";;;;;;;;;;;;;AAeA,IAAM,QAAQ;;;AAId,SAAgB,sBACd,OACsB;CACtB,MAAM,WAAW,UAAU,KAAK;CAChC,OAAO,eAAqC;EAC1C,IAAI;EACJ,OAAO;EACP,OAAO;EACP,OAAO,SAAS;GACd,MAAM,WAAW,gBAAgB,IAAI,CAAC,EAAE,aAAa,CAAC;GACtD,MAAM,KAAK,SAAS;GACpB,MAAM,SAAS,mBAAmB,SAAS,CAAE;GAC7C,MAAM,QAAQ,eAAe,EAAE;GAC/B,MAAM,QAAuB,CAAC;GAC9B,KAAK,MAAM,MAAM,UAAU;IACzB,MAAM,OAAO,GAAG,IAAI,EAAE;IACtB,IAAI,aAAa,IAAI,CAAC,EAAE,gBAAgB,QAAQ,CAAC,MAAM,QAAQ,KAAM,SAAS,GAAG;IACjF,KAAK,MAAM,UAAU,KAAM,WAAgC;KACzD,IAAI,CAAC,GAAG,SAAS,MAAM,GAAG;KAC1B,MAAM,MAAM,MAAM,MAAM;KACxB,MAAM,OAAO,QAAQ,SAAY,OAAO,iBAAiB,GAAY,CAAC,EAAE,SAAS;KACjF,IAAI,SAAS,MAAM,MAAM,KAAK;MAAE,MAAM;MAAQ;MAAM,QAAQ;OAAE,OAAO;OAAQ,OAAO,EAAE,IAAI,EAAE;MAAE;KAAE,CAAC;IACnG;GACF;GACA,OAAO,MAAM,WAAW,IAAI,CAAC,IAAI,CAAC;IAAE,MAAM;IAAS,OAAO;IAAO,UAAU;GAAM,CAAC;EACpF;CACF,IAAI,CAAC,QAAQ,CAAC;AAChB"}
1
+ {"version":3,"file":"useEffectSourcesLayer.js","names":[],"sources":["../../../src/features/pathEffects/useEffectSourcesLayer.ts"],"sourcesContent":["/**\n * The layer that shows what a selected path effect was made from: the outlines\n * of the sources it hides, drawn faintly under the selection chrome.\n */\nimport { useMemo } from 'react';\nimport { useLatest } from '@weasel-js/react';\nimport type { DrawCommand } from '@weasel-js/renderer';\nimport { PATH_OVERLAY_ROLES } from 'canvas/pathOverlay';\nimport { sceneDepLookup } from 'canvas/derivedPath';\nimport { chromeStateFrom } from 'canvas/drawEnvelope';\nimport type { RenderLayer } from 'core/layers/render';\nimport { sourcesHiddenBy } from './effectSources';\nimport type { NodeId, Scene } from 'core/scene/types';\nimport { readEffectSource } from './readSource';\n\nconst FAINT = 0.4;\n\n/** For each selected path-effect node, the outlines of the sources it holds hidden (`sourcesHiddenBy`) as the effect\n * reads them, in a one-screen-pixel stroke of the `'chrome'` overlay color at 40% opacity. The selection comes off the draw envelope. */\nexport function useEffectSourcesLayer<TData, TLayer extends string, TPose>(\n scene: Scene<TData, TLayer, TPose>,\n): RenderLayer<unknown> {\n const sceneRef = useLatest(scene);\n return useMemo<RenderLayer<unknown>>(() => ({\n id: 'effect-sources',\n label: 'Hidden effect sources',\n space: 'world',\n draw: (data) => {\n const selected = chromeStateFrom(data)?.selection ?? [];\n const sc = sceneRef.current;\n const stroke = PATH_OVERLAY_ROLES['chrome']!.stroke!;\n const depOf = sceneDepLookup(sc);\n const lines: DrawCommand[] = [];\n for (const id of selected) {\n for (const source of sourcesHiddenBy(sc, id as NodeId)) {\n if (!sc.isHidden(source)) continue;\n const dep = depOf(source);\n const path = dep === undefined ? null : readEffectSource(dep as never)?.world ?? null;\n if (path !== null) lines.push({ kind: 'path', path, stroke: { paint: stroke, width: { px: 1 } } });\n }\n }\n return lines.length === 0 ? [] : [{ kind: 'group', alpha: FAINT, children: lines }];\n },\n }), [sceneRef]);\n}\n"],"mappings":";;;;;;;;;;;;;AAeA,IAAM,QAAQ;;;AAId,SAAgB,sBACd,OACsB;CACtB,MAAM,WAAW,UAAU,KAAK;CAChC,OAAO,eAAqC;EAC1C,IAAI;EACJ,OAAO;EACP,OAAO;EACP,OAAO,SAAS;GACd,MAAM,WAAW,gBAAgB,IAAI,CAAC,EAAE,aAAa,CAAC;GACtD,MAAM,KAAK,SAAS;GACpB,MAAM,SAAS,mBAAmB,SAAS,CAAE;GAC7C,MAAM,QAAQ,eAAe,EAAE;GAC/B,MAAM,QAAuB,CAAC;GAC9B,KAAK,MAAM,MAAM,UACf,KAAK,MAAM,UAAU,gBAAgB,IAAI,EAAY,GAAG;IACtD,IAAI,CAAC,GAAG,SAAS,MAAM,GAAG;IAC1B,MAAM,MAAM,MAAM,MAAM;IACxB,MAAM,OAAO,QAAQ,SAAY,OAAO,iBAAiB,GAAY,CAAC,EAAE,SAAS;IACjF,IAAI,SAAS,MAAM,MAAM,KAAK;KAAE,MAAM;KAAQ;KAAM,QAAQ;MAAE,OAAO;MAAQ,OAAO,EAAE,IAAI,EAAE;KAAE;IAAE,CAAC;GACnG;GAEF,OAAO,MAAM,WAAW,IAAI,CAAC,IAAI,CAAC;IAAE,MAAM;IAAS,OAAO;IAAO,UAAU;GAAM,CAAC;EACpF;CACF,IAAI,CAAC,QAAQ,CAAC;AAChB"}
@@ -17,7 +17,7 @@ import { asPolygonPath, pathInWorld, rectPath, worldEditToStorage } from "@wease
17
17
  /** Whether `node` is a leaf whose only geometry is its rect pose, drawn by
18
18
  * the kit's rect fallback. */
19
19
  function drawsPoseAlone(node) {
20
- if (node.kind !== "leaf" || node.data?.path != null) return false;
20
+ if (node.kind !== "mark" || node.data?.path != null) return false;
21
21
  return findNodeShape(node) === RECT_FALLBACK_PAINTER;
22
22
  }
23
23
  /** Resolve where a node's editable polygon lives. Returns null when the
@@ -1 +1 @@
1
- {"version":3,"file":"editablePath.js","names":[],"sources":["../../../src/features/paths/editablePath.ts"],"sourcesContent":["/**\n * A node's editable polygon. It lives in one of two places: the pose itself\n * (`pose.kind === 'polygon'`), or `data.path` with a rect pose, stored pose-local.\n * A rect `data.path` is edited as its four corners and is stored as a polygon\n * from the first edit on; reading it changes nothing. A leaf drawn as its rect\n * pose alone reads as a rect `data.path` the size of its pose, and gains one\n * on the first edit.\n */\nimport { asPolygonPath, pathInWorld, rectPath, worldEditToStorage, type Path, type PolygonPath } from '@weasel-js/geom';\nimport type { GesturePreviewSource } from 'canvas/gestureBounds';\nimport { findNodeShape } from 'canvas/NodeShape';\nimport { RECT_FALLBACK_PAINTER } from 'canvas/nodeShapes/rectFallbackPainter';\nimport { pathFromPlane } from 'canvas/planeClips';\nimport { isRectPose } from 'core/geometry/autoPoseDescriptor';\nimport type { RectPose } from 'core/scene/types';\nimport type { PlaneMap } from 'core/viewport/parallax';\nimport type { PathEditExtension } from './pathEditExtension';\nimport { pathFormOf, representationOf, withoutPathForm, type AnchorMarkerShape } from './pathRepresentation';\n\n/** The fields of a node the editable path is read from. A node's `kind`\n * says whether a rect pose with no path is a leaf drawn as that rect. */\nexport interface EditableShape {\n pose: unknown;\n data: unknown;\n kind?: string;\n}\n\n/** Whether `node` is a leaf whose only geometry is its rect pose, drawn by\n * the kit's rect fallback. */\nfunction drawsPoseAlone(node: EditableShape): boolean {\n if (node.kind !== 'leaf' || (node.data as { path?: unknown } | null)?.path != null) return false;\n return findNodeShape(node as never) === RECT_FALLBACK_PAINTER;\n}\n\n/** Resolve where a node's editable polygon lives. Returns null when the\n * node has neither a polygon pose nor a polygon or rect on `data.path`.\n * A rect comes back as the polygon it draws. */\nexport function classifyPathStorage(\n node: EditableShape | undefined | null,\n): { kind: 'pose' } | { kind: 'data'; pose: RectPose; data: { path: PolygonPath } } | null {\n if (!node) return null;\n const pose = node.pose as { kind?: string } | undefined;\n if (pose?.kind === 'polygon') return { kind: 'pose' };\n if (isRectPose(node.pose) && drawsPoseAlone(node)) {\n const { width, height } = node.pose;\n return { kind: 'data', pose: node.pose, data: { ...(node.data as object), path: asPolygonPath(rectPath(0, 0, width, height)) } };\n }\n const data = node.data as { path?: Path } | null;\n const kind = (data?.path as { kind?: string } | undefined)?.kind;\n if (!data?.path || (kind !== 'polygon' && kind !== 'rect') || !isRectPose(node.pose)) return null;\n const path = asPolygonPath(data.path);\n return {\n kind: 'data',\n pose: node.pose,\n data: (path === data.path ? data : { ...data, path }) as { path: PolygonPath },\n };\n}\n\n/** Whether `node` has a path that path edit can show anchors for. */\nexport function hasEditablePath(node: EditableShape | undefined | null): boolean {\n return classifyPathStorage(node) !== null;\n}\n\n/** A node's editable polygon in world coords. Does NOT consult any preview state —\n * callers that want live state read in-flight handles themselves. */\nexport function resolveEditablePathOf(\n node: EditableShape | undefined | null,\n): PolygonPath | null {\n if (!node) return null;\n const storage = classifyPathStorage(node);\n if (!storage) return null;\n // Pose-as-polygon: the pose IS the polygon (polygon poses can't carry\n // rotation), so it's already world.\n if (storage.kind === 'pose') return (node as { pose: PolygonPath }).pose;\n // data.path: translate to the pose origin AND bake `pose.rotation`, so anchors render on\n // the rotated contour the renderer draws.\n return pathInWorld(storage.data.path, storage.pose) as PolygonPath;\n}\n\n/**\n * What path edit edits on a node, in world coords: the edit path of the form\n * the node's path was drawn from, where one of `extensions` reads it\n * (`PathRepresentation`), and otherwise its editable polygon. Null when the\n * node has no editable polygon.\n */\nexport function pathEditPathOf(\n node: EditableShape | undefined | null,\n extensions: readonly PathEditExtension[] = [],\n): PolygonPath | null {\n const path = resolveEditablePathOf(node);\n const held = path ? representationOf(extensions, node!.data) : null;\n return (held && held.representation.editPath(path!, held.form)) ?? path;\n}\n\n/**\n * The pose and data that store `worldPath` where `node` stores its editable\n * polygon — the inverse of {@link resolveEditablePathOf}, so a `data.path`\n * node keeps its rotation. On a node whose path was drawn from a form one of\n * `extensions` reads, `worldPath` is an edit of what {@link pathEditPathOf}\n * gave, and the representation turns it into the drawn path; a form none\n * reads, or that no longer describes the path, is dropped. Null when `node`\n * has no editable polygon.\n */\nexport function storeEditablePath(\n node: EditableShape | undefined | null,\n worldPath: PolygonPath,\n extensions: readonly PathEditExtension[] = [],\n): EditableShape | null {\n const storage = classifyPathStorage(node);\n if (!storage) return null;\n let path = worldPath;\n let data = node!.data;\n if (pathFormOf(data)) {\n const held = representationOf(extensions, data);\n const solved = held ? held.representation.store(resolveEditablePathOf(node)!, held.form, worldPath) : null;\n path = solved?.path ?? worldPath;\n data = solved ? { ...(data as object), representation: solved.form } : withoutPathForm(data);\n }\n if (storage.kind === 'pose') return { pose: path, data };\n const stored = worldEditToStorage(storage.pose, path);\n return { pose: stored.pose, data: { ...(data as object), path: stored.path } };\n}\n\n/** `node` as the in-flight gestures in `previews` would leave it: a handle\n * previewing `id` contributes its pose and its data. */\nfunction previewedShape(\n id: string,\n node: EditableShape,\n previews: readonly GesturePreviewSource[],\n): EditableShape {\n let pose = node.pose;\n let data = node.data;\n let touched = false;\n const kind = node.kind;\n for (const source of previews) {\n const previewIds = source.previewIds?.();\n if (!previewIds) continue;\n let owns = false;\n for (const pid of previewIds) {\n if (pid === id) { owns = true; break; }\n }\n if (!owns) continue;\n const p = source.previewPose?.(id);\n if (p != null && !touched) { pose = p; touched = true; }\n const d = source.previewData?.(id);\n if (d != null) { data = d; touched = true; }\n }\n return { pose, data, kind };\n}\n\n/**\n * What path edit shows of node `id` right now ({@link pathEditPathOf}): following any\n * in-flight gesture in `previews`, and in the world of the camera whose\n * `plane` this is — a path on a parallax plane is stored in the plane's.\n */\nexport function liveEditablePath(\n id: string,\n node: EditableShape | undefined | null,\n previews: readonly GesturePreviewSource[],\n plane: PlaneMap | null,\n extensions: readonly PathEditExtension[] = [],\n): PolygonPath | null {\n if (!node) return null;\n return pathFromPlane(pathEditPathOf(previewedShape(id, node, previews), extensions), plane);\n}\n\n/** The marker of each anchor {@link liveEditablePath} shows for node `id`, as\n * the representation reading its form draws them; null for a node it reads\n * none of. */\nexport function liveAnchorMarkers(\n id: string,\n node: EditableShape | undefined | null,\n previews: readonly GesturePreviewSource[],\n extensions: readonly PathEditExtension[],\n): readonly (AnchorMarkerShape | null)[] | null {\n if (!node) return null;\n const held = representationOf(extensions, previewedShape(id, node, previews).data);\n return held?.representation.anchorMarkers?.(held.form) ?? null;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"editablePath.js","names":[],"sources":["../../../src/features/paths/editablePath.ts"],"sourcesContent":["/**\n * A node's editable polygon. It lives in one of two places: the pose itself\n * (`pose.kind === 'polygon'`), or `data.path` with a rect pose, stored pose-local.\n * A rect `data.path` is edited as its four corners and is stored as a polygon\n * from the first edit on; reading it changes nothing. A leaf drawn as its rect\n * pose alone reads as a rect `data.path` the size of its pose, and gains one\n * on the first edit.\n */\nimport { asPolygonPath, pathInWorld, rectPath, worldEditToStorage, type Path, type PolygonPath } from '@weasel-js/geom';\nimport type { GesturePreviewSource } from 'canvas/gestureBounds';\nimport { findNodeShape } from 'canvas/NodeShape';\nimport { RECT_FALLBACK_PAINTER } from 'canvas/nodeShapes/rectFallbackPainter';\nimport { pathFromPlane } from 'canvas/planeClips';\nimport { isRectPose } from 'core/geometry/autoPoseDescriptor';\nimport type { RectPose } from 'core/scene/types';\nimport type { PlaneMap } from 'core/viewport/parallax';\nimport type { PathEditExtension } from './pathEditExtension';\nimport { pathFormOf, representationOf, withoutPathForm, type AnchorMarkerShape } from './pathRepresentation';\n\n/** The fields of a node the editable path is read from. A node's `kind`\n * says whether a rect pose with no path is a leaf drawn as that rect. */\nexport interface EditableShape {\n pose: unknown;\n data: unknown;\n kind?: string;\n}\n\n/** Whether `node` is a leaf whose only geometry is its rect pose, drawn by\n * the kit's rect fallback. */\nfunction drawsPoseAlone(node: EditableShape): boolean {\n if (node.kind !== 'mark' || (node.data as { path?: unknown } | null)?.path != null) return false;\n return findNodeShape(node as never) === RECT_FALLBACK_PAINTER;\n}\n\n/** Resolve where a node's editable polygon lives. Returns null when the\n * node has neither a polygon pose nor a polygon or rect on `data.path`.\n * A rect comes back as the polygon it draws. */\nexport function classifyPathStorage(\n node: EditableShape | undefined | null,\n): { kind: 'pose' } | { kind: 'data'; pose: RectPose; data: { path: PolygonPath } } | null {\n if (!node) return null;\n const pose = node.pose as { kind?: string } | undefined;\n if (pose?.kind === 'polygon') return { kind: 'pose' };\n if (isRectPose(node.pose) && drawsPoseAlone(node)) {\n const { width, height } = node.pose;\n return { kind: 'data', pose: node.pose, data: { ...(node.data as object), path: asPolygonPath(rectPath(0, 0, width, height)) } };\n }\n const data = node.data as { path?: Path } | null;\n const kind = (data?.path as { kind?: string } | undefined)?.kind;\n if (!data?.path || (kind !== 'polygon' && kind !== 'rect') || !isRectPose(node.pose)) return null;\n const path = asPolygonPath(data.path);\n return {\n kind: 'data',\n pose: node.pose,\n data: (path === data.path ? data : { ...data, path }) as { path: PolygonPath },\n };\n}\n\n/** Whether `node` has a path that path edit can show anchors for. */\nexport function hasEditablePath(node: EditableShape | undefined | null): boolean {\n return classifyPathStorage(node) !== null;\n}\n\n/** A node's editable polygon in world coords. Does NOT consult any preview state —\n * callers that want live state read in-flight handles themselves. */\nexport function resolveEditablePathOf(\n node: EditableShape | undefined | null,\n): PolygonPath | null {\n if (!node) return null;\n const storage = classifyPathStorage(node);\n if (!storage) return null;\n // Pose-as-polygon: the pose IS the polygon (polygon poses can't carry\n // rotation), so it's already world.\n if (storage.kind === 'pose') return (node as { pose: PolygonPath }).pose;\n // data.path: translate to the pose origin AND bake `pose.rotation`, so anchors render on\n // the rotated contour the renderer draws.\n return pathInWorld(storage.data.path, storage.pose) as PolygonPath;\n}\n\n/**\n * What path edit edits on a node, in world coords: the edit path of the form\n * the node's path was drawn from, where one of `extensions` reads it\n * (`PathRepresentation`), and otherwise its editable polygon. Null when the\n * node has no editable polygon.\n */\nexport function pathEditPathOf(\n node: EditableShape | undefined | null,\n extensions: readonly PathEditExtension[] = [],\n): PolygonPath | null {\n const path = resolveEditablePathOf(node);\n const held = path ? representationOf(extensions, node!.data) : null;\n return (held && held.representation.editPath(path!, held.form)) ?? path;\n}\n\n/**\n * The pose and data that store `worldPath` where `node` stores its editable\n * polygon — the inverse of {@link resolveEditablePathOf}, so a `data.path`\n * node keeps its rotation. On a node whose path was drawn from a form one of\n * `extensions` reads, `worldPath` is an edit of what {@link pathEditPathOf}\n * gave, and the representation turns it into the drawn path; a form none\n * reads, or that no longer describes the path, is dropped. Null when `node`\n * has no editable polygon.\n */\nexport function storeEditablePath(\n node: EditableShape | undefined | null,\n worldPath: PolygonPath,\n extensions: readonly PathEditExtension[] = [],\n): EditableShape | null {\n const storage = classifyPathStorage(node);\n if (!storage) return null;\n let path = worldPath;\n let data = node!.data;\n if (pathFormOf(data)) {\n const held = representationOf(extensions, data);\n const solved = held ? held.representation.store(resolveEditablePathOf(node)!, held.form, worldPath) : null;\n path = solved?.path ?? worldPath;\n data = solved ? { ...(data as object), representation: solved.form } : withoutPathForm(data);\n }\n if (storage.kind === 'pose') return { pose: path, data };\n const stored = worldEditToStorage(storage.pose, path);\n return { pose: stored.pose, data: { ...(data as object), path: stored.path } };\n}\n\n/** `node` as the in-flight gestures in `previews` would leave it: a handle\n * previewing `id` contributes its pose and its data. */\nfunction previewedShape(\n id: string,\n node: EditableShape,\n previews: readonly GesturePreviewSource[],\n): EditableShape {\n let pose = node.pose;\n let data = node.data;\n let touched = false;\n const kind = node.kind;\n for (const source of previews) {\n const previewIds = source.previewIds?.();\n if (!previewIds) continue;\n let owns = false;\n for (const pid of previewIds) {\n if (pid === id) { owns = true; break; }\n }\n if (!owns) continue;\n const p = source.previewPose?.(id);\n if (p != null && !touched) { pose = p; touched = true; }\n const d = source.previewData?.(id);\n if (d != null) { data = d; touched = true; }\n }\n return { pose, data, kind };\n}\n\n/**\n * What path edit shows of node `id` right now ({@link pathEditPathOf}): following any\n * in-flight gesture in `previews`, and in the world of the camera whose\n * `plane` this is — a path on a parallax plane is stored in the plane's.\n */\nexport function liveEditablePath(\n id: string,\n node: EditableShape | undefined | null,\n previews: readonly GesturePreviewSource[],\n plane: PlaneMap | null,\n extensions: readonly PathEditExtension[] = [],\n): PolygonPath | null {\n if (!node) return null;\n return pathFromPlane(pathEditPathOf(previewedShape(id, node, previews), extensions), plane);\n}\n\n/** The marker of each anchor {@link liveEditablePath} shows for node `id`, as\n * the representation reading its form draws them; null for a node it reads\n * none of. */\nexport function liveAnchorMarkers(\n id: string,\n node: EditableShape | undefined | null,\n previews: readonly GesturePreviewSource[],\n extensions: readonly PathEditExtension[],\n): readonly (AnchorMarkerShape | null)[] | null {\n if (!node) return null;\n const held = representationOf(extensions, previewedShape(id, node, previews).data);\n return held?.representation.anchorMarkers?.(held.form) ?? null;\n}\n"],"mappings":"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"}
@@ -29,7 +29,7 @@ function createSceneSnapIndexer(scene) {
29
29
  const paths = [];
30
30
  const extra = [];
31
31
  for (const node of scene.nodes.values()) {
32
- if (node.kind !== "leaf" || notes.has(node.layer) || gates.hidden(node.id)) continue;
32
+ if (node.kind !== "mark" || notes.has(node.layer) || gates.hidden(node.id)) continue;
33
33
  const g = nodeSnapGeometry(node);
34
34
  if (!g) continue;
35
35
  for (const p of g.points) extra.push({
@@ -1 +1 @@
1
- {"version":3,"file":"sceneIndex.js","names":[],"sources":["../../../../src/features/paths/pathSnap/sceneIndex.ts"],"sourcesContent":["import {\n createPathSnapIndex, type Path, type PathSnapIndex, type PathSnapTarget,\n} from '@weasel-js/geom';\nimport type { Scene } from 'core/scene/types';\nimport { isAnnotationLayer } from 'core/scene/annotation';\nimport { nodeGates } from 'core/scene/nodeGates';\nimport type { SnapGeometry } from 'interactions/actions/snapDep';\nimport { nodeSnapGeometry } from './snapGeometry';\n\ntype SnapScene = Pick<Scene<unknown, string, unknown>, 'nodes' | 'layers' | 'getVersion'>;\n\n/** A scene's paths indexed for snapping; `ids[i]` is the node path `i` came\n * from, or null for a path of the query's own (`also`). */\nexport interface SceneSnapIndex {\n ids: (string | null)[];\n index: PathSnapIndex;\n}\n\n/** Indexes every leaf of `scene` with geometry (`nodeSnapGeometry`), but\n * hidden ones (`nodeGates`) and those on an annotation layer, once per scene\n * version, and again when a query brings geometry of its own. The scene\n * alone and the scene with the last `also` are cached apart, so a query\n * without geometry does not evict a gesture's. */\nexport function createSceneSnapIndexer(scene: SnapScene): (also?: SnapGeometry) => SceneSnapIndex {\n type Slot = { version: number; layers: unknown; also: SnapGeometry | undefined; built: SceneSnapIndex };\n const slots: { bare: Slot | null; also: Slot | null } = { bare: null, also: null };\n return (also) => {\n const version = scene.getVersion();\n const layers = scene.layers;\n const key = also ? 'also' : 'bare';\n const cached = slots[key];\n if (cached?.version === version && cached.layers === layers && cached.also === also) return cached.built;\n const notes = new Set(layers.filter(isAnnotationLayer).map((l) => l.id));\n const gates = nodeGates({ layers, get: (id) => scene.nodes.get(id) });\n const ids: (string | null)[] = [];\n const paths: Path[] = [];\n const extra: PathSnapTarget[] = [];\n for (const node of scene.nodes.values()) {\n if (node.kind !== 'leaf' || notes.has(node.layer) || gates.hidden(node.id)) continue;\n const g = nodeSnapGeometry(node);\n if (!g) continue;\n for (const p of g.points) extra.push({ ...p, paths: [paths.length] });\n ids.push(node.id);\n paths.push(g.path);\n }\n for (const path of also?.paths ?? []) { ids.push(null); paths.push(path); }\n if (also?.anchors) {\n ids.push(null);\n for (const p of also.anchors) extra.push({ kind: 'anchor', x: p.x, y: p.y, paths: [paths.length] });\n paths.push({ kind: 'polygon', commands: new Uint8Array(), coords: new Float32Array(), fillRule: 'nonzero' });\n }\n const built = { ids, index: createPathSnapIndex(paths, extra) };\n slots[key] = { version, layers, also, built };\n return built;\n };\n}\n\nconst indexers = new WeakMap<object, ReturnType<typeof createSceneSnapIndexer>>();\n\n/** {@link createSceneSnapIndexer}, one per scene, so every source over a\n * scene shares its index. */\nexport function sceneSnapIndex(scene: SnapScene, also?: SnapGeometry): SceneSnapIndex {\n let indexer = indexers.get(scene);\n if (!indexer) indexers.set(scene, (indexer = createSceneSnapIndexer(scene)));\n return indexer(also);\n}\n"],"mappings":";;;;;;;;;;;AAuBA,SAAgB,uBAAuB,OAA2D;CAEhG,MAAM,QAAkD;EAAE,MAAM;EAAM,MAAM;CAAK;CACjF,QAAQ,SAAS;EACf,MAAM,UAAU,MAAM,WAAW;EACjC,MAAM,SAAS,MAAM;EACrB,MAAM,MAAM,OAAO,SAAS;EAC5B,MAAM,SAAS,MAAM;EACrB,IAAI,QAAQ,YAAY,WAAW,OAAO,WAAW,UAAU,OAAO,SAAS,MAAM,OAAO,OAAO;EACnG,MAAM,QAAQ,IAAI,IAAI,OAAO,OAAO,iBAAiB,CAAC,CAAC,KAAK,MAAM,EAAE,EAAE,CAAC;EACvE,MAAM,QAAQ,UAAU;GAAE;GAAQ,MAAM,OAAO,MAAM,MAAM,IAAI,EAAE;EAAE,CAAC;EACpE,MAAM,MAAyB,CAAC;EAChC,MAAM,QAAgB,CAAC;EACvB,MAAM,QAA0B,CAAC;EACjC,KAAK,MAAM,QAAQ,MAAM,MAAM,OAAO,GAAG;GACvC,IAAI,KAAK,SAAS,UAAU,MAAM,IAAI,KAAK,KAAK,KAAK,MAAM,OAAO,KAAK,EAAE,GAAG;GAC5E,MAAM,IAAI,iBAAiB,IAAI;GAC/B,IAAI,CAAC,GAAG;GACR,KAAK,MAAM,KAAK,EAAE,QAAQ,MAAM,KAAK;IAAE,GAAG;IAAG,OAAO,CAAC,MAAM,MAAM;GAAE,CAAC;GACpE,IAAI,KAAK,KAAK,EAAE;GAChB,MAAM,KAAK,EAAE,IAAI;EACnB;EACA,KAAK,MAAM,QAAQ,MAAM,SAAS,CAAC,GAAG;GAAE,IAAI,KAAK,IAAI;GAAG,MAAM,KAAK,IAAI;EAAG;EAC1E,IAAI,MAAM,SAAS;GACjB,IAAI,KAAK,IAAI;GACb,KAAK,MAAM,KAAK,KAAK,SAAS,MAAM,KAAK;IAAE,MAAM;IAAU,GAAG,EAAE;IAAG,GAAG,EAAE;IAAG,OAAO,CAAC,MAAM,MAAM;GAAE,CAAC;GAClG,MAAM,KAAK;IAAE,MAAM;IAAW,0BAAU,IAAI,WAAW;IAAG,wBAAQ,IAAI,aAAa;IAAG,UAAU;GAAU,CAAC;EAC7G;EACA,MAAM,QAAQ;GAAE;GAAK,OAAO,oBAAoB,OAAO,KAAK;EAAE;EAC9D,MAAM,OAAO;GAAE;GAAS;GAAQ;GAAM;EAAM;EAC5C,OAAO;CACT;AACF;AAEA,IAAM,2BAAW,IAAI,QAA2D;;;AAIhF,SAAgB,eAAe,OAAkB,MAAqC;CACpF,IAAI,UAAU,SAAS,IAAI,KAAK;CAChC,IAAI,CAAC,SAAS,SAAS,IAAI,OAAQ,UAAU,uBAAuB,KAAK,CAAE;CAC3E,OAAO,QAAQ,IAAI;AACrB"}
1
+ {"version":3,"file":"sceneIndex.js","names":[],"sources":["../../../../src/features/paths/pathSnap/sceneIndex.ts"],"sourcesContent":["import {\n createPathSnapIndex, type Path, type PathSnapIndex, type PathSnapTarget,\n} from '@weasel-js/geom';\nimport type { Scene } from 'core/scene/types';\nimport { isAnnotationLayer } from 'core/scene/annotation';\nimport { nodeGates } from 'core/scene/nodeGates';\nimport type { SnapGeometry } from 'interactions/actions/snapDep';\nimport { nodeSnapGeometry } from './snapGeometry';\n\ntype SnapScene = Pick<Scene<unknown, string, unknown>, 'nodes' | 'layers' | 'getVersion'>;\n\n/** A scene's paths indexed for snapping; `ids[i]` is the node path `i` came\n * from, or null for a path of the query's own (`also`). */\nexport interface SceneSnapIndex {\n ids: (string | null)[];\n index: PathSnapIndex;\n}\n\n/** Indexes every leaf of `scene` with geometry (`nodeSnapGeometry`), but\n * hidden ones (`nodeGates`) and those on an annotation layer, once per scene\n * version, and again when a query brings geometry of its own. The scene\n * alone and the scene with the last `also` are cached apart, so a query\n * without geometry does not evict a gesture's. */\nexport function createSceneSnapIndexer(scene: SnapScene): (also?: SnapGeometry) => SceneSnapIndex {\n type Slot = { version: number; layers: unknown; also: SnapGeometry | undefined; built: SceneSnapIndex };\n const slots: { bare: Slot | null; also: Slot | null } = { bare: null, also: null };\n return (also) => {\n const version = scene.getVersion();\n const layers = scene.layers;\n const key = also ? 'also' : 'bare';\n const cached = slots[key];\n if (cached?.version === version && cached.layers === layers && cached.also === also) return cached.built;\n const notes = new Set(layers.filter(isAnnotationLayer).map((l) => l.id));\n const gates = nodeGates({ layers, get: (id) => scene.nodes.get(id) });\n const ids: (string | null)[] = [];\n const paths: Path[] = [];\n const extra: PathSnapTarget[] = [];\n for (const node of scene.nodes.values()) {\n if (node.kind !== 'mark' || notes.has(node.layer) || gates.hidden(node.id)) continue;\n const g = nodeSnapGeometry(node);\n if (!g) continue;\n for (const p of g.points) extra.push({ ...p, paths: [paths.length] });\n ids.push(node.id);\n paths.push(g.path);\n }\n for (const path of also?.paths ?? []) { ids.push(null); paths.push(path); }\n if (also?.anchors) {\n ids.push(null);\n for (const p of also.anchors) extra.push({ kind: 'anchor', x: p.x, y: p.y, paths: [paths.length] });\n paths.push({ kind: 'polygon', commands: new Uint8Array(), coords: new Float32Array(), fillRule: 'nonzero' });\n }\n const built = { ids, index: createPathSnapIndex(paths, extra) };\n slots[key] = { version, layers, also, built };\n return built;\n };\n}\n\nconst indexers = new WeakMap<object, ReturnType<typeof createSceneSnapIndexer>>();\n\n/** {@link createSceneSnapIndexer}, one per scene, so every source over a\n * scene shares its index. */\nexport function sceneSnapIndex(scene: SnapScene, also?: SnapGeometry): SceneSnapIndex {\n let indexer = indexers.get(scene);\n if (!indexer) indexers.set(scene, (indexer = createSceneSnapIndexer(scene)));\n return indexer(also);\n}\n"],"mappings":";;;;;;;;;;;AAuBA,SAAgB,uBAAuB,OAA2D;CAEhG,MAAM,QAAkD;EAAE,MAAM;EAAM,MAAM;CAAK;CACjF,QAAQ,SAAS;EACf,MAAM,UAAU,MAAM,WAAW;EACjC,MAAM,SAAS,MAAM;EACrB,MAAM,MAAM,OAAO,SAAS;EAC5B,MAAM,SAAS,MAAM;EACrB,IAAI,QAAQ,YAAY,WAAW,OAAO,WAAW,UAAU,OAAO,SAAS,MAAM,OAAO,OAAO;EACnG,MAAM,QAAQ,IAAI,IAAI,OAAO,OAAO,iBAAiB,CAAC,CAAC,KAAK,MAAM,EAAE,EAAE,CAAC;EACvE,MAAM,QAAQ,UAAU;GAAE;GAAQ,MAAM,OAAO,MAAM,MAAM,IAAI,EAAE;EAAE,CAAC;EACpE,MAAM,MAAyB,CAAC;EAChC,MAAM,QAAgB,CAAC;EACvB,MAAM,QAA0B,CAAC;EACjC,KAAK,MAAM,QAAQ,MAAM,MAAM,OAAO,GAAG;GACvC,IAAI,KAAK,SAAS,UAAU,MAAM,IAAI,KAAK,KAAK,KAAK,MAAM,OAAO,KAAK,EAAE,GAAG;GAC5E,MAAM,IAAI,iBAAiB,IAAI;GAC/B,IAAI,CAAC,GAAG;GACR,KAAK,MAAM,KAAK,EAAE,QAAQ,MAAM,KAAK;IAAE,GAAG;IAAG,OAAO,CAAC,MAAM,MAAM;GAAE,CAAC;GACpE,IAAI,KAAK,KAAK,EAAE;GAChB,MAAM,KAAK,EAAE,IAAI;EACnB;EACA,KAAK,MAAM,QAAQ,MAAM,SAAS,CAAC,GAAG;GAAE,IAAI,KAAK,IAAI;GAAG,MAAM,KAAK,IAAI;EAAG;EAC1E,IAAI,MAAM,SAAS;GACjB,IAAI,KAAK,IAAI;GACb,KAAK,MAAM,KAAK,KAAK,SAAS,MAAM,KAAK;IAAE,MAAM;IAAU,GAAG,EAAE;IAAG,GAAG,EAAE;IAAG,OAAO,CAAC,MAAM,MAAM;GAAE,CAAC;GAClG,MAAM,KAAK;IAAE,MAAM;IAAW,0BAAU,IAAI,WAAW;IAAG,wBAAQ,IAAI,aAAa;IAAG,UAAU;GAAU,CAAC;EAC7G;EACA,MAAM,QAAQ;GAAE;GAAK,OAAO,oBAAoB,OAAO,KAAK;EAAE;EAC9D,MAAM,OAAO;GAAE;GAAS;GAAQ;GAAM;EAAM;EAC5C,OAAO;CACT;AACF;AAEA,IAAM,2BAAW,IAAI,QAA2D;;;AAIhF,SAAgB,eAAe,OAAkB,MAAqC;CACpF,IAAI,UAAU,SAAS,IAAI,KAAK;CAChC,IAAI,CAAC,SAAS,SAAS,IAAI,OAAQ,UAAU,uBAAuB,KAAK,CAAE;CAC3E,OAAO,QAAQ,IAAI;AACrB"}
@@ -0,0 +1,65 @@
1
+ import { viewToTransform } from "../../core/viewport/view.js";
2
+ import { worldToScreen } from "../../core/viewport/viewTransform.js";
3
+ import { rectPathFor } from "./handleCommands.js";
4
+ import { useMemo } from "react";
5
+
6
+ //#region src/features/selection/enteredGroupFrame.ts
7
+ /** The theme token the frame reads its color from, set on the canvas or any element above it. */
8
+ var ENTERED_FRAME_TOKEN = "--wzl-entered-group";
9
+ /** The frame's color where nothing sets {@link ENTERED_FRAME_TOKEN}. */
10
+ var ENTERED_FRAME_COLOR = "#a48bd4";
11
+ var FRAME_WIDTH = 1;
12
+ var FRAME_DASH = [6, 4];
13
+ /**
14
+ * The frame around the group a canvas is working inside: one dashed outline on the group's
15
+ * box, in the selection chrome's own screen space, repainting when the entered group changes.
16
+ */
17
+ function createEnteredGroupFrameLayer(opts) {
18
+ return {
19
+ id: "entered-group-frame",
20
+ label: "Entered group",
21
+ space: "screen",
22
+ subscribe: (repaint) => {
23
+ const offGroup = opts.entered.subscribe(repaint);
24
+ const offColor = opts.colorChanged?.(repaint);
25
+ return () => {
26
+ offGroup();
27
+ offColor?.();
28
+ };
29
+ },
30
+ draw: (_data, view) => {
31
+ const id = opts.entered.get();
32
+ const b = id === null ? null : opts.boundsOf(id);
33
+ if (b === null) return [];
34
+ const [x, y] = worldToScreen(b.x, b.y, viewToTransform(view));
35
+ return [{
36
+ kind: "path",
37
+ path: rectPathFor(x, y, b.width * view.scale.x, b.height * view.scale.y),
38
+ stroke: {
39
+ paint: {
40
+ kind: "solid",
41
+ color: opts.color?.() ?? ENTERED_FRAME_COLOR
42
+ },
43
+ width: FRAME_WIDTH,
44
+ dash: FRAME_DASH
45
+ }
46
+ }];
47
+ }
48
+ };
49
+ }
50
+ function useEnteredGroupFrameLayer(entered, boundsOf, color) {
51
+ return useMemo(() => createEnteredGroupFrameLayer({
52
+ entered,
53
+ boundsOf,
54
+ color: color.read,
55
+ colorChanged: color.changed
56
+ }), [
57
+ entered,
58
+ boundsOf,
59
+ color
60
+ ]);
61
+ }
62
+
63
+ //#endregion
64
+ export { ENTERED_FRAME_COLOR, ENTERED_FRAME_TOKEN, createEnteredGroupFrameLayer, useEnteredGroupFrameLayer };
65
+ //# sourceMappingURL=enteredGroupFrame.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"enteredGroupFrame.js","names":[],"sources":["../../../src/features/selection/enteredGroupFrame.ts"],"sourcesContent":["import { useMemo } from 'react';\nimport type { RenderLayer } from 'core/layers/render';\nimport type { Bounds } from 'core/viewport/fitViewToBounds';\nimport { viewToTransform } from 'core/viewport/view';\nimport { worldToScreen } from 'core/viewport/viewTransform';\nimport type { EnteredGroupApi } from 'canvas/deps/enteredGroup';\nimport { rectPathFor } from './handleCommands';\n\n/** The theme token the frame reads its color from, set on the canvas or any element above it. */\nexport const ENTERED_FRAME_TOKEN = '--wzl-entered-group';\n/** The frame's color where nothing sets {@link ENTERED_FRAME_TOKEN}. */\nexport const ENTERED_FRAME_COLOR = '#a48bd4';\nconst FRAME_WIDTH = 1;\nconst FRAME_DASH = [6, 4];\n\n/**\n * The frame around the group a canvas is working inside: one dashed outline on the group's\n * box, in the selection chrome's own screen space, repainting when the entered group changes.\n */\nexport function createEnteredGroupFrameLayer(opts: {\n entered: EnteredGroupApi;\n /** The box to frame, in world units. */\n boundsOf: (id: string) => Bounds | null;\n /** The frame's color, read at each paint. Default {@link ENTERED_FRAME_COLOR}. */\n color?: () => string;\n /** Hears that `color` may answer differently: a theme or mode switch. */\n colorChanged?: (fn: () => void) => () => void;\n}): RenderLayer<unknown> {\n return {\n id: 'entered-group-frame',\n label: 'Entered group',\n space: 'screen',\n subscribe: (repaint) => {\n const offGroup = opts.entered.subscribe(repaint);\n const offColor = opts.colorChanged?.(repaint);\n return () => { offGroup(); offColor?.(); };\n },\n draw: (_data, view) => {\n const id = opts.entered.get();\n const b = id === null ? null : opts.boundsOf(id);\n if (b === null) return [];\n const [x, y] = worldToScreen(b.x, b.y, viewToTransform(view));\n return [{\n kind: 'path',\n path: rectPathFor(x, y, b.width * view.scale.x, b.height * view.scale.y),\n stroke: {\n paint: { kind: 'solid', color: opts.color?.() ?? ENTERED_FRAME_COLOR },\n width: FRAME_WIDTH,\n dash: FRAME_DASH,\n },\n }];\n },\n };\n}\n\nexport function useEnteredGroupFrameLayer(\n entered: EnteredGroupApi,\n boundsOf: (id: string) => Bounds | null,\n color: { read: () => string; changed: (fn: () => void) => () => void },\n): RenderLayer<unknown> {\n return useMemo(\n () => createEnteredGroupFrameLayer({ entered, boundsOf, color: color.read, colorChanged: color.changed }),\n [entered, boundsOf, color],\n );\n}\n"],"mappings":";;;;;;;AASA,IAAa,sBAAsB;;AAEnC,IAAa,sBAAsB;AACnC,IAAM,cAAc;AACpB,IAAM,aAAa,CAAC,GAAG,CAAC;;;;;AAMxB,SAAgB,6BAA6B,MAQpB;CACvB,OAAO;EACL,IAAI;EACJ,OAAO;EACP,OAAO;EACP,YAAY,YAAY;GACtB,MAAM,WAAW,KAAK,QAAQ,UAAU,OAAO;GAC/C,MAAM,WAAW,KAAK,eAAe,OAAO;GAC5C,aAAa;IAAE,SAAS;IAAG,WAAW;GAAG;EAC3C;EACA,OAAO,OAAO,SAAS;GACrB,MAAM,KAAK,KAAK,QAAQ,IAAI;GAC5B,MAAM,IAAI,OAAO,OAAO,OAAO,KAAK,SAAS,EAAE;GAC/C,IAAI,MAAM,MAAM,OAAO,CAAC;GACxB,MAAM,CAAC,GAAG,KAAK,cAAc,EAAE,GAAG,EAAE,GAAG,gBAAgB,IAAI,CAAC;GAC5D,OAAO,CAAC;IACN,MAAM;IACN,MAAM,YAAY,GAAG,GAAG,EAAE,QAAQ,KAAK,MAAM,GAAG,EAAE,SAAS,KAAK,MAAM,CAAC;IACvE,QAAQ;KACN,OAAO;MAAE,MAAM;MAAS,OAAO,KAAK,QAAQ,KAAK;KAAoB;KACrE,OAAO;KACP,MAAM;IACR;GACF,CAAC;EACH;CACF;AACF;AAEA,SAAgB,0BACd,SACA,UACA,OACsB;CACtB,OAAO,cACC,6BAA6B;EAAE;EAAS;EAAU,OAAO,MAAM;EAAM,cAAc,MAAM;CAAQ,CAAC,GACxG;EAAC;EAAS;EAAU;CAAK,CAC3B;AACF"}
@@ -0,0 +1,44 @@
1
+ import { reachableInGroup } from "../../canvas/deps/enteredGroup.js";
2
+ import { createEmitter } from "@weasel-js/registry";
3
+ import { OUT_OF_SCOPE_ALPHA } from "@weasel-js/modes";
4
+
5
+ //#region src/features/selection/enteredGroupScope.ts
6
+ /**
7
+ * The entered group as a scope: everything outside its subtree dims and stops taking hits, and
8
+ * so does the group itself, whose empty box is empty canvas while it is entered. With nothing
9
+ * entered every node is lit and live. Compose `alphaFor` into `<SceneCanvas alphaFor>` and the
10
+ * scope into `redrawOn` to show it.
11
+ */
12
+ function createEnteredGroupScope(opts) {
13
+ const { entered, scene } = opts;
14
+ const dim = opts.dimAlpha ?? OUT_OF_SCOPE_ALPHA;
15
+ const listeners = createEmitter();
16
+ let version = 0;
17
+ let stop = null;
18
+ return {
19
+ alphaFor: (id) => {
20
+ const group = entered.get();
21
+ return id === group || reachableInGroup(scene, group, id) ? 1 : dim;
22
+ },
23
+ isPointerInteractive: (id) => reachableInGroup(scene, entered.get(), id),
24
+ getVersion: () => version,
25
+ subscribe(fn) {
26
+ if (stop === null) stop = entered.subscribe(() => {
27
+ version++;
28
+ listeners.emit();
29
+ });
30
+ const off = listeners.subscribe(fn);
31
+ return () => {
32
+ off();
33
+ if (listeners.size === 0) {
34
+ stop?.();
35
+ stop = null;
36
+ }
37
+ };
38
+ }
39
+ };
40
+ }
41
+
42
+ //#endregion
43
+ export { createEnteredGroupScope };
44
+ //# sourceMappingURL=enteredGroupScope.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"enteredGroupScope.js","names":[],"sources":["../../../src/features/selection/enteredGroupScope.ts"],"sourcesContent":["import { OUT_OF_SCOPE_ALPHA, type NodeScope } from '@weasel-js/modes';\nimport { createEmitter } from '@weasel-js/registry';\nimport { reachableInGroup, type EnteredGroupApi } from 'canvas/deps/enteredGroup';\nimport type { Scene } from 'core/scene/types';\n\n/**\n * The entered group as a scope: everything outside its subtree dims and stops taking hits, and\n * so does the group itself, whose empty box is empty canvas while it is entered. With nothing\n * entered every node is lit and live. Compose `alphaFor` into `<SceneCanvas alphaFor>` and the\n * scope into `redrawOn` to show it.\n */\nexport function createEnteredGroupScope(opts: {\n entered: EnteredGroupApi;\n scene: Pick<Scene<unknown, string, unknown>, 'ancestorsOf'>;\n /** Alpha for a node outside the entered group. Default `OUT_OF_SCOPE_ALPHA`. */\n dimAlpha?: number;\n}): NodeScope {\n const { entered, scene } = opts;\n const dim = opts.dimAlpha ?? OUT_OF_SCOPE_ALPHA;\n const listeners = createEmitter();\n let version = 0;\n let stop: (() => void) | null = null;\n\n return {\n alphaFor: (id) => {\n const group = entered.get();\n return id === group || reachableInGroup(scene, group, id) ? 1 : dim;\n },\n isPointerInteractive: (id) => reachableInGroup(scene, entered.get(), id),\n getVersion: () => version,\n subscribe(fn) {\n if (stop === null) stop = entered.subscribe(() => { version++; listeners.emit(); });\n const off = listeners.subscribe(fn);\n return () => {\n off();\n if (listeners.size === 0) { stop?.(); stop = null; }\n };\n },\n };\n}\n"],"mappings":";;;;;;;;;;;AAWA,SAAgB,wBAAwB,MAK1B;CACZ,MAAM,EAAE,SAAS,UAAU;CAC3B,MAAM,MAAM,KAAK,YAAY;CAC7B,MAAM,YAAY,cAAc;CAChC,IAAI,UAAU;CACd,IAAI,OAA4B;CAEhC,OAAO;EACL,WAAW,OAAO;GAChB,MAAM,QAAQ,QAAQ,IAAI;GAC1B,OAAO,OAAO,SAAS,iBAAiB,OAAO,OAAO,EAAE,IAAI,IAAI;EAClE;EACA,uBAAuB,OAAO,iBAAiB,OAAO,QAAQ,IAAI,GAAG,EAAE;EACvE,kBAAkB;EAClB,UAAU,IAAI;GACZ,IAAI,SAAS,MAAM,OAAO,QAAQ,gBAAgB;IAAE;IAAW,UAAU,KAAK;GAAG,CAAC;GAClF,MAAM,MAAM,UAAU,UAAU,EAAE;GAClC,aAAa;IACX,IAAI;IACJ,IAAI,UAAU,SAAS,GAAG;KAAE,OAAO;KAAG,OAAO;IAAM;GACrD;EACF;CACF;AACF"}
@@ -1,11 +1,10 @@
1
- import { sceneDependentsOf } from "../../core/scene/dependents.js";
2
1
  import { asNodeId } from "../../core/scene/types.js";
3
2
 
4
3
  //#region src/features/text/sceneTextDrafts.ts
5
4
  /**
6
- * A text edit written through to the scene keystroke by keystroke, for a node
7
- * the canvas shows somewhere besides the overlay: text something derives
8
- * from, such as the hidden text a path effect lays along a path.
5
+ * A text edit shown to the scene keystroke by keystroke, for the readers that
6
+ * see a node somewhere besides the overlay: text something derives from, such
7
+ * as the hidden text a path effect lays along a path, or a second view.
9
8
  */
10
9
  /** `data` with an edit's text, runs and style folded in through the options' setters, for one `scene.update`. */
11
10
  function editedData(data, edit, { setText, setRuns, setStyle }) {
@@ -23,39 +22,39 @@ function editedData(data, edit, { setText, setRuns, setStyle }) {
23
22
  };
24
23
  return next;
25
24
  }
26
- var nextSession = 0;
27
- /** Where an edit's writes go. Each `draft` is written through when something derives from the node; the `commit`
28
- * writes the result; `end` closes the edit, first putting back the data the drafts replaced when no commit came. All
29
- * the writes of one edit fold into one undo entry, so a commit undoes to the text before the first draft, and a
30
- * cancel leaves an entry that changes nothing. */
25
+ /** Where an edit's writes go. Each `draft` is shown as a data override folding the draft into whatever the node stores,
26
+ * so a recorded write made meanwhile shows through it and the document holds no draft; the `commit` records the
27
+ * result as one undo entry and then lets the override go; `end` lets it go, so a canceled edit leaves no entry and
28
+ * the redo stack as it was. */
31
29
  function sceneTextDrafts(sceneRef, optsRef) {
32
- let from = null;
33
- let session = nextSession++;
34
- const write = (id, data) => {
35
- const scene = sceneRef.current;
36
- scene.batch("Edit text", () => scene.update(asNodeId(id), { data }), { coalesceKey: `text-edit:${id}:${session}` });
30
+ /** The open edit's claim on the scene's overrides, with the scene it was made on. */
31
+ let held = null;
32
+ const release = (id) => {
33
+ if (held?.id !== id) return;
34
+ held.hold.drop();
35
+ held = null;
37
36
  };
38
37
  return {
39
38
  draft: (id, draft) => {
40
39
  const scene = sceneRef.current;
41
- const nid = asNodeId(id);
42
- const node = scene.get(nid);
43
- if (!node || sceneDependentsOf(scene, nid).length === 0) return;
44
- if (from?.id !== id) from = {
40
+ if (scene.get(asNodeId(id)) === void 0) return;
41
+ if (held !== null && (held.id !== id || held.scene !== scene)) release(held.id);
42
+ held ??= {
45
43
  id,
46
- data: node.data
44
+ scene,
45
+ hold: scene.overrides.hold()
47
46
  };
48
- write(id, editedData(node.data, draft, optsRef.current));
47
+ const opts = optsRef.current;
48
+ held.hold.show([[asNodeId(id), { data: (stored) => editedData(stored, draft, opts) }]]);
49
49
  },
50
50
  commit: (id, data) => {
51
- from = null;
52
- write(id, data);
51
+ const scene = sceneRef.current;
52
+ scene.batch("Edit text", () => scene.update(asNodeId(id), { data }));
53
+ release(id);
53
54
  },
54
- end: (id) => {
55
- const held = from;
56
- from = null;
57
- if (held?.id === id && sceneRef.current.get(asNodeId(id))) write(id, held.data);
58
- session = nextSession++;
55
+ end: release,
56
+ close: () => {
57
+ if (held !== null) release(held.id);
59
58
  }
60
59
  };
61
60
  }
@@ -1 +1 @@
1
- {"version":3,"file":"sceneTextDrafts.js","names":[],"sources":["../../../src/features/text/sceneTextDrafts.ts"],"sourcesContent":["/**\n * A text edit written through to the scene keystroke by keystroke, for a node\n * the canvas shows somewhere besides the overlay: text something derives\n * from, such as the hidden text a path effect lays along a path.\n */\nimport { sceneDependentsOf } from '../../core/scene/dependents';\nimport { asNodeId, type Scene } from '../../core/scene/types';\nimport type { StyledRun, TextStyle } from '@weasel-js/text';\nimport type { TextEditDraft } from '@weasel-js/text-dom';\nimport type { DefaultTextData, UseSceneTextEditOptions } from './useSceneTextEdit';\n\n/** `data` with an edit's text, runs and style folded in through the options' setters, for one `scene.update`. */\nexport function editedData<TData extends DefaultTextData>(\n data: TData,\n edit: { text: string; runs?: readonly StyledRun[]; style?: TextStyle },\n { setText, setRuns, setStyle }: UseSceneTextEditOptions<TData>,\n): TData {\n let next = setText ? setText(data, edit.text) : { ...data, text: edit.text };\n if (edit.runs) next = setRuns ? setRuns(next, [...edit.runs]) : { ...next, runs: edit.runs };\n if (edit.style) next = setStyle ? setStyle(next, edit.style) : { ...next, style: edit.style };\n return next;\n}\n\n// Shared by every instance, so two editors of one node never fold their edits together.\nlet nextSession = 0;\n\n/** Where an edit's writes go. Each `draft` is written through when something derives from the node; the `commit`\n * writes the result; `end` closes the edit, first putting back the data the drafts replaced when no commit came. All\n * the writes of one edit fold into one undo entry, so a commit undoes to the text before the first draft, and a\n * cancel leaves an entry that changes nothing. */\nexport function sceneTextDrafts<TData extends DefaultTextData, TLayer extends string, TPose>(\n sceneRef: { readonly current: Scene<TData, TLayer, TPose> },\n optsRef: { readonly current: UseSceneTextEditOptions<TData> },\n): {\n draft: (id: string, draft: TextEditDraft) => void;\n commit: (id: string, data: TData) => void;\n end: (id: string) => void;\n} {\n let from: { id: string; data: TData } | null = null;\n let session = nextSession++;\n const write = (id: string, data: TData): void => {\n const scene = sceneRef.current;\n scene.batch('Edit text', () => scene.update(asNodeId(id), { data }), { coalesceKey: `text-edit:${id}:${session}` });\n };\n return {\n draft: (id, draft) => {\n const scene = sceneRef.current;\n const nid = asNodeId(id);\n const node = scene.get(nid);\n if (!node || sceneDependentsOf(scene, nid).length === 0) return;\n if (from?.id !== id) from = { id, data: node.data };\n write(id, editedData(node.data, draft, optsRef.current));\n },\n commit: (id, data) => {\n from = null;\n write(id, data);\n },\n end: (id) => {\n const held = from;\n from = null;\n if (held?.id === id && sceneRef.current.get(asNodeId(id))) write(id, held.data);\n session = nextSession++;\n },\n };\n}\n"],"mappings":";;;;;;;;;;AAYA,SAAgB,WACd,MACA,MACA,EAAE,SAAS,SAAS,YACb;CACP,IAAI,OAAO,UAAU,QAAQ,MAAM,KAAK,IAAI,IAAI;EAAE,GAAG;EAAM,MAAM,KAAK;CAAK;CAC3E,IAAI,KAAK,MAAM,OAAO,UAAU,QAAQ,MAAM,CAAC,GAAG,KAAK,IAAI,CAAC,IAAI;EAAE,GAAG;EAAM,MAAM,KAAK;CAAK;CAC3F,IAAI,KAAK,OAAO,OAAO,WAAW,SAAS,MAAM,KAAK,KAAK,IAAI;EAAE,GAAG;EAAM,OAAO,KAAK;CAAM;CAC5F,OAAO;AACT;AAGA,IAAI,cAAc;;;;;AAMlB,SAAgB,gBACd,UACA,SAKA;CACA,IAAI,OAA2C;CAC/C,IAAI,UAAU;CACd,MAAM,SAAS,IAAY,SAAsB;EAC/C,MAAM,QAAQ,SAAS;EACvB,MAAM,MAAM,mBAAmB,MAAM,OAAO,SAAS,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,aAAa,aAAa,GAAG,GAAG,UAAU,CAAC;CACpH;CACA,OAAO;EACL,QAAQ,IAAI,UAAU;GACpB,MAAM,QAAQ,SAAS;GACvB,MAAM,MAAM,SAAS,EAAE;GACvB,MAAM,OAAO,MAAM,IAAI,GAAG;GAC1B,IAAI,CAAC,QAAQ,kBAAkB,OAAO,GAAG,CAAC,CAAC,WAAW,GAAG;GACzD,IAAI,MAAM,OAAO,IAAI,OAAO;IAAE;IAAI,MAAM,KAAK;GAAK;GAClD,MAAM,IAAI,WAAW,KAAK,MAAM,OAAO,QAAQ,OAAO,CAAC;EACzD;EACA,SAAS,IAAI,SAAS;GACpB,OAAO;GACP,MAAM,IAAI,IAAI;EAChB;EACA,MAAM,OAAO;GACX,MAAM,OAAO;GACb,OAAO;GACP,IAAI,MAAM,OAAO,MAAM,SAAS,QAAQ,IAAI,SAAS,EAAE,CAAC,GAAG,MAAM,IAAI,KAAK,IAAI;GAC9E,UAAU;EACZ;CACF;AACF"}
1
+ {"version":3,"file":"sceneTextDrafts.js","names":[],"sources":["../../../src/features/text/sceneTextDrafts.ts"],"sourcesContent":["/**\n * A text edit shown to the scene keystroke by keystroke, for the readers that\n * see a node somewhere besides the overlay: text something derives from, such\n * as the hidden text a path effect lays along a path, or a second view.\n */\nimport { asNodeId, type PoseHold, type Scene } from '../../core/scene/types';\nimport type { StyledRun, TextStyle } from '@weasel-js/text';\nimport type { TextEditDraft } from '@weasel-js/text-dom';\nimport type { DefaultTextData, UseSceneTextEditOptions } from './useSceneTextEdit';\n\n/** `data` with an edit's text, runs and style folded in through the options' setters, for one `scene.update`. */\nexport function editedData<TData extends DefaultTextData>(\n data: TData,\n edit: { text: string; runs?: readonly StyledRun[]; style?: TextStyle },\n { setText, setRuns, setStyle }: UseSceneTextEditOptions<TData>,\n): TData {\n let next = setText ? setText(data, edit.text) : { ...data, text: edit.text };\n if (edit.runs) next = setRuns ? setRuns(next, [...edit.runs]) : { ...next, runs: edit.runs };\n if (edit.style) next = setStyle ? setStyle(next, edit.style) : { ...next, style: edit.style };\n return next;\n}\n\n/** Where an edit's writes go. Each `draft` is shown as a data override folding the draft into whatever the node stores,\n * so a recorded write made meanwhile shows through it and the document holds no draft; the `commit` records the\n * result as one undo entry and then lets the override go; `end` lets it go, so a canceled edit leaves no entry and\n * the redo stack as it was. */\nexport function sceneTextDrafts<TData extends DefaultTextData, TLayer extends string, TPose>(\n sceneRef: { readonly current: Scene<TData, TLayer, TPose> },\n optsRef: { readonly current: UseSceneTextEditOptions<TData> },\n): {\n draft: (id: string, draft: TextEditDraft) => void;\n commit: (id: string, data: TData) => void;\n end: (id: string) => void;\n /** Let any open edit's draft go, as an unmount must: the overlay goes without an `end`. */\n close: () => void;\n} {\n /** The open edit's claim on the scene's overrides, with the scene it was made on. */\n let held: { id: string; scene: Scene<TData, TLayer, TPose>; hold: PoseHold<TPose, TData> } | null = null;\n const release = (id: string): void => {\n if (held?.id !== id) return;\n held.hold.drop();\n held = null;\n };\n return {\n draft: (id, draft) => {\n const scene = sceneRef.current;\n if (scene.get(asNodeId(id)) === undefined) return;\n if (held !== null && (held.id !== id || held.scene !== scene)) release(held.id);\n held ??= { id, scene, hold: scene.overrides.hold() };\n const opts = optsRef.current;\n held.hold.show([[asNodeId(id), { data: (stored) => editedData(stored, draft, opts) }]]);\n },\n commit: (id, data) => {\n const scene = sceneRef.current;\n scene.batch('Edit text', () => scene.update(asNodeId(id), { data }));\n // After the write, so no frame shows the text from before the edit in between.\n release(id);\n },\n end: release,\n close: () => {\n if (held !== null) release(held.id);\n },\n };\n}\n"],"mappings":";;;;;;;;;AAWA,SAAgB,WACd,MACA,MACA,EAAE,SAAS,SAAS,YACb;CACP,IAAI,OAAO,UAAU,QAAQ,MAAM,KAAK,IAAI,IAAI;EAAE,GAAG;EAAM,MAAM,KAAK;CAAK;CAC3E,IAAI,KAAK,MAAM,OAAO,UAAU,QAAQ,MAAM,CAAC,GAAG,KAAK,IAAI,CAAC,IAAI;EAAE,GAAG;EAAM,MAAM,KAAK;CAAK;CAC3F,IAAI,KAAK,OAAO,OAAO,WAAW,SAAS,MAAM,KAAK,KAAK,IAAI;EAAE,GAAG;EAAM,OAAO,KAAK;CAAM;CAC5F,OAAO;AACT;;;;;AAMA,SAAgB,gBACd,UACA,SAOA;;CAEA,IAAI,OAAgG;CACpG,MAAM,WAAW,OAAqB;EACpC,IAAI,MAAM,OAAO,IAAI;EACrB,KAAK,KAAK,KAAK;EACf,OAAO;CACT;CACA,OAAO;EACL,QAAQ,IAAI,UAAU;GACpB,MAAM,QAAQ,SAAS;GACvB,IAAI,MAAM,IAAI,SAAS,EAAE,CAAC,MAAM,QAAW;GAC3C,IAAI,SAAS,SAAS,KAAK,OAAO,MAAM,KAAK,UAAU,QAAQ,QAAQ,KAAK,EAAE;GAC9E,SAAS;IAAE;IAAI;IAAO,MAAM,MAAM,UAAU,KAAK;GAAE;GACnD,MAAM,OAAO,QAAQ;GACrB,KAAK,KAAK,KAAK,CAAC,CAAC,SAAS,EAAE,GAAG,EAAE,OAAO,WAAW,WAAW,QAAQ,OAAO,IAAI,EAAE,CAAC,CAAC,CAAC;EACxF;EACA,SAAS,IAAI,SAAS;GACpB,MAAM,QAAQ,SAAS;GACvB,MAAM,MAAM,mBAAmB,MAAM,OAAO,SAAS,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;GAEnE,QAAQ,EAAE;EACZ;EACA,KAAK;EACL,aAAa;GACX,IAAI,SAAS,MAAM,QAAQ,KAAK,EAAE;EACpC;CACF;AACF"}
@@ -1,4 +1,5 @@
1
1
  import { derivedPartsOf } from "../../core/scene/derivedParts.js";
2
+ import { nodesAsShown, shownLookup } from "../../core/scene/nodeAsShown.js";
2
3
  import { targetTransformOf, worldToLocal } from "../../core/geometry/targetFrame.js";
3
4
  import { textBoxOf } from "../../canvas/nodeShapes/textPainter.js";
4
5
  import { sceneDerivedPathOf, scenePaintedPoseOf } from "../../canvas/derivedPath.js";
@@ -20,7 +21,8 @@ import { caretIndexAt, pointInTextPose } from "@weasel-js/text";
20
21
  * than the ink selection picks.
21
22
  */
22
23
  function textHitAt(scene, read, cx, cy) {
23
- const order = scene.renderOrderNodes();
24
+ const order = nodesAsShown(scene.overrides, scene.renderOrderNodes());
25
+ const shown = shownLookup(scene);
24
26
  const paintedPoseOf = scenePaintedPoseOf(scene);
25
27
  const derivedPathOf = sceneDerivedPathOf(scene);
26
28
  for (let i = order.length - 1; i >= 0; i--) {
@@ -28,7 +30,7 @@ function textHitAt(scene, read, cx, cy) {
28
30
  if (scene.isLocked(node.id) || scene.isHidden(node.id)) continue;
29
31
  if (!read.isText(node.data)) {
30
32
  const source = effectSourceFor(scene, node.id, "text");
31
- const text = source === null || scene.isLocked(source) ? void 0 : scene.get(source);
33
+ const text = source === null || scene.isLocked(source) ? void 0 : shown.get(source);
32
34
  if (text === void 0 || !read.isText(text.data)) continue;
33
35
  const path = derivedPathOf(node);
34
36
  if (path !== null && onAGlyph(path, cx, cy)) return {
@@ -1 +1 @@
1
- {"version":3,"file":"textHitAt.js","names":[],"sources":["../../../src/features/text/textHitAt.ts"],"sourcesContent":["import type { Scene } from '../../core/scene/types';\nimport type { RectPose } from 'core/geometry/unionBounds';\nimport { boundsOfPath, pathContainsPoint, type Path } from '@weasel-js/geom';\nimport { sceneDerivedPathOf, scenePaintedPoseOf } from '../../canvas/derivedPath';\nimport { derivedPartsOf } from '../../core/scene/derivedParts';\nimport { effectSourceFor } from '../pathEffects/effectSources';\nimport { targetTransformOf, worldToLocal } from 'core/geometry/targetFrame';\nimport { caretIndexAt, pointInTextPose, type StyledRun, type TextStyle, type TextVerticalAlign } from '@weasel-js/text';\nimport { textBoxOf } from '../../canvas/nodeShapes/textPainter';\n\n/** How a node's text fields are read from its `data`. */\nexport interface TextReads<TData> {\n isText(data: TData): boolean;\n text(data: TData): string;\n style(data: TData): TextStyle | undefined;\n runs(data: TData): readonly StyledRun[] | undefined;\n verticalAlign(data: TData): TextVerticalAlign | undefined;\n padding(data: TData): number | undefined;\n}\n\n/**\n * The top-most unlocked text node at world `(cx, cy)`, and the caret there.\n *\n * `pointInTextPose` and `caretIndexAt` work in the node's own box, so the\n * point is taken back through the frame the node is drawn in. The whole box\n * takes the point, padding included; the caret is placed in the inset one.\n *\n * A path effect hiding a text source answers for it within the bounds of any\n * glyph it lays, counters included, with the caret at the end of the text.\n * A curved glyph takes a double-click anywhere in its axis-aligned box, wider\n * than the ink selection picks.\n */\nexport function textHitAt<TData, TPose extends RectPose>(\n scene: Scene<TData, string, TPose>,\n read: TextReads<TData>,\n cx: number,\n cy: number,\n): { id: string; caret: number } | null {\n // renderOrder() is back-to-front, so iterate in reverse and stop on the first hit.\n const order = scene.renderOrderNodes();\n const paintedPoseOf = scenePaintedPoseOf(scene);\n const derivedPathOf = sceneDerivedPathOf(scene);\n for (let i = order.length - 1; i >= 0; i--) {\n const node = order[i];\n if (scene.isLocked(node.id) || scene.isHidden(node.id)) continue;\n if (!read.isText(node.data)) {\n const source = effectSourceFor(scene, node.id, 'text');\n const text = source === null || scene.isLocked(source) ? undefined : scene.get(source);\n if (text === undefined || !read.isText(text.data)) continue;\n const path = derivedPathOf(node);\n if (path !== null && onAGlyph(path, cx, cy)) return { id: String(source), caret: read.text(text.data).length };\n continue;\n }\n const at = paintedPoseOf(node);\n const p = worldToLocal(targetTransformOf(at), cx, cy);\n if (!p) continue;\n const pose = {\n ...textBoxOf({ padding: read.padding(node.data) }, at),\n text: read.text(node.data),\n runs: read.runs(node.data) as StyledRun[] | undefined,\n style: read.style(node.data),\n verticalAlign: read.verticalAlign(node.data),\n };\n const box = { x: at.x, y: at.y, width: at.width, height: at.height };\n if (!pointInTextPose(p.x, p.y, { ...pose, ...box })) continue;\n return { id: String(node.id), caret: caretIndexAt(p.x, p.y, pose) };\n }\n return null;\n}\n\n/** Whether `(x, y)` is within the bounds of one of `path`'s parts: a glyph's box, so a stem too thin to land on and the\n * hole in an o still take the click. */\nfunction onAGlyph(path: Path, x: number, y: number): boolean {\n const parts = derivedPartsOf(path);\n if (parts === undefined) return pathContainsPoint(path, x, y);\n return parts.some(({ path: part }) => {\n const b = boundsOfPath(part);\n return x >= b.x && x <= b.x + b.width && y >= b.y && y <= b.y + b.height;\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAgCA,SAAgB,UACd,OACA,MACA,IACA,IACsC;CAEtC,MAAM,QAAQ,MAAM,iBAAiB;CACrC,MAAM,gBAAgB,mBAAmB,KAAK;CAC9C,MAAM,gBAAgB,mBAAmB,KAAK;CAC9C,KAAK,IAAI,IAAI,MAAM,SAAS,GAAG,KAAK,GAAG,KAAK;EAC1C,MAAM,OAAO,MAAM;EACnB,IAAI,MAAM,SAAS,KAAK,EAAE,KAAK,MAAM,SAAS,KAAK,EAAE,GAAG;EACxD,IAAI,CAAC,KAAK,OAAO,KAAK,IAAI,GAAG;GAC3B,MAAM,SAAS,gBAAgB,OAAO,KAAK,IAAI,MAAM;GACrD,MAAM,OAAO,WAAW,QAAQ,MAAM,SAAS,MAAM,IAAI,SAAY,MAAM,IAAI,MAAM;GACrF,IAAI,SAAS,UAAa,CAAC,KAAK,OAAO,KAAK,IAAI,GAAG;GACnD,MAAM,OAAO,cAAc,IAAI;GAC/B,IAAI,SAAS,QAAQ,SAAS,MAAM,IAAI,EAAE,GAAG,OAAO;IAAE,IAAI,OAAO,MAAM;IAAG,OAAO,KAAK,KAAK,KAAK,IAAI,CAAC,CAAC;GAAO;GAC7G;EACF;EACA,MAAM,KAAK,cAAc,IAAI;EAC7B,MAAM,IAAI,aAAa,kBAAkB,EAAE,GAAG,IAAI,EAAE;EACpD,IAAI,CAAC,GAAG;EACR,MAAM,OAAO;GACX,GAAG,UAAU,EAAE,SAAS,KAAK,QAAQ,KAAK,IAAI,EAAE,GAAG,EAAE;GACrD,MAAM,KAAK,KAAK,KAAK,IAAI;GACzB,MAAM,KAAK,KAAK,KAAK,IAAI;GACzB,OAAO,KAAK,MAAM,KAAK,IAAI;GAC3B,eAAe,KAAK,cAAc,KAAK,IAAI;EAC7C;EACA,MAAM,MAAM;GAAE,GAAG,GAAG;GAAG,GAAG,GAAG;GAAG,OAAO,GAAG;GAAO,QAAQ,GAAG;EAAO;EACnE,IAAI,CAAC,gBAAgB,EAAE,GAAG,EAAE,GAAG;GAAE,GAAG;GAAM,GAAG;EAAI,CAAC,GAAG;EACrD,OAAO;GAAE,IAAI,OAAO,KAAK,EAAE;GAAG,OAAO,aAAa,EAAE,GAAG,EAAE,GAAG,IAAI;EAAE;CACpE;CACA,OAAO;AACT;;;AAIA,SAAS,SAAS,MAAY,GAAW,GAAoB;CAC3D,MAAM,QAAQ,eAAe,IAAI;CACjC,IAAI,UAAU,QAAW,OAAO,kBAAkB,MAAM,GAAG,CAAC;CAC5D,OAAO,MAAM,MAAM,EAAE,MAAM,WAAW;EACpC,MAAM,IAAI,aAAa,IAAI;EAC3B,OAAO,KAAK,EAAE,KAAK,KAAK,EAAE,IAAI,EAAE,SAAS,KAAK,EAAE,KAAK,KAAK,EAAE,IAAI,EAAE;CACpE,CAAC;AACH"}
1
+ {"version":3,"file":"textHitAt.js","names":[],"sources":["../../../src/features/text/textHitAt.ts"],"sourcesContent":["import type { Scene } from '../../core/scene/types';\nimport type { RectPose } from 'core/geometry/unionBounds';\nimport { boundsOfPath, pathContainsPoint, type Path } from '@weasel-js/geom';\nimport { sceneDerivedPathOf, scenePaintedPoseOf } from '../../canvas/derivedPath';\nimport { derivedPartsOf } from '../../core/scene/derivedParts';\nimport { nodesAsShown, shownLookup } from '../../core/scene/nodeAsShown';\nimport { effectSourceFor } from '../pathEffects/effectSources';\nimport { targetTransformOf, worldToLocal } from 'core/geometry/targetFrame';\nimport { caretIndexAt, pointInTextPose, type StyledRun, type TextStyle, type TextVerticalAlign } from '@weasel-js/text';\nimport { textBoxOf } from '../../canvas/nodeShapes/textPainter';\n\n/** How a node's text fields are read from its `data`. */\nexport interface TextReads<TData> {\n isText(data: TData): boolean;\n text(data: TData): string;\n style(data: TData): TextStyle | undefined;\n runs(data: TData): readonly StyledRun[] | undefined;\n verticalAlign(data: TData): TextVerticalAlign | undefined;\n padding(data: TData): number | undefined;\n}\n\n/**\n * The top-most unlocked text node at world `(cx, cy)`, and the caret there.\n *\n * `pointInTextPose` and `caretIndexAt` work in the node's own box, so the\n * point is taken back through the frame the node is drawn in. The whole box\n * takes the point, padding included; the caret is placed in the inset one.\n *\n * A path effect hiding a text source answers for it within the bounds of any\n * glyph it lays, counters included, with the caret at the end of the text.\n * A curved glyph takes a double-click anywhere in its axis-aligned box, wider\n * than the ink selection picks.\n */\nexport function textHitAt<TData, TPose extends RectPose>(\n scene: Scene<TData, string, TPose>,\n read: TextReads<TData>,\n cx: number,\n cy: number,\n): { id: string; caret: number } | null {\n // renderOrder() is back-to-front, so iterate in reverse and stop on the first hit.\n // Hit as painted: an open edit's draft is what the glyphs and the caret are laid out from.\n const order = nodesAsShown(scene.overrides, scene.renderOrderNodes());\n const shown = shownLookup(scene);\n const paintedPoseOf = scenePaintedPoseOf(scene);\n const derivedPathOf = sceneDerivedPathOf(scene);\n for (let i = order.length - 1; i >= 0; i--) {\n const node = order[i];\n if (scene.isLocked(node.id) || scene.isHidden(node.id)) continue;\n if (!read.isText(node.data)) {\n const source = effectSourceFor(scene, node.id, 'text');\n const text = source === null || scene.isLocked(source) ? undefined : shown.get(source);\n if (text === undefined || !read.isText(text.data)) continue;\n const path = derivedPathOf(node);\n if (path !== null && onAGlyph(path, cx, cy)) return { id: String(source), caret: read.text(text.data).length };\n continue;\n }\n const at = paintedPoseOf(node);\n const p = worldToLocal(targetTransformOf(at), cx, cy);\n if (!p) continue;\n const pose = {\n ...textBoxOf({ padding: read.padding(node.data) }, at),\n text: read.text(node.data),\n runs: read.runs(node.data) as StyledRun[] | undefined,\n style: read.style(node.data),\n verticalAlign: read.verticalAlign(node.data),\n };\n const box = { x: at.x, y: at.y, width: at.width, height: at.height };\n if (!pointInTextPose(p.x, p.y, { ...pose, ...box })) continue;\n return { id: String(node.id), caret: caretIndexAt(p.x, p.y, pose) };\n }\n return null;\n}\n\n/** Whether `(x, y)` is within the bounds of one of `path`'s parts: a glyph's box, so a stem too thin to land on and the\n * hole in an o still take the click. */\nfunction onAGlyph(path: Path, x: number, y: number): boolean {\n const parts = derivedPartsOf(path);\n if (parts === undefined) return pathContainsPoint(path, x, y);\n return parts.some(({ path: part }) => {\n const b = boundsOfPath(part);\n return x >= b.x && x <= b.x + b.width && y >= b.y && y <= b.y + b.height;\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAiCA,SAAgB,UACd,OACA,MACA,IACA,IACsC;CAGtC,MAAM,QAAQ,aAAa,MAAM,WAAW,MAAM,iBAAiB,CAAC;CACpE,MAAM,QAAQ,YAAY,KAAK;CAC/B,MAAM,gBAAgB,mBAAmB,KAAK;CAC9C,MAAM,gBAAgB,mBAAmB,KAAK;CAC9C,KAAK,IAAI,IAAI,MAAM,SAAS,GAAG,KAAK,GAAG,KAAK;EAC1C,MAAM,OAAO,MAAM;EACnB,IAAI,MAAM,SAAS,KAAK,EAAE,KAAK,MAAM,SAAS,KAAK,EAAE,GAAG;EACxD,IAAI,CAAC,KAAK,OAAO,KAAK,IAAI,GAAG;GAC3B,MAAM,SAAS,gBAAgB,OAAO,KAAK,IAAI,MAAM;GACrD,MAAM,OAAO,WAAW,QAAQ,MAAM,SAAS,MAAM,IAAI,SAAY,MAAM,IAAI,MAAM;GACrF,IAAI,SAAS,UAAa,CAAC,KAAK,OAAO,KAAK,IAAI,GAAG;GACnD,MAAM,OAAO,cAAc,IAAI;GAC/B,IAAI,SAAS,QAAQ,SAAS,MAAM,IAAI,EAAE,GAAG,OAAO;IAAE,IAAI,OAAO,MAAM;IAAG,OAAO,KAAK,KAAK,KAAK,IAAI,CAAC,CAAC;GAAO;GAC7G;EACF;EACA,MAAM,KAAK,cAAc,IAAI;EAC7B,MAAM,IAAI,aAAa,kBAAkB,EAAE,GAAG,IAAI,EAAE;EACpD,IAAI,CAAC,GAAG;EACR,MAAM,OAAO;GACX,GAAG,UAAU,EAAE,SAAS,KAAK,QAAQ,KAAK,IAAI,EAAE,GAAG,EAAE;GACrD,MAAM,KAAK,KAAK,KAAK,IAAI;GACzB,MAAM,KAAK,KAAK,KAAK,IAAI;GACzB,OAAO,KAAK,MAAM,KAAK,IAAI;GAC3B,eAAe,KAAK,cAAc,KAAK,IAAI;EAC7C;EACA,MAAM,MAAM;GAAE,GAAG,GAAG;GAAG,GAAG,GAAG;GAAG,OAAO,GAAG;GAAO,QAAQ,GAAG;EAAO;EACnE,IAAI,CAAC,gBAAgB,EAAE,GAAG,EAAE,GAAG;GAAE,GAAG;GAAM,GAAG;EAAI,CAAC,GAAG;EACrD,OAAO;GAAE,IAAI,OAAO,KAAK,EAAE;GAAG,OAAO,aAAa,EAAE,GAAG,EAAE,GAAG,IAAI;EAAE;CACpE;CACA,OAAO;AACT;;;AAIA,SAAS,SAAS,MAAY,GAAW,GAAoB;CAC3D,MAAM,QAAQ,eAAe,IAAI;CACjC,IAAI,UAAU,QAAW,OAAO,kBAAkB,MAAM,GAAG,CAAC;CAC5D,OAAO,MAAM,MAAM,EAAE,MAAM,WAAW;EACpC,MAAM,IAAI,aAAa,IAAI;EAC3B,OAAO,KAAK,EAAE,KAAK,KAAK,EAAE,IAAI,EAAE,SAAS,KAAK,EAAE,KAAK,KAAK,EAAE,IAAI,EAAE;CACpE,CAAC;AACH"}
@@ -1,5 +1,4 @@
1
- import { withInheritedPaint } from "../../core/scene/inheritedPaint.js";
2
- import { inheritedPaintIn } from "../../canvas/inheritedPaints.js";
1
+ import { inheritedPaintIn, withInheritedPaint } from "../../core/scene/inheritedPaint.js";
3
2
  import { textBoxOf } from "../../canvas/nodeShapes/textPainter.js";
4
3
  import { asNodeId } from "../../core/scene/types.js";
5
4
  import { scenePaintedPoseOf } from "../../canvas/derivedPath.js";
@@ -8,7 +7,7 @@ import { drawOneEditing } from "./editingDrawOne.js";
8
7
  import { textHitAt } from "./textHitAt.js";
9
8
  import { editedData, sceneTextDrafts } from "./sceneTextDrafts.js";
10
9
  import { poseFrameOf } from "@weasel-js/geom";
11
- import { useCallback, useMemo, useRef } from "react";
10
+ import { useCallback, useEffect, useMemo, useRef } from "react";
12
11
  import { useLatest } from "@weasel-js/react";
13
12
  import { clientToCanvas } from "@weasel-js/routing";
14
13
  import { resolveScreenLength } from "@weasel-js/paint";
@@ -51,6 +50,7 @@ function useSceneTextEdit(scene, container, options = {}) {
51
50
  const containerRef = useLatest(container);
52
51
  const openedOnRef = useRef(null);
53
52
  const drafts = useMemo(() => sceneTextDrafts(sceneRef, optsRef), [sceneRef, optsRef]);
53
+ useEffect(() => () => drafts.close(), [drafts]);
54
54
  const surface = () => {
55
55
  const root = containerRef.current;
56
56
  const canvas = root ? findMountedCanvas(root, openedOnRef.current) : null;