@weasel-js/core 1.14.2 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +167 -0
- package/dist/affordances/pathAnchors.js +0 -1
- package/dist/affordances/pathAnchors.js.map +1 -1
- package/dist/barrel/paths.js +1 -1
- package/dist/barrel/renderer.js +2 -2
- package/dist/barrel/routing.js +6 -4
- package/dist/barrel/text.js +5 -3
- package/dist/canvas/Canvas.js +5 -1
- package/dist/canvas/Canvas.js.map +1 -1
- package/dist/canvas/DrawCanvas.js +5 -1
- package/dist/canvas/DrawCanvas.js.map +1 -1
- package/dist/canvas/SceneCanvas/SceneCanvasInput.js +4 -0
- package/dist/canvas/SceneCanvas/SceneCanvasInput.js.map +1 -1
- package/dist/canvas/SceneCanvas/builtinToolSets.js +6 -3
- package/dist/canvas/SceneCanvas/builtinToolSets.js.map +1 -1
- package/dist/canvas/SceneCanvas/defaultNodeProperties.js +1 -1
- package/dist/canvas/SceneCanvas/presets/paths.js +6 -6
- package/dist/canvas/SceneCanvas/presets/paths.js.map +1 -1
- package/dist/canvas/SceneCanvas/shapeGeneratorProperties.js +1 -1
- package/dist/canvas/SceneCanvas/shapeGeneratorProperties.js.map +1 -1
- package/dist/canvas/SceneCanvas/useBuiltinTools.js +2 -0
- package/dist/canvas/SceneCanvas/useBuiltinTools.js.map +1 -1
- package/dist/canvas/SceneCanvas/useContributionRoles.js +2 -0
- package/dist/canvas/SceneCanvas/useContributionRoles.js.map +1 -1
- package/dist/canvas/SceneCanvas/useSceneChrome.js +17 -5
- package/dist/canvas/SceneCanvas/useSceneChrome.js.map +1 -1
- package/dist/canvas/SceneCanvas/useSceneStandardActions.js +3 -0
- package/dist/canvas/SceneCanvas/useSceneStandardActions.js.map +1 -1
- package/dist/canvas/SceneCanvas.js +5 -4
- package/dist/canvas/SceneCanvas.js.map +1 -1
- package/dist/canvas/SceneViewCanvas.js +5 -1
- package/dist/canvas/SceneViewCanvas.js.map +1 -1
- package/dist/canvas/affordanceAt.js +0 -1
- package/dist/canvas/affordanceAt.js.map +1 -1
- package/dist/canvas/deps/chromeRules.js +37 -0
- package/dist/canvas/deps/chromeRules.js.map +1 -0
- package/dist/canvas/deps/editAnchors.js +0 -6
- package/dist/canvas/deps/editAnchors.js.map +1 -1
- package/dist/canvas/deps/hitTestArea.js +3 -1
- package/dist/canvas/deps/hitTestArea.js.map +1 -1
- package/dist/canvas/deps/modeDefaultTool.js +23 -0
- package/dist/canvas/deps/modeDefaultTool.js.map +1 -0
- package/dist/canvas/deps/pathEditCommits.js +1 -1
- package/dist/canvas/deps/selectionSnapshot.js +23 -0
- package/dist/canvas/deps/selectionSnapshot.js.map +1 -0
- package/dist/canvas/deps/snap.js +1 -0
- package/dist/canvas/deps/snap.js.map +1 -1
- package/dist/canvas/pickReach.js +12 -8
- package/dist/canvas/pickReach.js.map +1 -1
- package/dist/canvas/pickWalk.js +7 -5
- package/dist/canvas/pickWalk.js.map +1 -1
- package/dist/canvas/scenePickSource.js +2 -2
- package/dist/canvas/scenePickSource.js.map +1 -1
- package/dist/canvas/useCanvasSurface.js +21 -3
- package/dist/canvas/useCanvasSurface.js.map +1 -1
- package/dist/canvas/useFrameLoop.js +6 -7
- package/dist/canvas/useFrameLoop.js.map +1 -1
- package/dist/canvas/useSurfaceSize.js +4 -3
- package/dist/canvas/useSurfaceSize.js.map +1 -1
- package/dist/core/geometry/regionHit.js +35 -16
- package/dist/core/geometry/regionHit.js.map +1 -1
- package/dist/core/scene/editMode.js +4 -1
- package/dist/core/scene/editMode.js.map +1 -1
- package/dist/core/scene/historyFacade.js +7 -50
- package/dist/core/scene/historyFacade.js.map +1 -1
- package/dist/core/scene/nodeGates.js +2 -2
- package/dist/core/scene/nodeGates.js.map +1 -1
- package/dist/core/scene/opTable.js +8 -8
- package/dist/core/scene/opTable.js.map +1 -1
- package/dist/core/scene/outerOps.js +2 -2
- package/dist/core/scene/outerOps.js.map +1 -1
- package/dist/core/scene/poseWrites.js +16 -15
- package/dist/core/scene/poseWrites.js.map +1 -1
- package/dist/core/scene/sceneJournals.js +78 -0
- package/dist/core/scene/sceneJournals.js.map +1 -0
- package/dist/core/scene/sceneRecording.js +3 -2
- package/dist/core/scene/sceneRecording.js.map +1 -1
- package/dist/core/viewport/useCanvasSize.js +47 -26
- package/dist/core/viewport/useCanvasSize.js.map +1 -1
- package/dist/{depSchema-DD9H89gX.d.ts → depSchema-ChQPzEkW.d.ts} +40 -13
- package/dist/domains/chrome-caps/resolve.js +1 -0
- package/dist/domains/chrome-caps/resolve.js.map +1 -1
- package/dist/domains/chrome-caps/useChromeCaps.js +4 -2
- package/dist/domains/chrome-caps/useChromeCaps.js.map +1 -1
- package/dist/domains/paths/pathEditState.js +0 -24
- package/dist/domains/paths/pathEditState.js.map +1 -1
- package/dist/domains/snap/probes2d.js +5 -0
- package/dist/domains/snap/probes2d.js.map +1 -1
- package/dist/domains/snap/sceneReads.js +11 -1
- package/dist/domains/snap/sceneReads.js.map +1 -1
- package/dist/domains/snap/sceneSession2d.js +4 -2
- package/dist/domains/snap/sceneSession2d.js.map +1 -1
- package/dist/domains/text/index.js +2 -2
- package/dist/icons.table.js +1 -1
- package/dist/icons.table.js.map +1 -1
- package/dist/icons.table.json +5 -0
- package/dist/index.d.ts +112 -60
- package/dist/index.js +12 -8
- package/dist/interactions/actions/booleans/action.js +4 -2
- package/dist/interactions/actions/booleans/action.js.map +1 -1
- package/dist/interactions/actions/defaults/anchorEditing.js +3 -6
- package/dist/interactions/actions/defaults/anchorEditing.js.map +1 -1
- package/dist/interactions/actions/defaults/bendSegment.js +1 -2
- package/dist/interactions/actions/defaults/bendSegment.js.map +1 -1
- package/dist/interactions/actions/defaults/exitPathEdit.js +17 -1
- package/dist/interactions/actions/defaults/exitPathEdit.js.map +1 -1
- package/dist/interactions/actions/edit-anchors/action.js +3 -4
- package/dist/interactions/actions/edit-anchors/action.js.map +1 -1
- package/dist/interactions/actions/insert/action.js +21 -2
- package/dist/interactions/actions/insert/action.js.map +1 -1
- package/dist/interactions/actions/insert/params.js +43 -0
- package/dist/interactions/actions/insert/params.js.map +1 -0
- package/dist/interactions/actions/notices.js +17 -0
- package/dist/interactions/actions/notices.js.map +1 -0
- package/dist/interactions/actions/outlines/createOutlines.js +8 -6
- package/dist/interactions/actions/outlines/createOutlines.js.map +1 -1
- package/dist/interactions/actions/outlines/createOutlinesAction.js +13 -8
- package/dist/interactions/actions/outlines/createOutlinesAction.js.map +1 -1
- package/dist/interactions/actions/outlines/outlineStrokeAction.js +3 -2
- package/dist/interactions/actions/outlines/outlineStrokeAction.js.map +1 -1
- package/dist/interactions/actions/pathCommands/pathNodes.js +1 -1
- package/dist/interactions/actions/testUtils.js +0 -7
- package/dist/interactions/actions/testUtils.js.map +1 -1
- package/dist/interactions/actions/useNotices.js +13 -0
- package/dist/interactions/actions/useNotices.js.map +1 -0
- package/dist/test-seams.d.ts +5 -12
- package/dist/tools/builtin/anchors/anchorsTool.js +31 -0
- package/dist/tools/builtin/anchors/anchorsTool.js.map +1 -0
- package/dist/tools/builtin/polygon/usePolygonTool.js +1 -1
- package/dist/tools/builtin/star/useStarTool.js +1 -1
- package/dist/tools/index.js +4 -2
- package/dist/tools/toolOptions.js +47 -0
- package/dist/tools/toolOptions.js.map +1 -0
- package/dist/version.js +1 -1
- package/package.json +25 -25
- package/dist/interactions/actions/defaults/pathEditTools.js +0 -60
- package/dist/interactions/actions/defaults/pathEditTools.js.map +0 -1
|
@@ -54,8 +54,8 @@ function scenePickSource(scene, opts = {}) {
|
|
|
54
54
|
poseOf,
|
|
55
55
|
parentsOf: sceneParentsOf(scene),
|
|
56
56
|
...readAlpha ? { alphaOf: readAlpha } : {},
|
|
57
|
-
isLocked: (node) => gates.locked(node
|
|
58
|
-
isHidden: (node) => gates.hidden(node
|
|
57
|
+
isLocked: (node) => gates.locked(node),
|
|
58
|
+
isHidden: (node) => gates.hidden(node),
|
|
59
59
|
...annotations.size > 0 || layerIsPainted ? { layerIsPainted: (layer) => !annotations.has(layer) && (layerIsPainted === void 0 || layerIsPainted(layer)) } : {}
|
|
60
60
|
};
|
|
61
61
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scenePickSource.js","names":[],"sources":["../../src/canvas/scenePickSource.ts"],"sourcesContent":["/** The pick source over a `Scene`: its render order, painted poses and gates,\n * and, given a descriptor, the pick index that narrows the walk. */\nimport type { Path } from '@weasel-js/geom';\nimport type { DerivedDep, Node, NodeId, Scene } from 'core/scene/types';\nimport { asNodeId } from 'core/scene/types';\nimport { annotationLayerIds } from 'core/scene/annotation';\nimport { nodeGates } from 'core/scene/nodeGates';\nimport { planeMap, type ParallaxOpts, type PlaneMap } from 'core/viewport/parallax';\nimport type { View } from 'core/viewport/view';\nimport type { PoseDescriptor } from 'interactions/actions/resize/geometry';\nimport { resolveDerivedPath, scenePaintedPoseOf } from './derivedPath';\nimport { indexedCandidates } from './pickIndex/scenePickIndex';\nimport type { PickBroad, PickCandidate, PickSource, ViewPickGates } from './pickWalk';\nimport { sceneParentsOf, sceneShownPathOf } from './scenePickReads';\nimport { nodeAsShown, nodesAsShown } from 'core/scene/nodeAsShown';\n\nexport interface ScenePickSourceOptions<TPose> extends ViewPickGates {\n /** Pose lookup, when the caller holds an adapter that already resolves\n * overrides. Defaults to `effectivePose` against the scene's own. */\n getPose?: (id: string) => TPose;\n /** The asking view's composed alpha — `alphaFor(id)` times override alpha.\n * Defaults to the override alpha alone, which is all a bare scene knows. */\n alphaOf?: (id: string) => number;\n /** The asking view's answer to \"does this scene layer reach the screen\",\n * which is `layerVisibility` and `layerOrder` on top of the scene's own\n * `visible` flag. Consulted after the scene's flag, never instead of it. */\n layerIsPainted?: (layer: string) => boolean;\n /** The asking view's camera. With it, a layer carrying `parallax` is picked\n * through its plane; without it, every layer is taken to move with the\n * camera. */\n camera?: View;\n /** Box every node once per scene change, so a query carrying a `broad` box\n * visits only the nodes it can hit. `descriptor` must be the one the\n * query reads poses through. */\n index?: { descriptor: PoseDescriptor<unknown> };\n}\n\nexport function scenePickSource<TData, TLayer extends string, TPose>(\n scene: Scene<TData, TLayer, TPose>,\n opts: ScenePickSourceOptions<TPose> = {},\n): PickSource<TPose> {\n // Partial scene stand-ins carry only `layers` and `get`, which is all the gates read.\n const gates = nodeGates(scene as never);\n const enumerable = typeof scene.overrides?.ids === 'function';\n // Pick candidates carry a plain string id; the scene answers them all the same.\n const asShown = <N extends { id: string; data?: unknown }>(node: N): N =>\n (scene.overrides ? nodeAsShown(scene.overrides, node as N & { id: NodeId }) : node);\n const allAsShown = <N extends { id: string; data?: unknown }>(nodes: readonly N[]): readonly N[] =>\n (enumerable ? nodesAsShown(scene.overrides, nodes as readonly (N & { id: NodeId })[]) : nodes.map(asShown));\n const annotations = annotationLayerIds(scene.layers);\n const { getPose, alphaOf, layerIsPainted } = opts;\n\n // Resolved once per walk rather than once per candidate. Needs enumeration — a\n // partial scene stand-in carrying only `read` falls back to reading per node.\n let faded: Set<string> | null = null;\n if (!alphaOf && enumerable) {\n for (const id of scene.overrides.ids()) {\n const a = scene.overrides.read(id)?.alpha;\n if (a !== undefined && a <= 0) (faded ??= new Set()).add(id);\n }\n }\n const readAlpha = alphaOf\n ?? (enumerable\n ? (faded ? (id: string) => (faded.has(id) ? 0 : 1) : undefined)\n : (id: string) => scene.overrides.read(asNodeId(id))?.alpha ?? 1);\n\n // A composing strategy makes a node's own pose local, so picking folds the\n // parent chain the same way the render walk does: one definition of both.\n const paintedPoseOf = scenePaintedPoseOf(scene);\n const poseOf = getPose\n ? (node: PickCandidate<TPose>) => getPose(node.id)\n : (node: PickCandidate<TPose>) => paintedPoseOf(node as never);\n\n // Through `poseOf`, not the document pose: a derived node has to be tested\n // where the renderer draws its dependencies, which is what an override says.\n const childrenOf = (id: NodeId): readonly NodeId[] => scene.childrenOf(id);\n const depOf = (id: NodeId): DerivedDep<TPose> | undefined => {\n const stored = scene.get(id);\n if (stored === undefined) return undefined;\n const dep = asShown(stored);\n return {\n node: dep as Node<unknown, string, TPose>,\n pose: poseOf(dep as never),\n get path(): Path | null {\n return resolveDerivedPath(dep as never, depOf, childrenOf);\n },\n };\n };\n\n const derivedPathOf = getPose\n ? (node: PickCandidate<TPose>): Path | null => {\n const n = node as unknown as Node<TData, TLayer, TPose>;\n return n.dependsOn === undefined ? null : resolveDerivedPath(n, depOf, childrenOf);\n }\n : sceneShownPathOf(scene);\n\n const planeOf = opts.camera ? scenePlaneOf(scene.layers, opts.camera) : null;\n\n // The index boxes nodes at the scene's painted poses; a custom `getPose` must be shown to agree first.\n // A partial stand-in has no versions for it to key on.\n const index = getPose || typeof scene.getVersion !== 'function' ? undefined : opts.index;\n return {\n ...(planeOf ? { planeOf } : {}),\n ...(index ? {\n candidates: (broadFor: (layer: string | undefined) => PickBroad | null | undefined) => {\n const found = indexedCandidates(scene as never, index.descriptor, broadFor) as readonly PickCandidate<TPose>[] | null;\n return found === null ? null : allAsShown(found);\n },\n } : {}),\n order: () => allAsShown(scene.renderOrderNodes() as unknown as readonly PickCandidate<TPose>[]),\n derivedPathOf,\n poseOf,\n parentsOf: sceneParentsOf(scene),\n // Absent entirely when nothing is faded, so the walk skips the gate.\n ...(readAlpha ? { alphaOf: readAlpha } : {}),\n isLocked: (node: PickCandidate<TPose>) => gates.locked(node
|
|
1
|
+
{"version":3,"file":"scenePickSource.js","names":[],"sources":["../../src/canvas/scenePickSource.ts"],"sourcesContent":["/** The pick source over a `Scene`: its render order, painted poses and gates,\n * and, given a descriptor, the pick index that narrows the walk. */\nimport type { Path } from '@weasel-js/geom';\nimport type { DerivedDep, Node, NodeId, Scene } from 'core/scene/types';\nimport { asNodeId } from 'core/scene/types';\nimport { annotationLayerIds } from 'core/scene/annotation';\nimport { nodeGates, type GatedNode } from 'core/scene/nodeGates';\nimport { planeMap, type ParallaxOpts, type PlaneMap } from 'core/viewport/parallax';\nimport type { View } from 'core/viewport/view';\nimport type { PoseDescriptor } from 'interactions/actions/resize/geometry';\nimport { resolveDerivedPath, scenePaintedPoseOf } from './derivedPath';\nimport { indexedCandidates } from './pickIndex/scenePickIndex';\nimport type { PickBroad, PickCandidate, PickSource, ViewPickGates } from './pickWalk';\nimport { sceneParentsOf, sceneShownPathOf } from './scenePickReads';\nimport { nodeAsShown, nodesAsShown } from 'core/scene/nodeAsShown';\n\nexport interface ScenePickSourceOptions<TPose> extends ViewPickGates {\n /** Pose lookup, when the caller holds an adapter that already resolves\n * overrides. Defaults to `effectivePose` against the scene's own. */\n getPose?: (id: string) => TPose;\n /** The asking view's composed alpha — `alphaFor(id)` times override alpha.\n * Defaults to the override alpha alone, which is all a bare scene knows. */\n alphaOf?: (id: string) => number;\n /** The asking view's answer to \"does this scene layer reach the screen\",\n * which is `layerVisibility` and `layerOrder` on top of the scene's own\n * `visible` flag. Consulted after the scene's flag, never instead of it. */\n layerIsPainted?: (layer: string) => boolean;\n /** The asking view's camera. With it, a layer carrying `parallax` is picked\n * through its plane; without it, every layer is taken to move with the\n * camera. */\n camera?: View;\n /** Box every node once per scene change, so a query carrying a `broad` box\n * visits only the nodes it can hit. `descriptor` must be the one the\n * query reads poses through. */\n index?: { descriptor: PoseDescriptor<unknown> };\n}\n\nexport function scenePickSource<TData, TLayer extends string, TPose>(\n scene: Scene<TData, TLayer, TPose>,\n opts: ScenePickSourceOptions<TPose> = {},\n): PickSource<TPose> {\n // Partial scene stand-ins carry only `layers` and `get`, which is all the gates read.\n const gates = nodeGates(scene as never);\n const enumerable = typeof scene.overrides?.ids === 'function';\n // Pick candidates carry a plain string id; the scene answers them all the same.\n const asShown = <N extends { id: string; data?: unknown }>(node: N): N =>\n (scene.overrides ? nodeAsShown(scene.overrides, node as N & { id: NodeId }) : node);\n const allAsShown = <N extends { id: string; data?: unknown }>(nodes: readonly N[]): readonly N[] =>\n (enumerable ? nodesAsShown(scene.overrides, nodes as readonly (N & { id: NodeId })[]) : nodes.map(asShown));\n const annotations = annotationLayerIds(scene.layers);\n const { getPose, alphaOf, layerIsPainted } = opts;\n\n // Resolved once per walk rather than once per candidate. Needs enumeration — a\n // partial scene stand-in carrying only `read` falls back to reading per node.\n let faded: Set<string> | null = null;\n if (!alphaOf && enumerable) {\n for (const id of scene.overrides.ids()) {\n const a = scene.overrides.read(id)?.alpha;\n if (a !== undefined && a <= 0) (faded ??= new Set()).add(id);\n }\n }\n const readAlpha = alphaOf\n ?? (enumerable\n ? (faded ? (id: string) => (faded.has(id) ? 0 : 1) : undefined)\n : (id: string) => scene.overrides.read(asNodeId(id))?.alpha ?? 1);\n\n // A composing strategy makes a node's own pose local, so picking folds the\n // parent chain the same way the render walk does: one definition of both.\n const paintedPoseOf = scenePaintedPoseOf(scene);\n const poseOf = getPose\n ? (node: PickCandidate<TPose>) => getPose(node.id)\n : (node: PickCandidate<TPose>) => paintedPoseOf(node as never);\n\n // Through `poseOf`, not the document pose: a derived node has to be tested\n // where the renderer draws its dependencies, which is what an override says.\n const childrenOf = (id: NodeId): readonly NodeId[] => scene.childrenOf(id);\n const depOf = (id: NodeId): DerivedDep<TPose> | undefined => {\n const stored = scene.get(id);\n if (stored === undefined) return undefined;\n const dep = asShown(stored);\n return {\n node: dep as Node<unknown, string, TPose>,\n pose: poseOf(dep as never),\n get path(): Path | null {\n return resolveDerivedPath(dep as never, depOf, childrenOf);\n },\n };\n };\n\n const derivedPathOf = getPose\n ? (node: PickCandidate<TPose>): Path | null => {\n const n = node as unknown as Node<TData, TLayer, TPose>;\n return n.dependsOn === undefined ? null : resolveDerivedPath(n, depOf, childrenOf);\n }\n : sceneShownPathOf(scene);\n\n const planeOf = opts.camera ? scenePlaneOf(scene.layers, opts.camera) : null;\n\n // The index boxes nodes at the scene's painted poses; a custom `getPose` must be shown to agree first.\n // A partial stand-in has no versions for it to key on.\n const index = getPose || typeof scene.getVersion !== 'function' ? undefined : opts.index;\n return {\n ...(planeOf ? { planeOf } : {}),\n ...(index ? {\n candidates: (broadFor: (layer: string | undefined) => PickBroad | null | undefined) => {\n const found = indexedCandidates(scene as never, index.descriptor, broadFor) as readonly PickCandidate<TPose>[] | null;\n return found === null ? null : allAsShown(found);\n },\n } : {}),\n order: () => allAsShown(scene.renderOrderNodes() as unknown as readonly PickCandidate<TPose>[]),\n derivedPathOf,\n poseOf,\n parentsOf: sceneParentsOf(scene),\n // Absent entirely when nothing is faded, so the walk skips the gate.\n ...(readAlpha ? { alphaOf: readAlpha } : {}),\n isLocked: (node: PickCandidate<TPose>) => gates.locked(node as GatedNode),\n isHidden: (node: PickCandidate<TPose>) => gates.hidden(node as GatedNode),\n ...(annotations.size > 0 || layerIsPainted\n ? {\n layerIsPainted: (layer: string) =>\n !annotations.has(layer) && (layerIsPainted === undefined || layerIsPainted(layer)),\n }\n : {}),\n };\n}\n\n/** Each parallax layer's plane map under `camera`, or null when no layer is a\n * plane — which keeps the per-layer lookup off the walk entirely. */\nexport function scenePlaneOf(\n layers: readonly { id: string; parallax?: ParallaxOpts }[] | undefined,\n camera: View,\n): ((layer: string) => PlaneMap | null) | null {\n let maps: Map<string, PlaneMap> | null = null;\n for (const l of layers ?? []) {\n if (l.parallax) (maps ??= new Map()).set(l.id, planeMap(camera, l.parallax));\n }\n if (maps === null) return null;\n const found = maps;\n return (layer) => found.get(layer) ?? null;\n}\n"],"mappings":";;;;;;;;;;;AAqCA,SAAgB,gBACd,OACA,OAAsC,CAAC,GACpB;CAEnB,MAAM,QAAQ,UAAU,KAAc;CACtC,MAAM,aAAa,OAAO,MAAM,WAAW,QAAQ;CAEnD,MAAM,WAAqD,SACxD,MAAM,YAAY,YAAY,MAAM,WAAW,IAA0B,IAAI;CAChF,MAAM,cAAwD,UAC3D,aAAa,aAAa,MAAM,WAAW,KAAwC,IAAI,MAAM,IAAI,OAAO;CAC3G,MAAM,cAAc,mBAAmB,MAAM,MAAM;CACnD,MAAM,EAAE,SAAS,SAAS,mBAAmB;CAI7C,IAAI,QAA4B;CAChC,IAAI,CAAC,WAAW,YACd,KAAK,MAAM,MAAM,MAAM,UAAU,IAAI,GAAG;EACtC,MAAM,IAAI,MAAM,UAAU,KAAK,EAAE,CAAC,EAAE;EACpC,IAAI,MAAM,UAAa,KAAK,GAAG,CAAC,0BAAU,IAAI,IAAI,EAAA,CAAG,IAAI,EAAE;CAC7D;CAEF,MAAM,YAAY,YACZ,aACC,SAAS,OAAgB,MAAM,IAAI,EAAE,IAAI,IAAI,IAAK,UAClD,OAAe,MAAM,UAAU,KAAK,SAAS,EAAE,CAAC,CAAC,EAAE,SAAS;CAInE,MAAM,gBAAgB,mBAAmB,KAAK;CAC9C,MAAM,SAAS,WACV,SAA+B,QAAQ,KAAK,EAAE,KAC9C,SAA+B,cAAc,IAAa;CAI/D,MAAM,cAAc,OAAkC,MAAM,WAAW,EAAE;CACzE,MAAM,SAAS,OAA8C;EAC3D,MAAM,SAAS,MAAM,IAAI,EAAE;EAC3B,IAAI,WAAW,QAAW,OAAO;EACjC,MAAM,MAAM,QAAQ,MAAM;EAC1B,OAAO;GACL,MAAM;GACN,MAAM,OAAO,GAAY;GACzB,IAAI,OAAoB;IACtB,OAAO,mBAAmB,KAAc,OAAO,UAAU;GAC3D;EACF;CACF;CAEA,MAAM,gBAAgB,WACjB,SAA4C;EAC7C,MAAM,IAAI;EACV,OAAO,EAAE,cAAc,SAAY,OAAO,mBAAmB,GAAG,OAAO,UAAU;CACnF,IACE,iBAAiB,KAAK;CAE1B,MAAM,UAAU,KAAK,SAAS,aAAa,MAAM,QAAQ,KAAK,MAAM,IAAI;CAIxE,MAAM,QAAQ,WAAW,OAAO,MAAM,eAAe,aAAa,SAAY,KAAK;CACnF,OAAO;EACL,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;EAC7B,GAAI,QAAQ,EACV,aAAa,aAA0E;GACrF,MAAM,QAAQ,kBAAkB,OAAgB,MAAM,YAAY,QAAQ;GAC1E,OAAO,UAAU,OAAO,OAAO,WAAW,KAAK;EACjD,EACF,IAAI,CAAC;EACL,aAAa,WAAW,MAAM,iBAAiB,CAA+C;EAC9F;EACA;EACA,WAAW,eAAe,KAAK;EAE/B,GAAI,YAAY,EAAE,SAAS,UAAU,IAAI,CAAC;EAC1C,WAAW,SAA+B,MAAM,OAAO,IAAiB;EACxE,WAAW,SAA+B,MAAM,OAAO,IAAiB;EACxE,GAAI,YAAY,OAAO,KAAK,iBACxB,EACA,iBAAiB,UACf,CAAC,YAAY,IAAI,KAAK,MAAM,mBAAmB,UAAa,eAAe,KAAK,GACpF,IACE,CAAC;CACP;AACF;;;AAIA,SAAgB,aACd,QACA,QAC6C;CAC7C,IAAI,OAAqC;CACzC,KAAK,MAAM,KAAK,UAAU,CAAC,GACzB,IAAI,EAAE,UAAU,CAAC,yBAAS,IAAI,IAAI,EAAA,CAAG,IAAI,EAAE,IAAI,SAAS,QAAQ,EAAE,QAAQ,CAAC;CAE7E,IAAI,SAAS,MAAM,OAAO;CAC1B,MAAM,QAAQ;CACd,QAAQ,UAAU,MAAM,IAAI,KAAK,KAAK;AACxC"}
|
|
@@ -22,16 +22,34 @@ function useCanvasSurface(paint, options = {}) {
|
|
|
22
22
|
useLateContentRedraw(requestRedraw);
|
|
23
23
|
useRedrawOnShaderSource(options.shaders, requestRedraw);
|
|
24
24
|
useRedrawOn(options.redrawOn, requestRedraw);
|
|
25
|
-
|
|
25
|
+
const resized = usePaintOnResize(options.size, loop.paintNow);
|
|
26
|
+
useRedrawOnCommit(options.onCommit, () => paint(lease), requestRedraw, resized);
|
|
26
27
|
return {
|
|
27
28
|
...loop,
|
|
28
29
|
lease
|
|
29
30
|
};
|
|
30
31
|
}
|
|
31
|
-
|
|
32
|
+
/** Paints a commit that resized the surface in place. Returns whether this commit did. */
|
|
33
|
+
function usePaintOnResize(size, paintNow) {
|
|
34
|
+
const last = useRef(null);
|
|
35
|
+
const resized = useRef(false);
|
|
36
|
+
const width = size?.width ?? 0;
|
|
37
|
+
const height = size?.height ?? 0;
|
|
38
|
+
useLayoutEffect(() => {
|
|
39
|
+
const was = last.current;
|
|
40
|
+
last.current = {
|
|
41
|
+
width,
|
|
42
|
+
height
|
|
43
|
+
};
|
|
44
|
+
resized.current = was !== null && (was.width !== width || was.height !== height);
|
|
45
|
+
if (resized.current) paintNow();
|
|
46
|
+
});
|
|
47
|
+
return resized;
|
|
48
|
+
}
|
|
49
|
+
function useRedrawOnCommit(mode, paintNow, requestRedraw, resized) {
|
|
32
50
|
const paintedRef = useRef(false);
|
|
33
51
|
useLayoutEffect(() => {
|
|
34
|
-
if (!mode) return;
|
|
52
|
+
if (!mode || resized.current) return;
|
|
35
53
|
if (mode === "redraw" || paintedRef.current) {
|
|
36
54
|
requestRedraw();
|
|
37
55
|
return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useCanvasSurface.js","names":[],"sources":["../../src/canvas/useCanvasSurface.ts"],"sourcesContent":["import { useEffect, useLayoutEffect, useRef } from 'react';\nimport type { ShaderProgramHandle } from '@weasel-js/renderer';\nimport type { RedrawSource } from '../core/layers/render';\nimport type { CanvasRendererLease } from './canvasRenderer';\nimport {\n useCanvasRenderer, useRedrawOnDensityChange, useRedrawOnRestore, useRedrawOnShaderSource,\n} from './useCanvasRenderer';\nimport { useFrameLoop, type FrameLoop, type FrameLoopOptions } from './useFrameLoop';\nimport { useLateContentRedraw } from './useLateContentRedraw';\nimport { useRedrawOn } from './useRedrawOn';\n\n/** What a surface tells {@link useCanvasSurface} about itself, beyond its paint. */\nexport interface CanvasSurfaceOptions extends FrameLoopOptions {\n /** The surface's own density. Omit to follow `devicePixelRatio`. */\n dpr?: number;\n /** External state the paint reads; each source repaints when it notifies. */\n redrawOn?: readonly RedrawSource[];\n /** Custom programs the paint draws with; new source for one repaints. */\n shaders?: readonly ShaderProgramHandle[];\n /** Mark the surface dirty on every committed render, so a surface whose\n * paint reads its props cannot paint them stale. `'paint'` also paints the\n * first commit in place, in the caller's stack, so the first frame shown is\n * the content and not a blank canvas. */\n onCommit?: 'redraw' | 'paint';\n}\n\n/** A surface's frame loop and the renderer lease its paint draws through. */\nexport interface CanvasSurface extends FrameLoop {\n lease: CanvasRendererLease;\n}\n\n/**\n * Everything a canvas surface in core needs around its paint: the renderer\n * lease, the frame loop, and a repaint for each thing that changes pixels with\n * no React render behind it — a restored GL context, a density change, late\n * content, a re-registered shader, and the caller's own `redrawOn` sources.\n *\n * @param paint As `useFrameLoop`'s, handed the lease to draw through.\n */\nexport function useCanvasSurface(\n paint: (lease: CanvasRendererLease) => boolean,\n options: CanvasSurfaceOptions = {},\n): CanvasSurface {\n const lease = useCanvasRenderer();\n const loop = useFrameLoop(() => paint(lease), options);\n const { requestRedraw } = loop;\n useRedrawOnRestore(lease, requestRedraw);\n useRedrawOnDensityChange(requestRedraw, options.dpr);\n useLateContentRedraw(requestRedraw);\n useRedrawOnShaderSource(options.shaders, requestRedraw);\n useRedrawOn(options.redrawOn, requestRedraw);\n useRedrawOnCommit(options.onCommit, () => paint(lease), requestRedraw);\n return { ...loop, lease };\n}\n\nfunction useRedrawOnCommit(\n mode: CanvasSurfaceOptions['onCommit'],\n paintNow: () => boolean,\n requestRedraw: () => void,\n): void {\n const paintedRef = useRef(false);\n // No dependency array: the paint reads whatever the render did.\n useLayoutEffect(() => {\n if (!mode) return;\n if (mode === 'redraw' || paintedRef.current) { requestRedraw(); return; }\n paintedRef.current = paintNow();\n if (!paintedRef.current) requestRedraw();\n });\n // The renderer is freed on unmount; a remount (StrictMode) paints its first\n // frame in place again.\n useEffect(() => () => { paintedRef.current = false; }, []);\n}\n"],"mappings":";;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"useCanvasSurface.js","names":[],"sources":["../../src/canvas/useCanvasSurface.ts"],"sourcesContent":["import { useEffect, useLayoutEffect, useRef } from 'react';\nimport type { ShaderProgramHandle } from '@weasel-js/renderer';\nimport type { RedrawSource } from '../core/layers/render';\nimport type { CanvasRendererLease } from './canvasRenderer';\nimport {\n useCanvasRenderer, useRedrawOnDensityChange, useRedrawOnRestore, useRedrawOnShaderSource,\n} from './useCanvasRenderer';\nimport { useFrameLoop, type FrameLoop, type FrameLoopOptions } from './useFrameLoop';\nimport { useLateContentRedraw } from './useLateContentRedraw';\nimport { useRedrawOn } from './useRedrawOn';\n\n/** What a surface tells {@link useCanvasSurface} about itself, beyond its paint. */\nexport interface CanvasSurfaceOptions extends FrameLoopOptions {\n /** The surface's own density. Omit to follow `devicePixelRatio`. */\n dpr?: number;\n /** External state the paint reads; each source repaints when it notifies. */\n redrawOn?: readonly RedrawSource[];\n /** Custom programs the paint draws with; new source for one repaints. */\n shaders?: readonly ShaderProgramHandle[];\n /** Mark the surface dirty on every committed render, so a surface whose\n * paint reads its props cannot paint them stale. `'paint'` also paints the\n * first commit in place, in the caller's stack, so the first frame shown is\n * the content and not a blank canvas. */\n onCommit?: 'redraw' | 'paint';\n /** The size the surface paints at. A commit that changes it paints in place: React has written the element's\n * new size, which clears its buffer, and a resize committed from a `ResizeObserver` callback has no frame\n * left before the browser paints. */\n size?: { width: number; height: number };\n}\n\n/** A surface's frame loop and the renderer lease its paint draws through. */\nexport interface CanvasSurface extends FrameLoop {\n lease: CanvasRendererLease;\n}\n\n/**\n * Everything a canvas surface in core needs around its paint: the renderer\n * lease, the frame loop, and a repaint for each thing that changes pixels with\n * no React render behind it — a restored GL context, a density change, late\n * content, a re-registered shader, and the caller's own `redrawOn` sources.\n *\n * @param paint As `useFrameLoop`'s, handed the lease to draw through.\n */\nexport function useCanvasSurface(\n paint: (lease: CanvasRendererLease) => boolean,\n options: CanvasSurfaceOptions = {},\n): CanvasSurface {\n const lease = useCanvasRenderer();\n const loop = useFrameLoop(() => paint(lease), options);\n const { requestRedraw } = loop;\n useRedrawOnRestore(lease, requestRedraw);\n useRedrawOnDensityChange(requestRedraw, options.dpr);\n useLateContentRedraw(requestRedraw);\n useRedrawOnShaderSource(options.shaders, requestRedraw);\n useRedrawOn(options.redrawOn, requestRedraw);\n const resized = usePaintOnResize(options.size, loop.paintNow);\n useRedrawOnCommit(options.onCommit, () => paint(lease), requestRedraw, resized);\n return { ...loop, lease };\n}\n\n/** Paints a commit that resized the surface in place. Returns whether this commit did. */\nfunction usePaintOnResize(size: CanvasSurfaceOptions['size'], paintNow: () => void): { readonly current: boolean } {\n const last = useRef<{ width: number; height: number } | null>(null);\n const resized = useRef(false);\n const width = size?.width ?? 0;\n const height = size?.height ?? 0;\n useLayoutEffect(() => {\n const was = last.current;\n last.current = { width, height };\n resized.current = was !== null && (was.width !== width || was.height !== height);\n if (resized.current) paintNow();\n });\n return resized;\n}\n\nfunction useRedrawOnCommit(\n mode: CanvasSurfaceOptions['onCommit'],\n paintNow: () => boolean,\n requestRedraw: () => void,\n resized: { readonly current: boolean },\n): void {\n const paintedRef = useRef(false);\n // No dependency array: the paint reads whatever the render did.\n useLayoutEffect(() => {\n if (!mode || resized.current) return;\n if (mode === 'redraw' || paintedRef.current) { requestRedraw(); return; }\n paintedRef.current = paintNow();\n if (!paintedRef.current) requestRedraw();\n });\n // The renderer is freed on unmount; a remount (StrictMode) paints its first\n // frame in place again.\n useEffect(() => () => { paintedRef.current = false; }, []);\n}\n"],"mappings":";;;;;;;;;;;;;;;AA2CA,SAAgB,iBACd,OACA,UAAgC,CAAC,GAClB;CACf,MAAM,QAAQ,kBAAkB;CAChC,MAAM,OAAO,mBAAmB,MAAM,KAAK,GAAG,OAAO;CACrD,MAAM,EAAE,kBAAkB;CAC1B,mBAAmB,OAAO,aAAa;CACvC,yBAAyB,eAAe,QAAQ,GAAG;CACnD,qBAAqB,aAAa;CAClC,wBAAwB,QAAQ,SAAS,aAAa;CACtD,YAAY,QAAQ,UAAU,aAAa;CAC3C,MAAM,UAAU,iBAAiB,QAAQ,MAAM,KAAK,QAAQ;CAC5D,kBAAkB,QAAQ,gBAAgB,MAAM,KAAK,GAAG,eAAe,OAAO;CAC9E,OAAO;EAAE,GAAG;EAAM;CAAM;AAC1B;;AAGA,SAAS,iBAAiB,MAAoC,UAAqD;CACjH,MAAM,OAAO,OAAiD,IAAI;CAClE,MAAM,UAAU,OAAO,KAAK;CAC5B,MAAM,QAAQ,MAAM,SAAS;CAC7B,MAAM,SAAS,MAAM,UAAU;CAC/B,sBAAsB;EACpB,MAAM,MAAM,KAAK;EACjB,KAAK,UAAU;GAAE;GAAO;EAAO;EAC/B,QAAQ,UAAU,QAAQ,SAAS,IAAI,UAAU,SAAS,IAAI,WAAW;EACzE,IAAI,QAAQ,SAAS,SAAS;CAChC,CAAC;CACD,OAAO;AACT;AAEA,SAAS,kBACP,MACA,UACA,eACA,SACM;CACN,MAAM,aAAa,OAAO,KAAK;CAE/B,sBAAsB;EACpB,IAAI,CAAC,QAAQ,QAAQ,SAAS;EAC9B,IAAI,SAAS,YAAY,WAAW,SAAS;GAAE,cAAc;GAAG;EAAQ;EACxE,WAAW,UAAU,SAAS;EAC9B,IAAI,CAAC,WAAW,SAAS,cAAc;CACzC,CAAC;CAGD,sBAAsB;EAAE,WAAW,UAAU;CAAO,GAAG,CAAC,CAAC;AAC3D"}
|
|
@@ -73,18 +73,16 @@ function useFrameLoop(paint, options = {}) {
|
|
|
73
73
|
frame.request();
|
|
74
74
|
};
|
|
75
75
|
});
|
|
76
|
-
const
|
|
76
|
+
const redraw = useCallback((inPlace) => {
|
|
77
77
|
dirtyRef.current = true;
|
|
78
|
-
if (
|
|
78
|
+
if (inPlace && aliveRef.current && !paintingRef.current && frame.isVisible()) {
|
|
79
79
|
runPaint(performance.now());
|
|
80
80
|
return;
|
|
81
81
|
}
|
|
82
82
|
frame.request();
|
|
83
|
-
}, [
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
syncRef
|
|
87
|
-
]);
|
|
83
|
+
}, [frame, runPaint]);
|
|
84
|
+
const requestRedraw = useCallback(() => redraw(syncRef.current), [redraw, syncRef]);
|
|
85
|
+
const paintNow = useCallback(() => redraw(true), [redraw]);
|
|
88
86
|
const subscribeFrame = useCallback((fn) => {
|
|
89
87
|
subsRef.current.add(fn);
|
|
90
88
|
return () => {
|
|
@@ -117,6 +115,7 @@ function useFrameLoop(paint, options = {}) {
|
|
|
117
115
|
}, []);
|
|
118
116
|
return {
|
|
119
117
|
requestRedraw,
|
|
118
|
+
paintNow,
|
|
120
119
|
subscribeFrame,
|
|
121
120
|
subscribeBeforePaint,
|
|
122
121
|
subscribeResume
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useFrameLoop.js","names":[],"sources":["../../src/canvas/useFrameLoop.ts"],"sourcesContent":["/**\n * The frame loop behind `<Canvas>`: a dirty flag, one frame in flight at a\n * time, and a subscriber set notified after a paint lands. When frames may run\n * at all is `useVisibleRaf`'s question, not this hook's — a redraw requested\n * while nothing can see the surface is held there and re-armed on resume.\n *\n * @internal Not consumer surface. Canvas exposes `requestRedraw` and\n * `subscribeFrame` on its ref handle; this is how they are implemented.\n */\n\nimport { useCallback, useInsertionEffect, useLayoutEffect, useRef } from 'react';\nimport { useLatest, useVisibleRaf, type VisibleRafTarget } from '@weasel-js/react';\n\nexport interface FrameLoop {\n /** Mark the surface dirty and schedule a frame. Identity is stable for the\n * lifetime of the component — consumers capture it. */\n requestRedraw(): void;\n /** Run `fn` after every landed paint, with the frame's time. Returns an\n * unsubscribe. */\n subscribeFrame(fn: (time: number) => void): () => void;\n /** Run `fn` on every frame just before its paint, with the frame's time.\n * Returns an unsubscribe. */\n subscribeBeforePaint(fn: (time: number) => void): () => void;\n /** Run `fn` when the loop leaves suspension, before the frame that follows.\n * Returns an unsubscribe. */\n subscribeResume(fn: () => void): () => void;\n}\n\nexport interface FrameLoopOptions {\n /** Paint in the caller's own stack — the commit or event that asked for the\n * redraw — instead of on the next animation frame. Read live, so toggling\n * it takes effect from the next `requestRedraw` on. */\n syncPaint?: boolean;\n /** The surface being painted. Given one, the loop also stops while that\n * element sits outside the viewport, not only while the tab is hidden. */\n target?: VisibleRafTarget;\n}\n\n/**\n * @param paint Runs on the frame; returns whether pixels landed. A `false`\n * leaves the surface dirty and notifies nobody, so the next request retries.\n */\nexport function useFrameLoop(paint: () => boolean, options: FrameLoopOptions = {}): FrameLoop {\n const dirtyRef = useRef(true);\n const aliveRef = useRef(true);\n const paintingRef = useRef(false);\n const paintRef = useLatest(paint);\n const syncRef = useLatest(options.syncPaint ?? false);\n const subsRef = useRef<Set<(time: number) => void>>(new Set());\n const beforeRef = useRef<Set<(time: number) => void>>(new Set());\n const resumeRef = useRef<Set<() => void>>(new Set());\n /** The last time handed to subscribers. A sync paint reads the clock at the\n * call and a rAF frame carries its frame's start, so within one animation\n * frame the second can be the earlier of the two. */\n const lastTimeRef = useRef(-Infinity);\n\n /** Set while recovering from a paint that threw, so one bad frame is retried\n * and a permanently throwing one still doesn't spin at frame rate. */\n const retriedRef = useRef(false);\n /** `frame.request`, which doesn't exist until after `runPaint` is built. */\n const rearmRef = useRef<(() => void) | null>(null);\n\n const runPaint = useCallback((at: number) => {\n const time = Math.max(at, lastTimeRef.current);\n lastTimeRef.current = time;\n dirtyRef.current = false;\n paintingRef.current = true;\n let landed = false;\n try {\n for (const fn of beforeRef.current) fn(time);\n landed = paintRef.current();\n if (!landed) {\n dirtyRef.current = true;\n return;\n }\n retriedRef.current = false;\n // Notified inside the guard: a subscriber calling `requestRedraw` under\n // `syncPaint` would otherwise re-enter this synchronously, without bound.\n for (const fn of subsRef.current) fn(time);\n } catch (err) {\n // A throw skips the `!landed` branch above, so without this the surface\n // is left clean but unpainted — the pixels on screen are whatever the\n // last landed frame left, and nothing repaints until something\n // unrelated happens to request a redraw. That is how a single\n // malformed node blanked a whole document until the pointer moved.\n dirtyRef.current = true;\n if (!retriedRef.current) {\n retriedRef.current = true;\n rearmRef.current?.();\n }\n throw err;\n } finally {\n paintingRef.current = false;\n }\n }, [paintRef]);\n\n const frame = useVisibleRaf(\n useCallback((time: number) => {\n if (!aliveRef.current || !dirtyRef.current) return;\n runPaint(time);\n }, [runPaint]),\n {\n target: options.target,\n onResume: useCallback(() => { for (const fn of resumeRef.current) fn(); }, []),\n },\n );\n useInsertionEffect(() => {\n rearmRef.current = () => { frame.request(); };\n });\n\n const
|
|
1
|
+
{"version":3,"file":"useFrameLoop.js","names":[],"sources":["../../src/canvas/useFrameLoop.ts"],"sourcesContent":["/**\n * The frame loop behind `<Canvas>`: a dirty flag, one frame in flight at a\n * time, and a subscriber set notified after a paint lands. When frames may run\n * at all is `useVisibleRaf`'s question, not this hook's — a redraw requested\n * while nothing can see the surface is held there and re-armed on resume.\n *\n * @internal Not consumer surface. Canvas exposes `requestRedraw` and\n * `subscribeFrame` on its ref handle; this is how they are implemented.\n */\n\nimport { useCallback, useInsertionEffect, useLayoutEffect, useRef } from 'react';\nimport { useLatest, useVisibleRaf, type VisibleRafTarget } from '@weasel-js/react';\n\nexport interface FrameLoop {\n /** Mark the surface dirty and schedule a frame. Identity is stable for the\n * lifetime of the component — consumers capture it. */\n requestRedraw(): void;\n /** `requestRedraw`, painting in the caller's stack as `syncPaint` does whatever that is set to. */\n paintNow(): void;\n /** Run `fn` after every landed paint, with the frame's time. Returns an\n * unsubscribe. */\n subscribeFrame(fn: (time: number) => void): () => void;\n /** Run `fn` on every frame just before its paint, with the frame's time.\n * Returns an unsubscribe. */\n subscribeBeforePaint(fn: (time: number) => void): () => void;\n /** Run `fn` when the loop leaves suspension, before the frame that follows.\n * Returns an unsubscribe. */\n subscribeResume(fn: () => void): () => void;\n}\n\nexport interface FrameLoopOptions {\n /** Paint in the caller's own stack — the commit or event that asked for the\n * redraw — instead of on the next animation frame. Read live, so toggling\n * it takes effect from the next `requestRedraw` on. */\n syncPaint?: boolean;\n /** The surface being painted. Given one, the loop also stops while that\n * element sits outside the viewport, not only while the tab is hidden. */\n target?: VisibleRafTarget;\n}\n\n/**\n * @param paint Runs on the frame; returns whether pixels landed. A `false`\n * leaves the surface dirty and notifies nobody, so the next request retries.\n */\nexport function useFrameLoop(paint: () => boolean, options: FrameLoopOptions = {}): FrameLoop {\n const dirtyRef = useRef(true);\n const aliveRef = useRef(true);\n const paintingRef = useRef(false);\n const paintRef = useLatest(paint);\n const syncRef = useLatest(options.syncPaint ?? false);\n const subsRef = useRef<Set<(time: number) => void>>(new Set());\n const beforeRef = useRef<Set<(time: number) => void>>(new Set());\n const resumeRef = useRef<Set<() => void>>(new Set());\n /** The last time handed to subscribers. A sync paint reads the clock at the\n * call and a rAF frame carries its frame's start, so within one animation\n * frame the second can be the earlier of the two. */\n const lastTimeRef = useRef(-Infinity);\n\n /** Set while recovering from a paint that threw, so one bad frame is retried\n * and a permanently throwing one still doesn't spin at frame rate. */\n const retriedRef = useRef(false);\n /** `frame.request`, which doesn't exist until after `runPaint` is built. */\n const rearmRef = useRef<(() => void) | null>(null);\n\n const runPaint = useCallback((at: number) => {\n const time = Math.max(at, lastTimeRef.current);\n lastTimeRef.current = time;\n dirtyRef.current = false;\n paintingRef.current = true;\n let landed = false;\n try {\n for (const fn of beforeRef.current) fn(time);\n landed = paintRef.current();\n if (!landed) {\n dirtyRef.current = true;\n return;\n }\n retriedRef.current = false;\n // Notified inside the guard: a subscriber calling `requestRedraw` under\n // `syncPaint` would otherwise re-enter this synchronously, without bound.\n for (const fn of subsRef.current) fn(time);\n } catch (err) {\n // A throw skips the `!landed` branch above, so without this the surface\n // is left clean but unpainted — the pixels on screen are whatever the\n // last landed frame left, and nothing repaints until something\n // unrelated happens to request a redraw. That is how a single\n // malformed node blanked a whole document until the pointer moved.\n dirtyRef.current = true;\n if (!retriedRef.current) {\n retriedRef.current = true;\n rearmRef.current?.();\n }\n throw err;\n } finally {\n paintingRef.current = false;\n }\n }, [paintRef]);\n\n const frame = useVisibleRaf(\n useCallback((time: number) => {\n if (!aliveRef.current || !dirtyRef.current) return;\n runPaint(time);\n }, [runPaint]),\n {\n target: options.target,\n onResume: useCallback(() => { for (const fn of resumeRef.current) fn(); }, []),\n },\n );\n useInsertionEffect(() => {\n rearmRef.current = () => { frame.request(); };\n });\n\n const redraw = useCallback((inPlace: boolean) => {\n dirtyRef.current = true;\n // The gate suppresses the sync path too: a background tab still commits\n // React updates, and that is the one paint browser throttling does not stop.\n // A request made from inside a draw or a frame subscriber would recurse\n // forever if it painted here, so re-entrant ones fall through to a frame.\n if (inPlace && aliveRef.current && !paintingRef.current && frame.isVisible()) {\n runPaint(performance.now());\n return;\n }\n frame.request();\n }, [frame, runPaint]);\n const requestRedraw = useCallback(() => redraw(syncRef.current), [redraw, syncRef]);\n const paintNow = useCallback(() => redraw(true), [redraw]);\n\n const subscribeFrame = useCallback((fn: (time: number) => void) => {\n subsRef.current.add(fn);\n return () => { subsRef.current.delete(fn); };\n }, []);\n const subscribeBeforePaint = useCallback((fn: (time: number) => void) => {\n beforeRef.current.add(fn);\n return () => { beforeRef.current.delete(fn); };\n }, []);\n\n const subscribeResume = useCallback((fn: () => void) => {\n resumeRef.current.add(fn);\n return () => { resumeRef.current.delete(fn); };\n }, []);\n\n // `requestRedraw` outlives the component: `@weasel-js/hud` calls it from a\n // `.then()`, and `viewRegistry.attachSurface` hands it to the consumer.\n // Re-armed in setup because refs survive StrictMode's simulated remount, and\n // a layout effect so the arming precedes Canvas's own layout effects — the\n // sync-paint path runs in one of those.\n useLayoutEffect(() => {\n aliveRef.current = true;\n const subs = subsRef.current;\n const before = beforeRef.current;\n const resume = resumeRef.current;\n return () => {\n aliveRef.current = false;\n subs.clear();\n before.clear();\n resume.clear();\n };\n }, []);\n\n return { requestRedraw, paintNow, subscribeFrame, subscribeBeforePaint, subscribeResume };\n}\n"],"mappings":"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"}
|
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
import { useElementSize } from "../core/viewport/useCanvasSize.js";
|
|
2
|
-
import {
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
3
|
import { useLatest } from "@weasel-js/react";
|
|
4
4
|
|
|
5
5
|
//#region src/canvas/useSurfaceSize.ts
|
|
6
6
|
var NO_ELEMENT = { current: null };
|
|
7
|
+
var FILL = { beforePaint: true };
|
|
7
8
|
/**
|
|
8
9
|
* The CSS-pixel size a canvas host paints at: its `width` and `height`, with
|
|
9
10
|
* an axis left out measured off `element`, which fills its parent on it.
|
|
10
11
|
*/
|
|
11
12
|
function useSurfaceSize(props, element) {
|
|
12
13
|
const fills = props.width === void 0 || props.height === void 0;
|
|
13
|
-
const measured = useElementSize(fills ? element : NO_ELEMENT);
|
|
14
|
+
const measured = useElementSize(fills ? element : NO_ELEMENT, FILL);
|
|
14
15
|
const width = props.width ?? measured.width;
|
|
15
16
|
const height = props.height ?? measured.height;
|
|
16
17
|
const onResize = useLatest(props.onResize);
|
|
17
18
|
const reported = useRef(null);
|
|
18
|
-
|
|
19
|
+
useEffect(() => {
|
|
19
20
|
if (width === 0 || height === 0) return;
|
|
20
21
|
const was = reported.current;
|
|
21
22
|
if (was && was.width === width && was.height === height) return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useSurfaceSize.js","names":[],"sources":["../../src/canvas/useSurfaceSize.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"useSurfaceSize.js","names":[],"sources":["../../src/canvas/useSurfaceSize.ts"],"sourcesContent":["import { useEffect, useRef, type RefObject } from 'react';\nimport { useLatest } from '@weasel-js/react';\nimport type { CanvasSize } from 'core/viewport/clampView';\nimport { useElementSize } from 'core/viewport/useCanvasSize';\n\n/** The size props every canvas host takes. */\nexport interface SurfaceSizeProps {\n /** CSS-pixel width. Omit it, or `height`, and the element fills its parent\n * on that axis and is measured: the parent's box decides the size, and the\n * canvas adds nothing to it, so the parent needs a size of its own. Nothing\n * paints until the measurement lands. */\n width?: number;\n /** CSS-pixel height. Omit it to fill, as `width`. */\n height?: number;\n /** Every size the surface takes, once at mount and again on each change —\n * the measured one for an axis left to fill. */\n onResize?: (size: CanvasSize) => void;\n}\n\nconst NO_ELEMENT: RefObject<HTMLElement | null> = { current: null };\nconst FILL = { beforePaint: true };\n\n/**\n * The CSS-pixel size a canvas host paints at: its `width` and `height`, with\n * an axis left out measured off `element`, which fills its parent on it.\n */\nexport function useSurfaceSize(props: SurfaceSizeProps, element: RefObject<HTMLElement | null>): CanvasSize {\n const fills = props.width === undefined || props.height === undefined;\n // The canvas element is all a filling host lays out, so it can take its size inside the resize callback.\n const measured = useElementSize(fills ? element : NO_ELEMENT, FILL);\n const width = props.width ?? measured.width;\n const height = props.height ?? measured.height;\n\n const onResize = useLatest(props.onResize);\n const reported = useRef<CanvasSize | null>(null);\n // Passive, so a consumer's state set from it renders after the resize callback rather than inside it.\n useEffect(() => {\n // A filling axis reads 0 until measured, and that is no size to report.\n if (width === 0 || height === 0) return;\n const was = reported.current;\n if (was && was.width === width && was.height === height) return;\n reported.current = { width, height };\n onResize.current?.({ width, height });\n }, [width, height, onResize]);\n\n return { width, height };\n}\n"],"mappings":";;;;;AAmBA,IAAM,aAA4C,EAAE,SAAS,KAAK;AAClE,IAAM,OAAO,EAAE,aAAa,KAAK;;;;;AAMjC,SAAgB,eAAe,OAAyB,SAAoD;CAC1G,MAAM,QAAQ,MAAM,UAAU,UAAa,MAAM,WAAW;CAE5D,MAAM,WAAW,eAAe,QAAQ,UAAU,YAAY,IAAI;CAClE,MAAM,QAAQ,MAAM,SAAS,SAAS;CACtC,MAAM,SAAS,MAAM,UAAU,SAAS;CAExC,MAAM,WAAW,UAAU,MAAM,QAAQ;CACzC,MAAM,WAAW,OAA0B,IAAI;CAE/C,gBAAgB;EAEd,IAAI,UAAU,KAAK,WAAW,GAAG;EACjC,MAAM,MAAM,SAAS;EACrB,IAAI,OAAO,IAAI,UAAU,SAAS,IAAI,WAAW,QAAQ;EACzD,SAAS,UAAU;GAAE;GAAO;EAAO;EACnC,SAAS,UAAU;GAAE;GAAO;EAAO,CAAC;CACtC,GAAG;EAAC;EAAO;EAAQ;CAAQ,CAAC;CAE5B,OAAO;EAAE;EAAO;CAAO;AACzB"}
|
|
@@ -103,23 +103,16 @@ function poseOutline(pose, b) {
|
|
|
103
103
|
};
|
|
104
104
|
}
|
|
105
105
|
/**
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
* - `intersect` — the outline meets the region anywhere: its interior too,
|
|
110
|
-
* unless `filled` says nothing paints it.
|
|
111
|
-
* - `enclosed` — the whole outline lies inside it.
|
|
112
|
-
* - `centers` — the center of `b` lies inside it.
|
|
113
|
-
*
|
|
114
|
-
* `b` only rejects: it encloses the outline and may reach past it, so a node
|
|
115
|
-
* is taken by the outline alone. A node without finite bounds is never taken.
|
|
106
|
+
* What `b` alone decides of {@link regionTakes}, or undefined when the
|
|
107
|
+
* outline has to be asked: a caller that builds the outline's thunks per node
|
|
108
|
+
* asks this first and builds them only for the nodes it leaves open.
|
|
116
109
|
*/
|
|
117
|
-
function
|
|
110
|
+
function regionTakesByBounds(region, mode, b) {
|
|
118
111
|
if (!Number.isFinite(b.x) || !Number.isFinite(b.y) || !Number.isFinite(b.width) || !Number.isFinite(b.height)) return false;
|
|
119
112
|
const ab = region.bounds;
|
|
120
113
|
if (b.x >= ab.x + ab.width || b.x + b.width <= ab.x || b.y >= ab.y + ab.height || b.y + b.height <= ab.y) return false;
|
|
121
114
|
if (mode === "centers") return pointInPolygon(region.coords, b.x + b.width / 2, b.y + b.height / 2);
|
|
122
|
-
|
|
115
|
+
if (mode === "intersect" && region.isRect && boxContainsBox([
|
|
123
116
|
ab.x,
|
|
124
117
|
ab.y,
|
|
125
118
|
ab.x + ab.width,
|
|
@@ -129,13 +122,39 @@ function regionTakes(region, mode, pose, b, outline, filled = () => true) {
|
|
|
129
122
|
b.y,
|
|
130
123
|
b.x + b.width,
|
|
131
124
|
b.y + b.height
|
|
132
|
-
]);
|
|
125
|
+
])) return true;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Whether `region` takes a node under `mode`, given its pose, its bounds `b`
|
|
129
|
+
* from {@link regionBoundsOf}, and its drawn outline on demand.
|
|
130
|
+
*
|
|
131
|
+
* - `intersect` — the outline meets the region anywhere: its interior too,
|
|
132
|
+
* unless `filled` says nothing paints it.
|
|
133
|
+
* - `enclosed` — the whole outline lies inside it.
|
|
134
|
+
* - `centers` — the center of `b` lies inside it.
|
|
135
|
+
*
|
|
136
|
+
* `b` only rejects: it encloses the outline and may reach past it, so a node
|
|
137
|
+
* is taken by the outline alone. A node without finite bounds is never taken.
|
|
138
|
+
*/
|
|
139
|
+
function regionTakes(region, mode, pose, b, outline, filled = () => true) {
|
|
140
|
+
const decided = regionTakesByBounds(region, mode, b);
|
|
141
|
+
if (decided !== void 0) return decided;
|
|
142
|
+
const ab = region.bounds;
|
|
133
143
|
const silhouette = isSilhouettePose(pose);
|
|
134
144
|
if (mode === "enclosed") {
|
|
135
|
-
if (silhouette && !
|
|
145
|
+
if (silhouette && !boxContainsBox([
|
|
146
|
+
ab.x,
|
|
147
|
+
ab.y,
|
|
148
|
+
ab.x + ab.width,
|
|
149
|
+
ab.y + ab.height
|
|
150
|
+
], [
|
|
151
|
+
b.x,
|
|
152
|
+
b.y,
|
|
153
|
+
b.x + b.width,
|
|
154
|
+
b.y + b.height
|
|
155
|
+
])) return false;
|
|
136
156
|
return polygonContainsPath(region.verts, silhouette ? pose : outline());
|
|
137
157
|
}
|
|
138
|
-
if (region.isRect && inBox) return true;
|
|
139
158
|
const drawn = silhouette ? pose : outline();
|
|
140
159
|
if (!filled()) return polygonIntersectsPath(region.verts, drawn, { filled: false });
|
|
141
160
|
if (drawn.kind === "rect" && region.isRect) return drawn.x < ab.x + ab.width && drawn.x + drawn.width > ab.x && drawn.y < ab.y + ab.height && drawn.y + drawn.height > ab.y;
|
|
@@ -143,5 +162,5 @@ function regionTakes(region, mode, pose, b, outline, filled = () => true) {
|
|
|
143
162
|
}
|
|
144
163
|
|
|
145
164
|
//#endregion
|
|
146
|
-
export { isSilhouettePose, polygonRegion, poseOutline, rectRegion, regionBoundsOf, regionOf, regionTakes };
|
|
165
|
+
export { isSilhouettePose, polygonRegion, poseOutline, rectRegion, regionBoundsOf, regionOf, regionTakes, regionTakesByBounds };
|
|
147
166
|
//# sourceMappingURL=regionHit.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"regionHit.js","names":[],"sources":["../../../src/core/geometry/regionHit.ts"],"sourcesContent":["/**\n * Whether a marquee or a lasso takes one node: the per-node decision the\n * scene's area walk (`canvas/deps/hitTestArea`) makes.\n *\n * What a node draws is not knowable here — the painters sit above `core/` — so\n * the caller hands in the node's outline as a thunk, asked for only when the\n * bounds cannot decide. A pose that is itself a polygon path is its own\n * outline and never asks.\n */\nimport {\n boxContainsBox,\n pointInPolygon,\n polygonContainsPath,\n polygonIntersectsPath,\n type Path,\n type PolygonPath,\n} from '@weasel-js/geom';\nimport type { Bounds } from 'core/viewport/fitViewToBounds';\nimport type { LassoHitMode } from 'core/adapters/types';\nimport { nodeMemo } from 'core/scene/nodeMemo';\nimport type { Vec2 } from './vec2';\nimport { pathThroughPoseFrame, poseFrameOf } from '@weasel-js/geom';\nimport { visualBoundsViaDescriptor, type PoseDescriptor } from './poseDescriptor';\n\n/** A closed selection area: a marquee rect or a lasso polygon. */\nexport interface SelectRegion {\n /** The area's vertices; the closing edge back to the first is implicit. */\n readonly verts: readonly Vec2[];\n /** `verts` interleaved as `[x0, y0, x1, y1, …]`. */\n readonly coords: readonly number[];\n readonly bounds: Bounds;\n /** True when the area is its own bounding rect, so a node inside `bounds`\n * is inside the area without further work. Never true for a lasso: a node\n * inside the hull's box can still miss the hull. */\n readonly isRect: boolean;\n}\n\n/** A marquee's region. */\nexport function rectRegion(r: Bounds): SelectRegion {\n const { x, y, width: w, height: h } = r;\n return regionOf([x, y, x + w, y, x + w, y + h, x, y + h], { x, y, width: w, height: h }, true)!;\n}\n\n/** A region from interleaved coords. `bounds` is derived when omitted; `null`\n * when there are no coords to bound. */\nexport function regionOf(\n coords: ArrayLike<number>,\n bounds?: Bounds,\n isRect = false,\n): SelectRegion | null {\n const verts: Vec2[] = [];\n const flat: number[] = [];\n let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;\n for (let i = 0; i + 1 < coords.length; i += 2) {\n const x = coords[i], y = coords[i + 1];\n verts.push({ x, y });\n flat.push(x, y);\n if (x < minX) minX = x;\n if (y < minY) minY = y;\n if (x > maxX) maxX = x;\n if (y > maxY) maxY = y;\n }\n if (verts.length === 0) return null;\n return {\n verts,\n coords: flat,\n bounds: bounds ?? { x: minX, y: minY, width: maxX - minX, height: maxY - minY },\n isRect,\n };\n}\n\n/** A lasso's region, or `null` for fewer than three vertices. */\nexport function polygonRegion(polygon: ReadonlyArray<Vec2>): SelectRegion | null {\n if (polygon.length < 3) return null;\n const coords: number[] = [];\n for (const p of polygon) coords.push(p.x, p.y);\n return regionOf(coords);\n}\n\n/** A pose that is its own drawn outline. */\nexport function isSilhouettePose(pose: unknown): pose is PolygonPath {\n return pose !== null && typeof pose === 'object' && (pose as Path).kind === 'polygon';\n}\n\n/**\n * The box a region fast-rejects a node against: a silhouette pose's own bounds,\n * else the pose's visual bounds — which for a rotated pose is the box of its\n * rotated ink, reaching past the pose box. `g` is the node's descriptor.\n */\nexport function regionBoundsOf(node: object, pose: unknown, g: PoseDescriptor<unknown>): Bounds {\n // Memo is silhouettes-only: a rect pose's bounds are the pose itself, and\n // memoizing it costs more than it saves. The result may be shared; never mutate it.\n return isSilhouettePose(pose)\n ? nodeMemo(node, 'aabb', pose, () => g.getBounds(pose))\n : visualBoundsViaDescriptor(pose, g);\n}\n\n/** The outline of a node nothing paints: its pose rect through the pose's\n * frame, else its visual bounds `b`. */\nexport function poseOutline(pose: unknown, b: Bounds): Path {\n if (poseFrameOf(pose)) {\n const p = pose as Bounds;\n return pathThroughPoseFrame({ kind: 'rect', x: p.x, y: p.y, width: p.width, height: p.height }, pose);\n }\n return { kind: 'rect', x: b.x, y: b.y, width: b.width, height: b.height };\n}\n\n/**\n *
|
|
1
|
+
{"version":3,"file":"regionHit.js","names":[],"sources":["../../../src/core/geometry/regionHit.ts"],"sourcesContent":["/**\n * Whether a marquee or a lasso takes one node: the per-node decision the\n * scene's area walk (`canvas/deps/hitTestArea`) makes.\n *\n * What a node draws is not knowable here — the painters sit above `core/` — so\n * the caller hands in the node's outline as a thunk, asked for only when the\n * bounds cannot decide. A pose that is itself a polygon path is its own\n * outline and never asks.\n */\nimport {\n boxContainsBox,\n pointInPolygon,\n polygonContainsPath,\n polygonIntersectsPath,\n type Path,\n type PolygonPath,\n} from '@weasel-js/geom';\nimport type { Bounds } from 'core/viewport/fitViewToBounds';\nimport type { LassoHitMode } from 'core/adapters/types';\nimport { nodeMemo } from 'core/scene/nodeMemo';\nimport type { Vec2 } from './vec2';\nimport { pathThroughPoseFrame, poseFrameOf } from '@weasel-js/geom';\nimport { visualBoundsViaDescriptor, type PoseDescriptor } from './poseDescriptor';\n\n/** A closed selection area: a marquee rect or a lasso polygon. */\nexport interface SelectRegion {\n /** The area's vertices; the closing edge back to the first is implicit. */\n readonly verts: readonly Vec2[];\n /** `verts` interleaved as `[x0, y0, x1, y1, …]`. */\n readonly coords: readonly number[];\n readonly bounds: Bounds;\n /** True when the area is its own bounding rect, so a node inside `bounds`\n * is inside the area without further work. Never true for a lasso: a node\n * inside the hull's box can still miss the hull. */\n readonly isRect: boolean;\n}\n\n/** A marquee's region. */\nexport function rectRegion(r: Bounds): SelectRegion {\n const { x, y, width: w, height: h } = r;\n return regionOf([x, y, x + w, y, x + w, y + h, x, y + h], { x, y, width: w, height: h }, true)!;\n}\n\n/** A region from interleaved coords. `bounds` is derived when omitted; `null`\n * when there are no coords to bound. */\nexport function regionOf(\n coords: ArrayLike<number>,\n bounds?: Bounds,\n isRect = false,\n): SelectRegion | null {\n const verts: Vec2[] = [];\n const flat: number[] = [];\n let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;\n for (let i = 0; i + 1 < coords.length; i += 2) {\n const x = coords[i], y = coords[i + 1];\n verts.push({ x, y });\n flat.push(x, y);\n if (x < minX) minX = x;\n if (y < minY) minY = y;\n if (x > maxX) maxX = x;\n if (y > maxY) maxY = y;\n }\n if (verts.length === 0) return null;\n return {\n verts,\n coords: flat,\n bounds: bounds ?? { x: minX, y: minY, width: maxX - minX, height: maxY - minY },\n isRect,\n };\n}\n\n/** A lasso's region, or `null` for fewer than three vertices. */\nexport function polygonRegion(polygon: ReadonlyArray<Vec2>): SelectRegion | null {\n if (polygon.length < 3) return null;\n const coords: number[] = [];\n for (const p of polygon) coords.push(p.x, p.y);\n return regionOf(coords);\n}\n\n/** A pose that is its own drawn outline. */\nexport function isSilhouettePose(pose: unknown): pose is PolygonPath {\n return pose !== null && typeof pose === 'object' && (pose as Path).kind === 'polygon';\n}\n\n/**\n * The box a region fast-rejects a node against: a silhouette pose's own bounds,\n * else the pose's visual bounds — which for a rotated pose is the box of its\n * rotated ink, reaching past the pose box. `g` is the node's descriptor.\n */\nexport function regionBoundsOf(node: object, pose: unknown, g: PoseDescriptor<unknown>): Bounds {\n // Memo is silhouettes-only: a rect pose's bounds are the pose itself, and\n // memoizing it costs more than it saves. The result may be shared; never mutate it.\n return isSilhouettePose(pose)\n ? nodeMemo(node, 'aabb', pose, () => g.getBounds(pose))\n : visualBoundsViaDescriptor(pose, g);\n}\n\n/** The outline of a node nothing paints: its pose rect through the pose's\n * frame, else its visual bounds `b`. */\nexport function poseOutline(pose: unknown, b: Bounds): Path {\n if (poseFrameOf(pose)) {\n const p = pose as Bounds;\n return pathThroughPoseFrame({ kind: 'rect', x: p.x, y: p.y, width: p.width, height: p.height }, pose);\n }\n return { kind: 'rect', x: b.x, y: b.y, width: b.width, height: b.height };\n}\n\n/**\n * What `b` alone decides of {@link regionTakes}, or undefined when the\n * outline has to be asked: a caller that builds the outline's thunks per node\n * asks this first and builds them only for the nodes it leaves open.\n */\nexport function regionTakesByBounds(region: SelectRegion, mode: LassoHitMode, b: Bounds): boolean | undefined {\n if (\n !Number.isFinite(b.x) || !Number.isFinite(b.y) ||\n !Number.isFinite(b.width) || !Number.isFinite(b.height)\n ) {\n return false;\n }\n const ab = region.bounds;\n if (\n b.x >= ab.x + ab.width || b.x + b.width <= ab.x ||\n b.y >= ab.y + ab.height || b.y + b.height <= ab.y\n ) {\n return false;\n }\n if (mode === 'centers') {\n return pointInPolygon(region.coords, b.x + b.width / 2, b.y + b.height / 2);\n }\n // Swallowed whole by a marquee: no outline can change the answer.\n if (mode === 'intersect' && region.isRect && boxContainsBox(\n [ab.x, ab.y, ab.x + ab.width, ab.y + ab.height],\n [b.x, b.y, b.x + b.width, b.y + b.height],\n )) return true;\n return undefined;\n}\n\n/**\n * Whether `region` takes a node under `mode`, given its pose, its bounds `b`\n * from {@link regionBoundsOf}, and its drawn outline on demand.\n *\n * - `intersect` — the outline meets the region anywhere: its interior too,\n * unless `filled` says nothing paints it.\n * - `enclosed` — the whole outline lies inside it.\n * - `centers` — the center of `b` lies inside it.\n *\n * `b` only rejects: it encloses the outline and may reach past it, so a node\n * is taken by the outline alone. A node without finite bounds is never taken.\n */\nexport function regionTakes(\n region: SelectRegion,\n mode: LassoHitMode,\n pose: unknown,\n b: Bounds,\n outline: () => Path,\n filled: () => boolean = () => true,\n): boolean {\n const decided = regionTakesByBounds(region, mode, b);\n if (decided !== undefined) return decided;\n const ab = region.bounds;\n const silhouette = isSilhouettePose(pose);\n if (mode === 'enclosed') {\n // A silhouette pose's bounds are its outline's: sticking out of the\n // region's box, it cannot be inside the region.\n if (silhouette && !boxContainsBox(\n [ab.x, ab.y, ab.x + ab.width, ab.y + ab.height],\n [b.x, b.y, b.x + b.width, b.y + b.height],\n )) return false;\n return polygonContainsPath(region.verts, silhouette ? pose : outline());\n }\n const drawn = silhouette ? pose : outline();\n if (!filled()) return polygonIntersectsPath(region.verts, drawn, { filled: false });\n // Two axis-aligned rects meet exactly when their boxes overlap.\n if (drawn.kind === 'rect' && region.isRect) {\n return (\n drawn.x < ab.x + ab.width && drawn.x + drawn.width > ab.x &&\n drawn.y < ab.y + ab.height && drawn.y + drawn.height > ab.y\n );\n }\n return polygonIntersectsPath(region.verts, drawn);\n}\n"],"mappings":"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"}
|
|
@@ -6,16 +6,19 @@ function createEditMode() {
|
|
|
6
6
|
untracked: 0,
|
|
7
7
|
own: 0
|
|
8
8
|
};
|
|
9
|
+
let open = 0;
|
|
9
10
|
return {
|
|
10
|
-
open: () =>
|
|
11
|
+
open: () => open > 0,
|
|
11
12
|
records: () => depth.engine + depth.untracked === 0,
|
|
12
13
|
untracked: () => depth.untracked > 0,
|
|
13
14
|
during(driver, fn) {
|
|
14
15
|
depth[driver]++;
|
|
16
|
+
open++;
|
|
15
17
|
try {
|
|
16
18
|
return fn();
|
|
17
19
|
} finally {
|
|
18
20
|
depth[driver]--;
|
|
21
|
+
open--;
|
|
19
22
|
}
|
|
20
23
|
}
|
|
21
24
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"editMode.js","names":[],"sources":["../../../src/core/scene/editMode.ts"],"sourcesContent":["/**\n * Who is driving the scene's mutations right now.\n *\n * A bare call records itself. Inside a `batch` the batch records it; while a\n * history engine applies or replays ops the engine's entry already holds it;\n * inside `untracked` nothing does; and `own` is the scene handing a bare call's\n * op to its engine, which records it.\n */\nexport type EditDriver = 'batch' | 'engine' | 'untracked' | 'own';\n\nexport interface EditMode {\n /** Whether any driver is running, so a scene call is part of its edit\n * rather than an edit of its own. */\n open(): boolean;\n /** Whether a scene call is recorded by the scene: false under an engine\n * or an untracked run, however deep. */\n records(): boolean;\n untracked(): boolean;\n /** Run `fn` under `driver`. */\n during<T>(driver: EditDriver, fn: () => T): T;\n}\n\nexport function createEditMode(): EditMode {\n const depth: Record<EditDriver, number> = { batch: 0, engine: 0, untracked: 0, own: 0 };\n\n return {\n open: () =>
|
|
1
|
+
{"version":3,"file":"editMode.js","names":[],"sources":["../../../src/core/scene/editMode.ts"],"sourcesContent":["/**\n * Who is driving the scene's mutations right now.\n *\n * A bare call records itself. Inside a `batch` the batch records it; while a\n * history engine applies or replays ops the engine's entry already holds it;\n * inside `untracked` nothing does; and `own` is the scene handing a bare call's\n * op to its engine, which records it.\n */\nexport type EditDriver = 'batch' | 'engine' | 'untracked' | 'own';\n\nexport interface EditMode {\n /** Whether any driver is running, so a scene call is part of its edit\n * rather than an edit of its own. */\n open(): boolean;\n /** Whether a scene call is recorded by the scene: false under an engine\n * or an untracked run, however deep. */\n records(): boolean;\n untracked(): boolean;\n /** Run `fn` under `driver`. */\n during<T>(driver: EditDriver, fn: () => T): T;\n}\n\nexport function createEditMode(): EditMode {\n const depth: Record<EditDriver, number> = { batch: 0, engine: 0, untracked: 0, own: 0 };\n let open = 0;\n\n return {\n open: () => open > 0,\n records: () => depth.engine + depth.untracked === 0,\n untracked: () => depth.untracked > 0,\n during(driver, fn) {\n depth[driver]++;\n open++;\n try {\n return fn();\n } finally {\n depth[driver]--;\n open--;\n }\n },\n };\n}\n"],"mappings":";AAsBA,SAAgB,iBAA2B;CACzC,MAAM,QAAoC;EAAE,OAAO;EAAG,QAAQ;EAAG,WAAW;EAAG,KAAK;CAAE;CACtF,IAAI,OAAO;CAEX,OAAO;EACL,YAAY,OAAO;EACnB,eAAe,MAAM,SAAS,MAAM,cAAc;EAClD,iBAAiB,MAAM,YAAY;EACnC,OAAO,QAAQ,IAAI;GACjB,MAAM,OAAO;GACb;GACA,IAAI;IACF,OAAO,GAAG;GACZ,UAAU;IACR,MAAM,OAAO;IACb;GACF;EACF;CACF;AACF"}
|
|
@@ -1,51 +1,11 @@
|
|
|
1
1
|
import { dwarn } from "../../debug/flag.js";
|
|
2
2
|
import { SCENE_HISTORY_FORM } from "./historyForm.js";
|
|
3
|
+
import { createSceneJournals } from "./sceneJournals.js";
|
|
3
4
|
|
|
4
5
|
//#region src/core/scene/historyFacade.ts
|
|
5
|
-
/** A journal drives adapter mutation itself, so its applies and undos
|
|
6
|
-
* re-enter scene mutation methods the way the facade's do. */
|
|
7
|
-
function wrapJournal(raw, env) {
|
|
8
|
-
const { replay, notify } = env;
|
|
9
|
-
return {
|
|
10
|
-
targetId: raw.targetId,
|
|
11
|
-
forkedAtEntryId: raw.forkedAtEntryId,
|
|
12
|
-
applyBatch(ops, label) {
|
|
13
|
-
env.labeled(label, () => env.runArrivalEdit(ops, (all) => {
|
|
14
|
-
env.applying(() => raw.applyBatch(all.map(env.causing), label));
|
|
15
|
-
notify();
|
|
16
|
-
}));
|
|
17
|
-
},
|
|
18
|
-
undo() {
|
|
19
|
-
replay(() => raw.undo());
|
|
20
|
-
notify();
|
|
21
|
-
},
|
|
22
|
-
redo() {
|
|
23
|
-
replay(() => raw.redo());
|
|
24
|
-
notify();
|
|
25
|
-
},
|
|
26
|
-
canUndo: () => raw.canUndo(),
|
|
27
|
-
canRedo: () => raw.canRedo(),
|
|
28
|
-
entries: () => raw.entries(),
|
|
29
|
-
seal: () => raw.seal(),
|
|
30
|
-
getVersion: () => raw.getVersion(),
|
|
31
|
-
subscribe: (listener) => raw.subscribe(listener),
|
|
32
|
-
commit(label) {
|
|
33
|
-
raw.commit(label);
|
|
34
|
-
notify();
|
|
35
|
-
},
|
|
36
|
-
cancel() {
|
|
37
|
-
replay(() => raw.cancel());
|
|
38
|
-
notify();
|
|
39
|
-
},
|
|
40
|
-
suspend: () => raw.suspend(),
|
|
41
|
-
isActive: () => raw.isActive()
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
6
|
function createSceneHistory(env) {
|
|
45
7
|
const { engine, replay, notify, refuseUntracked, runArrivalEdit } = env;
|
|
46
|
-
|
|
47
|
-
* resumability off journal identity, which a wrapper would not match. */
|
|
48
|
-
const rawJournals = /* @__PURE__ */ new WeakMap();
|
|
8
|
+
const journals = createSceneJournals(env);
|
|
49
9
|
function step(can, run) {
|
|
50
10
|
if (!can()) return false;
|
|
51
11
|
replay(run);
|
|
@@ -159,15 +119,12 @@ function createSceneHistory(env) {
|
|
|
159
119
|
allForwardOps: () => engine.allForwardOps(),
|
|
160
120
|
seal: () => engine.seal(),
|
|
161
121
|
currentEntryId: () => engine.currentEntryId(),
|
|
162
|
-
beginJournal
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
rawJournals.set(wrapped, raw);
|
|
166
|
-
return wrapped;
|
|
167
|
-
},
|
|
168
|
-
resumeJournal: (journal) => engine.resumeJournal(rawJournals.get(journal) ?? journal)
|
|
122
|
+
beginJournal: journals.begin,
|
|
123
|
+
resumeJournal: journals.resume,
|
|
124
|
+
activeJournal: () => env.activeJournal()
|
|
169
125
|
},
|
|
170
|
-
methods
|
|
126
|
+
methods,
|
|
127
|
+
openJournal: journals.innermost
|
|
171
128
|
};
|
|
172
129
|
}
|
|
173
130
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"historyFacade.js","names":[],"sources":["../../../src/core/scene/historyFacade.ts"],"sourcesContent":["/**\n * `scene.history`: the `History` the kit's `undo`/`redo` actions resolve\n * their `history` dep to, and the handle a mode machine opens journals\n * against.\n *\n * Handing out the engine directly would let an action-driven undo replay ops\n * with scene-side recording still armed and without bumping the scene's\n * version, so every member that mutates routes through the scene's own\n * wrappers and one `notify()`; reads forward to the engine.\n */\nimport type { History, Journal } from '@weasel-js/history';\nimport type { Op } from 'core/ops/types';\nimport { dwarn } from 'debug/flag';\nimport { SCENE_HISTORY_FORM, type SerializedSceneHistory } from './historyForm';\nimport type { Scene } from './types';\n\n/** What the facade needs from the scene it fronts. */\nexport interface SceneHistoryEnv {\n /** The scene's own undo/redo engine. */\n engine: History;\n /** Run a step that applies ops through an engine: all-or-nothing, with\n * scene-side recording suppressed. */\n applying(step: () => void): void;\n /** An op as the engine applies it: its entry records the kit ops it\n * caused. */\n causing(op: Op): Op;\n /** Run a step that replays recorded ops: an undo, a redo, a cancel. */\n replay(step: () => void): void;\n notify(): void;\n refuseUntracked(verb: string): void;\n /** Run an op-driven edit with its arrivals settled into the same batch. */\n runArrivalEdit(ops: Op[], run: (ops: Op[]) => void): void;\n /** Run an edit applying ops, labeled `label` for the pose writes it reports. */\n labeled<T>(label: string, fn: () => T): T;\n /** The journal `applyBatch` writes to now, or null. Undo and redo step it\n * instead of the engine, so they take back the edits that went there. */\n activeJournal(): Journal | null;\n}\n\n/** The scene's own spellings of its history, which answer what a caller\n * outside the engine's vocabulary asks: whether a step moved anything. */\nexport type SceneHistoryMethods = Pick<\n Scene<unknown, string, unknown>,\n | 'undo' | 'redo' | 'canUndo' | 'canRedo' | 'historyEntries' | 'historyIndex'\n | 'jumpToHistoryIndex' | 'serializeHistory' | 'restoreHistory'\n>;\n\n/** A journal drives adapter mutation itself, so its applies and undos\n * re-enter scene mutation methods the way the facade's do. */\nfunction wrapJournal(raw: Journal, env: SceneHistoryEnv): Journal {\n const { replay, notify } = env;\n return {\n targetId: raw.targetId,\n forkedAtEntryId: raw.forkedAtEntryId,\n applyBatch(ops, label) {\n env.labeled(label, () => env.runArrivalEdit(ops, (all) => {\n env.applying(() => raw.applyBatch(all.map(env.causing), label));\n notify();\n }));\n },\n undo() {\n replay(() => raw.undo());\n notify();\n },\n redo() {\n replay(() => raw.redo());\n notify();\n },\n canUndo: () => raw.canUndo(),\n canRedo: () => raw.canRedo(),\n entries: () => raw.entries(),\n seal: () => raw.seal(),\n getVersion: () => raw.getVersion(),\n subscribe: (listener) => raw.subscribe(listener),\n commit(label) {\n // Flushes the session's net ops to the parent stack — no apply, but\n // the parent's undo depth changes and listeners read that.\n raw.commit(label);\n notify();\n },\n cancel() {\n replay(() => raw.cancel());\n notify();\n },\n suspend: () => raw.suspend(),\n isActive: () => raw.isActive(),\n };\n}\n\nexport function createSceneHistory(env: SceneHistoryEnv): { history: History; methods: SceneHistoryMethods } {\n const { engine, replay, notify, refuseUntracked, runArrivalEdit } = env;\n\n /** Wrapped journal → the engine's own, for `resumeJournal`: the engine keys\n * resumability off journal identity, which a wrapper would not match. */\n const rawJournals = new WeakMap<Journal, Journal>();\n\n function step(can: () => boolean, run: () => void): boolean {\n if (!can()) return false;\n replay(run);\n notify();\n return true;\n }\n\n /** What undo and redo step: the open journal, else the engine. */\n const stack = (): Pick<History, 'canUndo' | 'canRedo' | 'undo' | 'redo'> => env.activeJournal() ?? engine;\n\n const methods: SceneHistoryMethods = {\n undo() {\n const j = env.activeJournal();\n if (j) { if (!j.canUndo()) return false; j.undo(); return true; }\n return step(() => engine.canUndo(), () => engine.undo());\n },\n redo() {\n const j = env.activeJournal();\n if (j) { if (!j.canRedo()) return false; j.redo(); return true; }\n return step(() => engine.canRedo(), () => engine.redo());\n },\n canUndo: () => stack().canUndo(),\n canRedo: () => stack().canRedo(),\n historyEntries() {\n const { undo, redo } = engine.entries();\n // entries().redo is already chronological: the next redo comes first.\n return [...undo, ...redo].map((e) => ({ id: String(e.id), label: e.label }));\n },\n historyIndex: () => engine.undoDepth(),\n jumpToHistoryIndex(index) {\n const total = engine.undoDepth() + engine.redoDepth();\n const target = Math.max(0, Math.min(total, index));\n return step(() => target !== engine.undoDepth(), () => engine.goto(target));\n },\n serializeHistory: () => ({ ...engine.serialize(), form: SCENE_HISTORY_FORM }),\n restoreHistory(snapshot) {\n if ((snapshot as Partial<SerializedSceneHistory>).form !== SCENE_HISTORY_FORM) {\n dwarn('scene', 'restoreHistory: snapshot is not in the current recorded form — dropped');\n return;\n }\n // Only the stacks are rebuilt: no op applies.\n engine.restore(snapshot);\n notify();\n },\n };\n\n const history: History = {\n // Mutating: applied ops re-enter the scene's own mutation methods.\n apply(op, label) {\n refuseUntracked('history.apply');\n env.labeled(label ?? op.label ?? '', () => runArrivalEdit([op], (all) => {\n const run = () => (all.length === 1\n ? engine.apply(env.causing(op), label)\n : engine.applyOps(all.map(env.causing), label ?? op.label ?? ''));\n env.applying(run);\n notify();\n }));\n },\n applyOps(ops, label) {\n refuseUntracked('history.applyOps');\n env.labeled(label, () => runArrivalEdit(ops, (all) => {\n env.applying(() => engine.applyOps(all.map(env.causing), label));\n notify();\n }));\n },\n undo: () => { methods.undo(); },\n redo: () => { methods.redo(); },\n goto: (n) => { methods.jumpToHistoryIndex(n); },\n // Stack-only: nothing applies, but what `canUndo()` answers changes.\n clear() {\n engine.clear();\n notify();\n },\n recordEntry(ops, label, opts) {\n engine.recordEntry(ops, label, opts);\n notify();\n },\n prune(t) {\n engine.prune(t);\n notify();\n },\n switchBranch(id) {\n engine.switchBranch(id);\n notify();\n },\n restore: methods.restoreHistory,\n // Reads forward to the engine, `version` included: it counts history\n // operations, where the scene's counts every mutation.\n canUndo: methods.canUndo,\n canRedo: methods.canRedo,\n undoDepth: () => engine.undoDepth(),\n redoDepth: () => engine.redoDepth(),\n entries: () => engine.entries(),\n timestampAt: (i) => engine.timestampAt(i),\n depthAt: (t) => engine.depthAt(t),\n branches: () => engine.branches(),\n getVersion: () => engine.getVersion(),\n subscribe: (listener) => engine.subscribe(listener),\n observe: (listener) => engine.observe(listener),\n serialize: methods.serializeHistory,\n allForwardOps: () => engine.allForwardOps(),\n seal: () => engine.seal(),\n currentEntryId: () => engine.currentEntryId(),\n beginJournal(opts) {\n const raw = engine.beginJournal(opts);\n const wrapped = wrapJournal(raw, env);\n rawJournals.set(wrapped, raw);\n return wrapped;\n },\n resumeJournal: (journal) => engine.resumeJournal(rawJournals.get(journal) ?? journal),\n };\n return { history, methods };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"historyFacade.js","names":[],"sources":["../../../src/core/scene/historyFacade.ts"],"sourcesContent":["/**\n * `scene.history`: the `History` the kit's `undo`/`redo` actions resolve\n * their `history` dep to, and the handle a mode machine opens journals\n * against.\n *\n * Handing out the engine directly would let an action-driven undo replay ops\n * with scene-side recording still armed and without bumping the scene's\n * version, so every member that mutates routes through the scene's own\n * wrappers and one `notify()`; reads forward to the engine.\n */\nimport type { History, Journal } from '@weasel-js/history';\nimport type { Op } from 'core/ops/types';\nimport { dwarn } from 'debug/flag';\nimport { SCENE_HISTORY_FORM, type SerializedSceneHistory } from './historyForm';\nimport { createSceneJournals } from './sceneJournals';\nimport type { Scene } from './types';\n\n/** What the facade needs from the scene it fronts. */\nexport interface SceneHistoryEnv {\n /** The scene's own undo/redo engine. */\n engine: History;\n /** Run a step that applies ops through an engine: all-or-nothing, with\n * scene-side recording suppressed. */\n applying(step: () => void): void;\n /** An op as the engine applies it: its entry records the kit ops it\n * caused. */\n causing(op: Op): Op;\n /** Run a step that replays recorded ops: an undo, a redo, a cancel. */\n replay(step: () => void): void;\n notify(): void;\n refuseUntracked(verb: string): void;\n /** Run an op-driven edit with its arrivals settled into the same batch. */\n runArrivalEdit(ops: Op[], run: (ops: Op[]) => void): void;\n /** Run an edit applying ops, labeled `label` for the pose writes it reports. */\n labeled<T>(label: string, fn: () => T): T;\n /** The journal `applyBatch` writes to now, or null. Undo and redo step it\n * instead of the engine, so they take back the edits that went there. */\n activeJournal(): Journal | null;\n}\n\n/** The scene's own spellings of its history, which answer what a caller\n * outside the engine's vocabulary asks: whether a step moved anything. */\nexport type SceneHistoryMethods = Pick<\n Scene<unknown, string, unknown>,\n | 'undo' | 'redo' | 'canUndo' | 'canRedo' | 'historyEntries' | 'historyIndex'\n | 'jumpToHistoryIndex' | 'serializeHistory' | 'restoreHistory'\n>;\n\nexport function createSceneHistory(env: SceneHistoryEnv): {\n history: History;\n methods: SceneHistoryMethods;\n /** The innermost active journal opened through `history`, or null. */\n openJournal(): Journal | null;\n} {\n const { engine, replay, notify, refuseUntracked, runArrivalEdit } = env;\n\n const journals = createSceneJournals(env);\n\n function step(can: () => boolean, run: () => void): boolean {\n if (!can()) return false;\n replay(run);\n notify();\n return true;\n }\n\n /** What undo and redo step: the open journal, else the engine. */\n const stack = (): Pick<History, 'canUndo' | 'canRedo' | 'undo' | 'redo'> => env.activeJournal() ?? engine;\n\n const methods: SceneHistoryMethods = {\n undo() {\n const j = env.activeJournal();\n if (j) { if (!j.canUndo()) return false; j.undo(); return true; }\n return step(() => engine.canUndo(), () => engine.undo());\n },\n redo() {\n const j = env.activeJournal();\n if (j) { if (!j.canRedo()) return false; j.redo(); return true; }\n return step(() => engine.canRedo(), () => engine.redo());\n },\n canUndo: () => stack().canUndo(),\n canRedo: () => stack().canRedo(),\n historyEntries() {\n const { undo, redo } = engine.entries();\n // entries().redo is already chronological: the next redo comes first.\n return [...undo, ...redo].map((e) => ({ id: String(e.id), label: e.label }));\n },\n historyIndex: () => engine.undoDepth(),\n jumpToHistoryIndex(index) {\n const total = engine.undoDepth() + engine.redoDepth();\n const target = Math.max(0, Math.min(total, index));\n return step(() => target !== engine.undoDepth(), () => engine.goto(target));\n },\n serializeHistory: () => ({ ...engine.serialize(), form: SCENE_HISTORY_FORM }),\n restoreHistory(snapshot) {\n if ((snapshot as Partial<SerializedSceneHistory>).form !== SCENE_HISTORY_FORM) {\n dwarn('scene', 'restoreHistory: snapshot is not in the current recorded form — dropped');\n return;\n }\n // Only the stacks are rebuilt: no op applies.\n engine.restore(snapshot);\n notify();\n },\n };\n\n const history: History = {\n // Mutating: applied ops re-enter the scene's own mutation methods.\n apply(op, label) {\n refuseUntracked('history.apply');\n env.labeled(label ?? op.label ?? '', () => runArrivalEdit([op], (all) => {\n const run = () => (all.length === 1\n ? engine.apply(env.causing(op), label)\n : engine.applyOps(all.map(env.causing), label ?? op.label ?? ''));\n env.applying(run);\n notify();\n }));\n },\n applyOps(ops, label) {\n refuseUntracked('history.applyOps');\n env.labeled(label, () => runArrivalEdit(ops, (all) => {\n env.applying(() => engine.applyOps(all.map(env.causing), label));\n notify();\n }));\n },\n undo: () => { methods.undo(); },\n redo: () => { methods.redo(); },\n goto: (n) => { methods.jumpToHistoryIndex(n); },\n // Stack-only: nothing applies, but what `canUndo()` answers changes.\n clear() {\n engine.clear();\n notify();\n },\n recordEntry(ops, label, opts) {\n engine.recordEntry(ops, label, opts);\n notify();\n },\n prune(t) {\n engine.prune(t);\n notify();\n },\n switchBranch(id) {\n engine.switchBranch(id);\n notify();\n },\n restore: methods.restoreHistory,\n // Reads forward to the engine, `version` included: it counts history\n // operations, where the scene's counts every mutation.\n canUndo: methods.canUndo,\n canRedo: methods.canRedo,\n undoDepth: () => engine.undoDepth(),\n redoDepth: () => engine.redoDepth(),\n entries: () => engine.entries(),\n timestampAt: (i) => engine.timestampAt(i),\n depthAt: (t) => engine.depthAt(t),\n branches: () => engine.branches(),\n getVersion: () => engine.getVersion(),\n subscribe: (listener) => engine.subscribe(listener),\n observe: (listener) => engine.observe(listener),\n serialize: methods.serializeHistory,\n allForwardOps: () => engine.allForwardOps(),\n seal: () => engine.seal(),\n currentEntryId: () => engine.currentEntryId(),\n beginJournal: journals.begin,\n resumeJournal: journals.resume,\n activeJournal: () => env.activeJournal(),\n };\n return { history, methods, openJournal: journals.innermost };\n}\n"],"mappings":"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"}
|
|
@@ -53,12 +53,12 @@ function nodeGates(scene) {
|
|
|
53
53
|
const lockedAbove = above(ownLock);
|
|
54
54
|
return {
|
|
55
55
|
hidden(id) {
|
|
56
|
-
const node = scene.get(id);
|
|
56
|
+
const node = typeof id === "string" ? scene.get(id) : id;
|
|
57
57
|
if (node === void 0) return false;
|
|
58
58
|
return hiddenLayers?.has(node.layer) === true || hidesSubtree(node) || hiddenAbove(node.parent ?? null);
|
|
59
59
|
},
|
|
60
60
|
locked(id) {
|
|
61
|
-
const node = scene.get(id);
|
|
61
|
+
const node = typeof id === "string" ? scene.get(id) : id;
|
|
62
62
|
if (node === void 0) return false;
|
|
63
63
|
return ownLock(node) || lockedAbove(node.parent ?? null);
|
|
64
64
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nodeGates.js","names":[],"sources":["../../../src/core/scene/nodeGates.ts"],"sourcesContent":["/**\n * Whether a node is shown, and whether it is in reach — the one place either\n * question is answered. Painting, picking, area selection, select-all,\n * snapping, export, and the scene's own lock guard all ask here.\n *\n * - **Hidden**: its own layer is not visible, or it or any ancestor carries\n * `hidden`. A layer hides only the nodes tagged to it; a hidden node hides\n * its whole subtree, as SVG's `display=\"none\"` does.\n * - **Locked**: it or any ancestor carries `locked`, or sits on a locked\n * layer. A lock covers the whole subtree, whatever layers it spans.\n */\nimport type { NodeId } from './types';\n\n/** The node fields the gates read. A partial scene stand-in may leave `parent` out. */\nexport interface GatedNode {\n readonly layer: string;\n readonly parent?: string | null;\n readonly hidden?: boolean;\n readonly locked?: boolean;\n}\n\n/** The layer fields the gates read. Only an explicit `visible: false` hides. */\nexport interface GatedLayer {\n readonly id: string;\n readonly visible?: boolean;\n readonly locked?: boolean;\n}\n\n/** What the gates read of a scene. A `Scene` is one. */\nexport interface GateSource {\n readonly layers?: readonly GatedLayer[];\n get(id: NodeId): GatedNode | undefined;\n}\n\n/** Whether `node`'s own flag hides it and its subtree. A walk from the roots\n * down prunes where this holds instead of asking {@link NodeGates.hidden}\n * per node; the two agree because this is the flag that answer climbs for. */\nexport function hidesSubtree(node: { readonly hidden?: boolean }): boolean {\n return node.hidden === true;\n}\n\n/** Both answers, for one read of a scene. */\nexport interface NodeGates {\n /** Whether `id` is not drawn. An id not in the scene is not hidden. */\n hidden(id: string): boolean;\n /** Whether `id` is out of reach. An id not in the scene is not locked. */\n locked(id: string): boolean;\n}\n\n/**\n * The gates over `scene` as it stands now. Ancestor answers are remembered,\n * so a walk over every node climbs each chain once: build one per walk, and\n * a fresh one after an edit.\n */\nexport function nodeGates(scene: GateSource): NodeGates {\n const hiddenLayers = flaggedLayers(scene, 'hidden');\n const lockedLayers = flaggedLayers(scene, 'locked');\n const ownLock = lockedLayers === null\n ? (node: GatedNode) => node.locked === true\n : (node: GatedNode) => node.locked === true || lockedLayers.has(node.layer);\n\n /** Whether `test` holds on the node at `parent` or any of its ancestors.\n * Only ancestors are remembered, so a walk over a flat scene allocates\n * nothing; that walk is every pick made after an edit. */\n const above = (test: (node: GatedNode) => boolean) => {\n const memo = new Map<string, boolean>();\n const path: string[] = [];\n return (parent: string | null): boolean => {\n if (parent === null) return false;\n let out = false;\n let seen: Set<string> | null = null;\n path.length = 0;\n for (let at: string | null = parent; at !== null; ) {\n const known = memo.get(at);\n if (known !== undefined) { out = known; break; }\n const node = scene.get(at as NodeId);\n if (node === undefined) break;\n if (path.length > 64) {\n seen ??= new Set(path);\n if (seen.has(at)) break;\n seen.add(at);\n }\n path.push(at);\n if (test(node)) { out = true; break; }\n at = node.parent ?? null;\n }\n for (const id of path) memo.set(id, out);\n return out;\n };\n };\n\n const hiddenAbove = above(hidesSubtree);\n const lockedAbove = above(ownLock);\n return {\n hidden(id) {\n const node = scene.get(id as NodeId);\n if (node === undefined) return false;\n return hiddenLayers?.has(node.layer) === true || hidesSubtree(node) || hiddenAbove(node.parent ?? null);\n },\n locked(id) {\n const node = scene.get(id as NodeId);\n if (node === undefined) return false;\n return ownLock(node) || lockedAbove(node.parent ?? null);\n },\n };\n}\n\n/** Whether `test` holds on the node at `id` or any ancestor, without the\n * allocation {@link nodeGates} spends on a walk: the scene's lock guard asks\n * this on every mutation. A parent cycle answers false, as the walk's does. */\nfunction climbs(scene: GateSource, id: string, test: (node: GatedNode) => boolean): boolean {\n let seen: Set<string> | null = null;\n let hops = 0;\n for (let at: string | null = id; at !== null; ) {\n const node = scene.get(at as NodeId);\n if (node === undefined) return false;\n if (++hops > 64) {\n seen ??= new Set();\n if (seen.has(at)) return false;\n seen.add(at);\n }\n if (test(node)) return true;\n at = node.parent ?? null;\n }\n return false;\n}\n\n/** The layers flagged `flag`, or null when none is: the usual answer, which\n * lets a climb skip the layer test at every hop. */\nfunction flaggedLayers(scene: GateSource, flag: 'locked' | 'hidden'): Set<string> | null {\n let out: Set<string> | null = null;\n for (const l of scene.layers ?? []) {\n if (flag === 'locked' ? l.locked === true : l.visible === false) (out ??= new Set()).add(l.id);\n }\n return out;\n}\n\n/** {@link NodeGates.hidden} for one id. */\nexport function isNodeHidden(scene: GateSource, id: NodeId): boolean {\n const node = scene.get(id);\n if (node === undefined) return false;\n return flaggedLayers(scene, 'hidden')?.has(node.layer) === true || climbs(scene, id, hidesSubtree);\n}\n\n/** {@link NodeGates.locked} for one id. */\nexport function isNodeLocked(scene: GateSource, id: NodeId): boolean {\n const layers = flaggedLayers(scene, 'locked');\n return climbs(scene, id, layers === null\n ? (node) => node.locked === true\n : (node) => node.locked === true || layers.has(node.layer));\n}\n"],"mappings":";;;;AAqCA,SAAgB,aAAa,MAA8C;CACzE,OAAO,KAAK,WAAW;AACzB;;;;;;
|
|
1
|
+
{"version":3,"file":"nodeGates.js","names":[],"sources":["../../../src/core/scene/nodeGates.ts"],"sourcesContent":["/**\n * Whether a node is shown, and whether it is in reach — the one place either\n * question is answered. Painting, picking, area selection, select-all,\n * snapping, export, and the scene's own lock guard all ask here.\n *\n * - **Hidden**: its own layer is not visible, or it or any ancestor carries\n * `hidden`. A layer hides only the nodes tagged to it; a hidden node hides\n * its whole subtree, as SVG's `display=\"none\"` does.\n * - **Locked**: it or any ancestor carries `locked`, or sits on a locked\n * layer. A lock covers the whole subtree, whatever layers it spans.\n */\nimport type { NodeId } from './types';\n\n/** The node fields the gates read. A partial scene stand-in may leave `parent` out. */\nexport interface GatedNode {\n readonly layer: string;\n readonly parent?: string | null;\n readonly hidden?: boolean;\n readonly locked?: boolean;\n}\n\n/** The layer fields the gates read. Only an explicit `visible: false` hides. */\nexport interface GatedLayer {\n readonly id: string;\n readonly visible?: boolean;\n readonly locked?: boolean;\n}\n\n/** What the gates read of a scene. A `Scene` is one. */\nexport interface GateSource {\n readonly layers?: readonly GatedLayer[];\n get(id: NodeId): GatedNode | undefined;\n}\n\n/** Whether `node`'s own flag hides it and its subtree. A walk from the roots\n * down prunes where this holds instead of asking {@link NodeGates.hidden}\n * per node; the two agree because this is the flag that answer climbs for. */\nexport function hidesSubtree(node: { readonly hidden?: boolean }): boolean {\n return node.hidden === true;\n}\n\n/** Both answers, for one read of a scene. Each takes an id, or the node a\n * walk already holds, which saves looking it up. */\nexport interface NodeGates {\n /** Whether `id` is not drawn. An id not in the scene is not hidden. */\n hidden(id: string | GatedNode): boolean;\n /** Whether `id` is out of reach. An id not in the scene is not locked. */\n locked(id: string | GatedNode): boolean;\n}\n\n/**\n * The gates over `scene` as it stands now. Ancestor answers are remembered,\n * so a walk over every node climbs each chain once: build one per walk, and\n * a fresh one after an edit.\n */\nexport function nodeGates(scene: GateSource): NodeGates {\n const hiddenLayers = flaggedLayers(scene, 'hidden');\n const lockedLayers = flaggedLayers(scene, 'locked');\n const ownLock = lockedLayers === null\n ? (node: GatedNode) => node.locked === true\n : (node: GatedNode) => node.locked === true || lockedLayers.has(node.layer);\n\n /** Whether `test` holds on the node at `parent` or any of its ancestors.\n * Only ancestors are remembered, so a walk over a flat scene allocates\n * nothing; that walk is every pick made after an edit. */\n const above = (test: (node: GatedNode) => boolean) => {\n const memo = new Map<string, boolean>();\n const path: string[] = [];\n return (parent: string | null): boolean => {\n if (parent === null) return false;\n let out = false;\n let seen: Set<string> | null = null;\n path.length = 0;\n for (let at: string | null = parent; at !== null; ) {\n const known = memo.get(at);\n if (known !== undefined) { out = known; break; }\n const node = scene.get(at as NodeId);\n if (node === undefined) break;\n if (path.length > 64) {\n seen ??= new Set(path);\n if (seen.has(at)) break;\n seen.add(at);\n }\n path.push(at);\n if (test(node)) { out = true; break; }\n at = node.parent ?? null;\n }\n for (const id of path) memo.set(id, out);\n return out;\n };\n };\n\n const hiddenAbove = above(hidesSubtree);\n const lockedAbove = above(ownLock);\n return {\n hidden(id) {\n const node = typeof id === 'string' ? scene.get(id as NodeId) : id;\n if (node === undefined) return false;\n return hiddenLayers?.has(node.layer) === true || hidesSubtree(node) || hiddenAbove(node.parent ?? null);\n },\n locked(id) {\n const node = typeof id === 'string' ? scene.get(id as NodeId) : id;\n if (node === undefined) return false;\n return ownLock(node) || lockedAbove(node.parent ?? null);\n },\n };\n}\n\n/** Whether `test` holds on the node at `id` or any ancestor, without the\n * allocation {@link nodeGates} spends on a walk: the scene's lock guard asks\n * this on every mutation. A parent cycle answers false, as the walk's does. */\nfunction climbs(scene: GateSource, id: string, test: (node: GatedNode) => boolean): boolean {\n let seen: Set<string> | null = null;\n let hops = 0;\n for (let at: string | null = id; at !== null; ) {\n const node = scene.get(at as NodeId);\n if (node === undefined) return false;\n if (++hops > 64) {\n seen ??= new Set();\n if (seen.has(at)) return false;\n seen.add(at);\n }\n if (test(node)) return true;\n at = node.parent ?? null;\n }\n return false;\n}\n\n/** The layers flagged `flag`, or null when none is: the usual answer, which\n * lets a climb skip the layer test at every hop. */\nfunction flaggedLayers(scene: GateSource, flag: 'locked' | 'hidden'): Set<string> | null {\n let out: Set<string> | null = null;\n for (const l of scene.layers ?? []) {\n if (flag === 'locked' ? l.locked === true : l.visible === false) (out ??= new Set()).add(l.id);\n }\n return out;\n}\n\n/** {@link NodeGates.hidden} for one id. */\nexport function isNodeHidden(scene: GateSource, id: NodeId): boolean {\n const node = scene.get(id);\n if (node === undefined) return false;\n return flaggedLayers(scene, 'hidden')?.has(node.layer) === true || climbs(scene, id, hidesSubtree);\n}\n\n/** {@link NodeGates.locked} for one id. */\nexport function isNodeLocked(scene: GateSource, id: NodeId): boolean {\n const layers = flaggedLayers(scene, 'locked');\n return climbs(scene, id, layers === null\n ? (node) => node.locked === true\n : (node) => node.locked === true || layers.has(node.layer));\n}\n"],"mappings":"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"}
|