@weasel-js/core 1.14.1 → 1.14.2

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.
@@ -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 = /* @__PURE__ */ new Set();
15
- const lockedLayers = /* @__PURE__ */ new Set();
16
- for (const l of scene.layers ?? []) {
17
- if (l.visible === false) hiddenLayers.add(l.id);
18
- if (l.locked === true) lockedLayers.add(l.id);
19
- }
20
- /** Whether `flag` holds on the node at `id` or any ancestor, the node's
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 climb = (id, seen) => {
25
- const known = memo.get(id);
26
- if (known !== void 0) return known;
27
- const node = scene.get(id);
28
- if (node === void 0 || seen.has(id)) return false;
29
- seen.add(id);
30
- const parent = node.parent ?? null;
31
- const out = (flag === "hidden" ? hidesSubtree(node) : node.locked === true) || layers !== null && layers.has(node.layer) || parent !== null && climb(parent, seen);
32
- memo.set(id, out);
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 flaggedHidden = inherited("hidden", null);
52
+ const hiddenAbove = above(hidesSubtree);
53
+ const lockedAbove = above(ownLock);
38
54
  return {
39
55
  hidden(id) {
40
56
  const node = scene.get(id);
41
57
  if (node === void 0) return false;
42
- return hiddenLayers.has(node.layer) || flaggedHidden(id);
58
+ return hiddenLayers?.has(node.layer) === true || hidesSubtree(node) || hiddenAbove(node.parent ?? null);
43
59
  },
44
- locked: inherited("locked", lockedLayers)
60
+ locked(id) {
61
+ const node = scene.get(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 = new Set<string>();\n const lockedLayers = new Set<string>();\n for (const l of scene.layers ?? []) {\n if (l.visible === false) hiddenLayers.add(l.id);\n if (l.locked === true) lockedLayers.add(l.id);\n }\n\n /** Whether `flag` holds on the node at `id` or any ancestor, the node's\n * layer counting when `layers` holds it. */\n const inherited = (flag: 'hidden' | 'locked', layers: Set<string> | null) => {\n const memo = new Map<string, boolean>();\n const climb = (id: string, seen: Set<string>): boolean => {\n const known = memo.get(id);\n if (known !== undefined) return known;\n const node = scene.get(id as NodeId);\n if (node === undefined || seen.has(id)) return false;\n seen.add(id);\n const parent = node.parent ?? null;\n const out = (flag === 'hidden' ? hidesSubtree(node) : node.locked === true)\n || (layers !== null && layers.has(node.layer))\n || (parent !== null && climb(parent, seen));\n memo.set(id, out);\n return out;\n };\n return (id: string) => climb(id, new Set());\n };\n\n const flaggedHidden = inherited('hidden', null);\n const locked = inherited('locked', lockedLayers);\n return {\n hidden(id) {\n const node = scene.get(id as NodeId);\n if (node === undefined) return false;\n return hiddenLayers.has(node.layer) || flaggedHidden(id);\n },\n locked,\n };\n}\n\n/** Whether `test` holds on the node at `id` or any ancestor, without the\n * allocation {@link nodeGates} spends on a walk: the scene's lock guard asks\n * this on every mutation. A parent cycle answers false, as the walk's does. */\nfunction climbs(scene: GateSource, id: string, test: (node: GatedNode) => boolean): boolean {\n let seen: Set<string> | null = null;\n let hops = 0;\n for (let at: string | null = id; at !== null; ) {\n const node = scene.get(at as NodeId);\n if (node === undefined) return false;\n if (++hops > 64) {\n seen ??= new Set();\n if (seen.has(at)) return false;\n seen.add(at);\n }\n if (test(node)) return true;\n at = node.parent ?? null;\n }\n return false;\n}\n\n/** The layers flagged `flag`, or null when none is: the usual answer, which\n * lets a climb skip the layer test at every hop. */\nfunction flaggedLayers(scene: GateSource, flag: 'locked' | 'hidden'): Set<string> | null {\n let out: Set<string> | null = null;\n for (const l of scene.layers ?? []) {\n if (flag === 'locked' ? l.locked === true : l.visible === false) (out ??= new Set()).add(l.id);\n }\n return out;\n}\n\n/** {@link NodeGates.hidden} for one id. */\nexport function isNodeHidden(scene: GateSource, id: NodeId): boolean {\n const node = scene.get(id);\n if (node === undefined) return false;\n return flaggedLayers(scene, 'hidden')?.has(node.layer) === true || climbs(scene, id, hidesSubtree);\n}\n\n/** {@link NodeGates.locked} for one id. */\nexport function isNodeLocked(scene: GateSource, id: NodeId): boolean {\n const layers = flaggedLayers(scene, 'locked');\n return climbs(scene, id, layers === null\n ? (node) => node.locked === true\n : (node) => node.locked === true || layers.has(node.layer));\n}\n"],"mappings":";;;;AAqCA,SAAgB,aAAa,MAA8C;CACzE,OAAO,KAAK,WAAW;AACzB;;;;;;AAeA,SAAgB,UAAU,OAA8B;CACtD,MAAM,+BAAe,IAAI,IAAY;CACrC,MAAM,+BAAe,IAAI,IAAY;CACrC,KAAK,MAAM,KAAK,MAAM,UAAU,CAAC,GAAG;EAClC,IAAI,EAAE,YAAY,OAAO,aAAa,IAAI,EAAE,EAAE;EAC9C,IAAI,EAAE,WAAW,MAAM,aAAa,IAAI,EAAE,EAAE;CAC9C;;;CAIA,MAAM,aAAa,MAA2B,WAA+B;EAC3E,MAAM,uBAAO,IAAI,IAAqB;EACtC,MAAM,SAAS,IAAY,SAA+B;GACxD,MAAM,QAAQ,KAAK,IAAI,EAAE;GACzB,IAAI,UAAU,QAAW,OAAO;GAChC,MAAM,OAAO,MAAM,IAAI,EAAY;GACnC,IAAI,SAAS,UAAa,KAAK,IAAI,EAAE,GAAG,OAAO;GAC/C,KAAK,IAAI,EAAE;GACX,MAAM,SAAS,KAAK,UAAU;GAC9B,MAAM,OAAO,SAAS,WAAW,aAAa,IAAI,IAAI,KAAK,WAAW,SAChE,WAAW,QAAQ,OAAO,IAAI,KAAK,KAAK,KACxC,WAAW,QAAQ,MAAM,QAAQ,IAAI;GAC3C,KAAK,IAAI,IAAI,GAAG;GAChB,OAAO;EACT;EACA,QAAQ,OAAe,MAAM,oBAAI,IAAI,IAAI,CAAC;CAC5C;CAEA,MAAM,gBAAgB,UAAU,UAAU,IAAI;CAE9C,OAAO;EACL,OAAO,IAAI;GACT,MAAM,OAAO,MAAM,IAAI,EAAY;GACnC,IAAI,SAAS,QAAW,OAAO;GAC/B,OAAO,aAAa,IAAI,KAAK,KAAK,KAAK,cAAc,EAAE;EACzD;EACA,QAPa,UAAU,UAAU,YAOjC;CACF;AACF;;;;AAKA,SAAS,OAAO,OAAmB,IAAY,MAA6C;CAC1F,IAAI,OAA2B;CAC/B,IAAI,OAAO;CACX,KAAK,IAAI,KAAoB,IAAI,OAAO,OAAQ;EAC9C,MAAM,OAAO,MAAM,IAAI,EAAY;EACnC,IAAI,SAAS,QAAW,OAAO;EAC/B,IAAI,EAAE,OAAO,IAAI;GACf,yBAAS,IAAI,IAAI;GACjB,IAAI,KAAK,IAAI,EAAE,GAAG,OAAO;GACzB,KAAK,IAAI,EAAE;EACb;EACA,IAAI,KAAK,IAAI,GAAG,OAAO;EACvB,KAAK,KAAK,UAAU;CACtB;CACA,OAAO;AACT;;;AAIA,SAAS,cAAc,OAAmB,MAA+C;CACvF,IAAI,MAA0B;CAC9B,KAAK,MAAM,KAAK,MAAM,UAAU,CAAC,GAC/B,IAAI,SAAS,WAAW,EAAE,WAAW,OAAO,EAAE,YAAY,OAAO,CAAC,wBAAQ,IAAI,IAAI,EAAA,CAAG,IAAI,EAAE,EAAE;CAE/F,OAAO;AACT;;AAGA,SAAgB,aAAa,OAAmB,IAAqB;CACnE,MAAM,OAAO,MAAM,IAAI,EAAE;CACzB,IAAI,SAAS,QAAW,OAAO;CAC/B,OAAO,cAAc,OAAO,QAAQ,CAAC,EAAE,IAAI,KAAK,KAAK,MAAM,QAAQ,OAAO,OAAO,IAAI,YAAY;AACnG;;AAGA,SAAgB,aAAa,OAAmB,IAAqB;CACnE,MAAM,SAAS,cAAc,OAAO,QAAQ;CAC5C,OAAO,OAAO,OAAO,IAAI,WAAW,QAC/B,SAAS,KAAK,WAAW,QACzB,SAAS,KAAK,WAAW,QAAQ,OAAO,IAAI,KAAK,KAAK,CAAC;AAC9D"}
1
+ {"version":3,"file":"nodeGates.js","names":[],"sources":["../../../src/core/scene/nodeGates.ts"],"sourcesContent":["/**\n * Whether a node is shown, and whether it is in reach — the one place either\n * question is answered. Painting, picking, area selection, select-all,\n * snapping, export, and the scene's own lock guard all ask here.\n *\n * - **Hidden**: its own layer is not visible, or it or any ancestor carries\n * `hidden`. A layer hides only the nodes tagged to it; a hidden node hides\n * its whole subtree, as SVG's `display=\"none\"` does.\n * - **Locked**: it or any ancestor carries `locked`, or sits on a locked\n * layer. A lock covers the whole subtree, whatever layers it spans.\n */\nimport type { NodeId } from './types';\n\n/** The node fields the gates read. A partial scene stand-in may leave `parent` out. */\nexport interface GatedNode {\n readonly layer: string;\n readonly parent?: string | null;\n readonly hidden?: boolean;\n readonly locked?: boolean;\n}\n\n/** The layer fields the gates read. Only an explicit `visible: false` hides. */\nexport interface GatedLayer {\n readonly id: string;\n readonly visible?: boolean;\n readonly locked?: boolean;\n}\n\n/** What the gates read of a scene. A `Scene` is one. */\nexport interface GateSource {\n readonly layers?: readonly GatedLayer[];\n get(id: NodeId): GatedNode | undefined;\n}\n\n/** Whether `node`'s own flag hides it and its subtree. A walk from the roots\n * down prunes where this holds instead of asking {@link NodeGates.hidden}\n * per node; the two agree because this is the flag that answer climbs for. */\nexport function hidesSubtree(node: { readonly hidden?: boolean }): boolean {\n return node.hidden === true;\n}\n\n/** Both answers, for one read of a scene. */\nexport interface NodeGates {\n /** Whether `id` is not drawn. An id not in the scene is not hidden. */\n hidden(id: string): boolean;\n /** Whether `id` is out of reach. An id not in the scene is not locked. */\n locked(id: string): boolean;\n}\n\n/**\n * The gates over `scene` as it stands now. Ancestor answers are remembered,\n * so a walk over every node climbs each chain once: build one per walk, and\n * a fresh one after an edit.\n */\nexport function nodeGates(scene: GateSource): NodeGates {\n const hiddenLayers = flaggedLayers(scene, 'hidden');\n const lockedLayers = flaggedLayers(scene, 'locked');\n const ownLock = lockedLayers === null\n ? (node: GatedNode) => node.locked === true\n : (node: GatedNode) => node.locked === true || lockedLayers.has(node.layer);\n\n /** Whether `test` holds on the node at `parent` or any of its ancestors.\n * Only ancestors are remembered, so a walk over a flat scene allocates\n * nothing; that walk is every pick made after an edit. */\n const above = (test: (node: GatedNode) => boolean) => {\n const memo = new Map<string, boolean>();\n const path: string[] = [];\n return (parent: string | null): boolean => {\n if (parent === null) return false;\n let out = false;\n let seen: Set<string> | null = null;\n path.length = 0;\n for (let at: string | null = parent; at !== null; ) {\n const known = memo.get(at);\n if (known !== undefined) { out = known; break; }\n const node = scene.get(at as NodeId);\n if (node === undefined) break;\n if (path.length > 64) {\n seen ??= new Set(path);\n if (seen.has(at)) break;\n seen.add(at);\n }\n path.push(at);\n if (test(node)) { out = true; break; }\n at = node.parent ?? null;\n }\n for (const id of path) memo.set(id, out);\n return out;\n };\n };\n\n const hiddenAbove = above(hidesSubtree);\n const lockedAbove = above(ownLock);\n return {\n hidden(id) {\n const node = scene.get(id as NodeId);\n if (node === undefined) return false;\n return hiddenLayers?.has(node.layer) === true || hidesSubtree(node) || hiddenAbove(node.parent ?? null);\n },\n locked(id) {\n const node = scene.get(id as NodeId);\n if (node === undefined) return false;\n return ownLock(node) || lockedAbove(node.parent ?? null);\n },\n };\n}\n\n/** Whether `test` holds on the node at `id` or any ancestor, without the\n * allocation {@link nodeGates} spends on a walk: the scene's lock guard asks\n * this on every mutation. A parent cycle answers false, as the walk's does. */\nfunction climbs(scene: GateSource, id: string, test: (node: GatedNode) => boolean): boolean {\n let seen: Set<string> | null = null;\n let hops = 0;\n for (let at: string | null = id; at !== null; ) {\n const node = scene.get(at as NodeId);\n if (node === undefined) return false;\n if (++hops > 64) {\n seen ??= new Set();\n if (seen.has(at)) return false;\n seen.add(at);\n }\n if (test(node)) return true;\n at = node.parent ?? null;\n }\n return false;\n}\n\n/** The layers flagged `flag`, or null when none is: the usual answer, which\n * lets a climb skip the layer test at every hop. */\nfunction flaggedLayers(scene: GateSource, flag: 'locked' | 'hidden'): Set<string> | null {\n let out: Set<string> | null = null;\n for (const l of scene.layers ?? []) {\n if (flag === 'locked' ? l.locked === true : l.visible === false) (out ??= new Set()).add(l.id);\n }\n return out;\n}\n\n/** {@link NodeGates.hidden} for one id. */\nexport function isNodeHidden(scene: GateSource, id: NodeId): boolean {\n const node = scene.get(id);\n if (node === undefined) return false;\n return flaggedLayers(scene, 'hidden')?.has(node.layer) === true || climbs(scene, id, hidesSubtree);\n}\n\n/** {@link NodeGates.locked} for one id. */\nexport function isNodeLocked(scene: GateSource, id: NodeId): boolean {\n const layers = flaggedLayers(scene, 'locked');\n return climbs(scene, id, layers === null\n ? (node) => node.locked === true\n : (node) => node.locked === true || layers.has(node.layer));\n}\n"],"mappings":"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"}
@@ -41,6 +41,7 @@ function createRemovalLog(shown) {
41
41
  return gone?.pose;
42
42
  },
