@weasel-js/core 1.14.1 → 1.15.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 +218 -0
- package/README.md +1 -1
- package/dist/affordances/pathAnchors.js +0 -1
- package/dist/affordances/pathAnchors.js.map +1 -1
- package/dist/barrel/paths.js +1 -1
- package/dist/barrel/renderer.js +2 -2
- package/dist/barrel/routing.js +6 -4
- package/dist/barrel/text.js +5 -3
- package/dist/canvas/Canvas.js +5 -1
- package/dist/canvas/Canvas.js.map +1 -1
- package/dist/canvas/DrawCanvas.js +5 -1
- package/dist/canvas/DrawCanvas.js.map +1 -1
- package/dist/canvas/SceneCanvas/SceneCanvasInput.js +4 -0
- package/dist/canvas/SceneCanvas/SceneCanvasInput.js.map +1 -1
- package/dist/canvas/SceneCanvas/builtinToolSets.js +6 -3
- package/dist/canvas/SceneCanvas/builtinToolSets.js.map +1 -1
- package/dist/canvas/SceneCanvas/defaultNodeProperties.js +1 -1
- package/dist/canvas/SceneCanvas/presets/paths.js +6 -6
- package/dist/canvas/SceneCanvas/presets/paths.js.map +1 -1
- package/dist/canvas/SceneCanvas/shapeGeneratorProperties.js +1 -1
- package/dist/canvas/SceneCanvas/shapeGeneratorProperties.js.map +1 -1
- package/dist/canvas/SceneCanvas/useBuiltinTools.js +2 -0
- package/dist/canvas/SceneCanvas/useBuiltinTools.js.map +1 -1
- package/dist/canvas/SceneCanvas/useContributionRoles.js +2 -0
- package/dist/canvas/SceneCanvas/useContributionRoles.js.map +1 -1
- package/dist/canvas/SceneCanvas/useSceneChrome.js +17 -5
- package/dist/canvas/SceneCanvas/useSceneChrome.js.map +1 -1
- package/dist/canvas/SceneCanvas/useSceneStandardActions.js +3 -0
- package/dist/canvas/SceneCanvas/useSceneStandardActions.js.map +1 -1
- package/dist/canvas/SceneCanvas.js +5 -4
- package/dist/canvas/SceneCanvas.js.map +1 -1
- package/dist/canvas/SceneViewCanvas.js +5 -1
- package/dist/canvas/SceneViewCanvas.js.map +1 -1
- package/dist/canvas/affordanceAt.js +0 -1
- package/dist/canvas/affordanceAt.js.map +1 -1
- package/dist/canvas/deps/chromeRules.js +37 -0
- package/dist/canvas/deps/chromeRules.js.map +1 -0
- package/dist/canvas/deps/editAnchors.js +0 -6
- package/dist/canvas/deps/editAnchors.js.map +1 -1
- package/dist/canvas/deps/hitTestArea.js +3 -1
- package/dist/canvas/deps/hitTestArea.js.map +1 -1
- package/dist/canvas/deps/modeDefaultTool.js +23 -0
- package/dist/canvas/deps/modeDefaultTool.js.map +1 -0
- package/dist/canvas/deps/pathEditCommits.js +1 -1
- package/dist/canvas/deps/selectionSnapshot.js +23 -0
- package/dist/canvas/deps/selectionSnapshot.js.map +1 -0
- package/dist/canvas/deps/snap.js +1 -0
- package/dist/canvas/deps/snap.js.map +1 -1
- package/dist/canvas/pickReach.js +12 -8
- package/dist/canvas/pickReach.js.map +1 -1
- package/dist/canvas/pickWalk.js +7 -5
- package/dist/canvas/pickWalk.js.map +1 -1
- package/dist/canvas/scenePickSource.js +2 -2
- package/dist/canvas/scenePickSource.js.map +1 -1
- package/dist/canvas/useCanvasSurface.js +21 -3
- package/dist/canvas/useCanvasSurface.js.map +1 -1
- package/dist/canvas/useFrameLoop.js +6 -7
- package/dist/canvas/useFrameLoop.js.map +1 -1
- package/dist/canvas/useSurfaceSize.js +4 -3
- package/dist/canvas/useSurfaceSize.js.map +1 -1
- package/dist/core/geometry/regionHit.js +35 -16
- package/dist/core/geometry/regionHit.js.map +1 -1
- package/dist/core/paint/meshKindLazy.js +9 -10
- package/dist/core/paint/meshKindLazy.js.map +1 -1
- package/dist/core/scene/editMode.js +4 -1
- package/dist/core/scene/editMode.js.map +1 -1
- package/dist/core/scene/historyFacade.js +7 -50
- package/dist/core/scene/historyFacade.js.map +1 -1
- package/dist/core/scene/lockGuard.js +1 -0
- package/dist/core/scene/lockGuard.js.map +1 -1
- package/dist/core/scene/nodeGates.js +43 -23
- package/dist/core/scene/nodeGates.js.map +1 -1
- package/dist/core/scene/opTable.js +8 -8
- package/dist/core/scene/opTable.js.map +1 -1
- package/dist/core/scene/outerOps.js +2 -2
- package/dist/core/scene/outerOps.js.map +1 -1
- package/dist/core/scene/poseRemovals.js +1 -0
- package/dist/core/scene/poseRemovals.js.map +1 -1
- package/dist/core/scene/poseWrites.js +16 -15
- package/dist/core/scene/poseWrites.js.map +1 -1
- package/dist/core/scene/sceneJournals.js +78 -0
- package/dist/core/scene/sceneJournals.js.map +1 -0
- package/dist/core/scene/sceneRecording.js +3 -2
- package/dist/core/scene/sceneRecording.js.map +1 -1
- package/dist/core/scene/statedFields.js +10 -8
- package/dist/core/scene/statedFields.js.map +1 -1
- package/dist/core/scene/treeEdits.js +4 -9
- package/dist/core/scene/treeEdits.js.map +1 -1
- package/dist/core/viewport/useCanvasSize.js +47 -26
- package/dist/core/viewport/useCanvasSize.js.map +1 -1
- package/dist/{depSchema-C3BwxitB.d.ts → depSchema-ChQPzEkW.d.ts} +56 -25
- package/dist/domains/chrome-caps/resolve.js +1 -0
- package/dist/domains/chrome-caps/resolve.js.map +1 -1
- package/dist/domains/chrome-caps/useChromeCaps.js +4 -2
- package/dist/domains/chrome-caps/useChromeCaps.js.map +1 -1
- package/dist/domains/paths/pathEditState.js +0 -24
- package/dist/domains/paths/pathEditState.js.map +1 -1
- package/dist/domains/snap/probes2d.js +5 -0
- package/dist/domains/snap/probes2d.js.map +1 -1
- package/dist/domains/snap/resizeBox.js +43 -22
- package/dist/domains/snap/resizeBox.js.map +1 -1
- package/dist/domains/snap/sceneReads.js +11 -1
- package/dist/domains/snap/sceneReads.js.map +1 -1
- package/dist/domains/snap/sceneSession2d.js +4 -2
- package/dist/domains/snap/sceneSession2d.js.map +1 -1
- package/dist/domains/snap/session2d.js +14 -7
- package/dist/domains/snap/session2d.js.map +1 -1
- package/dist/domains/text/index.js +2 -2
- package/dist/icons.table.js +1 -1
- package/dist/icons.table.js.map +1 -1
- package/dist/icons.table.json +5 -0
- package/dist/index.d.ts +114 -62
- package/dist/index.js +12 -8
- package/dist/interactions/actions/booleans/action.js +4 -2
- package/dist/interactions/actions/booleans/action.js.map +1 -1
- package/dist/interactions/actions/defaults/anchorEditing.js +3 -6
- package/dist/interactions/actions/defaults/anchorEditing.js.map +1 -1
- package/dist/interactions/actions/defaults/bendSegment.js +1 -2
- package/dist/interactions/actions/defaults/bendSegment.js.map +1 -1
- package/dist/interactions/actions/defaults/exitPathEdit.js +17 -1
- package/dist/interactions/actions/defaults/exitPathEdit.js.map +1 -1
- package/dist/interactions/actions/edit-anchors/action.js +3 -4
- package/dist/interactions/actions/edit-anchors/action.js.map +1 -1
- package/dist/interactions/actions/insert/action.js +21 -2
- package/dist/interactions/actions/insert/action.js.map +1 -1
- package/dist/interactions/actions/insert/params.js +43 -0
- package/dist/interactions/actions/insert/params.js.map +1 -0
- package/dist/interactions/actions/notices.js +17 -0
- package/dist/interactions/actions/notices.js.map +1 -0
- package/dist/interactions/actions/outlines/createOutlines.js +8 -6
- package/dist/interactions/actions/outlines/createOutlines.js.map +1 -1
- package/dist/interactions/actions/outlines/createOutlinesAction.js +13 -8
- package/dist/interactions/actions/outlines/createOutlinesAction.js.map +1 -1
- package/dist/interactions/actions/outlines/outlineStrokeAction.js +3 -2
- package/dist/interactions/actions/outlines/outlineStrokeAction.js.map +1 -1
- package/dist/interactions/actions/pathCommands/pathNodes.js +1 -1
- package/dist/interactions/actions/testUtils.js +0 -7
- package/dist/interactions/actions/testUtils.js.map +1 -1
- package/dist/interactions/actions/useNotices.js +13 -0
- package/dist/interactions/actions/useNotices.js.map +1 -0
- package/dist/test-seams.d.ts +5 -12
- package/dist/tools/builtin/anchors/anchorsTool.js +31 -0
- package/dist/tools/builtin/anchors/anchorsTool.js.map +1 -0
- package/dist/tools/builtin/polygon/usePolygonTool.js +1 -1
- package/dist/tools/builtin/star/useStarTool.js +1 -1
- package/dist/tools/index.js +4 -2
- package/dist/tools/toolOptions.js +47 -0
- package/dist/tools/toolOptions.js.map +1 -0
- package/dist/version.js +1 -1
- package/package.json +25 -25
- package/dist/interactions/actions/defaults/pathEditTools.js +0 -60
- package/dist/interactions/actions/defaults/pathEditTools.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pickReach.js","names":[],"sources":["../../src/canvas/pickReach.ts"],"sourcesContent":["import type { Affine, Path } from '@weasel-js/geom';\nimport type { PickCandidate, PickQuery } from './pickWalk';\nimport type { InheritedPaint } from 'core/scene/inheritedPaint';\n\n/** What {@link reaches} needs from the walk, each memoized per walk there. */\nexport interface ReachLookups<TPose> {\n /** An ancestor's own clip, or null. */\n clipOf(ancestor: PickCandidate<TPose>): Path | null;\n /** The maps from world into the source frame of each copy `ancestor` draws:\n * the inverses of its `copiesFromPose`. Empty when it draws none. */\n copiesOf(ancestor: PickCandidate<TPose>): readonly Affine[];\n /** A clip carried from its container's plane into the node's. */\n carry(clip: Path, fromLayer: string | undefined, toLayer: string | undefined): Path;\n}\n\n/**\n * Whether `q` lands on `node` anywhere it is painted: as authored, or in any\n * copy an ancestor draws of it. `
|
|
1
|
+
{"version":3,"file":"pickReach.js","names":[],"sources":["../../src/canvas/pickReach.ts"],"sourcesContent":["import type { Affine, Path } from '@weasel-js/geom';\nimport type { PickCandidate, PickQuery } from './pickWalk';\nimport type { InheritedPaint } from 'core/scene/inheritedPaint';\n\n/** What {@link reaches} needs from the walk, each memoized per walk there. */\nexport interface ReachLookups<TPose> {\n /** An ancestor's own clip, or null. */\n clipOf(ancestor: PickCandidate<TPose>): Path | null;\n /** The maps from world into the source frame of each copy `ancestor` draws:\n * the inverses of its `copiesFromPose`. Empty when it draws none. */\n copiesOf(ancestor: PickCandidate<TPose>): readonly Affine[];\n /** A clip carried from its container's plane into the node's. */\n carry(clip: Path, fromLayer: string | undefined, toLayer: string | undefined): Path;\n}\n\n/**\n * Whether `q` lands on `node` anywhere it is painted: as authored, or in any\n * copy an ancestor draws of it. `parentsOf` answers innermost first.\n *\n * A copy is the clipped content transformed, so the query is carried into the\n * copy's source frame before the copying container's own clip is asked, and\n * every clip below it is asked in that frame too.\n *\n * `parentsOf` is called only when the answer needs it — always when\n * `copying` says some container in the walk draws copies, otherwise only\n * after the node hits as authored.\n */\nexport function reaches<TPose>(\n node: PickCandidate<TPose>,\n pose: TPose,\n derived: Path | null | undefined,\n inherited: InheritedPaint | undefined,\n q: PickQuery<TPose>,\n parentsOf: (node: PickCandidate<TPose>) => readonly PickCandidate<TPose>[],\n copying: boolean,\n look: ReachLookups<TPose>,\n): boolean {\n const parents = copying && q.inCopy !== undefined ? parentsOf(node) : null;\n if (parents === null || !parents.some((a) => look.copiesOf(a).length > 0)) {\n if (!q.hits(node, pose, derived, inherited)) return false;\n const all = parents ?? parentsOf(node);\n for (let i = 0; i < all.length; i++) if (!admitsIn(look, q, all[i]!, node, pose, derived)) return false;\n return true;\n }\n\n const admits = (query: PickQuery<TPose>, ancestor: PickCandidate<TPose>): boolean =>\n admitsIn(look, query, ancestor, node, pose, derived);\n const outermostFirst = [...parents].reverse();\n const from = (i: number, query: PickQuery<TPose>): boolean => {\n if (i === outermostFirst.length) return query.hits(node, pose, derived, inherited);\n const ancestor = outermostFirst[i];\n if (admits(query, ancestor) && from(i + 1, query)) return true;\n for (const m of look.copiesOf(ancestor)) {\n const inCopy = query.inCopy?.(m);\n if (inCopy !== undefined && admits(inCopy, ancestor) && from(i + 1, inCopy)) return true;\n }\n return false;\n };\n return from(0, q);\n}\n\n/** Whether `ancestor`'s clip, carried into `node`'s plane, leaves `query` anything of it. */\nfunction admitsIn<TPose>(\n look: ReachLookups<TPose>,\n query: PickQuery<TPose>,\n ancestor: PickCandidate<TPose>,\n node: PickCandidate<TPose>,\n pose: TPose,\n derived: Path | null | undefined,\n): boolean {\n const clip = look.clipOf(ancestor);\n return clip === null\n || query.clipAdmits(look.carry(clip, ancestor.layer, node.layer), node, pose, derived);\n}\n"],"mappings":";;;;;;;;;;;;;AA2BA,SAAgB,QACd,MACA,MACA,SACA,WACA,GACA,WACA,SACA,MACS;CACT,MAAM,UAAU,WAAW,EAAE,WAAW,SAAY,UAAU,IAAI,IAAI;CACtE,IAAI,YAAY,QAAQ,CAAC,QAAQ,MAAM,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG;EACzE,IAAI,CAAC,EAAE,KAAK,MAAM,MAAM,SAAS,SAAS,GAAG,OAAO;EACpD,MAAM,MAAM,WAAW,UAAU,IAAI;EACrC,KAAK,IAAI,IAAI,GAAG,IAAI,IAAI,QAAQ,KAAK,IAAI,CAAC,SAAS,MAAM,GAAG,IAAI,IAAK,MAAM,MAAM,OAAO,GAAG,OAAO;EAClG,OAAO;CACT;CAEA,MAAM,UAAU,OAAyB,aACvC,SAAS,MAAM,OAAO,UAAU,MAAM,MAAM,OAAO;CACrD,MAAM,iBAAiB,CAAC,GAAG,OAAO,CAAC,CAAC,QAAQ;CAC5C,MAAM,QAAQ,GAAW,UAAqC;EAC5D,IAAI,MAAM,eAAe,QAAQ,OAAO,MAAM,KAAK,MAAM,MAAM,SAAS,SAAS;EACjF,MAAM,WAAW,eAAe;EAChC,IAAI,OAAO,OAAO,QAAQ,KAAK,KAAK,IAAI,GAAG,KAAK,GAAG,OAAO;EAC1D,KAAK,MAAM,KAAK,KAAK,SAAS,QAAQ,GAAG;GACvC,MAAM,SAAS,MAAM,SAAS,CAAC;GAC/B,IAAI,WAAW,UAAa,OAAO,QAAQ,QAAQ,KAAK,KAAK,IAAI,GAAG,MAAM,GAAG,OAAO;EACtF;EACA,OAAO;CACT;CACA,OAAO,KAAK,GAAG,CAAC;AAClB;;AAGA,SAAS,SACP,MACA,OACA,UACA,MACA,MACA,SACS;CACT,MAAM,OAAO,KAAK,OAAO,QAAQ;CACjC,OAAO,SAAS,QACX,MAAM,WAAW,KAAK,MAAM,MAAM,SAAS,OAAO,KAAK,KAAK,GAAG,MAAM,MAAM,OAAO;AACzF"}
|
package/dist/canvas/pickWalk.js
CHANGED
|
@@ -62,6 +62,12 @@ function pickWalk(src, q) {
|
|
|
62
62
|
}
|
|
63
63
|
return found;
|
|
64
64
|
};
|
|
65
|
+
const lookups = {
|
|
66
|
+
clipOf,
|
|
67
|
+
copiesOf,
|
|
68
|
+
carry
|
|
69
|
+
};
|
|
70
|
+
const parentsOf = (node) => src.parentsOf(node);
|
|
65
71
|
const order = src.order();
|
|
66
72
|
const copying = order.some((n) => typeof n.copiesFromPose === "function");
|
|
67
73
|
const visit = (q.broad !== void 0 ? src.candidates?.((layer) => {
|
|
@@ -80,11 +86,7 @@ function pickWalk(src, q) {
|
|
|
80
86
|
const pose = src.poseOf(node);
|
|
81
87
|
const derived = src.derivedPathOf?.(node);
|
|
82
88
|
const inherited = inheritsPaint(node) ? inheritedPaintOf(node, src.parentsOf(node)) : void 0;
|
|
83
|
-
if (!reaches(node, pose, derived, inherited, query,
|
|
84
|
-
clipOf,
|
|
85
|
-
copiesOf,
|
|
86
|
-
carry
|
|
87
|
-
})) continue;
|
|
89
|
+
if (!reaches(node, pose, derived, inherited, query, parentsOf, copying, lookups)) continue;
|
|
88
90
|
out.push(node.id);
|
|
89
91
|
}
|
|
90
92
|
return out;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pickWalk.js","names":[],"sources":["../../src/canvas/pickWalk.ts"],"sourcesContent":["/**\n * The one hit-test walk. Everything that decides whether a node *can* be hit lives here; a\n * query supplies only the shape test and the clip predicate for its own region.\n *\n * The gates, in the order the walk applies them:\n * 1. groups, when the query does not want them\n * 2. the node is painted at all — not hidden in the scene (`nodeGates`),\n * and on a layer the asking view's `layerVisibility` / `layerOrder` paints\n * 3. painted alpha is above zero\n * 4. the node is not locked, unless the query asks for locked nodes\n * 5. the query's own shape test\n * 6. every ancestor clip admits the query region\n *\n * 2, 3 and 6 are all \"the renderer did not put this on screen\". A pick path\n * that skips one answers for a node the user cannot see. 4 is the other axis:\n * a locked node is on screen and still out of reach.\n */\n\nimport { invert, type Affine, type Path } from '@weasel-js/geom';\nimport type { CopiesFromPoseFn } from 'core/scene/types';\nimport { ownClipOf, type ClipBearing } from './ownClip';\nimport { inheritedPaintOf, inheritsPaint, type InheritedPaint } from 'core/scene/inheritedPaint';\nimport { rectToPlane, type PlaneMap } from 'core/viewport/parallax';\nimport { clipCarrier } from './planeClips';\nimport { reaches } from './pickReach';\n\n/** The subset of a node the walk itself reads. Deliberately structural: a\n * bare adapter's node satisfies it as readily as a `SceneNode`. */\nexport interface PickCandidate<TPose> extends ClipBearing<TPose> {\n id: string;\n kind?: string;\n layer?: string;\n pose: TPose;\n /** `false` makes the node transparent to the walk — see `SceneNode.pickable`. */\n pickable?: boolean;\n /** Read for an `'inherit'` fill or stroke, which takes its containers'. */\n data?: unknown;\n copiesFromPose?: CopiesFromPoseFn<TPose>;\n}\n\n/**\n * Everything the walk reads that a **view** owns.\n *\n * A layer must not hold state a view owns, so none of these are closed over\n * at construction — each is asked per walk, of the view being dispatched to.\n */\nexport interface PickSource<TPose> {\n /** Candidates in paint order, back-to-front. Containers included; the\n * query decides whether they can be hit. */\n order(): readonly PickCandidate<TPose>[];\n /** The pose the renderer draws — override-aware. Not `node.pose`, which is\n * how the render and pick paths came to disagree about where a node is. */\n poseOf(node: PickCandidate<TPose>): TPose;\n /** Ancestors, innermost first. Only walked for candidates that already\n * passed their own shape test. */\n parentsOf(node: PickCandidate<TPose>): readonly PickCandidate<TPose>[];\n /** Painted alpha for the asking view: the view's `alphaFor` times any\n * per-node override alpha. Omit when the source has no alpha concept. */\n alphaOf?(id: string): number;\n /** Whether this layer reaches the screen in the asking view. Omit for a\n * source with no layers. */\n layerIsPainted?(layer: string): boolean;\n /** Whether the node is locked — painted, but not something a pointer can\n * take. Omit for a source with no lock concept. */\n isLocked?(node: PickCandidate<TPose>): boolean;\n /** Whether the source hides the node, so nothing of it is on screen. Omit\n * for a source that hides nothing. */\n isHidden?(node: PickCandidate<TPose>): boolean;\n /** The path a node computes from its dependencies, or null for none. */\n derivedPathOf?(node: PickCandidate<TPose>): Path | null;\n /** How the asking view's world maps into a parallax layer's, or null for a\n * layer that moves with the camera. Omit for a source with no planes. */\n planeOf?(layer: string): PlaneMap | null;\n /**\n * The candidates a query with these broad boxes can hit, back-to-front: a\n * subset of `order()` that drops only nodes no query inside the box could\n * hit. `broadFor(layer)` is the box for that layer's query: `null` for a\n * layer it cannot reach, `undefined` for one with no box, which makes this\n * answer `null` and the walk visit `order()`. Omit for a source with no index.\n */\n candidates?(\n broadFor: (layer: string | undefined) => PickBroad | null | undefined,\n ): readonly PickCandidate<TPose>[] | null;\n}\n\n/** The box a query cannot hit outside of, for a source's index to look up. */\nexport interface PickBroad {\n /** World box, already grown by the query's own slop. */\n box: { x: number; y: number; width: number; height: number };\n /** Screen px per world unit at the asking view (least axis); grows the box\n * by the largest `outsetPx` the index holds. Omit for a query whose test\n * reads no `outsetPx`. */\n pxPerWorld?: number;\n}\n\n/** `broad` carried into a plane's world, as `inPlane` carries the query. */\nexport function broadInPlane(broad: PickBroad, m: PlaneMap): PickBroad {\n const box = rectToPlane(m, broad.box);\n if (broad.pxPerWorld === undefined) return { box };\n // A plane's pixel is the same on screen, so its world unit holds fewer of them.\n const k = Math.max(Math.abs(m.scale.x), Math.abs(m.scale.y));\n return { box, pxPerWorld: broad.pxPerWorld / k };\n}\n\n/** One hit-test question: what region, and what counts as covering it. */\nexport interface PickQuery<TPose> {\n /** Default `true`. The area walks exclude groups so a marquee does not\n * return a group and its members both. */\n includeGroups?: boolean;\n /** Default `false`. Every selecting or editing query passes over locked\n * nodes; one that only samples what is painted can take them back. */\n includeLocked?: boolean;\n /** Where the query can hit at all, for a source with an index. `inPlane`\n * carries it with the query; without it the walk visits every node. */\n broad?: PickBroad;\n /** Does this node, drawn at this pose, cover the query region? `derived`\n * is the node's resolved path when it has one — a derived node's pose is a\n * placeholder, so a region test that reads only the pose answers for the\n * wrong box. `inherited` is what its `'inherit'` fields take from its\n * containers, for the ink it is tested against; absent when it inherits\n * nothing. */\n hits(node: PickCandidate<TPose>, pose: TPose, derived?: Path | null, inherited?: InheritedPaint): boolean;\n /**\n * Does an ancestor's clip leave anything of `node` for this query to hit?\n *\n * A point query asks point-in-clip and ignores the node. An area query has\n * to ask about the node as well: the clip and the area can overlap where\n * the node is not, and the node and the clip can overlap where the area is\n * not, and only the two terms together reject both.\n */\n clipAdmits(\n clip: Path,\n node: PickCandidate<TPose>,\n pose: TPose,\n derived?: Path | null,\n ): boolean;\n /** The same question asked in a parallax plane's world: the region carried\n * through `map`, and any screen-pixel slop re-derived at the plane's scale.\n * A query without it cannot reach nodes on a plane — it would test them\n * where their poses sit rather than where they are painted. */\n inPlane?(map: PlaneMap): PickQuery<TPose>;\n /** The same question carried by `m`, an affine from world into the source\n * frame of a copy a container draws. A query without it cannot reach\n * copies — see `GroupNode.copiesFromPose`. */\n inCopy?(m: Affine): PickQuery<TPose>;\n}\n\n/**\n * Collect every candidate the query hits, back-to-front — the last element is\n * the topmost, which is the order `pickTopMostHit` expects.\n */\nexport function pickWalk<TPose>(\n src: PickSource<TPose>,\n q: PickQuery<TPose>,\n): string[] {\n const includeGroups = q.includeGroups ?? true;\n const isLocked = q.includeLocked === true ? undefined : src.isLocked;\n const out: string[] = [];\n\n // Resolving a container's clip is the expensive half, so the *path* is memoized per\n // container for the walk. The predicate re-runs per candidate: caching its answer per\n // container would answer the wrong question for an area query.\n const clipPath = new Map<string, Path | null>();\n // A clip is in its container's plane; the query is in the candidate's.\n const carry = clipCarrier(src.planeOf ?? null);\n\n const clipOf = (ancestor: PickCandidate<TPose>): Path | null => {\n const cached = clipPath.get(ancestor.id);\n if (cached !== undefined) return cached;\n const resolved = ownClipOf(\n ancestor, src.poseOf(ancestor), () => src.derivedPathOf?.(ancestor),\n );\n clipPath.set(ancestor.id, resolved);\n return resolved;\n };\n\n const copyMaps = new Map<string, readonly Affine[]>();\n const copiesOf = (ancestor: PickCandidate<TPose>): readonly Affine[] => {\n let found = copyMaps.get(ancestor.id);\n if (found === undefined) {\n const fn = ancestor.copiesFromPose;\n const copies = fn ? fn(src.poseOf(ancestor), ancestor as never) ?? [] : [];\n found = copies.map(invert).filter((m): m is Affine => m !== null);\n copyMaps.set(ancestor.id, found);\n }\n return found;\n };\n\n // One reprojected query per plane per walk; `null` marks a plane this\n // query cannot reach.\n const planeQueries = new Map<string, PickQuery<TPose> | null>();\n const queryFor = (layer: string | undefined): PickQuery<TPose> | null => {\n if (layer === undefined || src.planeOf === undefined) return q;\n let found = planeQueries.get(layer);\n if (found === undefined) {\n const map = src.planeOf(layer);\n found = map === null ? q : (q.inPlane?.(map) ?? null);\n planeQueries.set(layer, found);\n }\n return found;\n };\n\n const order = src.order();\n const copying = order.some((n) => typeof n.copiesFromPose === 'function');\n const visit = (q.broad !== undefined ? src.candidates?.((layer) => {\n const query = queryFor(layer);\n return query === null ? null : query.broad;\n }) : null) ?? order;\n for (const node of visit) {\n if (!includeGroups && node.kind === 'group') continue;\n // Transparent to picking: the click belongs to whatever is behind it,\n // which for a label inside a box is the box.\n if (node.pickable === false) continue;\n if (src.isHidden?.(node) === true) continue;\n if (node.layer !== undefined && src.layerIsPainted?.(node.layer) === false) continue;\n if (src.alphaOf !== undefined && src.alphaOf(node.id) <= 0) continue;\n if (isLocked !== undefined && isLocked(node)) continue;\n\n const query = queryFor(node.layer);\n if (query === null) continue;\n const pose = src.poseOf(node);\n const derived = src.derivedPathOf?.(node);\n const inherited = inheritsPaint(node) ? inheritedPaintOf(node, src.parentsOf(node)) : undefined;\n if (!reaches(node, pose, derived, inherited, query, () => src.parentsOf(node), copying, { clipOf, copiesOf, carry })) continue;\n out.push(node.id);\n }\n\n return out;\n}\n\n/**\n * The two answers a pick needs from the view it is asked for rather than from\n * the data behind it: how that view paints a node, and whether it paints a\n * layer at all. A scene and a bare adapter take the same pair.\n */\nexport interface ViewPickGates {\n /** Painted alpha: the view's `alphaFor` times any per-node override alpha.\n * A node painted at alpha 0 cannot be hit. */\n alphaOf?: (id: string) => number;\n /** Whether a node's `layer` reaches the screen in the asking view. */\n layerIsPainted?: (layer: string) => boolean;\n}\n\n/** The gates a bare adapter takes from its view, plus how that view's camera\n * maps into each parallax layer — see `scenePlaneOf`. */\nexport interface AdapterPickGates extends ViewPickGates {\n planeOf?: (layer: string) => PlaneMap | null;\n}\n\nexport { scenePickSource, scenePlaneOf, type ScenePickSourceOptions } from './scenePickSource';\nexport { adapterPickSource } from './adapterPickSource';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuJA,SAAgB,SACd,KACA,GACU;CACV,MAAM,gBAAgB,EAAE,iBAAiB;CACzC,MAAM,WAAW,EAAE,kBAAkB,OAAO,SAAY,IAAI;CAC5D,MAAM,MAAgB,CAAC;CAKvB,MAAM,2BAAW,IAAI,IAAyB;CAE9C,MAAM,QAAQ,YAAY,IAAI,WAAW,IAAI;CAE7C,MAAM,UAAU,aAAgD;EAC9D,MAAM,SAAS,SAAS,IAAI,SAAS,EAAE;EACvC,IAAI,WAAW,QAAW,OAAO;EACjC,MAAM,WAAW,UACf,UAAU,IAAI,OAAO,QAAQ,SAAS,IAAI,gBAAgB,QAAQ,CACpE;EACA,SAAS,IAAI,SAAS,IAAI,QAAQ;EAClC,OAAO;CACT;CAEA,MAAM,2BAAW,IAAI,IAA+B;CACpD,MAAM,YAAY,aAAsD;EACtE,IAAI,QAAQ,SAAS,IAAI,SAAS,EAAE;EACpC,IAAI,UAAU,QAAW;GACvB,MAAM,KAAK,SAAS;GAEpB,SADe,KAAK,GAAG,IAAI,OAAO,QAAQ,GAAG,QAAiB,KAAK,CAAC,IAAI,CAAC,EACjE,CAAO,IAAI,MAAM,CAAC,CAAC,QAAQ,MAAmB,MAAM,IAAI;GAChE,SAAS,IAAI,SAAS,IAAI,KAAK;EACjC;EACA,OAAO;CACT;CAIA,MAAM,+BAAe,IAAI,IAAqC;CAC9D,MAAM,YAAY,UAAuD;EACvE,IAAI,UAAU,UAAa,IAAI,YAAY,QAAW,OAAO;EAC7D,IAAI,QAAQ,aAAa,IAAI,KAAK;EAClC,IAAI,UAAU,QAAW;GACvB,MAAM,MAAM,IAAI,QAAQ,KAAK;GAC7B,QAAQ,QAAQ,OAAO,IAAK,EAAE,UAAU,GAAG,KAAK;GAChD,aAAa,IAAI,OAAO,KAAK;EAC/B;EACA,OAAO;CACT;CAEA,MAAM,QAAQ,IAAI,MAAM;CACxB,MAAM,UAAU,MAAM,MAAM,MAAM,OAAO,EAAE,mBAAmB,UAAU;CACxE,MAAM,SAAS,EAAE,UAAU,SAAY,IAAI,cAAc,UAAU;EACjE,MAAM,QAAQ,SAAS,KAAK;EAC5B,OAAO,UAAU,OAAO,OAAO,MAAM;CACvC,CAAC,IAAI,SAAS;CACd,KAAK,MAAM,QAAQ,OAAO;EACxB,IAAI,CAAC,iBAAiB,KAAK,SAAS,SAAS;EAG7C,IAAI,KAAK,aAAa,OAAO;EAC7B,IAAI,IAAI,WAAW,IAAI,MAAM,MAAM;EACnC,IAAI,KAAK,UAAU,UAAa,IAAI,iBAAiB,KAAK,KAAK,MAAM,OAAO;EAC5E,IAAI,IAAI,YAAY,UAAa,IAAI,QAAQ,KAAK,EAAE,KAAK,GAAG;EAC5D,IAAI,aAAa,UAAa,SAAS,IAAI,GAAG;EAE9C,MAAM,QAAQ,SAAS,KAAK,KAAK;EACjC,IAAI,UAAU,MAAM;EACpB,MAAM,OAAO,IAAI,OAAO,IAAI;EAC5B,MAAM,UAAU,IAAI,gBAAgB,IAAI;EACxC,MAAM,YAAY,cAAc,IAAI,IAAI,iBAAiB,MAAM,IAAI,UAAU,IAAI,CAAC,IAAI;EACtF,IAAI,CAAC,QAAQ,MAAM,MAAM,SAAS,WAAW,aAAa,IAAI,UAAU,IAAI,GAAG,SAAS;GAAE;GAAQ;GAAU;EAAM,CAAC,GAAG;EACtH,IAAI,KAAK,KAAK,EAAE;CAClB;CAEA,OAAO;AACT"}
|
|
1
|
+
{"version":3,"file":"pickWalk.js","names":[],"sources":["../../src/canvas/pickWalk.ts"],"sourcesContent":["/**\n * The one hit-test walk. Everything that decides whether a node *can* be hit lives here; a\n * query supplies only the shape test and the clip predicate for its own region.\n *\n * The gates, in the order the walk applies them:\n * 1. groups, when the query does not want them\n * 2. the node is painted at all — not hidden in the scene (`nodeGates`),\n * and on a layer the asking view's `layerVisibility` / `layerOrder` paints\n * 3. painted alpha is above zero\n * 4. the node is not locked, unless the query asks for locked nodes\n * 5. the query's own shape test\n * 6. every ancestor clip admits the query region\n *\n * 2, 3 and 6 are all \"the renderer did not put this on screen\". A pick path\n * that skips one answers for a node the user cannot see. 4 is the other axis:\n * a locked node is on screen and still out of reach.\n */\n\nimport { invert, type Affine, type Path } from '@weasel-js/geom';\nimport type { CopiesFromPoseFn } from 'core/scene/types';\nimport { ownClipOf, type ClipBearing } from './ownClip';\nimport { inheritedPaintOf, inheritsPaint, type InheritedPaint } from 'core/scene/inheritedPaint';\nimport { rectToPlane, type PlaneMap } from 'core/viewport/parallax';\nimport { clipCarrier } from './planeClips';\nimport { reaches } from './pickReach';\n\n/** The subset of a node the walk itself reads. Deliberately structural: a\n * bare adapter's node satisfies it as readily as a `SceneNode`. */\nexport interface PickCandidate<TPose> extends ClipBearing<TPose> {\n id: string;\n kind?: string;\n layer?: string;\n pose: TPose;\n /** `false` makes the node transparent to the walk — see `SceneNode.pickable`. */\n pickable?: boolean;\n /** Read for an `'inherit'` fill or stroke, which takes its containers'. */\n data?: unknown;\n copiesFromPose?: CopiesFromPoseFn<TPose>;\n}\n\n/**\n * Everything the walk reads that a **view** owns.\n *\n * A layer must not hold state a view owns, so none of these are closed over\n * at construction — each is asked per walk, of the view being dispatched to.\n */\nexport interface PickSource<TPose> {\n /** Candidates in paint order, back-to-front. Containers included; the\n * query decides whether they can be hit. */\n order(): readonly PickCandidate<TPose>[];\n /** The pose the renderer draws — override-aware. Not `node.pose`, which is\n * how the render and pick paths came to disagree about where a node is. */\n poseOf(node: PickCandidate<TPose>): TPose;\n /** Ancestors, innermost first. Only walked for candidates that already\n * passed their own shape test. */\n parentsOf(node: PickCandidate<TPose>): readonly PickCandidate<TPose>[];\n /** Painted alpha for the asking view: the view's `alphaFor` times any\n * per-node override alpha. Omit when the source has no alpha concept. */\n alphaOf?(id: string): number;\n /** Whether this layer reaches the screen in the asking view. Omit for a\n * source with no layers. */\n layerIsPainted?(layer: string): boolean;\n /** Whether the node is locked — painted, but not something a pointer can\n * take. Omit for a source with no lock concept. */\n isLocked?(node: PickCandidate<TPose>): boolean;\n /** Whether the source hides the node, so nothing of it is on screen. Omit\n * for a source that hides nothing. */\n isHidden?(node: PickCandidate<TPose>): boolean;\n /** The path a node computes from its dependencies, or null for none. */\n derivedPathOf?(node: PickCandidate<TPose>): Path | null;\n /** How the asking view's world maps into a parallax layer's, or null for a\n * layer that moves with the camera. Omit for a source with no planes. */\n planeOf?(layer: string): PlaneMap | null;\n /**\n * The candidates a query with these broad boxes can hit, back-to-front: a\n * subset of `order()` that drops only nodes no query inside the box could\n * hit. `broadFor(layer)` is the box for that layer's query: `null` for a\n * layer it cannot reach, `undefined` for one with no box, which makes this\n * answer `null` and the walk visit `order()`. Omit for a source with no index.\n */\n candidates?(\n broadFor: (layer: string | undefined) => PickBroad | null | undefined,\n ): readonly PickCandidate<TPose>[] | null;\n}\n\n/** The box a query cannot hit outside of, for a source's index to look up. */\nexport interface PickBroad {\n /** World box, already grown by the query's own slop. */\n box: { x: number; y: number; width: number; height: number };\n /** Screen px per world unit at the asking view (least axis); grows the box\n * by the largest `outsetPx` the index holds. Omit for a query whose test\n * reads no `outsetPx`. */\n pxPerWorld?: number;\n}\n\n/** `broad` carried into a plane's world, as `inPlane` carries the query. */\nexport function broadInPlane(broad: PickBroad, m: PlaneMap): PickBroad {\n const box = rectToPlane(m, broad.box);\n if (broad.pxPerWorld === undefined) return { box };\n // A plane's pixel is the same on screen, so its world unit holds fewer of them.\n const k = Math.max(Math.abs(m.scale.x), Math.abs(m.scale.y));\n return { box, pxPerWorld: broad.pxPerWorld / k };\n}\n\n/** One hit-test question: what region, and what counts as covering it. */\nexport interface PickQuery<TPose> {\n /** Default `true`. The area walks exclude groups so a marquee does not\n * return a group and its members both. */\n includeGroups?: boolean;\n /** Default `false`. Every selecting or editing query passes over locked\n * nodes; one that only samples what is painted can take them back. */\n includeLocked?: boolean;\n /** Where the query can hit at all, for a source with an index. `inPlane`\n * carries it with the query; without it the walk visits every node. */\n broad?: PickBroad;\n /** Does this node, drawn at this pose, cover the query region? `derived`\n * is the node's resolved path when it has one — a derived node's pose is a\n * placeholder, so a region test that reads only the pose answers for the\n * wrong box. `inherited` is what its `'inherit'` fields take from its\n * containers, for the ink it is tested against; absent when it inherits\n * nothing. */\n hits(node: PickCandidate<TPose>, pose: TPose, derived?: Path | null, inherited?: InheritedPaint): boolean;\n /**\n * Does an ancestor's clip leave anything of `node` for this query to hit?\n *\n * A point query asks point-in-clip and ignores the node. An area query has\n * to ask about the node as well: the clip and the area can overlap where\n * the node is not, and the node and the clip can overlap where the area is\n * not, and only the two terms together reject both.\n */\n clipAdmits(\n clip: Path,\n node: PickCandidate<TPose>,\n pose: TPose,\n derived?: Path | null,\n ): boolean;\n /** The same question asked in a parallax plane's world: the region carried\n * through `map`, and any screen-pixel slop re-derived at the plane's scale.\n * A query without it cannot reach nodes on a plane — it would test them\n * where their poses sit rather than where they are painted. */\n inPlane?(map: PlaneMap): PickQuery<TPose>;\n /** The same question carried by `m`, an affine from world into the source\n * frame of a copy a container draws. A query without it cannot reach\n * copies — see `GroupNode.copiesFromPose`. */\n inCopy?(m: Affine): PickQuery<TPose>;\n}\n\n/**\n * Collect every candidate the query hits, back-to-front — the last element is\n * the topmost, which is the order `pickTopMostHit` expects.\n */\nexport function pickWalk<TPose>(\n src: PickSource<TPose>,\n q: PickQuery<TPose>,\n): string[] {\n const includeGroups = q.includeGroups ?? true;\n const isLocked = q.includeLocked === true ? undefined : src.isLocked;\n const out: string[] = [];\n\n // Resolving a container's clip is the expensive half, so the *path* is memoized per\n // container for the walk. The predicate re-runs per candidate: caching its answer per\n // container would answer the wrong question for an area query.\n const clipPath = new Map<string, Path | null>();\n // A clip is in its container's plane; the query is in the candidate's.\n const carry = clipCarrier(src.planeOf ?? null);\n\n const clipOf = (ancestor: PickCandidate<TPose>): Path | null => {\n const cached = clipPath.get(ancestor.id);\n if (cached !== undefined) return cached;\n const resolved = ownClipOf(\n ancestor, src.poseOf(ancestor), () => src.derivedPathOf?.(ancestor),\n );\n clipPath.set(ancestor.id, resolved);\n return resolved;\n };\n\n const copyMaps = new Map<string, readonly Affine[]>();\n const copiesOf = (ancestor: PickCandidate<TPose>): readonly Affine[] => {\n let found = copyMaps.get(ancestor.id);\n if (found === undefined) {\n const fn = ancestor.copiesFromPose;\n const copies = fn ? fn(src.poseOf(ancestor), ancestor as never) ?? [] : [];\n found = copies.map(invert).filter((m): m is Affine => m !== null);\n copyMaps.set(ancestor.id, found);\n }\n return found;\n };\n\n // One reprojected query per plane per walk; `null` marks a plane this\n // query cannot reach.\n const planeQueries = new Map<string, PickQuery<TPose> | null>();\n const queryFor = (layer: string | undefined): PickQuery<TPose> | null => {\n if (layer === undefined || src.planeOf === undefined) return q;\n let found = planeQueries.get(layer);\n if (found === undefined) {\n const map = src.planeOf(layer);\n found = map === null ? q : (q.inPlane?.(map) ?? null);\n planeQueries.set(layer, found);\n }\n return found;\n };\n\n const lookups = { clipOf, copiesOf, carry };\n const parentsOf = (node: PickCandidate<TPose>) => src.parentsOf(node);\n const order = src.order();\n const copying = order.some((n) => typeof n.copiesFromPose === 'function');\n const visit = (q.broad !== undefined ? src.candidates?.((layer) => {\n const query = queryFor(layer);\n return query === null ? null : query.broad;\n }) : null) ?? order;\n for (const node of visit) {\n if (!includeGroups && node.kind === 'group') continue;\n // Transparent to picking: the click belongs to whatever is behind it,\n // which for a label inside a box is the box.\n if (node.pickable === false) continue;\n if (src.isHidden?.(node) === true) continue;\n if (node.layer !== undefined && src.layerIsPainted?.(node.layer) === false) continue;\n if (src.alphaOf !== undefined && src.alphaOf(node.id) <= 0) continue;\n if (isLocked !== undefined && isLocked(node)) continue;\n\n const query = queryFor(node.layer);\n if (query === null) continue;\n const pose = src.poseOf(node);\n const derived = src.derivedPathOf?.(node);\n const inherited = inheritsPaint(node) ? inheritedPaintOf(node, src.parentsOf(node)) : undefined;\n if (!reaches(node, pose, derived, inherited, query, parentsOf, copying, lookups)) continue;\n out.push(node.id);\n }\n\n return out;\n}\n\n/**\n * The two answers a pick needs from the view it is asked for rather than from\n * the data behind it: how that view paints a node, and whether it paints a\n * layer at all. A scene and a bare adapter take the same pair.\n */\nexport interface ViewPickGates {\n /** Painted alpha: the view's `alphaFor` times any per-node override alpha.\n * A node painted at alpha 0 cannot be hit. */\n alphaOf?: (id: string) => number;\n /** Whether a node's `layer` reaches the screen in the asking view. */\n layerIsPainted?: (layer: string) => boolean;\n}\n\n/** The gates a bare adapter takes from its view, plus how that view's camera\n * maps into each parallax layer — see `scenePlaneOf`. */\nexport interface AdapterPickGates extends ViewPickGates {\n planeOf?: (layer: string) => PlaneMap | null;\n}\n\nexport { scenePickSource, scenePlaneOf, type ScenePickSourceOptions } from './scenePickSource';\nexport { adapterPickSource } from './adapterPickSource';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuJA,SAAgB,SACd,KACA,GACU;CACV,MAAM,gBAAgB,EAAE,iBAAiB;CACzC,MAAM,WAAW,EAAE,kBAAkB,OAAO,SAAY,IAAI;CAC5D,MAAM,MAAgB,CAAC;CAKvB,MAAM,2BAAW,IAAI,IAAyB;CAE9C,MAAM,QAAQ,YAAY,IAAI,WAAW,IAAI;CAE7C,MAAM,UAAU,aAAgD;EAC9D,MAAM,SAAS,SAAS,IAAI,SAAS,EAAE;EACvC,IAAI,WAAW,QAAW,OAAO;EACjC,MAAM,WAAW,UACf,UAAU,IAAI,OAAO,QAAQ,SAAS,IAAI,gBAAgB,QAAQ,CACpE;EACA,SAAS,IAAI,SAAS,IAAI,QAAQ;EAClC,OAAO;CACT;CAEA,MAAM,2BAAW,IAAI,IAA+B;CACpD,MAAM,YAAY,aAAsD;EACtE,IAAI,QAAQ,SAAS,IAAI,SAAS,EAAE;EACpC,IAAI,UAAU,QAAW;GACvB,MAAM,KAAK,SAAS;GAEpB,SADe,KAAK,GAAG,IAAI,OAAO,QAAQ,GAAG,QAAiB,KAAK,CAAC,IAAI,CAAC,EACjE,CAAO,IAAI,MAAM,CAAC,CAAC,QAAQ,MAAmB,MAAM,IAAI;GAChE,SAAS,IAAI,SAAS,IAAI,KAAK;EACjC;EACA,OAAO;CACT;CAIA,MAAM,+BAAe,IAAI,IAAqC;CAC9D,MAAM,YAAY,UAAuD;EACvE,IAAI,UAAU,UAAa,IAAI,YAAY,QAAW,OAAO;EAC7D,IAAI,QAAQ,aAAa,IAAI,KAAK;EAClC,IAAI,UAAU,QAAW;GACvB,MAAM,MAAM,IAAI,QAAQ,KAAK;GAC7B,QAAQ,QAAQ,OAAO,IAAK,EAAE,UAAU,GAAG,KAAK;GAChD,aAAa,IAAI,OAAO,KAAK;EAC/B;EACA,OAAO;CACT;CAEA,MAAM,UAAU;EAAE;EAAQ;EAAU;CAAM;CAC1C,MAAM,aAAa,SAA+B,IAAI,UAAU,IAAI;CACpE,MAAM,QAAQ,IAAI,MAAM;CACxB,MAAM,UAAU,MAAM,MAAM,MAAM,OAAO,EAAE,mBAAmB,UAAU;CACxE,MAAM,SAAS,EAAE,UAAU,SAAY,IAAI,cAAc,UAAU;EACjE,MAAM,QAAQ,SAAS,KAAK;EAC5B,OAAO,UAAU,OAAO,OAAO,MAAM;CACvC,CAAC,IAAI,SAAS;CACd,KAAK,MAAM,QAAQ,OAAO;EACxB,IAAI,CAAC,iBAAiB,KAAK,SAAS,SAAS;EAG7C,IAAI,KAAK,aAAa,OAAO;EAC7B,IAAI,IAAI,WAAW,IAAI,MAAM,MAAM;EACnC,IAAI,KAAK,UAAU,UAAa,IAAI,iBAAiB,KAAK,KAAK,MAAM,OAAO;EAC5E,IAAI,IAAI,YAAY,UAAa,IAAI,QAAQ,KAAK,EAAE,KAAK,GAAG;EAC5D,IAAI,aAAa,UAAa,SAAS,IAAI,GAAG;EAE9C,MAAM,QAAQ,SAAS,KAAK,KAAK;EACjC,IAAI,UAAU,MAAM;EACpB,MAAM,OAAO,IAAI,OAAO,IAAI;EAC5B,MAAM,UAAU,IAAI,gBAAgB,IAAI;EACxC,MAAM,YAAY,cAAc,IAAI,IAAI,iBAAiB,MAAM,IAAI,UAAU,IAAI,CAAC,IAAI;EACtF,IAAI,CAAC,QAAQ,MAAM,MAAM,SAAS,WAAW,OAAO,WAAW,SAAS,OAAO,GAAG;EAClF,IAAI,KAAK,KAAK,EAAE;CAClB;CAEA,OAAO;AACT"}
|
|
@@ -54,8 +54,8 @@ function scenePickSource(scene, opts = {}) {
|
|
|
54
54
|
poseOf,
|
|
55
55
|
parentsOf: sceneParentsOf(scene),
|
|
56
56
|
...readAlpha ? { alphaOf: readAlpha } : {},
|
|
57
|
-
isLocked: (node) => gates.locked(node
|
|
58
|
-
isHidden: (node) => gates.hidden(node
|
|
57
|
+
isLocked: (node) => gates.locked(node),
|
|
58
|
+
isHidden: (node) => gates.hidden(node),
|
|
59
59
|
...annotations.size > 0 || layerIsPainted ? { layerIsPainted: (layer) => !annotations.has(layer) && (layerIsPainted === void 0 || layerIsPainted(layer)) } : {}
|
|
60
60
|
};
|
|
61
61
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scenePickSource.js","names":[],"sources":["../../src/canvas/scenePickSource.ts"],"sourcesContent":["/** The pick source over a `Scene`: its render order, painted poses and gates,\n * and, given a descriptor, the pick index that narrows the walk. */\nimport type { Path } from '@weasel-js/geom';\nimport type { DerivedDep, Node, NodeId, Scene } from 'core/scene/types';\nimport { asNodeId } from 'core/scene/types';\nimport { annotationLayerIds } from 'core/scene/annotation';\nimport { nodeGates } from 'core/scene/nodeGates';\nimport { planeMap, type ParallaxOpts, type PlaneMap } from 'core/viewport/parallax';\nimport type { View } from 'core/viewport/view';\nimport type { PoseDescriptor } from 'interactions/actions/resize/geometry';\nimport { resolveDerivedPath, scenePaintedPoseOf } from './derivedPath';\nimport { indexedCandidates } from './pickIndex/scenePickIndex';\nimport type { PickBroad, PickCandidate, PickSource, ViewPickGates } from './pickWalk';\nimport { sceneParentsOf, sceneShownPathOf } from './scenePickReads';\nimport { nodeAsShown, nodesAsShown } from 'core/scene/nodeAsShown';\n\nexport interface ScenePickSourceOptions<TPose> extends ViewPickGates {\n /** Pose lookup, when the caller holds an adapter that already resolves\n * overrides. Defaults to `effectivePose` against the scene's own. */\n getPose?: (id: string) => TPose;\n /** The asking view's composed alpha — `alphaFor(id)` times override alpha.\n * Defaults to the override alpha alone, which is all a bare scene knows. */\n alphaOf?: (id: string) => number;\n /** The asking view's answer to \"does this scene layer reach the screen\",\n * which is `layerVisibility` and `layerOrder` on top of the scene's own\n * `visible` flag. Consulted after the scene's flag, never instead of it. */\n layerIsPainted?: (layer: string) => boolean;\n /** The asking view's camera. With it, a layer carrying `parallax` is picked\n * through its plane; without it, every layer is taken to move with the\n * camera. */\n camera?: View;\n /** Box every node once per scene change, so a query carrying a `broad` box\n * visits only the nodes it can hit. `descriptor` must be the one the\n * query reads poses through. */\n index?: { descriptor: PoseDescriptor<unknown> };\n}\n\nexport function scenePickSource<TData, TLayer extends string, TPose>(\n scene: Scene<TData, TLayer, TPose>,\n opts: ScenePickSourceOptions<TPose> = {},\n): PickSource<TPose> {\n // Partial scene stand-ins carry only `layers` and `get`, which is all the gates read.\n const gates = nodeGates(scene as never);\n const enumerable = typeof scene.overrides?.ids === 'function';\n // Pick candidates carry a plain string id; the scene answers them all the same.\n const asShown = <N extends { id: string; data?: unknown }>(node: N): N =>\n (scene.overrides ? nodeAsShown(scene.overrides, node as N & { id: NodeId }) : node);\n const allAsShown = <N extends { id: string; data?: unknown }>(nodes: readonly N[]): readonly N[] =>\n (enumerable ? nodesAsShown(scene.overrides, nodes as readonly (N & { id: NodeId })[]) : nodes.map(asShown));\n const annotations = annotationLayerIds(scene.layers);\n const { getPose, alphaOf, layerIsPainted } = opts;\n\n // Resolved once per walk rather than once per candidate. Needs enumeration — a\n // partial scene stand-in carrying only `read` falls back to reading per node.\n let faded: Set<string> | null = null;\n if (!alphaOf && enumerable) {\n for (const id of scene.overrides.ids()) {\n const a = scene.overrides.read(id)?.alpha;\n if (a !== undefined && a <= 0) (faded ??= new Set()).add(id);\n }\n }\n const readAlpha = alphaOf\n ?? (enumerable\n ? (faded ? (id: string) => (faded.has(id) ? 0 : 1) : undefined)\n : (id: string) => scene.overrides.read(asNodeId(id))?.alpha ?? 1);\n\n // A composing strategy makes a node's own pose local, so picking folds the\n // parent chain the same way the render walk does: one definition of both.\n const paintedPoseOf = scenePaintedPoseOf(scene);\n const poseOf = getPose\n ? (node: PickCandidate<TPose>) => getPose(node.id)\n : (node: PickCandidate<TPose>) => paintedPoseOf(node as never);\n\n // Through `poseOf`, not the document pose: a derived node has to be tested\n // where the renderer draws its dependencies, which is what an override says.\n const childrenOf = (id: NodeId): readonly NodeId[] => scene.childrenOf(id);\n const depOf = (id: NodeId): DerivedDep<TPose> | undefined => {\n const stored = scene.get(id);\n if (stored === undefined) return undefined;\n const dep = asShown(stored);\n return {\n node: dep as Node<unknown, string, TPose>,\n pose: poseOf(dep as never),\n get path(): Path | null {\n return resolveDerivedPath(dep as never, depOf, childrenOf);\n },\n };\n };\n\n const derivedPathOf = getPose\n ? (node: PickCandidate<TPose>): Path | null => {\n const n = node as unknown as Node<TData, TLayer, TPose>;\n return n.dependsOn === undefined ? null : resolveDerivedPath(n, depOf, childrenOf);\n }\n : sceneShownPathOf(scene);\n\n const planeOf = opts.camera ? scenePlaneOf(scene.layers, opts.camera) : null;\n\n // The index boxes nodes at the scene's painted poses; a custom `getPose` must be shown to agree first.\n // A partial stand-in has no versions for it to key on.\n const index = getPose || typeof scene.getVersion !== 'function' ? undefined : opts.index;\n return {\n ...(planeOf ? { planeOf } : {}),\n ...(index ? {\n candidates: (broadFor: (layer: string | undefined) => PickBroad | null | undefined) => {\n const found = indexedCandidates(scene as never, index.descriptor, broadFor) as readonly PickCandidate<TPose>[] | null;\n return found === null ? null : allAsShown(found);\n },\n } : {}),\n order: () => allAsShown(scene.renderOrderNodes() as unknown as readonly PickCandidate<TPose>[]),\n derivedPathOf,\n poseOf,\n parentsOf: sceneParentsOf(scene),\n // Absent entirely when nothing is faded, so the walk skips the gate.\n ...(readAlpha ? { alphaOf: readAlpha } : {}),\n isLocked: (node: PickCandidate<TPose>) => gates.locked(node
|
|
1
|
+
{"version":3,"file":"scenePickSource.js","names":[],"sources":["../../src/canvas/scenePickSource.ts"],"sourcesContent":["/** The pick source over a `Scene`: its render order, painted poses and gates,\n * and, given a descriptor, the pick index that narrows the walk. */\nimport type { Path } from '@weasel-js/geom';\nimport type { DerivedDep, Node, NodeId, Scene } from 'core/scene/types';\nimport { asNodeId } from 'core/scene/types';\nimport { annotationLayerIds } from 'core/scene/annotation';\nimport { nodeGates, type GatedNode } from 'core/scene/nodeGates';\nimport { planeMap, type ParallaxOpts, type PlaneMap } from 'core/viewport/parallax';\nimport type { View } from 'core/viewport/view';\nimport type { PoseDescriptor } from 'interactions/actions/resize/geometry';\nimport { resolveDerivedPath, scenePaintedPoseOf } from './derivedPath';\nimport { indexedCandidates } from './pickIndex/scenePickIndex';\nimport type { PickBroad, PickCandidate, PickSource, ViewPickGates } from './pickWalk';\nimport { sceneParentsOf, sceneShownPathOf } from './scenePickReads';\nimport { nodeAsShown, nodesAsShown } from 'core/scene/nodeAsShown';\n\nexport interface ScenePickSourceOptions<TPose> extends ViewPickGates {\n /** Pose lookup, when the caller holds an adapter that already resolves\n * overrides. Defaults to `effectivePose` against the scene's own. */\n getPose?: (id: string) => TPose;\n /** The asking view's composed alpha — `alphaFor(id)` times override alpha.\n * Defaults to the override alpha alone, which is all a bare scene knows. */\n alphaOf?: (id: string) => number;\n /** The asking view's answer to \"does this scene layer reach the screen\",\n * which is `layerVisibility` and `layerOrder` on top of the scene's own\n * `visible` flag. Consulted after the scene's flag, never instead of it. */\n layerIsPainted?: (layer: string) => boolean;\n /** The asking view's camera. With it, a layer carrying `parallax` is picked\n * through its plane; without it, every layer is taken to move with the\n * camera. */\n camera?: View;\n /** Box every node once per scene change, so a query carrying a `broad` box\n * visits only the nodes it can hit. `descriptor` must be the one the\n * query reads poses through. */\n index?: { descriptor: PoseDescriptor<unknown> };\n}\n\nexport function scenePickSource<TData, TLayer extends string, TPose>(\n scene: Scene<TData, TLayer, TPose>,\n opts: ScenePickSourceOptions<TPose> = {},\n): PickSource<TPose> {\n // Partial scene stand-ins carry only `layers` and `get`, which is all the gates read.\n const gates = nodeGates(scene as never);\n const enumerable = typeof scene.overrides?.ids === 'function';\n // Pick candidates carry a plain string id; the scene answers them all the same.\n const asShown = <N extends { id: string; data?: unknown }>(node: N): N =>\n (scene.overrides ? nodeAsShown(scene.overrides, node as N & { id: NodeId }) : node);\n const allAsShown = <N extends { id: string; data?: unknown }>(nodes: readonly N[]): readonly N[] =>\n (enumerable ? nodesAsShown(scene.overrides, nodes as readonly (N & { id: NodeId })[]) : nodes.map(asShown));\n const annotations = annotationLayerIds(scene.layers);\n const { getPose, alphaOf, layerIsPainted } = opts;\n\n // Resolved once per walk rather than once per candidate. Needs enumeration — a\n // partial scene stand-in carrying only `read` falls back to reading per node.\n let faded: Set<string> | null = null;\n if (!alphaOf && enumerable) {\n for (const id of scene.overrides.ids()) {\n const a = scene.overrides.read(id)?.alpha;\n if (a !== undefined && a <= 0) (faded ??= new Set()).add(id);\n }\n }\n const readAlpha = alphaOf\n ?? (enumerable\n ? (faded ? (id: string) => (faded.has(id) ? 0 : 1) : undefined)\n : (id: string) => scene.overrides.read(asNodeId(id))?.alpha ?? 1);\n\n // A composing strategy makes a node's own pose local, so picking folds the\n // parent chain the same way the render walk does: one definition of both.\n const paintedPoseOf = scenePaintedPoseOf(scene);\n const poseOf = getPose\n ? (node: PickCandidate<TPose>) => getPose(node.id)\n : (node: PickCandidate<TPose>) => paintedPoseOf(node as never);\n\n // Through `poseOf`, not the document pose: a derived node has to be tested\n // where the renderer draws its dependencies, which is what an override says.\n const childrenOf = (id: NodeId): readonly NodeId[] => scene.childrenOf(id);\n const depOf = (id: NodeId): DerivedDep<TPose> | undefined => {\n const stored = scene.get(id);\n if (stored === undefined) return undefined;\n const dep = asShown(stored);\n return {\n node: dep as Node<unknown, string, TPose>,\n pose: poseOf(dep as never),\n get path(): Path | null {\n return resolveDerivedPath(dep as never, depOf, childrenOf);\n },\n };\n };\n\n const derivedPathOf = getPose\n ? (node: PickCandidate<TPose>): Path | null => {\n const n = node as unknown as Node<TData, TLayer, TPose>;\n return n.dependsOn === undefined ? null : resolveDerivedPath(n, depOf, childrenOf);\n }\n : sceneShownPathOf(scene);\n\n const planeOf = opts.camera ? scenePlaneOf(scene.layers, opts.camera) : null;\n\n // The index boxes nodes at the scene's painted poses; a custom `getPose` must be shown to agree first.\n // A partial stand-in has no versions for it to key on.\n const index = getPose || typeof scene.getVersion !== 'function' ? undefined : opts.index;\n return {\n ...(planeOf ? { planeOf } : {}),\n ...(index ? {\n candidates: (broadFor: (layer: string | undefined) => PickBroad | null | undefined) => {\n const found = indexedCandidates(scene as never, index.descriptor, broadFor) as readonly PickCandidate<TPose>[] | null;\n return found === null ? null : allAsShown(found);\n },\n } : {}),\n order: () => allAsShown(scene.renderOrderNodes() as unknown as readonly PickCandidate<TPose>[]),\n derivedPathOf,\n poseOf,\n parentsOf: sceneParentsOf(scene),\n // Absent entirely when nothing is faded, so the walk skips the gate.\n ...(readAlpha ? { alphaOf: readAlpha } : {}),\n isLocked: (node: PickCandidate<TPose>) => gates.locked(node as GatedNode),\n isHidden: (node: PickCandidate<TPose>) => gates.hidden(node as GatedNode),\n ...(annotations.size > 0 || layerIsPainted\n ? {\n layerIsPainted: (layer: string) =>\n !annotations.has(layer) && (layerIsPainted === undefined || layerIsPainted(layer)),\n }\n : {}),\n };\n}\n\n/** Each parallax layer's plane map under `camera`, or null when no layer is a\n * plane — which keeps the per-layer lookup off the walk entirely. */\nexport function scenePlaneOf(\n layers: readonly { id: string; parallax?: ParallaxOpts }[] | undefined,\n camera: View,\n): ((layer: string) => PlaneMap | null) | null {\n let maps: Map<string, PlaneMap> | null = null;\n for (const l of layers ?? []) {\n if (l.parallax) (maps ??= new Map()).set(l.id, planeMap(camera, l.parallax));\n }\n if (maps === null) return null;\n const found = maps;\n return (layer) => found.get(layer) ?? null;\n}\n"],"mappings":";;;;;;;;;;;AAqCA,SAAgB,gBACd,OACA,OAAsC,CAAC,GACpB;CAEnB,MAAM,QAAQ,UAAU,KAAc;CACtC,MAAM,aAAa,OAAO,MAAM,WAAW,QAAQ;CAEnD,MAAM,WAAqD,SACxD,MAAM,YAAY,YAAY,MAAM,WAAW,IAA0B,IAAI;CAChF,MAAM,cAAwD,UAC3D,aAAa,aAAa,MAAM,WAAW,KAAwC,IAAI,MAAM,IAAI,OAAO;CAC3G,MAAM,cAAc,mBAAmB,MAAM,MAAM;CACnD,MAAM,EAAE,SAAS,SAAS,mBAAmB;CAI7C,IAAI,QAA4B;CAChC,IAAI,CAAC,WAAW,YACd,KAAK,MAAM,MAAM,MAAM,UAAU,IAAI,GAAG;EACtC,MAAM,IAAI,MAAM,UAAU,KAAK,EAAE,CAAC,EAAE;EACpC,IAAI,MAAM,UAAa,KAAK,GAAG,CAAC,0BAAU,IAAI,IAAI,EAAA,CAAG,IAAI,EAAE;CAC7D;CAEF,MAAM,YAAY,YACZ,aACC,SAAS,OAAgB,MAAM,IAAI,EAAE,IAAI,IAAI,IAAK,UAClD,OAAe,MAAM,UAAU,KAAK,SAAS,EAAE,CAAC,CAAC,EAAE,SAAS;CAInE,MAAM,gBAAgB,mBAAmB,KAAK;CAC9C,MAAM,SAAS,WACV,SAA+B,QAAQ,KAAK,EAAE,KAC9C,SAA+B,cAAc,IAAa;CAI/D,MAAM,cAAc,OAAkC,MAAM,WAAW,EAAE;CACzE,MAAM,SAAS,OAA8C;EAC3D,MAAM,SAAS,MAAM,IAAI,EAAE;EAC3B,IAAI,WAAW,QAAW,OAAO;EACjC,MAAM,MAAM,QAAQ,MAAM;EAC1B,OAAO;GACL,MAAM;GACN,MAAM,OAAO,GAAY;GACzB,IAAI,OAAoB;IACtB,OAAO,mBAAmB,KAAc,OAAO,UAAU;GAC3D;EACF;CACF;CAEA,MAAM,gBAAgB,WACjB,SAA4C;EAC7C,MAAM,IAAI;EACV,OAAO,EAAE,cAAc,SAAY,OAAO,mBAAmB,GAAG,OAAO,UAAU;CACnF,IACE,iBAAiB,KAAK;CAE1B,MAAM,UAAU,KAAK,SAAS,aAAa,MAAM,QAAQ,KAAK,MAAM,IAAI;CAIxE,MAAM,QAAQ,WAAW,OAAO,MAAM,eAAe,aAAa,SAAY,KAAK;CACnF,OAAO;EACL,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;EAC7B,GAAI,QAAQ,EACV,aAAa,aAA0E;GACrF,MAAM,QAAQ,kBAAkB,OAAgB,MAAM,YAAY,QAAQ;GAC1E,OAAO,UAAU,OAAO,OAAO,WAAW,KAAK;EACjD,EACF,IAAI,CAAC;EACL,aAAa,WAAW,MAAM,iBAAiB,CAA+C;EAC9F;EACA;EACA,WAAW,eAAe,KAAK;EAE/B,GAAI,YAAY,EAAE,SAAS,UAAU,IAAI,CAAC;EAC1C,WAAW,SAA+B,MAAM,OAAO,IAAiB;EACxE,WAAW,SAA+B,MAAM,OAAO,IAAiB;EACxE,GAAI,YAAY,OAAO,KAAK,iBACxB,EACA,iBAAiB,UACf,CAAC,YAAY,IAAI,KAAK,MAAM,mBAAmB,UAAa,eAAe,KAAK,GACpF,IACE,CAAC;CACP;AACF;;;AAIA,SAAgB,aACd,QACA,QAC6C;CAC7C,IAAI,OAAqC;CACzC,KAAK,MAAM,KAAK,UAAU,CAAC,GACzB,IAAI,EAAE,UAAU,CAAC,yBAAS,IAAI,IAAI,EAAA,CAAG,IAAI,EAAE,IAAI,SAAS,QAAQ,EAAE,QAAQ,CAAC;CAE7E,IAAI,SAAS,MAAM,OAAO;CAC1B,MAAM,QAAQ;CACd,QAAQ,UAAU,MAAM,IAAI,KAAK,KAAK;AACxC"}
|
|
@@ -22,16 +22,34 @@ function useCanvasSurface(paint, options = {}) {
|
|
|
22
22
|
useLateContentRedraw(requestRedraw);
|
|
23
23
|
useRedrawOnShaderSource(options.shaders, requestRedraw);
|
|
24
24
|
useRedrawOn(options.redrawOn, requestRedraw);
|
|
25
|
-
|
|
25
|
+
const resized = usePaintOnResize(options.size, loop.paintNow);
|
|
26
|
+
useRedrawOnCommit(options.onCommit, () => paint(lease), requestRedraw, resized);
|
|
26
27
|
return {
|
|
27
28
|
...loop,
|
|
28
29
|
lease
|
|
29
30
|
};
|
|
30
31
|
}
|
|
31
|
-
|
|
32
|
+
/** Paints a commit that resized the surface in place. Returns whether this commit did. */
|
|
33
|
+
function usePaintOnResize(size, paintNow) {
|
|
34
|
+
const last = useRef(null);
|
|
35
|
+
const resized = useRef(false);
|
|
36
|
+
const width = size?.width ?? 0;
|
|
37
|
+
const height = size?.height ?? 0;
|
|
38
|
+
useLayoutEffect(() => {
|
|
39
|
+
const was = last.current;
|
|
40
|
+
last.current = {
|
|
41
|
+
width,
|
|
42
|
+
height
|
|
43
|
+
};
|
|
44
|
+
resized.current = was !== null && (was.width !== width || was.height !== height);
|
|
45
|
+
if (resized.current) paintNow();
|
|
46
|
+
});
|
|
47
|
+
return resized;
|
|
48
|
+
}
|
|
49
|
+
function useRedrawOnCommit(mode, paintNow, requestRedraw, resized) {
|
|
32
50
|
const paintedRef = useRef(false);
|
|
33
51
|
useLayoutEffect(() => {
|
|
34
|
-
if (!mode) return;
|
|
52
|
+
if (!mode || resized.current) return;
|
|
35
53
|
if (mode === "redraw" || paintedRef.current) {
|
|
36
54
|
requestRedraw();
|
|
37
55
|
return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useCanvasSurface.js","names":[],"sources":["../../src/canvas/useCanvasSurface.ts"],"sourcesContent":["import { useEffect, useLayoutEffect, useRef } from 'react';\nimport type { ShaderProgramHandle } from '@weasel-js/renderer';\nimport type { RedrawSource } from '../core/layers/render';\nimport type { CanvasRendererLease } from './canvasRenderer';\nimport {\n useCanvasRenderer, useRedrawOnDensityChange, useRedrawOnRestore, useRedrawOnShaderSource,\n} from './useCanvasRenderer';\nimport { useFrameLoop, type FrameLoop, type FrameLoopOptions } from './useFrameLoop';\nimport { useLateContentRedraw } from './useLateContentRedraw';\nimport { useRedrawOn } from './useRedrawOn';\n\n/** What a surface tells {@link useCanvasSurface} about itself, beyond its paint. */\nexport interface CanvasSurfaceOptions extends FrameLoopOptions {\n /** The surface's own density. Omit to follow `devicePixelRatio`. */\n dpr?: number;\n /** External state the paint reads; each source repaints when it notifies. */\n redrawOn?: readonly RedrawSource[];\n /** Custom programs the paint draws with; new source for one repaints. */\n shaders?: readonly ShaderProgramHandle[];\n /** Mark the surface dirty on every committed render, so a surface whose\n * paint reads its props cannot paint them stale. `'paint'` also paints the\n * first commit in place, in the caller's stack, so the first frame shown is\n * the content and not a blank canvas. */\n onCommit?: 'redraw' | 'paint';\n}\n\n/** A surface's frame loop and the renderer lease its paint draws through. */\nexport interface CanvasSurface extends FrameLoop {\n lease: CanvasRendererLease;\n}\n\n/**\n * Everything a canvas surface in core needs around its paint: the renderer\n * lease, the frame loop, and a repaint for each thing that changes pixels with\n * no React render behind it — a restored GL context, a density change, late\n * content, a re-registered shader, and the caller's own `redrawOn` sources.\n *\n * @param paint As `useFrameLoop`'s, handed the lease to draw through.\n */\nexport function useCanvasSurface(\n paint: (lease: CanvasRendererLease) => boolean,\n options: CanvasSurfaceOptions = {},\n): CanvasSurface {\n const lease = useCanvasRenderer();\n const loop = useFrameLoop(() => paint(lease), options);\n const { requestRedraw } = loop;\n useRedrawOnRestore(lease, requestRedraw);\n useRedrawOnDensityChange(requestRedraw, options.dpr);\n useLateContentRedraw(requestRedraw);\n useRedrawOnShaderSource(options.shaders, requestRedraw);\n useRedrawOn(options.redrawOn, requestRedraw);\n useRedrawOnCommit(options.onCommit, () => paint(lease), requestRedraw);\n return { ...loop, lease };\n}\n\nfunction useRedrawOnCommit(\n mode: CanvasSurfaceOptions['onCommit'],\n paintNow: () => boolean,\n requestRedraw: () => void,\n): void {\n const paintedRef = useRef(false);\n // No dependency array: the paint reads whatever the render did.\n useLayoutEffect(() => {\n if (!mode) return;\n if (mode === 'redraw' || paintedRef.current) { requestRedraw(); return; }\n paintedRef.current = paintNow();\n if (!paintedRef.current) requestRedraw();\n });\n // The renderer is freed on unmount; a remount (StrictMode) paints its first\n // frame in place again.\n useEffect(() => () => { paintedRef.current = false; }, []);\n}\n"],"mappings":";;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"useCanvasSurface.js","names":[],"sources":["../../src/canvas/useCanvasSurface.ts"],"sourcesContent":["import { useEffect, useLayoutEffect, useRef } from 'react';\nimport type { ShaderProgramHandle } from '@weasel-js/renderer';\nimport type { RedrawSource } from '../core/layers/render';\nimport type { CanvasRendererLease } from './canvasRenderer';\nimport {\n useCanvasRenderer, useRedrawOnDensityChange, useRedrawOnRestore, useRedrawOnShaderSource,\n} from './useCanvasRenderer';\nimport { useFrameLoop, type FrameLoop, type FrameLoopOptions } from './useFrameLoop';\nimport { useLateContentRedraw } from './useLateContentRedraw';\nimport { useRedrawOn } from './useRedrawOn';\n\n/** What a surface tells {@link useCanvasSurface} about itself, beyond its paint. */\nexport interface CanvasSurfaceOptions extends FrameLoopOptions {\n /** The surface's own density. Omit to follow `devicePixelRatio`. */\n dpr?: number;\n /** External state the paint reads; each source repaints when it notifies. */\n redrawOn?: readonly RedrawSource[];\n /** Custom programs the paint draws with; new source for one repaints. */\n shaders?: readonly ShaderProgramHandle[];\n /** Mark the surface dirty on every committed render, so a surface whose\n * paint reads its props cannot paint them stale. `'paint'` also paints the\n * first commit in place, in the caller's stack, so the first frame shown is\n * the content and not a blank canvas. */\n onCommit?: 'redraw' | 'paint';\n /** The size the surface paints at. A commit that changes it paints in place: React has written the element's\n * new size, which clears its buffer, and a resize committed from a `ResizeObserver` callback has no frame\n * left before the browser paints. */\n size?: { width: number; height: number };\n}\n\n/** A surface's frame loop and the renderer lease its paint draws through. */\nexport interface CanvasSurface extends FrameLoop {\n lease: CanvasRendererLease;\n}\n\n/**\n * Everything a canvas surface in core needs around its paint: the renderer\n * lease, the frame loop, and a repaint for each thing that changes pixels with\n * no React render behind it — a restored GL context, a density change, late\n * content, a re-registered shader, and the caller's own `redrawOn` sources.\n *\n * @param paint As `useFrameLoop`'s, handed the lease to draw through.\n */\nexport function useCanvasSurface(\n paint: (lease: CanvasRendererLease) => boolean,\n options: CanvasSurfaceOptions = {},\n): CanvasSurface {\n const lease = useCanvasRenderer();\n const loop = useFrameLoop(() => paint(lease), options);\n const { requestRedraw } = loop;\n useRedrawOnRestore(lease, requestRedraw);\n useRedrawOnDensityChange(requestRedraw, options.dpr);\n useLateContentRedraw(requestRedraw);\n useRedrawOnShaderSource(options.shaders, requestRedraw);\n useRedrawOn(options.redrawOn, requestRedraw);\n const resized = usePaintOnResize(options.size, loop.paintNow);\n useRedrawOnCommit(options.onCommit, () => paint(lease), requestRedraw, resized);\n return { ...loop, lease };\n}\n\n/** Paints a commit that resized the surface in place. Returns whether this commit did. */\nfunction usePaintOnResize(size: CanvasSurfaceOptions['size'], paintNow: () => void): { readonly current: boolean } {\n const last = useRef<{ width: number; height: number } | null>(null);\n const resized = useRef(false);\n const width = size?.width ?? 0;\n const height = size?.height ?? 0;\n useLayoutEffect(() => {\n const was = last.current;\n last.current = { width, height };\n resized.current = was !== null && (was.width !== width || was.height !== height);\n if (resized.current) paintNow();\n });\n return resized;\n}\n\nfunction useRedrawOnCommit(\n mode: CanvasSurfaceOptions['onCommit'],\n paintNow: () => boolean,\n requestRedraw: () => void,\n resized: { readonly current: boolean },\n): void {\n const paintedRef = useRef(false);\n // No dependency array: the paint reads whatever the render did.\n useLayoutEffect(() => {\n if (!mode || resized.current) return;\n if (mode === 'redraw' || paintedRef.current) { requestRedraw(); return; }\n paintedRef.current = paintNow();\n if (!paintedRef.current) requestRedraw();\n });\n // The renderer is freed on unmount; a remount (StrictMode) paints its first\n // frame in place again.\n useEffect(() => () => { paintedRef.current = false; }, []);\n}\n"],"mappings":";;;;;;;;;;;;;;;AA2CA,SAAgB,iBACd,OACA,UAAgC,CAAC,GAClB;CACf,MAAM,QAAQ,kBAAkB;CAChC,MAAM,OAAO,mBAAmB,MAAM,KAAK,GAAG,OAAO;CACrD,MAAM,EAAE,kBAAkB;CAC1B,mBAAmB,OAAO,aAAa;CACvC,yBAAyB,eAAe,QAAQ,GAAG;CACnD,qBAAqB,aAAa;CAClC,wBAAwB,QAAQ,SAAS,aAAa;CACtD,YAAY,QAAQ,UAAU,aAAa;CAC3C,MAAM,UAAU,iBAAiB,QAAQ,MAAM,KAAK,QAAQ;CAC5D,kBAAkB,QAAQ,gBAAgB,MAAM,KAAK,GAAG,eAAe,OAAO;CAC9E,OAAO;EAAE,GAAG;EAAM;CAAM;AAC1B;;AAGA,SAAS,iBAAiB,MAAoC,UAAqD;CACjH,MAAM,OAAO,OAAiD,IAAI;CAClE,MAAM,UAAU,OAAO,KAAK;CAC5B,MAAM,QAAQ,MAAM,SAAS;CAC7B,MAAM,SAAS,MAAM,UAAU;CAC/B,sBAAsB;EACpB,MAAM,MAAM,KAAK;EACjB,KAAK,UAAU;GAAE;GAAO;EAAO;EAC/B,QAAQ,UAAU,QAAQ,SAAS,IAAI,UAAU,SAAS,IAAI,WAAW;EACzE,IAAI,QAAQ,SAAS,SAAS;CAChC,CAAC;CACD,OAAO;AACT;AAEA,SAAS,kBACP,MACA,UACA,eACA,SACM;CACN,MAAM,aAAa,OAAO,KAAK;CAE/B,sBAAsB;EACpB,IAAI,CAAC,QAAQ,QAAQ,SAAS;EAC9B,IAAI,SAAS,YAAY,WAAW,SAAS;GAAE,cAAc;GAAG;EAAQ;EACxE,WAAW,UAAU,SAAS;EAC9B,IAAI,CAAC,WAAW,SAAS,cAAc;CACzC,CAAC;CAGD,sBAAsB;EAAE,WAAW,UAAU;CAAO,GAAG,CAAC,CAAC;AAC3D"}
|
|
@@ -73,18 +73,16 @@ function useFrameLoop(paint, options = {}) {
|
|
|
73
73
|
frame.request();
|
|
74
74
|
};
|
|
75
75
|
});
|
|
76
|
-
const
|
|
76
|
+
const redraw = useCallback((inPlace) => {
|
|
77
77
|
dirtyRef.current = true;
|
|
78
|
-
if (
|
|
78
|
+
if (inPlace && aliveRef.current && !paintingRef.current && frame.isVisible()) {
|
|
79
79
|
runPaint(performance.now());
|
|
80
80
|
return;
|
|
81
81
|
}
|
|
82
82
|
frame.request();
|
|
83
|
-
}, [
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
syncRef
|
|
87
|
-
]);
|
|
83
|
+
}, [frame, runPaint]);
|
|
84
|
+
const requestRedraw = useCallback(() => redraw(syncRef.current), [redraw, syncRef]);
|
|
85
|
+
const paintNow = useCallback(() => redraw(true), [redraw]);
|
|
88
86
|
const subscribeFrame = useCallback((fn) => {
|
|
89
87
|
subsRef.current.add(fn);
|
|
90
88
|
return () => {
|
|
@@ -117,6 +115,7 @@ function useFrameLoop(paint, options = {}) {
|
|
|
117
115
|
}, []);
|
|
118
116
|
return {
|
|
119
117
|
requestRedraw,
|
|
118
|
+
paintNow,
|
|
120
119
|
subscribeFrame,
|
|
121
120
|
subscribeBeforePaint,
|
|
122
121
|
subscribeResume
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useFrameLoop.js","names":[],"sources":["../../src/canvas/useFrameLoop.ts"],"sourcesContent":["/**\n * The frame loop behind `<Canvas>`: a dirty flag, one frame in flight at a\n * time, and a subscriber set notified after a paint lands. When frames may run\n * at all is `useVisibleRaf`'s question, not this hook's — a redraw requested\n * while nothing can see the surface is held there and re-armed on resume.\n *\n * @internal Not consumer surface. Canvas exposes `requestRedraw` and\n * `subscribeFrame` on its ref handle; this is how they are implemented.\n */\n\nimport { useCallback, useInsertionEffect, useLayoutEffect, useRef } from 'react';\nimport { useLatest, useVisibleRaf, type VisibleRafTarget } from '@weasel-js/react';\n\nexport interface FrameLoop {\n /** Mark the surface dirty and schedule a frame. Identity is stable for the\n * lifetime of the component — consumers capture it. */\n requestRedraw(): void;\n /** Run `fn` after every landed paint, with the frame's time. Returns an\n * unsubscribe. */\n subscribeFrame(fn: (time: number) => void): () => void;\n /** Run `fn` on every frame just before its paint, with the frame's time.\n * Returns an unsubscribe. */\n subscribeBeforePaint(fn: (time: number) => void): () => void;\n /** Run `fn` when the loop leaves suspension, before the frame that follows.\n * Returns an unsubscribe. */\n subscribeResume(fn: () => void): () => void;\n}\n\nexport interface FrameLoopOptions {\n /** Paint in the caller's own stack — the commit or event that asked for the\n * redraw — instead of on the next animation frame. Read live, so toggling\n * it takes effect from the next `requestRedraw` on. */\n syncPaint?: boolean;\n /** The surface being painted. Given one, the loop also stops while that\n * element sits outside the viewport, not only while the tab is hidden. */\n target?: VisibleRafTarget;\n}\n\n/**\n * @param paint Runs on the frame; returns whether pixels landed. A `false`\n * leaves the surface dirty and notifies nobody, so the next request retries.\n */\nexport function useFrameLoop(paint: () => boolean, options: FrameLoopOptions = {}): FrameLoop {\n const dirtyRef = useRef(true);\n const aliveRef = useRef(true);\n const paintingRef = useRef(false);\n const paintRef = useLatest(paint);\n const syncRef = useLatest(options.syncPaint ?? false);\n const subsRef = useRef<Set<(time: number) => void>>(new Set());\n const beforeRef = useRef<Set<(time: number) => void>>(new Set());\n const resumeRef = useRef<Set<() => void>>(new Set());\n /** The last time handed to subscribers. A sync paint reads the clock at the\n * call and a rAF frame carries its frame's start, so within one animation\n * frame the second can be the earlier of the two. */\n const lastTimeRef = useRef(-Infinity);\n\n /** Set while recovering from a paint that threw, so one bad frame is retried\n * and a permanently throwing one still doesn't spin at frame rate. */\n const retriedRef = useRef(false);\n /** `frame.request`, which doesn't exist until after `runPaint` is built. */\n const rearmRef = useRef<(() => void) | null>(null);\n\n const runPaint = useCallback((at: number) => {\n const time = Math.max(at, lastTimeRef.current);\n lastTimeRef.current = time;\n dirtyRef.current = false;\n paintingRef.current = true;\n let landed = false;\n try {\n for (const fn of beforeRef.current) fn(time);\n landed = paintRef.current();\n if (!landed) {\n dirtyRef.current = true;\n return;\n }\n retriedRef.current = false;\n // Notified inside the guard: a subscriber calling `requestRedraw` under\n // `syncPaint` would otherwise re-enter this synchronously, without bound.\n for (const fn of subsRef.current) fn(time);\n } catch (err) {\n // A throw skips the `!landed` branch above, so without this the surface\n // is left clean but unpainted — the pixels on screen are whatever the\n // last landed frame left, and nothing repaints until something\n // unrelated happens to request a redraw. That is how a single\n // malformed node blanked a whole document until the pointer moved.\n dirtyRef.current = true;\n if (!retriedRef.current) {\n retriedRef.current = true;\n rearmRef.current?.();\n }\n throw err;\n } finally {\n paintingRef.current = false;\n }\n }, [paintRef]);\n\n const frame = useVisibleRaf(\n useCallback((time: number) => {\n if (!aliveRef.current || !dirtyRef.current) return;\n runPaint(time);\n }, [runPaint]),\n {\n target: options.target,\n onResume: useCallback(() => { for (const fn of resumeRef.current) fn(); }, []),\n },\n );\n useInsertionEffect(() => {\n rearmRef.current = () => { frame.request(); };\n });\n\n const
|
|
1
|
+
{"version":3,"file":"useFrameLoop.js","names":[],"sources":["../../src/canvas/useFrameLoop.ts"],"sourcesContent":["/**\n * The frame loop behind `<Canvas>`: a dirty flag, one frame in flight at a\n * time, and a subscriber set notified after a paint lands. When frames may run\n * at all is `useVisibleRaf`'s question, not this hook's — a redraw requested\n * while nothing can see the surface is held there and re-armed on resume.\n *\n * @internal Not consumer surface. Canvas exposes `requestRedraw` and\n * `subscribeFrame` on its ref handle; this is how they are implemented.\n */\n\nimport { useCallback, useInsertionEffect, useLayoutEffect, useRef } from 'react';\nimport { useLatest, useVisibleRaf, type VisibleRafTarget } from '@weasel-js/react';\n\nexport interface FrameLoop {\n /** Mark the surface dirty and schedule a frame. Identity is stable for the\n * lifetime of the component — consumers capture it. */\n requestRedraw(): void;\n /** `requestRedraw`, painting in the caller's stack as `syncPaint` does whatever that is set to. */\n paintNow(): void;\n /** Run `fn` after every landed paint, with the frame's time. Returns an\n * unsubscribe. */\n subscribeFrame(fn: (time: number) => void): () => void;\n /** Run `fn` on every frame just before its paint, with the frame's time.\n * Returns an unsubscribe. */\n subscribeBeforePaint(fn: (time: number) => void): () => void;\n /** Run `fn` when the loop leaves suspension, before the frame that follows.\n * Returns an unsubscribe. */\n subscribeResume(fn: () => void): () => void;\n}\n\nexport interface FrameLoopOptions {\n /** Paint in the caller's own stack — the commit or event that asked for the\n * redraw — instead of on the next animation frame. Read live, so toggling\n * it takes effect from the next `requestRedraw` on. */\n syncPaint?: boolean;\n /** The surface being painted. Given one, the loop also stops while that\n * element sits outside the viewport, not only while the tab is hidden. */\n target?: VisibleRafTarget;\n}\n\n/**\n * @param paint Runs on the frame; returns whether pixels landed. A `false`\n * leaves the surface dirty and notifies nobody, so the next request retries.\n */\nexport function useFrameLoop(paint: () => boolean, options: FrameLoopOptions = {}): FrameLoop {\n const dirtyRef = useRef(true);\n const aliveRef = useRef(true);\n const paintingRef = useRef(false);\n const paintRef = useLatest(paint);\n const syncRef = useLatest(options.syncPaint ?? false);\n const subsRef = useRef<Set<(time: number) => void>>(new Set());\n const beforeRef = useRef<Set<(time: number) => void>>(new Set());\n const resumeRef = useRef<Set<() => void>>(new Set());\n /** The last time handed to subscribers. A sync paint reads the clock at the\n * call and a rAF frame carries its frame's start, so within one animation\n * frame the second can be the earlier of the two. */\n const lastTimeRef = useRef(-Infinity);\n\n /** Set while recovering from a paint that threw, so one bad frame is retried\n * and a permanently throwing one still doesn't spin at frame rate. */\n const retriedRef = useRef(false);\n /** `frame.request`, which doesn't exist until after `runPaint` is built. */\n const rearmRef = useRef<(() => void) | null>(null);\n\n const runPaint = useCallback((at: number) => {\n const time = Math.max(at, lastTimeRef.current);\n lastTimeRef.current = time;\n dirtyRef.current = false;\n paintingRef.current = true;\n let landed = false;\n try {\n for (const fn of beforeRef.current) fn(time);\n landed = paintRef.current();\n if (!landed) {\n dirtyRef.current = true;\n return;\n }\n retriedRef.current = false;\n // Notified inside the guard: a subscriber calling `requestRedraw` under\n // `syncPaint` would otherwise re-enter this synchronously, without bound.\n for (const fn of subsRef.current) fn(time);\n } catch (err) {\n // A throw skips the `!landed` branch above, so without this the surface\n // is left clean but unpainted — the pixels on screen are whatever the\n // last landed frame left, and nothing repaints until something\n // unrelated happens to request a redraw. That is how a single\n // malformed node blanked a whole document until the pointer moved.\n dirtyRef.current = true;\n if (!retriedRef.current) {\n retriedRef.current = true;\n rearmRef.current?.();\n }\n throw err;\n } finally {\n paintingRef.current = false;\n }\n }, [paintRef]);\n\n const frame = useVisibleRaf(\n useCallback((time: number) => {\n if (!aliveRef.current || !dirtyRef.current) return;\n runPaint(time);\n }, [runPaint]),\n {\n target: options.target,\n onResume: useCallback(() => { for (const fn of resumeRef.current) fn(); }, []),\n },\n );\n useInsertionEffect(() => {\n rearmRef.current = () => { frame.request(); };\n });\n\n const redraw = useCallback((inPlace: boolean) => {\n dirtyRef.current = true;\n // The gate suppresses the sync path too: a background tab still commits\n // React updates, and that is the one paint browser throttling does not stop.\n // A request made from inside a draw or a frame subscriber would recurse\n // forever if it painted here, so re-entrant ones fall through to a frame.\n if (inPlace && aliveRef.current && !paintingRef.current && frame.isVisible()) {\n runPaint(performance.now());\n return;\n }\n frame.request();\n }, [frame, runPaint]);\n const requestRedraw = useCallback(() => redraw(syncRef.current), [redraw, syncRef]);\n const paintNow = useCallback(() => redraw(true), [redraw]);\n\n const subscribeFrame = useCallback((fn: (time: number) => void) => {\n subsRef.current.add(fn);\n return () => { subsRef.current.delete(fn); };\n }, []);\n const subscribeBeforePaint = useCallback((fn: (time: number) => void) => {\n beforeRef.current.add(fn);\n return () => { beforeRef.current.delete(fn); };\n }, []);\n\n const subscribeResume = useCallback((fn: () => void) => {\n resumeRef.current.add(fn);\n return () => { resumeRef.current.delete(fn); };\n }, []);\n\n // `requestRedraw` outlives the component: `@weasel-js/hud` calls it from a\n // `.then()`, and `viewRegistry.attachSurface` hands it to the consumer.\n // Re-armed in setup because refs survive StrictMode's simulated remount, and\n // a layout effect so the arming precedes Canvas's own layout effects — the\n // sync-paint path runs in one of those.\n useLayoutEffect(() => {\n aliveRef.current = true;\n const subs = subsRef.current;\n const before = beforeRef.current;\n const resume = resumeRef.current;\n return () => {\n aliveRef.current = false;\n subs.clear();\n before.clear();\n resume.clear();\n };\n }, []);\n\n return { requestRedraw, paintNow, subscribeFrame, subscribeBeforePaint, subscribeResume };\n}\n"],"mappings":"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"}
|
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
import { useElementSize } from "../core/viewport/useCanvasSize.js";
|
|
2
|
-
import {
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
3
|
import { useLatest } from "@weasel-js/react";
|
|
4
4
|
|
|
5
5
|
//#region src/canvas/useSurfaceSize.ts
|
|
6
6
|
var NO_ELEMENT = { current: null };
|
|
7
|
+
var FILL = { beforePaint: true };
|
|
7
8
|
/**
|
|
8
9
|
* The CSS-pixel size a canvas host paints at: its `width` and `height`, with
|
|
9
10
|
* an axis left out measured off `element`, which fills its parent on it.
|
|
10
11
|
*/
|
|
11
12
|
function useSurfaceSize(props, element) {
|
|
12
13
|
const fills = props.width === void 0 || props.height === void 0;
|
|
13
|
-
const measured = useElementSize(fills ? element : NO_ELEMENT);
|
|
14
|
+
const measured = useElementSize(fills ? element : NO_ELEMENT, FILL);
|
|
14
15
|
const width = props.width ?? measured.width;
|
|
15
16
|
const height = props.height ?? measured.height;
|
|
16
17
|
const onResize = useLatest(props.onResize);
|
|
17
18
|
const reported = useRef(null);
|
|
18
|
-
|
|
19
|
+
useEffect(() => {
|
|
19
20
|
if (width === 0 || height === 0) return;
|
|
20
21
|
const was = reported.current;
|
|
21
22
|
if (was && was.width === width && was.height === height) return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useSurfaceSize.js","names":[],"sources":["../../src/canvas/useSurfaceSize.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"useSurfaceSize.js","names":[],"sources":["../../src/canvas/useSurfaceSize.ts"],"sourcesContent":["import { useEffect, useRef, type RefObject } from 'react';\nimport { useLatest } from '@weasel-js/react';\nimport type { CanvasSize } from 'core/viewport/clampView';\nimport { useElementSize } from 'core/viewport/useCanvasSize';\n\n/** The size props every canvas host takes. */\nexport interface SurfaceSizeProps {\n /** CSS-pixel width. Omit it, or `height`, and the element fills its parent\n * on that axis and is measured: the parent's box decides the size, and the\n * canvas adds nothing to it, so the parent needs a size of its own. Nothing\n * paints until the measurement lands. */\n width?: number;\n /** CSS-pixel height. Omit it to fill, as `width`. */\n height?: number;\n /** Every size the surface takes, once at mount and again on each change —\n * the measured one for an axis left to fill. */\n onResize?: (size: CanvasSize) => void;\n}\n\nconst NO_ELEMENT: RefObject<HTMLElement | null> = { current: null };\nconst FILL = { beforePaint: true };\n\n/**\n * The CSS-pixel size a canvas host paints at: its `width` and `height`, with\n * an axis left out measured off `element`, which fills its parent on it.\n */\nexport function useSurfaceSize(props: SurfaceSizeProps, element: RefObject<HTMLElement | null>): CanvasSize {\n const fills = props.width === undefined || props.height === undefined;\n // The canvas element is all a filling host lays out, so it can take its size inside the resize callback.\n const measured = useElementSize(fills ? element : NO_ELEMENT, FILL);\n const width = props.width ?? measured.width;\n const height = props.height ?? measured.height;\n\n const onResize = useLatest(props.onResize);\n const reported = useRef<CanvasSize | null>(null);\n // Passive, so a consumer's state set from it renders after the resize callback rather than inside it.\n useEffect(() => {\n // A filling axis reads 0 until measured, and that is no size to report.\n if (width === 0 || height === 0) return;\n const was = reported.current;\n if (was && was.width === width && was.height === height) return;\n reported.current = { width, height };\n onResize.current?.({ width, height });\n }, [width, height, onResize]);\n\n return { width, height };\n}\n"],"mappings":";;;;;AAmBA,IAAM,aAA4C,EAAE,SAAS,KAAK;AAClE,IAAM,OAAO,EAAE,aAAa,KAAK;;;;;AAMjC,SAAgB,eAAe,OAAyB,SAAoD;CAC1G,MAAM,QAAQ,MAAM,UAAU,UAAa,MAAM,WAAW;CAE5D,MAAM,WAAW,eAAe,QAAQ,UAAU,YAAY,IAAI;CAClE,MAAM,QAAQ,MAAM,SAAS,SAAS;CACtC,MAAM,SAAS,MAAM,UAAU,SAAS;CAExC,MAAM,WAAW,UAAU,MAAM,QAAQ;CACzC,MAAM,WAAW,OAA0B,IAAI;CAE/C,gBAAgB;EAEd,IAAI,UAAU,KAAK,WAAW,GAAG;EACjC,MAAM,MAAM,SAAS;EACrB,IAAI,OAAO,IAAI,UAAU,SAAS,IAAI,WAAW,QAAQ;EACzD,SAAS,UAAU;GAAE;GAAO;EAAO;EACnC,SAAS,UAAU;GAAE;GAAO;EAAO,CAAC;CACtC,GAAG;EAAC;EAAO;EAAQ;CAAQ,CAAC;CAE5B,OAAO;EAAE;EAAO;CAAO;AACzB"}
|
|
@@ -103,23 +103,16 @@ function poseOutline(pose, b) {
|
|
|
103
103
|
};
|
|
104
104
|
}
|
|
105
105
|
/**
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
* - `intersect` — the outline meets the region anywhere: its interior too,
|
|
110
|
-
* unless `filled` says nothing paints it.
|
|
111
|
-
* - `enclosed` — the whole outline lies inside it.
|
|
112
|
-
* - `centers` — the center of `b` lies inside it.
|
|
113
|
-
*
|
|
114
|
-
* `b` only rejects: it encloses the outline and may reach past it, so a node
|
|
115
|
-
* is taken by the outline alone. A node without finite bounds is never taken.
|
|
106
|
+
* What `b` alone decides of {@link regionTakes}, or undefined when the
|
|
107
|
+
* outline has to be asked: a caller that builds the outline's thunks per node
|
|
108
|
+
* asks this first and builds them only for the nodes it leaves open.
|
|
116
109
|
*/
|
|
117
|
-
function
|
|
110
|
+
function regionTakesByBounds(region, mode, b) {
|
|
118
111
|
if (!Number.isFinite(b.x) || !Number.isFinite(b.y) || !Number.isFinite(b.width) || !Number.isFinite(b.height)) return false;
|
|
119
112
|
const ab = region.bounds;
|
|
120
113
|
if (b.x >= ab.x + ab.width || b.x + b.width <= ab.x || b.y >= ab.y + ab.height || b.y + b.height <= ab.y) return false;
|
|
121
114
|
if (mode === "centers") return pointInPolygon(region.coords, b.x + b.width / 2, b.y + b.height / 2);
|
|
122
|
-
|
|
115
|
+
if (mode === "intersect" && region.isRect && boxContainsBox([
|
|
123
116
|
ab.x,
|
|
124
117
|
ab.y,
|
|
125
118
|
ab.x + ab.width,
|
|
@@ -129,13 +122,39 @@ function regionTakes(region, mode, pose, b, outline, filled = () => true) {
|
|
|
129
122
|
b.y,
|
|
130
123
|
b.x + b.width,
|
|
131
124
|
b.y + b.height
|
|
132
|
-
]);
|
|
125
|
+
])) return true;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Whether `region` takes a node under `mode`, given its pose, its bounds `b`
|
|
129
|
+
* from {@link regionBoundsOf}, and its drawn outline on demand.
|
|
130
|
+
*
|
|
131
|
+
* - `intersect` — the outline meets the region anywhere: its interior too,
|
|
132
|
+
* unless `filled` says nothing paints it.
|
|
133
|
+
* - `enclosed` — the whole outline lies inside it.
|
|
134
|
+
* - `centers` — the center of `b` lies inside it.
|
|
135
|
+
*
|
|
136
|
+
* `b` only rejects: it encloses the outline and may reach past it, so a node
|
|
137
|
+
* is taken by the outline alone. A node without finite bounds is never taken.
|
|
138
|
+
*/
|
|
139
|
+
function regionTakes(region, mode, pose, b, outline, filled = () => true) {
|
|
140
|
+
const decided = regionTakesByBounds(region, mode, b);
|
|
141
|
+
if (decided !== void 0) return decided;
|
|
142
|
+
const ab = region.bounds;
|
|
133
143
|
const silhouette = isSilhouettePose(pose);
|
|
134
144
|
if (mode === "enclosed") {
|
|
135
|
-
if (silhouette && !
|
|
145
|
+
if (silhouette && !boxContainsBox([
|
|
146
|
+
ab.x,
|
|
147
|
+
ab.y,
|
|
148
|
+
ab.x + ab.width,
|
|
149
|
+
ab.y + ab.height
|
|
150
|
+
], [
|
|
151
|
+
b.x,
|
|
152
|
+
b.y,
|
|
153
|
+
b.x + b.width,
|
|
154
|
+
b.y + b.height
|
|
155
|
+
])) return false;
|
|
136
156
|
return polygonContainsPath(region.verts, silhouette ? pose : outline());
|
|
137
157
|
}
|
|
138
|
-
if (region.isRect && inBox) return true;
|
|
139
158
|
const drawn = silhouette ? pose : outline();
|
|
140
159
|
if (!filled()) return polygonIntersectsPath(region.verts, drawn, { filled: false });
|
|
141
160
|
if (drawn.kind === "rect" && region.isRect) return drawn.x < ab.x + ab.width && drawn.x + drawn.width > ab.x && drawn.y < ab.y + ab.height && drawn.y + drawn.height > ab.y;
|
|
@@ -143,5 +162,5 @@ function regionTakes(region, mode, pose, b, outline, filled = () => true) {
|
|
|
143
162
|
}
|
|
144
163
|
|
|
145
164
|
//#endregion
|
|
146
|
-
export { isSilhouettePose, polygonRegion, poseOutline, rectRegion, regionBoundsOf, regionOf, regionTakes };
|
|
165
|
+
export { isSilhouettePose, polygonRegion, poseOutline, rectRegion, regionBoundsOf, regionOf, regionTakes, regionTakesByBounds };
|
|
147
166
|
//# sourceMappingURL=regionHit.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"regionHit.js","names":[],"sources":["../../../src/core/geometry/regionHit.ts"],"sourcesContent":["/**\n * Whether a marquee or a lasso takes one node: the per-node decision the\n * scene's area walk (`canvas/deps/hitTestArea`) makes.\n *\n * What a node draws is not knowable here — the painters sit above `core/` — so\n * the caller hands in the node's outline as a thunk, asked for only when the\n * bounds cannot decide. A pose that is itself a polygon path is its own\n * outline and never asks.\n */\nimport {\n boxContainsBox,\n pointInPolygon,\n polygonContainsPath,\n polygonIntersectsPath,\n type Path,\n type PolygonPath,\n} from '@weasel-js/geom';\nimport type { Bounds } from 'core/viewport/fitViewToBounds';\nimport type { LassoHitMode } from 'core/adapters/types';\nimport { nodeMemo } from 'core/scene/nodeMemo';\nimport type { Vec2 } from './vec2';\nimport { pathThroughPoseFrame, poseFrameOf } from '@weasel-js/geom';\nimport { visualBoundsViaDescriptor, type PoseDescriptor } from './poseDescriptor';\n\n/** A closed selection area: a marquee rect or a lasso polygon. */\nexport interface SelectRegion {\n /** The area's vertices; the closing edge back to the first is implicit. */\n readonly verts: readonly Vec2[];\n /** `verts` interleaved as `[x0, y0, x1, y1, …]`. */\n readonly coords: readonly number[];\n readonly bounds: Bounds;\n /** True when the area is its own bounding rect, so a node inside `bounds`\n * is inside the area without further work. Never true for a lasso: a node\n * inside the hull's box can still miss the hull. */\n readonly isRect: boolean;\n}\n\n/** A marquee's region. */\nexport function rectRegion(r: Bounds): SelectRegion {\n const { x, y, width: w, height: h } = r;\n return regionOf([x, y, x + w, y, x + w, y + h, x, y + h], { x, y, width: w, height: h }, true)!;\n}\n\n/** A region from interleaved coords. `bounds` is derived when omitted; `null`\n * when there are no coords to bound. */\nexport function regionOf(\n coords: ArrayLike<number>,\n bounds?: Bounds,\n isRect = false,\n): SelectRegion | null {\n const verts: Vec2[] = [];\n const flat: number[] = [];\n let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;\n for (let i = 0; i + 1 < coords.length; i += 2) {\n const x = coords[i], y = coords[i + 1];\n verts.push({ x, y });\n flat.push(x, y);\n if (x < minX) minX = x;\n if (y < minY) minY = y;\n if (x > maxX) maxX = x;\n if (y > maxY) maxY = y;\n }\n if (verts.length === 0) return null;\n return {\n verts,\n coords: flat,\n bounds: bounds ?? { x: minX, y: minY, width: maxX - minX, height: maxY - minY },\n isRect,\n };\n}\n\n/** A lasso's region, or `null` for fewer than three vertices. */\nexport function polygonRegion(polygon: ReadonlyArray<Vec2>): SelectRegion | null {\n if (polygon.length < 3) return null;\n const coords: number[] = [];\n for (const p of polygon) coords.push(p.x, p.y);\n return regionOf(coords);\n}\n\n/** A pose that is its own drawn outline. */\nexport function isSilhouettePose(pose: unknown): pose is PolygonPath {\n return pose !== null && typeof pose === 'object' && (pose as Path).kind === 'polygon';\n}\n\n/**\n * The box a region fast-rejects a node against: a silhouette pose's own bounds,\n * else the pose's visual bounds — which for a rotated pose is the box of its\n * rotated ink, reaching past the pose box. `g` is the node's descriptor.\n */\nexport function regionBoundsOf(node: object, pose: unknown, g: PoseDescriptor<unknown>): Bounds {\n // Memo is silhouettes-only: a rect pose's bounds are the pose itself, and\n // memoizing it costs more than it saves. The result may be shared; never mutate it.\n return isSilhouettePose(pose)\n ? nodeMemo(node, 'aabb', pose, () => g.getBounds(pose))\n : visualBoundsViaDescriptor(pose, g);\n}\n\n/** The outline of a node nothing paints: its pose rect through the pose's\n * frame, else its visual bounds `b`. */\nexport function poseOutline(pose: unknown, b: Bounds): Path {\n if (poseFrameOf(pose)) {\n const p = pose as Bounds;\n return pathThroughPoseFrame({ kind: 'rect', x: p.x, y: p.y, width: p.width, height: p.height }, pose);\n }\n return { kind: 'rect', x: b.x, y: b.y, width: b.width, height: b.height };\n}\n\n/**\n *
|
|
1
|
+
{"version":3,"file":"regionHit.js","names":[],"sources":["../../../src/core/geometry/regionHit.ts"],"sourcesContent":["/**\n * Whether a marquee or a lasso takes one node: the per-node decision the\n * scene's area walk (`canvas/deps/hitTestArea`) makes.\n *\n * What a node draws is not knowable here — the painters sit above `core/` — so\n * the caller hands in the node's outline as a thunk, asked for only when the\n * bounds cannot decide. A pose that is itself a polygon path is its own\n * outline and never asks.\n */\nimport {\n boxContainsBox,\n pointInPolygon,\n polygonContainsPath,\n polygonIntersectsPath,\n type Path,\n type PolygonPath,\n} from '@weasel-js/geom';\nimport type { Bounds } from 'core/viewport/fitViewToBounds';\nimport type { LassoHitMode } from 'core/adapters/types';\nimport { nodeMemo } from 'core/scene/nodeMemo';\nimport type { Vec2 } from './vec2';\nimport { pathThroughPoseFrame, poseFrameOf } from '@weasel-js/geom';\nimport { visualBoundsViaDescriptor, type PoseDescriptor } from './poseDescriptor';\n\n/** A closed selection area: a marquee rect or a lasso polygon. */\nexport interface SelectRegion {\n /** The area's vertices; the closing edge back to the first is implicit. */\n readonly verts: readonly Vec2[];\n /** `verts` interleaved as `[x0, y0, x1, y1, …]`. */\n readonly coords: readonly number[];\n readonly bounds: Bounds;\n /** True when the area is its own bounding rect, so a node inside `bounds`\n * is inside the area without further work. Never true for a lasso: a node\n * inside the hull's box can still miss the hull. */\n readonly isRect: boolean;\n}\n\n/** A marquee's region. */\nexport function rectRegion(r: Bounds): SelectRegion {\n const { x, y, width: w, height: h } = r;\n return regionOf([x, y, x + w, y, x + w, y + h, x, y + h], { x, y, width: w, height: h }, true)!;\n}\n\n/** A region from interleaved coords. `bounds` is derived when omitted; `null`\n * when there are no coords to bound. */\nexport function regionOf(\n coords: ArrayLike<number>,\n bounds?: Bounds,\n isRect = false,\n): SelectRegion | null {\n const verts: Vec2[] = [];\n const flat: number[] = [];\n let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;\n for (let i = 0; i + 1 < coords.length; i += 2) {\n const x = coords[i], y = coords[i + 1];\n verts.push({ x, y });\n flat.push(x, y);\n if (x < minX) minX = x;\n if (y < minY) minY = y;\n if (x > maxX) maxX = x;\n if (y > maxY) maxY = y;\n }\n if (verts.length === 0) return null;\n return {\n verts,\n coords: flat,\n bounds: bounds ?? { x: minX, y: minY, width: maxX - minX, height: maxY - minY },\n isRect,\n };\n}\n\n/** A lasso's region, or `null` for fewer than three vertices. */\nexport function polygonRegion(polygon: ReadonlyArray<Vec2>): SelectRegion | null {\n if (polygon.length < 3) return null;\n const coords: number[] = [];\n for (const p of polygon) coords.push(p.x, p.y);\n return regionOf(coords);\n}\n\n/** A pose that is its own drawn outline. */\nexport function isSilhouettePose(pose: unknown): pose is PolygonPath {\n return pose !== null && typeof pose === 'object' && (pose as Path).kind === 'polygon';\n}\n\n/**\n * The box a region fast-rejects a node against: a silhouette pose's own bounds,\n * else the pose's visual bounds — which for a rotated pose is the box of its\n * rotated ink, reaching past the pose box. `g` is the node's descriptor.\n */\nexport function regionBoundsOf(node: object, pose: unknown, g: PoseDescriptor<unknown>): Bounds {\n // Memo is silhouettes-only: a rect pose's bounds are the pose itself, and\n // memoizing it costs more than it saves. The result may be shared; never mutate it.\n return isSilhouettePose(pose)\n ? nodeMemo(node, 'aabb', pose, () => g.getBounds(pose))\n : visualBoundsViaDescriptor(pose, g);\n}\n\n/** The outline of a node nothing paints: its pose rect through the pose's\n * frame, else its visual bounds `b`. */\nexport function poseOutline(pose: unknown, b: Bounds): Path {\n if (poseFrameOf(pose)) {\n const p = pose as Bounds;\n return pathThroughPoseFrame({ kind: 'rect', x: p.x, y: p.y, width: p.width, height: p.height }, pose);\n }\n return { kind: 'rect', x: b.x, y: b.y, width: b.width, height: b.height };\n}\n\n/**\n * What `b` alone decides of {@link regionTakes}, or undefined when the\n * outline has to be asked: a caller that builds the outline's thunks per node\n * asks this first and builds them only for the nodes it leaves open.\n */\nexport function regionTakesByBounds(region: SelectRegion, mode: LassoHitMode, b: Bounds): boolean | undefined {\n if (\n !Number.isFinite(b.x) || !Number.isFinite(b.y) ||\n !Number.isFinite(b.width) || !Number.isFinite(b.height)\n ) {\n return false;\n }\n const ab = region.bounds;\n if (\n b.x >= ab.x + ab.width || b.x + b.width <= ab.x ||\n b.y >= ab.y + ab.height || b.y + b.height <= ab.y\n ) {\n return false;\n }\n if (mode === 'centers') {\n return pointInPolygon(region.coords, b.x + b.width / 2, b.y + b.height / 2);\n }\n // Swallowed whole by a marquee: no outline can change the answer.\n if (mode === 'intersect' && region.isRect && boxContainsBox(\n [ab.x, ab.y, ab.x + ab.width, ab.y + ab.height],\n [b.x, b.y, b.x + b.width, b.y + b.height],\n )) return true;\n return undefined;\n}\n\n/**\n * Whether `region` takes a node under `mode`, given its pose, its bounds `b`\n * from {@link regionBoundsOf}, and its drawn outline on demand.\n *\n * - `intersect` — the outline meets the region anywhere: its interior too,\n * unless `filled` says nothing paints it.\n * - `enclosed` — the whole outline lies inside it.\n * - `centers` — the center of `b` lies inside it.\n *\n * `b` only rejects: it encloses the outline and may reach past it, so a node\n * is taken by the outline alone. A node without finite bounds is never taken.\n */\nexport function regionTakes(\n region: SelectRegion,\n mode: LassoHitMode,\n pose: unknown,\n b: Bounds,\n outline: () => Path,\n filled: () => boolean = () => true,\n): boolean {\n const decided = regionTakesByBounds(region, mode, b);\n if (decided !== undefined) return decided;\n const ab = region.bounds;\n const silhouette = isSilhouettePose(pose);\n if (mode === 'enclosed') {\n // A silhouette pose's bounds are its outline's: sticking out of the\n // region's box, it cannot be inside the region.\n if (silhouette && !boxContainsBox(\n [ab.x, ab.y, ab.x + ab.width, ab.y + ab.height],\n [b.x, b.y, b.x + b.width, b.y + b.height],\n )) return false;\n return polygonContainsPath(region.verts, silhouette ? pose : outline());\n }\n const drawn = silhouette ? pose : outline();\n if (!filled()) return polygonIntersectsPath(region.verts, drawn, { filled: false });\n // Two axis-aligned rects meet exactly when their boxes overlap.\n if (drawn.kind === 'rect' && region.isRect) {\n return (\n drawn.x < ab.x + ab.width && drawn.x + drawn.width > ab.x &&\n drawn.y < ab.y + ab.height && drawn.y + drawn.height > ab.y\n );\n }\n return polygonIntersectsPath(region.verts, drawn);\n}\n"],"mappings":"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"}
|
|
@@ -1160,9 +1160,9 @@ var MeshBakeCache = class {
|
|
|
1160
1160
|
* patches cover, not the whole shape it is filling, and a clamped read would
|
|
1161
1161
|
* otherwise smear its border colors across the rest.
|
|
1162
1162
|
*
|
|
1163
|
-
* `
|
|
1164
|
-
* bake maps that box onto texel
|
|
1165
|
-
* rescale accounts for. Output is PREMULTIPLIED alpha.
|
|
1163
|
+
* `u_meshSize` is the bake's box in paint space, measured from its corner,
|
|
1164
|
+
* which `u_worldInv` puts at zero; the bake maps that box onto texel
|
|
1165
|
+
* *centers*, which is the half-texel the uv rescale accounts for. Output is PREMULTIPLIED alpha.
|
|
1166
1166
|
*/
|
|
1167
1167
|
/** The id this program is registered under. */
|
|
1168
1168
|
const MESH_PROGRAM_ID = "kit:mesh-gradient";
|
|
@@ -1170,10 +1170,11 @@ const MESH_VERT_SRC = `#version 300 es
|
|
|
1170
1170
|
in vec2 a_position;
|
|
1171
1171
|
uniform mat3 u_proj;
|
|
1172
1172
|
uniform mat3 u_model;
|
|
1173
|
+
uniform vec2 u_origin;
|
|
1173
1174
|
uniform mat3 u_worldInv;
|
|
1174
1175
|
out vec2 v_world;
|
|
1175
1176
|
void main() {
|
|
1176
|
-
vec3 screen = u_model * vec3(a_position, 1.0);
|
|
1177
|
+
vec3 screen = u_model * vec3(a_position - u_origin, 1.0);
|
|
1177
1178
|
vec3 clip = u_proj * vec3(screen.xy, 1.0);
|
|
1178
1179
|
gl_Position = vec4(clip.xy, 0.0, 1.0);
|
|
1179
1180
|
vec3 world = u_worldInv * vec3(screen.xy, 1.0);
|
|
@@ -1184,13 +1185,12 @@ const MESH_FRAG_SRC = `#version 300 es
|
|
|
1184
1185
|
precision highp float;
|
|
1185
1186
|
in vec2 v_world;
|
|
1186
1187
|
uniform sampler2D u_sampler;
|
|
1187
|
-
uniform vec2 u_meshOrigin;
|
|
1188
1188
|
uniform vec2 u_meshSize;
|
|
1189
1189
|
uniform float u_opacity;
|
|
1190
1190
|
uniform float u_alpha;
|
|
1191
1191
|
out vec4 outColor;
|
|
1192
1192
|
void main() {
|
|
1193
|
-
vec2 t =
|
|
1193
|
+
vec2 t = v_world / max(u_meshSize, vec2(0.0001));
|
|
1194
1194
|
if (t.x < 0.0 || t.x > 1.0 || t.y < 0.0 || t.y > 1.0) {
|
|
1195
1195
|
outColor = vec4(0.0);
|
|
1196
1196
|
return;
|
|
@@ -1432,12 +1432,13 @@ function bakeFor(ctx, mesh) {
|
|
|
1432
1432
|
function bindMesh(ctx, fill) {
|
|
1433
1433
|
const mesh = asMesh$1(fill);
|
|
1434
1434
|
if (!mesh.patches.some(isValidPatch)) return null;
|
|
1435
|
-
const inverse = ctx.spaceInverse(mesh.units);
|
|
1436
|
-
if (!inverse) return null;
|
|
1437
1435
|
const program = ctx.program(MESH_PROGRAM_ID);
|
|
1438
1436
|
if (!program) return null;
|
|
1439
1437
|
const entry = bakeFor(ctx, mesh);
|
|
1440
1438
|
if (!entry) return null;
|
|
1439
|
+
const { box } = entry;
|
|
1440
|
+
const inverse = ctx.spaceInverse(mesh.units, box);
|
|
1441
|
+
if (!inverse) return null;
|
|
1441
1442
|
const gl = ctx.gl;
|
|
1442
1443
|
gl.useProgram(program.handle);
|
|
1443
1444
|
ctx.setProjAndModel(program);
|
|
@@ -1445,8 +1446,6 @@ function bindMesh(ctx, fill) {
|
|
|
1445
1446
|
gl.activeTexture(gl.TEXTURE0);
|
|
1446
1447
|
gl.bindTexture(gl.TEXTURE_2D, entry.texture);
|
|
1447
1448
|
gl.uniform1i(program.uniform("u_sampler"), 0);
|
|
1448
|
-
const { box } = entry;
|
|
1449
|
-
gl.uniform2f(program.uniform("u_meshOrigin"), box.x, box.y);
|
|
1450
1449
|
gl.uniform2f(program.uniform("u_meshSize"), box.width, box.height);
|
|
1451
1450
|
gl.uniform1f(program.uniform("u_opacity"), mesh.opacity ?? 1);
|
|
1452
1451
|
gl.uniform1f(program.uniform("u_alpha"), ctx.alpha);
|