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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (292) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/dist/animation/poseTarget.js +46 -15
  3. package/dist/animation/poseTarget.js.map +1 -1
  4. package/dist/animation/poseTransition.js +3 -3
  5. package/dist/animation/poseTransition.js.map +1 -1
  6. package/dist/animation/reflow.js +3 -6
  7. package/dist/animation/reflow.js.map +1 -1
  8. package/dist/animation/rig/bindRig.js +2 -2
  9. package/dist/animation/rig/bindRig.js.map +1 -1
  10. package/dist/animation/rig/skinBinding.js +2 -2
  11. package/dist/animation/rig/skinBinding.js.map +1 -1
  12. package/dist/barrel/canvas.js +5 -3
  13. package/dist/barrel/chrome.js +3 -1
  14. package/dist/barrel/paths.js +3 -2
  15. package/dist/barrel/routing.js +2 -2
  16. package/dist/barrel/scene.js +6 -1
  17. package/dist/canvas/NodeShape.js +1 -0
  18. package/dist/canvas/NodeShape.js.map +1 -1
  19. package/dist/canvas/SceneCanvas/SceneCanvasInput.js +2 -0
  20. package/dist/canvas/SceneCanvas/SceneCanvasInput.js.map +1 -1
  21. package/dist/canvas/SceneCanvas/canvasPassThrough.js +1 -1
  22. package/dist/canvas/SceneCanvas/canvasPassThrough.js.map +1 -1
  23. package/dist/canvas/SceneCanvas/defaultNodeRouting.js +2 -2
  24. package/dist/canvas/SceneCanvas/defaultNodeRouting.js.map +1 -1
  25. package/dist/canvas/SceneCanvas/scenePointQuery.js +20 -2
  26. package/dist/canvas/SceneCanvas/scenePointQuery.js.map +1 -1
  27. package/dist/canvas/SceneCanvas/useCanvasTools.js +3 -0
  28. package/dist/canvas/SceneCanvas/useCanvasTools.js.map +1 -1
  29. package/dist/canvas/SceneCanvas/usePreviewGhostLayer.js +25 -19
  30. package/dist/canvas/SceneCanvas/usePreviewGhostLayer.js.map +1 -1
  31. package/dist/canvas/SceneCanvas/useSceneCanvasLayers.js +7 -0
  32. package/dist/canvas/SceneCanvas/useSceneCanvasLayers.js.map +1 -1
  33. package/dist/canvas/SceneCanvas/useSceneCanvasTools.js +1 -0
  34. package/dist/canvas/SceneCanvas/useSceneCanvasTools.js.map +1 -1
  35. package/dist/canvas/SceneCanvas/useScenePicking.js +31 -6
  36. package/dist/canvas/SceneCanvas/useScenePicking.js.map +1 -1
  37. package/dist/canvas/SceneCanvas/useSceneSelectTool.js +4 -5
  38. package/dist/canvas/SceneCanvas/useSceneSelectTool.js.map +1 -1
  39. package/dist/canvas/SceneCanvas/useSceneStandardActions.js +5 -3
  40. package/dist/canvas/SceneCanvas/useSceneStandardActions.js.map +1 -1
  41. package/dist/canvas/SceneCanvas.js +19 -2
  42. package/dist/canvas/SceneCanvas.js.map +1 -1
  43. package/dist/canvas/accentColor.js +26 -3
  44. package/dist/canvas/accentColor.js.map +1 -1
  45. package/dist/canvas/buildSceneTree.js +14 -20
  46. package/dist/canvas/buildSceneTree.js.map +1 -1
  47. package/dist/canvas/copiesAbove.js +1 -1
  48. package/dist/canvas/copiesAbove.js.map +1 -1
  49. package/dist/canvas/defaultDrawOne.js +10 -7
  50. package/dist/canvas/defaultDrawOne.js.map +1 -1
  51. package/dist/canvas/deps/areaSelect.js +5 -2
  52. package/dist/canvas/deps/areaSelect.js.map +1 -1
  53. package/dist/canvas/deps/booleans.js +1 -1
  54. package/dist/canvas/deps/booleans.js.map +1 -1
  55. package/dist/canvas/deps/enteredGroup.js +81 -0
  56. package/dist/canvas/deps/enteredGroup.js.map +1 -0
  57. package/dist/canvas/deps/hitTestArea.js +27 -15
  58. package/dist/canvas/deps/hitTestArea.js.map +1 -1
  59. package/dist/canvas/deps/insert.js +4 -36
  60. package/dist/canvas/deps/insert.js.map +1 -1
  61. package/dist/canvas/deps/insertSite.js +39 -0
  62. package/dist/canvas/deps/insertSite.js.map +1 -0
  63. package/dist/canvas/deps/lassoSelect.js +6 -3
  64. package/dist/canvas/deps/lassoSelect.js.map +1 -1
  65. package/dist/canvas/deps/pathLeaf.js +1 -1
  66. package/dist/canvas/deps/pathLeaf.js.map +1 -1
  67. package/dist/canvas/deps/slice.js +2 -1
  68. package/dist/canvas/deps/slice.js.map +1 -1
  69. package/dist/canvas/derivedPath.js +14 -6
  70. package/dist/canvas/derivedPath.js.map +1 -1
  71. package/dist/canvas/doubleClickTarget.js +9 -11
  72. package/dist/canvas/doubleClickTarget.js.map +1 -1
  73. package/dist/canvas/drawnBounds.js +16 -7
  74. package/dist/canvas/drawnBounds.js.map +1 -1
  75. package/dist/canvas/inheritedPaints.js +22 -11
  76. package/dist/canvas/inheritedPaints.js.map +1 -1
  77. package/dist/canvas/nodeShapes/nodePaint.js +1 -7
  78. package/dist/canvas/nodeShapes/nodePaint.js.map +1 -1
  79. package/dist/canvas/nodeShapes/pathPainter.js +2 -3
  80. package/dist/canvas/nodeShapes/pathPainter.js.map +1 -1
  81. package/dist/canvas/nodeShapes/rectFallbackPainter.js +1 -1
  82. package/dist/canvas/nodeShapes/rectFallbackPainter.js.map +1 -1
  83. package/dist/canvas/nodeShapes/shapePainter.js +2 -3
  84. package/dist/canvas/nodeShapes/shapePainter.js.map +1 -1
  85. package/dist/canvas/ownClip.js +24 -0
  86. package/dist/canvas/ownClip.js.map +1 -0
  87. package/dist/canvas/paintCull.js.map +1 -1
  88. package/dist/canvas/pickIndex/nodeBroadBox.js +86 -0
  89. package/dist/canvas/pickIndex/nodeBroadBox.js.map +1 -0
  90. package/dist/canvas/pickIndex/scenePickIndex.js +208 -0
  91. package/dist/canvas/pickIndex/scenePickIndex.js.map +1 -0
  92. package/dist/canvas/pickWalk.js +14 -90
  93. package/dist/canvas/pickWalk.js.map +1 -1
  94. package/dist/canvas/poseCascade.js +1 -1
  95. package/dist/canvas/poseCascade.js.map +1 -1
  96. package/dist/canvas/publishPreviews.js +97 -0
  97. package/dist/canvas/publishPreviews.js.map +1 -0
  98. package/dist/canvas/sceneAdapter.js +7 -5
  99. package/dist/canvas/sceneAdapter.js.map +1 -1
  100. package/dist/canvas/sceneClipboard.js +17 -10
  101. package/dist/canvas/sceneClipboard.js.map +1 -1
  102. package/dist/canvas/scenePickReads.js +37 -0
  103. package/dist/canvas/scenePickReads.js.map +1 -0
  104. package/dist/canvas/scenePickSource.js +74 -0
  105. package/dist/canvas/scenePickSource.js.map +1 -0
  106. package/dist/canvas/sceneSlotWiring.js +9 -4
  107. package/dist/canvas/sceneSlotWiring.js.map +1 -1
  108. package/dist/canvas/sceneViewRender.js +9 -5
  109. package/dist/canvas/sceneViewRender.js.map +1 -1
  110. package/dist/canvas/shownNodes.js +15 -0
  111. package/dist/canvas/shownNodes.js.map +1 -0
  112. package/dist/canvas/useSceneAdapter.js +5 -3
  113. package/dist/canvas/useSceneAdapter.js.map +1 -1
  114. package/dist/clipboard.d.ts +2 -2
  115. package/dist/{clone-C3e0OTuF.d.ts → clone-qb2e1F5p.d.ts} +2 -2
  116. package/dist/clone.d.ts +3 -3
  117. package/dist/core/isPlainObject.js +2 -2
  118. package/dist/core/layers/layerGroup.js.map +1 -1
  119. package/dist/core/scene/NodeRouting.js.map +1 -1
  120. package/dist/core/scene/arrivalWindow.js +1 -1
  121. package/dist/core/scene/arrivalWindow.js.map +1 -1
  122. package/dist/core/scene/clipNothing.js +2 -5
  123. package/dist/core/scene/clipNothing.js.map +1 -1
  124. package/dist/core/scene/declaredFieldSetters.js +44 -0
  125. package/dist/core/scene/declaredFieldSetters.js.map +1 -0
  126. package/dist/core/scene/depMemo.js +3 -0
  127. package/dist/core/scene/depMemo.js.map +1 -1
  128. package/dist/core/scene/dependents.js +3 -1
  129. package/dist/core/scene/dependents.js.map +1 -1
  130. package/dist/core/scene/documentOf.js +43 -0
  131. package/dist/core/scene/documentOf.js.map +1 -0
  132. package/dist/core/scene/effectivePose.js +37 -13
  133. package/dist/core/scene/effectivePose.js.map +1 -1
  134. package/dist/core/scene/fieldEdits.js +17 -14
  135. package/dist/core/scene/fieldEdits.js.map +1 -1
  136. package/dist/core/scene/historyForm.js +4 -4
  137. package/dist/core/scene/historyForm.js.map +1 -1
  138. package/dist/core/scene/inheritedPaint.js +27 -5
  139. package/dist/core/scene/inheritedPaint.js.map +1 -1
  140. package/dist/core/scene/kitOps.js +3 -23
  141. package/dist/core/scene/kitOps.js.map +1 -1
  142. package/dist/core/scene/layoutPass.js +1 -2
  143. package/dist/core/scene/layoutPass.js.map +1 -1
  144. package/dist/core/scene/nodeAsShown.js +72 -0
  145. package/dist/core/scene/nodeAsShown.js.map +1 -0
  146. package/dist/core/scene/nodeFnFields.js +2 -10
  147. package/dist/core/scene/nodeFnFields.js.map +1 -1
  148. package/dist/core/scene/nodeMemo.js +6 -1
  149. package/dist/core/scene/nodeMemo.js.map +1 -1
  150. package/dist/core/scene/nodeStore.js +2 -4
  151. package/dist/core/scene/nodeStore.js.map +1 -1
  152. package/dist/core/scene/pathEffects/derive.js +3 -2
  153. package/dist/core/scene/pathEffects/derive.js.map +1 -1
  154. package/dist/core/scene/pathEffects/foundHidden.js +35 -0
  155. package/dist/core/scene/pathEffects/foundHidden.js.map +1 -0
  156. package/dist/core/scene/plyStacks.js +89 -0
  157. package/dist/core/scene/plyStacks.js.map +1 -0
  158. package/dist/core/scene/poseHold.js +42 -24
  159. package/dist/core/scene/poseHold.js.map +1 -1
  160. package/dist/core/scene/poseOverrides.js +35 -38
  161. package/dist/core/scene/poseOverrides.js.map +1 -1
  162. package/dist/core/scene/posePlies.js +59 -0
  163. package/dist/core/scene/posePlies.js.map +1 -0
  164. package/dist/core/scene/reconcileSpecs.js +6 -15
  165. package/dist/core/scene/reconcileSpecs.js.map +1 -1
  166. package/dist/core/scene/renderOrder.js +10 -4
  167. package/dist/core/scene/renderOrder.js.map +1 -1
  168. package/dist/core/scene/sameValue.js +17 -0
  169. package/dist/core/scene/sameValue.js.map +1 -0
  170. package/dist/core/scene/scene.js +3 -3
  171. package/dist/core/scene/scene.js.map +1 -1
  172. package/dist/core/scene/sceneEdits.js +8 -0
  173. package/dist/core/scene/sceneEdits.js.map +1 -1
  174. package/dist/core/scene/sceneState.js +1 -2
  175. package/dist/core/scene/sceneState.js.map +1 -1
  176. package/dist/core/scene/serialization.js +45 -9
  177. package/dist/core/scene/serialization.js.map +1 -1
  178. package/dist/core/scene/staleReplay.js +5 -2
  179. package/dist/core/scene/staleReplay.js.map +1 -1
  180. package/dist/core/scene/statedFields.js +4 -2
  181. package/dist/core/scene/statedFields.js.map +1 -1
  182. package/dist/core/scene/treeEdits.js +2 -4
  183. package/dist/core/scene/treeEdits.js.map +1 -1
  184. package/dist/core/scene/treeOrder.js +3 -1
  185. package/dist/core/scene/treeOrder.js.map +1 -1
  186. package/dist/core/scene/treeReads.js +1 -2
  187. package/dist/core/scene/treeReads.js.map +1 -1
  188. package/dist/core/scene/treeShape.js +13 -0
  189. package/dist/core/scene/treeShape.js.map +1 -0
  190. package/dist/core/scene/types.js.map +1 -1
  191. package/dist/core/scene/useScene.js +1 -1
  192. package/dist/core/scene/useScene.js.map +1 -1
  193. package/dist/{cornerHandles-p8fDIAKt.d.ts → cornerHandles-CNCb7aTY.d.ts} +1 -1
  194. package/dist/{depSchema-mG19aEdD.d.ts → depSchema-B1RS-IJx.d.ts} +27 -54
  195. package/dist/devtools/handlers.js +1 -1
  196. package/dist/devtools/handlers.js.map +1 -1
  197. package/dist/devtools/protocol.js.map +1 -1
  198. package/dist/devtools-protocol.d.ts +4 -3
  199. package/dist/devtools.d.ts +3 -2
  200. package/dist/features/measure/measureHit.js +5 -3
  201. package/dist/features/measure/measureHit.js.map +1 -1
  202. package/dist/features/measure/measurement.js +1 -1
  203. package/dist/features/measure/measurement.js.map +1 -1
  204. package/dist/features/modes/modeLayers.js +14 -7
  205. package/dist/features/modes/modeLayers.js.map +1 -1
  206. package/dist/features/modes/modeTriggers.js +3 -3
  207. package/dist/features/modes/modeTriggers.js.map +1 -1
  208. package/dist/features/pathEffects/effectSources.js +25 -16
  209. package/dist/features/pathEffects/effectSources.js.map +1 -1
  210. package/dist/features/pathEffects/readSource.js +1 -1
  211. package/dist/features/pathEffects/readSource.js.map +1 -1
  212. package/dist/features/pathEffects/useEffectSourcesLayer.js +15 -19
  213. package/dist/features/pathEffects/useEffectSourcesLayer.js.map +1 -1
  214. package/dist/features/paths/editablePath.js +1 -1
  215. package/dist/features/paths/editablePath.js.map +1 -1
  216. package/dist/features/paths/pathSnap/sceneIndex.js +1 -1
  217. package/dist/features/paths/pathSnap/sceneIndex.js.map +1 -1
  218. package/dist/features/selection/enteredGroupFrame.js +65 -0
  219. package/dist/features/selection/enteredGroupFrame.js.map +1 -0
  220. package/dist/features/selection/enteredGroupScope.js +44 -0
  221. package/dist/features/selection/enteredGroupScope.js.map +1 -0
  222. package/dist/features/text/sceneTextDrafts.js +26 -27
  223. package/dist/features/text/sceneTextDrafts.js.map +1 -1
  224. package/dist/features/text/textHitAt.js +4 -2
  225. package/dist/features/text/textHitAt.js.map +1 -1
  226. package/dist/features/text/useSceneTextEdit.js +3 -3
  227. package/dist/features/text/useSceneTextEdit.js.map +1 -1
  228. package/dist/index.d.ts +225 -73
  229. package/dist/index.js +15 -5
  230. package/dist/interactions/actions/admitsChild.js +11 -0
  231. package/dist/interactions/actions/admitsChild.js.map +1 -0
  232. package/dist/interactions/actions/defaults/contextMenu.js +16 -3
  233. package/dist/interactions/actions/defaults/contextMenu.js.map +1 -1
  234. package/dist/interactions/actions/defaults/createPaintAction.js +4 -1
  235. package/dist/interactions/actions/defaults/createPaintAction.js.map +1 -1
  236. package/dist/interactions/actions/defaults/enterGroup.js +88 -0
  237. package/dist/interactions/actions/defaults/enterGroup.js.map +1 -0
  238. package/dist/interactions/actions/defaults/enterPathEdit.js +1 -1
  239. package/dist/interactions/actions/defaults/enterPathEdit.js.map +1 -1
  240. package/dist/interactions/actions/defaults/enterTextEdit.js +1 -1
  241. package/dist/interactions/actions/defaults/enterTextEdit.js.map +1 -1
  242. package/dist/interactions/actions/defaults/group.js +7 -7
  243. package/dist/interactions/actions/defaults/group.js.map +1 -1
  244. package/dist/interactions/actions/defaults/selectAll.js +7 -3
  245. package/dist/interactions/actions/defaults/selectAll.js.map +1 -1
  246. package/dist/interactions/actions/defaults/selectDeep.js +35 -0
  247. package/dist/interactions/actions/defaults/selectDeep.js.map +1 -0
  248. package/dist/interactions/actions/drawnBoxes.js +2 -1
  249. package/dist/interactions/actions/drawnBoxes.js.map +1 -1
  250. package/dist/interactions/actions/insert/action.js +1 -0
  251. package/dist/interactions/actions/insert/action.js.map +1 -1
  252. package/dist/interactions/actions/insert/parent.js +7 -5
  253. package/dist/interactions/actions/insert/parent.js.map +1 -1
  254. package/dist/interactions/actions/insert/placeAction.js +3 -2
  255. package/dist/interactions/actions/insert/placeAction.js.map +1 -1
  256. package/dist/interactions/actions/insert/session.js +2 -2
  257. package/dist/interactions/actions/insert/session.js.map +1 -1
  258. package/dist/interactions/actions/move/commit/reparent.js +1 -1
  259. package/dist/interactions/actions/move/commit/reparent.js.map +1 -1
  260. package/dist/interactions/actions/move/gestureAdapter.js +6 -7
  261. package/dist/interactions/actions/move/gestureAdapter.js.map +1 -1
  262. package/dist/interactions/actions/move/layout/exits.js +1 -1
  263. package/dist/interactions/actions/move/layout/exits.js.map +1 -1
  264. package/dist/interactions/actions/move/session.js +4 -4
  265. package/dist/interactions/actions/move/session.js.map +1 -1
  266. package/dist/interactions/actions/pathCommands/pathNodes.js +14 -5
  267. package/dist/interactions/actions/pathCommands/pathNodes.js.map +1 -1
  268. package/dist/interactions/gestures/thresholdDrag/thresholdDrag.js +14 -2
  269. package/dist/interactions/gestures/thresholdDrag/thresholdDrag.js.map +1 -1
  270. package/dist/{math-B56zxVs5.d.ts → math-C_elShP_.d.ts} +2 -5
  271. package/dist/math.d.ts +1 -1
  272. package/dist/move.d.ts +4 -4
  273. package/dist/{options-DBxEryYq.d.ts → options-ClEm3SEl.d.ts} +1 -1
  274. package/dist/resize.d.ts +4 -4
  275. package/dist/test-seams.d.ts +6 -6
  276. package/dist/tools/builtin/pressTarget.js +22 -0
  277. package/dist/tools/builtin/pressTarget.js.map +1 -0
  278. package/dist/tools/builtin/select/adapterPointQuery.js +3 -3
  279. package/dist/tools/builtin/select/adapterPointQuery.js.map +1 -1
  280. package/dist/tools/builtin/select/collapseDeferred.js +40 -0
  281. package/dist/tools/builtin/select/collapseDeferred.js.map +1 -0
  282. package/dist/tools/builtin/select/groupEntry.js +82 -0
  283. package/dist/tools/builtin/select/groupEntry.js.map +1 -0
  284. package/dist/tools/builtin/select/pressDecision.js +1 -1
  285. package/dist/tools/builtin/select/pressDecision.js.map +1 -1
  286. package/dist/tools/builtin/select/useSelectTool.js +61 -45
  287. package/dist/tools/builtin/select/useSelectTool.js.map +1 -1
  288. package/dist/{types-7puAuVD2.d.ts → types-49uYJN3j.d.ts} +1 -1
  289. package/dist/{types-CniVctmk.d.ts → types-BEdRIhNc.d.ts} +272 -109
  290. package/dist/{types-oFeQ5VjV.d.ts → types-p2alu-R1.d.ts} +1 -1
  291. package/dist/version.js +1 -1
  292. package/package.json +25 -25