43
43
  drain() {
44
+ if (removals.size === 0) return [];
44
45
  const out = [];
45
46
  for (const r of removals.values()) {
46
47
  const up = r.parent !== null ? removals.get(r.parent) : void 0;
@@ -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;GAEN,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"}
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"}
@@ -3,14 +3,16 @@
3
3
  * each present only when stated, so an unstated one keeps the scene's
4
4
  * default, and a flag states only `true`. What a copy of a node carries over
5
5
  * besides its kind, layer, pose, data and functions. */
6
- var statedFields = (n) => ({
7
- ...n.pickable !== void 0 ? { pickable: n.pickable } : {},
8
- ...n.hidden === true ? { hidden: true } : {},
9
- ...n.locked === true ? { locked: true } : {},
10
- ...n.units !== void 0 ? { units: n.units } : {},
11
- ...n.clip !== void 0 ? { clip: n.clip } : {},
12
- ...n.consumesChildren === true ? { consumesChildren: true } : {}
13
- });
6
+ function statedFields(n) {
7
+ const out = {};
8
+ if (n.pickable !== void 0) out.pickable = n.pickable;
9
+ if (n.hidden === true) out.hidden = true;
10
+ if (n.locked === true) out.locked = true;
11
+ if (n.units !== void 0) out.units = n.units;
12
+ if (n.clip !== void 0) out.clip = n.clip;
13
+ if (n.consumesChildren === true) out.consumesChildren = true;
14
+ return out;
15
+ }
14
16
 
15
17
  //#endregion
16
18
  export { statedFields };
@@ -1 +1 @@
1
- {"version":3,"file":"statedFields.js","names":[],"sources":["../../../src/core/scene/statedFields.ts"],"sourcesContent":["import type { UnitScope } from '@weasel-js/quantity';\nimport type { NodeClip } from './types';\n\n/** The plain optional fields a node or an `AddNodeSpec` may state. */\nexport interface StatedNodeFields {\n pickable?: boolean;\n hidden?: boolean;\n locked?: boolean;\n units?: UnitScope;\n clip?: NodeClip;\n consumesChildren?: boolean;\n}\n\n/** The plain optional fields of a node or spec as a spread for `scene.add`:\n * each present only when stated, so an unstated one keeps the scene's\n * default, and a flag states only `true`. What a copy of a node carries over\n * besides its kind, layer, pose, data and functions. */\nexport const statedFields = (n: StatedNodeFields & { kind?: string }): StatedNodeFields => ({\n ...(n.pickable !== undefined ? { pickable: n.pickable } : {}),\n ...(n.hidden === true ? { hidden: true } : {}),\n ...(n.locked === true ? { locked: true } : {}),\n ...(n.units !== undefined ? { units: n.units } : {}),\n ...(n.clip !== undefined ? { clip: n.clip } : {}),\n ...(n.consumesChildren === true ? { consumesChildren: true } : {}),\n});\n"],"mappings":";;;;;AAiBA,IAAa,gBAAgB,OAA+D;CAC1F,GAAI,EAAE,aAAa,SAAY,EAAE,UAAU,EAAE,SAAS,IAAI,CAAC;CAC3D,GAAI,EAAE,WAAW,OAAO,EAAE,QAAQ,KAAK,IAAI,CAAC;CAC5C,GAAI,EAAE,WAAW,OAAO,EAAE,QAAQ,KAAK,IAAI,CAAC;CAC5C,GAAI,EAAE,UAAU,SAAY,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC;CAClD,GAAI,EAAE,SAAS,SAAY,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;CAC/C,GAAI,EAAE,qBAAqB,OAAO,EAAE,kBAAkB,KAAK,IAAI,CAAC;AAClE"}
1
+ {"version":3,"file":"statedFields.js","names":[],"sources":["../../../src/core/scene/statedFields.ts"],"sourcesContent":["import type { UnitScope } from '@weasel-js/quantity';\nimport type { NodeClip } from './types';\n\n/** The plain optional fields a node or an `AddNodeSpec` may state. */\nexport interface StatedNodeFields {\n pickable?: boolean;\n hidden?: boolean;\n locked?: boolean;\n units?: UnitScope;\n clip?: NodeClip;\n consumesChildren?: boolean;\n}\n\n/** The plain optional fields of a node or spec as a spread for `scene.add`:\n * each present only when stated, so an unstated one keeps the scene's\n * default, and a flag states only `true`. What a copy of a node carries over\n * besides its kind, layer, pose, data and functions. */\nexport function statedFields(n: StatedNodeFields & { kind?: string }): StatedNodeFields {\n const out: StatedNodeFields = {};\n if (n.pickable !== undefined) out.pickable = n.pickable;\n if (n.hidden === true) out.hidden = true;\n if (n.locked === true) out.locked = true;\n if (n.units !== undefined) out.units = n.units;\n if (n.clip !== undefined) out.clip = n.clip;\n if (n.consumesChildren === true) out.consumesChildren = true;\n return out;\n}\n"],"mappings":";;;;;AAiBA,SAAgB,aAAa,GAA2D;CACtF,MAAM,MAAwB,CAAC;CAC/B,IAAI,EAAE,aAAa,QAAW,IAAI,WAAW,EAAE;CAC/C,IAAI,EAAE,WAAW,MAAM,IAAI,SAAS;CACpC,IAAI,EAAE,WAAW,MAAM,IAAI,SAAS;CACpC,IAAI,EAAE,UAAU,QAAW,IAAI,QAAQ,EAAE;CACzC,IAAI,EAAE,SAAS,QAAW,IAAI,OAAO,EAAE;CACvC,IAAI,EAAE,qBAAqB,MAAM,IAAI,mBAAmB;CACxD,OAAO;AACT"}
@@ -53,22 +53,17 @@ function createTreeEdits(env) {
53
53
  assertSubtreeLayer(spec.id, spec.layer, parent, p.layer);
54
54
  refuse(parent, "add under");
55
55
  }
56
- return {
56
+ const payload = {
57
57
  id,
58
58
  kind: spec.kind,
59
59
  layer: spec.layer,
60
60
  pose: spec.pose,
61
61
  data: spec.data,
62
62
  parent,
63
- index: spec.index ?? siblingsOf(parent).length,
64
- ...spec.dependsOn !== void 0 ? { dependsOn: spec.dependsOn } : {},
65
- ...statedFields(spec),
66
- ...fnKeysOf(fnFields, {
67
- ...spec,
68
- id,
69
- kind: spec.kind
70
- }, "opKey")
63
+ index: spec.index ?? siblingsOf(parent).length
71
64
  };
65
+ if (spec.dependsOn !== void 0) payload.dependsOn = spec.dependsOn;
66
+ return Object.assign(payload, statedFields(spec), fnKeysOf(fnFields, spec, "opKey"));
72
67
  }
73
68
  function removeMany(rootIds) {
74
69
  if (rootIds.length === 0) return;
@@ -1 +1 @@
1
- {"version":3,"file":"treeEdits.js","names":[],"sources":["../../../src/core/scene/treeEdits.ts"],"sourcesContent":["/**\n * The scene's edits to its tree: adding and removing nodes, reparenting, and\n * reordering siblings. Each checks its arguments, then records one kit op.\n */\nimport type { ArrivalWindow } from './arrivalWindow';\nimport type { RemoveSnapshot } from './kitOps';\nimport type { createLayerStack } from './layerStack';\nimport type { LockGuard } from './lockGuard';\nimport { fnKeysOf, type FnBearingNode, type NodeFnField } from './nodeFnFields';\nimport type { createNodeStore } from './nodeStore';\nimport type { Recorder } from './recorder';\nimport { SceneMembershipError } from './staleReplay';\nimport { statedFields } from './statedFields';\nimport type { AddNodeSpec, Node, NodeId, Scene } from './types';\n\nexport interface TreeEditsEnv<TData, TLayer extends string, TPose> {\n nodes: ReadonlyMap<NodeId, Node<TData, TLayer, TPose>>;\n generateId(): NodeId;\n store: Pick<\n ReturnType<typeof createNodeStore<TData, TLayer, TPose>>,\n 'requireNode' | 'siblingsOf' | 'descendants' | 'removalClosure' | 'patchNodeFunctions'\n >;\n locks: Pick<LockGuard<TLayer>, 'refuse' | 'refuseLayer' | 'lifted'>;\n layers: Pick<ReturnType<typeof createLayerStack<TLayer>>, 'requireLayerIndex' | 'assertSubtreeLayer'>;\n fnFields: readonly NodeFnField[];\n /** Run a registered op without recording it. */\n runOp(kind: string, payload: unknown): void;\n recorder: Pick<Recorder, 'record' | 'edit'>;\n arrivals: Pick<ArrivalWindow<TPose>, 'noteArrival' | 'noteDeparture' | 'noteChange'>;\n}\n\nexport interface TreeEdits<TData, TLayer extends string, TPose>\n extends Pick<Scene<TData, TLayer, TPose>, 'add' | 'remove' | 'removalClosure' | 'removeMany' | 'move' | 'reorder'> {\n /** Place nodes as the document a scene starts from, or is loaded with:\n * nothing is recorded, and no lock refuses. Specs list parents before\n * children, the order `toJSON()` emits. */\n construct(specs: readonly AddNodeSpec<TData, TLayer, TPose>[]): void;\n}\n\n/** What `kit:remove` carries for the nodes `ids`, read before any of them\n * goes. Throws on an id that is not in the scene. */\nfunction removeSnapshot<TData, TLayer extends string, TPose>(\n ids: readonly NodeId[],\n removing: ReadonlySet<NodeId>,\n store: Pick<TreeEditsEnv<TData, TLayer, TPose>['store'], 'requireNode' | 'siblingsOf'>,\n fnFields: readonly NodeFnField[],\n): RemoveSnapshot<TData, TLayer, TPose> {\n const nodes: Node<TData, TLayer, TPose>[] = ids.map((nid) => {\n const n = store.requireNode(nid);\n return n.children ? { ...n, children: [...n.children] } : { ...n };\n });\n // The rest is reachable through a snapshotted parent's cloned `children`.\n const detached = nodes\n .filter((n) => n.parent === null || !removing.has(n.parent))\n .map((n) => ({ id: n.id, parent: n.parent, index: store.siblingsOf(n.parent).indexOf(n.id) }))\n .sort((l, r) => l.index - r.index);\n const fnKeys = nodes.flatMap((n) => {\n const keys = fnKeysOf(fnFields, n as unknown as FnBearingNode, 'opKey');\n return Object.keys(keys).length === 0 ? [] : [{ id: n.id, ...keys }];\n });\n return { nodes, detached, ...(fnKeys.length > 0 ? { fnKeys } : {}) };\n}\n\nexport function createTreeEdits<TData, TLayer extends string, TPose>(\n env: TreeEditsEnv<TData, TLayer, TPose>,\n): TreeEdits<TData, TLayer, TPose> {\n const { nodes, fnFields } = env;\n const { requireNode, siblingsOf, descendants, removalClosure, patchNodeFunctions } = env.store;\n const { refuse, refuseLayer } = env.locks;\n const { requireLayerIndex, assertSubtreeLayer } = env.layers;\n const { record, edit } = env.recorder;\n const { noteArrival, noteDeparture, noteChange } = env.arrivals;\n\n /** Check that `spec` can be placed, and answer with the `kit:add` payload\n * that places it. Throws on an id collision, an unknown or locked layer,\n * a locked parent, and a layer below the\n * parent's. */\n function addPayload(spec: AddNodeSpec<TData, TLayer, TPose>) {\n const id = spec.id ?? env.generateId();\n if (nodes.has(id)) throw new SceneMembershipError(`Scene: id collision on \"${id}\"`);\n requireLayerIndex(spec.layer);\n refuseLayer(spec.layer, 'add to');\n const parent = spec.parent ?? null;\n if (parent !== null) {\n const p = requireNode(parent);\n assertSubtreeLayer(spec.id, spec.layer, parent, p.layer);\n refuse(parent, 'add under');\n }\n return {\n id, kind: spec.kind, layer: spec.layer, pose: spec.pose, data: spec.data,\n parent, index: spec.index ?? siblingsOf(parent).length,\n ...(spec.dependsOn !== undefined ? { dependsOn: spec.dependsOn } : {}),\n ...statedFields(spec),\n ...fnKeysOf(fnFields, { ...spec, id, kind: spec.kind } as unknown as FnBearingNode, 'opKey'),\n };\n }\n\n function removeMany(rootIds: readonly NodeId[]): void {\n if (rootIds.length === 0) return;\n // Every id resolves against the pre-removal tree, so an id another one\n // cascades away is absorbed rather than being a second, failing removal.\n const { ids, removing } = removalClosure(rootIds);\n for (const nid of ids) refuse(nid, 'remove');\n const payload = removeSnapshot(ids, removing, env.store, fnFields);\n const { detached } = payload;\n edit('remove', detached.some((d) => d.parent !== null), () => {\n for (const d of detached) noteDeparture(d.id, d.parent, null);\n record('kit:remove', payload, 'remove');\n });\n }\n\n return {\n construct(specs) {\n env.locks.lifted(() => {\n for (const spec of specs) {\n const payload = addPayload(spec);\n env.runOp('kit:add', payload);\n patchNodeFunctions(spec, payload.id, null);\n }\n });\n },\n\n add(spec) {\n const payload = addPayload(spec);\n const label = `add ${spec.kind}`;\n return edit(label, payload.parent !== null, () => {\n noteArrival(payload.id, payload.parent, null);\n record('kit:add', payload, label);\n patchNodeFunctions(spec, payload.id, payload);\n return payload.id;\n });\n },\n\n remove: (id) => removeMany([id]),\n\n removalClosure: (rootIds) => removalClosure(rootIds).ids,\n\n removeMany,\n\n move(id, parent, index) {\n const node = requireNode(id);\n refuse(id, 'move');\n if (parent !== null) {\n refuse(parent, 'move nodes into');\n const p = requireNode(parent);\n assertSubtreeLayer(id, node.layer, parent, p.layer);\n if (parent === id) throw new Error('Scene: cannot parent a node to itself');\n const desc: NodeId[] = [];\n descendants(id, desc);\n if (desc.includes(parent)) {\n throw new Error('Scene: cannot parent a node to its own descendant');\n }\n }\n const fromParent = node.parent;\n edit('move', parent !== null || fromParent !== null, () => {\n const fromIndex = siblingsOf(fromParent).indexOf(id);\n const toIndex = index ?? siblingsOf(parent).length;\n noteArrival(id, parent, fromParent);\n noteDeparture(id, fromParent, parent);\n noteChange(fromParent);\n record('kit:move', { id, fromParent, fromIndex, toParent: parent, toIndex }, 'move');\n });\n },\n\n reorder(id, index) {\n const node = requireNode(id);\n refuse(id, 'reorder');\n const parent = node.parent;\n edit('reorder', parent !== null, () => {\n noteChange(parent);\n const fromIndex = siblingsOf(parent).indexOf(id);\n record('kit:move', { id, fromParent: parent, fromIndex, toParent: parent, toIndex: index }, 'reorder');\n });\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"treeEdits.js","names":[],"sources":["../../../src/core/scene/treeEdits.ts"],"sourcesContent":["/**\n * The scene's edits to its tree: adding and removing nodes, reparenting, and\n * reordering siblings. Each checks its arguments, then records one kit op.\n */\nimport type { ArrivalWindow } from './arrivalWindow';\nimport type { RemoveSnapshot } from './kitOps';\nimport type { createLayerStack } from './layerStack';\nimport type { LockGuard } from './lockGuard';\nimport { fnKeysOf, type FnBearingNode, type NodeFnField } from './nodeFnFields';\nimport type { createNodeStore } from './nodeStore';\nimport type { Recorder } from './recorder';\nimport { SceneMembershipError } from './staleReplay';\nimport { statedFields } from './statedFields';\nimport type { AddNodeSpec, Node, NodeId, Scene } from './types';\n\nexport interface TreeEditsEnv<TData, TLayer extends string, TPose> {\n nodes: ReadonlyMap<NodeId, Node<TData, TLayer, TPose>>;\n generateId(): NodeId;\n store: Pick<\n ReturnType<typeof createNodeStore<TData, TLayer, TPose>>,\n 'requireNode' | 'siblingsOf' | 'descendants' | 'removalClosure' | 'patchNodeFunctions'\n >;\n locks: Pick<LockGuard<TLayer>, 'refuse' | 'refuseLayer' | 'lifted'>;\n layers: Pick<ReturnType<typeof createLayerStack<TLayer>>, 'requireLayerIndex' | 'assertSubtreeLayer'>;\n fnFields: readonly NodeFnField[];\n /** Run a registered op without recording it. */\n runOp(kind: string, payload: unknown): void;\n recorder: Pick<Recorder, 'record' | 'edit'>;\n arrivals: Pick<ArrivalWindow<TPose>, 'noteArrival' | 'noteDeparture' | 'noteChange'>;\n}\n\nexport interface TreeEdits<TData, TLayer extends string, TPose>\n extends Pick<Scene<TData, TLayer, TPose>, 'add' | 'remove' | 'removalClosure' | 'removeMany' | 'move' | 'reorder'> {\n /** Place nodes as the document a scene starts from, or is loaded with:\n * nothing is recorded, and no lock refuses. Specs list parents before\n * children, the order `toJSON()` emits. */\n construct(specs: readonly AddNodeSpec<TData, TLayer, TPose>[]): void;\n}\n\n/** What `kit:remove` carries for the nodes `ids`, read before any of them\n * goes. Throws on an id that is not in the scene. */\nfunction removeSnapshot<TData, TLayer extends string, TPose>(\n ids: readonly NodeId[],\n removing: ReadonlySet<NodeId>,\n store: Pick<TreeEditsEnv<TData, TLayer, TPose>['store'], 'requireNode' | 'siblingsOf'>,\n fnFields: readonly NodeFnField[],\n): RemoveSnapshot<TData, TLayer, TPose> {\n const nodes: Node<TData, TLayer, TPose>[] = ids.map((nid) => {\n const n = store.requireNode(nid);\n return n.children ? { ...n, children: [...n.children] } : { ...n };\n });\n // The rest is reachable through a snapshotted parent's cloned `children`.\n const detached = nodes\n .filter((n) => n.parent === null || !removing.has(n.parent))\n .map((n) => ({ id: n.id, parent: n.parent, index: store.siblingsOf(n.parent).indexOf(n.id) }))\n .sort((l, r) => l.index - r.index);\n const fnKeys = nodes.flatMap((n) => {\n const keys = fnKeysOf(fnFields, n as unknown as FnBearingNode, 'opKey');\n return Object.keys(keys).length === 0 ? [] : [{ id: n.id, ...keys }];\n });\n return { nodes, detached, ...(fnKeys.length > 0 ? { fnKeys } : {}) };\n}\n\nexport function createTreeEdits<TData, TLayer extends string, TPose>(\n env: TreeEditsEnv<TData, TLayer, TPose>,\n): TreeEdits<TData, TLayer, TPose> {\n const { nodes, fnFields } = env;\n const { requireNode, siblingsOf, descendants, removalClosure, patchNodeFunctions } = env.store;\n const { refuse, refuseLayer } = env.locks;\n const { requireLayerIndex, assertSubtreeLayer } = env.layers;\n const { record, edit } = env.recorder;\n const { noteArrival, noteDeparture, noteChange } = env.arrivals;\n\n /** Check that `spec` can be placed, and answer with the `kit:add` payload\n * that places it. Throws on an id collision, an unknown or locked layer,\n * a locked parent, and a layer below the\n * parent's. */\n function addPayload(spec: AddNodeSpec<TData, TLayer, TPose>) {\n const id = spec.id ?? env.generateId();\n if (nodes.has(id)) throw new SceneMembershipError(`Scene: id collision on \"${id}\"`);\n requireLayerIndex(spec.layer);\n refuseLayer(spec.layer, 'add to');\n const parent = spec.parent ?? null;\n if (parent !== null) {\n const p = requireNode(parent);\n assertSubtreeLayer(spec.id, spec.layer, parent, p.layer);\n refuse(parent, 'add under');\n }\n const payload: {\n id: NodeId; kind: 'mark' | 'group'; layer: TLayer; pose: TPose; data: TData;\n parent: NodeId | null; index: number; dependsOn?: AddNodeSpec<TData, TLayer, TPose>['dependsOn'];\n } = {\n id, kind: spec.kind, layer: spec.layer, pose: spec.pose, data: spec.data,\n parent, index: spec.index ?? siblingsOf(parent).length,\n };\n if (spec.dependsOn !== undefined) payload.dependsOn = spec.dependsOn;\n // Assigned rather than spread into the literal: a spread after named fields is V8's slow clone.\n return Object.assign(payload, statedFields(spec), fnKeysOf(fnFields, spec as unknown as FnBearingNode, 'opKey'));\n }\n\n function removeMany(rootIds: readonly NodeId[]): void {\n if (rootIds.length === 0) return;\n // Every id resolves against the pre-removal tree, so an id another one\n // cascades away is absorbed rather than being a second, failing removal.\n const { ids, removing } = removalClosure(rootIds);\n for (const nid of ids) refuse(nid, 'remove');\n const payload = removeSnapshot(ids, removing, env.store, fnFields);\n const { detached } = payload;\n edit('remove', detached.some((d) => d.parent !== null), () => {\n for (const d of detached) noteDeparture(d.id, d.parent, null);\n record('kit:remove', payload, 'remove');\n });\n }\n\n return {\n construct(specs) {\n env.locks.lifted(() => {\n for (const spec of specs) {\n const payload = addPayload(spec);\n env.runOp('kit:add', payload);\n patchNodeFunctions(spec, payload.id, null);\n }\n });\n },\n\n add(spec) {\n const payload = addPayload(spec);\n const label = `add ${spec.kind}`;\n return edit(label, payload.parent !== null, () => {\n noteArrival(payload.id, payload.parent, null);\n record('kit:add', payload, label);\n patchNodeFunctions(spec, payload.id, payload);\n return payload.id;\n });\n },\n\n remove: (id) => removeMany([id]),\n\n removalClosure: (rootIds) => removalClosure(rootIds).ids,\n\n removeMany,\n\n move(id, parent, index) {\n const node = requireNode(id);\n refuse(id, 'move');\n if (parent !== null) {\n refuse(parent, 'move nodes into');\n const p = requireNode(parent);\n assertSubtreeLayer(id, node.layer, parent, p.layer);\n if (parent === id) throw new Error('Scene: cannot parent a node to itself');\n const desc: NodeId[] = [];\n descendants(id, desc);\n if (desc.includes(parent)) {\n throw new Error('Scene: cannot parent a node to its own descendant');\n }\n }\n const fromParent = node.parent;\n edit('move', parent !== null || fromParent !== null, () => {\n const fromIndex = siblingsOf(fromParent).indexOf(id);\n const toIndex = index ?? siblingsOf(parent).length;\n noteArrival(id, parent, fromParent);\n noteDeparture(id, fromParent, parent);\n noteChange(fromParent);\n record('kit:move', { id, fromParent, fromIndex, toParent: parent, toIndex }, 'move');\n });\n },\n\n reorder(id, index) {\n const node = requireNode(id);\n refuse(id, 'reorder');\n const parent = node.parent;\n edit('reorder', parent !== null, () => {\n noteChange(parent);\n const fromIndex = siblingsOf(parent).indexOf(id);\n record('kit:move', { id, fromParent: parent, fromIndex, toParent: parent, toIndex: index }, 'reorder');\n });\n },\n };\n}\n"],"mappings":"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"}
@@ -1249,12 +1249,13 @@ interface SourceCandidate<P = Point> {
1249
1249
  /** `'handle'` is an angle about an anchor, as `'rotate'` is about a pivot,
1250
1250
  * but asks only sources that name it. */
1251
1251
  type SnapGesture = 'point' | 'move' | 'resize' | 'rotate' | 'handle';
1252
- /** The sides of a box a gesture moves on one axis; null for neither. */
1253
- type SnapEdge = 'min' | 'max' | 'both' | null;
1254
- /** Which sides of the subject's box a gesture moves, per axis. */
1255
- interface SnapEdges {
1256
- x: SnapEdge;
1257
- y: SnapEdge;
1252
+ /** One side of the subject's box a resize moves: on `axis`, its `side` moves
1253
+ * by `rate · (delta − delta0)`. A turned box's axis-aligned extent moves at
1254
+ * a rate off its axis, and both its sides on one axis may move at once. */
1255
+ interface SnapSide<P = Point> {
1256
+ axis: 'x' | 'y';
1257
+ side: 'min' | 'max';
1258
+ rate: P;
1258
1259
  }
1259
1260
  /** A point on the subject that should land on a locus. */
1260
1261
  interface SnapProbeAt<P = Point> {
@@ -1274,9 +1275,12 @@ interface SnapCollect<P = Point> {
1274
1275
  * puts it: a move's union of boxes, or the box a resize drags. Absent for
1275
1276
  * other gestures. */
1276
1277
  box?: FramedBox;
1277
- /** Which sides of `box` the gesture moves on each axis. Default: both, as
1278
- * a move does. */
1279
- edges?: SnapEdges;
1278
+ /** The sides of `box` a resize moves; a side not listed stands still.
1279
+ * Absent for a move, which carries the whole box with it. */
1280
+ sides?: readonly SnapSide<P>[];
1281
+ /** The subject's drawn box where the resolved move `delta` (a function
1282
+ * mark's argument) puts it. Absent where `box` is. */
1283
+ boxAt?(delta: P): FramedBox;
1280
1284
  /** `id`'s drawn box, measured as `box` is. Absent in a host with no 2D
1281
1285
  * scene. */
1282
1286
  drawnBox?(id: string): FramedBox | undefined;
@@ -1402,9 +1406,9 @@ interface SceneCanvasSnap {
1402
1406
  /** The reach of a source that sets none, in screen px. Default: the kit
1403
1407
  * setting `targets.snapPx`. */
1404
1408
  reachPx?: number;
1405
- /** How a pose's origin is read, for the `origin` probe a grid or a ruler
1406
- * guide snaps: a pose neither a rect nor a `Path` needs its own. Default
1407
- * `AUTO_ORIGIN_PROJECTION`. */
1409
+ /** How a pose's origin is read, for the `origin` probe a grid snaps on a
1410
+ * turned container's lattice: a pose neither a rect nor a `Path` needs
1411
+ * its own. Default `AUTO_ORIGIN_PROJECTION`. */
1408
1412
  originProjection?: OriginProjection<unknown>;
1409
1413
  /** Whether the canvas draws the snap marks. Default `true`. With `false`
1410
1414
  * the marks are still written to the `snapReadout` dep's store, for a
@@ -2847,4 +2851,4 @@ declare module '@weasel-js/routing' {
2847
2851
  }
2848
2852
  }
2849
2853
 
2850
- export { type SourceCandidate as $, type AnchorMarkerShape as A, type CreateOutlinesAdapter as B, type ContentHandlerEntry as C, type Dims as D, type EditAnchorsDep as E, type FitViewToBoundsOptions as F, type GestureLifecycleOptions as G, type HandleLocks as H, type InertiaConfig as I, type PaintKindEntry as J, type PathEditTarget as K, type LayerHit as L, type FinishedEditFrame as M, type PathSymmetryDep as N, type BooleanOpOptions as O, type PathEditMark as P, type BooleansAdapter as Q, type RenderLayer as R, type SnapSource as S, type TextOverflow as T, type BooleanOp as U, type ViewAnimationOptions as V, type PathPaint as W, type BooleanOpResult as X, type BooleanStrokes as Y, type SnapMods as Z, _resetPaintKindsForTests as _, type PathEditExtension as a, type PreviewPathsDep as a$, type SnapProbeAt as a0, type SnapCollect as a1, type SnapMark as a2, type SnapDep as a3, type SnapOutcome as a4, type SnapReadoutStyle as a5, type AngleCandidate as a6, type AngleCollect as a7, type AnimateToBoundsOptions as a8, type AreaSelectDep as a9, type LayerRetainPolicy as aA, type LayoutDep as aB, type Locus as aC, type MinSize as aD, type NodeAtPointDep as aE, type OutlinePathSpec as aF, PATH_OVERLAY_ROLES as aG, type PaintKind as aH, type PaintKindEditorProps as aI, type PaintKindFamily as aJ, type PaintKindLoader as aK, type PanBounds as aL, type PathEditDrag as aM, type PathEditDragStart as aN, type PathEditFinishCtx as aO, type PathEditHandleMove as aP, type PathEditMarksCtx as aQ, type PathEditOther as aR, type PathEditPart as aS, type PathEditStore as aT, type PathForm as aU, type PathOverlay as aV, type PathOverlayOptions as aW, type PathOverlayStyle as aX, type PathRepresentation as aY, type PathTransformFinishCtx as aZ, type PoseChange as a_, type BooleanOpPreview as aa, type BooleanPreviewPiece as ab, type CanDropIntoDep as ac, type ClipboardDep as ad, type ClipboardIngestCtx as ae, type CreateOutlinesResult as af, type CustomPaintContext as ag, DEFAULT_LAYER_RETAIN as ah, type DecayLoopConfig as ai, type DropRefusal as aj, type DropVerdict as ak, type FinishedPathEdit as al, GEOMETRY_BOUND_DATA as am, type HandleLock as an, type HitTestView as ao, type IngestCtx as ap, type IngestionDep as aq, type InsertDep as ar, type InsertEnds as as, type InsertExtras as at, type InsertIntoDep as au, type InsertLive as av, type LassoSelectDep as aw, type LayerCommandCache as ax, type LayerDrawFailure as ay, type LayerDrawSpan as az, type SnapGesture as b, type ResizePolicy as b0, STICKY_DEFAULTS as b1, type SnapBegin as b2, type SnapEdge as b3, type SnapEdges as b4, type SnapFrame as b5, type SnapMarks as b6, type SnapReadoutDep as b7, type SvgUnpacker as b8, type TextEditDep as b9, paintsLayer as bA, paletteInsertPaint as bB, pathPaintOf as bC, patternFamily as bD, previewBooleanOp as bE, refusalOf as bF, registerContentHandler as bG, registerPaintKind as bH, registerPaintKindLoader as bI, seedTile as bJ, stickyData as bK, stickyNodeFactory as bL, subscribeToSources as bM, switchGradientKind as bN, textBoxOf as bO, useDecayLoop as bP, useEnteredGroup as bQ, useViewAnimation as bR, useViewAnimationOn as bS, viewAxes as bT, withoutGeometryBoundData as bU, type TextNodeData as ba, type TransformPreviewNode as bb, type TransientPath as bc, VIEW_ANIMATION_KEY as bd, type ViewAnimationApi as be, type ViewApi as bf, type ViewChannel as bg, applyBooleanOp as bh, applyCreateOutlines as bi, asPaint as bj, createEnteredGroup as bk, createPathOverlay as bl, createSnapOutcomeStore as bm, depsUnchanged as bn, drawLayers as bo, drawOneLayer as bp, fitViewToBounds as bq, getPaintKind as br, isLayerPainted as bs, isLayerVisible as bt, listGradientKinds as bu, listPaintKinds as bv, minSizeOf as bw, paintKindOf as bx, paintKindRegistry as by, paintedLayers as bz, type SnapConstrain as c, type SnapResizeBox as d, type SnapGeometry as e, type SnapOutcomeStore as f, type SnapSession as g, type LayerDims as h, type ViewportDims as i, type RedrawSource as j, type EditAnchorsParams as k, type SliceDep as l, type StickyStyle as m, type EnteredGroupApi as n, type ChromeState as o, type LayerGroup as p, type TransformFollower as q, type GeometryProjection as r, type InsertNodeFactory as s, type InsertPaint as t, type SvgIngestOptions as u, type SceneCanvasSnap as v, type Affordance as w, type AffordanceRegion as x, type AffordanceBinding as y, type CommonAffordanceScratch as z };
2854
+ export { type SourceCandidate as $, type AnchorMarkerShape as A, type CreateOutlinesAdapter as B, type ContentHandlerEntry as C, type Dims as D, type EditAnchorsDep as E, type FitViewToBoundsOptions as F, type GestureLifecycleOptions as G, type HandleLocks as H, type InertiaConfig as I, type PaintKindEntry as J, type PathEditTarget as K, type LayerHit as L, type FinishedEditFrame as M, type PathSymmetryDep as N, type BooleanOpOptions as O, type PathEditMark as P, type BooleansAdapter as Q, type RenderLayer as R, type SnapSource as S, type TextOverflow as T, type BooleanOp as U, type ViewAnimationOptions as V, type PathPaint as W, type BooleanOpResult as X, type BooleanStrokes as Y, type SnapMods as Z, _resetPaintKindsForTests as _, type PathEditExtension as a, type PreviewPathsDep as a$, type SnapProbeAt as a0, type SnapCollect as a1, type SnapMark as a2, type SnapDep as a3, type SnapOutcome as a4, type SnapReadoutStyle as a5, type AngleCandidate as a6, type AngleCollect as a7, type AnimateToBoundsOptions as a8, type AreaSelectDep as a9, type LayerRetainPolicy as aA, type LayoutDep as aB, type Locus as aC, type MinSize as aD, type NodeAtPointDep as aE, type OutlinePathSpec as aF, PATH_OVERLAY_ROLES as aG, type PaintKind as aH, type PaintKindEditorProps as aI, type PaintKindFamily as aJ, type PaintKindLoader as aK, type PanBounds as aL, type PathEditDrag as aM, type PathEditDragStart as aN, type PathEditFinishCtx as aO, type PathEditHandleMove as aP, type PathEditMarksCtx as aQ, type PathEditOther as aR, type PathEditPart as aS, type PathEditStore as aT, type PathForm as aU, type PathOverlay as aV, type PathOverlayOptions as aW, type PathOverlayStyle as aX, type PathRepresentation as aY, type PathTransformFinishCtx as aZ, type PoseChange as a_, type BooleanOpPreview as aa, type BooleanPreviewPiece as ab, type CanDropIntoDep as ac, type ClipboardDep as ad, type ClipboardIngestCtx as ae, type CreateOutlinesResult as af, type CustomPaintContext as ag, DEFAULT_LAYER_RETAIN as ah, type DecayLoopConfig as ai, type DropRefusal as aj, type DropVerdict as ak, type FinishedPathEdit as al, GEOMETRY_BOUND_DATA as am, type HandleLock as an, type HitTestView as ao, type IngestCtx as ap, type IngestionDep as aq, type InsertDep as ar, type InsertEnds as as, type InsertExtras as at, type InsertIntoDep as au, type InsertLive as av, type LassoSelectDep as aw, type LayerCommandCache as ax, type LayerDrawFailure as ay, type LayerDrawSpan as az, type SnapGesture as b, type ResizePolicy as b0, STICKY_DEFAULTS as b1, type SnapBegin as b2, type SnapFrame as b3, type SnapMarks as b4, type SnapReadoutDep as b5, type SnapSide as b6, type SvgUnpacker as b7, type TextEditDep as b8, type TextNodeData as b9, paletteInsertPaint as bA, pathPaintOf as bB, patternFamily as bC, previewBooleanOp as bD, refusalOf as bE, registerContentHandler as bF, registerPaintKind as bG, registerPaintKindLoader as bH, seedTile as bI, stickyData as bJ, stickyNodeFactory as bK, subscribeToSources as bL, switchGradientKind as bM, textBoxOf as bN, useDecayLoop as bO, useEnteredGroup as bP, useViewAnimation as bQ, useViewAnimationOn as bR, viewAxes as bS, withoutGeometryBoundData as bT, type TransformPreviewNode as ba, type TransientPath as bb, VIEW_ANIMATION_KEY as bc, type ViewAnimationApi as bd, type ViewApi as be, type ViewChannel as bf, applyBooleanOp as bg, applyCreateOutlines as bh, asPaint as bi, createEnteredGroup as bj, createPathOverlay as bk, createSnapOutcomeStore as bl, depsUnchanged as bm, drawLayers as bn, drawOneLayer as bo, fitViewToBounds as bp, getPaintKind as bq, isLayerPainted as br, isLayerVisible as bs, listGradientKinds as bt, listPaintKinds as bu, minSizeOf as bv, paintKindOf as bw, paintKindRegistry as bx, paintedLayers as by, paintsLayer as bz, type SnapConstrain as c, type SnapResizeBox as d, type SnapGeometry as e, type SnapOutcomeStore as f, type SnapSession as g, type LayerDims as h, type ViewportDims as i, type RedrawSource as j, type EditAnchorsParams as k, type SliceDep as l, type StickyStyle as m, type EnteredGroupApi as n, type ChromeState as o, type LayerGroup as p, type TransformFollower as q, type GeometryProjection as r, type InsertNodeFactory as s, type InsertPaint as t, type SvgIngestOptions as u, type SceneCanvasSnap as v, type Affordance as w, type AffordanceRegion as x, type AffordanceBinding as y, type CommonAffordanceScratch as z };
@@ -1 +1 @@
1
- {"version":3,"file":"probes2d.js","names":[],"sources":["../../../src/domains/snap/probes2d.ts"],"sourcesContent":["import { unionBounds } from 'core/geometry/unionBounds';\nimport type { Bounds } from 'core/viewport/fitViewToBounds';\nimport type { SnapBegin } from 'interactions/actions/snapDep';\nimport type { SnapProbeAt } from './types';\nimport { resizeProbes, resizeSubject, type ResizeSubject } from './resizeBox';\nimport type { Point } from '@weasel-js/geom';\n\n/** How a 2D session reads its subject at press. */\nexport interface ProbeReads {\n /** `id`'s world pose now, or undefined when it is gone. */\n pose(id: string): unknown;\n /** The axis-aligned box `id` is drawn with at `pose`, stroke included. */\n drawnBox(id: string, pose: unknown): Bounds;\n /** The point of `pose` a grid rounds: its `OriginProjection` origin. */\n origin(pose: unknown): Point;\n /** The point of the subject's own geometry nearest `press`, or null. */\n geometryAt?(press: Point, ids: readonly string[]): Point | null;\n}\n\n/** The union drawn box of the subjects, or null when none is left. */\nexport function subjectBox(ids: readonly string[], reads: ProbeReads): Bounds | null {\n const boxes: Bounds[] = [];\n for (const id of ids) {\n const pose = reads.pose(id);\n if (pose !== undefined) boxes.push(reads.drawnBox(id, pose));\n }\n return unionBounds(boxes);\n}\n\n/** What a gesture drags, read once at press. */\nexport interface Subject2d {\n /** A move's subjects' union drawn box. */\n box: Bounds | null;\n /** A resize's box and frame: the one it names, else the subjects' drawn\n * box, axis-aligned. */\n resize: ResizeSubject | null;\n}\n\n/** The subject of `req`, null where the gesture has none or nothing is left. */\nexport function subjectOf(req: SnapBegin, reads: ProbeReads): Subject2d {\n if (req.gesture === 'move') return { box: subjectBox(req.ids ?? [], reads), resize: null };\n if (req.gesture !== 'resize' || !req.handle) return { box: null, resize: null };\n if (req.box) return { box: null, resize: resizeSubject(req.box, req.handle) };\n const bounds = subjectBox(req.ids ?? [], reads);\n return { box: null, resize: bounds ? resizeSubject({ bounds }, req.handle) : null };\n}\n\n/** The probes a gesture snaps, where they are at press. */\nexport function buildProbes(req: SnapBegin, reads: ProbeReads, subject = subjectOf(req, reads)): SnapProbeAt[] {\n const ids = req.ids ?? [];\n switch (req.gesture) {\n case 'point':\n return [{ id: 'pointer', at: req.press, role: 'pointer' }];\n case 'resize':\n return subject.resize ? resizeProbes(subject.resize) : [{ id: 'pointer', at: req.press, role: 'pointer' }];\n case 'move':\n return moveProbes(req.press, ids, reads, subject.box);\n case 'rotate':\n case 'handle':\n return [];\n }\n}\n\nfunction moveProbes(press: Point, ids: readonly string[], reads: ProbeReads, box: Bounds | null): SnapProbeAt[] {\n const out: SnapProbeAt[] = [];\n for (const id of ids) {\n const pose = reads.pose(id);\n if (pose !== undefined) out.push({ id: `origin:${id}`, at: reads.origin(pose), role: 'origin' });\n }\n if (box) {\n out.push(\n { id: 'box.min', at: { x: box.x, y: box.y }, role: 'box' },\n { id: 'box.center', at: { x: box.x + box.width / 2, y: box.y + box.height / 2 }, role: 'box' },\n { id: 'box.max', at: { x: box.x + box.width, y: box.y + box.height }, role: 'box' },\n );\n }\n const geometry = reads.geometryAt?.(press, ids) ?? press;\n out.push({ id: 'geometry', at: { x: geometry.x, y: geometry.y }, role: 'geometry' });\n return out;\n}\n"],"mappings":";;;;;AAoBA,SAAgB,WAAW,KAAwB,OAAkC;CACnF,MAAM,QAAkB,CAAC;CACzB,KAAK,MAAM,MAAM,KAAK;EACpB,MAAM,OAAO,MAAM,KAAK,EAAE;EAC1B,IAAI,SAAS,QAAW,MAAM,KAAK,MAAM,SAAS,IAAI,IAAI,CAAC;CAC7D;CACA,OAAO,YAAY,KAAK;AAC1B;;AAYA,SAAgB,UAAU,KAAgB,OAA8B;CACtE,IAAI,IAAI,YAAY,QAAQ,OAAO;EAAE,KAAK,WAAW,IAAI,OAAO,CAAC,GAAG,KAAK;EAAG,QAAQ;CAAK;CACzF,IAAI,IAAI,YAAY,YAAY,CAAC,IAAI,QAAQ,OAAO;EAAE,KAAK;EAAM,QAAQ;CAAK;CAC9E,IAAI,IAAI,KAAK,OAAO;EAAE,KAAK;EAAM,QAAQ,cAAc,IAAI,KAAK,IAAI,MAAM;CAAE;CAC5E,MAAM,SAAS,WAAW,IAAI,OAAO,CAAC,GAAG,KAAK;CAC9C,OAAO;EAAE,KAAK;EAAM,QAAQ,SAAS,cAAc,EAAE,OAAO,GAAG,IAAI,MAAM,IAAI;CAAK;AACpF;;AAGA,SAAgB,YAAY,KAAgB,OAAmB,UAAU,UAAU,KAAK,KAAK,GAAkB;CAC7G,MAAM,MAAM,IAAI,OAAO,CAAC;CACxB,QAAQ,IAAI,SAAZ;EACE,KAAK,SACH,OAAO,CAAC;GAAE,IAAI;GAAW,IAAI,IAAI;GAAO,MAAM;EAAU,CAAC;EAC3D,KAAK,UACH,OAAO,QAAQ,SAAS,aAAa,QAAQ,MAAM,IAAI,CAAC;GAAE,IAAI;GAAW,IAAI,IAAI;GAAO,MAAM;EAAU,CAAC;EAC3G,KAAK,QACH,OAAO,WAAW,IAAI,OAAO,KAAK,OAAO,QAAQ,GAAG;EACtD,KAAK;EACL,KAAK,UACH,OAAO,CAAC;CACZ;AACF;AAEA,SAAS,WAAW,OAAc,KAAwB,OAAmB,KAAmC;CAC9G,MAAM,MAAqB,CAAC;CAC5B,KAAK,MAAM,MAAM,KAAK;EACpB,MAAM,OAAO,MAAM,KAAK,EAAE;EAC1B,IAAI,SAAS,QAAW,IAAI,KAAK;GAAE,IAAI,UAAU;GAAM,IAAI,MAAM,OAAO,IAAI;GAAG,MAAM;EAAS,CAAC;CACjG;CACA,IAAI,KACF,IAAI,KACF;EAAE,IAAI;EAAW,IAAI;GAAE,GAAG,IAAI;GAAG,GAAG,IAAI;EAAE;EAAG,MAAM;CAAM,GACzD;EAAE,IAAI;EAAc,IAAI;GAAE,GAAG,IAAI,IAAI,IAAI,QAAQ;GAAG,GAAG,IAAI,IAAI,IAAI,SAAS;EAAE;EAAG,MAAM;CAAM,GAC7F;EAAE,IAAI;EAAW,IAAI;GAAE,GAAG,IAAI,IAAI,IAAI;GAAO,GAAG,IAAI,IAAI,IAAI;EAAO;EAAG,MAAM;CAAM,CACpF;CAEF,MAAM,WAAW,MAAM,aAAa,OAAO,GAAG,KAAK;CACnD,IAAI,KAAK;EAAE,IAAI;EAAY,IAAI;GAAE,GAAG,SAAS;GAAG,GAAG,SAAS;EAAE;EAAG,MAAM;CAAW,CAAC;CACnF,OAAO;AACT"}
1
+ {"version":3,"file":"probes2d.js","names":[],"sources":["../../../src/domains/snap/probes2d.ts"],"sourcesContent":["import { unionBounds } from 'core/geometry/unionBounds';\nimport type { Bounds } from 'core/viewport/fitViewToBounds';\nimport type { SnapBegin } from 'interactions/actions/snapDep';\nimport type { SnapProbeAt } from './types';\nimport { resizeProbes, resizeSubject, type ResizeSubject } from './resizeBox';\nimport type { Point } from '@weasel-js/geom';\n\n/** How a 2D session reads its subject at press. */\nexport interface ProbeReads {\n /** `id`'s world pose now, or undefined when it is gone. */\n pose(id: string): unknown;\n /** The axis-aligned box `id` is drawn with at `pose`, stroke included. */\n drawnBox(id: string, pose: unknown): Bounds;\n /** The point of `pose` the `origin` probe sits at: its `OriginProjection` origin. */\n origin(pose: unknown): Point;\n /** The point of the subject's own geometry nearest `press`, or null. */\n geometryAt?(press: Point, ids: readonly string[]): Point | null;\n}\n\n/** The union drawn box of the subjects, or null when none is left. */\nexport function subjectBox(ids: readonly string[], reads: ProbeReads): Bounds | null {\n const boxes: Bounds[] = [];\n for (const id of ids) {\n const pose = reads.pose(id);\n if (pose !== undefined) boxes.push(reads.drawnBox(id, pose));\n }\n return unionBounds(boxes);\n}\n\n/** What a gesture drags, read once at press. */\nexport interface Subject2d {\n /** A move's subjects' union drawn box. */\n box: Bounds | null;\n /** A resize's box and frame: the one it names, else the subjects' drawn\n * box, axis-aligned. */\n resize: ResizeSubject | null;\n}\n\n/** The subject of `req`, null where the gesture has none or nothing is left. */\nexport function subjectOf(req: SnapBegin, reads: ProbeReads): Subject2d {\n if (req.gesture === 'move') return { box: subjectBox(req.ids ?? [], reads), resize: null };\n if (req.gesture !== 'resize' || !req.handle) return { box: null, resize: null };\n if (req.box) return { box: null, resize: resizeSubject(req.box, req.handle) };\n const bounds = subjectBox(req.ids ?? [], reads);\n return { box: null, resize: bounds ? resizeSubject({ bounds }, req.handle) : null };\n}\n\n/** The probes a gesture snaps, where they are at press. */\nexport function buildProbes(req: SnapBegin, reads: ProbeReads, subject = subjectOf(req, reads)): SnapProbeAt[] {\n const ids = req.ids ?? [];\n switch (req.gesture) {\n case 'point':\n return [{ id: 'pointer', at: req.press, role: 'pointer' }];\n case 'resize':\n return subject.resize ? resizeProbes(subject.resize) : [{ id: 'pointer', at: req.press, role: 'pointer' }];\n case 'move':\n return moveProbes(req.press, ids, reads, subject.box);\n case 'rotate':\n case 'handle':\n return [];\n }\n}\n\nfunction moveProbes(press: Point, ids: readonly string[], reads: ProbeReads, box: Bounds | null): SnapProbeAt[] {\n const out: SnapProbeAt[] = [];\n for (const id of ids) {\n const pose = reads.pose(id);\n if (pose !== undefined) out.push({ id: `origin:${id}`, at: reads.origin(pose), role: 'origin' });\n }\n if (box) {\n out.push(\n { id: 'box.min', at: { x: box.x, y: box.y }, role: 'box' },\n { id: 'box.center', at: { x: box.x + box.width / 2, y: box.y + box.height / 2 }, role: 'box' },\n { id: 'box.max', at: { x: box.x + box.width, y: box.y + box.height }, role: 'box' },\n );\n }\n const geometry = reads.geometryAt?.(press, ids) ?? press;\n out.push({ id: 'geometry', at: { x: geometry.x, y: geometry.y }, role: 'geometry' });\n return out;\n}\n"],"mappings":";;;;;AAoBA,SAAgB,WAAW,KAAwB,OAAkC;CACnF,MAAM,QAAkB,CAAC;CACzB,KAAK,MAAM,MAAM,KAAK;EACpB,MAAM,OAAO,MAAM,KAAK,EAAE;EAC1B,IAAI,SAAS,QAAW,MAAM,KAAK,MAAM,SAAS,IAAI,IAAI,CAAC;CAC7D;CACA,OAAO,YAAY,KAAK;AAC1B;;AAYA,SAAgB,UAAU,KAAgB,OAA8B;CACtE,IAAI,IAAI,YAAY,QAAQ,OAAO;EAAE,KAAK,WAAW,IAAI,OAAO,CAAC,GAAG,KAAK;EAAG,QAAQ;CAAK;CACzF,IAAI,IAAI,YAAY,YAAY,CAAC,IAAI,QAAQ,OAAO;EAAE,KAAK;EAAM,QAAQ;CAAK;CAC9E,IAAI,IAAI,KAAK,OAAO;EAAE,KAAK;EAAM,QAAQ,cAAc,IAAI,KAAK,IAAI,MAAM;CAAE;CAC5E,MAAM,SAAS,WAAW,IAAI,OAAO,CAAC,GAAG,KAAK;CAC9C,OAAO;EAAE,KAAK;EAAM,QAAQ,SAAS,cAAc,EAAE,OAAO,GAAG,IAAI,MAAM,IAAI;CAAK;AACpF;;AAGA,SAAgB,YAAY,KAAgB,OAAmB,UAAU,UAAU,KAAK,KAAK,GAAkB;CAC7G,MAAM,MAAM,IAAI,OAAO,CAAC;CACxB,QAAQ,IAAI,SAAZ;EACE,KAAK,SACH,OAAO,CAAC;GAAE,IAAI;GAAW,IAAI,IAAI;GAAO,MAAM;EAAU,CAAC;EAC3D,KAAK,UACH,OAAO,QAAQ,SAAS,aAAa,QAAQ,MAAM,IAAI,CAAC;GAAE,IAAI;GAAW,IAAI,IAAI;GAAO,MAAM;EAAU,CAAC;EAC3G,KAAK,QACH,OAAO,WAAW,IAAI,OAAO,KAAK,OAAO,QAAQ,GAAG;EACtD,KAAK;EACL,KAAK,UACH,OAAO,CAAC;CACZ;AACF;AAEA,SAAS,WAAW,OAAc,KAAwB,OAAmB,KAAmC;CAC9G,MAAM,MAAqB,CAAC;CAC5B,KAAK,MAAM,MAAM,KAAK;EACpB,MAAM,OAAO,MAAM,KAAK,EAAE;EAC1B,IAAI,SAAS,QAAW,IAAI,KAAK;GAAE,IAAI,UAAU;GAAM,IAAI,MAAM,OAAO,IAAI;GAAG,MAAM;EAAS,CAAC;CACjG;CACA,IAAI,KACF,IAAI,KACF;EAAE,IAAI;EAAW,IAAI;GAAE,GAAG,IAAI;GAAG,GAAG,IAAI;EAAE;EAAG,MAAM;CAAM,GACzD;EAAE,IAAI;EAAc,IAAI;GAAE,GAAG,IAAI,IAAI,IAAI,QAAQ;GAAG,GAAG,IAAI,IAAI,IAAI,SAAS;EAAE;EAAG,MAAM;CAAM,GAC7F;EAAE,IAAI;EAAW,IAAI;GAAE,GAAG,IAAI,IAAI,IAAI;GAAO,GAAG,IAAI,IAAI,IAAI;EAAO;EAAG,MAAM;CAAM,CACpF;CAEF,MAAM,WAAW,MAAM,aAAa,OAAO,GAAG,KAAK;CACnD,IAAI,KAAK;EAAE,IAAI;EAAY,IAAI;GAAE,GAAG,SAAS;GAAG,GAAG,SAAS;EAAE;EAAG,MAAM;CAAW,CAAC;CACnF,OAAO;AACT"}
@@ -80,10 +80,10 @@ function movedBox(s, delta) {
80
80
  };
81
81
  return movedEdges(s.bounds, s.handle, local.x, local.y);
82
82
  }
83
- /** The box's axis-aligned extent once the handle has moved by `delta`. */
84
- function resizedBox(s, delta) {
83
+ /** The box's corners in ctx coordinates once the handle has moved by `delta`. */
84
+ function cornersAt(s, delta) {
85
85
  const b = movedBox(s, delta);
86
- const corners = [
86
+ return [
87
87
  s.at({
88
88
  x: b.x,
89
89
  y: b.y
@@ -101,6 +101,10 @@ function resizedBox(s, delta) {
101
101
  y: b.y + b.height
102
102
  })
103
103
  ];
104
+ }
105
+ /** The box's axis-aligned extent once the handle has moved by `delta`. */
106
+ function resizedBox(s, delta) {
107
+ const corners = cornersAt(s, delta);
104
108
  const xs = corners.map((c) => c.x);
105
109
  const ys = corners.map((c) => c.y);
106
110
  const x = Math.min(...xs);
@@ -112,26 +116,43 @@ function resizedBox(s, delta) {
112
116
  height: Math.max(...ys) - y
113
117
  };
114
118
  }
115
- /** The sides of {@link resizedBox} a drag of `delta` moves: the side across
116
- * from the pinned one, or the pinned side's once the drag has flipped the
117
- * box past it. A frame that turns or flips the box moves no side of its
118
- * extent alone. */
119
- function resizeEdges(s, delta) {
120
- if (!(Math.abs(s.ex.y) < EPS && Math.abs(s.ey.x) < EPS && s.ex.x > 0 && s.ey.y > 0)) return {
121
- x: null,
122
- y: null
123
- };
124
- const b = movedBox(s, delta);
125
- const moved = (pin, size) => {
126
- if (pin === "free") return null;
127
- return size >= 0 ? pin === "min" ? "max" : "min" : pin;
128
- };
129
- return {
130
- x: moved(s.handle.x, b.width),
131
- y: moved(s.handle.y, b.height)
132
- };
119
+ var near = (a, b, scale) => Math.abs(a - b) <= 1e-9 * Math.max(1, scale);
120
+ /** The sides of {@link resizedBox} a drag at `delta` moves, and how fast. A
121
+ * side moves with the corners at its extreme; where two of them tie and move
122
+ * apart, as a turned box's do at a kink of its extent, it is left out. */
123
+ function resizeSides(s, delta) {
124
+ const now = cornersAt(s, delta);
125
+ const stepX = cornersAt(s, {
126
+ x: delta.x + 1,
127
+ y: delta.y
128
+ });
129
+ const stepY = cornersAt(s, {
130
+ x: delta.x,
131
+ y: delta.y + 1
132
+ });
133
+ const out = [];
134
+ for (const axis of ["x", "y"]) {
135
+ const vs = now.map((c) => c[axis]);
136
+ const span = Math.max(...vs) - Math.min(...vs);
137
+ for (const side of ["min", "max"]) {
138
+ const edge = side === "min" ? Math.min(...vs) : Math.max(...vs);
139
+ const rates = now.flatMap((c, i) => near(c[axis], edge, span) ? [{
140
+ x: stepX[i][axis] - c[axis],
141
+ y: stepY[i][axis] - c[axis]
142
+ }] : []);
143
+ const rate = rates[0];
144
+ const size = Math.hypot(rate.x, rate.y);
145
+ if (size < EPS || rates.some((r) => !near(r.x, rate.x, size) || !near(r.y, rate.y, size))) continue;
146
+ out.push({
147
+ axis,
148
+ side,
149
+ rate
150
+ });
151
+ }
152
+ }
153
+ return out;
133
154
  }
134
155
 
135
156
  //#endregion
136
- export { EPS, diagonal, edgeAxis, resizeEdges, resizeProbes, resizeSubject, resizedBox };
157
+ export { EPS, diagonal, edgeAxis, resizeProbes, resizeSides, resizeSubject, resizedBox };
137
158
  //# sourceMappingURL=resizeBox.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"resizeBox.js","names":[],"sources":["../../../src/domains/snap/resizeBox.ts"],"sourcesContent":["import type { ResizeAnchor } from '@weasel-js/routing';\nimport type { Bounds } from 'core/viewport/fitViewToBounds';\nimport { fixedCornerOf, movedEdges, movingPoint } from 'interactions/actions/resize/cornerHandles';\nimport type { SnapResizeBox } from 'interactions/actions/snapDep';\nimport type { SnapEdge, SnapEdges, SnapProbeAt } from './types';\nimport type { Point } from '@weasel-js/geom';\n\nexport const EPS = 1e-9;\n\n/** A resize's box with its frame read out, in ctx coordinates. */\nexport interface ResizeSubject {\n /** In the box's own frame. */\n bounds: Bounds;\n handle: ResizeAnchor;\n at(p: Point): Point;\n /** Where the frame's unit axes point. */\n ex: Point;\n ey: Point;\n}\n\nexport function resizeSubject(box: SnapResizeBox, handle: ResizeAnchor): ResizeSubject {\n const at = box.toWorld ?? ((p: Point) => p);\n const o = at({ x: 0, y: 0 });\n const ax = at({ x: 1, y: 0 });\n const ay = at({ x: 0, y: 1 });\n return { bounds: box.bounds, handle, at, ex: { x: ax.x - o.x, y: ax.y - o.y }, ey: { x: ay.x - o.x, y: ay.y - o.y } };\n}\n\n/** The dragged corner or edge midpoint as `pointer`, and as `box` the dragged\n * corner or both ends of the dragged edge: the points a drag δ moves by δ. */\nexport function resizeProbes(s: ResizeSubject): SnapProbeAt[] {\n const { bounds: b, handle } = s;\n const out: SnapProbeAt[] = [{ id: 'pointer', at: s.at(movingPoint(b, handle)), role: 'pointer' }];\n const sides = (pin: ResizeAnchor['x'], lo: number, size: number): number[] =>\n pin === 'min' ? [lo + size] : pin === 'max' ? [lo] : [lo, lo + size];\n const xs = sides(handle.x, b.x, b.width);\n const ys = sides(handle.y, b.y, b.height);\n if (xs.length === 2 && ys.length === 2) return out;\n for (const x of xs) for (const y of ys) out.push({ id: `box.${out.length - 1}`, at: s.at({ x, y }), role: 'box' });\n return out;\n}\n\n/** The direction an edge handle's edge moves along; null for a corner. */\nexport function edgeAxis(s: ResizeSubject): Point | null {\n const { x, y } = s.handle;\n if ((x === 'free') === (y === 'free')) return null;\n return y === 'free' ? s.ex : s.ey;\n}\n\n/** From the fixed corner to the dragged one. */\nexport function diagonal(s: ResizeSubject): Point {\n const f = s.at(fixedCornerOf(s.bounds, s.handle));\n const m = s.at(movingPoint(s.bounds, s.handle));\n return { x: m.x - f.x, y: m.y - f.y };\n}\n\n/** The box in its own frame once the handle has moved by `delta`. */\nfunction movedBox(s: ResizeSubject, delta: Point): Bounds {\n const det = s.ex.x * s.ey.y - s.ey.x * s.ex.y;\n const local = Math.abs(det) < EPS\n ? { x: 0, y: 0 }\n : { x: (s.ey.y * delta.x - s.ey.x * delta.y) / det, y: (s.ex.x * delta.y - s.ex.y * delta.x) / det };\n return movedEdges(s.bounds, s.handle, local.x, local.y);\n}\n\n/** The box's axis-aligned extent once the handle has moved by `delta`. */\nexport function resizedBox(s: ResizeSubject, delta: Point): Bounds {\n const b = movedBox(s, delta);\n const corners = [\n s.at({ x: b.x, y: b.y }), s.at({ x: b.x + b.width, y: b.y }),\n s.at({ x: b.x, y: b.y + b.height }), s.at({ x: b.x + b.width, y: b.y + b.height }),\n ];\n const xs = corners.map((c) => c.x);\n const ys = corners.map((c) => c.y);\n const x = Math.min(...xs);\n const y = Math.min(...ys);\n return { x, y, width: Math.max(...xs) - x, height: Math.max(...ys) - y };\n}\n\n/** The sides of {@link resizedBox} a drag of `delta` moves: the side across\n * from the pinned one, or the pinned side's once the drag has flipped the\n * box past it. A frame that turns or flips the box moves no side of its\n * extent alone. */\nexport function resizeEdges(s: ResizeSubject, delta: Point): SnapEdges {\n const square = Math.abs(s.ex.y) < EPS && Math.abs(s.ey.x) < EPS && s.ex.x > 0 && s.ey.y > 0;\n if (!square) return { x: null, y: null };\n const b = movedBox(s, delta);\n const moved = (pin: ResizeAnchor['x'], size: number): SnapEdge => {\n if (pin === 'free') return null;\n const far = pin === 'min' ? 'max' : 'min';\n return size >= 0 ? far : pin;\n };\n return { x: moved(s.handle.x, b.width), y: moved(s.handle.y, b.height) };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"resizeBox.js","names":[],"sources":["../../../src/domains/snap/resizeBox.ts"],"sourcesContent":["import type { ResizeAnchor } from '@weasel-js/routing';\nimport type { Bounds } from 'core/viewport/fitViewToBounds';\nimport { fixedCornerOf, movedEdges, movingPoint } from 'interactions/actions/resize/cornerHandles';\nimport type { SnapResizeBox } from 'interactions/actions/snapDep';\nimport type { SnapProbeAt, SnapSide } from './types';\nimport type { Point } from '@weasel-js/geom';\n\nexport const EPS = 1e-9;\n\n/** A resize's box with its frame read out, in ctx coordinates. */\nexport interface ResizeSubject {\n /** In the box's own frame. */\n bounds: Bounds;\n handle: ResizeAnchor;\n at(p: Point): Point;\n /** Where the frame's unit axes point. */\n ex: Point;\n ey: Point;\n}\n\nexport function resizeSubject(box: SnapResizeBox, handle: ResizeAnchor): ResizeSubject {\n const at = box.toWorld ?? ((p: Point) => p);\n const o = at({ x: 0, y: 0 });\n const ax = at({ x: 1, y: 0 });\n const ay = at({ x: 0, y: 1 });\n return { bounds: box.bounds, handle, at, ex: { x: ax.x - o.x, y: ax.y - o.y }, ey: { x: ay.x - o.x, y: ay.y - o.y } };\n}\n\n/** The dragged corner or edge midpoint as `pointer`, and as `box` the dragged\n * corner or both ends of the dragged edge: the points a drag δ moves by δ. */\nexport function resizeProbes(s: ResizeSubject): SnapProbeAt[] {\n const { bounds: b, handle } = s;\n const out: SnapProbeAt[] = [{ id: 'pointer', at: s.at(movingPoint(b, handle)), role: 'pointer' }];\n const sides = (pin: ResizeAnchor['x'], lo: number, size: number): number[] =>\n pin === 'min' ? [lo + size] : pin === 'max' ? [lo] : [lo, lo + size];\n const xs = sides(handle.x, b.x, b.width);\n const ys = sides(handle.y, b.y, b.height);\n if (xs.length === 2 && ys.length === 2) return out;\n for (const x of xs) for (const y of ys) out.push({ id: `box.${out.length - 1}`, at: s.at({ x, y }), role: 'box' });\n return out;\n}\n\n/** The direction an edge handle's edge moves along; null for a corner. */\nexport function edgeAxis(s: ResizeSubject): Point | null {\n const { x, y } = s.handle;\n if ((x === 'free') === (y === 'free')) return null;\n return y === 'free' ? s.ex : s.ey;\n}\n\n/** From the fixed corner to the dragged one. */\nexport function diagonal(s: ResizeSubject): Point {\n const f = s.at(fixedCornerOf(s.bounds, s.handle));\n const m = s.at(movingPoint(s.bounds, s.handle));\n return { x: m.x - f.x, y: m.y - f.y };\n}\n\n/** The box in its own frame once the handle has moved by `delta`. */\nfunction movedBox(s: ResizeSubject, delta: Point): Bounds {\n const det = s.ex.x * s.ey.y - s.ey.x * s.ex.y;\n const local = Math.abs(det) < EPS\n ? { x: 0, y: 0 }\n : { x: (s.ey.y * delta.x - s.ey.x * delta.y) / det, y: (s.ex.x * delta.y - s.ex.y * delta.x) / det };\n return movedEdges(s.bounds, s.handle, local.x, local.y);\n}\n\n/** The box's corners in ctx coordinates once the handle has moved by `delta`. */\nfunction cornersAt(s: ResizeSubject, delta: Point): Point[] {\n const b = movedBox(s, delta);\n return [\n s.at({ x: b.x, y: b.y }), s.at({ x: b.x + b.width, y: b.y }),\n s.at({ x: b.x, y: b.y + b.height }), s.at({ x: b.x + b.width, y: b.y + b.height }),\n ];\n}\n\n/** The box's axis-aligned extent once the handle has moved by `delta`. */\nexport function resizedBox(s: ResizeSubject, delta: Point): Bounds {\n const corners = cornersAt(s, delta);\n const xs = corners.map((c) => c.x);\n const ys = corners.map((c) => c.y);\n const x = Math.min(...xs);\n const y = Math.min(...ys);\n return { x, y, width: Math.max(...xs) - x, height: Math.max(...ys) - y };\n}\n\nconst near = (a: number, b: number, scale: number): boolean => Math.abs(a - b) <= 1e-9 * Math.max(1, scale);\n\n/** The sides of {@link resizedBox} a drag at `delta` moves, and how fast. A\n * side moves with the corners at its extreme; where two of them tie and move\n * apart, as a turned box's do at a kink of its extent, it is left out. */\nexport function resizeSides(s: ResizeSubject, delta: Point): SnapSide[] {\n const now = cornersAt(s, delta);\n // The corners are affine in the drag, so a unit step reads their rate exactly.\n const stepX = cornersAt(s, { x: delta.x + 1, y: delta.y });\n const stepY = cornersAt(s, { x: delta.x, y: delta.y + 1 });\n const out: SnapSide[] = [];\n for (const axis of ['x', 'y'] as const) {\n const vs = now.map((c) => c[axis]);\n const span = Math.max(...vs) - Math.min(...vs);\n for (const side of ['min', 'max'] as const) {\n const edge = side === 'min' ? Math.min(...vs) : Math.max(...vs);\n const rates = now.flatMap((c, i) => (near(c[axis], edge, span)\n ? [{ x: stepX[i]![axis] - c[axis], y: stepY[i]![axis] - c[axis] }]\n : []));\n const rate = rates[0]!;\n const size = Math.hypot(rate.x, rate.y);\n if (size < EPS || rates.some((r) => !near(r.x, rate.x, size) || !near(r.y, rate.y, size))) continue;\n out.push({ axis, side, rate });\n }\n }\n return out;\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { resizeEdges, resizeSubject, resizedBox } from "./resizeBox.js";
1
+ import { resizeSides, resizeSubject, resizedBox } from "./resizeBox.js";
2
2
  import { markFromPlane } from "./marks.js";
3
3
  import { screenOf } from "./screen.js";
4
4
  import { angleAt } from "./angle2d.js";
@@ -113,17 +113,24 @@ function collectAll(sources, probesAtPress, fixed, { delta0, view, plane }, reac
113
113
  ...fixed.origin ? { origin: fixed.origin } : {}
114
114
  });
115
115
  }
116
- /** The subject's box where `delta0` puts it, and the sides that moved. */
116
+ /** The subject's box where `delta0` puts it, the sides that move, and where
117
+ * any other move would put it. */
117
118
  function subjectAt({ box, resize }, delta0) {
118
119
  if (resize) return {
119
120
  box: resizedBox(resize, delta0),
120
- edges: resizeEdges(resize, delta0)
121
+ sides: resizeSides(resize, delta0),
122
+ boxAt: (d) => resizedBox(resize, d)
121
123
  };
122
- return box ? { box: {
124
+ if (!box) return {};
125
+ const boxAt = (d) => ({
123
126
  ...box,
124
- x: box.x + delta0.x,
125
- y: box.y + delta0.y
126
- } } : {};
127
+ x: box.x + d.x,
128
+ y: box.y + d.y
129
+ });
130
+ return {
131
+ box: boxAt(delta0),
132
+ boxAt
133
+ };
127
134
  }
128
135
  /** A session no source answers: it only holds the point to the gesture's
129
136
  * input subspace, as when a host provides no `snap` dep. */