@weasel-js/core 1.12.0 → 1.13.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 (304) hide show
  1. package/CHANGELOG.md +199 -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/barrel/text.js +2 -2
  18. package/dist/canvas/NodeShape.js +1 -0
  19. package/dist/canvas/NodeShape.js.map +1 -1
  20. package/dist/canvas/SceneCanvas/SceneCanvasInput.js +2 -0
  21. package/dist/canvas/SceneCanvas/SceneCanvasInput.js.map +1 -1
  22. package/dist/canvas/SceneCanvas/canvasPassThrough.js +1 -1
  23. package/dist/canvas/SceneCanvas/canvasPassThrough.js.map +1 -1
  24. package/dist/canvas/SceneCanvas/defaultNodeRouting.js +2 -2
  25. package/dist/canvas/SceneCanvas/defaultNodeRouting.js.map +1 -1
  26. package/dist/canvas/SceneCanvas/scenePointQuery.js +20 -2
  27. package/dist/canvas/SceneCanvas/scenePointQuery.js.map +1 -1
  28. package/dist/canvas/SceneCanvas/useCanvasTools.js +3 -0
  29. package/dist/canvas/SceneCanvas/useCanvasTools.js.map +1 -1
  30. package/dist/canvas/SceneCanvas/useContributionRoles.js +2 -0
  31. package/dist/canvas/SceneCanvas/useContributionRoles.js.map +1 -1
  32. package/dist/canvas/SceneCanvas/usePreviewGhostLayer.js +25 -19
  33. package/dist/canvas/SceneCanvas/usePreviewGhostLayer.js.map +1 -1
  34. package/dist/canvas/SceneCanvas/useSceneCanvasLayers.js +7 -0
  35. package/dist/canvas/SceneCanvas/useSceneCanvasLayers.js.map +1 -1
  36. package/dist/canvas/SceneCanvas/useSceneCanvasTools.js +1 -0
  37. package/dist/canvas/SceneCanvas/useSceneCanvasTools.js.map +1 -1
  38. package/dist/canvas/SceneCanvas/useScenePicking.js +31 -6
  39. package/dist/canvas/SceneCanvas/useScenePicking.js.map +1 -1
  40. package/dist/canvas/SceneCanvas/useSceneSelectTool.js +4 -5
  41. package/dist/canvas/SceneCanvas/useSceneSelectTool.js.map +1 -1
  42. package/dist/canvas/SceneCanvas/useSceneStandardActions.js +5 -3
  43. package/dist/canvas/SceneCanvas/useSceneStandardActions.js.map +1 -1
  44. package/dist/canvas/SceneCanvas.js +19 -2
  45. package/dist/canvas/SceneCanvas.js.map +1 -1
  46. package/dist/canvas/accentColor.js +26 -3
  47. package/dist/canvas/accentColor.js.map +1 -1
  48. package/dist/canvas/buildSceneLayer.js +1 -1
  49. package/dist/canvas/buildSceneLayer.js.map +1 -1
  50. package/dist/canvas/buildSceneTree.js +14 -20
  51. package/dist/canvas/buildSceneTree.js.map +1 -1
  52. package/dist/canvas/canvasRenderer.js +6 -2
  53. package/dist/canvas/canvasRenderer.js.map +1 -1
  54. package/dist/canvas/copiesAbove.js +1 -1
  55. package/dist/canvas/copiesAbove.js.map +1 -1
  56. package/dist/canvas/defaultDrawOne.js +10 -7
  57. package/dist/canvas/defaultDrawOne.js.map +1 -1
  58. package/dist/canvas/deps/areaSelect.js +5 -2
  59. package/dist/canvas/deps/areaSelect.js.map +1 -1
  60. package/dist/canvas/deps/booleans.js +1 -1
  61. package/dist/canvas/deps/booleans.js.map +1 -1
  62. package/dist/canvas/deps/enteredGroup.js +81 -0
  63. package/dist/canvas/deps/enteredGroup.js.map +1 -0
  64. package/dist/canvas/deps/hitTestArea.js +27 -15
  65. package/dist/canvas/deps/hitTestArea.js.map +1 -1
  66. package/dist/canvas/deps/insert.js +4 -36
  67. package/dist/canvas/deps/insert.js.map +1 -1
  68. package/dist/canvas/deps/insertSite.js +39 -0
  69. package/dist/canvas/deps/insertSite.js.map +1 -0
  70. package/dist/canvas/deps/lassoSelect.js +6 -3
  71. package/dist/canvas/deps/lassoSelect.js.map +1 -1
  72. package/dist/canvas/deps/pathLeaf.js +1 -1
  73. package/dist/canvas/deps/pathLeaf.js.map +1 -1
  74. package/dist/canvas/deps/slice.js +2 -1
  75. package/dist/canvas/deps/slice.js.map +1 -1
  76. package/dist/canvas/derivedPath.js +14 -6
  77. package/dist/canvas/derivedPath.js.map +1 -1
  78. package/dist/canvas/doubleClickTarget.js +9 -11
  79. package/dist/canvas/doubleClickTarget.js.map +1 -1
  80. package/dist/canvas/drawnBounds.js +16 -7
  81. package/dist/canvas/drawnBounds.js.map +1 -1
  82. package/dist/canvas/inheritedPaints.js +22 -11
  83. package/dist/canvas/inheritedPaints.js.map +1 -1
  84. package/dist/canvas/nodeShapes/nodePaint.js +1 -7
  85. package/dist/canvas/nodeShapes/nodePaint.js.map +1 -1
  86. package/dist/canvas/nodeShapes/pathPainter.js +2 -3
  87. package/dist/canvas/nodeShapes/pathPainter.js.map +1 -1
  88. package/dist/canvas/nodeShapes/rectFallbackPainter.js +1 -1
  89. package/dist/canvas/nodeShapes/rectFallbackPainter.js.map +1 -1
  90. package/dist/canvas/nodeShapes/shapePainter.js +2 -3
  91. package/dist/canvas/nodeShapes/shapePainter.js.map +1 -1
  92. package/dist/canvas/ownClip.js +24 -0
  93. package/dist/canvas/ownClip.js.map +1 -0
  94. package/dist/canvas/paintCull.js.map +1 -1
  95. package/dist/canvas/pickIndex/nodeBroadBox.js +86 -0
  96. package/dist/canvas/pickIndex/nodeBroadBox.js.map +1 -0
  97. package/dist/canvas/pickIndex/scenePickIndex.js +208 -0
  98. package/dist/canvas/pickIndex/scenePickIndex.js.map +1 -0
  99. package/dist/canvas/pickWalk.js +14 -90
  100. package/dist/canvas/pickWalk.js.map +1 -1
  101. package/dist/canvas/poseCascade.js +1 -1
  102. package/dist/canvas/poseCascade.js.map +1 -1
  103. package/dist/canvas/publishPreviews.js +97 -0
  104. package/dist/canvas/publishPreviews.js.map +1 -0
  105. package/dist/canvas/sceneAdapter.js +7 -5
  106. package/dist/canvas/sceneAdapter.js.map +1 -1
  107. package/dist/canvas/sceneClipboard.js +17 -10
  108. package/dist/canvas/sceneClipboard.js.map +1 -1
  109. package/dist/canvas/scenePickReads.js +37 -0
  110. package/dist/canvas/scenePickReads.js.map +1 -0
  111. package/dist/canvas/scenePickSource.js +74 -0
  112. package/dist/canvas/scenePickSource.js.map +1 -0
  113. package/dist/canvas/sceneSlotWiring.js +9 -4
  114. package/dist/canvas/sceneSlotWiring.js.map +1 -1
  115. package/dist/canvas/sceneViewRender.js +13 -8
  116. package/dist/canvas/sceneViewRender.js.map +1 -1
  117. package/dist/canvas/shownNodes.js +15 -0
  118. package/dist/canvas/shownNodes.js.map +1 -0
  119. package/dist/canvas/useCanvasPaint.js +3 -1
  120. package/dist/canvas/useCanvasPaint.js.map +1 -1
  121. package/dist/canvas/useSceneAdapter.js +5 -3
  122. package/dist/canvas/useSceneAdapter.js.map +1 -1
  123. package/dist/clipboard.d.ts +2 -2
  124. package/dist/{clone-C3e0OTuF.d.ts → clone-qb2e1F5p.d.ts} +2 -2
  125. package/dist/clone.d.ts +3 -3
  126. package/dist/core/isPlainObject.js +2 -2
  127. package/dist/core/layers/layerGroup.js.map +1 -1
  128. package/dist/core/layers/render.js.map +1 -1
  129. package/dist/core/scene/NodeRouting.js.map +1 -1
  130. package/dist/core/scene/arrivalWindow.js +1 -1
  131. package/dist/core/scene/arrivalWindow.js.map +1 -1
  132. package/dist/core/scene/clipNothing.js +2 -5
  133. package/dist/core/scene/clipNothing.js.map +1 -1
  134. package/dist/core/scene/declaredFieldSetters.js +44 -0
  135. package/dist/core/scene/declaredFieldSetters.js.map +1 -0
  136. package/dist/core/scene/depMemo.js +3 -0
  137. package/dist/core/scene/depMemo.js.map +1 -1
  138. package/dist/core/scene/dependents.js +3 -1
  139. package/dist/core/scene/dependents.js.map +1 -1
  140. package/dist/core/scene/documentOf.js +43 -0
  141. package/dist/core/scene/documentOf.js.map +1 -0
  142. package/dist/core/scene/effectivePose.js +37 -13
  143. package/dist/core/scene/effectivePose.js.map +1 -1
  144. package/dist/core/scene/fieldEdits.js +17 -14
  145. package/dist/core/scene/fieldEdits.js.map +1 -1
  146. package/dist/core/scene/historyForm.js +4 -4
  147. package/dist/core/scene/historyForm.js.map +1 -1
  148. package/dist/core/scene/inheritedPaint.js +27 -5
  149. package/dist/core/scene/inheritedPaint.js.map +1 -1
  150. package/dist/core/scene/kitOps.js +3 -23
  151. package/dist/core/scene/kitOps.js.map +1 -1
  152. package/dist/core/scene/layoutPass.js +1 -2
  153. package/dist/core/scene/layoutPass.js.map +1 -1
  154. package/dist/core/scene/nodeAsShown.js +72 -0
  155. package/dist/core/scene/nodeAsShown.js.map +1 -0
  156. package/dist/core/scene/nodeFnFields.js +2 -10
  157. package/dist/core/scene/nodeFnFields.js.map +1 -1
  158. package/dist/core/scene/nodeMemo.js +6 -1
  159. package/dist/core/scene/nodeMemo.js.map +1 -1
  160. package/dist/core/scene/nodeStore.js +2 -4
  161. package/dist/core/scene/nodeStore.js.map +1 -1
  162. package/dist/core/scene/pathEffects/derive.js +3 -2
  163. package/dist/core/scene/pathEffects/derive.js.map +1 -1
  164. package/dist/core/scene/pathEffects/foundHidden.js +35 -0
  165. package/dist/core/scene/pathEffects/foundHidden.js.map +1 -0
  166. package/dist/core/scene/plyStacks.js +89 -0
  167. package/dist/core/scene/plyStacks.js.map +1 -0
  168. package/dist/core/scene/poseHold.js +42 -24
  169. package/dist/core/scene/poseHold.js.map +1 -1
  170. package/dist/core/scene/poseOverrides.js +35 -38
  171. package/dist/core/scene/poseOverrides.js.map +1 -1
  172. package/dist/core/scene/posePlies.js +59 -0
  173. package/dist/core/scene/posePlies.js.map +1 -0
  174. package/dist/core/scene/reconcileSpecs.js +6 -15
  175. package/dist/core/scene/reconcileSpecs.js.map +1 -1
  176. package/dist/core/scene/renderOrder.js +10 -4
  177. package/dist/core/scene/renderOrder.js.map +1 -1
  178. package/dist/core/scene/sameValue.js +17 -0
  179. package/dist/core/scene/sameValue.js.map +1 -0
  180. package/dist/core/scene/scene.js +3 -3
  181. package/dist/core/scene/scene.js.map +1 -1
  182. package/dist/core/scene/sceneEdits.js +8 -0
  183. package/dist/core/scene/sceneEdits.js.map +1 -1
  184. package/dist/core/scene/sceneState.js +1 -2
  185. package/dist/core/scene/sceneState.js.map +1 -1
  186. package/dist/core/scene/serialization.js +45 -9
  187. package/dist/core/scene/serialization.js.map +1 -1
  188. package/dist/core/scene/staleReplay.js +5 -2
  189. package/dist/core/scene/staleReplay.js.map +1 -1
  190. package/dist/core/scene/statedFields.js +4 -2
  191. package/dist/core/scene/statedFields.js.map +1 -1
  192. package/dist/core/scene/treeEdits.js +2 -4
  193. package/dist/core/scene/treeEdits.js.map +1 -1
  194. package/dist/core/scene/treeOrder.js +3 -1
  195. package/dist/core/scene/treeOrder.js.map +1 -1
  196. package/dist/core/scene/treeReads.js +1 -2
  197. package/dist/core/scene/treeReads.js.map +1 -1
  198. package/dist/core/scene/treeShape.js +13 -0
  199. package/dist/core/scene/treeShape.js.map +1 -0
  200. package/dist/core/scene/types.js.map +1 -1
  201. package/dist/core/scene/useScene.js +1 -1
  202. package/dist/core/scene/useScene.js.map +1 -1
  203. package/dist/{cornerHandles-p8fDIAKt.d.ts → cornerHandles-CNCb7aTY.d.ts} +1 -1
  204. package/dist/{depSchema-mG19aEdD.d.ts → depSchema-CpIQzKWQ.d.ts} +38 -59
  205. package/dist/devtools/handlers.js +1 -1
  206. package/dist/devtools/handlers.js.map +1 -1
  207. package/dist/devtools/protocol.js.map +1 -1
  208. package/dist/devtools-protocol.d.ts +4 -3
  209. package/dist/devtools.d.ts +3 -2
  210. package/dist/features/measure/measureHit.js +5 -3
  211. package/dist/features/measure/measureHit.js.map +1 -1
  212. package/dist/features/measure/measurement.js +1 -1
  213. package/dist/features/measure/measurement.js.map +1 -1
  214. package/dist/features/modes/modeLayers.js +14 -7
  215. package/dist/features/modes/modeLayers.js.map +1 -1
  216. package/dist/features/modes/modeTriggers.js +3 -3
  217. package/dist/features/modes/modeTriggers.js.map +1 -1
  218. package/dist/features/pathEffects/effectSources.js +25 -16
  219. package/dist/features/pathEffects/effectSources.js.map +1 -1
  220. package/dist/features/pathEffects/readSource.js +1 -1
  221. package/dist/features/pathEffects/readSource.js.map +1 -1
  222. package/dist/features/pathEffects/useEffectSourcesLayer.js +15 -19
  223. package/dist/features/pathEffects/useEffectSourcesLayer.js.map +1 -1
  224. package/dist/features/paths/editablePath.js +1 -1
  225. package/dist/features/paths/editablePath.js.map +1 -1
  226. package/dist/features/paths/pathSnap/sceneIndex.js +1 -1
  227. package/dist/features/paths/pathSnap/sceneIndex.js.map +1 -1
  228. package/dist/features/selection/enteredGroupFrame.js +65 -0
  229. package/dist/features/selection/enteredGroupFrame.js.map +1 -0
  230. package/dist/features/selection/enteredGroupScope.js +44 -0
  231. package/dist/features/selection/enteredGroupScope.js.map +1 -0
  232. package/dist/features/text/sceneTextDrafts.js +26 -27
  233. package/dist/features/text/sceneTextDrafts.js.map +1 -1
  234. package/dist/features/text/textHitAt.js +4 -2
  235. package/dist/features/text/textHitAt.js.map +1 -1
  236. package/dist/features/text/useSceneTextEdit.js +3 -3
  237. package/dist/features/text/useSceneTextEdit.js.map +1 -1
  238. package/dist/features/viewports/viewportLayer.js +2 -1
  239. package/dist/features/viewports/viewportLayer.js.map +1 -1
  240. package/dist/index.d.ts +230 -77
  241. package/dist/index.js +16 -6
  242. package/dist/interactions/actions/admitsChild.js +11 -0
  243. package/dist/interactions/actions/admitsChild.js.map +1 -0
  244. package/dist/interactions/actions/defaults/contextMenu.js +16 -3
  245. package/dist/interactions/actions/defaults/contextMenu.js.map +1 -1
  246. package/dist/interactions/actions/defaults/createPaintAction.js +4 -1
  247. package/dist/interactions/actions/defaults/createPaintAction.js.map +1 -1
  248. package/dist/interactions/actions/defaults/enterGroup.js +88 -0
  249. package/dist/interactions/actions/defaults/enterGroup.js.map +1 -0
  250. package/dist/interactions/actions/defaults/enterPathEdit.js +1 -1
  251. package/dist/interactions/actions/defaults/enterPathEdit.js.map +1 -1
  252. package/dist/interactions/actions/defaults/enterTextEdit.js +1 -1
  253. package/dist/interactions/actions/defaults/enterTextEdit.js.map +1 -1
  254. package/dist/interactions/actions/defaults/group.js +7 -7
  255. package/dist/interactions/actions/defaults/group.js.map +1 -1
  256. package/dist/interactions/actions/defaults/selectAll.js +7 -3
  257. package/dist/interactions/actions/defaults/selectAll.js.map +1 -1
  258. package/dist/interactions/actions/defaults/selectDeep.js +35 -0
  259. package/dist/interactions/actions/defaults/selectDeep.js.map +1 -0
  260. package/dist/interactions/actions/drawnBoxes.js +2 -1
  261. package/dist/interactions/actions/drawnBoxes.js.map +1 -1
  262. package/dist/interactions/actions/insert/action.js +1 -0
  263. package/dist/interactions/actions/insert/action.js.map +1 -1
  264. package/dist/interactions/actions/insert/parent.js +7 -5
  265. package/dist/interactions/actions/insert/parent.js.map +1 -1
  266. package/dist/interactions/actions/insert/placeAction.js +3 -2
  267. package/dist/interactions/actions/insert/placeAction.js.map +1 -1
  268. package/dist/interactions/actions/insert/session.js +2 -2
  269. package/dist/interactions/actions/insert/session.js.map +1 -1
  270. package/dist/interactions/actions/move/commit/reparent.js +1 -1
  271. package/dist/interactions/actions/move/commit/reparent.js.map +1 -1
  272. package/dist/interactions/actions/move/gestureAdapter.js +6 -7
  273. package/dist/interactions/actions/move/gestureAdapter.js.map +1 -1
  274. package/dist/interactions/actions/move/layout/exits.js +1 -1
  275. package/dist/interactions/actions/move/layout/exits.js.map +1 -1
  276. package/dist/interactions/actions/move/session.js +4 -4
  277. package/dist/interactions/actions/move/session.js.map +1 -1
  278. package/dist/interactions/actions/pathCommands/pathNodes.js +14 -5
  279. package/dist/interactions/actions/pathCommands/pathNodes.js.map +1 -1
  280. package/dist/interactions/gestures/thresholdDrag/thresholdDrag.js +14 -2
  281. package/dist/interactions/gestures/thresholdDrag/thresholdDrag.js.map +1 -1
  282. package/dist/{math-B56zxVs5.d.ts → math-C_elShP_.d.ts} +2 -5
  283. package/dist/math.d.ts +1 -1
  284. package/dist/move.d.ts +4 -4
  285. package/dist/{options-DBxEryYq.d.ts → options-ClEm3SEl.d.ts} +1 -1
  286. package/dist/resize.d.ts +4 -4
  287. package/dist/test-seams.d.ts +6 -6
  288. package/dist/tools/builtin/pressTarget.js +22 -0
  289. package/dist/tools/builtin/pressTarget.js.map +1 -0
  290. package/dist/tools/builtin/select/adapterPointQuery.js +3 -3
  291. package/dist/tools/builtin/select/adapterPointQuery.js.map +1 -1
  292. package/dist/tools/builtin/select/collapseDeferred.js +40 -0
  293. package/dist/tools/builtin/select/collapseDeferred.js.map +1 -0
  294. package/dist/tools/builtin/select/groupEntry.js +82 -0
  295. package/dist/tools/builtin/select/groupEntry.js.map +1 -0
  296. package/dist/tools/builtin/select/pressDecision.js +1 -1
  297. package/dist/tools/builtin/select/pressDecision.js.map +1 -1
  298. package/dist/tools/builtin/select/useSelectTool.js +61 -45
  299. package/dist/tools/builtin/select/useSelectTool.js.map +1 -1
  300. package/dist/{types-7puAuVD2.d.ts → types-49uYJN3j.d.ts} +1 -1
  301. package/dist/{types-CniVctmk.d.ts → types-BEdRIhNc.d.ts} +272 -109
  302. package/dist/{types-oFeQ5VjV.d.ts → types-p2alu-R1.d.ts} +1 -1
  303. package/dist/version.js +1 -1
  304. package/package.json +25 -25
