slidemind 0.3.19 → 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 (186) 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/engine-slides.mjs +1 -1
  16. package/src/bridge/client/render.mjs +27 -1
  17. package/src/bridge/client/transport.mjs +499 -23
  18. package/src/bridge/client.mjs +10 -0
  19. package/src/bridge/config.mjs +67 -6
  20. package/src/bridge/engine-download.mjs +0 -10
  21. package/src/bridge/graph-auth.mjs +55 -3
  22. package/src/bridge/image-intent-envelope.mjs +239 -0
  23. package/src/bridge/package-preview.mjs +67 -0
  24. package/src/bridge/sharepoint.mjs +914 -0
  25. package/src/builders/asset-fetch.mjs +203 -0
  26. package/src/builders/block-dividers.mjs +83 -0
  27. package/src/builders/build.mjs +171 -87
  28. package/src/builders/card-height.mjs +294 -0
  29. package/src/builders/chart-hole.mjs +183 -0
  30. package/src/builders/chart-jsx.mjs +410 -0
  31. package/src/builders/chart-labels.mjs +326 -0
  32. package/src/builders/concept-geometry.mjs +370 -0
  33. package/src/builders/concept-intent.mjs +200 -0
  34. package/src/builders/concept-layout.mjs +79 -0
  35. package/src/builders/concept-lines.mjs +509 -0
  36. package/src/builders/concept-lists.mjs +270 -0
  37. package/src/builders/concept-marks.mjs +84 -0
  38. package/src/builders/concept-relationships.mjs +12 -0
  39. package/src/builders/concept-spec.mjs +645 -0
  40. package/src/builders/concept-text.mjs +140 -0
  41. package/src/builders/concept-waves.mjs +57 -0
  42. package/src/builders/copy-fit.mjs +139 -0
  43. package/src/builders/design-tokens.mjs +347 -0
  44. package/src/builders/draft-check.mjs +552 -340
  45. package/src/builders/draft-lines.mjs +369 -0
  46. package/src/builders/edit-route.mjs +29 -0
  47. package/src/builders/figure-sketch.mjs +375 -0
  48. package/src/builders/fold-sets.mjs +639 -0
  49. package/src/builders/gate-server.mjs +9 -4
  50. package/src/builders/gate-session.js +273 -12
  51. package/src/builders/gate-shell.js +1494 -0
  52. package/src/builders/image-intent-check.mjs +308 -0
  53. package/src/builders/image-use-reconcile.mjs +107 -0
  54. package/src/builders/kit-helpers.mjs +121 -0
  55. package/src/builders/layout-names.mjs +32 -0
  56. package/src/builders/legacy-reference-uses.mjs +179 -0
  57. package/src/builders/line-fit.mjs +224 -0
  58. package/src/builders/line-native.mjs +895 -0
  59. package/src/builders/line-relations.mjs +230 -0
  60. package/src/builders/lint-fix.mjs +117 -9
  61. package/src/builders/lint.mjs +25 -20
  62. package/src/builders/list-jsx.mjs +88 -0
  63. package/src/builders/placement.mjs +133 -0
  64. package/src/builders/plan-checks.mjs +135 -0
  65. package/src/builders/plan-comments.mjs +19 -0
  66. package/src/builders/plan-handlers.mjs +95 -0
  67. package/src/builders/plan-markdown-edit.mjs +3 -3
  68. package/src/builders/plan-overlay.js +1530 -1608
  69. package/src/builders/plan.mjs +689 -186
  70. package/src/builders/preset-definitions.json +1 -0
  71. package/src/builders/preset-eval.mjs +244 -0
  72. package/src/builders/preview.html +51 -120
  73. package/src/builders/red-thread-backbone.mjs +135 -36
  74. package/src/builders/repair.mjs +205 -0
  75. package/src/builders/repeat-groups.mjs +540 -0
  76. package/src/builders/resolve-skill.mjs +5 -4
  77. package/src/builders/review-overlay.js +296 -293
  78. package/src/builders/review.mjs +305 -84
  79. package/src/builders/rule-shift.mjs +96 -0
  80. package/src/builders/seat.mjs +354 -0
  81. package/src/builders/settle-driver.mjs +236 -0
  82. package/src/builders/settle.mjs +1448 -0
  83. package/src/builders/slide-prep.mjs +67 -0
  84. package/src/builders/spec-assets.mjs +236 -0
  85. package/src/builders/spec-match.mjs +56 -0
  86. package/src/builders/stack.mjs +413 -0
  87. package/src/builders/storyboard-markdown.mjs +70 -27
  88. package/src/builders/storyboard-schema.mjs +54 -114
  89. package/src/builders/text-blocks.mjs +212 -0
  90. package/src/builders/text-card-fit.mjs +552 -0
  91. package/src/builders/text-stack.mjs +276 -0
  92. package/src/builders/transcribe.mjs +1069 -0
  93. package/src/cli/assets.mjs +164 -0
  94. package/src/cli/common.mjs +173 -12
  95. package/src/cli/concept-spec-support.mjs +76 -0
  96. package/src/cli/concepts.mjs +1002 -103
  97. package/src/cli/deck.mjs +401 -65
  98. package/src/cli/diagnose.mjs +4 -2
  99. package/src/cli/engine-helpers.mjs +17 -10
  100. package/src/cli/engine-results.mjs +76 -3
  101. package/src/cli/help.mjs +232 -28
  102. package/src/cli/install.mjs +9 -0
  103. package/src/cli/maintenance.mjs +104 -0
  104. package/src/cli/read.mjs +2 -1
  105. package/src/cli/ready.mjs +6 -3
  106. package/src/cli/record.mjs +522 -0
  107. package/src/cli/remote.mjs +818 -0
  108. package/src/cli/revision.mjs +686 -0
  109. package/src/cli/search.mjs +30 -11
  110. package/src/cli/session.mjs +56 -13
  111. package/src/cli/setup.mjs +46 -2
  112. package/src/cli/status.mjs +142 -33
  113. package/src/cli/tasks.mjs +246 -0
  114. package/src/cli/transcribe.mjs +854 -0
  115. package/src/cli/workspace-ui.mjs +260 -0
  116. package/src/cli/workspace.mjs +212 -34
  117. package/src/install-channel.mjs +64 -7
  118. package/src/lib/adopt.mjs +346 -25
  119. package/src/lib/concept-spec-binding.mjs +96 -0
  120. package/src/lib/conversions.mjs +70 -6
  121. package/src/lib/convert-folded.mjs +107 -0
  122. package/src/lib/deck-paths.mjs +418 -0
  123. package/src/lib/durable-ids.mjs +47 -0
  124. package/src/lib/edit-operations.mjs +316 -0
  125. package/src/lib/edit-state.mjs +1244 -0
  126. package/src/lib/edit-transitions.mjs +120 -0
  127. package/src/lib/extensions.mjs +501 -0
  128. package/src/lib/image-assets.mjs +465 -0
  129. package/src/lib/image-derivatives.mjs +234 -0
  130. package/src/lib/image-intent-fingerprint.mjs +70 -0
  131. package/src/lib/image-intent-tags.mjs +176 -0
  132. package/src/lib/image-intent.mjs +802 -0
  133. package/src/lib/image-use-overrides.mjs +279 -0
  134. package/src/lib/inline-components.mjs +613 -0
  135. package/src/lib/lint-converted.mjs +68 -4
  136. package/src/lib/merge-json.mjs +163 -0
  137. package/src/lib/overwrite-guard.mjs +3 -3
  138. package/src/lib/presentation-context.mjs +531 -0
  139. package/src/lib/preservation-tags.mjs +157 -0
  140. package/src/lib/preservation-verdict.mjs +1342 -0
  141. package/src/lib/publish.mjs +1557 -0
  142. package/src/lib/rebase.mjs +945 -0
  143. package/src/lib/reconcile-ack.mjs +4 -6
  144. package/src/lib/reconcile.mjs +62 -24
  145. package/src/lib/record-client.mjs +175 -0
  146. package/src/lib/record-state.mjs +151 -0
  147. package/src/lib/remote.mjs +661 -0
  148. package/src/lib/revision.mjs +1210 -0
  149. package/src/lib/slide-guids.mjs +4 -6
  150. package/src/lib/slide-preservation.mjs +423 -0
  151. package/src/lib/slide-selection.mjs +9 -0
  152. package/src/lib/workspace-transaction.mjs +117 -0
  153. package/src/pptx/content-digest.mjs +334 -0
  154. package/src/pptx/deck-assemble.mjs +1278 -0
  155. package/src/pptx/fill/engine.mjs +2 -3
  156. package/src/pptx/fill/freeform.mjs +6 -3
  157. package/src/pptx/ooxml.mjs +20 -0
  158. package/src/pptx/package-evidence.mjs +383 -0
  159. package/src/pptx/tags.mjs +163 -11
  160. package/src/update-check.mjs +22 -4
  161. package/src/workspace/adapters.mjs +222 -0
  162. package/src/workspace/auth.mjs +256 -0
  163. package/src/workspace/autostart.mjs +204 -0
  164. package/src/workspace/daemon.mjs +360 -0
  165. package/src/workspace/discovery.mjs +180 -0
  166. package/src/workspace/edit.mjs +167 -0
  167. package/src/workspace/events.mjs +52 -0
  168. package/src/workspace/gate-client.mjs +290 -0
  169. package/src/workspace/handback.mjs +73 -0
  170. package/src/workspace/lifecycle.mjs +635 -0
  171. package/src/workspace/operations.mjs +200 -0
  172. package/src/workspace/pick-folder.mjs +152 -0
  173. package/src/workspace/registry.mjs +359 -0
  174. package/src/workspace/remote-links.mjs +397 -0
  175. package/src/workspace/remote-status.mjs +359 -0
  176. package/src/workspace/routes.mjs +1331 -0
  177. package/src/workspace/server.mjs +159 -0
  178. package/src/workspace/sync.mjs +1119 -0
  179. package/src/workspace/task-actions.mjs +215 -0
  180. package/src/workspace/tasks.mjs +507 -0
  181. package/src/workspace/ui/app.js +1886 -0
  182. package/src/workspace/ui/index.html +141 -0
  183. package/src/workspace/ui/plan-bridge.js +120 -0
  184. package/src/workspace/ui/sharepoint.svg +59 -0
  185. package/src/workspace/ui/styles.css +423 -0
  186. package/src/builders/visual-families.mjs +0 -46
