@weasel-js/core 1.7.3 → 1.8.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 +47 -0
- package/dist/animation/colorHelpers.js +58 -58
- package/dist/animation/colorHelpers.js.map +1 -1
- package/dist/animation/engine/axes.js +36 -0
- package/dist/animation/engine/axes.js.map +1 -0
- package/dist/animation/engine/codec.js +170 -0
- package/dist/animation/engine/codec.js.map +1 -0
- package/dist/animation/engine/integrator.js +12 -0
- package/dist/animation/engine/integrator.js.map +1 -0
- package/dist/animation/useAnimator.js +199 -157
- package/dist/animation/useAnimator.js.map +1 -1
- package/dist/{builtins-C-gRyVRx.d.ts → builtins-BW32dOXy.d.ts} +74 -8
- package/dist/canvas/SceneCanvas/defaultNodeProperties.js +12 -12
- package/dist/canvas/SceneCanvas/defaultNodeProperties.js.map +1 -1
- package/dist/canvas/pickWalk.js +2 -2
- package/dist/canvas/pickWalk.js.map +1 -1
- package/dist/canvas/sceneSlotWiring.js +1 -1
- package/dist/canvas/sceneSlotWiring.js.map +1 -1
- package/dist/canvas/sceneViewRender.js +1 -1
- package/dist/canvas/sceneViewRender.js.map +1 -1
- package/dist/core/scene/effectivePose.js +1 -1
- package/dist/core/scene/effectivePose.js.map +1 -1
- package/dist/core/scene/poseOverrides.js +42 -22
- package/dist/core/scene/poseOverrides.js.map +1 -1
- package/dist/core/scene/types.js.map +1 -1
- package/dist/index.d.ts +28 -18
- package/dist/renderer/WeaselRenderer.js +4 -1
- package/dist/renderer/WeaselRenderer.js.map +1 -1
- package/dist/renderer/batchClip.js +46 -0
- package/dist/renderer/batchClip.js.map +1 -0
- package/dist/renderer/draw.js +367 -294
- package/dist/renderer/draw.js.map +1 -1
- package/dist/renderer/drawBatch.js +75 -45
- package/dist/renderer/drawBatch.js.map +1 -1
- package/dist/renderer.d.ts +1 -1
- package/dist/tools/builtin/text/useTextTool.js +8 -8
- package/dist/tools/builtin/text/useTextTool.js.map +1 -1
- package/dist/tools/prefs.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +16 -15
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pickWalk.js","names":["EMPTY_PARENTS","ownClipOf","node","pose","findShapeSilhouette","derived","pickWalk","includeContainers","q","isLocked","src","out","clipPath","carry","clipCarrier","clipOf","cached","ancestor","resolved","planeQueries","queryFor","layer","found","map","query","clipped","clip","lockedNodeTest","locked","scene","lockedLayers","cur","asNodeId","id","seen","hiddenLayerIds","layers","scenePickSource","hidden","opts","faded","enumerable","alphaOf","a","readAlpha","composition","chain","effectivePose","definesFrame","poseOf","getPose","composeWorldPose","childrenOf","depOf","dep","resolveDerivedPath","derivedPathOf","n","planeOf","scenePlaneOf","parentId","parent","layerIsPainted","maps","l","planeMap","camera","adapterPickSource","hier","adapter","hierarchical","parents","worldPose","nodeOf","next","visit","childId","gates"],"sources":["../../src/canvas/pickWalk.ts"],"sourcesContent":["/**\n * The one hit-test walk.\n *\n * Four walks used to answer this question — two point queries, two area\n * queries — over the same tree with the same gates. They drifted three times\n * without a failing test, because nothing held them to each other. Everything\n * that decides whether a node *can* be hit lives here now; a query supplies\n * only the shape test and the clip predicate for its own region.\n *\n * The gates, in the order the walk applies them:\n * 1. containers, when the query does not want them\n * 2. the layer is painted at all — scene visibility, and the asking view's\n * `layerVisibility` / `layerOrder`\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 type { Path } from '@weasel-js/geom';\nimport { findShapeSilhouette } from 'canvas/NodeShape';\nimport type { DerivedDep, Node, NodeId, Scene } from 'core/scene/types';\nimport { asNodeId } from 'core/scene/types';\nimport { definesFrame, effectivePose } from 'core/scene/effectivePose';\nimport { resolveDerivedPath } from './derivedPath';\nimport { composeWorldPose, type PoseAdapter, type PoseComposition } from 'features/groups/composePose';\nimport { planeMap, type ParallaxOpts, type PlaneMap } from 'core/viewport/parallax';\nimport type { View } from 'core/viewport/view';\nimport { clipCarrier } from './planeClips';\n\n/** Shared, never mutated: most scenes are flat and every node returns it. */\nconst EMPTY_PARENTS: readonly never[] = [];\n\n/** The subset of a node the walk itself reads. Deliberately structural: a\n * bare adapter's node satisfies it as readily as a `SceneNode`. */\nexport interface PickCandidate<TPose> {\n id: string;\n kind?: string;\n layer?: string;\n pose: TPose;\n /** `false` makes the node transparent to the walk — see `SceneNode.pickable`. */\n pickable?: boolean;\n clipFromPose?: (pose: TPose) => Path | null;\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 /** The path a node computes from its dependencies' poses, or null when it\n * derives nothing. Only a scene-backed source can answer — deriving needs\n * the dependencies' poses — so a bare adapter omits it, and every derived\n * node there keeps answering from its own (placeholder) pose. */\n derivedPathOf?(node: PickCandidate<TPose>): Path | null;\n /** How the asking view's world maps into a parallax layer's, or null for a\n * layer that moves with the camera. Omit for a source with no planes. */\n planeOf?(layer: string): PlaneMap | null;\n}\n\n/** One hit-test question: what region, and what counts as covering it. */\nexport interface PickQuery<TPose> {\n /** Default `true`. The area walks exclude containers so a marquee does not\n * return a container and its children both. */\n includeContainers?: boolean;\n /** Default `false`. Every selecting or editing query passes over locked\n * nodes; one that only samples what is painted can take them back. */\n includeLocked?: boolean;\n /** Does this node, drawn at this pose, cover the query region? `derived`\n * is the node's resolved path when it has one — a derived node's pose is a\n * placeholder, so a region test that reads only the pose answers for the\n * wrong box. */\n hits(node: PickCandidate<TPose>, pose: TPose, derived?: Path | null): boolean;\n /**\n * Does an ancestor's clip leave anything of `node` for this query to hit?\n *\n * A point query asks point-in-clip and ignores the node. An area query has\n * to ask about the node as well: the clip and the area can overlap where\n * the node is not, and the node and the clip can overlap where the area is\n * not, and only the two terms together reject both.\n */\n clipAdmits(\n clip: Path,\n node: PickCandidate<TPose>,\n pose: TPose,\n derived?: Path | null,\n ): boolean;\n /** The same question asked in a parallax plane's world: the region carried\n * through `map`, and any screen-pixel slop re-derived at the plane's scale.\n * A query without it cannot reach nodes on a plane — it would test them\n * where their poses sit rather than where they are painted. */\n inPlane?(map: PlaneMap): PickQuery<TPose>;\n}\n\n/** The clip a container imposes on its subtree, or null when it imposes none. */\nexport function ownClipOf<TPose>(\n node: PickCandidate<TPose>,\n pose: TPose,\n derived?: Path | null,\n): Path | null {\n if (node.kind !== 'container') return null;\n if (typeof node.clipFromPose === 'function') return node.clipFromPose(pose);\n return findShapeSilhouette(\n node as unknown as Node<unknown, string, TPose>,\n pose,\n { derivedPath: derived },\n );\n}\n\n/**\n * Collect every candidate the query hits, back-to-front — the last element is\n * the topmost, which is the order `pickTopMostHit` expects.\n */\nexport function pickWalk<TPose>(\n src: PickSource<TPose>,\n q: PickQuery<TPose>,\n): string[] {\n const includeContainers = q.includeContainers ?? true;\n const isLocked = q.includeLocked === true ? undefined : src.isLocked;\n const out: string[] = [];\n\n // Resolving a container's clip means `clipFromPose` or a silhouette build,\n // which is the expensive half; the predicate against it is cheap. So the\n // *path* is memoized per container for the walk and the predicate re-runs\n // per candidate — `clipAdmits` needs the candidate, and caching its answer\n // per 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 // 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 for (const node of src.order()) {\n if (!includeContainers && node.kind === 'container') continue;\n // Transparent to picking: the click belongs to whatever is behind it,\n // which for a label inside a box is the box.\n if (node.pickable === false) continue;\n if (node.layer !== undefined && src.layerIsPainted?.(node.layer) === false) continue;\n if (src.alphaOf !== undefined && src.alphaOf(node.id) <= 0) continue;\n if (isLocked !== undefined && isLocked(node)) continue;\n\n const query = queryFor(node.layer);\n if (query === null) continue;\n const pose = src.poseOf(node);\n const derived = src.derivedPathOf?.(node);\n if (!query.hits(node, pose, derived)) continue;\n\n let clipped = false;\n for (const ancestor of src.parentsOf(node)) {\n const clip = clipOf(ancestor);\n if (clip === null) continue;\n if (!query.clipAdmits(carry(clip, ancestor.layer, node.layer), node, pose, derived)) {\n clipped = true;\n break;\n }\n }\n if (clipped) continue;\n\n out.push(node.id);\n }\n\n return out;\n}\n\n// ---------------------------------------------------------------------------\n// Sources\n// ---------------------------------------------------------------------------\n\n/** Answers `Scene.isLocked` from the layer records and parent links alone,\n * so a partial scene stand-in works too. Null when nothing is locked, which\n * is what keeps the gate off the walk in the common case. */\nfunction lockedNodeTest(\n scene: Pick<Scene<unknown, string, unknown>, 'layers' | 'get'>,\n): ((id: string) => boolean) | null {\n let locked: Set<string> | null = null;\n for (const layer of scene.layers ?? []) {\n if (layer.locked === true) (locked ??= new Set()).add(layer.id);\n }\n if (locked === null) return null;\n const lockedLayers = locked;\n return (id) => {\n let cur = scene.get(asNodeId(id));\n const seen = new Set<string>();\n while (cur !== undefined && !seen.has(cur.id)) {\n if (lockedLayers.has(cur.layer)) return true;\n seen.add(cur.id);\n cur = cur.parent === null ? undefined : scene.get(cur.parent);\n }\n return false;\n };\n}\n\n/** Layer ids the scene reports as hidden. Only an explicit `false` hides a\n * layer: a partial scene stand-in that omits the flag stays pickable. */\nexport function hiddenLayerIds(\n layers: readonly { id: string; visible?: boolean }[] | undefined,\n): Set<string> {\n const out = new Set<string>();\n for (const layer of layers ?? []) {\n if (layer.visible === false) out.add(layer.id);\n }\n return out;\n}\n\n/**\n * The two answers a pick needs from the view it is asked for rather than from\n * the data behind it: how that view paints a node, and whether it paints a\n * layer at all. A scene and a bare adapter take the same pair.\n */\nexport interface ViewPickGates {\n /** Painted alpha: the view's `alphaFor` times any per-node override alpha.\n * A node painted at alpha 0 cannot be hit. */\n alphaOf?: (id: string) => number;\n /** Whether a node's `layer` reaches the screen in the asking view. */\n layerIsPainted?: (layer: string) => boolean;\n}\n\n/** The gates a bare adapter takes from its view, plus how that view's camera\n * maps into each parallax layer — see `scenePlaneOf`. */\nexport interface AdapterPickGates extends ViewPickGates {\n planeOf?: (layer: string) => PlaneMap | null;\n}\n\nexport interface ScenePickSourceOptions<TPose> extends ViewPickGates {\n /** Pose lookup, when the caller holds an adapter that already resolves\n * overrides. Defaults to `effectivePose` against the scene's own. */\n getPose?: (id: string) => TPose;\n /** The asking view's composed alpha — `alphaFor(id)` times override alpha.\n * Defaults to the override alpha alone, which is all a bare scene knows. */\n alphaOf?: (id: string) => number;\n /** The asking view's answer to \"does this scene layer reach the screen\",\n * which is `layerVisibility` and `layerOrder` on top of the scene's own\n * `visible` flag. Consulted after the scene's flag, never instead of it. */\n layerIsPainted?: (layer: string) => boolean;\n /** How a child's stored pose folds into its parent's frame. Needed because\n * picking tests where a node is drawn, and under a composing strategy that\n * is not where its own pose says. Ignored when `getPose` already answers in\n * world coordinates. */\n poseComposition?: PoseComposition<TPose>;\n /** The asking view's camera. With it, a layer carrying `parallax` is picked\n * through its plane; without it, every layer is taken to move with the\n * camera. */\n camera?: View;\n}\n\nexport function scenePickSource<TData, TLayer extends string, TPose>(\n scene: Scene<TData, TLayer, TPose>,\n opts: ScenePickSourceOptions<TPose> = {},\n): PickSource<TPose> {\n const hidden = hiddenLayerIds(scene.layers);\n const locked = lockedNodeTest(scene as never);\n const { getPose, alphaOf, layerIsPainted } = opts;\n\n // Resolved once per walk rather than once per candidate: a scene with\n // nothing faded has nothing to say about alpha, and asking it per node is a\n // Map lookup on a linear scan that has no other reason to touch the\n // override table. Needs enumeration — a partial scene stand-in carrying\n // only `get` falls back to reading per node.\n let faded: Set<string> | null = null;\n const enumerable = typeof scene.overrides?.ids === 'function';\n if (!alphaOf && enumerable) {\n for (const id of scene.overrides.ids()) {\n const a = scene.overrides.get(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.get(asNodeId(id))?.alpha ?? 1);\n\n // A composing strategy makes a node's own pose local, so picking has to fold\n // the parent chain the same way the render walk does or it tests where the\n // node is stored rather than where it is drawn.\n const composition = opts.poseComposition;\n const chain: PoseAdapter<TPose> | null =\n composition !== undefined && composition.closure !== 'identity'\n ? {\n getPose: (id: string) => {\n const n = scene.get(asNodeId(id));\n return n ? effectivePose(scene, n) : (undefined as unknown as TPose);\n },\n getParent: (id: string) => scene.get(asNodeId(id))?.parent ?? null,\n definesFrame: (id: string) => {\n const n = scene.get(asNodeId(id));\n return n === undefined || definesFrame(n);\n },\n }\n : null;\n\n const poseOf = getPose\n ? (node: PickCandidate<TPose>) => getPose(node.id)\n : chain\n ? (node: PickCandidate<TPose>) => composeWorldPose(chain, node.id, composition!.compose)\n : (node: PickCandidate<TPose>) => effectivePose(scene, node as never);\n\n // Through `poseOf`, not the document pose: a derived node has to be tested\n // where the renderer draws its dependencies, which is what an override says.\n const childrenOf = (id: NodeId): readonly NodeId[] => scene.childrenOf(id);\n const depOf = (id: NodeId): DerivedDep<TPose> | undefined => {\n const dep = scene.get(id);\n if (dep === undefined) return undefined;\n return {\n node: dep as Node<unknown, string, TPose>,\n pose: poseOf(dep as never),\n get path(): Path | null {\n return resolveDerivedPath(dep as never, depOf, childrenOf);\n },\n };\n };\n\n const derivedPathOf = (node: PickCandidate<TPose>): Path | null => {\n const n = node as unknown as Node<TData, TLayer, TPose>;\n if (n.dependsOn === undefined) return null;\n return resolveDerivedPath(n, depOf, childrenOf);\n };\n\n const planeOf = opts.camera ? scenePlaneOf(scene.layers, opts.camera) : null;\n\n return {\n ...(planeOf ? { planeOf } : {}),\n order: () => scene.renderOrderNodes() as unknown as readonly PickCandidate<TPose>[],\n derivedPathOf,\n poseOf,\n parentsOf(node) {\n let parentId = (node as { parent?: string | null }).parent ?? null;\n if (parentId === null) return EMPTY_PARENTS;\n const chain: PickCandidate<TPose>[] = [];\n const seen = new Set<string>();\n while (parentId !== null) {\n if (seen.has(parentId)) break;\n seen.add(parentId);\n const parent = scene.get(asNodeId(parentId));\n if (!parent) break;\n chain.push(parent as unknown as PickCandidate<TPose>);\n parentId = parent.parent ?? null;\n }\n return chain;\n },\n // Absent entirely when nothing is faded, so the walk skips the gate.\n ...(readAlpha ? { alphaOf: readAlpha } : {}),\n ...(locked ? { isLocked: (node: PickCandidate<TPose>) => locked(node.id) } : {}),\n ...(hidden.size > 0 || layerIsPainted\n ? {\n layerIsPainted: (layer: string) =>\n !hidden.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\n/** The adapter surface the walk can reach without a `Scene`. */\ninterface PickAdapter<TPose> {\n /** Composed world pose, when the adapter has a composition strategy.\n * Falls back to `getPose`, which is the same value under the\n * absolute-pose model. */\n getWorldPose?(id: string): TPose;\n getNodes(): readonly { id: string }[];\n getPose(id: string): TPose;\n getNode?: (id: string) => unknown;\n getChildren?: (parentId: string | null) => readonly string[];\n}\n\n/**\n * A source over a bare `SelectAdapter`, for consumers with no `Scene`.\n *\n * A hierarchical adapter is walked depth-first, parents before children —\n * the same back-to-front order `renderOrderNodes` produces for a single\n * layer. A flat adapter falls back to `getNodes()`, which carries no\n * parentage, so nothing clips.\n *\n * `gates` come from whichever view is picking, never from the adapter.\n */\nexport function adapterPickSource<TPose>(\n adapter: PickAdapter<TPose>,\n gates: AdapterPickGates = {},\n): PickSource<TPose> {\n const hier = adapter as Required<Pick<PickAdapter<TPose>, 'getNode' | 'getChildren'>>;\n const hierarchical =\n typeof adapter.getChildren === 'function' && typeof adapter.getNode === 'function';\n\n // Rebuilt per walk: the tree may have changed between queries, and a walk\n // is the only thing that reads it.\n let parents = new Map<string, PickCandidate<TPose>>();\n\n // The adapter composes if it has a strategy; `getPose` alone is the node's\n // own pose, which is not where a framed child is drawn.\n const worldPose = (id: string): TPose =>\n (typeof adapter.getWorldPose === 'function' ? adapter.getWorldPose(id) : adapter.getPose(id));\n\n const nodeOf = (id: string): PickCandidate<TPose> => {\n const raw = hier.getNode(id) as Partial<PickCandidate<TPose>>;\n return { ...raw, id, pose: worldPose(id) } as PickCandidate<TPose>;\n };\n\n return {\n order() {\n if (!hierarchical) {\n parents = new Map();\n return adapter.getNodes().map((n) => ({\n ...(n as object), id: n.id, pose: worldPose(n.id),\n })) as PickCandidate<TPose>[];\n }\n const out: PickCandidate<TPose>[] = [];\n const next = new Map<string, PickCandidate<TPose>>();\n const visit = (parentId: string | null, parent: PickCandidate<TPose> | null): void => {\n for (const childId of hier.getChildren(parentId)) {\n const node = nodeOf(childId);\n if (parent) next.set(childId, parent);\n out.push(node);\n visit(childId, node);\n }\n };\n visit(null, null);\n parents = next;\n return out;\n },\n ...(gates.alphaOf ? { alphaOf: gates.alphaOf } : {}),\n ...(gates.layerIsPainted ? { layerIsPainted: gates.layerIsPainted } : {}),\n ...(gates.planeOf ? { planeOf: gates.planeOf } : {}),\n poseOf: (node) => worldPose(node.id),\n parentsOf(node) {\n const chain: PickCandidate<TPose>[] = [];\n let cur = parents.get(node.id);\n const seen = new Set<string>();\n while (cur && !seen.has(cur.id)) {\n seen.add(cur.id);\n chain.push(cur);\n cur = parents.get(cur.id);\n }\n return chain;\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"pickWalk.js","names":["EMPTY_PARENTS","ownClipOf","node","pose","findShapeSilhouette","derived","pickWalk","includeContainers","q","isLocked","src","out","clipPath","carry","clipCarrier","clipOf","cached","ancestor","resolved","planeQueries","queryFor","layer","found","map","query","clipped","clip","lockedNodeTest","locked","scene","lockedLayers","cur","asNodeId","id","seen","hiddenLayerIds","layers","scenePickSource","hidden","opts","faded","enumerable","alphaOf","a","readAlpha","composition","chain","effectivePose","definesFrame","poseOf","getPose","composeWorldPose","childrenOf","depOf","dep","resolveDerivedPath","derivedPathOf","n","planeOf","scenePlaneOf","parentId","parent","layerIsPainted","maps","l","planeMap","camera","adapterPickSource","hier","adapter","hierarchical","parents","worldPose","nodeOf","next","visit","childId","gates"],"sources":["../../src/canvas/pickWalk.ts"],"sourcesContent":["/**\n * The one hit-test walk.\n *\n * Four walks used to answer this question — two point queries, two area\n * queries — over the same tree with the same gates. They drifted three times\n * without a failing test, because nothing held them to each other. Everything\n * that decides whether a node *can* be hit lives here now; a query supplies\n * only the shape test and the clip predicate for its own region.\n *\n * The gates, in the order the walk applies them:\n * 1. containers, when the query does not want them\n * 2. the layer is painted at all — scene visibility, and the asking view's\n * `layerVisibility` / `layerOrder`\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 type { Path } from '@weasel-js/geom';\nimport { findShapeSilhouette } from 'canvas/NodeShape';\nimport type { DerivedDep, Node, NodeId, Scene } from 'core/scene/types';\nimport { asNodeId } from 'core/scene/types';\nimport { definesFrame, effectivePose } from 'core/scene/effectivePose';\nimport { resolveDerivedPath } from './derivedPath';\nimport { composeWorldPose, type PoseAdapter, type PoseComposition } from 'features/groups/composePose';\nimport { planeMap, type ParallaxOpts, type PlaneMap } from 'core/viewport/parallax';\nimport type { View } from 'core/viewport/view';\nimport { clipCarrier } from './planeClips';\n\n/** Shared, never mutated: most scenes are flat and every node returns it. */\nconst EMPTY_PARENTS: readonly never[] = [];\n\n/** The subset of a node the walk itself reads. Deliberately structural: a\n * bare adapter's node satisfies it as readily as a `SceneNode`. */\nexport interface PickCandidate<TPose> {\n id: string;\n kind?: string;\n layer?: string;\n pose: TPose;\n /** `false` makes the node transparent to the walk — see `SceneNode.pickable`. */\n pickable?: boolean;\n clipFromPose?: (pose: TPose) => Path | null;\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 /** The path a node computes from its dependencies' poses, or null when it\n * derives nothing. Only a scene-backed source can answer — deriving needs\n * the dependencies' poses — so a bare adapter omits it, and every derived\n * node there keeps answering from its own (placeholder) pose. */\n derivedPathOf?(node: PickCandidate<TPose>): Path | null;\n /** How the asking view's world maps into a parallax layer's, or null for a\n * layer that moves with the camera. Omit for a source with no planes. */\n planeOf?(layer: string): PlaneMap | null;\n}\n\n/** One hit-test question: what region, and what counts as covering it. */\nexport interface PickQuery<TPose> {\n /** Default `true`. The area walks exclude containers so a marquee does not\n * return a container and its children both. */\n includeContainers?: boolean;\n /** Default `false`. Every selecting or editing query passes over locked\n * nodes; one that only samples what is painted can take them back. */\n includeLocked?: boolean;\n /** Does this node, drawn at this pose, cover the query region? `derived`\n * is the node's resolved path when it has one — a derived node's pose is a\n * placeholder, so a region test that reads only the pose answers for the\n * wrong box. */\n hits(node: PickCandidate<TPose>, pose: TPose, derived?: Path | null): boolean;\n /**\n * Does an ancestor's clip leave anything of `node` for this query to hit?\n *\n * A point query asks point-in-clip and ignores the node. An area query has\n * to ask about the node as well: the clip and the area can overlap where\n * the node is not, and the node and the clip can overlap where the area is\n * not, and only the two terms together reject both.\n */\n clipAdmits(\n clip: Path,\n node: PickCandidate<TPose>,\n pose: TPose,\n derived?: Path | null,\n ): boolean;\n /** The same question asked in a parallax plane's world: the region carried\n * through `map`, and any screen-pixel slop re-derived at the plane's scale.\n * A query without it cannot reach nodes on a plane — it would test them\n * where their poses sit rather than where they are painted. */\n inPlane?(map: PlaneMap): PickQuery<TPose>;\n}\n\n/** The clip a container imposes on its subtree, or null when it imposes none. */\nexport function ownClipOf<TPose>(\n node: PickCandidate<TPose>,\n pose: TPose,\n derived?: Path | null,\n): Path | null {\n if (node.kind !== 'container') return null;\n if (typeof node.clipFromPose === 'function') return node.clipFromPose(pose);\n return findShapeSilhouette(\n node as unknown as Node<unknown, string, TPose>,\n pose,\n { derivedPath: derived },\n );\n}\n\n/**\n * Collect every candidate the query hits, back-to-front — the last element is\n * the topmost, which is the order `pickTopMostHit` expects.\n */\nexport function pickWalk<TPose>(\n src: PickSource<TPose>,\n q: PickQuery<TPose>,\n): string[] {\n const includeContainers = q.includeContainers ?? true;\n const isLocked = q.includeLocked === true ? undefined : src.isLocked;\n const out: string[] = [];\n\n // Resolving a container's clip means `clipFromPose` or a silhouette build,\n // which is the expensive half; the predicate against it is cheap. So the\n // *path* is memoized per container for the walk and the predicate re-runs\n // per candidate — `clipAdmits` needs the candidate, and caching its answer\n // per 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 // 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 for (const node of src.order()) {\n if (!includeContainers && node.kind === 'container') continue;\n // Transparent to picking: the click belongs to whatever is behind it,\n // which for a label inside a box is the box.\n if (node.pickable === false) continue;\n if (node.layer !== undefined && src.layerIsPainted?.(node.layer) === false) continue;\n if (src.alphaOf !== undefined && src.alphaOf(node.id) <= 0) continue;\n if (isLocked !== undefined && isLocked(node)) continue;\n\n const query = queryFor(node.layer);\n if (query === null) continue;\n const pose = src.poseOf(node);\n const derived = src.derivedPathOf?.(node);\n if (!query.hits(node, pose, derived)) continue;\n\n let clipped = false;\n for (const ancestor of src.parentsOf(node)) {\n const clip = clipOf(ancestor);\n if (clip === null) continue;\n if (!query.clipAdmits(carry(clip, ancestor.layer, node.layer), node, pose, derived)) {\n clipped = true;\n break;\n }\n }\n if (clipped) continue;\n\n out.push(node.id);\n }\n\n return out;\n}\n\n// ---------------------------------------------------------------------------\n// Sources\n// ---------------------------------------------------------------------------\n\n/** Answers `Scene.isLocked` from the layer records and parent links alone,\n * so a partial scene stand-in works too. Null when nothing is locked, which\n * is what keeps the gate off the walk in the common case. */\nfunction lockedNodeTest(\n scene: Pick<Scene<unknown, string, unknown>, 'layers' | 'get'>,\n): ((id: string) => boolean) | null {\n let locked: Set<string> | null = null;\n for (const layer of scene.layers ?? []) {\n if (layer.locked === true) (locked ??= new Set()).add(layer.id);\n }\n if (locked === null) return null;\n const lockedLayers = locked;\n return (id) => {\n let cur = scene.get(asNodeId(id));\n const seen = new Set<string>();\n while (cur !== undefined && !seen.has(cur.id)) {\n if (lockedLayers.has(cur.layer)) return true;\n seen.add(cur.id);\n cur = cur.parent === null ? undefined : scene.get(cur.parent);\n }\n return false;\n };\n}\n\n/** Layer ids the scene reports as hidden. Only an explicit `false` hides a\n * layer: a partial scene stand-in that omits the flag stays pickable. */\nexport function hiddenLayerIds(\n layers: readonly { id: string; visible?: boolean }[] | undefined,\n): Set<string> {\n const out = new Set<string>();\n for (const layer of layers ?? []) {\n if (layer.visible === false) out.add(layer.id);\n }\n return out;\n}\n\n/**\n * The two answers a pick needs from the view it is asked for rather than from\n * the data behind it: how that view paints a node, and whether it paints a\n * layer at all. A scene and a bare adapter take the same pair.\n */\nexport interface ViewPickGates {\n /** Painted alpha: the view's `alphaFor` times any per-node override alpha.\n * A node painted at alpha 0 cannot be hit. */\n alphaOf?: (id: string) => number;\n /** Whether a node's `layer` reaches the screen in the asking view. */\n layerIsPainted?: (layer: string) => boolean;\n}\n\n/** The gates a bare adapter takes from its view, plus how that view's camera\n * maps into each parallax layer — see `scenePlaneOf`. */\nexport interface AdapterPickGates extends ViewPickGates {\n planeOf?: (layer: string) => PlaneMap | null;\n}\n\nexport interface ScenePickSourceOptions<TPose> extends ViewPickGates {\n /** Pose lookup, when the caller holds an adapter that already resolves\n * overrides. Defaults to `effectivePose` against the scene's own. */\n getPose?: (id: string) => TPose;\n /** The asking view's composed alpha — `alphaFor(id)` times override alpha.\n * Defaults to the override alpha alone, which is all a bare scene knows. */\n alphaOf?: (id: string) => number;\n /** The asking view's answer to \"does this scene layer reach the screen\",\n * which is `layerVisibility` and `layerOrder` on top of the scene's own\n * `visible` flag. Consulted after the scene's flag, never instead of it. */\n layerIsPainted?: (layer: string) => boolean;\n /** How a child's stored pose folds into its parent's frame. Needed because\n * picking tests where a node is drawn, and under a composing strategy that\n * is not where its own pose says. Ignored when `getPose` already answers in\n * world coordinates. */\n poseComposition?: PoseComposition<TPose>;\n /** The asking view's camera. With it, a layer carrying `parallax` is picked\n * through its plane; without it, every layer is taken to move with the\n * camera. */\n camera?: View;\n}\n\nexport function scenePickSource<TData, TLayer extends string, TPose>(\n scene: Scene<TData, TLayer, TPose>,\n opts: ScenePickSourceOptions<TPose> = {},\n): PickSource<TPose> {\n const hidden = hiddenLayerIds(scene.layers);\n const locked = lockedNodeTest(scene as never);\n const { getPose, alphaOf, layerIsPainted } = opts;\n\n // Resolved once per walk rather than once per candidate: a scene with\n // nothing faded has nothing to say about alpha, and asking it per node is a\n // Map lookup on a linear scan that has no other reason to touch the\n // override table. Needs enumeration — a partial scene stand-in carrying\n // only `read` falls back to reading per node.\n let faded: Set<string> | null = null;\n const enumerable = typeof scene.overrides?.ids === 'function';\n if (!alphaOf && enumerable) {\n for (const id of scene.overrides.ids()) {\n const a = scene.overrides.read(id)?.alpha;\n if (a !== undefined && a <= 0) (faded ??= new Set()).add(id);\n }\n }\n const readAlpha = alphaOf\n ?? (enumerable\n ? (faded ? (id: string) => (faded.has(id) ? 0 : 1) : undefined)\n : (id: string) => scene.overrides.read(asNodeId(id))?.alpha ?? 1);\n\n // A composing strategy makes a node's own pose local, so picking has to fold\n // the parent chain the same way the render walk does or it tests where the\n // node is stored rather than where it is drawn.\n const composition = opts.poseComposition;\n const chain: PoseAdapter<TPose> | null =\n composition !== undefined && composition.closure !== 'identity'\n ? {\n getPose: (id: string) => {\n const n = scene.get(asNodeId(id));\n return n ? effectivePose(scene, n) : (undefined as unknown as TPose);\n },\n getParent: (id: string) => scene.get(asNodeId(id))?.parent ?? null,\n definesFrame: (id: string) => {\n const n = scene.get(asNodeId(id));\n return n === undefined || definesFrame(n);\n },\n }\n : null;\n\n const poseOf = getPose\n ? (node: PickCandidate<TPose>) => getPose(node.id)\n : chain\n ? (node: PickCandidate<TPose>) => composeWorldPose(chain, node.id, composition!.compose)\n : (node: PickCandidate<TPose>) => effectivePose(scene, node as never);\n\n // Through `poseOf`, not the document pose: a derived node has to be tested\n // where the renderer draws its dependencies, which is what an override says.\n const childrenOf = (id: NodeId): readonly NodeId[] => scene.childrenOf(id);\n const depOf = (id: NodeId): DerivedDep<TPose> | undefined => {\n const dep = scene.get(id);\n if (dep === undefined) return undefined;\n return {\n node: dep as Node<unknown, string, TPose>,\n pose: poseOf(dep as never),\n get path(): Path | null {\n return resolveDerivedPath(dep as never, depOf, childrenOf);\n },\n };\n };\n\n const derivedPathOf = (node: PickCandidate<TPose>): Path | null => {\n const n = node as unknown as Node<TData, TLayer, TPose>;\n if (n.dependsOn === undefined) return null;\n return resolveDerivedPath(n, depOf, childrenOf);\n };\n\n const planeOf = opts.camera ? scenePlaneOf(scene.layers, opts.camera) : null;\n\n return {\n ...(planeOf ? { planeOf } : {}),\n order: () => scene.renderOrderNodes() as unknown as readonly PickCandidate<TPose>[],\n derivedPathOf,\n poseOf,\n parentsOf(node) {\n let parentId = (node as { parent?: string | null }).parent ?? null;\n if (parentId === null) return EMPTY_PARENTS;\n const chain: PickCandidate<TPose>[] = [];\n const seen = new Set<string>();\n while (parentId !== null) {\n if (seen.has(parentId)) break;\n seen.add(parentId);\n const parent = scene.get(asNodeId(parentId));\n if (!parent) break;\n chain.push(parent as unknown as PickCandidate<TPose>);\n parentId = parent.parent ?? null;\n }\n return chain;\n },\n // Absent entirely when nothing is faded, so the walk skips the gate.\n ...(readAlpha ? { alphaOf: readAlpha } : {}),\n ...(locked ? { isLocked: (node: PickCandidate<TPose>) => locked(node.id) } : {}),\n ...(hidden.size > 0 || layerIsPainted\n ? {\n layerIsPainted: (layer: string) =>\n !hidden.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\n/** The adapter surface the walk can reach without a `Scene`. */\ninterface PickAdapter<TPose> {\n /** Composed world pose, when the adapter has a composition strategy.\n * Falls back to `getPose`, which is the same value under the\n * absolute-pose model. */\n getWorldPose?(id: string): TPose;\n getNodes(): readonly { id: string }[];\n getPose(id: string): TPose;\n getNode?: (id: string) => unknown;\n getChildren?: (parentId: string | null) => readonly string[];\n}\n\n/**\n * A source over a bare `SelectAdapter`, for consumers with no `Scene`.\n *\n * A hierarchical adapter is walked depth-first, parents before children —\n * the same back-to-front order `renderOrderNodes` produces for a single\n * layer. A flat adapter falls back to `getNodes()`, which carries no\n * parentage, so nothing clips.\n *\n * `gates` come from whichever view is picking, never from the adapter.\n */\nexport function adapterPickSource<TPose>(\n adapter: PickAdapter<TPose>,\n gates: AdapterPickGates = {},\n): PickSource<TPose> {\n const hier = adapter as Required<Pick<PickAdapter<TPose>, 'getNode' | 'getChildren'>>;\n const hierarchical =\n typeof adapter.getChildren === 'function' && typeof adapter.getNode === 'function';\n\n // Rebuilt per walk: the tree may have changed between queries, and a walk\n // is the only thing that reads it.\n let parents = new Map<string, PickCandidate<TPose>>();\n\n // The adapter composes if it has a strategy; `getPose` alone is the node's\n // own pose, which is not where a framed child is drawn.\n const worldPose = (id: string): TPose =>\n (typeof adapter.getWorldPose === 'function' ? adapter.getWorldPose(id) : adapter.getPose(id));\n\n const nodeOf = (id: string): PickCandidate<TPose> => {\n const raw = hier.getNode(id) as Partial<PickCandidate<TPose>>;\n return { ...raw, id, pose: worldPose(id) } as PickCandidate<TPose>;\n };\n\n return {\n order() {\n if (!hierarchical) {\n parents = new Map();\n return adapter.getNodes().map((n) => ({\n ...(n as object), id: n.id, pose: worldPose(n.id),\n })) as PickCandidate<TPose>[];\n }\n const out: PickCandidate<TPose>[] = [];\n const next = new Map<string, PickCandidate<TPose>>();\n const visit = (parentId: string | null, parent: PickCandidate<TPose> | null): void => {\n for (const childId of hier.getChildren(parentId)) {\n const node = nodeOf(childId);\n if (parent) next.set(childId, parent);\n out.push(node);\n visit(childId, node);\n }\n };\n visit(null, null);\n parents = next;\n return out;\n },\n ...(gates.alphaOf ? { alphaOf: gates.alphaOf } : {}),\n ...(gates.layerIsPainted ? { layerIsPainted: gates.layerIsPainted } : {}),\n ...(gates.planeOf ? { planeOf: gates.planeOf } : {}),\n poseOf: (node) => worldPose(node.id),\n parentsOf(node) {\n const chain: PickCandidate<TPose>[] = [];\n let cur = parents.get(node.id);\n const seen = new Set<string>();\n while (cur && !seen.has(cur.id)) {\n seen.add(cur.id);\n chain.push(cur);\n cur = parents.get(cur.id);\n }\n return chain;\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -2,7 +2,7 @@ import { sceneDerivedPathOf as e, withDerivedPaths as t } from "./derivedPath.js
|
|
|
2
2
|
import { withColorOverrides as n } from "./colorOverrides.js";
|
|
3
3
|
//#region src/canvas/sceneSlotWiring.ts
|
|
4
4
|
function r(e, t) {
|
|
5
|
-
let n = (t) => e.overrides.
|
|
5
|
+
let n = (t) => e.overrides.read(t)?.alpha ?? 1;
|
|
6
6
|
return t ? (e) => t(e) * n(e) : n;
|
|
7
7
|
}
|
|
8
8
|
function i(i, a, o, s) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sceneSlotWiring.js","names":["composeAlphaFor","overrideAlphaFor","scene","id","alphaFor","wireSceneSlotToScene","derived","withDerivedPaths","slot","derivedPathOf","sceneDerivedPathOf","colorOverrides","withColorOverrides"],"sources":["../../src/canvas/sceneSlotWiring.ts"],"sourcesContent":["/**\n * What the scene slot needs from the `Scene` that `<Canvas>` cannot see.\n *\n * `<Canvas>` is scene-agnostic: it walks an adapter. Three things a node is\n * painted with are therefore invisible to it — an ephemeral override's alpha, a\n * derived path, which needs other nodes' poses to resolve, and animated vertex\n * colors, which live in an animator. `<SceneCanvas>` folds them into the slot\n * here; `buildSceneViewCommands` does the same for the headless walk.\n */\nimport type { SceneSlotConfig } from './Canvas';\nimport type { Node, NodeId, Scene } from 'core/scene/types';\nimport { withDerivedPaths, sceneDerivedPathOf } from './derivedPath';\nimport { withColorOverrides } from './colorOverrides';\nimport type { ColorOverrideRegistry } from '../animation/colorRegistry';\n\n/**\n * The alpha a view paints a node at: the view's own `alphaFor` times any\n * ephemeral override alpha.\n *\n * The hit path asks this too — a node painted at alpha 0 is not on screen and\n * must not claim clicks — so it is one function rather than the painter's and\n * picking's separate readings of the same two inputs.\n */\nexport function composeAlphaFor<TData, TLayer extends string, TPose>(\n scene: Scene<TData, TLayer, TPose>,\n alphaFor?: (id: string) => number,\n): (id: string) => number {\n const overrideAlphaFor = (id: string) => scene.overrides.
|
|
1
|
+
{"version":3,"file":"sceneSlotWiring.js","names":["composeAlphaFor","overrideAlphaFor","scene","id","alphaFor","wireSceneSlotToScene","derived","withDerivedPaths","slot","derivedPathOf","sceneDerivedPathOf","colorOverrides","withColorOverrides"],"sources":["../../src/canvas/sceneSlotWiring.ts"],"sourcesContent":["/**\n * What the scene slot needs from the `Scene` that `<Canvas>` cannot see.\n *\n * `<Canvas>` is scene-agnostic: it walks an adapter. Three things a node is\n * painted with are therefore invisible to it — an ephemeral override's alpha, a\n * derived path, which needs other nodes' poses to resolve, and animated vertex\n * colors, which live in an animator. `<SceneCanvas>` folds them into the slot\n * here; `buildSceneViewCommands` does the same for the headless walk.\n */\nimport type { SceneSlotConfig } from './Canvas';\nimport type { Node, NodeId, Scene } from 'core/scene/types';\nimport { withDerivedPaths, sceneDerivedPathOf } from './derivedPath';\nimport { withColorOverrides } from './colorOverrides';\nimport type { ColorOverrideRegistry } from '../animation/colorRegistry';\n\n/**\n * The alpha a view paints a node at: the view's own `alphaFor` times any\n * ephemeral override alpha.\n *\n * The hit path asks this too — a node painted at alpha 0 is not on screen and\n * must not claim clicks — so it is one function rather than the painter's and\n * picking's separate readings of the same two inputs.\n */\nexport function composeAlphaFor<TData, TLayer extends string, TPose>(\n scene: Scene<TData, TLayer, TPose>,\n alphaFor?: (id: string) => number,\n): (id: string) => number {\n const overrideAlphaFor = (id: string) => scene.overrides.read(id as NodeId)?.alpha ?? 1;\n return alphaFor ? (id: string) => alphaFor(id) * overrideAlphaFor(id) : overrideAlphaFor;\n}\n\nexport function wireSceneSlotToScene<TData, TLayer extends string, TPose>(\n slot: SceneSlotConfig<Node<TData, TLayer, TPose>, TPose>,\n scene: Scene<TData, TLayer, TPose>,\n alphaFor?: (id: string) => number,\n colorOverrides?: ColorOverrideRegistry,\n): SceneSlotConfig<Node<TData, TLayer, TPose>, TPose> {\n const derived = withDerivedPaths(scene, slot.drawOne, slot.toPose);\n const derivedPathOf = sceneDerivedPathOf(scene, slot.toPose);\n return {\n ...slot,\n drawOne: colorOverrides ? withColorOverrides(colorOverrides, derived) : derived,\n derivedPathOf,\n alphaFor: composeAlphaFor(scene, alphaFor),\n };\n}\n"],"mappings":";;;AAuBA,SAAgBA,EACd,GACA,GACwB;CACxB,IAAMC,KAAoB,MAAeC,EAAM,UAAU,KAAKC,CAAY,CAAC,EAAE,SAAS;CACtF,OAAOC,KAAY,MAAeA,EAASD,CAAE,IAAIF,EAAiBE,CAAE,IAAIF;AAC1E;AAEA,SAAgBI,EACd,GACA,GACA,GACA,GACoD;CACpD,IAAMC,IAAUC,EAAiBL,GAAOM,EAAK,SAASA,EAAK,MAAM,GAC3DC,IAAgBC,EAAmBR,GAAOM,EAAK,MAAM;CAC3D,OAAO;EACL,GAAGA;EACH,SAASG,IAAiBC,EAAmBD,GAAgBL,CAAO,IAAIA;EACxE;EACA,UAAUN,EAAgBE,GAAOE,CAAQ;CAC3C;AACF"}
|
|
@@ -29,7 +29,7 @@ function p(e, t = {}) {
|
|
|
29
29
|
}
|
|
30
30
|
function m(r, o, s, c, m, h, g, _, v) {
|
|
31
31
|
let y = a(r, s), b = _ ? f(_, y) : y, x = i(r), S = (e, t, n) => {
|
|
32
|
-
let i = r.overrides.
|
|
32
|
+
let i = r.overrides.read(e.id)?.alpha ?? 1;
|
|
33
33
|
return u(b(e, t, n), t, (m ? m(e.id) : 1) * i);
|
|
34
34
|
}, C = h !== void 0 && h.closure !== "identity" ? {
|
|
35
35
|
...p(r, g),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sceneViewRender.js","names":["sceneAsHierarchy","paintedSceneLayers","scene","layers","l","id","parentId","node","effectivePose","buildSceneViewCommands","derivedOnly","withDerivedPaths","drawOne","derived","colorOverrides","withColorOverrides","depLookup","sceneDepLookup","wrappedDrawOne","overrideAlpha","wrapNodeOutput","pose","v","alphaFor","hierarchy","poseComposition","cullRect","cull","culled","paintMissesView","view","tree","buildSceneTree","resolveDerivedPath","children","cullDrawCommands","viewToMat3","extraCommands","cmd","sceneViewFrame","args","paintBoundsFor","renderSceneToCanvas","canvas"],"sources":["../../src/canvas/sceneViewRender.ts"],"sourcesContent":["/**\n * `renderSceneToCanvas` — substrate for detached, read-only scene views\n * (`<SceneViewCanvas>` / `<MinimapCanvas>`). Given a `<canvas>`, a `Scene`,\n * a `View`, and a per-node `drawOne`, render one frame.\n *\n * The full `<SceneCanvas>` render pipeline does much more than this (slots, layer\n * ordering, debug sinks, tool overlays). The helper deliberately re-implements\n * the small subset needed for a non-interactive scene view rather than\n * refactoring `<SceneCanvas>` to share code — the spec calls for that\n * dedupe as a follow-up.\n *\n * Note: this duplicates `<SceneCanvas>`'s GL setup + view-wrap pattern in\n * miniature. When `<SceneCanvas>` is eventually migrated onto this helper,\n * the duplication collapses; until then, keep the two render paths\n * in sync by hand where it matters (DPR handling, viewToMat3 wrap).\n */\n\nimport type { WeaselRenderer } from '../renderer/WeaselRenderer';\nimport { cachedCanvasRenderer, paintCanvas } from './canvasRenderer';\nimport type { PoseComposition } from 'features/groups/composePose';\nimport { viewToMat3 } from '../renderer/math/viewToMat3';\nimport type { DrawCommand } from '../renderer/DrawCommand';\nimport type { View } from '../core/viewport/view';\nimport type { Node, NodeId, Scene } from '../core/scene/types';\nimport { wrapNodeOutput } from './wrapNodeOutput';\nimport { buildSceneTree, type HierarchicalAdapter } from './buildSceneTree';\nimport { effectivePose } from 'core/scene/effectivePose';\nimport { withDerivedPaths, resolveDerivedPath, sceneDepLookup } from './derivedPath';\nimport { withColorOverrides } from './colorOverrides';\nimport type { ColorOverrideRegistry } from '../animation/colorRegistry';\nimport type { SceneViewDrawOne } from './NodeShape';\nimport { paintedSceneLayers } from './sceneLayerPaint';\nimport { cullDrawCommands } from '../renderer/cullDrawCommands';\nimport { paintMissesView, type PaintBoundsFn } from './paintCull';\nimport { paintBoundsFor } from './defaultDrawOne';\n\nexport type { SceneViewDrawOne } from './NodeShape';\n\n/**\n * Which scene layers a detached view paints, on top of each layer's own\n * `visible` flag. Keyed the way `<SceneCanvas>` keys its scene layers —\n * `scene:<layerId>` — so the map a main canvas takes serves its minimap too.\n */\nexport interface SceneViewLayers {\n /** `false` hides that layer in this view. It cannot show one the scene hides. */\n layerVisibility?: Record<string, boolean>;\n /** Paint order, bottom first. A listed order is the whole list: a scene\n * layer left out of it is not painted. */\n layerOrder?: readonly string[];\n}\n\n/** The view size `buildSceneViewCommands` culls to, and how it bounds a\n * node's paint before painting it. */\nexport interface SceneViewCull<TData, TLayer extends string, TPose> {\n width: number;\n height: number;\n paintBounds?: PaintBoundsFn<Node<TData, TLayer, TPose>, TPose>;\n}\n\n/** What to draw into an existing canvas: the scene, the view, and the same\n * painting hooks `<SceneCanvas>` takes. */\nexport interface RenderSceneToCanvasArgs<TData, TLayer extends string, TPose> {\n /** The DOM canvas to paint into. The helper owns its WebGL2 context: the\n * first call on a given canvas creates a renderer and stashes it; subsequent\n * calls reuse the same renderer (and resize the drawing buffer on dim\n * changes). `releaseCanvasRenderer(canvas)` frees it. */\n canvas: HTMLCanvasElement;\n /** Scene to walk. `scene.renderOrder()` provides the per-node order. */\n scene: Scene<TData, TLayer, TPose>;\n /** Camera view applied to scene + extras (both ride the same world→screen\n * transform; extras living in screen space should be supplied via a\n * separately-prepared `extraCommands` group or rendered outside this\n * helper). */\n view: View;\n /** CSS-pixel size. The drawing buffer is set to `width × dpr` and\n * `height × dpr`; CSS size to `width` / `height`. */\n width: number;\n height: number;\n /** Per-node draw callback. */\n drawOne: SceneViewDrawOne<TData, TLayer, TPose>;\n /** Optional commands appended after the scene-node output. Used for the\n * minimap's visible-window indicator. Wrapped in the same view transform\n * as scene commands. */\n extraCommands?: DrawCommand[];\n /** Optional per-id alpha multiplier, mirroring `<SceneCanvas>`'s scene-slot\n * `alphaFor`. Pass the same function the main canvas uses to keep a\n * scoping-dim treatment consistent across both. Defaults to `() => 1`. */\n alphaFor?: (id: string) => number;\n /** Per-view layer visibility. See {@link SceneViewLayers}. */\n layerVisibility?: SceneViewLayers['layerVisibility'];\n /** Per-view layer order. See {@link SceneViewLayers}. */\n layerOrder?: SceneViewLayers['layerOrder'];\n /** Animated vertex colors to paint, typically an animator's\n * `colorOverrides` — see `NodePaintCtx.vertexColors`. */\n colorOverrides?: ColorOverrideRegistry;\n /** Skip what cannot reach the `width × height` view: nodes `paintBounds`\n * places outside it are not painted, and scene commands that cannot reach\n * it are dropped before the renderer — the scene slot's `cull`. Off by\n * default. `extraCommands` are never culled. */\n cull?: boolean;\n /** Where `drawOne`'s output for a node can reach, as on the scene slot.\n * Defaults to `defaultPaintBounds` when `drawOne` is `defaultDrawOne`. */\n paintBounds?: PaintBoundsFn<Node<TData, TLayer, TPose>, TPose>;\n /** Optional device-pixel ratio. Defaults to `window.devicePixelRatio || 1`\n * when available, otherwise 1. Tests typically pin this to 1 or 2. */\n dpr?: number;\n}\n\n/** Test seam: the renderer `renderSceneToCanvas` created for a canvas. Not\n * re-exported from the barrel. */\nexport function __getCachedRendererForTest(\n canvas: HTMLCanvasElement,\n): WeaselRenderer | undefined {\n return cachedCanvasRenderer(canvas);\n}\n\n/**\n * Present a `Scene` as the reading surface `buildSceneTree` walks. Built\n * inline rather than via `sceneToAdapter` so this module stays free of React\n * — `renderSceneToPixels` runs headless.\n */\nfunction sceneAsHierarchy<TData, TLayer extends string, TPose>(\n scene: Scene<TData, TLayer, TPose>,\n layers: SceneViewLayers = {},\n): HierarchicalAdapter<Node<TData, TLayer, TPose>, TPose> {\n return {\n getLayers: () =>\n paintedSceneLayers(scene.layers, layers)\n .map((l) => ({ id: l.id, visible: l.visible, parallax: l.parallax })),\n getNode: (id) => scene.get(id as NodeId),\n getChildren: (parentId) => {\n if (parentId === null) return scene.roots as readonly string[];\n const node = scene.get(parentId as NodeId);\n return node && node.kind === 'container' ? (node.children as readonly string[]) : [];\n },\n getPose: (id) => effectivePose(scene, scene.get(id as NodeId)!),\n } as HierarchicalAdapter<Node<TData, TLayer, TPose>, TPose>;\n}\n\n/**\n * Build the DrawCommand list that `renderSceneToCanvas` would dispatch.\n *\n * Pure — no GL, no DOM, no side effects. Walks the scene through\n * `buildSceneTree` — the same walk `<SceneCanvas>`'s `buildSceneLayer` uses —\n * appends any `extraCommands`, and wraps the result in a single\n * `{ kind: 'group', transform: viewToMat3(view) }` so the output is in screen\n * space. Sharing the walk is what keeps hidden layers unpainted and container\n * clips applied here; a hand-rolled `renderOrder()` loop silently dropped both.\n *\n * Each node's commands additionally go through `wrapNodeOutput` — pose\n * rotation, then the `alphaFor` multiplier — exactly as `buildSceneLayer`\n * does, so a `drawOne` shared between the two paints the same thing in both.\n * A `drawOne` that rotates its own output will therefore rotate twice here;\n * emit unrotated geometry and let the pose drive it.\n *\n * Output shape follows `buildSceneTree`: one group per **visible** layer, in\n * scene-layer order (or `layers.layerOrder`), each holding one group per node\n * on that layer.\n *\n * Exported for tests and for callers that want to render into something\n * other than a real `<canvas>` (e.g. an offscreen renderer or a\n * snapshot fixture).\n */\nexport function buildSceneViewCommands<TData, TLayer extends string, TPose>(\n scene: Scene<TData, TLayer, TPose>,\n view: View,\n drawOne: SceneViewDrawOne<TData, TLayer, TPose>,\n extraCommands?: ReadonlyArray<DrawCommand>,\n alphaFor?: (id: string) => number,\n /** How a child's stored pose folds into its parent's frame. Omit for the\n * absolute-pose model. */\n poseComposition?: PoseComposition<TPose>,\n /** This view's own layer visibility and order. */\n layers?: SceneViewLayers,\n /** Animated vertex colors to paint, typically an animator's `colorOverrides`. */\n colorOverrides?: ColorOverrideRegistry,\n /** Cull the scene to this CSS-pixel view size — see\n * `RenderSceneToCanvasArgs.cull`. Omit to paint every node. */\n cull?: SceneViewCull<TData, TLayer, TPose>,\n): DrawCommand[] {\n // The scene is in scope here, so this is where the override's alpha and the\n // derived paths are applied on the headless path; `SceneCanvas` does the same\n // for the live one. `Canvas` itself never sees a scene, so it can't and doesn't.\n const derivedOnly = withDerivedPaths(scene, drawOne);\n const derived = colorOverrides ? withColorOverrides(colorOverrides, derivedOnly) : derivedOnly;\n const depLookup = sceneDepLookup(scene);\n const wrappedDrawOne = (\n node: Node<TData, TLayer, TPose>,\n pose: TPose,\n v: View,\n ): DrawCommand[] => {\n const overrideAlpha = scene.overrides.get(node.id as NodeId)?.alpha ?? 1;\n return wrapNodeOutput(\n derived(node, pose, v),\n pose,\n (alphaFor ? alphaFor(node.id) : 1) * overrideAlpha,\n );\n };\n\n const composes = poseComposition !== undefined && poseComposition.closure !== 'identity';\n const hierarchy = composes\n ? { ...sceneAsHierarchy(scene, layers), composePose: poseComposition!.compose }\n : sceneAsHierarchy(scene, layers);\n\n const cullRect = cull ? { x: 0, y: 0, width: cull.width, height: cull.height } : undefined;\n const culled = cullRect && cull?.paintBounds ? paintMissesView(cull.paintBounds, view, cullRect) : undefined;\n const tree: DrawCommand[] = buildSceneTree(\n hierarchy as Parameters<typeof buildSceneTree>[0],\n wrappedDrawOne as unknown as Parameters<typeof buildSceneTree>[1],\n view,\n undefined,\n ((node: unknown) =>\n resolveDerivedPath(\n node as Node<TData, TLayer, TPose>, depLookup, (id) => scene.childrenOf(id),\n )) as Parameters<typeof buildSceneTree>[4],\n culled as Parameters<typeof buildSceneTree>[5],\n );\n const children = cullRect ? cullDrawCommands(tree, viewToMat3(view), cullRect) : tree;\n if (extraCommands && extraCommands.length > 0) {\n for (const cmd of extraCommands) children.push(cmd);\n }\n return [{\n kind: 'group',\n transform: viewToMat3(view),\n children,\n }];\n}\n\n/**\n * The commands `renderSceneToCanvas` paints for one frame — everything in\n * `args` but the canvas. `<SceneViewCanvas>` paints these through its own\n * renderer lease.\n */\nexport function sceneViewFrame<TData, TLayer extends string, TPose>(\n args: Omit<RenderSceneToCanvasArgs<TData, TLayer, TPose>, 'canvas'>,\n): DrawCommand[] {\n const {\n scene, view, width, height, drawOne, extraCommands, alphaFor, layerVisibility, layerOrder,\n colorOverrides, cull,\n } = args;\n return buildSceneViewCommands(\n scene, view, drawOne, extraCommands, alphaFor, undefined, { layerVisibility, layerOrder }, colorOverrides,\n cull ? { width, height, paintBounds: args.paintBounds ?? paintBoundsFor(drawOne) } : undefined,\n );\n}\n\n/**\n * Render one frame of `scene` at `view` into `canvas`. See module docstring\n * for the design context.\n *\n * Paints through `paintCanvas` (`./canvasRenderer`): one renderer per canvas,\n * created on the first call and resized when `width` / `height` / `dpr`\n * change. Where no WebGL2 context is available (jsdom) the call paints\n * nothing. The renderer lives until `releaseCanvasRenderer(canvas)`.\n */\nexport function renderSceneToCanvas<TData, TLayer extends string, TPose>(\n args: RenderSceneToCanvasArgs<TData, TLayer, TPose>,\n): void {\n const { canvas, view, width, height, dpr } = args;\n paintCanvas(canvas, sceneViewFrame(args), view, { width, height, dpr });\n}\n"],"mappings":";;;;;;;;;;;;;AAyHA,SAASA,EACP,GACA,IAA0B,CAAC,GAC6B;CACxD,OAAO;EACL,iBACEC,EAAmBC,EAAM,QAAQC,CAAM,CAAC,CACrC,KAAK,OAAO;GAAE,IAAIC,EAAE;GAAI,SAASA,EAAE;GAAS,UAAUA,EAAE;EAAS,EAAE;EACxE,UAAU,MAAOF,EAAM,IAAIG,CAAY;EACvC,cAAc,MAAa;GACzB,IAAIC,MAAa,MAAM,OAAOJ,EAAM;GACpC,IAAMK,IAAOL,EAAM,IAAII,CAAkB;GACzC,OAAOC,KAAQA,EAAK,SAAS,cAAeA,EAAK,WAAiC,CAAC;EACrF;EACA,UAAU,MAAOC,EAAcN,GAAOA,EAAM,IAAIG,CAAY,CAAE;CAChE;AACF;AA0BA,SAAgBI,EACd,GACA,GACA,GACA,GACA,GAGA,GAEA,GAEA,GAGA,GACe;CAIf,IAAMC,IAAcC,EAAiBT,GAAOU,CAAO,GAC7CC,IAAUC,IAAiBC,EAAmBD,GAAgBJ,CAAW,IAAIA,GAC7EM,IAAYC,EAAef,CAAK,GAChCgB,KACJ,GACA,GACA,MACkB;EAClB,IAAMC,IAAgBjB,EAAM,UAAU,IAAIK,EAAK,EAAY,CAAC,EAAE,SAAS;EACvE,OAAOa,EACLP,EAAQN,GAAMc,GAAMC,CAAC,GACrBD,IACCE,IAAWA,EAAShB,EAAK,EAAE,IAAI,KAAKY,CACvC;CACF,GAGMK,IADWC,MAAoB,KAAA,KAAaA,EAAgB,YAAY,aAE1E;EAAE,GAAGzB,EAAiBE,GAAOC,CAAM;EAAG,aAAasB,EAAiB;CAAQ,IAC5EzB,EAAiBE,GAAOC,CAAM,GAE5BuB,IAAWC,IAAO;EAAE,GAAG;EAAG,GAAG;EAAG,OAAOA,EAAK;EAAO,QAAQA,EAAK;CAAO,IAAI,KAAA,GAC3EC,IAASF,KAAYC,GAAM,cAAcE,EAAgBF,EAAK,aAAaG,GAAMJ,CAAQ,IAAI,KAAA,GAC7FK,IAAsBC,EAC1BR,GACAN,GACAY,GACA,KAAA,KACE,MACAG,EACE1B,GAAoCS,IAAY,MAAOd,EAAM,WAAWG,CAAE,CAC5E,IACFuB,CACF,GACMM,IAAWR,IAAWS,EAAiBJ,GAAMK,EAAWN,CAAI,GAAGJ,CAAQ,IAAIK;CACjF,IAAIM,KAAiBA,EAAc,SAAS,GAC1C,KAAK,IAAMC,KAAOD,GAAe,EAAS,KAAKC,CAAG;CAEpD,OAAO,CAAC;EACN,MAAM;EACN,WAAWF,EAAWN,CAAI;EAC1B;CACF,CAAC;AACH;AAOA,SAAgBS,EACd,GACe;CACf,IAAM,EACJ,UAAO,SAAM,UAAO,WAAQ,YAAS,kBAAe,aAAU,oBAAiB,eAC/E,mBAAgB,YACdC;CACJ,OAAO/B,EACLP,GAAO4B,GAAMlB,GAASyB,GAAed,GAAU,KAAA,GAAW;EAAE;EAAiB;CAAW,GAAGT,GAC3Fa,IAAO;EAAE;EAAO;EAAQ,aAAaa,EAAK,eAAeC,EAAe7B,CAAO;CAAE,IAAI,KAAA,CACvF;AACF;AAWA,SAAgB8B,EACd,GACM;CACN,IAAM,EAAE,WAAQ,SAAM,UAAO,WAAQ,WAAQF;CAC7C,EAAYG,GAAQJ,EAAeC,CAAI,GAAGV,GAAM;EAAE;EAAO;EAAQ;CAAI,CAAC;AACxE"}
|
|
1
|
+
{"version":3,"file":"sceneViewRender.js","names":["sceneAsHierarchy","paintedSceneLayers","scene","layers","l","id","parentId","node","effectivePose","buildSceneViewCommands","derivedOnly","withDerivedPaths","drawOne","derived","colorOverrides","withColorOverrides","depLookup","sceneDepLookup","wrappedDrawOne","overrideAlpha","wrapNodeOutput","pose","v","alphaFor","hierarchy","poseComposition","cullRect","cull","culled","paintMissesView","view","tree","buildSceneTree","resolveDerivedPath","children","cullDrawCommands","viewToMat3","extraCommands","cmd","sceneViewFrame","args","paintBoundsFor","renderSceneToCanvas","canvas"],"sources":["../../src/canvas/sceneViewRender.ts"],"sourcesContent":["/**\n * `renderSceneToCanvas` — substrate for detached, read-only scene views\n * (`<SceneViewCanvas>` / `<MinimapCanvas>`). Given a `<canvas>`, a `Scene`,\n * a `View`, and a per-node `drawOne`, render one frame.\n *\n * The full `<SceneCanvas>` render pipeline does much more than this (slots, layer\n * ordering, debug sinks, tool overlays). The helper deliberately re-implements\n * the small subset needed for a non-interactive scene view rather than\n * refactoring `<SceneCanvas>` to share code — the spec calls for that\n * dedupe as a follow-up.\n *\n * Note: this duplicates `<SceneCanvas>`'s GL setup + view-wrap pattern in\n * miniature. When `<SceneCanvas>` is eventually migrated onto this helper,\n * the duplication collapses; until then, keep the two render paths\n * in sync by hand where it matters (DPR handling, viewToMat3 wrap).\n */\n\nimport type { WeaselRenderer } from '../renderer/WeaselRenderer';\nimport { cachedCanvasRenderer, paintCanvas } from './canvasRenderer';\nimport type { PoseComposition } from 'features/groups/composePose';\nimport { viewToMat3 } from '../renderer/math/viewToMat3';\nimport type { DrawCommand } from '../renderer/DrawCommand';\nimport type { View } from '../core/viewport/view';\nimport type { Node, NodeId, Scene } from '../core/scene/types';\nimport { wrapNodeOutput } from './wrapNodeOutput';\nimport { buildSceneTree, type HierarchicalAdapter } from './buildSceneTree';\nimport { effectivePose } from 'core/scene/effectivePose';\nimport { withDerivedPaths, resolveDerivedPath, sceneDepLookup } from './derivedPath';\nimport { withColorOverrides } from './colorOverrides';\nimport type { ColorOverrideRegistry } from '../animation/colorRegistry';\nimport type { SceneViewDrawOne } from './NodeShape';\nimport { paintedSceneLayers } from './sceneLayerPaint';\nimport { cullDrawCommands } from '../renderer/cullDrawCommands';\nimport { paintMissesView, type PaintBoundsFn } from './paintCull';\nimport { paintBoundsFor } from './defaultDrawOne';\n\nexport type { SceneViewDrawOne } from './NodeShape';\n\n/**\n * Which scene layers a detached view paints, on top of each layer's own\n * `visible` flag. Keyed the way `<SceneCanvas>` keys its scene layers —\n * `scene:<layerId>` — so the map a main canvas takes serves its minimap too.\n */\nexport interface SceneViewLayers {\n /** `false` hides that layer in this view. It cannot show one the scene hides. */\n layerVisibility?: Record<string, boolean>;\n /** Paint order, bottom first. A listed order is the whole list: a scene\n * layer left out of it is not painted. */\n layerOrder?: readonly string[];\n}\n\n/** The view size `buildSceneViewCommands` culls to, and how it bounds a\n * node's paint before painting it. */\nexport interface SceneViewCull<TData, TLayer extends string, TPose> {\n width: number;\n height: number;\n paintBounds?: PaintBoundsFn<Node<TData, TLayer, TPose>, TPose>;\n}\n\n/** What to draw into an existing canvas: the scene, the view, and the same\n * painting hooks `<SceneCanvas>` takes. */\nexport interface RenderSceneToCanvasArgs<TData, TLayer extends string, TPose> {\n /** The DOM canvas to paint into. The helper owns its WebGL2 context: the\n * first call on a given canvas creates a renderer and stashes it; subsequent\n * calls reuse the same renderer (and resize the drawing buffer on dim\n * changes). `releaseCanvasRenderer(canvas)` frees it. */\n canvas: HTMLCanvasElement;\n /** Scene to walk. `scene.renderOrder()` provides the per-node order. */\n scene: Scene<TData, TLayer, TPose>;\n /** Camera view applied to scene + extras (both ride the same world→screen\n * transform; extras living in screen space should be supplied via a\n * separately-prepared `extraCommands` group or rendered outside this\n * helper). */\n view: View;\n /** CSS-pixel size. The drawing buffer is set to `width × dpr` and\n * `height × dpr`; CSS size to `width` / `height`. */\n width: number;\n height: number;\n /** Per-node draw callback. */\n drawOne: SceneViewDrawOne<TData, TLayer, TPose>;\n /** Optional commands appended after the scene-node output. Used for the\n * minimap's visible-window indicator. Wrapped in the same view transform\n * as scene commands. */\n extraCommands?: DrawCommand[];\n /** Optional per-id alpha multiplier, mirroring `<SceneCanvas>`'s scene-slot\n * `alphaFor`. Pass the same function the main canvas uses to keep a\n * scoping-dim treatment consistent across both. Defaults to `() => 1`. */\n alphaFor?: (id: string) => number;\n /** Per-view layer visibility. See {@link SceneViewLayers}. */\n layerVisibility?: SceneViewLayers['layerVisibility'];\n /** Per-view layer order. See {@link SceneViewLayers}. */\n layerOrder?: SceneViewLayers['layerOrder'];\n /** Animated vertex colors to paint, typically an animator's\n * `colorOverrides` — see `NodePaintCtx.vertexColors`. */\n colorOverrides?: ColorOverrideRegistry;\n /** Skip what cannot reach the `width × height` view: nodes `paintBounds`\n * places outside it are not painted, and scene commands that cannot reach\n * it are dropped before the renderer — the scene slot's `cull`. Off by\n * default. `extraCommands` are never culled. */\n cull?: boolean;\n /** Where `drawOne`'s output for a node can reach, as on the scene slot.\n * Defaults to `defaultPaintBounds` when `drawOne` is `defaultDrawOne`. */\n paintBounds?: PaintBoundsFn<Node<TData, TLayer, TPose>, TPose>;\n /** Optional device-pixel ratio. Defaults to `window.devicePixelRatio || 1`\n * when available, otherwise 1. Tests typically pin this to 1 or 2. */\n dpr?: number;\n}\n\n/** Test seam: the renderer `renderSceneToCanvas` created for a canvas. Not\n * re-exported from the barrel. */\nexport function __getCachedRendererForTest(\n canvas: HTMLCanvasElement,\n): WeaselRenderer | undefined {\n return cachedCanvasRenderer(canvas);\n}\n\n/**\n * Present a `Scene` as the reading surface `buildSceneTree` walks. Built\n * inline rather than via `sceneToAdapter` so this module stays free of React\n * — `renderSceneToPixels` runs headless.\n */\nfunction sceneAsHierarchy<TData, TLayer extends string, TPose>(\n scene: Scene<TData, TLayer, TPose>,\n layers: SceneViewLayers = {},\n): HierarchicalAdapter<Node<TData, TLayer, TPose>, TPose> {\n return {\n getLayers: () =>\n paintedSceneLayers(scene.layers, layers)\n .map((l) => ({ id: l.id, visible: l.visible, parallax: l.parallax })),\n getNode: (id) => scene.get(id as NodeId),\n getChildren: (parentId) => {\n if (parentId === null) return scene.roots as readonly string[];\n const node = scene.get(parentId as NodeId);\n return node && node.kind === 'container' ? (node.children as readonly string[]) : [];\n },\n getPose: (id) => effectivePose(scene, scene.get(id as NodeId)!),\n } as HierarchicalAdapter<Node<TData, TLayer, TPose>, TPose>;\n}\n\n/**\n * Build the DrawCommand list that `renderSceneToCanvas` would dispatch.\n *\n * Pure — no GL, no DOM, no side effects. Walks the scene through\n * `buildSceneTree` — the same walk `<SceneCanvas>`'s `buildSceneLayer` uses —\n * appends any `extraCommands`, and wraps the result in a single\n * `{ kind: 'group', transform: viewToMat3(view) }` so the output is in screen\n * space. Sharing the walk is what keeps hidden layers unpainted and container\n * clips applied here; a hand-rolled `renderOrder()` loop silently dropped both.\n *\n * Each node's commands additionally go through `wrapNodeOutput` — pose\n * rotation, then the `alphaFor` multiplier — exactly as `buildSceneLayer`\n * does, so a `drawOne` shared between the two paints the same thing in both.\n * A `drawOne` that rotates its own output will therefore rotate twice here;\n * emit unrotated geometry and let the pose drive it.\n *\n * Output shape follows `buildSceneTree`: one group per **visible** layer, in\n * scene-layer order (or `layers.layerOrder`), each holding one group per node\n * on that layer.\n *\n * Exported for tests and for callers that want to render into something\n * other than a real `<canvas>` (e.g. an offscreen renderer or a\n * snapshot fixture).\n */\nexport function buildSceneViewCommands<TData, TLayer extends string, TPose>(\n scene: Scene<TData, TLayer, TPose>,\n view: View,\n drawOne: SceneViewDrawOne<TData, TLayer, TPose>,\n extraCommands?: ReadonlyArray<DrawCommand>,\n alphaFor?: (id: string) => number,\n /** How a child's stored pose folds into its parent's frame. Omit for the\n * absolute-pose model. */\n poseComposition?: PoseComposition<TPose>,\n /** This view's own layer visibility and order. */\n layers?: SceneViewLayers,\n /** Animated vertex colors to paint, typically an animator's `colorOverrides`. */\n colorOverrides?: ColorOverrideRegistry,\n /** Cull the scene to this CSS-pixel view size — see\n * `RenderSceneToCanvasArgs.cull`. Omit to paint every node. */\n cull?: SceneViewCull<TData, TLayer, TPose>,\n): DrawCommand[] {\n // The scene is in scope here, so this is where the override's alpha and the\n // derived paths are applied on the headless path; `SceneCanvas` does the same\n // for the live one. `Canvas` itself never sees a scene, so it can't and doesn't.\n const derivedOnly = withDerivedPaths(scene, drawOne);\n const derived = colorOverrides ? withColorOverrides(colorOverrides, derivedOnly) : derivedOnly;\n const depLookup = sceneDepLookup(scene);\n const wrappedDrawOne = (\n node: Node<TData, TLayer, TPose>,\n pose: TPose,\n v: View,\n ): DrawCommand[] => {\n const overrideAlpha = scene.overrides.read(node.id as NodeId)?.alpha ?? 1;\n return wrapNodeOutput(\n derived(node, pose, v),\n pose,\n (alphaFor ? alphaFor(node.id) : 1) * overrideAlpha,\n );\n };\n\n const composes = poseComposition !== undefined && poseComposition.closure !== 'identity';\n const hierarchy = composes\n ? { ...sceneAsHierarchy(scene, layers), composePose: poseComposition!.compose }\n : sceneAsHierarchy(scene, layers);\n\n const cullRect = cull ? { x: 0, y: 0, width: cull.width, height: cull.height } : undefined;\n const culled = cullRect && cull?.paintBounds ? paintMissesView(cull.paintBounds, view, cullRect) : undefined;\n const tree: DrawCommand[] = buildSceneTree(\n hierarchy as Parameters<typeof buildSceneTree>[0],\n wrappedDrawOne as unknown as Parameters<typeof buildSceneTree>[1],\n view,\n undefined,\n ((node: unknown) =>\n resolveDerivedPath(\n node as Node<TData, TLayer, TPose>, depLookup, (id) => scene.childrenOf(id),\n )) as Parameters<typeof buildSceneTree>[4],\n culled as Parameters<typeof buildSceneTree>[5],\n );\n const children = cullRect ? cullDrawCommands(tree, viewToMat3(view), cullRect) : tree;\n if (extraCommands && extraCommands.length > 0) {\n for (const cmd of extraCommands) children.push(cmd);\n }\n return [{\n kind: 'group',\n transform: viewToMat3(view),\n children,\n }];\n}\n\n/**\n * The commands `renderSceneToCanvas` paints for one frame — everything in\n * `args` but the canvas. `<SceneViewCanvas>` paints these through its own\n * renderer lease.\n */\nexport function sceneViewFrame<TData, TLayer extends string, TPose>(\n args: Omit<RenderSceneToCanvasArgs<TData, TLayer, TPose>, 'canvas'>,\n): DrawCommand[] {\n const {\n scene, view, width, height, drawOne, extraCommands, alphaFor, layerVisibility, layerOrder,\n colorOverrides, cull,\n } = args;\n return buildSceneViewCommands(\n scene, view, drawOne, extraCommands, alphaFor, undefined, { layerVisibility, layerOrder }, colorOverrides,\n cull ? { width, height, paintBounds: args.paintBounds ?? paintBoundsFor(drawOne) } : undefined,\n );\n}\n\n/**\n * Render one frame of `scene` at `view` into `canvas`. See module docstring\n * for the design context.\n *\n * Paints through `paintCanvas` (`./canvasRenderer`): one renderer per canvas,\n * created on the first call and resized when `width` / `height` / `dpr`\n * change. Where no WebGL2 context is available (jsdom) the call paints\n * nothing. The renderer lives until `releaseCanvasRenderer(canvas)`.\n */\nexport function renderSceneToCanvas<TData, TLayer extends string, TPose>(\n args: RenderSceneToCanvasArgs<TData, TLayer, TPose>,\n): void {\n const { canvas, view, width, height, dpr } = args;\n paintCanvas(canvas, sceneViewFrame(args), view, { width, height, dpr });\n}\n"],"mappings":";;;;;;;;;;;;;AAyHA,SAASA,EACP,GACA,IAA0B,CAAC,GAC6B;CACxD,OAAO;EACL,iBACEC,EAAmBC,EAAM,QAAQC,CAAM,CAAC,CACrC,KAAK,OAAO;GAAE,IAAIC,EAAE;GAAI,SAASA,EAAE;GAAS,UAAUA,EAAE;EAAS,EAAE;EACxE,UAAU,MAAOF,EAAM,IAAIG,CAAY;EACvC,cAAc,MAAa;GACzB,IAAIC,MAAa,MAAM,OAAOJ,EAAM;GACpC,IAAMK,IAAOL,EAAM,IAAII,CAAkB;GACzC,OAAOC,KAAQA,EAAK,SAAS,cAAeA,EAAK,WAAiC,CAAC;EACrF;EACA,UAAU,MAAOC,EAAcN,GAAOA,EAAM,IAAIG,CAAY,CAAE;CAChE;AACF;AA0BA,SAAgBI,EACd,GACA,GACA,GACA,GACA,GAGA,GAEA,GAEA,GAGA,GACe;CAIf,IAAMC,IAAcC,EAAiBT,GAAOU,CAAO,GAC7CC,IAAUC,IAAiBC,EAAmBD,GAAgBJ,CAAW,IAAIA,GAC7EM,IAAYC,EAAef,CAAK,GAChCgB,KACJ,GACA,GACA,MACkB;EAClB,IAAMC,IAAgBjB,EAAM,UAAU,KAAKK,EAAK,EAAY,CAAC,EAAE,SAAS;EACxE,OAAOa,EACLP,EAAQN,GAAMc,GAAMC,CAAC,GACrBD,IACCE,IAAWA,EAAShB,EAAK,EAAE,IAAI,KAAKY,CACvC;CACF,GAGMK,IADWC,MAAoB,KAAA,KAAaA,EAAgB,YAAY,aAE1E;EAAE,GAAGzB,EAAiBE,GAAOC,CAAM;EAAG,aAAasB,EAAiB;CAAQ,IAC5EzB,EAAiBE,GAAOC,CAAM,GAE5BuB,IAAWC,IAAO;EAAE,GAAG;EAAG,GAAG;EAAG,OAAOA,EAAK;EAAO,QAAQA,EAAK;CAAO,IAAI,KAAA,GAC3EC,IAASF,KAAYC,GAAM,cAAcE,EAAgBF,EAAK,aAAaG,GAAMJ,CAAQ,IAAI,KAAA,GAC7FK,IAAsBC,EAC1BR,GACAN,GACAY,GACA,KAAA,KACE,MACAG,EACE1B,GAAoCS,IAAY,MAAOd,EAAM,WAAWG,CAAE,CAC5E,IACFuB,CACF,GACMM,IAAWR,IAAWS,EAAiBJ,GAAMK,EAAWN,CAAI,GAAGJ,CAAQ,IAAIK;CACjF,IAAIM,KAAiBA,EAAc,SAAS,GAC1C,KAAK,IAAMC,KAAOD,GAAe,EAAS,KAAKC,CAAG;CAEpD,OAAO,CAAC;EACN,MAAM;EACN,WAAWF,EAAWN,CAAI;EAC1B;CACF,CAAC;AACH;AAOA,SAAgBS,EACd,GACe;CACf,IAAM,EACJ,UAAO,SAAM,UAAO,WAAQ,YAAS,kBAAe,aAAU,oBAAiB,eAC/E,mBAAgB,YACdC;CACJ,OAAO/B,EACLP,GAAO4B,GAAMlB,GAASyB,GAAed,GAAU,KAAA,GAAW;EAAE;EAAiB;CAAW,GAAGT,GAC3Fa,IAAO;EAAE;EAAO;EAAQ,aAAaa,EAAK,eAAeC,EAAe7B,CAAO;CAAE,IAAI,KAAA,CACvF;AACF;AAWA,SAAgB8B,EACd,GACM;CACN,IAAM,EAAE,WAAQ,SAAM,UAAO,WAAQ,WAAQF;CAC7C,EAAYG,GAAQJ,EAAeC,CAAI,GAAGV,GAAM;EAAE;EAAO;EAAQ;CAAI,CAAC;AACxE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"effectivePose.js","names":["POSE_SLOT","PATH_SLOT","resolving","cycleHits","derivedPose","derivedPoseIn","source","node","derive","ids","dependencyIdsOf","hitsBefore","depOf","depIn","id","reading","computed","record","nodeMemo","run","sameDeps","dropPoseKeyedMemoSlots","deps","recordDeps","into","derivedDepOf","SceneDep","posed","poseIn","resolveDerivedPath","depId","override","documentPose","effectivePose","definesFrame"],"sources":["../../../src/core/scene/effectivePose.ts"],"sourcesContent":["/**\n * `effectivePose` — the pose to draw, pick and measure a node at.\n *\n * Three answers, in order: the ephemeral override a gesture published, the\n * pose the node derives from its dependencies, or the pose the document\n * stores. Reading `node.pose` directly is how the render and hit-test paths\n * came to disagree about where a node is, and a derived pose widens that gap\n * from \"mid-drag\" to \"always\".\n */\nimport { recordDeps, sameDeps, type DepRecord } from './depMemo';\nimport { dependencyIdsOf } from './dependents';\nimport { resolveDerivedPath } from './derivedPath';\nimport { dropPoseKeyedMemoSlots, nodeMemo } from './nodeMemo';\nimport type { Path } from '@weasel-js/geom';\nimport type { DerivedDep, Node, NodeId, PoseOverrides } from './types';\n\n/**\n * Which pose a resolution answers: `effective` honors overrides all the way\n * down the dependency chain, `document` ignores them all the way down. Each\n * reading memoizes in its own slots — the memo key is the node and its\n * authored pose, which is the same for both.\n */\ntype Reading = 'effective' | 'document';\n\nconst POSE_SLOT: Record<Reading, string> = {\n effective: 'kit:derivedPose',\n document: 'kit:derivedPose:document',\n};\nconst PATH_SLOT: Record<Reading, string | undefined> = {\n effective: undefined,\n document: 'kit:derivedPath:document',\n};\n\n/** The subset of a node this module reads. Structural so a `Node` satisfies\n * it without importing the full generic shape. */\nexport interface PosedNode<TPose> {\n id: NodeId;\n kind?: 'leaf' | 'container';\n pose: TPose;\n /** Read only by the memo, which keys on its reference alongside the pose. */\n data?: unknown;\n dependsOn?: readonly NodeId[] | 'children';\n derivePose?: (\n node: never,\n deps: readonly (DerivedDep<TPose> | undefined)[],\n ) => TPose | null;\n /** Read only when something asks a dependency for its `path`. */\n derivePath?: (\n node: never,\n deps: readonly (DerivedDep<TPose> | undefined)[],\n ) => Path | null;\n}\n\n/** What resolving a pose needs: the overrides, and enough of the scene to\n * reach a node's dependencies. A `Scene` satisfies it. */\nexport interface PoseSource<TPose> {\n readonly overrides: Pick<PoseOverrides<TPose>, 'get'>;\n get(id: NodeId): PosedNode<TPose> | undefined;\n childrenOf(id: NodeId): readonly NodeId[];\n}\n\n/** A cached derived pose, with what it was derived from. */\ninterface PoseMemo<TPose> extends DepRecord<TPose> {\n value: TPose | null;\n}\n\n/** Ids whose derivation is on the stack. A dependency graph is meant to be\n * acyclic and `dependsOn` is unvalidated, so a cycle resolves to the authored\n * pose at whichever node closes it rather than overflowing the stack. */\nconst resolving = new Set<NodeId>();\n\n/** Bumped each time the guard above fires. A derivation that closed a cycle\n * answered from a fallback rather than from its dependencies, so its value is\n * an artifact of which node was asked first and must not be cached. */\nlet cycleHits = 0;\n\n/**\n * The pose `node` computes from its dependencies, or `null` when it derives\n * nothing — it has no `derivePose`, nothing to derive from, or its\n * `derivePose` returned `null`.\n *\n * A dependency is read through {@link effectivePose}, so a group of groups\n * resolves bottom-up and a dragged dependency's override reaches the\n * derivation the same frame it is published.\n */\nexport function derivedPose<TPose>(\n source: PoseSource<TPose>,\n node: PosedNode<TPose>,\n): TPose | null {\n return derivedPoseIn(source, node, 'effective');\n}\n\nfunction derivedPoseIn<TPose>(\n source: PoseSource<TPose>,\n node: PosedNode<TPose>,\n reading: Reading,\n): TPose | null {\n const derive = node.derivePose;\n if (derive === undefined) return null;\n const ids = dependencyIdsOf(node, source.childrenOf);\n if (ids.length === 0) return null;\n if (resolving.has(node.id)) {\n cycleHits++;\n return null;\n }\n resolving.add(node.id);\n const hitsBefore = cycleHits;\n try {\n const depOf = (id: NodeId) => depIn(source, id, reading);\n // Keyed on the authored pose, which is what `dropPoseKeyedMemoSlots`\n // clears. That key only sees a change someone pushed, so the record also\n // carries what the derivation read and re-checks it by value on a hit —\n // see `depMemo.ts`.\n let computed = false;\n const record = nodeMemo<PoseMemo<TPose>>(node, POSE_SLOT[reading], node.pose, () => {\n computed = true;\n return run(node, derive, ids.map(depOf), { nodes: [], poses: [], value: null });\n });\n // Mutating the record in place is what updates the memo slot, which holds\n // this object.\n if (!computed && !sameDeps(record, ids, depOf)) {\n run(node, derive, ids.map(depOf), record);\n }\n if (cycleHits !== hitsBefore) dropPoseKeyedMemoSlots(node);\n return record.value;\n } finally {\n resolving.delete(node.id);\n }\n}\n\n/** Run `derivePose` against `deps` and record what it was run against. */\nfunction run<TPose>(\n node: PosedNode<TPose>,\n derive: NonNullable<PosedNode<TPose>['derivePose']>,\n deps: readonly (DerivedDep<TPose> | undefined)[],\n into: PoseMemo<TPose>,\n): PoseMemo<TPose> {\n into.value = derive(node as never, deps);\n recordDeps(into, deps);\n return into;\n}\n\n/**\n * `id` as a derivation sees it: the node, the pose it is painted at, and the\n * path it derives.\n *\n * The pose is resolved eagerly — every derivation reads it — and the path\n * lazily, because most do not and resolving a route costs a router run. Both\n * resolve *through* this function, so a dependency that is itself derived is\n * resolved before it is read.\n */\nexport function derivedDepOf<TPose>(\n source: PoseSource<TPose>,\n id: NodeId,\n): DerivedDep<TPose> | undefined {\n return depIn(source, id, 'effective');\n}\n\nfunction depIn<TPose>(\n source: PoseSource<TPose>,\n id: NodeId,\n reading: Reading,\n): DerivedDep<TPose> | undefined {\n const node = source.get(id);\n if (node === undefined) return undefined;\n return new SceneDep(source, node, reading);\n}\n\n/**\n * A class rather than an object literal with a `get path()`: one of these is\n * built per dependency per derived node per frame, and an own accessor costs\n * an order of magnitude more to define than a prototype one does to inherit.\n */\nclass SceneDep<TPose> implements DerivedDep<TPose> {\n readonly node: Node<unknown, string, TPose>;\n readonly pose: TPose;\n\n constructor(\n private readonly source: PoseSource<TPose>,\n private readonly posed: PosedNode<TPose>,\n private readonly reading: Reading,\n ) {\n this.node = posed as unknown as Node<unknown, string, TPose>;\n this.pose = poseIn(source, posed, reading);\n }\n\n get path(): Path | null {\n const { source, reading } = this;\n return resolveDerivedPath(\n this.posed,\n (depId) => depIn(source, depId, reading),\n (depId) => source.childrenOf(depId),\n PATH_SLOT[reading],\n );\n }\n}\n\nfunction poseIn<TPose>(\n source: PoseSource<TPose>,\n node: PosedNode<TPose>,\n reading: Reading,\n): TPose {\n if (reading === 'effective') {\n const override = source.overrides.get(node.id)?.pose;\n if (override !== undefined) return override;\n }\n return derivedPoseIn(source, node, reading) ?? node.pose;\n}\n\n/**\n * The pose the *document* says `node` is at: derived when it derives, else\n * authored. `effectivePose` minus the override step, at every link of the\n * dependency chain — a derived node's document pose holds still while\n * something it derives from is being dragged.\n *\n * For a reader that must not see an in-flight gesture — an action capturing\n * the `from` of a transform op, a placement computed against a sibling, the\n * minimap's framing. A derived pose belongs here and an override does not:\n * derivation is what the document means, an override is what one gesture is\n * currently showing.\n */\nexport function documentPose<TPose>(\n source: PoseSource<TPose>,\n node: PosedNode<TPose>,\n): TPose {\n return poseIn(source, node, 'document');\n}\n\n/**\n * The pose to draw, pick and measure `node` at.\n *\n * The one rule. Every pose a reader acts on comes from here: the three render\n * walks, the pick walk, the adapters tools and actions commit through.\n */\nexport function effectivePose<TPose>(\n source: PoseSource<TPose>,\n node: PosedNode<TPose>,\n): TPose {\n return poseIn(source, node, 'effective');\n}\n\n/**\n * Whether `node`'s pose is a frame its children are expressed in, under a\n * composing strategy.\n *\n * A container that derives its pose from its children cannot be one: moving\n * the frame to cover the children would move the children, and a `RectPose`\n * frame's origin is its corner, so no envelope leaves them in place. Such a\n * container is an envelope instead — its children are stored in its own\n * frame — and the composition fold passes over it.\n */\nexport function definesFrame(node: { dependsOn?: unknown; derivePose?: unknown }): boolean {\n return !(node.dependsOn === 'children' && node.derivePose !== undefined);\n}\n"],"mappings":";;;;;AAwBA,IAAMA,IAAqC;CACzC,WAAW;CACX,UAAU;AACZ,GACMC,IAAiD;CACrD,WAAW,KAAA;CACX,UAAU;AACZ,GAsCMC,oBAAY,IAAI,IAAY,GAK9BC,IAAY;AAWhB,SAAgBC,EACd,GACA,GACc;CACd,OAAOC,EAAcC,GAAQC,GAAM,WAAW;AAChD;AAEA,SAASF,EACP,GACA,GACA,GACc;CACd,IAAMG,IAASD,EAAK;CACpB,IAAIC,MAAW,KAAA,GAAW,OAAO;CACjC,IAAMC,IAAMC,EAAgBH,GAAMD,EAAO,UAAU;CACnD,IAAIG,EAAI,WAAW,GAAG,OAAO;CAC7B,IAAIP,EAAU,IAAIK,EAAK,EAAE,GAEvB,OADA,KACO;CAET,EAAU,IAAIA,EAAK,EAAE;CACrB,IAAMI,IAAaR;CACnB,IAAI;EACF,IAAMS,KAAS,MAAeC,EAAMP,GAAQQ,GAAIC,CAAO,GAKnDC,IAAW,IACTC,IAASC,EAA0BX,GAAMP,EAAUe,IAAUR,EAAK,aACtE,IAAW,IACJY,EAAIZ,GAAMC,GAAQC,EAAI,IAAIG,CAAK,GAAG;GAAE,OAAO,CAAC;GAAG,OAAO,CAAC;GAAG,OAAO;EAAK,CAAC,EAC/E;EAOD,OAJI,CAACI,KAAY,CAACI,EAASH,GAAQR,GAAKG,CAAK,KAC3CO,EAAIZ,GAAMC,GAAQC,EAAI,IAAIG,CAAK,GAAGK,CAAM,GAEtCd,MAAcQ,KAAYU,EAAuBd,CAAI,GAClDU,EAAO;CAChB,UAAU;EACR,EAAU,OAAOV,EAAK,EAAE;CAC1B;AACF;AAGA,SAASY,EACP,GACA,GACA,GACA,GACiB;CAGjB,OAFA,EAAK,QAAQX,EAAOD,GAAee,CAAI,GACvCC,EAAWC,GAAMF,CAAI,GACdE;AACT;AAWA,SAAgBC,EACd,GACA,GAC+B;CAC/B,OAAOZ,EAAMP,GAAQQ,GAAI,WAAW;AACtC;AAEA,SAASD,EACP,GACA,GACA,GAC+B;CAC/B,IAAMN,IAAOD,EAAO,IAAIQ,CAAE;CAC1B,IAAIP,MAAS,KAAA,GACb,OAAO,IAAImB,EAASpB,GAAQC,GAAMQ,CAAO;AAC3C;AAOA,IAAMW,IAAN,MAAmD;CAK9B;CACA;CACA;CANnB;CACA;CAEA,YACE,GACA,GACA,GACA;EAEA,AALiB,KAAA,SAAApB,GACA,KAAA,QAAAqB,GACA,KAAA,UAAAZ,GAEjB,KAAK,OAAOY,GACZ,KAAK,OAAOC,EAAOtB,GAAQqB,GAAOZ,CAAO;CAC3C;CAEA,IAAI,OAAoB;EACtB,IAAM,EAAE,WAAQ,eAAY;EAC5B,OAAOc,EACL,KAAK,QACJ,MAAUhB,EAAMP,GAAQwB,GAAOf,CAAO,IACtC,MAAUT,EAAO,WAAWwB,CAAK,GAClC7B,EAAUc,EACZ;CACF;AACF;AAEA,SAASa,EACP,GACA,GACA,GACO;CACP,IAAIb,MAAY,aAAa;EAC3B,IAAMgB,IAAWzB,EAAO,UAAU,IAAIC,EAAK,EAAE,CAAC,EAAE;EAChD,IAAIwB,MAAa,KAAA,GAAW,OAAOA;CACrC;CACA,OAAO1B,EAAcC,GAAQC,GAAMQ,CAAO,KAAKR,EAAK;AACtD;AAcA,SAAgByB,EACd,GACA,GACO;CACP,OAAOJ,EAAOtB,GAAQC,GAAM,UAAU;AACxC;AAQA,SAAgB0B,EACd,GACA,GACO;CACP,OAAOL,EAAOtB,GAAQC,GAAM,WAAW;AACzC;AAYA,SAAgB2B,EAAa,GAA8D;CACzF,OAAS3B,EAAK,cAAc,cAAcA,EAAK,eAAe,KAAA;AAChE"}
|
|
1
|
+
{"version":3,"file":"effectivePose.js","names":["POSE_SLOT","PATH_SLOT","resolving","cycleHits","derivedPose","derivedPoseIn","source","node","derive","ids","dependencyIdsOf","hitsBefore","depOf","depIn","id","reading","computed","record","nodeMemo","run","sameDeps","dropPoseKeyedMemoSlots","deps","recordDeps","into","derivedDepOf","SceneDep","posed","poseIn","resolveDerivedPath","depId","override","documentPose","effectivePose","definesFrame"],"sources":["../../../src/core/scene/effectivePose.ts"],"sourcesContent":["/**\n * `effectivePose` — the pose to draw, pick and measure a node at.\n *\n * Three answers, in order: the ephemeral override a gesture published, the\n * pose the node derives from its dependencies, or the pose the document\n * stores. Reading `node.pose` directly is how the render and hit-test paths\n * came to disagree about where a node is, and a derived pose widens that gap\n * from \"mid-drag\" to \"always\".\n */\nimport { recordDeps, sameDeps, type DepRecord } from './depMemo';\nimport { dependencyIdsOf } from './dependents';\nimport { resolveDerivedPath } from './derivedPath';\nimport { dropPoseKeyedMemoSlots, nodeMemo } from './nodeMemo';\nimport type { Path } from '@weasel-js/geom';\nimport type { DerivedDep, Node, NodeId, PoseOverrides } from './types';\n\n/**\n * Which pose a resolution answers: `effective` honors overrides all the way\n * down the dependency chain, `document` ignores them all the way down. Each\n * reading memoizes in its own slots — the memo key is the node and its\n * authored pose, which is the same for both.\n */\ntype Reading = 'effective' | 'document';\n\nconst POSE_SLOT: Record<Reading, string> = {\n effective: 'kit:derivedPose',\n document: 'kit:derivedPose:document',\n};\nconst PATH_SLOT: Record<Reading, string | undefined> = {\n effective: undefined,\n document: 'kit:derivedPath:document',\n};\n\n/** The subset of a node this module reads. Structural so a `Node` satisfies\n * it without importing the full generic shape. */\nexport interface PosedNode<TPose> {\n id: NodeId;\n kind?: 'leaf' | 'container';\n pose: TPose;\n /** Read only by the memo, which keys on its reference alongside the pose. */\n data?: unknown;\n dependsOn?: readonly NodeId[] | 'children';\n derivePose?: (\n node: never,\n deps: readonly (DerivedDep<TPose> | undefined)[],\n ) => TPose | null;\n /** Read only when something asks a dependency for its `path`. */\n derivePath?: (\n node: never,\n deps: readonly (DerivedDep<TPose> | undefined)[],\n ) => Path | null;\n}\n\n/** What resolving a pose needs: the overrides, and enough of the scene to\n * reach a node's dependencies. A `Scene` satisfies it. */\nexport interface PoseSource<TPose> {\n readonly overrides: Pick<PoseOverrides<TPose>, 'read'>;\n get(id: NodeId): PosedNode<TPose> | undefined;\n childrenOf(id: NodeId): readonly NodeId[];\n}\n\n/** A cached derived pose, with what it was derived from. */\ninterface PoseMemo<TPose> extends DepRecord<TPose> {\n value: TPose | null;\n}\n\n/** Ids whose derivation is on the stack. A dependency graph is meant to be\n * acyclic and `dependsOn` is unvalidated, so a cycle resolves to the authored\n * pose at whichever node closes it rather than overflowing the stack. */\nconst resolving = new Set<NodeId>();\n\n/** Bumped each time the guard above fires. A derivation that closed a cycle\n * answered from a fallback rather than from its dependencies, so its value is\n * an artifact of which node was asked first and must not be cached. */\nlet cycleHits = 0;\n\n/**\n * The pose `node` computes from its dependencies, or `null` when it derives\n * nothing — it has no `derivePose`, nothing to derive from, or its\n * `derivePose` returned `null`.\n *\n * A dependency is read through {@link effectivePose}, so a group of groups\n * resolves bottom-up and a dragged dependency's override reaches the\n * derivation the same frame it is published.\n */\nexport function derivedPose<TPose>(\n source: PoseSource<TPose>,\n node: PosedNode<TPose>,\n): TPose | null {\n return derivedPoseIn(source, node, 'effective');\n}\n\nfunction derivedPoseIn<TPose>(\n source: PoseSource<TPose>,\n node: PosedNode<TPose>,\n reading: Reading,\n): TPose | null {\n const derive = node.derivePose;\n if (derive === undefined) return null;\n const ids = dependencyIdsOf(node, source.childrenOf);\n if (ids.length === 0) return null;\n if (resolving.has(node.id)) {\n cycleHits++;\n return null;\n }\n resolving.add(node.id);\n const hitsBefore = cycleHits;\n try {\n const depOf = (id: NodeId) => depIn(source, id, reading);\n // Keyed on the authored pose, which is what `dropPoseKeyedMemoSlots`\n // clears. That key only sees a change someone pushed, so the record also\n // carries what the derivation read and re-checks it by value on a hit —\n // see `depMemo.ts`.\n let computed = false;\n const record = nodeMemo<PoseMemo<TPose>>(node, POSE_SLOT[reading], node.pose, () => {\n computed = true;\n return run(node, derive, ids.map(depOf), { nodes: [], poses: [], value: null });\n });\n // Mutating the record in place is what updates the memo slot, which holds\n // this object.\n if (!computed && !sameDeps(record, ids, depOf)) {\n run(node, derive, ids.map(depOf), record);\n }\n if (cycleHits !== hitsBefore) dropPoseKeyedMemoSlots(node);\n return record.value;\n } finally {\n resolving.delete(node.id);\n }\n}\n\n/** Run `derivePose` against `deps` and record what it was run against. */\nfunction run<TPose>(\n node: PosedNode<TPose>,\n derive: NonNullable<PosedNode<TPose>['derivePose']>,\n deps: readonly (DerivedDep<TPose> | undefined)[],\n into: PoseMemo<TPose>,\n): PoseMemo<TPose> {\n into.value = derive(node as never, deps);\n recordDeps(into, deps);\n return into;\n}\n\n/**\n * `id` as a derivation sees it: the node, the pose it is painted at, and the\n * path it derives.\n *\n * The pose is resolved eagerly — every derivation reads it — and the path\n * lazily, because most do not and resolving a route costs a router run. Both\n * resolve *through* this function, so a dependency that is itself derived is\n * resolved before it is read.\n */\nexport function derivedDepOf<TPose>(\n source: PoseSource<TPose>,\n id: NodeId,\n): DerivedDep<TPose> | undefined {\n return depIn(source, id, 'effective');\n}\n\nfunction depIn<TPose>(\n source: PoseSource<TPose>,\n id: NodeId,\n reading: Reading,\n): DerivedDep<TPose> | undefined {\n const node = source.get(id);\n if (node === undefined) return undefined;\n return new SceneDep(source, node, reading);\n}\n\n/**\n * A class rather than an object literal with a `get path()`: one of these is\n * built per dependency per derived node per frame, and an own accessor costs\n * an order of magnitude more to define than a prototype one does to inherit.\n */\nclass SceneDep<TPose> implements DerivedDep<TPose> {\n readonly node: Node<unknown, string, TPose>;\n readonly pose: TPose;\n\n constructor(\n private readonly source: PoseSource<TPose>,\n private readonly posed: PosedNode<TPose>,\n private readonly reading: Reading,\n ) {\n this.node = posed as unknown as Node<unknown, string, TPose>;\n this.pose = poseIn(source, posed, reading);\n }\n\n get path(): Path | null {\n const { source, reading } = this;\n return resolveDerivedPath(\n this.posed,\n (depId) => depIn(source, depId, reading),\n (depId) => source.childrenOf(depId),\n PATH_SLOT[reading],\n );\n }\n}\n\nfunction poseIn<TPose>(\n source: PoseSource<TPose>,\n node: PosedNode<TPose>,\n reading: Reading,\n): TPose {\n if (reading === 'effective') {\n const override = source.overrides.read(node.id)?.pose;\n if (override !== undefined) return override;\n }\n return derivedPoseIn(source, node, reading) ?? node.pose;\n}\n\n/**\n * The pose the *document* says `node` is at: derived when it derives, else\n * authored. `effectivePose` minus the override step, at every link of the\n * dependency chain — a derived node's document pose holds still while\n * something it derives from is being dragged.\n *\n * For a reader that must not see an in-flight gesture — an action capturing\n * the `from` of a transform op, a placement computed against a sibling, the\n * minimap's framing. A derived pose belongs here and an override does not:\n * derivation is what the document means, an override is what one gesture is\n * currently showing.\n */\nexport function documentPose<TPose>(\n source: PoseSource<TPose>,\n node: PosedNode<TPose>,\n): TPose {\n return poseIn(source, node, 'document');\n}\n\n/**\n * The pose to draw, pick and measure `node` at.\n *\n * The one rule. Every pose a reader acts on comes from here: the three render\n * walks, the pick walk, the adapters tools and actions commit through.\n */\nexport function effectivePose<TPose>(\n source: PoseSource<TPose>,\n node: PosedNode<TPose>,\n): TPose {\n return poseIn(source, node, 'effective');\n}\n\n/**\n * Whether `node`'s pose is a frame its children are expressed in, under a\n * composing strategy.\n *\n * A container that derives its pose from its children cannot be one: moving\n * the frame to cover the children would move the children, and a `RectPose`\n * frame's origin is its corner, so no envelope leaves them in place. Such a\n * container is an envelope instead — its children are stored in its own\n * frame — and the composition fold passes over it.\n */\nexport function definesFrame(node: { dependsOn?: unknown; derivePose?: unknown }): boolean {\n return !(node.dependsOn === 'children' && node.derivePose !== undefined);\n}\n"],"mappings":";;;;;AAwBA,IAAMA,IAAqC;CACzC,WAAW;CACX,UAAU;AACZ,GACMC,IAAiD;CACrD,WAAW,KAAA;CACX,UAAU;AACZ,GAsCMC,oBAAY,IAAI,IAAY,GAK9BC,IAAY;AAWhB,SAAgBC,EACd,GACA,GACc;CACd,OAAOC,EAAcC,GAAQC,GAAM,WAAW;AAChD;AAEA,SAASF,EACP,GACA,GACA,GACc;CACd,IAAMG,IAASD,EAAK;CACpB,IAAIC,MAAW,KAAA,GAAW,OAAO;CACjC,IAAMC,IAAMC,EAAgBH,GAAMD,EAAO,UAAU;CACnD,IAAIG,EAAI,WAAW,GAAG,OAAO;CAC7B,IAAIP,EAAU,IAAIK,EAAK,EAAE,GAEvB,OADA,KACO;CAET,EAAU,IAAIA,EAAK,EAAE;CACrB,IAAMI,IAAaR;CACnB,IAAI;EACF,IAAMS,KAAS,MAAeC,EAAMP,GAAQQ,GAAIC,CAAO,GAKnDC,IAAW,IACTC,IAASC,EAA0BX,GAAMP,EAAUe,IAAUR,EAAK,aACtE,IAAW,IACJY,EAAIZ,GAAMC,GAAQC,EAAI,IAAIG,CAAK,GAAG;GAAE,OAAO,CAAC;GAAG,OAAO,CAAC;GAAG,OAAO;EAAK,CAAC,EAC/E;EAOD,OAJI,CAACI,KAAY,CAACI,EAASH,GAAQR,GAAKG,CAAK,KAC3CO,EAAIZ,GAAMC,GAAQC,EAAI,IAAIG,CAAK,GAAGK,CAAM,GAEtCd,MAAcQ,KAAYU,EAAuBd,CAAI,GAClDU,EAAO;CAChB,UAAU;EACR,EAAU,OAAOV,EAAK,EAAE;CAC1B;AACF;AAGA,SAASY,EACP,GACA,GACA,GACA,GACiB;CAGjB,OAFA,EAAK,QAAQX,EAAOD,GAAee,CAAI,GACvCC,EAAWC,GAAMF,CAAI,GACdE;AACT;AAWA,SAAgBC,EACd,GACA,GAC+B;CAC/B,OAAOZ,EAAMP,GAAQQ,GAAI,WAAW;AACtC;AAEA,SAASD,EACP,GACA,GACA,GAC+B;CAC/B,IAAMN,IAAOD,EAAO,IAAIQ,CAAE;CAC1B,IAAIP,MAAS,KAAA,GACb,OAAO,IAAImB,EAASpB,GAAQC,GAAMQ,CAAO;AAC3C;AAOA,IAAMW,IAAN,MAAmD;CAK9B;CACA;CACA;CANnB;CACA;CAEA,YACE,GACA,GACA,GACA;EAEA,AALiB,KAAA,SAAApB,GACA,KAAA,QAAAqB,GACA,KAAA,UAAAZ,GAEjB,KAAK,OAAOY,GACZ,KAAK,OAAOC,EAAOtB,GAAQqB,GAAOZ,CAAO;CAC3C;CAEA,IAAI,OAAoB;EACtB,IAAM,EAAE,WAAQ,eAAY;EAC5B,OAAOc,EACL,KAAK,QACJ,MAAUhB,EAAMP,GAAQwB,GAAOf,CAAO,IACtC,MAAUT,EAAO,WAAWwB,CAAK,GAClC7B,EAAUc,EACZ;CACF;AACF;AAEA,SAASa,EACP,GACA,GACA,GACO;CACP,IAAIb,MAAY,aAAa;EAC3B,IAAMgB,IAAWzB,EAAO,UAAU,KAAKC,EAAK,EAAE,CAAC,EAAE;EACjD,IAAIwB,MAAa,KAAA,GAAW,OAAOA;CACrC;CACA,OAAO1B,EAAcC,GAAQC,GAAMQ,CAAO,KAAKR,EAAK;AACtD;AAcA,SAAgByB,EACd,GACA,GACO;CACP,OAAOJ,EAAOtB,GAAQC,GAAM,UAAU;AACxC;AAQA,SAAgB0B,EACd,GACA,GACO;CACP,OAAOL,EAAOtB,GAAQC,GAAM,WAAW;AACzC;AAYA,SAAgB2B,EAAa,GAA8D;CACzF,OAAS3B,EAAK,cAAc,cAAcA,EAAK,eAAe,KAAA;AAChE"}
|
|
@@ -1,52 +1,72 @@
|
|
|
1
1
|
import { dropPoseKeyedMemoSlots as e } from "./nodeMemo.js";
|
|
2
|
+
import { kit as t, last as n, mix as r, mul as i, patch as a } from "@msb235/blits";
|
|
2
3
|
//#region src/core/scene/poseOverrides.ts
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
var o = Object.freeze({});
|
|
5
|
+
function s(s, c) {
|
|
6
|
+
let l = /* @__PURE__ */ new Map(), u = /* @__PURE__ */ new Set(), d = 0, f = r(t({
|
|
7
|
+
pose: n(),
|
|
8
|
+
alpha: i()
|
|
9
|
+
}));
|
|
10
|
+
f.cue({ patch: a(0, (e, t) => l.get(t) ?? o, { writes: ["pose", "alpha"] }) });
|
|
11
|
+
let p = 0;
|
|
12
|
+
f.sync(p);
|
|
13
|
+
let m = /* @__PURE__ */ new Map();
|
|
14
|
+
function h(t) {
|
|
15
|
+
let n = s(t);
|
|
16
|
+
n && e(n), c?.(t);
|
|
8
17
|
}
|
|
9
|
-
function
|
|
10
|
-
|
|
11
|
-
for (let e of
|
|
18
|
+
function g() {
|
|
19
|
+
d++, f.sync(++p);
|
|
20
|
+
for (let e of u) e();
|
|
21
|
+
}
|
|
22
|
+
function _(e) {
|
|
23
|
+
m.delete(e), f.drop(e);
|
|
12
24
|
}
|
|
13
25
|
return {
|
|
14
26
|
set(e, t) {
|
|
15
|
-
|
|
27
|
+
l.set(e, t), h(e), g();
|
|
16
28
|
},
|
|
17
29
|
get(e) {
|
|
18
|
-
return
|
|
30
|
+
return l.get(e);
|
|
31
|
+
},
|
|
32
|
+
read(e) {
|
|
33
|
+
if (!l.has(e)) return;
|
|
34
|
+
let t = m.get(e);
|
|
35
|
+
return t === void 0 && (t = {
|
|
36
|
+
out: {},
|
|
37
|
+
at: -1
|
|
38
|
+
}, m.set(e, t)), t.at !== d && (f.probe(e, t.out), t.at = d), t.out;
|
|
19
39
|
},
|
|
20
40
|
has(e) {
|
|
21
|
-
return
|
|
41
|
+
return l.has(e);
|
|
22
42
|
},
|
|
23
43
|
ids() {
|
|
24
|
-
return [...
|
|
44
|
+
return [...l.keys()];
|
|
25
45
|
},
|
|
26
46
|
clear(e) {
|
|
27
|
-
|
|
47
|
+
l.delete(e) && (_(e), h(e), g());
|
|
28
48
|
},
|
|
29
49
|
clearAll() {
|
|
30
|
-
if (
|
|
31
|
-
for (let e of
|
|
32
|
-
|
|
50
|
+
if (l.size !== 0) {
|
|
51
|
+
for (let e of l.keys()) _(e), h(e);
|
|
52
|
+
l.clear(), g();
|
|
33
53
|
}
|
|
34
54
|
},
|
|
35
55
|
commit() {
|
|
36
|
-
for (let e of
|
|
37
|
-
|
|
56
|
+
for (let e of l.keys()) h(e);
|
|
57
|
+
g();
|
|
38
58
|
},
|
|
39
59
|
subscribe(e) {
|
|
40
|
-
return
|
|
41
|
-
|
|
60
|
+
return u.add(e), () => {
|
|
61
|
+
u.delete(e);
|
|
42
62
|
};
|
|
43
63
|
},
|
|
44
64
|
getGeneration() {
|
|
45
|
-
return
|
|
65
|
+
return d;
|
|
46
66
|
}
|
|
47
67
|
};
|
|
48
68
|
}
|
|
49
69
|
//#endregion
|
|
50
|
-
export {
|
|
70
|
+
export { s as createPoseOverrides };
|
|
51
71
|
|
|
52
72
|
//# sourceMappingURL=poseOverrides.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"poseOverrides.js","names":["createPoseOverrides","entries","listeners","generation","
|
|
1
|
+
{"version":3,"file":"poseOverrides.js","names":["NOTHING","createPoseOverrides","entries","listeners","generation","folded","mix","kit","last","mul","patch","id","clock","reads","invalidate","node","getNode","dropPoseKeyedMemoSlots","onInvalidate","published","listener","forget","entry","held","fn"],"sources":["../../../src/core/scene/poseOverrides.ts"],"sourcesContent":["import { kit, last, mix, mul, patch } from '@msb235/blits';\nimport { dropPoseKeyedMemoSlots } from './nodeMemo';\nimport type { NodeId, PoseOverride, PoseOverrides } from './types';\n\n/** What the table's voice says about a node it holds no entry for. */\nconst NOTHING: PoseOverride<never> = Object.freeze({});\n\n/**\n * Build a {@link PoseOverrides} map.\n *\n * `getNode` resolves an id to the node object the painter memo is keyed on —\n * the only thing this module needs from the scene, and the reason it doesn't\n * import one. `onInvalidate` lets the scene invalidate derived nodes from the\n * same chokepoint.\n *\n * What `read` answers is a blits mix folded per node. The entries a writer\n * `set`s are one voice on it, and the nodes holding an entry are the mix's\n * reach: a node outside it is never probed.\n */\nexport function createPoseOverrides<TPose>(\n getNode: (id: NodeId) => { data?: unknown } | undefined,\n onInvalidate?: (id: NodeId) => void,\n): PoseOverrides<TPose> {\n const entries = new Map<NodeId, PoseOverride<TPose>>();\n const listeners = new Set<() => void>();\n let generation = 0;\n\n const folded = mix<NodeId, PoseOverride<TPose>>(\n kit<PoseOverride<TPose>>({ pose: last<NonNullable<TPose>>(), alpha: mul() }),\n );\n folded.cue({\n patch: patch<NodeId, PoseOverride<TPose>>(\n 0,\n (_phase, id) => entries.get(id) ?? (NOTHING as PoseOverride<TPose>),\n { writes: ['pose', 'alpha'] },\n ),\n });\n // Mix time only has to move between publishes, so a probe after one never\n // hands back the delta a probe before it cached.\n let clock = 0;\n folded.sync(clock);\n\n /** Per node: the object `read` folds into and returns, and the generation\n * it was last folded at. */\n const reads = new Map<NodeId, { out: PoseOverride<TPose>; at: number }>();\n\n function invalidate(id: NodeId): void {\n const node = getNode(id);\n if (node) dropPoseKeyedMemoSlots(node);\n onInvalidate?.(id);\n }\n\n function published(): void {\n generation++;\n folded.sync(++clock);\n for (const listener of listeners) listener();\n }\n\n function forget(id: NodeId): void {\n reads.delete(id);\n folded.drop(id);\n }\n\n return {\n set(id, entry) {\n entries.set(id, entry);\n invalidate(id);\n published();\n },\n get(id) {\n return entries.get(id);\n },\n read(id) {\n if (!entries.has(id)) return undefined;\n let held = reads.get(id);\n if (held === undefined) {\n held = { out: {}, at: -1 };\n reads.set(id, held);\n }\n if (held.at !== generation) {\n folded.probe(id, held.out);\n held.at = generation;\n }\n return held.out;\n },\n has(id) {\n return entries.has(id);\n },\n ids() {\n return [...entries.keys()];\n },\n clear(id) {\n if (!entries.delete(id)) return;\n forget(id);\n invalidate(id);\n published();\n },\n clearAll() {\n if (entries.size === 0) return;\n for (const id of entries.keys()) {\n forget(id);\n invalidate(id);\n }\n entries.clear();\n published();\n },\n commit() {\n for (const id of entries.keys()) invalidate(id);\n published();\n },\n subscribe(fn) {\n listeners.add(fn);\n return () => { listeners.delete(fn); };\n },\n getGeneration() {\n return generation;\n },\n };\n}\n"],"mappings":";;;AAKA,IAAMA,IAA+B,OAAO,OAAO,CAAC,CAAC;AAcrD,SAAgBC,EACd,GACA,GACsB;CACtB,IAAMC,oBAAU,IAAI,IAAiC,GAC/CC,oBAAY,IAAI,IAAgB,GAClCC,IAAa,GAEXC,IAASC,EACbC,EAAyB;EAAE,MAAMC,EAAyB;EAAG,OAAOC,EAAI;CAAE,CAAC,CAC7E;CACA,EAAO,IAAI,EACT,OAAOC,EACL,IACC,GAAQ,MAAOR,EAAQ,IAAIS,CAAE,KAAMX,GACpC,EAAE,QAAQ,CAAC,QAAQ,OAAO,EAAE,CAC9B,EACF,CAAC;CAGD,IAAIY,IAAQ;CACZ,EAAO,KAAKA,CAAK;CAIjB,IAAMC,oBAAQ,IAAI,IAAsD;CAExE,SAASC,EAAW,GAAkB;EACpC,IAAMC,IAAOC,EAAQL,CAAE;EAEvB,AADII,KAAME,EAAuBF,CAAI,GACrCG,IAAeP,CAAE;CACnB;CAEA,SAASQ,IAAkB;EAEzB,AADA,KACAd,EAAO,KAAK,EAAEO,CAAK;EACnB,KAAK,IAAMQ,KAAYjB,GAAW,EAAS;CAC7C;CAEA,SAASkB,EAAO,GAAkB;EAEhC,AADAR,EAAM,OAAOF,CAAE,GACfN,EAAO,KAAKM,CAAE;CAChB;CAEA,OAAO;EACL,IAAI,GAAI,GAAO;GAGb,AAFAT,EAAQ,IAAIS,GAAIW,CAAK,GACrBR,EAAWH,CAAE,GACbQ,EAAU;EACZ;EACA,IAAI,GAAI;GACN,OAAOjB,EAAQ,IAAIS,CAAE;EACvB;EACA,KAAK,GAAI;GACP,IAAI,CAACT,EAAQ,IAAIS,CAAE,GAAG;GACtB,IAAIY,IAAOV,EAAM,IAAIF,CAAE;GASvB,OARIY,MAAS,KAAA,MACX,IAAO;IAAE,KAAK,CAAC;IAAG,IAAI;GAAG,GACzBV,EAAM,IAAIF,GAAIY,CAAI,IAEhBA,EAAK,OAAOnB,MACdC,EAAO,MAAMM,GAAIY,EAAK,GAAG,GACzB,EAAK,KAAKnB,IAELmB,EAAK;EACd;EACA,IAAI,GAAI;GACN,OAAOrB,EAAQ,IAAIS,CAAE;EACvB;EACA,MAAM;GACJ,OAAO,CAAC,GAAGT,EAAQ,KAAK,CAAC;EAC3B;EACA,MAAM,GAAI;GACR,AAAKA,EAAQ,OAAOS,CAAE,MACtBU,EAAOV,CAAE,GACTG,EAAWH,CAAE,GACbQ,EAAU;EACZ;EACA,WAAW;GACT,IAAIjB,EAAQ,SAAS,GACrB;SAAK,IAAMS,KAAMT,EAAQ,KAAK,GAE5B,AADAmB,EAAOV,CAAE,GACTG,EAAWH,CAAE;IAGf,AADAT,EAAQ,MAAM,GACdiB,EAAU;GAFV;EAGF;EACA,SAAS;GACP,KAAK,IAAMR,KAAMT,EAAQ,KAAK,GAAG,EAAWS,CAAE;GAC9C,EAAU;EACZ;EACA,UAAU,GAAI;GAEZ,OADAR,EAAU,IAAIqB,CAAE,SACH;IAAE,EAAU,OAAOA,CAAE;GAAG;EACvC;EACA,gBAAgB;GACd,OAAOpB;EACT;CACF;AACF"}
|