@@ -1,11 +1,11 @@
1
1
  import { dropPoseKeyedMemoSlots } from "./nodeMemo.js";
2
+ import { shownCopyOf } from "./nodeAsShown.js";
2
3
  import { createPoseHold } from "./poseHold.js";
4
+ import { foldPlies } from "./posePlies.js";
5
+ import { createPlyStacks, plyPath } from "./plyStacks.js";
3
6
  import { createEmitter } from "@weasel-js/registry";
4
- import { kit, last, mix, mul, patch } from "@msb235/blits";
5
7
 
6
8
  //#region src/core/scene/poseOverrides.ts
7
- /** What the table's voice says about a node it holds no entry for. */
8
- var NOTHING = Object.freeze({});
9
9
  /**
10
10
  * Build a {@link PoseOverrides} map.
11
11
  *
@@ -13,53 +13,48 @@ var NOTHING = Object.freeze({});
13
13
  * `onInvalidate` lets the scene invalidate derived nodes from the same
14
14
  * chokepoint.
15
15
  *
16
- * What `read` answers is a blits mix folded per node. The entries a writer
17
- * `set`s are one voice on it, and a node holding no entry is never probed.
16
+ * What `read` answers is folded from the node's plies by `foldPlies`, its
17
+ * path by `plyPath`; a node holding no entry is never probed.
18
18
  */
