@weasel-js/core 1.15.2 → 1.16.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 (47) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/dist/canvas/NodeShape.js +23 -10
  3. package/dist/canvas/NodeShape.js.map +1 -1
  4. package/dist/canvas/SceneCanvas/props.js.map +1 -1
  5. package/dist/canvas/SceneCanvas/useSceneCanvasLayers.js +1 -1
  6. package/dist/canvas/SceneCanvas/useSceneCanvasLayers.js.map +1 -1
  7. package/dist/canvas/SceneCanvas/useSceneRedrawSources.js +4 -1
  8. package/dist/canvas/SceneCanvas/useSceneRedrawSources.js.map +1 -1
  9. package/dist/canvas/SceneCanvas/useSceneSlotLayers.js +3 -3
  10. package/dist/canvas/SceneCanvas/useSceneSlotLayers.js.map +1 -1
  11. package/dist/canvas/SceneCanvas.js +1 -1
  12. package/dist/canvas/SceneCanvas.js.map +1 -1
  13. package/dist/canvas/SceneViewCanvas.js +4 -1
  14. package/dist/canvas/SceneViewCanvas.js.map +1 -1
  15. package/dist/canvas/nodeShapes/derivedPainter.js +57 -8
  16. package/dist/canvas/nodeShapes/derivedPainter.js.map +1 -1
  17. package/dist/canvas/nodeShapes/nodePaint.js +33 -2
  18. package/dist/canvas/nodeShapes/nodePaint.js.map +1 -1
  19. package/dist/canvas/nodeShapes/pathPainter.js +5 -4
  20. package/dist/canvas/nodeShapes/pathPainter.js.map +1 -1
  21. package/dist/canvas/nodeShapes/pickOutline.js +48 -0
  22. package/dist/canvas/nodeShapes/pickOutline.js.map +1 -0
  23. package/dist/canvas/nodeShapes/shapePainter.js +6 -4
  24. package/dist/canvas/nodeShapes/shapePainter.js.map +1 -1
  25. package/dist/canvas/paintOverrides.js +43 -0
  26. package/dist/canvas/paintOverrides.js.map +1 -0
  27. package/dist/canvas/renderSceneToPixels.js +1 -1
  28. package/dist/canvas/renderSceneToPixels.js.map +1 -1
  29. package/dist/canvas/sceneSlotWiring.js +3 -3
  30. package/dist/canvas/sceneSlotWiring.js.map +1 -1
  31. package/dist/canvas/sceneViewRender.js +11 -7
  32. package/dist/canvas/sceneViewRender.js.map +1 -1
  33. package/dist/canvas/trimOverrides.js +18 -0
  34. package/dist/canvas/trimOverrides.js.map +1 -0
  35. package/dist/core/paint/meshKindLazy.js +4 -1
  36. package/dist/core/paint/meshKindLazy.js.map +1 -1
  37. package/dist/core/scene/pathEffects/textAlong.js +2 -2
  38. package/dist/core/scene/pathEffects/textAlong.js.map +1 -1
  39. package/dist/domains/paths/pathLayer.js +20 -6
  40. package/dist/domains/paths/pathLayer.js.map +1 -1
  41. package/dist/domains/snap/angle2d.js +2 -2
  42. package/dist/domains/snap/angle2d.js.map +1 -1
  43. package/dist/index.d.ts +48 -31
  44. package/dist/version.js +1 -1
  45. package/package.json +25 -25
  46. package/dist/canvas/colorOverrides.js +0 -23
  47. package/dist/canvas/colorOverrides.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,60 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.16.0
4
+
5
+ ### Patch Changes
6
+
7
+ - 63056ee: A stroke can now draw only part of its path. `Stroke.trim` is a `StrokeTrim`, `{ start?, end?, pace? }`, and the stroke draws the stretch between `start` and `end` in the order the path was authored. Each edge is a `TrimAt`: a bare number is a fraction of the whole path, `{ length }` an absolute distance, `{ anchor }` a vertex index (`2.5` is halfway along the segment after anchor 2) and `{ subpath }` a subpath index. `pace` (`TrimPace`) says what a fraction is a fraction of: `'length'` (the default) moves at constant speed, and `'anchor'` gives every segment an equal share. An edge omitted is the path's start or end, and a window whose start is at or past its end draws nothing. Lengths are exact arc lengths in the path's own space, so the cut doesn't move with zoom or flattening tolerance. Tween `end` from 0 to 1 to draw a line on, then `start` after it to draw it off.
8
+
9
+ `markerStart` and `markerEnd` sit at each visible run's ends, so an end arrow rides the growing line; a run cut from a closed subpath takes neither. `markerMid` shows on the vertices inside the window. Dashes keep the pattern the untrimmed stroke has, so drawing on reveals the pattern rather than sliding it. Picking hits only the visible part of the stored trim. SVG and PDF export write the visible geometry, with curves split rather than flattened. SVG also writes `stroke-dashoffset` where a run starts mid-pattern. Import recovers no trim. Outlined text ignores its stroke's trim, since each glyph is stroked on its own.
10
+
11
+ To animate a trim without ops or undo, `Animator.trimOverrides` is a `TrimOverrideRegistry` (with `TrimOverride` and `TrimEdge`), read at draw time the way vertex-color overrides are. `<SceneCanvas animator>`, `<SceneViewCanvas animator>` and `MinimapCanvas` paint it and repaint when an override is set or cleared. `renderSceneToCanvas`, `renderSceneToPixels` and `createPathLayer` take `trimOverrides` beside `colorOverrides`. core exports both as one `PaintOverrides` type, which each of those argument types extends. `NodePaintCtx.trim` and `NodeInk.trim` carry a node's trim to its painter. In diagrams, `EdgeStyle.trim` trims an edge, and `DiagramViewProps.animator` animates it by the edge's scene node id. The draw.io, Mermaid and DOT writers warn when they drop a trim, and the scene reader reads it back.
12
+
13
+ New exports:
14
+
15
+ - paint: `StrokeTrim`, `TrimAt`, `TrimPace` and `strokeTrimKey`, a stable cache key per trim. core re-exports the three types.
16
+ - geom: `lengthTable`, `pathLengthTable`, `LengthTable`, `LengthRun`, `LengthSpan`, `lengthAtAnchor`, `anchorAtLength`, `lengthAtSubpath`, `lengthAtSegmentFraction`, `closingAnchor` and `pointAtLength`: exact pen-order arc length over 2D or 3D runs of lines and Bézier curves. Also `curveSpan`, `lengthAtCurve`, `runSegments`, `CurvePosition` and `CurveSpan`, which name a place on a path as a segment and curve parameter so every flattening cuts at the same point. Also `segmentLength(coords, dim, a?, b?)` and `segmentTAtLength(coords, dim, length, total?)` over flat control points, and the new fields `StrokeHitTestOptions.span` and `Polyline.closeAnchor`. In `geom/tessellate`: `polylineAtCurve` and `PolylinePlace`. In `geom/3d`: `pointAtLength3`.
17
+ - stroke: `resolveTrim`, `trimSpan`, `trimmedPath`, `TrimmedPath`, `trimmedRuns` and `TrimmedRun`. `splitForDash` takes an optional `phase`.
18
+ - animator: `ColorOverrideRegistry.subscribe`.
19
+ - svg: `SvgStroke.trim` and `SvgStroke.dashOffset`. svg now depends on `@weasel-js/stroke`.
20
+
21
+ Breaking and behavior changes:
22
+
23
+ - `buildSceneViewCommands` (core) takes a `PaintOverrides` object as its seventh argument instead of a `ColorOverrideRegistry`. Pass `{ colorOverrides }` where you passed the registry.
24
+ - `ColorOverrideRegistry.has(id)` (animator) no longer counts a target that was asked for and never moved. It is true only for an override from `set`, or for a target an animation has committed.
25
+ - `measurePath` and `pointAlongPath` (geom) now measure exact arc length rather than a flattening. Lengths and points move by up to a few hundredths of a unit on curved paths, and a label at 0.5 now sits where a trim at 0.5 cuts. `flattenTolerance` now affects only the headings and the `nearest` search.
26
+ - Rounded corners are measured by their exact arc length rather than by a fixed 32-step table, so corner segment lengths change slightly.
27
+ - `pathThroughPoseFrame` (geom) rounds a path's `corners` before applying a stretch or skew, as the renderer does, and returns them already rounded. A uniform frame still keeps them as corners.
28
+ - `extractPolylines` records, in `closeAnchor`, the anchor of a closing point it drops as a duplicate of the first.
29
+ - Diagram edges whose style sets no width now draw at `EDGE_STROKE_WIDTH`, 1.5, instead of 2 (`DEFAULT_EDGE_STROKE` and `themeEdgeStroke`).
30
+ - `svgPaintFromKit`, `svgStrokeFromKit` and `SvgKitPose` moved to their own module in svg; they are still exported from the package root.
31
+ - f9cfd00: Degree and radian conversions in these packages use geom's `DEG_TO_RAD` and `RAD_TO_DEG` instead of
32
+ writing `Math.PI / 180` out by hand. Results are unchanged up to floating-point rounding.
33
+ - Updated dependencies [22451fd]
34
+ - Updated dependencies [63056ee]
35
+ - Updated dependencies [f9cfd00]
36
+ - @weasel-js/prefs@1.16.0
37
+ - @weasel-js/routing@1.16.0
38
+ - @weasel-js/paint@1.16.0
39
+ - @weasel-js/geom@1.16.0
40
+ - @weasel-js/stroke@1.16.0
41
+ - @weasel-js/renderer@1.16.0
42
+ - @weasel-js/animator@1.16.0
43
+ - @weasel-js/text@1.16.0
44
+ - @weasel-js/text-dom@1.16.0
45
+ - @weasel-js/ui-logic@1.16.0
46
+ - @weasel-js/gestures@1.16.0
47
+ - @weasel-js/cursor@1.16.0
48
+ - @weasel-js/font@1.16.0
49
+ - @weasel-js/history@1.16.0
50
+ - @weasel-js/icons@1.16.0
51
+ - @weasel-js/modes@1.16.0
52
+ - @weasel-js/quantity@1.16.0
53
+ - @weasel-js/react@1.16.0
54
+ - @weasel-js/registry@1.16.0
55
+ - @weasel-js/select@1.16.0
56
+ - @weasel-js/simulation@1.16.0
57
+
3
58
  ## 1.15.2
4
59
 
5
60
  ### Patch Changes
@@ -1,7 +1,8 @@
1
1
  import { bumpNodeMemoGeneration, nodeMemo } from "../core/scene/nodeMemo.js";
2
2
  import { setDepOutlineReader } from "../core/scene/booleanOfChildren.js";
3
- import { hasSlop, slopForStrokeHit, widenSlop } from "../core/viewport/screenSlop.js";
4
- import { DERIVED_PAINTER } from "./nodeShapes/derivedPainter.js";
3
+ import { derivedPartsOf } from "../core/scene/derivedParts.js";
4
+ import { DERIVED_PAINTER, partsCover } from "./nodeShapes/derivedPainter.js";
5
+ import { inkCovers, trimmedOutline } from "./nodeShapes/pickOutline.js";
5
6
  import { IMAGE_PAINTER } from "./nodeShapes/imagePainter.js";
6
7
  import { MEASURE_PAINTER } from "./nodeShapes/measurePainter.js";
7
8
  import { derivesPath } from "./nodeShapes/memo.js";
@@ -10,8 +11,9 @@ import { paintsPose } from "./nodeShapes/poseBox.js";
10
11
  import { RECT_FALLBACK_PAINTER } from "./nodeShapes/rectFallbackPainter.js";
11
12
  import { SHAPE_PAINTER } from "./nodeShapes/shapePainter.js";
12
13
  import { TEXT_PAINTER } from "./nodeShapes/textPainter.js";
13
- import { pathContainsPoint, pathThroughPoseFrame, strokeHitTest } from "@weasel-js/geom";
14
+ import { pathThroughPoseFrame, poseFrameOf } from "@weasel-js/geom";
14
15
  import { glyphGeneration } from "@weasel-js/font";
16
+ import { strokeTrimKey } from "@weasel-js/paint";
15
17
 
16
18
  // src/canvas/NodeShape.ts
17
19
  /**
@@ -66,6 +68,7 @@ var PAINTER_SLOT_DERIVED = "shape:painter:derived";
66
68
  var PAINTER_SLOT_DERIVED_NONRECT = "shape:painter:derived:nonrect";
67
69
  var SILHOUETTE_SLOT = "shape:silhouette";
68
70
  var TEXT_SILHOUETTE_SLOT = "shape:silhouette:text";
71
+ var TRIM_PICK_SLOT = "shape:trim-pick";
69
72
  /** The painter that will render `node` — first match in priority order — or undefined if
70
73
  * none accepts it. */
