@weasel-js/core 1.15.1 → 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.
- package/CHANGELOG.md +170 -0
- package/dist/animation/poseTarget.js +3 -3
- package/dist/animation/poseTarget.js.map +1 -1
- package/dist/animation/reflow.js +7 -7
- package/dist/animation/reflow.js.map +1 -1
- package/dist/animation/rig/rigNode.js +2 -2
- package/dist/animation/rig/rigNode.js.map +1 -1
- package/dist/barrel/input.js +2 -1
- package/dist/barrel/paint.js +2 -2
- package/dist/barrel/viewport.js +3 -3
- package/dist/canvas/NodeShape.js +23 -10
- package/dist/canvas/NodeShape.js.map +1 -1
- package/dist/canvas/SceneCanvas/PointerProviderIfRoot.js +17 -64
- package/dist/canvas/SceneCanvas/PointerProviderIfRoot.js.map +1 -1
- package/dist/canvas/SceneCanvas/props.js.map +1 -1
- package/dist/canvas/SceneCanvas/useSceneCanvasLayers.js +1 -1
- package/dist/canvas/SceneCanvas/useSceneCanvasLayers.js.map +1 -1
- package/dist/canvas/SceneCanvas/useSceneRedrawSources.js +4 -1
- package/dist/canvas/SceneCanvas/useSceneRedrawSources.js.map +1 -1
- package/dist/canvas/SceneCanvas/useSceneSlotLayers.js +3 -3
- package/dist/canvas/SceneCanvas/useSceneSlotLayers.js.map +1 -1
- package/dist/canvas/SceneCanvas.js +1 -1
- package/dist/canvas/SceneCanvas.js.map +1 -1
- package/dist/canvas/SceneViewCanvas.js +4 -1
- package/dist/canvas/SceneViewCanvas.js.map +1 -1
- package/dist/canvas/accentColor.js +3 -3
- package/dist/canvas/accentColor.js.map +1 -1
- package/dist/canvas/deps/enteredGroup.js +5 -4
- package/dist/canvas/deps/enteredGroup.js.map +1 -1
- package/dist/canvas/deps/selectionSnapshot.js +7 -2
- package/dist/canvas/deps/selectionSnapshot.js.map +1 -1
- package/dist/canvas/nodeShapes/derivedPainter.js +57 -8
- package/dist/canvas/nodeShapes/derivedPainter.js.map +1 -1
- package/dist/canvas/nodeShapes/nodePaint.js +33 -2
- package/dist/canvas/nodeShapes/nodePaint.js.map +1 -1
- package/dist/canvas/nodeShapes/pathPainter.js +5 -4
- package/dist/canvas/nodeShapes/pathPainter.js.map +1 -1
- package/dist/canvas/nodeShapes/pickOutline.js +48 -0
- package/dist/canvas/nodeShapes/pickOutline.js.map +1 -0
- package/dist/canvas/nodeShapes/shapePainter.js +6 -4
- package/dist/canvas/nodeShapes/shapePainter.js.map +1 -1
- package/dist/canvas/paintOverrides.js +43 -0
- package/dist/canvas/paintOverrides.js.map +1 -0
- package/dist/canvas/renderSceneToPixels.js +1 -1
- package/dist/canvas/renderSceneToPixels.js.map +1 -1
- package/dist/canvas/sceneSlotWiring.js +3 -3
- package/dist/canvas/sceneSlotWiring.js.map +1 -1
- package/dist/canvas/sceneViewRender.js +11 -7
- package/dist/canvas/sceneViewRender.js.map +1 -1
- package/dist/canvas/slopsDebugLayer.js +8 -2
- package/dist/canvas/slopsDebugLayer.js.map +1 -1
- package/dist/canvas/trimOverrides.js +18 -0
- package/dist/canvas/trimOverrides.js.map +1 -0
- package/dist/canvas/useViewRegistrySurface.js +6 -3
- package/dist/canvas/useViewRegistrySurface.js.map +1 -1
- package/dist/canvas/viewRegistry.js +5 -5
- package/dist/canvas/viewRegistry.js.map +1 -1
- package/dist/core/ops/reorder/index.js +2 -6
- package/dist/core/ops/reorder/index.js.map +1 -1
- package/dist/core/pages/fitViewToPage.js +2 -7
- package/dist/core/pages/fitViewToPage.js.map +1 -1
- package/dist/core/pages/pages.js +3 -9
- package/dist/core/pages/pages.js.map +1 -1
- package/dist/core/paint/meshKindLazy.js +13 -6
- package/dist/core/paint/meshKindLazy.js.map +1 -1
- package/dist/core/scene/pathEffects/blend.js +2 -2
- package/dist/core/scene/pathEffects/blend.js.map +1 -1
- package/dist/core/scene/pathEffects/textAlong.js +6 -5
- package/dist/core/scene/pathEffects/textAlong.js.map +1 -1
- package/dist/core/scene/scene.js +12 -1
- package/dist/core/scene/scene.js.map +1 -1
- package/dist/core/viewport/clientToWorld.js +7 -1
- package/dist/core/viewport/clientToWorld.js.map +1 -1
- package/dist/core/viewport/zoomAt.js +6 -6
- package/dist/core/viewport/zoomAt.js.map +1 -1
- package/dist/devtools/root.js +11 -4
- package/dist/devtools/root.js.map +1 -1
- package/dist/devtools/transport.js +12 -16
- package/dist/devtools/transport.js.map +1 -1
- package/dist/devtools-protocol.d.ts +0 -11
- package/dist/domains/measure/measureHit.js +2 -8
- package/dist/domains/measure/measureHit.js.map +1 -1
- package/dist/domains/paths/anchorMarkers.js +5 -11
- package/dist/domains/paths/anchorMarkers.js.map +1 -1
- package/dist/domains/paths/pathEditingOverlayLayer.js +5 -9
- package/dist/domains/paths/pathEditingOverlayLayer.js.map +1 -1
- package/dist/domains/paths/pathLayer.js +20 -6
- package/dist/domains/paths/pathLayer.js.map +1 -1
- package/dist/domains/paths/penPreviewLayer.js +9 -12
- package/dist/domains/paths/penPreviewLayer.js.map +1 -1
- package/dist/domains/paths/shapeGenerator.js +1 -2
- package/dist/domains/paths/shapeGenerator.js.map +1 -1
- package/dist/domains/snap/angle2d.js +2 -1
- package/dist/domains/snap/angle2d.js.map +1 -1
- package/dist/domains/snap/outcomeStore.js +5 -8
- package/dist/domains/snap/outcomeStore.js.map +1 -1
- package/dist/domains/text/useSceneTextEdit.js +9 -4
- package/dist/domains/text/useSceneTextEdit.js.map +1 -1
- package/dist/domains/viewports/aim.js +3 -8
- package/dist/domains/viewports/aim.js.map +1 -1
- package/dist/domains/viewports/viewResolver.js +5 -3
- package/dist/domains/viewports/viewResolver.js.map +1 -1
- package/dist/index.d.ts +66 -50
- package/dist/index.js +5 -4
- package/dist/input/usePublishPointer.js +59 -0
- package/dist/input/usePublishPointer.js.map +1 -0
- package/dist/interactions/actions/defaults/containerScroll.js +9 -3
- package/dist/interactions/actions/defaults/containerScroll.js.map +1 -1
- package/dist/interactions/actions/defaults/enterGroup.js +3 -3
- package/dist/interactions/actions/defaults/enterGroup.js.map +1 -1
- package/dist/interactions/actions/defaults/pathEditMarks.js +2 -5
- package/dist/interactions/actions/defaults/pathEditMarks.js.map +1 -1
- package/dist/interactions/actions/edit-anchors/handleDrag.js +4 -7
- package/dist/interactions/actions/edit-anchors/handleDrag.js.map +1 -1
- package/dist/interactions/actions/move/behaviors/clampMove.js +42 -0
- package/dist/interactions/actions/move/behaviors/clampMove.js.map +1 -0
- package/dist/interactions/actions/move/layout/container.js +3 -6
- package/dist/interactions/actions/move/layout/container.js.map +1 -1
- package/dist/interactions/actions/move/layout/exits.js +2 -2
- package/dist/interactions/actions/move/layout/exits.js.map +1 -1
- package/dist/interactions/actions/transform-again/memory.js +2 -2
- package/dist/interactions/actions/transform-again/memory.js.map +1 -1
- package/dist/move.d.ts +18 -3
- package/dist/move.js +2 -1
- package/dist/version.js +1 -1
- package/package.json +25 -25
- package/dist/canvas/colorOverrides.js +0 -23
- package/dist/canvas/colorOverrides.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pathPainter.js","names":[],"sources":["../../../src/canvas/nodeShapes/pathPainter.ts"],"sourcesContent":["import type {\n Path\n} from '@weasel-js/geom';\nimport type { Stroke, Fill, Inherit } from '@weasel-js/paint';\nimport type { NodeShapeEntry } from 'canvas/NodeShape';\nimport { nodeMemo } from 'core/scene/nodeMemo';\nimport type { RectPose } from 'core/scene/types';\nimport { pathInPoseFrame } from '@weasel-js/geom';\nimport { commandFill, commandStroke } from '@weasel-js/renderer';\nimport type { DrawCommand } from '@weasel-js/renderer';\nimport { DERIVED_PAINTER, derivedDrawnBounds } from './derivedPainter';\nimport { PAINT_SLOT } from './memo';\nimport { inkReach, pathPaintOf, withAnimatedVertexColors } from './nodePaint';\nimport { resolveStroke } from '@weasel-js/paint';\nimport { paintsPose, poseBoxBounds } from './poseBox';\nimport { strokedPoseBox } from './strokedBounds';\n\ntype PathData = { fill?: Fill | Inherit | null; stroke?: Stroke | Inherit | null; vertexColors?: number[] };\n\n// ─── Built-in painters ─────────────────────────────────────────────────\nexport const PATH_PAINTER: NodeShapeEntry<unknown, RectPose> = {\n id: 'kit:path',\n matches: (node) => {\n if (!paintsPose(node.pose)) return false;\n const d = node.data as { path?: Path } | null;\n return d?.path != null;\n },\n // Memoized: a pure function of `(data, pose)`, and `pathInPoseFrame`\n // allocates on every call but the identity (`kind: 'rect'`) branch. See\n // PAINT_SLOT.\n paint: (node, pose, ctx) => ctx?.derivedPath != null\n // A pose override's path stands in for `data.path`, already where it is drawn.\n ? DERIVED_PAINTER.paint(node, pose, ctx)\n : withAnimatedVertexColors(nodeMemo(node, PAINT_SLOT, pose, () => {\n const d = node.data as PathData & { path: Path };\n const projected = pathInPoseFrame(d.path, pose);\n const { fill: declared, stroke: strokeSpec } = pathPaintOf(d, ctx?.inherited);\n // The pose is baked into `projected` rather than emitted as a transform,\n // so a box-relative gradient has to be baked onto the same box here or\n // it would arrive in the renderer referring to a frame that never exists.\n const fill = declared && commandFill(declared, pose);\n const stroke = strokeSpec && commandStroke(strokeSpec, pose);\n const cmd: DrawCommand = {\n kind: 'path',\n path: projected,\n ...(fill ? { fill } : {}),\n ...(fill && d.vertexColors ? { vertexColors: d.vertexColors } : {}),\n ...(stroke ? { stroke } : {}),\n };\n return [cmd];\n }, ctx?.inherited), ctx),\n silhouette: (node, pose, ctx) => {\n if (ctx?.derivedPath != null) return ctx.derivedPath;\n const d = node.data as { path: Path };\n return pathInPoseFrame(d.path, pose);\n },\n ink: (node, _pose, ctx) => {\n const { fill, stroke } = pathPaintOf(node.data as PathData, ctx?.inherited);\n return { filled: fill !== null, ...inkReach(stroke ?? null, ctx?.scale) };\n },\n // `pathInPoseFrame` fits the path's own bounds to the pose box.\n bounds: (node, pose, ctx) =>\n poseBoxBounds(pose, resolveStroke((node.data as PathData).stroke, ctx?.inherited?.stroke), ctx),\n drawnBounds: (node, pose, ctx) => {\n if (ctx?.derivedPath != null) return derivedDrawnBounds(node, ctx);\n const d = node.data as PathData & { path: Path };\n return strokedPoseBox(node, pose, resolveStroke(d.stroke, ctx?.inherited?.stroke), ctx, (at) => pathInPoseFrame(d.path, at));\n },\n};\n"],"mappings":";;;;;;;;;;;AAoBA,IAAa,eAAkD;CAC7D,IAAI;CACJ,UAAU,SAAS;EACjB,IAAI,CAAC,WAAW,KAAK,IAAI,GAAG,OAAO;EAEnC,OADU,KAAK,MACL,QAAQ;CACpB;CAIA,QAAQ,MAAM,MAAM,QAAQ,KAAK,eAAe,OAE5C,gBAAgB,MAAM,MAAM,MAAM,GAAG,IACrC,yBAAyB,SAAS,MAAM,YAAY,YAAY;
|
|
1
|
+
{"version":3,"file":"pathPainter.js","names":[],"sources":["../../../src/canvas/nodeShapes/pathPainter.ts"],"sourcesContent":["import type {\n Path\n} from '@weasel-js/geom';\nimport type { Stroke, Fill, Inherit } from '@weasel-js/paint';\nimport type { NodeShapeEntry } from 'canvas/NodeShape';\nimport { nodeMemo } from 'core/scene/nodeMemo';\nimport type { RectPose } from 'core/scene/types';\nimport { pathInPoseFrame } from '@weasel-js/geom';\nimport { commandFill, commandStroke } from '@weasel-js/renderer';\nimport type { DrawCommand } from '@weasel-js/renderer';\nimport { DERIVED_PAINTER, derivedDrawnBounds } from './derivedPainter';\nimport { PAINT_SLOT } from './memo';\nimport { inkReach, inkTrim, pathPaintOf, withAnimatedTrim, withAnimatedVertexColors } from './nodePaint';\nimport { resolveStroke } from '@weasel-js/paint';\nimport { paintsPose, poseBoxBounds } from './poseBox';\nimport { strokedPoseBox } from './strokedBounds';\n\ntype PathData = { fill?: Fill | Inherit | null; stroke?: Stroke | Inherit | null; vertexColors?: number[] };\n\n// ─── Built-in painters ─────────────────────────────────────────────────\nexport const PATH_PAINTER: NodeShapeEntry<unknown, RectPose> = {\n id: 'kit:path',\n matches: (node) => {\n if (!paintsPose(node.pose)) return false;\n const d = node.data as { path?: Path } | null;\n return d?.path != null;\n },\n // Memoized: a pure function of `(data, pose)`, and `pathInPoseFrame`\n // allocates on every call but the identity (`kind: 'rect'`) branch. See\n // PAINT_SLOT.\n paint: (node, pose, ctx) => ctx?.derivedPath != null\n // A pose override's path stands in for `data.path`, already where it is drawn.\n ? DERIVED_PAINTER.paint(node, pose, ctx)\n : withAnimatedTrim(withAnimatedVertexColors(nodeMemo(node, PAINT_SLOT, pose, () => {\n const d = node.data as PathData & { path: Path };\n const projected = pathInPoseFrame(d.path, pose);\n const { fill: declared, stroke: strokeSpec } = pathPaintOf(d, ctx?.inherited);\n // The pose is baked into `projected` rather than emitted as a transform,\n // so a box-relative gradient has to be baked onto the same box here or\n // it would arrive in the renderer referring to a frame that never exists.\n const fill = declared && commandFill(declared, pose);\n const stroke = strokeSpec && commandStroke(strokeSpec, pose);\n const cmd: DrawCommand = {\n kind: 'path',\n path: projected,\n ...(fill ? { fill } : {}),\n ...(fill && d.vertexColors ? { vertexColors: d.vertexColors } : {}),\n ...(stroke ? { stroke } : {}),\n };\n return [cmd];\n }, ctx?.inherited), ctx), ctx),\n silhouette: (node, pose, ctx) => {\n if (ctx?.derivedPath != null) return ctx.derivedPath;\n const d = node.data as { path: Path };\n return pathInPoseFrame(d.path, pose);\n },\n ink: (node, _pose, ctx) => {\n const { fill, stroke } = pathPaintOf(node.data as PathData, ctx?.inherited);\n return { filled: fill !== null, ...inkReach(stroke ?? null, ctx?.scale), ...inkTrim(stroke ?? null) };\n },\n // `pathInPoseFrame` fits the path's own bounds to the pose box.\n bounds: (node, pose, ctx) =>\n poseBoxBounds(pose, resolveStroke((node.data as PathData).stroke, ctx?.inherited?.stroke), ctx),\n drawnBounds: (node, pose, ctx) => {\n if (ctx?.derivedPath != null) return derivedDrawnBounds(node, ctx);\n const d = node.data as PathData & { path: Path };\n return strokedPoseBox(node, pose, resolveStroke(d.stroke, ctx?.inherited?.stroke), ctx, (at) => pathInPoseFrame(d.path, at));\n },\n};\n"],"mappings":";;;;;;;;;;;AAoBA,IAAa,eAAkD;CAC7D,IAAI;CACJ,UAAU,SAAS;EACjB,IAAI,CAAC,WAAW,KAAK,IAAI,GAAG,OAAO;EAEnC,OADU,KAAK,MACL,QAAQ;CACpB;CAIA,QAAQ,MAAM,MAAM,QAAQ,KAAK,eAAe,OAE5C,gBAAgB,MAAM,MAAM,MAAM,GAAG,IACrC,iBAAiB,yBAAyB,SAAS,MAAM,YAAY,YAAY;EACnF,MAAM,IAAI,KAAK;EACf,MAAM,YAAY,gBAAgB,EAAE,MAAM,IAAI;EAC9C,MAAM,EAAE,MAAM,UAAU,QAAQ,eAAe,YAAY,GAAG,KAAK,SAAS;EAI5E,MAAM,OAAO,YAAY,YAAY,UAAU,IAAI;EACnD,MAAM,SAAS,cAAc,cAAc,YAAY,IAAI;EAQ3D,OAAO,CAAC;GANN,MAAM;GACN,MAAM;GACN,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC;GACvB,GAAI,QAAQ,EAAE,eAAe,EAAE,cAAc,EAAE,aAAa,IAAI,CAAC;GACjE,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;EAErB,CAAG;CACb,GAAG,KAAK,SAAS,GAAG,GAAG,GAAG,GAAG;CAC7B,aAAa,MAAM,MAAM,QAAQ;EAC/B,IAAI,KAAK,eAAe,MAAM,OAAO,IAAI;EACzC,MAAM,IAAI,KAAK;EACf,OAAO,gBAAgB,EAAE,MAAM,IAAI;CACrC;CACA,MAAM,MAAM,OAAO,QAAQ;EACzB,MAAM,EAAE,MAAM,WAAW,YAAY,KAAK,MAAkB,KAAK,SAAS;EAC1E,OAAO;GAAE,QAAQ,SAAS;GAAM,GAAG,SAAS,UAAU,MAAM,KAAK,KAAK;GAAG,GAAG,QAAQ,UAAU,IAAI;EAAE;CACtG;CAEA,SAAS,MAAM,MAAM,QACnB,cAAc,MAAM,cAAe,KAAK,KAAkB,QAAQ,KAAK,WAAW,MAAM,GAAG,GAAG;CAChG,cAAc,MAAM,MAAM,QAAQ;EAChC,IAAI,KAAK,eAAe,MAAM,OAAO,mBAAmB,MAAM,GAAG;EACjE,MAAM,IAAI,KAAK;EACf,OAAO,eAAe,MAAM,MAAM,cAAc,EAAE,QAAQ,KAAK,WAAW,MAAM,GAAG,MAAM,OAAO,gBAAgB,EAAE,MAAM,EAAE,CAAC;CAC7H;AACF"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { hasSlop, slopForStrokeHit, widenSlop } from "../../core/viewport/screenSlop.js";
|
|
2
|
+
import { asPolygonPath, pathContainsPoint, pathThroughPoseFrame, poseFrameOf, strokeHitTest } from "@weasel-js/geom";
|
|
3
|
+
import { trimSpan } from "@weasel-js/stroke";
|
|
4
|
+
|
|
5
|
+
// src/canvas/nodeShapes/pickOutline.ts
|
|
6
|
+
/**
|
|
7
|
+
* Whether one painted outline covers a world point: its fill, and its
|
|
8
|
+
* stroke's reach, over only the stretch a trim draws.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Where `trim` draws along `local`, the outline as the painter strokes it, once
|
|
12
|
+
* the pose frame carries it to `world`. The ribbon measures before the frame,
|
|
13
|
+
* so the span is resolved on `local`; its curve positions name the same points
|
|
14
|
+
* on the world outline, which the frame carries command for command, rounding
|
|
15
|
+
* corners first wherever it does not keep their shape.
|
|
16
|
+
*/
|
|
17
|
+
function trimmedOutline(local, world, pose, trim) {
|
|
18
|
+
return {
|
|
19
|
+
drawn: poseFrameOf(pose) === null ? world : pathThroughPoseFrame(asPolygonPath(local), pose),
|
|
20
|
+
span: trimSpan(local, trim)
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Does the outline `fill` (world), inked as `ink`, cover (x, y)? The stroke
|
|
25
|
+
* reaches a different distance on each side, so which side the point is on
|
|
26
|
+
* picks the reach; `tolerance` also makes a filled shape grabbable a few
|
|
27
|
+
* pixels outside its edge. `trimmed` limits the stroke's reach to its span.
|
|
28
|
+
*/
|
|
29
|
+
function inkCovers(fill, ink, trimmed, x, y, opts) {
|
|
30
|
+
const inside = pathContainsPoint(fill, x, y);
|
|
31
|
+
if (ink.filled && inside) return true;
|
|
32
|
+
const slop = widenSlop(opts.tolerance, (inside ? ink.insetPx : ink.outsetPx) ?? 0, opts.scale);
|
|
33
|
+
const reach = inside ? ink.inset : ink.outset;
|
|
34
|
+
if (trimmed === void 0) return outlineWithin(fill, x, y, reach, slop);
|
|
35
|
+
if (trimmed.span && outlineWithin(trimmed.drawn, x, y, reach, slop, trimmed.span)) return true;
|
|
36
|
+
return ink.filled && outlineWithin(fill, x, y, 0, opts.tolerance);
|
|
37
|
+
}
|
|
38
|
+
function outlineWithin(path, x, y, reach, slop, span) {
|
|
39
|
+
const { extra, opts: hit } = slopForStrokeHit(slop);
|
|
40
|
+
const r = reach + extra;
|
|
41
|
+
return (r > 0 || hasSlop(slop)) && strokeHitTest(path, x, y, r, span ? {
|
|
42
|
+
...hit,
|
|
43
|
+
span
|
|
44
|
+
} : hit);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export { inkCovers, trimmedOutline };
|
|
48
|
+
//# sourceMappingURL=pickOutline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pickOutline.js","names":[],"sources":["../../../src/canvas/nodeShapes/pickOutline.ts"],"sourcesContent":["/**\n * Whether one painted outline covers a world point: its fill, and its\n * stroke's reach, over only the stretch a trim draws.\n */\nimport {\n asPolygonPath, pathContainsPoint, pathThroughPoseFrame, poseFrameOf, strokeHitTest,\n type CurveSpan, type Path,\n} from '@weasel-js/geom';\nimport type { StrokeTrim } from '@weasel-js/paint';\nimport { trimSpan } from '@weasel-js/stroke';\nimport { hasSlop, slopForStrokeHit, widenSlop, type PickSlop } from 'core/viewport/screenSlop';\nimport type { NodeInk } from '../nodeShapeTypes';\n\n/** A trimmed stroke's outline in world, and the stretch of it that is drawn; `span` null draws nothing. */\nexport interface TrimmedOutline {\n drawn: Path;\n span: CurveSpan | null;\n}\n\n/** What a pick reads besides the point. */\nexport interface PickReach {\n tolerance?: PickSlop;\n scale?: number;\n}\n\n/**\n * Where `trim` draws along `local`, the outline as the painter strokes it, once\n * the pose frame carries it to `world`. The ribbon measures before the frame,\n * so the span is resolved on `local`; its curve positions name the same points\n * on the world outline, which the frame carries command for command, rounding\n * corners first wherever it does not keep their shape.\n */\nexport function trimmedOutline(local: Path, world: Path, pose: unknown, trim: StrokeTrim): TrimmedOutline {\n // A rect goes through as a polygon: a mirrored rect would come out restarted.\n const drawn = poseFrameOf(pose) === null ? world : pathThroughPoseFrame(asPolygonPath(local), pose);\n return { drawn, span: trimSpan(local, trim) };\n}\n\n/**\n * Does the outline `fill` (world), inked as `ink`, cover (x, y)? The stroke\n * 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\n * pixels outside its edge. `trimmed` limits the stroke's reach to its span.\n */\nexport function inkCovers(\n fill: Path, ink: NodeInk, trimmed: TrimmedOutline | undefined, x: number, y: number, opts: PickReach,\n): boolean {\n const inside = pathContainsPoint(fill, x, y);\n if (ink.filled && inside) return true;\n const slop = widenSlop(opts.tolerance, (inside ? ink.insetPx : ink.outsetPx) ?? 0, opts.scale);\n const reach = inside ? ink.inset : ink.outset;\n if (trimmed === undefined) return outlineWithin(fill, x, y, reach, slop);\n if (trimmed.span && outlineWithin(trimmed.drawn, x, y, reach, slop, trimmed.span)) return true;\n // A fill's edge is grabbable by the tolerance alone, all the way round.\n return ink.filled && outlineWithin(fill, x, y, 0, opts.tolerance);\n}\n\nfunction outlineWithin(\n path: Path, x: number, y: number, reach: number, slop: PickSlop | undefined, span?: CurveSpan,\n): boolean {\n const { extra, opts: hit } = slopForStrokeHit(slop);\n const r = reach + extra;\n return (r > 0 || hasSlop(slop)) && strokeHitTest(path, x, y, r, span ? { ...hit, span } : hit);\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAgCA,SAAgB,eAAe,OAAa,OAAa,MAAe,MAAkC;CAGxG,OAAO;EAAE,OADK,YAAY,IAAI,MAAM,OAAO,QAAQ,qBAAqB,cAAc,KAAK,GAAG,IAAI;EAClF,MAAM,SAAS,OAAO,IAAI;CAAE;AAC9C;;;;;;;AAQA,SAAgB,UACd,MAAY,KAAc,SAAqC,GAAW,GAAW,MAC5E;CACT,MAAM,SAAS,kBAAkB,MAAM,GAAG,CAAC;CAC3C,IAAI,IAAI,UAAU,QAAQ,OAAO;CACjC,MAAM,OAAO,UAAU,KAAK,YAAY,SAAS,IAAI,UAAU,IAAI,aAAa,GAAG,KAAK,KAAK;CAC7F,MAAM,QAAQ,SAAS,IAAI,QAAQ,IAAI;CACvC,IAAI,YAAY,QAAW,OAAO,cAAc,MAAM,GAAG,GAAG,OAAO,IAAI;CACvE,IAAI,QAAQ,QAAQ,cAAc,QAAQ,OAAO,GAAG,GAAG,OAAO,MAAM,QAAQ,IAAI,GAAG,OAAO;CAE1F,OAAO,IAAI,UAAU,cAAc,MAAM,GAAG,GAAG,GAAG,KAAK,SAAS;AAClE;AAEA,SAAS,cACP,MAAY,GAAW,GAAW,OAAe,MAA4B,MACpE;CACT,MAAM,EAAE,OAAO,MAAM,QAAQ,iBAAiB,IAAI;CAClD,MAAM,IAAI,QAAQ;CAClB,QAAQ,IAAI,KAAK,QAAQ,IAAI,MAAM,cAAc,MAAM,GAAG,GAAG,GAAG,OAAO;EAAE,GAAG;EAAK;CAAK,IAAI,GAAG;AAC/F"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { nodeMemo } from "../../core/scene/nodeMemo.js";
|
|
2
2
|
import { PAINT_SLOT } from "./memo.js";
|
|
3
|
-
import { inkReach, withAnimatedVertexColors } from "./nodePaint.js";
|
|
3
|
+
import { inkReach, inkTrim, withAnimatedTrim, withAnimatedVertexColors } from "./nodePaint.js";
|
|
4
4
|
import { paintsPose, poseBoxBounds } from "./poseBox.js";
|
|
5
5
|
import { outsetPoseBox, strokedPoseBox } from "./strokedBounds.js";
|
|
6
6
|
import { commandFill, commandStroke } from "@weasel-js/renderer";
|
|
@@ -19,7 +19,7 @@ var SHAPE_PAINTER = {
|
|
|
19
19
|
const s = node.data?.shape;
|
|
20
20
|
return s != null && SHAPE_KINDS.has(s);
|
|
21
21
|
},
|
|
22
|
-
paint: (node, pose, ctx) => withAnimatedVertexColors(nodeMemo(node, PAINT_SLOT, pose, () => {
|
|
22
|
+
paint: (node, pose, ctx) => withAnimatedTrim(withAnimatedVertexColors(nodeMemo(node, PAINT_SLOT, pose, () => {
|
|
23
23
|
const d = node.data;
|
|
24
24
|
const path = pathForShape(d, pose);
|
|
25
25
|
const declaredFill = resolveFill(d.fill, ctx?.inherited?.fill);
|
|
@@ -32,16 +32,18 @@ var SHAPE_PAINTER = {
|
|
|
32
32
|
...shapeFill ? { fill: shapeFill } : {},
|
|
33
33
|
...stroke ? { stroke } : {}
|
|
34
34
|
}];
|
|
35
|
-
}, ctx?.inherited), ctx),
|
|
35
|
+
}, ctx?.inherited), ctx), ctx),
|
|
36
36
|
silhouette: (node, pose) => {
|
|
37
37
|
const d = node.data;
|
|
38
38
|
return pathForShape(d, pose);
|
|
39
39
|
},
|
|
40
40
|
ink: (node, _pose, ctx) => {
|
|
41
41
|
const d = node.data;
|
|
42
|
+
const stroke = resolveStroke(d.stroke, ctx?.inherited?.stroke);
|
|
42
43
|
return {
|
|
43
44
|
filled: resolveFill(d.fill, ctx?.inherited?.fill) !== null,
|
|
44
|
-
...inkReach(
|
|
45
|
+
...inkReach(stroke, ctx?.scale),
|
|
46
|
+
...inkTrim(stroke)
|
|
45
47
|
};
|
|
46
48
|
},
|
|
47
49
|
bounds: (node, pose, ctx) => poseBoxBounds(pose, resolveStroke(node.data.stroke, ctx?.inherited?.stroke), ctx),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shapePainter.js","names":[],"sources":["../../../src/canvas/nodeShapes/shapePainter.ts"],"sourcesContent":["import {\n ellipsePath,\n regularPolygonPath,\n starPath,\n type Path\n} from '@weasel-js/geom';\nimport type { Stroke, Fill, Inherit } from '@weasel-js/paint';\nimport type { NodeShapeEntry } from 'canvas/NodeShape';\nimport { nodeMemo } from 'core/scene/nodeMemo';\nimport type { RectPose } from 'core/scene/types';\nimport { commandFill, commandStroke } from '@weasel-js/renderer';\nimport { PAINT_SLOT } from './memo';\nimport { inkReach, withAnimatedVertexColors } from './nodePaint';\nimport { resolveFill, resolveStroke } from '@weasel-js/paint';\nimport { paintsPose, poseBoxBounds } from './poseBox';\nimport { outsetPoseBox, strokedPoseBox } from './strokedBounds';\n\ntype ShapeData = {\n shape: string;\n fill?: Fill | Inherit | null;\n stroke?: Stroke | Inherit | null;\n sides?: number;\n points?: number;\n};\n\n// ─── Built-in painters ─────────────────────────────────────────────────\n/** Built-in shape dispatcher — matches when `data.shape` names a kit-known\n * shape kind. Computes both paint and silhouette from the pose so consumer\n * scenes can declare clipping/rendering directly in JSON without\n * registering a custom painter. */\nexport const SHAPE_PAINTER: NodeShapeEntry<unknown, RectPose> = {\n id: 'kit:shape',\n matches: (node) => {\n if (!paintsPose(node.pose)) return false;\n const s = (node.data as { shape?: string } | null)?.shape;\n return s != null && SHAPE_KINDS.has(s);\n },\n // Memoized: a pure function of `(data, pose)`, and `pathForShape` builds a\n // fresh `Uint8Array` + `Float32Array` for every ellipse, polygon and star.\n // See PAINT_SLOT.\n paint: (node, pose, ctx) => withAnimatedVertexColors(nodeMemo(node, PAINT_SLOT, pose, () => {\n const d = node.data as ShapeData;\n const path = pathForShape(d, pose);\n const declaredFill = resolveFill(d.fill, ctx?.inherited?.fill);\n const shapeFill = declaredFill && commandFill(declaredFill, pose);\n const strokeSpec = resolveStroke(d.stroke, ctx?.inherited?.stroke);\n const stroke = strokeSpec && commandStroke(strokeSpec, pose);\n return [{\n kind: 'path',\n path,\n ...(shapeFill ? { fill: shapeFill } : {}),\n ...(stroke ? { stroke } : {}),\n }];\n }, ctx?.inherited), ctx),\n silhouette: (node, pose) => {\n const d = node.data as { shape: string; sides?: number; points?: number };\n return pathForShape(d, pose);\n },\n ink: (node, _pose, ctx) => {\n const d = node.data as ShapeData;\n return {\n filled: resolveFill(d.fill, ctx?.inherited?.fill) !== null,\n ...inkReach(
|
|
1
|
+
{"version":3,"file":"shapePainter.js","names":[],"sources":["../../../src/canvas/nodeShapes/shapePainter.ts"],"sourcesContent":["import {\n ellipsePath,\n regularPolygonPath,\n starPath,\n type Path\n} from '@weasel-js/geom';\nimport type { Stroke, Fill, Inherit } from '@weasel-js/paint';\nimport type { NodeShapeEntry } from 'canvas/NodeShape';\nimport { nodeMemo } from 'core/scene/nodeMemo';\nimport type { RectPose } from 'core/scene/types';\nimport { commandFill, commandStroke } from '@weasel-js/renderer';\nimport { PAINT_SLOT } from './memo';\nimport { inkReach, inkTrim, withAnimatedTrim, withAnimatedVertexColors } from './nodePaint';\nimport { resolveFill, resolveStroke } from '@weasel-js/paint';\nimport { paintsPose, poseBoxBounds } from './poseBox';\nimport { outsetPoseBox, strokedPoseBox } from './strokedBounds';\n\ntype ShapeData = {\n shape: string;\n fill?: Fill | Inherit | null;\n stroke?: Stroke | Inherit | null;\n sides?: number;\n points?: number;\n};\n\n// ─── Built-in painters ─────────────────────────────────────────────────\n/** Built-in shape dispatcher — matches when `data.shape` names a kit-known\n * shape kind. Computes both paint and silhouette from the pose so consumer\n * scenes can declare clipping/rendering directly in JSON without\n * registering a custom painter. */\nexport const SHAPE_PAINTER: NodeShapeEntry<unknown, RectPose> = {\n id: 'kit:shape',\n matches: (node) => {\n if (!paintsPose(node.pose)) return false;\n const s = (node.data as { shape?: string } | null)?.shape;\n return s != null && SHAPE_KINDS.has(s);\n },\n // Memoized: a pure function of `(data, pose)`, and `pathForShape` builds a\n // fresh `Uint8Array` + `Float32Array` for every ellipse, polygon and star.\n // See PAINT_SLOT.\n paint: (node, pose, ctx) => withAnimatedTrim(withAnimatedVertexColors(nodeMemo(node, PAINT_SLOT, pose, () => {\n const d = node.data as ShapeData;\n const path = pathForShape(d, pose);\n const declaredFill = resolveFill(d.fill, ctx?.inherited?.fill);\n const shapeFill = declaredFill && commandFill(declaredFill, pose);\n const strokeSpec = resolveStroke(d.stroke, ctx?.inherited?.stroke);\n const stroke = strokeSpec && commandStroke(strokeSpec, pose);\n return [{\n kind: 'path',\n path,\n ...(shapeFill ? { fill: shapeFill } : {}),\n ...(stroke ? { stroke } : {}),\n }];\n }, ctx?.inherited), ctx), ctx),\n silhouette: (node, pose) => {\n const d = node.data as { shape: string; sides?: number; points?: number };\n return pathForShape(d, pose);\n },\n ink: (node, _pose, ctx) => {\n const d = node.data as ShapeData;\n const stroke = resolveStroke(d.stroke, ctx?.inherited?.stroke);\n return {\n filled: resolveFill(d.fill, ctx?.inherited?.fill) !== null,\n ...inkReach(stroke, ctx?.scale),\n ...inkTrim(stroke),\n };\n },\n // Every shape kind is inscribed in the pose box.\n bounds: (node, pose, ctx) =>\n poseBoxBounds(pose, resolveStroke((node.data as ShapeData).stroke, ctx?.inherited?.stroke), ctx),\n // A rect's or an ellipse's stroke reaches its box evenly; a polygon's or a\n // star's corners are sharper than a right angle, so their miters are measured.\n drawnBounds: (node, pose, ctx) => {\n const d = node.data as ShapeData;\n const stroke = resolveStroke(d.stroke, ctx?.inherited?.stroke);\n return d.shape === 'polygon' || d.shape === 'star'\n ? strokedPoseBox(node, pose, stroke, ctx, (at) => pathForShape(d, at))\n : outsetPoseBox(pose, stroke, ctx);\n },\n};\n\nexport const SHAPE_KINDS = new Set(['rect', 'ellipse', 'polygon', 'star']);\n\nexport function pathForShape(\n d: { shape: string; sides?: number; points?: number },\n p: RectPose,\n): Path {\n switch (d.shape) {\n case 'ellipse':\n return ellipsePath(p);\n case 'polygon': {\n const cx = p.x + p.width / 2;\n const cy = p.y + p.height / 2;\n const r = Math.min(p.width, p.height) / 2;\n return regularPolygonPath({ x: cx, y: cy }, r, d.sides ?? 6);\n }\n case 'star': {\n const cx = p.x + p.width / 2;\n const cy = p.y + p.height / 2;\n const r = Math.min(p.width, p.height) / 2;\n return starPath({ x: cx, y: cy }, r, d.points ?? 5);\n }\n case 'rect':\n default:\n return { kind: 'rect', x: p.x, y: p.y, width: p.width, height: p.height };\n }\n}\n"],"mappings":";;;;;;;;;;;;;;AA8BA,IAAa,gBAAmD;CAC9D,IAAI;CACJ,UAAU,SAAS;EACjB,IAAI,CAAC,WAAW,KAAK,IAAI,GAAG,OAAO;EACnC,MAAM,IAAK,KAAK,MAAoC;EACpD,OAAO,KAAK,QAAQ,YAAY,IAAI,CAAC;CACvC;CAIA,QAAQ,MAAM,MAAM,QAAQ,iBAAiB,yBAAyB,SAAS,MAAM,YAAY,YAAY;EAC3G,MAAM,IAAI,KAAK;EACf,MAAM,OAAO,aAAa,GAAG,IAAI;EACjC,MAAM,eAAe,YAAY,EAAE,MAAM,KAAK,WAAW,IAAI;EAC7D,MAAM,YAAY,gBAAgB,YAAY,cAAc,IAAI;EAChE,MAAM,aAAa,cAAc,EAAE,QAAQ,KAAK,WAAW,MAAM;EACjE,MAAM,SAAS,cAAc,cAAc,YAAY,IAAI;EAC3D,OAAO,CAAC;GACN,MAAM;GACN;GACA,GAAI,YAAY,EAAE,MAAM,UAAU,IAAI,CAAC;GACvC,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;EAC7B,CAAC;CACH,GAAG,KAAK,SAAS,GAAG,GAAG,GAAG,GAAG;CAC7B,aAAa,MAAM,SAAS;EAC1B,MAAM,IAAI,KAAK;EACf,OAAO,aAAa,GAAG,IAAI;CAC7B;CACA,MAAM,MAAM,OAAO,QAAQ;EACzB,MAAM,IAAI,KAAK;EACf,MAAM,SAAS,cAAc,EAAE,QAAQ,KAAK,WAAW,MAAM;EAC7D,OAAO;GACL,QAAQ,YAAY,EAAE,MAAM,KAAK,WAAW,IAAI,MAAM;GACtD,GAAG,SAAS,QAAQ,KAAK,KAAK;GAC9B,GAAG,QAAQ,MAAM;EACnB;CACF;CAEA,SAAS,MAAM,MAAM,QACnB,cAAc,MAAM,cAAe,KAAK,KAAmB,QAAQ,KAAK,WAAW,MAAM,GAAG,GAAG;CAGjG,cAAc,MAAM,MAAM,QAAQ;EAChC,MAAM,IAAI,KAAK;EACf,MAAM,SAAS,cAAc,EAAE,QAAQ,KAAK,WAAW,MAAM;EAC7D,OAAO,EAAE,UAAU,aAAa,EAAE,UAAU,SACxC,eAAe,MAAM,MAAM,QAAQ,MAAM,OAAO,aAAa,GAAG,EAAE,CAAC,IACnE,cAAc,MAAM,QAAQ,GAAG;CACrC;AACF;AAEA,IAAa,8BAAc,IAAI,IAAI;CAAC;CAAQ;CAAW;CAAW;AAAM,CAAC;AAEzE,SAAgB,aACd,GACA,GACM;CACN,QAAQ,EAAE,OAAV;EACE,KAAK,WACH,OAAO,YAAY,CAAC;EACtB,KAAK,WAAW;GACd,MAAM,KAAK,EAAE,IAAI,EAAE,QAAQ;GAC3B,MAAM,KAAK,EAAE,IAAI,EAAE,SAAS;GAC5B,MAAM,IAAI,KAAK,IAAI,EAAE,OAAO,EAAE,MAAM,IAAI;GACxC,OAAO,mBAAmB;IAAE,GAAG;IAAI,GAAG;GAAG,GAAG,GAAG,EAAE,SAAS,CAAC;EAC7D;EACA,KAAK,QAAQ;GACX,MAAM,KAAK,EAAE,IAAI,EAAE,QAAQ;GAC3B,MAAM,KAAK,EAAE,IAAI,EAAE,SAAS;GAC5B,MAAM,IAAI,KAAK,IAAI,EAAE,OAAO,EAAE,MAAM,IAAI;GACxC,OAAO,SAAS;IAAE,GAAG;IAAI,GAAG;GAAG,GAAG,GAAG,EAAE,UAAU,CAAC;EACpD;EAEA,SACE,OAAO;GAAE,MAAM;GAAQ,GAAG,EAAE;GAAG,GAAG,EAAE;GAAG,OAAO,EAAE;GAAO,QAAQ,EAAE;EAAO;CAC5E;AACF"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { overrideTrim } from "./trimOverrides.js";
|
|
2
|
+
|
|
3
|
+
// src/canvas/paintOverrides.ts
|
|
4
|
+
var clock = () => typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
5
|
+
/**
|
|
6
|
+
* Wrap a `drawOne` so a node with a live override reaches its painter with
|
|
7
|
+
* {@link NodePaintCtx.vertexColors} and {@link NodePaintCtx.trim}, in one
|
|
8
|
+
* context. Both scene walks wrap here; `createPathLayer` resolves through the
|
|
9
|
+
* same registries' `resolve`.
|
|
10
|
+
*
|
|
11
|
+
* A node nothing overrides passes the caller's `ctx` through untouched, so
|
|
12
|
+
* the common case allocates nothing.
|
|
13
|
+
*/
|
|
14
|
+
function withPaintOverrides(overrides, drawOne, now = clock) {
|
|
15
|
+
const colors = overrides?.colorOverrides;
|
|
16
|
+
const trims = overrides?.trimOverrides;
|
|
17
|
+
if (!colors && !trims) return drawOne;
|
|
18
|
+
return (node, pose, view, ctx) => {
|
|
19
|
+
const colored = colors?.has(node.id) === true;
|
|
20
|
+
const trimmed = trims?.has(node.id) === true;
|
|
21
|
+
if (!colored && !trimmed) return drawOne(node, pose, view, ctx);
|
|
22
|
+
const painted = { ...ctx };
|
|
23
|
+
if (colored) {
|
|
24
|
+
const tMs = now();
|
|
25
|
+
painted.vertexColors = (channel, base) => colors.resolve(node.id, channel, base, tMs);
|
|
26
|
+
}
|
|
27
|
+
if (trimmed) {
|
|
28
|
+
const override = trims.resolve(node.id);
|
|
29
|
+
painted.trim = (base) => overrideTrim(base, override);
|
|
30
|
+
}
|
|
31
|
+
return drawOne(node, pose, view, painted);
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/** What repaints for a `set` or `clear` on `overrides`, which no animation frame announces. */
|
|
35
|
+
function paintOverrideSources(overrides) {
|
|
36
|
+
const sources = [];
|
|
37
|
+
if (overrides?.colorOverrides) sources.push(overrides.colorOverrides);
|
|
38
|
+
if (overrides?.trimOverrides) sources.push(overrides.trimOverrides);
|
|
39
|
+
return sources;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export { paintOverrideSources, withPaintOverrides };
|
|
43
|
+
//# sourceMappingURL=paintOverrides.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paintOverrides.js","names":[],"sources":["../../src/canvas/paintOverrides.ts"],"sourcesContent":["/**\n * The render walk's half of animated paint: handing a node's live vertex-color\n * and stroke-trim overrides to its painter. The overrides live in an animator's\n * `ColorOverrideRegistry` and `TrimOverrideRegistry`; picking never reads them.\n */\nimport type { ColorOverrideRegistry, TrimOverrideRegistry } from '@weasel-js/animator';\nimport type { RedrawSource } from 'core/layers/render';\nimport type { NodePaintCtx, SceneViewDrawOne } from './NodeShape';\nimport { overrideTrim } from './trimOverrides';\n\n/** Animated paint to lay over a scene's nodes, typically an `animator`. */\nexport interface PaintOverrides {\n /** Per-anchor colors; see `NodePaintCtx.vertexColors`. */\n colorOverrides?: ColorOverrideRegistry;\n /** Stroke trims; see `NodePaintCtx.trim`. */\n trimOverrides?: TrimOverrideRegistry;\n}\n\nconst clock = (): number => (typeof performance !== 'undefined' ? performance.now() : Date.now());\n\n/**\n * Wrap a `drawOne` so a node with a live override reaches its painter with\n * {@link NodePaintCtx.vertexColors} and {@link NodePaintCtx.trim}, in one\n * context. Both scene walks wrap here; `createPathLayer` resolves through the\n * same registries' `resolve`.\n *\n * A node nothing overrides passes the caller's `ctx` through untouched, so\n * the common case allocates nothing.\n */\nexport function withPaintOverrides<TData, TLayer extends string, TPose>(\n overrides: PaintOverrides | undefined,\n drawOne: SceneViewDrawOne<TData, TLayer, TPose>,\n now: () => number = clock,\n): SceneViewDrawOne<TData, TLayer, TPose> {\n const colors = overrides?.colorOverrides;\n const trims = overrides?.trimOverrides;\n if (!colors && !trims) return drawOne;\n return (node, pose, view, ctx) => {\n const colored = colors?.has(node.id) === true;\n const trimmed = trims?.has(node.id) === true;\n if (!colored && !trimmed) return drawOne(node, pose, view, ctx);\n const painted: NodePaintCtx = { ...ctx };\n if (colored) {\n const tMs = now();\n painted.vertexColors = (channel, base) => colors!.resolve(node.id, channel, base, tMs);\n }\n if (trimmed) {\n const override = trims!.resolve(node.id);\n painted.trim = (base) => overrideTrim(base, override);\n }\n return drawOne(node, pose, view, painted);\n };\n}\n\n/** What repaints for a `set` or `clear` on `overrides`, which no animation frame announces. */\nexport function paintOverrideSources(overrides: PaintOverrides | undefined): RedrawSource[] {\n const sources: RedrawSource[] = [];\n if (overrides?.colorOverrides) sources.push(overrides.colorOverrides);\n if (overrides?.trimOverrides) sources.push(overrides.trimOverrides);\n return sources;\n}\n"],"mappings":";;;AAkBA,IAAM,cAAuB,OAAO,gBAAgB,cAAc,YAAY,IAAI,IAAI,KAAK,IAAI;;;;;;;;;;AAW/F,SAAgB,mBACd,WACA,SACA,MAAoB,OACoB;CACxC,MAAM,SAAS,WAAW;CAC1B,MAAM,QAAQ,WAAW;CACzB,IAAI,CAAC,UAAU,CAAC,OAAO,OAAO;CAC9B,QAAQ,MAAM,MAAM,MAAM,QAAQ;EAChC,MAAM,UAAU,QAAQ,IAAI,KAAK,EAAE,MAAM;EACzC,MAAM,UAAU,OAAO,IAAI,KAAK,EAAE,MAAM;EACxC,IAAI,CAAC,WAAW,CAAC,SAAS,OAAO,QAAQ,MAAM,MAAM,MAAM,GAAG;EAC9D,MAAM,UAAwB,EAAE,GAAG,IAAI;EACvC,IAAI,SAAS;GACX,MAAM,MAAM,IAAI;GAChB,QAAQ,gBAAgB,SAAS,SAAS,OAAQ,QAAQ,KAAK,IAAI,SAAS,MAAM,GAAG;EACvF;EACA,IAAI,SAAS;GACX,MAAM,WAAW,MAAO,QAAQ,KAAK,EAAE;GACvC,QAAQ,QAAQ,SAAS,aAAa,MAAM,QAAQ;EACtD;EACA,OAAO,QAAQ,MAAM,MAAM,MAAM,OAAO;CAC1C;AACF;;AAGA,SAAgB,qBAAqB,WAAuD;CAC1F,MAAM,UAA0B,CAAC;CACjC,IAAI,WAAW,gBAAgB,QAAQ,KAAK,UAAU,cAAc;CACpE,IAAI,WAAW,eAAe,QAAQ,KAAK,UAAU,aAAa;CAClE,OAAO;AACT"}
|
|
@@ -74,7 +74,7 @@ function planPixelRender(args) {
|
|
|
74
74
|
} }
|
|
75
75
|
});
|
|
76
76
|
const layers = args.annotations ? void 0 : { layerVisibility: Object.fromEntries([...annotationLayerIds(args.scene.layers)].map((id) => [sceneLayerKey(id), false])) };
|
|
77
|
-
commands.push(...buildSceneViewCommands(args.scene, view, drawOne, void 0, args.alphaFor, layers, args
|
|
77
|
+
commands.push(...buildSceneViewCommands(args.scene, view, drawOne, void 0, args.alphaFor, layers, args));
|
|
78
78
|
if (args.overlay) commands.push(...args.overlay);
|
|
79
79
|
return {
|
|
80
80
|
width,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"renderSceneToPixels.js","names":["viewToMat3"],"sources":["../../src/canvas/renderSceneToPixels.ts"],"sourcesContent":["/**\n * `renderSceneToPixels` — headless rasterization of a scene-space rect to raw RGBA pixels at an\n * explicit, per-axis scale. It drives the same WebGL2 pipeline (`WeaselRenderer` +\n * `buildSceneViewCommands`) as the on-screen view, so print, thumbnail, and export callers get\n * the same pixels the screen would produce at that scale.\n *\n * Environment contract:\n * - Density arrives exclusively via `scale`; this function never reads\n * `window.devicePixelRatio`.\n * - Rounding: output width = max(1, round(sourceRect.width × |scale.x|)), height analogously.\n * When round() lands below rect × scale, the rightmost/bottom sub-pixel band of scene content\n * is cropped; when it rounds up, that band is padded (the `background` fill, or transparent).\n * - Context lifetime: a caller-supplied `gl` is caller-owned. It is never disposed here, and\n * its drawing buffer must be at least width × height pixels (the render targets the\n * bottom-left region). Auto-created canvases are discarded with the session.\n * - Context loss: throws — a lost context cannot produce pixels, and the silent-noop\n * convention of the screen path would return all-zero bytes.\n * - Output: top-down row order, NON-premultiplied (\"straight\") RGBA. The GL framebuffer is\n * premultiplied and bottom-up; readback flips and unpremultiplies.\n * - Determinism: same context + same inputs → same bytes. Cross-driver / cross-machine byte\n * equality is NOT guaranteed; do not build golden-image tests on committed bytes.\n * - Image quality: image textures upload with mipmaps so minified bitmaps don't moiré, and\n * curve tessellation runs at an output-scale tolerance (`flattenTolerancePx`, default 0.25\n * output px).\n */\nimport { WeaselRenderer } from '@weasel-js/renderer';\nimport { viewToMat3, type GlMat3 } from '@weasel-js/geom';\nimport type { DrawCommand } from '@weasel-js/renderer';\nimport type { View } from '../core/viewport/view';\nimport type { Node, Scene } from 'core/scene/types';\nimport { buildSceneViewCommands, type SceneViewDrawOne } from './sceneViewRender';\nimport type { ColorOverrideRegistry } from '@weasel-js/animator';\nimport { defaultDrawOne } from './defaultDrawOne';\nimport { warmFonts, type FontRequest } from '@weasel-js/font';\nimport { isPaintKindKnown, warmPaintKinds } from '@weasel-js/renderer';\nimport { renderNeeds } from './renderNeeds';\nimport { straightTopDown } from './rasterReadback';\nimport { annotationLayerIds } from 'core/scene/annotation';\nimport { sceneLayerKey } from './sceneLayerPaint';\nimport { CANVAS_CONTEXT_ATTRIBUTES } from './contextAttributes';\n\n/** Plain RGBA raster — structurally `ImageData`-compatible ({ width, height,\n * data }), deliberately free of printer/dpi/physical-unit concepts. */\nexport interface RasterImage {\n width: number;\n height: number;\n /** Top-down, straight (non-premultiplied) RGBA, 4 bytes per pixel. */\n data: Uint8ClampedArray;\n}\n\n/** Minimal canvas contract for `createCanvas` injection: `OffscreenCanvas`,\n * an HTML canvas, or a test fake. */\nexport interface HeadlessCanvasLike {\n width: number;\n height: number;\n getContext(contextId: 'webgl2', options?: WebGLContextAttributes): unknown;\n}\n\n/** What to render, at what resolution, and with which quality settings. */\nexport interface RenderSceneToPixelsArgs<TData, TLayer extends string, TPose> {\n scene: Scene<TData, TLayer, TPose>;\n /** Scene-space rect to render (origin + size in scene units). Output pixel\n * dimensions follow from rect × scale (round, min 1 — see module doc). */\n sourceRect: { x: number; y: number; width: number; height: number };\n /** Output pixels per scene unit, per axis. Anisotropic values supported. A\n * negative axis is flipped: the output is sized from its magnitude, and the\n * rect's far edge on that axis lands on the output's top or left. */\n scale: { x: number; y: number };\n /** Per-node draw callback. Default: `defaultDrawOne` with `resolveImage`\n * merged into the `NodePaintCtx` the scene walk supplies. Custom `drawOne`\n * callers that still want resolver injection should call\n * `defaultDrawOne(node, pose, view, { ...ctx, resolveImage })` themselves —\n * dropping the walk's `ctx` drops derived geometry with it. */\n drawOne?: SceneViewDrawOne<TData, TLayer, TPose>;\n /** Bitmap resolver for image nodes — lets consumers reuse their own decode\n * caches. `undefined` results paint the deterministic grey placeholder\n * outline (see `NodePaintCtx.resolveImage`). */\n resolveImage?: (node: Node<TData, TLayer, TPose>) => ImageBitmap | undefined;\n /** Per-id alpha multiplier, mirroring `<SceneCanvas>`'s scene-slot\n * `alphaFor`. Pass the same function the on-screen canvas uses so an\n * export matches what the user is looking at. Defaults to `() => 1`. */\n alphaFor?: (id: string) => number;\n /** Animated vertex colors to paint, typically an animator's\n * `colorOverrides`, so an export shows the frame on screen. */\n colorOverrides?: ColorOverrideRegistry;\n /** Background fill (any CSS color accepted by the renderer). Default:\n * fully transparent. Passing a color is always valid. */\n background?: string;\n /** Caller-owned WebGL2 context to render with. Mutually exclusive with\n * `createCanvas`. Never disposed by this call. */\n gl?: WebGL2RenderingContext;\n /** One-shot canvas factory (DOM canvas, OffscreenCanvas, or test fake).\n * Mutually exclusive with `gl`. Default: `OffscreenCanvas` when\n * available, else `document.createElement('canvas')`. */\n createCanvas?: (widthPx: number, heightPx: number) => HeadlessCanvasLike;\n /** Max curve-flattening error in OUTPUT pixels. Default 0.25. Measured\n * along each path's most stretched axis. */\n flattenTolerancePx?: number;\n /** Commands drawn over the scene in output-pixel space — origin at the\n * output's top-left, one unit per output pixel. Annotations, watermarks,\n * and `renderDebugSnapshot`'s overlay ride here. */\n overlay?: readonly DrawCommand[];\n /** Draw the scene's annotation layers (`isAnnotationLayer`). Default\n * `false`: a raster is an export, which leaves them out as the SVG export\n * does. Pass `true` to reproduce the canvas as it looked. */\n annotations?: boolean;\n /** Screen space → output pixels, which `'screen'`-unit paint is measured in. Default: output pixels. A\n * raster standing in for part of a larger surface, such as an export's page, names the whole here. */\n screen?: GlMat3;\n}\n\n/** The result of planning a headless render: how big the output is, the view\n * that maps the source rect onto it, and the commands to draw. */\nexport interface PixelRenderPlan {\n width: number;\n height: number;\n view: View;\n commands: DrawCommand[];\n}\n\n/** Pure planning half of `renderSceneToPixels`: output dimensions, the\n * anisotropic `View`, and the full command list (background + view-wrapped\n * scene). Exported for tests and for callers targeting their own renderer. */\nexport function planPixelRender<TData, TLayer extends string, TPose>(\n args: Omit<RenderSceneToPixelsArgs<TData, TLayer, TPose>, 'gl' | 'createCanvas' | 'flattenTolerancePx'>,\n): PixelRenderPlan {\n const { sourceRect, scale } = args;\n for (const [label, v] of [\n ['sourceRect.width', sourceRect.width], ['sourceRect.height', sourceRect.height],\n ] as const) {\n if (!Number.isFinite(v) || v <= 0) {\n throw new Error(`renderSceneToPixels: ${label} must be a positive finite number, got ${v}`);\n }\n }\n for (const [label, v] of [['scale.x', scale.x], ['scale.y', scale.y]] as const) {\n if (!Number.isFinite(v) || v === 0) {\n throw new Error(`renderSceneToPixels: ${label} must be a nonzero finite number, got ${v}`);\n }\n }\n for (const [label, v] of [\n ['sourceRect.x', sourceRect.x], ['sourceRect.y', sourceRect.y],\n ] as const) {\n if (!Number.isFinite(v)) {\n throw new Error(`renderSceneToPixels: ${label} must be a finite number, got ${v}`);\n }\n }\n const width = Math.max(1, Math.round(sourceRect.width * Math.abs(scale.x)));\n const height = Math.max(1, Math.round(sourceRect.height * Math.abs(scale.y)));\n const view: View = {\n x: scale.x < 0 ? sourceRect.x + sourceRect.width : sourceRect.x,\n y: scale.y < 0 ? sourceRect.y + sourceRect.height : sourceRect.y,\n scale: { x: scale.x, y: scale.y },\n };\n\n const resolveImage = args.resolveImage as ((n: Node<unknown, string, unknown>) => ImageBitmap | undefined) | undefined;\n const drawOne: SceneViewDrawOne<TData, TLayer, TPose> =\n args.drawOne ?? ((node, pose, view, ctx) => defaultDrawOne(node, pose, view, { ...ctx, resolveImage }));\n\n const commands: DrawCommand[] = [];\n if (args.background !== undefined) {\n // Screen-space (pre-view) fill so rounding can never leave uncovered\n // edge pixels.\n commands.push({\n kind: 'path',\n path: { kind: 'rect', x: 0, y: 0, width, height },\n fill: { paint: { kind: 'solid', color: args.background } },\n });\n }\n const layers = args.annotations ? undefined : {\n layerVisibility: Object.fromEntries([...annotationLayerIds(args.scene.layers)].map((id) => [sceneLayerKey(id), false])),\n };\n commands.push(...buildSceneViewCommands(\n args.scene, view, drawOne, undefined, args.alphaFor, layers, args.colorOverrides,\n ));\n if (args.overlay) commands.push(...args.overlay);\n return { width, height, view, commands };\n}\n\nfunction defaultCreateCanvas(width: number, height: number): HeadlessCanvasLike {\n if (typeof OffscreenCanvas !== 'undefined') {\n return new OffscreenCanvas(width, height);\n }\n if (typeof document !== 'undefined') {\n const c = document.createElement('canvas');\n c.width = width;\n c.height = height;\n return c;\n }\n throw new Error('renderSceneToPixels: no canvas source in this environment — supply `gl` or `createCanvas`');\n}\n\n/** Where a {@link RasterSession} draws. Pass `gl` or `createCanvas`, or\n * neither for the default canvas source. */\nexport interface RasterSessionOptions {\n /** Caller-owned WebGL2 context. The session never disposes it. Its drawing\n * buffer must be at least as large as every render the session does. */\n gl?: WebGL2RenderingContext;\n /** Canvas factory, called once, at the size of the first render. The\n * session grows the canvas when a later render needs more room and never\n * shrinks it. Default: `OffscreenCanvas` when available, else a detached\n * DOM canvas. */\n createCanvas?: (widthPx: number, heightPx: number) => HeadlessCanvasLike;\n}\n\n/** One render through a {@link RasterSession}: everything\n * `renderSceneToPixels` takes except where to draw. */\nexport type RasterRenderArgs<TData, TLayer extends string, TPose> =\n Omit<RenderSceneToPixelsArgs<TData, TLayer, TPose>, 'gl' | 'createCanvas'>;\n\n/** One renderer and its GPU caches, kept across any number of headless\n * renders. Scenes, source rects and scales may differ per render. */\nexport interface RasterSession {\n /** Same contract as `renderSceneToPixels`. Throws once disposed. */\n render<TData, TLayer extends string, TPose>(args: RasterRenderArgs<TData, TLayer, TPose>): RasterImage;\n /** Free every GL resource the session created — programs, geometry, and\n * the texture and mesh caches — and drop its own canvas. A caller-owned\n * context stays open. Idempotent. */\n dispose(): void;\n}\n\n/**\n * Open a raster session: one `WeaselRenderer` that many headless renders\n * share, for a consumer producing thumbnails or pages in bulk. Programs\n * compile once, and a bitmap or recurring mesh uploads once, rather than per\n * render. `renderSceneToPixels` is a session opened for one render.\n *\n * The renderer is created on the first render, so opening a session costs\n * nothing and fails nothing. Each render draws into the bottom-left\n * `width × height` of the buffer and reads that region back.\n */\nexport function createRasterSession(options: RasterSessionOptions = {}): RasterSession {\n if (options.gl && options.createCanvas) {\n throw new Error('raster session: `gl` and `createCanvas` are mutually exclusive');\n }\n let canvas: HeadlessCanvasLike | null = null;\n let gl: WebGL2RenderingContext | undefined = options.gl;\n let renderer: WeaselRenderer | null = null;\n let disposed = false;\n\n function contextFor(width: number, height: number): WebGL2RenderingContext {\n if (!options.gl) {\n if (!canvas) {\n canvas = (options.createCanvas ?? defaultCreateCanvas)(width, height);\n canvas.width = width;\n canvas.height = height;\n gl = (canvas.getContext('webgl2', CANVAS_CONTEXT_ATTRIBUTES) as WebGL2RenderingContext | null) ?? undefined;\n } else {\n if (canvas.width < width) canvas.width = width;\n if (canvas.height < height) canvas.height = height;\n }\n }\n if (!gl || typeof (gl as Partial<WebGL2RenderingContext>).enable !== 'function') {\n throw new Error('raster session: WebGL2 is unavailable — supply `gl` or a WebGL2-capable `createCanvas`');\n }\n if (isLost(gl)) {\n throw new Error('raster session: the WebGL2 context is lost — dispose this session and open another');\n }\n // A context clamps a drawing buffer it cannot allocate, and reading past\n // the real one returns transparent pixels rather than failing.\n const { drawingBufferWidth: bufferW, drawingBufferHeight: bufferH } = gl;\n if (bufferW < width || bufferH < height) {\n throw new Error(\n `raster session: a ${width} × ${height} render does not fit the context's `\n + `${bufferW} × ${bufferH} drawing buffer — lower \\`scale\\` or render the source rect in tiles`,\n );\n }\n return gl;\n }\n\n return {\n render(args) {\n if (disposed) throw new Error('raster session: render() on a disposed session');\n const plan = planPixelRender(args);\n const { width, height } = plan;\n const ctx = contextFor(width, height);\n const flattenTolerance = { px: args.flattenTolerancePx ?? 0.25 };\n if (!renderer) {\n renderer = new WeaselRenderer({\n gl: ctx,\n width,\n height,\n dpr: 1,\n imageMinification: 'mipmap',\n flattenTolerance,\n // Dynamic canvas-SDF glyphs must all bake inline — this path is\n // synchronous with no notify-and-redraw, and print must be complete.\n bakeBudget: Infinity,\n });\n } else {\n renderer.resize({ width, height, dpr: 1 });\n renderer.setFlattenTolerance(flattenTolerance);\n }\n renderer.render(plan.commands, viewToMat3(plan.view), { screen: args.screen });\n if (isLost(ctx)) {\n throw new Error('raster session: the WebGL2 context was lost during render');\n }\n const raw = new Uint8Array(width * height * 4);\n ctx.readPixels(0, 0, width, height, ctx.RGBA, ctx.UNSIGNED_BYTE, raw);\n return { width, height, data: straightTopDown(raw, width, height) };\n },\n dispose() {\n if (disposed) return;\n disposed = true;\n renderer?.dispose();\n renderer = null;\n // A browser caps live contexts per page and evicts the oldest, which is\n // a canvas on screen; a dropped reference frees nothing until collected.\n if (canvas && !options.createCanvas) gl?.getExtension('WEBGL_lose_context')?.loseContext();\n canvas = null;\n gl = undefined;\n },\n };\n}\n\nfunction isLost(gl: WebGL2RenderingContext): boolean {\n return typeof gl.isContextLost === 'function' && gl.isContextLost() === true;\n}\n\n/** What {@link warmRender} loads. */\nexport interface WarmRenderOptions<TData = unknown, TLayer extends string = string, TPose = unknown> {\n /** The render about to happen, as `renderSceneToPixels` or\n * `RasterSession.render` will take it. Its commands are built the way the\n * render builds them, and only the faces and paint kinds they draw with\n * load. */\n render?: RasterRenderArgs<TData, TLayer, TPose>;\n /** Commands about to be drawn, read the same way as `render`'s. Both may be\n * given; what either needs loads. */\n commands?: readonly DrawCommand[];\n /** Fonts to load in place of the ones derived — family names or single\n * variants, as `warmFonts` takes them. */\n families?: readonly (string | FontRequest)[];\n /** Paint kinds to load in place of the ones derived, as `warmPaintKinds`\n * takes them. */\n paintKinds?: readonly string[];\n}\n\n/**\n * Load everything a synchronous render would otherwise draw as nothing — the\n * lazily registered font atlases and the paint kinds loaded on demand — so\n * that a `renderSceneToPixels` or `RasterSession.render` issued after it\n * resolves is complete.\n *\n * Pass the render (`{ render: args }`) or its commands, and only what they\n * draw with loads: each face their text is set in, through the same\n * resolution the renderer does, and each paint kind they name. A rejection\n * then means something this render needs failed to load. `families` and\n * `paintKinds` replace either derived list.\n *\n * With neither `render` nor `commands`, an omitted list loads everything\n * registered: every font family and every kind that has a loader — which\n * fails when any one of them does, and fetches what nothing draws.\n */\nexport function warmRender<TData, TLayer extends string, TPose>(\n opts: WarmRenderOptions<TData, TLayer, TPose> = {},\n): Promise<void> {\n let { families, paintKinds } = opts;\n if (opts.render || opts.commands) {\n let planned: DrawCommand[];\n try {\n planned = opts.render ? planPixelRender(opts.render).commands : [];\n } catch (err) {\n return Promise.reject(err);\n }\n const needs = renderNeeds([...planned, ...(opts.commands ?? [])]);\n families ??= needs.fonts;\n // A kind nothing registered draws nothing whether or not this waits.\n paintKinds ??= needs.paintKinds.filter(isPaintKindKnown);\n }\n return Promise.all([warmFonts(families), warmPaintKinds(paintKinds)]).then(() => undefined);\n}\n\n/**\n * Render part of a scene to raw pixels, with no canvas mounted and no React\n * involved — for export, thumbnails, print, and pixel-diff tests.\n *\n * Output size follows from the source rect and the requested scale, so\n * rendering the same region at a higher scale is a real resolution increase\n * rather than an upscale: curves are re-flattened and glyphs re-rasterized for\n * the output resolution.\n *\n * Synchronous, so nothing lands mid-render: a paint kind loaded on demand\n * (`mesh-gradient`, or one declared with `registerPaintKindLoader`) that has\n * not loaded yet draws nothing, and neither does text set in a font atlas\n * still fetching — one registered `{ lazy: true }` is not fetched until text\n * first asks for it. `await warmRender({ render: args })` first.\n *\n * Each call opens a {@link RasterSession} and disposes it before returning.\n * Rendering many scenes, open one with `createRasterSession` instead.\n */\nexport function renderSceneToPixels<TData, TLayer extends string, TPose>(\n args: RenderSceneToPixelsArgs<TData, TLayer, TPose>,\n): RasterImage {\n const { gl, createCanvas, ...renderArgs } = args;\n const session = createRasterSession({ gl, createCanvas });\n try {\n return session.render(renderArgs);\n } finally {\n session.dispose();\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"renderSceneToPixels.js","names":["viewToMat3"],"sources":["../../src/canvas/renderSceneToPixels.ts"],"sourcesContent":["/**\n * `renderSceneToPixels` — headless rasterization of a scene-space rect to raw RGBA pixels at an\n * explicit, per-axis scale. It drives the same WebGL2 pipeline (`WeaselRenderer` +\n * `buildSceneViewCommands`) as the on-screen view, so print, thumbnail, and export callers get\n * the same pixels the screen would produce at that scale.\n *\n * Environment contract:\n * - Density arrives exclusively via `scale`; this function never reads\n * `window.devicePixelRatio`.\n * - Rounding: output width = max(1, round(sourceRect.width × |scale.x|)), height analogously.\n * When round() lands below rect × scale, the rightmost/bottom sub-pixel band of scene content\n * is cropped; when it rounds up, that band is padded (the `background` fill, or transparent).\n * - Context lifetime: a caller-supplied `gl` is caller-owned. It is never disposed here, and\n * its drawing buffer must be at least width × height pixels (the render targets the\n * bottom-left region). Auto-created canvases are discarded with the session.\n * - Context loss: throws — a lost context cannot produce pixels, and the silent-noop\n * convention of the screen path would return all-zero bytes.\n * - Output: top-down row order, NON-premultiplied (\"straight\") RGBA. The GL framebuffer is\n * premultiplied and bottom-up; readback flips and unpremultiplies.\n * - Determinism: same context + same inputs → same bytes. Cross-driver / cross-machine byte\n * equality is NOT guaranteed; do not build golden-image tests on committed bytes.\n * - Image quality: image textures upload with mipmaps so minified bitmaps don't moiré, and\n * curve tessellation runs at an output-scale tolerance (`flattenTolerancePx`, default 0.25\n * output px).\n */\nimport { WeaselRenderer } from '@weasel-js/renderer';\nimport { viewToMat3 } from '@weasel-js/geom';\nimport type { DrawCommand } from '@weasel-js/renderer';\nimport type { View } from '../core/viewport/view';\nimport type { Node } from 'core/scene/types';\nimport { buildSceneViewCommands, type SceneViewDrawOne } from './sceneViewRender';\nimport { defaultDrawOne } from './defaultDrawOne';\nimport { warmFonts, type FontRequest } from '@weasel-js/font';\nimport { isPaintKindKnown, warmPaintKinds } from '@weasel-js/renderer';\nimport { renderNeeds } from './renderNeeds';\nimport { straightTopDown } from './rasterReadback';\nimport { annotationLayerIds } from 'core/scene/annotation';\nimport { sceneLayerKey } from './sceneLayerPaint';\nimport { CANVAS_CONTEXT_ATTRIBUTES } from './contextAttributes';\n\nexport type {\n HeadlessCanvasLike, PixelRenderPlan, RasterImage, RenderSceneToPixelsArgs,\n} from './renderSceneToPixelsTypes';\nimport type {\n HeadlessCanvasLike, PixelRenderPlan, RasterImage, RenderSceneToPixelsArgs,\n} from './renderSceneToPixelsTypes';\n\n/** Pure planning half of `renderSceneToPixels`: output dimensions, the\n * anisotropic `View`, and the full command list (background + view-wrapped\n * scene). Exported for tests and for callers targeting their own renderer. */\nexport function planPixelRender<TData, TLayer extends string, TPose>(\n args: Omit<RenderSceneToPixelsArgs<TData, TLayer, TPose>, 'gl' | 'createCanvas' | 'flattenTolerancePx'>,\n): PixelRenderPlan {\n const { sourceRect, scale } = args;\n for (const [label, v] of [\n ['sourceRect.width', sourceRect.width], ['sourceRect.height', sourceRect.height],\n ] as const) {\n if (!Number.isFinite(v) || v <= 0) {\n throw new Error(`renderSceneToPixels: ${label} must be a positive finite number, got ${v}`);\n }\n }\n for (const [label, v] of [['scale.x', scale.x], ['scale.y', scale.y]] as const) {\n if (!Number.isFinite(v) || v === 0) {\n throw new Error(`renderSceneToPixels: ${label} must be a nonzero finite number, got ${v}`);\n }\n }\n for (const [label, v] of [\n ['sourceRect.x', sourceRect.x], ['sourceRect.y', sourceRect.y],\n ] as const) {\n if (!Number.isFinite(v)) {\n throw new Error(`renderSceneToPixels: ${label} must be a finite number, got ${v}`);\n }\n }\n const width = Math.max(1, Math.round(sourceRect.width * Math.abs(scale.x)));\n const height = Math.max(1, Math.round(sourceRect.height * Math.abs(scale.y)));\n const view: View = {\n x: scale.x < 0 ? sourceRect.x + sourceRect.width : sourceRect.x,\n y: scale.y < 0 ? sourceRect.y + sourceRect.height : sourceRect.y,\n scale: { x: scale.x, y: scale.y },\n };\n\n const resolveImage = args.resolveImage as ((n: Node<unknown, string, unknown>) => ImageBitmap | undefined) | undefined;\n const drawOne: SceneViewDrawOne<TData, TLayer, TPose> =\n args.drawOne ?? ((node, pose, view, ctx) => defaultDrawOne(node, pose, view, { ...ctx, resolveImage }));\n\n const commands: DrawCommand[] = [];\n if (args.background !== undefined) {\n // Screen-space (pre-view) fill so rounding can never leave uncovered\n // edge pixels.\n commands.push({\n kind: 'path',\n path: { kind: 'rect', x: 0, y: 0, width, height },\n fill: { paint: { kind: 'solid', color: args.background } },\n });\n }\n const layers = args.annotations ? undefined : {\n layerVisibility: Object.fromEntries([...annotationLayerIds(args.scene.layers)].map((id) => [sceneLayerKey(id), false])),\n };\n commands.push(...buildSceneViewCommands(\n args.scene, view, drawOne, undefined, args.alphaFor, layers, args,\n ));\n if (args.overlay) commands.push(...args.overlay);\n return { width, height, view, commands };\n}\n\nfunction defaultCreateCanvas(width: number, height: number): HeadlessCanvasLike {\n if (typeof OffscreenCanvas !== 'undefined') {\n return new OffscreenCanvas(width, height);\n }\n if (typeof document !== 'undefined') {\n const c = document.createElement('canvas');\n c.width = width;\n c.height = height;\n return c;\n }\n throw new Error('renderSceneToPixels: no canvas source in this environment — supply `gl` or `createCanvas`');\n}\n\n/** Where a {@link RasterSession} draws. Pass `gl` or `createCanvas`, or\n * neither for the default canvas source. */\nexport interface RasterSessionOptions {\n /** Caller-owned WebGL2 context. The session never disposes it. Its drawing\n * buffer must be at least as large as every render the session does. */\n gl?: WebGL2RenderingContext;\n /** Canvas factory, called once, at the size of the first render. The\n * session grows the canvas when a later render needs more room and never\n * shrinks it. Default: `OffscreenCanvas` when available, else a detached\n * DOM canvas. */\n createCanvas?: (widthPx: number, heightPx: number) => HeadlessCanvasLike;\n}\n\n/** One render through a {@link RasterSession}: everything\n * `renderSceneToPixels` takes except where to draw. */\nexport type RasterRenderArgs<TData, TLayer extends string, TPose> =\n Omit<RenderSceneToPixelsArgs<TData, TLayer, TPose>, 'gl' | 'createCanvas'>;\n\n/** One renderer and its GPU caches, kept across any number of headless\n * renders. Scenes, source rects and scales may differ per render. */\nexport interface RasterSession {\n /** Same contract as `renderSceneToPixels`. Throws once disposed. */\n render<TData, TLayer extends string, TPose>(args: RasterRenderArgs<TData, TLayer, TPose>): RasterImage;\n /** Free every GL resource the session created — programs, geometry, and\n * the texture and mesh caches — and drop its own canvas. A caller-owned\n * context stays open. Idempotent. */\n dispose(): void;\n}\n\n/**\n * Open a raster session: one `WeaselRenderer` that many headless renders\n * share, for a consumer producing thumbnails or pages in bulk. Programs\n * compile once, and a bitmap or recurring mesh uploads once, rather than per\n * render. `renderSceneToPixels` is a session opened for one render.\n *\n * The renderer is created on the first render, so opening a session costs\n * nothing and fails nothing. Each render draws into the bottom-left\n * `width × height` of the buffer and reads that region back.\n */\nexport function createRasterSession(options: RasterSessionOptions = {}): RasterSession {\n if (options.gl && options.createCanvas) {\n throw new Error('raster session: `gl` and `createCanvas` are mutually exclusive');\n }\n let canvas: HeadlessCanvasLike | null = null;\n let gl: WebGL2RenderingContext | undefined = options.gl;\n let renderer: WeaselRenderer | null = null;\n let disposed = false;\n\n function contextFor(width: number, height: number): WebGL2RenderingContext {\n if (!options.gl) {\n if (!canvas) {\n canvas = (options.createCanvas ?? defaultCreateCanvas)(width, height);\n canvas.width = width;\n canvas.height = height;\n gl = (canvas.getContext('webgl2', CANVAS_CONTEXT_ATTRIBUTES) as WebGL2RenderingContext | null) ?? undefined;\n } else {\n if (canvas.width < width) canvas.width = width;\n if (canvas.height < height) canvas.height = height;\n }\n }\n if (!gl || typeof (gl as Partial<WebGL2RenderingContext>).enable !== 'function') {\n throw new Error('raster session: WebGL2 is unavailable — supply `gl` or a WebGL2-capable `createCanvas`');\n }\n if (isLost(gl)) {\n throw new Error('raster session: the WebGL2 context is lost — dispose this session and open another');\n }\n // A context clamps a drawing buffer it cannot allocate, and reading past\n // the real one returns transparent pixels rather than failing.\n const { drawingBufferWidth: bufferW, drawingBufferHeight: bufferH } = gl;\n if (bufferW < width || bufferH < height) {\n throw new Error(\n `raster session: a ${width} × ${height} render does not fit the context's `\n + `${bufferW} × ${bufferH} drawing buffer — lower \\`scale\\` or render the source rect in tiles`,\n );\n }\n return gl;\n }\n\n return {\n render(args) {\n if (disposed) throw new Error('raster session: render() on a disposed session');\n const plan = planPixelRender(args);\n const { width, height } = plan;\n const ctx = contextFor(width, height);\n const flattenTolerance = { px: args.flattenTolerancePx ?? 0.25 };\n if (!renderer) {\n renderer = new WeaselRenderer({\n gl: ctx,\n width,\n height,\n dpr: 1,\n imageMinification: 'mipmap',\n flattenTolerance,\n // Dynamic canvas-SDF glyphs must all bake inline — this path is\n // synchronous with no notify-and-redraw, and print must be complete.\n bakeBudget: Infinity,\n });\n } else {\n renderer.resize({ width, height, dpr: 1 });\n renderer.setFlattenTolerance(flattenTolerance);\n }\n renderer.render(plan.commands, viewToMat3(plan.view), { screen: args.screen });\n if (isLost(ctx)) {\n throw new Error('raster session: the WebGL2 context was lost during render');\n }\n const raw = new Uint8Array(width * height * 4);\n ctx.readPixels(0, 0, width, height, ctx.RGBA, ctx.UNSIGNED_BYTE, raw);\n return { width, height, data: straightTopDown(raw, width, height) };\n },\n dispose() {\n if (disposed) return;\n disposed = true;\n renderer?.dispose();\n renderer = null;\n // A browser caps live contexts per page and evicts the oldest, which is\n // a canvas on screen; a dropped reference frees nothing until collected.\n if (canvas && !options.createCanvas) gl?.getExtension('WEBGL_lose_context')?.loseContext();\n canvas = null;\n gl = undefined;\n },\n };\n}\n\nfunction isLost(gl: WebGL2RenderingContext): boolean {\n return typeof gl.isContextLost === 'function' && gl.isContextLost() === true;\n}\n\n/** What {@link warmRender} loads. */\nexport interface WarmRenderOptions<TData = unknown, TLayer extends string = string, TPose = unknown> {\n /** The render about to happen, as `renderSceneToPixels` or\n * `RasterSession.render` will take it. Its commands are built the way the\n * render builds them, and only the faces and paint kinds they draw with\n * load. */\n render?: RasterRenderArgs<TData, TLayer, TPose>;\n /** Commands about to be drawn, read the same way as `render`'s. Both may be\n * given; what either needs loads. */\n commands?: readonly DrawCommand[];\n /** Fonts to load in place of the ones derived — family names or single\n * variants, as `warmFonts` takes them. */\n families?: readonly (string | FontRequest)[];\n /** Paint kinds to load in place of the ones derived, as `warmPaintKinds`\n * takes them. */\n paintKinds?: readonly string[];\n}\n\n/**\n * Load everything a synchronous render would otherwise draw as nothing — the\n * lazily registered font atlases and the paint kinds loaded on demand — so\n * that a `renderSceneToPixels` or `RasterSession.render` issued after it\n * resolves is complete.\n *\n * Pass the render (`{ render: args }`) or its commands, and only what they\n * draw with loads: each face their text is set in, through the same\n * resolution the renderer does, and each paint kind they name. A rejection\n * then means something this render needs failed to load. `families` and\n * `paintKinds` replace either derived list.\n *\n * With neither `render` nor `commands`, an omitted list loads everything\n * registered: every font family and every kind that has a loader — which\n * fails when any one of them does, and fetches what nothing draws.\n */\nexport function warmRender<TData, TLayer extends string, TPose>(\n opts: WarmRenderOptions<TData, TLayer, TPose> = {},\n): Promise<void> {\n let { families, paintKinds } = opts;\n if (opts.render || opts.commands) {\n let planned: DrawCommand[];\n try {\n planned = opts.render ? planPixelRender(opts.render).commands : [];\n } catch (err) {\n return Promise.reject(err);\n }\n const needs = renderNeeds([...planned, ...(opts.commands ?? [])]);\n families ??= needs.fonts;\n // A kind nothing registered draws nothing whether or not this waits.\n paintKinds ??= needs.paintKinds.filter(isPaintKindKnown);\n }\n return Promise.all([warmFonts(families), warmPaintKinds(paintKinds)]).then(() => undefined);\n}\n\n/**\n * Render part of a scene to raw pixels, with no canvas mounted and no React\n * involved — for export, thumbnails, print, and pixel-diff tests.\n *\n * Output size follows from the source rect and the requested scale, so\n * rendering the same region at a higher scale is a real resolution increase\n * rather than an upscale: curves are re-flattened and glyphs re-rasterized for\n * the output resolution.\n *\n * Synchronous, so nothing lands mid-render: a paint kind loaded on demand\n * (`mesh-gradient`, or one declared with `registerPaintKindLoader`) that has\n * not loaded yet draws nothing, and neither does text set in a font atlas\n * still fetching — one registered `{ lazy: true }` is not fetched until text\n * first asks for it. `await warmRender({ render: args })` first.\n *\n * Each call opens a {@link RasterSession} and disposes it before returning.\n * Rendering many scenes, open one with `createRasterSession` instead.\n */\nexport function renderSceneToPixels<TData, TLayer extends string, TPose>(\n args: RenderSceneToPixelsArgs<TData, TLayer, TPose>,\n): RasterImage {\n const { gl, createCanvas, ...renderArgs } = args;\n const session = createRasterSession({ gl, createCanvas });\n try {\n return session.render(renderArgs);\n } finally {\n session.dispose();\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { withInheritedPaintBounds, withInheritedPaints } from "./inheritedPaints.js";
|
|
2
2
|
import { sceneDerivedPathOf, sceneShownToPose, withDerivedPaths } from "./derivedPath.js";
|
|
3
|
-
import {
|
|
3
|
+
import { withPaintOverrides } from "./paintOverrides.js";
|
|
4
4
|
import { nodeAsShown, shownLookup } from "../core/scene/nodeAsShown.js";
|
|
5
5
|
import { withShownNode } from "./shownNodes.js";
|
|
6
6
|
|
|
@@ -17,10 +17,10 @@ function composeAlphaFor(scene, alphaFor) {
|
|
|
17
17
|
const overrideAlphaFor = (id) => scene.overrides.read(id)?.alpha ?? 1;
|
|
18
18
|
return alphaFor ? (id) => alphaFor(id) * overrideAlphaFor(id) : overrideAlphaFor;
|
|
19
19
|
}
|
|
20
|
-
function wireSceneSlotToScene(slot, scene, alphaFor,
|
|
20
|
+
function wireSceneSlotToScene(slot, scene, alphaFor, overrides) {
|
|
21
21
|
const shown = shownLookup(scene);
|
|
22
22
|
const derived = withDerivedPaths(scene, withInheritedPaints(shown, slot.drawOne), slot.toPose);
|
|
23
|
-
const painted =
|
|
23
|
+
const painted = withPaintOverrides(overrides, derived);
|
|
24
24
|
const derivedPathOf = sceneDerivedPathOf(scene, slot.toPose);
|
|
25
25
|
return {
|
|
26
26
|
...slot,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sceneSlotWiring.js","names":[],"sources":["../../src/canvas/sceneSlotWiring.ts"],"sourcesContent":["/**\n * What the scene slot needs from the `Scene` that `<Canvas>` cannot see.\n *\n * `<Canvas>` is scene-agnostic: it walks an adapter. Five things a node is\n * painted with are therefore invisible to it — an ephemeral override's alpha\n * and data, a derived path, which needs other nodes' poses to resolve, the\n * paint an `'inherit'` field takes from its containers, and animated vertex\n * colors, which live in an animator. `<SceneCanvas>` folds them into the slot\n * here; `buildSceneViewCommands` does the same for the headless walk.\n */\nimport type { SceneSlotConfig } from './buildSceneLayer';\nimport type { Node, NodeId, Scene } from 'core/scene/types';\nimport { withInheritedPaintBounds, withInheritedPaints } from './inheritedPaints';\nimport { withDerivedPaths, sceneDerivedPathOf, sceneShownToPose } from './derivedPath';\nimport {
|
|
1
|
+
{"version":3,"file":"sceneSlotWiring.js","names":[],"sources":["../../src/canvas/sceneSlotWiring.ts"],"sourcesContent":["/**\n * What the scene slot needs from the `Scene` that `<Canvas>` cannot see.\n *\n * `<Canvas>` is scene-agnostic: it walks an adapter. Five things a node is\n * painted with are therefore invisible to it — an ephemeral override's alpha\n * and data, a derived path, which needs other nodes' poses to resolve, the\n * paint an `'inherit'` field takes from its containers, and animated vertex\n * colors and stroke trims, which live in an animator. `<SceneCanvas>` folds them into the slot\n * here; `buildSceneViewCommands` does the same for the headless walk.\n */\nimport type { SceneSlotConfig } from './buildSceneLayer';\nimport type { Node, NodeId, Scene } from 'core/scene/types';\nimport { withInheritedPaintBounds, withInheritedPaints } from './inheritedPaints';\nimport { withDerivedPaths, sceneDerivedPathOf, sceneShownToPose } from './derivedPath';\nimport { withPaintOverrides, type PaintOverrides } from './paintOverrides';\nimport { nodeAsShown, shownLookup } from 'core/scene/nodeAsShown';\nimport { withShownNode } from './shownNodes';\n\n/**\n * The alpha a view paints a node at: the view's own `alphaFor` times any\n * ephemeral override alpha.\n *\n * The hit path asks this too — a node painted at alpha 0 is not on screen and\n * must not claim clicks — so it is one function rather than the painter's and\n * picking's separate readings of the same two inputs.\n */\nexport function composeAlphaFor<TData, TLayer extends string, TPose>(\n scene: Scene<TData, TLayer, TPose>,\n alphaFor?: (id: string) => number,\n): (id: string) => number {\n const overrideAlphaFor = (id: string) => scene.overrides.read(id as NodeId)?.alpha ?? 1;\n return alphaFor ? (id: string) => alphaFor(id) * overrideAlphaFor(id) : overrideAlphaFor;\n}\n\nexport function wireSceneSlotToScene<TData, TLayer extends string, TPose>(\n slot: SceneSlotConfig<Node<TData, TLayer, TPose>, TPose>,\n scene: Scene<TData, TLayer, TPose>,\n alphaFor?: (id: string) => number,\n overrides?: PaintOverrides,\n): SceneSlotConfig<Node<TData, TLayer, TPose>, TPose> {\n const shown = shownLookup(scene);\n const derived = withDerivedPaths(scene, withInheritedPaints(shown, slot.drawOne), slot.toPose);\n const painted = withPaintOverrides(overrides, derived);\n const derivedPathOf = sceneDerivedPathOf(scene, slot.toPose);\n return {\n ...slot,\n ...(slot.toPose ? { toPose: sceneShownToPose(scene, slot.toPose) } : {}),\n ...(slot.paintBounds\n ? { paintBounds: withShownNode(scene.overrides, withInheritedPaintBounds(shown, slot.paintBounds)) }\n : {}),\n drawOne: withShownNode(scene.overrides, painted),\n derivedPathOf: (node) => derivedPathOf(nodeAsShown(scene.overrides, node)),\n alphaFor: composeAlphaFor(scene, alphaFor),\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;AA0BA,SAAgB,gBACd,OACA,UACwB;CACxB,MAAM,oBAAoB,OAAe,MAAM,UAAU,KAAK,EAAY,CAAC,EAAE,SAAS;CACtF,OAAO,YAAY,OAAe,SAAS,EAAE,IAAI,iBAAiB,EAAE,IAAI;AAC1E;AAEA,SAAgB,qBACd,MACA,OACA,UACA,WACoD;CACpD,MAAM,QAAQ,YAAY,KAAK;CAC/B,MAAM,UAAU,iBAAiB,OAAO,oBAAoB,OAAO,KAAK,OAAO,GAAG,KAAK,MAAM;CAC7F,MAAM,UAAU,mBAAmB,WAAW,OAAO;CACrD,MAAM,gBAAgB,mBAAmB,OAAO,KAAK,MAAM;CAC3D,OAAO;EACL,GAAG;EACH,GAAI,KAAK,SAAS,EAAE,QAAQ,iBAAiB,OAAO,KAAK,MAAM,EAAE,IAAI,CAAC;EACtE,GAAI,KAAK,cACL,EAAE,aAAa,cAAc,MAAM,WAAW,yBAAyB,OAAO,KAAK,WAAW,CAAC,EAAE,IACjG,CAAC;EACL,SAAS,cAAc,MAAM,WAAW,OAAO;EAC/C,gBAAgB,SAAS,cAAc,YAAY,MAAM,WAAW,IAAI,CAAC;EACzE,UAAU,gBAAgB,OAAO,QAAQ;CAC3C;AACF"}
|
|
@@ -4,7 +4,7 @@ import { buildSceneTree } from "./buildSceneTree.js";
|
|
|
4
4
|
import { sceneToAdapter } from "./sceneAdapter.js";
|
|
5
5
|
import { withInheritedPaintBounds, withInheritedPaints } from "./inheritedPaints.js";
|
|
6
6
|
import { sceneDerivedPathOf, withDerivedPaths } from "./derivedPath.js";
|
|
7
|
-
import {
|
|
7
|
+
import { withPaintOverrides } from "./paintOverrides.js";
|
|
8
8
|
import { paintedSceneLayers } from "./sceneLayerPaint.js";
|
|
9
9
|
import { paintMissesView } from "./paintCull.js";
|
|
10
10
|
import { paintBoundsFor } from "./defaultDrawOne.js";
|
|
@@ -46,10 +46,10 @@ function sceneAsHierarchy(scene, layers = {}) {
|
|
|
46
46
|
* Output is one group per **visible** layer, in scene-layer order (or
|
|
47
47
|
* `layers.layerOrder`), each holding one group per node on that layer.
|
|
48
48
|
*/
|
|
49
|
-
function buildSceneViewCommands(scene, view, drawOne, extraCommands, alphaFor, layers,
|
|
49
|
+
function buildSceneViewCommands(scene, view, drawOne, extraCommands, alphaFor, layers, overrides, cull) {
|
|
50
50
|
const shown = shownLookup(scene);
|
|
51
51
|
const derivedOnly = withDerivedPaths(scene, withInheritedPaints(shown, drawOne));
|
|
52
|
-
const derived =
|
|
52
|
+
const derived = withPaintOverrides(overrides, derivedOnly);
|
|
53
53
|
const derivedPathAt = sceneDerivedPathOf(scene);
|
|
54
54
|
const derivedPathOf = (node) => derivedPathAt(nodeAsShown(scene.overrides, node));
|
|
55
55
|
const wrappedDrawOne = (node, pose, v) => {
|
|
@@ -76,14 +76,15 @@ function buildSceneViewCommands(scene, view, drawOne, extraCommands, alphaFor, l
|
|
|
76
76
|
/**
|
|
77
77
|
* The commands `renderSceneToCanvas` paints for one frame — everything in
|
|
78
78
|
* `args` but the canvas. `<SceneViewCanvas>` paints these through its own
|
|
79
|
-
* renderer lease.
|
|
79
|
+
* renderer lease. Its animated paint comes as one `overrides`, which an
|
|
80
|
+
* animator is.
|
|
80
81
|
*/
|
|
81
82
|
function sceneViewFrame(args) {
|
|
82
|
-
const { scene, view, width, height, drawOne, extraCommands, alphaFor, layerVisibility, layerOrder,
|
|
83
|
+
const { scene, view, width, height, drawOne, extraCommands, alphaFor, layerVisibility, layerOrder, overrides, cull, dpr } = args;
|
|
83
84
|
return buildSceneViewCommands(scene, view, drawOne, extraCommands, alphaFor, {
|
|
84
85
|
layerVisibility,
|
|
85
86
|
layerOrder
|
|
86
|
-
},
|
|
87
|
+
}, overrides, cull ? {
|
|
87
88
|
width,
|
|
88
89
|
height,
|
|
89
90
|
dpr: resolveDpr(dpr),
|
|
@@ -97,7 +98,10 @@ function sceneViewFrame(args) {
|
|
|
97
98
|
*/
|
|
98
99
|
function renderSceneToCanvas(args) {
|
|
99
100
|
const { canvas, view, width, height, dpr } = args;
|
|
100
|
-
paintCanvas(canvas, sceneViewFrame(
|
|
101
|
+
paintCanvas(canvas, sceneViewFrame({
|
|
102
|
+
...args,
|
|
103
|
+
overrides: args
|
|
104
|
+
}), view, {
|
|
101
105
|
width,
|
|
102
106
|
height,
|
|
103
107
|
dpr
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sceneViewRender.js","names":["viewToMat3"],"sources":["../../src/canvas/sceneViewRender.ts"],"sourcesContent":["/**\n * `renderSceneToCanvas` — substrate for detached, read-only scene views\n * (`<SceneViewCanvas>` / `<MinimapCanvas>`). Given a `<canvas>`, a `Scene`,\n * a `View`, and a per-node `drawOne`, render one frame.\n */\n\nimport type { WeaselRenderer } from '@weasel-js/renderer';\nimport { cachedCanvasRenderer, paintCanvas, resolveDpr } from './canvasRenderer';\nimport { viewToMat3 } from '@weasel-js/geom';\nimport type { DrawCommand } from '@weasel-js/renderer';\nimport type { View } from '../core/viewport/view';\nimport type { Node, NodeId, Scene } from '../core/scene/types';\nimport { wrapNodeOutput } from './wrapNodeOutput';\nimport { buildSceneTree, type HierarchicalAdapter } from './buildSceneTree';\nimport { sceneToAdapter } from './sceneAdapter';\nimport { withInheritedPaintBounds, withInheritedPaints } from './inheritedPaints';\nimport { withDerivedPaths, sceneDerivedPathOf } from './derivedPath';\nimport { withColorOverrides } from './colorOverrides';\nimport type { ColorOverrideRegistry } from '@weasel-js/animator';\nimport type { SceneViewDrawOne } from './NodeShape';\nimport { paintedSceneLayers } from './sceneLayerPaint';\nimport { cullDrawCommands } from '@weasel-js/renderer';\nimport { paintMissesView, type PaintBoundsFn } from './paintCull';\nimport { paintBoundsFor } from './defaultDrawOne';\nimport { nodeAsShown, shownLookup } from 'core/scene/nodeAsShown';\nimport { withShownNode } from './shownNodes';\n\nexport type { SceneViewDrawOne } from './NodeShape';\n\n/**\n * Which scene layers a detached view paints, on top of each layer's own\n * `visible` flag. Keyed the way `<SceneCanvas>` keys its scene layers —\n * `scene:<layerId>` — so the map a main canvas takes serves its minimap too.\n */\nexport interface SceneViewLayers {\n /** `false` hides that layer in this view. It cannot show one the scene hides. */\n layerVisibility?: Record<string, boolean>;\n /** Paint order, bottom first. A listed order is the whole list: a scene\n * layer left out of it is not painted. */\n layerOrder?: readonly string[];\n}\n\n/** The view size `buildSceneViewCommands` culls to, the density it will be\n * painted at, and how it bounds a node's paint before painting it. */\nexport interface SceneViewCull<TData, TLayer extends string, TPose> {\n width: number;\n height: number;\n dpr: number;\n paintBounds?: PaintBoundsFn<Node<TData, TLayer, TPose>, TPose>;\n}\n\n/** What to draw into an existing canvas: the scene, the view, and the same\n * painting hooks `<SceneCanvas>` takes. */\nexport interface RenderSceneToCanvasArgs<TData, TLayer extends string, TPose> {\n /** The DOM canvas to paint into. The first call on a given canvas creates\n * its renderer and later calls reuse it;\n * `releaseCanvasRenderer(canvas)` frees it. */\n canvas: HTMLCanvasElement;\n scene: Scene<TData, TLayer, TPose>;\n /** Camera view applied to scene and `extraCommands` alike. */\n view: View;\n /** CSS-pixel size. The drawing buffer is set to `width × dpr` and\n * `height × dpr`; the element's CSS size is the caller's to set. */\n width: number;\n height: number;\n drawOne: SceneViewDrawOne<TData, TLayer, TPose>;\n /** Commands appended after the scene-node output, wrapped in the same view\n * transform as scene commands. */\n extraCommands?: DrawCommand[];\n /** Per-id alpha multiplier, mirroring `<SceneCanvas>`'s scene-slot\n * `alphaFor`. Defaults to `() => 1`. */\n alphaFor?: (id: string) => number;\n /** Per-view layer visibility. See {@link SceneViewLayers}. */\n layerVisibility?: SceneViewLayers['layerVisibility'];\n /** Per-view layer order. See {@link SceneViewLayers}. */\n layerOrder?: SceneViewLayers['layerOrder'];\n /** Animated vertex colors to paint, typically an animator's\n * `colorOverrides` — see `NodePaintCtx.vertexColors`. */\n colorOverrides?: ColorOverrideRegistry;\n /** Skip what cannot reach the `width × height` view: nodes `paintBounds`\n * places outside it are not painted, and scene commands that cannot reach\n * it are dropped before the renderer — the scene slot's `cull`. Off by\n * default. `extraCommands` are never culled. */\n cull?: boolean;\n /** Where `drawOne`'s output for a node can reach, as on the scene slot.\n * Defaults to `defaultPaintBounds` when `drawOne` is `defaultDrawOne`. */\n paintBounds?: PaintBoundsFn<Node<TData, TLayer, TPose>, TPose>;\n /** Device-pixel ratio. Defaults to `window.devicePixelRatio || 1` when\n * available, otherwise 1. */\n dpr?: number;\n}\n\n/** Test seam: the renderer `renderSceneToCanvas` created for a canvas. Not\n * re-exported from the barrel. */\nexport function __getCachedRendererForTest(\n canvas: HTMLCanvasElement,\n): WeaselRenderer | undefined {\n return cachedCanvasRenderer(canvas);\n}\n\n/**\n * Present a `Scene` as the reading surface `buildSceneTree` walks: the scene\n * adapter's own reads, under this view's layer list.\n */\nfunction sceneAsHierarchy<TData, TLayer extends string, TPose>(\n scene: Scene<TData, TLayer, TPose>,\n layers: SceneViewLayers = {},\n): HierarchicalAdapter<Node<TData, TLayer, TPose>, TPose> {\n const { getNode, getChildren, getPose, composePose, childFrame } = sceneToAdapter(scene);\n return {\n getLayers: () =>\n paintedSceneLayers(scene.layers, layers)\n .map((l) => ({ id: l.id, visible: l.visible, parallax: l.parallax })),\n getNode,\n getChildren,\n getPose,\n ...(composePose ? { composePose } : {}),\n ...(childFrame ? { childFrame } : {}),\n };\n}\n\n/**\n * Build the DrawCommand list that `renderSceneToCanvas` would dispatch, in\n * screen space. Pure — no GL, no DOM. Walks the scene through `buildSceneTree`,\n * the same walk `<SceneCanvas>` uses, which is what keeps hidden layers\n * unpainted and container clips applied here.\n *\n * Each node's commands go through `wrapNodeOutput` — pose rotation, then the\n * `alphaFor` multiplier — so a `drawOne` that rotates its own output rotates\n * twice; emit unrotated geometry and let the pose drive it.\n *\n * Output is one group per **visible** layer, in scene-layer order (or\n * `layers.layerOrder`), each holding one group per node on that layer.\n */\nexport function buildSceneViewCommands<TData, TLayer extends string, TPose>(\n scene: Scene<TData, TLayer, TPose>,\n view: View,\n drawOne: SceneViewDrawOne<TData, TLayer, TPose>,\n extraCommands?: ReadonlyArray<DrawCommand>,\n alphaFor?: (id: string) => number,\n /** This view's own layer visibility and order. */\n layers?: SceneViewLayers,\n /** Animated vertex colors to paint, typically an animator's `colorOverrides`. */\n colorOverrides?: ColorOverrideRegistry,\n /** Cull the scene to this CSS-pixel view size — see\n * `RenderSceneToCanvasArgs.cull`. Omit to paint every node. */\n cull?: SceneViewCull<TData, TLayer, TPose>,\n): DrawCommand[] {\n const shown = shownLookup(scene);\n const derivedOnly = withDerivedPaths(scene, withInheritedPaints(shown, drawOne));\n const derived = colorOverrides ? withColorOverrides(colorOverrides, derivedOnly) : derivedOnly;\n const derivedPathAt = sceneDerivedPathOf(scene);\n const derivedPathOf = (node: Node<TData, TLayer, TPose>) => derivedPathAt(nodeAsShown(scene.overrides, node));\n const wrappedDrawOne = (\n node: Node<TData, TLayer, TPose>,\n pose: TPose,\n v: View,\n ): DrawCommand[] => {\n const overrideAlpha = scene.overrides.read(node.id as NodeId)?.alpha ?? 1;\n return wrapNodeOutput(\n derived(nodeAsShown(scene.overrides, node), pose, v),\n pose,\n (alphaFor ? alphaFor(node.id) : 1) * overrideAlpha,\n );\n };\n\n const hierarchy = sceneAsHierarchy(scene, layers);\n\n const cullRect = cull ? { x: 0, y: 0, width: cull.width, height: cull.height } : undefined;\n const culled = cullRect && cull?.paintBounds\n ? paintMissesView(withShownNode(scene.overrides, withInheritedPaintBounds(shown, cull.paintBounds)), view, cullRect)\n : undefined;\n const tree: DrawCommand[] = buildSceneTree(\n hierarchy as Parameters<typeof buildSceneTree>[0],\n wrappedDrawOne as unknown as Parameters<typeof buildSceneTree>[1],\n view,\n undefined,\n derivedPathOf as unknown as Parameters<typeof buildSceneTree>[4],\n culled as Parameters<typeof buildSceneTree>[5],\n );\n const children = cull && cullRect ? cullDrawCommands(tree, viewToMat3(view), cullRect, cull.dpr) : tree;\n if (extraCommands && extraCommands.length > 0) {\n for (const cmd of extraCommands) children.push(cmd);\n }\n return [{\n kind: 'group',\n transform: viewToMat3(view),\n children,\n }];\n}\n\n/**\n * The commands `renderSceneToCanvas` paints for one frame — everything in\n * `args` but the canvas. `<SceneViewCanvas>` paints these through its own\n * renderer lease.\n */\nexport function sceneViewFrame<TData, TLayer extends string, TPose>(\n args: Omit<RenderSceneToCanvasArgs<TData, TLayer, TPose>, 'canvas'>,\n): DrawCommand[] {\n const {\n scene, view, width, height, drawOne, extraCommands, alphaFor, layerVisibility, layerOrder,\n colorOverrides, cull, dpr,\n } = args;\n return buildSceneViewCommands(\n scene, view, drawOne, extraCommands, alphaFor, { layerVisibility, layerOrder }, colorOverrides,\n cull ? { width, height, dpr: resolveDpr(dpr), paintBounds: args.paintBounds ?? paintBoundsFor(drawOne) } : undefined,\n );\n}\n\n/**\n * Render one frame of `scene` at `view` into `canvas`. Where no WebGL2\n * context is available (jsdom) the call paints nothing. The canvas's renderer\n * lives until `releaseCanvasRenderer(canvas)`.\n */\nexport function renderSceneToCanvas<TData, TLayer extends string, TPose>(\n args: RenderSceneToCanvasArgs<TData, TLayer, TPose>,\n): void {\n const { canvas, view, width, height, dpr } = args;\n paintCanvas(canvas, sceneViewFrame(args), view, { width, height, dpr });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAwGA,SAAS,iBACP,OACA,SAA0B,CAAC,GAC6B;CACxD,MAAM,EAAE,SAAS,aAAa,SAAS,aAAa,eAAe,eAAe,KAAK;CACvF,OAAO;EACL,iBACE,mBAAmB,MAAM,QAAQ,MAAM,CAAC,CACrC,KAAK,OAAO;GAAE,IAAI,EAAE;GAAI,SAAS,EAAE;GAAS,UAAU,EAAE;EAAS,EAAE;EACxE;EACA;EACA;EACA,GAAI,cAAc,EAAE,YAAY,IAAI,CAAC;EACrC,GAAI,aAAa,EAAE,WAAW,IAAI,CAAC;CACrC;AACF;;;;;;;;;;;;;;AAeA,SAAgB,uBACd,OACA,MACA,SACA,eACA,UAEA,QAEA,gBAGA,MACe;CACf,MAAM,QAAQ,YAAY,KAAK;CAC/B,MAAM,cAAc,iBAAiB,OAAO,oBAAoB,OAAO,OAAO,CAAC;CAC/E,MAAM,UAAU,iBAAiB,mBAAmB,gBAAgB,WAAW,IAAI;CACnF,MAAM,gBAAgB,mBAAmB,KAAK;CAC9C,MAAM,iBAAiB,SAAqC,cAAc,YAAY,MAAM,WAAW,IAAI,CAAC;CAC5G,MAAM,kBACJ,MACA,MACA,MACkB;EAClB,MAAM,gBAAgB,MAAM,UAAU,KAAK,KAAK,EAAY,CAAC,EAAE,SAAS;EACxE,OAAO,eACL,QAAQ,YAAY,MAAM,WAAW,IAAI,GAAG,MAAM,CAAC,GACnD,OACC,WAAW,SAAS,KAAK,EAAE,IAAI,KAAK,aACvC;CACF;CAEA,MAAM,YAAY,iBAAiB,OAAO,MAAM;CAEhD,MAAM,WAAW,OAAO;EAAE,GAAG;EAAG,GAAG;EAAG,OAAO,KAAK;EAAO,QAAQ,KAAK;CAAO,IAAI;CACjF,MAAM,SAAS,YAAY,MAAM,cAC7B,gBAAgB,cAAc,MAAM,WAAW,yBAAyB,OAAO,KAAK,WAAW,CAAC,GAAG,MAAM,QAAQ,IACjH;CACJ,MAAM,OAAsB,eAC1B,WACA,gBACA,MACA,QACA,eACA,MACF;CACA,MAAM,WAAW,QAAQ,WAAW,iBAAiB,MAAMA,aAAW,IAAI,GAAG,UAAU,KAAK,GAAG,IAAI;CACnG,IAAI,iBAAiB,cAAc,SAAS,GAC1C,KAAK,MAAM,OAAO,eAAe,SAAS,KAAK,GAAG;CAEpD,OAAO,CAAC;EACN,MAAM;EACN,WAAWA,aAAW,IAAI;EAC1B;CACF,CAAC;AACH;;;;;;AAOA,SAAgB,eACd,MACe;CACf,MAAM,EACJ,OAAO,MAAM,OAAO,QAAQ,SAAS,eAAe,UAAU,iBAAiB,YAC/E,gBAAgB,MAAM,QACpB;CACJ,OAAO,uBACL,OAAO,MAAM,SAAS,eAAe,UAAU;EAAE;EAAiB;CAAW,GAAG,gBAChF,OAAO;EAAE;EAAO;EAAQ,KAAK,WAAW,GAAG;EAAG,aAAa,KAAK,eAAe,eAAe,OAAO;CAAE,IAAI,MAC7G;AACF;;;;;;AAOA,SAAgB,oBACd,MACM;CACN,MAAM,EAAE,QAAQ,MAAM,OAAO,QAAQ,QAAQ;CAC7C,YAAY,QAAQ,eAAe,IAAI,GAAG,MAAM;EAAE;EAAO;EAAQ;CAAI,CAAC;AACxE"}
|
|
1
|
+
{"version":3,"file":"sceneViewRender.js","names":["viewToMat3"],"sources":["../../src/canvas/sceneViewRender.ts"],"sourcesContent":["/**\n * `renderSceneToCanvas` — substrate for detached, read-only scene views\n * (`<SceneViewCanvas>` / `<MinimapCanvas>`). Given a `<canvas>`, a `Scene`,\n * a `View`, and a per-node `drawOne`, render one frame.\n */\n\nimport type { WeaselRenderer } from '@weasel-js/renderer';\nimport { cachedCanvasRenderer, paintCanvas, resolveDpr } from './canvasRenderer';\nimport { viewToMat3 } from '@weasel-js/geom';\nimport type { DrawCommand } from '@weasel-js/renderer';\nimport type { View } from '../core/viewport/view';\nimport type { Node, NodeId, Scene } from '../core/scene/types';\nimport { wrapNodeOutput } from './wrapNodeOutput';\nimport { buildSceneTree, type HierarchicalAdapter } from './buildSceneTree';\nimport { sceneToAdapter } from './sceneAdapter';\nimport { withInheritedPaintBounds, withInheritedPaints } from './inheritedPaints';\nimport { withDerivedPaths, sceneDerivedPathOf } from './derivedPath';\nimport { withPaintOverrides, type PaintOverrides } from './paintOverrides';\nimport type { SceneViewDrawOne } from './NodeShape';\nimport { paintedSceneLayers } from './sceneLayerPaint';\nimport { cullDrawCommands } from '@weasel-js/renderer';\nimport { paintMissesView, type PaintBoundsFn } from './paintCull';\nimport { paintBoundsFor } from './defaultDrawOne';\nimport { nodeAsShown, shownLookup } from 'core/scene/nodeAsShown';\nimport { withShownNode } from './shownNodes';\n\nexport type { SceneViewDrawOne } from './NodeShape';\n\n/**\n * Which scene layers a detached view paints, on top of each layer's own\n * `visible` flag. Keyed the way `<SceneCanvas>` keys its scene layers —\n * `scene:<layerId>` — so the map a main canvas takes serves its minimap too.\n */\nexport interface SceneViewLayers {\n /** `false` hides that layer in this view. It cannot show one the scene hides. */\n layerVisibility?: Record<string, boolean>;\n /** Paint order, bottom first. A listed order is the whole list: a scene\n * layer left out of it is not painted. */\n layerOrder?: readonly string[];\n}\n\n/** The view size `buildSceneViewCommands` culls to, the density it will be\n * painted at, and how it bounds a node's paint before painting it. */\nexport interface SceneViewCull<TData, TLayer extends string, TPose> {\n width: number;\n height: number;\n dpr: number;\n paintBounds?: PaintBoundsFn<Node<TData, TLayer, TPose>, TPose>;\n}\n\n/** What to draw into an existing canvas: the scene, the view, and the same\n * painting hooks `<SceneCanvas>` takes, its animated paint among them. */\nexport interface RenderSceneToCanvasArgs<TData, TLayer extends string, TPose> extends PaintOverrides {\n /** The DOM canvas to paint into. The first call on a given canvas creates\n * its renderer and later calls reuse it;\n * `releaseCanvasRenderer(canvas)` frees it. */\n canvas: HTMLCanvasElement;\n scene: Scene<TData, TLayer, TPose>;\n /** Camera view applied to scene and `extraCommands` alike. */\n view: View;\n /** CSS-pixel size. The drawing buffer is set to `width × dpr` and\n * `height × dpr`; the element's CSS size is the caller's to set. */\n width: number;\n height: number;\n drawOne: SceneViewDrawOne<TData, TLayer, TPose>;\n /** Commands appended after the scene-node output, wrapped in the same view\n * transform as scene commands. */\n extraCommands?: DrawCommand[];\n /** Per-id alpha multiplier, mirroring `<SceneCanvas>`'s scene-slot\n * `alphaFor`. Defaults to `() => 1`. */\n alphaFor?: (id: string) => number;\n /** Per-view layer visibility. See {@link SceneViewLayers}. */\n layerVisibility?: SceneViewLayers['layerVisibility'];\n /** Per-view layer order. See {@link SceneViewLayers}. */\n layerOrder?: SceneViewLayers['layerOrder'];\n /** Skip what cannot reach the `width × height` view: nodes `paintBounds`\n * places outside it are not painted, and scene commands that cannot reach\n * it are dropped before the renderer — the scene slot's `cull`. Off by\n * default. `extraCommands` are never culled. */\n cull?: boolean;\n /** Where `drawOne`'s output for a node can reach, as on the scene slot.\n * Defaults to `defaultPaintBounds` when `drawOne` is `defaultDrawOne`. */\n paintBounds?: PaintBoundsFn<Node<TData, TLayer, TPose>, TPose>;\n /** Device-pixel ratio. Defaults to `window.devicePixelRatio || 1` when\n * available, otherwise 1. */\n dpr?: number;\n}\n\n/** Test seam: the renderer `renderSceneToCanvas` created for a canvas. Not\n * re-exported from the barrel. */\nexport function __getCachedRendererForTest(\n canvas: HTMLCanvasElement,\n): WeaselRenderer | undefined {\n return cachedCanvasRenderer(canvas);\n}\n\n/**\n * Present a `Scene` as the reading surface `buildSceneTree` walks: the scene\n * adapter's own reads, under this view's layer list.\n */\nfunction sceneAsHierarchy<TData, TLayer extends string, TPose>(\n scene: Scene<TData, TLayer, TPose>,\n layers: SceneViewLayers = {},\n): HierarchicalAdapter<Node<TData, TLayer, TPose>, TPose> {\n const { getNode, getChildren, getPose, composePose, childFrame } = sceneToAdapter(scene);\n return {\n getLayers: () =>\n paintedSceneLayers(scene.layers, layers)\n .map((l) => ({ id: l.id, visible: l.visible, parallax: l.parallax })),\n getNode,\n getChildren,\n getPose,\n ...(composePose ? { composePose } : {}),\n ...(childFrame ? { childFrame } : {}),\n };\n}\n\n/**\n * Build the DrawCommand list that `renderSceneToCanvas` would dispatch, in\n * screen space. Pure — no GL, no DOM. Walks the scene through `buildSceneTree`,\n * the same walk `<SceneCanvas>` uses, which is what keeps hidden layers\n * unpainted and container clips applied here.\n *\n * Each node's commands go through `wrapNodeOutput` — pose rotation, then the\n * `alphaFor` multiplier — so a `drawOne` that rotates its own output rotates\n * twice; emit unrotated geometry and let the pose drive it.\n *\n * Output is one group per **visible** layer, in scene-layer order (or\n * `layers.layerOrder`), each holding one group per node on that layer.\n */\nexport function buildSceneViewCommands<TData, TLayer extends string, TPose>(\n scene: Scene<TData, TLayer, TPose>,\n view: View,\n drawOne: SceneViewDrawOne<TData, TLayer, TPose>,\n extraCommands?: ReadonlyArray<DrawCommand>,\n alphaFor?: (id: string) => number,\n /** This view's own layer visibility and order. */\n layers?: SceneViewLayers,\n /** Animated paint to lay over the nodes, typically an animator. */\n overrides?: PaintOverrides,\n /** Cull the scene to this CSS-pixel view size — see\n * `RenderSceneToCanvasArgs.cull`. Omit to paint every node. */\n cull?: SceneViewCull<TData, TLayer, TPose>,\n): DrawCommand[] {\n const shown = shownLookup(scene);\n const derivedOnly = withDerivedPaths(scene, withInheritedPaints(shown, drawOne));\n const derived = withPaintOverrides(overrides, derivedOnly);\n const derivedPathAt = sceneDerivedPathOf(scene);\n const derivedPathOf = (node: Node<TData, TLayer, TPose>) => derivedPathAt(nodeAsShown(scene.overrides, node));\n const wrappedDrawOne = (\n node: Node<TData, TLayer, TPose>,\n pose: TPose,\n v: View,\n ): DrawCommand[] => {\n const overrideAlpha = scene.overrides.read(node.id as NodeId)?.alpha ?? 1;\n return wrapNodeOutput(\n derived(nodeAsShown(scene.overrides, node), pose, v),\n pose,\n (alphaFor ? alphaFor(node.id) : 1) * overrideAlpha,\n );\n };\n\n const hierarchy = sceneAsHierarchy(scene, layers);\n\n const cullRect = cull ? { x: 0, y: 0, width: cull.width, height: cull.height } : undefined;\n const culled = cullRect && cull?.paintBounds\n ? paintMissesView(withShownNode(scene.overrides, withInheritedPaintBounds(shown, cull.paintBounds)), view, cullRect)\n : undefined;\n const tree: DrawCommand[] = buildSceneTree(\n hierarchy as Parameters<typeof buildSceneTree>[0],\n wrappedDrawOne as unknown as Parameters<typeof buildSceneTree>[1],\n view,\n undefined,\n derivedPathOf as unknown as Parameters<typeof buildSceneTree>[4],\n culled as Parameters<typeof buildSceneTree>[5],\n );\n const children = cull && cullRect ? cullDrawCommands(tree, viewToMat3(view), cullRect, cull.dpr) : tree;\n if (extraCommands && extraCommands.length > 0) {\n for (const cmd of extraCommands) children.push(cmd);\n }\n return [{\n kind: 'group',\n transform: viewToMat3(view),\n children,\n }];\n}\n\n/**\n * The commands `renderSceneToCanvas` paints for one frame — everything in\n * `args` but the canvas. `<SceneViewCanvas>` paints these through its own\n * renderer lease. Its animated paint comes as one `overrides`, which an\n * animator is.\n */\nexport function sceneViewFrame<TData, TLayer extends string, TPose>(\n args: Omit<RenderSceneToCanvasArgs<TData, TLayer, TPose>, 'canvas' | keyof PaintOverrides>\n & { overrides?: PaintOverrides },\n): DrawCommand[] {\n const {\n scene, view, width, height, drawOne, extraCommands, alphaFor, layerVisibility, layerOrder,\n overrides, cull, dpr,\n } = args;\n return buildSceneViewCommands(\n scene, view, drawOne, extraCommands, alphaFor, { layerVisibility, layerOrder }, overrides,\n cull ? { width, height, dpr: resolveDpr(dpr), paintBounds: args.paintBounds ?? paintBoundsFor(drawOne) } : undefined,\n );\n}\n\n/**\n * Render one frame of `scene` at `view` into `canvas`. Where no WebGL2\n * context is available (jsdom) the call paints nothing. The canvas's renderer\n * lives until `releaseCanvasRenderer(canvas)`.\n */\nexport function renderSceneToCanvas<TData, TLayer extends string, TPose>(\n args: RenderSceneToCanvasArgs<TData, TLayer, TPose>,\n): void {\n const { canvas, view, width, height, dpr } = args;\n paintCanvas(canvas, sceneViewFrame({ ...args, overrides: args }), view, { width, height, dpr });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAoGA,SAAS,iBACP,OACA,SAA0B,CAAC,GAC6B;CACxD,MAAM,EAAE,SAAS,aAAa,SAAS,aAAa,eAAe,eAAe,KAAK;CACvF,OAAO;EACL,iBACE,mBAAmB,MAAM,QAAQ,MAAM,CAAC,CACrC,KAAK,OAAO;GAAE,IAAI,EAAE;GAAI,SAAS,EAAE;GAAS,UAAU,EAAE;EAAS,EAAE;EACxE;EACA;EACA;EACA,GAAI,cAAc,EAAE,YAAY,IAAI,CAAC;EACrC,GAAI,aAAa,EAAE,WAAW,IAAI,CAAC;CACrC;AACF;;;;;;;;;;;;;;AAeA,SAAgB,uBACd,OACA,MACA,SACA,eACA,UAEA,QAEA,WAGA,MACe;CACf,MAAM,QAAQ,YAAY,KAAK;CAC/B,MAAM,cAAc,iBAAiB,OAAO,oBAAoB,OAAO,OAAO,CAAC;CAC/E,MAAM,UAAU,mBAAmB,WAAW,WAAW;CACzD,MAAM,gBAAgB,mBAAmB,KAAK;CAC9C,MAAM,iBAAiB,SAAqC,cAAc,YAAY,MAAM,WAAW,IAAI,CAAC;CAC5G,MAAM,kBACJ,MACA,MACA,MACkB;EAClB,MAAM,gBAAgB,MAAM,UAAU,KAAK,KAAK,EAAY,CAAC,EAAE,SAAS;EACxE,OAAO,eACL,QAAQ,YAAY,MAAM,WAAW,IAAI,GAAG,MAAM,CAAC,GACnD,OACC,WAAW,SAAS,KAAK,EAAE,IAAI,KAAK,aACvC;CACF;CAEA,MAAM,YAAY,iBAAiB,OAAO,MAAM;CAEhD,MAAM,WAAW,OAAO;EAAE,GAAG;EAAG,GAAG;EAAG,OAAO,KAAK;EAAO,QAAQ,KAAK;CAAO,IAAI;CACjF,MAAM,SAAS,YAAY,MAAM,cAC7B,gBAAgB,cAAc,MAAM,WAAW,yBAAyB,OAAO,KAAK,WAAW,CAAC,GAAG,MAAM,QAAQ,IACjH;CACJ,MAAM,OAAsB,eAC1B,WACA,gBACA,MACA,QACA,eACA,MACF;CACA,MAAM,WAAW,QAAQ,WAAW,iBAAiB,MAAMA,aAAW,IAAI,GAAG,UAAU,KAAK,GAAG,IAAI;CACnG,IAAI,iBAAiB,cAAc,SAAS,GAC1C,KAAK,MAAM,OAAO,eAAe,SAAS,KAAK,GAAG;CAEpD,OAAO,CAAC;EACN,MAAM;EACN,WAAWA,aAAW,IAAI;EAC1B;CACF,CAAC;AACH;;;;;;;AAQA,SAAgB,eACd,MAEe;CACf,MAAM,EACJ,OAAO,MAAM,OAAO,QAAQ,SAAS,eAAe,UAAU,iBAAiB,YAC/E,WAAW,MAAM,QACf;CACJ,OAAO,uBACL,OAAO,MAAM,SAAS,eAAe,UAAU;EAAE;EAAiB;CAAW,GAAG,WAChF,OAAO;EAAE;EAAO;EAAQ,KAAK,WAAW,GAAG;EAAG,aAAa,KAAK,eAAe,eAAe,OAAO;CAAE,IAAI,MAC7G;AACF;;;;;;AAOA,SAAgB,oBACd,MACM;CACN,MAAM,EAAE,QAAQ,MAAM,OAAO,QAAQ,QAAQ;CAC7C,YAAY,QAAQ,eAAe;EAAE,GAAG;EAAM,WAAW;CAAK,CAAC,GAAG,MAAM;EAAE;EAAO;EAAQ;CAAI,CAAC;AAChG"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { pointRegionLive, pointRegionScreenQuad, transformOf } from "../affordances/hitAffordanceRegions.js";
|
|
2
2
|
import { chromeStateFrom, isVisibleFrom } from "./drawEnvelope.js";
|
|
3
|
-
import { PATH_L, PATH_M, PATH_Z } from "@weasel-js/geom";
|
|
3
|
+
import { PATH_L, PATH_M, PATH_Z, viewWorldToScreen } from "@weasel-js/geom";
|
|
4
4
|
|
|
5
5
|
// src/canvas/slopsDebugLayer.ts
|
|
6
6
|
var SLOP_FILL = "rgba(255, 80, 140, 0.18)";
|
|
@@ -14,7 +14,13 @@ function createSlopsDebugLayer(opts) {
|
|
|
14
14
|
const chrome = chromeStateFrom(data);
|
|
15
15
|
if (!chrome) return [];
|
|
16
16
|
const isVisible = isVisibleFrom(data);
|
|
17
|
-
const toScreen = (x, y) =>
|
|
17
|
+
const toScreen = (x, y) => {
|
|
18
|
+
const p = viewWorldToScreen(view, {
|
|
19
|
+
x,
|
|
20
|
+
y
|
|
21
|
+
});
|
|
22
|
+
return [p.x, p.y];
|
|
23
|
+
};
|
|
18
24
|
const out = [];
|
|
19
25
|
for (const a of opts.getAffordances()) {
|
|
20
26
|
if (isVisible && !isVisible(a.id)) continue;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slopsDebugLayer.js","names":[],"sources":["../../src/canvas/slopsDebugLayer.ts"],"sourcesContent":["/**\n * Screen-space debug overlay rendering the kit's hit-test \"slops\" — the\n * forgiveness regions around each point affordance (corner resize handles,\n * the rotate badge, anchor / control-handle markers). Off by default; gated\n * by SceneCanvas's `debug.slops` prop.\n *\n * It walks the same affordances the hit-test does, and draws each point\n * region's square from the same frame the hit-test reads, so a halo shows\n * exactly the pixels that grab.\n */\n\nimport type { DrawCommand } from '@weasel-js/renderer';\nimport type { RenderLayer } from 'core/layers/render';\nimport type { Affordance } from 'affordances/types';\nimport { pointRegionLive, pointRegionScreenQuad, transformOf } from 'affordances/hitAffordanceRegions';\nimport { PATH_L, PATH_M, PATH_Z } from '@weasel-js/geom';\nimport { chromeStateFrom, isVisibleFrom } from './drawEnvelope';\n\nexport interface CreateSlopsDebugLayerOptions {\n /** The affordances the canvas hit-tests — `chromeAffordances`, the same\n * list handed to `buildAffordanceAt`. */\n getAffordances: () => readonly Affordance[];\n}\n\nconst SLOP_FILL = 'rgba(255, 80, 140, 0.18)';\nconst SLOP_STROKE = 'rgba(255, 80, 140, 0.55)';\n\nexport function createSlopsDebugLayer(\n opts: CreateSlopsDebugLayerOptions,\n): RenderLayer<unknown> {\n return {\n id: 'slops-debug',\n label: 'Slops (debug)',\n space: 'screen',\n draw: (data, view) => {\n // Selection, bounds and visibility come off the envelope: the halos\n // have to sit on the chrome the drawing view actually painted.\n const chrome = chromeStateFrom(data);\n if (!chrome) return [];\n const isVisible = isVisibleFrom(data);\n const toScreen = (x: number, y: number): [number, number]
|
|
1
|
+
{"version":3,"file":"slopsDebugLayer.js","names":[],"sources":["../../src/canvas/slopsDebugLayer.ts"],"sourcesContent":["/**\n * Screen-space debug overlay rendering the kit's hit-test \"slops\" — the\n * forgiveness regions around each point affordance (corner resize handles,\n * the rotate badge, anchor / control-handle markers). Off by default; gated\n * by SceneCanvas's `debug.slops` prop.\n *\n * It walks the same affordances the hit-test does, and draws each point\n * region's square from the same frame the hit-test reads, so a halo shows\n * exactly the pixels that grab.\n */\n\nimport type { DrawCommand } from '@weasel-js/renderer';\nimport type { RenderLayer } from 'core/layers/render';\nimport type { Affordance } from 'affordances/types';\nimport { pointRegionLive, pointRegionScreenQuad, transformOf } from 'affordances/hitAffordanceRegions';\nimport { PATH_L, PATH_M, PATH_Z, viewWorldToScreen } from '@weasel-js/geom';\nimport { chromeStateFrom, isVisibleFrom } from './drawEnvelope';\n\nexport interface CreateSlopsDebugLayerOptions {\n /** The affordances the canvas hit-tests — `chromeAffordances`, the same\n * list handed to `buildAffordanceAt`. */\n getAffordances: () => readonly Affordance[];\n}\n\nconst SLOP_FILL = 'rgba(255, 80, 140, 0.18)';\nconst SLOP_STROKE = 'rgba(255, 80, 140, 0.55)';\n\nexport function createSlopsDebugLayer(\n opts: CreateSlopsDebugLayerOptions,\n): RenderLayer<unknown> {\n return {\n id: 'slops-debug',\n label: 'Slops (debug)',\n space: 'screen',\n draw: (data, view) => {\n // Selection, bounds and visibility come off the envelope: the halos\n // have to sit on the chrome the drawing view actually painted.\n const chrome = chromeStateFrom(data);\n if (!chrome) return [];\n const isVisible = isVisibleFrom(data);\n const toScreen = (x: number, y: number): [number, number] => {\n const p = viewWorldToScreen(view, { x, y });\n return [p.x, p.y];\n };\n const out: DrawCommand[] = [];\n for (const a of opts.getAffordances()) {\n if (isVisible && !isVisible(a.id)) continue;\n for (const region of a.regions(chrome)) {\n if (region.shape.kind !== 'point') continue;\n const xf = transformOf(chrome, region.targetId);\n if (!pointRegionLive(region.shape, xf, view)) continue;\n const q = pointRegionScreenQuad(region.shape, xf, view, toScreen);\n out.push({\n kind: 'path',\n path: {\n kind: 'polygon',\n commands: new Uint8Array([PATH_M, PATH_L, PATH_L, PATH_L, PATH_Z]),\n coords: new Float32Array([q[0].x, q[0].y, q[1].x, q[1].y, q[2].x, q[2].y, q[3].x, q[3].y]),\n fillRule: 'nonzero',\n },\n fill: { paint: { kind: 'solid', color: SLOP_FILL } },\n stroke: { paint: { kind: 'solid', color: SLOP_STROKE }, width: 1 },\n });\n }\n }\n return out;\n },\n };\n}\n"],"mappings":";;;;;AAwBA,IAAM,YAAY;AAClB,IAAM,cAAc;AAEpB,SAAgB,sBACd,MACsB;CACtB,OAAO;EACL,IAAI;EACJ,OAAO;EACP,OAAO;EACP,OAAO,MAAM,SAAS;GAGpB,MAAM,SAAS,gBAAgB,IAAI;GACnC,IAAI,CAAC,QAAQ,OAAO,CAAC;GACrB,MAAM,YAAY,cAAc,IAAI;GACpC,MAAM,YAAY,GAAW,MAAgC;IAC3D,MAAM,IAAI,kBAAkB,MAAM;KAAE;KAAG;IAAE,CAAC;IAC1C,OAAO,CAAC,EAAE,GAAG,EAAE,CAAC;GAClB;GACA,MAAM,MAAqB,CAAC;GAC5B,KAAK,MAAM,KAAK,KAAK,eAAe,GAAG;IACrC,IAAI,aAAa,CAAC,UAAU,EAAE,EAAE,GAAG;IACnC,KAAK,MAAM,UAAU,EAAE,QAAQ,MAAM,GAAG;KACtC,IAAI,OAAO,MAAM,SAAS,SAAS;KACnC,MAAM,KAAK,YAAY,QAAQ,OAAO,QAAQ;KAC9C,IAAI,CAAC,gBAAgB,OAAO,OAAO,IAAI,IAAI,GAAG;KAC9C,MAAM,IAAI,sBAAsB,OAAO,OAAO,IAAI,MAAM,QAAQ;KAChE,IAAI,KAAK;MACP,MAAM;MACN,MAAM;OACJ,MAAM;OACN,UAAU,IAAI,WAAW;QAAC;QAAQ;QAAQ;QAAQ;QAAQ;OAAM,CAAC;OACjE,QAAQ,IAAI,aAAa;QAAC,EAAE,EAAE,CAAC;QAAG,EAAE,EAAE,CAAC;QAAG,EAAE,EAAE,CAAC;QAAG,EAAE,EAAE,CAAC;QAAG,EAAE,EAAE,CAAC;QAAG,EAAE,EAAE,CAAC;QAAG,EAAE,EAAE,CAAC;QAAG,EAAE,EAAE,CAAC;OAAC,CAAC;OACzF,UAAU;MACZ;MACA,MAAM,EAAE,OAAO;OAAE,MAAM;OAAS,OAAO;MAAU,EAAE;MACnD,QAAQ;OAAE,OAAO;QAAE,MAAM;QAAS,OAAO;OAAY;OAAG,OAAO;MAAE;KACnE,CAAC;IACH;GACF;GACA,OAAO;EACT;CACF;AACF"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// src/canvas/trimOverrides.ts
|
|
2
|
+
/**
|
|
3
|
+
* `base` with `override`'s edges in place of its own: each edge the override
|
|
4
|
+
* sets becomes that fraction, read by `base`'s `pace`, and an edge it leaves
|
|
5
|
+
* out stays as `base` gives it, an absolute measure included. With no base,
|
|
6
|
+
* the override alone, at the default pace.
|
|
7
|
+
*/
|
|
8
|
+
function overrideTrim(base, override) {
|
|
9
|
+
if (override === void 0 || override.start === void 0 && override.end === void 0) return base;
|
|
10
|
+
return {
|
|
11
|
+
...base,
|
|
12
|
+
...override.start !== void 0 ? { start: override.start } : {},
|
|
13
|
+
...override.end !== void 0 ? { end: override.end } : {}
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export { overrideTrim };
|
|
18
|
+
//# sourceMappingURL=trimOverrides.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trimOverrides.js","names":[],"sources":["../../src/canvas/trimOverrides.ts"],"sourcesContent":["/** How an animated trim override lays over a stroke's own trim. */\nimport type { TrimOverride } from '@weasel-js/animator';\nimport type { StrokeTrim } from '@weasel-js/paint';\n\n/**\n * `base` with `override`'s edges in place of its own: each edge the override\n * sets becomes that fraction, read by `base`'s `pace`, and an edge it leaves\n * out stays as `base` gives it, an absolute measure included. With no base,\n * the override alone, at the default pace.\n */\nexport function overrideTrim(\n base: StrokeTrim | undefined,\n override: TrimOverride | undefined,\n): StrokeTrim | undefined {\n if (override === undefined || (override.start === undefined && override.end === undefined)) return base;\n return {\n ...base,\n ...(override.start !== undefined ? { start: override.start } : {}),\n ...(override.end !== undefined ? { end: override.end } : {}),\n };\n}\n"],"mappings":";;;;;;;AAUA,SAAgB,aACd,MACA,UACwB;CACxB,IAAI,aAAa,UAAc,SAAS,UAAU,UAAa,SAAS,QAAQ,QAAY,OAAO;CACnG,OAAO;EACL,GAAG;EACH,GAAI,SAAS,UAAU,SAAY,EAAE,OAAO,SAAS,MAAM,IAAI,CAAC;EAChE,GAAI,SAAS,QAAQ,SAAY,EAAE,KAAK,SAAS,IAAI,IAAI,CAAC;CAC5D;AACF"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { viewScreenToWorld } from "@weasel-js/geom";
|
|
1
2
|
import { useEffect } from "react";
|
|
2
3
|
import { useLatest } from "@weasel-js/react";
|
|
3
4
|
|
|
@@ -23,9 +24,11 @@ function useViewRegistrySurface(viewRegistry, layers, frame, services) {
|
|
|
23
24
|
setView,
|
|
24
25
|
claimsAbove: (x, y) => {
|
|
25
26
|
const view = viewRef.current;
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
const world = viewScreenToWorld(view, {
|
|
28
|
+
x,
|
|
29
|
+
y
|
|
30
|
+
});
|
|
31
|
+
return hitTestExtrasIn(world.x, world.y, view, dimsRef.current, helpersRef.current) !== null;
|
|
29
32
|
}
|
|
30
33
|
});
|
|
31
34
|
}, [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useViewRegistrySurface.js","names":[],"sources":["../../src/canvas/useViewRegistrySurface.ts"],"sourcesContent":["import { useEffect } from 'react';\nimport { useLatest } from '@weasel-js/react';\nimport type { View } from 'core/viewport/view';\nimport type { Dims, RenderLayer } from 'core/layers/render';\nimport type { CanvasHelpers } from './useViewHelpers';\nimport type { SurfaceHandle, ViewRegistry } from './viewRegistry';\n\n/** What the canvas reads at draw and hit time that changes outside a render. */\nexport interface SurfaceFrame {\n viewRef: { readonly current: View };\n dimsRef: { readonly current: Dims };\n helpersRef: { readonly current: Pick<CanvasHelpers<unknown>, 'getChromeState'> | null };\n}\n\n/** What the canvas does for a view that asks. */\nexport interface SurfaceServices {\n requestRedraw: () => void;\n hitTestExtrasIn: SurfaceHandle['hitTestExtras'];\n clientToWorld: SurfaceHandle['clientToWorld'];\n setView: NonNullable<SurfaceHandle['setView']>;\n}\n\n/**\n * Attach this canvas to the view registry as its surface. The stack a view paints through its\n * own camera is this canvas's `layers`, before the views themselves are folded in — otherwise\n * a view would contain itself. Published by reference and read at draw time.\n */\nexport function useViewRegistrySurface(\n viewRegistry: ViewRegistry | null,\n layers: readonly RenderLayer<unknown>[],\n frame: SurfaceFrame,\n services: SurfaceServices,\n): void {\n const surfaceLayersRef = useLatest<readonly RenderLayer<unknown>[]>(layers);\n const { viewRef, dimsRef, helpersRef } = frame;\n const { requestRedraw, hitTestExtrasIn, clientToWorld, setView } = services;\n useEffect(() => {\n return viewRegistry?.attachSurface({\n view: () => viewRef.current,\n dims: () => dimsRef.current,\n layers: () => surfaceLayersRef.current,\n requestRedraw,\n chromeState: () => helpersRef.current!.getChromeState(),\n hitTestExtras: hitTestExtrasIn,\n clientToWorld,\n setView,\n claimsAbove: (x, y) => {\n const view = viewRef.current;\n const
|
|
1
|
+
{"version":3,"file":"useViewRegistrySurface.js","names":[],"sources":["../../src/canvas/useViewRegistrySurface.ts"],"sourcesContent":["import { useEffect } from 'react';\nimport { useLatest } from '@weasel-js/react';\nimport { viewScreenToWorld } from '@weasel-js/geom';\nimport type { View } from 'core/viewport/view';\nimport type { Dims, RenderLayer } from 'core/layers/render';\nimport type { CanvasHelpers } from './useViewHelpers';\nimport type { SurfaceHandle, ViewRegistry } from './viewRegistry';\n\n/** What the canvas reads at draw and hit time that changes outside a render. */\nexport interface SurfaceFrame {\n viewRef: { readonly current: View };\n dimsRef: { readonly current: Dims };\n helpersRef: { readonly current: Pick<CanvasHelpers<unknown>, 'getChromeState'> | null };\n}\n\n/** What the canvas does for a view that asks. */\nexport interface SurfaceServices {\n requestRedraw: () => void;\n hitTestExtrasIn: SurfaceHandle['hitTestExtras'];\n clientToWorld: SurfaceHandle['clientToWorld'];\n setView: NonNullable<SurfaceHandle['setView']>;\n}\n\n/**\n * Attach this canvas to the view registry as its surface. The stack a view paints through its\n * own camera is this canvas's `layers`, before the views themselves are folded in — otherwise\n * a view would contain itself. Published by reference and read at draw time.\n */\nexport function useViewRegistrySurface(\n viewRegistry: ViewRegistry | null,\n layers: readonly RenderLayer<unknown>[],\n frame: SurfaceFrame,\n services: SurfaceServices,\n): void {\n const surfaceLayersRef = useLatest<readonly RenderLayer<unknown>[]>(layers);\n const { viewRef, dimsRef, helpersRef } = frame;\n const { requestRedraw, hitTestExtrasIn, clientToWorld, setView } = services;\n useEffect(() => {\n return viewRegistry?.attachSurface({\n view: () => viewRef.current,\n dims: () => dimsRef.current,\n layers: () => surfaceLayersRef.current,\n requestRedraw,\n chromeState: () => helpersRef.current!.getChromeState(),\n hitTestExtras: hitTestExtrasIn,\n clientToWorld,\n setView,\n claimsAbove: (x, y) => {\n const view = viewRef.current;\n const world = viewScreenToWorld(view, { x, y });\n return hitTestExtrasIn(world.x, world.y, view, dimsRef.current, helpersRef.current) !== null;\n },\n });\n }, [viewRegistry, requestRedraw, hitTestExtrasIn, dimsRef, surfaceLayersRef, viewRef, helpersRef, clientToWorld, setView]);\n}\n"],"mappings":";;;;;;;;;;AA4BA,SAAgB,uBACd,cACA,QACA,OACA,UACM;CACN,MAAM,mBAAmB,UAA2C,MAAM;CAC1E,MAAM,EAAE,SAAS,SAAS,eAAe;CACzC,MAAM,EAAE,eAAe,iBAAiB,eAAe,YAAY;CACnE,gBAAgB;EACd,OAAO,cAAc,cAAc;GACjC,YAAY,QAAQ;GACpB,YAAY,QAAQ;GACpB,cAAc,iBAAiB;GAC/B;GACA,mBAAmB,WAAW,QAAS,eAAe;GACtD,eAAe;GACf;GACA;GACA,cAAc,GAAG,MAAM;IACrB,MAAM,OAAO,QAAQ;IACrB,MAAM,QAAQ,kBAAkB,MAAM;KAAE;KAAG;IAAE,CAAC;IAC9C,OAAO,gBAAgB,MAAM,GAAG,MAAM,GAAG,MAAM,QAAQ,SAAS,WAAW,OAAO,MAAM;GAC1F;EACF,CAAC;CACH,GAAG;EAAC;EAAc;EAAe;EAAiB;EAAS;EAAkB;EAAS;EAAY;EAAe;CAAO,CAAC;AAC3H"}
|