slidemind 0.3.20 → 0.4.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.
Files changed (185) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/AGENTS.md +591 -15
  4. package/README.md +202 -7
  5. package/bin/slidemind.mjs +50 -2
  6. package/package.json +2 -2
  7. package/skills/create-slides/SKILL.md +484 -886
  8. package/skills/search/SKILL.md +46 -23
  9. package/src/bridge/build-and-push.mjs +548 -73
  10. package/src/bridge/bundle.mjs +100 -31
  11. package/src/bridge/capture.mjs +14 -17
  12. package/src/bridge/client/brands.mjs +157 -18
  13. package/src/bridge/client/engine-routing.mjs +48 -13
  14. package/src/bridge/client/engine-search.mjs +24 -1
  15. package/src/bridge/client/render.mjs +27 -1
  16. package/src/bridge/client/transport.mjs +499 -23
  17. package/src/bridge/client.mjs +10 -0
  18. package/src/bridge/config.mjs +67 -6
  19. package/src/bridge/engine-download.mjs +0 -10
  20. package/src/bridge/graph-auth.mjs +55 -3
  21. package/src/bridge/image-intent-envelope.mjs +239 -0
  22. package/src/bridge/package-preview.mjs +67 -0
  23. package/src/bridge/sharepoint.mjs +914 -0
  24. package/src/builders/asset-fetch.mjs +203 -0
  25. package/src/builders/block-dividers.mjs +83 -0
  26. package/src/builders/build.mjs +171 -87
  27. package/src/builders/card-height.mjs +294 -0
  28. package/src/builders/chart-hole.mjs +183 -0
  29. package/src/builders/chart-jsx.mjs +410 -0
  30. package/src/builders/chart-labels.mjs +326 -0
  31. package/src/builders/concept-geometry.mjs +370 -0
  32. package/src/builders/concept-intent.mjs +200 -0
  33. package/src/builders/concept-layout.mjs +79 -0
  34. package/src/builders/concept-lines.mjs +509 -0
  35. package/src/builders/concept-lists.mjs +270 -0
  36. package/src/builders/concept-marks.mjs +84 -0
  37. package/src/builders/concept-relationships.mjs +12 -0
  38. package/src/builders/concept-spec.mjs +645 -0
  39. package/src/builders/concept-text.mjs +140 -0
  40. package/src/builders/concept-waves.mjs +57 -0
  41. package/src/builders/copy-fit.mjs +139 -0
  42. package/src/builders/design-tokens.mjs +347 -0
  43. package/src/builders/draft-check.mjs +552 -340
  44. package/src/builders/draft-lines.mjs +369 -0
  45. package/src/builders/edit-route.mjs +29 -0
  46. package/src/builders/figure-sketch.mjs +375 -0
  47. package/src/builders/fold-sets.mjs +639 -0
  48. package/src/builders/gate-server.mjs +9 -4
  49. package/src/builders/gate-session.js +273 -12
  50. package/src/builders/gate-shell.js +1494 -0
  51. package/src/builders/image-intent-check.mjs +308 -0
  52. package/src/builders/image-use-reconcile.mjs +107 -0
  53. package/src/builders/kit-helpers.mjs +121 -0
  54. package/src/builders/layout-names.mjs +32 -0
  55. package/src/builders/legacy-reference-uses.mjs +179 -0
  56. package/src/builders/line-fit.mjs +224 -0
  57. package/src/builders/line-native.mjs +895 -0
  58. package/src/builders/line-relations.mjs +230 -0
  59. package/src/builders/lint-fix.mjs +117 -9
  60. package/src/builders/lint.mjs +25 -20
  61. package/src/builders/list-jsx.mjs +88 -0
  62. package/src/builders/placement.mjs +133 -0
  63. package/src/builders/plan-checks.mjs +135 -0
  64. package/src/builders/plan-comments.mjs +19 -0
  65. package/src/builders/plan-handlers.mjs +95 -0
  66. package/src/builders/plan-markdown-edit.mjs +3 -3
  67. package/src/builders/plan-overlay.js +1530 -1608
  68. package/src/builders/plan.mjs +689 -186
  69. package/src/builders/preset-definitions.json +1 -0
  70. package/src/builders/preset-eval.mjs +244 -0
  71. package/src/builders/preview.html +51 -120
  72. package/src/builders/red-thread-backbone.mjs +135 -36
  73. package/src/builders/repair.mjs +205 -0
  74. package/src/builders/repeat-groups.mjs +540 -0
  75. package/src/builders/resolve-skill.mjs +5 -4
  76. package/src/builders/review-overlay.js +296 -293
  77. package/src/builders/review.mjs +305 -84
  78. package/src/builders/rule-shift.mjs +96 -0
  79. package/src/builders/seat.mjs +354 -0
  80. package/src/builders/settle-driver.mjs +236 -0
  81. package/src/builders/settle.mjs +1448 -0
  82. package/src/builders/slide-prep.mjs +67 -0
  83. package/src/builders/spec-assets.mjs +236 -0
  84. package/src/builders/spec-match.mjs +56 -0
  85. package/src/builders/stack.mjs +413 -0
  86. package/src/builders/storyboard-markdown.mjs +70 -27
  87. package/src/builders/storyboard-schema.mjs +54 -114
  88. package/src/builders/text-blocks.mjs +212 -0
  89. package/src/builders/text-card-fit.mjs +552 -0
  90. package/src/builders/text-stack.mjs +276 -0
  91. package/src/builders/transcribe.mjs +1069 -0
  92. package/src/cli/assets.mjs +164 -0
  93. package/src/cli/common.mjs +173 -12
  94. package/src/cli/concept-spec-support.mjs +76 -0
  95. package/src/cli/concepts.mjs +1002 -103
  96. package/src/cli/deck.mjs +401 -65
  97. package/src/cli/diagnose.mjs +4 -2
  98. package/src/cli/engine-helpers.mjs +17 -10
  99. package/src/cli/engine-results.mjs +76 -3
  100. package/src/cli/help.mjs +232 -28
  101. package/src/cli/install.mjs +9 -0
  102. package/src/cli/maintenance.mjs +104 -0
  103. package/src/cli/read.mjs +2 -1
  104. package/src/cli/ready.mjs +6 -3
  105. package/src/cli/record.mjs +522 -0
  106. package/src/cli/remote.mjs +818 -0
  107. package/src/cli/revision.mjs +686 -0
  108. package/src/cli/search.mjs +30 -11
  109. package/src/cli/session.mjs +56 -13
  110. package/src/cli/setup.mjs +46 -2
  111. package/src/cli/status.mjs +142 -33
  112. package/src/cli/tasks.mjs +246 -0
  113. package/src/cli/transcribe.mjs +854 -0
  114. package/src/cli/workspace-ui.mjs +260 -0
  115. package/src/cli/workspace.mjs +212 -34
  116. package/src/install-channel.mjs +64 -7
  117. package/src/lib/adopt.mjs +346 -25
  118. package/src/lib/concept-spec-binding.mjs +96 -0
  119. package/src/lib/conversions.mjs +70 -6
  120. package/src/lib/convert-folded.mjs +107 -0
  121. package/src/lib/deck-paths.mjs +418 -0
  122. package/src/lib/durable-ids.mjs +47 -0
  123. package/src/lib/edit-operations.mjs +316 -0
  124. package/src/lib/edit-state.mjs +1244 -0
  125. package/src/lib/edit-transitions.mjs +120 -0
  126. package/src/lib/extensions.mjs +501 -0
  127. package/src/lib/image-assets.mjs +465 -0
  128. package/src/lib/image-derivatives.mjs +234 -0
  129. package/src/lib/image-intent-fingerprint.mjs +70 -0
  130. package/src/lib/image-intent-tags.mjs +176 -0
  131. package/src/lib/image-intent.mjs +802 -0
  132. package/src/lib/image-use-overrides.mjs +279 -0
  133. package/src/lib/inline-components.mjs +613 -0
  134. package/src/lib/lint-converted.mjs +68 -4
  135. package/src/lib/merge-json.mjs +163 -0
  136. package/src/lib/overwrite-guard.mjs +3 -3
  137. package/src/lib/presentation-context.mjs +531 -0
  138. package/src/lib/preservation-tags.mjs +157 -0
  139. package/src/lib/preservation-verdict.mjs +1342 -0
  140. package/src/lib/publish.mjs +1557 -0
  141. package/src/lib/rebase.mjs +945 -0
  142. package/src/lib/reconcile-ack.mjs +4 -6
  143. package/src/lib/reconcile.mjs +62 -24
  144. package/src/lib/record-client.mjs +175 -0
  145. package/src/lib/record-state.mjs +151 -0
  146. package/src/lib/remote.mjs +661 -0
  147. package/src/lib/revision.mjs +1210 -0
  148. package/src/lib/slide-guids.mjs +4 -6
  149. package/src/lib/slide-preservation.mjs +423 -0
  150. package/src/lib/slide-selection.mjs +9 -0
  151. package/src/lib/workspace-transaction.mjs +117 -0
  152. package/src/pptx/content-digest.mjs +334 -0
  153. package/src/pptx/deck-assemble.mjs +1278 -0
  154. package/src/pptx/fill/engine.mjs +2 -3
  155. package/src/pptx/fill/freeform.mjs +6 -3
  156. package/src/pptx/ooxml.mjs +20 -0
  157. package/src/pptx/package-evidence.mjs +383 -0
  158. package/src/pptx/tags.mjs +163 -11
  159. package/src/update-check.mjs +22 -4
  160. package/src/workspace/adapters.mjs +222 -0
  161. package/src/workspace/auth.mjs +256 -0
  162. package/src/workspace/autostart.mjs +204 -0
  163. package/src/workspace/daemon.mjs +360 -0
  164. package/src/workspace/discovery.mjs +180 -0
  165. package/src/workspace/edit.mjs +167 -0
  166. package/src/workspace/events.mjs +52 -0
  167. package/src/workspace/gate-client.mjs +290 -0
  168. package/src/workspace/handback.mjs +73 -0
  169. package/src/workspace/lifecycle.mjs +635 -0
  170. package/src/workspace/operations.mjs +200 -0
  171. package/src/workspace/pick-folder.mjs +152 -0
  172. package/src/workspace/registry.mjs +359 -0
  173. package/src/workspace/remote-links.mjs +397 -0
  174. package/src/workspace/remote-status.mjs +359 -0
  175. package/src/workspace/routes.mjs +1331 -0
  176. package/src/workspace/server.mjs +159 -0
  177. package/src/workspace/sync.mjs +1119 -0
  178. package/src/workspace/task-actions.mjs +215 -0
  179. package/src/workspace/tasks.mjs +507 -0
  180. package/src/workspace/ui/app.js +1886 -0
  181. package/src/workspace/ui/index.html +141 -0
  182. package/src/workspace/ui/plan-bridge.js +120 -0
  183. package/src/workspace/ui/sharepoint.svg +59 -0
  184. package/src/workspace/ui/styles.css +423 -0
  185. package/src/builders/visual-families.mjs +0 -46
