@weasel-js/core 1.11.1 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (297) hide show
  1. package/CHANGELOG.md +184 -0
  2. package/dist/WeaselProvider.js +1 -1
  3. package/dist/affordances/pathAnchors.js +10 -6
  4. package/dist/affordances/pathAnchors.js.map +1 -1
  5. package/dist/barrel/canvas.js +3 -1
  6. package/dist/barrel/paint.js +4 -4
  7. package/dist/barrel/paths.js +8 -4
  8. package/dist/barrel/renderer.js +3 -2
  9. package/dist/barrel/routing.js +4 -4
  10. package/dist/barrel/scene.js +2 -1
  11. package/dist/barrel/text.js +2 -2
  12. package/dist/barrel/viewport.js +6 -2
  13. package/dist/canvas/Canvas.js +2 -1
  14. package/dist/canvas/Canvas.js.map +1 -1
  15. package/dist/canvas/CanvasView.js +23 -34
  16. package/dist/canvas/CanvasView.js.map +1 -1
  17. package/dist/canvas/ModalityHud.js +1 -1
  18. package/dist/canvas/NodeShape.js +4 -1
  19. package/dist/canvas/NodeShape.js.map +1 -1
  20. package/dist/canvas/SceneCanvas/ActionsProviderIfRoot.js +1 -1
  21. package/dist/canvas/SceneCanvas/DepRegistryProviderIfRoot.js +1 -1
  22. package/dist/canvas/SceneCanvas/PointerProviderIfRoot.js +1 -1
  23. package/dist/canvas/SceneCanvas/SurfaceDeclarations.js +23 -0
  24. package/dist/canvas/SceneCanvas/SurfaceDeclarations.js.map +1 -0
  25. package/dist/canvas/SceneCanvas/canvasPassThrough.js +1 -0
  26. package/dist/canvas/SceneCanvas/canvasPassThrough.js.map +1 -1
  27. package/dist/canvas/SceneCanvas/props.js.map +1 -1
  28. package/dist/canvas/SceneCanvas/scenePointQuery.js +7 -3
  29. package/dist/canvas/SceneCanvas/scenePointQuery.js.map +1 -1
  30. package/dist/canvas/SceneCanvas/useActionTunings.js +6 -1
  31. package/dist/canvas/SceneCanvas/useActionTunings.js.map +1 -1
  32. package/dist/canvas/SceneCanvas/useCanvasDevtoolsRoot.js +1 -0
  33. package/dist/canvas/SceneCanvas/useCanvasDevtoolsRoot.js.map +1 -1
  34. package/dist/canvas/SceneCanvas/useCanvasTools.js +7 -2
  35. package/dist/canvas/SceneCanvas/useCanvasTools.js.map +1 -1
  36. package/dist/canvas/SceneCanvas/useSceneCanvasHandle.js +9 -3
  37. package/dist/canvas/SceneCanvas/useSceneCanvasHandle.js.map +1 -1
  38. package/dist/canvas/SceneCanvas/useSceneCanvasLayers.js +10 -2
  39. package/dist/canvas/SceneCanvas/useSceneCanvasLayers.js.map +1 -1
  40. package/dist/canvas/SceneCanvas/useSceneCanvasTools.js +1 -0
  41. package/dist/canvas/SceneCanvas/useSceneCanvasTools.js.map +1 -1
  42. package/dist/canvas/SceneCanvas/useSceneGestureDispatcher.js +19 -4
  43. package/dist/canvas/SceneCanvas/useSceneGestureDispatcher.js.map +1 -1
  44. package/dist/canvas/SceneCanvas/useSceneStandardActions.js +2 -1
  45. package/dist/canvas/SceneCanvas/useSceneStandardActions.js.map +1 -1
  46. package/dist/canvas/SceneCanvas.js +11 -10
  47. package/dist/canvas/SceneCanvas.js.map +1 -1
  48. package/dist/canvas/ViewMarker.js +35 -0
  49. package/dist/canvas/ViewMarker.js.map +1 -0
  50. package/dist/canvas/defaultDrawOne.js +2 -0
  51. package/dist/canvas/defaultDrawOne.js.map +1 -1
  52. package/dist/canvas/deps/booleans.js +2 -1
  53. package/dist/canvas/deps/booleans.js.map +1 -1
  54. package/dist/canvas/deps/hitTestArea.js +2 -2
  55. package/dist/canvas/deps/hitTestArea.js.map +1 -1
  56. package/dist/canvas/deps/insert.js +6 -2
  57. package/dist/canvas/deps/insert.js.map +1 -1
  58. package/dist/canvas/deps/pathEditCommits.js +3 -7
  59. package/dist/canvas/deps/pathEditCommits.js.map +1 -1
  60. package/dist/canvas/drawnBounds.js +11 -3
  61. package/dist/canvas/drawnBounds.js.map +1 -1
  62. package/dist/canvas/inheritedPaints.js +44 -0
  63. package/dist/canvas/inheritedPaints.js.map +1 -0
  64. package/dist/canvas/nodeShapes/derivedPainter.js +15 -16
  65. package/dist/canvas/nodeShapes/derivedPainter.js.map +1 -1
  66. package/dist/canvas/nodeShapes/nodePaint.js +13 -34
  67. package/dist/canvas/nodeShapes/nodePaint.js.map +1 -1
  68. package/dist/canvas/nodeShapes/pathPainter.js +9 -8
  69. package/dist/canvas/nodeShapes/pathPainter.js.map +1 -1
  70. package/dist/canvas/nodeShapes/rectFallbackPainter.js +4 -5
  71. package/dist/canvas/nodeShapes/rectFallbackPainter.js.map +1 -1
  72. package/dist/canvas/nodeShapes/shapePainter.js +11 -10
  73. package/dist/canvas/nodeShapes/shapePainter.js.map +1 -1
  74. package/dist/canvas/nodeShapes/textPainter.js +13 -12
  75. package/dist/canvas/nodeShapes/textPainter.js.map +1 -1
  76. package/dist/canvas/pickReach.js +3 -3
  77. package/dist/canvas/pickReach.js.map +1 -1
  78. package/dist/canvas/pickWalk.js +3 -1
  79. package/dist/canvas/pickWalk.js.map +1 -1
  80. package/dist/canvas/renderSceneToPixels.js +1 -1
  81. package/dist/canvas/renderSceneToPixels.js.map +1 -1
  82. package/dist/canvas/sceneSlotWiring.js +2 -1
  83. package/dist/canvas/sceneSlotWiring.js.map +1 -1
  84. package/dist/canvas/sceneViewRender.js +2 -1
  85. package/dist/canvas/sceneViewRender.js.map +1 -1
  86. package/dist/canvas/surfaceContribution.js +12 -8
  87. package/dist/canvas/surfaceContribution.js.map +1 -1
  88. package/dist/canvas/useCanvasView.js +4 -2
  89. package/dist/canvas/useCanvasView.js.map +1 -1
  90. package/dist/canvas/useLayerStack.js +6 -2
  91. package/dist/canvas/useLayerStack.js.map +1 -1
  92. package/dist/canvas/useViewAim.js +121 -0
  93. package/dist/canvas/useViewAim.js.map +1 -0
  94. package/dist/canvas/useViewNesting.js +3 -2
  95. package/dist/canvas/useViewNesting.js.map +1 -1
  96. package/dist/canvas/useViewRegistrySurface.js +4 -2
  97. package/dist/canvas/useViewRegistrySurface.js.map +1 -1
  98. package/dist/canvas/viewMarkers.js +63 -0
  99. package/dist/canvas/viewMarkers.js.map +1 -0
  100. package/dist/canvas/viewRegistry.js +27 -1
  101. package/dist/canvas/viewRegistry.js.map +1 -1
  102. package/dist/core/pages/pageEdits.js +2 -3
  103. package/dist/core/pages/pageEdits.js.map +1 -1
  104. package/dist/core/paint/meshKindLazy.js +59 -2
  105. package/dist/core/paint/meshKindLazy.js.map +1 -1
  106. package/dist/core/scene/clipNothing.js +13 -0
  107. package/dist/core/scene/clipNothing.js.map +1 -0
  108. package/dist/core/scene/depMemo.js +11 -3
  109. package/dist/core/scene/depMemo.js.map +1 -1
  110. package/dist/core/scene/dependents.js +7 -1
  111. package/dist/core/scene/dependents.js.map +1 -1
  112. package/dist/core/scene/derivedParts.js +10 -11
  113. package/dist/core/scene/derivedParts.js.map +1 -1
  114. package/dist/core/scene/effectivePose.js +7 -2
  115. package/dist/core/scene/effectivePose.js.map +1 -1
  116. package/dist/core/scene/fontReads.js +37 -0
  117. package/dist/core/scene/fontReads.js.map +1 -0
  118. package/dist/core/scene/inheritedPaint.js +75 -0
  119. package/dist/core/scene/inheritedPaint.js.map +1 -0
  120. package/dist/core/scene/kitRegistry.js +5 -0
  121. package/dist/core/scene/kitRegistry.js.map +1 -1
  122. package/dist/core/scene/nodeMemo.js +15 -7
  123. package/dist/core/scene/nodeMemo.js.map +1 -1
  124. package/dist/core/scene/pathEffects/blend.js.map +1 -1
  125. package/dist/core/scene/pathEffects/derive.js +7 -2
  126. package/dist/core/scene/pathEffects/derive.js.map +1 -1
  127. package/dist/core/scene/pathEffects/effects.js +3 -1
  128. package/dist/core/scene/pathEffects/effects.js.map +1 -1
  129. package/dist/core/scene/pathEffects/textAlong.js +95 -0
  130. package/dist/core/scene/pathEffects/textAlong.js.map +1 -0
  131. package/dist/{cornerHandles-BHuoMs1o.d.ts → cornerHandles-p8fDIAKt.d.ts} +2 -1
  132. package/dist/{depSchema-BpZNOv7q.d.ts → depSchema-mG19aEdD.d.ts} +61 -13
  133. package/dist/devtools/registry.js +4 -4
  134. package/dist/devtools/registry.js.map +1 -1
  135. package/dist/features/measure/measureContribution.js +4 -2
  136. package/dist/features/measure/measureContribution.js.map +1 -1
  137. package/dist/features/modes/modeTriggers.js +7 -3
  138. package/dist/features/modes/modeTriggers.js.map +1 -1
  139. package/dist/features/pathEffects/effectSources.js +60 -0
  140. package/dist/features/pathEffects/effectSources.js.map +1 -0
  141. package/dist/features/pathEffects/readSource.js +17 -1
  142. package/dist/features/pathEffects/readSource.js.map +1 -1
  143. package/dist/features/pathEffects/textItems.js +53 -0
  144. package/dist/features/pathEffects/textItems.js.map +1 -0
  145. package/dist/features/pathEffects/useEffectSourcesLayer.js +57 -0
  146. package/dist/features/pathEffects/useEffectSourcesLayer.js.map +1 -0
  147. package/dist/features/paths/pathEditState.js +5 -2
  148. package/dist/features/paths/pathEditState.js.map +1 -1
  149. package/dist/features/text/sceneTextDrafts.js +65 -0
  150. package/dist/features/text/sceneTextDrafts.js.map +1 -0
  151. package/dist/features/text/textHitAt.js +31 -2
  152. package/dist/features/text/textHitAt.js.map +1 -1
  153. package/dist/features/text/useSceneTextEdit.js +22 -18
  154. package/dist/features/text/useSceneTextEdit.js.map +1 -1
  155. package/dist/features/viewports/aim.js +105 -0
  156. package/dist/features/viewports/aim.js.map +1 -0
  157. package/dist/features/viewports/viewMarker.js +82 -0
  158. package/dist/features/viewports/viewMarker.js.map +1 -0
  159. package/dist/features/viewports/viewMarkerDrag.js +55 -0
  160. package/dist/features/viewports/viewMarkerDrag.js.map +1 -0
  161. package/dist/index.d.ts +500 -201
  162. package/dist/index.js +23 -12
  163. package/dist/interactions/actions/booleans/booleans.js +2 -1
  164. package/dist/interactions/actions/booleans/booleans.js.map +1 -1
  165. package/dist/interactions/actions/booleans/operands.js +6 -5
  166. package/dist/interactions/actions/booleans/operands.js.map +1 -1
  167. package/dist/interactions/actions/clipboard/action.js +27 -17
  168. package/dist/interactions/actions/clipboard/action.js.map +1 -1
  169. package/dist/interactions/actions/defaults/anchorEditing.js +44 -23
  170. package/dist/interactions/actions/defaults/anchorEditing.js.map +1 -1
  171. package/dist/interactions/actions/defaults/anchorJoin.js +12 -6
  172. package/dist/interactions/actions/defaults/anchorJoin.js.map +1 -1
  173. package/dist/interactions/actions/defaults/bendSegment.js +7 -4
  174. package/dist/interactions/actions/defaults/bendSegment.js.map +1 -1
  175. package/dist/interactions/actions/defaults/cancelGesture.js +11 -8
  176. package/dist/interactions/actions/defaults/cancelGesture.js.map +1 -1
  177. package/dist/interactions/actions/defaults/contextMenu.js +17 -8
  178. package/dist/interactions/actions/defaults/contextMenu.js.map +1 -1
  179. package/dist/interactions/actions/defaults/createPaintAction.js +1 -1
  180. package/dist/interactions/actions/defaults/createPaintAction.js.map +1 -1
  181. package/dist/interactions/actions/defaults/delete.js +28 -14
  182. package/dist/interactions/actions/defaults/delete.js.map +1 -1
  183. package/dist/interactions/actions/defaults/duplicate.js +11 -8
  184. package/dist/interactions/actions/defaults/duplicate.js.map +1 -1
  185. package/dist/interactions/actions/defaults/enterPathEdit.js +19 -9
  186. package/dist/interactions/actions/defaults/enterPathEdit.js.map +1 -1
  187. package/dist/interactions/actions/defaults/escape.js +11 -8
  188. package/dist/interactions/actions/defaults/escape.js.map +1 -1
  189. package/dist/interactions/actions/defaults/exitPathEdit.js +7 -4
  190. package/dist/interactions/actions/defaults/exitPathEdit.js.map +1 -1
  191. package/dist/interactions/actions/defaults/group.js +19 -13
  192. package/dist/interactions/actions/defaults/group.js.map +1 -1
  193. package/dist/interactions/actions/defaults/ingest.js +10 -4
  194. package/dist/interactions/actions/defaults/ingest.js.map +1 -1
  195. package/dist/interactions/actions/defaults/insertPathAnchor.js +7 -4
  196. package/dist/interactions/actions/defaults/insertPathAnchor.js.map +1 -1
  197. package/dist/interactions/actions/defaults/nudge.js +8 -2
  198. package/dist/interactions/actions/defaults/nudge.js.map +1 -1
  199. package/dist/interactions/actions/defaults/pathEditTools.js +11 -8
  200. package/dist/interactions/actions/defaults/pathEditTools.js.map +1 -1
  201. package/dist/interactions/actions/defaults/pinchZoom.js +7 -4
  202. package/dist/interactions/actions/defaults/pinchZoom.js.map +1 -1
  203. package/dist/interactions/actions/defaults/reorder.js +24 -6
  204. package/dist/interactions/actions/defaults/reorder.js.map +1 -1
  205. package/dist/interactions/actions/defaults/selectAll.js +8 -5
  206. package/dist/interactions/actions/defaults/selectAll.js.map +1 -1
  207. package/dist/interactions/actions/defaults/setFill.js +7 -3
  208. package/dist/interactions/actions/defaults/setFill.js.map +1 -1
  209. package/dist/interactions/actions/defaults/setFillOpacity.js +1 -1
  210. package/dist/interactions/actions/defaults/setStroke.js +12 -11
  211. package/dist/interactions/actions/defaults/setStroke.js.map +1 -1
  212. package/dist/interactions/actions/defaults/setStrokeOpacity.js +1 -1
  213. package/dist/interactions/actions/defaults/toolActivate.js +7 -1
  214. package/dist/interactions/actions/defaults/toolActivate.js.map +1 -1
  215. package/dist/interactions/actions/defaults/toolResetToDefault.js +7 -4
  216. package/dist/interactions/actions/defaults/toolResetToDefault.js.map +1 -1
  217. package/dist/interactions/actions/defaults/undoRedo.js +19 -13
  218. package/dist/interactions/actions/defaults/undoRedo.js.map +1 -1
  219. package/dist/interactions/actions/defaults/viewportWheelPan.js +16 -7
  220. package/dist/interactions/actions/defaults/viewportWheelPan.js.map +1 -1
  221. package/dist/interactions/actions/defaults/viewportZoom.js +24 -6
  222. package/dist/interactions/actions/defaults/viewportZoom.js.map +1 -1
  223. package/dist/interactions/actions/edit-anchors/action.js +7 -4
  224. package/dist/interactions/actions/edit-anchors/action.js.map +1 -1
  225. package/dist/interactions/actions/flip/action.js +8 -2
  226. package/dist/interactions/actions/flip/action.js.map +1 -1
  227. package/dist/interactions/actions/insert/repositionAction.js +2 -1
  228. package/dist/interactions/actions/insert/repositionAction.js.map +1 -1
  229. package/dist/interactions/actions/move/action.js +7 -4
  230. package/dist/interactions/actions/move/action.js.map +1 -1
  231. package/dist/interactions/actions/outlines/createOutlinesAction.js +8 -5
  232. package/dist/interactions/actions/outlines/createOutlinesAction.js.map +1 -1
  233. package/dist/interactions/actions/outlines/outlineStroke.js +5 -3
  234. package/dist/interactions/actions/outlines/outlineStroke.js.map +1 -1
  235. package/dist/interactions/actions/pathCommands/cutJoin.js +8 -5
  236. package/dist/interactions/actions/pathCommands/cutJoin.js.map +1 -1
  237. package/dist/interactions/actions/resize/cornerHandles.js +2 -1
  238. package/dist/interactions/actions/resize/cornerHandles.js.map +1 -1
  239. package/dist/interactions/actions/selectionSubtree.js +12 -7
  240. package/dist/interactions/actions/selectionSubtree.js.map +1 -1
  241. package/dist/interactions/actions/transform-again/action.js +8 -5
  242. package/dist/interactions/actions/transform-again/action.js.map +1 -1
  243. package/dist/json.d.ts +4 -0
  244. package/dist/json.js +3 -0
  245. package/dist/kitSettingsSchema.js +8 -1
  246. package/dist/kitSettingsSchema.js.map +1 -1
  247. package/dist/{math-CrJULaZI.d.ts → math-B56zxVs5.d.ts} +10 -1
  248. package/dist/math.d.ts +1 -1
  249. package/dist/math.js +2 -1
  250. package/dist/resize.d.ts +1 -1
  251. package/dist/test-seams.d.ts +2 -2
  252. package/dist/tools/builtin/ellipse/useEllipseTool.js +1 -0
  253. package/dist/tools/builtin/ellipse/useEllipseTool.js.map +1 -1
  254. package/dist/tools/builtin/eyedropper/useEyedropperTool.js +2 -1
  255. package/dist/tools/builtin/eyedropper/useEyedropperTool.js.map +1 -1
  256. package/dist/tools/builtin/hand/dragPanContribution.js +4 -1
  257. package/dist/tools/builtin/hand/dragPanContribution.js.map +1 -1
  258. package/dist/tools/builtin/hand/useHandTool.js +7 -4
  259. package/dist/tools/builtin/hand/useHandTool.js.map +1 -1
  260. package/dist/tools/builtin/image/useImageTool.js +1 -0
  261. package/dist/tools/builtin/image/useImageTool.js.map +1 -1
  262. package/dist/tools/builtin/lasso/useLassoTool.js +1 -0
  263. package/dist/tools/builtin/lasso/useLassoTool.js.map +1 -1
  264. package/dist/tools/builtin/line/useLineTool.js +1 -0
  265. package/dist/tools/builtin/line/useLineTool.js.map +1 -1
  266. package/dist/tools/builtin/measure/useMeasureTool.js +12 -5
  267. package/dist/tools/builtin/measure/useMeasureTool.js.map +1 -1
  268. package/dist/tools/builtin/pen/usePenTool.js +14 -6
  269. package/dist/tools/builtin/pen/usePenTool.js.map +1 -1
  270. package/dist/tools/builtin/pencil/usePencilTool.js +1 -0
  271. package/dist/tools/builtin/pencil/usePencilTool.js.map +1 -1
  272. package/dist/tools/builtin/polygon/usePolygonTool.js +13 -4
  273. package/dist/tools/builtin/polygon/usePolygonTool.js.map +1 -1
  274. package/dist/tools/builtin/rect/useRectTool.js +1 -0
  275. package/dist/tools/builtin/rect/useRectTool.js.map +1 -1
  276. package/dist/tools/builtin/select/adapterPointQuery.js +3 -2
  277. package/dist/tools/builtin/select/adapterPointQuery.js.map +1 -1
  278. package/dist/tools/builtin/select/pressDecision.js +32 -0
  279. package/dist/tools/builtin/select/pressDecision.js.map +1 -0
  280. package/dist/tools/builtin/select/selectionContributions.js +22 -13
  281. package/dist/tools/builtin/select/selectionContributions.js.map +1 -1
  282. package/dist/tools/builtin/select/useSelectTool.js +13 -10
  283. package/dist/tools/builtin/select/useSelectTool.js.map +1 -1
  284. package/dist/tools/builtin/slice/useSliceTool.js +22 -8
  285. package/dist/tools/builtin/slice/useSliceTool.js.map +1 -1
  286. package/dist/tools/builtin/star/useStarTool.js +13 -4
  287. package/dist/tools/builtin/star/useStarTool.js.map +1 -1
  288. package/dist/tools/builtin/sticky/stickyNode.js +4 -1
  289. package/dist/tools/builtin/sticky/stickyNode.js.map +1 -1
  290. package/dist/tools/builtin/sticky/useStickyTool.js +10 -5
  291. package/dist/tools/builtin/sticky/useStickyTool.js.map +1 -1
  292. package/dist/tools/builtin/text/useTextTool.js +3 -1
  293. package/dist/tools/builtin/text/useTextTool.js.map +1 -1
  294. package/dist/util/paint.js +1 -3
  295. package/dist/util/paint.js.map +1 -1
  296. package/dist/version.js +1 -1
  297. package/package.json +28 -24
