@weasel-js/core 1.14.2 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +167 -0
- package/dist/affordances/pathAnchors.js +0 -1
- package/dist/affordances/pathAnchors.js.map +1 -1
- package/dist/barrel/paths.js +1 -1
- package/dist/barrel/renderer.js +2 -2
- package/dist/barrel/routing.js +6 -4
- package/dist/barrel/text.js +5 -3
- package/dist/canvas/Canvas.js +5 -1
- package/dist/canvas/Canvas.js.map +1 -1
- package/dist/canvas/DrawCanvas.js +5 -1
- package/dist/canvas/DrawCanvas.js.map +1 -1
- package/dist/canvas/SceneCanvas/SceneCanvasInput.js +4 -0
- package/dist/canvas/SceneCanvas/SceneCanvasInput.js.map +1 -1
- package/dist/canvas/SceneCanvas/builtinToolSets.js +6 -3
- package/dist/canvas/SceneCanvas/builtinToolSets.js.map +1 -1
- package/dist/canvas/SceneCanvas/defaultNodeProperties.js +1 -1
- package/dist/canvas/SceneCanvas/presets/paths.js +6 -6
- package/dist/canvas/SceneCanvas/presets/paths.js.map +1 -1
- package/dist/canvas/SceneCanvas/shapeGeneratorProperties.js +1 -1
- package/dist/canvas/SceneCanvas/shapeGeneratorProperties.js.map +1 -1
- package/dist/canvas/SceneCanvas/useBuiltinTools.js +2 -0
- package/dist/canvas/SceneCanvas/useBuiltinTools.js.map +1 -1
- package/dist/canvas/SceneCanvas/useContributionRoles.js +2 -0
- package/dist/canvas/SceneCanvas/useContributionRoles.js.map +1 -1
- package/dist/canvas/SceneCanvas/useSceneChrome.js +17 -5
- package/dist/canvas/SceneCanvas/useSceneChrome.js.map +1 -1
- package/dist/canvas/SceneCanvas/useSceneStandardActions.js +3 -0
- package/dist/canvas/SceneCanvas/useSceneStandardActions.js.map +1 -1
- package/dist/canvas/SceneCanvas.js +5 -4
- package/dist/canvas/SceneCanvas.js.map +1 -1
- package/dist/canvas/SceneViewCanvas.js +5 -1
- package/dist/canvas/SceneViewCanvas.js.map +1 -1
- package/dist/canvas/affordanceAt.js +0 -1
- package/dist/canvas/affordanceAt.js.map +1 -1
- package/dist/canvas/deps/chromeRules.js +37 -0
- package/dist/canvas/deps/chromeRules.js.map +1 -0
- package/dist/canvas/deps/editAnchors.js +0 -6
- package/dist/canvas/deps/editAnchors.js.map +1 -1
- package/dist/canvas/deps/hitTestArea.js +3 -1
- package/dist/canvas/deps/hitTestArea.js.map +1 -1
- package/dist/canvas/deps/modeDefaultTool.js +23 -0
- package/dist/canvas/deps/modeDefaultTool.js.map +1 -0
- package/dist/canvas/deps/pathEditCommits.js +1 -1
- package/dist/canvas/deps/selectionSnapshot.js +23 -0
- package/dist/canvas/deps/selectionSnapshot.js.map +1 -0
- package/dist/canvas/deps/snap.js +1 -0
- package/dist/canvas/deps/snap.js.map +1 -1
- package/dist/canvas/pickReach.js +12 -8
- package/dist/canvas/pickReach.js.map +1 -1
- package/dist/canvas/pickWalk.js +7 -5
- package/dist/canvas/pickWalk.js.map +1 -1
- package/dist/canvas/scenePickSource.js +2 -2
- package/dist/canvas/scenePickSource.js.map +1 -1
- package/dist/canvas/useCanvasSurface.js +21 -3
- package/dist/canvas/useCanvasSurface.js.map +1 -1
- package/dist/canvas/useFrameLoop.js +6 -7
- package/dist/canvas/useFrameLoop.js.map +1 -1
- package/dist/canvas/useSurfaceSize.js +4 -3
- package/dist/canvas/useSurfaceSize.js.map +1 -1
- package/dist/core/geometry/regionHit.js +35 -16
- package/dist/core/geometry/regionHit.js.map +1 -1
- package/dist/core/scene/editMode.js +4 -1
- package/dist/core/scene/editMode.js.map +1 -1
- package/dist/core/scene/historyFacade.js +7 -50
- package/dist/core/scene/historyFacade.js.map +1 -1
- package/dist/core/scene/nodeGates.js +2 -2
- package/dist/core/scene/nodeGates.js.map +1 -1
- package/dist/core/scene/opTable.js +8 -8
- package/dist/core/scene/opTable.js.map +1 -1
- package/dist/core/scene/outerOps.js +2 -2
- package/dist/core/scene/outerOps.js.map +1 -1
- package/dist/core/scene/poseWrites.js +16 -15
- package/dist/core/scene/poseWrites.js.map +1 -1
- package/dist/core/scene/sceneJournals.js +78 -0
- package/dist/core/scene/sceneJournals.js.map +1 -0
- package/dist/core/scene/sceneRecording.js +3 -2
- package/dist/core/scene/sceneRecording.js.map +1 -1
- package/dist/core/viewport/useCanvasSize.js +47 -26
- package/dist/core/viewport/useCanvasSize.js.map +1 -1
- package/dist/{depSchema-DD9H89gX.d.ts → depSchema-ChQPzEkW.d.ts} +40 -13
- package/dist/domains/chrome-caps/resolve.js +1 -0
- package/dist/domains/chrome-caps/resolve.js.map +1 -1
- package/dist/domains/chrome-caps/useChromeCaps.js +4 -2
- package/dist/domains/chrome-caps/useChromeCaps.js.map +1 -1
- package/dist/domains/paths/pathEditState.js +0 -24
- package/dist/domains/paths/pathEditState.js.map +1 -1
- package/dist/domains/snap/probes2d.js +5 -0
- package/dist/domains/snap/probes2d.js.map +1 -1
- package/dist/domains/snap/sceneReads.js +11 -1
- package/dist/domains/snap/sceneReads.js.map +1 -1
- package/dist/domains/snap/sceneSession2d.js +4 -2
- package/dist/domains/snap/sceneSession2d.js.map +1 -1
- package/dist/domains/text/index.js +2 -2
- package/dist/icons.table.js +1 -1
- package/dist/icons.table.js.map +1 -1
- package/dist/icons.table.json +5 -0
- package/dist/index.d.ts +112 -60
- package/dist/index.js +12 -8
- package/dist/interactions/actions/booleans/action.js +4 -2
- package/dist/interactions/actions/booleans/action.js.map +1 -1
- package/dist/interactions/actions/defaults/anchorEditing.js +3 -6
- package/dist/interactions/actions/defaults/anchorEditing.js.map +1 -1
- package/dist/interactions/actions/defaults/bendSegment.js +1 -2
- package/dist/interactions/actions/defaults/bendSegment.js.map +1 -1
- package/dist/interactions/actions/defaults/exitPathEdit.js +17 -1
- package/dist/interactions/actions/defaults/exitPathEdit.js.map +1 -1
- package/dist/interactions/actions/edit-anchors/action.js +3 -4
- package/dist/interactions/actions/edit-anchors/action.js.map +1 -1
- package/dist/interactions/actions/insert/action.js +21 -2
- package/dist/interactions/actions/insert/action.js.map +1 -1
- package/dist/interactions/actions/insert/params.js +43 -0
- package/dist/interactions/actions/insert/params.js.map +1 -0
- package/dist/interactions/actions/notices.js +17 -0
- package/dist/interactions/actions/notices.js.map +1 -0
- package/dist/interactions/actions/outlines/createOutlines.js +8 -6
- package/dist/interactions/actions/outlines/createOutlines.js.map +1 -1
- package/dist/interactions/actions/outlines/createOutlinesAction.js +13 -8
- package/dist/interactions/actions/outlines/createOutlinesAction.js.map +1 -1
- package/dist/interactions/actions/outlines/outlineStrokeAction.js +3 -2
- package/dist/interactions/actions/outlines/outlineStrokeAction.js.map +1 -1
- package/dist/interactions/actions/pathCommands/pathNodes.js +1 -1
- package/dist/interactions/actions/testUtils.js +0 -7
- package/dist/interactions/actions/testUtils.js.map +1 -1
- package/dist/interactions/actions/useNotices.js +13 -0
- package/dist/interactions/actions/useNotices.js.map +1 -0
- package/dist/test-seams.d.ts +5 -12
- package/dist/tools/builtin/anchors/anchorsTool.js +31 -0
- package/dist/tools/builtin/anchors/anchorsTool.js.map +1 -0
- package/dist/tools/builtin/polygon/usePolygonTool.js +1 -1
- package/dist/tools/builtin/star/useStarTool.js +1 -1
- package/dist/tools/index.js +4 -2
- package/dist/tools/toolOptions.js +47 -0
- package/dist/tools/toolOptions.js.map +1 -0
- package/dist/version.js +1 -1
- package/package.json +25 -25
- package/dist/interactions/actions/defaults/pathEditTools.js +0 -60
- package/dist/interactions/actions/defaults/pathEditTools.js.map +0 -1
|
@@ -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"}
|
|
@@ -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":"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"}
|
|
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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"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
//#region src/core/scene/sceneJournals.ts
|
|
2
|
+
/** A journal drives adapter mutation itself, so its applies and undos
|
|
3
|
+
* re-enter scene mutation methods the way the facade's do. */
|
|
4
|
+
function wrapJournal(raw, env, open) {
|
|
5
|
+
const { replay, notify } = env;
|
|
6
|
+
const wrapped = {
|
|
7
|
+
targetId: raw.targetId,
|
|
8
|
+
forkedAtEntryId: raw.forkedAtEntryId,
|
|
9
|
+
applyBatch(ops, label) {
|
|
10
|
+
env.labeled(label, () => env.runArrivalEdit(ops, (all) => {
|
|
11
|
+
env.applying(() => raw.applyBatch(all.map(env.causing), label));
|
|
12
|
+
notify();
|
|
13
|
+
}));
|
|
14
|
+
},
|
|
15
|
+
undo() {
|
|
16
|
+
replay(() => raw.undo());
|
|
17
|
+
notify();
|
|
18
|
+
},
|
|
19
|
+
redo() {
|
|
20
|
+
replay(() => raw.redo());
|
|
21
|
+
notify();
|
|
22
|
+
},
|
|
23
|
+
canUndo: () => raw.canUndo(),
|
|
24
|
+
canRedo: () => raw.canRedo(),
|
|
25
|
+
entries: () => raw.entries(),
|
|
26
|
+
seal: () => raw.seal(),
|
|
27
|
+
observe: (listener) => raw.observe(listener),
|
|
28
|
+
beginJournal: (opts) => open.wrap(raw.beginJournal(opts)),
|
|
29
|
+
getVersion: () => raw.getVersion(),
|
|
30
|
+
subscribe: (listener) => raw.subscribe(listener),
|
|
31
|
+
commit(label) {
|
|
32
|
+
raw.commit(label);
|
|
33
|
+
open.close(wrapped);
|
|
34
|
+
notify();
|
|
35
|
+
},
|
|
36
|
+
cancel() {
|
|
37
|
+
replay(() => raw.cancel());
|
|
38
|
+
open.close(wrapped);
|
|
39
|
+
notify();
|
|
40
|
+
},
|
|
41
|
+
suspend: () => raw.suspend(),
|
|
42
|
+
isActive: () => raw.isActive()
|
|
43
|
+
};
|
|
44
|
+
return wrapped;
|
|
45
|
+
}
|
|
46
|
+
function createSceneJournals(env) {
|
|
47
|
+
/** Wrapped journal → the engine's own: the engine keys resumability off
|
|
48
|
+
* journal identity, which a wrapper would not match. */
|
|
49
|
+
const raws = /* @__PURE__ */ new WeakMap();
|
|
50
|
+
const opened = [];
|
|
51
|
+
const open = {
|
|
52
|
+
wrap(raw) {
|
|
53
|
+
const wrapped = wrapJournal(raw, env, open);
|
|
54
|
+
raws.set(wrapped, raw);
|
|
55
|
+
opened.push(wrapped);
|
|
56
|
+
return wrapped;
|
|
57
|
+
},
|
|
58
|
+
close(wrapped) {
|
|
59
|
+
const i = opened.indexOf(wrapped);
|
|
60
|
+
if (i >= 0) opened.splice(i, 1);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
return {
|
|
64
|
+
begin(opts) {
|
|
65
|
+
const parent = env.activeJournal();
|
|
66
|
+
return open.wrap(parent ? (raws.get(parent) ?? parent).beginJournal(opts) : env.engine.beginJournal(opts));
|
|
67
|
+
},
|
|
68
|
+
resume: (journal) => env.engine.resumeJournal(raws.get(journal) ?? journal),
|
|
69
|
+
innermost() {
|
|
70
|
+
for (let i = opened.length - 1; i >= 0; i--) if (opened[i].isActive()) return opened[i];
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
//#endregion
|
|
77
|
+
export { createSceneJournals };
|
|
78
|
+
//# sourceMappingURL=sceneJournals.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sceneJournals.js","names":[],"sources":["../../../src/core/scene/sceneJournals.ts"],"sourcesContent":["/**\n * The journals opened on `scene.history`: each wrapped so its applies and\n * undos re-enter scene mutation the way the facade's do, and tracked while\n * open, so the scene's edits go to the innermost.\n */\nimport type { BeginJournalOptions, Journal } from '@weasel-js/history';\nimport type { SceneHistoryEnv } from './historyFacade';\n\ninterface OpenJournals {\n wrap(raw: Journal): Journal;\n close(wrapped: Journal): void;\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, open: OpenJournals): Journal {\n const { replay, notify } = env;\n const wrapped: Journal = {\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 observe: (listener) => raw.observe(listener),\n beginJournal: (opts) => open.wrap(raw.beginJournal(opts)),\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 open.close(wrapped);\n notify();\n },\n cancel() {\n replay(() => raw.cancel());\n open.close(wrapped);\n notify();\n },\n suspend: () => raw.suspend(),\n isActive: () => raw.isActive(),\n };\n return wrapped;\n}\n\nexport interface SceneJournals {\n /** Open a journal inside the one edits go to now, else on the engine: an\n * edit session started during a mode's lands in the mode's as one entry. */\n begin(opts: BeginJournalOptions): Journal;\n resume(journal: Journal): void;\n /** The innermost active journal opened here, or null. */\n innermost(): Journal | null;\n}\n\nexport function createSceneJournals(env: SceneHistoryEnv): SceneJournals {\n /** Wrapped journal → the engine's own: the engine keys resumability off\n * journal identity, which a wrapper would not match. */\n const raws = new WeakMap<Journal, Journal>();\n const opened: Journal[] = [];\n const open: OpenJournals = {\n wrap(raw) {\n const wrapped = wrapJournal(raw, env, open);\n raws.set(wrapped, raw);\n opened.push(wrapped);\n return wrapped;\n },\n close(wrapped) {\n const i = opened.indexOf(wrapped);\n if (i >= 0) opened.splice(i, 1);\n },\n };\n return {\n begin(opts) {\n const parent = env.activeJournal();\n return open.wrap(parent ? (raws.get(parent) ?? parent).beginJournal(opts) : env.engine.beginJournal(opts));\n },\n resume: (journal) => env.engine.resumeJournal(raws.get(journal) ?? journal),\n innermost() {\n for (let i = opened.length - 1; i >= 0; i--) if (opened[i]!.isActive()) return opened[i]!;\n return null;\n },\n };\n}\n"],"mappings":";;;AAeA,SAAS,YAAY,KAAc,KAAsB,MAA6B;CACpF,MAAM,EAAE,QAAQ,WAAW;CAC3B,MAAM,UAAmB;EACvB,UAAU,IAAI;EACd,iBAAiB,IAAI;EACrB,WAAW,KAAK,OAAO;GACrB,IAAI,QAAQ,aAAa,IAAI,eAAe,MAAM,QAAQ;IACxD,IAAI,eAAe,IAAI,WAAW,IAAI,IAAI,IAAI,OAAO,GAAG,KAAK,CAAC;IAC9D,OAAO;GACT,CAAC,CAAC;EACJ;EACA,OAAO;GACL,aAAa,IAAI,KAAK,CAAC;GACvB,OAAO;EACT;EACA,OAAO;GACL,aAAa,IAAI,KAAK,CAAC;GACvB,OAAO;EACT;EACA,eAAe,IAAI,QAAQ;EAC3B,eAAe,IAAI,QAAQ;EAC3B,eAAe,IAAI,QAAQ;EAC3B,YAAY,IAAI,KAAK;EACrB,UAAU,aAAa,IAAI,QAAQ,QAAQ;EAC3C,eAAe,SAAS,KAAK,KAAK,IAAI,aAAa,IAAI,CAAC;EACxD,kBAAkB,IAAI,WAAW;EACjC,YAAY,aAAa,IAAI,UAAU,QAAQ;EAC/C,OAAO,OAAO;GAGZ,IAAI,OAAO,KAAK;GAChB,KAAK,MAAM,OAAO;GAClB,OAAO;EACT;EACA,SAAS;GACP,aAAa,IAAI,OAAO,CAAC;GACzB,KAAK,MAAM,OAAO;GAClB,OAAO;EACT;EACA,eAAe,IAAI,QAAQ;EAC3B,gBAAgB,IAAI,SAAS;CAC/B;CACA,OAAO;AACT;AAWA,SAAgB,oBAAoB,KAAqC;;;CAGvE,MAAM,uBAAO,IAAI,QAA0B;CAC3C,MAAM,SAAoB,CAAC;CAC3B,MAAM,OAAqB;EACzB,KAAK,KAAK;GACR,MAAM,UAAU,YAAY,KAAK,KAAK,IAAI;GAC1C,KAAK,IAAI,SAAS,GAAG;GACrB,OAAO,KAAK,OAAO;GACnB,OAAO;EACT;EACA,MAAM,SAAS;GACb,MAAM,IAAI,OAAO,QAAQ,OAAO;GAChC,IAAI,KAAK,GAAG,OAAO,OAAO,GAAG,CAAC;EAChC;CACF;CACA,OAAO;EACL,MAAM,MAAM;GACV,MAAM,SAAS,IAAI,cAAc;GACjC,OAAO,KAAK,KAAK,UAAU,KAAK,IAAI,MAAM,KAAK,OAAA,CAAQ,aAAa,IAAI,IAAI,IAAI,OAAO,aAAa,IAAI,CAAC;EAC3G;EACA,SAAS,YAAY,IAAI,OAAO,cAAc,KAAK,IAAI,OAAO,KAAK,OAAO;EAC1E,YAAY;GACV,KAAK,IAAI,IAAI,OAAO,SAAS,GAAG,KAAK,GAAG,KAAK,IAAI,OAAO,EAAE,CAAE,SAAS,GAAG,OAAO,OAAO;GACtF,OAAO;EACT;CACF;AACF"}
|
|
@@ -39,7 +39,8 @@ function createSceneRecording(env) {
|
|
|
39
39
|
own: (name, args) => ops.handlers.has(name) ? ops.op(name, args) : null,
|
|
40
40
|
recorder: () => recorder,
|
|
41
41
|
driving: (step) => notifier.deferred(() => mode.during("engine", () => atomically(step, () => true))),
|
|
42
|
-
getActiveJournal: config.getActiveJournal ?? null
|
|
42
|
+
getActiveJournal: config.getActiveJournal ?? null,
|
|
43
|
+
openJournal: () => openJournal()
|
|
43
44
|
});
|
|
44
45
|
const engine = createHistory(void 0, {
|
|
45
46
|
selection: {
|
|
@@ -81,7 +82,7 @@ function createSceneRecording(env) {
|
|
|
81
82
|
function refuseUntracked(verb) {
|
|
82
83
|
if (mode.untracked()) throw new Error(`Scene: cannot ${verb} inside untracked() — it records`);
|
|
83
84
|
}
|
|
84
|
-
const { history, methods } = createSceneHistory({
|
|
85
|
+
const { history, methods, openJournal } = createSceneHistory({
|
|
85
86
|
engine,
|
|
86
87
|
applying: (step) => atomically(() => engineDriven(step), () => true),
|
|
87
88
|
causing: outer.causing,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sceneRecording.js","names":[],"sources":["../../../src/core/scene/sceneRecording.ts"],"sourcesContent":["/**\n * How a scene's edits reach its undo stack: the op table, the log that takes\n * back an edit refused partway, the engine, and the recorder between them.\n */\nimport type { Journal } from '@weasel-js/history';\nimport type { Op } from 'core/ops/types';\nimport { createHistory } from 'core/ops/createHistory';\nimport { createAppliedLog } from './appliedLog';\nimport type { ArrivalWindow } from './arrivalWindow';\nimport type { EditMode } from './editMode';\nimport { createEditPasses } from './editPass';\nimport { createSceneHistory, type SceneHistoryMethods } from './historyFacade';\nimport type { Notifier } from './notifier';\nimport type { PoseWriteLog } from './poseWrites';\nimport { createOpTable, type OpTable } from './opTable';\nimport { createOuterOps } from './outerOps';\nimport { createRecorder, type Recorder, type RecorderEnv } from './recorder';\nimport type { SceneSelection } from './sceneSelection';\nimport type { StaleReplay } from './staleReplay';\nimport type { NodeId, RegisteredOp, Scene } from './types';\n\nexport interface SceneRecordingEnv {\n notifier: Notifier;\n mode: EditMode;\n stale: StaleReplay;\n /** Run with the lock guard lifted. */\n lifted<T>(fn: () => T): T;\n selection: Pick<SceneSelection, 'get' | 'set' | 'replace'>;\n arrivals: RecorderEnv['arrivals'] & Pick<ArrivalWindow<unknown>, 'runEdit'>;\n /** The adapter an edit pass's ops apply through. */\n passAdapter(): unknown;\n writePose(id: string, pose: unknown): void;\n poseWrites: Pick<PoseWriteLog<unknown>, 'labeled' | 'replaying'>;\n config: {\n getActiveJournal?: (() => Journal | null) | undefined;\n coalesceWindowMs?: number | undefined;\n historyLimit?: number | undefined;\n };\n}\n\nexport interface SceneRecording {\n ops: Pick<OpTable, 'register' | 'run'>;\n recorder: Recorder;\n clearHistory(): void;\n methods: SceneHistoryMethods & Pick<\n Scene<unknown, string, unknown>,\n | 'registerOp' | 'recordOp' | 'applyBatch' | 'history' | 'batch' | 'untracked'\n | 'setActiveJournalAccessor' | 'setHistoryAdapter' | 'addEditPass'\n >;\n}\n\nexport function createSceneRecording(env: SceneRecordingEnv): SceneRecording {\n const { notifier, mode, stale, lifted, selection, arrivals, config, poseWrites } = env;\n const { notify } = notifier;\n const passes = createEditPasses({ adapter: env.passAdapter, untracked: mode.untracked });\n\n /** Run an op-driven edit with its arrivals settled and its passes run at\n * its end, in the same entry, when this call owns them. */\n const runEdit = (ops: Op[], run: (ops: Op[]) => void): boolean => arrivals.runEdit(ops, (all) => {\n if (!passes.open()) return run(all);\n try {\n run([...all, passes.op()]);\n } finally {\n passes.abandon();\n }\n });\n\n const ops = createOpTable({\n driven: mode.open,\n replayAlone: (step) => { replay(step); notify(); },\n skips: stale.skips,\n // Only a replay is logged: a first apply is recorded by the entry it\n // creates, and reverting it would leave that entry describing nothing.\n applied: (kind, payload, reverted) => { if (!mode.records()) logApplied(kind, payload, reverted); },\n });\n\n const outer = createOuterOps({\n own: (name, args) => (ops.handlers.has(name) ? ops.op(name, args) : null),\n recorder: () => recorder,\n // Deferred rather than muted, so a throwing op cannot strand subscribers\n // after earlier ops in the batch already mutated state.\n driving: (step) => notifier.deferred(() => mode.during('engine', () => atomically(step, () => true))),\n getActiveJournal: config.getActiveJournal ?? null,\n });\n\n // The scene's undo/redo engine. Its recorded ops close over their payloads,\n // so the engine's adapter argument is unused.\n const engine = createHistory(undefined, {\n selection: { get: selection.get, set: (ids) => selection.set(ids as readonly NodeId[]) },\n ...(config.coalesceWindowMs !== undefined ? { coalesceWindowMs: config.coalesceWindowMs } : {}),\n ...(config.historyLimit !== undefined ? { historyLimit: config.historyLimit } : {}),\n rebuildOp: outer.rebuild,\n });\n\n const log = createAppliedLog({\n registered: ops.handlers,\n lifted,\n getSelection: selection.get,\n setSelection: selection.replace,\n notify,\n });\n const { atomically, logApplied } = log;\n\n const recorder = createRecorder({\n notifier,\n mode,\n engine,\n runOp: ops.run,\n makeOp: ops.op,\n log,\n arrivals,\n passes,\n selection: selection.get,\n writePose: env.writePose,\n labeled: poseWrites.labeled,\n });\n const { record, engineDriven } = recorder;\n\n /** Run an engine step that replays recorded ops — undo, redo, goto, a\n * journal stepping. The engine inverts whole ops when one throws; this\n * also reverts the scene writes of the op that threw partway. */\n function replay(step: () => void): void {\n poseWrites.replaying(() => atomically(() => lifted(() => stale.during(() => engineDriven(step))), () => true));\n }\n\n function refuseUntracked(verb: string): void {\n if (mode.untracked()) throw new Error(`Scene: cannot ${verb} inside untracked() — it records`);\n }\n\n const { history, methods } = createSceneHistory({\n engine,\n applying: (step) => atomically(() => engineDriven(step), () => true),\n causing: outer.causing,\n replay,\n notify,\n refuseUntracked,\n runArrivalEdit: runEdit,\n labeled: (label, fn) => poseWrites.labeled(label, 'ops', fn),\n activeJournal: outer.activeJournal,\n });\n\n return {\n ops,\n recorder,\n clearHistory: () => engine.clear(),\n methods: {\n registerOp(kind, handler) {\n if (kind.startsWith('kit:')) {\n throw new Error(`Scene: op kind \"${kind}\" is reserved (kit:* prefix)`);\n }\n ops.register(kind, handler as RegisteredOp<unknown>);\n },\n recordOp(op) {\n record(op.kind, op.payload, op.kind);\n },\n applyBatch(batch, label, adapter) {\n refuseUntracked('applyBatch');\n return poseWrites.labeled(label, 'ops', () =>\n runEdit(batch, (all) => outer.apply(engine, all, label, adapter)));\n },\n history,\n ...methods,\n batch: recorder.batch,\n untracked: recorder.untracked,\n setActiveJournalAccessor: outer.setActiveJournalAccessor,\n setHistoryAdapter: outer.setHistoryAdapter,\n addEditPass: passes.add,\n },\n };\n}\n"],"mappings":";;;;;;;;;AAmDA,SAAgB,qBAAqB,KAAwC;CAC3E,MAAM,EAAE,UAAU,MAAM,OAAO,QAAQ,WAAW,UAAU,QAAQ,eAAe;CACnF,MAAM,EAAE,WAAW;CACnB,MAAM,SAAS,iBAAiB;EAAE,SAAS,IAAI;EAAa,WAAW,KAAK;CAAU,CAAC;;;CAIvF,MAAM,WAAW,KAAW,QAAsC,SAAS,QAAQ,MAAM,QAAQ;EAC/F,IAAI,CAAC,OAAO,KAAK,GAAG,OAAO,IAAI,GAAG;EAClC,IAAI;GACF,IAAI,CAAC,GAAG,KAAK,OAAO,GAAG,CAAC,CAAC;EAC3B,UAAU;GACR,OAAO,QAAQ;EACjB;CACF,CAAC;CAED,MAAM,MAAM,cAAc;EACxB,QAAQ,KAAK;EACb,cAAc,SAAS;GAAE,OAAO,IAAI;GAAG,OAAO;EAAG;EACjD,OAAO,MAAM;EAGb,UAAU,MAAM,SAAS,aAAa;GAAE,IAAI,CAAC,KAAK,QAAQ,GAAG,WAAW,MAAM,SAAS,QAAQ;EAAG;CACpG,CAAC;CAED,MAAM,QAAQ,eAAe;EAC3B,MAAM,MAAM,SAAU,IAAI,SAAS,IAAI,IAAI,IAAI,IAAI,GAAG,MAAM,IAAI,IAAI;EACpE,gBAAgB;EAGhB,UAAU,SAAS,SAAS,eAAe,KAAK,OAAO,gBAAgB,WAAW,YAAY,IAAI,CAAC,CAAC;EACpG,kBAAkB,OAAO,oBAAoB;
|
|
1
|
+
{"version":3,"file":"sceneRecording.js","names":[],"sources":["../../../src/core/scene/sceneRecording.ts"],"sourcesContent":["/**\n * How a scene's edits reach its undo stack: the op table, the log that takes\n * back an edit refused partway, the engine, and the recorder between them.\n */\nimport type { Journal } from '@weasel-js/history';\nimport type { Op } from 'core/ops/types';\nimport { createHistory } from 'core/ops/createHistory';\nimport { createAppliedLog } from './appliedLog';\nimport type { ArrivalWindow } from './arrivalWindow';\nimport type { EditMode } from './editMode';\nimport { createEditPasses } from './editPass';\nimport { createSceneHistory, type SceneHistoryMethods } from './historyFacade';\nimport type { Notifier } from './notifier';\nimport type { PoseWriteLog } from './poseWrites';\nimport { createOpTable, type OpTable } from './opTable';\nimport { createOuterOps } from './outerOps';\nimport { createRecorder, type Recorder, type RecorderEnv } from './recorder';\nimport type { SceneSelection } from './sceneSelection';\nimport type { StaleReplay } from './staleReplay';\nimport type { NodeId, RegisteredOp, Scene } from './types';\n\nexport interface SceneRecordingEnv {\n notifier: Notifier;\n mode: EditMode;\n stale: StaleReplay;\n /** Run with the lock guard lifted. */\n lifted<T>(fn: () => T): T;\n selection: Pick<SceneSelection, 'get' | 'set' | 'replace'>;\n arrivals: RecorderEnv['arrivals'] & Pick<ArrivalWindow<unknown>, 'runEdit'>;\n /** The adapter an edit pass's ops apply through. */\n passAdapter(): unknown;\n writePose(id: string, pose: unknown): void;\n poseWrites: Pick<PoseWriteLog<unknown>, 'labeled' | 'replaying'>;\n config: {\n getActiveJournal?: (() => Journal | null) | undefined;\n coalesceWindowMs?: number | undefined;\n historyLimit?: number | undefined;\n };\n}\n\nexport interface SceneRecording {\n ops: Pick<OpTable, 'register' | 'run'>;\n recorder: Recorder;\n clearHistory(): void;\n methods: SceneHistoryMethods & Pick<\n Scene<unknown, string, unknown>,\n | 'registerOp' | 'recordOp' | 'applyBatch' | 'history' | 'batch' | 'untracked'\n | 'setActiveJournalAccessor' | 'setHistoryAdapter' | 'addEditPass'\n >;\n}\n\nexport function createSceneRecording(env: SceneRecordingEnv): SceneRecording {\n const { notifier, mode, stale, lifted, selection, arrivals, config, poseWrites } = env;\n const { notify } = notifier;\n const passes = createEditPasses({ adapter: env.passAdapter, untracked: mode.untracked });\n\n /** Run an op-driven edit with its arrivals settled and its passes run at\n * its end, in the same entry, when this call owns them. */\n const runEdit = (ops: Op[], run: (ops: Op[]) => void): boolean => arrivals.runEdit(ops, (all) => {\n if (!passes.open()) return run(all);\n try {\n run([...all, passes.op()]);\n } finally {\n passes.abandon();\n }\n });\n\n const ops = createOpTable({\n driven: mode.open,\n replayAlone: (step) => { replay(step); notify(); },\n skips: stale.skips,\n // Only a replay is logged: a first apply is recorded by the entry it\n // creates, and reverting it would leave that entry describing nothing.\n applied: (kind, payload, reverted) => { if (!mode.records()) logApplied(kind, payload, reverted); },\n });\n\n const outer = createOuterOps({\n own: (name, args) => (ops.handlers.has(name) ? ops.op(name, args) : null),\n recorder: () => recorder,\n // Deferred rather than muted, so a throwing op cannot strand subscribers\n // after earlier ops in the batch already mutated state.\n driving: (step) => notifier.deferred(() => mode.during('engine', () => atomically(step, () => true))),\n getActiveJournal: config.getActiveJournal ?? null, openJournal: () => openJournal(),\n });\n\n // The scene's undo/redo engine. Its recorded ops close over their payloads,\n // so the engine's adapter argument is unused.\n const engine = createHistory(undefined, {\n selection: { get: selection.get, set: (ids) => selection.set(ids as readonly NodeId[]) },\n ...(config.coalesceWindowMs !== undefined ? { coalesceWindowMs: config.coalesceWindowMs } : {}),\n ...(config.historyLimit !== undefined ? { historyLimit: config.historyLimit } : {}),\n rebuildOp: outer.rebuild,\n });\n\n const log = createAppliedLog({\n registered: ops.handlers,\n lifted,\n getSelection: selection.get,\n setSelection: selection.replace,\n notify,\n });\n const { atomically, logApplied } = log;\n\n const recorder = createRecorder({\n notifier,\n mode,\n engine,\n runOp: ops.run,\n makeOp: ops.op,\n log,\n arrivals,\n passes,\n selection: selection.get,\n writePose: env.writePose,\n labeled: poseWrites.labeled,\n });\n const { record, engineDriven } = recorder;\n\n /** Run an engine step that replays recorded ops — undo, redo, goto, a\n * journal stepping. The engine inverts whole ops when one throws; this\n * also reverts the scene writes of the op that threw partway. */\n function replay(step: () => void): void {\n poseWrites.replaying(() => atomically(() => lifted(() => stale.during(() => engineDriven(step))), () => true));\n }\n\n function refuseUntracked(verb: string): void {\n if (mode.untracked()) throw new Error(`Scene: cannot ${verb} inside untracked() — it records`);\n }\n\n const { history, methods, openJournal } = createSceneHistory({\n engine,\n applying: (step) => atomically(() => engineDriven(step), () => true),\n causing: outer.causing,\n replay,\n notify,\n refuseUntracked,\n runArrivalEdit: runEdit,\n labeled: (label, fn) => poseWrites.labeled(label, 'ops', fn),\n activeJournal: outer.activeJournal,\n });\n\n return {\n ops,\n recorder,\n clearHistory: () => engine.clear(),\n methods: {\n registerOp(kind, handler) {\n if (kind.startsWith('kit:')) {\n throw new Error(`Scene: op kind \"${kind}\" is reserved (kit:* prefix)`);\n }\n ops.register(kind, handler as RegisteredOp<unknown>);\n },\n recordOp(op) {\n record(op.kind, op.payload, op.kind);\n },\n applyBatch(batch, label, adapter) {\n refuseUntracked('applyBatch');\n return poseWrites.labeled(label, 'ops', () =>\n runEdit(batch, (all) => outer.apply(engine, all, label, adapter)));\n },\n history,\n ...methods,\n batch: recorder.batch,\n untracked: recorder.untracked,\n setActiveJournalAccessor: outer.setActiveJournalAccessor,\n setHistoryAdapter: outer.setHistoryAdapter,\n addEditPass: passes.add,\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useDeviceProfile } from "../device/useDeviceProfile.js";
|
|
2
|
-
import {
|
|
2
|
+
import { useMemo, useSyncExternalStore } from "react";
|
|
3
3
|
|
|
4
4
|
//#region src/core/viewport/useCanvasSize.ts
|
|
5
5
|
/** Track a container's content-rect size via `ResizeObserver`, and density via
|
|
@@ -9,40 +9,61 @@ import { useCallback, useEffect, useState } from "react";
|
|
|
9
9
|
* inside the resize callback: that only refreshes when the element resizes,
|
|
10
10
|
* so dragging a window to a different-density display without resizing it
|
|
11
11
|
* leaves the snapshot stale. */
|
|
12
|
-
function useCanvasSize(containerRef) {
|
|
12
|
+
function useCanvasSize(containerRef, options) {
|
|
13
13
|
const { dpr } = useDeviceProfile();
|
|
14
|
-
const { width, height } = useElementSize(containerRef);
|
|
14
|
+
const { width, height } = useElementSize(containerRef, options);
|
|
15
15
|
return {
|
|
16
16
|
width,
|
|
17
17
|
height,
|
|
18
18
|
dpr
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const measure =
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
21
|
+
var UNMEASURED = {
|
|
22
|
+
width: 0,
|
|
23
|
+
height: 0
|
|
24
|
+
};
|
|
25
|
+
/** `useSyncExternalStore`'s view of an element's size: measured when subscribed and on each resize. */
|
|
26
|
+
function elementSizeStore(containerRef, beforePaint) {
|
|
27
|
+
let size = UNMEASURED;
|
|
28
|
+
const measure = (el) => {
|
|
29
|
+
const rect = el.getBoundingClientRect();
|
|
30
|
+
if (rect.width === size.width && rect.height === size.height) return false;
|
|
31
|
+
size = {
|
|
32
32
|
width: rect.width,
|
|
33
33
|
height: rect.height
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
34
|
+
};
|
|
35
|
+
return true;
|
|
36
|
+
};
|
|
37
|
+
return {
|
|
38
|
+
subscribe(changed) {
|
|
39
|
+
const el = containerRef.current;
|
|
40
|
+
if (!el) return () => {};
|
|
41
|
+
measure(el);
|
|
42
|
+
if (typeof ResizeObserver === "undefined") return () => {};
|
|
43
|
+
let pending;
|
|
44
|
+
const observer = new ResizeObserver(() => {
|
|
45
|
+
if (beforePaint) {
|
|
46
|
+
if (measure(el)) changed();
|
|
47
|
+
} else pending ??= setTimeout(() => {
|
|
48
|
+
pending = void 0;
|
|
49
|
+
if (measure(el)) changed();
|
|
50
|
+
}, 0);
|
|
51
|
+
});
|
|
52
|
+
observer.observe(el);
|
|
53
|
+
return () => {
|
|
54
|
+
observer.disconnect();
|
|
55
|
+
clearTimeout(pending);
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
get: () => size
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/** An element's CSS-pixel size, `0 × 0` until it is mounted and measured.
|
|
62
|
+
* Re-measured whenever a `ResizeObserver` reports it resized. */
|
|
63
|
+
function useElementSize(containerRef, options) {
|
|
64
|
+
const beforePaint = options?.beforePaint ?? false;
|
|
65
|
+
const store = useMemo(() => elementSizeStore(containerRef, beforePaint), [containerRef, beforePaint]);
|
|
66
|
+
return useSyncExternalStore(store.subscribe, store.get, store.get);
|
|
46
67
|
}
|
|
47
68
|
|
|
48
69
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useCanvasSize.js","names":[],"sources":["../../../src/core/viewport/useCanvasSize.ts"],"sourcesContent":["import { type RefObject,
|
|
1
|
+
{"version":3,"file":"useCanvasSize.js","names":[],"sources":["../../../src/core/viewport/useCanvasSize.ts"],"sourcesContent":["import { type RefObject, useMemo, useSyncExternalStore } from 'react';\nimport type { CanvasSize } from './clampView';\nimport { useDeviceProfile } from '../device/useDeviceProfile';\n\n/** Size snapshot returned by `useCanvasSize` — the kit-wide `CanvasSize`\n * (width × height in CSS pixels) plus the current devicePixelRatio. */\nexport interface CanvasSizeSnapshot extends CanvasSize {\n dpr: number;\n}\n\n/** Track a container's content-rect size via `ResizeObserver`, and density via\n * the ambient `DeviceProfile`.\n *\n * Density deliberately does NOT come from a `window.devicePixelRatio` read\n * inside the resize callback: that only refreshes when the element resizes,\n * so dragging a window to a different-density display without resizing it\n * leaves the snapshot stale. */\nexport function useCanvasSize(containerRef: RefObject<HTMLElement | null>, options?: ElementSizeOptions): CanvasSizeSnapshot {\n const { dpr } = useDeviceProfile();\n const { width, height } = useElementSize(containerRef, options);\n return { width, height, dpr };\n}\n\n/** How {@link useElementSize} commits a resize. */\nexport interface ElementSizeOptions {\n /** Render the caller with a new size inside the `ResizeObserver` callback, so the frame it resized in paints\n * it. Only for a caller whose render lays out nothing outside the observed element: the browser delivers a\n * resize it causes outside it only next frame, and reports the loop as an error. By default the size\n * arrives a frame later. */\n beforePaint?: boolean;\n}\n\nconst UNMEASURED: CanvasSize = { width: 0, height: 0 };\n\n/** `useSyncExternalStore`'s view of an element's size: measured when subscribed and on each resize. */\nfunction elementSizeStore(containerRef: RefObject<HTMLElement | null>, beforePaint: boolean) {\n let size = UNMEASURED;\n const measure = (el: HTMLElement): boolean => {\n const rect = el.getBoundingClientRect();\n if (rect.width === size.width && rect.height === size.height) return false;\n size = { width: rect.width, height: rect.height };\n return true;\n };\n return {\n subscribe(changed: () => void): () => void {\n const el = containerRef.current;\n if (!el) return () => {};\n measure(el);\n // Absent under jsdom and in older embedders: one measurement is all there is.\n if (typeof ResizeObserver === 'undefined') return () => {};\n let pending: ReturnType<typeof setTimeout> | undefined;\n const observer = new ResizeObserver(() => {\n // A store update renders on a microtask, still inside the browser's resize loop; a task waits for it.\n if (beforePaint) { if (measure(el)) changed(); }\n else pending ??= setTimeout(() => { pending = undefined; if (measure(el)) changed(); }, 0);\n });\n observer.observe(el);\n return () => { observer.disconnect(); clearTimeout(pending); };\n },\n get: () => size,\n };\n}\n\n/** An element's CSS-pixel size, `0 × 0` until it is mounted and measured.\n * Re-measured whenever a `ResizeObserver` reports it resized. */\nexport function useElementSize(containerRef: RefObject<HTMLElement | null>, options?: ElementSizeOptions): CanvasSize {\n const beforePaint = options?.beforePaint ?? false;\n const store = useMemo(() => elementSizeStore(containerRef, beforePaint), [containerRef, beforePaint]);\n return useSyncExternalStore(store.subscribe, store.get, store.get);\n}\n"],"mappings":";;;;;;;;;;;AAiBA,SAAgB,cAAc,cAA6C,SAAkD;CAC3H,MAAM,EAAE,QAAQ,iBAAiB;CACjC,MAAM,EAAE,OAAO,WAAW,eAAe,cAAc,OAAO;CAC9D,OAAO;EAAE;EAAO;EAAQ;CAAI;AAC9B;AAWA,IAAM,aAAyB;CAAE,OAAO;CAAG,QAAQ;AAAE;;AAGrD,SAAS,iBAAiB,cAA6C,aAAsB;CAC3F,IAAI,OAAO;CACX,MAAM,WAAW,OAA6B;EAC5C,MAAM,OAAO,GAAG,sBAAsB;EACtC,IAAI,KAAK,UAAU,KAAK,SAAS,KAAK,WAAW,KAAK,QAAQ,OAAO;EACrE,OAAO;GAAE,OAAO,KAAK;GAAO,QAAQ,KAAK;EAAO;EAChD,OAAO;CACT;CACA,OAAO;EACL,UAAU,SAAiC;GACzC,MAAM,KAAK,aAAa;GACxB,IAAI,CAAC,IAAI,aAAa,CAAC;GACvB,QAAQ,EAAE;GAEV,IAAI,OAAO,mBAAmB,aAAa,aAAa,CAAC;GACzD,IAAI;GACJ,MAAM,WAAW,IAAI,qBAAqB;IAExC,IAAI,aAAe;SAAI,QAAQ,EAAE,GAAG,QAAQ;IAAA,OACvC,YAAY,iBAAiB;KAAE,UAAU;KAAW,IAAI,QAAQ,EAAE,GAAG,QAAQ;IAAG,GAAG,CAAC;GAC3F,CAAC;GACD,SAAS,QAAQ,EAAE;GACnB,aAAa;IAAE,SAAS,WAAW;IAAG,aAAa,OAAO;GAAG;EAC/D;EACA,WAAW;CACb;AACF;;;AAIA,SAAgB,eAAe,cAA6C,SAA0C;CACpH,MAAM,cAAc,SAAS,eAAe;CAC5C,MAAM,QAAQ,cAAc,iBAAiB,cAAc,WAAW,GAAG,CAAC,cAAc,WAAW,CAAC;CACpG,OAAO,qBAAqB,MAAM,WAAW,MAAM,KAAK,MAAM,GAAG;AACnE"}
|