@weasel-js/core 1.14.1 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +218 -0
- package/README.md +1 -1
- package/dist/affordances/pathAnchors.js +0 -1
- package/dist/affordances/pathAnchors.js.map +1 -1
- package/dist/barrel/paths.js +1 -1
- package/dist/barrel/renderer.js +2 -2
- package/dist/barrel/routing.js +6 -4
- package/dist/barrel/text.js +5 -3
- package/dist/canvas/Canvas.js +5 -1
- package/dist/canvas/Canvas.js.map +1 -1
- package/dist/canvas/DrawCanvas.js +5 -1
- package/dist/canvas/DrawCanvas.js.map +1 -1
- package/dist/canvas/SceneCanvas/SceneCanvasInput.js +4 -0
- package/dist/canvas/SceneCanvas/SceneCanvasInput.js.map +1 -1
- package/dist/canvas/SceneCanvas/builtinToolSets.js +6 -3
- package/dist/canvas/SceneCanvas/builtinToolSets.js.map +1 -1
- package/dist/canvas/SceneCanvas/defaultNodeProperties.js +1 -1
- package/dist/canvas/SceneCanvas/presets/paths.js +6 -6
- package/dist/canvas/SceneCanvas/presets/paths.js.map +1 -1
- package/dist/canvas/SceneCanvas/shapeGeneratorProperties.js +1 -1
- package/dist/canvas/SceneCanvas/shapeGeneratorProperties.js.map +1 -1
- package/dist/canvas/SceneCanvas/useBuiltinTools.js +2 -0
- package/dist/canvas/SceneCanvas/useBuiltinTools.js.map +1 -1
- package/dist/canvas/SceneCanvas/useContributionRoles.js +2 -0
- package/dist/canvas/SceneCanvas/useContributionRoles.js.map +1 -1
- package/dist/canvas/SceneCanvas/useSceneChrome.js +17 -5
- package/dist/canvas/SceneCanvas/useSceneChrome.js.map +1 -1
- package/dist/canvas/SceneCanvas/useSceneStandardActions.js +3 -0
- package/dist/canvas/SceneCanvas/useSceneStandardActions.js.map +1 -1
- package/dist/canvas/SceneCanvas.js +5 -4
- package/dist/canvas/SceneCanvas.js.map +1 -1
- package/dist/canvas/SceneViewCanvas.js +5 -1
- package/dist/canvas/SceneViewCanvas.js.map +1 -1
- package/dist/canvas/affordanceAt.js +0 -1
- package/dist/canvas/affordanceAt.js.map +1 -1
- package/dist/canvas/deps/chromeRules.js +37 -0
- package/dist/canvas/deps/chromeRules.js.map +1 -0
- package/dist/canvas/deps/editAnchors.js +0 -6
- package/dist/canvas/deps/editAnchors.js.map +1 -1
- package/dist/canvas/deps/hitTestArea.js +3 -1
- package/dist/canvas/deps/hitTestArea.js.map +1 -1
- package/dist/canvas/deps/modeDefaultTool.js +23 -0
- package/dist/canvas/deps/modeDefaultTool.js.map +1 -0
- package/dist/canvas/deps/pathEditCommits.js +1 -1
- package/dist/canvas/deps/selectionSnapshot.js +23 -0
- package/dist/canvas/deps/selectionSnapshot.js.map +1 -0
- package/dist/canvas/deps/snap.js +1 -0
- package/dist/canvas/deps/snap.js.map +1 -1
- package/dist/canvas/pickReach.js +12 -8
- package/dist/canvas/pickReach.js.map +1 -1
- package/dist/canvas/pickWalk.js +7 -5
- package/dist/canvas/pickWalk.js.map +1 -1
- package/dist/canvas/scenePickSource.js +2 -2
- package/dist/canvas/scenePickSource.js.map +1 -1
- package/dist/canvas/useCanvasSurface.js +21 -3
- package/dist/canvas/useCanvasSurface.js.map +1 -1
- package/dist/canvas/useFrameLoop.js +6 -7
- package/dist/canvas/useFrameLoop.js.map +1 -1
- package/dist/canvas/useSurfaceSize.js +4 -3
- package/dist/canvas/useSurfaceSize.js.map +1 -1
- package/dist/core/geometry/regionHit.js +35 -16
- package/dist/core/geometry/regionHit.js.map +1 -1
- package/dist/core/paint/meshKindLazy.js +9 -10
- package/dist/core/paint/meshKindLazy.js.map +1 -1
- package/dist/core/scene/editMode.js +4 -1
- package/dist/core/scene/editMode.js.map +1 -1
- package/dist/core/scene/historyFacade.js +7 -50
- package/dist/core/scene/historyFacade.js.map +1 -1
- package/dist/core/scene/lockGuard.js +1 -0
- package/dist/core/scene/lockGuard.js.map +1 -1
- package/dist/core/scene/nodeGates.js +43 -23
- package/dist/core/scene/nodeGates.js.map +1 -1
- package/dist/core/scene/opTable.js +8 -8
- package/dist/core/scene/opTable.js.map +1 -1
- package/dist/core/scene/outerOps.js +2 -2
- package/dist/core/scene/outerOps.js.map +1 -1
- package/dist/core/scene/poseRemovals.js +1 -0
- package/dist/core/scene/poseRemovals.js.map +1 -1
- package/dist/core/scene/poseWrites.js +16 -15
- package/dist/core/scene/poseWrites.js.map +1 -1
- package/dist/core/scene/sceneJournals.js +78 -0
- package/dist/core/scene/sceneJournals.js.map +1 -0
- package/dist/core/scene/sceneRecording.js +3 -2
- package/dist/core/scene/sceneRecording.js.map +1 -1
- package/dist/core/scene/statedFields.js +10 -8
- package/dist/core/scene/statedFields.js.map +1 -1
- package/dist/core/scene/treeEdits.js +4 -9
- package/dist/core/scene/treeEdits.js.map +1 -1
- package/dist/core/viewport/useCanvasSize.js +47 -26
- package/dist/core/viewport/useCanvasSize.js.map +1 -1
- package/dist/{depSchema-C3BwxitB.d.ts → depSchema-ChQPzEkW.d.ts} +56 -25
- package/dist/domains/chrome-caps/resolve.js +1 -0
- package/dist/domains/chrome-caps/resolve.js.map +1 -1
- package/dist/domains/chrome-caps/useChromeCaps.js +4 -2
- package/dist/domains/chrome-caps/useChromeCaps.js.map +1 -1
- package/dist/domains/paths/pathEditState.js +0 -24
- package/dist/domains/paths/pathEditState.js.map +1 -1
- package/dist/domains/snap/probes2d.js +5 -0
- package/dist/domains/snap/probes2d.js.map +1 -1
- package/dist/domains/snap/resizeBox.js +43 -22
- package/dist/domains/snap/resizeBox.js.map +1 -1
- package/dist/domains/snap/sceneReads.js +11 -1
- package/dist/domains/snap/sceneReads.js.map +1 -1
- package/dist/domains/snap/sceneSession2d.js +4 -2
- package/dist/domains/snap/sceneSession2d.js.map +1 -1
- package/dist/domains/snap/session2d.js +14 -7
- package/dist/domains/snap/session2d.js.map +1 -1
- package/dist/domains/text/index.js +2 -2
- package/dist/icons.table.js +1 -1
- package/dist/icons.table.js.map +1 -1
- package/dist/icons.table.json +5 -0
- package/dist/index.d.ts +114 -62
- package/dist/index.js +12 -8
- package/dist/interactions/actions/booleans/action.js +4 -2
- package/dist/interactions/actions/booleans/action.js.map +1 -1
- package/dist/interactions/actions/defaults/anchorEditing.js +3 -6
- package/dist/interactions/actions/defaults/anchorEditing.js.map +1 -1
- package/dist/interactions/actions/defaults/bendSegment.js +1 -2
- package/dist/interactions/actions/defaults/bendSegment.js.map +1 -1
- package/dist/interactions/actions/defaults/exitPathEdit.js +17 -1
- package/dist/interactions/actions/defaults/exitPathEdit.js.map +1 -1
- package/dist/interactions/actions/edit-anchors/action.js +3 -4
- package/dist/interactions/actions/edit-anchors/action.js.map +1 -1
- package/dist/interactions/actions/insert/action.js +21 -2
- package/dist/interactions/actions/insert/action.js.map +1 -1
- package/dist/interactions/actions/insert/params.js +43 -0
- package/dist/interactions/actions/insert/params.js.map +1 -0
- package/dist/interactions/actions/notices.js +17 -0
- package/dist/interactions/actions/notices.js.map +1 -0
- package/dist/interactions/actions/outlines/createOutlines.js +8 -6
- package/dist/interactions/actions/outlines/createOutlines.js.map +1 -1
- package/dist/interactions/actions/outlines/createOutlinesAction.js +13 -8
- package/dist/interactions/actions/outlines/createOutlinesAction.js.map +1 -1
- package/dist/interactions/actions/outlines/outlineStrokeAction.js +3 -2
- package/dist/interactions/actions/outlines/outlineStrokeAction.js.map +1 -1
- package/dist/interactions/actions/pathCommands/pathNodes.js +1 -1
- package/dist/interactions/actions/testUtils.js +0 -7
- package/dist/interactions/actions/testUtils.js.map +1 -1
- package/dist/interactions/actions/useNotices.js +13 -0
- package/dist/interactions/actions/useNotices.js.map +1 -0
- package/dist/test-seams.d.ts +5 -12
- package/dist/tools/builtin/anchors/anchorsTool.js +31 -0
- package/dist/tools/builtin/anchors/anchorsTool.js.map +1 -0
- package/dist/tools/builtin/polygon/usePolygonTool.js +1 -1
- package/dist/tools/builtin/star/useStarTool.js +1 -1
- package/dist/tools/index.js +4 -2
- package/dist/tools/toolOptions.js +47 -0
- package/dist/tools/toolOptions.js.map +1 -0
- package/dist/version.js +1 -1
- package/package.json +25 -25
- package/dist/interactions/actions/defaults/pathEditTools.js +0 -60
- package/dist/interactions/actions/defaults/pathEditTools.js.map +0 -1
|
@@ -1,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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"}
|
|
@@ -29,6 +29,7 @@ function createLockGuard(state) {
|
|
|
29
29
|
throw new Error(`Scene: cannot ${verb} layer "${layer}" — it is locked`);
|
|
30
30
|
},
|
|
31
31
|
reachable(ids) {
|
|
32
|
+
if (ids.length === 0) return ids;
|
|
32
33
|
const gates = nodeGates(source);
|
|
33
34
|
return ids.some((id) => gates.locked(id) || gates.hidden(id)) ? ids.filter((id) => !gates.locked(id) && !gates.hidden(id)) : ids;
|
|
34
35
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lockGuard.js","names":[],"sources":["../../../src/core/scene/lockGuard.ts"],"sourcesContent":["/**\n * The scene's lock guard: what a lock refuses, what the selection may hold,\n * and how a caller that is restoring an already-legal state gets past it.\n */\nimport { isNodeHidden, isNodeLocked, nodeGates } from './nodeGates';\nimport type { Node, NodeId } from './types';\n\n/** What the guard reads of a scene's state. */\nexport interface LockGuardState<TData, TLayer extends string, TPose> {\n nodes: Map<NodeId, Node<TData, TLayer, TPose>>;\n layers: { id: string; visible: boolean; locked: boolean }[];\n layerIndex: Map<TLayer, number>;\n}\n\nexport interface LockGuard<TLayer extends string> {\n /** See `Scene.isLocked`. */\n isLocked(id: NodeId): boolean;\n /** See `Scene.isHidden`. */\n isHidden(id: NodeId): boolean;\n /** Throw when `id` is locked and the guard is not lifted. */\n refuse(id: NodeId, verb: string): void;\n /** Throw when `layer` is locked and the guard is not lifted. */\n refuseLayer(layer: TLayer, verb: string): void;\n /** `ids` less the locked and hidden ones: the scene's selection never\n * holds either, whoever writes it. */\n reachable(ids: readonly NodeId[]): readonly NodeId[];\n /** Tell the guard `node` was just written. Until some node carries\n * `locked`, and no layer is locked, nothing is locked and a check need\n * not climb. Every write that can set the flag calls this. */\n noteWritten(node: { readonly locked?: boolean }): void;\n /** Run `fn` with the guard lifted: `unlocked()`, `removeLayer`, and every\n * replay — undo, redo, a journal stepping, a rollback — which restores a\n * state that was legal when it was recorded. */\n lifted<T>(fn: () => T): T;\n}\n\nexport function createLockGuard<TData, TLayer extends string, TPose>(\n state: LockGuardState<TData, TLayer, TPose>,\n): LockGuard<TLayer> {\n let bypass = 0;\n /** Whether any node has carried `locked` since the scene was made. It is\n * never cleared, so it can only err toward climbing. */\n let nodeLockSeen = false;\n\n const source = { get layers() { return state.layers; }, get: (id: NodeId) => state.nodes.get(id) };\n const isLocked = (id: NodeId): boolean =>\n (nodeLockSeen || state.layers.some((l) => l.locked)) && isNodeLocked(source, id);\n\n return {\n isLocked,\n isHidden: (id) => isNodeHidden(source, id),\n refuse(id, verb) {\n if (bypass > 0 || !isLocked(id)) return;\n throw new Error(`Scene: cannot ${verb} \"${id}\" — it is locked`);\n },\n refuseLayer(layer, verb) {\n if (bypass > 0 || !state.layers[state.layerIndex.get(layer) ?? -1]?.locked) return;\n throw new Error(`Scene: cannot ${verb} layer \"${layer}\" — it is locked`);\n },\n reachable(ids) {\n const gates = nodeGates(source);\n return ids.some((id) => gates.locked(id) || gates.hidden(id))\n ? ids.filter((id) => !gates.locked(id) && !gates.hidden(id))\n : ids;\n },\n noteWritten(node) {\n if (node.locked === true) nodeLockSeen = true;\n },\n lifted(fn) {\n bypass++;\n try {\n return fn();\n } finally {\n bypass--;\n }\n },\n };\n}\n"],"mappings":";;;;;;;AAoCA,SAAgB,gBACd,OACmB;CACnB,IAAI,SAAS;;;CAGb,IAAI,eAAe;CAEnB,MAAM,SAAS;EAAE,IAAI,SAAS;GAAE,OAAO,MAAM;EAAQ;EAAG,MAAM,OAAe,MAAM,MAAM,IAAI,EAAE;CAAE;CACjG,MAAM,YAAY,QACf,gBAAgB,MAAM,OAAO,MAAM,MAAM,EAAE,MAAM,MAAM,aAAa,QAAQ,EAAE;CAEjF,OAAO;EACL;EACA,WAAW,OAAO,aAAa,QAAQ,EAAE;EACzC,OAAO,IAAI,MAAM;GACf,IAAI,SAAS,KAAK,CAAC,SAAS,EAAE,GAAG;GACjC,MAAM,IAAI,MAAM,iBAAiB,KAAK,IAAI,GAAG,iBAAiB;EAChE;EACA,YAAY,OAAO,MAAM;GACvB,IAAI,SAAS,KAAK,CAAC,MAAM,OAAO,MAAM,WAAW,IAAI,KAAK,KAAK,GAAG,EAAE,QAAQ;GAC5E,MAAM,IAAI,MAAM,iBAAiB,KAAK,UAAU,MAAM,iBAAiB;EACzE;EACA,UAAU,KAAK;GACb,MAAM,QAAQ,UAAU,MAAM;GAC9B,OAAO,IAAI,MAAM,OAAO,MAAM,OAAO,EAAE,KAAK,MAAM,OAAO,EAAE,CAAC,IACxD,IAAI,QAAQ,OAAO,CAAC,MAAM,OAAO,EAAE,KAAK,CAAC,MAAM,OAAO,EAAE,CAAC,IACzD;EACN;EACA,YAAY,MAAM;GAChB,IAAI,KAAK,WAAW,MAAM,eAAe;EAC3C;EACA,OAAO,IAAI;GACT;GACA,IAAI;IACF,OAAO,GAAG;GACZ,UAAU;IACR;GACF;EACF;CACF;AACF"}
|
|
1
|
+
{"version":3,"file":"lockGuard.js","names":[],"sources":["../../../src/core/scene/lockGuard.ts"],"sourcesContent":["/**\n * The scene's lock guard: what a lock refuses, what the selection may hold,\n * and how a caller that is restoring an already-legal state gets past it.\n */\nimport { isNodeHidden, isNodeLocked, nodeGates } from './nodeGates';\nimport type { Node, NodeId } from './types';\n\n/** What the guard reads of a scene's state. */\nexport interface LockGuardState<TData, TLayer extends string, TPose> {\n nodes: Map<NodeId, Node<TData, TLayer, TPose>>;\n layers: { id: string; visible: boolean; locked: boolean }[];\n layerIndex: Map<TLayer, number>;\n}\n\nexport interface LockGuard<TLayer extends string> {\n /** See `Scene.isLocked`. */\n isLocked(id: NodeId): boolean;\n /** See `Scene.isHidden`. */\n isHidden(id: NodeId): boolean;\n /** Throw when `id` is locked and the guard is not lifted. */\n refuse(id: NodeId, verb: string): void;\n /** Throw when `layer` is locked and the guard is not lifted. */\n refuseLayer(layer: TLayer, verb: string): void;\n /** `ids` less the locked and hidden ones: the scene's selection never\n * holds either, whoever writes it. */\n reachable(ids: readonly NodeId[]): readonly NodeId[];\n /** Tell the guard `node` was just written. Until some node carries\n * `locked`, and no layer is locked, nothing is locked and a check need\n * not climb. Every write that can set the flag calls this. */\n noteWritten(node: { readonly locked?: boolean }): void;\n /** Run `fn` with the guard lifted: `unlocked()`, `removeLayer`, and every\n * replay — undo, redo, a journal stepping, a rollback — which restores a\n * state that was legal when it was recorded. */\n lifted<T>(fn: () => T): T;\n}\n\nexport function createLockGuard<TData, TLayer extends string, TPose>(\n state: LockGuardState<TData, TLayer, TPose>,\n): LockGuard<TLayer> {\n let bypass = 0;\n /** Whether any node has carried `locked` since the scene was made. It is\n * never cleared, so it can only err toward climbing. */\n let nodeLockSeen = false;\n\n const source = { get layers() { return state.layers; }, get: (id: NodeId) => state.nodes.get(id) };\n const isLocked = (id: NodeId): boolean =>\n (nodeLockSeen || state.layers.some((l) => l.locked)) && isNodeLocked(source, id);\n\n return {\n isLocked,\n isHidden: (id) => isNodeHidden(source, id),\n refuse(id, verb) {\n if (bypass > 0 || !isLocked(id)) return;\n throw new Error(`Scene: cannot ${verb} \"${id}\" — it is locked`);\n },\n refuseLayer(layer, verb) {\n if (bypass > 0 || !state.layers[state.layerIndex.get(layer) ?? -1]?.locked) return;\n throw new Error(`Scene: cannot ${verb} layer \"${layer}\" — it is locked`);\n },\n reachable(ids) {\n if (ids.length === 0) return ids;\n const gates = nodeGates(source);\n return ids.some((id) => gates.locked(id) || gates.hidden(id))\n ? ids.filter((id) => !gates.locked(id) && !gates.hidden(id))\n : ids;\n },\n noteWritten(node) {\n if (node.locked === true) nodeLockSeen = true;\n },\n lifted(fn) {\n bypass++;\n try {\n return fn();\n } finally {\n bypass--;\n }\n },\n };\n}\n"],"mappings":";;;;;;;AAoCA,SAAgB,gBACd,OACmB;CACnB,IAAI,SAAS;;;CAGb,IAAI,eAAe;CAEnB,MAAM,SAAS;EAAE,IAAI,SAAS;GAAE,OAAO,MAAM;EAAQ;EAAG,MAAM,OAAe,MAAM,MAAM,IAAI,EAAE;CAAE;CACjG,MAAM,YAAY,QACf,gBAAgB,MAAM,OAAO,MAAM,MAAM,EAAE,MAAM,MAAM,aAAa,QAAQ,EAAE;CAEjF,OAAO;EACL;EACA,WAAW,OAAO,aAAa,QAAQ,EAAE;EACzC,OAAO,IAAI,MAAM;GACf,IAAI,SAAS,KAAK,CAAC,SAAS,EAAE,GAAG;GACjC,MAAM,IAAI,MAAM,iBAAiB,KAAK,IAAI,GAAG,iBAAiB;EAChE;EACA,YAAY,OAAO,MAAM;GACvB,IAAI,SAAS,KAAK,CAAC,MAAM,OAAO,MAAM,WAAW,IAAI,KAAK,KAAK,GAAG,EAAE,QAAQ;GAC5E,MAAM,IAAI,MAAM,iBAAiB,KAAK,UAAU,MAAM,iBAAiB;EACzE;EACA,UAAU,KAAK;GACb,IAAI,IAAI,WAAW,GAAG,OAAO;GAC7B,MAAM,QAAQ,UAAU,MAAM;GAC9B,OAAO,IAAI,MAAM,OAAO,MAAM,OAAO,EAAE,KAAK,MAAM,OAAO,EAAE,CAAC,IACxD,IAAI,QAAQ,OAAO,CAAC,MAAM,OAAO,EAAE,KAAK,CAAC,MAAM,OAAO,EAAE,CAAC,IACzD;EACN;EACA,YAAY,MAAM;GAChB,IAAI,KAAK,WAAW,MAAM,eAAe;EAC3C;EACA,OAAO,IAAI;GACT;GACA,IAAI;IACF,OAAO,GAAG;GACZ,UAAU;IACR;GACF;EACF;CACF;AACF"}
|
|
@@ -11,37 +11,57 @@ function hidesSubtree(node) {
|
|
|
11
11
|
* a fresh one after an edit.
|
|
12
12
|
*/
|
|
13
13
|
function nodeGates(scene) {
|
|
14
|
-
const hiddenLayers =
|
|
15
|
-
const lockedLayers =
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
* layer counting when `layers` holds it. */
|
|
22
|
-
const inherited = (flag, layers) => {
|
|
14
|
+
const hiddenLayers = flaggedLayers(scene, "hidden");
|
|
15
|
+
const lockedLayers = flaggedLayers(scene, "locked");
|
|
16
|
+
const ownLock = lockedLayers === null ? (node) => node.locked === true : (node) => node.locked === true || lockedLayers.has(node.layer);
|
|
17
|
+
/** Whether `test` holds on the node at `parent` or any of its ancestors.
|
|
18
|
+
* Only ancestors are remembered, so a walk over a flat scene allocates
|
|
19
|
+
* nothing; that walk is every pick made after an edit. */
|
|
20
|
+
const above = (test) => {
|
|
23
21
|
const memo = /* @__PURE__ */ new Map();
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
if (
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
22
|
+
const path = [];
|
|
23
|
+
return (parent) => {
|
|
24
|
+
if (parent === null) return false;
|
|
25
|
+
let out = false;
|
|
26
|
+
let seen = null;
|
|
27
|
+
path.length = 0;
|
|
28
|
+
for (let at = parent; at !== null;) {
|
|
29
|
+
const known = memo.get(at);
|
|
30
|
+
if (known !== void 0) {
|
|
31
|
+
out = known;
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
const node = scene.get(at);
|
|
35
|
+
if (node === void 0) break;
|
|
36
|
+
if (path.length > 64) {
|
|
37
|
+
seen ??= new Set(path);
|
|
38
|
+
if (seen.has(at)) break;
|
|
39
|
+
seen.add(at);
|
|
40
|
+
}
|
|
41
|
+
path.push(at);
|
|
42
|
+
if (test(node)) {
|
|
43
|
+
out = true;
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
at = node.parent ?? null;
|
|
47
|
+
}
|
|
48
|
+
for (const id of path) memo.set(id, out);
|
|
33
49
|
return out;
|
|
34
50
|
};
|
|
35
|
-
return (id) => climb(id, /* @__PURE__ */ new Set());
|
|
36
51
|
};
|
|
37
|
-
const
|
|
52
|
+
const hiddenAbove = above(hidesSubtree);
|
|
53
|
+
const lockedAbove = above(ownLock);
|
|
38
54
|
return {
|
|
39
55
|
hidden(id) {
|
|
40
|
-
const node = scene.get(id);
|
|
56
|
+
const node = typeof id === "string" ? scene.get(id) : id;
|
|
41
57
|
if (node === void 0) return false;
|
|
42
|
-
return hiddenLayers
|
|
58
|
+
return hiddenLayers?.has(node.layer) === true || hidesSubtree(node) || hiddenAbove(node.parent ?? null);
|
|
43
59
|
},
|
|
44
|
-
locked
|
|
60
|
+
locked(id) {
|
|
61
|
+
const node = typeof id === "string" ? scene.get(id) : id;
|
|
62
|
+
if (node === void 0) return false;
|
|
63
|
+
return ownLock(node) || lockedAbove(node.parent ?? null);
|
|
64
|
+
}
|
|
45
65
|
};
|
|
46
66
|
}
|
|
47
67
|
/** Whether `test` holds on the node at `id` or any ancestor, without the
|
|
@@ -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 =
|
|
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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"}
|
|
@@ -18,6 +18,12 @@ function createOpTable(env) {
|
|
|
18
18
|
} else token = handlers.get(kind)?.coalesceKey?.(payload);
|
|
19
19
|
return token === void 0 ? void 0 : `${kind}:${token}`;
|
|
20
20
|
}
|
|
21
|
+
function run(kind, payload, forward) {
|
|
22
|
+
const handler = handlerFor(kind);
|
|
23
|
+
if (env.skips(kind, payload, forward)) return "noop";
|
|
24
|
+
(forward ? handler.apply : handler.revert)(payload);
|
|
25
|
+
env.applied(kind, payload, !forward);
|
|
26
|
+
}
|
|
21
27
|
function op(kind, payload, forward = true) {
|
|
22
28
|
handlerFor(kind);
|
|
23
29
|
return {
|
|
@@ -25,16 +31,10 @@ function createOpTable(env) {
|
|
|
25
31
|
args: payload,
|
|
26
32
|
coalesceKey: coalesceKeyFor(kind, payload),
|
|
27
33
|
apply: () => {
|
|
28
|
-
|
|
29
|
-
const handler = handlerFor(kind);
|
|
30
|
-
if (env.skips(kind, payload, forward)) return "noop";
|
|
31
|
-
(forward ? handler.apply : handler.revert)(payload);
|
|
32
|
-
env.applied(kind, payload, !forward);
|
|
33
|
-
};
|
|
34
|
-
if (env.driven()) return run();
|
|
34
|
+
if (env.driven()) return run(kind, payload, forward);
|
|
35
35
|
let result;
|
|
36
36
|
env.replayAlone(() => {
|
|
37
|
-
result = run();
|
|
37
|
+
result = run(kind, payload, forward);
|
|
38
38
|
});
|
|
39
39
|
return result;
|
|
40
40
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"opTable.js","names":[],"sources":["../../../src/core/scene/opTable.ts"],"sourcesContent":["/**\n * The scene's registered ops: the kit's own kinds and a consumer's, each a\n * pair of functions over a serializable payload, and the engine `Op` built\n * from one.\n */\nimport type { Op } from 'core/ops/types';\nimport type { RegisteredOp } from './types';\n\n/** What the table asks of the scene when one of its ops runs. */\nexport interface OpTableEnv {\n /** Whether replaying this op would address a node that is no longer where\n * its entry left it; such an op reports a no-op. */\n skips(kind: string, payload: unknown, forward: boolean): boolean;\n /** Called after an op built by {@link OpTable.op} has applied, with the\n * direction it ran in. */\n applied(kind: string, payload: unknown, reverted: boolean): void;\n /** Whether an edit of the scene's is running. An op applied outside one\n * is a replay by a history the scene does not own: a mode journal's undo,\n * redo or cancel. */\n driven(): boolean;\n /** Run `step` as a replay step of its own, and notify. */\n replayAlone(step: () => void): void;\n}\n\nexport interface OpTable {\n handlers: ReadonlyMap<string, RegisteredOp<unknown>>;\n register(kind: string, handler: RegisteredOp<unknown>): void;\n /** Apply a kind's forward half. Throws when nothing is registered for it. */\n run(kind: string, payload: unknown): void;\n /** A registered op as an engine `Op`. `invert()` flips the direction with\n * the same payload, so undo replays `revert` and redo replays `apply`.\n * The handler is looked up at apply time, so a re-registered kind takes\n * effect on replay; that one exists is checked here, to fail at the call\n * site. */\n op(kind: string, payload: unknown): Op;\n}\n\nexport function createOpTable(env: OpTableEnv): OpTable {\n const handlers = new Map<string, RegisteredOp<unknown>>();\n\n function handlerFor(kind: string): RegisteredOp<unknown> {\n const handler = handlers.get(kind);\n if (!handler) throw new Error(`Scene: no registered op for kind \"${kind}\"`);\n return handler;\n }\n\n /** Coalesce-grouping key. Kit ops are keyed by the node or layer they\n * address; a consumer kind only by the `coalesceKey` it declares. Keys\n * take effect only under `UseSceneOptions.coalesceWindowMs`. */\n function coalesceKeyFor(kind: string, payload: unknown): string | undefined {\n let token: string | undefined;\n if (kind.startsWith('kit:')) {\n if (payload === null || typeof payload !== 'object') return undefined;\n const p = payload as { id?: unknown; layer?: unknown };\n token =\n typeof p.id === 'string' ? p.id\n : typeof p.layer === 'string' ? p.layer\n : undefined;\n } else {\n token = handlers.get(kind)?.coalesceKey?.(payload);\n }\n return token === undefined ? undefined : `${kind}:${token}`;\n }\n\n function
|
|
1
|
+
{"version":3,"file":"opTable.js","names":[],"sources":["../../../src/core/scene/opTable.ts"],"sourcesContent":["/**\n * The scene's registered ops: the kit's own kinds and a consumer's, each a\n * pair of functions over a serializable payload, and the engine `Op` built\n * from one.\n */\nimport type { Op } from 'core/ops/types';\nimport type { RegisteredOp } from './types';\n\n/** What the table asks of the scene when one of its ops runs. */\nexport interface OpTableEnv {\n /** Whether replaying this op would address a node that is no longer where\n * its entry left it; such an op reports a no-op. */\n skips(kind: string, payload: unknown, forward: boolean): boolean;\n /** Called after an op built by {@link OpTable.op} has applied, with the\n * direction it ran in. */\n applied(kind: string, payload: unknown, reverted: boolean): void;\n /** Whether an edit of the scene's is running. An op applied outside one\n * is a replay by a history the scene does not own: a mode journal's undo,\n * redo or cancel. */\n driven(): boolean;\n /** Run `step` as a replay step of its own, and notify. */\n replayAlone(step: () => void): void;\n}\n\nexport interface OpTable {\n handlers: ReadonlyMap<string, RegisteredOp<unknown>>;\n register(kind: string, handler: RegisteredOp<unknown>): void;\n /** Apply a kind's forward half. Throws when nothing is registered for it. */\n run(kind: string, payload: unknown): void;\n /** A registered op as an engine `Op`. `invert()` flips the direction with\n * the same payload, so undo replays `revert` and redo replays `apply`.\n * The handler is looked up at apply time, so a re-registered kind takes\n * effect on replay; that one exists is checked here, to fail at the call\n * site. */\n op(kind: string, payload: unknown): Op;\n}\n\nexport function createOpTable(env: OpTableEnv): OpTable {\n const handlers = new Map<string, RegisteredOp<unknown>>();\n\n function handlerFor(kind: string): RegisteredOp<unknown> {\n const handler = handlers.get(kind);\n if (!handler) throw new Error(`Scene: no registered op for kind \"${kind}\"`);\n return handler;\n }\n\n /** Coalesce-grouping key. Kit ops are keyed by the node or layer they\n * address; a consumer kind only by the `coalesceKey` it declares. Keys\n * take effect only under `UseSceneOptions.coalesceWindowMs`. */\n function coalesceKeyFor(kind: string, payload: unknown): string | undefined {\n let token: string | undefined;\n if (kind.startsWith('kit:')) {\n if (payload === null || typeof payload !== 'object') return undefined;\n const p = payload as { id?: unknown; layer?: unknown };\n token =\n typeof p.id === 'string' ? p.id\n : typeof p.layer === 'string' ? p.layer\n : undefined;\n } else {\n token = handlers.get(kind)?.coalesceKey?.(payload);\n }\n return token === undefined ? undefined : `${kind}:${token}`;\n }\n\n function run(kind: string, payload: unknown, forward: boolean): 'noop' | undefined {\n const handler = handlerFor(kind);\n if (env.skips(kind, payload, forward)) return 'noop';\n (forward ? handler.apply : handler.revert)(payload);\n env.applied(kind, payload, !forward);\n return undefined;\n }\n\n function op(kind: string, payload: unknown, forward = true): Op {\n handlerFor(kind);\n return {\n name: kind,\n args: payload,\n coalesceKey: coalesceKeyFor(kind, payload),\n apply: () => {\n if (env.driven()) return run(kind, payload, forward);\n let result: 'noop' | undefined;\n env.replayAlone(() => { result = run(kind, payload, forward); });\n return result;\n },\n invert: () => op(kind, payload, !forward),\n };\n }\n\n return {\n handlers,\n register: (kind, handler) => { handlers.set(kind, handler); },\n run: (kind, payload) => handlerFor(kind).apply(payload),\n op: (kind, payload) => op(kind, payload),\n };\n}\n"],"mappings":";AAqCA,SAAgB,cAAc,KAA0B;CACtD,MAAM,2BAAW,IAAI,IAAmC;CAExD,SAAS,WAAW,MAAqC;EACvD,MAAM,UAAU,SAAS,IAAI,IAAI;EACjC,IAAI,CAAC,SAAS,MAAM,IAAI,MAAM,qCAAqC,KAAK,EAAE;EAC1E,OAAO;CACT;;;;CAKA,SAAS,eAAe,MAAc,SAAsC;EAC1E,IAAI;EACJ,IAAI,KAAK,WAAW,MAAM,GAAG;GAC3B,IAAI,YAAY,QAAQ,OAAO,YAAY,UAAU,OAAO;GAC5D,MAAM,IAAI;GACV,QACE,OAAO,EAAE,OAAO,WAAW,EAAE,KAC3B,OAAO,EAAE,UAAU,WAAW,EAAE,QAChC;EACN,OACE,QAAQ,SAAS,IAAI,IAAI,CAAC,EAAE,cAAc,OAAO;EAEnD,OAAO,UAAU,SAAY,SAAY,GAAG,KAAK,GAAG;CACtD;CAEA,SAAS,IAAI,MAAc,SAAkB,SAAsC;EACjF,MAAM,UAAU,WAAW,IAAI;EAC/B,IAAI,IAAI,MAAM,MAAM,SAAS,OAAO,GAAG,OAAO;EAC9C,CAAC,UAAU,QAAQ,QAAQ,QAAQ,OAAA,CAAQ,OAAO;EAClD,IAAI,QAAQ,MAAM,SAAS,CAAC,OAAO;CAErC;CAEA,SAAS,GAAG,MAAc,SAAkB,UAAU,MAAU;EAC9D,WAAW,IAAI;EACf,OAAO;GACL,MAAM;GACN,MAAM;GACN,aAAa,eAAe,MAAM,OAAO;GACzC,aAAa;IACX,IAAI,IAAI,OAAO,GAAG,OAAO,IAAI,MAAM,SAAS,OAAO;IACnD,IAAI;IACJ,IAAI,kBAAkB;KAAE,SAAS,IAAI,MAAM,SAAS,OAAO;IAAG,CAAC;IAC/D,OAAO;GACT;GACA,cAAc,GAAG,MAAM,SAAS,CAAC,OAAO;EAC1C;CACF;CAEA,OAAO;EACL;EACA,WAAW,MAAM,YAAY;GAAE,SAAS,IAAI,MAAM,OAAO;EAAG;EAC5D,MAAM,MAAM,YAAY,WAAW,IAAI,CAAC,CAAC,MAAM,OAAO;EACtD,KAAK,MAAM,YAAY,GAAG,MAAM,OAAO;CACzC;AACF"}
|
|
@@ -51,7 +51,7 @@ function createOuterOps(env) {
|
|
|
51
51
|
causing: (op) => causing(op),
|
|
52
52
|
apply(engine, ops, label, adapter) {
|
|
53
53
|
if (env.recorder().joins()) return env.recorder().join(ops, adapter);
|
|
54
|
-
const journal = activeJournal?.() ?? null;
|
|
54
|
+
const journal = env.openJournal() ?? activeJournal?.() ?? null;
|
|
55
55
|
env.driving(() => {
|
|
56
56
|
const bound = ops.map((op) => causing(op, bindOpToAdapter(op, adapter)));
|
|
57
57
|
if (journal) journal.applyBatch(bound, label);
|
|
@@ -64,7 +64,7 @@ function createOuterOps(env) {
|
|
|
64
64
|
setActiveJournalAccessor(fn) {
|
|
65
65
|
activeJournal = fn;
|
|
66
66
|
},
|
|
67
|
-
activeJournal: () => activeJournal?.() ?? null
|
|
67
|
+
activeJournal: () => env.openJournal() ?? activeJournal?.() ?? null
|
|
68
68
|
};
|
|
69
69
|
}
|
|
70
70
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"outerOps.js","names":["rebuildGlobalOp"],"sources":["../../../src/core/scene/outerOps.ts"],"sourcesContent":["/**\n * Ops built outside the scene and handed to `applyBatch` or `scene.history`.\n *\n * An undo entry holds the scene's own ops, so an outer op is recorded as the\n * kit ops it caused. One that wrote no scene state is recorded as itself, and\n * needs an adapter to replay through: the one it was applied with, or after a\n * reload the one `setHistoryAdapter` supplies.\n */\nimport type { History, Journal } from '@weasel-js/history';\nimport type { Op } from 'core/ops/types';\nimport { rebuildOp as rebuildGlobalOp } from 'core/ops/registry';\nimport { bindOpToAdapter, bindOpToHistoryAdapter } from './boundOps';\nimport type { Recorder } from './recorder';\n\n/** What the outer ops need from the scene they are applied to. */\nexport interface OuterOpsEnv {\n /** The scene's own op for a kind it has registered, or `null`. */\n own(name: string, args: unknown): Op | null;\n /** The scene's recorder, built after this is. */\n recorder(): Pick<Recorder, 'capturing' | 'joins' | 'join'>;\n /** Run a step that applies ops through a history engine: all-or-nothing,\n * unrecorded by the scene, with subscribers called once after. */\n driving(step: () => void): void;\n /** Where `applyBatch` looks for an open journal until an accessor is set. */\n getActiveJournal: (() => Journal | null) | null;\n}\n\nexport interface OuterOps {\n /** The op a restored history names: the scene's own, or an outer op bound\n * to the adapter `setHistoryAdapter` supplies. `null` when nothing knows\n * the name, and the engine places its no-op placeholder. */\n rebuild(name: string, args: unknown): Op | null;\n /** `op` as an engine applies it: its entry records the kit ops it caused,\n * or the op itself when it caused none. */\n causing(op: Op): Op;\n /** Apply `ops` against `adapter` as one entry: part of the open `batch`\n * when there is one, else on the active journal, else on `engine`. */\n apply(engine: History, ops: Op[], label: string, adapter: unknown): void;\n setHistoryAdapter(fn: (() => unknown) | null): void;\n setActiveJournalAccessor(fn: (() => Journal | null) | null): void;\n /** The journal edits go to now, or null for the scene's own history. */\n activeJournal(): Journal | null;\n}\n\n/** A kit op under the key of the outer op that caused it, so two edits merge\n * when their outer ops would have. */\nfunction keyed(kit: Op, outerKey: string | undefined): Op {\n const key = outerKey === undefined || kit.coalesceKey === undefined ? undefined : `${outerKey}>${kit.coalesceKey}`;\n return { name: kit.name, args: kit.args, coalesceKey: key, apply: kit.apply, invert: kit.invert };\n}\n\nexport function createOuterOps(env: OuterOpsEnv): OuterOps {\n let historyAdapter: (() => unknown) | null = null;\n let activeJournal = env.getActiveJournal;\n\n function causing(op: Op, given: Op = op): Op {\n if (op.caused) return op;\n let caused: Op[] = [given];\n return {\n get name() { return op.name; },\n get args() { return op.args; },\n label: op.label,\n // Read when asked: an arrange op has no key until it has applied.\n get coalesceKey() { return op.coalesceKey; },\n elideNoop: op.elideNoop,\n apply(adapter) {\n const { result, ops } = env.recorder().capturing(() => given.apply(adapter));\n if (ops.length > 0) caused = ops.map((kit) => keyed(kit, op.coalesceKey));\n return result;\n },\n caused: () => caused,\n invert: () => given.invert(),\n };\n }\n\n return {\n rebuild(name, args) {\n const own = env.own(name, args);\n if (own !== null) return own;\n const rebuilt = rebuildGlobalOp(name, args);\n return rebuilt === null ? null : bindOpToHistoryAdapter(rebuilt, () => historyAdapter);\n },\n causing: (op) => causing(op),\n apply(engine, ops, label, adapter) {\n if (env.recorder().joins()) return env.recorder().join(ops, adapter);\n const journal = activeJournal?.() ?? null;\n env.driving(() => {\n // Neither engine was built with an adapter, so an op is bound to\n // this call's before either applies or stores it.\n const bound = ops.map((op) => causing(op, bindOpToAdapter(op, adapter)));\n if (journal) journal.applyBatch(bound, label);\n else engine.applyOps(bound, label);\n });\n },\n setHistoryAdapter(fn) {\n historyAdapter = fn;\n },\n setActiveJournalAccessor(fn) {\n activeJournal = fn;\n },\n activeJournal: () => activeJournal?.() ?? null,\n };\n}\n"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"outerOps.js","names":["rebuildGlobalOp"],"sources":["../../../src/core/scene/outerOps.ts"],"sourcesContent":["/**\n * Ops built outside the scene and handed to `applyBatch` or `scene.history`.\n *\n * An undo entry holds the scene's own ops, so an outer op is recorded as the\n * kit ops it caused. One that wrote no scene state is recorded as itself, and\n * needs an adapter to replay through: the one it was applied with, or after a\n * reload the one `setHistoryAdapter` supplies.\n */\nimport type { History, Journal } from '@weasel-js/history';\nimport type { Op } from 'core/ops/types';\nimport { rebuildOp as rebuildGlobalOp } from 'core/ops/registry';\nimport { bindOpToAdapter, bindOpToHistoryAdapter } from './boundOps';\nimport type { Recorder } from './recorder';\n\n/** What the outer ops need from the scene they are applied to. */\nexport interface OuterOpsEnv {\n /** The scene's own op for a kind it has registered, or `null`. */\n own(name: string, args: unknown): Op | null;\n /** The scene's recorder, built after this is. */\n recorder(): Pick<Recorder, 'capturing' | 'joins' | 'join'>;\n /** Run a step that applies ops through a history engine: all-or-nothing,\n * unrecorded by the scene, with subscribers called once after. */\n driving(step: () => void): void;\n /** Where `applyBatch` looks for an open journal until an accessor is set. */\n getActiveJournal: (() => Journal | null) | null;\n /** The innermost journal opened through `scene.history`, which edits go to\n * ahead of the accessor's: it is opened inside that one, if anything. */\n openJournal(): Journal | null;\n}\n\nexport interface OuterOps {\n /** The op a restored history names: the scene's own, or an outer op bound\n * to the adapter `setHistoryAdapter` supplies. `null` when nothing knows\n * the name, and the engine places its no-op placeholder. */\n rebuild(name: string, args: unknown): Op | null;\n /** `op` as an engine applies it: its entry records the kit ops it caused,\n * or the op itself when it caused none. */\n causing(op: Op): Op;\n /** Apply `ops` against `adapter` as one entry: part of the open `batch`\n * when there is one, else on the active journal, else on `engine`. */\n apply(engine: History, ops: Op[], label: string, adapter: unknown): void;\n setHistoryAdapter(fn: (() => unknown) | null): void;\n setActiveJournalAccessor(fn: (() => Journal | null) | null): void;\n /** The journal edits go to now, or null for the scene's own history. */\n activeJournal(): Journal | null;\n}\n\n/** A kit op under the key of the outer op that caused it, so two edits merge\n * when their outer ops would have. */\nfunction keyed(kit: Op, outerKey: string | undefined): Op {\n const key = outerKey === undefined || kit.coalesceKey === undefined ? undefined : `${outerKey}>${kit.coalesceKey}`;\n return { name: kit.name, args: kit.args, coalesceKey: key, apply: kit.apply, invert: kit.invert };\n}\n\nexport function createOuterOps(env: OuterOpsEnv): OuterOps {\n let historyAdapter: (() => unknown) | null = null;\n let activeJournal = env.getActiveJournal;\n\n function causing(op: Op, given: Op = op): Op {\n if (op.caused) return op;\n let caused: Op[] = [given];\n return {\n get name() { return op.name; },\n get args() { return op.args; },\n label: op.label,\n // Read when asked: an arrange op has no key until it has applied.\n get coalesceKey() { return op.coalesceKey; },\n elideNoop: op.elideNoop,\n apply(adapter) {\n const { result, ops } = env.recorder().capturing(() => given.apply(adapter));\n if (ops.length > 0) caused = ops.map((kit) => keyed(kit, op.coalesceKey));\n return result;\n },\n caused: () => caused,\n invert: () => given.invert(),\n };\n }\n\n return {\n rebuild(name, args) {\n const own = env.own(name, args);\n if (own !== null) return own;\n const rebuilt = rebuildGlobalOp(name, args);\n return rebuilt === null ? null : bindOpToHistoryAdapter(rebuilt, () => historyAdapter);\n },\n causing: (op) => causing(op),\n apply(engine, ops, label, adapter) {\n if (env.recorder().joins()) return env.recorder().join(ops, adapter);\n const journal = env.openJournal() ?? activeJournal?.() ?? null;\n env.driving(() => {\n // Neither engine was built with an adapter, so an op is bound to\n // this call's before either applies or stores it.\n const bound = ops.map((op) => causing(op, bindOpToAdapter(op, adapter)));\n if (journal) journal.applyBatch(bound, label);\n else engine.applyOps(bound, label);\n });\n },\n setHistoryAdapter(fn) {\n historyAdapter = fn;\n },\n setActiveJournalAccessor(fn) {\n activeJournal = fn;\n },\n activeJournal: () => env.openJournal() ?? activeJournal?.() ?? null,\n };\n}\n"],"mappings":";;;;;;AAiDA,SAAS,MAAM,KAAS,UAAkC;CACxD,MAAM,MAAM,aAAa,UAAa,IAAI,gBAAgB,SAAY,SAAY,GAAG,SAAS,GAAG,IAAI;CACrG,OAAO;EAAE,MAAM,IAAI;EAAM,MAAM,IAAI;EAAM,aAAa;EAAK,OAAO,IAAI;EAAO,QAAQ,IAAI;CAAO;AAClG;AAEA,SAAgB,eAAe,KAA4B;CACzD,IAAI,iBAAyC;CAC7C,IAAI,gBAAgB,IAAI;CAExB,SAAS,QAAQ,IAAQ,QAAY,IAAQ;EAC3C,IAAI,GAAG,QAAQ,OAAO;EACtB,IAAI,SAAe,CAAC,KAAK;EACzB,OAAO;GACL,IAAI,OAAO;IAAE,OAAO,GAAG;GAAM;GAC7B,IAAI,OAAO;IAAE,OAAO,GAAG;GAAM;GAC7B,OAAO,GAAG;GAEV,IAAI,cAAc;IAAE,OAAO,GAAG;GAAa;GAC3C,WAAW,GAAG;GACd,MAAM,SAAS;IACb,MAAM,EAAE,QAAQ,QAAQ,IAAI,SAAS,CAAC,CAAC,gBAAgB,MAAM,MAAM,OAAO,CAAC;IAC3E,IAAI,IAAI,SAAS,GAAG,SAAS,IAAI,KAAK,QAAQ,MAAM,KAAK,GAAG,WAAW,CAAC;IACxE,OAAO;GACT;GACA,cAAc;GACd,cAAc,MAAM,OAAO;EAC7B;CACF;CAEA,OAAO;EACL,QAAQ,MAAM,MAAM;GAClB,MAAM,MAAM,IAAI,IAAI,MAAM,IAAI;GAC9B,IAAI,QAAQ,MAAM,OAAO;GACzB,MAAM,UAAUA,UAAgB,MAAM,IAAI;GAC1C,OAAO,YAAY,OAAO,OAAO,uBAAuB,eAAe,cAAc;EACvF;EACA,UAAU,OAAO,QAAQ,EAAE;EAC3B,MAAM,QAAQ,KAAK,OAAO,SAAS;GACjC,IAAI,IAAI,SAAS,CAAC,CAAC,MAAM,GAAG,OAAO,IAAI,SAAS,CAAC,CAAC,KAAK,KAAK,OAAO;GACnE,MAAM,UAAU,IAAI,YAAY,KAAK,gBAAgB,KAAK;GAC1D,IAAI,cAAc;IAGhB,MAAM,QAAQ,IAAI,KAAK,OAAO,QAAQ,IAAI,gBAAgB,IAAI,OAAO,CAAC,CAAC;IACvE,IAAI,SAAS,QAAQ,WAAW,OAAO,KAAK;SACvC,OAAO,SAAS,OAAO,KAAK;GACnC,CAAC;EACH;EACA,kBAAkB,IAAI;GACpB,iBAAiB;EACnB;EACA,yBAAyB,IAAI;GAC3B,gBAAgB;EAClB;EACA,qBAAqB,IAAI,YAAY,KAAK,gBAAgB,KAAK;CACjE;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"poseRemovals.js","names":[],"sources":["../../../src/core/scene/poseRemovals.ts"],"sourcesContent":["/**\n * The removal half of a scene's pose-write report: each node a change takes\n * off screen, read as it was shown just before it went.\n */\nimport type { PoseRemoval, PoseWrite, RemovedNode } from './poseWrites';\nimport type { NodeId } from './types';\n\ninterface Parented { id: NodeId; parent: NodeId | null }\n\nexport interface RemovalLog<TPose, TNode> {\n /** Read what removing `roots` and their descendants among `nodes` takes off\n * screen, before the removal runs; the returned call records it once it\n * has. A node `pending` holds as added by this change is dropped from it\n * and goes unreported. */\n seeOff(\n roots: readonly Parented[],\n nodes: readonly Parented[],\n how: Pick<PoseRemoval<TPose, TNode>, 'origin' | 'label'>,\n pending: Map<NodeId, PoseWrite<TPose>>,\n ): () => void;\n /** Forget `id`'s removal — it is back — and answer where it was shown. */\n takeBack(id: NodeId): TPose | undefined;\n /** The removals since the last drain, a child removed with its parent\n * carried beneath it. */\n drain(): PoseRemoval<TPose, TNode>[];\n}\n\nexport function createRemovalLog<TPose, TNode>(\n shown: (id: NodeId) => { node: TNode; pose: TPose } | undefined,\n): RemovalLog<TPose, TNode> {\n const removals = new Map<NodeId, PoseRemoval<TPose, TNode>>();\n return {\n seeOff(roots, nodes, how, pending) {\n const under = new Map<NodeId | null, Parented[]>();\n for (const n of nodes) {\n const list = under.get(n.parent);\n if (list) list.push(n);\n else under.set(n.parent, [n]);\n }\n const removedNode = (n: Parented): RemovedNode<TPose, TNode> | null => {\n const s = shown(n.id);\n if (!s) return null;\n const children = (under.get(n.id) ?? []).map(removedNode).filter((c) => c !== null);\n return { node: s.node, pose: s.pose, children };\n };\n const seen = roots.map((r) => [r, removedNode(r)] as const);\n return () => {\n const added = new Set<NodeId>();\n for (const n of nodes) {\n const w = pending.get(n.id);\n if (w && w.from === undefined) added.add(n.id);\n pending.delete(n.id);\n }\n for (const [r, removed] of seen) {\n if (removed && !added.has(r.id)) removals.set(r.id, { id: r.id, parent: r.parent, ...removed, ...how });\n }\n };\n },\n takeBack(id) {\n const gone = removals.get(id);\n removals.delete(id);\n return gone?.pose;\n },\n drain() {\n // Undoing a group's add takes its children first, each its own root.\n const out: PoseRemoval<TPose, TNode>[] = [];\n for (const r of removals.values()) {\n const up = r.parent !== null ? removals.get(r.parent) : undefined;\n if (up) (up.children as RemovedNode<TPose, TNode>[]).push(r);\n else out.push(r);\n }\n removals.clear();\n return out;\n },\n };\n}\n"],"mappings":";AA2BA,SAAgB,iBACd,OAC0B;CAC1B,MAAM,2BAAW,IAAI,IAAuC;CAC5D,OAAO;EACL,OAAO,OAAO,OAAO,KAAK,SAAS;GACjC,MAAM,wBAAQ,IAAI,IAA+B;GACjD,KAAK,MAAM,KAAK,OAAO;IACrB,MAAM,OAAO,MAAM,IAAI,EAAE,MAAM;IAC/B,IAAI,MAAM,KAAK,KAAK,CAAC;SAChB,MAAM,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC;GAC9B;GACA,MAAM,eAAe,MAAkD;IACrE,MAAM,IAAI,MAAM,EAAE,EAAE;IACpB,IAAI,CAAC,GAAG,OAAO;IACf,MAAM,YAAY,MAAM,IAAI,EAAE,EAAE,KAAK,CAAC,EAAA,CAAG,IAAI,WAAW,CAAC,CAAC,QAAQ,MAAM,MAAM,IAAI;IAClF,OAAO;KAAE,MAAM,EAAE;KAAM,MAAM,EAAE;KAAM;IAAS;GAChD;GACA,MAAM,OAAO,MAAM,KAAK,MAAM,CAAC,GAAG,YAAY,CAAC,CAAC,CAAU;GAC1D,aAAa;IACX,MAAM,wBAAQ,IAAI,IAAY;IAC9B,KAAK,MAAM,KAAK,OAAO;KACrB,MAAM,IAAI,QAAQ,IAAI,EAAE,EAAE;KAC1B,IAAI,KAAK,EAAE,SAAS,QAAW,MAAM,IAAI,EAAE,EAAE;KAC7C,QAAQ,OAAO,EAAE,EAAE;IACrB;IACA,KAAK,MAAM,CAAC,GAAG,YAAY,MACzB,IAAI,WAAW,CAAC,MAAM,IAAI,EAAE,EAAE,GAAG,SAAS,IAAI,EAAE,IAAI;KAAE,IAAI,EAAE;KAAI,QAAQ,EAAE;KAAQ,GAAG;KAAS,GAAG;IAAI,CAAC;GAE1G;EACF;EACA,SAAS,IAAI;GACX,MAAM,OAAO,SAAS,IAAI,EAAE;GAC5B,SAAS,OAAO,EAAE;GAClB,OAAO,MAAM;EACf;EACA,QAAQ;
|
|
1
|
+
{"version":3,"file":"poseRemovals.js","names":[],"sources":["../../../src/core/scene/poseRemovals.ts"],"sourcesContent":["/**\n * The removal half of a scene's pose-write report: each node a change takes\n * off screen, read as it was shown just before it went.\n */\nimport type { PoseRemoval, PoseWrite, RemovedNode } from './poseWrites';\nimport type { NodeId } from './types';\n\ninterface Parented { id: NodeId; parent: NodeId | null }\n\nexport interface RemovalLog<TPose, TNode> {\n /** Read what removing `roots` and their descendants among `nodes` takes off\n * screen, before the removal runs; the returned call records it once it\n * has. A node `pending` holds as added by this change is dropped from it\n * and goes unreported. */\n seeOff(\n roots: readonly Parented[],\n nodes: readonly Parented[],\n how: Pick<PoseRemoval<TPose, TNode>, 'origin' | 'label'>,\n pending: Map<NodeId, PoseWrite<TPose>>,\n ): () => void;\n /** Forget `id`'s removal — it is back — and answer where it was shown. */\n takeBack(id: NodeId): TPose | undefined;\n /** The removals since the last drain, a child removed with its parent\n * carried beneath it. */\n drain(): PoseRemoval<TPose, TNode>[];\n}\n\nexport function createRemovalLog<TPose, TNode>(\n shown: (id: NodeId) => { node: TNode; pose: TPose } | undefined,\n): RemovalLog<TPose, TNode> {\n const removals = new Map<NodeId, PoseRemoval<TPose, TNode>>();\n return {\n seeOff(roots, nodes, how, pending) {\n const under = new Map<NodeId | null, Parented[]>();\n for (const n of nodes) {\n const list = under.get(n.parent);\n if (list) list.push(n);\n else under.set(n.parent, [n]);\n }\n const removedNode = (n: Parented): RemovedNode<TPose, TNode> | null => {\n const s = shown(n.id);\n if (!s) return null;\n const children = (under.get(n.id) ?? []).map(removedNode).filter((c) => c !== null);\n return { node: s.node, pose: s.pose, children };\n };\n const seen = roots.map((r) => [r, removedNode(r)] as const);\n return () => {\n const added = new Set<NodeId>();\n for (const n of nodes) {\n const w = pending.get(n.id);\n if (w && w.from === undefined) added.add(n.id);\n pending.delete(n.id);\n }\n for (const [r, removed] of seen) {\n if (removed && !added.has(r.id)) removals.set(r.id, { id: r.id, parent: r.parent, ...removed, ...how });\n }\n };\n },\n takeBack(id) {\n const gone = removals.get(id);\n removals.delete(id);\n return gone?.pose;\n },\n drain() {\n if (removals.size === 0) return [];\n // Undoing a group's add takes its children first, each its own root.\n const out: PoseRemoval<TPose, TNode>[] = [];\n for (const r of removals.values()) {\n const up = r.parent !== null ? removals.get(r.parent) : undefined;\n if (up) (up.children as RemovedNode<TPose, TNode>[]).push(r);\n else out.push(r);\n }\n removals.clear();\n return out;\n },\n };\n}\n"],"mappings":";AA2BA,SAAgB,iBACd,OAC0B;CAC1B,MAAM,2BAAW,IAAI,IAAuC;CAC5D,OAAO;EACL,OAAO,OAAO,OAAO,KAAK,SAAS;GACjC,MAAM,wBAAQ,IAAI,IAA+B;GACjD,KAAK,MAAM,KAAK,OAAO;IACrB,MAAM,OAAO,MAAM,IAAI,EAAE,MAAM;IAC/B,IAAI,MAAM,KAAK,KAAK,CAAC;SAChB,MAAM,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC;GAC9B;GACA,MAAM,eAAe,MAAkD;IACrE,MAAM,IAAI,MAAM,EAAE,EAAE;IACpB,IAAI,CAAC,GAAG,OAAO;IACf,MAAM,YAAY,MAAM,IAAI,EAAE,EAAE,KAAK,CAAC,EAAA,CAAG,IAAI,WAAW,CAAC,CAAC,QAAQ,MAAM,MAAM,IAAI;IAClF,OAAO;KAAE,MAAM,EAAE;KAAM,MAAM,EAAE;KAAM;IAAS;GAChD;GACA,MAAM,OAAO,MAAM,KAAK,MAAM,CAAC,GAAG,YAAY,CAAC,CAAC,CAAU;GAC1D,aAAa;IACX,MAAM,wBAAQ,IAAI,IAAY;IAC9B,KAAK,MAAM,KAAK,OAAO;KACrB,MAAM,IAAI,QAAQ,IAAI,EAAE,EAAE;KAC1B,IAAI,KAAK,EAAE,SAAS,QAAW,MAAM,IAAI,EAAE,EAAE;KAC7C,QAAQ,OAAO,EAAE,EAAE;IACrB;IACA,KAAK,MAAM,CAAC,GAAG,YAAY,MACzB,IAAI,WAAW,CAAC,MAAM,IAAI,EAAE,EAAE,GAAG,SAAS,IAAI,EAAE,IAAI;KAAE,IAAI,EAAE;KAAI,QAAQ,EAAE;KAAQ,GAAG;KAAS,GAAG;IAAI,CAAC;GAE1G;EACF;EACA,SAAS,IAAI;GACX,MAAM,OAAO,SAAS,IAAI,EAAE;GAC5B,SAAS,OAAO,EAAE;GAClB,OAAO,MAAM;EACf;EACA,QAAQ;GACN,IAAI,SAAS,SAAS,GAAG,OAAO,CAAC;GAEjC,MAAM,MAAmC,CAAC;GAC1C,KAAK,MAAM,KAAK,SAAS,OAAO,GAAG;IACjC,MAAM,KAAK,EAAE,WAAW,OAAO,SAAS,IAAI,EAAE,MAAM,IAAI;IACxD,IAAI,IAAI,GAAI,SAAyC,KAAK,CAAC;SACtD,IAAI,KAAK,CAAC;GACjB;GACA,SAAS,MAAM;GACf,OAAO;EACT;CACF;AACF"}
|
|
@@ -8,11 +8,13 @@ import { createEmitter, deepEqual } from "@weasel-js/registry";
|
|
|
8
8
|
* from, handed to `onPoseWrite` once the change reaches the scene's
|
|
9
9
|
* subscribers.
|
|
10
10
|
*/
|
|
11
|
+
var NOTHING = () => {};
|
|
11
12
|
function createPoseWriteLog(env) {
|
|
12
13
|
const listeners = createEmitter();
|
|
13
14
|
const pending = /* @__PURE__ */ new Map();
|
|
14
15
|
const removals = createRemovalLog(env.shown);
|
|
15
|
-
let
|
|
16
|
+
let editLabel = null;
|
|
17
|
+
let editVia = "call";
|
|
16
18
|
let replays = 0;
|
|
17
19
|
let layouts = 0;
|
|
18
20
|
let quiets = 0;
|
|
@@ -22,9 +24,9 @@ function createPoseWriteLog(env) {
|
|
|
22
24
|
if (replays > 0) return { origin: reverted ? "undo" : "redo" };
|
|
23
25
|
if (layouts > 0) return { origin: "layout" };
|
|
24
26
|
if (env.untracked()) return { origin: "untracked" };
|
|
25
|
-
return
|
|
26
|
-
origin:
|
|
27
|
-
label:
|
|
27
|
+
return editLabel !== null ? {
|
|
28
|
+
origin: editVia,
|
|
29
|
+
label: editLabel
|
|
28
30
|
} : { origin: "call" };
|
|
29
31
|
}
|
|
30
32
|
function noteSet(id, from, to, reverted) {
|
|
@@ -47,7 +49,7 @@ function createPoseWriteLog(env) {
|
|
|
47
49
|
...stamp(reverted)
|
|
48
50
|
});
|
|
49
51
|
}
|
|
50
|
-
const seeOff = (roots, nodes, reverted) => collecting() ? removals.seeOff(roots, nodes, stamp(reverted), pending) :
|
|
52
|
+
const seeOff = (roots, nodes, reverted) => collecting() ? removals.seeOff(roots, nodes, stamp(reverted), pending) : NOTHING;
|
|
51
53
|
function around(fn, enter, leave) {
|
|
52
54
|
enter();
|
|
53
55
|
try {
|
|
@@ -74,7 +76,7 @@ function createPoseWriteLog(env) {
|
|
|
74
76
|
...h,
|
|
75
77
|
apply: (p) => {
|
|
76
78
|
h.apply(p);
|
|
77
|
-
noteAdded([p], false);
|
|
79
|
+
if (collecting()) noteAdded([p], false);
|
|
78
80
|
},
|
|
79
81
|
revert: (p) => {
|
|
80
82
|
const done = seeOff([p], [p], true);
|
|
@@ -117,15 +119,14 @@ function createPoseWriteLog(env) {
|
|
|
117
119
|
};
|
|
118
120
|
},
|
|
119
121
|
labeled(label, via, fn) {
|
|
120
|
-
if (
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
});
|
|
122
|
+
if (editLabel !== null) return fn();
|
|
123
|
+
editLabel = label;
|
|
124
|
+
editVia = via;
|
|
125
|
+
try {
|
|
126
|
+
return fn();
|
|
127
|
+
} finally {
|
|
128
|
+
editLabel = null;
|
|
129
|
+
}
|
|
129
130
|
},
|
|
130
131
|
replaying: (fn) => around(fn, () => {
|
|
131
132
|
replays++;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"poseWrites.js","names":[],"sources":["../../../src/core/scene/poseWrites.ts"],"sourcesContent":["/**\n * What a scene reports about the poses it writes: each change's net pose per\n * node, the nodes it removed as they were last shown, and where each came\n * from, handed to `onPoseWrite` once the change reaches the scene's\n * subscribers.\n */\nimport { createEmitter, deepEqual } from '@weasel-js/registry';\nimport { createRemovalLog } from './poseRemovals';\nimport type { RegisteredOp } from './types';\nimport type { LayoutMove } from './layoutTypes';\nimport type { NodeId } from '@weasel-js/routing';\n\n/**\n * Where a reported pose write came from.\n *\n * - `'call'` — a scene method called directly, alone or inside `scene.batch`:\n * a button's `scene.setPose`, a `scene.add`.\n * - `'ops'` — ops applied as one edit through `applyBatch` or `history.apply`,\n * which is how actions commit: a drag's release, a resize, a nudge, an align.\n * - `'undo'` / `'redo'` — a history step, a jump through the history included.\n * - `'layout'` — a container's layout arranging what a live edit changed; also\n * reported to `onReflow`.\n * - `'untracked'` — a write inside `scene.untracked`, such as a pose target\n * committing an animation's destination.\n */\nexport type PoseWriteOrigin = 'call' | 'ops' | 'undo' | 'redo' | 'layout' | 'untracked';\n\n/** One node's pose as a scene change left it, in the node's stored frame. */\nexport interface PoseWrite<TPose> {\n id: NodeId;\n /** The pose before the change; `undefined` for a node the change added. */\n from: TPose | undefined;\n to: TPose;\n origin: PoseWriteOrigin;\n /** The edit's label (`'setPose'`, a batch's, an action's `'Move'`) for\n * `'call'` and `'ops'`; `undefined` otherwise. */\n label?: string;\n}\n\n/** A removed node as it was last shown, with the descendants it took along. */\nexport interface RemovedNode<TPose, TNode = unknown> {\n /** The node as it stood before the change removed it. */\n node: TNode;\n /** Where it was shown, override included, in its stored frame. */\n pose: TPose;\n children: readonly RemovedNode<TPose, TNode>[];\n}\n\n/** One node a scene change removed, a removal's root: its descendants ride\n * beneath it rather than being reported apart. */\nexport interface PoseRemoval<TPose, TNode = unknown> extends RemovedNode<TPose, TNode> {\n id: NodeId;\n /** The parent it was removed from, which may still be in the scene. */\n parent: NodeId | null;\n origin: PoseWriteOrigin;\n label?: string;\n}\n\n/** What a scene reports about the poses its edits write. */\nexport interface ScenePoseReports<TPose, TNode = unknown> {\n /** Called with the poses each live edit's layout pass wrote, after the\n * edit lands — never for an undo or redo, which restore poses rather than\n * arrange them. What a reflow animation glides from. */\n onReflow(listener: (moves: readonly LayoutMove<TPose>[]) => void): () => void;\n /** Called once per change, before the scene's subscribers hear of it, with\n * every node whose pose the change wrote or that it added, and every node\n * it removed — an undone add among them. A node written back to the pose\n * it had, or both added and removed by the change, is left out of both.\n * Not called for `loadState` or the scene's initial nodes. */\n onPoseWrite(\n listener: (writes: readonly PoseWrite<TPose>[], removed: readonly PoseRemoval<TPose, TNode>[]) => void,\n ): () => void;\n}\n\n/** What the log reads of the scene it reports for. */\nexport interface PoseWriteEnv<TPose, TNode> {\n /** A node still in the scene, with the pose it is shown at. */\n shown(id: NodeId): { node: TNode; pose: TPose } | undefined;\n /** Whether the edit running now is inside `scene.untracked`. */\n untracked(): boolean;\n /** The scene's change notice. The log reports on it, subscribed before\n * anyone else so a change's writes arrive ahead of its re-reads. */\n changes: { subscribe(listener: () => void): () => void };\n}\n\nexport interface PoseWriteLog<TPose, TNode = unknown> {\n subscribe: ScenePoseReports<TPose, TNode>['onPoseWrite'];\n /** Run `fn` as an edit labeled `label`; an enclosing edit's label stands. */\n labeled<T>(label: string, via: 'call' | 'ops', fn: () => T): T;\n /** Run `fn` as a history step replaying recorded ops. */\n replaying<T>(fn: () => T): T;\n /** Run `fn` as a layout pass writing arranged poses. */\n layout<T>(fn: () => T): T;\n /** Run `fn` with nothing it writes reported. */\n quiet<T>(fn: () => T): T;\n /** `register` with the kit's pose-bearing ops reporting what they write. */\n reporting(register: (kind: string, handler: RegisteredOp<unknown>) => void):\n (kind: string, handler: RegisteredOp<unknown>) => void;\n}\n\ninterface Posed<TPose> { id: NodeId; pose: TPose }\ninterface Parented { id: NodeId; parent: NodeId | null }\n\nexport function createPoseWriteLog<TPose, TNode = unknown>(env: PoseWriteEnv<TPose, TNode>): PoseWriteLog<TPose, TNode> {\n const listeners = createEmitter<[writes: readonly PoseWrite<TPose>[], removed: readonly PoseRemoval<TPose, TNode>[]]>();\n const pending = new Map<NodeId, PoseWrite<TPose>>();\n const removals = createRemovalLog<TPose, TNode>(env.shown);\n let edit: { label: string; via: 'call' | 'ops' } | null = null;\n let replays = 0;\n let layouts = 0;\n let quiets = 0;\n let listening = 0;\n\n const collecting = (): boolean => listening > 0 && quiets === 0;\n\n function stamp(reverted: boolean): Pick<PoseWrite<TPose>, 'origin' | 'label'> {\n if (replays > 0) return { origin: reverted ? 'undo' : 'redo' };\n if (layouts > 0) return { origin: 'layout' };\n if (env.untracked()) return { origin: 'untracked' };\n return edit ? { origin: edit.via, label: edit.label } : { origin: 'call' };\n }\n\n function noteSet(id: NodeId, from: TPose, to: TPose, reverted: boolean): void {\n if (!collecting()) return;\n const held = pending.get(id);\n if (held) held.to = to;\n else pending.set(id, { id, from, to, ...stamp(reverted) });\n }\n\n function noteAdded(nodes: readonly Posed<TPose>[], reverted: boolean): void {\n if (!collecting()) return;\n for (const n of nodes) {\n // Removed and back within one change: it moved, if anything.\n pending.set(n.id, { id: n.id, from: removals.takeBack(n.id), to: n.pose, ...stamp(reverted) });\n }\n }\n\n const seeOff = (roots: readonly Parented[], nodes: readonly Parented[], reverted: boolean): () => void =>\n collecting() ? removals.seeOff(roots, nodes, stamp(reverted), pending) : () => {};\n\n function around<T>(fn: () => T, enter: () => void, leave: () => void): T {\n enter();\n try {\n return fn();\n } finally {\n leave();\n }\n }\n\n type SetPayload = { id: NodeId; from: TPose; to: TPose };\n type AddPayload = Posed<TPose> & Parented;\n type RemovePayload = { nodes: readonly (Posed<TPose> & Parented)[]; detached: readonly Parented[] };\n const wrap: Record<string, (h: RegisteredOp<unknown>) => RegisteredOp<unknown>> = {\n 'kit:setPose': (h) => ({\n ...h,\n apply: (p) => { h.apply(p); const s = p as SetPayload; noteSet(s.id, s.from, s.to, false); },\n revert: (p) => { h.revert(p); const s = p as SetPayload; noteSet(s.id, s.to, s.from, true); },\n }),\n 'kit:add': (h) => ({\n ...h,\n apply: (p) => { h.apply(p); noteAdded([p as AddPayload], false); },\n revert: (p) => { const done = seeOff([p as AddPayload], [p as AddPayload], true); h.revert(p); done(); },\n }),\n 'kit:remove': (h) => ({\n ...h,\n apply: (p) => {\n const r = p as RemovePayload;\n const done = seeOff(r.detached, r.nodes, false);\n h.apply(p);\n done();\n },\n revert: (p) => { h.revert(p); noteAdded((p as RemovePayload).nodes, true); },\n }),\n };\n\n function flush(): void {\n const removed = removals.drain();\n if (pending.size === 0 && removed.length === 0) return;\n const writes = [...pending.values()].filter((w) => !deepEqual(w.from, w.to));\n pending.clear();\n if (writes.length > 0 || removed.length > 0) listeners.emit(writes, removed);\n }\n env.changes.subscribe(flush);\n\n return {\n subscribe(listener) {\n const off = listeners.subscribe(listener);\n listening++;\n let on = true;\n return () => {\n if (!on) return;\n on = false;\n listening--;\n off();\n };\n },\n labeled(label, via, fn) {\n if (edit) return fn();\n return around(fn, () => { edit = { label, via }; }, () => { edit = null; });\n },\n replaying: (fn) => around(fn, () => { replays++; }, () => { replays--; }),\n layout: (fn) => around(fn, () => { layouts++; }, () => { layouts--; }),\n quiet: (fn) => around(fn, () => { quiets++; }, () => { quiets--; }),\n reporting: (register) => (kind, handler) => register(kind, wrap[kind]?.(handler) ?? handler),\n };\n}\n"],"mappings":";;;;;;;;;;AAuGA,SAAgB,mBAA2C,KAA6D;CACtH,MAAM,YAAY,cAAoG;CACtH,MAAM,0BAAU,IAAI,IAA8B;CAClD,MAAM,WAAW,iBAA+B,IAAI,KAAK;CACzD,IAAI,OAAsD;CAC1D,IAAI,UAAU;CACd,IAAI,UAAU;CACd,IAAI,SAAS;CACb,IAAI,YAAY;CAEhB,MAAM,mBAA4B,YAAY,KAAK,WAAW;CAE9D,SAAS,MAAM,UAA+D;EAC5E,IAAI,UAAU,GAAG,OAAO,EAAE,QAAQ,WAAW,SAAS,OAAO;EAC7D,IAAI,UAAU,GAAG,OAAO,EAAE,QAAQ,SAAS;EAC3C,IAAI,IAAI,UAAU,GAAG,OAAO,EAAE,QAAQ,YAAY;EAClD,OAAO,OAAO;GAAE,QAAQ,KAAK;GAAK,OAAO,KAAK;EAAM,IAAI,EAAE,QAAQ,OAAO;CAC3E;CAEA,SAAS,QAAQ,IAAY,MAAa,IAAW,UAAyB;EAC5E,IAAI,CAAC,WAAW,GAAG;EACnB,MAAM,OAAO,QAAQ,IAAI,EAAE;EAC3B,IAAI,MAAM,KAAK,KAAK;OACf,QAAQ,IAAI,IAAI;GAAE;GAAI;GAAM;GAAI,GAAG,MAAM,QAAQ;EAAE,CAAC;CAC3D;CAEA,SAAS,UAAU,OAAgC,UAAyB;EAC1E,IAAI,CAAC,WAAW,GAAG;EACnB,KAAK,MAAM,KAAK,OAEd,QAAQ,IAAI,EAAE,IAAI;GAAE,IAAI,EAAE;GAAI,MAAM,SAAS,SAAS,EAAE,EAAE;GAAG,IAAI,EAAE;GAAM,GAAG,MAAM,QAAQ;EAAE,CAAC;CAEjG;CAEA,MAAM,UAAU,OAA4B,OAA4B,aACtE,WAAW,IAAI,SAAS,OAAO,OAAO,OAAO,MAAM,QAAQ,GAAG,OAAO,UAAU,CAAC;CAElF,SAAS,OAAU,IAAa,OAAmB,OAAsB;EACvE,MAAM;EACN,IAAI;GACF,OAAO,GAAG;EACZ,UAAU;GACR,MAAM;EACR;CACF;CAKA,MAAM,OAA4E;EAChF,gBAAgB,OAAO;GACrB,GAAG;GACH,QAAQ,MAAM;IAAE,EAAE,MAAM,CAAC;IAAG,MAAM,IAAI;IAAiB,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,KAAK;GAAG;GAC3F,SAAS,MAAM;IAAE,EAAE,OAAO,CAAC;IAAG,MAAM,IAAI;IAAiB,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,IAAI;GAAG;EAC9F;EACA,YAAY,OAAO;GACjB,GAAG;GACH,QAAQ,MAAM;IAAE,EAAE,MAAM,CAAC;IAAG,UAAU,CAAC,CAAe,GAAG,KAAK;GAAG;GACjE,SAAS,MAAM;IAAE,MAAM,OAAO,OAAO,CAAC,CAAe,GAAG,CAAC,CAAe,GAAG,IAAI;IAAG,EAAE,OAAO,CAAC;IAAG,KAAK;GAAG;EACzG;EACA,eAAe,OAAO;GACpB,GAAG;GACH,QAAQ,MAAM;IACZ,MAAM,IAAI;IACV,MAAM,OAAO,OAAO,EAAE,UAAU,EAAE,OAAO,KAAK;IAC9C,EAAE,MAAM,CAAC;IACT,KAAK;GACP;GACA,SAAS,MAAM;IAAE,EAAE,OAAO,CAAC;IAAG,UAAW,EAAoB,OAAO,IAAI;GAAG;EAC7E;CACF;CAEA,SAAS,QAAc;EACrB,MAAM,UAAU,SAAS,MAAM;EAC/B,IAAI,QAAQ,SAAS,KAAK,QAAQ,WAAW,GAAG;EAChD,MAAM,SAAS,CAAC,GAAG,QAAQ,OAAO,CAAC,CAAC,CAAC,QAAQ,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,EAAE,CAAC;EAC3E,QAAQ,MAAM;EACd,IAAI,OAAO,SAAS,KAAK,QAAQ,SAAS,GAAG,UAAU,KAAK,QAAQ,OAAO;CAC7E;CACA,IAAI,QAAQ,UAAU,KAAK;CAE3B,OAAO;EACL,UAAU,UAAU;GAClB,MAAM,MAAM,UAAU,UAAU,QAAQ;GACxC;GACA,IAAI,KAAK;GACT,aAAa;IACX,IAAI,CAAC,IAAI;IACT,KAAK;IACL;IACA,IAAI;GACN;EACF;EACA,QAAQ,OAAO,KAAK,IAAI;GACtB,IAAI,MAAM,OAAO,GAAG;GACpB,OAAO,OAAO,UAAU;IAAE,OAAO;KAAE;KAAO;IAAI;GAAG,SAAS;IAAE,OAAO;GAAM,CAAC;EAC5E;EACA,YAAY,OAAO,OAAO,UAAU;GAAE;EAAW,SAAS;GAAE;EAAW,CAAC;EACxE,SAAS,OAAO,OAAO,UAAU;GAAE;EAAW,SAAS;GAAE;EAAW,CAAC;EACrE,QAAQ,OAAO,OAAO,UAAU;GAAE;EAAU,SAAS;GAAE;EAAU,CAAC;EAClE,YAAY,cAAc,MAAM,YAAY,SAAS,MAAM,KAAK,KAAK,GAAG,OAAO,KAAK,OAAO;CAC7F;AACF"}
|
|
1
|
+
{"version":3,"file":"poseWrites.js","names":[],"sources":["../../../src/core/scene/poseWrites.ts"],"sourcesContent":["/**\n * What a scene reports about the poses it writes: each change's net pose per\n * node, the nodes it removed as they were last shown, and where each came\n * from, handed to `onPoseWrite` once the change reaches the scene's\n * subscribers.\n */\nimport { createEmitter, deepEqual } from '@weasel-js/registry';\nimport { createRemovalLog } from './poseRemovals';\nimport type { RegisteredOp } from './types';\nimport type { LayoutMove } from './layoutTypes';\nimport type { NodeId } from '@weasel-js/routing';\n\n/**\n * Where a reported pose write came from.\n *\n * - `'call'` — a scene method called directly, alone or inside `scene.batch`:\n * a button's `scene.setPose`, a `scene.add`.\n * - `'ops'` — ops applied as one edit through `applyBatch` or `history.apply`,\n * which is how actions commit: a drag's release, a resize, a nudge, an align.\n * - `'undo'` / `'redo'` — a history step, a jump through the history included.\n * - `'layout'` — a container's layout arranging what a live edit changed; also\n * reported to `onReflow`.\n * - `'untracked'` — a write inside `scene.untracked`, such as a pose target\n * committing an animation's destination.\n */\nexport type PoseWriteOrigin = 'call' | 'ops' | 'undo' | 'redo' | 'layout' | 'untracked';\n\n/** One node's pose as a scene change left it, in the node's stored frame. */\nexport interface PoseWrite<TPose> {\n id: NodeId;\n /** The pose before the change; `undefined` for a node the change added. */\n from: TPose | undefined;\n to: TPose;\n origin: PoseWriteOrigin;\n /** The edit's label (`'setPose'`, a batch's, an action's `'Move'`) for\n * `'call'` and `'ops'`; `undefined` otherwise. */\n label?: string;\n}\n\n/** A removed node as it was last shown, with the descendants it took along. */\nexport interface RemovedNode<TPose, TNode = unknown> {\n /** The node as it stood before the change removed it. */\n node: TNode;\n /** Where it was shown, override included, in its stored frame. */\n pose: TPose;\n children: readonly RemovedNode<TPose, TNode>[];\n}\n\n/** One node a scene change removed, a removal's root: its descendants ride\n * beneath it rather than being reported apart. */\nexport interface PoseRemoval<TPose, TNode = unknown> extends RemovedNode<TPose, TNode> {\n id: NodeId;\n /** The parent it was removed from, which may still be in the scene. */\n parent: NodeId | null;\n origin: PoseWriteOrigin;\n label?: string;\n}\n\n/** What a scene reports about the poses its edits write. */\nexport interface ScenePoseReports<TPose, TNode = unknown> {\n /** Called with the poses each live edit's layout pass wrote, after the\n * edit lands — never for an undo or redo, which restore poses rather than\n * arrange them. What a reflow animation glides from. */\n onReflow(listener: (moves: readonly LayoutMove<TPose>[]) => void): () => void;\n /** Called once per change, before the scene's subscribers hear of it, with\n * every node whose pose the change wrote or that it added, and every node\n * it removed — an undone add among them. A node written back to the pose\n * it had, or both added and removed by the change, is left out of both.\n * Not called for `loadState` or the scene's initial nodes. */\n onPoseWrite(\n listener: (writes: readonly PoseWrite<TPose>[], removed: readonly PoseRemoval<TPose, TNode>[]) => void,\n ): () => void;\n}\n\n/** What the log reads of the scene it reports for. */\nexport interface PoseWriteEnv<TPose, TNode> {\n /** A node still in the scene, with the pose it is shown at. */\n shown(id: NodeId): { node: TNode; pose: TPose } | undefined;\n /** Whether the edit running now is inside `scene.untracked`. */\n untracked(): boolean;\n /** The scene's change notice. The log reports on it, subscribed before\n * anyone else so a change's writes arrive ahead of its re-reads. */\n changes: { subscribe(listener: () => void): () => void };\n}\n\nexport interface PoseWriteLog<TPose, TNode = unknown> {\n subscribe: ScenePoseReports<TPose, TNode>['onPoseWrite'];\n /** Run `fn` as an edit labeled `label`; an enclosing edit's label stands. */\n labeled<T>(label: string, via: 'call' | 'ops', fn: () => T): T;\n /** Run `fn` as a history step replaying recorded ops. */\n replaying<T>(fn: () => T): T;\n /** Run `fn` as a layout pass writing arranged poses. */\n layout<T>(fn: () => T): T;\n /** Run `fn` with nothing it writes reported. */\n quiet<T>(fn: () => T): T;\n /** `register` with the kit's pose-bearing ops reporting what they write. */\n reporting(register: (kind: string, handler: RegisteredOp<unknown>) => void):\n (kind: string, handler: RegisteredOp<unknown>) => void;\n}\n\nconst NOTHING = (): void => {};\n\ninterface Posed<TPose> { id: NodeId; pose: TPose }\ninterface Parented { id: NodeId; parent: NodeId | null }\n\nexport function createPoseWriteLog<TPose, TNode = unknown>(env: PoseWriteEnv<TPose, TNode>): PoseWriteLog<TPose, TNode> {\n const listeners = createEmitter<[writes: readonly PoseWrite<TPose>[], removed: readonly PoseRemoval<TPose, TNode>[]]>();\n const pending = new Map<NodeId, PoseWrite<TPose>>();\n const removals = createRemovalLog<TPose, TNode>(env.shown);\n // Two fields rather than one object: every bare scene call opens an edit.\n let editLabel: string | null = null;\n let editVia: 'call' | 'ops' = 'call';\n let replays = 0;\n let layouts = 0;\n let quiets = 0;\n let listening = 0;\n\n const collecting = (): boolean => listening > 0 && quiets === 0;\n\n function stamp(reverted: boolean): Pick<PoseWrite<TPose>, 'origin' | 'label'> {\n if (replays > 0) return { origin: reverted ? 'undo' : 'redo' };\n if (layouts > 0) return { origin: 'layout' };\n if (env.untracked()) return { origin: 'untracked' };\n return editLabel !== null ? { origin: editVia, label: editLabel } : { origin: 'call' };\n }\n\n function noteSet(id: NodeId, from: TPose, to: TPose, reverted: boolean): void {\n if (!collecting()) return;\n const held = pending.get(id);\n if (held) held.to = to;\n else pending.set(id, { id, from, to, ...stamp(reverted) });\n }\n\n function noteAdded(nodes: readonly Posed<TPose>[], reverted: boolean): void {\n if (!collecting()) return;\n for (const n of nodes) {\n // Removed and back within one change: it moved, if anything.\n pending.set(n.id, { id: n.id, from: removals.takeBack(n.id), to: n.pose, ...stamp(reverted) });\n }\n }\n\n const seeOff = (roots: readonly Parented[], nodes: readonly Parented[], reverted: boolean): () => void =>\n collecting() ? removals.seeOff(roots, nodes, stamp(reverted), pending) : NOTHING;\n\n function around<T>(fn: () => T, enter: () => void, leave: () => void): T {\n enter();\n try {\n return fn();\n } finally {\n leave();\n }\n }\n\n type SetPayload = { id: NodeId; from: TPose; to: TPose };\n type AddPayload = Posed<TPose> & Parented;\n type RemovePayload = { nodes: readonly (Posed<TPose> & Parented)[]; detached: readonly Parented[] };\n const wrap: Record<string, (h: RegisteredOp<unknown>) => RegisteredOp<unknown>> = {\n 'kit:setPose': (h) => ({\n ...h,\n apply: (p) => { h.apply(p); const s = p as SetPayload; noteSet(s.id, s.from, s.to, false); },\n revert: (p) => { h.revert(p); const s = p as SetPayload; noteSet(s.id, s.to, s.from, true); },\n }),\n 'kit:add': (h) => ({\n ...h,\n apply: (p) => { h.apply(p); if (collecting()) noteAdded([p as AddPayload], false); },\n revert: (p) => { const done = seeOff([p as AddPayload], [p as AddPayload], true); h.revert(p); done(); },\n }),\n 'kit:remove': (h) => ({\n ...h,\n apply: (p) => {\n const r = p as RemovePayload;\n const done = seeOff(r.detached, r.nodes, false);\n h.apply(p);\n done();\n },\n revert: (p) => { h.revert(p); noteAdded((p as RemovePayload).nodes, true); },\n }),\n };\n\n function flush(): void {\n const removed = removals.drain();\n if (pending.size === 0 && removed.length === 0) return;\n const writes = [...pending.values()].filter((w) => !deepEqual(w.from, w.to));\n pending.clear();\n if (writes.length > 0 || removed.length > 0) listeners.emit(writes, removed);\n }\n env.changes.subscribe(flush);\n\n return {\n subscribe(listener) {\n const off = listeners.subscribe(listener);\n listening++;\n let on = true;\n return () => {\n if (!on) return;\n on = false;\n listening--;\n off();\n };\n },\n labeled(label, via, fn) {\n if (editLabel !== null) return fn();\n editLabel = label;\n editVia = via;\n try {\n return fn();\n } finally {\n editLabel = null;\n }\n },\n replaying: (fn) => around(fn, () => { replays++; }, () => { replays--; }),\n layout: (fn) => around(fn, () => { layouts++; }, () => { layouts--; }),\n quiet: (fn) => around(fn, () => { quiets++; }, () => { quiets--; }),\n reporting: (register) => (kind, handler) => register(kind, wrap[kind]?.(handler) ?? handler),\n };\n}\n"],"mappings":"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"}
|