@@ -1,11 +1,11 @@
1
1
  import { nodeMemo } from "../../core/scene/nodeMemo.js";
2
- import { DEFAULT_SHAPE_FILL } from "../../util/paint.js";
3
2
  import { PAINT_SLOT } from "./memo.js";
4
- import { inkReach, resolveNodeFill, resolveNodeStroke, withAnimatedVertexColors } from "./nodePaint.js";
3
+ import { CONTAINER_INK, inkReach, withAnimatedVertexColors } from "./nodePaint.js";
5
4
  import { outsetPoseBox, strokedPoseBox } from "./strokedBounds.js";
6
5
  import { paintsPose, poseBoxBounds } from "./poseBox.js";
7
6
  import { commandFill, commandStroke } from "@weasel-js/renderer";
8
7
  import { ellipsePath, regularPolygonPath, starPath } from "@weasel-js/geom";
8
+ import { resolveFill, resolveStroke } from "@weasel-js/paint";
9
9
 
10
10
  //#region src/canvas/nodeShapes/shapePainter.ts
11
11
  /** Built-in shape dispatcher — matches when `data.shape` names a kit-known
@@ -19,12 +19,12 @@ var SHAPE_PAINTER = {
19
19
  const s = node.data?.shape;
20
20
  return s != null && SHAPE_KINDS.has(s);
21
21
  },
22
- paint: (node, pose, ctx) => withAnimatedVertexColors(nodeMemo(node, PAINT_SLOT, pose, () => {
22
+ paint: (node, pose, ctx) => node.kind === "container" ? [] : withAnimatedVertexColors(nodeMemo(node, PAINT_SLOT, pose, () => {
23
23
  const d = node.data;
24
24
  const path = pathForShape(d, pose);
25
- const declaredFill = resolveNodeFill(d.fill, DEFAULT_SHAPE_FILL);
25
+ const declaredFill = resolveFill(d.fill, ctx?.inherited?.fill);
26
26
  const shapeFill = declaredFill && commandFill(declaredFill, pose);
27
- const strokeSpec = resolveNodeStroke(d.stroke);
27
+ const strokeSpec = resolveStroke(d.stroke, ctx?.inherited?.stroke);
28
28
  const stroke = strokeSpec && commandStroke(strokeSpec, pose);
29
29
  return [{
30
30
  kind: "path",
@@ -32,22 +32,23 @@ var SHAPE_PAINTER = {
32
32
  ...shapeFill ? { fill: shapeFill } : {},
33
33
  ...stroke ? { stroke } : {}
34
34
  }];
35
- }), ctx),
35
+ }, ctx?.inherited), ctx),
36
36
  silhouette: (node, pose) => {
37
37
  const d = node.data;
38
38
  return pathForShape(d, pose);
39
39
  },
40
40
  ink: (node, _pose, ctx) => {
41
+ if (node.kind === "container") return CONTAINER_INK;
41
42
  const d = node.data;
42
43
  return {
43
- filled: resolveNodeFill(d.fill, DEFAULT_SHAPE_FILL) !== null,
44
- ...inkReach(resolveNodeStroke(d.stroke), ctx?.scale)
44
+ filled: resolveFill(d.fill, ctx?.inherited?.fill) !== null,
45
+ ...inkReach(resolveStroke(d.stroke, ctx?.inherited?.stroke), ctx?.scale)
45
46
  };
46
47
  },
47
- bounds: (node, pose, ctx) => poseBoxBounds(pose, resolveNodeStroke(node.data.stroke), ctx),
48
+ bounds: (node, pose, ctx) => poseBoxBounds(pose, resolveStroke(node.data.stroke, ctx?.inherited?.stroke), ctx),
48
49
  drawnBounds: (node, pose, ctx) => {
49
50
  const d = node.data;
50
- const stroke = resolveNodeStroke(d.stroke);
51
+ const stroke = resolveStroke(d.stroke, ctx?.inherited?.stroke);
51
52
  return d.shape === "polygon" || d.shape === "star" ? strokedPoseBox(node, pose, stroke, ctx, (at) => pathForShape(d, at)) : outsetPoseBox(pose, stroke, ctx);
52
53
  }
53
54
  };
@@ -1 +1 @@
1
- {"version":3,"file":"shapePainter.js","names":[],"sources":["../../../src/canvas/nodeShapes/shapePainter.ts"],"sourcesContent":["import {\n ellipsePath,\n regularPolygonPath,\n starPath,\n type Path\n} from '@weasel-js/geom';\nimport type { Stroke, Fill } from '@weasel-js/paint';\nimport type { NodeShapeEntry } from 'canvas/NodeShape';\nimport { nodeMemo } from 'core/scene/nodeMemo';\nimport type { RectPose } from 'core/scene/types';\nimport { commandFill, commandStroke } from '@weasel-js/renderer';\nimport { DEFAULT_SHAPE_FILL } from '../../util/paint';\nimport { PAINT_SLOT } from './memo';\nimport { inkReach, withAnimatedVertexColors, resolveNodeFill, resolveNodeStroke } from './nodePaint';\nimport { paintsPose, poseBoxBounds } from './poseBox';\nimport { outsetPoseBox, strokedPoseBox } from './strokedBounds';\n\n// ─── Built-in painters ─────────────────────────────────────────────────\n/** Built-in shape dispatcher — matches when `data.shape` names a kit-known\n * shape kind. Computes both paint and silhouette from the pose so consumer\n * scenes can declare clipping/rendering directly in JSON without\n * registering a custom painter. */\nexport const SHAPE_PAINTER: NodeShapeEntry<unknown, RectPose> = {\n id: 'kit:shape',\n matches: (node) => {\n if (!paintsPose(node.pose)) return false;\n const s = (node.data as { shape?: string } | null)?.shape;\n return s != null && SHAPE_KINDS.has(s);\n },\n // Memoized: a pure function of `(data, pose)`, and `pathForShape` builds a\n // fresh `Uint8Array` + `Float32Array` for every ellipse, polygon and star.\n // See PAINT_SLOT.\n paint: (node, pose, ctx) => withAnimatedVertexColors(nodeMemo(node, PAINT_SLOT, pose, () => {\n const d = node.data as { shape: string; fill?: Fill | null; stroke?: Stroke | null; sides?: number; points?: number };\n const path = pathForShape(d, pose);\n const declaredFill = resolveNodeFill(d.fill, DEFAULT_SHAPE_FILL);\n const shapeFill = declaredFill && commandFill(declaredFill, pose);\n const strokeSpec = resolveNodeStroke(d.stroke);\n const stroke = strokeSpec && commandStroke(strokeSpec, pose);\n return [{\n kind: 'path',\n path,\n ...(shapeFill ? { fill: shapeFill } : {}),\n ...(stroke ? { stroke } : {}),\n }];\n }), ctx),\n silhouette: (node, pose) => {\n const d = node.data as { shape: string; sides?: number; points?: number };\n return pathForShape(d, pose);\n },\n // `paint` above emits the default fill when none is declared, so a shape\n // is filled unless it declares `fill: null` — unlike `kit:path`, it has no\n // implicit stroke-only form.\n ink: (node, _pose, ctx) => {\n const d = node.data as { fill?: Fill | null; stroke?: Stroke | null };\n return {\n filled: resolveNodeFill(d.fill, DEFAULT_SHAPE_FILL) !== null,\n ...inkReach(resolveNodeStroke(d.stroke), ctx?.scale),\n };\n },\n // Every shape kind is inscribed in the pose box.\n bounds: (node, pose, ctx) =>\n poseBoxBounds(pose, resolveNodeStroke((node.data as { stroke?: Stroke | null }).stroke), ctx),\n // A rect's or an ellipse's stroke reaches its box evenly; a polygon's or a\n // star's corners are sharper than a right angle, so their miters are measured.\n drawnBounds: (node, pose, ctx) => {\n const d = node.data as { shape: string; stroke?: Stroke | null; sides?: number; points?: number };\n const stroke = resolveNodeStroke(d.stroke);\n return d.shape === 'polygon' || d.shape === 'star'\n ? strokedPoseBox(node, pose, stroke, ctx, (at) => pathForShape(d, at))\n : outsetPoseBox(pose, stroke, ctx);\n },\n};\n\nexport const SHAPE_KINDS = new Set(['rect', 'ellipse', 'polygon', 'star']);\n\nexport function pathForShape(\n d: { shape: string; sides?: number; points?: number },\n p: RectPose,\n): Path {\n switch (d.shape) {\n case 'ellipse':\n return ellipsePath(p);\n case 'polygon': {\n const cx = p.x + p.width / 2;\n const cy = p.y + p.height / 2;\n const r = Math.min(p.width, p.height) / 2;\n return regularPolygonPath({ x: cx, y: cy }, r, d.sides ?? 6);\n }\n case 'star': {\n const cx = p.x + p.width / 2;\n const cy = p.y + p.height / 2;\n const r = Math.min(p.width, p.height) / 2;\n return starPath({ x: cx, y: cy }, r, d.points ?? 5);\n }\n case 'rect':\n default:\n return { kind: 'rect', x: p.x, y: p.y, width: p.width, height: p.height };\n }\n}\n"],"mappings":";;;;;;;;;;;;;;AAsBA,IAAa,gBAAmD;CAC9D,IAAI;CACJ,UAAU,SAAS;EACjB,IAAI,CAAC,WAAW,KAAK,IAAI,GAAG,OAAO;EACnC,MAAM,IAAK,KAAK,MAAoC;EACpD,OAAO,KAAK,QAAQ,YAAY,IAAI,CAAC;CACvC;CAIA,QAAQ,MAAM,MAAM,QAAQ,yBAAyB,SAAS,MAAM,YAAY,YAAY;EAC1F,MAAM,IAAI,KAAK;EACf,MAAM,OAAO,aAAa,GAAG,IAAI;EACjC,MAAM,eAAe,gBAAgB,EAAE,MAAM,kBAAkB;EAC/D,MAAM,YAAY,gBAAgB,YAAY,cAAc,IAAI;EAChE,MAAM,aAAa,kBAAkB,EAAE,MAAM;EAC7C,MAAM,SAAS,cAAc,cAAc,YAAY,IAAI;EAC3D,OAAO,CAAC;GACN,MAAM;GACN;GACA,GAAI,YAAY,EAAE,MAAM,UAAU,IAAI,CAAC;GACvC,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;EAC7B,CAAC;CACH,CAAC,GAAG,GAAG;CACP,aAAa,MAAM,SAAS;EAC1B,MAAM,IAAI,KAAK;EACf,OAAO,aAAa,GAAG,IAAI;CAC7B;CAIA,MAAM,MAAM,OAAO,QAAQ;EACzB,MAAM,IAAI,KAAK;EACf,OAAO;GACL,QAAQ,gBAAgB,EAAE,MAAM,kBAAkB,MAAM;GACxD,GAAG,SAAS,kBAAkB,EAAE,MAAM,GAAG,KAAK,KAAK;EACrD;CACF;CAEA,SAAS,MAAM,MAAM,QACnB,cAAc,MAAM,kBAAmB,KAAK,KAAoC,MAAM,GAAG,GAAG;CAG9F,cAAc,MAAM,MAAM,QAAQ;EAChC,MAAM,IAAI,KAAK;EACf,MAAM,SAAS,kBAAkB,EAAE,MAAM;EACzC,OAAO,EAAE,UAAU,aAAa,EAAE,UAAU,SACxC,eAAe,MAAM,MAAM,QAAQ,MAAM,OAAO,aAAa,GAAG,EAAE,CAAC,IACnE,cAAc,MAAM,QAAQ,GAAG;CACrC;AACF;AAEA,IAAa,8BAAc,IAAI,IAAI;CAAC;CAAQ;CAAW;CAAW;AAAM,CAAC;AAEzE,SAAgB,aACd,GACA,GACM;CACN,QAAQ,EAAE,OAAV;EACE,KAAK,WACH,OAAO,YAAY,CAAC;EACtB,KAAK,WAAW;GACd,MAAM,KAAK,EAAE,IAAI,EAAE,QAAQ;GAC3B,MAAM,KAAK,EAAE,IAAI,EAAE,SAAS;GAC5B,MAAM,IAAI,KAAK,IAAI,EAAE,OAAO,EAAE,MAAM,IAAI;GACxC,OAAO,mBAAmB;IAAE,GAAG;IAAI,GAAG;GAAG,GAAG,GAAG,EAAE,SAAS,CAAC;EAC7D;EACA,KAAK,QAAQ;GACX,MAAM,KAAK,EAAE,IAAI,EAAE,QAAQ;GAC3B,MAAM,KAAK,EAAE,IAAI,EAAE,SAAS;GAC5B,MAAM,IAAI,KAAK,IAAI,EAAE,OAAO,EAAE,MAAM,IAAI;GACxC,OAAO,SAAS;IAAE,GAAG;IAAI,GAAG;GAAG,GAAG,GAAG,EAAE,UAAU,CAAC;EACpD;EAEA,SACE,OAAO;GAAE,MAAM;GAAQ,GAAG,EAAE;GAAG,GAAG,EAAE;GAAG,OAAO,EAAE;GAAO,QAAQ,EAAE;EAAO;CAC5E;AACF"}
1
+ {"version":3,"file":"shapePainter.js","names":[],"sources":["../../../src/canvas/nodeShapes/shapePainter.ts"],"sourcesContent":["import {\n ellipsePath,\n regularPolygonPath,\n starPath,\n type Path\n} from '@weasel-js/geom';\nimport type { Stroke, Fill, Inherit } from '@weasel-js/paint';\nimport type { NodeShapeEntry } from 'canvas/NodeShape';\nimport { nodeMemo } from 'core/scene/nodeMemo';\nimport type { RectPose } from 'core/scene/types';\nimport { commandFill, commandStroke } from '@weasel-js/renderer';\nimport { PAINT_SLOT } from './memo';\nimport { CONTAINER_INK, inkReach, withAnimatedVertexColors } from './nodePaint';\nimport { resolveFill, resolveStroke } from '@weasel-js/paint';\nimport { paintsPose, poseBoxBounds } from './poseBox';\nimport { outsetPoseBox, strokedPoseBox } from './strokedBounds';\n\ntype ShapeData = {\n shape: string;\n fill?: Fill | Inherit | null;\n stroke?: Stroke | Inherit | null;\n sides?: number;\n points?: number;\n};\n\n// ─── Built-in painters ─────────────────────────────────────────────────\n/** Built-in shape dispatcher — matches when `data.shape` names a kit-known\n * shape kind. Computes both paint and silhouette from the pose so consumer\n * scenes can declare clipping/rendering directly in JSON without\n * registering a custom painter. */\nexport const SHAPE_PAINTER: NodeShapeEntry<unknown, RectPose> = {\n id: 'kit:shape',\n matches: (node) => {\n if (!paintsPose(node.pose)) return false;\n const s = (node.data as { shape?: string } | null)?.shape;\n return s != null && SHAPE_KINDS.has(s);\n },\n // Memoized: a pure function of `(data, pose)`, and `pathForShape` builds a\n // fresh `Uint8Array` + `Float32Array` for every ellipse, polygon and star.\n // See PAINT_SLOT.\n // A container's shape is its clip, and its paint is only what its\n // children's 'inherit' takes.\n paint: (node, pose, ctx) => node.kind === 'container' ? [] : withAnimatedVertexColors(nodeMemo(node, PAINT_SLOT, pose, () => {\n const d = node.data as ShapeData;\n const path = pathForShape(d, pose);\n const declaredFill = resolveFill(d.fill, ctx?.inherited?.fill);\n const shapeFill = declaredFill && commandFill(declaredFill, pose);\n const strokeSpec = resolveStroke(d.stroke, ctx?.inherited?.stroke);\n const stroke = strokeSpec && commandStroke(strokeSpec, pose);\n return [{\n kind: 'path',\n path,\n ...(shapeFill ? { fill: shapeFill } : {}),\n ...(stroke ? { stroke } : {}),\n }];\n }, ctx?.inherited), ctx),\n silhouette: (node, pose) => {\n const d = node.data as { shape: string; sides?: number; points?: number };\n return pathForShape(d, pose);\n },\n ink: (node, _pose, ctx) => {\n if (node.kind === 'container') return CONTAINER_INK;\n const d = node.data as ShapeData;\n return {\n filled: resolveFill(d.fill, ctx?.inherited?.fill) !== null,\n ...inkReach(resolveStroke(d.stroke, ctx?.inherited?.stroke), ctx?.scale),\n };\n },\n // Every shape kind is inscribed in the pose box.\n bounds: (node, pose, ctx) =>\n poseBoxBounds(pose, resolveStroke((node.data as ShapeData).stroke, ctx?.inherited?.stroke), ctx),\n // A rect's or an ellipse's stroke reaches its box evenly; a polygon's or a\n // star's corners are sharper than a right angle, so their miters are measured.\n drawnBounds: (node, pose, ctx) => {\n const d = node.data as ShapeData;\n const stroke = resolveStroke(d.stroke, ctx?.inherited?.stroke);\n return d.shape === 'polygon' || d.shape === 'star'\n ? strokedPoseBox(node, pose, stroke, ctx, (at) => pathForShape(d, at))\n : outsetPoseBox(pose, stroke, ctx);\n },\n};\n\nexport const SHAPE_KINDS = new Set(['rect', 'ellipse', 'polygon', 'star']);\n\nexport function pathForShape(\n d: { shape: string; sides?: number; points?: number },\n p: RectPose,\n): Path {\n switch (d.shape) {\n case 'ellipse':\n return ellipsePath(p);\n case 'polygon': {\n const cx = p.x + p.width / 2;\n const cy = p.y + p.height / 2;\n const r = Math.min(p.width, p.height) / 2;\n return regularPolygonPath({ x: cx, y: cy }, r, d.sides ?? 6);\n }\n case 'star': {\n const cx = p.x + p.width / 2;\n const cy = p.y + p.height / 2;\n const r = Math.min(p.width, p.height) / 2;\n return starPath({ x: cx, y: cy }, r, d.points ?? 5);\n }\n case 'rect':\n default:\n return { kind: 'rect', x: p.x, y: p.y, width: p.width, height: p.height };\n }\n}\n"],"mappings":";;;;;;;;;;;;;;AA8BA,IAAa,gBAAmD;CAC9D,IAAI;CACJ,UAAU,SAAS;EACjB,IAAI,CAAC,WAAW,KAAK,IAAI,GAAG,OAAO;EACnC,MAAM,IAAK,KAAK,MAAoC;EACpD,OAAO,KAAK,QAAQ,YAAY,IAAI,CAAC;CACvC;CAMA,QAAQ,MAAM,MAAM,QAAQ,KAAK,SAAS,cAAc,CAAC,IAAI,yBAAyB,SAAS,MAAM,YAAY,YAAY;EAC3H,MAAM,IAAI,KAAK;EACf,MAAM,OAAO,aAAa,GAAG,IAAI;EACjC,MAAM,eAAe,YAAY,EAAE,MAAM,KAAK,WAAW,IAAI;EAC7D,MAAM,YAAY,gBAAgB,YAAY,cAAc,IAAI;EAChE,MAAM,aAAa,cAAc,EAAE,QAAQ,KAAK,WAAW,MAAM;EACjE,MAAM,SAAS,cAAc,cAAc,YAAY,IAAI;EAC3D,OAAO,CAAC;GACN,MAAM;GACN;GACA,GAAI,YAAY,EAAE,MAAM,UAAU,IAAI,CAAC;GACvC,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;EAC7B,CAAC;CACH,GAAG,KAAK,SAAS,GAAG,GAAG;CACvB,aAAa,MAAM,SAAS;EAC1B,MAAM,IAAI,KAAK;EACf,OAAO,aAAa,GAAG,IAAI;CAC7B;CACA,MAAM,MAAM,OAAO,QAAQ;EACzB,IAAI,KAAK,SAAS,aAAa,OAAO;EACtC,MAAM,IAAI,KAAK;EACf,OAAO;GACL,QAAQ,YAAY,EAAE,MAAM,KAAK,WAAW,IAAI,MAAM;GACtD,GAAG,SAAS,cAAc,EAAE,QAAQ,KAAK,WAAW,MAAM,GAAG,KAAK,KAAK;EACzE;CACF;CAEA,SAAS,MAAM,MAAM,QACnB,cAAc,MAAM,cAAe,KAAK,KAAmB,QAAQ,KAAK,WAAW,MAAM,GAAG,GAAG;CAGjG,cAAc,MAAM,MAAM,QAAQ;EAChC,MAAM,IAAI,KAAK;EACf,MAAM,SAAS,cAAc,EAAE,QAAQ,KAAK,WAAW,MAAM;EAC7D,OAAO,EAAE,UAAU,aAAa,EAAE,UAAU,SACxC,eAAe,MAAM,MAAM,QAAQ,MAAM,OAAO,aAAa,GAAG,EAAE,CAAC,IACnE,cAAc,MAAM,QAAQ,GAAG;CACrC;AACF;AAEA,IAAa,8BAAc,IAAI,IAAI;CAAC;CAAQ;CAAW;CAAW;AAAM,CAAC;AAEzE,SAAgB,aACd,GACA,GACM;CACN,QAAQ,EAAE,OAAV;EACE,KAAK,WACH,OAAO,YAAY,CAAC;EACtB,KAAK,WAAW;GACd,MAAM,KAAK,EAAE,IAAI,EAAE,QAAQ;GAC3B,MAAM,KAAK,EAAE,IAAI,EAAE,SAAS;GAC5B,MAAM,IAAI,KAAK,IAAI,EAAE,OAAO,EAAE,MAAM,IAAI;GACxC,OAAO,mBAAmB;IAAE,GAAG;IAAI,GAAG;GAAG,GAAG,GAAG,EAAE,SAAS,CAAC;EAC7D;EACA,KAAK,QAAQ;GACX,MAAM,KAAK,EAAE,IAAI,EAAE,QAAQ;GAC3B,MAAM,KAAK,EAAE,IAAI,EAAE,SAAS;GAC5B,MAAM,IAAI,KAAK,IAAI,EAAE,OAAO,EAAE,MAAM,IAAI;GACxC,OAAO,SAAS;IAAE,GAAG;IAAI,GAAG;GAAG,GAAG,GAAG,EAAE,UAAU,CAAC;EACpD;EAEA,SACE,OAAO;GAAE,MAAM;GAAQ,GAAG,EAAE;GAAG,GAAG,EAAE;GAAG,OAAO,EAAE;GAAO,QAAQ,EAAE;EAAO;CAC5E;AACF"}
@@ -1,9 +1,10 @@
1
1
  import { nodeMemo } from "../../core/scene/nodeMemo.js";
2
2
  import { PAINT_SLOT } from "./memo.js";
3
- import { inkReach, resolveNodeFill, resolveNodeStroke, strokeOutsetPerAxis } from "./nodePaint.js";
3
+ import { inkReach, strokeOutsetPerAxis } from "./nodePaint.js";
4
4
  import { paintsPose } from "./poseBox.js";
5
5
  import { commandFill, textCommandFromPose } from "@weasel-js/renderer";
6
6
  import { PATH_L, PATH_M, PATH_Z } from "@weasel-js/geom";
7
+ import { INHERIT, resolveFill, resolveStroke } from "@weasel-js/paint";
7
8
  import { textLineBoxes } from "@weasel-js/text";
8
9
 
9
10
  //#region src/canvas/nodeShapes/textPainter.ts
@@ -39,14 +40,14 @@ function textBoxOf(d, p) {
39
40
  * {@link textBoxOf} insets. The stroke goes through the same resolver every
40
41
  * other node kind's does, so one carrying no paint is dropped here rather
41
42
  * than throwing in the renderer. */
42
- function textPoseOf(d, p) {
43
+ function textPoseOf(d, p, inherited) {
43
44
  return {
44
45
  ...textBoxOf(d, p),
45
46
  text: d.text,
46
47
  runs: d.runs,
47
48
  style: d.style,
48
- fill: d.fill,
49
- stroke: resolveNodeStroke(d.stroke) ?? void 0,
49
+ fill: d.fill === INHERIT ? resolveFill(d.fill, inherited?.fill) : d.fill,
50
+ stroke: resolveStroke(d.stroke, inherited?.stroke) ?? void 0,
50
51
  verticalAlign: d.verticalAlign
51
52
  };
52
53
  }
@@ -57,11 +58,11 @@ var TEXT_PAINTER = {
57
58
  if (!paintsPose(node.pose)) return false;
58
59
  return node.data?.text != null;
59
60
  },
60
- paint: (node, pose) => nodeMemo(node, PAINT_SLOT, pose, () => {
61
+ paint: (node, pose, ctx) => nodeMemo(node, PAINT_SLOT, pose, () => {
61
62
  const d = node.data;
62
- const laid = textCommandFromPose(textPoseOf(d, pose));
63
+ const laid = textCommandFromPose(textPoseOf(d, pose, ctx?.inherited));
63
64
  const text = d.overflow === "clip" ? clippedToBox(laid, pose) : laid;
64
- const bg = resolveNodeFill(d.background, null);
65
+ const bg = resolveFill(d.background);
65
66
  if (!bg) return [text];
66
67
  const fill = commandFill(bg, pose);
67
68
  return [{
@@ -75,10 +76,10 @@ var TEXT_PAINTER = {
75
76
  },
76
77
  ...fill ? { fill } : {}
77
78
  }, text];
78
- }),
79
+ }, ctx?.inherited),
79
80
  silhouette: (node, pose) => {
80
81
  const d = node.data;
81
- if (resolveNodeFill(d.background, null)) return {
82
+ if (resolveFill(d.background)) return {
82
83
  kind: "rect",
83
84
  x: pose.x,
84
85
  y: pose.y,
@@ -94,13 +95,13 @@ var TEXT_PAINTER = {
94
95
  const d = node.data;
95
96
  return {
96
97
  filled: true,
97
- ...inkReach(resolveNodeStroke(d.stroke), ctx?.scale)
98
+ ...inkReach(resolveStroke(d.stroke, ctx?.inherited?.stroke), ctx?.scale)
98
99
  };
99
100
  },
100
101
  drawnBounds: (node, pose, ctx) => {
101
102
  const d = node.data;
102
- const stroke = resolveNodeStroke(d.stroke);
103
- return stroke ? strokedLinesBox(pose, textLineBoxes(textPoseOf(d, pose)), strokeOutsetPerAxis(stroke, ctx)) : null;
103
+ const stroke = resolveStroke(d.stroke, ctx?.inherited?.stroke);
104
+ return stroke ? strokedLinesBox(pose, textLineBoxes(textPoseOf(d, pose, ctx?.inherited)), strokeOutsetPerAxis(stroke, ctx)) : null;
104
105
  }
105
106
  };
106
107
  /** Each of `rects` cut to `box`, leaving out any it does not overlap. */
@@ -1 +1 @@
1
- {"version":3,"file":"textPainter.js","names":[],"sources":["../../../src/canvas/nodeShapes/textPainter.ts"],"sourcesContent":["import {\n PATH_L,\n PATH_M,\n PATH_Z,\n type PolygonPath,\n type Rect,\n} from '@weasel-js/geom';\nimport type { Stroke, Fill } from '@weasel-js/paint';\nimport type { StyledRun, TextPose, TextStyle, TextVerticalAlign } from '@weasel-js/text';\nimport { textLineBoxes } from '@weasel-js/text';\nimport type { NodeShapeEntry } from 'canvas/NodeShape';\nimport { nodeMemo } from 'core/scene/nodeMemo';\nimport type { RectPose } from 'core/scene/types';\nimport { commandFill, textCommandFromPose, type DrawCommand } from '@weasel-js/renderer';\nimport { PAINT_SLOT } from './memo';\nimport { inkReach, resolveNodeFill, resolveNodeStroke, strokeOutsetPerAxis } from './nodePaint';\nimport { paintsPose } from './poseBox';\n\n/** The `data` a `kit:text` node carries. */\nexport interface TextNodeData {\n text: string;\n style?: TextStyle;\n runs?: readonly StyledRun[];\n fill?: Fill | null;\n stroke?: Stroke | null;\n verticalAlign?: TextVerticalAlign;\n /** Paint for the whole pose box, drawn under the glyphs. `fill` is the\n * glyphs' own paint, so a box behind them needs a key of its own. */\n background?: Fill | null;\n /** World units between the pose box and the box the text lays out in, on\n * every side. */\n padding?: number;\n /** What happens to text that runs past the pose box: `'visible'` (the\n * default) draws it there, `'clip'` cuts it at the box's edge. */\n overflow?: TextOverflow;\n}\n\n/** How a text node treats lines that run past its box. */\nexport type TextOverflow = 'visible' | 'clip';\n\ntype Box = { x: number; y: number; width: number; height: number };\n\n/** `cmd` cut to `box`: a group whose clip is the box as a rect, which the\n * renderer cuts on the CPU when the frame keeps it axis-aligned. */\nexport function clippedToBox(cmd: DrawCommand, box: Box): DrawCommand {\n return {\n kind: 'group',\n clip: { kind: 'rect', x: box.x, y: box.y, width: box.width, height: box.height },\n children: [cmd],\n };\n}\n\n/** The box a text node's lines lay out in: its pose box, inset by\n * `data.padding` and never past its center. */\nexport function textBoxOf(\n d: Pick<TextNodeData, 'padding'>,\n p: RectPose,\n): { x: number; y: number; width: number; height: number } {\n const pad = Math.max(0, d.padding ?? 0);\n const px = Math.min(pad, p.width / 2);\n const py = Math.min(pad, p.height / 2);\n return { x: p.x + px, y: p.y + py, width: p.width - 2 * px, height: p.height - 2 * py };\n}\n\n/** A `kit:text` node as the `TextPose` every text consumer reads, in the box\n * {@link textBoxOf} insets. The stroke goes through the same resolver every\n * other node kind's does, so one carrying no paint is dropped here rather\n * than throwing in the renderer. */\nexport function textPoseOf(d: TextNodeData, p: RectPose): TextPose {\n return {\n ...textBoxOf(d, p),\n text: d.text,\n runs: d.runs as StyledRun[] | undefined,\n style: d.style,\n fill: d.fill,\n stroke: resolveNodeStroke(d.stroke) ?? undefined,\n verticalAlign: d.verticalAlign,\n };\n}\n\nexport const TEXT_PAINTER: NodeShapeEntry<unknown, RectPose> = {\n id: 'kit:text',\n snapsToBox: true,\n matches: (node) => {\n if (!paintsPose(node.pose)) return false;\n const d = node.data as { text?: string } | null;\n return d?.text != null;\n },\n // Memoized because the renderer's layout cache keys on the `ResolvedRun[]` array\n // identity: a fresh runs array per frame would leave that cache missing on every\n // draw. Both resolvers are pure style merging — no font reads — so `(data, pose)` is\n // the whole key. See PAINT_SLOT.\n //\n // `y` is the top of the first line box, not a baseline — see `TextDrawCommand.y`.\n paint: (node, pose) => nodeMemo(node, PAINT_SLOT, pose, (): DrawCommand[] => {\n const d = node.data as TextNodeData;\n const laid = textCommandFromPose(textPoseOf(d, pose));\n const text = d.overflow === 'clip' ? clippedToBox(laid, pose) : laid;\n const bg = resolveNodeFill(d.background, null);\n if (!bg) return [text];\n const fill = commandFill(bg, pose);\n return [\n { kind: 'path', path: { kind: 'rect', x: pose.x, y: pose.y, width: pose.width, height: pose.height }, ...(fill ? { fill } : {}) },\n text,\n ];\n }),\n // The pose is a layout box, not a bounding box: the union of the line boxes is what\n // the node actually covers, so picking, lasso, and clipping stop grabbing blank space.\n //\n // `null` rather than an empty path when there are no non-blank lines: a caller\n // reading \"no silhouette\" falls back to the pose rect, which keeps an empty text\n // node pickable.\n //\n // A background is ink across the whole box, so a node with one is its box.\n // A clipped node's lines are cut to the box, as they are drawn.\n silhouette: (node, pose) => {\n const d = node.data as TextNodeData;\n if (resolveNodeFill(d.background, null)) {\n return { kind: 'rect', x: pose.x, y: pose.y, width: pose.width, height: pose.height };\n }\n const lines = textLineBoxes(textPoseOf(d, pose));\n const boxes = d.overflow === 'clip' ? cutRects(lines, pose) : lines;\n if (boxes.length === 0) return null;\n return rectsToPath(boxes);\n },\n // `filled` is unconditional, `data.fill: null` included: the silhouette above is line\n // boxes, not glyph ink, so reporting outline-only text as unfilled would leave it\n // grabbable within a stroke width of a box edge and nowhere near the letters.\n ink: (node, _pose, ctx) => {\n const d = node.data as { stroke?: Stroke | null };\n return { filled: true, ...inkReach(resolveNodeStroke(d.stroke), ctx?.scale) };\n },\n drawnBounds: (node, pose, ctx) => {\n const d = node.data as TextNodeData;\n const stroke = resolveNodeStroke(d.stroke);\n return stroke ? strokedLinesBox(pose, textLineBoxes(textPoseOf(d, pose)), strokeOutsetPerAxis(stroke, ctx)) : null;\n },\n};\n\n/** Each of `rects` cut to `box`, leaving out any it does not overlap. */\nfunction cutRects(rects: readonly Box[], box: Box): Box[] {\n const out: Box[] = [];\n for (const r of rects) {\n const x0 = Math.max(r.x, box.x);\n const y0 = Math.max(r.y, box.y);\n const x1 = Math.min(r.x + r.width, box.x + box.width);\n const y1 = Math.min(r.y + r.height, box.y + box.height);\n if (x1 > x0 && y1 > y0) out.push({ x: x0, y: y0, width: x1 - x0, height: y1 - y0 });\n }\n return out;\n}\n\n/**\n * The layout box grown where a stroked line reaches it: each line box, cut to\n * the layout box, then outset by the stroke's reach. A line that overflows the\n * layout box is left to it — the layout box is what a text node claims.\n */\nfunction strokedLinesBox(pose: RectPose, lines: readonly Rect[], reach: { x: number; y: number }): Rect | null {\n if (!(reach.x > 0 || reach.y > 0)) return null;\n let x0 = pose.x, y0 = pose.y, x1 = pose.x + pose.width, y1 = pose.y + pose.height;\n for (const l of lines) {\n const lx0 = Math.max(l.x, pose.x), ly0 = Math.max(l.y, pose.y);\n const lx1 = Math.min(l.x + l.width, pose.x + pose.width), ly1 = Math.min(l.y + l.height, pose.y + pose.height);\n if (lx1 <= lx0 || ly1 <= ly0) continue;\n x0 = Math.min(x0, lx0 - reach.x); y0 = Math.min(y0, ly0 - reach.y);\n x1 = Math.max(x1, lx1 + reach.x); y1 = Math.max(y1, ly1 + reach.y);\n }\n return { x: x0, y: y0, width: x1 - x0, height: y1 - y0 };\n}\n\n/**\n * Union of axis-aligned rects as one multi-contour `PolygonPath`.\n *\n * Every contour winds the same way, so `'nonzero'` reads overlapping rects as\n * covered rather than punching a hole where two lines happen to overlap —\n * which they do the moment `lineHeight` drops below 1.\n */\nexport function rectsToPath(rects: readonly { x: number; y: number; width: number; height: number }[]): PolygonPath {\n const commands = new Uint8Array(rects.length * 6);\n const coords = new Float32Array(rects.length * 10);\n let ci = 0;\n let cmd = 0;\n for (const r of rects) {\n const { x, y, width: w, height: h } = r;\n commands[cmd++] = PATH_M; coords[ci++] = x; coords[ci++] = y;\n commands[cmd++] = PATH_L; coords[ci++] = x + w; coords[ci++] = y;\n commands[cmd++] = PATH_L; coords[ci++] = x + w; coords[ci++] = y + h;\n commands[cmd++] = PATH_L; coords[ci++] = x; coords[ci++] = y + h;\n commands[cmd++] = PATH_L; coords[ci++] = x; coords[ci++] = y;\n commands[cmd++] = PATH_Z;\n }\n return { kind: 'polygon', commands, coords, fillRule: 'nonzero' };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"textPainter.js","names":[],"sources":["../../../src/canvas/nodeShapes/textPainter.ts"],"sourcesContent":["import {\n PATH_L,\n PATH_M,\n PATH_Z,\n type PolygonPath,\n type Rect,\n} from '@weasel-js/geom';\nimport type { Stroke, Fill, Inherit } from '@weasel-js/paint';\nimport type { StyledRun, TextPose, TextStyle, TextVerticalAlign } from '@weasel-js/text';\nimport { textLineBoxes } from '@weasel-js/text';\nimport type { NodeShapeEntry } from 'canvas/NodeShape';\nimport { nodeMemo } from 'core/scene/nodeMemo';\nimport type { RectPose } from 'core/scene/types';\nimport { commandFill, textCommandFromPose, type DrawCommand } from '@weasel-js/renderer';\nimport { PAINT_SLOT } from './memo';\nimport { inkReach, strokeOutsetPerAxis } from './nodePaint';\nimport { INHERIT, resolveFill, resolveStroke } from '@weasel-js/paint';\nimport type { InheritedPaint } from 'core/scene/inheritedPaint';\nimport { paintsPose } from './poseBox';\n\n/** The `data` a `kit:text` node carries. */\nexport interface TextNodeData {\n text: string;\n style?: TextStyle;\n runs?: readonly StyledRun[];\n fill?: Fill | Inherit | null;\n stroke?: Stroke | Inherit | null;\n verticalAlign?: TextVerticalAlign;\n /** Paint for the whole pose box, drawn under the glyphs. `fill` is the\n * glyphs' own paint, so a box behind them needs a key of its own. */\n background?: Fill | null;\n /** World units between the pose box and the box the text lays out in, on\n * every side. */\n padding?: number;\n /** What happens to text that runs past the pose box: `'visible'` (the\n * default) draws it there, `'clip'` cuts it at the box's edge. */\n overflow?: TextOverflow;\n}\n\n/** How a text node treats lines that run past its box. */\nexport type TextOverflow = 'visible' | 'clip';\n\ntype Box = { x: number; y: number; width: number; height: number };\n\n/** `cmd` cut to `box`: a group whose clip is the box as a rect, which the\n * renderer cuts on the CPU when the frame keeps it axis-aligned. */\nexport function clippedToBox(cmd: DrawCommand, box: Box): DrawCommand {\n return {\n kind: 'group',\n clip: { kind: 'rect', x: box.x, y: box.y, width: box.width, height: box.height },\n children: [cmd],\n };\n}\n\n/** The box a text node's lines lay out in: its pose box, inset by\n * `data.padding` and never past its center. */\nexport function textBoxOf(\n d: Pick<TextNodeData, 'padding'>,\n p: RectPose,\n): { x: number; y: number; width: number; height: number } {\n const pad = Math.max(0, d.padding ?? 0);\n const px = Math.min(pad, p.width / 2);\n const py = Math.min(pad, p.height / 2);\n return { x: p.x + px, y: p.y + py, width: p.width - 2 * px, height: p.height - 2 * py };\n}\n\n/** A `kit:text` node as the `TextPose` every text consumer reads, in the box\n * {@link textBoxOf} insets. The stroke goes through the same resolver every\n * other node kind's does, so one carrying no paint is dropped here rather\n * than throwing in the renderer. */\nexport function textPoseOf(d: TextNodeData, p: RectPose, inherited?: InheritedPaint): TextPose {\n return {\n ...textBoxOf(d, p),\n text: d.text,\n runs: d.runs as StyledRun[] | undefined,\n style: d.style,\n fill: d.fill === INHERIT ? resolveFill(d.fill, inherited?.fill) : d.fill,\n stroke: resolveStroke(d.stroke, inherited?.stroke) ?? undefined,\n verticalAlign: d.verticalAlign,\n };\n}\n\nexport const TEXT_PAINTER: NodeShapeEntry<unknown, RectPose> = {\n id: 'kit:text',\n snapsToBox: true,\n matches: (node) => {\n if (!paintsPose(node.pose)) return false;\n const d = node.data as { text?: string } | null;\n return d?.text != null;\n },\n // Memoized because the renderer's layout cache keys on the `ResolvedRun[]` array\n // identity: a fresh runs array per frame would leave that cache missing on every\n // draw. Both resolvers are pure style merging — no font reads — so `(data, pose)` is\n // the whole key. See PAINT_SLOT.\n //\n // `y` is the top of the first line box, not a baseline — see `TextDrawCommand.y`.\n paint: (node, pose, ctx) => nodeMemo(node, PAINT_SLOT, pose, (): DrawCommand[] => {\n const d = node.data as TextNodeData;\n const laid = textCommandFromPose(textPoseOf(d, pose, ctx?.inherited));\n const text = d.overflow === 'clip' ? clippedToBox(laid, pose) : laid;\n const bg = resolveFill(d.background);\n if (!bg) return [text];\n const fill = commandFill(bg, pose);\n return [\n { kind: 'path', path: { kind: 'rect', x: pose.x, y: pose.y, width: pose.width, height: pose.height }, ...(fill ? { fill } : {}) },\n text,\n ];\n }, ctx?.inherited),\n // The pose is a layout box, not a bounding box: the union of the line boxes is what\n // the node actually covers, so picking, lasso, and clipping stop grabbing blank space.\n //\n // `null` rather than an empty path when there are no non-blank lines: a caller\n // reading \"no silhouette\" falls back to the pose rect, which keeps an empty text\n // node pickable.\n //\n // A background is ink across the whole box, so a node with one is its box.\n // A clipped node's lines are cut to the box, as they are drawn.\n silhouette: (node, pose) => {\n const d = node.data as TextNodeData;\n if (resolveFill(d.background)) {\n return { kind: 'rect', x: pose.x, y: pose.y, width: pose.width, height: pose.height };\n }\n const lines = textLineBoxes(textPoseOf(d, pose));\n const boxes = d.overflow === 'clip' ? cutRects(lines, pose) : lines;\n if (boxes.length === 0) return null;\n return rectsToPath(boxes);\n },\n // `filled` is unconditional, `data.fill: null` included: the silhouette above is line\n // boxes, not glyph ink, so reporting outline-only text as unfilled would leave it\n // grabbable within a stroke width of a box edge and nowhere near the letters.\n ink: (node, _pose, ctx) => {\n const d = node.data as TextNodeData;\n return { filled: true, ...inkReach(resolveStroke(d.stroke, ctx?.inherited?.stroke), ctx?.scale) };\n },\n drawnBounds: (node, pose, ctx) => {\n const d = node.data as TextNodeData;\n const stroke = resolveStroke(d.stroke, ctx?.inherited?.stroke);\n return stroke ? strokedLinesBox(pose, textLineBoxes(textPoseOf(d, pose, ctx?.inherited)), strokeOutsetPerAxis(stroke, ctx)) : null;\n },\n};\n\n/** Each of `rects` cut to `box`, leaving out any it does not overlap. */\nfunction cutRects(rects: readonly Box[], box: Box): Box[] {\n const out: Box[] = [];\n for (const r of rects) {\n const x0 = Math.max(r.x, box.x);\n const y0 = Math.max(r.y, box.y);\n const x1 = Math.min(r.x + r.width, box.x + box.width);\n const y1 = Math.min(r.y + r.height, box.y + box.height);\n if (x1 > x0 && y1 > y0) out.push({ x: x0, y: y0, width: x1 - x0, height: y1 - y0 });\n }\n return out;\n}\n\n/**\n * The layout box grown where a stroked line reaches it: each line box, cut to\n * the layout box, then outset by the stroke's reach. A line that overflows the\n * layout box is left to it — the layout box is what a text node claims.\n */\nfunction strokedLinesBox(pose: RectPose, lines: readonly Rect[], reach: { x: number; y: number }): Rect | null {\n if (!(reach.x > 0 || reach.y > 0)) return null;\n let x0 = pose.x, y0 = pose.y, x1 = pose.x + pose.width, y1 = pose.y + pose.height;\n for (const l of lines) {\n const lx0 = Math.max(l.x, pose.x), ly0 = Math.max(l.y, pose.y);\n const lx1 = Math.min(l.x + l.width, pose.x + pose.width), ly1 = Math.min(l.y + l.height, pose.y + pose.height);\n if (lx1 <= lx0 || ly1 <= ly0) continue;\n x0 = Math.min(x0, lx0 - reach.x); y0 = Math.min(y0, ly0 - reach.y);\n x1 = Math.max(x1, lx1 + reach.x); y1 = Math.max(y1, ly1 + reach.y);\n }\n return { x: x0, y: y0, width: x1 - x0, height: y1 - y0 };\n}\n\n/**\n * Union of axis-aligned rects as one multi-contour `PolygonPath`.\n *\n * Every contour winds the same way, so `'nonzero'` reads overlapping rects as\n * covered rather than punching a hole where two lines happen to overlap —\n * which they do the moment `lineHeight` drops below 1.\n */\nexport function rectsToPath(rects: readonly { x: number; y: number; width: number; height: number }[]): PolygonPath {\n const commands = new Uint8Array(rects.length * 6);\n const coords = new Float32Array(rects.length * 10);\n let ci = 0;\n let cmd = 0;\n for (const r of rects) {\n const { x, y, width: w, height: h } = r;\n commands[cmd++] = PATH_M; coords[ci++] = x; coords[ci++] = y;\n commands[cmd++] = PATH_L; coords[ci++] = x + w; coords[ci++] = y;\n commands[cmd++] = PATH_L; coords[ci++] = x + w; coords[ci++] = y + h;\n commands[cmd++] = PATH_L; coords[ci++] = x; coords[ci++] = y + h;\n commands[cmd++] = PATH_L; coords[ci++] = x; coords[ci++] = y;\n commands[cmd++] = PATH_Z;\n }\n return { kind: 'polygon', commands, coords, fillRule: 'nonzero' };\n}\n"],"mappings":"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"}
@@ -11,19 +11,19 @@
11
11
  * `copying` says some container in the walk draws copies, otherwise only
12
12
  * after the node hits as authored.
13
13
  */
14
- function reaches(node, pose, derived, q, parentsOf, copying, look) {
14
+ function reaches(node, pose, derived, inherited, q, parentsOf, copying, look) {
15
15
  const admits = (query, ancestor) => {
16
16
  const clip = look.clipOf(ancestor);
17
17
  return clip === null || query.clipAdmits(look.carry(clip, ancestor.layer, node.layer), node, pose, derived);
18
18
  };
19
19
  const parents = copying && q.inCopy !== void 0 ? parentsOf() : null;
20
20
  if (parents === null || !parents.some((a) => look.copiesOf(a).length > 0)) {
21
- if (!q.hits(node, pose, derived)) return false;
21
+ if (!q.hits(node, pose, derived, inherited)) return false;
22
22
  return (parents ?? parentsOf()).every((a) => admits(q, a));
23
23
  }
24
24
  const outermostFirst = [...parents].reverse();
25
25
  const from = (i, query) => {
26
- if (i === outermostFirst.length) return query.hits(node, pose, derived);
26
+ if (i === outermostFirst.length) return query.hits(node, pose, derived, inherited);
27
27
  const ancestor = outermostFirst[i];
28
28
  if (admits(query, ancestor) && from(i + 1, query)) return true;
29
29
  for (const m of look.copiesOf(ancestor)) {
@@ -1 +1 @@
1
- {"version":3,"file":"pickReach.js","names":[],"sources":["../../src/canvas/pickReach.ts"],"sourcesContent":["import type { Affine, Path } from '@weasel-js/geom';\nimport type { PickCandidate, PickQuery } from './pickWalk';\n\n/** What {@link reaches} needs from the walk, each memoized per walk there. */\nexport interface ReachLookups<TPose> {\n /** An ancestor's own clip, or null. */\n clipOf(ancestor: PickCandidate<TPose>): Path | null;\n /** The maps from world into the source frame of each copy `ancestor` draws:\n * the inverses of its `copiesFromPose`. Empty when it draws none. */\n copiesOf(ancestor: PickCandidate<TPose>): readonly Affine[];\n /** A clip carried from its container's plane into the node's. */\n carry(clip: Path, fromLayer: string | undefined, toLayer: string | undefined): Path;\n}\n\n/**\n * Whether `q` lands on `node` anywhere it is painted: as authored, or in any\n * copy an ancestor draws of it. `parents` is innermost first.\n *\n * A copy is the clipped content transformed, so the query is carried into the\n * copy's source frame before the copying container's own clip is asked, and\n * every clip below it is asked in that frame too.\n *\n * `parents` is called only when the answer needs it — always when\n * `copying` says some container in the walk draws copies, otherwise only\n * after the node hits as authored.\n */\nexport function reaches<TPose>(\n node: PickCandidate<TPose>,\n pose: TPose,\n derived: Path | null | undefined,\n q: PickQuery<TPose>,\n parentsOf: () => readonly PickCandidate<TPose>[],\n copying: boolean,\n look: ReachLookups<TPose>,\n): boolean {\n const admits = (query: PickQuery<TPose>, ancestor: PickCandidate<TPose>): boolean => {\n const clip = look.clipOf(ancestor);\n return clip === null\n || query.clipAdmits(look.carry(clip, ancestor.layer, node.layer), node, pose, derived);\n };\n\n const parents = copying && q.inCopy !== undefined ? parentsOf() : null;\n if (parents === null || !parents.some((a) => look.copiesOf(a).length > 0)) {\n if (!q.hits(node, pose, derived)) return false;\n return (parents ?? parentsOf()).every((a) => admits(q, a));\n }\n\n const outermostFirst = [...parents].reverse();\n const from = (i: number, query: PickQuery<TPose>): boolean => {\n if (i === outermostFirst.length) return query.hits(node, pose, derived);\n const ancestor = outermostFirst[i];\n if (admits(query, ancestor) && from(i + 1, query)) return true;\n for (const m of look.copiesOf(ancestor)) {\n const inCopy = query.inCopy?.(m);\n if (inCopy !== undefined && admits(inCopy, ancestor) && from(i + 1, inCopy)) return true;\n }\n return false;\n };\n return from(0, q);\n}\n"],"mappings":";;;;;;;;;;;;;AA0BA,SAAgB,QACd,MACA,MACA,SACA,GACA,WACA,SACA,MACS;CACT,MAAM,UAAU,OAAyB,aAA4C;EACnF,MAAM,OAAO,KAAK,OAAO,QAAQ;EACjC,OAAO,SAAS,QACX,MAAM,WAAW,KAAK,MAAM,MAAM,SAAS,OAAO,KAAK,KAAK,GAAG,MAAM,MAAM,OAAO;CACzF;CAEA,MAAM,UAAU,WAAW,EAAE,WAAW,SAAY,UAAU,IAAI;CAClE,IAAI,YAAY,QAAQ,CAAC,QAAQ,MAAM,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG;EACzE,IAAI,CAAC,EAAE,KAAK,MAAM,MAAM,OAAO,GAAG,OAAO;EACzC,QAAQ,WAAW,UAAU,EAAA,CAAG,OAAO,MAAM,OAAO,GAAG,CAAC,CAAC;CAC3D;CAEA,MAAM,iBAAiB,CAAC,GAAG,OAAO,CAAC,CAAC,QAAQ;CAC5C,MAAM,QAAQ,GAAW,UAAqC;EAC5D,IAAI,MAAM,eAAe,QAAQ,OAAO,MAAM,KAAK,MAAM,MAAM,OAAO;EACtE,MAAM,WAAW,eAAe;EAChC,IAAI,OAAO,OAAO,QAAQ,KAAK,KAAK,IAAI,GAAG,KAAK,GAAG,OAAO;EAC1D,KAAK,MAAM,KAAK,KAAK,SAAS,QAAQ,GAAG;GACvC,MAAM,SAAS,MAAM,SAAS,CAAC;GAC/B,IAAI,WAAW,UAAa,OAAO,QAAQ,QAAQ,KAAK,KAAK,IAAI,GAAG,MAAM,GAAG,OAAO;EACtF;EACA,OAAO;CACT;CACA,OAAO,KAAK,GAAG,CAAC;AAClB"}
1
+ {"version":3,"file":"pickReach.js","names":[],"sources":["../../src/canvas/pickReach.ts"],"sourcesContent":["import type { Affine, Path } from '@weasel-js/geom';\nimport type { PickCandidate, PickQuery } from './pickWalk';\nimport type { InheritedPaint } from 'core/scene/inheritedPaint';\n\n/** What {@link reaches} needs from the walk, each memoized per walk there. */\nexport interface ReachLookups<TPose> {\n /** An ancestor's own clip, or null. */\n clipOf(ancestor: PickCandidate<TPose>): Path | null;\n /** The maps from world into the source frame of each copy `ancestor` draws:\n * the inverses of its `copiesFromPose`. Empty when it draws none. */\n copiesOf(ancestor: PickCandidate<TPose>): readonly Affine[];\n /** A clip carried from its container's plane into the node's. */\n carry(clip: Path, fromLayer: string | undefined, toLayer: string | undefined): Path;\n}\n\n/**\n * Whether `q` lands on `node` anywhere it is painted: as authored, or in any\n * copy an ancestor draws of it. `parents` is innermost first.\n *\n * A copy is the clipped content transformed, so the query is carried into the\n * copy's source frame before the copying container's own clip is asked, and\n * every clip below it is asked in that frame too.\n *\n * `parents` is called only when the answer needs it — always when\n * `copying` says some container in the walk draws copies, otherwise only\n * after the node hits as authored.\n */\nexport function reaches<TPose>(\n node: PickCandidate<TPose>,\n pose: TPose,\n derived: Path | null | undefined,\n inherited: InheritedPaint | undefined,\n q: PickQuery<TPose>,\n parentsOf: () => readonly PickCandidate<TPose>[],\n copying: boolean,\n look: ReachLookups<TPose>,\n): boolean {\n const admits = (query: PickQuery<TPose>, ancestor: PickCandidate<TPose>): boolean => {\n const clip = look.clipOf(ancestor);\n return clip === null\n || query.clipAdmits(look.carry(clip, ancestor.layer, node.layer), node, pose, derived);\n };\n\n const parents = copying && q.inCopy !== undefined ? parentsOf() : null;\n if (parents === null || !parents.some((a) => look.copiesOf(a).length > 0)) {\n if (!q.hits(node, pose, derived, inherited)) return false;\n return (parents ?? parentsOf()).every((a) => admits(q, a));\n }\n\n const outermostFirst = [...parents].reverse();\n const from = (i: number, query: PickQuery<TPose>): boolean => {\n if (i === outermostFirst.length) return query.hits(node, pose, derived, inherited);\n const ancestor = outermostFirst[i];\n if (admits(query, ancestor) && from(i + 1, query)) return true;\n for (const m of look.copiesOf(ancestor)) {\n const inCopy = query.inCopy?.(m);\n if (inCopy !== undefined && admits(inCopy, ancestor) && from(i + 1, inCopy)) return true;\n }\n return false;\n };\n return from(0, q);\n}\n"],"mappings":";;;;;;;;;;;;;AA2BA,SAAgB,QACd,MACA,MACA,SACA,WACA,GACA,WACA,SACA,MACS;CACT,MAAM,UAAU,OAAyB,aAA4C;EACnF,MAAM,OAAO,KAAK,OAAO,QAAQ;EACjC,OAAO,SAAS,QACX,MAAM,WAAW,KAAK,MAAM,MAAM,SAAS,OAAO,KAAK,KAAK,GAAG,MAAM,MAAM,OAAO;CACzF;CAEA,MAAM,UAAU,WAAW,EAAE,WAAW,SAAY,UAAU,IAAI;CAClE,IAAI,YAAY,QAAQ,CAAC,QAAQ,MAAM,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG;EACzE,IAAI,CAAC,EAAE,KAAK,MAAM,MAAM,SAAS,SAAS,GAAG,OAAO;EACpD,QAAQ,WAAW,UAAU,EAAA,CAAG,OAAO,MAAM,OAAO,GAAG,CAAC,CAAC;CAC3D;CAEA,MAAM,iBAAiB,CAAC,GAAG,OAAO,CAAC,CAAC,QAAQ;CAC5C,MAAM,QAAQ,GAAW,UAAqC;EAC5D,IAAI,MAAM,eAAe,QAAQ,OAAO,MAAM,KAAK,MAAM,MAAM,SAAS,SAAS;EACjF,MAAM,WAAW,eAAe;EAChC,IAAI,OAAO,OAAO,QAAQ,KAAK,KAAK,IAAI,GAAG,KAAK,GAAG,OAAO;EAC1D,KAAK,MAAM,KAAK,KAAK,SAAS,QAAQ,GAAG;GACvC,MAAM,SAAS,MAAM,SAAS,CAAC;GAC/B,IAAI,WAAW,UAAa,OAAO,QAAQ,QAAQ,KAAK,KAAK,IAAI,GAAG,MAAM,GAAG,OAAO;EACtF;EACA,OAAO;CACT;CACA,OAAO,KAAK,GAAG,CAAC;AAClB"}
@@ -1,5 +1,6 @@
1
1
  import { planeMap } from "../core/viewport/parallax.js";
2
2
  import { resolveDerivedPath } from "../core/scene/derivedPath.js";
3
+ import { inheritedPaintOf, inheritsPaint } from "../core/scene/inheritedPaint.js";
3
4
  import { findShapeSilhouette } from "./NodeShape.js";
4
5
  import { clipCarrier } from "./planeClips.js";
5
6
  import { asNodeId } from "../core/scene/types.js";
@@ -87,7 +88,8 @@ function pickWalk(src, q) {
87
88
  if (query === null) continue;
88
89
  const pose = src.poseOf(node);
89
90
  const derived = src.derivedPathOf?.(node);
90
- if (!reaches(node, pose, derived, query, () => src.parentsOf(node), copying, {
91
+ const inherited = inheritsPaint(node) ? inheritedPaintOf(node, src.parentsOf(node)) : void 0;
92
+ if (!reaches(node, pose, derived, inherited, query, () => src.parentsOf(node), copying, {
91
93
  clipOf,
92
94
  copiesOf,
93
95
  carry
@@ -1 +1 @@
1
- {"version":3,"file":"pickWalk.js","names":[],"sources":["../../src/canvas/pickWalk.ts"],"sourcesContent":["/**\n * The one hit-test walk. Everything that decides whether a node *can* be hit lives here; a\n * query supplies only the shape test and the clip predicate for its own region.\n *\n * The gates, in the order the walk applies them:\n * 1. containers, when the query does not want them\n * 2. the node is painted at all — not hidden in the scene (`nodeGates`),\n * and on a layer the asking view's `layerVisibility` / `layerOrder` paints\n * 3. painted alpha is above zero\n * 4. the node is not locked, unless the query asks for locked nodes\n * 5. the query's own shape test\n * 6. every ancestor clip admits the query region\n *\n * 2, 3 and 6 are all \"the renderer did not put this on screen\". A pick path\n * that skips one answers for a node the user cannot see. 4 is the other axis:\n * a locked node is on screen and still out of reach.\n */\n\nimport { invert, type Affine, type Path } from '@weasel-js/geom';\nimport { findShapeSilhouette } from 'canvas/NodeShape';\nimport type { ClipFromPoseFn, CopiesFromPoseFn, DerivedDep, Node, NodeId, Scene } from 'core/scene/types';\nimport { asNodeId } from 'core/scene/types';\nimport { annotationLayerIds } from 'core/scene/annotation';\nimport { nodeGates } from 'core/scene/nodeGates';\nimport { resolveDerivedPath, sceneDerivedPathOf, scenePaintedPoseOf, showsComputedPath } from './derivedPath';\nimport { planeMap, type ParallaxOpts, type PlaneMap } from 'core/viewport/parallax';\nimport type { View } from 'core/viewport/view';\nimport { clipCarrier } from './planeClips';\nimport { reaches } from './pickReach';\n\n/** Shared, never mutated: most scenes are flat and every node returns it. */\nconst EMPTY_PARENTS: readonly never[] = [];\n\n/** The subset of a node the walk itself reads. Deliberately structural: a\n * bare adapter's node satisfies it as readily as a `SceneNode`. */\nexport interface PickCandidate<TPose> {\n id: string;\n kind?: string;\n layer?: string;\n pose: TPose;\n /** `false` makes the node transparent to the walk — see `SceneNode.pickable`. */\n pickable?: boolean;\n clipFromPose?: ClipFromPoseFn<TPose>;\n copiesFromPose?: CopiesFromPoseFn<TPose>;\n}\n\n/**\n * Everything the walk reads that a **view** owns.\n *\n * A layer must not hold state a view owns, so none of these are closed over\n * at construction — each is asked per walk, of the view being dispatched to.\n */\nexport interface PickSource<TPose> {\n /** Candidates in paint order, back-to-front. Containers included; the\n * query decides whether they can be hit. */\n order(): readonly PickCandidate<TPose>[];\n /** The pose the renderer draws — override-aware. Not `node.pose`, which is\n * how the render and pick paths came to disagree about where a node is. */\n poseOf(node: PickCandidate<TPose>): TPose;\n /** Ancestors, innermost first. Only walked for candidates that already\n * passed their own shape test. */\n parentsOf(node: PickCandidate<TPose>): readonly PickCandidate<TPose>[];\n /** Painted alpha for the asking view: the view's `alphaFor` times any\n * per-node override alpha. Omit when the source has no alpha concept. */\n alphaOf?(id: string): number;\n /** Whether this layer reaches the screen in the asking view. Omit for a\n * source with no layers. */\n layerIsPainted?(layer: string): boolean;\n /** Whether the node is locked — painted, but not something a pointer can\n * take. Omit for a source with no lock concept. */\n isLocked?(node: PickCandidate<TPose>): boolean;\n /** Whether the source hides the node, so nothing of it is on screen. Omit\n * for a source that hides nothing. */\n isHidden?(node: PickCandidate<TPose>): boolean;\n /** The path a node computes from its dependencies, or null for none. */\n derivedPathOf?(node: PickCandidate<TPose>): Path | null;\n /** How the asking view's world maps into a parallax layer's, or null for a\n * layer that moves with the camera. Omit for a source with no planes. */\n planeOf?(layer: string): PlaneMap | null;\n}\n\n/** One hit-test question: what region, and what counts as covering it. */\nexport interface PickQuery<TPose> {\n /** Default `true`. The area walks exclude containers so a marquee does not\n * return a container and its children both. */\n includeContainers?: boolean;\n /** Default `false`. Every selecting or editing query passes over locked\n * nodes; one that only samples what is painted can take them back. */\n includeLocked?: boolean;\n /** Does this node, drawn at this pose, cover the query region? `derived`\n * is the node's resolved path when it has one — a derived node's pose is a\n * placeholder, so a region test that reads only the pose answers for the\n * wrong box. */\n hits(node: PickCandidate<TPose>, pose: TPose, derived?: Path | null): boolean;\n /**\n * Does an ancestor's clip leave anything of `node` for this query to hit?\n *\n * A point query asks point-in-clip and ignores the node. An area query has\n * to ask about the node as well: the clip and the area can overlap where\n * the node is not, and the node and the clip can overlap where the area is\n * not, and only the two terms together reject both.\n */\n clipAdmits(\n clip: Path,\n node: PickCandidate<TPose>,\n pose: TPose,\n derived?: Path | null,\n ): boolean;\n /** The same question asked in a parallax plane's world: the region carried\n * through `map`, and any screen-pixel slop re-derived at the plane's scale.\n * A query without it cannot reach nodes on a plane — it would test them\n * where their poses sit rather than where they are painted. */\n inPlane?(map: PlaneMap): PickQuery<TPose>;\n /** The same question carried by `m`, an affine from world into the source\n * frame of a copy a container draws. A query without it cannot reach\n * copies — see `ContainerNode.copiesFromPose`. */\n inCopy?(m: Affine): PickQuery<TPose>;\n}\n\n/** The clip a container imposes on its subtree, or null when it imposes none. */\nexport function ownClipOf<TPose>(\n node: PickCandidate<TPose>,\n pose: TPose,\n derived?: Path | null,\n): Path | null {\n if (node.kind !== 'container') return null;\n if (typeof node.clipFromPose === 'function') return node.clipFromPose(pose);\n return findShapeSilhouette(\n node as unknown as Node<unknown, string, TPose>,\n pose,\n { derivedPath: derived },\n );\n}\n\n/**\n * Collect every candidate the query hits, back-to-front — the last element is\n * the topmost, which is the order `pickTopMostHit` expects.\n */\nexport function pickWalk<TPose>(\n src: PickSource<TPose>,\n q: PickQuery<TPose>,\n): string[] {\n const includeContainers = q.includeContainers ?? true;\n const isLocked = q.includeLocked === true ? undefined : src.isLocked;\n const out: string[] = [];\n\n // Resolving a container's clip is the expensive half, so the *path* is memoized per\n // container for the walk. The predicate re-runs per candidate: caching its answer per\n // container would answer the wrong question for an area query.\n const clipPath = new Map<string, Path | null>();\n // A clip is in its container's plane; the query is in the candidate's.\n const carry = clipCarrier(src.planeOf ?? null);\n\n const clipOf = (ancestor: PickCandidate<TPose>): Path | null => {\n const cached = clipPath.get(ancestor.id);\n if (cached !== undefined) return cached;\n const resolved = ownClipOf(\n ancestor, src.poseOf(ancestor), src.derivedPathOf?.(ancestor),\n );\n clipPath.set(ancestor.id, resolved);\n return resolved;\n };\n\n const copyMaps = new Map<string, readonly Affine[]>();\n const copiesOf = (ancestor: PickCandidate<TPose>): readonly Affine[] => {\n let found = copyMaps.get(ancestor.id);\n if (found === undefined) {\n const fn = ancestor.kind === 'container' ? ancestor.copiesFromPose : undefined;\n const copies = fn ? fn(src.poseOf(ancestor), ancestor as never) ?? [] : [];\n found = copies.map(invert).filter((m): m is Affine => m !== null);\n copyMaps.set(ancestor.id, found);\n }\n return found;\n };\n\n // One reprojected query per plane per walk; `null` marks a plane this\n // query cannot reach.\n const planeQueries = new Map<string, PickQuery<TPose> | null>();\n const queryFor = (layer: string | undefined): PickQuery<TPose> | null => {\n if (layer === undefined || src.planeOf === undefined) return q;\n let found = planeQueries.get(layer);\n if (found === undefined) {\n const map = src.planeOf(layer);\n found = map === null ? q : (q.inPlane?.(map) ?? null);\n planeQueries.set(layer, found);\n }\n return found;\n };\n\n const order = src.order();\n const copying = order.some((n) => n.kind === 'container' && typeof n.copiesFromPose === 'function');\n for (const node of order) {\n if (!includeContainers && node.kind === 'container') continue;\n // Transparent to picking: the click belongs to whatever is behind it,\n // which for a label inside a box is the box.\n if (node.pickable === false) continue;\n if (src.isHidden?.(node) === true) continue;\n if (node.layer !== undefined && src.layerIsPainted?.(node.layer) === false) continue;\n if (src.alphaOf !== undefined && src.alphaOf(node.id) <= 0) continue;\n if (isLocked !== undefined && isLocked(node)) continue;\n\n const query = queryFor(node.layer);\n if (query === null) continue;\n const pose = src.poseOf(node);\n const derived = src.derivedPathOf?.(node);\n if (!reaches(node, pose, derived, query, () => src.parentsOf(node), copying, { clipOf, copiesOf, carry })) continue;\n out.push(node.id);\n }\n\n return out;\n}\n\n/**\n * The two answers a pick needs from the view it is asked for rather than from\n * the data behind it: how that view paints a node, and whether it paints a\n * layer at all. A scene and a bare adapter take the same pair.\n */\nexport interface ViewPickGates {\n /** Painted alpha: the view's `alphaFor` times any per-node override alpha.\n * A node painted at alpha 0 cannot be hit. */\n alphaOf?: (id: string) => number;\n /** Whether a node's `layer` reaches the screen in the asking view. */\n layerIsPainted?: (layer: string) => boolean;\n}\n\n/** The gates a bare adapter takes from its view, plus how that view's camera\n * maps into each parallax layer — see `scenePlaneOf`. */\nexport interface AdapterPickGates extends ViewPickGates {\n planeOf?: (layer: string) => PlaneMap | null;\n}\n\nexport interface ScenePickSourceOptions<TPose> extends ViewPickGates {\n /** Pose lookup, when the caller holds an adapter that already resolves\n * overrides. Defaults to `effectivePose` against the scene's own. */\n getPose?: (id: string) => TPose;\n /** The asking view's composed alpha — `alphaFor(id)` times override alpha.\n * Defaults to the override alpha alone, which is all a bare scene knows. */\n alphaOf?: (id: string) => number;\n /** The asking view's answer to \"does this scene layer reach the screen\",\n * which is `layerVisibility` and `layerOrder` on top of the scene's own\n * `visible` flag. Consulted after the scene's flag, never instead of it. */\n layerIsPainted?: (layer: string) => boolean;\n /** The asking view's camera. With it, a layer carrying `parallax` is picked\n * through its plane; without it, every layer is taken to move with the\n * camera. */\n camera?: View;\n}\n\nexport function scenePickSource<TData, TLayer extends string, TPose>(\n scene: Scene<TData, TLayer, TPose>,\n opts: ScenePickSourceOptions<TPose> = {},\n): PickSource<TPose> {\n // Partial scene stand-ins carry only `layers` and `get`, which is all the gates read.\n const gates = nodeGates(scene as never);\n const annotations = annotationLayerIds(scene.layers);\n const { getPose, alphaOf, layerIsPainted } = opts;\n\n // Resolved once per walk rather than once per candidate. Needs enumeration — a\n // partial scene stand-in carrying only `read` falls back to reading per node.\n let faded: Set<string> | null = null;\n const enumerable = typeof scene.overrides?.ids === 'function';\n if (!alphaOf && enumerable) {\n for (const id of scene.overrides.ids()) {\n const a = scene.overrides.read(id)?.alpha;\n if (a !== undefined && a <= 0) (faded ??= new Set()).add(id);\n }\n }\n const readAlpha = alphaOf\n ?? (enumerable\n ? (faded ? (id: string) => (faded.has(id) ? 0 : 1) : undefined)\n : (id: string) => scene.overrides.read(asNodeId(id))?.alpha ?? 1);\n\n // A composing strategy makes a node's own pose local, so picking folds the\n // parent chain the same way the render walk does: one definition of both.\n const paintedPoseOf = scenePaintedPoseOf(scene);\n const poseOf = getPose\n ? (node: PickCandidate<TPose>) => getPose(node.id)\n : (node: PickCandidate<TPose>) => paintedPoseOf(node as never);\n\n // Through `poseOf`, not the document pose: a derived node has to be tested\n // where the renderer draws its dependencies, which is what an override says.\n const childrenOf = (id: NodeId): readonly NodeId[] => scene.childrenOf(id);\n const depOf = (id: NodeId): DerivedDep<TPose> | undefined => {\n const dep = scene.get(id);\n if (dep === undefined) return undefined;\n return {\n node: dep as Node<unknown, string, TPose>,\n pose: poseOf(dep as never),\n get path(): Path | null {\n return resolveDerivedPath(dep as never, depOf, childrenOf);\n },\n };\n };\n\n const sceneDerivedPath = getPose ? null : sceneDerivedPathOf(scene);\n const derivedPathOf = (node: PickCandidate<TPose>): Path | null => {\n const n = node as unknown as Node<TData, TLayer, TPose>;\n if (!sceneDerivedPath) return n.dependsOn === undefined ? null : resolveDerivedPath(n, depOf, childrenOf);\n return showsComputedPath(scene, n) ? sceneDerivedPath(n) : null;\n };\n\n const planeOf = opts.camera ? scenePlaneOf(scene.layers, opts.camera) : null;\n\n return {\n ...(planeOf ? { planeOf } : {}),\n order: () => scene.renderOrderNodes() as unknown as readonly PickCandidate<TPose>[],\n derivedPathOf,\n poseOf,\n parentsOf(node) {\n let parentId = (node as { parent?: string | null }).parent ?? null;\n if (parentId === null) return EMPTY_PARENTS;\n const chain: PickCandidate<TPose>[] = [];\n const seen = new Set<string>();\n while (parentId !== null) {\n if (seen.has(parentId)) break;\n seen.add(parentId);\n const parent = scene.get(asNodeId(parentId));\n if (!parent) break;\n chain.push(parent as unknown as PickCandidate<TPose>);\n parentId = parent.parent ?? null;\n }\n return chain;\n },\n // Absent entirely when nothing is faded, so the walk skips the gate.\n ...(readAlpha ? { alphaOf: readAlpha } : {}),\n isLocked: (node: PickCandidate<TPose>) => gates.locked(node.id),\n isHidden: (node: PickCandidate<TPose>) => gates.hidden(node.id),\n ...(annotations.size > 0 || layerIsPainted\n ? {\n layerIsPainted: (layer: string) =>\n !annotations.has(layer) && (layerIsPainted === undefined || layerIsPainted(layer)),\n }\n : {}),\n };\n}\n\n/** Each parallax layer's plane map under `camera`, or null when no layer is a\n * plane — which keeps the per-layer lookup off the walk entirely. */\nexport function scenePlaneOf(\n layers: readonly { id: string; parallax?: ParallaxOpts }[] | undefined,\n camera: View,\n): ((layer: string) => PlaneMap | null) | null {\n let maps: Map<string, PlaneMap> | null = null;\n for (const l of layers ?? []) {\n if (l.parallax) (maps ??= new Map()).set(l.id, planeMap(camera, l.parallax));\n }\n if (maps === null) return null;\n const found = maps;\n return (layer) => found.get(layer) ?? null;\n}\n\nexport { adapterPickSource } from './adapterPickSource';\n"],"mappings":"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"}
1
+ {"version":3,"file":"pickWalk.js","names":[],"sources":["../../src/canvas/pickWalk.ts"],"sourcesContent":["/**\n * The one hit-test walk. Everything that decides whether a node *can* be hit lives here; a\n * query supplies only the shape test and the clip predicate for its own region.\n *\n * The gates, in the order the walk applies them:\n * 1. containers, when the query does not want them\n * 2. the node is painted at all — not hidden in the scene (`nodeGates`),\n * and on a layer the asking view's `layerVisibility` / `layerOrder` paints\n * 3. painted alpha is above zero\n * 4. the node is not locked, unless the query asks for locked nodes\n * 5. the query's own shape test\n * 6. every ancestor clip admits the query region\n *\n * 2, 3 and 6 are all \"the renderer did not put this on screen\". A pick path\n * that skips one answers for a node the user cannot see. 4 is the other axis:\n * a locked node is on screen and still out of reach.\n */\n\nimport { invert, type Affine, type Path } from '@weasel-js/geom';\nimport { findShapeSilhouette } from 'canvas/NodeShape';\nimport type { ClipFromPoseFn, CopiesFromPoseFn, DerivedDep, Node, NodeId, Scene } from 'core/scene/types';\nimport { asNodeId } from 'core/scene/types';\nimport { annotationLayerIds } from 'core/scene/annotation';\nimport { nodeGates } from 'core/scene/nodeGates';\nimport { inheritedPaintOf, inheritsPaint, type InheritedPaint } from 'core/scene/inheritedPaint';\nimport { resolveDerivedPath, sceneDerivedPathOf, scenePaintedPoseOf, showsComputedPath } from './derivedPath';\nimport { planeMap, type ParallaxOpts, type PlaneMap } from 'core/viewport/parallax';\nimport type { View } from 'core/viewport/view';\nimport { clipCarrier } from './planeClips';\nimport { reaches } from './pickReach';\n\n/** Shared, never mutated: most scenes are flat and every node returns it. */\nconst EMPTY_PARENTS: readonly never[] = [];\n\n/** The subset of a node the walk itself reads. Deliberately structural: a\n * bare adapter's node satisfies it as readily as a `SceneNode`. */\nexport interface PickCandidate<TPose> {\n id: string;\n kind?: string;\n layer?: string;\n pose: TPose;\n /** `false` makes the node transparent to the walk — see `SceneNode.pickable`. */\n pickable?: boolean;\n /** Read for an `'inherit'` fill or stroke, which takes its containers'. */\n data?: unknown;\n clipFromPose?: ClipFromPoseFn<TPose>;\n copiesFromPose?: CopiesFromPoseFn<TPose>;\n}\n\n/**\n * Everything the walk reads that a **view** owns.\n *\n * A layer must not hold state a view owns, so none of these are closed over\n * at construction — each is asked per walk, of the view being dispatched to.\n */\nexport interface PickSource<TPose> {\n /** Candidates in paint order, back-to-front. Containers included; the\n * query decides whether they can be hit. */\n order(): readonly PickCandidate<TPose>[];\n /** The pose the renderer draws — override-aware. Not `node.pose`, which is\n * how the render and pick paths came to disagree about where a node is. */\n poseOf(node: PickCandidate<TPose>): TPose;\n /** Ancestors, innermost first. Only walked for candidates that already\n * passed their own shape test. */\n parentsOf(node: PickCandidate<TPose>): readonly PickCandidate<TPose>[];\n /** Painted alpha for the asking view: the view's `alphaFor` times any\n * per-node override alpha. Omit when the source has no alpha concept. */\n alphaOf?(id: string): number;\n /** Whether this layer reaches the screen in the asking view. Omit for a\n * source with no layers. */\n layerIsPainted?(layer: string): boolean;\n /** Whether the node is locked — painted, but not something a pointer can\n * take. Omit for a source with no lock concept. */\n isLocked?(node: PickCandidate<TPose>): boolean;\n /** Whether the source hides the node, so nothing of it is on screen. Omit\n * for a source that hides nothing. */\n isHidden?(node: PickCandidate<TPose>): boolean;\n /** The path a node computes from its dependencies, or null for none. */\n derivedPathOf?(node: PickCandidate<TPose>): Path | null;\n /** How the asking view's world maps into a parallax layer's, or null for a\n * layer that moves with the camera. Omit for a source with no planes. */\n planeOf?(layer: string): PlaneMap | null;\n}\n\n/** One hit-test question: what region, and what counts as covering it. */\nexport interface PickQuery<TPose> {\n /** Default `true`. The area walks exclude containers so a marquee does not\n * return a container and its children both. */\n includeContainers?: boolean;\n /** Default `false`. Every selecting or editing query passes over locked\n * nodes; one that only samples what is painted can take them back. */\n includeLocked?: boolean;\n /** Does this node, drawn at this pose, cover the query region? `derived`\n * is the node's resolved path when it has one — a derived node's pose is a\n * placeholder, so a region test that reads only the pose answers for the\n * wrong box. `inherited` is what its `'inherit'` fields take from its\n * containers, for the ink it is tested against; absent when it inherits\n * nothing. */\n hits(node: PickCandidate<TPose>, pose: TPose, derived?: Path | null, inherited?: InheritedPaint): boolean;\n /**\n * Does an ancestor's clip leave anything of `node` for this query to hit?\n *\n * A point query asks point-in-clip and ignores the node. An area query has\n * to ask about the node as well: the clip and the area can overlap where\n * the node is not, and the node and the clip can overlap where the area is\n * not, and only the two terms together reject both.\n */\n clipAdmits(\n clip: Path,\n node: PickCandidate<TPose>,\n pose: TPose,\n derived?: Path | null,\n ): boolean;\n /** The same question asked in a parallax plane's world: the region carried\n * through `map`, and any screen-pixel slop re-derived at the plane's scale.\n * A query without it cannot reach nodes on a plane — it would test them\n * where their poses sit rather than where they are painted. */\n inPlane?(map: PlaneMap): PickQuery<TPose>;\n /** The same question carried by `m`, an affine from world into the source\n * frame of a copy a container draws. A query without it cannot reach\n * copies — see `ContainerNode.copiesFromPose`. */\n inCopy?(m: Affine): PickQuery<TPose>;\n}\n\n/** The clip a container imposes on its subtree, or null when it imposes none. */\nexport function ownClipOf<TPose>(\n node: PickCandidate<TPose>,\n pose: TPose,\n derived?: Path | null,\n): Path | null {\n if (node.kind !== 'container') return null;\n if (typeof node.clipFromPose === 'function') return node.clipFromPose(pose);\n return findShapeSilhouette(\n node as unknown as Node<unknown, string, TPose>,\n pose,\n { derivedPath: derived },\n );\n}\n\n/**\n * Collect every candidate the query hits, back-to-front — the last element is\n * the topmost, which is the order `pickTopMostHit` expects.\n */\nexport function pickWalk<TPose>(\n src: PickSource<TPose>,\n q: PickQuery<TPose>,\n): string[] {\n const includeContainers = q.includeContainers ?? true;\n const isLocked = q.includeLocked === true ? undefined : src.isLocked;\n const out: string[] = [];\n\n // Resolving a container's clip is the expensive half, so the *path* is memoized per\n // container for the walk. The predicate re-runs per candidate: caching its answer per\n // container would answer the wrong question for an area query.\n const clipPath = new Map<string, Path | null>();\n // A clip is in its container's plane; the query is in the candidate's.\n const carry = clipCarrier(src.planeOf ?? null);\n\n const clipOf = (ancestor: PickCandidate<TPose>): Path | null => {\n const cached = clipPath.get(ancestor.id);\n if (cached !== undefined) return cached;\n const resolved = ownClipOf(\n ancestor, src.poseOf(ancestor), src.derivedPathOf?.(ancestor),\n );\n clipPath.set(ancestor.id, resolved);\n return resolved;\n };\n\n const copyMaps = new Map<string, readonly Affine[]>();\n const copiesOf = (ancestor: PickCandidate<TPose>): readonly Affine[] => {\n let found = copyMaps.get(ancestor.id);\n if (found === undefined) {\n const fn = ancestor.kind === 'container' ? ancestor.copiesFromPose : undefined;\n const copies = fn ? fn(src.poseOf(ancestor), ancestor as never) ?? [] : [];\n found = copies.map(invert).filter((m): m is Affine => m !== null);\n copyMaps.set(ancestor.id, found);\n }\n return found;\n };\n\n // One reprojected query per plane per walk; `null` marks a plane this\n // query cannot reach.\n const planeQueries = new Map<string, PickQuery<TPose> | null>();\n const queryFor = (layer: string | undefined): PickQuery<TPose> | null => {\n if (layer === undefined || src.planeOf === undefined) return q;\n let found = planeQueries.get(layer);\n if (found === undefined) {\n const map = src.planeOf(layer);\n found = map === null ? q : (q.inPlane?.(map) ?? null);\n planeQueries.set(layer, found);\n }\n return found;\n };\n\n const order = src.order();\n const copying = order.some((n) => n.kind === 'container' && typeof n.copiesFromPose === 'function');\n for (const node of order) {\n if (!includeContainers && node.kind === 'container') continue;\n // Transparent to picking: the click belongs to whatever is behind it,\n // which for a label inside a box is the box.\n if (node.pickable === false) continue;\n if (src.isHidden?.(node) === true) continue;\n if (node.layer !== undefined && src.layerIsPainted?.(node.layer) === false) continue;\n if (src.alphaOf !== undefined && src.alphaOf(node.id) <= 0) continue;\n if (isLocked !== undefined && isLocked(node)) continue;\n\n const query = queryFor(node.layer);\n if (query === null) continue;\n const pose = src.poseOf(node);\n const derived = src.derivedPathOf?.(node);\n const inherited = inheritsPaint(node) ? inheritedPaintOf(node, src.parentsOf(node)) : undefined;\n if (!reaches(node, pose, derived, inherited, query, () => src.parentsOf(node), copying, { clipOf, copiesOf, carry })) continue;\n out.push(node.id);\n }\n\n return out;\n}\n\n/**\n * The two answers a pick needs from the view it is asked for rather than from\n * the data behind it: how that view paints a node, and whether it paints a\n * layer at all. A scene and a bare adapter take the same pair.\n */\nexport interface ViewPickGates {\n /** Painted alpha: the view's `alphaFor` times any per-node override alpha.\n * A node painted at alpha 0 cannot be hit. */\n alphaOf?: (id: string) => number;\n /** Whether a node's `layer` reaches the screen in the asking view. */\n layerIsPainted?: (layer: string) => boolean;\n}\n\n/** The gates a bare adapter takes from its view, plus how that view's camera\n * maps into each parallax layer — see `scenePlaneOf`. */\nexport interface AdapterPickGates extends ViewPickGates {\n planeOf?: (layer: string) => PlaneMap | null;\n}\n\nexport interface ScenePickSourceOptions<TPose> extends ViewPickGates {\n /** Pose lookup, when the caller holds an adapter that already resolves\n * overrides. Defaults to `effectivePose` against the scene's own. */\n getPose?: (id: string) => TPose;\n /** The asking view's composed alpha — `alphaFor(id)` times override alpha.\n * Defaults to the override alpha alone, which is all a bare scene knows. */\n alphaOf?: (id: string) => number;\n /** The asking view's answer to \"does this scene layer reach the screen\",\n * which is `layerVisibility` and `layerOrder` on top of the scene's own\n * `visible` flag. Consulted after the scene's flag, never instead of it. */\n layerIsPainted?: (layer: string) => boolean;\n /** The asking view's camera. With it, a layer carrying `parallax` is picked\n * through its plane; without it, every layer is taken to move with the\n * camera. */\n camera?: View;\n}\n\nexport function scenePickSource<TData, TLayer extends string, TPose>(\n scene: Scene<TData, TLayer, TPose>,\n opts: ScenePickSourceOptions<TPose> = {},\n): PickSource<TPose> {\n // Partial scene stand-ins carry only `layers` and `get`, which is all the gates read.\n const gates = nodeGates(scene as never);\n const annotations = annotationLayerIds(scene.layers);\n const { getPose, alphaOf, layerIsPainted } = opts;\n\n // Resolved once per walk rather than once per candidate. Needs enumeration — a\n // partial scene stand-in carrying only `read` falls back to reading per node.\n let faded: Set<string> | null = null;\n const enumerable = typeof scene.overrides?.ids === 'function';\n if (!alphaOf && enumerable) {\n for (const id of scene.overrides.ids()) {\n const a = scene.overrides.read(id)?.alpha;\n if (a !== undefined && a <= 0) (faded ??= new Set()).add(id);\n }\n }\n const readAlpha = alphaOf\n ?? (enumerable\n ? (faded ? (id: string) => (faded.has(id) ? 0 : 1) : undefined)\n : (id: string) => scene.overrides.read(asNodeId(id))?.alpha ?? 1);\n\n // A composing strategy makes a node's own pose local, so picking folds the\n // parent chain the same way the render walk does: one definition of both.\n const paintedPoseOf = scenePaintedPoseOf(scene);\n const poseOf = getPose\n ? (node: PickCandidate<TPose>) => getPose(node.id)\n : (node: PickCandidate<TPose>) => paintedPoseOf(node as never);\n\n // Through `poseOf`, not the document pose: a derived node has to be tested\n // where the renderer draws its dependencies, which is what an override says.\n const childrenOf = (id: NodeId): readonly NodeId[] => scene.childrenOf(id);\n const depOf = (id: NodeId): DerivedDep<TPose> | undefined => {\n const dep = scene.get(id);\n if (dep === undefined) return undefined;\n return {\n node: dep as Node<unknown, string, TPose>,\n pose: poseOf(dep as never),\n get path(): Path | null {\n return resolveDerivedPath(dep as never, depOf, childrenOf);\n },\n };\n };\n\n const sceneDerivedPath = getPose ? null : sceneDerivedPathOf(scene);\n const derivedPathOf = (node: PickCandidate<TPose>): Path | null => {\n const n = node as unknown as Node<TData, TLayer, TPose>;\n if (!sceneDerivedPath) return n.dependsOn === undefined ? null : resolveDerivedPath(n, depOf, childrenOf);\n return showsComputedPath(scene, n) ? sceneDerivedPath(n) : null;\n };\n\n const planeOf = opts.camera ? scenePlaneOf(scene.layers, opts.camera) : null;\n\n return {\n ...(planeOf ? { planeOf } : {}),\n order: () => scene.renderOrderNodes() as unknown as readonly PickCandidate<TPose>[],\n derivedPathOf,\n poseOf,\n parentsOf(node) {\n let parentId = (node as { parent?: string | null }).parent ?? null;\n if (parentId === null) return EMPTY_PARENTS;\n const chain: PickCandidate<TPose>[] = [];\n const seen = new Set<string>();\n while (parentId !== null) {\n if (seen.has(parentId)) break;\n seen.add(parentId);\n const parent = scene.get(asNodeId(parentId));\n if (!parent) break;\n chain.push(parent as unknown as PickCandidate<TPose>);\n parentId = parent.parent ?? null;\n }\n return chain;\n },\n // Absent entirely when nothing is faded, so the walk skips the gate.\n ...(readAlpha ? { alphaOf: readAlpha } : {}),\n isLocked: (node: PickCandidate<TPose>) => gates.locked(node.id),\n isHidden: (node: PickCandidate<TPose>) => gates.hidden(node.id),\n ...(annotations.size > 0 || layerIsPainted\n ? {\n layerIsPainted: (layer: string) =>\n !annotations.has(layer) && (layerIsPainted === undefined || layerIsPainted(layer)),\n }\n : {}),\n };\n}\n\n/** Each parallax layer's plane map under `camera`, or null when no layer is a\n * plane — which keeps the per-layer lookup off the walk entirely. */\nexport function scenePlaneOf(\n layers: readonly { id: string; parallax?: ParallaxOpts }[] | undefined,\n camera: View,\n): ((layer: string) => PlaneMap | null) | null {\n let maps: Map<string, PlaneMap> | null = null;\n for (const l of layers ?? []) {\n if (l.parallax) (maps ??= new Map()).set(l.id, planeMap(camera, l.parallax));\n }\n if (maps === null) return null;\n const found = maps;\n return (layer) => found.get(layer) ?? null;\n}\n\nexport { adapterPickSource } from './adapterPickSource';\n"],"mappings":"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"}
@@ -151,7 +151,7 @@ function createRasterSession(options = {}) {
151
151
  });
152
152
  renderer.setFlattenTolerance(flattenTolerance);
153
153
  }
154
- renderer.render(plan.commands, viewToMat3$1(plan.view));
154
+ renderer.render(plan.commands, viewToMat3$1(plan.view), { screen: args.screen });
155
155
  if (isLost(ctx)) throw new Error("raster session: the WebGL2 context was lost during render");
156
156
  const raw = new Uint8Array(width * height * 4);
157
157
  ctx.readPixels(0, 0, width, height, ctx.RGBA, ctx.UNSIGNED_BYTE, raw);
@@ -1 +1 @@
1
- {"version":3,"file":"renderSceneToPixels.js","names":["viewToMat3"],"sources":["../../src/canvas/renderSceneToPixels.ts"],"sourcesContent":["/**\n * `renderSceneToPixels` — headless rasterization of a scene-space rect to raw RGBA pixels at an\n * explicit, per-axis scale. It drives the same WebGL2 pipeline (`WeaselRenderer` +\n * `buildSceneViewCommands`) as the on-screen view, so print, thumbnail, and export callers get\n * the same pixels the screen would produce at that scale.\n *\n * Environment contract:\n * - Density arrives exclusively via `scale`; this function never reads\n * `window.devicePixelRatio`.\n * - Rounding: output width = max(1, round(sourceRect.width × |scale.x|)), height analogously.\n * When round() lands below rect × scale, the rightmost/bottom sub-pixel band of scene content\n * is cropped; when it rounds up, that band is padded (the `background` fill, or transparent).\n * - Context lifetime: a caller-supplied `gl` is caller-owned. It is never disposed here, and\n * its drawing buffer must be at least width × height pixels (the render targets the\n * bottom-left region). Auto-created canvases are discarded with the session.\n * - Context loss: throws — a lost context cannot produce pixels, and the silent-noop\n * convention of the screen path would return all-zero bytes.\n * - Output: top-down row order, NON-premultiplied (\"straight\") RGBA. The GL framebuffer is\n * premultiplied and bottom-up; readback flips and unpremultiplies.\n * - Determinism: same context + same inputs → same bytes. Cross-driver / cross-machine byte\n * equality is NOT guaranteed; do not build golden-image tests on committed bytes.\n * - Image quality: image textures upload with mipmaps so minified bitmaps don't moiré, and\n * curve tessellation runs at an output-scale tolerance (`flattenTolerancePx`, default 0.25\n * output px).\n */\nimport { WeaselRenderer } from '@weasel-js/renderer';\nimport { viewToMat3 } from '@weasel-js/geom';\nimport type { DrawCommand } from '@weasel-js/renderer';\nimport type { View } from '../core/viewport/view';\nimport type { Node, Scene } from 'core/scene/types';\nimport { buildSceneViewCommands, type SceneViewDrawOne } from './sceneViewRender';\nimport type { ColorOverrideRegistry } from '@weasel-js/animator';\nimport { defaultDrawOne } from './defaultDrawOne';\nimport { warmFonts, type FontRequest } from '@weasel-js/font';\nimport { isPaintKindKnown, warmPaintKinds } from '@weasel-js/renderer';\nimport { renderNeeds } from './renderNeeds';\nimport { straightTopDown } from './rasterReadback';\nimport { annotationLayerIds } from 'core/scene/annotation';\nimport { sceneLayerKey } from './sceneLayerPaint';\nimport { CANVAS_CONTEXT_ATTRIBUTES } from './contextAttributes';\n\n/** Plain RGBA raster — structurally `ImageData`-compatible ({ width, height,\n * data }), deliberately free of printer/dpi/physical-unit concepts. */\nexport interface RasterImage {\n width: number;\n height: number;\n /** Top-down, straight (non-premultiplied) RGBA, 4 bytes per pixel. */\n data: Uint8ClampedArray;\n}\n\n/** Minimal canvas contract for `createCanvas` injection: `OffscreenCanvas`,\n * an HTML canvas, or a test fake. */\nexport interface HeadlessCanvasLike {\n width: number;\n height: number;\n getContext(contextId: 'webgl2', options?: WebGLContextAttributes): unknown;\n}\n\n/** What to render, at what resolution, and with which quality settings. */\nexport interface RenderSceneToPixelsArgs<TData, TLayer extends string, TPose> {\n scene: Scene<TData, TLayer, TPose>;\n /** Scene-space rect to render (origin + size in scene units). Output pixel\n * dimensions follow from rect × scale (round, min 1 — see module doc). */\n sourceRect: { x: number; y: number; width: number; height: number };\n /** Output pixels per scene unit, per axis. Anisotropic values supported. A\n * negative axis is flipped: the output is sized from its magnitude, and the\n * rect's far edge on that axis lands on the output's top or left. */\n scale: { x: number; y: number };\n /** Per-node draw callback. Default: `defaultDrawOne` with `resolveImage`\n * merged into the `NodePaintCtx` the scene walk supplies. Custom `drawOne`\n * callers that still want resolver injection should call\n * `defaultDrawOne(node, pose, view, { ...ctx, resolveImage })` themselves —\n * dropping the walk's `ctx` drops derived geometry with it. */\n drawOne?: SceneViewDrawOne<TData, TLayer, TPose>;\n /** Bitmap resolver for image nodes — lets consumers reuse their own decode\n * caches. `undefined` results paint the deterministic grey placeholder\n * outline (see `NodePaintCtx.resolveImage`). */\n resolveImage?: (node: Node<TData, TLayer, TPose>) => ImageBitmap | undefined;\n /** Per-id alpha multiplier, mirroring `<SceneCanvas>`'s scene-slot\n * `alphaFor`. Pass the same function the on-screen canvas uses so an\n * export matches what the user is looking at. Defaults to `() => 1`. */\n alphaFor?: (id: string) => number;\n /** Animated vertex colors to paint, typically an animator's\n * `colorOverrides`, so an export shows the frame on screen. */\n colorOverrides?: ColorOverrideRegistry;\n /** Background fill (any CSS color accepted by the renderer). Default:\n * fully transparent. Passing a color is always valid. */\n background?: string;\n /** Caller-owned WebGL2 context to render with. Mutually exclusive with\n * `createCanvas`. Never disposed by this call. */\n gl?: WebGL2RenderingContext;\n /** One-shot canvas factory (DOM canvas, OffscreenCanvas, or test fake).\n * Mutually exclusive with `gl`. Default: `OffscreenCanvas` when\n * available, else `document.createElement('canvas')`. */\n createCanvas?: (widthPx: number, heightPx: number) => HeadlessCanvasLike;\n /** Max curve-flattening error in OUTPUT pixels. Default 0.25. Measured\n * along each path's most stretched axis. */\n flattenTolerancePx?: number;\n /** Commands drawn over the scene in output-pixel space — origin at the\n * output's top-left, one unit per output pixel. Annotations, watermarks,\n * and `renderDebugSnapshot`'s overlay ride here. */\n overlay?: readonly DrawCommand[];\n /** Draw the scene's annotation layers (`isAnnotationLayer`). Default\n * `false`: a raster is an export, which leaves them out as the SVG export\n * does. Pass `true` to reproduce the canvas as it looked. */\n annotations?: boolean;\n}\n\n/** The result of planning a headless render: how big the output is, the view\n * that maps the source rect onto it, and the commands to draw. */\nexport interface PixelRenderPlan {\n width: number;\n height: number;\n view: View;\n commands: DrawCommand[];\n}\n\n/** Pure planning half of `renderSceneToPixels`: output dimensions, the\n * anisotropic `View`, and the full command list (background + view-wrapped\n * scene). Exported for tests and for callers targeting their own renderer. */\nexport function planPixelRender<TData, TLayer extends string, TPose>(\n args: Omit<RenderSceneToPixelsArgs<TData, TLayer, TPose>, 'gl' | 'createCanvas' | 'flattenTolerancePx'>,\n): PixelRenderPlan {\n const { sourceRect, scale } = args;\n for (const [label, v] of [\n ['sourceRect.width', sourceRect.width], ['sourceRect.height', sourceRect.height],\n ] as const) {\n if (!Number.isFinite(v) || v <= 0) {\n throw new Error(`renderSceneToPixels: ${label} must be a positive finite number, got ${v}`);\n }\n }\n for (const [label, v] of [['scale.x', scale.x], ['scale.y', scale.y]] as const) {\n if (!Number.isFinite(v) || v === 0) {\n throw new Error(`renderSceneToPixels: ${label} must be a nonzero finite number, got ${v}`);\n }\n }\n for (const [label, v] of [\n ['sourceRect.x', sourceRect.x], ['sourceRect.y', sourceRect.y],\n ] as const) {\n if (!Number.isFinite(v)) {\n throw new Error(`renderSceneToPixels: ${label} must be a finite number, got ${v}`);\n }\n }\n const width = Math.max(1, Math.round(sourceRect.width * Math.abs(scale.x)));\n const height = Math.max(1, Math.round(sourceRect.height * Math.abs(scale.y)));\n const view: View = {\n x: scale.x < 0 ? sourceRect.x + sourceRect.width : sourceRect.x,\n y: scale.y < 0 ? sourceRect.y + sourceRect.height : sourceRect.y,\n scale: { x: scale.x, y: scale.y },\n };\n\n const resolveImage = args.resolveImage as ((n: Node<unknown, string, unknown>) => ImageBitmap | undefined) | undefined;\n const drawOne: SceneViewDrawOne<TData, TLayer, TPose> =\n args.drawOne ?? ((node, pose, view, ctx) => defaultDrawOne(node, pose, view, { ...ctx, resolveImage }));\n\n const commands: DrawCommand[] = [];\n if (args.background !== undefined) {\n // Screen-space (pre-view) fill so rounding can never leave uncovered\n // edge pixels.\n commands.push({\n kind: 'path',\n path: { kind: 'rect', x: 0, y: 0, width, height },\n fill: { paint: { kind: 'solid', color: args.background } },\n });\n }\n const layers = args.annotations ? undefined : {\n layerVisibility: Object.fromEntries([...annotationLayerIds(args.scene.layers)].map((id) => [sceneLayerKey(id), false])),\n };\n commands.push(...buildSceneViewCommands(\n args.scene, view, drawOne, undefined, args.alphaFor, layers, args.colorOverrides,\n ));\n if (args.overlay) commands.push(...args.overlay);\n return { width, height, view, commands };\n}\n\nfunction defaultCreateCanvas(width: number, height: number): HeadlessCanvasLike {\n if (typeof OffscreenCanvas !== 'undefined') {\n return new OffscreenCanvas(width, height);\n }\n if (typeof document !== 'undefined') {\n const c = document.createElement('canvas');\n c.width = width;\n c.height = height;\n return c;\n }\n throw new Error('renderSceneToPixels: no canvas source in this environment — supply `gl` or `createCanvas`');\n}\n\n/** Where a {@link RasterSession} draws. Pass `gl` or `createCanvas`, or\n * neither for the default canvas source. */\nexport interface RasterSessionOptions {\n /** Caller-owned WebGL2 context. The session never disposes it. Its drawing\n * buffer must be at least as large as every render the session does. */\n gl?: WebGL2RenderingContext;\n /** Canvas factory, called once, at the size of the first render. The\n * session grows the canvas when a later render needs more room and never\n * shrinks it. Default: `OffscreenCanvas` when available, else a detached\n * DOM canvas. */\n createCanvas?: (widthPx: number, heightPx: number) => HeadlessCanvasLike;\n}\n\n/** One render through a {@link RasterSession}: everything\n * `renderSceneToPixels` takes except where to draw. */\nexport type RasterRenderArgs<TData, TLayer extends string, TPose> =\n Omit<RenderSceneToPixelsArgs<TData, TLayer, TPose>, 'gl' | 'createCanvas'>;\n\n/** One renderer and its GPU caches, kept across any number of headless\n * renders. Scenes, source rects and scales may differ per render. */\nexport interface RasterSession {\n /** Same contract as `renderSceneToPixels`. Throws once disposed. */\n render<TData, TLayer extends string, TPose>(args: RasterRenderArgs<TData, TLayer, TPose>): RasterImage;\n /** Free every GL resource the session created — programs, geometry, and\n * the texture and mesh caches — and drop its own canvas. A caller-owned\n * context stays open. Idempotent. */\n dispose(): void;\n}\n\n/**\n * Open a raster session: one `WeaselRenderer` that many headless renders\n * share, for a consumer producing thumbnails or pages in bulk. Programs\n * compile once, and a bitmap or recurring mesh uploads once, rather than per\n * render. `renderSceneToPixels` is a session opened for one render.\n *\n * The renderer is created on the first render, so opening a session costs\n * nothing and fails nothing. Each render draws into the bottom-left\n * `width × height` of the buffer and reads that region back.\n */\nexport function createRasterSession(options: RasterSessionOptions = {}): RasterSession {\n if (options.gl && options.createCanvas) {\n throw new Error('raster session: `gl` and `createCanvas` are mutually exclusive');\n }\n let canvas: HeadlessCanvasLike | null = null;\n let gl: WebGL2RenderingContext | undefined = options.gl;\n let renderer: WeaselRenderer | null = null;\n let disposed = false;\n\n function contextFor(width: number, height: number): WebGL2RenderingContext {\n if (!options.gl) {\n if (!canvas) {\n canvas = (options.createCanvas ?? defaultCreateCanvas)(width, height);\n canvas.width = width;\n canvas.height = height;\n gl = (canvas.getContext('webgl2', CANVAS_CONTEXT_ATTRIBUTES) as WebGL2RenderingContext | null) ?? undefined;\n } else {\n if (canvas.width < width) canvas.width = width;\n if (canvas.height < height) canvas.height = height;\n }\n }\n if (!gl || typeof (gl as Partial<WebGL2RenderingContext>).enable !== 'function') {\n throw new Error('raster session: WebGL2 is unavailable — supply `gl` or a WebGL2-capable `createCanvas`');\n }\n if (isLost(gl)) {\n throw new Error('raster session: the WebGL2 context is lost — dispose this session and open another');\n }\n // A context clamps a drawing buffer it cannot allocate, and reading past\n // the real one returns transparent pixels rather than failing.\n const { drawingBufferWidth: bufferW, drawingBufferHeight: bufferH } = gl;\n if (bufferW < width || bufferH < height) {\n throw new Error(\n `raster session: a ${width} × ${height} render does not fit the context's `\n + `${bufferW} × ${bufferH} drawing buffer — lower \\`scale\\` or render the source rect in tiles`,\n );\n }\n return gl;\n }\n\n return {\n render(args) {\n if (disposed) throw new Error('raster session: render() on a disposed session');\n const plan = planPixelRender(args);\n const { width, height } = plan;\n const ctx = contextFor(width, height);\n const flattenTolerance = { px: args.flattenTolerancePx ?? 0.25 };\n if (!renderer) {\n renderer = new WeaselRenderer({\n gl: ctx,\n width,\n height,\n dpr: 1,\n imageMinification: 'mipmap',\n flattenTolerance,\n // Dynamic canvas-SDF glyphs must all bake inline — this path is\n // synchronous with no notify-and-redraw, and print must be complete.\n bakeBudget: Infinity,\n });\n } else {\n renderer.resize({ width, height, dpr: 1 });\n renderer.setFlattenTolerance(flattenTolerance);\n }\n renderer.render(plan.commands, viewToMat3(plan.view));\n if (isLost(ctx)) {\n throw new Error('raster session: the WebGL2 context was lost during render');\n }\n const raw = new Uint8Array(width * height * 4);\n ctx.readPixels(0, 0, width, height, ctx.RGBA, ctx.UNSIGNED_BYTE, raw);\n return { width, height, data: straightTopDown(raw, width, height) };\n },\n dispose() {\n if (disposed) return;\n disposed = true;\n renderer?.dispose();\n renderer = null;\n // A browser caps live contexts per page and evicts the oldest, which is\n // a canvas on screen; a dropped reference frees nothing until collected.\n if (canvas && !options.createCanvas) gl?.getExtension('WEBGL_lose_context')?.loseContext();\n canvas = null;\n gl = undefined;\n },\n };\n}\n\nfunction isLost(gl: WebGL2RenderingContext): boolean {\n return typeof gl.isContextLost === 'function' && gl.isContextLost() === true;\n}\n\n/** What {@link warmRender} loads. */\nexport interface WarmRenderOptions<TData = unknown, TLayer extends string = string, TPose = unknown> {\n /** The render about to happen, as `renderSceneToPixels` or\n * `RasterSession.render` will take it. Its commands are built the way the\n * render builds them, and only the faces and paint kinds they draw with\n * load. */\n render?: RasterRenderArgs<TData, TLayer, TPose>;\n /** Commands about to be drawn, read the same way as `render`'s. Both may be\n * given; what either needs loads. */\n commands?: readonly DrawCommand[];\n /** Fonts to load in place of the ones derived — family names or single\n * variants, as `warmFonts` takes them. */\n families?: readonly (string | FontRequest)[];\n /** Paint kinds to load in place of the ones derived, as `warmPaintKinds`\n * takes them. */\n paintKinds?: readonly string[];\n}\n\n/**\n * Load everything a synchronous render would otherwise draw as nothing — the\n * lazily registered font atlases and the paint kinds loaded on demand — so\n * that a `renderSceneToPixels` or `RasterSession.render` issued after it\n * resolves is complete.\n *\n * Pass the render (`{ render: args }`) or its commands, and only what they\n * draw with loads: each face their text is set in, through the same\n * resolution the renderer does, and each paint kind they name. A rejection\n * then means something this render needs failed to load. `families` and\n * `paintKinds` replace either derived list.\n *\n * With neither `render` nor `commands`, an omitted list loads everything\n * registered: every font family and every kind that has a loader — which\n * fails when any one of them does, and fetches what nothing draws.\n */\nexport function warmRender<TData, TLayer extends string, TPose>(\n opts: WarmRenderOptions<TData, TLayer, TPose> = {},\n): Promise<void> {\n let { families, paintKinds } = opts;\n if (opts.render || opts.commands) {\n let planned: DrawCommand[];\n try {\n planned = opts.render ? planPixelRender(opts.render).commands : [];\n } catch (err) {\n return Promise.reject(err);\n }\n const needs = renderNeeds([...planned, ...(opts.commands ?? [])]);\n families ??= needs.fonts;\n // A kind nothing registered draws nothing whether or not this waits.\n paintKinds ??= needs.paintKinds.filter(isPaintKindKnown);\n }\n return Promise.all([warmFonts(families), warmPaintKinds(paintKinds)]).then(() => undefined);\n}\n\n/**\n * Render part of a scene to raw pixels, with no canvas mounted and no React\n * involved — for export, thumbnails, print, and pixel-diff tests.\n *\n * Output size follows from the source rect and the requested scale, so\n * rendering the same region at a higher scale is a real resolution increase\n * rather than an upscale: curves are re-flattened and glyphs re-rasterized for\n * the output resolution.\n *\n * Synchronous, so nothing lands mid-render: a paint kind loaded on demand\n * (`mesh-gradient`, or one declared with `registerPaintKindLoader`) that has\n * not loaded yet draws nothing, and neither does text set in a font atlas\n * still fetching — one registered `{ lazy: true }` is not fetched until text\n * first asks for it. `await warmRender({ render: args })` first.\n *\n * Each call opens a {@link RasterSession} and disposes it before returning.\n * Rendering many scenes, open one with `createRasterSession` instead.\n */\nexport function renderSceneToPixels<TData, TLayer extends string, TPose>(\n args: RenderSceneToPixelsArgs<TData, TLayer, TPose>,\n): RasterImage {\n const { gl, createCanvas, ...renderArgs } = args;\n const session = createRasterSession({ gl, createCanvas });\n try {\n return session.render(renderArgs);\n } finally {\n session.dispose();\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"renderSceneToPixels.js","names":["viewToMat3"],"sources":["../../src/canvas/renderSceneToPixels.ts"],"sourcesContent":["/**\n * `renderSceneToPixels` — headless rasterization of a scene-space rect to raw RGBA pixels at an\n * explicit, per-axis scale. It drives the same WebGL2 pipeline (`WeaselRenderer` +\n * `buildSceneViewCommands`) as the on-screen view, so print, thumbnail, and export callers get\n * the same pixels the screen would produce at that scale.\n *\n * Environment contract:\n * - Density arrives exclusively via `scale`; this function never reads\n * `window.devicePixelRatio`.\n * - Rounding: output width = max(1, round(sourceRect.width × |scale.x|)), height analogously.\n * When round() lands below rect × scale, the rightmost/bottom sub-pixel band of scene content\n * is cropped; when it rounds up, that band is padded (the `background` fill, or transparent).\n * - Context lifetime: a caller-supplied `gl` is caller-owned. It is never disposed here, and\n * its drawing buffer must be at least width × height pixels (the render targets the\n * bottom-left region). Auto-created canvases are discarded with the session.\n * - Context loss: throws — a lost context cannot produce pixels, and the silent-noop\n * convention of the screen path would return all-zero bytes.\n * - Output: top-down row order, NON-premultiplied (\"straight\") RGBA. The GL framebuffer is\n * premultiplied and bottom-up; readback flips and unpremultiplies.\n * - Determinism: same context + same inputs → same bytes. Cross-driver / cross-machine byte\n * equality is NOT guaranteed; do not build golden-image tests on committed bytes.\n * - Image quality: image textures upload with mipmaps so minified bitmaps don't moiré, and\n * curve tessellation runs at an output-scale tolerance (`flattenTolerancePx`, default 0.25\n * output px).\n */\nimport { WeaselRenderer } from '@weasel-js/renderer';\nimport { viewToMat3, type GlMat3 } from '@weasel-js/geom';\nimport type { DrawCommand } from '@weasel-js/renderer';\nimport type { View } from '../core/viewport/view';\nimport type { Node, Scene } from 'core/scene/types';\nimport { buildSceneViewCommands, type SceneViewDrawOne } from './sceneViewRender';\nimport type { ColorOverrideRegistry } from '@weasel-js/animator';\nimport { defaultDrawOne } from './defaultDrawOne';\nimport { warmFonts, type FontRequest } from '@weasel-js/font';\nimport { isPaintKindKnown, warmPaintKinds } from '@weasel-js/renderer';\nimport { renderNeeds } from './renderNeeds';\nimport { straightTopDown } from './rasterReadback';\nimport { annotationLayerIds } from 'core/scene/annotation';\nimport { sceneLayerKey } from './sceneLayerPaint';\nimport { CANVAS_CONTEXT_ATTRIBUTES } from './contextAttributes';\n\n/** Plain RGBA raster — structurally `ImageData`-compatible ({ width, height,\n * data }), deliberately free of printer/dpi/physical-unit concepts. */\nexport interface RasterImage {\n width: number;\n height: number;\n /** Top-down, straight (non-premultiplied) RGBA, 4 bytes per pixel. */\n data: Uint8ClampedArray;\n}\n\n/** Minimal canvas contract for `createCanvas` injection: `OffscreenCanvas`,\n * an HTML canvas, or a test fake. */\nexport interface HeadlessCanvasLike {\n width: number;\n height: number;\n getContext(contextId: 'webgl2', options?: WebGLContextAttributes): unknown;\n}\n\n/** What to render, at what resolution, and with which quality settings. */\nexport interface RenderSceneToPixelsArgs<TData, TLayer extends string, TPose> {\n scene: Scene<TData, TLayer, TPose>;\n /** Scene-space rect to render (origin + size in scene units). Output pixel\n * dimensions follow from rect × scale (round, min 1 — see module doc). */\n sourceRect: { x: number; y: number; width: number; height: number };\n /** Output pixels per scene unit, per axis. Anisotropic values supported. A\n * negative axis is flipped: the output is sized from its magnitude, and the\n * rect's far edge on that axis lands on the output's top or left. */\n scale: { x: number; y: number };\n /** Per-node draw callback. Default: `defaultDrawOne` with `resolveImage`\n * merged into the `NodePaintCtx` the scene walk supplies. Custom `drawOne`\n * callers that still want resolver injection should call\n * `defaultDrawOne(node, pose, view, { ...ctx, resolveImage })` themselves —\n * dropping the walk's `ctx` drops derived geometry with it. */\n drawOne?: SceneViewDrawOne<TData, TLayer, TPose>;\n /** Bitmap resolver for image nodes — lets consumers reuse their own decode\n * caches. `undefined` results paint the deterministic grey placeholder\n * outline (see `NodePaintCtx.resolveImage`). */\n resolveImage?: (node: Node<TData, TLayer, TPose>) => ImageBitmap | undefined;\n /** Per-id alpha multiplier, mirroring `<SceneCanvas>`'s scene-slot\n * `alphaFor`. Pass the same function the on-screen canvas uses so an\n * export matches what the user is looking at. Defaults to `() => 1`. */\n alphaFor?: (id: string) => number;\n /** Animated vertex colors to paint, typically an animator's\n * `colorOverrides`, so an export shows the frame on screen. */\n colorOverrides?: ColorOverrideRegistry;\n /** Background fill (any CSS color accepted by the renderer). Default:\n * fully transparent. Passing a color is always valid. */\n background?: string;\n /** Caller-owned WebGL2 context to render with. Mutually exclusive with\n * `createCanvas`. Never disposed by this call. */\n gl?: WebGL2RenderingContext;\n /** One-shot canvas factory (DOM canvas, OffscreenCanvas, or test fake).\n * Mutually exclusive with `gl`. Default: `OffscreenCanvas` when\n * available, else `document.createElement('canvas')`. */\n createCanvas?: (widthPx: number, heightPx: number) => HeadlessCanvasLike;\n /** Max curve-flattening error in OUTPUT pixels. Default 0.25. Measured\n * along each path's most stretched axis. */\n flattenTolerancePx?: number;\n /** Commands drawn over the scene in output-pixel space — origin at the\n * output's top-left, one unit per output pixel. Annotations, watermarks,\n * and `renderDebugSnapshot`'s overlay ride here. */\n overlay?: readonly DrawCommand[];\n /** Draw the scene's annotation layers (`isAnnotationLayer`). Default\n * `false`: a raster is an export, which leaves them out as the SVG export\n * does. Pass `true` to reproduce the canvas as it looked. */\n annotations?: boolean;\n /** Screen space → output pixels, which `'screen'`-unit paint is measured in. Default: output pixels. A\n * raster standing in for part of a larger surface, such as an export's page, names the whole here. */\n screen?: GlMat3;\n}\n\n/** The result of planning a headless render: how big the output is, the view\n * that maps the source rect onto it, and the commands to draw. */\nexport interface PixelRenderPlan {\n width: number;\n height: number;\n view: View;\n commands: DrawCommand[];\n}\n\n/** Pure planning half of `renderSceneToPixels`: output dimensions, the\n * anisotropic `View`, and the full command list (background + view-wrapped\n * scene). Exported for tests and for callers targeting their own renderer. */\nexport function planPixelRender<TData, TLayer extends string, TPose>(\n args: Omit<RenderSceneToPixelsArgs<TData, TLayer, TPose>, 'gl' | 'createCanvas' | 'flattenTolerancePx'>,\n): PixelRenderPlan {\n const { sourceRect, scale } = args;\n for (const [label, v] of [\n ['sourceRect.width', sourceRect.width], ['sourceRect.height', sourceRect.height],\n ] as const) {\n if (!Number.isFinite(v) || v <= 0) {\n throw new Error(`renderSceneToPixels: ${label} must be a positive finite number, got ${v}`);\n }\n }\n for (const [label, v] of [['scale.x', scale.x], ['scale.y', scale.y]] as const) {\n if (!Number.isFinite(v) || v === 0) {\n throw new Error(`renderSceneToPixels: ${label} must be a nonzero finite number, got ${v}`);\n }\n }\n for (const [label, v] of [\n ['sourceRect.x', sourceRect.x], ['sourceRect.y', sourceRect.y],\n ] as const) {\n if (!Number.isFinite(v)) {\n throw new Error(`renderSceneToPixels: ${label} must be a finite number, got ${v}`);\n }\n }\n const width = Math.max(1, Math.round(sourceRect.width * Math.abs(scale.x)));\n const height = Math.max(1, Math.round(sourceRect.height * Math.abs(scale.y)));\n const view: View = {\n x: scale.x < 0 ? sourceRect.x + sourceRect.width : sourceRect.x,\n y: scale.y < 0 ? sourceRect.y + sourceRect.height : sourceRect.y,\n scale: { x: scale.x, y: scale.y },\n };\n\n const resolveImage = args.resolveImage as ((n: Node<unknown, string, unknown>) => ImageBitmap | undefined) | undefined;\n const drawOne: SceneViewDrawOne<TData, TLayer, TPose> =\n args.drawOne ?? ((node, pose, view, ctx) => defaultDrawOne(node, pose, view, { ...ctx, resolveImage }));\n\n const commands: DrawCommand[] = [];\n if (args.background !== undefined) {\n // Screen-space (pre-view) fill so rounding can never leave uncovered\n // edge pixels.\n commands.push({\n kind: 'path',\n path: { kind: 'rect', x: 0, y: 0, width, height },\n fill: { paint: { kind: 'solid', color: args.background } },\n });\n }\n const layers = args.annotations ? undefined : {\n layerVisibility: Object.fromEntries([...annotationLayerIds(args.scene.layers)].map((id) => [sceneLayerKey(id), false])),\n };\n commands.push(...buildSceneViewCommands(\n args.scene, view, drawOne, undefined, args.alphaFor, layers, args.colorOverrides,\n ));\n if (args.overlay) commands.push(...args.overlay);\n return { width, height, view, commands };\n}\n\nfunction defaultCreateCanvas(width: number, height: number): HeadlessCanvasLike {\n if (typeof OffscreenCanvas !== 'undefined') {\n return new OffscreenCanvas(width, height);\n }\n if (typeof document !== 'undefined') {\n const c = document.createElement('canvas');\n c.width = width;\n c.height = height;\n return c;\n }\n throw new Error('renderSceneToPixels: no canvas source in this environment — supply `gl` or `createCanvas`');\n}\n\n/** Where a {@link RasterSession} draws. Pass `gl` or `createCanvas`, or\n * neither for the default canvas source. */\nexport interface RasterSessionOptions {\n /** Caller-owned WebGL2 context. The session never disposes it. Its drawing\n * buffer must be at least as large as every render the session does. */\n gl?: WebGL2RenderingContext;\n /** Canvas factory, called once, at the size of the first render. The\n * session grows the canvas when a later render needs more room and never\n * shrinks it. Default: `OffscreenCanvas` when available, else a detached\n * DOM canvas. */\n createCanvas?: (widthPx: number, heightPx: number) => HeadlessCanvasLike;\n}\n\n/** One render through a {@link RasterSession}: everything\n * `renderSceneToPixels` takes except where to draw. */\nexport type RasterRenderArgs<TData, TLayer extends string, TPose> =\n Omit<RenderSceneToPixelsArgs<TData, TLayer, TPose>, 'gl' | 'createCanvas'>;\n\n/** One renderer and its GPU caches, kept across any number of headless\n * renders. Scenes, source rects and scales may differ per render. */\nexport interface RasterSession {\n /** Same contract as `renderSceneToPixels`. Throws once disposed. */\n render<TData, TLayer extends string, TPose>(args: RasterRenderArgs<TData, TLayer, TPose>): RasterImage;\n /** Free every GL resource the session created — programs, geometry, and\n * the texture and mesh caches — and drop its own canvas. A caller-owned\n * context stays open. Idempotent. */\n dispose(): void;\n}\n\n/**\n * Open a raster session: one `WeaselRenderer` that many headless renders\n * share, for a consumer producing thumbnails or pages in bulk. Programs\n * compile once, and a bitmap or recurring mesh uploads once, rather than per\n * render. `renderSceneToPixels` is a session opened for one render.\n *\n * The renderer is created on the first render, so opening a session costs\n * nothing and fails nothing. Each render draws into the bottom-left\n * `width × height` of the buffer and reads that region back.\n */\nexport function createRasterSession(options: RasterSessionOptions = {}): RasterSession {\n if (options.gl && options.createCanvas) {\n throw new Error('raster session: `gl` and `createCanvas` are mutually exclusive');\n }\n let canvas: HeadlessCanvasLike | null = null;\n let gl: WebGL2RenderingContext | undefined = options.gl;\n let renderer: WeaselRenderer | null = null;\n let disposed = false;\n\n function contextFor(width: number, height: number): WebGL2RenderingContext {\n if (!options.gl) {\n if (!canvas) {\n canvas = (options.createCanvas ?? defaultCreateCanvas)(width, height);\n canvas.width = width;\n canvas.height = height;\n gl = (canvas.getContext('webgl2', CANVAS_CONTEXT_ATTRIBUTES) as WebGL2RenderingContext | null) ?? undefined;\n } else {\n if (canvas.width < width) canvas.width = width;\n if (canvas.height < height) canvas.height = height;\n }\n }\n if (!gl || typeof (gl as Partial<WebGL2RenderingContext>).enable !== 'function') {\n throw new Error('raster session: WebGL2 is unavailable — supply `gl` or a WebGL2-capable `createCanvas`');\n }\n if (isLost(gl)) {\n throw new Error('raster session: the WebGL2 context is lost — dispose this session and open another');\n }\n // A context clamps a drawing buffer it cannot allocate, and reading past\n // the real one returns transparent pixels rather than failing.\n const { drawingBufferWidth: bufferW, drawingBufferHeight: bufferH } = gl;\n if (bufferW < width || bufferH < height) {\n throw new Error(\n `raster session: a ${width} × ${height} render does not fit the context's `\n + `${bufferW} × ${bufferH} drawing buffer — lower \\`scale\\` or render the source rect in tiles`,\n );\n }\n return gl;\n }\n\n return {\n render(args) {\n if (disposed) throw new Error('raster session: render() on a disposed session');\n const plan = planPixelRender(args);\n const { width, height } = plan;\n const ctx = contextFor(width, height);\n const flattenTolerance = { px: args.flattenTolerancePx ?? 0.25 };\n if (!renderer) {\n renderer = new WeaselRenderer({\n gl: ctx,\n width,\n height,\n dpr: 1,\n imageMinification: 'mipmap',\n flattenTolerance,\n // Dynamic canvas-SDF glyphs must all bake inline — this path is\n // synchronous with no notify-and-redraw, and print must be complete.\n bakeBudget: Infinity,\n });\n } else {\n renderer.resize({ width, height, dpr: 1 });\n renderer.setFlattenTolerance(flattenTolerance);\n }\n renderer.render(plan.commands, viewToMat3(plan.view), { screen: args.screen });\n if (isLost(ctx)) {\n throw new Error('raster session: the WebGL2 context was lost during render');\n }\n const raw = new Uint8Array(width * height * 4);\n ctx.readPixels(0, 0, width, height, ctx.RGBA, ctx.UNSIGNED_BYTE, raw);\n return { width, height, data: straightTopDown(raw, width, height) };\n },\n dispose() {\n if (disposed) return;\n disposed = true;\n renderer?.dispose();\n renderer = null;\n // A browser caps live contexts per page and evicts the oldest, which is\n // a canvas on screen; a dropped reference frees nothing until collected.\n if (canvas && !options.createCanvas) gl?.getExtension('WEBGL_lose_context')?.loseContext();\n canvas = null;\n gl = undefined;\n },\n };\n}\n\nfunction isLost(gl: WebGL2RenderingContext): boolean {\n return typeof gl.isContextLost === 'function' && gl.isContextLost() === true;\n}\n\n/** What {@link warmRender} loads. */\nexport interface WarmRenderOptions<TData = unknown, TLayer extends string = string, TPose = unknown> {\n /** The render about to happen, as `renderSceneToPixels` or\n * `RasterSession.render` will take it. Its commands are built the way the\n * render builds them, and only the faces and paint kinds they draw with\n * load. */\n render?: RasterRenderArgs<TData, TLayer, TPose>;\n /** Commands about to be drawn, read the same way as `render`'s. Both may be\n * given; what either needs loads. */\n commands?: readonly DrawCommand[];\n /** Fonts to load in place of the ones derived — family names or single\n * variants, as `warmFonts` takes them. */\n families?: readonly (string | FontRequest)[];\n /** Paint kinds to load in place of the ones derived, as `warmPaintKinds`\n * takes them. */\n paintKinds?: readonly string[];\n}\n\n/**\n * Load everything a synchronous render would otherwise draw as nothing — the\n * lazily registered font atlases and the paint kinds loaded on demand — so\n * that a `renderSceneToPixels` or `RasterSession.render` issued after it\n * resolves is complete.\n *\n * Pass the render (`{ render: args }`) or its commands, and only what they\n * draw with loads: each face their text is set in, through the same\n * resolution the renderer does, and each paint kind they name. A rejection\n * then means something this render needs failed to load. `families` and\n * `paintKinds` replace either derived list.\n *\n * With neither `render` nor `commands`, an omitted list loads everything\n * registered: every font family and every kind that has a loader — which\n * fails when any one of them does, and fetches what nothing draws.\n */\nexport function warmRender<TData, TLayer extends string, TPose>(\n opts: WarmRenderOptions<TData, TLayer, TPose> = {},\n): Promise<void> {\n let { families, paintKinds } = opts;\n if (opts.render || opts.commands) {\n let planned: DrawCommand[];\n try {\n planned = opts.render ? planPixelRender(opts.render).commands : [];\n } catch (err) {\n return Promise.reject(err);\n }\n const needs = renderNeeds([...planned, ...(opts.commands ?? [])]);\n families ??= needs.fonts;\n // A kind nothing registered draws nothing whether or not this waits.\n paintKinds ??= needs.paintKinds.filter(isPaintKindKnown);\n }\n return Promise.all([warmFonts(families), warmPaintKinds(paintKinds)]).then(() => undefined);\n}\n\n/**\n * Render part of a scene to raw pixels, with no canvas mounted and no React\n * involved — for export, thumbnails, print, and pixel-diff tests.\n *\n * Output size follows from the source rect and the requested scale, so\n * rendering the same region at a higher scale is a real resolution increase\n * rather than an upscale: curves are re-flattened and glyphs re-rasterized for\n * the output resolution.\n *\n * Synchronous, so nothing lands mid-render: a paint kind loaded on demand\n * (`mesh-gradient`, or one declared with `registerPaintKindLoader`) that has\n * not loaded yet draws nothing, and neither does text set in a font atlas\n * still fetching — one registered `{ lazy: true }` is not fetched until text\n * first asks for it. `await warmRender({ render: args })` first.\n *\n * Each call opens a {@link RasterSession} and disposes it before returning.\n * Rendering many scenes, open one with `createRasterSession` instead.\n */\nexport function renderSceneToPixels<TData, TLayer extends string, TPose>(\n args: RenderSceneToPixelsArgs<TData, TLayer, TPose>,\n): RasterImage {\n const { gl, createCanvas, ...renderArgs } = args;\n const session = createRasterSession({ gl, createCanvas });\n try {\n return session.render(renderArgs);\n } finally {\n session.dispose();\n }\n}\n"],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { withInheritedPaints } from "./inheritedPaints.js";
1
2
  import { sceneDerivedPathOf, sceneShownToPose, withDerivedPaths } from "./derivedPath.js";
2
3
  import { withColorOverrides } from "./colorOverrides.js";
3
4
 
@@ -15,7 +16,7 @@ function composeAlphaFor(scene, alphaFor) {
15
16
  return alphaFor ? (id) => alphaFor(id) * overrideAlphaFor(id) : overrideAlphaFor;
16
17
  }
17
18
  function wireSceneSlotToScene(slot, scene, alphaFor, colorOverrides) {
18
- const derived = withDerivedPaths(scene, slot.drawOne, slot.toPose);
19
+ const derived = withDerivedPaths(scene, withInheritedPaints(scene, slot.drawOne), slot.toPose);
19
20
  const derivedPathOf = sceneDerivedPathOf(scene, slot.toPose);
20
21
  return {
21
22
  ...slot,
@@ -1 +1 @@
1
- {"version":3,"file":"sceneSlotWiring.js","names":[],"sources":["../../src/canvas/sceneSlotWiring.ts"],"sourcesContent":["/**\n * What the scene slot needs from the `Scene` that `<Canvas>` cannot see.\n *\n * `<Canvas>` is scene-agnostic: it walks an adapter. Three things a node is\n * painted with are therefore invisible to it — an ephemeral override's alpha, a\n * derived path, which needs other nodes' poses to resolve, and animated vertex\n * colors, which live in an animator. `<SceneCanvas>` folds them into the slot\n * here; `buildSceneViewCommands` does the same for the headless walk.\n */\nimport type { SceneSlotConfig } from './buildSceneLayer';\nimport type { Node, NodeId, Scene } from 'core/scene/types';\nimport { withDerivedPaths, sceneDerivedPathOf, sceneShownToPose } from './derivedPath';\nimport { withColorOverrides } from './colorOverrides';\nimport type { ColorOverrideRegistry } from '@weasel-js/animator';\n\n/**\n * The alpha a view paints a node at: the view's own `alphaFor` times any\n * ephemeral override alpha.\n *\n * The hit path asks this too — a node painted at alpha 0 is not on screen and\n * must not claim clicks — so it is one function rather than the painter's and\n * picking's separate readings of the same two inputs.\n */\nexport function composeAlphaFor<TData, TLayer extends string, TPose>(\n scene: Scene<TData, TLayer, TPose>,\n alphaFor?: (id: string) => number,\n): (id: string) => number {\n const overrideAlphaFor = (id: string) => scene.overrides.read(id as NodeId)?.alpha ?? 1;\n return alphaFor ? (id: string) => alphaFor(id) * overrideAlphaFor(id) : overrideAlphaFor;\n}\n\nexport function wireSceneSlotToScene<TData, TLayer extends string, TPose>(\n slot: SceneSlotConfig<Node<TData, TLayer, TPose>, TPose>,\n scene: Scene<TData, TLayer, TPose>,\n alphaFor?: (id: string) => number,\n colorOverrides?: ColorOverrideRegistry,\n): SceneSlotConfig<Node<TData, TLayer, TPose>, TPose> {\n const derived = withDerivedPaths(scene, slot.drawOne, slot.toPose);\n const derivedPathOf = sceneDerivedPathOf(scene, slot.toPose);\n return {\n ...slot,\n ...(slot.toPose ? { toPose: sceneShownToPose(scene, slot.toPose) } : {}),\n drawOne: colorOverrides ? withColorOverrides(colorOverrides, derived) : derived,\n derivedPathOf,\n alphaFor: composeAlphaFor(scene, alphaFor),\n };\n}\n"],"mappings":";;;;;;;;;;;;AAuBA,SAAgB,gBACd,OACA,UACwB;CACxB,MAAM,oBAAoB,OAAe,MAAM,UAAU,KAAK,EAAY,CAAC,EAAE,SAAS;CACtF,OAAO,YAAY,OAAe,SAAS,EAAE,IAAI,iBAAiB,EAAE,IAAI;AAC1E;AAEA,SAAgB,qBACd,MACA,OACA,UACA,gBACoD;CACpD,MAAM,UAAU,iBAAiB,OAAO,KAAK,SAAS,KAAK,MAAM;CACjE,MAAM,gBAAgB,mBAAmB,OAAO,KAAK,MAAM;CAC3D,OAAO;EACL,GAAG;EACH,GAAI,KAAK,SAAS,EAAE,QAAQ,iBAAiB,OAAO,KAAK,MAAM,EAAE,IAAI,CAAC;EACtE,SAAS,iBAAiB,mBAAmB,gBAAgB,OAAO,IAAI;EACxE;EACA,UAAU,gBAAgB,OAAO,QAAQ;CAC3C;AACF"}
1
+ {"version":3,"file":"sceneSlotWiring.js","names":[],"sources":["../../src/canvas/sceneSlotWiring.ts"],"sourcesContent":["/**\n * What the scene slot needs from the `Scene` that `<Canvas>` cannot see.\n *\n * `<Canvas>` is scene-agnostic: it walks an adapter. Four things a node is\n * painted with are therefore invisible to it — an ephemeral override's alpha, a\n * derived path, which needs other nodes' poses to resolve, the paint an\n * `'inherit'` field takes from its containers, and animated vertex colors,\n * which live in an animator. `<SceneCanvas>` folds them into the slot\n * here; `buildSceneViewCommands` does the same for the headless walk.\n */\nimport type { SceneSlotConfig } from './buildSceneLayer';\nimport type { Node, NodeId, Scene } from 'core/scene/types';\nimport { withInheritedPaints } from './inheritedPaints';\nimport { withDerivedPaths, sceneDerivedPathOf, sceneShownToPose } from './derivedPath';\nimport { withColorOverrides } from './colorOverrides';\nimport type { ColorOverrideRegistry } from '@weasel-js/animator';\n\n/**\n * The alpha a view paints a node at: the view's own `alphaFor` times any\n * ephemeral override alpha.\n *\n * The hit path asks this too — a node painted at alpha 0 is not on screen and\n * must not claim clicks — so it is one function rather than the painter's and\n * picking's separate readings of the same two inputs.\n */\nexport function composeAlphaFor<TData, TLayer extends string, TPose>(\n scene: Scene<TData, TLayer, TPose>,\n alphaFor?: (id: string) => number,\n): (id: string) => number {\n const overrideAlphaFor = (id: string) => scene.overrides.read(id as NodeId)?.alpha ?? 1;\n return alphaFor ? (id: string) => alphaFor(id) * overrideAlphaFor(id) : overrideAlphaFor;\n}\n\nexport function wireSceneSlotToScene<TData, TLayer extends string, TPose>(\n slot: SceneSlotConfig<Node<TData, TLayer, TPose>, TPose>,\n scene: Scene<TData, TLayer, TPose>,\n alphaFor?: (id: string) => number,\n colorOverrides?: ColorOverrideRegistry,\n): SceneSlotConfig<Node<TData, TLayer, TPose>, TPose> {\n const derived = withDerivedPaths(scene, withInheritedPaints(scene, slot.drawOne), slot.toPose);\n const derivedPathOf = sceneDerivedPathOf(scene, slot.toPose);\n return {\n ...slot,\n ...(slot.toPose ? { toPose: sceneShownToPose(scene, slot.toPose) } : {}),\n drawOne: colorOverrides ? withColorOverrides(colorOverrides, derived) : derived,\n derivedPathOf,\n alphaFor: composeAlphaFor(scene, alphaFor),\n };\n}\n"],"mappings":";;;;;;;;;;;;;AAyBA,SAAgB,gBACd,OACA,UACwB;CACxB,MAAM,oBAAoB,OAAe,MAAM,UAAU,KAAK,EAAY,CAAC,EAAE,SAAS;CACtF,OAAO,YAAY,OAAe,SAAS,EAAE,IAAI,iBAAiB,EAAE,IAAI;AAC1E;AAEA,SAAgB,qBACd,MACA,OACA,UACA,gBACoD;CACpD,MAAM,UAAU,iBAAiB,OAAO,oBAAoB,OAAO,KAAK,OAAO,GAAG,KAAK,MAAM;CAC7F,MAAM,gBAAgB,mBAAmB,OAAO,KAAK,MAAM;CAC3D,OAAO;EACL,GAAG;EACH,GAAI,KAAK,SAAS,EAAE,QAAQ,iBAAiB,OAAO,KAAK,MAAM,EAAE,IAAI,CAAC;EACtE,SAAS,iBAAiB,mBAAmB,gBAAgB,OAAO,IAAI;EACxE;EACA,UAAU,gBAAgB,OAAO,QAAQ;CAC3C;AACF"}
@@ -1,3 +1,4 @@
1
+ import { withInheritedPaints } from "./inheritedPaints.js";
1
2
  import { paintedSceneLayers } from "./sceneLayerPaint.js";
2
3
  import { paintCanvas } from "./canvasRenderer.js";
3
4
  import { wrapNodeOutput } from "./wrapNodeOutput.js";
@@ -43,7 +44,7 @@ function sceneAsHierarchy(scene, layers = {}) {
43
44
  * `layers.layerOrder`), each holding one group per node on that layer.
44
45
  */
45
46
  function buildSceneViewCommands(scene, view, drawOne, extraCommands, alphaFor, layers, colorOverrides, cull) {
46
- const derivedOnly = withDerivedPaths(scene, drawOne);
47
+ const derivedOnly = withDerivedPaths(scene, withInheritedPaints(scene, drawOne));
47
48
  const derived = colorOverrides ? withColorOverrides(colorOverrides, derivedOnly) : derivedOnly;
48
49
  const derivedPathOf = sceneDerivedPathOf(scene);
49
50
  const wrappedDrawOne = (node, pose, v) => {