@@ -0,0 +1,294 @@
1
+ // The height lever of text-card-fit.mjs (3): one new height for a card — for
2
+ // every member of its declared set alike, placed by the set's arrangement —
3
+ // with everything on it re-anchored, what sits right under it moved down into
4
+ // its free room, and nothing landed on what lies over it or crossed into a
5
+ // line it did not cross before. `cx` is text-card-fit's view of the slide:
6
+ // { s, byId, slot, changes, at, ink, heightOf, isText, filled, kids, subtree,
7
+ // inTree, move, block, pastOf, alone }.
8
+ import { gluedIds, linePoints, lineTrace, linesWithin, moveLine } from "./repeat-groups.mjs";
9
+ import { segmentHitsRect } from "./line-fit.mjs";
10
+ import { paintOrdered } from "./concept-layout.mjs";
11
+ import { seatFrame, seatOf, seatRect } from "./seat.mjs";
12
+
13
+ const INSET = 8; // px kept between a text and its card's edge
14
+ const GAP = 4; // px kept from a neighbour
15
+ const ESCAPE_PX = 3; // a descender past the edge is no escape (overlap-check.ts)
16
+
17
+ const span = (a0, a1, b0, b1) => a0 < b1 && b0 < a1;
18
+ const hit = (a, b, gap = 0) => span(a.x, a.x + a.w, b.x, b.x + b.w) && span(a.y - gap, a.y + a.h + gap, b.y, b.y + b.h);
19
+ const median = (v) => {
20
+ const s = [...v].sort((a, b) => a - b);
21
+ const m = s.length >> 1;
22
+ return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
23
+ };
24
+
25
+ export function cardHeight({ s, byId, slot, changes, at, ink, heightOf, isText, filled, kids, subtree, inTree, move, block, pastOf, alone }) {
26
+ const boundsOf = (card) => {
27
+ const h = card.parent ? byId.get(card.parent) : null;
28
+ return h && filled(h) ? [h.y + INSET, h.y + h.h - INSET] : [0, slot.h];
29
+ };
30
+ // What is drawn on a card: children (with their content) and its own lines.
31
+ // A child hung across an edge (a badge on the corner) follows that edge;
32
+ // everything inside moves as ONE block, so a row's disc and its text, or a
33
+ // title and its divider, never come apart. A child a stack push (`pushed`)
34
+ // just moved past the edge is content, not a badge.
35
+ const contentOf = (card, pushed) => {
36
+ const items = kids(card).map((k) => {
37
+ const y = isText(k) ? at(k).y : k.y;
38
+ const glyphs = isText(k) ? ink(k) : { top: y, bottom: y + heightOf(k) };
39
+ return { k, top: y, bottom: y + heightOf(k), inkTop: glyphs.top, inkBottom: glyphs.bottom, imgTop: k.y, imgBottom: k.y + k.h };
40
+ });
41
+ const lines = linesWithin(s, card).map((c) => {
42
+ const ys = linePoints(c).map((p) => p.y);
43
+ const [top, bottom] = [Math.min(...ys), Math.max(...ys)];
44
+ return { c, top, bottom, inkTop: top, inkBottom: bottom, imgTop: top, imgBottom: bottom };
45
+ });
46
+ const back = (it) => pushed?.get(it.k) ?? 0;
47
+ const onEdge = (it) => !isText(it.k) && (it.top - back(it) < card.y - 1 || it.bottom - back(it) > card.y + card.h + 1);
48
+ const inner = [...items.filter((it) => !onEdge(it)), ...lines];
49
+ if (!inner.length) return null;
50
+ const top = Math.min(...inner.map((it) => it.top));
51
+ const bottom = Math.max(...inner.map((it) => it.bottom));
52
+ // Where the image set it: centred, or nearer the top or the bottom.
53
+ const gt = Math.min(...inner.map((it) => it.imgTop)) - card.y;
54
+ const gb = card.y + card.h - Math.max(...inner.map((it) => it.imgBottom));
55
+ const anchor = Math.abs(gt - gb) <= card.h * 0.15 ? "center" : gt < gb ? "top" : "bottom";
56
+ // Glyphs measured as glyphs (text-stack.mjs): the least move keeps the insets from the ink.
57
+ const inkTop = Math.min(...inner.map((it) => it.inkTop));
58
+ const inkBottom = Math.max(...inner.map((it) => it.inkBottom));
59
+ return { inner, edges: items.filter(onEdge), top, bottom, inkTop, inkBottom, gapTop: top - card.y, gapBottom: card.y + card.h - bottom, anchor };
60
+ };
61
+ // The card at `top`, H tall, with everything on it re-anchored: dy per element.
62
+ // `least`: the content moves only as far as the insets from its ink make it.
63
+ const layout = (card, top, H, mode, pushed) => {
64
+ const c = contentOf(card, pushed);
65
+ const moves = new Map([[card, top - card.y]]);
66
+ const cH = c.bottom - c.top;
67
+ const keep = (v) => Math.min(Math.max(v, INSET), H - cH - INSET);
68
+ const at0 = mode === "least" ? c.top + Math.min(Math.max(0, top + INSET - c.inkTop), top + H - INSET - c.inkBottom)
69
+ : c.anchor === "top" ? top + keep(c.gapTop) : c.anchor === "bottom" ? top + H - cH - keep(c.gapBottom) : top + (H - cH) / 2;
70
+ const dy = at0 - c.top;
71
+ const lines = [];
72
+ for (const it of c.inner) {
73
+ if (it.c) lines.push([it.c, dy]);
74
+ else {
75
+ // A child the reader seated in the card itself keeps its seat in the
76
+ // new height (seat.mjs): a middle-seated text re-centres, a top-seated
77
+ // one keeps its inset. One seated in its row moves with the block;
78
+ // the fit's seat lever re-seats it in the row on the next pass.
79
+ const atRect = (o) => (isText(o) ? { x: o.x, w: o.w, ...at(o) } : o);
80
+ const whole = () => { const f = seatFrame(s, it.k, card, atRect); return f.y === card.y && f.h === card.h; };
81
+ const seated = seatOf(it.k)?.y && it.k.seatBand && it.k.seatInset && mode !== "least" && whole()
82
+ ? seatRect(it.k, { x: card.x, y: top, w: card.w, h: H }, { w: it.k.w, h: it.bottom - it.top }, { spec: s, at: atRect })?.y
83
+ : null;
84
+ const d = seated == null ? dy : seated - it.top;
85
+ for (const o of subtree(it.k)) moves.set(o, d);
86
+ }
87
+ }
88
+ for (const it of c.edges) {
89
+ const d = it.top < card.y - 1 ? top - card.y : top + H - card.y - card.h;
90
+ for (const o of subtree(it.k)) moves.set(o, d);
91
+ }
92
+ lineMoves.set(card, lines);
93
+ return moves;
94
+ };
95
+ const lineMoves = new Map();
96
+ let paint = null; // paint order (transcribe.mjs): what is drawn over a card
97
+ // How far a card may grow down: to what lies under it in its columns (GAP
98
+ // kept; what it already overlaps does not count), its panel's or the slot's floor.
99
+ const floorOf = (card) => {
100
+ let floor = boundsOf(card)[1];
101
+ for (const o of s.elements) {
102
+ if (inTree(o, card) || inTree(card, o) || !(o.w > 0 && o.h > 0) || hit(o, card) || !span(o.x, o.x + o.w, card.x, card.x + card.w)) continue;
103
+ const y = isText(o) ? at(o).y : o.y;
104
+ if (y >= card.y + card.h - 1) floor = Math.min(floor, y - GAP);
105
+ }
106
+ return floor;
107
+ };
108
+ const need = (card, pushed) => {
109
+ const c = contentOf(card, pushed);
110
+ return c.bottom - c.top + 2 * INSET;
111
+ };
112
+ // Tops for the members under one arrangement, or null.
113
+ const topsFor = (u, H, anchor) => {
114
+ const m = u.members;
115
+ if (u.arrangement === "ring") return m.map((c) => c.y + c.h / 2 - H / 2);
116
+ if (u.arrangement === "column" || u.arrangement === "grid") {
117
+ // Rows (a column is one member per row) keep their gaps and their centre.
118
+ const order = [...m].sort((a, b) => a.y - b.y);
119
+ const rows = [];
120
+ for (const c of order) {
121
+ const last = rows.at(-1);
122
+ if (last && c.y + c.h / 2 - (last[0].y + last[0].h / 2) <= last[0].h / 2) last.push(c);
123
+ else rows.push([c]);
124
+ }
125
+ if (rows.length === 1) return m.map((c) => c.y);
126
+ const gaps = rows.slice(1).map((r, k) => r[0].y - (rows[k][0].y + rows[k][0].h));
127
+ const gap = Math.max(GAP, median(gaps));
128
+ const mid = (rows[0][0].y + rows.at(-1)[0].y + rows.at(-1)[0].h) / 2;
129
+ const top0 = mid - (rows.length * H + (rows.length - 1) * gap) / 2;
130
+ const topOf = new Map(rows.flatMap((r, k) => r.map((c) => [c, top0 + k * (H + gap)])));
131
+ return m.map((c) => topOf.get(c));
132
+ }
133
+ return m.map((c) => (anchor === "top" || anchor === "least" ? c.y : anchor === "bottom" ? c.y + c.h - H : c.y + c.h / 2 - H / 2));
134
+ };
135
+ // `hubFixed`: a ring keeps its hub (it anchors the ring to the rest of the
136
+ // slide) and closes in, both sides of the hub by the same amount; otherwise
137
+ // an out-of-bounds set moves as one — a ring with its hub. `pushed`: the px
138
+ // a stack was just moved down by (1a), so a landing counts from before it.
139
+ const tryPlace = (u, H, anchor, hubFixed, pushed) => {
140
+ const tops = topsFor(u, H, anchor);
141
+ const plans = u.members.map((c, i) => ({ card: c, moves: layout(c, tops[i], H, anchor, pushed), rect: { x: c.x, y: tops[i], w: c.w, h: H } }));
142
+ const ring = u.arrangement === "ring" && u.hub;
143
+ const hubTree = ring && !hubFixed ? subtree(u.hub) : [];
144
+ const [ceil, floor] = boundsOf(u.members[0]);
145
+ const range = () => {
146
+ let lo = Infinity;
147
+ let hi = -Infinity;
148
+ for (const p of plans) for (const [o, dy] of p.moves) {
149
+ const y = (isText(o) ? at(o).y : o.y) + dy;
150
+ lo = Math.min(lo, y);
151
+ hi = Math.max(hi, y + (o === p.card ? H : heightOf(o)));
152
+ }
153
+ for (const o of hubTree) { lo = Math.min(lo, o.y); hi = Math.max(hi, o.y + heightOf(o)); }
154
+ return [lo, hi];
155
+ };
156
+ const closeIn = (inward) => {
157
+ const cy = u.hub.y + u.hub.h / 2;
158
+ for (const p of plans) {
159
+ const mc = p.rect.y + H / 2;
160
+ const dy = mc < cy - 1 ? inward : mc > cy + 1 ? -inward : 0;
161
+ for (const [o, v] of p.moves) p.moves.set(o, v + dy);
162
+ lineMoves.set(p.card, (lineMoves.get(p.card) ?? []).map(([c, v]) => [c, v + dy]));
163
+ p.rect.y += dy;
164
+ }
165
+ };
166
+ let [lo, hi] = range();
167
+ if (ring && hubFixed) {
168
+ const inward = Math.max(0, ceil - lo, hi - floor);
169
+ if (inward > 0) closeIn(inward);
170
+ [lo, hi] = range();
171
+ if (lo < ceil - 0.5 || hi > floor + 0.5) return null;
172
+ } else if (ring && hi - lo > floor - ceil) {
173
+ closeIn((hi - lo - (floor - ceil)) / 2);
174
+ [lo, hi] = range();
175
+ }
176
+ if (hi - lo > floor - ceil + 0.5) return null;
177
+ const all = lo < ceil ? ceil - lo : hi > floor ? floor - hi : 0;
178
+ const moved = new Set([...plans.flatMap((p) => [...p.moves.keys()]), ...hubTree]);
179
+ const rects = [...plans.map((p) => ({ ...p.rect, y: p.rect.y + all })), ...(all ? hubTree.map((o) => ({ x: o.x, y: o.y + all, w: o.w, h: o.h })) : [])];
180
+ const old = [...u.members, ...hubTree];
181
+ const hubAt = ring ? { x: u.hub.x, y: u.hub.y + (hubFixed ? 0 : all), w: u.hub.w, h: u.hub.h } : null;
182
+ const apart = rects.every((r, i) => rects.every((q, j) => j === i || j >= plans.length || i >= plans.length || !hit(q, r)))
183
+ && (!hubAt || plans.every((p) => !hit({ ...p.rect, y: p.rect.y + all }, hubAt, GAP)));
184
+ const exempt = (o) => moved.has(o) || o === u.hub || !(o.w > 0 && o.h > 0) || old.some((c) => hit(o, c));
185
+ const blocking = s.elements.filter((o) => !exempt(o) && rects.some((r) => hit(o, r, GAP)));
186
+ const room = blocking.length ? makeRoom(blocking, rects, old, moved, floor) : { push: () => {} };
187
+ // What lies over a card without being its content (a step badge on its
188
+ // edge) stays where it is: the content re-set inside the card never lands
189
+ // on it (pm-03: the launch panel's title lifted onto badge "2").
190
+ const over = s.elements.filter((o) => !moved.has(o) && o.w > 0 && o.h > 0 && old.some((c) => hit(o, c) && !inTree(c, o)));
191
+ const rectOf = (o) => ({ x: o.x, y: isText(o) ? at(o).y : o.y, w: o.w, h: heightOf(o) });
192
+ // Lines cross glyphs, not a text box's empty margin.
193
+ const drawn = (o) => (isText(o) ? { x: o.x, y: ink(o).top, w: o.w, h: ink(o).bottom - ink(o).top } : rectOf(o));
194
+ const landsOn = plans.some((p) => [...p.moves].some(([o, dy]) => {
195
+ if (o === p.card) return false;
196
+ const back = pushed?.get(o) ?? 0;
197
+ const [r, g] = [rectOf(o), drawn(o)];
198
+ return over.some((f) => !hit(f, { ...r, y: r.y - back }) && hit(f, { ...r, y: r.y + dy + all }))
199
+ || crossesLine({ ...g, y: g.y + dy + all }, [{ ...g, y: g.y - back }], [p.card]);
200
+ }));
201
+ // Nor does the card grow under more of what is drawn over it (a caption
202
+ // across its edge); a backdrop it lies on is drawn under it.
203
+ paint ??= new Map(paintOrdered(s.elements).map((e, i) => [e, i]));
204
+ const covered = (f, r) => (span(f.x, f.x + f.w, r.x, r.x + r.w) ? Math.max(0, Math.min(f.y + f.h, r.y + r.h) - Math.max(f.y, r.y)) : 0);
205
+ const growsOver = plans.some((p) => over.some((f) => f !== u.hub && paint.get(f) > paint.get(p.card)
206
+ && covered(rectOf(f), { ...p.rect, y: p.rect.y + all }) > covered(rectOf(f), p.card) + 0.5));
207
+ if (!apart || !room || landsOn || growsOver || plans.some((p) => crossesLine({ ...p.rect, y: p.rect.y + all }, old, u.members))) return null;
208
+ return () => {
209
+ room.push();
210
+ for (const p of plans) {
211
+ const from = p.card.h;
212
+ for (const [o, dy] of p.moves) if (dy + all) move(o, Math.round((dy + all) * 10) / 10);
213
+ for (const [c, dy] of lineMoves.get(p.card) ?? []) if (dy + all) moveLine(c, 0, Math.round((dy + all) * 10) / 10);
214
+ p.card.h = H;
215
+ changes.push(from === H ? `${p.card.id}: content re-set inside its ${H}px` : `${p.card.id}: height ${Math.round(from)}→${H}px${u.id ? ` (${u.id}: one height for ${u.members.map((c) => c.id).join(", ")})` : ""}`);
216
+ }
217
+ if (all) for (const o of hubTree) move(o, all);
218
+ };
219
+ };
220
+ // A slide line (a loop around panels, a connector) a new rect would cross
221
+ // and the old one did not: growing or pushing a box into it trades a text
222
+ // spill for a line through a card (pm-03 after-panel into its loop). A
223
+ // card's own lines move with it and never count.
224
+ const crossesLine = (rect, was, owners, moving = []) => {
225
+ const own = new Set([...owners.flatMap((m) => linesWithin(s, m)), ...moving]);
226
+ // A connector glued to the card or its content follows it (the arrow
227
+ // leaving the edge that grows): it starts ON the old edge, so the strict
228
+ // hit test never counted it as crossed before.
229
+ const tree = new Set(owners.flatMap((m) => subtree(m).map((o) => o.id)));
230
+ return (s.connectors ?? []).some((c) => {
231
+ const t = own.has(c) || gluedIds(c).some((id) => tree.has(id)) ? null : lineTrace(c);
232
+ const hits = (box) => t.some((p, i) => i > 0 && segmentHitsRect(t[i - 1], p, box));
233
+ return Boolean(t) && hits(rect) && !was.some((o) => hits(o));
234
+ });
235
+ };
236
+ // What sits right under a growing card moves down with it, all of it by one
237
+ // step, into the free room it has below — never out of the slot or its
238
+ // panel, never onto anything else (qbr-04: the banner under the panel had
239
+ // 10px to spare, the panel needed 4 of them).
240
+ const makeRoom = (blocking, rects, old, moved, floor) => {
241
+ const oldBottom = Math.max(...old.map((c) => c.y + c.h));
242
+ const rootOf = (o) => {
243
+ let r = o;
244
+ for (let p = r.parent ? byId.get(r.parent) : null; p && !old.some((c) => inTree(c, p)); p = p.parent ? byId.get(p.parent) : null) r = p;
245
+ return r;
246
+ };
247
+ const roots = [...new Set(blocking.map(rootOf))];
248
+ if (roots.some((r) => r.y < oldBottom - 1 || moved.has(r))) return null;
249
+ const extent = (r) => ({ x: r.x, y: r.y, w: r.w, h: Math.max(...subtree(r).map((o) => (isText(o) ? at(o).y : o.y) + heightOf(o))) - r.y });
250
+ const d = Math.ceil(Math.max(...roots.flatMap((r) => rects.filter((q) => span(q.x, q.x + q.w, r.x, r.x + r.w)).map((q) => q.y + q.h + GAP - r.y))));
251
+ if (!(d > 0) || roots.some((r) => { const e = extent(r); return e.y + e.h + d > floor + 0.5; })) return null;
252
+ const pushed = new Set(roots.flatMap(subtree));
253
+ const free = s.elements.every((o) => pushed.has(o) || moved.has(o) || old.includes(o) || !(o.w > 0 && o.h > 0)
254
+ || roots.every((r) => { const e = extent(r); return hit(o, e) || !hit(o, { ...e, y: e.y + d }, GAP); }));
255
+ if (!free || roots.some((r) => crossesLine({ ...extent(r), y: extent(r).y + d }, [extent(r)], [r]))) return null;
256
+ const lines = roots.flatMap((r) => linesWithin(s, r));
257
+ return {
258
+ push: () => {
259
+ for (const o of pushed) move(o, d);
260
+ for (const c of lines) moveLine(c, 0, d);
261
+ changes.push(`${roots.map((r) => r.id).join(", ")}: +${d}px down to make room`);
262
+ },
263
+ };
264
+ };
265
+ // `pushed` (element → px): a stack moved down on the card (1a) asks for the
266
+ // height its content now needs inside the insets, before any text is past
267
+ // the edge.
268
+ const heightUnit = (u, pushed = null) => {
269
+ const over = pushed ? (c) => contentOf(c, pushed).bottom > c.y + c.h - INSET + 0.5 : (c) => pastOf(c) > ESCAPE_PX;
270
+ if (alone(u) || u.members.some((c) => !block(c)) || !u.members.some(over)) return false;
271
+ const H = Math.ceil(Math.max(...u.members.map((c) => c.h), ...u.members.map((c) => need(c, pushed))));
272
+ const anchors = u.arrangement === "row" ? ["top", "bottom", "center"] : u.arrangement ? ["center"] : ["top", "bottom", "center"];
273
+ const tries = u.arrangement === "ring" ? [["center", true], ["center", false]] : anchors.map((a) => [a, false]);
274
+ // A lone card whose content, re-anchored, would land on what lies over it
275
+ // (pm-03: the after panel's bullets onto the step badge on its edge): the
276
+ // content moves only as far as the insets make it — in the card's height,
277
+ // else in the height its free room below gives it, up to the one that
278
+ // holds its content where it is. A set keeps one inner layout.
279
+ if (u.members.length === 1) {
280
+ const c = u.members[0];
281
+ const grow = Math.min(Math.ceil(contentOf(c, pushed).inkBottom + INSET - c.y), Math.floor(floorOf(c) - c.y));
282
+ tries.push(["least", false, H], ...(grow > H ? [["least", false, grow]] : []));
283
+ }
284
+ for (const [anchor, hubFixed, h = H] of tries) {
285
+ const commit = tryPlace(u, h, anchor, hubFixed, pushed);
286
+ if (commit) {
287
+ commit();
288
+ return true;
289
+ }
290
+ }
291
+ return false;
292
+ };
293
+ return { heightUnit, crossesLine };
294
+ }
@@ -0,0 +1,183 @@
1
+ // What a doughnut paints inside its hole — the figure the slide is about, the
2
+ // caption under it, an icon — stands ON the chart, centred in its ring. It is
3
+ // none of the chart's labelling (chart-labels.mjs): a doughnut's native data
4
+ // labels sit on its ring, and OOXML has no element for the hole. x114 GTM 1
5
+ // and cloud 4/5 lost six such figures: the reader listed each among the
6
+ // chart's own (`owns`), the fold took it as a value label, and the chart
7
+ // printed every slice's value on the ring instead ("9%" and "91%", the hole
8
+ // empty). x114 QBR 3's globe sat on the ring's edge once the native legend
9
+ // pushed the ring away from where the image painted it.
10
+ //
11
+ // The reader names what stands in the hole (`chart.hole`). Here that becomes
12
+ // the chart's own content, on declaration only: each member is a child of the
13
+ // chart, no label fold takes it, it leaves any text block that reaches outside
14
+ // the hole, and the members are centred as ONE unit on the ring the image
15
+ // painted — among themselves they keep their painted arrangement. The chart
16
+ // draws its ring there (chart-jsx.mjs pins the plot area when the frame grew
17
+ // to hold folded labelling).
18
+ import { translateElement } from "./concept-geometry.mjs";
19
+ import { fixCopies, translateFigure } from "./repeat-groups.mjs";
20
+ import { readingRows } from "./text-blocks.mjs";
21
+
22
+ const half = (v) => Math.round(v * 2) / 2;
23
+ // The hole PowerPoint draws when a doughnut names none, and the widest OOXML
24
+ // allows (`c:holeSize`, percent of the ring's diameter).
25
+ const HOLE_DEFAULT = 50;
26
+ const HOLE_MAX = 90;
27
+
28
+ /** The ids the reader named in a doughnut's hole; none for any other chart. */
29
+ export function holeIds(e) {
30
+ const c = e?.kind === "chart" ? e.chart : null;
31
+ if (c?.type !== "doughnut" || !Array.isArray(c.hole)) return [];
32
+ return [...new Set(c.hole.filter((id) => typeof id === "string" && id && id !== e.id))];
33
+ }
34
+
35
+ /** Where the image painted the ring: the chart's rect before its frame grew to hold folded labelling. */
36
+ export function ringRect(e) {
37
+ const from = e.chart?.folded?.frame?.from;
38
+ return from && [from.x, from.y, from.w, from.h].every(Number.isFinite) ? { x: from.x, y: from.y, w: from.w, h: from.h } : { x: e.x, y: e.y, w: e.w, h: e.h };
39
+ }
40
+
41
+ /** Each declared hole member's chart, by member id (the first chart that names it). */
42
+ export function holeMembers(elements) {
43
+ const ids = new Set((elements ?? []).map((e) => e.id));
44
+ const out = new Map();
45
+ for (const c of elements ?? []) for (const id of holeIds(c)) if (ids.has(id) && !out.has(id)) out.set(id, c);
46
+ return out;
47
+ }
48
+
49
+ /**
50
+ * Make every declared hole member its chart's own content: the chart is its
51
+ * parent, it leaves the chart's `owns` (no label fold takes it) and its own
52
+ * seat (the hole centres it), and a text block that mixes it with texts outside
53
+ * that hole lets it go — the members of one hole that shared the block keep
54
+ * one block of their own. Mutates `spec`; idempotent; returns notes.
55
+ */
56
+ export function claimHoles(spec) {
57
+ const els = spec.elements ?? [];
58
+ const holeOf = holeMembers(els);
59
+ if (!holeOf.size) return [];
60
+ const byId = new Map(els.map((e) => [e.id, e]));
61
+ const notes = [];
62
+ for (const [id, c] of holeOf) {
63
+ const e = byId.get(id);
64
+ // A member inside another member (an icon on its disc) rides with it.
65
+ if (!holeOf.has(e.parent)) e.parent = c.id;
66
+ if (e.seat) e.seat = null;
67
+ if (Array.isArray(c.chart.owns) && c.chart.owns.includes(id)) {
68
+ c.chart.owns = c.chart.owns.filter((x) => x !== id);
69
+ notes.push(`chart ${c.id}: ${id} stands in the doughnut's hole — drawn on the chart, centred in its ring, not folded into its labels`);
70
+ }
71
+ }
72
+ const blocks = new Map();
73
+ for (const e of els) if (e.block) blocks.set(e.block, [...(blocks.get(e.block) ?? []), e]);
74
+ for (const [block, members] of blocks) {
75
+ const owners = new Set(members.map((m) => holeOf.get(m.id)?.id ?? ""));
76
+ if (owners.size < 2) continue;
77
+ for (const owner of owners) {
78
+ if (!owner) continue;
79
+ const inHole = members.filter((m) => holeOf.get(m.id)?.id === owner);
80
+ for (const m of inHole) {
81
+ if (inHole.length > 1) m.block = `${block}@${owner}`;
82
+ else delete m.block;
83
+ }
84
+ notes.push(`block ${block}: ${inHole.map((m) => m.id).join(", ")} stand${inHole.length === 1 ? "s" : ""} in ${owner}'s hole — written there, apart from the block's texts outside it`);
85
+ }
86
+ // What the block read before the hole and what it read after it are two
87
+ // texts, on either side of the ring (x114 GTM 1: "Below 300 employees" over
88
+ // the ring, "convert" under it — one text would pull "convert" up under the
89
+ // header and onto the ring).
90
+ const runs = [[]];
91
+ for (const m of readingRows(members).flat()) {
92
+ if (holeOf.has(m.id)) { if (runs.at(-1).length) runs.push([]); }
93
+ else runs.at(-1).push(m);
94
+ }
95
+ const parts = runs.filter((r) => r.length);
96
+ if (parts.length < 2) continue;
97
+ parts.forEach((run, i) => {
98
+ for (const m of run) {
99
+ if (run.length > 1) m.block = i ? `${block}~${i + 1}` : block;
100
+ else delete m.block;
101
+ }
102
+ });
103
+ notes.push(`block ${block}: the hole parts it — ${parts.map((r) => r.map((m) => m.id).join(" + ")).join(" | ")} written as ${parts.length} texts`);
104
+ }
105
+ return notes;
106
+ }
107
+
108
+ // Members of one hole that shared a block are written as ONE text
109
+ // (text-blocks.mjs, which keeps each member's id on its paragraph): the hole
110
+ // names that text from then on.
111
+ function followFolds(els) {
112
+ const ids = new Set(els.map((e) => e.id));
113
+ const textOf = new Map();
114
+ for (const e of els) for (const p of e.paragraphs ?? []) if (p?.id && !textOf.has(p.id)) textOf.set(p.id, e.id);
115
+ for (const c of els) {
116
+ if (!holeIds(c).length) continue;
117
+ const hole = [...new Set(c.chart.hole.map((id) => (ids.has(id) ? id : textOf.get(id) ?? id)))];
118
+ if (hole.join("\n") !== c.chart.hole.join("\n")) c.chart.hole = hole;
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Centre each doughnut's hole members, as one unit, on the ring the image
124
+ * painted (`ringRect`); a member's own children move with it. Returns one line
125
+ * per hole moved.
126
+ */
127
+ export function centreHoles(spec) {
128
+ const els = spec.elements ?? [];
129
+ followFolds(els);
130
+ const holeOf = holeMembers(els);
131
+ if (!holeOf.size) return [];
132
+ const byChart = new Map();
133
+ for (const [id, c] of holeOf) byChart.set(c, [...(byChart.get(c) ?? []), id]);
134
+ const byId = new Map(els.map((e) => [e.id, e]));
135
+ const figures = new Set((spec.sketch?.figures ?? []).map((f) => f.id));
136
+ const deltas = new Map();
137
+ const movedCircles = new Set();
138
+ const kids = (id) => els.filter((o) => o.parent === id);
139
+ const move = (e, dx, dy) => {
140
+ translateElement(e, dx, dy);
141
+ if (figures.has(e.id)) translateFigure(spec.sketch, e.id, dx, dy, deltas, movedCircles);
142
+ for (const k of kids(e.id)) move(k, dx, dy);
143
+ };
144
+ const changes = [];
145
+ for (const [c, ids] of byChart) {
146
+ // The unit: the members the chart carries directly (a member on another
147
+ // member moves with its carrier).
148
+ const unit = ids.map((id) => byId.get(id)).filter((e) => e.parent === c.id && [e.x, e.y, e.w, e.h].every(Number.isFinite));
149
+ if (!unit.length) continue;
150
+ const ring = ringRect(c);
151
+ const x0 = Math.min(...unit.map((e) => e.x));
152
+ const y0 = Math.min(...unit.map((e) => e.y));
153
+ const x1 = Math.max(...unit.map((e) => e.x + e.w));
154
+ const y1 = Math.max(...unit.map((e) => e.y + e.h));
155
+ const dx = half(ring.x + ring.w / 2 - (x0 + x1) / 2);
156
+ const dy = half(ring.y + ring.h / 2 - (y0 + y1) / 2);
157
+ if (!dx && !dy) continue;
158
+ for (const e of unit) move(e, dx, dy);
159
+ changes.push(`${c.id}: its hole (${unit.map((e) => e.id).join(", ")}) centred in the ring (${dx >= 0 ? "+" : ""}${dx}, ${dy >= 0 ? "+" : ""}${dy})`);
160
+ }
161
+ fixCopies(spec.sketch, deltas);
162
+ return changes;
163
+ }
164
+
165
+ /**
166
+ * The hole that holds what stands in it: the circle around the members' box,
167
+ * as a percent of the ring's diameter (`c:holeSize`) — never below the hole
168
+ * PowerPoint draws by default, at most the widest OOXML allows. A ring gauge's
169
+ * "27%" in a default hole ran onto its ring (x114 GTM 1). null for a doughnut
170
+ * with nothing in its hole; `fits` false where even the widest hole is too
171
+ * small.
172
+ */
173
+ export function holeSizeFor(spec, chart) {
174
+ const ids = new Set(holeIds(chart));
175
+ const unit = (spec.elements ?? []).filter((e) => ids.has(e.id) && e.parent === chart.id && [e.x, e.y, e.w, e.h].every(Number.isFinite));
176
+ const ring = ringRect(chart);
177
+ const d = Math.min(ring.w, ring.h);
178
+ if (!unit.length || !(d > 0)) return null;
179
+ const w = Math.max(...unit.map((e) => e.x + e.w)) - Math.min(...unit.map((e) => e.x));
180
+ const h = Math.max(...unit.map((e) => e.y + e.h)) - Math.min(...unit.map((e) => e.y));
181
+ const need = Math.ceil((100 * Math.hypot(w, h)) / d);
182
+ return { size: Math.min(HOLE_MAX, Math.max(HOLE_DEFAULT, need)), fits: need <= HOLE_MAX };
183
+ }