19
19
  function createPoseOverrides(getNode, onInvalidate) {
20
- const entries = /* @__PURE__ */ new Map();
20
+ const stacks = createPlyStacks();
21
+ const { plies } = stacks;
22
+ const fold = foldPlies(plies);
21
23
  const listeners = createEmitter();
22
24
  let generation = 0;
23
- const folded = mix(kit({
24
- pose: last(),
25
- alpha: mul(),
26
- path: last()
27
- }));
28
- folded.cue({ patch: patch(0, (_phase, id) => entries.get(id) ?? NOTHING, { writes: [
29
- "pose",
30
- "alpha",
31
- "path"
32
- ] }) });
33
- let clock = 0;
34
- folded.sync(clock);
35
- /** Per node: the object `read` folds into and returns, and the generation
36
- * it was last folded at. */
25
+ /** Per node: the object `read` folds into and returns, the generation it
26
+ * was last folded at, and the last path it carried. */
37
27
  const reads = /* @__PURE__ */ new Map();
38
28
  function invalidate(id) {
39
29
  const node = getNode(id);
40
- if (node) dropPoseKeyedMemoSlots(node);
30
+ if (node) {
31
+ dropPoseKeyedMemoSlots(node);
32
+ const shown = shownCopyOf(node);
33
+ if (shown) dropPoseKeyedMemoSlots(shown);
34
+ }
41
35
  onInvalidate?.(id);
42
36
  }
43
37
  function published() {
44
38
  generation++;
45
- folded.sync(++clock);
39
+ fold.publish(stacks.depth());
46
40
  listeners.emit();
47
41
  }
48
42
  function forget(id) {
49
43
  reads.delete(id);
50
- folded.drop(id);
44
+ fold.drop(id);
51
45
  }
52
46
  const table = {
53
47
  set(id, entry) {
54
- entries.set(id, entry);
48
+ stacks.claim(id, entry);
55
49
  invalidate(id);
56
50
  published();
57
51
  },
58
52
  get(id) {
59
- return entries.get(id);
53
+ return plies.get(id)?.at(-1);
60
54
  },
61
55
  read(id) {
62
- if (!entries.has(id)) return void 0;
56
+ const stack = plies.get(id);
57
+ if (stack === void 0) return void 0;
63
58
  let held = reads.get(id);
64
59
  if (held === void 0) {
65
60
  held = {
@@ -69,41 +64,43 @@ function createPoseOverrides(getNode, onInvalidate) {
69
64
  reads.set(id, held);
70
65
  }
71
66
  if (held.at !== generation) {
72
- folded.probe(id, held.out);
67
+ fold.probe(id, held.out);
68
+ held.out.path = plyPath(stack, held.out.pose, held);
73
69
  held.at = generation;
74
70
  }
75
71
  return held.out;
76
72
  },
77
- has(id) {
78
- return entries.has(id);
73
+ has(id, entry) {
74
+ const stack = plies.get(id);
75
+ return stack !== void 0 && (entry === void 0 || stack.includes(entry));
79
76
  },
80
77
  ids() {
81
- return [...entries.keys()];
78
+ return [...plies.keys()];
82
79
  },
83
- clear(id) {
84
- if (!entries.delete(id)) return;
85
- forget(id);
80
+ clear(id, entry) {
81
+ if (!(entry === void 0 ? stacks.drop(id) : stacks.release(id, entry))) return;
82
+ if (!plies.has(id)) forget(id);
86
83
  invalidate(id);
87
84
  published();
88
85
  },
89
86
  clearAll() {
90
- if (entries.size === 0) return;
91
- for (const id of entries.keys()) {
87
+ if (plies.size === 0) return;
88
+ for (const id of plies.keys()) {
92
89
  forget(id);
93
90
  invalidate(id);
94
91
  }
95
- entries.clear();
92
+ stacks.dropAll();
96
93
  published();
97
94
  },
98
95
  commit() {
99
- for (const id of entries.keys()) invalidate(id);
96
+ for (const id of plies.keys()) invalidate(id);
100
97
  published();
101
98
  },
102
99
  subscribe: listeners.subscribe,
103
100
  getGeneration() {
104
101
  return generation;
105
102
  },
106
- hold: () => createPoseHold(table)
103
+ hold: () => createPoseHold(table, (id) => plies.get(id))
107
104
  };
108
105
  return table;
109
106
  }
@@ -1 +1 @@
1
- {"version":3,"file":"poseOverrides.js","names":[],"sources":["../../../src/core/scene/poseOverrides.ts"],"sourcesContent":["import type { Path } from '@weasel-js/geom';\nimport { createEmitter } from '@weasel-js/registry';\nimport { kit, last, mix, mul, patch } from '@msb235/blits';\nimport { dropPoseKeyedMemoSlots } from './nodeMemo';\nimport { createPoseHold } from './poseHold';\nimport type { NodeId, PoseOverride, PoseOverrides } from './types';\n\n/** What the table's voice says about a node it holds no entry for. */\nconst NOTHING: PoseOverride<never> = Object.freeze({});\n\n/**\n * Build a {@link PoseOverrides} map.\n *\n * `getNode` resolves an id to the node object the painter memo is keyed on.\n * `onInvalidate` lets the scene invalidate derived nodes from the same\n * chokepoint.\n *\n * What `read` answers is a blits mix folded per node. The entries a writer\n * `set`s are one voice on it, and a node holding no entry is never probed.\n */\nexport function createPoseOverrides<TPose>(\n getNode: (id: NodeId) => { data?: unknown } | undefined,\n onInvalidate?: (id: NodeId) => void,\n): PoseOverrides<TPose> {\n const entries = new Map<NodeId, PoseOverride<TPose>>();\n const listeners = createEmitter();\n let generation = 0;\n\n const folded = mix<NodeId, PoseOverride<TPose>>(\n kit<PoseOverride<TPose>>({ pose: last<NonNullable<TPose>>(), alpha: mul(), path: last<Path>() }),\n );\n folded.cue({\n patch: patch<NodeId, PoseOverride<TPose>>(\n 0,\n (_phase, id) => entries.get(id) ?? (NOTHING as PoseOverride<TPose>),\n { writes: ['pose', 'alpha', 'path'] },\n ),\n });\n // Mix time only has to move between publishes, so a probe after one never\n // hands back the delta a probe before it cached.\n let clock = 0;\n folded.sync(clock);\n\n /** Per node: the object `read` folds into and returns, and the generation\n * it was last folded at. */\n const reads = new Map<NodeId, { out: PoseOverride<TPose>; at: number }>();\n\n function invalidate(id: NodeId): void {\n const node = getNode(id);\n if (node) dropPoseKeyedMemoSlots(node);\n onInvalidate?.(id);\n }\n\n function published(): void {\n generation++;\n folded.sync(++clock);\n listeners.emit();\n }\n\n function forget(id: NodeId): void {\n reads.delete(id);\n folded.drop(id);\n }\n\n const table: PoseOverrides<TPose> = {\n set(id, entry) {\n entries.set(id, entry);\n invalidate(id);\n published();\n },\n get(id) {\n return entries.get(id);\n },\n read(id) {\n if (!entries.has(id)) return undefined;\n let held = reads.get(id);\n if (held === undefined) {\n held = { out: {}, at: -1 };\n reads.set(id, held);\n }\n if (held.at !== generation) {\n folded.probe(id, held.out);\n held.at = generation;\n }\n return held.out;\n },\n has(id) {\n return entries.has(id);\n },\n ids() {\n return [...entries.keys()];\n },\n clear(id) {\n if (!entries.delete(id)) return;\n forget(id);\n invalidate(id);\n published();\n },\n clearAll() {\n if (entries.size === 0) return;\n for (const id of entries.keys()) {\n forget(id);\n invalidate(id);\n }\n entries.clear();\n published();\n },\n commit() {\n for (const id of entries.keys()) invalidate(id);\n published();\n },\n subscribe: listeners.subscribe,\n getGeneration() {\n return generation;\n },\n hold: () => createPoseHold(table),\n };\n return table;\n}\n"],"mappings":";;;;;;;AAQA,IAAM,UAA+B,OAAO,OAAO,CAAC,CAAC;;;;;;;;;;;AAYrD,SAAgB,oBACd,SACA,cACsB;CACtB,MAAM,0BAAU,IAAI,IAAiC;CACrD,MAAM,YAAY,cAAc;CAChC,IAAI,aAAa;CAEjB,MAAM,SAAS,IACb,IAAyB;EAAE,MAAM,KAAyB;EAAG,OAAO,IAAI;EAAG,MAAM,KAAW;CAAE,CAAC,CACjG;CACA,OAAO,IAAI,EACT,OAAO,MACL,IACC,QAAQ,OAAO,QAAQ,IAAI,EAAE,KAAM,SACpC,EAAE,QAAQ;EAAC;EAAQ;EAAS;CAAM,EAAE,CACtC,EACF,CAAC;CAGD,IAAI,QAAQ;CACZ,OAAO,KAAK,KAAK;;;CAIjB,MAAM,wBAAQ,IAAI,IAAsD;CAExE,SAAS,WAAW,IAAkB;EACpC,MAAM,OAAO,QAAQ,EAAE;EACvB,IAAI,MAAM,uBAAuB,IAAI;EACrC,eAAe,EAAE;CACnB;CAEA,SAAS,YAAkB;EACzB;EACA,OAAO,KAAK,EAAE,KAAK;EACnB,UAAU,KAAK;CACjB;CAEA,SAAS,OAAO,IAAkB;EAChC,MAAM,OAAO,EAAE;EACf,OAAO,KAAK,EAAE;CAChB;CAEA,MAAM,QAA8B;EAClC,IAAI,IAAI,OAAO;GACb,QAAQ,IAAI,IAAI,KAAK;GACrB,WAAW,EAAE;GACb,UAAU;EACZ;EACA,IAAI,IAAI;GACN,OAAO,QAAQ,IAAI,EAAE;EACvB;EACA,KAAK,IAAI;GACP,IAAI,CAAC,QAAQ,IAAI,EAAE,GAAG,OAAO;GAC7B,IAAI,OAAO,MAAM,IAAI,EAAE;GACvB,IAAI,SAAS,QAAW;IACtB,OAAO;KAAE,KAAK,CAAC;KAAG,IAAI;IAAG;IACzB,MAAM,IAAI,IAAI,IAAI;GACpB;GACA,IAAI,KAAK,OAAO,YAAY;IAC1B,OAAO,MAAM,IAAI,KAAK,GAAG;IACzB,KAAK,KAAK;GACZ;GACA,OAAO,KAAK;EACd;EACA,IAAI,IAAI;GACN,OAAO,QAAQ,IAAI,EAAE;EACvB;EACA,MAAM;GACJ,OAAO,CAAC,GAAG,QAAQ,KAAK,CAAC;EAC3B;EACA,MAAM,IAAI;GACR,IAAI,CAAC,QAAQ,OAAO,EAAE,GAAG;GACzB,OAAO,EAAE;GACT,WAAW,EAAE;GACb,UAAU;EACZ;EACA,WAAW;GACT,IAAI,QAAQ,SAAS,GAAG;GACxB,KAAK,MAAM,MAAM,QAAQ,KAAK,GAAG;IAC/B,OAAO,EAAE;IACT,WAAW,EAAE;GACf;GACA,QAAQ,MAAM;GACd,UAAU;EACZ;EACA,SAAS;GACP,KAAK,MAAM,MAAM,QAAQ,KAAK,GAAG,WAAW,EAAE;GAC9C,UAAU;EACZ;EACA,WAAW,UAAU;EACrB,gBAAgB;GACd,OAAO;EACT;EACA,YAAY,eAAe,KAAK;CAClC;CACA,OAAO;AACT"}
1
+ {"version":3,"file":"poseOverrides.js","names":[],"sources":["../../../src/core/scene/poseOverrides.ts"],"sourcesContent":["import { createEmitter } from '@weasel-js/registry';\nimport { dropPoseKeyedMemoSlots } from './nodeMemo';\nimport { shownCopyOf } from './nodeAsShown';\nimport { createPoseHold } from './poseHold';\nimport { foldPlies } from './posePlies';\nimport { createPlyStacks, plyPath, type CarriedPath } from './plyStacks';\nimport type { NodeId, PoseOverride, PoseOverrides } from './types';\n\n/**\n * Build a {@link PoseOverrides} map.\n *\n * `getNode` resolves an id to the node object the painter memo is keyed on.\n * `onInvalidate` lets the scene invalidate derived nodes from the same\n * chokepoint.\n *\n * What `read` answers is folded from the node's plies by `foldPlies`, its\n * path by `plyPath`; a node holding no entry is never probed.\n */\nexport function createPoseOverrides<TPose, TData = unknown>(\n getNode: (id: NodeId) => { data?: unknown } | undefined,\n onInvalidate?: (id: NodeId) => void,\n): PoseOverrides<TPose, TData> {\n type Entry = PoseOverride<TPose, TData>;\n const stacks = createPlyStacks<Entry>();\n const { plies } = stacks;\n const fold = foldPlies(plies);\n const listeners = createEmitter();\n let generation = 0;\n\n /** Per node: the object `read` folds into and returns, the generation it\n * was last folded at, and the last path it carried. */\n const reads = new Map<NodeId, { out: Entry; at: number; carried?: CarriedPath }>();\n\n function invalidate(id: NodeId): void {\n const node = getNode(id);\n if (node) {\n dropPoseKeyedMemoSlots(node);\n // A painter handed the node as it shows memoized against that copy.\n const shown = shownCopyOf(node);\n if (shown) dropPoseKeyedMemoSlots(shown);\n }\n onInvalidate?.(id);\n }\n\n function published(): void {\n generation++;\n fold.publish(stacks.depth());\n listeners.emit();\n }\n\n function forget(id: NodeId): void {\n reads.delete(id);\n fold.drop(id);\n }\n\n const table: PoseOverrides<TPose, TData> = {\n set(id, entry) {\n stacks.claim(id, entry);\n invalidate(id);\n published();\n },\n get(id) {\n return plies.get(id)?.at(-1);\n },\n read(id) {\n const stack = plies.get(id);\n if (stack === undefined) return undefined;\n let held = reads.get(id);\n if (held === undefined) {\n held = { out: {}, at: -1 };\n reads.set(id, held);\n }\n if (held.at !== generation) {\n fold.probe(id, held.out);\n held.out.path = plyPath(stack, held.out.pose, held);\n held.at = generation;\n }\n return held.out;\n },\n has(id, entry) {\n const stack = plies.get(id);\n return stack !== undefined && (entry === undefined || stack.includes(entry));\n },\n ids() {\n return [...plies.keys()];\n },\n clear(id, entry) {\n if (!(entry === undefined ? stacks.drop(id) : stacks.release(id, entry))) return;\n if (!plies.has(id)) forget(id);\n invalidate(id);\n published();\n },\n clearAll() {\n if (plies.size === 0) return;\n for (const id of plies.keys()) {\n forget(id);\n invalidate(id);\n }\n stacks.dropAll();\n published();\n },\n commit() {\n for (const id of plies.keys()) invalidate(id);\n published();\n },\n subscribe: listeners.subscribe,\n getGeneration() {\n return generation;\n },\n hold: () => createPoseHold(table, (id) => plies.get(id)),\n };\n return table;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAkBA,SAAgB,oBACd,SACA,cAC6B;CAE7B,MAAM,SAAS,gBAAuB;CACtC,MAAM,EAAE,UAAU;CAClB,MAAM,OAAO,UAAU,KAAK;CAC5B,MAAM,YAAY,cAAc;CAChC,IAAI,aAAa;;;CAIjB,MAAM,wBAAQ,IAAI,IAA+D;CAEjF,SAAS,WAAW,IAAkB;EACpC,MAAM,OAAO,QAAQ,EAAE;EACvB,IAAI,MAAM;GACR,uBAAuB,IAAI;GAE3B,MAAM,QAAQ,YAAY,IAAI;GAC9B,IAAI,OAAO,uBAAuB,KAAK;EACzC;EACA,eAAe,EAAE;CACnB;CAEA,SAAS,YAAkB;EACzB;EACA,KAAK,QAAQ,OAAO,MAAM,CAAC;EAC3B,UAAU,KAAK;CACjB;CAEA,SAAS,OAAO,IAAkB;EAChC,MAAM,OAAO,EAAE;EACf,KAAK,KAAK,EAAE;CACd;CAEA,MAAM,QAAqC;EACzC,IAAI,IAAI,OAAO;GACb,OAAO,MAAM,IAAI,KAAK;GACtB,WAAW,EAAE;GACb,UAAU;EACZ;EACA,IAAI,IAAI;GACN,OAAO,MAAM,IAAI,EAAE,CAAC,EAAE,GAAG,EAAE;EAC7B;EACA,KAAK,IAAI;GACP,MAAM,QAAQ,MAAM,IAAI,EAAE;GAC1B,IAAI,UAAU,QAAW,OAAO;GAChC,IAAI,OAAO,MAAM,IAAI,EAAE;GACvB,IAAI,SAAS,QAAW;IACtB,OAAO;KAAE,KAAK,CAAC;KAAG,IAAI;IAAG;IACzB,MAAM,IAAI,IAAI,IAAI;GACpB;GACA,IAAI,KAAK,OAAO,YAAY;IAC1B,KAAK,MAAM,IAAI,KAAK,GAAG;IACvB,KAAK,IAAI,OAAO,QAAQ,OAAO,KAAK,IAAI,MAAM,IAAI;IAClD,KAAK,KAAK;GACZ;GACA,OAAO,KAAK;EACd;EACA,IAAI,IAAI,OAAO;GACb,MAAM,QAAQ,MAAM,IAAI,EAAE;GAC1B,OAAO,UAAU,WAAc,UAAU,UAAa,MAAM,SAAS,KAAK;EAC5E;EACA,MAAM;GACJ,OAAO,CAAC,GAAG,MAAM,KAAK,CAAC;EACzB;EACA,MAAM,IAAI,OAAO;GACf,IAAI,EAAE,UAAU,SAAY,OAAO,KAAK,EAAE,IAAI,OAAO,QAAQ,IAAI,KAAK,IAAI;GAC1E,IAAI,CAAC,MAAM,IAAI,EAAE,GAAG,OAAO,EAAE;GAC7B,WAAW,EAAE;GACb,UAAU;EACZ;EACA,WAAW;GACT,IAAI,MAAM,SAAS,GAAG;GACtB,KAAK,MAAM,MAAM,MAAM,KAAK,GAAG;IAC7B,OAAO,EAAE;IACT,WAAW,EAAE;GACf;GACA,OAAO,QAAQ;GACf,UAAU;EACZ;EACA,SAAS;GACP,KAAK,MAAM,MAAM,MAAM,KAAK,GAAG,WAAW,EAAE;GAC5C,UAAU;EACZ;EACA,WAAW,UAAU;EACrB,gBAAgB;GACd,OAAO;EACT;EACA,YAAY,eAAe,QAAQ,OAAO,MAAM,IAAI,EAAE,CAAC;CACzD;CACA,OAAO;AACT"}
@@ -0,0 +1,59 @@
1
+ import { kit, last, mix, mul, patch } from "@msb235/blits";
2
+
3
+ //#region src/core/scene/posePlies.ts
4
+ /** What a ply says about a node with fewer plies than its depth. */
5
+ var NOTHING = Object.freeze({});
6
+ /** Per lower function, per upper one, the two run in turn: kept, so a node
7
+ * shows one function while neither ply replaces its own. */
8
+ var composed = /* @__PURE__ */ new WeakMap();
9
+ function after(lower, upper) {
10
+ let byUpper = composed.get(lower);
11
+ if (byUpper === void 0) composed.set(lower, byUpper = /* @__PURE__ */ new WeakMap());
12
+ let both = byUpper.get(upper);
13
+ if (both === void 0) byUpper.set(upper, both = (stored) => upper(lower(stored)));
14
+ return both;
15
+ }
16
+ /** A later ply's data runs on what the plies under it show. */
17
+ var DATA = {
18
+ merge: after,
19
+ lerp: (a, b, u) => u < .5 ? a : b
20
+ };
21
+ /** A fold over `plies`, each node's entries bottom first. */
22
+ function foldPlies(plies) {
23
+ const folded = mix(kit({
24
+ pose: last(),
25
+ alpha: mul(),
26
+ path: last(),
27
+ data: DATA
28
+ }));
29
+ /** One voice per depth, bottom first; only ever cut from the top, so cue order stays depth order. */
30
+ const voices = [];
31
+ let clock = 0;
32
+ const publish = (depth) => {
33
+ while (voices.length > depth) voices.pop().fade({ over: 0 });
34
+ while (voices.length < depth) {
35
+ const k = voices.length;
36
+ voices.push(folded.cue({ patch: patch(0, (_phase, id) => plies.get(id)?.[k] ?? NOTHING, { writes: [
37
+ "pose",
38
+ "alpha",
39
+ "path",
40
+ "data"
41
+ ] }) }));
42
+ }
43
+ folded.sync(clock++);
44
+ };
45
+ publish(1);
46
+ return {
47
+ probe: (id, out) => {
48
+ folded.probe(id, out);
49
+ },
50
+ publish,
51
+ drop: (id) => {
52
+ folded.drop(id);
53
+ }
54
+ };
55
+ }
56
+
57
+ //#endregion
58
+ export { foldPlies };
59
+ //# sourceMappingURL=posePlies.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"posePlies.js","names":[],"sources":["../../../src/core/scene/posePlies.ts"],"sourcesContent":["/**\n * The fold behind `PoseOverrides.read`: a blits mix over each node's plies of\n * overrides, one voice per depth, cued bottom first, so a later ply folds\n * over an earlier one.\n */\nimport type { Path } from '@weasel-js/geom';\nimport { kit, last, mix, mul, patch, type Channel, type Handle } from '@msb235/blits';\nimport type { NodeId, PoseOverride } from './types';\n\n/** What a ply says about a node with fewer plies than its depth. */\nconst NOTHING: PoseOverride<never> = Object.freeze({});\n\ntype ShowData = (stored: never) => unknown;\n\n/** Per lower function, per upper one, the two run in turn: kept, so a node\n * shows one function while neither ply replaces its own. */\nconst composed = new WeakMap<ShowData, WeakMap<ShowData, ShowData>>();\n\nfunction after(lower: ShowData, upper: ShowData): ShowData {\n let byUpper = composed.get(lower);\n if (byUpper === undefined) composed.set(lower, byUpper = new WeakMap());\n let both = byUpper.get(upper);\n if (both === undefined) byUpper.set(upper, both = (stored) => upper(lower(stored) as never));\n return both;\n}\n\n/** A later ply's data runs on what the plies under it show. */\nconst DATA: Channel<ShowData> = { merge: after, lerp: (a, b, u) => (u < 0.5 ? a : b) };\n\n/** {@link foldPlies}'s handle. */\nexport interface PlyFold<Entry> {\n /** Fold `id`'s plies into `out`, as of the last `publish`. */\n probe(id: NodeId, out: Entry): void;\n /** Make the next probe of every node fold afresh, `depth` plies deep. */\n publish(depth: number): void;\n /** Forget what the fold keeps for `id`. */\n drop(id: NodeId): void;\n}\n\n/** A fold over `plies`, each node's entries bottom first. */\nexport function foldPlies<TPose, TData>(\n plies: ReadonlyMap<NodeId, readonly PoseOverride<TPose, TData>[]>,\n): PlyFold<PoseOverride<TPose, TData>> {\n type Entry = PoseOverride<TPose, TData>;\n const folded = mix<NodeId, Entry>(\n kit<Entry>({\n pose: last<NonNullable<TPose>>(),\n alpha: mul(),\n path: last<Path>(),\n data: DATA as unknown as Channel<NonNullable<Entry['data']>>,\n }),\n );\n /** One voice per depth, bottom first; only ever cut from the top, so cue order stays depth order. */\n const voices: Handle<NodeId>[] = [];\n // Mix time only has to move between publishes, so a probe after one never\n // hands back the delta a probe before it cached.\n let clock = 0;\n const publish = (depth: number): void => {\n while (voices.length > depth) voices.pop()!.fade({ over: 0 });\n while (voices.length < depth) {\n const k = voices.length;\n voices.push(folded.cue({\n patch: patch<NodeId, Entry>(\n 0,\n (_phase, id) => plies.get(id)?.[k] ?? (NOTHING as Entry),\n { writes: ['pose', 'alpha', 'path', 'data'] },\n ),\n }));\n }\n folded.sync(clock++);\n };\n publish(1);\n return {\n probe: (id, out) => { folded.probe(id, out); },\n publish,\n drop: (id) => { folded.drop(id); },\n };\n}\n"],"mappings":";;;;AAUA,IAAM,UAA+B,OAAO,OAAO,CAAC,CAAC;;;AAMrD,IAAM,2BAAW,IAAI,QAA+C;AAEpE,SAAS,MAAM,OAAiB,OAA2B;CACzD,IAAI,UAAU,SAAS,IAAI,KAAK;CAChC,IAAI,YAAY,QAAW,SAAS,IAAI,OAAO,0BAAU,IAAI,QAAQ,CAAC;CACtE,IAAI,OAAO,QAAQ,IAAI,KAAK;CAC5B,IAAI,SAAS,QAAW,QAAQ,IAAI,OAAO,QAAQ,WAAW,MAAM,MAAM,MAAM,CAAU,CAAC;CAC3F,OAAO;AACT;;AAGA,IAAM,OAA0B;CAAE,OAAO;CAAO,OAAO,GAAG,GAAG,MAAO,IAAI,KAAM,IAAI;AAAG;;AAarF,SAAgB,UACd,OACqC;CAErC,MAAM,SAAS,IACb,IAAW;EACT,MAAM,KAAyB;EAC/B,OAAO,IAAI;EACX,MAAM,KAAW;EACjB,MAAM;CACR,CAAC,CACH;;CAEA,MAAM,SAA2B,CAAC;CAGlC,IAAI,QAAQ;CACZ,MAAM,WAAW,UAAwB;EACvC,OAAO,OAAO,SAAS,OAAO,OAAO,IAAI,CAAC,CAAE,KAAK,EAAE,MAAM,EAAE,CAAC;EAC5D,OAAO,OAAO,SAAS,OAAO;GAC5B,MAAM,IAAI,OAAO;GACjB,OAAO,KAAK,OAAO,IAAI,EACrB,OAAO,MACL,IACC,QAAQ,OAAO,MAAM,IAAI,EAAE,CAAC,GAAG,MAAO,SACvC,EAAE,QAAQ;IAAC;IAAQ;IAAS;IAAQ;GAAM,EAAE,CAC9C,EACF,CAAC,CAAC;EACJ;EACA,OAAO,KAAK,OAAO;CACrB;CACA,QAAQ,CAAC;CACT,OAAO;EACL,QAAQ,IAAI,QAAQ;GAAE,OAAO,MAAM,IAAI,GAAG;EAAG;EAC7C;EACA,OAAO,OAAO;GAAE,OAAO,KAAK,EAAE;EAAG;CACnC;AACF"}
@@ -1,4 +1,4 @@
1
- import { isPlainObject } from "../isPlainObject.js";
1
+ import { sameValue } from "./sameValue.js";
2
2
 
3
3
  //#region src/core/scene/reconcileSpecs.ts
4
4
  /**
@@ -39,10 +39,11 @@ function reconcileSpecs(scene, prev, next) {
39
39
  const parent = spec.parent ?? null;
40
40
  if (parent !== (before.parent ?? null)) scene.move(id, parent);
41
41
  if (spec.layer !== before.layer) scene.setLayer(id, spec.layer);
42
- if (!same(spec.data, before.data)) scene.update(id, { data: spec.data });
43
- if (!same(spec.dependsOn, before.dependsOn)) scene.setDependsOn(id, spec.dependsOn);
44
- if (spec.kind === "container" && !same(spec.units, before.units)) scene.setUnits(id, spec.units ?? null);
45
- if (!same(spec.pose, before.pose)) scene.setPose(id, spec.pose);
42
+ if (!sameValue(spec.data, before.data)) scene.update(id, { data: spec.data });
43
+ if (!sameValue(spec.dependsOn, before.dependsOn)) scene.setDependsOn(id, spec.dependsOn);
44
+ if (!sameValue(spec.units, before.units)) scene.setUnits(id, spec.units ?? null);
45
+ if (spec.clip !== before.clip) scene.setClip(id, spec.clip ?? null);
46
+ if (!sameValue(spec.pose, before.pose)) scene.setPose(id, spec.pose);
46
47
  if (spec.hidden === true !== (before.hidden === true)) scene.setHidden(id, spec.hidden === true);
47
48
  if (spec.locked === true && before.locked !== true) scene.setLocked(id, true);
48
49
  }
@@ -61,16 +62,6 @@ function byId(specs) {
61
62
  function needsReplace(a, b) {
62
63
  return a.kind !== b.kind || a.pickable !== b.pickable || a.derivePath !== b.derivePath || a.derivePose !== b.derivePose || a.clipFromPose !== b.clipFromPose || a.copiesFromPose !== b.copiesFromPose || a.layout !== b.layout;
63
64
  }
64
- /** Structural equality over plain objects and arrays; anything else by identity. */
65
- function same(a, b) {
66
- if (Object.is(a, b)) return true;
67
- if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((v, i) => same(v, b[i]));
68
- if (isPlainObject(a) && isPlainObject(b)) {
69
- const ka = Object.keys(a);
70
- return ka.length === Object.keys(b).length && ka.every((k) => k in b && same(a[k], b[k]));
71
- }
72
- return false;
73
- }
74
65
 
75
66
  //#endregion
76
67
  export { reconcileSpecs };
@@ -1 +1 @@
1
- {"version":3,"file":"reconcileSpecs.js","names":[],"sources":["../../../src/core/scene/reconcileSpecs.ts"],"sourcesContent":["import { isPlainObject } from '../isPlainObject';\nimport type { AddNodeSpec, NodeId, Scene } from './types';\n\n/**\n * Bring `scene` from `prev` to `next`: add what `next` gained, remove what it\n * lost, and write only the fields that changed between the two.\n *\n * The diff is spec against spec, not spec against scene, so whatever the scene\n * did since `prev` — a node dragged, an edge authored by a gesture — survives\n * unless `next` changes that same field. The writes are untracked: syncing to\n * outside data is not an undo step.\n *\n * Every spec needs an `id`, and `next` must be in an order `createScene` would\n * accept — parents and dependencies before what names them. A node whose\n * `kind` or derive functions change is removed and added again, along with\n * everything its removal takes with it, and stays selected if it was.\n */\nexport function reconcileSpecs<TData, TLayer extends string, TPose>(\n scene: Scene<TData, TLayer, TPose>,\n prev: readonly AddNodeSpec<TData, TLayer, TPose>[],\n next: readonly AddNodeSpec<TData, TLayer, TPose>[],\n): void {\n const prevById = byId(prev);\n const nextById = byId(next);\n const gone: NodeId[] = [];\n for (const [id, before] of prevById) {\n const after = nextById.get(id);\n if (after === undefined || needsReplace(before, after)) gone.push(id);\n }\n const removing = scene.removalClosure(gone.filter((id) => scene.get(id) !== undefined));\n const removed = new Set(removing);\n\n // A replaced node is the same node to whoever selected it.\n const selected = scene.getSelection();\n\n scene.untracked(() => {\n scene.removeMany(removing);\n for (const spec of next) {\n const id = spec.id!;\n const before = prevById.get(id);\n if (scene.get(id) === undefined || before === undefined || removed.has(id)) {\n scene.add(spec);\n continue;\n }\n // An unlock goes first and a lock last, so the writes between can land.\n if (before.locked === true && spec.locked !== true) scene.setLocked(id, false);\n const parent = spec.parent ?? null;\n if (parent !== (before.parent ?? null)) scene.move(id, parent);\n if (spec.layer !== before.layer) scene.setLayer(id, spec.layer);\n if (!same(spec.data, before.data)) scene.update(id, { data: spec.data });\n if (!same(spec.dependsOn, before.dependsOn)) scene.setDependsOn(id, spec.dependsOn);\n if (spec.kind === 'container' && !same(spec.units, before.units)) scene.setUnits(id, spec.units ?? null);\n if (!same(spec.pose, before.pose)) scene.setPose(id, spec.pose);\n if ((spec.hidden === true) !== (before.hidden === true)) scene.setHidden(id, spec.hidden === true);\n if (spec.locked === true && before.locked !== true) scene.setLocked(id, true);\n }\n if (selected.some((id) => removed.has(id))) {\n scene.setSelection(selected.filter((id) => scene.get(id) !== undefined));\n }\n });\n}\n\nfunction byId<T extends { id?: NodeId }>(specs: readonly T[]): Map<NodeId, T> {\n const out = new Map<NodeId, T>();\n for (const spec of specs) {\n if (spec.id === undefined) throw new Error('reconcileSpecs: every spec needs an id');\n if (out.has(spec.id)) throw new Error(`reconcileSpecs: id \"${spec.id}\" appears more than once`);\n out.set(spec.id, spec);\n }\n return out;\n}\n\nfunction needsReplace<TData, TLayer extends string, TPose>(\n a: AddNodeSpec<TData, TLayer, TPose>,\n b: AddNodeSpec<TData, TLayer, TPose>,\n): boolean {\n return a.kind !== b.kind\n || a.pickable !== b.pickable\n || a.derivePath !== b.derivePath\n || a.derivePose !== b.derivePose\n || a.clipFromPose !== b.clipFromPose\n || a.copiesFromPose !== b.copiesFromPose\n || a.layout !== b.layout;\n}\n\n/** Structural equality over plain objects and arrays; anything else by identity. */\nfunction same(a: unknown, b: unknown): boolean {\n if (Object.is(a, b)) return true;\n if (Array.isArray(a) && Array.isArray(b)) {\n return a.length === b.length && a.every((v, i) => same(v, b[i]));\n }\n if (isPlainObject(a) && isPlainObject(b)) {\n const ka = Object.keys(a);\n return ka.length === Object.keys(b).length && ka.every((k) => k in b && same(a[k], b[k]));\n }\n return false;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"reconcileSpecs.js","names":["same"],"sources":["../../../src/core/scene/reconcileSpecs.ts"],"sourcesContent":["import { sameValue as same } from './sameValue';\nimport type { AddNodeSpec, NodeId, Scene } from './types';\n\n/**\n * Bring `scene` from `prev` to `next`: add what `next` gained, remove what it\n * lost, and write only the fields that changed between the two.\n *\n * The diff is spec against spec, not spec against scene, so whatever the scene\n * did since `prev` — a node dragged, an edge authored by a gesture — survives\n * unless `next` changes that same field. The writes are untracked: syncing to\n * outside data is not an undo step.\n *\n * Every spec needs an `id`, and `next` must be in an order `createScene` would\n * accept — parents and dependencies before what names them. A node whose\n * `kind` or derive functions change is removed and added again, along with\n * everything its removal takes with it, and stays selected if it was.\n */\nexport function reconcileSpecs<TData, TLayer extends string, TPose>(\n scene: Scene<TData, TLayer, TPose>,\n prev: readonly AddNodeSpec<TData, TLayer, TPose>[],\n next: readonly AddNodeSpec<TData, TLayer, TPose>[],\n): void {\n const prevById = byId(prev);\n const nextById = byId(next);\n const gone: NodeId[] = [];\n for (const [id, before] of prevById) {\n const after = nextById.get(id);\n if (after === undefined || needsReplace(before, after)) gone.push(id);\n }\n const removing = scene.removalClosure(gone.filter((id) => scene.get(id) !== undefined));\n const removed = new Set(removing);\n\n // A replaced node is the same node to whoever selected it.\n const selected = scene.getSelection();\n\n scene.untracked(() => {\n scene.removeMany(removing);\n for (const spec of next) {\n const id = spec.id!;\n const before = prevById.get(id);\n if (scene.get(id) === undefined || before === undefined || removed.has(id)) {\n scene.add(spec);\n continue;\n }\n // An unlock goes first and a lock last, so the writes between can land.\n if (before.locked === true && spec.locked !== true) scene.setLocked(id, false);\n const parent = spec.parent ?? null;\n if (parent !== (before.parent ?? null)) scene.move(id, parent);\n if (spec.layer !== before.layer) scene.setLayer(id, spec.layer);\n if (!same(spec.data, before.data)) scene.update(id, { data: spec.data });\n if (!same(spec.dependsOn, before.dependsOn)) scene.setDependsOn(id, spec.dependsOn);\n if (!same(spec.units, before.units)) scene.setUnits(id, spec.units ?? null);\n if (spec.clip !== before.clip) scene.setClip(id, spec.clip ?? null);\n if (!same(spec.pose, before.pose)) scene.setPose(id, spec.pose);\n if ((spec.hidden === true) !== (before.hidden === true)) scene.setHidden(id, spec.hidden === true);\n if (spec.locked === true && before.locked !== true) scene.setLocked(id, true);\n }\n if (selected.some((id) => removed.has(id))) {\n scene.setSelection(selected.filter((id) => scene.get(id) !== undefined));\n }\n });\n}\n\nfunction byId<T extends { id?: NodeId }>(specs: readonly T[]): Map<NodeId, T> {\n const out = new Map<NodeId, T>();\n for (const spec of specs) {\n if (spec.id === undefined) throw new Error('reconcileSpecs: every spec needs an id');\n if (out.has(spec.id)) throw new Error(`reconcileSpecs: id \"${spec.id}\" appears more than once`);\n out.set(spec.id, spec);\n }\n return out;\n}\n\nfunction needsReplace<TData, TLayer extends string, TPose>(\n a: AddNodeSpec<TData, TLayer, TPose>,\n b: AddNodeSpec<TData, TLayer, TPose>,\n): boolean {\n return a.kind !== b.kind\n || a.pickable !== b.pickable\n || a.derivePath !== b.derivePath\n || a.derivePose !== b.derivePose\n || a.clipFromPose !== b.clipFromPose\n || a.copiesFromPose !== b.copiesFromPose\n || a.layout !== b.layout;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAiBA,SAAgB,eACd,OACA,MACA,MACM;CACN,MAAM,WAAW,KAAK,IAAI;CAC1B,MAAM,WAAW,KAAK,IAAI;CAC1B,MAAM,OAAiB,CAAC;CACxB,KAAK,MAAM,CAAC,IAAI,WAAW,UAAU;EACnC,MAAM,QAAQ,SAAS,IAAI,EAAE;EAC7B,IAAI,UAAU,UAAa,aAAa,QAAQ,KAAK,GAAG,KAAK,KAAK,EAAE;CACtE;CACA,MAAM,WAAW,MAAM,eAAe,KAAK,QAAQ,OAAO,MAAM,IAAI,EAAE,MAAM,MAAS,CAAC;CACtF,MAAM,UAAU,IAAI,IAAI,QAAQ;CAGhC,MAAM,WAAW,MAAM,aAAa;CAEpC,MAAM,gBAAgB;EACpB,MAAM,WAAW,QAAQ;EACzB,KAAK,MAAM,QAAQ,MAAM;GACvB,MAAM,KAAK,KAAK;GAChB,MAAM,SAAS,SAAS,IAAI,EAAE;GAC9B,IAAI,MAAM,IAAI,EAAE,MAAM,UAAa,WAAW,UAAa,QAAQ,IAAI,EAAE,GAAG;IAC1E,MAAM,IAAI,IAAI;IACd;GACF;GAEA,IAAI,OAAO,WAAW,QAAQ,KAAK,WAAW,MAAM,MAAM,UAAU,IAAI,KAAK;GAC7E,MAAM,SAAS,KAAK,UAAU;GAC9B,IAAI,YAAY,OAAO,UAAU,OAAO,MAAM,KAAK,IAAI,MAAM;GAC7D,IAAI,KAAK,UAAU,OAAO,OAAO,MAAM,SAAS,IAAI,KAAK,KAAK;GAC9D,IAAI,CAACA,UAAK,KAAK,MAAM,OAAO,IAAI,GAAG,MAAM,OAAO,IAAI,EAAE,MAAM,KAAK,KAAK,CAAC;GACvE,IAAI,CAACA,UAAK,KAAK,WAAW,OAAO,SAAS,GAAG,MAAM,aAAa,IAAI,KAAK,SAAS;GAClF,IAAI,CAACA,UAAK,KAAK,OAAO,OAAO,KAAK,GAAG,MAAM,SAAS,IAAI,KAAK,SAAS,IAAI;GAC1E,IAAI,KAAK,SAAS,OAAO,MAAM,MAAM,QAAQ,IAAI,KAAK,QAAQ,IAAI;GAClE,IAAI,CAACA,UAAK,KAAK,MAAM,OAAO,IAAI,GAAG,MAAM,QAAQ,IAAI,KAAK,IAAI;GAC9D,IAAK,KAAK,WAAW,UAAW,OAAO,WAAW,OAAO,MAAM,UAAU,IAAI,KAAK,WAAW,IAAI;GACjG,IAAI,KAAK,WAAW,QAAQ,OAAO,WAAW,MAAM,MAAM,UAAU,IAAI,IAAI;EAC9E;EACA,IAAI,SAAS,MAAM,OAAO,QAAQ,IAAI,EAAE,CAAC,GACvC,MAAM,aAAa,SAAS,QAAQ,OAAO,MAAM,IAAI,EAAE,MAAM,MAAS,CAAC;CAE3E,CAAC;AACH;AAEA,SAAS,KAAgC,OAAqC;CAC5E,MAAM,sBAAM,IAAI,IAAe;CAC/B,KAAK,MAAM,QAAQ,OAAO;EACxB,IAAI,KAAK,OAAO,QAAW,MAAM,IAAI,MAAM,wCAAwC;EACnF,IAAI,IAAI,IAAI,KAAK,EAAE,GAAG,MAAM,IAAI,MAAM,uBAAuB,KAAK,GAAG,yBAAyB;EAC9F,IAAI,IAAI,KAAK,IAAI,IAAI;CACvB;CACA,OAAO;AACT;AAEA,SAAS,aACP,GACA,GACS;CACT,OAAO,EAAE,SAAS,EAAE,QACf,EAAE,aAAa,EAAE,YACjB,EAAE,eAAe,EAAE,cACnB,EAAE,eAAe,EAAE,cACnB,EAAE,iBAAiB,EAAE,gBACrB,EAAE,mBAAmB,EAAE,kBACvB,EAAE,WAAW,EAAE;AACtB"}
@@ -1,4 +1,10 @@
1
+ import { hasChildren } from "./treeShape.js";
2
+
1
3
  //#region src/core/scene/renderOrder.ts
4
+ /**
5
+ * A scene's render order: its nodes layer-major, each layer in tree preorder,
6
+ * cached until the structure changes.
7
+ */
2
8
  function createRenderOrder(state) {
3
9
  /**
4
10
  * Layer-major DFS-preorder ids, and the same sequence as nodes. Four walks
@@ -49,7 +55,7 @@ function createRenderOrder(state) {
49
55
  const node = state.nodes.get(id);
50
56
  if (!node) continue;
51
57
  if (node.layer === only) out.push(id);
52
- if (node.kind === "container") for (let i = node.children.length - 1; i >= 0; i--) stack.push(node.children[i]);
58
+ if (hasChildren(node)) for (let i = node.children.length - 1; i >= 0; i--) stack.push(node.children[i]);
53
59
  }
54
60
  return out;
55
61
  }
@@ -62,7 +68,7 @@ function createRenderOrder(state) {
62
68
  const node = state.nodes.get(id);
63
69
  if (!node) continue;
64
70
  if (node.layer === only) out.push(node);
65
- if (node.kind === "container") for (let i = node.children.length - 1; i >= 0; i--) stack.push(node.children[i]);
71
+ if (hasChildren(node)) for (let i = node.children.length - 1; i >= 0; i--) stack.push(node.children[i]);
66
72
  }
67
73
  return out;
68
74
  }
@@ -77,7 +83,7 @@ function createRenderOrder(state) {
77
83
  if (!node) continue;
78
84
  const bucket = buckets[state.layerIndex.get(node.layer) ?? -1];
79
85
  if (bucket) bucket.push(id);
80
- if (node.kind === "container") for (let i = node.children.length - 1; i >= 0; i--) stack.push(node.children[i]);
86
+ if (hasChildren(node)) for (let i = node.children.length - 1; i >= 0; i--) stack.push(node.children[i]);
81
87
  }
82
88
  const out = [];
83
89
  for (const bucket of buckets) for (const id of bucket) out.push(id);
@@ -92,7 +98,7 @@ function createRenderOrder(state) {
92
98
  if (!node) continue;
93
99
  const bucket = buckets[state.layerIndex.get(node.layer) ?? -1];
94
100
  if (bucket) bucket.push(node);
95
- if (node.kind === "container") for (let i = node.children.length - 1; i >= 0; i--) stack.push(node.children[i]);
101
+ if (hasChildren(node)) for (let i = node.children.length - 1; i >= 0; i--) stack.push(node.children[i]);
96
102
  }
97
103
  const out = [];
98
104
  for (const bucket of buckets) for (const node of bucket) out.push(node);
@@ -1 +1 @@
1
- {"version":3,"file":"renderOrder.js","names":[],"sources":["../../../src/core/scene/renderOrder.ts"],"sourcesContent":["/**\n * A scene's render order: its nodes layer-major, each layer in tree preorder,\n * cached until the structure changes.\n */\nimport type { Node, NodeId } from './types';\n\n/** What the walks read of a scene's state. */\nexport interface RenderOrderState<TData, TLayer extends string, TPose> {\n nodes: Map<NodeId, Node<TData, TLayer, TPose>>;\n roots: NodeId[];\n layers: { id: TLayer }[];\n layerIndex: Map<TLayer, number>;\n}\n\nexport interface RenderOrder<TData, TLayer extends string, TPose> {\n ids(): NodeId[];\n nodes(): Node<TData, TLayer, TPose>[];\n nodesOnLayer(layer: TLayer): readonly Node<TData, TLayer, TPose>[];\n /** Call on every structural edit; see the cache note below. */\n invalidate(): void;\n}\n\nexport function createRenderOrder<TData, TLayer extends string, TPose>(\n state: RenderOrderState<TData, TLayer, TPose>,\n): RenderOrder<TData, TLayer, TPose> {\n /**\n * Layer-major DFS-preorder ids, and the same sequence as nodes. Four walks\n * for two sequences, which wants explaining, because collapsing them is the\n * obvious cleanup and each collapse was measured to cost something:\n *\n * - ids vs nodes: projecting one from the other is a whole extra pass, worth\n * 20% of `renderOrder()` on a multi-layer scene.\n * - flat vs bucketed: a single layer needs no buckets and can compare the\n * layer id instead of indexing it, which is 1.4x. Behind a branch in one\n * function it also slows the bucketed path by 20% — the two shapes share\n * one optimized body, and whichever ran first wins it.\n *\n * All four emit the same order and `renderOrder.test.ts` holds them to it.\n */\n function walkIds(): NodeId[] {\n return state.layers.length === 1 ? renderOrderFlat() : renderOrderBucketed();\n }\n\n function walkNodes(): Node<TData, TLayer, TPose>[] {\n return state.layers.length === 1 ? renderOrderNodesFlat() : renderOrderNodesBucketed();\n }\n\n /**\n * Render order survives everything except a structural edit, so both walks\n * are cached against a counter the structural writers bump. Pose and data\n * edits leave order alone and must NOT invalidate — they fire per frame\n * during a drag, which is exactly when the cache pays.\n *\n * A missed bump is a silent wrong answer: nodes painted in the wrong z-order,\n * or unpickable. `renderOrder.test.ts` walks each structural op and asserts\n * the order moved.\n */\n let structureGeneration = 0;\n let orderCache: {\n gen: number;\n ids?: NodeId[];\n nodes?: Node<TData, TLayer, TPose>[];\n byLayer?: Map<TLayer, Node<TData, TLayer, TPose>[]>;\n } = { gen: 0 };\n\n function invalidate(): void {\n structureGeneration++;\n }\n\n const EMPTY_NODES: readonly Node<TData, TLayer, TPose>[] = [];\n\n function currentOrderCache(): typeof orderCache {\n if (orderCache.gen !== structureGeneration) {\n orderCache = { gen: structureGeneration };\n }\n return orderCache;\n }\n\n function renderOrderFlat(): NodeId[] {\n const only = state.layers[0].id;\n const out: NodeId[] = [];\n const stack: NodeId[] = [...state.roots].reverse();\n while (stack.length > 0) {\n const id = stack.pop()!;\n const node = state.nodes.get(id);\n if (!node) continue;\n if (node.layer === only) out.push(id);\n if (node.kind === 'container') {\n for (let i = node.children.length - 1; i >= 0; i--) {\n stack.push(node.children[i]);\n }\n }\n }\n return out;\n }\n\n function renderOrderNodesFlat(): Node<TData, TLayer, TPose>[] {\n const only = state.layers[0].id;\n const out: Node<TData, TLayer, TPose>[] = [];\n const stack: NodeId[] = [...state.roots].reverse();\n while (stack.length > 0) {\n const id = stack.pop()!;\n const node = state.nodes.get(id);\n if (!node) continue;\n if (node.layer === only) out.push(node);\n if (node.kind === 'container') {\n for (let i = node.children.length - 1; i >= 0; i--) {\n stack.push(node.children[i]);\n }\n }\n }\n return out;\n }\n\n /** One DFS of the tree, bucketed by layer: appending in preorder keeps each\n * bucket in preorder, so concatenating them is the layer-major sequence. */\n function renderOrderBucketed(): NodeId[] {\n const buckets: NodeId[][] = state.layers.map(() => []);\n const stack: NodeId[] = [...state.roots].reverse();\n while (stack.length > 0) {\n const id = stack.pop()!;\n const node = state.nodes.get(id);\n if (!node) continue;\n const bucket = buckets[state.layerIndex.get(node.layer) ?? -1];\n if (bucket) bucket.push(id);\n if (node.kind === 'container') {\n for (let i = node.children.length - 1; i >= 0; i--) {\n stack.push(node.children[i]);\n }\n }\n }\n const out: NodeId[] = [];\n for (const bucket of buckets) {\n for (const id of bucket) out.push(id);\n }\n return out;\n }\n\n function renderOrderNodesBucketed(): Node<TData, TLayer, TPose>[] {\n const buckets: Node<TData, TLayer, TPose>[][] = state.layers.map(() => []);\n const stack: NodeId[] = [...state.roots].reverse();\n while (stack.length > 0) {\n const id = stack.pop()!;\n const node = state.nodes.get(id);\n if (!node) continue;\n const bucket = buckets[state.layerIndex.get(node.layer) ?? -1];\n if (bucket) bucket.push(node);\n if (node.kind === 'container') {\n for (let i = node.children.length - 1; i >= 0; i--) {\n stack.push(node.children[i]);\n }\n }\n }\n const out: Node<TData, TLayer, TPose>[] = [];\n for (const bucket of buckets) {\n for (const node of bucket) out.push(node);\n }\n return out;\n }\n\n return {\n ids() {\n const c = currentOrderCache();\n return (c.ids ??= walkIds());\n },\n nodes() {\n const c = currentOrderCache();\n return (c.nodes ??= walkNodes());\n },\n nodesOnLayer(layer) {\n const c = currentOrderCache();\n let byLayer = c.byLayer;\n if (!byLayer) {\n byLayer = new Map();\n for (const node of (c.nodes ??= walkNodes())) {\n let bucket = byLayer.get(node.layer);\n if (!bucket) byLayer.set(node.layer, (bucket = []));\n bucket.push(node);\n }\n c.byLayer = byLayer;\n }\n return byLayer.get(layer) ?? EMPTY_NODES;\n },\n invalidate,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"renderOrder.js","names":[],"sources":["../../../src/core/scene/renderOrder.ts"],"sourcesContent":["/**\n * A scene's render order: its nodes layer-major, each layer in tree preorder,\n * cached until the structure changes.\n */\nimport { hasChildren } from './treeShape';\nimport type { Node, NodeId } from './types';\n\n/** What the walks read of a scene's state. */\nexport interface RenderOrderState<TData, TLayer extends string, TPose> {\n nodes: Map<NodeId, Node<TData, TLayer, TPose>>;\n roots: NodeId[];\n layers: { id: TLayer }[];\n layerIndex: Map<TLayer, number>;\n}\n\nexport interface RenderOrder<TData, TLayer extends string, TPose> {\n ids(): NodeId[];\n nodes(): Node<TData, TLayer, TPose>[];\n nodesOnLayer(layer: TLayer): readonly Node<TData, TLayer, TPose>[];\n /** Call on every structural edit; see the cache note below. */\n invalidate(): void;\n}\n\nexport function createRenderOrder<TData, TLayer extends string, TPose>(\n state: RenderOrderState<TData, TLayer, TPose>,\n): RenderOrder<TData, TLayer, TPose> {\n /**\n * Layer-major DFS-preorder ids, and the same sequence as nodes. Four walks\n * for two sequences, which wants explaining, because collapsing them is the\n * obvious cleanup and each collapse was measured to cost something:\n *\n * - ids vs nodes: projecting one from the other is a whole extra pass, worth\n * 20% of `renderOrder()` on a multi-layer scene.\n * - flat vs bucketed: a single layer needs no buckets and can compare the\n * layer id instead of indexing it, which is 1.4x. Behind a branch in one\n * function it also slows the bucketed path by 20% — the two shapes share\n * one optimized body, and whichever ran first wins it.\n *\n * All four emit the same order and `renderOrder.test.ts` holds them to it.\n */\n function walkIds(): NodeId[] {\n return state.layers.length === 1 ? renderOrderFlat() : renderOrderBucketed();\n }\n\n function walkNodes(): Node<TData, TLayer, TPose>[] {\n return state.layers.length === 1 ? renderOrderNodesFlat() : renderOrderNodesBucketed();\n }\n\n /**\n * Render order survives everything except a structural edit, so both walks\n * are cached against a counter the structural writers bump. Pose and data\n * edits leave order alone and must NOT invalidate — they fire per frame\n * during a drag, which is exactly when the cache pays.\n *\n * A missed bump is a silent wrong answer: nodes painted in the wrong z-order,\n * or unpickable. `renderOrder.test.ts` walks each structural op and asserts\n * the order moved.\n */\n let structureGeneration = 0;\n let orderCache: {\n gen: number;\n ids?: NodeId[];\n nodes?: Node<TData, TLayer, TPose>[];\n byLayer?: Map<TLayer, Node<TData, TLayer, TPose>[]>;\n } = { gen: 0 };\n\n function invalidate(): void {\n structureGeneration++;\n }\n\n const EMPTY_NODES: readonly Node<TData, TLayer, TPose>[] = [];\n\n function currentOrderCache(): typeof orderCache {\n if (orderCache.gen !== structureGeneration) {\n orderCache = { gen: structureGeneration };\n }\n return orderCache;\n }\n\n function renderOrderFlat(): NodeId[] {\n const only = state.layers[0].id;\n const out: NodeId[] = [];\n const stack: NodeId[] = [...state.roots].reverse();\n while (stack.length > 0) {\n const id = stack.pop()!;\n const node = state.nodes.get(id);\n if (!node) continue;\n if (node.layer === only) out.push(id);\n if (hasChildren(node)) {\n for (let i = node.children.length - 1; i >= 0; i--) {\n stack.push(node.children[i]);\n }\n }\n }\n return out;\n }\n\n function renderOrderNodesFlat(): Node<TData, TLayer, TPose>[] {\n const only = state.layers[0].id;\n const out: Node<TData, TLayer, TPose>[] = [];\n const stack: NodeId[] = [...state.roots].reverse();\n while (stack.length > 0) {\n const id = stack.pop()!;\n const node = state.nodes.get(id);\n if (!node) continue;\n if (node.layer === only) out.push(node);\n if (hasChildren(node)) {\n for (let i = node.children.length - 1; i >= 0; i--) {\n stack.push(node.children[i]);\n }\n }\n }\n return out;\n }\n\n /** One DFS of the tree, bucketed by layer: appending in preorder keeps each\n * bucket in preorder, so concatenating them is the layer-major sequence. */\n function renderOrderBucketed(): NodeId[] {\n const buckets: NodeId[][] = state.layers.map(() => []);\n const stack: NodeId[] = [...state.roots].reverse();\n while (stack.length > 0) {\n const id = stack.pop()!;\n const node = state.nodes.get(id);\n if (!node) continue;\n const bucket = buckets[state.layerIndex.get(node.layer) ?? -1];\n if (bucket) bucket.push(id);\n if (hasChildren(node)) {\n for (let i = node.children.length - 1; i >= 0; i--) {\n stack.push(node.children[i]);\n }\n }\n }\n const out: NodeId[] = [];\n for (const bucket of buckets) {\n for (const id of bucket) out.push(id);\n }\n return out;\n }\n\n function renderOrderNodesBucketed(): Node<TData, TLayer, TPose>[] {\n const buckets: Node<TData, TLayer, TPose>[][] = state.layers.map(() => []);\n const stack: NodeId[] = [...state.roots].reverse();\n while (stack.length > 0) {\n const id = stack.pop()!;\n const node = state.nodes.get(id);\n if (!node) continue;\n const bucket = buckets[state.layerIndex.get(node.layer) ?? -1];\n if (bucket) bucket.push(node);\n if (hasChildren(node)) {\n for (let i = node.children.length - 1; i >= 0; i--) {\n stack.push(node.children[i]);\n }\n }\n }\n const out: Node<TData, TLayer, TPose>[] = [];\n for (const bucket of buckets) {\n for (const node of bucket) out.push(node);\n }\n return out;\n }\n\n return {\n ids() {\n const c = currentOrderCache();\n return (c.ids ??= walkIds());\n },\n nodes() {\n const c = currentOrderCache();\n return (c.nodes ??= walkNodes());\n },\n nodesOnLayer(layer) {\n const c = currentOrderCache();\n let byLayer = c.byLayer;\n if (!byLayer) {\n byLayer = new Map();\n for (const node of (c.nodes ??= walkNodes())) {\n let bucket = byLayer.get(node.layer);\n if (!bucket) byLayer.set(node.layer, (bucket = []));\n bucket.push(node);\n }\n c.byLayer = byLayer;\n }\n return byLayer.get(layer) ?? EMPTY_NODES;\n },\n invalidate,\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import { isPlainObject } from "../isPlainObject.js";
2
+
3
+ //#region src/core/scene/sameValue.ts
4
+ /** Structural equality over plain objects and arrays; anything else by identity. */
5
+ function sameValue(a, b) {
6
+ if (Object.is(a, b)) return true;
7
+ if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((v, i) => sameValue(v, b[i]));
8
+ if (isPlainObject(a) && isPlainObject(b)) {
9
+ const ka = Object.keys(a);
10
+ return ka.length === Object.keys(b).length && ka.every((k) => k in b && sameValue(a[k], b[k]));
11
+ }
12
+ return false;
13
+ }
14
+
15
+ //#endregion
16
+ export { sameValue };
17
+ //# sourceMappingURL=sameValue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sameValue.js","names":[],"sources":["../../../src/core/scene/sameValue.ts"],"sourcesContent":["import { isPlainObject } from '../isPlainObject';\n\n/** Structural equality over plain objects and arrays; anything else by identity. */\nexport function sameValue(a: unknown, b: unknown): boolean {\n if (Object.is(a, b)) return true;\n if (Array.isArray(a) && Array.isArray(b)) {\n return a.length === b.length && a.every((v, i) => sameValue(v, b[i]));\n }\n if (isPlainObject(a) && isPlainObject(b)) {\n const ka = Object.keys(a);\n return ka.length === Object.keys(b).length && ka.every((k) => k in b && sameValue(a[k], b[k]));\n }\n return false;\n}\n"],"mappings":";;;;AAGA,SAAgB,UAAU,GAAY,GAAqB;CACzD,IAAI,OAAO,GAAG,GAAG,CAAC,GAAG,OAAO;CAC5B,IAAI,MAAM,QAAQ,CAAC,KAAK,MAAM,QAAQ,CAAC,GACrC,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,OAAO,GAAG,MAAM,UAAU,GAAG,EAAE,EAAE,CAAC;CAEtE,IAAI,cAAc,CAAC,KAAK,cAAc,CAAC,GAAG;EACxC,MAAM,KAAK,OAAO,KAAK,CAAC;EACxB,OAAO,GAAG,WAAW,OAAO,KAAK,CAAC,CAAC,CAAC,UAAU,GAAG,OAAO,MAAM,KAAK,KAAK,UAAU,EAAE,IAAI,EAAE,EAAE,CAAC;CAC/F;CACA,OAAO;AACT"}
@@ -120,8 +120,8 @@ function buildScene(options, layers, initialNodes, initialDoc) {
120
120
  overrides,
121
121
  getSelection: selection.get,
122
122
  setSelection: (ids) => selection.set([...ids]),
123
- loadState(json) {
124
- const specs = specsFromSerialized(json, registry);
123
+ loadState(json, opts) {
124
+ const specs = specsFromSerialized(json, registry, opts);
125
125
  if (mode.open()) throw new Error("Scene: cannot loadState inside another edit — it replaces the state that edit would revert to");
126
126
  overrides.clearAll();
127
127
  doc.reset(json.systemLayers.map(layerFromSerialized), json.doc);
@@ -145,7 +145,7 @@ function buildScene(options, layers, initialNodes, initialDoc) {
145
145
  * keys, or invalid scene shape (cross-layer subtrees, unknown layer ids).
146
146
  * The scene starts with empty history. */
147
147
  function sceneFromJSON(json, options = {}) {
148
- return buildScene(options, json.systemLayers.map(layerFromSerialized), (registry) => specsFromSerialized(json, registry), json.doc);
148
+ return buildScene(options, json.systemLayers.map(layerFromSerialized), (registry) => specsFromSerialized(json, registry, options), json.doc);
149
149
  }
150
150
 
151
151
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"scene.js","names":[],"sources":["../../../src/core/scene/scene.ts"],"sourcesContent":["import { withKitRegistry } from './kitRegistry';\nimport { createPoseOverrides } from './poseOverrides';\nimport { createArrivalWindow } from './arrivalWindow';\nimport { documentReads, sceneMutators } from './sceneMutators';\nimport { createNotifier } from './notifier';\nimport type { SerializedDoc } from './docValues';\nimport { sceneChanges } from './sceneChanges';\nimport { createPoseWriteLog } from './poseWrites';\nimport { effectivePose } from './effectivePose';\nimport { createEditMode } from './editMode';\nimport { createSceneEdits } from './sceneEdits';\nimport { createSceneRecording } from './sceneRecording';\nimport { createSceneSelection } from './sceneSelection';\nimport { createSceneState } from './sceneState';\nimport { systemLayerRecord } from './layerStack';\nimport { layerFromSerialized, specsFromSerialized } from './serialization';\nimport { resolveLayoutFrame, runLayoutPass } from './layoutPass';\nimport { IDENTITY_POSE_COMPOSITION, type PoseComposition } from './composeFrame';\nimport {\n asNodeId,\n type AddNodeSpec,\n type LayerRecord,\n type NodeId,\n type RegisteredOp,\n type Scene,\n type SceneRegistry,\n type SerializedScene,\n type UseSceneOptions,\n} from './types';\n\n/** A node still in `scene`, with the pose it is shown at. */\nfunction shownNode<TData, TLayer extends string, TPose>(scene: Scene<TData, TLayer, TPose>, id: NodeId) {\n const node = scene.get(id);\n return node && { node, pose: effectivePose(scene, node) };\n}\n\nlet defaultIdCounter = 0;\nconst defaultGenerateId = (): NodeId =>\n asNodeId(`n${(defaultIdCounter++).toString(36)}-${Math.random().toString(36).slice(2, 8)}`);\n\n/** Build a scene outside React. `useScene` is the usual entry point; reach for\n * this in tests, in headless rendering, or wherever a scene must outlive a\n * component. Throws if no system layers are declared. */\nexport function createScene<TData, TLayer extends string, TPose = import('./composePose').RectPose>(\n options: UseSceneOptions<TData, TLayer, TPose>,\n): Scene<TData, TLayer, TPose> {\n return buildScene(options, (options.systemLayers ?? []).map(systemLayerRecord), () => options.initial, options.doc);\n}\n\ntype SceneConfig<TData, TLayer extends string, TPose> =\n Omit<UseSceneOptions<TData, TLayer, TPose>, 'systemLayers' | 'initial' | 'doc'>;\n\n/** A scene holding `layers`, the document values `initialDoc`, and the nodes\n * `initialNodes` answers with once it is handed the registry their functions\n * resolve against. */\nfunction buildScene<TData, TLayer extends string, TPose>(\n options: SceneConfig<TData, TLayer, TPose>,\n layers: readonly LayerRecord<TLayer>[],\n initialNodes: (registry: SceneRegistry<TPose, TData, TLayer>) => readonly AddNodeSpec<TData, TLayer, TPose>[] | undefined,\n initialDoc: SerializedDoc | undefined,\n): Scene<TData, TLayer, TPose> {\n if (layers.length === 0) throw new Error('Scene: systemLayers must declare at least one layer');\n\n // The kit's own registry entries sit under the consumer's, so a snapshot\n // carrying a kit-attached function (a grouped container's derived pose)\n // reloads in any scene, not only one whose consumer registered the key.\n const registry = withKitRegistry(options.registry ?? {});\n\n const doc = createSceneState<TData, TLayer, TPose>(registry, layers, () => arrivals.layoutDeclared(), initialDoc);\n const { state, locks, reads } = doc;\n const notifier = createNotifier();\n const mode = createEditMode();\n const poseWrites = createPoseWriteLog({ untracked: mode.untracked, changes: notifier, shown: (id) => shownNode(scene, id) });\n const writePose = (id: string, pose: unknown): void => poseWrites.layout(() => scene.setPose(asNodeId(id), pose as TPose));\n\n const layoutFrame = resolveLayoutFrame(options.layoutFrame);\n const arrivals = createArrivalWindow<TData, TLayer, TPose>({\n nodes: state.nodes,\n layoutPass: (arrived, touched, departed) =>\n runLayoutPass(scene, reads.layoutOf, arrived, touched, layoutFrame, departed),\n recordsBareEdits: () => !mode.open(),\n writePose,\n });\n\n const generateId = options.generateId ?? defaultGenerateId;\n const selection = createSceneSelection(locks.reachable);\n // A selection change is a scene change to every subscriber.\n selection.subscribe(notifier.notify);\n\n /** Ephemeral per-node overrides. Deliberately not part of `state`: not\n * document content, never reaches `toJSON`, and never touches history. */\n const overrides = createPoseOverrides<TPose>((id) => state.nodes.get(id), doc.store.invalidateDependents);\n\n const recording = createSceneRecording({\n notifier,\n mode,\n passAdapter: () => ({ ...sceneMutators(scene), ...documentReads(scene) }),\n stale: doc.stale,\n lifted: locks.lifted,\n selection,\n arrivals,\n writePose,\n poseWrites,\n config: options,\n });\n\n const edits = createSceneEdits<TData, TLayer, TPose>({\n state,\n store: doc.store,\n locks,\n layers: doc.layers,\n invalidateOrder: doc.order.invalidate,\n fnFields: doc.fnFields,\n ops: { ...recording.ops, register: poseWrites.reporting(recording.ops.register) },\n recorder: recording.recorder,\n arrivals,\n layoutFrame,\n generateId,\n narrowSelection: selection.narrow,\n clearOverride: overrides.clear,\n });\n for (const [kind, handler] of Object.entries(options.ops ?? {})) recording.ops.register(kind, handler);\n\n const scene: Scene<TData, TLayer, TPose> = {\n get nodes() { return state.nodes; },\n get roots() { return state.roots; },\n get layers() { return state.layers; },\n registry,\n\n ...reads,\n\n mintId: generateId,\n composition: options.composition ?? (IDENTITY_POSE_COMPOSITION as PoseComposition<TPose>),\n\n ...edits.methods,\n ...recording.methods,\n\n unlocked: (fn) => locks.lifted(fn),\n\n setArrivalHandler: arrivals.setHandler,\n onReflow: arrivals.onReflow,\n onPoseWrite: poseWrites.subscribe,\n\n overrides,\n\n getSelection: selection.get,\n setSelection: (ids) => selection.set([...ids]),\n\n loadState(json) {\n // Validate + map up front (throws before we touch live state on a bad\n // version or unknown registry key).\n const specs = specsFromSerialized(json, registry);\n if (mode.open()) {\n throw new Error('Scene: cannot loadState inside another edit — it replaces the state that edit would revert to');\n }\n // Ephemeral, and every id is a new node after this.\n overrides.clearAll();\n doc.reset(json.systemLayers.map(layerFromSerialized), json.doc);\n recording.clearHistory();\n poseWrites.quiet(() => edits.construct(specs));\n selection.replace(locks.reachable(selection.get().filter((id) => state.nodes.has(id))));\n notifier.notify();\n },\n\n ...sceneChanges(notifier, selection),\n };\n\n const initial = initialNodes(registry);\n if (initial) {\n poseWrites.quiet(() => edits.construct(initial));\n notifier.notify();\n }\n\n return scene;\n}\n\n/** Reconstruct a Scene from a JSON snapshot produced by `scene.toJSON()`.\n * Function fields (e.g., `clipFromPose`) are resolved by string key via the\n * registry passed in `options`. Throws on unknown version, unknown registry\n * keys, or invalid scene shape (cross-layer subtrees, unknown layer ids).\n * The scene starts with empty history. */\nexport function sceneFromJSON<TData, TLayer extends string, TPose>(\n json: SerializedScene<TData, TLayer, TPose>,\n options: {\n registry?: SceneRegistry<TPose, TData, TLayer>;\n historyLimit?: number;\n coalesceWindowMs?: number;\n generateId?: () => NodeId;\n ops?: Readonly<Record<string, RegisteredOp<unknown>>>;\n composition?: UseSceneOptions<TData, TLayer, TPose>['composition'];\n layoutFrame?: UseSceneOptions<TData, TLayer, TPose>['layoutFrame'];\n } = {},\n): Scene<TData, TLayer, TPose> {\n return buildScene(\n options,\n json.systemLayers.map(layerFromSerialized),\n (registry) => specsFromSerialized(json, registry),\n json.doc,\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"scene.js","names":[],"sources":["../../../src/core/scene/scene.ts"],"sourcesContent":["import { withKitRegistry } from './kitRegistry';\nimport { createPoseOverrides } from './poseOverrides';\nimport { createArrivalWindow } from './arrivalWindow';\nimport { documentReads, sceneMutators } from './sceneMutators';\nimport { createNotifier } from './notifier';\nimport type { SerializedDoc } from './docValues';\nimport { sceneChanges } from './sceneChanges';\nimport { createPoseWriteLog } from './poseWrites';\nimport { effectivePose } from './effectivePose';\nimport { createEditMode } from './editMode';\nimport { createSceneEdits } from './sceneEdits';\nimport { createSceneRecording } from './sceneRecording';\nimport { createSceneSelection } from './sceneSelection';\nimport { createSceneState } from './sceneState';\nimport { systemLayerRecord } from './layerStack';\nimport { layerFromSerialized, specsFromSerialized, type SpecsFromSerializedOptions } from './serialization';\nimport { resolveLayoutFrame, runLayoutPass } from './layoutPass';\nimport { IDENTITY_POSE_COMPOSITION, type PoseComposition } from './composeFrame';\nimport {\n asNodeId,\n type AddNodeSpec,\n type LayerRecord,\n type NodeId,\n type RegisteredOp,\n type Scene,\n type SceneRegistry,\n type SerializedScene,\n type SerializedSceneV1,\n type UseSceneOptions,\n} from './types';\n\n/** A node still in `scene`, with the pose it is shown at. */\nfunction shownNode<TData, TLayer extends string, TPose>(scene: Scene<TData, TLayer, TPose>, id: NodeId) {\n const node = scene.get(id);\n return node && { node, pose: effectivePose(scene, node) };\n}\n\nlet defaultIdCounter = 0;\nconst defaultGenerateId = (): NodeId =>\n asNodeId(`n${(defaultIdCounter++).toString(36)}-${Math.random().toString(36).slice(2, 8)}`);\n\n/** Build a scene outside React. `useScene` is the usual entry point; reach for\n * this in tests, in headless rendering, or wherever a scene must outlive a\n * component. Throws if no system layers are declared. */\nexport function createScene<TData, TLayer extends string, TPose = import('./composePose').RectPose>(\n options: UseSceneOptions<TData, TLayer, TPose>,\n): Scene<TData, TLayer, TPose> {\n return buildScene(options, (options.systemLayers ?? []).map(systemLayerRecord), () => options.initial, options.doc);\n}\n\ntype SceneConfig<TData, TLayer extends string, TPose> =\n Omit<UseSceneOptions<TData, TLayer, TPose>, 'systemLayers' | 'initial' | 'doc'>;\n\n/** A scene holding `layers`, the document values `initialDoc`, and the nodes\n * `initialNodes` answers with once it is handed the registry their functions\n * resolve against. */\nfunction buildScene<TData, TLayer extends string, TPose>(\n options: SceneConfig<TData, TLayer, TPose>,\n layers: readonly LayerRecord<TLayer>[],\n initialNodes: (registry: SceneRegistry<TPose, TData, TLayer>) => readonly AddNodeSpec<TData, TLayer, TPose>[] | undefined,\n initialDoc: SerializedDoc | undefined,\n): Scene<TData, TLayer, TPose> {\n if (layers.length === 0) throw new Error('Scene: systemLayers must declare at least one layer');\n\n // The kit's own registry entries sit under the consumer's, so a snapshot\n // carrying a kit-attached function (a grouped container's derived pose)\n // reloads in any scene, not only one whose consumer registered the key.\n const registry = withKitRegistry(options.registry ?? {});\n\n const doc = createSceneState<TData, TLayer, TPose>(registry, layers, () => arrivals.layoutDeclared(), initialDoc);\n const { state, locks, reads } = doc;\n const notifier = createNotifier();\n const mode = createEditMode();\n const poseWrites = createPoseWriteLog({ untracked: mode.untracked, changes: notifier, shown: (id) => shownNode(scene, id) });\n const writePose = (id: string, pose: unknown): void => poseWrites.layout(() => scene.setPose(asNodeId(id), pose as TPose));\n\n const layoutFrame = resolveLayoutFrame(options.layoutFrame);\n const arrivals = createArrivalWindow<TData, TLayer, TPose>({\n nodes: state.nodes,\n layoutPass: (arrived, touched, departed) =>\n runLayoutPass(scene, reads.layoutOf, arrived, touched, layoutFrame, departed),\n recordsBareEdits: () => !mode.open(),\n writePose,\n });\n\n const generateId = options.generateId ?? defaultGenerateId;\n const selection = createSceneSelection(locks.reachable);\n // A selection change is a scene change to every subscriber.\n selection.subscribe(notifier.notify);\n\n /** Ephemeral per-node overrides. Deliberately not part of `state`: not\n * document content, never reaches `toJSON`, and never touches history. */\n const overrides = createPoseOverrides<TPose, TData>((id) => state.nodes.get(id), doc.store.invalidateDependents);\n\n const recording = createSceneRecording({\n notifier,\n mode,\n passAdapter: () => ({ ...sceneMutators(scene), ...documentReads(scene) }),\n stale: doc.stale,\n lifted: locks.lifted,\n selection,\n arrivals,\n writePose,\n poseWrites,\n config: options,\n });\n\n const edits = createSceneEdits<TData, TLayer, TPose>({\n state,\n store: doc.store,\n locks,\n layers: doc.layers,\n invalidateOrder: doc.order.invalidate,\n fnFields: doc.fnFields,\n ops: { ...recording.ops, register: poseWrites.reporting(recording.ops.register) },\n recorder: recording.recorder,\n arrivals,\n layoutFrame,\n generateId,\n narrowSelection: selection.narrow,\n clearOverride: overrides.clear,\n });\n for (const [kind, handler] of Object.entries(options.ops ?? {})) recording.ops.register(kind, handler);\n\n const scene: Scene<TData, TLayer, TPose> = {\n get nodes() { return state.nodes; },\n get roots() { return state.roots; },\n get layers() { return state.layers; },\n registry,\n\n ...reads,\n\n mintId: generateId,\n composition: options.composition ?? (IDENTITY_POSE_COMPOSITION as PoseComposition<TPose>),\n\n ...edits.methods,\n ...recording.methods,\n\n unlocked: (fn) => locks.lifted(fn),\n\n setArrivalHandler: arrivals.setHandler,\n onReflow: arrivals.onReflow,\n onPoseWrite: poseWrites.subscribe,\n\n overrides,\n\n getSelection: selection.get,\n setSelection: (ids) => selection.set([...ids]),\n\n loadState(json, opts) {\n // Validate + map up front (throws before we touch live state on a bad\n // version or unknown registry key).\n const specs = specsFromSerialized(json, registry, opts);\n if (mode.open()) {\n throw new Error('Scene: cannot loadState inside another edit — it replaces the state that edit would revert to');\n }\n // Ephemeral, and every id is a new node after this.\n overrides.clearAll();\n doc.reset(json.systemLayers.map(layerFromSerialized), json.doc);\n recording.clearHistory();\n poseWrites.quiet(() => edits.construct(specs));\n selection.replace(locks.reachable(selection.get().filter((id) => state.nodes.has(id))));\n notifier.notify();\n },\n\n ...sceneChanges(notifier, selection),\n };\n\n const initial = initialNodes(registry);\n if (initial) {\n poseWrites.quiet(() => edits.construct(initial));\n notifier.notify();\n }\n\n return scene;\n}\n\n/** Reconstruct a Scene from a JSON snapshot produced by `scene.toJSON()`.\n * Function fields (e.g., `clipFromPose`) are resolved by string key via the\n * registry passed in `options`. Throws on unknown version, unknown registry\n * keys, or invalid scene shape (cross-layer subtrees, unknown layer ids).\n * The scene starts with empty history. */\nexport function sceneFromJSON<TData, TLayer extends string, TPose>(\n json: SerializedScene<TData, TLayer, TPose> | SerializedSceneV1<TData, TLayer, TPose>,\n options: {\n registry?: SceneRegistry<TPose, TData, TLayer>;\n historyLimit?: number;\n coalesceWindowMs?: number;\n generateId?: () => NodeId;\n ops?: Readonly<Record<string, RegisteredOp<unknown>>>;\n composition?: UseSceneOptions<TData, TLayer, TPose>['composition'];\n layoutFrame?: UseSceneOptions<TData, TLayer, TPose>['layoutFrame'];\n } & SpecsFromSerializedOptions<TData, TLayer, TPose> = {},\n): Scene<TData, TLayer, TPose> {\n return buildScene(\n options,\n json.systemLayers.map(layerFromSerialized),\n (registry) => specsFromSerialized(json, registry, options),\n json.doc,\n );\n}\n"],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import { createSceneDoc } from "./docValues.js";
2
+ import { createDeclaredFieldSetters } from "./declaredFieldSetters.js";
2
3
  import { createFieldEdits } from "./fieldEdits.js";
3
4
  import { registerKitOps } from "./kitOps.js";
4
5
  import { createNodeFlags } from "./nodeFlags.js";
@@ -56,6 +57,12 @@ function createSceneEdits(env) {
56
57
  record: recorder.record,
57
58
  narrowSelection: () => env.narrowSelection()
58
59
  });
60
+ const declaredEdits = createDeclaredFieldSetters({
61
+ register: env.ops.register,
62
+ requireNode: store.requireNode,
63
+ refuse: locks.refuse,
64
+ record: recorder.record
65
+ });
59
66
  const doc = createSceneDoc({
60
67
  values: state.docValues,
61
68
  register: env.ops.register,
@@ -68,6 +75,7 @@ function createSceneEdits(env) {
68
75
  ...fieldEdits,
69
76
  ...layerEdits,
70
77
  ...flagEdits,
78
+ ...declaredEdits,
71
79
  doc
72
80
  }
73
81
  };
@@ -1 +1 @@
1
- {"version":3,"file":"sceneEdits.js","names":[],"sources":["../../../src/core/scene/sceneEdits.ts"],"sourcesContent":["/**\n * The scene's edits to its document — tree, node fields, layers, and document values — with\n * the kit ops they record.\n */\nimport type { ArrivalWindow } from './arrivalWindow';\nimport { createSceneDoc, type SceneDocValues } from './docValues';\nimport { createFieldEdits, type FieldEdits } from './fieldEdits';\nimport { registerKitOps } from './kitOps';\nimport { createNodeFlags } from './nodeFlags';\nimport { createLayerEdits, type LayerEdits, type createLayerStack } from './layerStack';\nimport type { ResolvedLayoutFrame } from './layoutPass';\nimport type { LockGuard } from './lockGuard';\nimport type { NodeFnField } from './nodeFnFields';\nimport type { createNodeStore } from './nodeStore';\nimport type { OpTable } from './opTable';\nimport type { Recorder } from './recorder';\nimport { createTreeEdits, type TreeEdits } from './treeEdits';\nimport type { LayerRecord, Node, NodeId } from './types';\n\nexport interface SceneEditsEnv<TData, TLayer extends string, TPose> {\n state: {\n nodes: Map<NodeId, Node<TData, TLayer, TPose>>;\n layers: LayerRecord<TLayer>[];\n layerIndex: Map<TLayer, number>;\n /** The scene's document values, which `doc` reads and records. */\n docValues: Map<string, unknown>;\n };\n store: ReturnType<typeof createNodeStore<TData, TLayer, TPose>>;\n locks: Pick<LockGuard<TLayer>, 'refuse' | 'refuseLayer' | 'lifted'>;\n layers: Pick<\n ReturnType<typeof createLayerStack<TLayer>>,\n 'requireLayerIndex' | 'rebuildLayerIndex' | 'assertSubtreeLayer'\n >;\n invalidateOrder(): void;\n fnFields: readonly NodeFnField[];\n ops: Pick<OpTable, 'register' | 'run'>;\n recorder: Pick<Recorder, 'record' | 'edit' | 'batch'>;\n arrivals: Pick<ArrivalWindow<TPose>, 'noteArrival' | 'noteDeparture' | 'noteChange' | 'layoutDeclared'>;\n layoutFrame: ResolvedLayoutFrame<TPose>;\n generateId(): NodeId;\n narrowSelection(gone?: ReadonlySet<NodeId>): void;\n clearOverride(id: NodeId): void;\n}\n\nexport interface SceneEdits<TData, TLayer extends string, TPose> {\n construct: TreeEdits<TData, TLayer, TPose>['construct'];\n methods: Omit<TreeEdits<TData, TLayer, TPose>, 'construct'>\n & FieldEdits<TData, TLayer, TPose>\n & LayerEdits<TData, TLayer, TPose>\n & ReturnType<typeof createNodeFlags<TData, TLayer, TPose>>\n & SceneDocValues;\n}\n\nexport function createSceneEdits<TData, TLayer extends string, TPose>(\n env: SceneEditsEnv<TData, TLayer, TPose>,\n): SceneEdits<TData, TLayer, TPose> {\n const { state, store, locks, layers, fnFields, recorder, arrivals } = env;\n\n const { layoutRefs } = registerKitOps<TData, TLayer, TPose>({\n state,\n register: env.ops.register,\n store,\n requireLayerIndex: layers.requireLayerIndex,\n rebuildLayerIndex: layers.rebuildLayerIndex,\n invalidateOrder: env.invalidateOrder,\n fnFields,\n narrowSelection: env.narrowSelection,\n clearOverride: env.clearOverride,\n });\n\n const { construct, ...treeEdits } = createTreeEdits<TData, TLayer, TPose>({\n nodes: state.nodes,\n generateId: env.generateId,\n store,\n locks,\n layers,\n fnFields,\n runOp: env.ops.run,\n recorder,\n arrivals,\n });\n\n const fieldEdits = createFieldEdits<TData, TLayer, TPose>({\n store,\n locks,\n layers,\n recorder,\n arrivals,\n layoutFrame: env.layoutFrame,\n layoutField: fnFields.find((f) => f.spec.field === 'layout')!,\n layoutRefs,\n });\n\n const layerEdits = createLayerEdits<TData, TLayer, TPose>({\n layers: state.layers,\n layerIndex: state.layerIndex,\n nodes: state.nodes,\n requireLayerIndex: layers.requireLayerIndex,\n record: recorder.record,\n lifted: locks.lifted,\n batch: recorder.batch,\n removeMany: treeEdits.removeMany,\n });\n\n const flagEdits = createNodeFlags<TData, TLayer, TPose>({\n register: env.ops.register,\n requireNode: store.requireNode,\n record: recorder.record,\n narrowSelection: () => env.narrowSelection(),\n });\n\n const doc = createSceneDoc({ values: state.docValues, register: env.ops.register, record: recorder.record });\n\n return { construct, methods: { ...treeEdits, ...fieldEdits, ...layerEdits, ...flagEdits, doc } };\n}\n"],"mappings":";;;;;;;;AAqDA,SAAgB,iBACd,KACkC;CAClC,MAAM,EAAE,OAAO,OAAO,OAAO,QAAQ,UAAU,UAAU,aAAa;CAEtE,MAAM,EAAE,eAAe,eAAqC;EAC1D;EACA,UAAU,IAAI,IAAI;EAClB;EACA,mBAAmB,OAAO;EAC1B,mBAAmB,OAAO;EAC1B,iBAAiB,IAAI;EACrB;EACA,iBAAiB,IAAI;EACrB,eAAe,IAAI;CACrB,CAAC;CAED,MAAM,EAAE,WAAW,GAAG,cAAc,gBAAsC;EACxE,OAAO,MAAM;EACb,YAAY,IAAI;EAChB;EACA;EACA;EACA;EACA,OAAO,IAAI,IAAI;EACf;EACA;CACF,CAAC;CAED,MAAM,aAAa,iBAAuC;EACxD;EACA;EACA;EACA;EACA;EACA,aAAa,IAAI;EACjB,aAAa,SAAS,MAAM,MAAM,EAAE,KAAK,UAAU,QAAQ;EAC3D;CACF,CAAC;CAED,MAAM,aAAa,iBAAuC;EACxD,QAAQ,MAAM;EACd,YAAY,MAAM;EAClB,OAAO,MAAM;EACb,mBAAmB,OAAO;EAC1B,QAAQ,SAAS;EACjB,QAAQ,MAAM;EACd,OAAO,SAAS;EAChB,YAAY,UAAU;CACxB,CAAC;CAED,MAAM,YAAY,gBAAsC;EACtD,UAAU,IAAI,IAAI;EAClB,aAAa,MAAM;EACnB,QAAQ,SAAS;EACjB,uBAAuB,IAAI,gBAAgB;CAC7C,CAAC;CAED,MAAM,MAAM,eAAe;EAAE,QAAQ,MAAM;EAAW,UAAU,IAAI,IAAI;EAAU,QAAQ,SAAS;CAAO,CAAC;CAE3G,OAAO;EAAE;EAAW,SAAS;GAAE,GAAG;GAAW,GAAG;GAAY,GAAG;GAAY,GAAG;GAAW;EAAI;CAAE;AACjG"}
1
+ {"version":3,"file":"sceneEdits.js","names":[],"sources":["../../../src/core/scene/sceneEdits.ts"],"sourcesContent":["/**\n * The scene's edits to its document — tree, node fields, layers, and document values — with\n * the kit ops they record.\n */\nimport type { ArrivalWindow } from './arrivalWindow';\nimport { createSceneDoc, type SceneDocValues } from './docValues';\nimport { createDeclaredFieldSetters, type SceneDeclaredFieldSetters } from './declaredFieldSetters';\nimport { createFieldEdits, type FieldEdits } from './fieldEdits';\nimport { registerKitOps } from './kitOps';\nimport { createNodeFlags } from './nodeFlags';\nimport { createLayerEdits, type LayerEdits, type createLayerStack } from './layerStack';\nimport type { ResolvedLayoutFrame } from './layoutPass';\nimport type { LockGuard } from './lockGuard';\nimport type { NodeFnField } from './nodeFnFields';\nimport type { createNodeStore } from './nodeStore';\nimport type { OpTable } from './opTable';\nimport type { Recorder } from './recorder';\nimport { createTreeEdits, type TreeEdits } from './treeEdits';\nimport type { LayerRecord, Node, NodeId } from './types';\n\nexport interface SceneEditsEnv<TData, TLayer extends string, TPose> {\n state: {\n nodes: Map<NodeId, Node<TData, TLayer, TPose>>;\n layers: LayerRecord<TLayer>[];\n layerIndex: Map<TLayer, number>;\n /** The scene's document values, which `doc` reads and records. */\n docValues: Map<string, unknown>;\n };\n store: ReturnType<typeof createNodeStore<TData, TLayer, TPose>>;\n locks: Pick<LockGuard<TLayer>, 'refuse' | 'refuseLayer' | 'lifted'>;\n layers: Pick<\n ReturnType<typeof createLayerStack<TLayer>>,\n 'requireLayerIndex' | 'rebuildLayerIndex' | 'assertSubtreeLayer'\n >;\n invalidateOrder(): void;\n fnFields: readonly NodeFnField[];\n ops: Pick<OpTable, 'register' | 'run'>;\n recorder: Pick<Recorder, 'record' | 'edit' | 'batch'>;\n arrivals: Pick<ArrivalWindow<TPose>, 'noteArrival' | 'noteDeparture' | 'noteChange' | 'layoutDeclared'>;\n layoutFrame: ResolvedLayoutFrame<TPose>;\n generateId(): NodeId;\n narrowSelection(gone?: ReadonlySet<NodeId>): void;\n clearOverride(id: NodeId): void;\n}\n\nexport interface SceneEdits<TData, TLayer extends string, TPose> {\n construct: TreeEdits<TData, TLayer, TPose>['construct'];\n methods: Omit<TreeEdits<TData, TLayer, TPose>, 'construct'>\n & FieldEdits<TData, TLayer, TPose>\n & LayerEdits<TData, TLayer, TPose>\n & ReturnType<typeof createNodeFlags<TData, TLayer, TPose>>\n & SceneDeclaredFieldSetters\n & SceneDocValues;\n}\n\nexport function createSceneEdits<TData, TLayer extends string, TPose>(\n env: SceneEditsEnv<TData, TLayer, TPose>,\n): SceneEdits<TData, TLayer, TPose> {\n const { state, store, locks, layers, fnFields, recorder, arrivals } = env;\n\n const { layoutRefs } = registerKitOps<TData, TLayer, TPose>({\n state,\n register: env.ops.register,\n store,\n requireLayerIndex: layers.requireLayerIndex,\n rebuildLayerIndex: layers.rebuildLayerIndex,\n invalidateOrder: env.invalidateOrder,\n fnFields,\n narrowSelection: env.narrowSelection,\n clearOverride: env.clearOverride,\n });\n\n const { construct, ...treeEdits } = createTreeEdits<TData, TLayer, TPose>({\n nodes: state.nodes,\n generateId: env.generateId,\n store,\n locks,\n layers,\n fnFields,\n runOp: env.ops.run,\n recorder,\n arrivals,\n });\n\n const fieldEdits = createFieldEdits<TData, TLayer, TPose>({\n store,\n locks,\n layers,\n recorder,\n arrivals,\n layoutFrame: env.layoutFrame,\n layoutField: fnFields.find((f) => f.spec.field === 'layout')!,\n layoutRefs,\n });\n\n const layerEdits = createLayerEdits<TData, TLayer, TPose>({\n layers: state.layers,\n layerIndex: state.layerIndex,\n nodes: state.nodes,\n requireLayerIndex: layers.requireLayerIndex,\n record: recorder.record,\n lifted: locks.lifted,\n batch: recorder.batch,\n removeMany: treeEdits.removeMany,\n });\n\n const flagEdits = createNodeFlags<TData, TLayer, TPose>({\n register: env.ops.register,\n requireNode: store.requireNode,\n record: recorder.record,\n narrowSelection: () => env.narrowSelection(),\n });\n\n const declaredEdits = createDeclaredFieldSetters<TData, TLayer, TPose>({\n register: env.ops.register,\n requireNode: store.requireNode,\n refuse: locks.refuse,\n record: recorder.record,\n });\n\n const doc = createSceneDoc({ values: state.docValues, register: env.ops.register, record: recorder.record });\n\n return { construct, methods: { ...treeEdits, ...fieldEdits, ...layerEdits, ...flagEdits, ...declaredEdits, doc } };\n}\n"],"mappings":";;;;;;;;;AAuDA,SAAgB,iBACd,KACkC;CAClC,MAAM,EAAE,OAAO,OAAO,OAAO,QAAQ,UAAU,UAAU,aAAa;CAEtE,MAAM,EAAE,eAAe,eAAqC;EAC1D;EACA,UAAU,IAAI,IAAI;EAClB;EACA,mBAAmB,OAAO;EAC1B,mBAAmB,OAAO;EAC1B,iBAAiB,IAAI;EACrB;EACA,iBAAiB,IAAI;EACrB,eAAe,IAAI;CACrB,CAAC;CAED,MAAM,EAAE,WAAW,GAAG,cAAc,gBAAsC;EACxE,OAAO,MAAM;EACb,YAAY,IAAI;EAChB;EACA;EACA;EACA;EACA,OAAO,IAAI,IAAI;EACf;EACA;CACF,CAAC;CAED,MAAM,aAAa,iBAAuC;EACxD;EACA;EACA;EACA;EACA;EACA,aAAa,IAAI;EACjB,aAAa,SAAS,MAAM,MAAM,EAAE,KAAK,UAAU,QAAQ;EAC3D;CACF,CAAC;CAED,MAAM,aAAa,iBAAuC;EACxD,QAAQ,MAAM;EACd,YAAY,MAAM;EAClB,OAAO,MAAM;EACb,mBAAmB,OAAO;EAC1B,QAAQ,SAAS;EACjB,QAAQ,MAAM;EACd,OAAO,SAAS;EAChB,YAAY,UAAU;CACxB,CAAC;CAED,MAAM,YAAY,gBAAsC;EACtD,UAAU,IAAI,IAAI;EAClB,aAAa,MAAM;EACnB,QAAQ,SAAS;EACjB,uBAAuB,IAAI,gBAAgB;CAC7C,CAAC;CAED,MAAM,gBAAgB,2BAAiD;EACrE,UAAU,IAAI,IAAI;EAClB,aAAa,MAAM;EACnB,QAAQ,MAAM;EACd,QAAQ,SAAS;CACnB,CAAC;CAED,MAAM,MAAM,eAAe;EAAE,QAAQ,MAAM;EAAW,UAAU,IAAI,IAAI;EAAU,QAAQ,SAAS;CAAO,CAAC;CAE3G,OAAO;EAAE;EAAW,SAAS;GAAE,GAAG;GAAW,GAAG;GAAY,GAAG;GAAY,GAAG;GAAW,GAAG;GAAe;EAAI;CAAE;AACnH"}
@@ -41,8 +41,7 @@ function createSceneState(registry, initialLayers, layoutDeclared, initialDoc) {
41
41
  isLocked: (id) => locks.isLocked(id),
42
42
  isHidden: (id) => locks.isHidden(id),
43
43
  layoutOf(id) {
44
- const node = state.nodes.get(id);
45
- return node?.kind === "container" ? node.layout ?? null : null;
44
+ return state.nodes.get(id)?.layout ?? null;
46
45
  },
47
46
  toJSON: () => serializeScene(state, fnFields, docValues.toJSON())
48
47
  };
@@ -1 +1 @@
1
- {"version":3,"file":"sceneState.js","names":[],"sources":["../../../src/core/scene/sceneState.ts"],"sourcesContent":["/**\n * A scene's document state — its nodes, roots, and layers — with the modules\n * that read and write it directly.\n */\nimport { createDocStore, type SerializedDoc } from './docValues';\nimport { createLayerStack } from './layerStack';\nimport { createLockGuard } from './lockGuard';\nimport { createNodeFnFields } from './nodeFnFields';\nimport { createNodeStore } from './nodeStore';\nimport { createRenderOrder } from './renderOrder';\nimport { serializeScene } from './serialization';\nimport { createStaleReplay } from './staleReplay';\nimport { createTreeReads } from './treeReads';\nimport type { LayerRecord, Node, NodeId, Scene, SceneRegistry } from './types';\n\nexport function createSceneState<TData, TLayer extends string, TPose>(\n registry: SceneRegistry<TPose, TData, TLayer>,\n initialLayers: readonly LayerRecord<TLayer>[],\n /** Called when a node is given a layout. */\n layoutDeclared: () => void,\n initialDoc?: SerializedDoc,\n) {\n const state = {\n nodes: new Map<NodeId, Node<TData, TLayer, TPose>>(),\n roots: [] as NodeId[],\n layers: [] as LayerRecord<TLayer>[],\n layerIndex: new Map<TLayer, number>(),\n docValues: new Map<string, unknown>(),\n };\n const order = createRenderOrder(state);\n /** The registry-keyed function fields, each with its key maps. A function\n * cannot travel through a serializable op payload, so an op carries its key. */\n const fnFields = createNodeFnFields<TPose>(registry, (field) => {\n if (field === 'layout') layoutDeclared();\n });\n const store = createNodeStore(state, order.invalidate, fnFields);\n const layers = createLayerStack(state, order.invalidate);\n layers.reset(initialLayers);\n const locks = createLockGuard(state);\n const docValues = createDocStore(state.docValues);\n docValues.reset(initialDoc);\n\n const reads: Pick<\n Scene<TData, TLayer, TPose>,\n | 'get' | 'incarnation' | 'childrenOf' | 'ancestorsOf' | 'renderOrder' | 'renderOrderNodes'\n | 'nodesOnLayer' | 'isLocked' | 'isHidden' | 'layoutOf' | 'toJSON'\n > = {\n ...createTreeReads(state.nodes),\n renderOrder: order.ids,\n renderOrderNodes: order.nodes,\n nodesOnLayer: order.nodesOnLayer,\n isLocked: (id) => locks.isLocked(id),\n isHidden: (id) => locks.isHidden(id),\n layoutOf(id) {\n const node = state.nodes.get(id);\n return node?.kind === 'container' ? (node.layout ?? null) : null;\n },\n toJSON: () => serializeScene(state, fnFields, docValues.toJSON()),\n };\n\n return {\n state,\n order,\n locks,\n stale: createStaleReplay(state.nodes),\n fnFields,\n store,\n layers,\n reads,\n docValues,\n\n /** Empty the document and give it `records` as its layers and `values` as its document values. */\n reset(records: readonly LayerRecord<TLayer>[], values?: SerializedDoc): void {\n docValues.reset(values);\n store.dependents.clear();\n store.childDerived.clear();\n state.nodes.clear();\n state.roots.length = 0;\n layers.reset(records);\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;AAeA,SAAgB,iBACd,UACA,eAEA,gBACA,YACA;CACA,MAAM,QAAQ;EACZ,uBAAO,IAAI,IAAwC;EACnD,OAAO,CAAC;EACR,QAAQ,CAAC;EACT,4BAAY,IAAI,IAAoB;EACpC,2BAAW,IAAI,IAAqB;CACtC;CACA,MAAM,QAAQ,kBAAkB,KAAK;;;CAGrC,MAAM,WAAW,mBAA0B,WAAW,UAAU;EAC9D,IAAI,UAAU,UAAU,eAAe;CACzC,CAAC;CACD,MAAM,QAAQ,gBAAgB,OAAO,MAAM,YAAY,QAAQ;CAC/D,MAAM,SAAS,iBAAiB,OAAO,MAAM,UAAU;CACvD,OAAO,MAAM,aAAa;CAC1B,MAAM,QAAQ,gBAAgB,KAAK;CACnC,MAAM,YAAY,eAAe,MAAM,SAAS;CAChD,UAAU,MAAM,UAAU;CAE1B,MAAM,QAIF;EACF,GAAG,gBAAgB,MAAM,KAAK;EAC9B,aAAa,MAAM;EACnB,kBAAkB,MAAM;EACxB,cAAc,MAAM;EACpB,WAAW,OAAO,MAAM,SAAS,EAAE;EACnC,WAAW,OAAO,MAAM,SAAS,EAAE;EACnC,SAAS,IAAI;GACX,MAAM,OAAO,MAAM,MAAM,IAAI,EAAE;GAC/B,OAAO,MAAM,SAAS,cAAe,KAAK,UAAU,OAAQ;EAC9D;EACA,cAAc,eAAe,OAAO,UAAU,UAAU,OAAO,CAAC;CAClE;CAEA,OAAO;EACL;EACA;EACA;EACA,OAAO,kBAAkB,MAAM,KAAK;EACpC;EACA;EACA;EACA;EACA;;EAGA,MAAM,SAAyC,QAA8B;GAC3E,UAAU,MAAM,MAAM;GACtB,MAAM,WAAW,MAAM;GACvB,MAAM,aAAa,MAAM;GACzB,MAAM,MAAM,MAAM;GAClB,MAAM,MAAM,SAAS;GACrB,OAAO,MAAM,OAAO;EACtB;CACF;AACF"}
1
+ {"version":3,"file":"sceneState.js","names":[],"sources":["../../../src/core/scene/sceneState.ts"],"sourcesContent":["/**\n * A scene's document state — its nodes, roots, and layers — with the modules\n * that read and write it directly.\n */\nimport { createDocStore, type SerializedDoc } from './docValues';\nimport { createLayerStack } from './layerStack';\nimport { createLockGuard } from './lockGuard';\nimport { createNodeFnFields } from './nodeFnFields';\nimport { createNodeStore } from './nodeStore';\nimport { createRenderOrder } from './renderOrder';\nimport { serializeScene } from './serialization';\nimport { createStaleReplay } from './staleReplay';\nimport { createTreeReads } from './treeReads';\nimport type { LayerRecord, Node, NodeId, Scene, SceneRegistry } from './types';\n\nexport function createSceneState<TData, TLayer extends string, TPose>(\n registry: SceneRegistry<TPose, TData, TLayer>,\n initialLayers: readonly LayerRecord<TLayer>[],\n /** Called when a node is given a layout. */\n layoutDeclared: () => void,\n initialDoc?: SerializedDoc,\n) {\n const state = {\n nodes: new Map<NodeId, Node<TData, TLayer, TPose>>(),\n roots: [] as NodeId[],\n layers: [] as LayerRecord<TLayer>[],\n layerIndex: new Map<TLayer, number>(),\n docValues: new Map<string, unknown>(),\n };\n const order = createRenderOrder(state);\n /** The registry-keyed function fields, each with its key maps. A function\n * cannot travel through a serializable op payload, so an op carries its key. */\n const fnFields = createNodeFnFields<TPose>(registry, (field) => {\n if (field === 'layout') layoutDeclared();\n });\n const store = createNodeStore(state, order.invalidate, fnFields);\n const layers = createLayerStack(state, order.invalidate);\n layers.reset(initialLayers);\n const locks = createLockGuard(state);\n const docValues = createDocStore(state.docValues);\n docValues.reset(initialDoc);\n\n const reads: Pick<\n Scene<TData, TLayer, TPose>,\n | 'get' | 'incarnation' | 'childrenOf' | 'ancestorsOf' | 'renderOrder' | 'renderOrderNodes'\n | 'nodesOnLayer' | 'isLocked' | 'isHidden' | 'layoutOf' | 'toJSON'\n > = {\n ...createTreeReads(state.nodes),\n renderOrder: order.ids,\n renderOrderNodes: order.nodes,\n nodesOnLayer: order.nodesOnLayer,\n isLocked: (id) => locks.isLocked(id),\n isHidden: (id) => locks.isHidden(id),\n layoutOf(id) {\n const node = state.nodes.get(id);\n return node?.layout ?? null;\n },\n toJSON: () => serializeScene(state, fnFields, docValues.toJSON()),\n };\n\n return {\n state,\n order,\n locks,\n stale: createStaleReplay(state.nodes),\n fnFields,\n store,\n layers,\n reads,\n docValues,\n\n /** Empty the document and give it `records` as its layers and `values` as its document values. */\n reset(records: readonly LayerRecord<TLayer>[], values?: SerializedDoc): void {\n docValues.reset(values);\n store.dependents.clear();\n store.childDerived.clear();\n state.nodes.clear();\n state.roots.length = 0;\n layers.reset(records);\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;AAeA,SAAgB,iBACd,UACA,eAEA,gBACA,YACA;CACA,MAAM,QAAQ;EACZ,uBAAO,IAAI,IAAwC;EACnD,OAAO,CAAC;EACR,QAAQ,CAAC;EACT,4BAAY,IAAI,IAAoB;EACpC,2BAAW,IAAI,IAAqB;CACtC;CACA,MAAM,QAAQ,kBAAkB,KAAK;;;CAGrC,MAAM,WAAW,mBAA0B,WAAW,UAAU;EAC9D,IAAI,UAAU,UAAU,eAAe;CACzC,CAAC;CACD,MAAM,QAAQ,gBAAgB,OAAO,MAAM,YAAY,QAAQ;CAC/D,MAAM,SAAS,iBAAiB,OAAO,MAAM,UAAU;CACvD,OAAO,MAAM,aAAa;CAC1B,MAAM,QAAQ,gBAAgB,KAAK;CACnC,MAAM,YAAY,eAAe,MAAM,SAAS;CAChD,UAAU,MAAM,UAAU;CAE1B,MAAM,QAIF;EACF,GAAG,gBAAgB,MAAM,KAAK;EAC9B,aAAa,MAAM;EACnB,kBAAkB,MAAM;EACxB,cAAc,MAAM;EACpB,WAAW,OAAO,MAAM,SAAS,EAAE;EACnC,WAAW,OAAO,MAAM,SAAS,EAAE;EACnC,SAAS,IAAI;GAEX,OADa,MAAM,MAAM,IAAI,EACtB,CAAA,EAAM,UAAU;EACzB;EACA,cAAc,eAAe,OAAO,UAAU,UAAU,OAAO,CAAC;CAClE;CAEA,OAAO;EACL;EACA;EACA;EACA,OAAO,kBAAkB,MAAM,KAAK;EACpC;EACA;EACA;EACA;EACA;;EAGA,MAAM,SAAyC,QAA8B;GAC3E,UAAU,MAAM,MAAM;GACtB,MAAM,WAAW,MAAM;GACvB,MAAM,aAAa,MAAM;GACzB,MAAM,MAAM,MAAM;GAClB,MAAM,MAAM,SAAS;GACrB,OAAO,MAAM,OAAO;EACtB;CACF;AACF"}