71
74
  function findNodeShape(node) {
@@ -87,7 +90,7 @@ function matchNodeShape(node) {
87
90
  * the renderer draws. */
88
91
  function findShapeSilhouette(node, pose, ctx) {
89
92
  const build = () => {
90
- const sil = findNodeShape(node)?.silhouette?.(node, pose, ctx) ?? null;
93
+ const sil = localSilhouette(node, pose, ctx);
91
94
  return sil && pathThroughPoseFrame(sil, pose);
92
95
  };
93
96
  if (ctx?.derivedPath != null) return build();
@@ -105,6 +108,10 @@ function findShapeSilhouette(node, pose, ctx) {
105
108
  }
106
109
  return nodeMemo(node, SILHOUETTE_SLOT, pose, build);
107
110
  }
111
+ /** The silhouette `node`'s painter returns, in the pose's local frame: what its stroke is drawn along. */
112
+ function localSilhouette(node, pose, ctx) {
113
+ return findNodeShape(node)?.silhouette?.(node, pose, ctx) ?? null;
114
+ }
108
115
  setDepOutlineReader((dep) => findShapeSilhouette(dep.node, dep.pose, { derivedPath: dep.path }));
109
116
  /** Find the painter for `node` and ask how it inks its silhouette. Returns
110
117
  * the painter's declared {@link NodeInk}, or `null` when no painter matches
@@ -133,16 +140,22 @@ function shapeCoversPoint(node, pose, x, y, opts = {}) {
133
140
  if (node.kind === "group") return true;
134
141
  const sil = findShapeSilhouette(node, pose, { derivedPath: opts.derivedPath });
135
142
  if (sil === null) return true;
143
+ const parts = derivedPartsOf(opts.derivedPath);
144
+ if (parts !== void 0) return partsCover(node, parts, pose, x, y, opts);
136
145
  const ink = findShapeInk(node, pose, {
137
146
  scale: opts.scale,
138
147
  inherited: opts.inherited
139
148
  }) ?? DEFAULT_INK;
140
- const inside = pathContainsPoint(sil, x, y);
141
- if (ink.filled && inside) return true;
142
- const slop = widenSlop(opts.tolerance, (inside ? ink.insetPx : ink.outsetPx) ?? 0, opts.scale);
143
- const { extra, opts: hit } = slopForStrokeHit(slop);
144
- const reach = (inside ? ink.inset : ink.outset) + extra;
145
- return (reach > 0 || hasSlop(slop)) && strokeHitTest(sil, x, y, reach, hit);
149
+ const trimmed = ink.trim === void 0 ? void 0 : trimmedOutlineOf(node, pose, sil, ink.trim, opts.derivedPath);
150
+ return inkCovers(sil, ink, trimmed, x, y, opts);
151
+ }
152
+ /** {@link trimmedOutline} for `node`'s silhouette `sil`, memoized as the silhouette is. */
153
+ function trimmedOutlineOf(node, pose, sil, trim, derivedPath) {
154
+ const build = () => {
155
+ const local = poseFrameOf(pose) === null ? sil : localSilhouette(node, pose, { derivedPath }) ?? sil;
156
+ return trimmedOutline(local, sil, pose, trim);
157
+ };
158
+ return derivedPath != null ? build() : nodeMemo(node, TRIM_PICK_SLOT, pose, build, strokeTrimKey(trim));
146
159
  }
147
160
  /** The current painters in evaluation order — `'high'` tier first, then `'normal'`. */
148
161
  function getNodeShapes() {
@@ -1 +1 @@
1
- {"version":3,"file":"NodeShape.js","names":[],"sources":["../../src/canvas/NodeShape.ts"],"sourcesContent":["/**\n * NodeShape — the **shape trait's** registry: the per-kind `paint` + `silhouette` used by\n * `defaultDrawOne`, clipping, non-rect hit-testing, and lasso/area-select. Teaching the kit\n * about a new kind of shape goes through this registry rather than by overriding `drawOne`.\n *\n * The first entry whose `matches` predicate returns true paints the node: `'high'` entries\n * first, then the built-ins, then `'normal'` ones, in registration order within a tier. Every\n * built-in but `kit:derived` additionally requires a rect-shaped pose — a scene whose poses are\n * some other shape matches none of them, and paints through a consumer painter or not at all.\n */\nimport { glyphGeneration } from '@weasel-js/font';\nimport {\n pathContainsPoint,\n strokeHitTest,\n type Path\n} from '@weasel-js/geom';\nimport { pathThroughPoseFrame } from '@weasel-js/geom';\nimport { bumpNodeMemoGeneration, nodeMemo } from 'core/scene/nodeMemo';\nimport { setDepOutlineReader } from 'core/scene/booleanOfChildren';\nimport type { Node } from 'core/scene/types';\nimport type { Bounds } from 'core/viewport/fitViewToBounds';\nimport { hasSlop, slopForStrokeHit, widenSlop, type PickSlop } from 'core/viewport/screenSlop';\nimport { DERIVED_PAINTER } from './nodeShapes/derivedPainter';\nimport { IMAGE_PAINTER } from './nodeShapes/imagePainter';\nimport { MEASURE_PAINTER } from './nodeShapes/measurePainter';\nimport { derivesPath } from './nodeShapes/memo';\nimport { PATH_PAINTER } from './nodeShapes/pathPainter';\nimport { paintsPose } from './nodeShapes/poseBox';\nimport { RECT_FALLBACK_PAINTER } from './nodeShapes/rectFallbackPainter';\nimport { SHAPE_PAINTER } from './nodeShapes/shapePainter';\nimport { TEXT_PAINTER } from './nodeShapes/textPainter';\nimport type { NodeShapeEntry, NodeInk, NodeSilhouetteCtx, NodeInkCtx } from './nodeShapeTypes';\nexport type { NodePaintCtx, SceneViewDrawOne, NodeShapeEntry, NodeInk, NodeSilhouetteCtx, NodeInkCtx } from './nodeShapeTypes';\nexport type { InheritedPaint } from 'core/scene/inheritedPaint';\n\n/** What a painter that declares no `ink` is assumed to do. */\nexport const DEFAULT_INK: Readonly<NodeInk> = Object.freeze({ filled: true, outset: 0, inset: 0 });\n\n/** Options for `registerNodeShape`. */\nexport interface RegisterNodeShapeOptions {\n /** `'high'` puts the painter ahead of all normally-registered ones (so\n * it can win over a kit built-in). `'normal'` appends at the end. */\n priority?: 'high' | 'normal';\n}\n\nconst PAINTERS: { high: NodeShapeEntry[]; normal: NodeShapeEntry[] } = {\n high: [],\n normal: [],\n};\n\n// The built-ins go in on first use, not at module load: a bundler may drop an\n// import kept only for its side effect, and the registry would come up empty.\nlet builtInsRegistered = false;\nfunction ensureBuiltIns(): void {\n if (builtInsRegistered) return;\n builtInsRegistered = true;\n registerBuiltInShapePainters();\n}\n\n/** Register a shape painter. Returns a disposer that removes it. */\nexport function registerNodeShape<TData, TPose>(\n painter: NodeShapeEntry<TData, TPose>,\n opts: RegisterNodeShapeOptions = {},\n): () => void {\n ensureBuiltIns();\n const list = opts.priority === 'high' ? PAINTERS.high : PAINTERS.normal;\n list.push(painter as NodeShapeEntry);\n // The painter set is exactly the ambient state `findNodeShape` reads, and a\n // memo keyed on the node alone cannot see it change. See `nodeMemo`.\n bumpNodeMemoGeneration();\n return () => {\n const i = list.indexOf(painter as NodeShapeEntry);\n if (i >= 0) list.splice(i, 1);\n bumpNodeMemoGeneration();\n };\n}\n\n/**\n * Slot names for what picking asks per node per pointer move: which painter matches, and that\n * painter's silhouette path. Both go through {@link nodeMemo}, so they invalidate on `data`\n * (and, for the silhouette, `pose`) reference changes and on a painter registration.\n *\n * Separate slots rather than one entry: they mostly invalidate together, but not always, and a\n * shared entry would force the stricter key on both.\n */\nconst PAINTER_SLOT = 'shape:painter';\nconst PAINTER_SLOT_NONRECT = 'shape:painter:nonrect';\nconst PAINTER_SLOT_DERIVED = 'shape:painter:derived';\nconst PAINTER_SLOT_DERIVED_NONRECT = 'shape:painter:derived:nonrect';\nconst SILHOUETTE_SLOT = 'shape:silhouette';\nconst TEXT_SILHOUETTE_SLOT = 'shape:silhouette:text';\n\n/** The painter that will render `node` — first match in priority order — or undefined if\n * none accepts it. */\nexport function findNodeShape<TData, TPose>(\n node: Node<TData, string, TPose>,\n): NodeShapeEntry<TData, TPose> | undefined {\n // Built-in painters also gate on pose shape and on whether the node derives\n // its path, neither of which shows in `data`, so the key carries both.\n const rect = paintsPose(node.pose);\n const slot = derivesPath(node)\n ? (rect ? PAINTER_SLOT_DERIVED : PAINTER_SLOT_DERIVED_NONRECT)\n : (rect ? PAINTER_SLOT : PAINTER_SLOT_NONRECT);\n return nodeMemo(node as { data?: unknown }, slot, undefined, () =>\n matchNodeShape(node),\n );\n}\n\nfunction matchNodeShape<TData, TPose>(\n node: Node<TData, string, TPose>,\n): NodeShapeEntry<TData, TPose> | undefined {\n ensureBuiltIns();\n for (const p of PAINTERS.high) {\n if (p.matches(node as Node<unknown, string, unknown>)) {\n return p as NodeShapeEntry<TData, TPose>;\n }\n }\n for (const p of PAINTERS.normal) {\n if (p.matches(node as Node<unknown, string, unknown>)) {\n return p as NodeShapeEntry<TData, TPose>;\n }\n }\n return undefined;\n}\n\n/** The silhouette path of `node`, in **world** coords, or null if no painter matches or the\n * matching painter has none.\n *\n * Painters return their silhouette in the pose's local (unrotated) frame;\n * this bakes the pose's frame (`poseFrameOf`: its rotation and `transform`)\n * on top, so clips/area-select of a turned or skewed node use the boundary\n * the renderer draws. */\nexport function findShapeSilhouette<TData, TPose>(\n node: Node<TData, string, TPose>,\n pose: TPose,\n ctx?: NodeSilhouetteCtx,\n): Path | null {\n const build = (): Path | null => {\n const sil = findNodeShape(node)?.silhouette?.(node, pose, ctx) ?? null;\n return sil && pathThroughPoseFrame(sil, pose);\n };\n // `SILHOUETTE_SLOT`'s key is (node, pose, data) and cannot see\n // `ctx.derivedPath` — the same reason `kit:derived` skips `PAINT_SLOT`.\n // Memoizing here would serve one caller's silhouette to a caller that\n // passed a different path. Only an actual path skips it: a scene-backed\n // source answers `null` for every ordinary node, and treating that as a\n // reason to bypass would retire the memo for the whole scene.\n if (ctx?.derivedPath != null) return build();\n // Line boxes follow font metrics, and a face landing changes those with no\n // change to the node, so the text entry is checked against the glyph epoch.\n if (findNodeShape(node) === (TEXT_PAINTER as NodeShapeEntry<unknown, unknown>)) {\n const epoch = glyphGeneration();\n const held = nodeMemo(node as { data?: unknown }, TEXT_SILHOUETTE_SLOT, pose,\n (): { epoch: number; path: Path | null } => ({ epoch, path: build() }));\n if (held.epoch !== epoch) {\n held.epoch = epoch;\n held.path = build();\n }\n return held.path;\n }\n return nodeMemo(node as { data?: unknown }, SILHOUETTE_SLOT, pose, build);\n}\n\n// A boolean group's operands are its children's outlines.\nsetDepOutlineReader((dep) => findShapeSilhouette(dep.node, dep.pose, { derivedPath: dep.path }));\n\n/** Find the painter for `node` and ask how it inks its silhouette. Returns\n * the painter's declared {@link NodeInk}, or `null` when no painter matches\n * or it declares none — callers substitute {@link DEFAULT_INK}. */\nexport function findShapeInk<TData, TPose>(\n node: Node<TData, string, TPose>,\n pose: TPose,\n ctx?: NodeInkCtx,\n): NodeInk | null {\n return findNodeShape(node)?.ink?.(node, pose, ctx) ?? null;\n}\n\n/** Find the painter for `node` and ask for the box its paint fits in. `null`\n * when no painter matches, or the one that does cannot say — see\n * {@link NodeShapeEntry.bounds}. */\nexport function findShapeBounds<TData, TPose>(\n node: Node<TData, string, TPose>,\n pose: TPose,\n ctx?: NodeInkCtx,\n): Bounds | null {\n return findNodeShape(node)?.bounds?.(node, pose, ctx) ?? null;\n}\n\n/** Options for {@link shapeCoversPoint}. */\nexport interface ShapeCoversPointOptions {\n /** Extra grab distance around the outline: **world** units as a number,\n * or screen pixels under a view scale (`{ px, scale }`), which is measured\n * on screen and so stays exact under non-uniform zoom. Defaults to `0`,\n * where a hairline is too thin a target to hit. */\n tolerance?: PickSlop;\n /** View scale, passed to the painter's `ink`, and what a screen reach is\n * read at under a world `tolerance`. Defaults to 1. */\n scale?: number;\n /** The node's derived path — see {@link NodeSilhouetteCtx.derivedPath}.\n * Without it a derived node reports no silhouette and this answers `true`\n * everywhere, which degrades picking to the caller's AABB: for an edge,\n * a zero-sized pose. */\n derivedPath?: Path | null;\n /** See {@link NodeInkCtx.inherited}. Without it an `'inherit'` field is\n * read as none. */\n inherited?: NodeInkCtx['inherited'];\n}\n\n/**\n * Does the shape `node` actually paints cover the world point? Asks the painter's silhouette,\n * the same boundary used for clipping and area-select, rather than the pose rect.\n *\n * The ink counts too: a shape whose interior isn't filled is grabbable along its outline and\n * not through its empty middle. The outline's grab width is the stroke's half-width plus\n * `tolerance`.\n *\n * A painter with no `silhouette`, or one that returns `null` for this node, reports `true` —\n * \"no opinion\" — so callers should keep their own AABB test as a pre-filter.\n */\nexport function shapeCoversPoint<TData, TPose>(\n node: Node<TData, string, TPose>,\n pose: TPose,\n x: number,\n y: number,\n opts: ShapeCoversPointOptions = {},\n): boolean {\n // A group paints nothing, so its interior is its box, which the caller's pre-filter tested.\n if (node.kind === 'group') return true;\n const sil = findShapeSilhouette(node, pose, { derivedPath: opts.derivedPath });\n if (sil === null) return true;\n const ink = findShapeInk(node, pose, { scale: opts.scale, inherited: opts.inherited }) ?? DEFAULT_INK;\n const inside = pathContainsPoint(sil, x, y);\n if (ink.filled && inside) return true;\n // The stroke reaches a different distance on each side, so which side the point is on\n // picks the reach. `tolerance` also makes a filled shape grabbable a few pixels outside\n // its edge.\n const slop = widenSlop(opts.tolerance, (inside ? ink.insetPx : ink.outsetPx) ?? 0, opts.scale);\n const { extra, opts: hit } = slopForStrokeHit(slop);\n const reach = (inside ? ink.inset : ink.outset) + extra;\n return (reach > 0 || hasSlop(slop)) && strokeHitTest(sil, x, y, reach, hit);\n}\n\n/** The current painters in evaluation order — `'high'` tier first, then `'normal'`. */\nexport function getNodeShapes(): readonly NodeShapeEntry[] {\n ensureBuiltIns();\n return [...PAINTERS.high, ...PAINTERS.normal];\n}\n\n/** Test-only: clear the registry and re-register the built-ins. */\nexport function _resetShapePaintersForTests(): void {\n PAINTERS.high.length = 0;\n PAINTERS.normal.length = 0;\n builtInsRegistered = false;\n ensureBuiltIns();\n // `registerNodeShape` already bumps, but the clearing above happens without one.\n bumpNodeMemoGeneration();\n}\n\nfunction registerBuiltInShapePainters(): void {\n registerNodeShape(TEXT_PAINTER);\n registerNodeShape(MEASURE_PAINTER);\n // Ahead of path/shape/image: a derived node whose `data` also carries a\n // `path`, `shape` or `image` means the derived path, and those three match\n // on the presence of the field alone.\n registerNodeShape(DERIVED_PAINTER);\n registerNodeShape(PATH_PAINTER);\n registerNodeShape(SHAPE_PAINTER);\n registerNodeShape(IMAGE_PAINTER);\n registerNodeShape(RECT_FALLBACK_PAINTER);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"NodeShape.js","names":[],"sources":["../../src/canvas/NodeShape.ts"],"sourcesContent":["/**\n * NodeShape — the **shape trait's** registry: the per-kind `paint` + `silhouette` used by\n * `defaultDrawOne`, clipping, non-rect hit-testing, and lasso/area-select. Teaching the kit\n * about a new kind of shape goes through this registry rather than by overriding `drawOne`.\n *\n * The first entry whose `matches` predicate returns true paints the node: `'high'` entries\n * first, then the built-ins, then `'normal'` ones, in registration order within a tier. Every\n * built-in but `kit:derived` additionally requires a rect-shaped pose — a scene whose poses are\n * some other shape matches none of them, and paints through a consumer painter or not at all.\n */\nimport { glyphGeneration } from '@weasel-js/font';\nimport { pathThroughPoseFrame, poseFrameOf, type Path } from '@weasel-js/geom';\nimport { strokeTrimKey, type StrokeTrim } from '@weasel-js/paint';\nimport { bumpNodeMemoGeneration, nodeMemo } from 'core/scene/nodeMemo';\nimport { setDepOutlineReader } from 'core/scene/booleanOfChildren';\nimport type { Node } from 'core/scene/types';\nimport type { Bounds } from 'core/viewport/fitViewToBounds';\nimport type { PickSlop } from 'core/viewport/screenSlop';\nimport { derivedPartsOf } from 'core/scene/derivedParts';\nimport { DERIVED_PAINTER, partsCover } from './nodeShapes/derivedPainter';\nimport { inkCovers, trimmedOutline, type TrimmedOutline } from './nodeShapes/pickOutline';\nimport { IMAGE_PAINTER } from './nodeShapes/imagePainter';\nimport { MEASURE_PAINTER } from './nodeShapes/measurePainter';\nimport { derivesPath } from './nodeShapes/memo';\nimport { PATH_PAINTER } from './nodeShapes/pathPainter';\nimport { paintsPose } from './nodeShapes/poseBox';\nimport { RECT_FALLBACK_PAINTER } from './nodeShapes/rectFallbackPainter';\nimport { SHAPE_PAINTER } from './nodeShapes/shapePainter';\nimport { TEXT_PAINTER } from './nodeShapes/textPainter';\nimport type { NodeShapeEntry, NodeInk, NodeSilhouetteCtx, NodeInkCtx } from './nodeShapeTypes';\nexport type { NodePaintCtx, SceneViewDrawOne, NodeShapeEntry, NodeInk, NodeSilhouetteCtx, NodeInkCtx } from './nodeShapeTypes';\nexport type { InheritedPaint } from 'core/scene/inheritedPaint';\n\n/** What a painter that declares no `ink` is assumed to do. */\nexport const DEFAULT_INK: Readonly<NodeInk> = Object.freeze({ filled: true, outset: 0, inset: 0 });\n\n/** Options for `registerNodeShape`. */\nexport interface RegisterNodeShapeOptions {\n /** `'high'` puts the painter ahead of all normally-registered ones (so\n * it can win over a kit built-in). `'normal'` appends at the end. */\n priority?: 'high' | 'normal';\n}\n\nconst PAINTERS: { high: NodeShapeEntry[]; normal: NodeShapeEntry[] } = {\n high: [],\n normal: [],\n};\n\n// The built-ins go in on first use, not at module load: a bundler may drop an\n// import kept only for its side effect, and the registry would come up empty.\nlet builtInsRegistered = false;\nfunction ensureBuiltIns(): void {\n if (builtInsRegistered) return;\n builtInsRegistered = true;\n registerBuiltInShapePainters();\n}\n\n/** Register a shape painter. Returns a disposer that removes it. */\nexport function registerNodeShape<TData, TPose>(\n painter: NodeShapeEntry<TData, TPose>,\n opts: RegisterNodeShapeOptions = {},\n): () => void {\n ensureBuiltIns();\n const list = opts.priority === 'high' ? PAINTERS.high : PAINTERS.normal;\n list.push(painter as NodeShapeEntry);\n // The painter set is exactly the ambient state `findNodeShape` reads, and a\n // memo keyed on the node alone cannot see it change. See `nodeMemo`.\n bumpNodeMemoGeneration();\n return () => {\n const i = list.indexOf(painter as NodeShapeEntry);\n if (i >= 0) list.splice(i, 1);\n bumpNodeMemoGeneration();\n };\n}\n\n/**\n * Slot names for what picking asks per node per pointer move: which painter matches, and that\n * painter's silhouette path. Both go through {@link nodeMemo}, so they invalidate on `data`\n * (and, for the silhouette, `pose`) reference changes and on a painter registration.\n *\n * Separate slots rather than one entry: they mostly invalidate together, but not always, and a\n * shared entry would force the stricter key on both.\n */\nconst PAINTER_SLOT = 'shape:painter';\nconst PAINTER_SLOT_NONRECT = 'shape:painter:nonrect';\nconst PAINTER_SLOT_DERIVED = 'shape:painter:derived';\nconst PAINTER_SLOT_DERIVED_NONRECT = 'shape:painter:derived:nonrect';\nconst SILHOUETTE_SLOT = 'shape:silhouette';\nconst TEXT_SILHOUETTE_SLOT = 'shape:silhouette:text';\nconst TRIM_PICK_SLOT = 'shape:trim-pick';\n\n/** The painter that will render `node` — first match in priority order — or undefined if\n * none accepts it. */\nexport function findNodeShape<TData, TPose>(\n node: Node<TData, string, TPose>,\n): NodeShapeEntry<TData, TPose> | undefined {\n // Built-in painters also gate on pose shape and on whether the node derives\n // its path, neither of which shows in `data`, so the key carries both.\n const rect = paintsPose(node.pose);\n const slot = derivesPath(node)\n ? (rect ? PAINTER_SLOT_DERIVED : PAINTER_SLOT_DERIVED_NONRECT)\n : (rect ? PAINTER_SLOT : PAINTER_SLOT_NONRECT);\n return nodeMemo(node as { data?: unknown }, slot, undefined, () =>\n matchNodeShape(node),\n );\n}\n\nfunction matchNodeShape<TData, TPose>(\n node: Node<TData, string, TPose>,\n): NodeShapeEntry<TData, TPose> | undefined {\n ensureBuiltIns();\n for (const p of PAINTERS.high) {\n if (p.matches(node as Node<unknown, string, unknown>)) {\n return p as NodeShapeEntry<TData, TPose>;\n }\n }\n for (const p of PAINTERS.normal) {\n if (p.matches(node as Node<unknown, string, unknown>)) {\n return p as NodeShapeEntry<TData, TPose>;\n }\n }\n return undefined;\n}\n\n/** The silhouette path of `node`, in **world** coords, or null if no painter matches or the\n * matching painter has none.\n *\n * Painters return their silhouette in the pose's local (unrotated) frame;\n * this bakes the pose's frame (`poseFrameOf`: its rotation and `transform`)\n * on top, so clips/area-select of a turned or skewed node use the boundary\n * the renderer draws. */\nexport function findShapeSilhouette<TData, TPose>(\n node: Node<TData, string, TPose>,\n pose: TPose,\n ctx?: NodeSilhouetteCtx,\n): Path | null {\n const build = (): Path | null => {\n const sil = localSilhouette(node, pose, ctx);\n return sil && pathThroughPoseFrame(sil, pose);\n };\n // `SILHOUETTE_SLOT`'s key is (node, pose, data) and cannot see\n // `ctx.derivedPath` — the same reason `kit:derived` skips `PAINT_SLOT`.\n // Memoizing here would serve one caller's silhouette to a caller that\n // passed a different path. Only an actual path skips it: a scene-backed\n // source answers `null` for every ordinary node, and treating that as a\n // reason to bypass would retire the memo for the whole scene.\n if (ctx?.derivedPath != null) return build();\n // Line boxes follow font metrics, and a face landing changes those with no\n // change to the node, so the text entry is checked against the glyph epoch.\n if (findNodeShape(node) === (TEXT_PAINTER as NodeShapeEntry<unknown, unknown>)) {\n const epoch = glyphGeneration();\n const held = nodeMemo(node as { data?: unknown }, TEXT_SILHOUETTE_SLOT, pose,\n (): { epoch: number; path: Path | null } => ({ epoch, path: build() }));\n if (held.epoch !== epoch) {\n held.epoch = epoch;\n held.path = build();\n }\n return held.path;\n }\n return nodeMemo(node as { data?: unknown }, SILHOUETTE_SLOT, pose, build);\n}\n\n/** The silhouette `node`'s painter returns, in the pose's local frame: what its stroke is drawn along. */\nfunction localSilhouette<TData, TPose>(node: Node<TData, string, TPose>, pose: TPose, ctx?: NodeSilhouetteCtx): Path | null {\n return findNodeShape(node)?.silhouette?.(node, pose, ctx) ?? null;\n}\n\n// A boolean group's operands are its children's outlines.\nsetDepOutlineReader((dep) => findShapeSilhouette(dep.node, dep.pose, { derivedPath: dep.path }));\n\n/** Find the painter for `node` and ask how it inks its silhouette. Returns\n * the painter's declared {@link NodeInk}, or `null` when no painter matches\n * or it declares none — callers substitute {@link DEFAULT_INK}. */\nexport function findShapeInk<TData, TPose>(\n node: Node<TData, string, TPose>,\n pose: TPose,\n ctx?: NodeInkCtx,\n): NodeInk | null {\n return findNodeShape(node)?.ink?.(node, pose, ctx) ?? null;\n}\n\n/** Find the painter for `node` and ask for the box its paint fits in. `null`\n * when no painter matches, or the one that does cannot say — see\n * {@link NodeShapeEntry.bounds}. */\nexport function findShapeBounds<TData, TPose>(\n node: Node<TData, string, TPose>,\n pose: TPose,\n ctx?: NodeInkCtx,\n): Bounds | null {\n return findNodeShape(node)?.bounds?.(node, pose, ctx) ?? null;\n}\n\n/** Options for {@link shapeCoversPoint}. */\nexport interface ShapeCoversPointOptions {\n /** Extra grab distance around the outline: **world** units as a number,\n * or screen pixels under a view scale (`{ px, scale }`), which is measured\n * on screen and so stays exact under non-uniform zoom. Defaults to `0`,\n * where a hairline is too thin a target to hit. */\n tolerance?: PickSlop;\n /** View scale, passed to the painter's `ink`, and what a screen reach is\n * read at under a world `tolerance`. Defaults to 1. */\n scale?: number;\n /** The node's derived path — see {@link NodeSilhouetteCtx.derivedPath}.\n * Without it a derived node reports no silhouette and this answers `true`\n * everywhere, which degrades picking to the caller's AABB: for an edge,\n * a zero-sized pose. */\n derivedPath?: Path | null;\n /** See {@link NodeInkCtx.inherited}. Without it an `'inherit'` field is\n * read as none. */\n inherited?: NodeInkCtx['inherited'];\n}\n\n/**\n * Does the shape `node` actually paints cover the world point? Asks the painter's silhouette,\n * the same boundary used for clipping and area-select, rather than the pose rect.\n *\n * The ink counts too: a shape whose interior isn't filled is grabbable along its outline and\n * not through its empty middle. The outline's grab width is the stroke's half-width plus\n * `tolerance`.\n *\n * A painter with no `silhouette`, or one that returns `null` for this node, reports `true` —\n * \"no opinion\" — so callers should keep their own AABB test as a pre-filter.\n */\nexport function shapeCoversPoint<TData, TPose>(\n node: Node<TData, string, TPose>,\n pose: TPose,\n x: number,\n y: number,\n opts: ShapeCoversPointOptions = {},\n): boolean {\n // A group paints nothing, so its interior is its box, which the caller's pre-filter tested.\n if (node.kind === 'group') return true;\n const sil = findShapeSilhouette(node, pose, { derivedPath: opts.derivedPath });\n if (sil === null) return true;\n const parts = derivedPartsOf(opts.derivedPath);\n if (parts !== undefined) return partsCover(node, parts, pose, x, y, opts);\n const ink = findShapeInk(node, pose, { scale: opts.scale, inherited: opts.inherited }) ?? DEFAULT_INK;\n const trimmed = ink.trim === undefined ? undefined : trimmedOutlineOf(node, pose, sil, ink.trim, opts.derivedPath);\n return inkCovers(sil, ink, trimmed, x, y, opts);\n}\n\n/** {@link trimmedOutline} for `node`'s silhouette `sil`, memoized as the silhouette is. */\nfunction trimmedOutlineOf<TData, TPose>(\n node: Node<TData, string, TPose>, pose: TPose, sil: Path, trim: StrokeTrim, derivedPath: Path | null | undefined,\n): TrimmedOutline {\n const build = (): TrimmedOutline => {\n const local = poseFrameOf(pose) === null ? sil : localSilhouette(node, pose, { derivedPath }) ?? sil;\n return trimmedOutline(local, sil, pose, trim);\n };\n // The trim may be inherited, rebuilt per query, so its value keys the memo beside the node;\n // see `findShapeSilhouette` for the derived path.\n return derivedPath != null ? build() : nodeMemo(node as { data?: unknown }, TRIM_PICK_SLOT, pose, build, strokeTrimKey(trim));\n}\n\n/** The current painters in evaluation order — `'high'` tier first, then `'normal'`. */\nexport function getNodeShapes(): readonly NodeShapeEntry[] {\n ensureBuiltIns();\n return [...PAINTERS.high, ...PAINTERS.normal];\n}\n\n/** Test-only: clear the registry and re-register the built-ins. */\nexport function _resetShapePaintersForTests(): void {\n PAINTERS.high.length = 0;\n PAINTERS.normal.length = 0;\n builtInsRegistered = false;\n ensureBuiltIns();\n // `registerNodeShape` already bumps, but the clearing above happens without one.\n bumpNodeMemoGeneration();\n}\n\nfunction registerBuiltInShapePainters(): void {\n registerNodeShape(TEXT_PAINTER);\n registerNodeShape(MEASURE_PAINTER);\n // Ahead of path/shape/image: a derived node whose `data` also carries a\n // `path`, `shape` or `image` means the derived path, and those three match\n // on the presence of the field alone.\n registerNodeShape(DERIVED_PAINTER);\n registerNodeShape(PATH_PAINTER);\n registerNodeShape(SHAPE_PAINTER);\n registerNodeShape(IMAGE_PAINTER);\n registerNodeShape(RECT_FALLBACK_PAINTER);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCA,IAAa,cAAiC,OAAO,OAAO;CAAE,QAAQ;CAAM,QAAQ;CAAG,OAAO;AAAE,CAAC;AASjG,IAAM,WAAiE;CACrE,MAAM,CAAC;CACP,QAAQ,CAAC;AACX;AAIA,IAAI,qBAAqB;AACzB,SAAS,iBAAuB;CAC9B,IAAI,oBAAoB;CACxB,qBAAqB;CACrB,6BAA6B;AAC/B;;AAGA,SAAgB,kBACd,SACA,OAAiC,CAAC,GACtB;CACZ,eAAe;CACf,MAAM,OAAO,KAAK,aAAa,SAAS,SAAS,OAAO,SAAS;CACjE,KAAK,KAAK,OAAyB;CAGnC,uBAAuB;CACvB,aAAa;EACX,MAAM,IAAI,KAAK,QAAQ,OAAyB;EAChD,IAAI,KAAK,GAAG,KAAK,OAAO,GAAG,CAAC;EAC5B,uBAAuB;CACzB;AACF;;;;;;;;;AAUA,IAAM,eAAe;AACrB,IAAM,uBAAuB;AAC7B,IAAM,uBAAuB;AAC7B,IAAM,+BAA+B;AACrC,IAAM,kBAAkB;AACxB,IAAM,uBAAuB;AAC7B,IAAM,iBAAiB;;;AAIvB,SAAgB,cACd,MAC0C;CAG1C,MAAM,OAAO,WAAW,KAAK,IAAI;CACjC,MAAM,OAAO,YAAY,IAAI,IACxB,OAAO,uBAAuB,+BAC9B,OAAO,eAAe;CAC3B,OAAO,SAAS,MAA4B,MAAM,cAChD,eAAe,IAAI,CACrB;AACF;AAEA,SAAS,eACP,MAC0C;CAC1C,eAAe;CACf,KAAK,MAAM,KAAK,SAAS,MACvB,IAAI,EAAE,QAAQ,IAAsC,GAClD,OAAO;CAGX,KAAK,MAAM,KAAK,SAAS,QACvB,IAAI,EAAE,QAAQ,IAAsC,GAClD,OAAO;AAIb;;;;;;;;AASA,SAAgB,oBACd,MACA,MACA,KACa;CACb,MAAM,cAA2B;EAC/B,MAAM,MAAM,gBAAgB,MAAM,MAAM,GAAG;EAC3C,OAAO,OAAO,qBAAqB,KAAK,IAAI;CAC9C;CAOA,IAAI,KAAK,eAAe,MAAM,OAAO,MAAM;CAG3C,IAAI,cAAc,IAAI,MAAO,cAAmD;EAC9E,MAAM,QAAQ,gBAAgB;EAC9B,MAAM,OAAO,SAAS,MAA4B,sBAAsB,aACzB;GAAE;GAAO,MAAM,MAAM;EAAE,EAAE;EACxE,IAAI,KAAK,UAAU,OAAO;GACxB,KAAK,QAAQ;GACb,KAAK,OAAO,MAAM;EACpB;EACA,OAAO,KAAK;CACd;CACA,OAAO,SAAS,MAA4B,iBAAiB,MAAM,KAAK;AAC1E;;AAGA,SAAS,gBAA8B,MAAkC,MAAa,KAAsC;CAC1H,OAAO,cAAc,IAAI,CAAC,EAAE,aAAa,MAAM,MAAM,GAAG,KAAK;AAC/D;AAGA,qBAAqB,QAAQ,oBAAoB,IAAI,MAAM,IAAI,MAAM,EAAE,aAAa,IAAI,KAAK,CAAC,CAAC;;;;AAK/F,SAAgB,aACd,MACA,MACA,KACgB;CAChB,OAAO,cAAc,IAAI,CAAC,EAAE,MAAM,MAAM,MAAM,GAAG,KAAK;AACxD;;;;AAKA,SAAgB,gBACd,MACA,MACA,KACe;CACf,OAAO,cAAc,IAAI,CAAC,EAAE,SAAS,MAAM,MAAM,GAAG,KAAK;AAC3D;;;;;;;;;;;;AAiCA,SAAgB,iBACd,MACA,MACA,GACA,GACA,OAAgC,CAAC,GACxB;CAET,IAAI,KAAK,SAAS,SAAS,OAAO;CAClC,MAAM,MAAM,oBAAoB,MAAM,MAAM,EAAE,aAAa,KAAK,YAAY,CAAC;CAC7E,IAAI,QAAQ,MAAM,OAAO;CACzB,MAAM,QAAQ,eAAe,KAAK,WAAW;CAC7C,IAAI,UAAU,QAAW,OAAO,WAAW,MAAM,OAAO,MAAM,GAAG,GAAG,IAAI;CACxE,MAAM,MAAM,aAAa,MAAM,MAAM;EAAE,OAAO,KAAK;EAAO,WAAW,KAAK;CAAU,CAAC,KAAK;CAC1F,MAAM,UAAU,IAAI,SAAS,SAAY,SAAY,iBAAiB,MAAM,MAAM,KAAK,IAAI,MAAM,KAAK,WAAW;CACjH,OAAO,UAAU,KAAK,KAAK,SAAS,GAAG,GAAG,IAAI;AAChD;;AAGA,SAAS,iBACP,MAAkC,MAAa,KAAW,MAAkB,aAC5D;CAChB,MAAM,cAA8B;EAClC,MAAM,QAAQ,YAAY,IAAI,MAAM,OAAO,MAAM,gBAAgB,MAAM,MAAM,EAAE,YAAY,CAAC,KAAK;EACjG,OAAO,eAAe,OAAO,KAAK,MAAM,IAAI;CAC9C;CAGA,OAAO,eAAe,OAAO,MAAM,IAAI,SAAS,MAA4B,gBAAgB,MAAM,OAAO,cAAc,IAAI,CAAC;AAC9H;;AAGA,SAAgB,gBAA2C;CACzD,eAAe;CACf,OAAO,CAAC,GAAG,SAAS,MAAM,GAAG,SAAS,MAAM;AAC9C;AAYA,SAAS,+BAAqC;CAC5C,kBAAkB,YAAY;CAC9B,kBAAkB,eAAe;CAIjC,kBAAkB,eAAe;CACjC,kBAAkB,YAAY;CAC9B,kBAAkB,aAAa;CAC/B,kBAAkB,aAAa;CAC/B,kBAAkB,qBAAqB;AACzC"}
@@ -1 +1 @@
1
- {"version":3,"file":"props.js","names":[],"sources":["../../../src/canvas/SceneCanvas/props.ts"],"sourcesContent":["import type { ModeRegistry } from '@weasel-js/modes';\nimport type { PathEditHost, PathEditOpening } from './pathEditHost';\nimport type { SceneCanvasGeometry } from './geometryProps';\nimport type { SceneCanvasHit, SceneCanvasPointerProps } from './pointerProps';\nimport type { Paint } from '@weasel-js/paint';\nimport type { SceneCanvasActionProps } from './actionProps';\nimport { Yoke } from '@weasel-js/routing/react';\nimport type { PasteOmission } from 'core/adapters/types';\nimport type { Op } from 'core/ops/types';\nimport type { Node, Scene, SerializedScene } from 'core/scene/types';\nimport type { SelectionApi, UseSelectionOptions } from 'core/selection/useSelection';\nimport type { ContentHandlerEntry, KitHandlersOption } from 'domains/ingestion';\nimport type { SceneCanvasSnap } from 'domains/snap/types';\nimport type { SvgIngestOptions } from 'interactions/actions/depSchema';\nimport type { GeometryProjection } from 'interactions/actions/geometryProjection';\nimport type { UseMoveOptions } from 'interactions/actions/move/options';\nimport type { PoseDescriptor } from 'interactions/actions/resize/geometry';\nimport type { UseResizeOptions } from 'interactions/actions/resize/options';\nimport type { UseRotateOptions } from 'interactions/actions/rotate/options';\nimport type { ReactNode } from 'react';\nimport type { Animator } from '@weasel-js/animator';\nimport type { PoseTransitionSeat } from '../../animation/poseTransition';\nimport { HANDLE_BASE_PX } from '../../core/device/targets';\nimport type { DeviceProfile } from '../../core/device/types';\nimport type { NodeRoutingEntry } from '../../core/scene/NodeRouting';\nimport type { DebugConfig } from '../../debug/types';\nimport type { LayoutDropTargetMode, ReflowTransition } from '../../layout/types';\nimport type { FlattenTolerance, ShaderProgramHandle } from '@weasel-js/renderer';\nimport type { Tool, ToolsApi } from '../../tools/overlayBinding';\nimport type { SceneSlotConfig } from '../buildSceneLayer';\nimport type { CanvasProps } from '../Canvas';\nimport type { LayersMap } from '../layerSlots';\nimport type { CanvasViewProps } from '../CanvasView';\nimport type { InsertNodeFactory, InsertPaint } from '../deps';\nimport type { SurfaceContribution } from '../surfaceContribution';\nimport type { CameraProps } from '../useCanvasView';\nimport type { FeatureEntry } from './features';\nimport type { BuiltinToolOptions } from './useBuiltinShapeTools';\nimport type { VersionCheck } from './useContributionRoles';\nimport type { SceneCanvasViewport } from './viewportProp';\n\n/** The adapter surface delete / duplicate / group / ungroup need, narrowed from\n * `SceneCanvasAdapter` because `useSceneStandardActions` is non-generic. */\nexport interface BridgeAdapter {\n applyOps(ops: Op[], label?: string): void;\n insertNode(node: { id: string; [k: string]: unknown }): void;\n removeNode(id: string): void;\n setSelection(ids: string[]): void;\n}\n\n/** Default size in CSS pixels for selection corner-handles AND their\n * hit-test radius, at `targetScale = 1`. Deliberately unscaled; kit-internal\n * use sites multiply by `DeviceProfile.targetScale`. */\nexport const DEFAULT_HANDLE_SIZE = HANDLE_BASE_PX;\n\n/** `<SceneCanvas longPress>`. */\nexport interface SceneCanvasLongPress {\n /**\n * Shown while a long-press is held and some binding would fire on it.\n * Default: a ring that fills around the press point\n * (`createLongPressFeedbackContribution()`), static under\n * `prefers-reduced-motion`. `false` shows nothing; a contribution replaces\n * it, reading the `longPress` dep. Installed beside `ambient` and beside a `tools`\n * takeover, unless the takeover's ambient holds an entry of the same id.\n */\n feedback?: boolean | SurfaceContribution;\n /** Default true. A long-press some binding handled pulses\n * `navigator.vibrate` briefly, for touch and pen, where the API exists. */\n haptics?: boolean;\n /** Hold time in ms. Default `LONG_PRESS_MS` (500). */\n duration?: number;\n}\n\n/** `<SceneCanvas layers>`: a {@link LayersMap} whose scene slot may be partial,\n * since the defaults fill in whatever it leaves out. */\nexport type SceneCanvasLayers<TNode extends { id: string }, TPose> = LayersMap<\n TNode,\n TPose,\n Partial<SceneSlotConfig<TNode, TPose>>\n>;\n\nimport type { BuiltinToolId } from './builtinToolSets';\nimport type { ToolEntry } from '@weasel-js/routing';\nexport { BUILTIN_TOOL_IDS, type BuiltinToolId } from './builtinToolSets';\n\nexport type { SceneCanvasHit };\n\n/** Props for `<SceneCanvas>`. A scene is the only thing it needs. With nothing\n * else it renders that scene and keeps a selection no input sets; `features`\n * turns behavior on by preset, and the rest configures what those turn on. */\nexport type SceneCanvasProps<TData, TLayer extends string, TPose> =\n Omit<\n CanvasProps<TPose>,\n | 'adapter' | 'boundsOf' | 'selection' | 'tools'\n | 'viewState' // SceneCanvas makes the camera from the view props below\n | 'layers' // stripped so we can re-add as optional below\n | 'getIsVisible' // SceneCanvas synthesizes this from chromeVisibility\n | 'contentVersion' // SceneCanvas wires this to the scene's own version\n | 'layerVisibility' | 'layerOrder' // re-declared below: they gate picking here too\n | 'debug' // re-declared below with SceneCanvas's own `slops`\n > & CameraProps\n & {\n /** A `Scene` (typically from `useScene`) — or a `SerializedScene` JSON\n * object, which SceneCanvas bakes into a Scene on first render. The\n * serialized form is read once; pass a `key` on `<SceneCanvas>` to force\n * a fresh canvas from updated JSON.\n *\n * The JSON shape is relaxed (`version: number` rather than the `1`\n * literal) so an `import json from './x.json'` result satisfies the type\n * without an `as` cast. */\n scene:\n | Scene<TData, TLayer, TPose>\n | SerializedScene<TData, TLayer, TPose>\n | { version: number; systemLayers?: ReadonlyArray<{ id: string }>; nodes: ReadonlyArray<unknown> };\n\n /** Layer configuration. When omitted, SceneCanvas applies kit defaults\n * (a scene slot that paints `node.data.fill` rects + a default\n * selection overlay). A partial scene or selection-overlay config is\n * spread over that slot's defaults; pass `slot: null` to suppress a\n * default explicitly. */\n layers?: SceneCanvasLayers<Node<TData, TLayer, TPose>, TPose>;\n\n /** Which layout container a drag lands in when several contain the drop\n * point — the innermost (default), the topmost in paint order, or only\n * containers whose strategy declares a `dropRegion`. Decides both the\n * drag-time reflow preview and the commit. */\n layoutDropTarget?: LayoutDropTargetMode;\n\n /** Carries the siblings a layout reflow displaces to their slots over time\n * instead of snapping them — `useAnimatedReflow` builds one. Covers a\n * drag's reflow and every one the scene records (an insert, delete,\n * reorder or resize inside a laid-out container). Absent or `null`,\n * they snap. */\n reflowTransition?: ReflowTransition<TPose> | null;\n\n /** Glides what is shown when the scene writes a pose — a `scene.setPose`, an undo — and sees\n * removed nodes off with `exit`. Absent or `null`, writes snap. See {@link PoseTransitionSeat}. */\n poseTransition?: PoseTransitionSeat<TPose> | null;\n\n /**\n * Lets pose-transform actions (move, resize, nudge, flip — NOT rotate) also rewrite a\n * node's data-held geometry: `transform(node, m, change)` returns updated `data`, or\n * `null` for nodes with no data-held geometry. When absent, the kit emits only the pose\n * op and leaves `data` untouched.\n *\n * @see GeometryProjection\n */\n geometryProjection?: GeometryProjection;\n\n /**\n * Routing-trait classifiers. The kit builds a `NodeRouting` registry per `<SceneCanvas>`\n * from this prop and uses it to derive each hit's `kind`, which tool routing tables\n * (e.g. `{ target: 'rect', actionId: 'move' }`) match against. Pass `defaultNodeRouting`\n * for the kit's built-in shape kinds; spread additional entries for consumer-defined\n * kinds.\n *\n * **Memoize the `routing` value.** The registry is memoized on the prop's reference\n * identity: a fresh array each render rebuilds it and cascades into a new adapter,\n * churning gesture state.\n */\n routing?: readonly NodeRoutingEntry[];\n\n /** How to read and rewrite this scene's poses. Every built-in action,\n * the selection chrome and picking read it. Default `AUTO_POSE_DESCRIPTOR`\n * (rect poses and `Path` poses). */\n poseDescriptor?: PoseDescriptor<TPose>;\n\n // --- Geometry: hit-test + bounds overrides consumed by the internal\n // `useSelectTool`. Ignored if the consumer passes their own `tools`. ---\n geometry?: SceneCanvasGeometry;\n\n // --- Selection options. `pickBest` and `handleHitRadius` configure the\n // internal select tool, and are ignored under a `tools` takeover;\n // `move`, `resize` and `rotate` configure the `move`,\n // `resize` and `rotate` presets, and apply either way. ---\n selectTool?: {\n move?: UseMoveOptions<TPose>;\n resize?: UseResizeOptions<TPose>;\n /** Rotation options, or `false` to hide the rotation handle and bind it to\n * nothing; the `rotate` action stays, for other tools and `actions.rotate`. */\n rotate?: UseRotateOptions<TPose> | false;\n handleHitRadius?: number;\n /** Override the body-pick used on click/pointerdown. Alt-aware: receives\n * the live alt state + current selection so consumers can implement\n * alt-cycling through an overlapping stack. Default: top-most hit\n * (alt ignored). */\n pickBest?: (world: import('@weasel-js/routing').WorldPoint, alt: boolean, sel: readonly string[]) => string | null;\n };\n\n /** Consumer node factories for the `insert` action, keyed by tool `kind`.\n * Each factory receives the drag AABB + tool `extras` and returns the\n * node's `data` (in this canvas's own data shape) plus an optional `pose`.\n * A factory for a kit kind (`rect`, `line`, …) replaces the kit's default\n * `{ path, fill }` node for that kind; a factory for a novel kind (e.g.\n * `text`) adds insert support the kit doesn't ship. The dep supplies id,\n * layer, and the undoable op. Return `null` to reject an insert. */\n insertNodeFactories?: Record<string, InsertNodeFactory>;\n\n /** The fill, stroke, and fill rule a kit-built shape is inserted with, by\n * tool `kind` and insert count; the kit keeps the geometry. Default\n * `paletteInsertPaint`, the kit palette in turn. A kind with a factory in\n * `insertNodeFactories` is not asked. */\n insertPaint?: InsertPaint;\n\n /** External-content ingestion (OS drop / clipboard paste / picker).\n *\n * - `handlers` — content handlers for this canvas alone, priority 0 by default, above\n * the kit's. One for every canvas on the page goes through `registerContentHandler`.\n * - `kitHandlers` — which kit handlers it keeps (`KitHandlersOption`); default all.\n * - `resolveSrc` — overrides the image handler's `data:`-URI embed (e.g.\n * upload to an asset store, return the URL).\n * - `svg.unpack` — makes the kit SVG handler parse dropped SVG files into\n * native scene nodes instead of one embedded-image node each.\n * - `clipboard` — the kit weasel-JSON paste handler, enabled by default.\n * `reviver` restores JSON-unfriendly values the copying side encoded via\n * `jsonReplacer`. `enabled: false` opts the canvas out, but the kit\n * handler still consumes weasel-matching items and declines them inert\n * rather than letting them fall through. Also governs Cmd/Ctrl+V under\n * the `edit` preset; the rest takes the `ingest` preset.\n *\n * Memoize `handlers` and `clipboard.reviver`: inline literals rebuild them every render. */\n ingestion?: {\n handlers?: ContentHandlerEntry[];\n kitHandlers?: KitHandlersOption;\n resolveSrc?: (file: File) => Promise<string>;\n svg?: SvgIngestOptions;\n clipboard?: {\n reviver?: (key: string, value: unknown) => unknown;\n enabled?: boolean;\n /** Called when a paste left copied nodes out, with each one and the\n * reason. `useClipboardOps` takes the same callback as\n * `onPasteOmitted` for a paste from its own in-memory copy. */\n onOmitted?: (omitted: PasteOmission[]) => void;\n };\n };\n\n selection?: SelectionApi;\n selectionOptions?: UseSelectionOptions;\n\n /**\n * `false` stops the canvas writing the selection: no click, marquee,\n * lasso, action or tool, and no op committed through the adapter it hands\n * its tools. The consumer's own `SelectionApi` — the `selection` prop, or\n * `useSelection({ scene })` — still writes, and the canvas draws what it\n * holds. Undo and redo still restore the selection each history entry\n * recorded. Default `true`.\n *\n * Click policy (whether shift-click extends) is the selection's `mode`:\n * `useSelection({ mode: 'multi' })`, or `selectionOptions={{ mode: 'multi' }}`\n * for the selection the canvas builds itself.\n */\n selectable?: boolean;\n\n /**\n * Behavior presets to turn on, composable in any combination. A canvas\n * with none renders its scene and keeps a selection (the `selection` API\n * works) that no input sets: no tools, no standard actions or their keys,\n * no clipboard, no drop or paste, and no selection chrome. The dispatcher\n * is still mounted, so bindings a consumer adds work.\n *\n * - `view` — wheel pan and zoom, pinch, the zoom keys, and the hand tool\n * (H, or hold Space). Passing `viewport` implies it.\n * - `select` — the select tool, as the initial active tool and Escape's\n * return target: click to pick, drag on empty to marquee, click on empty\n * to clear.\n * - `outline` — the selection outline, whichever tool sets the selection.\n * - `pick` — `select` and `outline`.\n * - `move` — drag a body to move it, Alt-drag to clone it. Ambient, so it\n * runs under any tool that does not claim the drag.\n * - `resize` — resize handles, drawn and bound.\n * - `rotate` — the rotation handle, drawn and bound.\n * - `transform` — `resize` and `rotate`.\n * - `edit` — undo/redo, delete, duplicate, group/ungroup, nudge,\n * select-all, Escape, cancel-gesture, cut/copy/paste, fill and stroke,\n * and their keys. Cmd/Ctrl+V pastes nodes a kit canvas copied, from\n * this tab or another, and nothing else.\n * - `arrange` — align, distribute, reorder, flip.\n * - `paths` — pathfinder operations, path-edit entry and anchor editing.\n * - `ingest` — dropped and pasted content: images, SVG, files, and\n * whatever `ingestion.handlers` accept, on Cmd/Ctrl+V or an OS drop.\n * - `draw` — all of the above.\n *\n * Any `FeaturePreset` object goes beside the names: core's own\n * (`pathsPreset`), or one a package ships.\n * Tools add the actions they bind — a shape tool brings `insert` — so\n * `defaultTools` composes with any preset. `actions` then adds, overrides\n * or removes on top of what the presets registered.\n */\n features?: readonly FeatureEntry[];\n\n /** Extra tools or overrides keyed by id, or a full `ToolsApi` takeover.\n *\n * **Patch form** (`Record<string, Tool | true | false>`): merged\n * into the built-in registry on top of whatever `defaultTools` and\n * `features` already selected.\n * - `true` pulls in the built-in for this id (`'pen'`, `'lasso'`,\n * `'hand'`, …), the same as listing it in `defaultTools`. Unknown\n * built-in ids warn in dev and are ignored.\n * - `Tool` adds a new id or replaces an existing one in place\n * (dev-only warning if `defaultTools` also names that id).\n * - `false` omits a bundled tool entirely.\n * Auto-wiring (keybindings, dispatcher, action registry) still runs.\n *\n * **Takeover form** (`ToolsApi`): the internal registry is bypassed and\n * this `tools` value is forwarded to Canvas as-is — the consumer owns\n * active-slot management, and `pick`'s select tool is theirs to supply.\n * Every other preset still applies. Keybindings are still auto-wired\n * against the supplied registry; pass `enableKeybindings={false}` to opt\n * out. */\n tools?: ToolsApi | Record<string, Tool | true | false>;\n\n /**\n * Auto-wire keyboard shortcuts. Default `true`. When `false`, SceneCanvas still mounts\n * its tools but routes no keyboard input to them — neither tool hotkeys nor\n * keyboard-bound actions like delete / escape / nudge. Pointer, wheel, and contextmenu\n * interactions are unaffected.\n */\n enableKeybindings?: boolean;\n\n /**\n * Take the keyboard on mount, as a press in the canvas would. A key that\n * reaches the page goes to the canvas the user last pressed or scrolled\n * in, and to no canvas before the first such interaction, so a canvas in a\n * side panel does not take Space from a page that wants it. An app whose\n * canvas is the page sets this so its shortcuts work before the first\n * click. Default `false`.\n */\n claimKeys?: boolean;\n\n /**\n * Auto-mount the gesture dispatcher (`useGestureDispatcher`). Default `true`. `false` is\n * for tests or demos that drive actions another way, or that call\n * `useGestureDispatcher` themselves.\n */\n enableGestureDispatcher?: boolean;\n\n /** Called with the `ToolsApi` SceneCanvas constructs or receives, whenever its identity\n * changes. For introspection. */\n onToolsCreated?: (tools: ToolsApi) => void;\n\n /**\n * Built-in tools SceneCanvas registers in its internal `useTools`, on top\n * of the ones presets bring (`select` from `pick`, `hand` from `view`):\n * ids, or sets of them such as `SHAPE_TOOLS`, or `withoutTools(...)` of one.\n * Default: none. Each tool also registers the kit actions it binds, so\n * `['rect']` brings `insert`. Ignored when the consumer supplies their own\n * `tools` prop.\n */\n defaultTools?: readonly ToolEntry<BuiltinToolId>[];\n\n /** Per-tool option overrides for the built-in tools. Each entry is a\n * narrow subset of the underlying hook's options surface — `lasso.mode`,\n * `rig.state`, etc. */\n toolOptions?: BuiltinToolOptions;\n /** What gestures snap to, and how near it pulls. */\n snap?: SceneCanvasSnap;\n\n /** Initial active-slot tool id. Default: `'select'` when it is\n * registered, otherwise none — a canvas can run on ambient bindings\n * alone. Must be one of the registered tools. Ignored when the consumer\n * supplies their own `tools` prop. */\n initialActiveTool?: string;\n\n /** Always-on entries to register alongside the internal tools — every one\n * a `SurfaceContribution`, whose views, deps, overlay, bindings, actions\n * and `attach` all install from this one list, and uninstall when the\n * entry leaves it. A tool is a contribution too, so one can go here. If\n * you supply your own `tools` prop, this is ignored — wire `ambient`\n * through your own `useTools` call instead. */\n ambient?: readonly SurfaceContribution[];\n\n /** What an entry's `requires` does when the running kit does not meet\n * it: `'warn'` (the default) logs in development, `'strict'` throws,\n * `'off'` skips the check. */\n versionCheck?: VersionCheck;\n\n /** Touch and pen long-press: its hold time, its haptic, and the\n * feedback shown while it is held. */\n longPress?: SceneCanvasLongPress;\n\n /** Configures the `view` preset, and implies it. See {@link SceneCanvasViewport}. */\n viewport?: SceneCanvasViewport;\n\n /**\n * Given a scene node, return a short human-readable \"kind\" label (e.g. `'rectangle'`,\n * `'sticky note'`), which the kit publishes per id into any surrounding\n * `<SelectionContextProvider>`. Return `undefined` to skip an entry. When omitted,\n * each node reports its routing kind (see `routing`), and a node that routes as\n * `'unknown'` is left unlabeled.\n */\n describeKind?: (node: Node<TData, TLayer, TPose>) => string | undefined;\n\n /**\n * Animator to bind for per-frame redraws. SceneCanvas requests a redraw on every active\n * animation frame and paints `animator.colorOverrides` onto the scene's nodes: the\n * kit's path and shape painters apply them, and a custom `drawOne` receives them as\n * `ctx.vertexColors`.\n */\n animator?: Animator;\n\n /**\n * Extra views on this canvas: each is a camera over a rect of the same\n * surface, drawn through one GL context, with input routed to it. The flat\n * `view` / `onViewChange` props above stay the canvas's own camera — view\n * zero — and are unaffected by anything declared here.\n *\n * Order is paint and hit order, low to high. Each entry is a\n * `<CanvasView>`; mounting one as a child is the same declaration, and\n * children land after every entry here.\n */\n views?: readonly CanvasViewProps[];\n\n /**\n * Children rendered alongside the canvas, inside its input scope (e.g.\n * shortcuts overlays, probes).\n */\n children?: ReactNode;\n\n /** A yoke to join, from `useYoke()`: canvases on one yoke share the active tool, the gesture\n * in flight and history. Omitted, this canvas keeps its own. */\n yoke?: Yoke;\n\n /**\n * Chrome-caps visibility overrides, keyed by chrome id (`selection.outline`,\n * `selection.rotation-handle`, `gesture.marquee`, …). Each entry is a composable\n * {@link Condition} built from the `cond()` builder, merged on top of the kit's\n * `defaultVisibilityRules`. Set an id to `never` to suppress a chrome element\n * entirely (also unhittable), or to `always` to force-show.\n */\n chromeVisibility?: import('domains/chrome-caps').VisibilityRules;\n\n /**\n * The app's mode registry (`createModeRegistry` in `@weasel-js/modes`).\n * The canvas reads the active mode from it to gate chrome, tool\n * activation and the dispatcher's eligibility filter, repaints when it\n * switches, and hands every registered mode to the dev-time route-conflict\n * check. Omitted, the canvas behaves as the kit's normal mode and nothing\n * revokes anchor editing.\n */\n modes?: ModeRegistry;\n\n /** Hands which path is being anchor-edited to the host. See {@link PathEditHost}. */\n pathEdit?: PathEditHost;\n /** A path to open in anchor edit as the canvas mounts, with anchors selected. Read once. */\n defaultPathEdit?: PathEditOpening;\n\n /**\n * Override detected device facts. Merged over what `matchMedia` reports;\n * `targetScale` is re-derived from the merged `coarsePointer` unless you\n * override it explicitly.\n */\n device?: Partial<DeviceProfile>;\n\n /**\n * Live focus getter for chrome-caps' `focused` ctx field, for visibility rules that\n * read the `focused` atom (the kit's default `selection.rotation-handle` rule does).\n * Omit to default `focused` to `true`.\n */\n getFocused?: () => boolean;\n\n /** Custom shader programs to compile on the renderer. See `CanvasProps.shaders`. */\n shaders?: ShaderProgramHandle[];\n\n /** Flatness tolerance for curve tessellation. See `CanvasProps.flattenTolerance`. */\n flattenTolerance?: FlattenTolerance;\n\n /** Fill for the full canvas behind the scene, drawn as a screen-space layer slotted\n * before `'scene'` — independent of pan / zoom. */\n backgroundFill?: Paint;\n /** Dev HUD: a fixed-position widget showing live cursor coords in both viewport\n * (client) and canvas (world) frames. */\n cursorCoordsHud?: boolean;\n /** Dev HUD: a fixed-position widget listing the ids `pickEvery(world)` returns under\n * the cursor. */\n pickHud?: boolean;\n /**\n * The debug overlay: `<Canvas debug>`'s config, plus `slops: true` for\n * translucent halos at every affordance hit zone. `false` turns it off;\n * absent reads `?debug=…` from the URL, as `<Canvas>` does.\n */\n debug?: (DebugConfig & { slops?: boolean }) | false;\n /**\n * Dev HUD: when true (or object), mounts a fixed-position widget below\n * the pick HUD showing the active modality mode, active-slot tool, and\n * hotkey stack. Pass `{ modeId }` to populate the mode line.\n */\n modalityHud?: boolean | { modeId?: string };\n\n /**\n * Per-id alpha multiplier for the scene-render slot: each node's draw output is wrapped\n * in a group with the returned alpha, and 1 emits no wrapper. Typical use: dimming\n * non-active nodes during a mode transition.\n */\n alphaFor?: (id: string) => number;\n\n /**\n * Show or hide render layers in this canvas's own view, by id — the map\n * `<Canvas>` takes. Each scene layer paints as its own render layer, keyed\n * `scene:<layerId>`, so `{ 'scene:guides': false }` hides one scene layer\n * here and nowhere else.\n *\n * A layer hidden this way is gone from this view: it does not paint, and a\n * click, a marquee or Cmd+A here passes over it. Another view of the same\n * scene — a `<CanvasView>`, a second canvas, a minimap — still shows it and\n * can still take it. It applies on top of the scene's own\n * `LayerRecord.visible` and cannot show a layer the scene hides.\n */\n layerVisibility?: Record<string, boolean>;\n\n /**\n * Draw order for this view, by render-layer id, bottom first. A listed\n * order is the whole list: a scene layer left out of it is neither painted\n * nor picked here. See `CanvasProps.layerOrder`.\n */\n layerOrder?: string[];\n } & SceneCanvasPointerProps<TData> & SceneCanvasActionProps;\n"],"mappings":";;;;;;;;AAqDA,IAAa,sBAAsB"}
1
+ {"version":3,"file":"props.js","names":[],"sources":["../../../src/canvas/SceneCanvas/props.ts"],"sourcesContent":["import type { ModeRegistry } from '@weasel-js/modes';\nimport type { PathEditHost, PathEditOpening } from './pathEditHost';\nimport type { SceneCanvasGeometry } from './geometryProps';\nimport type { SceneCanvasHit, SceneCanvasPointerProps } from './pointerProps';\nimport type { Paint } from '@weasel-js/paint';\nimport type { SceneCanvasActionProps } from './actionProps';\nimport { Yoke } from '@weasel-js/routing/react';\nimport type { PasteOmission } from 'core/adapters/types';\nimport type { Op } from 'core/ops/types';\nimport type { Node, Scene, SerializedScene } from 'core/scene/types';\nimport type { SelectionApi, UseSelectionOptions } from 'core/selection/useSelection';\nimport type { ContentHandlerEntry, KitHandlersOption } from 'domains/ingestion';\nimport type { SceneCanvasSnap } from 'domains/snap/types';\nimport type { SvgIngestOptions } from 'interactions/actions/depSchema';\nimport type { GeometryProjection } from 'interactions/actions/geometryProjection';\nimport type { UseMoveOptions } from 'interactions/actions/move/options';\nimport type { PoseDescriptor } from 'interactions/actions/resize/geometry';\nimport type { UseResizeOptions } from 'interactions/actions/resize/options';\nimport type { UseRotateOptions } from 'interactions/actions/rotate/options';\nimport type { ReactNode } from 'react';\nimport type { Animator } from '@weasel-js/animator';\nimport type { PoseTransitionSeat } from '../../animation/poseTransition';\nimport { HANDLE_BASE_PX } from '../../core/device/targets';\nimport type { DeviceProfile } from '../../core/device/types';\nimport type { NodeRoutingEntry } from '../../core/scene/NodeRouting';\nimport type { DebugConfig } from '../../debug/types';\nimport type { LayoutDropTargetMode, ReflowTransition } from '../../layout/types';\nimport type { FlattenTolerance, ShaderProgramHandle } from '@weasel-js/renderer';\nimport type { Tool, ToolsApi } from '../../tools/overlayBinding';\nimport type { SceneSlotConfig } from '../buildSceneLayer';\nimport type { CanvasProps } from '../Canvas';\nimport type { LayersMap } from '../layerSlots';\nimport type { CanvasViewProps } from '../CanvasView';\nimport type { InsertNodeFactory, InsertPaint } from '../deps';\nimport type { SurfaceContribution } from '../surfaceContribution';\nimport type { CameraProps } from '../useCanvasView';\nimport type { FeatureEntry } from './features';\nimport type { BuiltinToolOptions } from './useBuiltinShapeTools';\nimport type { VersionCheck } from './useContributionRoles';\nimport type { SceneCanvasViewport } from './viewportProp';\n\n/** The adapter surface delete / duplicate / group / ungroup need, narrowed from\n * `SceneCanvasAdapter` because `useSceneStandardActions` is non-generic. */\nexport interface BridgeAdapter {\n applyOps(ops: Op[], label?: string): void;\n insertNode(node: { id: string; [k: string]: unknown }): void;\n removeNode(id: string): void;\n setSelection(ids: string[]): void;\n}\n\n/** Default size in CSS pixels for selection corner-handles AND their\n * hit-test radius, at `targetScale = 1`. Deliberately unscaled; kit-internal\n * use sites multiply by `DeviceProfile.targetScale`. */\nexport const DEFAULT_HANDLE_SIZE = HANDLE_BASE_PX;\n\n/** `<SceneCanvas longPress>`. */\nexport interface SceneCanvasLongPress {\n /**\n * Shown while a long-press is held and some binding would fire on it.\n * Default: a ring that fills around the press point\n * (`createLongPressFeedbackContribution()`), static under\n * `prefers-reduced-motion`. `false` shows nothing; a contribution replaces\n * it, reading the `longPress` dep. Installed beside `ambient` and beside a `tools`\n * takeover, unless the takeover's ambient holds an entry of the same id.\n */\n feedback?: boolean | SurfaceContribution;\n /** Default true. A long-press some binding handled pulses\n * `navigator.vibrate` briefly, for touch and pen, where the API exists. */\n haptics?: boolean;\n /** Hold time in ms. Default `LONG_PRESS_MS` (500). */\n duration?: number;\n}\n\n/** `<SceneCanvas layers>`: a {@link LayersMap} whose scene slot may be partial,\n * since the defaults fill in whatever it leaves out. */\nexport type SceneCanvasLayers<TNode extends { id: string }, TPose> = LayersMap<\n TNode,\n TPose,\n Partial<SceneSlotConfig<TNode, TPose>>\n>;\n\nimport type { BuiltinToolId } from './builtinToolSets';\nimport type { ToolEntry } from '@weasel-js/routing';\nexport { BUILTIN_TOOL_IDS, type BuiltinToolId } from './builtinToolSets';\n\nexport type { SceneCanvasHit };\n\n/** Props for `<SceneCanvas>`. A scene is the only thing it needs. With nothing\n * else it renders that scene and keeps a selection no input sets; `features`\n * turns behavior on by preset, and the rest configures what those turn on. */\nexport type SceneCanvasProps<TData, TLayer extends string, TPose> =\n Omit<\n CanvasProps<TPose>,\n | 'adapter' | 'boundsOf' | 'selection' | 'tools'\n | 'viewState' // SceneCanvas makes the camera from the view props below\n | 'layers' // stripped so we can re-add as optional below\n | 'getIsVisible' // SceneCanvas synthesizes this from chromeVisibility\n | 'contentVersion' // SceneCanvas wires this to the scene's own version\n | 'layerVisibility' | 'layerOrder' // re-declared below: they gate picking here too\n | 'debug' // re-declared below with SceneCanvas's own `slops`\n > & CameraProps\n & {\n /** A `Scene` (typically from `useScene`) — or a `SerializedScene` JSON\n * object, which SceneCanvas bakes into a Scene on first render. The\n * serialized form is read once; pass a `key` on `<SceneCanvas>` to force\n * a fresh canvas from updated JSON.\n *\n * The JSON shape is relaxed (`version: number` rather than the `1`\n * literal) so an `import json from './x.json'` result satisfies the type\n * without an `as` cast. */\n scene:\n | Scene<TData, TLayer, TPose>\n | SerializedScene<TData, TLayer, TPose>\n | { version: number; systemLayers?: ReadonlyArray<{ id: string }>; nodes: ReadonlyArray<unknown> };\n\n /** Layer configuration. When omitted, SceneCanvas applies kit defaults\n * (a scene slot that paints `node.data.fill` rects + a default\n * selection overlay). A partial scene or selection-overlay config is\n * spread over that slot's defaults; pass `slot: null` to suppress a\n * default explicitly. */\n layers?: SceneCanvasLayers<Node<TData, TLayer, TPose>, TPose>;\n\n /** Which layout container a drag lands in when several contain the drop\n * point — the innermost (default), the topmost in paint order, or only\n * containers whose strategy declares a `dropRegion`. Decides both the\n * drag-time reflow preview and the commit. */\n layoutDropTarget?: LayoutDropTargetMode;\n\n /** Carries the siblings a layout reflow displaces to their slots over time\n * instead of snapping them — `useAnimatedReflow` builds one. Covers a\n * drag's reflow and every one the scene records (an insert, delete,\n * reorder or resize inside a laid-out container). Absent or `null`,\n * they snap. */\n reflowTransition?: ReflowTransition<TPose> | null;\n\n /** Glides what is shown when the scene writes a pose — a `scene.setPose`, an undo — and sees\n * removed nodes off with `exit`. Absent or `null`, writes snap. See {@link PoseTransitionSeat}. */\n poseTransition?: PoseTransitionSeat<TPose> | null;\n\n /**\n * Lets pose-transform actions (move, resize, nudge, flip — NOT rotate) also rewrite a\n * node's data-held geometry: `transform(node, m, change)` returns updated `data`, or\n * `null` for nodes with no data-held geometry. When absent, the kit emits only the pose\n * op and leaves `data` untouched.\n *\n * @see GeometryProjection\n */\n geometryProjection?: GeometryProjection;\n\n /**\n * Routing-trait classifiers. The kit builds a `NodeRouting` registry per `<SceneCanvas>`\n * from this prop and uses it to derive each hit's `kind`, which tool routing tables\n * (e.g. `{ target: 'rect', actionId: 'move' }`) match against. Pass `defaultNodeRouting`\n * for the kit's built-in shape kinds; spread additional entries for consumer-defined\n * kinds.\n *\n * **Memoize the `routing` value.** The registry is memoized on the prop's reference\n * identity: a fresh array each render rebuilds it and cascades into a new adapter,\n * churning gesture state.\n */\n routing?: readonly NodeRoutingEntry[];\n\n /** How to read and rewrite this scene's poses. Every built-in action,\n * the selection chrome and picking read it. Default `AUTO_POSE_DESCRIPTOR`\n * (rect poses and `Path` poses). */\n poseDescriptor?: PoseDescriptor<TPose>;\n\n // --- Geometry: hit-test + bounds overrides consumed by the internal\n // `useSelectTool`. Ignored if the consumer passes their own `tools`. ---\n geometry?: SceneCanvasGeometry;\n\n // --- Selection options. `pickBest` and `handleHitRadius` configure the\n // internal select tool, and are ignored under a `tools` takeover;\n // `move`, `resize` and `rotate` configure the `move`,\n // `resize` and `rotate` presets, and apply either way. ---\n selectTool?: {\n move?: UseMoveOptions<TPose>;\n resize?: UseResizeOptions<TPose>;\n /** Rotation options, or `false` to hide the rotation handle and bind it to\n * nothing; the `rotate` action stays, for other tools and `actions.rotate`. */\n rotate?: UseRotateOptions<TPose> | false;\n handleHitRadius?: number;\n /** Override the body-pick used on click/pointerdown. Alt-aware: receives\n * the live alt state + current selection so consumers can implement\n * alt-cycling through an overlapping stack. Default: top-most hit\n * (alt ignored). */\n pickBest?: (world: import('@weasel-js/routing').WorldPoint, alt: boolean, sel: readonly string[]) => string | null;\n };\n\n /** Consumer node factories for the `insert` action, keyed by tool `kind`.\n * Each factory receives the drag AABB + tool `extras` and returns the\n * node's `data` (in this canvas's own data shape) plus an optional `pose`.\n * A factory for a kit kind (`rect`, `line`, …) replaces the kit's default\n * `{ path, fill }` node for that kind; a factory for a novel kind (e.g.\n * `text`) adds insert support the kit doesn't ship. The dep supplies id,\n * layer, and the undoable op. Return `null` to reject an insert. */\n insertNodeFactories?: Record<string, InsertNodeFactory>;\n\n /** The fill, stroke, and fill rule a kit-built shape is inserted with, by\n * tool `kind` and insert count; the kit keeps the geometry. Default\n * `paletteInsertPaint`, the kit palette in turn. A kind with a factory in\n * `insertNodeFactories` is not asked. */\n insertPaint?: InsertPaint;\n\n /** External-content ingestion (OS drop / clipboard paste / picker).\n *\n * - `handlers` — content handlers for this canvas alone, priority 0 by default, above\n * the kit's. One for every canvas on the page goes through `registerContentHandler`.\n * - `kitHandlers` — which kit handlers it keeps (`KitHandlersOption`); default all.\n * - `resolveSrc` — overrides the image handler's `data:`-URI embed (e.g.\n * upload to an asset store, return the URL).\n * - `svg.unpack` — makes the kit SVG handler parse dropped SVG files into\n * native scene nodes instead of one embedded-image node each.\n * - `clipboard` — the kit weasel-JSON paste handler, enabled by default.\n * `reviver` restores JSON-unfriendly values the copying side encoded via\n * `jsonReplacer`. `enabled: false` opts the canvas out, but the kit\n * handler still consumes weasel-matching items and declines them inert\n * rather than letting them fall through. Also governs Cmd/Ctrl+V under\n * the `edit` preset; the rest takes the `ingest` preset.\n *\n * Memoize `handlers` and `clipboard.reviver`: inline literals rebuild them every render. */\n ingestion?: {\n handlers?: ContentHandlerEntry[];\n kitHandlers?: KitHandlersOption;\n resolveSrc?: (file: File) => Promise<string>;\n svg?: SvgIngestOptions;\n clipboard?: {\n reviver?: (key: string, value: unknown) => unknown;\n enabled?: boolean;\n /** Called when a paste left copied nodes out, with each one and the\n * reason. `useClipboardOps` takes the same callback as\n * `onPasteOmitted` for a paste from its own in-memory copy. */\n onOmitted?: (omitted: PasteOmission[]) => void;\n };\n };\n\n selection?: SelectionApi;\n selectionOptions?: UseSelectionOptions;\n\n /**\n * `false` stops the canvas writing the selection: no click, marquee,\n * lasso, action or tool, and no op committed through the adapter it hands\n * its tools. The consumer's own `SelectionApi` — the `selection` prop, or\n * `useSelection({ scene })` — still writes, and the canvas draws what it\n * holds. Undo and redo still restore the selection each history entry\n * recorded. Default `true`.\n *\n * Click policy (whether shift-click extends) is the selection's `mode`:\n * `useSelection({ mode: 'multi' })`, or `selectionOptions={{ mode: 'multi' }}`\n * for the selection the canvas builds itself.\n */\n selectable?: boolean;\n\n /**\n * Behavior presets to turn on, composable in any combination. A canvas\n * with none renders its scene and keeps a selection (the `selection` API\n * works) that no input sets: no tools, no standard actions or their keys,\n * no clipboard, no drop or paste, and no selection chrome. The dispatcher\n * is still mounted, so bindings a consumer adds work.\n *\n * - `view` — wheel pan and zoom, pinch, the zoom keys, and the hand tool\n * (H, or hold Space). Passing `viewport` implies it.\n * - `select` — the select tool, as the initial active tool and Escape's\n * return target: click to pick, drag on empty to marquee, click on empty\n * to clear.\n * - `outline` — the selection outline, whichever tool sets the selection.\n * - `pick` — `select` and `outline`.\n * - `move` — drag a body to move it, Alt-drag to clone it. Ambient, so it\n * runs under any tool that does not claim the drag.\n * - `resize` — resize handles, drawn and bound.\n * - `rotate` — the rotation handle, drawn and bound.\n * - `transform` — `resize` and `rotate`.\n * - `edit` — undo/redo, delete, duplicate, group/ungroup, nudge,\n * select-all, Escape, cancel-gesture, cut/copy/paste, fill and stroke,\n * and their keys. Cmd/Ctrl+V pastes nodes a kit canvas copied, from\n * this tab or another, and nothing else.\n * - `arrange` — align, distribute, reorder, flip.\n * - `paths` — pathfinder operations, path-edit entry and anchor editing.\n * - `ingest` — dropped and pasted content: images, SVG, files, and\n * whatever `ingestion.handlers` accept, on Cmd/Ctrl+V or an OS drop.\n * - `draw` — all of the above.\n *\n * Any `FeaturePreset` object goes beside the names: core's own\n * (`pathsPreset`), or one a package ships.\n * Tools add the actions they bind — a shape tool brings `insert` — so\n * `defaultTools` composes with any preset. `actions` then adds, overrides\n * or removes on top of what the presets registered.\n */\n features?: readonly FeatureEntry[];\n\n /** Extra tools or overrides keyed by id, or a full `ToolsApi` takeover.\n *\n * **Patch form** (`Record<string, Tool | true | false>`): merged\n * into the built-in registry on top of whatever `defaultTools` and\n * `features` already selected.\n * - `true` pulls in the built-in for this id (`'pen'`, `'lasso'`,\n * `'hand'`, …), the same as listing it in `defaultTools`. Unknown\n * built-in ids warn in dev and are ignored.\n * - `Tool` adds a new id or replaces an existing one in place\n * (dev-only warning if `defaultTools` also names that id).\n * - `false` omits a bundled tool entirely.\n * Auto-wiring (keybindings, dispatcher, action registry) still runs.\n *\n * **Takeover form** (`ToolsApi`): the internal registry is bypassed and\n * this `tools` value is forwarded to Canvas as-is — the consumer owns\n * active-slot management, and `pick`'s select tool is theirs to supply.\n * Every other preset still applies. Keybindings are still auto-wired\n * against the supplied registry; pass `enableKeybindings={false}` to opt\n * out. */\n tools?: ToolsApi | Record<string, Tool | true | false>;\n\n /**\n * Auto-wire keyboard shortcuts. Default `true`. When `false`, SceneCanvas still mounts\n * its tools but routes no keyboard input to them — neither tool hotkeys nor\n * keyboard-bound actions like delete / escape / nudge. Pointer, wheel, and contextmenu\n * interactions are unaffected.\n */\n enableKeybindings?: boolean;\n\n /**\n * Take the keyboard on mount, as a press in the canvas would. A key that\n * reaches the page goes to the canvas the user last pressed or scrolled\n * in, and to no canvas before the first such interaction, so a canvas in a\n * side panel does not take Space from a page that wants it. An app whose\n * canvas is the page sets this so its shortcuts work before the first\n * click. Default `false`.\n */\n claimKeys?: boolean;\n\n /**\n * Auto-mount the gesture dispatcher (`useGestureDispatcher`). Default `true`. `false` is\n * for tests or demos that drive actions another way, or that call\n * `useGestureDispatcher` themselves.\n */\n enableGestureDispatcher?: boolean;\n\n /** Called with the `ToolsApi` SceneCanvas constructs or receives, whenever its identity\n * changes. For introspection. */\n onToolsCreated?: (tools: ToolsApi) => void;\n\n /**\n * Built-in tools SceneCanvas registers in its internal `useTools`, on top\n * of the ones presets bring (`select` from `pick`, `hand` from `view`):\n * ids, or sets of them such as `SHAPE_TOOLS`, or `withoutTools(...)` of one.\n * Default: none. Each tool also registers the kit actions it binds, so\n * `['rect']` brings `insert`. Ignored when the consumer supplies their own\n * `tools` prop.\n */\n defaultTools?: readonly ToolEntry<BuiltinToolId>[];\n\n /** Per-tool option overrides for the built-in tools. Each entry is a\n * narrow subset of the underlying hook's options surface — `lasso.mode`,\n * `rig.state`, etc. */\n toolOptions?: BuiltinToolOptions;\n /** What gestures snap to, and how near it pulls. */\n snap?: SceneCanvasSnap;\n\n /** Initial active-slot tool id. Default: `'select'` when it is\n * registered, otherwise none — a canvas can run on ambient bindings\n * alone. Must be one of the registered tools. Ignored when the consumer\n * supplies their own `tools` prop. */\n initialActiveTool?: string;\n\n /** Always-on entries to register alongside the internal tools — every one\n * a `SurfaceContribution`, whose views, deps, overlay, bindings, actions\n * and `attach` all install from this one list, and uninstall when the\n * entry leaves it. A tool is a contribution too, so one can go here. If\n * you supply your own `tools` prop, this is ignored — wire `ambient`\n * through your own `useTools` call instead. */\n ambient?: readonly SurfaceContribution[];\n\n /** What an entry's `requires` does when the running kit does not meet\n * it: `'warn'` (the default) logs in development, `'strict'` throws,\n * `'off'` skips the check. */\n versionCheck?: VersionCheck;\n\n /** Touch and pen long-press: its hold time, its haptic, and the\n * feedback shown while it is held. */\n longPress?: SceneCanvasLongPress;\n\n /** Configures the `view` preset, and implies it. See {@link SceneCanvasViewport}. */\n viewport?: SceneCanvasViewport;\n\n /**\n * Given a scene node, return a short human-readable \"kind\" label (e.g. `'rectangle'`,\n * `'sticky note'`), which the kit publishes per id into any surrounding\n * `<SelectionContextProvider>`. Return `undefined` to skip an entry. When omitted,\n * each node reports its routing kind (see `routing`), and a node that routes as\n * `'unknown'` is left unlabeled.\n */\n describeKind?: (node: Node<TData, TLayer, TPose>) => string | undefined;\n\n /**\n * Animator to bind for per-frame redraws. SceneCanvas requests a redraw on every active\n * animation frame and paints `animator.colorOverrides` and `animator.trimOverrides` onto\n * the scene's nodes: the kit's path and shape painters apply them, and a custom `drawOne`\n * receives them as `ctx.vertexColors` and `ctx.trim`. Picking reads the authored trim.\n */\n animator?: Animator;\n\n /**\n * Extra views on this canvas: each is a camera over a rect of the same\n * surface, drawn through one GL context, with input routed to it. The flat\n * `view` / `onViewChange` props above stay the canvas's own camera — view\n * zero — and are unaffected by anything declared here.\n *\n * Order is paint and hit order, low to high. Each entry is a\n * `<CanvasView>`; mounting one as a child is the same declaration, and\n * children land after every entry here.\n */\n views?: readonly CanvasViewProps[];\n\n /**\n * Children rendered alongside the canvas, inside its input scope (e.g.\n * shortcuts overlays, probes).\n */\n children?: ReactNode;\n\n /** A yoke to join, from `useYoke()`: canvases on one yoke share the active tool, the gesture\n * in flight and history. Omitted, this canvas keeps its own. */\n yoke?: Yoke;\n\n /**\n * Chrome-caps visibility overrides, keyed by chrome id (`selection.outline`,\n * `selection.rotation-handle`, `gesture.marquee`, …). Each entry is a composable\n * {@link Condition} built from the `cond()` builder, merged on top of the kit's\n * `defaultVisibilityRules`. Set an id to `never` to suppress a chrome element\n * entirely (also unhittable), or to `always` to force-show.\n */\n chromeVisibility?: import('domains/chrome-caps').VisibilityRules;\n\n /**\n * The app's mode registry (`createModeRegistry` in `@weasel-js/modes`).\n * The canvas reads the active mode from it to gate chrome, tool\n * activation and the dispatcher's eligibility filter, repaints when it\n * switches, and hands every registered mode to the dev-time route-conflict\n * check. Omitted, the canvas behaves as the kit's normal mode and nothing\n * revokes anchor editing.\n */\n modes?: ModeRegistry;\n\n /** Hands which path is being anchor-edited to the host. See {@link PathEditHost}. */\n pathEdit?: PathEditHost;\n /** A path to open in anchor edit as the canvas mounts, with anchors selected. Read once. */\n defaultPathEdit?: PathEditOpening;\n\n /**\n * Override detected device facts. Merged over what `matchMedia` reports;\n * `targetScale` is re-derived from the merged `coarsePointer` unless you\n * override it explicitly.\n */\n device?: Partial<DeviceProfile>;\n\n /**\n * Live focus getter for chrome-caps' `focused` ctx field, for visibility rules that\n * read the `focused` atom (the kit's default `selection.rotation-handle` rule does).\n * Omit to default `focused` to `true`.\n */\n getFocused?: () => boolean;\n\n /** Custom shader programs to compile on the renderer. See `CanvasProps.shaders`. */\n shaders?: ShaderProgramHandle[];\n\n /** Flatness tolerance for curve tessellation. See `CanvasProps.flattenTolerance`. */\n flattenTolerance?: FlattenTolerance;\n\n /** Fill for the full canvas behind the scene, drawn as a screen-space layer slotted\n * before `'scene'` — independent of pan / zoom. */\n backgroundFill?: Paint;\n /** Dev HUD: a fixed-position widget showing live cursor coords in both viewport\n * (client) and canvas (world) frames. */\n cursorCoordsHud?: boolean;\n /** Dev HUD: a fixed-position widget listing the ids `pickEvery(world)` returns under\n * the cursor. */\n pickHud?: boolean;\n /**\n * The debug overlay: `<Canvas debug>`'s config, plus `slops: true` for\n * translucent halos at every affordance hit zone. `false` turns it off;\n * absent reads `?debug=…` from the URL, as `<Canvas>` does.\n */\n debug?: (DebugConfig & { slops?: boolean }) | false;\n /**\n * Dev HUD: when true (or object), mounts a fixed-position widget below\n * the pick HUD showing the active modality mode, active-slot tool, and\n * hotkey stack. Pass `{ modeId }` to populate the mode line.\n */\n modalityHud?: boolean | { modeId?: string };\n\n /**\n * Per-id alpha multiplier for the scene-render slot: each node's draw output is wrapped\n * in a group with the returned alpha, and 1 emits no wrapper. Typical use: dimming\n * non-active nodes during a mode transition.\n */\n alphaFor?: (id: string) => number;\n\n /**\n * Show or hide render layers in this canvas's own view, by id — the map\n * `<Canvas>` takes. Each scene layer paints as its own render layer, keyed\n * `scene:<layerId>`, so `{ 'scene:guides': false }` hides one scene layer\n * here and nowhere else.\n *\n * A layer hidden this way is gone from this view: it does not paint, and a\n * click, a marquee or Cmd+A here passes over it. Another view of the same\n * scene — a `<CanvasView>`, a second canvas, a minimap — still shows it and\n * can still take it. It applies on top of the scene's own\n * `LayerRecord.visible` and cannot show a layer the scene hides.\n */\n layerVisibility?: Record<string, boolean>;\n\n /**\n * Draw order for this view, by render-layer id, bottom first. A listed\n * order is the whole list: a scene layer left out of it is neither painted\n * nor picked here. See `CanvasProps.layerOrder`.\n */\n layerOrder?: string[];\n } & SceneCanvasPointerProps<TData> & SceneCanvasActionProps;\n"],"mappings":";;;;;;;;AAqDA,IAAa,sBAAsB"}
@@ -40,7 +40,7 @@ function useSceneCanvasLayers(args) {
40
40
  scene,
41
41
  adapter,
42
42
  alphaFor,
43
- colorOverrides: args.colorOverrides,
43
+ paintOverrides: args.paintOverrides,
44
44
  boundsOf: args.boundsOf,
45
45
  previewIds: args.previewIds
46
46
  });
@@ -1 +1 @@
1
- {"version":3,"file":"useSceneCanvasLayers.js","names":[],"sources":["../../../src/canvas/SceneCanvas/useSceneCanvasLayers.ts"],"sourcesContent":["import { useMemo, useRef, useState, type MutableRefObject } from 'react';\nimport type { ColorOverrideRegistry } from '@weasel-js/animator';\nimport type { Dispatcher } from '@weasel-js/routing';\nimport type { Affordance } from 'affordances/types';\nimport type { Node, NodeId, Scene } from 'core/scene/types';\nimport type { Bounds } from 'core/viewport/fitViewToBounds';\nimport type { PlaneMap } from 'core/viewport/parallax';\nimport type { View } from 'core/viewport/view';\nimport type { PathEditState } from 'domains/paths/pathEditState';\nimport { usePathEditOverlayLayer } from 'domains/paths/usePathEdit';\nimport type { rotationBadgeOf } from 'domains/selection/overlay';\nimport type { PoseDescriptor } from 'interactions/actions/resize/geometry';\nimport type { HierarchicalAdapter } from '../buildSceneTree';\nimport { builtLayersOf, isCustomEntry, type CanvasLayers } from '../layerSlots';\nimport { createSlopsDebugLayer } from '../slopsDebugLayer';\nimport { useTargetBasePx } from 'core/device/useTargetBasePx';\nimport { targetSizesPx } from 'core/device/targets';\nimport type { ResizeHandleLayout } from 'interactions/actions/resize/cornerHandles';\nimport { layersOverPresets, mergeLayersWithDefaults } from './mergeLayers';\nimport type { SceneCanvasLayers } from './props';\nimport type { EnabledPresets } from './features';\nimport { useDispatcherOverlayLayer } from './useDispatcherOverlayLayer';\nimport { usePreviewGhostLayer } from './usePreviewGhostLayer';\nimport { useDepartureGhostLayer } from './useDepartureGhostLayer';\nimport { useEffectSourcesLayer } from 'domains/pathEffects/useEffectSourcesLayer';\nimport type { Departures } from '../../animation/departures';\nimport { usePoseTransition, type PoseTransitionSeat } from '../../animation/poseTransition';\nimport { useSceneSlotLayers } from './useSceneSlotLayers';\nimport { useSelectionOverlayLayer } from './useSelectionOverlayLayer';\nimport { drawnBoundsScene } from '../derivedPath';\nimport { useEnteredGroupFrameLayer } from 'domains/selection/enteredGroupFrame';\nimport type { EnteredGroupApi } from '../deps/enteredGroup';\nimport { createSnapOutcomeStore, type SnapOutcomeStore } from 'domains/snap/outcomeStore';\nimport { createSnapReadoutLayer } from 'domains/snap/readoutLayer';\nimport type { SnapReadoutStyle } from 'domains/snap/types';\n\ninterface SceneCanvasLayersArgs<TData, TLayer extends string, TPose> {\n /** `<SceneCanvas layers>`. Omitted, the result is the full default set. */\n layers: SceneCanvasLayers<Node<TData, TLayer, TPose>, TPose> | undefined;\n /** The enabled presets' layers, beneath `layers`: a slot both name is `layers`'. */\n presetLayers: EnabledPresets['layers'];\n /** The device profile's, which sizes the default handles. */\n targetScale: number;\n scene: Scene<TData, TLayer, TPose>;\n adapter: HierarchicalAdapter<Node<TData, TLayer, TPose>, TPose>;\n poseDescriptor: PoseDescriptor<TPose>;\n alphaFor: ((id: string) => number) | undefined;\n colorOverrides: ColorOverrideRegistry | undefined;\n boundsOf: (id: string) => Bounds | null; enteredGroup: EnteredGroupApi; frameColor: { read: () => string; changed: (fn: () => void) => () => void };\n planeOfNode: (id: string, view: View) => PlaneMap | null;\n /** `<SceneCanvas poseTransition>`, whose ghosts of removed nodes this draws. */\n poseTransition: PoseTransitionSeat<TPose> | null | undefined;\n /** Ids an in-flight gesture is ghosting. */\n previewIds: () => Iterable<string> | null;\n dispatcher: Dispatcher;\n /** Whether to mount the snap readout layer. */\n snapReadout: boolean;\n snapReadoutStyle?: SnapReadoutStyle;\n pathEditState: PathEditState;\n /** `debug.slops`: paint the hit regions of the chrome affordances. */\n slops: boolean;\n /** `geometry.boundsIncludeStroke`: whether chrome sits on the stroke. */\n boundsIncludeStroke: boolean;\n /** `selectTool`'s `handleHitRadius` and `resize.edgeHandles`, resolved here into\n * the one {@link ResizeHandleLayout} the overlay paints and the hit-test reads. */\n selectTool?: { handleHitRadius?: number; resize?: { edgeHandles?: boolean } };\n}\n\n/**\n * The layer map `<SceneCanvas>` hands `<Canvas>`: the caller's `layers` over\n * the kit defaults, with the scene and selection-overlay slots built, and the\n * canvas's own layers anchored around them. Removed nodes leave just above\n * the scene, the preview ghost above them, a selected path effect's hidden sources above that, the dispatcher's\n * overlay (marquee, lasso) above those, and the\n * anchor overlay above the selection chrome.\n */\nexport function useSceneCanvasLayers<TData, TLayer extends string, TPose>(\n args: SceneCanvasLayersArgs<TData, TLayer, TPose>,\n): {\n layers: CanvasLayers;\n /** Where the selection overlay paints its rotate badge, or null. */\n rotationBadge: ReturnType<typeof rotationBadgeOf>;\n /** Which resize handles exist and their hit radius, for every hit-test of this canvas. */\n resizeHandles: ResizeHandleLayout;\n /** The affordances the gesture mounter hit-tests, which it publishes here\n * so the slops halos are the hit regions rather than a copy of them. */\n chromeAffordancesRef: MutableRefObject<readonly Affordance[]>;\n /** The marks the readout layer draws; the canvas's snap dep writes them. */\n snapStore: SnapOutcomeStore;\n /** The removed nodes the seat is seeing off. */\n departures: Departures<TPose, Node<TData, TLayer, TPose>>;\n} {\n const { scene, adapter, poseDescriptor, alphaFor, dispatcher, pathEditState, slops, targetScale } = args;\n\n const baseSizes = useTargetBasePx();\n const merged = useMemo(\n () => mergeLayersWithDefaults(layersOverPresets(args.presetLayers, args.layers), targetScale, baseSizes),\n [args.layers, args.presetLayers, targetScale, baseSizes],\n );\n\n const { config: sceneSlot, layers: sceneLayers } = useSceneSlotLayers({\n slot: merged.scene,\n scene,\n adapter,\n alphaFor,\n colorOverrides: args.colorOverrides,\n boundsOf: args.boundsOf,\n previewIds: args.previewIds,\n });\n // Takes the scene-wired slot, so a ghosted derived node still resolves its path.\n const previewLayer = usePreviewGhostLayer<TData, TLayer, TPose>({ scene, sceneSlot, dispatcher });\n const departures = usePoseTransition(scene, args.poseTransition);\n const departureLayer = useDepartureGhostLayer({ scene, sceneSlot, departures });\n const effectSourcesLayer = useEffectSourcesLayer(scene);\n const dispatcherOverlay = useDispatcherOverlayLayer({ dispatcher });\n\n const hitRadiusPx = args.selectTool?.handleHitRadius ?? targetSizesPx(targetScale, baseSizes).handle;\n const edges = args.selectTool?.resize?.edgeHandles ?? true;\n const resizeHandles = useMemo<ResizeHandleLayout>(() => ({ edges, hitRadiusPx }), [edges, hitRadiusPx]);\n\n const chromeAffordancesRef = useRef<readonly Affordance[]>([]);\n const slopsLayer = useMemo(\n () => createSlopsDebugLayer({ getAffordances: () => chromeAffordancesRef.current }),\n [],\n );\n\n const [snapStore] = useState(() => createSnapOutcomeStore());\n // Keyed on the style's values, so an inline `readoutStyle` literal keeps the layer.\n const { color, gapColor, lineWidth } = args.snapReadoutStyle ?? {};\n const snapReadoutLayer = useMemo(() => createSnapReadoutLayer(snapStore, {\n ...(color !== undefined ? { color } : {}),\n ...(gapColor !== undefined ? { gapColor } : {}),\n ...(lineWidth !== undefined ? { lineWidth } : {}),\n }), [snapStore, color, gapColor, lineWidth]);\n const overlaySlot = merged.pathEditingOverlay;\n const overlayConfig = overlaySlot && !isCustomEntry(overlaySlot) ? overlaySlot : undefined;\n const overlayAnchor = overlaySlot === null ? 'dispatcherOverlay' : 'pathEditingOverlay';\n const pathEditingOverlayLayer = usePathEditOverlayLayer(pathEditState, {\n nodeOf: (id) => scene.get(id as NodeId),\n planeOfNode: args.planeOfNode,\n }, overlayConfig);\n\n const { layer: selectionOverlayLayer, rotationBadge } = useSelectionOverlayLayer<TPose>(\n merged.selectionOverlay,\n pathEditState.getEditingIds,\n poseDescriptor,\n args.boundsIncludeStroke ? drawnBoundsScene(scene) : undefined,\n resizeHandles,\n );\n\n const enteredFrameLayer = useEnteredGroupFrameLayer(args.enteredGroup, args.boundsOf, args.frameColor, scene as never);\n const layers = useMemo<CanvasLayers>(() => ({\n ...builtLayersOf(merged),\n scene: sceneLayers,\n selectionOverlay: selectionOverlayLayer ? { layer: selectionOverlayLayer } : null,\n enteredGroupFrame: { layer: enteredFrameLayer, before: 'selectionOverlay' },\n departureGhost: { layer: departureLayer, after: 'scene' },\n previewGhost: { layer: previewLayer, after: 'departureGhost' },\n effectSources: { layer: effectSourcesLayer, after: 'previewGhost' },\n dispatcherOverlay: { layer: dispatcherOverlay, after: 'effectSources' },\n pathEditingOverlay: overlaySlot === null || isCustomEntry(overlaySlot)\n ? overlaySlot\n : { layer: pathEditingOverlayLayer, after: 'selectionOverlay' },\n ...(args.snapReadout ? { snapReadout: { layer: snapReadoutLayer, after: overlayAnchor } } : {}),\n ...(slops ? { slopsDebug: { layer: slopsLayer, after: args.snapReadout ? 'snapReadout' : overlayAnchor } } : {}),\n }), [merged, sceneLayers, selectionOverlayLayer, enteredFrameLayer, departureLayer, previewLayer, effectSourcesLayer, dispatcherOverlay, pathEditingOverlayLayer, overlaySlot, overlayAnchor, snapReadoutLayer, args.snapReadout, slops, slopsLayer,\n ]);\n\n return { layers, rotationBadge, resizeHandles, chromeAffordancesRef, snapStore, departures };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"useSceneCanvasLayers.js","names":[],"sources":["../../../src/canvas/SceneCanvas/useSceneCanvasLayers.ts"],"sourcesContent":["import { useMemo, useRef, useState, type MutableRefObject } from 'react';\nimport type { PaintOverrides } from '../paintOverrides';\nimport type { Dispatcher } from '@weasel-js/routing';\nimport type { Affordance } from 'affordances/types';\nimport type { Node, NodeId, Scene } from 'core/scene/types';\nimport type { Bounds } from 'core/viewport/fitViewToBounds';\nimport type { PlaneMap } from 'core/viewport/parallax';\nimport type { View } from 'core/viewport/view';\nimport type { PathEditState } from 'domains/paths/pathEditState';\nimport { usePathEditOverlayLayer } from 'domains/paths/usePathEdit';\nimport type { rotationBadgeOf } from 'domains/selection/overlay';\nimport type { PoseDescriptor } from 'interactions/actions/resize/geometry';\nimport type { HierarchicalAdapter } from '../buildSceneTree';\nimport { builtLayersOf, isCustomEntry, type CanvasLayers } from '../layerSlots';\nimport { createSlopsDebugLayer } from '../slopsDebugLayer';\nimport { useTargetBasePx } from 'core/device/useTargetBasePx';\nimport { targetSizesPx } from 'core/device/targets';\nimport type { ResizeHandleLayout } from 'interactions/actions/resize/cornerHandles';\nimport { layersOverPresets, mergeLayersWithDefaults } from './mergeLayers';\nimport type { SceneCanvasLayers } from './props';\nimport type { EnabledPresets } from './features';\nimport { useDispatcherOverlayLayer } from './useDispatcherOverlayLayer';\nimport { usePreviewGhostLayer } from './usePreviewGhostLayer';\nimport { useDepartureGhostLayer } from './useDepartureGhostLayer';\nimport { useEffectSourcesLayer } from 'domains/pathEffects/useEffectSourcesLayer';\nimport type { Departures } from '../../animation/departures';\nimport { usePoseTransition, type PoseTransitionSeat } from '../../animation/poseTransition';\nimport { useSceneSlotLayers } from './useSceneSlotLayers';\nimport { useSelectionOverlayLayer } from './useSelectionOverlayLayer';\nimport { drawnBoundsScene } from '../derivedPath';\nimport { useEnteredGroupFrameLayer } from 'domains/selection/enteredGroupFrame';\nimport type { EnteredGroupApi } from '../deps/enteredGroup';\nimport { createSnapOutcomeStore, type SnapOutcomeStore } from 'domains/snap/outcomeStore';\nimport { createSnapReadoutLayer } from 'domains/snap/readoutLayer';\nimport type { SnapReadoutStyle } from 'domains/snap/types';\n\ninterface SceneCanvasLayersArgs<TData, TLayer extends string, TPose> {\n /** `<SceneCanvas layers>`. Omitted, the result is the full default set. */\n layers: SceneCanvasLayers<Node<TData, TLayer, TPose>, TPose> | undefined;\n /** The enabled presets' layers, beneath `layers`: a slot both name is `layers`'. */\n presetLayers: EnabledPresets['layers'];\n /** The device profile's, which sizes the default handles. */\n targetScale: number;\n scene: Scene<TData, TLayer, TPose>;\n adapter: HierarchicalAdapter<Node<TData, TLayer, TPose>, TPose>;\n poseDescriptor: PoseDescriptor<TPose>;\n alphaFor: ((id: string) => number) | undefined;\n /** Animated paint to lay over the scene's nodes, typically `<SceneCanvas animator>`. */\n paintOverrides: PaintOverrides | undefined;\n boundsOf: (id: string) => Bounds | null; enteredGroup: EnteredGroupApi; frameColor: { read: () => string; changed: (fn: () => void) => () => void };\n planeOfNode: (id: string, view: View) => PlaneMap | null;\n /** `<SceneCanvas poseTransition>`, whose ghosts of removed nodes this draws. */\n poseTransition: PoseTransitionSeat<TPose> | null | undefined;\n /** Ids an in-flight gesture is ghosting. */\n previewIds: () => Iterable<string> | null;\n dispatcher: Dispatcher;\n /** Whether to mount the snap readout layer. */\n snapReadout: boolean;\n snapReadoutStyle?: SnapReadoutStyle;\n pathEditState: PathEditState;\n /** `debug.slops`: paint the hit regions of the chrome affordances. */\n slops: boolean;\n /** `geometry.boundsIncludeStroke`: whether chrome sits on the stroke. */\n boundsIncludeStroke: boolean;\n /** `selectTool`'s `handleHitRadius` and `resize.edgeHandles`, resolved here into\n * the one {@link ResizeHandleLayout} the overlay paints and the hit-test reads. */\n selectTool?: { handleHitRadius?: number; resize?: { edgeHandles?: boolean } };\n}\n\n/**\n * The layer map `<SceneCanvas>` hands `<Canvas>`: the caller's `layers` over\n * the kit defaults, with the scene and selection-overlay slots built, and the\n * canvas's own layers anchored around them. Removed nodes leave just above\n * the scene, the preview ghost above them, a selected path effect's hidden sources above that, the dispatcher's\n * overlay (marquee, lasso) above those, and the\n * anchor overlay above the selection chrome.\n */\nexport function useSceneCanvasLayers<TData, TLayer extends string, TPose>(\n args: SceneCanvasLayersArgs<TData, TLayer, TPose>,\n): {\n layers: CanvasLayers;\n /** Where the selection overlay paints its rotate badge, or null. */\n rotationBadge: ReturnType<typeof rotationBadgeOf>;\n /** Which resize handles exist and their hit radius, for every hit-test of this canvas. */\n resizeHandles: ResizeHandleLayout;\n /** The affordances the gesture mounter hit-tests, which it publishes here\n * so the slops halos are the hit regions rather than a copy of them. */\n chromeAffordancesRef: MutableRefObject<readonly Affordance[]>;\n /** The marks the readout layer draws; the canvas's snap dep writes them. */\n snapStore: SnapOutcomeStore;\n /** The removed nodes the seat is seeing off. */\n departures: Departures<TPose, Node<TData, TLayer, TPose>>;\n} {\n const { scene, adapter, poseDescriptor, alphaFor, dispatcher, pathEditState, slops, targetScale } = args;\n\n const baseSizes = useTargetBasePx();\n const merged = useMemo(\n () => mergeLayersWithDefaults(layersOverPresets(args.presetLayers, args.layers), targetScale, baseSizes),\n [args.layers, args.presetLayers, targetScale, baseSizes],\n );\n\n const { config: sceneSlot, layers: sceneLayers } = useSceneSlotLayers({\n slot: merged.scene,\n scene,\n adapter,\n alphaFor,\n paintOverrides: args.paintOverrides,\n boundsOf: args.boundsOf,\n previewIds: args.previewIds,\n });\n // Takes the scene-wired slot, so a ghosted derived node still resolves its path.\n const previewLayer = usePreviewGhostLayer<TData, TLayer, TPose>({ scene, sceneSlot, dispatcher });\n const departures = usePoseTransition(scene, args.poseTransition);\n const departureLayer = useDepartureGhostLayer({ scene, sceneSlot, departures });\n const effectSourcesLayer = useEffectSourcesLayer(scene);\n const dispatcherOverlay = useDispatcherOverlayLayer({ dispatcher });\n\n const hitRadiusPx = args.selectTool?.handleHitRadius ?? targetSizesPx(targetScale, baseSizes).handle;\n const edges = args.selectTool?.resize?.edgeHandles ?? true;\n const resizeHandles = useMemo<ResizeHandleLayout>(() => ({ edges, hitRadiusPx }), [edges, hitRadiusPx]);\n\n const chromeAffordancesRef = useRef<readonly Affordance[]>([]);\n const slopsLayer = useMemo(\n () => createSlopsDebugLayer({ getAffordances: () => chromeAffordancesRef.current }),\n [],\n );\n\n const [snapStore] = useState(() => createSnapOutcomeStore());\n // Keyed on the style's values, so an inline `readoutStyle` literal keeps the layer.\n const { color, gapColor, lineWidth } = args.snapReadoutStyle ?? {};\n const snapReadoutLayer = useMemo(() => createSnapReadoutLayer(snapStore, {\n ...(color !== undefined ? { color } : {}),\n ...(gapColor !== undefined ? { gapColor } : {}),\n ...(lineWidth !== undefined ? { lineWidth } : {}),\n }), [snapStore, color, gapColor, lineWidth]);\n const overlaySlot = merged.pathEditingOverlay;\n const overlayConfig = overlaySlot && !isCustomEntry(overlaySlot) ? overlaySlot : undefined;\n const overlayAnchor = overlaySlot === null ? 'dispatcherOverlay' : 'pathEditingOverlay';\n const pathEditingOverlayLayer = usePathEditOverlayLayer(pathEditState, {\n nodeOf: (id) => scene.get(id as NodeId),\n planeOfNode: args.planeOfNode,\n }, overlayConfig);\n\n const { layer: selectionOverlayLayer, rotationBadge } = useSelectionOverlayLayer<TPose>(\n merged.selectionOverlay,\n pathEditState.getEditingIds,\n poseDescriptor,\n args.boundsIncludeStroke ? drawnBoundsScene(scene) : undefined,\n resizeHandles,\n );\n\n const enteredFrameLayer = useEnteredGroupFrameLayer(args.enteredGroup, args.boundsOf, args.frameColor, scene as never);\n const layers = useMemo<CanvasLayers>(() => ({\n ...builtLayersOf(merged),\n scene: sceneLayers,\n selectionOverlay: selectionOverlayLayer ? { layer: selectionOverlayLayer } : null,\n enteredGroupFrame: { layer: enteredFrameLayer, before: 'selectionOverlay' },\n departureGhost: { layer: departureLayer, after: 'scene' },\n previewGhost: { layer: previewLayer, after: 'departureGhost' },\n effectSources: { layer: effectSourcesLayer, after: 'previewGhost' },\n dispatcherOverlay: { layer: dispatcherOverlay, after: 'effectSources' },\n pathEditingOverlay: overlaySlot === null || isCustomEntry(overlaySlot)\n ? overlaySlot\n : { layer: pathEditingOverlayLayer, after: 'selectionOverlay' },\n ...(args.snapReadout ? { snapReadout: { layer: snapReadoutLayer, after: overlayAnchor } } : {}),\n ...(slops ? { slopsDebug: { layer: slopsLayer, after: args.snapReadout ? 'snapReadout' : overlayAnchor } } : {}),\n }), [merged, sceneLayers, selectionOverlayLayer, enteredFrameLayer, departureLayer, previewLayer, effectSourcesLayer, dispatcherOverlay, pathEditingOverlayLayer, overlaySlot, overlayAnchor, snapReadoutLayer, args.snapReadout, slops, slopsLayer,\n ]);\n\n return { layers, rotationBadge, resizeHandles, chromeAffordancesRef, snapStore, departures };\n}\n"],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { paintOverrideSources } from "../paintOverrides.js";
1
2
  import { useMemo } from "react";
2
3
 
3
4
  // src/canvas/SceneCanvas/useSceneRedrawSources.ts
@@ -6,7 +7,8 @@ import { useMemo } from "react";
6
7
  * sources, then the canvas's. Neither a scene write nor an override write
7
8
  * renders the component, and an override write does not bump the scene version
8
9
  * either; nor does a scroll. An animator frame repaints for a `drawOne` reading
9
- * `animator.colorOverrides`.
10
+ * `animator.colorOverrides` or `animator.trimOverrides`, and so does a bare
11
+ * `set` or `clear` on either.
10
12
  */
11
13
  function useSceneRedrawSources(args) {
12
14
  const { scene, heldModifiers, departures } = args;
@@ -18,6 +20,7 @@ function useSceneRedrawSources(args) {
18
20
  ...scene.scroll ? [scene.scroll] : [],
19
21
  heldModifiers,
20
22
  ...animator ? [{ subscribe: (repaint) => animator.onTick(repaint) }] : [],
23
+ ...paintOverrideSources(animator),
21
24
  ...modes ? [modes] : [],
22
25
  departures
23
26
  ], [
@@ -1 +1 @@
1
- {"version":3,"file":"useSceneRedrawSources.js","names":[],"sources":["../../../src/canvas/SceneCanvas/useSceneRedrawSources.ts"],"sourcesContent":["import { useMemo } from 'react';\nimport type { RedrawSource } from '../../core/layers/render';\n\n/**\n * What repaints a `<SceneCanvas>` besides its own render: the caller's\n * sources, then the canvas's. Neither a scene write nor an override write\n * renders the component, and an override write does not bump the scene version\n * either; nor does a scroll. An animator frame repaints for a `drawOne` reading\n * `animator.colorOverrides`.\n */\nexport function useSceneRedrawSources(args: {\n /** `<SceneCanvas>`'s own `redrawOn`, `animator` and `modes`. */\n props: {\n redrawOn?: readonly RedrawSource[];\n animator?: { onTick(listener: () => void): () => void };\n modes?: RedrawSource;\n };\n scene: RedrawSource & { overrides: RedrawSource; scroll?: RedrawSource };\n heldModifiers: RedrawSource;\n /** The ghosts `poseTransition` shows leaving, which move each frame. */\n departures: RedrawSource;\n}): readonly RedrawSource[] {\n const { scene, heldModifiers, departures } = args;\n const { redrawOn: supplied, animator, modes } = args.props;\n return useMemo((): readonly RedrawSource[] => [\n ...(supplied ?? []),\n scene,\n scene.overrides,\n ...(scene.scroll ? [scene.scroll] : []),\n heldModifiers,\n ...(animator ? [{ subscribe: (repaint: () => void) => animator.onTick(repaint) }] : []),\n ...(modes ? [modes] : []),\n departures,\n ], [scene, heldModifiers, animator, modes, supplied, departures]);\n}\n"],"mappings":";;;;;;;;;;AAUA,SAAgB,sBAAsB,MAWV;CAC1B,MAAM,EAAE,OAAO,eAAe,eAAe;CAC7C,MAAM,EAAE,UAAU,UAAU,UAAU,UAAU,KAAK;CACrD,OAAO,cAAuC;EAC5C,GAAI,YAAY,CAAC;EACjB;EACA,MAAM;EACN,GAAI,MAAM,SAAS,CAAC,MAAM,MAAM,IAAI,CAAC;EACrC;EACA,GAAI,WAAW,CAAC,EAAE,YAAY,YAAwB,SAAS,OAAO,OAAO,EAAE,CAAC,IAAI,CAAC;EACrF,GAAI,QAAQ,CAAC,KAAK,IAAI,CAAC;EACvB;CACF,GAAG;EAAC;EAAO;EAAe;EAAU;EAAO;EAAU;CAAU,CAAC;AAClE"}
1
+ {"version":3,"file":"useSceneRedrawSources.js","names":[],"sources":["../../../src/canvas/SceneCanvas/useSceneRedrawSources.ts"],"sourcesContent":["import { useMemo } from 'react';\nimport type { RedrawSource } from '../../core/layers/render';\nimport { paintOverrideSources, type PaintOverrides } from '../paintOverrides';\n\n/**\n * What repaints a `<SceneCanvas>` besides its own render: the caller's\n * sources, then the canvas's. Neither a scene write nor an override write\n * renders the component, and an override write does not bump the scene version\n * either; nor does a scroll. An animator frame repaints for a `drawOne` reading\n * `animator.colorOverrides` or `animator.trimOverrides`, and so does a bare\n * `set` or `clear` on either.\n */\nexport function useSceneRedrawSources(args: {\n /** `<SceneCanvas>`'s own `redrawOn`, `animator` and `modes`. */\n props: {\n redrawOn?: readonly RedrawSource[];\n animator?: { onTick(listener: () => void): () => void } & PaintOverrides;\n modes?: RedrawSource;\n };\n scene: RedrawSource & { overrides: RedrawSource; scroll?: RedrawSource };\n heldModifiers: RedrawSource;\n /** The ghosts `poseTransition` shows leaving, which move each frame. */\n departures: RedrawSource;\n}): readonly RedrawSource[] {\n const { scene, heldModifiers, departures } = args;\n const { redrawOn: supplied, animator, modes } = args.props;\n return useMemo((): readonly RedrawSource[] => [\n ...(supplied ?? []),\n scene,\n scene.overrides,\n ...(scene.scroll ? [scene.scroll] : []),\n heldModifiers,\n ...(animator ? [{ subscribe: (repaint: () => void) => animator.onTick(repaint) }] : []),\n ...paintOverrideSources(animator),\n ...(modes ? [modes] : []),\n departures,\n ], [scene, heldModifiers, animator, modes, supplied, departures]);\n}\n"],"mappings":";;;;;;;;;;;;AAYA,SAAgB,sBAAsB,MAWV;CAC1B,MAAM,EAAE,OAAO,eAAe,eAAe;CAC7C,MAAM,EAAE,UAAU,UAAU,UAAU,UAAU,KAAK;CACrD,OAAO,cAAuC;EAC5C,GAAI,YAAY,CAAC;EACjB;EACA,MAAM;EACN,GAAI,MAAM,SAAS,CAAC,MAAM,MAAM,IAAI,CAAC;EACrC;EACA,GAAI,WAAW,CAAC,EAAE,YAAY,YAAwB,SAAS,OAAO,OAAO,EAAE,CAAC,IAAI,CAAC;EACrF,GAAG,qBAAqB,QAAQ;EAChC,GAAI,QAAQ,CAAC,KAAK,IAAI,CAAC;EACvB;CACF,GAAG;EAAC;EAAO;EAAe;EAAU;EAAO;EAAU;CAAU,CAAC;AAClE"}
@@ -11,15 +11,15 @@ import { useMemo } from "react";
11
11
  * scene layer, so the list follows the scene's layers after mount.
12
12
  */
13
13
  function useSceneSlotLayers(args) {
14
- const { slot, scene, adapter, alphaFor, colorOverrides, boundsOf, previewIds } = args;
14
+ const { slot, scene, adapter, alphaFor, paintOverrides, boundsOf, previewIds } = args;
15
15
  const config = useMemo(() => {
16
16
  if (!slot || isCustomEntry(slot)) return slot;
17
- return wireSceneSlotToScene(slot, scene, alphaFor, colorOverrides);
17
+ return wireSceneSlotToScene(slot, scene, alphaFor, paintOverrides);
18
18
  }, [
19
19
  slot,
20
20
  alphaFor,
21
21
  scene,
22
- colorOverrides
22
+ paintOverrides
23
23
  ]);
24
24
  const sceneLayersKey = useSceneLayerList(adapter);
25
25
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"useSceneSlotLayers.js","names":[],"sources":["../../../src/canvas/SceneCanvas/useSceneSlotLayers.ts"],"sourcesContent":["/**\n * The scene slot, built: the caller's config wired to the scene, and the\n * canvas layers it paints as. A hierarchical scene gets one canvas layer per\n * scene layer, so the list follows the scene's layers after mount.\n */\nimport { useMemo } from 'react';\nimport type { ColorOverrideRegistry } from '@weasel-js/animator';\nimport type { Node, Scene } from 'core/scene/types';\nimport type { Bounds } from 'core/viewport/fitViewToBounds';\nimport { buildSceneLayers, type SceneSlotConfig } from '../buildSceneLayer';\nimport type { HierarchicalAdapter } from '../buildSceneTree';\nimport { isCustomEntry, type LayersMap, type SceneSlotLayers } from '../layerSlots';\nimport { wireSceneSlotToScene } from '../sceneSlotWiring';\nimport { useSceneLayerList } from '../useSceneLayerList';\n\ntype SceneSlot<TData, TLayer extends string, TPose> =\n LayersMap<Node<TData, TLayer, TPose>, TPose>['scene'];\n\nexport function useSceneSlotLayers<TData, TLayer extends string, TPose>(args: {\n /** The slot after `mergeLayersWithDefaults`. */\n slot: SceneSlot<TData, TLayer, TPose>;\n scene: Scene<TData, TLayer, TPose>;\n adapter: HierarchicalAdapter<Node<TData, TLayer, TPose>, TPose>;\n alphaFor: ((id: string) => number) | undefined;\n colorOverrides: ColorOverrideRegistry | undefined;\n /** Committed bounds of a node, for the debug overlay's record of it. */\n boundsOf: (id: string) => Bounds | null;\n /** Ids an in-flight gesture is ghosting; their committed paint is left out\n * so the source does not show through the ghost. */\n previewIds: () => Iterable<string> | null;\n}): {\n /** The scene-wired config, for whatever else paints with its `drawOne`. */\n config: SceneSlot<TData, TLayer, TPose>;\n /** Absent when the slot is empty or carries no `drawOne`; a pre-built layer is the whole slot. */\n layers: SceneSlotLayers | undefined;\n} {\n const { slot, scene, adapter, alphaFor, colorOverrides, boundsOf, previewIds } = args;\n\n const config = useMemo(() => {\n if (!slot || isCustomEntry(slot)) return slot;\n return wireSceneSlotToScene(\n slot as SceneSlotConfig<Node<TData, TLayer, TPose>, TPose>,\n scene,\n alphaFor,\n colorOverrides,\n );\n }, [slot, alphaFor, scene, colorOverrides]);\n\n const sceneLayersKey = useSceneLayerList(adapter);\n const layers = useMemo<SceneSlotLayers | undefined>(() => {\n if (isCustomEntry(config)) return { layers: [{ key: 'scene', layer: config.layer }] };\n if (!config || !config.drawOne) return undefined;\n return {\n layers: buildSceneLayers(config, adapter, boundsOf, () => {\n const ids = new Set(previewIds() ?? []);\n return ids.size > 0 ? ids : null;\n }),\n };\n // `sceneLayersKey` stands for `adapter.getLayers()`, which `buildSceneLayers` reads.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [config, adapter, boundsOf, previewIds, sceneLayersKey]);\n\n return { config, layers };\n}\n"],"mappings":";;;;;;;;;;;;AAkBA,SAAgB,mBAAwD,MAiBtE;CACA,MAAM,EAAE,MAAM,OAAO,SAAS,UAAU,gBAAgB,UAAU,eAAe;CAEjF,MAAM,SAAS,cAAc;EAC3B,IAAI,CAAC,QAAQ,cAAc,IAAI,GAAG,OAAO;EACzC,OAAO,qBACL,MACA,OACA,UACA,cACF;CACF,GAAG;EAAC;EAAM;EAAU;EAAO;CAAc,CAAC;CAE1C,MAAM,iBAAiB,kBAAkB,OAAO;CAchD,OAAO;EAAE;EAAQ,QAbF,cAA2C;GACxD,IAAI,cAAc,MAAM,GAAG,OAAO,EAAE,QAAQ,CAAC;IAAE,KAAK;IAAS,OAAO,OAAO;GAAM,CAAC,EAAE;GACpF,IAAI,CAAC,UAAU,CAAC,OAAO,SAAS,OAAO;GACvC,OAAO,EACL,QAAQ,iBAAiB,QAAQ,SAAS,gBAAgB;IACxD,MAAM,MAAM,IAAI,IAAI,WAAW,KAAK,CAAC,CAAC;IACtC,OAAO,IAAI,OAAO,IAAI,MAAM;GAC9B,CAAC,EACH;EAGF,GAAG;GAAC;GAAQ;GAAS;GAAU;GAAY;EAAc,CAExC;CAAO;AAC1B"}
1
+ {"version":3,"file":"useSceneSlotLayers.js","names":[],"sources":["../../../src/canvas/SceneCanvas/useSceneSlotLayers.ts"],"sourcesContent":["/**\n * The scene slot, built: the caller's config wired to the scene, and the\n * canvas layers it paints as. A hierarchical scene gets one canvas layer per\n * scene layer, so the list follows the scene's layers after mount.\n */\nimport { useMemo } from 'react';\nimport type { PaintOverrides } from '../paintOverrides';\nimport type { Node, Scene } from 'core/scene/types';\nimport type { Bounds } from 'core/viewport/fitViewToBounds';\nimport { buildSceneLayers, type SceneSlotConfig } from '../buildSceneLayer';\nimport type { HierarchicalAdapter } from '../buildSceneTree';\nimport { isCustomEntry, type LayersMap, type SceneSlotLayers } from '../layerSlots';\nimport { wireSceneSlotToScene } from '../sceneSlotWiring';\nimport { useSceneLayerList } from '../useSceneLayerList';\n\ntype SceneSlot<TData, TLayer extends string, TPose> =\n LayersMap<Node<TData, TLayer, TPose>, TPose>['scene'];\n\nexport function useSceneSlotLayers<TData, TLayer extends string, TPose>(args: {\n /** The slot after `mergeLayersWithDefaults`. */\n slot: SceneSlot<TData, TLayer, TPose>;\n scene: Scene<TData, TLayer, TPose>;\n adapter: HierarchicalAdapter<Node<TData, TLayer, TPose>, TPose>;\n alphaFor: ((id: string) => number) | undefined;\n paintOverrides: PaintOverrides | undefined;\n /** Committed bounds of a node, for the debug overlay's record of it. */\n boundsOf: (id: string) => Bounds | null;\n /** Ids an in-flight gesture is ghosting; their committed paint is left out\n * so the source does not show through the ghost. */\n previewIds: () => Iterable<string> | null;\n}): {\n /** The scene-wired config, for whatever else paints with its `drawOne`. */\n config: SceneSlot<TData, TLayer, TPose>;\n /** Absent when the slot is empty or carries no `drawOne`; a pre-built layer is the whole slot. */\n layers: SceneSlotLayers | undefined;\n} {\n const { slot, scene, adapter, alphaFor, paintOverrides, boundsOf, previewIds } = args;\n\n const config = useMemo(() => {\n if (!slot || isCustomEntry(slot)) return slot;\n return wireSceneSlotToScene(\n slot as SceneSlotConfig<Node<TData, TLayer, TPose>, TPose>,\n scene,\n alphaFor,\n paintOverrides,\n );\n }, [slot, alphaFor, scene, paintOverrides]);\n\n const sceneLayersKey = useSceneLayerList(adapter);\n const layers = useMemo<SceneSlotLayers | undefined>(() => {\n if (isCustomEntry(config)) return { layers: [{ key: 'scene', layer: config.layer }] };\n if (!config || !config.drawOne) return undefined;\n return {\n layers: buildSceneLayers(config, adapter, boundsOf, () => {\n const ids = new Set(previewIds() ?? []);\n return ids.size > 0 ? ids : null;\n }),\n };\n // `sceneLayersKey` stands for `adapter.getLayers()`, which `buildSceneLayers` reads.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [config, adapter, boundsOf, previewIds, sceneLayersKey]);\n\n return { config, layers };\n}\n"],"mappings":";;;;;;;;;;;;AAkBA,SAAgB,mBAAwD,MAiBtE;CACA,MAAM,EAAE,MAAM,OAAO,SAAS,UAAU,gBAAgB,UAAU,eAAe;CAEjF,MAAM,SAAS,cAAc;EAC3B,IAAI,CAAC,QAAQ,cAAc,IAAI,GAAG,OAAO;EACzC,OAAO,qBACL,MACA,OACA,UACA,cACF;CACF,GAAG;EAAC;EAAM;EAAU;EAAO;CAAc,CAAC;CAE1C,MAAM,iBAAiB,kBAAkB,OAAO;CAchD,OAAO;EAAE;EAAQ,QAbF,cAA2C;GACxD,IAAI,cAAc,MAAM,GAAG,OAAO,EAAE,QAAQ,CAAC;IAAE,KAAK;IAAS,OAAO,OAAO;GAAM,CAAC,EAAE;GACpF,IAAI,CAAC,UAAU,CAAC,OAAO,SAAS,OAAO;GACvC,OAAO,EACL,QAAQ,iBAAiB,QAAQ,SAAS,gBAAgB;IACxD,MAAM,MAAM,IAAI,IAAI,WAAW,KAAK,CAAC,CAAC;IACtC,OAAO,IAAI,OAAO,IAAI,MAAM;GAC9B,CAAC,EACH;EAGF,GAAG;GAAC;GAAQ;GAAS;GAAU;GAAY;EAAc,CAExC;CAAO;AAC1B"}
@@ -136,7 +136,7 @@ function SceneCanvasInner(props, ref) {
136
136
  enteredGroup: picking.enteredGroup,
137
137
  frameColor,
138
138
  alphaFor: props.alphaFor,
139
- colorOverrides: props.animator?.colorOverrides,
139
+ paintOverrides: props.animator,
140
140
  boundsOf,
141
141
  planeOfNode,
142
142
  poseTransition: props.poseTransition,