@@ -4,35 +4,30 @@ var NODE_FN_FIELDS = [
4
4
  field: "clipFromPose",
5
5
  opKey: "clipKey",
6
6
  jsonKey: "clipFromPoseKey",
7
- containersOnly: true,
8
7
  lost: "clip"
9
8
  },
10
9
  {
11
10
  field: "copiesFromPose",
12
11
  opKey: "copiesKey",
13
12
  jsonKey: "copiesFromPoseKey",
14
- containersOnly: true,
15
13
  lost: "copies"
16
14
  },
17
15
  {
18
16
  field: "derivePath",
19
17
  opKey: "derivePathKey",
20
18
  jsonKey: "derivePathKey",
21
- containersOnly: false,
22
19
  lost: "derived path"
23
20
  },
24
21
  {
25
22
  field: "derivePose",
26
23
  opKey: "derivePoseKey",
27
24
  jsonKey: "derivePoseKey",
28
- containersOnly: false,
29
25
  lost: "derived pose"
30
26
  },
31
27
  {
32
28
  field: "layout",
33
29
  opKey: "layoutKey",
34
30
  jsonKey: "layoutKey",
35
- containersOnly: true,
36
31
  lost: "layout"
37
32
  }
38
33
  ];
@@ -41,14 +36,12 @@ function createNodeFnFields(registry, onWrite) {
41
36
  const table = registry[spec.field];
42
37
  const reverse = /* @__PURE__ */ new Map();
43
38
  if (table) for (const [key, fn] of Object.entries(table)) reverse.set(fn, key);
44
- const applies = (node) => !spec.containersOnly || node.kind === "container";
45
39
  return {
46
40
  spec,
47
41
  keyOf: (fn) => reverse.get(fn),
48
42
  fnOf: (key) => table?.[key],
49
- read: (node) => applies(node) ? node[spec.field] : void 0,
43
+ read: (node) => node[spec.field],
50
44
  write: (node, fn) => {
51
- if (!applies(node)) return;
52
45
  node[spec.field] = fn;
53
46
  onWrite?.(spec.field, node);
54
47
  }
@@ -87,14 +80,13 @@ function fnKeysOf(fields, node, which, onUnregistered) {
87
80
  /**
88
81
  * The function fields to carry from a live node into an `AddNodeSpec`. An
89
82
  * insert that drops them brings an edge back as a static path that never
90
- * follows its endpoints again, and a clipped container back with nothing
83
+ * follows its endpoints again, and a clipped group back with nothing
91
84
  * clipped.
92
85
  */
93
86
  function fnFieldsOfNode(node) {
94
87
  const src = node;
95
88
  const out = {};
96
89
  for (const spec of NODE_FN_FIELDS) {
97
- if (spec.containersOnly && node.kind !== "container") continue;
98
90
  const fn = src[spec.field];
99
91
  if (fn !== void 0) out[spec.field] = fn;
100
92
  }
@@ -1 +1 @@
1
- {"version":3,"file":"nodeFnFields.js","names":[],"sources":["../../../src/core/scene/nodeFnFields.ts"],"sourcesContent":["/**\n * The registry-keyed function fields a node can carry, as a table.\n *\n * These fields are live functions on a node, and a function travels through\n * neither a serializable op payload nor a JSON snapshot. Each one therefore\n * needs the same things: a function→key reverse map, a name in the op\n * payload, a name in the snapshot, and a registry lookup on restore.\n */\nimport type { NodeId, SceneRegistry } from './types';\n\n/** A node's function field, opaque here: the table moves these around by\n * reference and never calls one. A container's `layout` is an object rather\n * than a function, and rides the same table for the same reason. */\nexport type NodeFn = ((...args: never[]) => unknown) | object;\n\n/** A node as this module reads it — a bag with the function fields on it. */\nexport type FnBearingNode = { id: NodeId; kind: 'leaf' | 'container' } & {\n [K in NodeFnFieldName]?: NodeFn;\n};\n\nexport type NodeFnFieldName = 'clipFromPose' | 'copiesFromPose' | 'derivePath' | 'derivePose' | 'layout';\n\nexport interface NodeFnFieldSpec {\n /** The field's name on `Node` — and, deliberately, its name in\n * `SceneRegistry` too, so one string indexes both. */\n readonly field: NodeFnFieldName;\n /** Its key in the `kit:add` and `kit:remove` op payloads. `clipFromPose`'s\n * is the short one because a persisted history already carries that name. */\n readonly opKey: string;\n /** Its key in a `SerializedNode`. */\n readonly jsonKey: string;\n /** True for a field only a `ContainerNode` can carry. */\n readonly containersOnly: boolean;\n /** What a node loses when its key is not in the registry, for the warning. */\n readonly lost: string;\n}\n\nexport const NODE_FN_FIELDS: readonly NodeFnFieldSpec[] = [\n {\n field: 'clipFromPose',\n opKey: 'clipKey',\n jsonKey: 'clipFromPoseKey',\n containersOnly: true,\n lost: 'clip',\n },\n {\n field: 'copiesFromPose',\n opKey: 'copiesKey',\n jsonKey: 'copiesFromPoseKey',\n containersOnly: true,\n lost: 'copies',\n },\n {\n field: 'derivePath',\n opKey: 'derivePathKey',\n jsonKey: 'derivePathKey',\n containersOnly: false,\n lost: 'derived path',\n },\n {\n field: 'derivePose',\n opKey: 'derivePoseKey',\n jsonKey: 'derivePoseKey',\n containersOnly: false,\n lost: 'derived pose',\n },\n {\n field: 'layout',\n opKey: 'layoutKey',\n jsonKey: 'layoutKey',\n containersOnly: true,\n lost: 'layout',\n },\n];\n\n/** One field's per-scene state. */\nexport interface NodeFnField {\n readonly spec: NodeFnFieldSpec;\n /** `fn → registry key`, for writing a payload or a snapshot. */\n keyOf(fn: NodeFn): string | undefined;\n /** `registry key → fn`, for restoring one. */\n fnOf(key: string): NodeFn | undefined;\n /** The function on `node`, or `undefined` — including when the field is\n * container-only and `node` is a leaf. */\n read(node: FnBearingNode): NodeFn | undefined;\n /** Attach `fn` to `node`, ignoring a container-only field on a leaf. */\n write(node: FnBearingNode, fn: NodeFn): void;\n}\n\nexport function createNodeFnFields<TPose>(\n registry: SceneRegistry<TPose>,\n /** Called after a field is written onto a node it applies to. */\n onWrite?: (field: NodeFnFieldName, node: FnBearingNode) => void,\n): readonly NodeFnField[] {\n return NODE_FN_FIELDS.map((spec) => {\n const table = registry[spec.field] as Readonly<Record<string, NodeFn>> | undefined;\n const reverse = new Map<NodeFn, string>();\n if (table) for (const [key, fn] of Object.entries(table)) reverse.set(fn, key);\n const applies = (node: FnBearingNode): boolean =>\n !spec.containersOnly || node.kind === 'container';\n return {\n spec,\n keyOf: (fn) => reverse.get(fn),\n fnOf: (key) => table?.[key],\n read: (node) => (applies(node) ? node[spec.field] : undefined),\n write: (node, fn) => {\n if (!applies(node)) return;\n node[spec.field] = fn;\n onWrite?.(spec.field, node);\n },\n };\n });\n}\n\n/**\n * Attach the function `key` names to `node`. Warns and leaves the field\n * unset when the key is not in this scene's registry — a snapshot outliving\n * the code that registered its functions must still load.\n */\nexport function restoreFn(\n field: NodeFnField,\n node: FnBearingNode,\n key: string,\n site: string,\n warn: (channel: string, message: string) => void,\n): void {\n const fn = field.fnOf(key);\n if (fn === undefined) {\n warn(\n 'scene',\n `${site}: ${field.spec.opKey} \"${key}\" not in this scene's registry — node \"${node.id}\" ` +\n `restored without its ${field.spec.lost}. Register a function with this key in the ` +\n `registry option to restore it.`,\n );\n return;\n }\n field.write(node, fn);\n}\n\n/** The `{ clipKey?, derivePathKey?, … }` an op payload or a snapshot carries\n * for `node`. Empty when the node has no keyed function on it. */\nexport function fnKeysOf(\n fields: readonly NodeFnField[],\n node: FnBearingNode,\n which: 'opKey' | 'jsonKey',\n onUnregistered?: (field: NodeFnField) => never,\n): Record<string, string> {\n const out: Record<string, string> = {};\n for (const field of fields) {\n const fn = field.read(node);\n if (fn === undefined) continue;\n const key = field.keyOf(fn);\n if (key === undefined) {\n onUnregistered?.(field);\n continue;\n }\n out[field.spec[which]] = key;\n }\n return out;\n}\n\n/**\n * The function fields to carry from a live node into an `AddNodeSpec`. An\n * insert that drops them brings an edge back as a static path that never\n * follows its endpoints again, and a clipped container back with nothing\n * clipped.\n */\nexport function fnFieldsOfNode<T extends { kind: 'leaf' | 'container' }>(\n node: T,\n): Partial<Pick<T, Extract<NodeFnFieldName, keyof T>>> {\n const src = node as Record<string, unknown>;\n const out: Record<string, unknown> = {};\n for (const spec of NODE_FN_FIELDS) {\n if (spec.containersOnly && node.kind !== 'container') continue;\n const fn = src[spec.field];\n if (fn !== undefined) out[spec.field] = fn;\n }\n return out as Partial<Pick<T, Extract<NodeFnFieldName, keyof T>>>;\n}\n"],"mappings":";AAqCA,IAAa,iBAA6C;CACxD;EACE,OAAO;EACP,OAAO;EACP,SAAS;EACT,gBAAgB;EAChB,MAAM;CACR;CACA;EACE,OAAO;EACP,OAAO;EACP,SAAS;EACT,gBAAgB;EAChB,MAAM;CACR;CACA;EACE,OAAO;EACP,OAAO;EACP,SAAS;EACT,gBAAgB;EAChB,MAAM;CACR;CACA;EACE,OAAO;EACP,OAAO;EACP,SAAS;EACT,gBAAgB;EAChB,MAAM;CACR;CACA;EACE,OAAO;EACP,OAAO;EACP,SAAS;EACT,gBAAgB;EAChB,MAAM;CACR;AACF;AAgBA,SAAgB,mBACd,UAEA,SACwB;CACxB,OAAO,eAAe,KAAK,SAAS;EAClC,MAAM,QAAQ,SAAS,KAAK;EAC5B,MAAM,0BAAU,IAAI,IAAoB;EACxC,IAAI,OAAO,KAAK,MAAM,CAAC,KAAK,OAAO,OAAO,QAAQ,KAAK,GAAG,QAAQ,IAAI,IAAI,GAAG;EAC7E,MAAM,WAAW,SACf,CAAC,KAAK,kBAAkB,KAAK,SAAS;EACxC,OAAO;GACL;GACA,QAAQ,OAAO,QAAQ,IAAI,EAAE;GAC7B,OAAO,QAAQ,QAAQ;GACvB,OAAO,SAAU,QAAQ,IAAI,IAAI,KAAK,KAAK,SAAS;GACpD,QAAQ,MAAM,OAAO;IACnB,IAAI,CAAC,QAAQ,IAAI,GAAG;IACpB,KAAK,KAAK,SAAS;IACnB,UAAU,KAAK,OAAO,IAAI;GAC5B;EACF;CACF,CAAC;AACH;;;;;;AAOA,SAAgB,UACd,OACA,MACA,KACA,MACA,MACM;CACN,MAAM,KAAK,MAAM,KAAK,GAAG;CACzB,IAAI,OAAO,QAAW;EACpB,KACE,SACA,GAAG,KAAK,IAAI,MAAM,KAAK,MAAM,IAAI,IAAI,yCAAyC,KAAK,GAAG,yBAC5D,MAAM,KAAK,KAAK,0EAE5C;EACA;CACF;CACA,MAAM,MAAM,MAAM,EAAE;AACtB;;;AAIA,SAAgB,SACd,QACA,MACA,OACA,gBACwB;CACxB,MAAM,MAA8B,CAAC;CACrC,KAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,KAAK,MAAM,KAAK,IAAI;EAC1B,IAAI,OAAO,QAAW;EACtB,MAAM,MAAM,MAAM,MAAM,EAAE;EAC1B,IAAI,QAAQ,QAAW;GACrB,iBAAiB,KAAK;GACtB;EACF;EACA,IAAI,MAAM,KAAK,UAAU;CAC3B;CACA,OAAO;AACT;;;;;;;AAQA,SAAgB,eACd,MACqD;CACrD,MAAM,MAAM;CACZ,MAAM,MAA+B,CAAC;CACtC,KAAK,MAAM,QAAQ,gBAAgB;EACjC,IAAI,KAAK,kBAAkB,KAAK,SAAS,aAAa;EACtD,MAAM,KAAK,IAAI,KAAK;EACpB,IAAI,OAAO,QAAW,IAAI,KAAK,SAAS;CAC1C;CACA,OAAO;AACT"}
1
+ {"version":3,"file":"nodeFnFields.js","names":[],"sources":["../../../src/core/scene/nodeFnFields.ts"],"sourcesContent":["/**\n * The registry-keyed function fields a node can carry, as a table.\n *\n * These fields are live functions on a node, and a function travels through\n * neither a serializable op payload nor a JSON snapshot. Each one therefore\n * needs the same things: a function→key reverse map, a name in the op\n * payload, a name in the snapshot, and a registry lookup on restore.\n */\nimport type { NodeId, SceneRegistry } from './types';\n\n/** A node's function field, opaque here: the table moves these around by\n * reference and never calls one. A node's `layout` is an object rather\n * than a function, and rides the same table for the same reason. */\nexport type NodeFn = ((...args: never[]) => unknown) | object;\n\n/** A node as this module reads it — a bag with the function fields on it. */\nexport type FnBearingNode = { id: NodeId; kind: 'mark' | 'group' } & {\n [K in NodeFnFieldName]?: NodeFn;\n};\n\nexport type NodeFnFieldName = 'clipFromPose' | 'copiesFromPose' | 'derivePath' | 'derivePose' | 'layout';\n\nexport interface NodeFnFieldSpec {\n /** The field's name on `Node` — and, deliberately, its name in\n * `SceneRegistry` too, so one string indexes both. */\n readonly field: NodeFnFieldName;\n /** Its key in the `kit:add` and `kit:remove` op payloads. `clipFromPose`'s\n * is the short one because a persisted history already carries that name. */\n readonly opKey: string;\n /** Its key in a `SerializedNode`. */\n readonly jsonKey: string;\n /** What a node loses when its key is not in the registry, for the warning. */\n readonly lost: string;\n}\n\nexport const NODE_FN_FIELDS: readonly NodeFnFieldSpec[] = [\n {\n field: 'clipFromPose',\n opKey: 'clipKey',\n jsonKey: 'clipFromPoseKey',\n lost: 'clip',\n },\n {\n field: 'copiesFromPose',\n opKey: 'copiesKey',\n jsonKey: 'copiesFromPoseKey',\n lost: 'copies',\n },\n {\n field: 'derivePath',\n opKey: 'derivePathKey',\n jsonKey: 'derivePathKey',\n lost: 'derived path',\n },\n {\n field: 'derivePose',\n opKey: 'derivePoseKey',\n jsonKey: 'derivePoseKey',\n lost: 'derived pose',\n },\n {\n field: 'layout',\n opKey: 'layoutKey',\n jsonKey: 'layoutKey',\n lost: 'layout',\n },\n];\n\n/** One field's per-scene state. */\nexport interface NodeFnField {\n readonly spec: NodeFnFieldSpec;\n /** `fn → registry key`, for writing a payload or a snapshot. */\n keyOf(fn: NodeFn): string | undefined;\n /** `registry key → fn`, for restoring one. */\n fnOf(key: string): NodeFn | undefined;\n /** The function on `node`, or `undefined`. */\n read(node: FnBearingNode): NodeFn | undefined;\n /** Attach `fn` to `node`. */\n write(node: FnBearingNode, fn: NodeFn): void;\n}\n\nexport function createNodeFnFields<TPose>(\n registry: SceneRegistry<TPose>,\n /** Called after a field is written onto a node it applies to. */\n onWrite?: (field: NodeFnFieldName, node: FnBearingNode) => void,\n): readonly NodeFnField[] {\n return NODE_FN_FIELDS.map((spec) => {\n const table = registry[spec.field] as Readonly<Record<string, NodeFn>> | undefined;\n const reverse = new Map<NodeFn, string>();\n if (table) for (const [key, fn] of Object.entries(table)) reverse.set(fn, key);\n return {\n spec,\n keyOf: (fn) => reverse.get(fn),\n fnOf: (key) => table?.[key],\n read: (node) => node[spec.field],\n write: (node, fn) => {\n node[spec.field] = fn;\n onWrite?.(spec.field, node);\n },\n };\n });\n}\n\n/**\n * Attach the function `key` names to `node`. Warns and leaves the field\n * unset when the key is not in this scene's registry — a snapshot outliving\n * the code that registered its functions must still load.\n */\nexport function restoreFn(\n field: NodeFnField,\n node: FnBearingNode,\n key: string,\n site: string,\n warn: (channel: string, message: string) => void,\n): void {\n const fn = field.fnOf(key);\n if (fn === undefined) {\n warn(\n 'scene',\n `${site}: ${field.spec.opKey} \"${key}\" not in this scene's registry — node \"${node.id}\" ` +\n `restored without its ${field.spec.lost}. Register a function with this key in the ` +\n `registry option to restore it.`,\n );\n return;\n }\n field.write(node, fn);\n}\n\n/** The `{ clipKey?, derivePathKey?, … }` an op payload or a snapshot carries\n * for `node`. Empty when the node has no keyed function on it. */\nexport function fnKeysOf(\n fields: readonly NodeFnField[],\n node: FnBearingNode,\n which: 'opKey' | 'jsonKey',\n onUnregistered?: (field: NodeFnField) => never,\n): Record<string, string> {\n const out: Record<string, string> = {};\n for (const field of fields) {\n const fn = field.read(node);\n if (fn === undefined) continue;\n const key = field.keyOf(fn);\n if (key === undefined) {\n onUnregistered?.(field);\n continue;\n }\n out[field.spec[which]] = key;\n }\n return out;\n}\n\n/**\n * The function fields to carry from a live node into an `AddNodeSpec`. An\n * insert that drops them brings an edge back as a static path that never\n * follows its endpoints again, and a clipped group back with nothing\n * clipped.\n */\nexport function fnFieldsOfNode<T extends { kind: 'mark' | 'group' }>(\n node: T,\n): Partial<Pick<T, Extract<NodeFnFieldName, keyof T>>> {\n const src = node as Record<string, unknown>;\n const out: Record<string, unknown> = {};\n for (const spec of NODE_FN_FIELDS) {\n const fn = src[spec.field];\n if (fn !== undefined) out[spec.field] = fn;\n }\n return out as Partial<Pick<T, Extract<NodeFnFieldName, keyof T>>>;\n}\n"],"mappings":";AAmCA,IAAa,iBAA6C;CACxD;EACE,OAAO;EACP,OAAO;EACP,SAAS;EACT,MAAM;CACR;CACA;EACE,OAAO;EACP,OAAO;EACP,SAAS;EACT,MAAM;CACR;CACA;EACE,OAAO;EACP,OAAO;EACP,SAAS;EACT,MAAM;CACR;CACA;EACE,OAAO;EACP,OAAO;EACP,SAAS;EACT,MAAM;CACR;CACA;EACE,OAAO;EACP,OAAO;EACP,SAAS;EACT,MAAM;CACR;AACF;AAeA,SAAgB,mBACd,UAEA,SACwB;CACxB,OAAO,eAAe,KAAK,SAAS;EAClC,MAAM,QAAQ,SAAS,KAAK;EAC5B,MAAM,0BAAU,IAAI,IAAoB;EACxC,IAAI,OAAO,KAAK,MAAM,CAAC,KAAK,OAAO,OAAO,QAAQ,KAAK,GAAG,QAAQ,IAAI,IAAI,GAAG;EAC7E,OAAO;GACL;GACA,QAAQ,OAAO,QAAQ,IAAI,EAAE;GAC7B,OAAO,QAAQ,QAAQ;GACvB,OAAO,SAAS,KAAK,KAAK;GAC1B,QAAQ,MAAM,OAAO;IACnB,KAAK,KAAK,SAAS;IACnB,UAAU,KAAK,OAAO,IAAI;GAC5B;EACF;CACF,CAAC;AACH;;;;;;AAOA,SAAgB,UACd,OACA,MACA,KACA,MACA,MACM;CACN,MAAM,KAAK,MAAM,KAAK,GAAG;CACzB,IAAI,OAAO,QAAW;EACpB,KACE,SACA,GAAG,KAAK,IAAI,MAAM,KAAK,MAAM,IAAI,IAAI,yCAAyC,KAAK,GAAG,yBAC5D,MAAM,KAAK,KAAK,0EAE5C;EACA;CACF;CACA,MAAM,MAAM,MAAM,EAAE;AACtB;;;AAIA,SAAgB,SACd,QACA,MACA,OACA,gBACwB;CACxB,MAAM,MAA8B,CAAC;CACrC,KAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,KAAK,MAAM,KAAK,IAAI;EAC1B,IAAI,OAAO,QAAW;EACtB,MAAM,MAAM,MAAM,MAAM,EAAE;EAC1B,IAAI,QAAQ,QAAW;GACrB,iBAAiB,KAAK;GACtB;EACF;EACA,IAAI,MAAM,KAAK,UAAU;CAC3B;CACA,OAAO;AACT;;;;;;;AAQA,SAAgB,eACd,MACqD;CACrD,MAAM,MAAM;CACZ,MAAM,MAA+B,CAAC;CACtC,KAAK,MAAM,QAAQ,gBAAgB;EACjC,MAAM,KAAK,IAAI,KAAK;EACpB,IAAI,OAAO,QAAW,IAAI,KAAK,SAAS;CAC1C;CACA,OAAO;AACT"}
@@ -86,6 +86,11 @@ function scopedNodeMemo(node, scope, pose, derive, also) {
86
86
  function bumpNodeMemoGeneration() {
87
87
  generation++;
88
88
  }
89
+ /** The generation {@link bumpNodeMemoGeneration} last moved to: a freshness
90
+ * key for a cache outside this memo that reads the same ambient state. */
91
+ function nodeMemoGeneration() {
92
+ return generation;
93
+ }
89
94
  /**
90
95
  * Drop the slots of `node` that were keyed on a pose, keeping the ones that
91
96
  * weren't.
@@ -110,5 +115,5 @@ function dropPoseKeyedMemoSlots(node) {
110
115
  }
111
116
 
112
117
  //#endregion
113
- export { bumpNodeMemoGeneration, createNodeMemoScope, dropPoseKeyedMemoSlots, nodeMemo };
118
+ export { bumpNodeMemoGeneration, createNodeMemoScope, dropPoseKeyedMemoSlots, nodeMemo, nodeMemoGeneration };
114
119
  //# sourceMappingURL=nodeMemo.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"nodeMemo.js","names":[],"sources":["../../../src/core/scene/nodeMemo.ts"],"sourcesContent":["/**\n * `nodeMemo` — memoize anything derived from a node, keyed on the node's\n * identity plus the `pose` / `data` references the scene swaps on every edit.\n *\n * The scene mutates node objects in place but *replaces* `pose` and `data`\n * with new references, so those two references are an exact freshness signal.\n * The assumption it rests on: nobody mutates `node.data` (or `node.pose`) in\n * place without replacing the reference. A consumer who violates it sees a\n * stale derivation until the next real edit.\n *\n * **What the key does not cover** is ambient state that `derive` reads — a\n * registry, a font set, a decode cache. Whoever owns the ambient state calls\n * {@link bumpNodeMemoGeneration}. Derivations that read ambient state with\n * *no* such signal (an async image decode landing, say) must not be memoized\n * here at all.\n */\n\n/** One memoized derivation: the references it was derived against, plus the\n * value. Presence in the slot map is what \"computed\" means — a `null` or\n * `undefined` value is a real answer and stays cached. */\ninterface MemoSlot {\n /** Reference-compared against the caller's `pose` argument. */\n pose: unknown;\n /** Reference-compared against `node.data` at call time. */\n data: unknown;\n /** Reference-compared against the caller's `also` argument. */\n also: unknown;\n value: unknown;\n}\n\ninterface NodeMemoRecord {\n /** `generation` at the time the slots were filled. */\n generation: number;\n slots: Map<string, MemoSlot>;\n /** How many times {@link dropPoseKeyedMemoSlots} has run for this node,\n * which is how a drop reaches the scopes it cannot enumerate. */\n drops: number;\n}\n\n/** A scoped entry also carries what a named slot's record holds for it. */\ninterface ScopedSlot extends MemoSlot {\n generation: number;\n drops: number;\n}\n\ndeclare const scopeBrand: unique symbol;\n\n/**\n * A private place to memoize one derivation, in place of a slot name.\n *\n * A named slot lives on the node, so it lasts as long as the node does. That\n * is wrong for a derivation that belongs to some shorter-lived reader — one\n * `toPose` function among many over the life of a scene. A scope is held by\n * that reader, and its entries go when the reader does.\n */\nexport type NodeMemoScope = { readonly [scopeBrand]: true };\n\nexport function createNodeMemoScope(): NodeMemoScope {\n return new WeakMap<object, ScopedSlot>() as unknown as NodeMemoScope;\n}\n\nconst MEMO = new WeakMap<object, NodeMemoRecord>();\n\n/** Bumped whenever ambient state a derivation reads has changed. Entries\n * stamped with an older value are dropped rather than served stale. */\nlet generation = 0;\n\n/** A node, as far as the memo is concerned: an object identity that may carry\n * a `data` reference. */\ntype MemoizableNode = { data?: unknown };\n\n/**\n * Return the value `derive` produces for `node`, recomputing only when the\n * node's `data` reference, the given `pose` reference, the `also` reference,\n * or the global generation has changed since the last call for this `slot`.\n *\n * `slot` names the derivation, so a node's several derived values do not\n * overwrite each other.\n *\n * `pose` is a **parameter rather than a read off the node**, because callers\n * legitimately derive against a pose that isn't `node.pose`: a preview or\n * ghost pose mid-drag. Pass `undefined` for derivations that don't depend on\n * the pose at all, leaving `data` as the only key.\n *\n * `also` is for an input that lives outside the node, such as the paint a\n * node inherits from its container. It must be a reference its owner keeps\n * while the input is unchanged.\n */\nexport function nodeMemo<T>(\n node: MemoizableNode,\n slot: string | NodeMemoScope,\n pose: unknown,\n derive: () => T,\n also?: unknown,\n): T {\n if (typeof slot !== 'string') return scopedNodeMemo(node, slot, pose, derive, also);\n let record = MEMO.get(node);\n if (record === undefined) {\n record = { generation, slots: new Map(), drops: 0 };\n MEMO.set(node, record);\n } else if (record.generation !== generation) {\n record.generation = generation;\n record.slots.clear();\n }\n\n const data = node.data;\n const hit = record.slots.get(slot);\n if (hit !== undefined && hit.pose === pose && hit.data === data && hit.also === also) {\n return hit.value as T;\n }\n\n const value = derive();\n // Reuse the slot object when there is one: this runs per node per frame, and\n // the entry churn is the only allocation the memo would otherwise add.\n if (hit !== undefined) {\n hit.pose = pose;\n hit.data = data;\n hit.also = also;\n hit.value = value;\n } else {\n record.slots.set(slot, { pose, data, also, value });\n }\n return value;\n}\n\nfunction scopedNodeMemo<T>(\n node: MemoizableNode,\n scope: NodeMemoScope,\n pose: unknown,\n derive: () => T,\n also: unknown,\n): T {\n const entries = scope as unknown as WeakMap<object, ScopedSlot>;\n const data = node.data;\n const drops = MEMO.get(node)?.drops ?? 0;\n const hit = entries.get(node);\n if (\n hit !== undefined && hit.generation === generation && hit.pose === pose && hit.data === data\n && hit.also === also && (pose === undefined || hit.drops === drops)\n ) {\n return hit.value as T;\n }\n const value = derive();\n // Read again: a derivation that hit a cycle drops this node's slots itself.\n const dropsNow = MEMO.get(node)?.drops ?? 0;\n if (hit !== undefined) {\n hit.pose = pose;\n hit.data = data;\n hit.also = also;\n hit.value = value;\n hit.generation = generation;\n hit.drops = dropsNow;\n } else {\n entries.set(node, { pose, data, also, value, generation, drops: dropsNow });\n }\n return value;\n}\n\n/** How many named slots `node` holds. For tests of what is kept on a node. */\nexport function nodeMemoSlotCount(node: MemoizableNode): number {\n const record = MEMO.get(node);\n return record === undefined || record.generation !== generation ? 0 : record.slots.size;\n}\n\n/**\n * Drop every memoized slot on every node. For owners of ambient state that\n * changes what a `derive` would return. Cheap — it bumps a counter.\n */\nexport function bumpNodeMemoGeneration(): void {\n generation++;\n}\n\n/**\n * Drop the slots of `node` that were keyed on a pose, keeping the ones that\n * weren't.\n *\n * For per-frame pose overrides, which mutate one buffer in place rather than\n * replacing the reference, which the `(pose, data)` key cannot see.\n * {@link bumpNodeMemoGeneration} is the wrong tool there — it would drop every\n * static node's slots too, every frame.\n */\nexport function dropPoseKeyedMemoSlots(node: MemoizableNode): void {\n const record = MEMO.get(node);\n if (record === undefined) {\n MEMO.set(node, { generation, slots: new Map(), drops: 1 });\n return;\n }\n record.drops++;\n for (const [slot, entry] of record.slots) {\n if (entry.pose !== undefined) record.slots.delete(slot);\n }\n}\n"],"mappings":";AAyDA,SAAgB,sBAAqC;CACnD,uBAAO,IAAI,QAA4B;AACzC;AAEA,IAAM,uBAAO,IAAI,QAAgC;;;AAIjD,IAAI,aAAa;;;;;;;;;;;;;;;;;;AAuBjB,SAAgB,SACd,MACA,MACA,MACA,QACA,MACG;CACH,IAAI,OAAO,SAAS,UAAU,OAAO,eAAe,MAAM,MAAM,MAAM,QAAQ,IAAI;CAClF,IAAI,SAAS,KAAK,IAAI,IAAI;CAC1B,IAAI,WAAW,QAAW;EACxB,SAAS;GAAE;GAAY,uBAAO,IAAI,IAAI;GAAG,OAAO;EAAE;EAClD,KAAK,IAAI,MAAM,MAAM;CACvB,OAAO,IAAI,OAAO,eAAe,YAAY;EAC3C,OAAO,aAAa;EACpB,OAAO,MAAM,MAAM;CACrB;CAEA,MAAM,OAAO,KAAK;CAClB,MAAM,MAAM,OAAO,MAAM,IAAI,IAAI;CACjC,IAAI,QAAQ,UAAa,IAAI,SAAS,QAAQ,IAAI,SAAS,QAAQ,IAAI,SAAS,MAC9E,OAAO,IAAI;CAGb,MAAM,QAAQ,OAAO;CAGrB,IAAI,QAAQ,QAAW;EACrB,IAAI,OAAO;EACX,IAAI,OAAO;EACX,IAAI,OAAO;EACX,IAAI,QAAQ;CACd,OACE,OAAO,MAAM,IAAI,MAAM;EAAE;EAAM;EAAM;EAAM;CAAM,CAAC;CAEpD,OAAO;AACT;AAEA,SAAS,eACP,MACA,OACA,MACA,QACA,MACG;CACH,MAAM,UAAU;CAChB,MAAM,OAAO,KAAK;CAClB,MAAM,QAAQ,KAAK,IAAI,IAAI,CAAC,EAAE,SAAS;CACvC,MAAM,MAAM,QAAQ,IAAI,IAAI;CAC5B,IACE,QAAQ,UAAa,IAAI,eAAe,cAAc,IAAI,SAAS,QAAQ,IAAI,SAAS,QACrF,IAAI,SAAS,SAAY,SAAS,UAAa,IAAI,UAAU,QAEhE,OAAO,IAAI;CAEb,MAAM,QAAQ,OAAO;CAErB,MAAM,WAAW,KAAK,IAAI,IAAI,CAAC,EAAE,SAAS;CAC1C,IAAI,QAAQ,QAAW;EACrB,IAAI,OAAO;EACX,IAAI,OAAO;EACX,IAAI,OAAO;EACX,IAAI,QAAQ;EACZ,IAAI,aAAa;EACjB,IAAI,QAAQ;CACd,OACE,QAAQ,IAAI,MAAM;EAAE;EAAM;EAAM;EAAM;EAAO;EAAY,OAAO;CAAS,CAAC;CAE5E,OAAO;AACT;;;;;AAYA,SAAgB,yBAA+B;CAC7C;AACF;;;;;;;;;;AAWA,SAAgB,uBAAuB,MAA4B;CACjE,MAAM,SAAS,KAAK,IAAI,IAAI;CAC5B,IAAI,WAAW,QAAW;EACxB,KAAK,IAAI,MAAM;GAAE;GAAY,uBAAO,IAAI,IAAI;GAAG,OAAO;EAAE,CAAC;EACzD;CACF;CACA,OAAO;CACP,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,OACjC,IAAI,MAAM,SAAS,QAAW,OAAO,MAAM,OAAO,IAAI;AAE1D"}
1
+ {"version":3,"file":"nodeMemo.js","names":[],"sources":["../../../src/core/scene/nodeMemo.ts"],"sourcesContent":["/**\n * `nodeMemo` — memoize anything derived from a node, keyed on the node's\n * identity plus the `pose` / `data` references the scene swaps on every edit.\n *\n * The scene mutates node objects in place but *replaces* `pose` and `data`\n * with new references, so those two references are an exact freshness signal.\n * The assumption it rests on: nobody mutates `node.data` (or `node.pose`) in\n * place without replacing the reference. A consumer who violates it sees a\n * stale derivation until the next real edit.\n *\n * **What the key does not cover** is ambient state that `derive` reads — a\n * registry, a font set, a decode cache. Whoever owns the ambient state calls\n * {@link bumpNodeMemoGeneration}. Derivations that read ambient state with\n * *no* such signal (an async image decode landing, say) must not be memoized\n * here at all.\n */\n\n/** One memoized derivation: the references it was derived against, plus the\n * value. Presence in the slot map is what \"computed\" means — a `null` or\n * `undefined` value is a real answer and stays cached. */\ninterface MemoSlot {\n /** Reference-compared against the caller's `pose` argument. */\n pose: unknown;\n /** Reference-compared against `node.data` at call time. */\n data: unknown;\n /** Reference-compared against the caller's `also` argument. */\n also: unknown;\n value: unknown;\n}\n\ninterface NodeMemoRecord {\n /** `generation` at the time the slots were filled. */\n generation: number;\n slots: Map<string, MemoSlot>;\n /** How many times {@link dropPoseKeyedMemoSlots} has run for this node,\n * which is how a drop reaches the scopes it cannot enumerate. */\n drops: number;\n}\n\n/** A scoped entry also carries what a named slot's record holds for it. */\ninterface ScopedSlot extends MemoSlot {\n generation: number;\n drops: number;\n}\n\ndeclare const scopeBrand: unique symbol;\n\n/**\n * A private place to memoize one derivation, in place of a slot name.\n *\n * A named slot lives on the node, so it lasts as long as the node does. That\n * is wrong for a derivation that belongs to some shorter-lived reader — one\n * `toPose` function among many over the life of a scene. A scope is held by\n * that reader, and its entries go when the reader does.\n */\nexport type NodeMemoScope = { readonly [scopeBrand]: true };\n\nexport function createNodeMemoScope(): NodeMemoScope {\n return new WeakMap<object, ScopedSlot>() as unknown as NodeMemoScope;\n}\n\nconst MEMO = new WeakMap<object, NodeMemoRecord>();\n\n/** Bumped whenever ambient state a derivation reads has changed. Entries\n * stamped with an older value are dropped rather than served stale. */\nlet generation = 0;\n\n/** A node, as far as the memo is concerned: an object identity that may carry\n * a `data` reference. */\ntype MemoizableNode = { data?: unknown };\n\n/**\n * Return the value `derive` produces for `node`, recomputing only when the\n * node's `data` reference, the given `pose` reference, the `also` reference,\n * or the global generation has changed since the last call for this `slot`.\n *\n * `slot` names the derivation, so a node's several derived values do not\n * overwrite each other.\n *\n * `pose` is a **parameter rather than a read off the node**, because callers\n * legitimately derive against a pose that isn't `node.pose`: a preview or\n * ghost pose mid-drag. Pass `undefined` for derivations that don't depend on\n * the pose at all, leaving `data` as the only key.\n *\n * `also` is for an input that lives outside the node, such as the paint a\n * node inherits from its container. It must be a reference its owner keeps\n * while the input is unchanged.\n */\nexport function nodeMemo<T>(\n node: MemoizableNode,\n slot: string | NodeMemoScope,\n pose: unknown,\n derive: () => T,\n also?: unknown,\n): T {\n if (typeof slot !== 'string') return scopedNodeMemo(node, slot, pose, derive, also);\n let record = MEMO.get(node);\n if (record === undefined) {\n record = { generation, slots: new Map(), drops: 0 };\n MEMO.set(node, record);\n } else if (record.generation !== generation) {\n record.generation = generation;\n record.slots.clear();\n }\n\n const data = node.data;\n const hit = record.slots.get(slot);\n if (hit !== undefined && hit.pose === pose && hit.data === data && hit.also === also) {\n return hit.value as T;\n }\n\n const value = derive();\n // Reuse the slot object when there is one: this runs per node per frame, and\n // the entry churn is the only allocation the memo would otherwise add.\n if (hit !== undefined) {\n hit.pose = pose;\n hit.data = data;\n hit.also = also;\n hit.value = value;\n } else {\n record.slots.set(slot, { pose, data, also, value });\n }\n return value;\n}\n\nfunction scopedNodeMemo<T>(\n node: MemoizableNode,\n scope: NodeMemoScope,\n pose: unknown,\n derive: () => T,\n also: unknown,\n): T {\n const entries = scope as unknown as WeakMap<object, ScopedSlot>;\n const data = node.data;\n const drops = MEMO.get(node)?.drops ?? 0;\n const hit = entries.get(node);\n if (\n hit !== undefined && hit.generation === generation && hit.pose === pose && hit.data === data\n && hit.also === also && (pose === undefined || hit.drops === drops)\n ) {\n return hit.value as T;\n }\n const value = derive();\n // Read again: a derivation that hit a cycle drops this node's slots itself.\n const dropsNow = MEMO.get(node)?.drops ?? 0;\n if (hit !== undefined) {\n hit.pose = pose;\n hit.data = data;\n hit.also = also;\n hit.value = value;\n hit.generation = generation;\n hit.drops = dropsNow;\n } else {\n entries.set(node, { pose, data, also, value, generation, drops: dropsNow });\n }\n return value;\n}\n\n/** How many named slots `node` holds. For tests of what is kept on a node. */\nexport function nodeMemoSlotCount(node: MemoizableNode): number {\n const record = MEMO.get(node);\n return record === undefined || record.generation !== generation ? 0 : record.slots.size;\n}\n\n/**\n * Drop every memoized slot on every node. For owners of ambient state that\n * changes what a `derive` would return. Cheap — it bumps a counter.\n */\nexport function bumpNodeMemoGeneration(): void {\n generation++;\n}\n\n/** The generation {@link bumpNodeMemoGeneration} last moved to: a freshness\n * key for a cache outside this memo that reads the same ambient state. */\nexport function nodeMemoGeneration(): number {\n return generation;\n}\n\n/**\n * Drop the slots of `node` that were keyed on a pose, keeping the ones that\n * weren't.\n *\n * For per-frame pose overrides, which mutate one buffer in place rather than\n * replacing the reference, which the `(pose, data)` key cannot see.\n * {@link bumpNodeMemoGeneration} is the wrong tool there — it would drop every\n * static node's slots too, every frame.\n */\nexport function dropPoseKeyedMemoSlots(node: MemoizableNode): void {\n const record = MEMO.get(node);\n if (record === undefined) {\n MEMO.set(node, { generation, slots: new Map(), drops: 1 });\n return;\n }\n record.drops++;\n for (const [slot, entry] of record.slots) {\n if (entry.pose !== undefined) record.slots.delete(slot);\n }\n}\n"],"mappings":";AAyDA,SAAgB,sBAAqC;CACnD,uBAAO,IAAI,QAA4B;AACzC;AAEA,IAAM,uBAAO,IAAI,QAAgC;;;AAIjD,IAAI,aAAa;;;;;;;;;;;;;;;;;;AAuBjB,SAAgB,SACd,MACA,MACA,MACA,QACA,MACG;CACH,IAAI,OAAO,SAAS,UAAU,OAAO,eAAe,MAAM,MAAM,MAAM,QAAQ,IAAI;CAClF,IAAI,SAAS,KAAK,IAAI,IAAI;CAC1B,IAAI,WAAW,QAAW;EACxB,SAAS;GAAE;GAAY,uBAAO,IAAI,IAAI;GAAG,OAAO;EAAE;EAClD,KAAK,IAAI,MAAM,MAAM;CACvB,OAAO,IAAI,OAAO,eAAe,YAAY;EAC3C,OAAO,aAAa;EACpB,OAAO,MAAM,MAAM;CACrB;CAEA,MAAM,OAAO,KAAK;CAClB,MAAM,MAAM,OAAO,MAAM,IAAI,IAAI;CACjC,IAAI,QAAQ,UAAa,IAAI,SAAS,QAAQ,IAAI,SAAS,QAAQ,IAAI,SAAS,MAC9E,OAAO,IAAI;CAGb,MAAM,QAAQ,OAAO;CAGrB,IAAI,QAAQ,QAAW;EACrB,IAAI,OAAO;EACX,IAAI,OAAO;EACX,IAAI,OAAO;EACX,IAAI,QAAQ;CACd,OACE,OAAO,MAAM,IAAI,MAAM;EAAE;EAAM;EAAM;EAAM;CAAM,CAAC;CAEpD,OAAO;AACT;AAEA,SAAS,eACP,MACA,OACA,MACA,QACA,MACG;CACH,MAAM,UAAU;CAChB,MAAM,OAAO,KAAK;CAClB,MAAM,QAAQ,KAAK,IAAI,IAAI,CAAC,EAAE,SAAS;CACvC,MAAM,MAAM,QAAQ,IAAI,IAAI;CAC5B,IACE,QAAQ,UAAa,IAAI,eAAe,cAAc,IAAI,SAAS,QAAQ,IAAI,SAAS,QACrF,IAAI,SAAS,SAAY,SAAS,UAAa,IAAI,UAAU,QAEhE,OAAO,IAAI;CAEb,MAAM,QAAQ,OAAO;CAErB,MAAM,WAAW,KAAK,IAAI,IAAI,CAAC,EAAE,SAAS;CAC1C,IAAI,QAAQ,QAAW;EACrB,IAAI,OAAO;EACX,IAAI,OAAO;EACX,IAAI,OAAO;EACX,IAAI,QAAQ;EACZ,IAAI,aAAa;EACjB,IAAI,QAAQ;CACd,OACE,QAAQ,IAAI,MAAM;EAAE;EAAM;EAAM;EAAM;EAAO;EAAY,OAAO;CAAS,CAAC;CAE5E,OAAO;AACT;;;;;AAYA,SAAgB,yBAA+B;CAC7C;AACF;;;AAIA,SAAgB,qBAA6B;CAC3C,OAAO;AACT;;;;;;;;;;AAWA,SAAgB,uBAAuB,MAA4B;CACjE,MAAM,SAAS,KAAK,IAAI,IAAI;CAC5B,IAAI,WAAW,QAAW;EACxB,KAAK,IAAI,MAAM;GAAE;GAAY,uBAAO,IAAI,IAAI;GAAG,OAAO;EAAE,CAAC;EACzD;CACF;CACA,OAAO;CACP,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,OACjC,IAAI,MAAM,SAAS,QAAW,OAAO,MAAM,OAAO,IAAI;AAE1D"}
@@ -85,8 +85,7 @@ function createNodeStore(state, invalidateOrder, fnFields) {
85
85
  function siblingsOf(parent) {
86
86
  if (parent === null) return state.roots;
87
87
  const p = requireNode(parent);
88
- if (p.kind !== "container") throw new Error(`Scene: parent "${parent}" is not a container`);
89
- return p.children;
88
+ return p.children ??= [];
90
89
  }
91
90
  function detach(id) {
92
91
  const node = requireNode(id);
@@ -110,7 +109,7 @@ function createNodeStore(state, invalidateOrder, fnFields) {
110
109
  }
111
110
  function descendants(id, out) {
112
111
  const n = state.nodes.get(id);
113
- if (!n || n.kind !== "container") return;
112
+ if (!n?.children) return;
114
113
  for (const c of n.children) {
115
114
  out.push(c);
116
115
  descendants(c, out);
@@ -164,7 +163,6 @@ function createNodeStore(state, invalidateOrder, fnFields) {
164
163
  for (const field of fnFields) {
165
164
  const fn = spec[field.spec.field];
166
165
  if (fn === void 0) continue;
167
- if (field.spec.containersOnly && spec.kind !== "container") continue;
168
166
  field.write(node, fn);
169
167
  attached.set(field, fn);
170
168
  }
@@ -1 +1 @@
1
- {"version":3,"file":"nodeStore.js","names":[],"sources":["../../../src/core/scene/nodeStore.ts"],"sourcesContent":["/**\n * The scene's node store: the tree links, the reverse index of who derives\n * from whom, and the function fields a node carries beside its data.\n *\n * Everything here reads and writes `state.nodes` and `state.roots` directly;\n * none of it records. The kit ops call it from both of their directions.\n */\nimport { dwarn } from 'debug/flag';\nimport { createDependentsIndex } from './dependents';\nimport { dropPoseKeyedMemoSlots } from './nodeMemo';\nimport { restoreFn, type FnBearingNode, type NodeFn, type NodeFnField } from './nodeFnFields';\nimport { SceneMembershipError } from './staleReplay';\nimport type { AddNodeSpec, Node, NodeId } from './types';\n\n/** What the store keeps of a scene's state. */\nexport interface NodeStoreState<TData, TLayer extends string, TPose> {\n nodes: Map<NodeId, Node<TData, TLayer, TPose>>;\n roots: NodeId[];\n}\n\nexport function createNodeStore<TData, TLayer extends string, TPose>(\n state: NodeStoreState<TData, TLayer, TPose>,\n /** Called on every edit to the tree's links. */\n invalidateOrder: () => void,\n fnFields: readonly NodeFnField[],\n) {\n /** Maintained from `kit:add` / `kit:remove` — the only two places a node is\n * created or destroyed, and unlike `add`/`remove` they replay on undo/redo.\n * Only explicit `dependsOn` id lists are registered here; `'children'` is\n * the ancestor walk in `invalidateDependents` instead. */\n const dependents = createDependentsIndex();\n\n /** Nodes carrying `dependsOn: 'children'`. Empty in every scene that has no\n * container hugging its contents, which is what keeps the ancestor walk off\n * the hot path. */\n const childDerived = new Set<NodeId>();\n\n /** Walks `id`'s whole subtree: under a composing pose strategy a container's\n * move carries every descendant, and whatever derives from one. */\n function invalidateDependents(id: NodeId): void {\n invalidateChildDerivedFrom(state.nodes.get(id)?.parent ?? null);\n if (dependents.isEmpty()) return;\n const subtree: NodeId[] = [id];\n descendants(id, subtree);\n for (const src of subtree) {\n for (const dep of dependents.transitiveDependentsOf(src)) {\n const node = state.nodes.get(dep);\n if (node !== undefined) dropPoseKeyedMemoSlots(node);\n }\n }\n }\n\n /** A `dependsOn: 'children'` container derives from nodes the reverse index\n * never sees, so its invalidation runs the other way: up the parent chain\n * from `start` inclusive, and on to each such ancestor's own dependents.\n *\n * Callers that removed or reparented a node pass the parent explicitly —\n * a gone node cannot say who its parent was. */\n function invalidateChildDerivedFrom(start: NodeId | null): void {\n if (childDerived.size === 0) return;\n let cursor = start;\n while (cursor !== null) {\n const node = state.nodes.get(cursor);\n if (node === undefined) return;\n if (childDerived.has(cursor)) {\n dropPoseKeyedMemoSlots(node);\n for (const dep of dependents.transitiveDependentsOf(cursor)) {\n const d = state.nodes.get(dep);\n if (d !== undefined) dropPoseKeyedMemoSlots(d);\n }\n }\n cursor = node.parent;\n }\n }\n\n /** Track (or forget) a node whose `dependsOn` is `'children'`. Paired with\n * `dependents.add` / `.remove` at every site a node is created or destroyed. */\n function trackChildDerived(id: NodeId, dependsOn: Node<TData, TLayer, TPose>['dependsOn']): void {\n if (dependsOn === 'children') childDerived.add(id);\n else childDerived.delete(id);\n }\n\n /** Point `id` at a different set of dependencies, keeping both indices and\n * every memo that reads them in step. Shared by `kit:setDependsOn`'s two\n * directions, which differ only in which value they install.\n *\n * `id`'s own memo is dropped as well as its dependents': its derivation now\n * reads different sources, and nothing else would clear it. */\n function retarget(id: NodeId, dependsOn: Node<TData, TLayer, TPose>['dependsOn']): void {\n const node = requireNode(id) as { dependsOn?: readonly NodeId[] | 'children' };\n if (dependsOn === undefined) delete node.dependsOn;\n else node.dependsOn = dependsOn;\n dependents.add(id, dependsOn === undefined || dependsOn === 'children' ? [] : dependsOn);\n trackChildDerived(id, dependsOn);\n dropPoseKeyedMemoSlots(requireNode(id));\n invalidateDependents(id);\n }\n\n function requireNode(id: NodeId): Node<TData, TLayer, TPose> {\n const n = state.nodes.get(id);\n if (!n) throw new SceneMembershipError(`Scene: unknown node id \"${id}\"`);\n return n;\n }\n\n function siblingsOf(parent: NodeId | null): NodeId[] {\n if (parent === null) return state.roots;\n const p = requireNode(parent);\n if (p.kind !== 'container') {\n throw new Error(`Scene: parent \"${parent}\" is not a container`);\n }\n return p.children;\n }\n\n function detach(id: NodeId): { parent: NodeId | null; index: number } {\n const node = requireNode(id);\n const sibs = siblingsOf(node.parent);\n const idx = sibs.indexOf(id);\n if (idx < 0) throw new Error(`Scene: node \"${id}\" not found in its parent's children`);\n sibs.splice(idx, 1);\n invalidateOrder();\n return { parent: node.parent, index: idx };\n }\n\n function attach(id: NodeId, parent: NodeId | null, index: number): void {\n const sibs = siblingsOf(parent);\n const i = Math.max(0, Math.min(index, sibs.length));\n sibs.splice(i, 0, id);\n const node = requireNode(id);\n (node as { parent: NodeId | null }).parent = parent;\n invalidateOrder();\n }\n\n function descendants(id: NodeId, out: NodeId[]): void {\n const n = state.nodes.get(id);\n if (!n || n.kind !== 'container') return;\n for (const c of n.children) {\n out.push(c);\n descendants(c, out);\n }\n }\n\n /** Everything removing `rootIds` takes with it: each root's subtree, then\n * every node deriving from any of those, then those nodes' subtrees in turn.\n * A dependent is not a descendant, so the second pass is a separate\n * reachability walk; `ids` grows as it runs, which is what carries the\n * cascade transitively and upward through a dependent's own ancestors.\n *\n * Published as {@link Scene.removalClosure}, which is what the delete op and\n * `buildDeleteOps` ask instead of rebuilding the walk from `dependsOn` and\n * `children` — a third cascade relation is then added here alone. */\n function removalClosure(rootIds: readonly NodeId[]): {\n ids: NodeId[];\n removing: Set<NodeId>;\n } {\n const ids: NodeId[] = [];\n const removing = new Set<NodeId>();\n function take(id: NodeId): void {\n if (removing.has(id)) return;\n removing.add(id);\n ids.push(id);\n const sub: NodeId[] = [];\n descendants(id, sub);\n for (const s of sub) {\n if (removing.has(s)) continue;\n removing.add(s);\n ids.push(s);\n }\n }\n for (const root of rootIds) take(root);\n for (let i = 0; i < ids.length; i++) {\n for (const dep of dependents.transitiveDependentsOf(ids[i])) take(dep);\n }\n return { ids, removing };\n }\n\n /** The functions a live `scene.add` attached, by the `kit:add` payload it\n * recorded: redo replays that payload without the spec. Keyed on the\n * payload rather than the node id because an id is reusable, and each\n * add of it owns its own functions. */\n const addedFns = new WeakMap<object, Map<NodeFnField, NodeFn>>();\n\n /** Post-patch the registry-keyed function fields onto a node after its\n * `kit:add` op runs. A function cannot travel through the serializable op\n * payload, so it is attached to the live node here, and kept against\n * `payload` for redo when there is one. `dependsOn` is plain data and\n * rides the payload instead. */\n function patchNodeFunctions(\n spec: AddNodeSpec<TData, TLayer, TPose>,\n id: NodeId,\n payload: object | null,\n ): void {\n const node = state.nodes.get(id) as FnBearingNode | undefined;\n if (node === undefined) return;\n const attached = new Map<NodeFnField, NodeFn>();\n for (const field of fnFields) {\n const fn = (spec as unknown as Record<string, unknown>)[field.spec.field] as NodeFn | undefined;\n if (fn === undefined) continue;\n if (field.spec.containersOnly && spec.kind !== 'container') continue;\n field.write(node, fn);\n attached.set(field, fn);\n }\n if (payload && attached.size > 0) addedFns.set(payload, attached);\n }\n\n /** Re-attach those fields to a node restored from a `kit:remove` snapshot.\n * In-session the clone already carries them; a persisted history does not,\n * so the registry keys are what is left. */\n function restoreNodeFunctions(\n node: Node<TData, TLayer, TPose>,\n keys: Record<string, string | undefined> | undefined,\n ): void {\n if (!keys) return;\n const target = node as unknown as FnBearingNode;\n for (const field of fnFields) {\n if (field.read(target) !== undefined) continue;\n const key = keys[field.spec.opKey];\n if (key === undefined) continue;\n restoreFn(field, target, key, 'kit:remove', dwarn);\n }\n }\n\n return {\n dependents, childDerived, invalidateDependents, invalidateChildDerivedFrom, trackChildDerived, retarget,\n requireNode, siblingsOf, detach, attach, descendants, removalClosure, addedFns, patchNodeFunctions,\n restoreNodeFunctions,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;AAoBA,SAAgB,gBACd,OAEA,iBACA,UACA;;;;;CAKA,MAAM,aAAa,sBAAsB;;;;CAKzC,MAAM,+BAAe,IAAI,IAAY;;;CAIrC,SAAS,qBAAqB,IAAkB;EAC9C,2BAA2B,MAAM,MAAM,IAAI,EAAE,CAAC,EAAE,UAAU,IAAI;EAC9D,IAAI,WAAW,QAAQ,GAAG;EAC1B,MAAM,UAAoB,CAAC,EAAE;EAC7B,YAAY,IAAI,OAAO;EACvB,KAAK,MAAM,OAAO,SAChB,KAAK,MAAM,OAAO,WAAW,uBAAuB,GAAG,GAAG;GACxD,MAAM,OAAO,MAAM,MAAM,IAAI,GAAG;GAChC,IAAI,SAAS,QAAW,uBAAuB,IAAI;EACrD;CAEJ;;;;;;;CAQA,SAAS,2BAA2B,OAA4B;EAC9D,IAAI,aAAa,SAAS,GAAG;EAC7B,IAAI,SAAS;EACb,OAAO,WAAW,MAAM;GACtB,MAAM,OAAO,MAAM,MAAM,IAAI,MAAM;GACnC,IAAI,SAAS,QAAW;GACxB,IAAI,aAAa,IAAI,MAAM,GAAG;IAC5B,uBAAuB,IAAI;IAC3B,KAAK,MAAM,OAAO,WAAW,uBAAuB,MAAM,GAAG;KAC3D,MAAM,IAAI,MAAM,MAAM,IAAI,GAAG;KAC7B,IAAI,MAAM,QAAW,uBAAuB,CAAC;IAC/C;GACF;GACA,SAAS,KAAK;EAChB;CACF;;;CAIA,SAAS,kBAAkB,IAAY,WAA0D;EAC/F,IAAI,cAAc,YAAY,aAAa,IAAI,EAAE;OAC5C,aAAa,OAAO,EAAE;CAC7B;;;;;;;CAQA,SAAS,SAAS,IAAY,WAA0D;EACtF,MAAM,OAAO,YAAY,EAAE;EAC3B,IAAI,cAAc,QAAW,OAAO,KAAK;OACpC,KAAK,YAAY;EACtB,WAAW,IAAI,IAAI,cAAc,UAAa,cAAc,aAAa,CAAC,IAAI,SAAS;EACvF,kBAAkB,IAAI,SAAS;EAC/B,uBAAuB,YAAY,EAAE,CAAC;EACtC,qBAAqB,EAAE;CACzB;CAEA,SAAS,YAAY,IAAwC;EAC3D,MAAM,IAAI,MAAM,MAAM,IAAI,EAAE;EAC5B,IAAI,CAAC,GAAG,MAAM,IAAI,qBAAqB,2BAA2B,GAAG,EAAE;EACvE,OAAO;CACT;CAEA,SAAS,WAAW,QAAiC;EACnD,IAAI,WAAW,MAAM,OAAO,MAAM;EAClC,MAAM,IAAI,YAAY,MAAM;EAC5B,IAAI,EAAE,SAAS,aACb,MAAM,IAAI,MAAM,kBAAkB,OAAO,qBAAqB;EAEhE,OAAO,EAAE;CACX;CAEA,SAAS,OAAO,IAAsD;EACpE,MAAM,OAAO,YAAY,EAAE;EAC3B,MAAM,OAAO,WAAW,KAAK,MAAM;EACnC,MAAM,MAAM,KAAK,QAAQ,EAAE;EAC3B,IAAI,MAAM,GAAG,MAAM,IAAI,MAAM,gBAAgB,GAAG,qCAAqC;EACrF,KAAK,OAAO,KAAK,CAAC;EAClB,gBAAgB;EAChB,OAAO;GAAE,QAAQ,KAAK;GAAQ,OAAO;EAAI;CAC3C;CAEA,SAAS,OAAO,IAAY,QAAuB,OAAqB;EACtE,MAAM,OAAO,WAAW,MAAM;EAC9B,MAAM,IAAI,KAAK,IAAI,GAAG,KAAK,IAAI,OAAO,KAAK,MAAM,CAAC;EAClD,KAAK,OAAO,GAAG,GAAG,EAAE;EACpB,MAAM,OAAO,YAAY,EAAE;EAC3B,KAAoC,SAAS;EAC7C,gBAAgB;CAClB;CAEA,SAAS,YAAY,IAAY,KAAqB;EACpD,MAAM,IAAI,MAAM,MAAM,IAAI,EAAE;EAC5B,IAAI,CAAC,KAAK,EAAE,SAAS,aAAa;EAClC,KAAK,MAAM,KAAK,EAAE,UAAU;GAC1B,IAAI,KAAK,CAAC;GACV,YAAY,GAAG,GAAG;EACpB;CACF;;;;;;;;;;CAWA,SAAS,eAAe,SAGtB;EACA,MAAM,MAAgB,CAAC;EACvB,MAAM,2BAAW,IAAI,IAAY;EACjC,SAAS,KAAK,IAAkB;GAC9B,IAAI,SAAS,IAAI,EAAE,GAAG;GACtB,SAAS,IAAI,EAAE;GACf,IAAI,KAAK,EAAE;GACX,MAAM,MAAgB,CAAC;GACvB,YAAY,IAAI,GAAG;GACnB,KAAK,MAAM,KAAK,KAAK;IACnB,IAAI,SAAS,IAAI,CAAC,GAAG;IACrB,SAAS,IAAI,CAAC;IACd,IAAI,KAAK,CAAC;GACZ;EACF;EACA,KAAK,MAAM,QAAQ,SAAS,KAAK,IAAI;EACrC,KAAK,IAAI,IAAI,GAAG,IAAI,IAAI,QAAQ,KAC9B,KAAK,MAAM,OAAO,WAAW,uBAAuB,IAAI,EAAE,GAAG,KAAK,GAAG;EAEvE,OAAO;GAAE;GAAK;EAAS;CACzB;;;;;CAMA,MAAM,2BAAW,IAAI,QAA0C;;;;;;CAO/D,SAAS,mBACP,MACA,IACA,SACM;EACN,MAAM,OAAO,MAAM,MAAM,IAAI,EAAE;EAC/B,IAAI,SAAS,QAAW;EACxB,MAAM,2BAAW,IAAI,IAAyB;EAC9C,KAAK,MAAM,SAAS,UAAU;GAC5B,MAAM,KAAM,KAA4C,MAAM,KAAK;GACnE,IAAI,OAAO,QAAW;GACtB,IAAI,MAAM,KAAK,kBAAkB,KAAK,SAAS,aAAa;GAC5D,MAAM,MAAM,MAAM,EAAE;GACpB,SAAS,IAAI,OAAO,EAAE;EACxB;EACA,IAAI,WAAW,SAAS,OAAO,GAAG,SAAS,IAAI,SAAS,QAAQ;CAClE;;;;CAKA,SAAS,qBACP,MACA,MACM;EACN,IAAI,CAAC,MAAM;EACX,MAAM,SAAS;EACf,KAAK,MAAM,SAAS,UAAU;GAC5B,IAAI,MAAM,KAAK,MAAM,MAAM,QAAW;GACtC,MAAM,MAAM,KAAK,MAAM,KAAK;GAC5B,IAAI,QAAQ,QAAW;GACvB,UAAU,OAAO,QAAQ,KAAK,cAAc,KAAK;EACnD;CACF;CAEA,OAAO;EACL;EAAY;EAAc;EAAsB;EAA4B;EAAmB;EAC/F;EAAa;EAAY;EAAQ;EAAQ;EAAa;EAAgB;EAAU;EAChF;CACF;AACF"}
1
+ {"version":3,"file":"nodeStore.js","names":[],"sources":["../../../src/core/scene/nodeStore.ts"],"sourcesContent":["/**\n * The scene's node store: the tree links, the reverse index of who derives\n * from whom, and the function fields a node carries beside its data.\n *\n * Everything here reads and writes `state.nodes` and `state.roots` directly;\n * none of it records. The kit ops call it from both of their directions.\n */\nimport { dwarn } from 'debug/flag';\nimport { createDependentsIndex } from './dependents';\nimport { dropPoseKeyedMemoSlots } from './nodeMemo';\nimport { restoreFn, type FnBearingNode, type NodeFn, type NodeFnField } from './nodeFnFields';\nimport { SceneMembershipError } from './staleReplay';\nimport type { AddNodeSpec, Node, NodeId } from './types';\n\n/** What the store keeps of a scene's state. */\nexport interface NodeStoreState<TData, TLayer extends string, TPose> {\n nodes: Map<NodeId, Node<TData, TLayer, TPose>>;\n roots: NodeId[];\n}\n\nexport function createNodeStore<TData, TLayer extends string, TPose>(\n state: NodeStoreState<TData, TLayer, TPose>,\n /** Called on every edit to the tree's links. */\n invalidateOrder: () => void,\n fnFields: readonly NodeFnField[],\n) {\n /** Maintained from `kit:add` / `kit:remove` — the only two places a node is\n * created or destroyed, and unlike `add`/`remove` they replay on undo/redo.\n * Only explicit `dependsOn` id lists are registered here; `'children'` is\n * the ancestor walk in `invalidateDependents` instead. */\n const dependents = createDependentsIndex();\n\n /** Nodes carrying `dependsOn: 'children'`. Empty in every scene that has no\n * container hugging its contents, which is what keeps the ancestor walk off\n * the hot path. */\n const childDerived = new Set<NodeId>();\n\n /** Walks `id`'s whole subtree: under a composing pose strategy a container's\n * move carries every descendant, and whatever derives from one. */\n function invalidateDependents(id: NodeId): void {\n invalidateChildDerivedFrom(state.nodes.get(id)?.parent ?? null);\n if (dependents.isEmpty()) return;\n const subtree: NodeId[] = [id];\n descendants(id, subtree);\n for (const src of subtree) {\n for (const dep of dependents.transitiveDependentsOf(src)) {\n const node = state.nodes.get(dep);\n if (node !== undefined) dropPoseKeyedMemoSlots(node);\n }\n }\n }\n\n /** A `dependsOn: 'children'` container derives from nodes the reverse index\n * never sees, so its invalidation runs the other way: up the parent chain\n * from `start` inclusive, and on to each such ancestor's own dependents.\n *\n * Callers that removed or reparented a node pass the parent explicitly —\n * a gone node cannot say who its parent was. */\n function invalidateChildDerivedFrom(start: NodeId | null): void {\n if (childDerived.size === 0) return;\n let cursor = start;\n while (cursor !== null) {\n const node = state.nodes.get(cursor);\n if (node === undefined) return;\n if (childDerived.has(cursor)) {\n dropPoseKeyedMemoSlots(node);\n for (const dep of dependents.transitiveDependentsOf(cursor)) {\n const d = state.nodes.get(dep);\n if (d !== undefined) dropPoseKeyedMemoSlots(d);\n }\n }\n cursor = node.parent;\n }\n }\n\n /** Track (or forget) a node whose `dependsOn` is `'children'`. Paired with\n * `dependents.add` / `.remove` at every site a node is created or destroyed. */\n function trackChildDerived(id: NodeId, dependsOn: Node<TData, TLayer, TPose>['dependsOn']): void {\n if (dependsOn === 'children') childDerived.add(id);\n else childDerived.delete(id);\n }\n\n /** Point `id` at a different set of dependencies, keeping both indices and\n * every memo that reads them in step. Shared by `kit:setDependsOn`'s two\n * directions, which differ only in which value they install.\n *\n * `id`'s own memo is dropped as well as its dependents': its derivation now\n * reads different sources, and nothing else would clear it. */\n function retarget(id: NodeId, dependsOn: Node<TData, TLayer, TPose>['dependsOn']): void {\n const node = requireNode(id) as { dependsOn?: readonly NodeId[] | 'children' };\n if (dependsOn === undefined) delete node.dependsOn;\n else node.dependsOn = dependsOn;\n dependents.add(id, dependsOn === undefined || dependsOn === 'children' ? [] : dependsOn);\n trackChildDerived(id, dependsOn);\n dropPoseKeyedMemoSlots(requireNode(id));\n invalidateDependents(id);\n }\n\n function requireNode(id: NodeId): Node<TData, TLayer, TPose> {\n const n = state.nodes.get(id);\n if (!n) throw new SceneMembershipError(`Scene: unknown node id \"${id}\"`);\n return n;\n }\n\n function siblingsOf(parent: NodeId | null): NodeId[] {\n if (parent === null) return state.roots;\n const p = requireNode(parent);\n return (p.children ??= []);\n }\n\n function detach(id: NodeId): { parent: NodeId | null; index: number } {\n const node = requireNode(id);\n const sibs = siblingsOf(node.parent);\n const idx = sibs.indexOf(id);\n if (idx < 0) throw new Error(`Scene: node \"${id}\" not found in its parent's children`);\n sibs.splice(idx, 1);\n invalidateOrder();\n return { parent: node.parent, index: idx };\n }\n\n function attach(id: NodeId, parent: NodeId | null, index: number): void {\n const sibs = siblingsOf(parent);\n const i = Math.max(0, Math.min(index, sibs.length));\n sibs.splice(i, 0, id);\n const node = requireNode(id);\n (node as { parent: NodeId | null }).parent = parent;\n invalidateOrder();\n }\n\n function descendants(id: NodeId, out: NodeId[]): void {\n const n = state.nodes.get(id);\n if (!n?.children) return;\n for (const c of n.children) {\n out.push(c);\n descendants(c, out);\n }\n }\n\n /** Everything removing `rootIds` takes with it: each root's subtree, then\n * every node deriving from any of those, then those nodes' subtrees in turn.\n * A dependent is not a descendant, so the second pass is a separate\n * reachability walk; `ids` grows as it runs, which is what carries the\n * cascade transitively and upward through a dependent's own ancestors.\n *\n * Published as {@link Scene.removalClosure}, which is what the delete op and\n * `buildDeleteOps` ask instead of rebuilding the walk from `dependsOn` and\n * `children` — a third cascade relation is then added here alone. */\n function removalClosure(rootIds: readonly NodeId[]): {\n ids: NodeId[];\n removing: Set<NodeId>;\n } {\n const ids: NodeId[] = [];\n const removing = new Set<NodeId>();\n function take(id: NodeId): void {\n if (removing.has(id)) return;\n removing.add(id);\n ids.push(id);\n const sub: NodeId[] = [];\n descendants(id, sub);\n for (const s of sub) {\n if (removing.has(s)) continue;\n removing.add(s);\n ids.push(s);\n }\n }\n for (const root of rootIds) take(root);\n for (let i = 0; i < ids.length; i++) {\n for (const dep of dependents.transitiveDependentsOf(ids[i])) take(dep);\n }\n return { ids, removing };\n }\n\n /** The functions a live `scene.add` attached, by the `kit:add` payload it\n * recorded: redo replays that payload without the spec. Keyed on the\n * payload rather than the node id because an id is reusable, and each\n * add of it owns its own functions. */\n const addedFns = new WeakMap<object, Map<NodeFnField, NodeFn>>();\n\n /** Post-patch the registry-keyed function fields onto a node after its\n * `kit:add` op runs. A function cannot travel through the serializable op\n * payload, so it is attached to the live node here, and kept against\n * `payload` for redo when there is one. `dependsOn` is plain data and\n * rides the payload instead. */\n function patchNodeFunctions(\n spec: AddNodeSpec<TData, TLayer, TPose>,\n id: NodeId,\n payload: object | null,\n ): void {\n const node = state.nodes.get(id) as FnBearingNode | undefined;\n if (node === undefined) return;\n const attached = new Map<NodeFnField, NodeFn>();\n for (const field of fnFields) {\n const fn = (spec as unknown as Record<string, unknown>)[field.spec.field] as NodeFn | undefined;\n if (fn === undefined) continue;\n field.write(node, fn);\n attached.set(field, fn);\n }\n if (payload && attached.size > 0) addedFns.set(payload, attached);\n }\n\n /** Re-attach those fields to a node restored from a `kit:remove` snapshot.\n * In-session the clone already carries them; a persisted history does not,\n * so the registry keys are what is left. */\n function restoreNodeFunctions(\n node: Node<TData, TLayer, TPose>,\n keys: Record<string, string | undefined> | undefined,\n ): void {\n if (!keys) return;\n const target = node as unknown as FnBearingNode;\n for (const field of fnFields) {\n if (field.read(target) !== undefined) continue;\n const key = keys[field.spec.opKey];\n if (key === undefined) continue;\n restoreFn(field, target, key, 'kit:remove', dwarn);\n }\n }\n\n return {\n dependents, childDerived, invalidateDependents, invalidateChildDerivedFrom, trackChildDerived, retarget,\n requireNode, siblingsOf, detach, attach, descendants, removalClosure, addedFns, patchNodeFunctions,\n restoreNodeFunctions,\n };\n}\n"],"mappings":"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"}
@@ -42,6 +42,7 @@ function createPathEffectDerivations(read) {
42
42
  depData: deps.map((d) => d?.node.data),
43
43
  poses: deps.map((d) => d === void 0 ? void 0 : snapshotPose(d.pose)),
44
44
  paths: deps.map((d) => d?.path ?? null),
45
+ inherited: deps.map((d) => d?.inherited),
45
46
  path: parts === null ? null : pathFromParts(parts),
46
47
  faces: fonts ? generation : null
47
48
  };
@@ -71,7 +72,7 @@ function current(held, node, deps) {
71
72
  if (held.nodes[i] !== d?.node) return false;
72
73
  if (d === void 0) continue;
73
74
  if (held.depData[i] !== d.node.data || !samePoseValue(held.poses[i], d.pose)) return false;
74
- if (held.paths[i] !== d.path) return false;
75
+ if (held.paths[i] !== d.path || held.inherited[i] !== d.inherited) return false;
75
76
  }
76
77
  if (held.faces !== null) noteFontRead();
77
78
  return true;
@@ -87,7 +88,7 @@ function pathEffectParts(node, deps) {
87
88
  * `derivations` — the ones the scene registers under {@link PATH_EFFECT}. */
88
89
  function pathEffectNode(effect, sources, layer, derivations) {
89
90
  return {
90
- kind: "leaf",
91
+ kind: "mark",
91
92
  layer,
92
93
  pose: {
93
94
  x: 0,
@@ -1 +1 @@
1
- {"version":3,"file":"derive.js","names":[],"sources":["../../../../src/core/scene/pathEffects/derive.ts"],"sourcesContent":["/**\n * A path effect as a scene node: a leaf whose `dependsOn` names its sources,\n * whose `data.pathEffect` says what to make of them, and whose path and pose\n * are both derived — the path to paint and pick, the pose as that path's\n * bounds, so selection chrome and marquee see where it is.\n *\n * Reading a source's outline and paint is its painter's business, which sits\n * above the scene, so the derivations are made from a reader:\n * `createPathEffectDerivations(read)`. The canvas layer makes the kit's, and\n * a scene that holds path effects registers them under {@link PATH_EFFECT}.\n */\nimport { boundsOfPath, type Path } from '@weasel-js/geom';\nimport { derivedPartsOf, pathFromParts, type DerivedPart } from '../derivedParts';\nimport { fontGeneration, noteFontRead, withFontReads } from '../fontReads';\nimport { samePoseValue, snapshotPose } from '../poseSnapshot';\nimport type { DerivedDep, NodeId } from '../types';\nimport { effectParts, type PathEffect } from './effects';\nimport type { EffectSourceReader } from './source';\n\n/** The registry key both derivations go by, for `derivePath` and `derivePose`. */\nexport const PATH_EFFECT = 'kit:pathEffect';\n\n/** A node's path effect, or `null` when its data holds none. */\nexport function pathEffectOf(node: { data?: unknown } | undefined): PathEffect | null {\n const effect = (node?.data as { pathEffect?: PathEffect } | null | undefined)?.pathEffect;\n return effect && typeof effect === 'object' && typeof effect.kind === 'string' ? effect : null;\n}\n\n/** A path-effect node's two derivations. */\nexport interface PathEffectDerivations {\n /** What it paints and picks by. */\n derivePath(node: { data?: unknown }, deps: readonly (DerivedDep<unknown> | undefined)[]): Path | null;\n /** The bounds of what it paints, or `null` — its authored pose — while it\n * paints nothing. */\n derivePose(node: { data?: unknown }, deps: readonly (DerivedDep<unknown> | undefined)[]): { x: number; y: number; width: number; height: number } | null;\n}\n\ninterface Derived {\n data: unknown;\n nodes: unknown[];\n depData: unknown[];\n poses: unknown[];\n paths: (Path | null)[];\n path: Path | null;\n /** The face generation it was worked out under, or `null` when it read no\n * glyph outlines. */\n faces: number | null;\n}\n\n/** The derivations of a path effect whose sources `read` reads. */\nexport function createPathEffectDerivations(read: EffectSourceReader): PathEffectDerivations {\n /** One result per effect node, shared by its two derivations: the pose is\n * the path's bounds, and working the effect out twice a frame would double\n * its cost. Checked against everything it read, by value for poses — a\n * drag's override mutates its pose in place. */\n const results = new WeakMap<object, Derived>();\n const derive = (node: { data?: unknown }, deps: readonly (DerivedDep<unknown> | undefined)[]): Derived => {\n const held = results.get(node);\n if (held !== undefined && current(held, node, deps)) return held;\n const effect = pathEffectOf(node);\n const generation = fontGeneration();\n const { value: parts, fonts } = withFontReads(() => (effect === null\n ? null\n : effectParts(effect, deps.map((d) => (d === undefined ? null : read(d))))));\n const result: Derived = {\n data: node.data,\n nodes: deps.map((d) => d?.node),\n depData: deps.map((d) => d?.node.data),\n poses: deps.map((d) => (d === undefined ? undefined : snapshotPose(d.pose))),\n paths: deps.map((d) => d?.path ?? null),\n path: parts === null ? null : pathFromParts(parts),\n faces: fonts ? generation : null,\n };\n results.set(node, result);\n return result;\n };\n return {\n derivePath: (node, deps) => derive(node, deps).path,\n derivePose: (node, deps) => {\n const path = derive(node, deps).path;\n if (path === null) return null;\n const { x, y, width, height } = boundsOfPath(path);\n return { x, y, width, height };\n },\n };\n}\n\nfunction current(held: Derived, node: { data?: unknown }, deps: readonly (DerivedDep<unknown> | undefined)[]): boolean {\n if (held.data !== node.data || held.nodes.length !== deps.length) return false;\n if (held.faces !== null && held.faces !== fontGeneration()) return false;\n for (let i = 0; i < deps.length; i++) {\n const d = deps[i];\n if (held.nodes[i] !== d?.node) return false;\n if (d === undefined) continue;\n if (held.depData[i] !== d.node.data || !samePoseValue(held.poses[i], d.pose)) return false;\n if (held.paths[i] !== d.path) return false;\n }\n // A hit is still a read, for the scene memo enclosing this one.\n if (held.faces !== null) noteFontRead();\n return true;\n}\n\n/** The parts a path-effect node paints, as its sources stand, or `null` when\n * it has no derivation or nothing to paint. */\nexport function pathEffectParts(\n node: { data?: unknown; derivePath?: unknown },\n deps: readonly (DerivedDep<unknown> | undefined)[],\n): readonly DerivedPart[] | null {\n if (typeof node.derivePath !== 'function') return null;\n return derivedPartsOf((node.derivePath as PathEffectDerivations['derivePath'])(node, deps)) ?? null;\n}\n\n/** What to hand `Scene.add` (or an insert op) for a path-effect node over\n * `sources`, in the order its kind reads them, deriving through\n * `derivations` — the ones the scene registers under {@link PATH_EFFECT}. */\nexport function pathEffectNode<TLayer extends string>(\n effect: PathEffect,\n sources: readonly NodeId[],\n layer: TLayer,\n derivations: PathEffectDerivations,\n): {\n kind: 'leaf';\n layer: TLayer;\n pose: { x: number; y: number; width: number; height: number };\n data: { pathEffect: PathEffect };\n dependsOn: readonly NodeId[];\n derivePath: PathEffectDerivations['derivePath'];\n derivePose: PathEffectDerivations['derivePose'];\n} {\n return {\n kind: 'leaf',\n layer,\n pose: { x: 0, y: 0, width: 0, height: 0 },\n data: { pathEffect: effect },\n dependsOn: [...sources],\n derivePath: derivations.derivePath,\n derivePose: derivations.derivePose,\n };\n}\n\n/** The path-effect derivations `registry` holds under {@link PATH_EFFECT},\n * or `null` when it holds none. */\nexport function registeredPathEffects(registry: {\n derivePath?: Readonly<Record<string, unknown>>;\n derivePose?: Readonly<Record<string, unknown>>;\n}): PathEffectDerivations | null {\n const derivePath = registry.derivePath?.[PATH_EFFECT];\n const derivePose = registry.derivePose?.[PATH_EFFECT];\n return typeof derivePath === 'function' && typeof derivePose === 'function'\n ? { derivePath, derivePose } as PathEffectDerivations\n : null;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAoBA,IAAa,cAAc;;AAG3B,SAAgB,aAAa,MAAyD;CACpF,MAAM,UAAU,MAAM,KAAA,EAAyD;CAC/E,OAAO,UAAU,OAAO,WAAW,YAAY,OAAO,OAAO,SAAS,WAAW,SAAS;AAC5F;;AAwBA,SAAgB,4BAA4B,MAAiD;;;;;CAK3F,MAAM,0BAAU,IAAI,QAAyB;CAC7C,MAAM,UAAU,MAA0B,SAAgE;EACxG,MAAM,OAAO,QAAQ,IAAI,IAAI;EAC7B,IAAI,SAAS,UAAa,QAAQ,MAAM,MAAM,IAAI,GAAG,OAAO;EAC5D,MAAM,SAAS,aAAa,IAAI;EAChC,MAAM,aAAa,eAAe;EAClC,MAAM,EAAE,OAAO,OAAO,UAAU,oBAAqB,WAAW,OAC5D,OACA,YAAY,QAAQ,KAAK,KAAK,MAAO,MAAM,SAAY,OAAO,KAAK,CAAC,CAAE,CAAC,CAAE;EAC7E,MAAM,SAAkB;GACtB,MAAM,KAAK;GACX,OAAO,KAAK,KAAK,MAAM,GAAG,IAAI;GAC9B,SAAS,KAAK,KAAK,MAAM,GAAG,KAAK,IAAI;GACrC,OAAO,KAAK,KAAK,MAAO,MAAM,SAAY,SAAY,aAAa,EAAE,IAAI,CAAE;GAC3E,OAAO,KAAK,KAAK,MAAM,GAAG,QAAQ,IAAI;GACtC,MAAM,UAAU,OAAO,OAAO,cAAc,KAAK;GACjD,OAAO,QAAQ,aAAa;EAC9B;EACA,QAAQ,IAAI,MAAM,MAAM;EACxB,OAAO;CACT;CACA,OAAO;EACL,aAAa,MAAM,SAAS,OAAO,MAAM,IAAI,CAAC,CAAC;EAC/C,aAAa,MAAM,SAAS;GAC1B,MAAM,OAAO,OAAO,MAAM,IAAI,CAAC,CAAC;GAChC,IAAI,SAAS,MAAM,OAAO;GAC1B,MAAM,EAAE,GAAG,GAAG,OAAO,WAAW,aAAa,IAAI;GACjD,OAAO;IAAE;IAAG;IAAG;IAAO;GAAO;EAC/B;CACF;AACF;AAEA,SAAS,QAAQ,MAAe,MAA0B,MAA6D;CACrH,IAAI,KAAK,SAAS,KAAK,QAAQ,KAAK,MAAM,WAAW,KAAK,QAAQ,OAAO;CACzE,IAAI,KAAK,UAAU,QAAQ,KAAK,UAAU,eAAe,GAAG,OAAO;CACnE,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;EACpC,MAAM,IAAI,KAAK;EACf,IAAI,KAAK,MAAM,OAAO,GAAG,MAAM,OAAO;EACtC,IAAI,MAAM,QAAW;EACrB,IAAI,KAAK,QAAQ,OAAO,EAAE,KAAK,QAAQ,CAAC,cAAc,KAAK,MAAM,IAAI,EAAE,IAAI,GAAG,OAAO;EACrF,IAAI,KAAK,MAAM,OAAO,EAAE,MAAM,OAAO;CACvC;CAEA,IAAI,KAAK,UAAU,MAAM,aAAa;CACtC,OAAO;AACT;;;AAIA,SAAgB,gBACd,MACA,MAC+B;CAC/B,IAAI,OAAO,KAAK,eAAe,YAAY,OAAO;CAClD,OAAO,eAAgB,KAAK,WAAmD,MAAM,IAAI,CAAC,KAAK;AACjG;;;;AAKA,SAAgB,eACd,QACA,SACA,OACA,aASA;CACA,OAAO;EACL,MAAM;EACN;EACA,MAAM;GAAE,GAAG;GAAG,GAAG;GAAG,OAAO;GAAG,QAAQ;EAAE;EACxC,MAAM,EAAE,YAAY,OAAO;EAC3B,WAAW,CAAC,GAAG,OAAO;EACtB,YAAY,YAAY;EACxB,YAAY,YAAY;CAC1B;AACF;;;AAIA,SAAgB,sBAAsB,UAGL;CAC/B,MAAM,aAAa,SAAS,aAAa;CACzC,MAAM,aAAa,SAAS,aAAa;CACzC,OAAO,OAAO,eAAe,cAAc,OAAO,eAAe,aAC7D;EAAE;EAAY;CAAW,IACzB;AACN"}
1
+ {"version":3,"file":"derive.js","names":[],"sources":["../../../../src/core/scene/pathEffects/derive.ts"],"sourcesContent":["/**\n * A path effect as a scene node: a leaf whose `dependsOn` names its sources,\n * whose `data.pathEffect` says what to make of them, and whose path and pose\n * are both derived — the path to paint and pick, the pose as that path's\n * bounds, so selection chrome and marquee see where it is.\n *\n * Reading a source's outline and paint is its painter's business, which sits\n * above the scene, so the derivations are made from a reader:\n * `createPathEffectDerivations(read)`. The canvas layer makes the kit's, and\n * a scene that holds path effects registers them under {@link PATH_EFFECT}.\n */\nimport { boundsOfPath, type Path } from '@weasel-js/geom';\nimport { derivedPartsOf, pathFromParts, type DerivedPart } from '../derivedParts';\nimport { fontGeneration, noteFontRead, withFontReads } from '../fontReads';\nimport { samePoseValue, snapshotPose } from '../poseSnapshot';\nimport type { DerivedDep, NodeId } from '../types';\nimport { effectParts, type PathEffect } from './effects';\nimport type { EffectSourceReader } from './source';\n\n/** The registry key both derivations go by, for `derivePath` and `derivePose`. */\nexport const PATH_EFFECT = 'kit:pathEffect';\n\n/** A node's path effect, or `null` when its data holds none. */\nexport function pathEffectOf(node: { data?: unknown } | undefined): PathEffect | null {\n const effect = (node?.data as { pathEffect?: PathEffect } | null | undefined)?.pathEffect;\n return effect && typeof effect === 'object' && typeof effect.kind === 'string' ? effect : null;\n}\n\n/** A path-effect node's two derivations. */\nexport interface PathEffectDerivations {\n /** What it paints and picks by. */\n derivePath(node: { data?: unknown }, deps: readonly (DerivedDep<unknown> | undefined)[]): Path | null;\n /** The bounds of what it paints, or `null` — its authored pose — while it\n * paints nothing. */\n derivePose(node: { data?: unknown }, deps: readonly (DerivedDep<unknown> | undefined)[]): { x: number; y: number; width: number; height: number } | null;\n}\n\ninterface Derived {\n data: unknown;\n nodes: unknown[];\n depData: unknown[];\n poses: unknown[];\n paths: (Path | null)[];\n inherited: unknown[];\n path: Path | null;\n /** The face generation it was worked out under, or `null` when it read no\n * glyph outlines. */\n faces: number | null;\n}\n\n/** The derivations of a path effect whose sources `read` reads. */\nexport function createPathEffectDerivations(read: EffectSourceReader): PathEffectDerivations {\n /** One result per effect node, shared by its two derivations: the pose is\n * the path's bounds, and working the effect out twice a frame would double\n * its cost. Checked against everything it read, by value for poses — a\n * drag's override mutates its pose in place. */\n const results = new WeakMap<object, Derived>();\n const derive = (node: { data?: unknown }, deps: readonly (DerivedDep<unknown> | undefined)[]): Derived => {\n const held = results.get(node);\n if (held !== undefined && current(held, node, deps)) return held;\n const effect = pathEffectOf(node);\n const generation = fontGeneration();\n const { value: parts, fonts } = withFontReads(() => (effect === null\n ? null\n : effectParts(effect, deps.map((d) => (d === undefined ? null : read(d))))));\n const result: Derived = {\n data: node.data,\n nodes: deps.map((d) => d?.node),\n depData: deps.map((d) => d?.node.data),\n poses: deps.map((d) => (d === undefined ? undefined : snapshotPose(d.pose))),\n paths: deps.map((d) => d?.path ?? null),\n inherited: deps.map((d) => d?.inherited),\n path: parts === null ? null : pathFromParts(parts),\n faces: fonts ? generation : null,\n };\n results.set(node, result);\n return result;\n };\n return {\n derivePath: (node, deps) => derive(node, deps).path,\n derivePose: (node, deps) => {\n const path = derive(node, deps).path;\n if (path === null) return null;\n const { x, y, width, height } = boundsOfPath(path);\n return { x, y, width, height };\n },\n };\n}\n\nfunction current(held: Derived, node: { data?: unknown }, deps: readonly (DerivedDep<unknown> | undefined)[]): boolean {\n if (held.data !== node.data || held.nodes.length !== deps.length) return false;\n if (held.faces !== null && held.faces !== fontGeneration()) return false;\n for (let i = 0; i < deps.length; i++) {\n const d = deps[i];\n if (held.nodes[i] !== d?.node) return false;\n if (d === undefined) continue;\n if (held.depData[i] !== d.node.data || !samePoseValue(held.poses[i], d.pose)) return false;\n if (held.paths[i] !== d.path || held.inherited[i] !== d.inherited) return false;\n }\n // A hit is still a read, for the scene memo enclosing this one.\n if (held.faces !== null) noteFontRead();\n return true;\n}\n\n/** The parts a path-effect node paints, as its sources stand, or `null` when\n * it has no derivation or nothing to paint. */\nexport function pathEffectParts(\n node: { data?: unknown; derivePath?: unknown },\n deps: readonly (DerivedDep<unknown> | undefined)[],\n): readonly DerivedPart[] | null {\n if (typeof node.derivePath !== 'function') return null;\n return derivedPartsOf((node.derivePath as PathEffectDerivations['derivePath'])(node, deps)) ?? null;\n}\n\n/** What to hand `Scene.add` (or an insert op) for a path-effect node over\n * `sources`, in the order its kind reads them, deriving through\n * `derivations` — the ones the scene registers under {@link PATH_EFFECT}. */\nexport function pathEffectNode<TLayer extends string>(\n effect: PathEffect,\n sources: readonly NodeId[],\n layer: TLayer,\n derivations: PathEffectDerivations,\n): {\n kind: 'mark';\n layer: TLayer;\n pose: { x: number; y: number; width: number; height: number };\n data: { pathEffect: PathEffect };\n dependsOn: readonly NodeId[];\n derivePath: PathEffectDerivations['derivePath'];\n derivePose: PathEffectDerivations['derivePose'];\n} {\n return {\n kind: 'mark',\n layer,\n pose: { x: 0, y: 0, width: 0, height: 0 },\n data: { pathEffect: effect },\n dependsOn: [...sources],\n derivePath: derivations.derivePath,\n derivePose: derivations.derivePose,\n };\n}\n\n/** The path-effect derivations `registry` holds under {@link PATH_EFFECT},\n * or `null` when it holds none. */\nexport function registeredPathEffects(registry: {\n derivePath?: Readonly<Record<string, unknown>>;\n derivePose?: Readonly<Record<string, unknown>>;\n}): PathEffectDerivations | null {\n const derivePath = registry.derivePath?.[PATH_EFFECT];\n const derivePose = registry.derivePose?.[PATH_EFFECT];\n return typeof derivePath === 'function' && typeof derivePose === 'function'\n ? { derivePath, derivePose } as PathEffectDerivations\n : null;\n}\n"],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ import { pathEffectOf } from "./derive.js";
2
+
3
+ //#region src/core/scene/pathEffects/foundHidden.ts
4
+ /**
5
+ * A path effect's `foundHidden` names its sources by position, so a retarget
6
+ * of its `dependsOn` carries the record along: every writer of `dependsOn`
7
+ * goes through `scene.setDependsOn`, which asks this.
8
+ */
9
+ /**
10
+ * `data` with its path effect's `foundHidden` moved from the positions the
11
+ * sources held in `from` to the ones they hold in `to`, dropping a source
12
+ * `to` no longer names; `null` where nothing moves.
13
+ */
14
+ function foundHiddenRetargeted(data, from, to) {
15
+ const effect = pathEffectOf({ data });
16
+ const found = effect?.foundHidden;
17
+ if (!effect || !found || found.length === 0) return null;
18
+ const was = Array.isArray(from) ? from : [];
19
+ const ids = new Set(found.map((at) => was[at]));
20
+ const next = (Array.isArray(to) ? to : []).flatMap((id, at) => ids.has(id) ? [at] : []);
21
+ if (next.length === found.length && next.every((at, k) => at === found[k])) return null;
22
+ const moved = {
23
+ ...effect,
24
+ foundHidden: next
25
+ };
26
+ if (next.length === 0) delete moved.foundHidden;
27
+ return {
28
+ ...data,
29
+ pathEffect: moved
30
+ };
31
+ }
32
+
33
+ //#endregion
34
+ export { foundHiddenRetargeted };
35
+ //# sourceMappingURL=foundHidden.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"foundHidden.js","names":[],"sources":["../../../../src/core/scene/pathEffects/foundHidden.ts"],"sourcesContent":["/**\n * A path effect's `foundHidden` names its sources by position, so a retarget\n * of its `dependsOn` carries the record along: every writer of `dependsOn`\n * goes through `scene.setDependsOn`, which asks this.\n */\nimport { pathEffectOf } from './derive';\nimport type { PathEffect } from './effects';\n\n/**\n * `data` with its path effect's `foundHidden` moved from the positions the\n * sources held in `from` to the ones they hold in `to`, dropping a source\n * `to` no longer names; `null` where nothing moves.\n */\nexport function foundHiddenRetargeted(data: unknown, from: unknown, to: unknown): unknown {\n const effect = pathEffectOf({ data });\n const found = effect?.foundHidden;\n if (!effect || !found || found.length === 0) return null;\n const was = Array.isArray(from) ? from : [];\n const ids = new Set(found.map((at) => was[at]));\n const next = (Array.isArray(to) ? to : []).flatMap((id, at) => (ids.has(id) ? [at] : []));\n if (next.length === found.length && next.every((at, k) => at === found[k])) return null;\n const moved: PathEffect = { ...effect, foundHidden: next };\n if (next.length === 0) delete moved.foundHidden;\n return { ...(data as object), pathEffect: moved };\n}\n"],"mappings":";;;;;;;;;;;;;AAaA,SAAgB,sBAAsB,MAAe,MAAe,IAAsB;CACxF,MAAM,SAAS,aAAa,EAAE,KAAK,CAAC;CACpC,MAAM,QAAQ,QAAQ;CACtB,IAAI,CAAC,UAAU,CAAC,SAAS,MAAM,WAAW,GAAG,OAAO;CACpD,MAAM,MAAM,MAAM,QAAQ,IAAI,IAAI,OAAO,CAAC;CAC1C,MAAM,MAAM,IAAI,IAAI,MAAM,KAAK,OAAO,IAAI,GAAG,CAAC;CAC9C,MAAM,QAAQ,MAAM,QAAQ,EAAE,IAAI,KAAK,CAAC,EAAA,CAAG,SAAS,IAAI,OAAQ,IAAI,IAAI,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC,CAAE;CACxF,IAAI,KAAK,WAAW,MAAM,UAAU,KAAK,OAAO,IAAI,MAAM,OAAO,MAAM,EAAE,GAAG,OAAO;CACnF,MAAM,QAAoB;EAAE,GAAG;EAAQ,aAAa;CAAK;CACzD,IAAI,KAAK,WAAW,GAAG,OAAO,MAAM;CACpC,OAAO;EAAE,GAAI;EAAiB,YAAY;CAAM;AAClD"}
@@ -0,0 +1,89 @@
1
+ import { carryBoxPath } from "./depFrame.js";
2
+
3
+ //#region src/core/scene/plyStacks.ts
4
+ /** Empty stacks. */
5
+ function createPlyStacks() {
6
+ const plies = /* @__PURE__ */ new Map();
7
+ /** Each entry's claim, in claiming order; outlives a full drop, so a reinstall goes back in place. */
8
+ const claims = /* @__PURE__ */ new WeakMap();
9
+ let claimed = 0;
10
+ /** How many nodes hold each number of plies. */
11
+ const sizes = [];
12
+ let deepest = 0;
13
+ const resized = (from, to) => {
14
+ if (from > 0) sizes[from]--;
15
+ if (to > 0) sizes[to] = (sizes[to] ?? 0) + 1;
16
+ if (to > deepest) deepest = to;
17
+ while (deepest > 0 && (sizes[deepest] ?? 0) === 0) deepest--;
18
+ };
19
+ return {
20
+ plies,
21
+ claim(id, entry) {
22
+ let stack = plies.get(id);
23
+ if (stack === void 0) plies.set(id, stack = []);
24
+ const at = stack.indexOf(entry);
25
+ if (at >= 0) stack.splice(at, 1);
26
+ let claim = at >= 0 ? void 0 : claims.get(entry);
27
+ if (claim === void 0) claims.set(entry, claim = ++claimed);
28
+ let i = stack.length;
29
+ while (i > 0 && claims.get(stack[i - 1]) > claim) i--;
30
+ stack.splice(i, 0, entry);
31
+ if (at < 0) resized(stack.length - 1, stack.length);
32
+ },
33
+ release(id, entry) {
34
+ claims.delete(entry);
35
+ const stack = plies.get(id);
36
+ const at = stack?.indexOf(entry) ?? -1;
37
+ if (stack === void 0 || at < 0) return false;
38
+ stack.splice(at, 1);
39
+ resized(stack.length + 1, stack.length);
40
+ if (stack.length === 0) plies.delete(id);
41
+ return true;
42
+ },
43
+ drop(id) {
44
+ const stack = plies.get(id);
45
+ if (stack === void 0) return false;
46
+ plies.delete(id);
47
+ resized(stack.length, 0);
48
+ return true;
49
+ },
50
+ dropAll() {
51
+ plies.clear();
52
+ sizes.length = 0;
53
+ deepest = 0;
54
+ },
55
+ depth: () => Math.max(deepest, 1)
56
+ };
57
+ }
58
+ var boxOf = (pose) => {
59
+ const p = pose;
60
+ return p !== null && typeof p === "object" && typeof p.x === "number" && typeof p.y === "number" && typeof p.width === "number" && typeof p.height === "number" ? p : null;
61
+ };
62
+ var sameBox = (a, b) => a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
63
+ /**
64
+ * The path `stack` shows, folded to `pose`: the topmost ply's path, in the box of the pose
65
+ * that ply gives beside it, carried to `pose` where a ply above gives another. A path given
66
+ * with no pose of its own is drawn where it is. `held` keeps the last carry.
67
+ */
68
+ function plyPath(stack, pose, held) {
69
+ for (let k = stack.length - 1; k >= 0; k--) {
70
+ const ply = stack[k];
71
+ if (ply.path === void 0) continue;
72
+ const from = boxOf(ply.pose);
73
+ const to = boxOf(pose);
74
+ if (from === null || to === null || sameBox(from, to)) return ply.path;
75
+ const last = held.carried;
76
+ if (last !== void 0 && last.source === ply.path && sameBox(last.from, from) && sameBox(last.to, to)) return last.path;
77
+ held.carried = {
78
+ source: ply.path,
79
+ from: { ...from },
80
+ to: { ...to },
81
+ path: carryBoxPath(ply.path, from, to)
82
+ };
83
+ return held.carried.path;
84
+ }
85
+ }
86
+
87
+ //#endregion
88
+ export { createPlyStacks, plyPath };
89
+ //# sourceMappingURL=plyStacks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plyStacks.js","names":[],"sources":["../../../src/core/scene/plyStacks.ts"],"sourcesContent":["/**\n * Each node's plies of overrides, kept in the order their writers claimed\n * the node, and the path a stack shows.\n */\nimport type { Path } from '@weasel-js/geom';\nimport { carryBoxPath } from './depFrame';\nimport type { NodeId, PoseOverride } from './types';\n\n/** {@link createPlyStacks}'s handle. */\nexport interface PlyStacks<Entry> {\n /** Per node, its plies bottom first. */\n readonly plies: ReadonlyMap<NodeId, readonly Entry[]>;\n /** Install `entry` on `id`: on top, or where it stood when a full drop took it off. An\n * installed entry moves to the top. */\n claim(id: NodeId, entry: Entry): void;\n /** Take `entry` off `id` and forget where it stood. Whether it was installed. */\n release(id: NodeId, entry: Entry): boolean;\n /** Take every ply off `id`, each keeping its place for when its writer reinstalls it.\n * Whether the node had any. */\n drop(id: NodeId): boolean;\n /** {@link PlyStacks.drop} for every node. */\n dropAll(): void;\n /** The deepest stack, and at least 1. */\n depth(): number;\n}\n\n/** Empty stacks. */\nexport function createPlyStacks<Entry extends object>(): PlyStacks<Entry> {\n const plies = new Map<NodeId, Entry[]>();\n /** Each entry's claim, in claiming order; outlives a full drop, so a reinstall goes back in place. */\n const claims = new WeakMap<Entry, number>();\n let claimed = 0;\n /** How many nodes hold each number of plies. */\n const sizes: number[] = [];\n let deepest = 0;\n\n const resized = (from: number, to: number): void => {\n if (from > 0) sizes[from]!--;\n if (to > 0) sizes[to] = (sizes[to] ?? 0) + 1;\n if (to > deepest) deepest = to;\n while (deepest > 0 && (sizes[deepest] ?? 0) === 0) deepest--;\n };\n\n return {\n plies,\n claim(id, entry) {\n let stack = plies.get(id);\n if (stack === undefined) plies.set(id, stack = []);\n const at = stack.indexOf(entry);\n if (at >= 0) stack.splice(at, 1);\n let claim = at >= 0 ? undefined : claims.get(entry);\n if (claim === undefined) claims.set(entry, claim = ++claimed);\n let i = stack.length;\n while (i > 0 && claims.get(stack[i - 1]!)! > claim) i--;\n stack.splice(i, 0, entry);\n if (at < 0) resized(stack.length - 1, stack.length);\n },\n release(id, entry) {\n claims.delete(entry);\n const stack = plies.get(id);\n const at = stack?.indexOf(entry) ?? -1;\n if (stack === undefined || at < 0) return false;\n stack.splice(at, 1);\n resized(stack.length + 1, stack.length);\n if (stack.length === 0) plies.delete(id);\n return true;\n },\n drop(id) {\n const stack = plies.get(id);\n if (stack === undefined) return false;\n plies.delete(id);\n resized(stack.length, 0);\n return true;\n },\n dropAll() {\n plies.clear();\n sizes.length = 0;\n deepest = 0;\n },\n depth: () => Math.max(deepest, 1),\n };\n}\n\ntype Box = { x: number; y: number; width: number; height: number };\n\nconst boxOf = (pose: unknown): Box | null => {\n const p = pose as Partial<Box> | null;\n return p !== null && typeof p === 'object' && typeof p.x === 'number' && typeof p.y === 'number'\n && typeof p.width === 'number' && typeof p.height === 'number'\n ? p as Box : null;\n};\n\nconst sameBox = (a: Box, b: Box): boolean => a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;\n\n/** A carried path and what it was carried from and to, so a stack whose poses hold still shows one path. */\nexport interface CarriedPath {\n source: Path;\n from: Box;\n to: Box;\n path: Path;\n}\n\n/**\n * The path `stack` shows, folded to `pose`: the topmost ply's path, in the box of the pose\n * that ply gives beside it, carried to `pose` where a ply above gives another. A path given\n * with no pose of its own is drawn where it is. `held` keeps the last carry.\n */\nexport function plyPath<TPose, TData>(\n stack: readonly PoseOverride<TPose, TData>[],\n pose: TPose | undefined,\n held: { carried?: CarriedPath },\n): Path | undefined {\n for (let k = stack.length - 1; k >= 0; k--) {\n const ply = stack[k]!;\n if (ply.path === undefined) continue;\n const from = boxOf(ply.pose);\n const to = boxOf(pose);\n if (from === null || to === null || sameBox(from, to)) return ply.path;\n const last = held.carried;\n if (last !== undefined && last.source === ply.path && sameBox(last.from, from) && sameBox(last.to, to)) return last.path;\n held.carried = { source: ply.path, from: { ...from }, to: { ...to }, path: carryBoxPath(ply.path, from, to) };\n return held.carried.path;\n }\n return undefined;\n}\n"],"mappings":"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"}
@@ -1,47 +1,65 @@
1
1
  //#region src/core/scene/poseHold.ts
2
- /** Build the {@link PoseHold} a table hands out from `hold()`. */
3
- function createPoseHold(table) {
2
+ /** Build the {@link PoseHold} a table hands out from `hold()`. `pliesOf`
3
+ * answers a node's plies, bottom first. */
4
+ function createPoseHold(table, pliesOf) {
4
5
  /** The entry published per id, held by reference so a frame mutates it in place. */
5
6
  const held = /* @__PURE__ */ new Map();
6
7
  const seen = /* @__PURE__ */ new Set();
8
+ const publish = (frame) => {
9
+ seen.clear();
10
+ for (const [id, shown] of frame) {
11
+ seen.add(id);
12
+ const existing = held.get(id);
13
+ if (existing) {
14
+ existing.pose = shown.pose;
15
+ existing.data = shown.data;
16
+ if (!table.has(id, existing)) table.set(id, existing);
17
+ continue;
18
+ }
19
+ const entry = {
20
+ pose: shown.pose,
21
+ data: shown.data
22
+ };
23
+ held.set(id, entry);
24
+ table.set(id, entry);
25
+ }
26
+ for (const [id, entry] of held) {
27
+ if (seen.has(id)) continue;
28
+ held.delete(id);
29
+ table.clear(id, entry);
30
+ }
31
+ table.commit();
32
+ };
7
33
  return {
8
34
  set(poses) {
9
- seen.clear();
10
- for (const [id, pose] of poses) {
11
- seen.add(id);
12
- const existing = held.get(id);
13
- if (existing) {
14
- existing.pose = pose;
15
- continue;
16
- }
17
- const entry = { pose };
18
- held.set(id, entry);
19
- table.set(id, entry);
20
- }
21
- for (const [id, entry] of held) {
22
- if (seen.has(id)) continue;
23
- held.delete(id);
24
- if (table.get(id) === entry) table.clear(id);
25
- }
26
- table.commit();
35
+ publish(mapped(poses));
27
36
  },
37
+ show: publish,
28
38
  others() {
29
39
  const out = /* @__PURE__ */ new Map();
30
40
  for (const id of table.ids()) {
31
- const entry = table.get(id);
32
- if (entry === void 0 || entry === held.get(id) || entry.pose === void 0) continue;
33
- out.set(id, entry.pose);
41
+ const stack = pliesOf(id) ?? [];
42
+ const mine = held.get(id);
43
+ for (let k = stack.length - 1; k >= 0; k--) {
44
+ const entry = stack[k];
45
+ if (entry === mine || entry.pose === void 0) continue;
46
+ out.set(id, entry.pose);
47
+ break;
48
+ }
34
49
  }
35
50
  return out;
36
51
  },
37
52
  drop() {
38
53
  if (held.size === 0) return;
39
- for (const [id, entry] of held) if (table.get(id) === entry) table.clear(id);
54
+ for (const [id, entry] of held) table.clear(id, entry);
40
55
  held.clear();
41
56
  table.commit();
42
57
  }
43
58
  };
44
59
  }
60
+ function* mapped(poses) {
61
+ for (const [id, pose] of poses) yield [id, { pose }];
62
+ }
45
63
 
46
64
  //#endregion
47
65
  export { createPoseHold };
@@ -1 +1 @@
1
- {"version":3,"file":"poseHold.js","names":[],"sources":["../../../src/core/scene/poseHold.ts"],"sourcesContent":["import type { NodeId, PoseHold, PoseOverride, PoseOverrides } from './types';\n\n/** The table methods a hold writes through. */\ntype HoldTable<TPose> = Pick<PoseOverrides<TPose>, 'set' | 'get' | 'ids' | 'clear' | 'commit'>;\n\n/** Build the {@link PoseHold} a table hands out from `hold()`. */\nexport function createPoseHold<TPose>(table: HoldTable<TPose>): PoseHold<TPose> {\n /** The entry published per id, held by reference so a frame mutates it in place. */\n const held = new Map<NodeId, PoseOverride<TPose>>();\n const seen = new Set<NodeId>();\n\n return {\n set(poses) {\n seen.clear();\n for (const [id, pose] of poses) {\n seen.add(id);\n const existing = held.get(id);\n if (existing) {\n existing.pose = pose;\n continue;\n }\n const entry: PoseOverride<TPose> = { pose };\n held.set(id, entry);\n // `table.set` notifies per call, so an id is set once and mutated after.\n table.set(id, entry);\n }\n for (const [id, entry] of held) {\n if (seen.has(id)) continue;\n held.delete(id);\n if (table.get(id) === entry) table.clear(id);\n }\n table.commit();\n },\n others() {\n const out = new Map<NodeId, TPose>();\n for (const id of table.ids()) {\n const entry = table.get(id);\n if (entry === undefined || entry === held.get(id) || entry.pose === undefined) continue;\n out.set(id, entry.pose);\n }\n return out;\n },\n drop() {\n if (held.size === 0) return;\n for (const [id, entry] of held) {\n if (table.get(id) === entry) table.clear(id);\n }\n held.clear();\n table.commit();\n },\n };\n}\n"],"mappings":";;AAMA,SAAgB,eAAsB,OAA0C;;CAE9E,MAAM,uBAAO,IAAI,IAAiC;CAClD,MAAM,uBAAO,IAAI,IAAY;CAE7B,OAAO;EACL,IAAI,OAAO;GACT,KAAK,MAAM;GACX,KAAK,MAAM,CAAC,IAAI,SAAS,OAAO;IAC9B,KAAK,IAAI,EAAE;IACX,MAAM,WAAW,KAAK,IAAI,EAAE;IAC5B,IAAI,UAAU;KACZ,SAAS,OAAO;KAChB;IACF;IACA,MAAM,QAA6B,EAAE,KAAK;IAC1C,KAAK,IAAI,IAAI,KAAK;IAElB,MAAM,IAAI,IAAI,KAAK;GACrB;GACA,KAAK,MAAM,CAAC,IAAI,UAAU,MAAM;IAC9B,IAAI,KAAK,IAAI,EAAE,GAAG;IAClB,KAAK,OAAO,EAAE;IACd,IAAI,MAAM,IAAI,EAAE,MAAM,OAAO,MAAM,MAAM,EAAE;GAC7C;GACA,MAAM,OAAO;EACf;EACA,SAAS;GACP,MAAM,sBAAM,IAAI,IAAmB;GACnC,KAAK,MAAM,MAAM,MAAM,IAAI,GAAG;IAC5B,MAAM,QAAQ,MAAM,IAAI,EAAE;IAC1B,IAAI,UAAU,UAAa,UAAU,KAAK,IAAI,EAAE,KAAK,MAAM,SAAS,QAAW;IAC/E,IAAI,IAAI,IAAI,MAAM,IAAI;GACxB;GACA,OAAO;EACT;EACA,OAAO;GACL,IAAI,KAAK,SAAS,GAAG;GACrB,KAAK,MAAM,CAAC,IAAI,UAAU,MACxB,IAAI,MAAM,IAAI,EAAE,MAAM,OAAO,MAAM,MAAM,EAAE;GAE7C,KAAK,MAAM;GACX,MAAM,OAAO;EACf;CACF;AACF"}
1
+ {"version":3,"file":"poseHold.js","names":[],"sources":["../../../src/core/scene/poseHold.ts"],"sourcesContent":["import type { NodeId, PoseHold, PoseOverride, PoseOverrides, ShownOverride } from './types';\n\n/** The table methods a hold writes through. */\ntype HoldTable<TPose, TData> = Pick<PoseOverrides<TPose, TData>, 'set' | 'has' | 'ids' | 'clear' | 'commit'>;\n\n/** Build the {@link PoseHold} a table hands out from `hold()`. `pliesOf`\n * answers a node's plies, bottom first. */\nexport function createPoseHold<TPose, TData = unknown>(\n table: HoldTable<TPose, TData>,\n pliesOf: (id: NodeId) => readonly PoseOverride<TPose, TData>[] | undefined,\n): PoseHold<TPose, TData> {\n /** The entry published per id, held by reference so a frame mutates it in place. */\n const held = new Map<NodeId, PoseOverride<TPose, TData>>();\n const seen = new Set<NodeId>();\n\n const publish = (frame: Iterable<readonly [NodeId, ShownOverride<TPose, TData>]>): void => {\n seen.clear();\n for (const [id, shown] of frame) {\n seen.add(id);\n const existing = held.get(id);\n if (existing) {\n existing.pose = shown.pose;\n existing.data = shown.data;\n // The table drops a node's plies on its own when the node goes; put back the node, and the frame goes back too.\n if (!table.has(id, existing)) table.set(id, existing);\n continue;\n }\n const entry: PoseOverride<TPose, TData> = { pose: shown.pose, data: shown.data };\n held.set(id, entry);\n // `table.set` notifies per call, so an id is set once and mutated after.\n table.set(id, entry);\n }\n for (const [id, entry] of held) {\n if (seen.has(id)) continue;\n held.delete(id);\n table.clear(id, entry);\n }\n table.commit();\n };\n\n return {\n set(poses) {\n publish(mapped(poses));\n },\n show: publish,\n others() {\n const out = new Map<NodeId, TPose>();\n for (const id of table.ids()) {\n const stack = pliesOf(id) ?? [];\n const mine = held.get(id);\n for (let k = stack.length - 1; k >= 0; k--) {\n const entry = stack[k]!;\n if (entry === mine || entry.pose === undefined) continue;\n out.set(id, entry.pose);\n break;\n }\n }\n return out;\n },\n drop() {\n if (held.size === 0) return;\n for (const [id, entry] of held) table.clear(id, entry);\n held.clear();\n table.commit();\n },\n };\n}\n\nfunction* mapped<TPose, TData>(poses: Iterable<readonly [NodeId, TPose]>): Iterable<readonly [NodeId, ShownOverride<TPose, TData>]> {\n for (const [id, pose] of poses) yield [id, { pose }];\n}\n"],"mappings":";;;AAOA,SAAgB,eACd,OACA,SACwB;;CAExB,MAAM,uBAAO,IAAI,IAAwC;CACzD,MAAM,uBAAO,IAAI,IAAY;CAE7B,MAAM,WAAW,UAA0E;EACzF,KAAK,MAAM;EACX,KAAK,MAAM,CAAC,IAAI,UAAU,OAAO;GAC/B,KAAK,IAAI,EAAE;GACX,MAAM,WAAW,KAAK,IAAI,EAAE;GAC5B,IAAI,UAAU;IACZ,SAAS,OAAO,MAAM;IACtB,SAAS,OAAO,MAAM;IAEtB,IAAI,CAAC,MAAM,IAAI,IAAI,QAAQ,GAAG,MAAM,IAAI,IAAI,QAAQ;IACpD;GACF;GACA,MAAM,QAAoC;IAAE,MAAM,MAAM;IAAM,MAAM,MAAM;GAAK;GAC/E,KAAK,IAAI,IAAI,KAAK;GAElB,MAAM,IAAI,IAAI,KAAK;EACrB;EACA,KAAK,MAAM,CAAC,IAAI,UAAU,MAAM;GAC9B,IAAI,KAAK,IAAI,EAAE,GAAG;GAClB,KAAK,OAAO,EAAE;GACd,MAAM,MAAM,IAAI,KAAK;EACvB;EACA,MAAM,OAAO;CACf;CAEA,OAAO;EACL,IAAI,OAAO;GACT,QAAQ,OAAO,KAAK,CAAC;EACvB;EACA,MAAM;EACN,SAAS;GACP,MAAM,sBAAM,IAAI,IAAmB;GACnC,KAAK,MAAM,MAAM,MAAM,IAAI,GAAG;IAC5B,MAAM,QAAQ,QAAQ,EAAE,KAAK,CAAC;IAC9B,MAAM,OAAO,KAAK,IAAI,EAAE;IACxB,KAAK,IAAI,IAAI,MAAM,SAAS,GAAG,KAAK,GAAG,KAAK;KAC1C,MAAM,QAAQ,MAAM;KACpB,IAAI,UAAU,QAAQ,MAAM,SAAS,QAAW;KAChD,IAAI,IAAI,IAAI,MAAM,IAAI;KACtB;IACF;GACF;GACA,OAAO;EACT;EACA,OAAO;GACL,IAAI,KAAK,SAAS,GAAG;GACrB,KAAK,MAAM,CAAC,IAAI,UAAU,MAAM,MAAM,MAAM,IAAI,KAAK;GACrD,KAAK,MAAM;GACX,MAAM,OAAO;EACf;CACF;AACF;AAEA,UAAU,OAAqB,OAAqG;CAClI,KAAK,MAAM,CAAC,IAAI,SAAS,OAAO,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC;AACrD"}