@@ -0,0 +1,96 @@
1
+ // A free vertical separator between two columns (live f85c57c3: four one-line
2
+ // titles, "Permissions", "Audit trail", each running 12–30px past its box
3
+ // through the separator beside it) is an ornament of the gutter, not a wall:
4
+ // where a title set at the ramp is wider than the room the concept drew up to
5
+ // it, the separator moves into its gutter, clear of the title's words, so
6
+ // long as the gutter still holds it clear of the next column. The title keeps
7
+ // its one line and its size; the separator keeps its length and its column,
8
+ // its card or panel, and the slot. The separators of a declared set move
9
+ // alike, by one px, or none does.
10
+ import { correspond, gluedIds, linePoints, lineSpans, moveLine, repeatGroups } from "./repeat-groups.mjs";
11
+
12
+ /**
13
+ * The gutter right of a free vertical rule: its stroke's left edge `x` and the
14
+ * px `room` it may move right, or null when it is no free vertical line or
15
+ * something already spans it. It ends at `right` (the inner edge of the room
16
+ * the text owns: its card, else the slot), the panel the rule lies on, and
17
+ * whatever lies right of it along its length — an element or a line segment.
18
+ */
19
+ function gutter(spec, rule, { right, beside }, gap) {
20
+ const pts = linePoints(rule);
21
+ if (rule.from || rule.to || gluedIds(rule).length || pts.length < 2) return null;
22
+ const xs = pts.map((p) => p.x);
23
+ const ys = pts.map((p) => p.y);
24
+ if (Math.max(...xs) - Math.min(...xs) > 1) return null;
25
+ const half = (Number.isFinite(rule.strokeWidth) ? rule.strokeWidth : 1) / 2;
26
+ const x = Math.max(...xs) + half;
27
+ const y0 = Math.min(...ys);
28
+ const y1 = Math.max(...ys);
29
+ let limit = right;
30
+ for (const o of spec.elements ?? []) {
31
+ if (!beside(o) || !(o.w > 0 && o.h > 0) || o.y >= y1 || o.y + o.h <= y0 || o.x + o.w <= x) continue;
32
+ // A panel behind the whole rule is its ground: the rule stays on it.
33
+ if (o.x <= x - 2 * half && o.x + o.w >= x && o.y <= y0 && o.y + o.h >= y1) limit = Math.min(limit, o.x + o.w);
34
+ else if (o.x < x) return null; // something already spans the rule: it is no gutter
35
+ else limit = Math.min(limit, o.x);
36
+ }
37
+ for (const l of lineSpans(spec, y0, y1)) if (l.line !== rule && l.x >= x) limit = Math.min(limit, l.x);
38
+ return { x: x - 2 * half, room: limit - gap - x };
39
+ }
40
+
41
+ // The texts that correspond to `text` across its declared set, member by member.
42
+ function counterparts(spec, text) {
43
+ const byId = new Map((spec.elements ?? []).map((e) => [e.id, e]));
44
+ // A virtual member (repeat-groups.mjs) is the loose parts it lists.
45
+ const inTree = (e, root) => {
46
+ const roots = root.ids ?? [root.id];
47
+ for (let p = e, g = 0; p && g < 32; p = p.parent ? byId.get(p.parent) : null, g++) if (roots.includes(p.id)) return true;
48
+ return false;
49
+ };
50
+ for (const g of repeatGroups(spec)) {
51
+ const i = g.members.findIndex((m) => inTree(text, m));
52
+ if (i < 0) continue;
53
+ if (g.members[i] === text) return g.members.map((m) => ({ member: m, t: m }));
54
+ const kids = correspond(spec.elements, g.members);
55
+ const k = kids?.[i].indexOf(text) ?? -1;
56
+ return k < 0 ? [] : g.members.map((m, j) => ({ member: m, t: kids[j][k] }));
57
+ }
58
+ return null;
59
+ }
60
+
61
+ /**
62
+ * Move the separator beside `text` right: its stroke at `least` or later
63
+ * (the text's ink plus the gap), as far as `to` (PowerPoint's slack) where
64
+ * the gutter allows. In a declared set every corresponding separator (the
65
+ * one at the same place in its member) moves by the same px — the least
66
+ * room across them, at least what the widest text needs — or none moves.
67
+ * `fit(t)` → `{ rule, right, to, least, beside, holds }` for a text (null
68
+ * when it has no rule beside it); a move after which a text that needed it
69
+ * still does not `holds()` its ink is undone. Returns the px moved, or null.
70
+ */
71
+ export function shiftSeparator(spec, text, fit, gap = 4) {
72
+ const lead = fit(text);
73
+ if (!lead) return null;
74
+ const set = counterparts(spec, text);
75
+ let rows = [lead];
76
+ if (set) {
77
+ const at = set.find((c) => c.t === text)?.member;
78
+ const offset = (rule, m) => linePoints(rule).map((p) => [p.x - m.x, p.y - m.y]);
79
+ const same = (a, b) => a.length === b.length && a.every((p, i) => Math.abs(p[0] - b[i][0]) <= 2 && Math.abs(p[1] - b[i][1]) <= 2);
80
+ rows = set.map(({ member, t }) => {
81
+ const f = t === text ? lead : t && fit(t);
82
+ return f && at && same(offset(f.rule, member), offset(lead.rule, at)) ? f : null;
83
+ });
84
+ if (!rows.length || rows.some((r) => !r) || new Set(rows.map((r) => r.rule)).size !== rows.length) return null;
85
+ }
86
+ const gutters = rows.map((r) => gutter(spec, r.rule, r, gap));
87
+ if (gutters.some((g) => !g)) return null;
88
+ const must = rows.map((r, i) => Math.ceil(r.least - gutters[i].x));
89
+ const want = Math.max(...rows.map((r, i) => Math.ceil(r.to - gutters[i].x)));
90
+ const dx = Math.min(want, Math.floor(Math.min(...gutters.map((g) => g.room))));
91
+ if (Math.max(...must) <= 0 || dx < Math.max(...must)) return null;
92
+ for (const r of rows) moveLine(r.rule, dx, 0);
93
+ if (rows.every((r, i) => must[i] <= 0 || r.holds())) return dx;
94
+ for (const r of rows) moveLine(r.rule, -dx, 0);
95
+ return null;
96
+ }
@@ -0,0 +1,354 @@
1
+ // Where a child sits in its carrier is a RELATION the reader declares (`seat`,
2
+ // concept-spec-schema.ts), built here from the carrier's rect and the child's
3
+ // own size — never the offset the painting happened to draw. The painting is a
4
+ // sketch: a glyph lands a few px off its disc's middle, a text beside a disc
5
+ // a little above the card's axis, an accent tab three px short of the card's
6
+ // edge (x100b QBR 5/6; 23% of the children of round carriers in 300 reader
7
+ // specs sit >2px off, 47% of rings off their hub). The same construction
8
+ // serves every pass: the normaliser (painted sizes), the template build
9
+ // (repeat-groups.mjs) and the settle (measured text heights, text-card-fit.mjs
10
+ // / card-height.mjs), so a seated child stays seated when its carrier moves,
11
+ // grows or is rebuilt from a template.
12
+ // seat.x: left | center | right | span — against that edge at the drawn
13
+ // inset, centred, or edge to edge (a text spans the inner width).
14
+ // seat.y: top | middle | bottom | span.
15
+ // around: the round element a ring, arc or loop stroke circles — it takes
16
+ // that hub's centre (the hub anchors the slide; the ring follows).
17
+ // A seat the reader did not declare changes nothing: the child keeps the
18
+ // painting's offset, as before. No tolerance decides anything here.
19
+ import { translateElement } from "./concept-geometry.mjs";
20
+ import { paintOrdered } from "./concept-layout.mjs";
21
+ import { fixCopies, linePoints, linesWithin, translateFigure } from "./repeat-groups.mjs";
22
+
23
+ export const SEAT_X = Object.freeze(["left", "center", "right", "span"]);
24
+ export const SEAT_Y = Object.freeze(["top", "middle", "bottom", "span"]);
25
+ const INSET = 8; // px a spanning text keeps from its carrier's edge (text-card-fit.mjs)
26
+ const half = (v) => Math.round(v * 2) / 2;
27
+ const isText = (e) => (e.kind === "text" || e.kind === "number") && e.text != null && !(e.fill && e.fill !== "none");
28
+
29
+ /** The reader's seat, validated per axis; null when none was declared. */
30
+ export function seatOf(e) {
31
+ const s = e?.seat;
32
+ if (!s || typeof s !== "object") return null;
33
+ const x = SEAT_X.includes(s.x) ? s.x : null;
34
+ const y = SEAT_Y.includes(s.y) ? s.y : null;
35
+ return x || y ? { x, y } : null;
36
+ }
37
+
38
+ /** The drawn insets of `e` in its frame `f` ({x,y,w,h}), kept for the edge anchors. */
39
+ const insetsOf = (e, f) => ({ left: e.x - f.x, top: e.y - f.y, right: f.x + f.w - e.x - e.w, bottom: f.y + f.h - e.y - e.h });
40
+
41
+ // ---- The frame a seat refers to -------------------------------------------
42
+ // A parent that stacks content — rows of an icon and its text between
43
+ // dividers, a header over them, columns beside each other — seats a child in
44
+ // the child's own ROW and COLUMN, not in the whole parent: `middle` is the
45
+ // middle of the band between what lies above the row (a divider, the header)
46
+ // and what lies below it (x101: four metric rows all seated "middle" in one
47
+ // card collapsed onto the card's centre, and their dividers ran through the
48
+ // text). The structure is read ONCE from the painting (`seatBand`: the
49
+ // siblings sharing the child's rows and columns, what lies beyond them, the
50
+ // dividers between) and the band is measured from where those are NOW, so a
51
+ // row follows a grown neighbour and a divider the levers moved.
52
+ const axisLine = (c, u) => {
53
+ const pts = linePoints(c);
54
+ if (pts.length < 2) return null;
55
+ const vs = pts.map((p) => p[u]);
56
+ const v = u === "y" ? "x" : "y";
57
+ if (Math.max(...vs) - Math.min(...vs) > 2) return null;
58
+ return { at: (Math.min(...vs) + Math.max(...vs)) / 2, lo: Math.min(...pts.map((p) => p[v])), hi: Math.max(...pts.map((p) => p[v])) };
59
+ };
60
+ const lineRef = (spec, c) => c.id ?? `#${(spec.connectors ?? []).indexOf(c)}`;
61
+ const lineByRef = (spec, ref) => (ref == null ? null : (spec.connectors ?? []).find((c, i) => c.id === ref || `#${i}` === ref) ?? null);
62
+
63
+ // One axis of the band: `u` is the axis the band runs along (y for rows).
64
+ // A LINE is the siblings sharing e's rows (an icon and its text); the band's
65
+ // bounds are what lies beyond the line: the nearest other sibling or divider.
66
+ function bandOf(spec, e, p, sibs, lines, u) {
67
+ const v = u === "y" ? "x" : "y";
68
+ const [size, cross] = u === "y" ? ["h", "w"] : ["w", "h"];
69
+ const overlap = (a, b) => a[u] < b[u] + b[size] && b[u] < a[u] + a[size];
70
+ const lineOf = (seed, pool) => {
71
+ const line = [seed];
72
+ for (let grew = true; grew;) {
73
+ grew = false;
74
+ for (const o of pool) if (!line.includes(o) && line.some((g) => overlap(g, o))) { line.push(o); grew = true; }
75
+ }
76
+ return line;
77
+ };
78
+ const extent = (items) => [Math.min(...items.map((g) => g[u])), Math.max(...items.map((g) => g[u] + g[size]))];
79
+ const spansOf = (items) => {
80
+ const c0 = Math.min(...items.map((g) => g[v]));
81
+ const c1 = Math.max(...items.map((g) => g[v] + g[cross]));
82
+ return (o) => o[v] < c1 && c0 < o[v] + o[cross];
83
+ };
84
+ const seps = lines.map((c) => ({ c, l: axisLine(c, u) })).filter((x) => x.l);
85
+ // Several children all seated "middle" in one band are NOT joined into a
86
+ // stack here: a stack is the reader's declaration (`stacks`, stack.mjs). A
87
+ // child's line is the siblings sharing its rows (an icon and its text).
88
+ const block = [lineOf(e, sibs)];
89
+ const members = block.flat();
90
+ const [lo, hi] = extent(members);
91
+ const spans = spansOf(members);
92
+ const ends = block.map(extent);
93
+ return {
94
+ group: block[block.findIndex((l) => l.includes(e))].map((g) => g.id),
95
+ block: block.map((l) => l.map((g) => g.id)),
96
+ gaps: ends.slice(1).map(([a], i) => a - ends[i][1]),
97
+ before: sibs.filter((o) => !members.includes(o) && o[u] + o[size] <= lo + 0.5 && spans(o)).map((o) => o.id),
98
+ after: sibs.filter((o) => !members.includes(o) && o[u] >= hi - 0.5 && spans(o)).map((o) => o.id),
99
+ sepBefore: seps.filter((x) => x.l.at <= lo + 0.5 && spans({ [v]: x.l.lo, [cross]: x.l.hi - x.l.lo })).sort((a, b) => b.l.at - a.l.at)[0]?.c ?? null,
100
+ sepAfter: seps.filter((x) => x.l.at >= hi - 0.5 && spans({ [v]: x.l.lo, [cross]: x.l.hi - x.l.lo })).sort((a, b) => a.l.at - b.l.at)[0]?.c ?? null,
101
+ };
102
+ }
103
+
104
+ /**
105
+ * Where a centred child sits along `u`: its block (seatBand) centred in the
106
+ * band `[lo, lo+len)`, lines stacked at their drawn gaps on their CURRENT
107
+ * sizes (`at(o)` → a text's measured rect), the child centred in its line.
108
+ * null without a recorded block.
109
+ */
110
+ export function blockStart(spec, e, u, lo, len, at = (o) => o) {
111
+ const band = e.seatBand?.[u];
112
+ if (!band?.block) return null;
113
+ const size = u === "y" ? "h" : "w";
114
+ const byId = new Map((spec.elements ?? []).map((o) => [o.id, o]));
115
+ const sizeOf = (id) => { const o = byId.get(id); return o ? (at(o)[size] ?? o[size]) : 0; };
116
+ const lineSizes = band.block.map((ids) => Math.max(0, ...ids.map(sizeOf)));
117
+ const gaps = band.gaps ?? [];
118
+ const total = lineSizes.reduce((a, b) => a + b, 0) + gaps.reduce((a, b) => a + b, 0);
119
+ let start = lo + (len - total) / 2;
120
+ for (const [i, ids] of band.block.entries()) {
121
+ if (ids.includes(e.id)) return half(start + (lineSizes[i] - sizeOf(e.id)) / 2);
122
+ start += lineSizes[i] + (gaps[i] ?? 0);
123
+ }
124
+ return null;
125
+ }
126
+
127
+ /** The painting's structure around `e` in `p`, recorded once (`seatBand`). */
128
+ export function recordBand(spec, e, p) {
129
+ const els = spec.elements ?? [];
130
+ const sibs = els.filter((o) => o.parent === p.id && o !== e && o.w > 0 && o.h > 0 && o.kind !== "rule");
131
+ const lines = linesWithin(spec, p);
132
+ const strip = (b) => ({ group: b.group, block: b.block, gaps: b.gaps, before: b.before, after: b.after, sepBefore: b.sepBefore ? lineRef(spec, b.sepBefore) : null, sepAfter: b.sepAfter ? lineRef(spec, b.sepAfter) : null });
133
+ return { y: strip(bandOf(spec, e, p, sibs, lines, "y")), x: strip(bandOf(spec, e, p, sibs, lines, "x")) };
134
+ }
135
+
136
+ /**
137
+ * The frame `e`'s seat refers to, measured now: the parent's box cut to the
138
+ * child's row and column by the nearest sibling or divider on each side.
139
+ * `at(o)` → the current rect of an element (a text's measured one).
140
+ */
141
+ export function seatFrame(spec, e, p, at = (o) => o) {
142
+ const byId = new Map((spec.elements ?? []).map((o) => [o.id, o]));
143
+ const rect = (o) => ({ x: o.x, y: o.y, w: o.w, h: o.h, ...at(o) });
144
+ const f = { x: p.x, y: p.y, w: p.w, h: p.h };
145
+ const b = e.seatBand;
146
+ if (!b) return f;
147
+ for (const [u, size] of [["y", "h"], ["x", "w"]]) {
148
+ const band = b[u];
149
+ if (!band) continue;
150
+ let lo = f[u];
151
+ let hi = f[u] + f[size];
152
+ for (const id of band.before ?? []) { const o = byId.get(id); if (o) lo = Math.max(lo, rect(o)[u] + rect(o)[size]); }
153
+ for (const id of band.after ?? []) { const o = byId.get(id); if (o) hi = Math.min(hi, rect(o)[u]); }
154
+ const la = axisLine(lineByRef(spec, band.sepBefore) ?? { }, u);
155
+ const lb = axisLine(lineByRef(spec, band.sepAfter) ?? { }, u);
156
+ if (la) lo = Math.max(lo, la.at);
157
+ if (lb) hi = Math.min(hi, lb.at);
158
+ if (hi - lo > 1) { f[u] = lo; f[size] = hi - lo; }
159
+ }
160
+ return f;
161
+ }
162
+
163
+ /**
164
+ * The top a y-seat gives a child `h` tall in a carrier band [top, top+H).
165
+ * `top` (and a text's `span`) keep the drawn inset; null for no y-seat.
166
+ */
167
+ export function seatTop(e, top, H, h, insets = e.seatInset ?? null) {
168
+ const y = seatOf(e)?.y;
169
+ if (!y) return null;
170
+ if (y === "middle" || (y === "span" && isText(e))) return half(top + (H - h) / 2);
171
+ if (y === "span") return half(top);
172
+ if (y === "bottom") return half(top + H - (insets?.bottom ?? 0) - h);
173
+ return half(top + (insets?.top ?? 0));
174
+ }
175
+
176
+ /** The left an x-seat gives a child `w` wide in a carrier band [left, left+W). */
177
+ export function seatLeft(e, left, W, w, insets = e.seatInset ?? null) {
178
+ const x = seatOf(e)?.x;
179
+ if (!x) return null;
180
+ if (x === "center") return half(left + (W - w) / 2);
181
+ if (x === "right") return half(left + W - (insets?.right ?? 0) - w);
182
+ if (x === "span") return half(left + (isText(e) ? INSET : 0));
183
+ return half(left + (insets?.left ?? 0));
184
+ }
185
+
186
+ /**
187
+ * The rect a seat gives `e` (of `size`) in its frame; spans resize, every
188
+ * other seat only moves. With `ctx` ({ spec, at }) a centred child sits by
189
+ * its block (blockStart); without, alone in the frame.
190
+ */
191
+ export function seatRect(e, frame, size = { w: e.w, h: e.h }, ctx = null) {
192
+ const s = seatOf(e);
193
+ if (!s) return null;
194
+ const insets = e.seatInset ?? insetsOf(e, frame);
195
+ const w = s.x === "span" ? frame.w - (isText(e) ? 2 * INSET : 0) : size.w;
196
+ const h = s.y === "span" && !isText(e) ? frame.h : size.h;
197
+ const at = ctx?.at ?? ((o) => o);
198
+ const atSized = (o) => (o === e ? { ...at(o), w, h } : at(o));
199
+ const bx = ctx && s.x === "center" ? blockStart(ctx.spec, e, "x", frame.x, frame.w, atSized) : null;
200
+ const by = ctx && (s.y === "middle" || (s.y === "span" && isText(e))) ? blockStart(ctx.spec, e, "y", frame.y, frame.h, atSized) : null;
201
+ return {
202
+ x: bx ?? seatLeft(e, frame.x, frame.w, w, insets) ?? e.x,
203
+ y: by ?? seatTop(e, frame.y, frame.h, h, insets) ?? e.y,
204
+ w: half(w),
205
+ h: s.y === "span" && !isText(e) ? half(h) : e.h,
206
+ };
207
+ }
208
+
209
+ // Scale a drawn element's own geometry along one axis (a span): its points,
210
+ // outline, spline knots, path commands, an arc's centre and radius.
211
+ function scaleAxis(e, axis, from, to) {
212
+ if (!(from.size > 0) || !(to.size > 0)) return;
213
+ const k = to.size / from.size;
214
+ const map = (v) => to.at + (v - from.at) * k;
215
+ for (const p of [...(e.points ?? []), ...(e.outline ?? [])]) p[axis] = map(p[axis]);
216
+ const g = e.geometry;
217
+ if (g?.type === "spline") for (const p of g.points ?? []) p[axis] = map(p[axis]);
218
+ else if (g?.type === "path") for (const c of g.commands ?? []) for (const kx of axis === "x" ? ["x", "x1", "x2"] : ["y", "y1", "y2"]) if (Number.isFinite(c[kx])) c[kx] = map(c[kx]);
219
+ else if (g) { g[`c${axis}`] = map(g[`c${axis}`]); g[`r${axis}`] *= k; }
220
+ if (e.textFrame) { e.textFrame[axis] = map(e.textFrame[axis]); e.textFrame[axis === "x" ? "w" : "h"] *= k; }
221
+ }
222
+
223
+ // A sketched figure spans only when no circle shapes it: an arc on a circle
224
+ // cannot stretch along one axis. Such a figure moves to the edge instead. A
225
+ // copy takes its source's shape, so it only moves (its source spans).
226
+ const figureOf = (sketch, id) => (sketch?.figures ?? []).find((x) => x.id === id);
227
+ const figureScalable = (f) => f && !f.copyOf && !(f.mirror && f.mirror.axis === "line")
228
+ && ![...(f.outline ?? []), ...(f.holes ?? []).flat()].some((p) => p?.next === "arc");
229
+ function scaleFigure(f, axis, from, to) {
230
+ const k = to.size / from.size;
231
+ const map = (v) => to.at + (v - from.at) * k;
232
+ for (const p of [...(f.outline ?? []), ...(f.holes ?? []).flat()]) p[axis] = map(p[axis]);
233
+ const m = f.mirror;
234
+ if (m && ((m.axis === "horizontal" && axis === "y") || (m.axis === "vertical" && axis === "x"))) m.at = map(m.at);
235
+ }
236
+
237
+ /**
238
+ * Seat every child the reader seated: parents before children, so a seat in
239
+ * a seated carrier lands in the carrier's new place. `sizeOf(e)` gives a
240
+ * text's MEASURED size where a render exists (the painting's height is a
241
+ * plan); `only` restricts the pass to those ids. The first pass records the
242
+ * drawn insets (`seatInset`) for the edge anchors. Returns one line per move.
243
+ */
244
+ export function applySeats(spec, { sizeOf = null, only = null, beforeFold = false } = {}) {
245
+ const els = spec.elements ?? [];
246
+ const byId = new Map(els.map((e) => [e.id, e]));
247
+ // Before the folds (`beforeFold`), the lines of one text block (`block`,
248
+ // foldTextBlocks) wait: they are seated as the ONE element they fold into,
249
+ // and seating each line alone would stack a title and its body on the same
250
+ // centre. A block the fold refused (lines side by side) is seated line by
251
+ // line afterwards like any other child.
252
+ const blockSize = new Map();
253
+ for (const e of els) if (e.block) blockSize.set(e.block, (blockSize.get(e.block) ?? 0) + 1);
254
+ const inBlock = (e) => beforeFold && e.block && blockSize.get(e.block) > 1 && !e.paragraphs;
255
+ const figures = new Set((spec.sketch?.figures ?? []).map((f) => f.id));
256
+ const deltas = new Map();
257
+ const movedCircles = new Set();
258
+ const changes = [];
259
+ const kids = (id) => els.filter((o) => o.parent === id);
260
+ const move = (e, dx, dy) => {
261
+ if (!dx && !dy) return;
262
+ translateElement(e, dx, dy);
263
+ if (figures.has(e.id)) translateFigure(spec.sketch, e.id, dx, dy, deltas, movedCircles);
264
+ for (const k of kids(e.id)) move(k, dx, dy);
265
+ };
266
+ // The painting's structure first, before anything moves in this pass.
267
+ for (const e of els) {
268
+ const p = e.parent ? byId.get(e.parent) : null;
269
+ if (p && seatOf(e) && !inBlock(e) && !e.seatBand && p.w > 0 && p.h > 0) e.seatBand = recordBand(spec, e, p);
270
+ }
271
+ for (const e of paintOrdered(els)) {
272
+ const p = e.parent ? byId.get(e.parent) : null;
273
+ const s = seatOf(e);
274
+ if (!p || !s || inBlock(e) || (only && !only.has(e.id)) || !(p.w > 0 && p.h > 0) || !(e.w >= 0 && e.h >= 0)) continue;
275
+ const frame = seatFrame(spec, e, p);
276
+ e.seatInset ??= insetsOf(e, frame);
277
+ const to = seatRect(e, frame, sizeOf?.(e) ?? { w: e.w, h: e.h }, { spec });
278
+ const spanW = s.x === "span" && to.w !== e.w;
279
+ const spanH = s.y === "span" && !isText(e) && to.h !== e.h;
280
+ const scaled = { dx: 0, dy: 0 };
281
+ if (spanW || spanH) {
282
+ const f = figures.has(e.id) ? figureOf(spec.sketch, e.id) : null;
283
+ const stretch = !f || figureScalable(f);
284
+ for (const [axis, on, size] of [["x", spanW, "w"], ["y", spanH, "h"]]) {
285
+ if (!on) continue;
286
+ const from = { at: e[axis], size: e[size] };
287
+ const dest = { at: to[axis], size: stretch || f?.copyOf ? to[size] : e[size] };
288
+ if (stretch) {
289
+ if (f) scaleFigure(f, axis, from, dest);
290
+ else scaleAxis(e, axis, from, dest);
291
+ scaled[`d${axis}`] = dest.at - from.at;
292
+ e[axis] = dest.at;
293
+ } else if (f && !f.copyOf) changes.push(`${e.id}: a figure on a circle cannot stretch to span ${p.id}; moved to its edge`);
294
+ e[size] = dest.size;
295
+ }
296
+ }
297
+ const dx = half(to.x - e.x);
298
+ const dy = half(to.y - e.y);
299
+ if (!dx && !dy && !spanW && !spanH) continue;
300
+ move(e, dx, dy);
301
+ // A scaled source moved its own origin too: its copies offset from that.
302
+ if (figures.has(e.id) && (scaled.dx || scaled.dy)) {
303
+ const d = deltas.get(e.id) ?? { dx: 0, dy: 0 };
304
+ deltas.set(e.id, { dx: d.dx + scaled.dx, dy: d.dy + scaled.dy });
305
+ }
306
+ changes.push(`${e.id}: seated ${[s.x, s.y].filter(Boolean).join("/")} in ${p.id}${dx || dy ? ` (${dx >= 0 ? "+" : ""}${dx}, ${dy >= 0 ? "+" : ""}${dy})` : ""}`);
307
+ }
308
+ fixCopies(spec.sketch, deltas);
309
+ return changes;
310
+ }
311
+
312
+ const roundish = (e) => e.kind === "disc" || ["circle", "ring", "ellipse"].includes(e.shape) || (e.geometry && ["arc", "sector"].includes(e.geometry.type));
313
+
314
+ /**
315
+ * A ring, arc or loop stroke that circles a hub (`around`) takes the hub's
316
+ * centre; the hub stays, it anchors the ring to the rest of the slide. Strokes
317
+ * sharing that arc's `loop` follow it. Returns one line per ring recentred.
318
+ */
319
+ export function applyAround(spec) {
320
+ const els = spec.elements ?? [];
321
+ const byId = new Map(els.map((e) => [e.id, e]));
322
+ const changes = [];
323
+ const centre = (e) => (e.geometry && ["arc", "sector"].includes(e.geometry.type) ? { x: e.geometry.cx, y: e.geometry.cy } : { x: e.x + e.w / 2, y: e.y + e.h / 2 });
324
+ const loopHub = new Map();
325
+ // A ring circles its hub, so it encloses the hub's centre: a stroked ring or
326
+ // circle whose box does not hold it is a satellite the reader misnamed
327
+ // (x101 cloud 4 named four quadrant sectors "around" their hub: sectors and
328
+ // arcs carry their own centre and are judged by it). It stays as drawn.
329
+ const encloses = (e, hub) => (e.geometry && ["arc", "sector"].includes(e.geometry.type))
330
+ || (hub.x + hub.w / 2 > e.x && hub.x + hub.w / 2 < e.x + e.w && hub.y + hub.h / 2 > e.y && hub.y + hub.h / 2 < e.y + e.h);
331
+ for (const e of els) {
332
+ const hub = e.around ? byId.get(e.around) : null;
333
+ if (!hub || hub === e || !roundish(e) || !(hub.w > 0 && hub.h > 0)) continue;
334
+ if (!encloses(e, hub)) { changes.push(`${e.id}: named around ${hub.id} but does not enclose it; kept as drawn`); continue; }
335
+ const c = centre(e);
336
+ const dx = hub.x + hub.w / 2 - c.x;
337
+ const dy = hub.y + hub.h / 2 - c.y;
338
+ if (Number.isInteger(e.loop)) loopHub.set(e.loop, hub);
339
+ if (!dx && !dy) continue;
340
+ translateElement(e, dx, dy);
341
+ changes.push(`${e.id}: centred on ${hub.id}, the hub it circles (${dx >= 0 ? "+" : ""}${Math.round(dx * 10) / 10}, ${dy >= 0 ? "+" : ""}${Math.round(dy * 10) / 10})`);
342
+ }
343
+ for (const e of els) {
344
+ const hub = Number.isInteger(e.loop) ? loopHub.get(e.loop) : null;
345
+ if (!hub || e.around || !roundish(e)) continue;
346
+ const c = centre(e);
347
+ const dx = hub.x + hub.w / 2 - c.x;
348
+ const dy = hub.y + hub.h / 2 - c.y;
349
+ if (!dx && !dy) continue;
350
+ translateElement(e, dx, dy);
351
+ changes.push(`${e.id}: centred on ${hub.id} with its loop`);
352
+ }
353
+ return changes;
354
+ }