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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/AGENTS.md +591 -15
- package/README.md +202 -7
- package/bin/slidemind.mjs +50 -2
- package/package.json +2 -2
- package/skills/create-slides/SKILL.md +484 -886
- package/skills/search/SKILL.md +46 -23
- package/src/bridge/build-and-push.mjs +548 -73
- package/src/bridge/bundle.mjs +100 -31
- package/src/bridge/capture.mjs +14 -17
- package/src/bridge/client/brands.mjs +157 -18
- package/src/bridge/client/engine-routing.mjs +48 -13
- package/src/bridge/client/engine-search.mjs +24 -1
- package/src/bridge/client/render.mjs +27 -1
- package/src/bridge/client/transport.mjs +499 -23
- package/src/bridge/client.mjs +10 -0
- package/src/bridge/config.mjs +67 -6
- package/src/bridge/engine-download.mjs +0 -10
- package/src/bridge/graph-auth.mjs +55 -3
- package/src/bridge/image-intent-envelope.mjs +239 -0
- package/src/bridge/package-preview.mjs +67 -0
- package/src/bridge/sharepoint.mjs +914 -0
- package/src/builders/asset-fetch.mjs +203 -0
- package/src/builders/block-dividers.mjs +83 -0
- package/src/builders/build.mjs +171 -87
- package/src/builders/card-height.mjs +294 -0
- package/src/builders/chart-hole.mjs +183 -0
- package/src/builders/chart-jsx.mjs +410 -0
- package/src/builders/chart-labels.mjs +326 -0
- package/src/builders/concept-geometry.mjs +370 -0
- package/src/builders/concept-intent.mjs +200 -0
- package/src/builders/concept-layout.mjs +79 -0
- package/src/builders/concept-lines.mjs +509 -0
- package/src/builders/concept-lists.mjs +270 -0
- package/src/builders/concept-marks.mjs +84 -0
- package/src/builders/concept-relationships.mjs +12 -0
- package/src/builders/concept-spec.mjs +645 -0
- package/src/builders/concept-text.mjs +140 -0
- package/src/builders/concept-waves.mjs +57 -0
- package/src/builders/copy-fit.mjs +139 -0
- package/src/builders/design-tokens.mjs +347 -0
- package/src/builders/draft-check.mjs +552 -340
- package/src/builders/draft-lines.mjs +369 -0
- package/src/builders/edit-route.mjs +29 -0
- package/src/builders/figure-sketch.mjs +375 -0
- package/src/builders/fold-sets.mjs +639 -0
- package/src/builders/gate-server.mjs +9 -4
- package/src/builders/gate-session.js +273 -12
- package/src/builders/gate-shell.js +1494 -0
- package/src/builders/image-intent-check.mjs +308 -0
- package/src/builders/image-use-reconcile.mjs +107 -0
- package/src/builders/kit-helpers.mjs +121 -0
- package/src/builders/layout-names.mjs +32 -0
- package/src/builders/legacy-reference-uses.mjs +179 -0
- package/src/builders/line-fit.mjs +224 -0
- package/src/builders/line-native.mjs +895 -0
- package/src/builders/line-relations.mjs +230 -0
- package/src/builders/lint-fix.mjs +117 -9
- package/src/builders/lint.mjs +25 -20
- package/src/builders/list-jsx.mjs +88 -0
- package/src/builders/placement.mjs +133 -0
- package/src/builders/plan-checks.mjs +135 -0
- package/src/builders/plan-comments.mjs +19 -0
- package/src/builders/plan-handlers.mjs +95 -0
- package/src/builders/plan-markdown-edit.mjs +3 -3
- package/src/builders/plan-overlay.js +1530 -1608
- package/src/builders/plan.mjs +689 -186
- package/src/builders/preset-definitions.json +1 -0
- package/src/builders/preset-eval.mjs +244 -0
- package/src/builders/preview.html +51 -120
- package/src/builders/red-thread-backbone.mjs +135 -36
- package/src/builders/repair.mjs +205 -0
- package/src/builders/repeat-groups.mjs +540 -0
- package/src/builders/resolve-skill.mjs +5 -4
- package/src/builders/review-overlay.js +296 -293
- package/src/builders/review.mjs +305 -84
- package/src/builders/rule-shift.mjs +96 -0
- package/src/builders/seat.mjs +354 -0
- package/src/builders/settle-driver.mjs +236 -0
- package/src/builders/settle.mjs +1448 -0
- package/src/builders/slide-prep.mjs +67 -0
- package/src/builders/spec-assets.mjs +236 -0
- package/src/builders/spec-match.mjs +56 -0
- package/src/builders/stack.mjs +413 -0
- package/src/builders/storyboard-markdown.mjs +70 -27
- package/src/builders/storyboard-schema.mjs +54 -114
- package/src/builders/text-blocks.mjs +212 -0
- package/src/builders/text-card-fit.mjs +552 -0
- package/src/builders/text-stack.mjs +276 -0
- package/src/builders/transcribe.mjs +1069 -0
- package/src/cli/assets.mjs +164 -0
- package/src/cli/common.mjs +173 -12
- package/src/cli/concept-spec-support.mjs +76 -0
- package/src/cli/concepts.mjs +1002 -103
- package/src/cli/deck.mjs +401 -65
- package/src/cli/diagnose.mjs +4 -2
- package/src/cli/engine-helpers.mjs +17 -10
- package/src/cli/engine-results.mjs +76 -3
- package/src/cli/help.mjs +232 -28
- package/src/cli/install.mjs +9 -0
- package/src/cli/maintenance.mjs +104 -0
- package/src/cli/read.mjs +2 -1
- package/src/cli/ready.mjs +6 -3
- package/src/cli/record.mjs +522 -0
- package/src/cli/remote.mjs +818 -0
- package/src/cli/revision.mjs +686 -0
- package/src/cli/search.mjs +30 -11
- package/src/cli/session.mjs +56 -13
- package/src/cli/setup.mjs +46 -2
- package/src/cli/status.mjs +142 -33
- package/src/cli/tasks.mjs +246 -0
- package/src/cli/transcribe.mjs +854 -0
- package/src/cli/workspace-ui.mjs +260 -0
- package/src/cli/workspace.mjs +212 -34
- package/src/install-channel.mjs +64 -7
- package/src/lib/adopt.mjs +346 -25
- package/src/lib/concept-spec-binding.mjs +96 -0
- package/src/lib/conversions.mjs +70 -6
- package/src/lib/convert-folded.mjs +107 -0
- package/src/lib/deck-paths.mjs +418 -0
- package/src/lib/durable-ids.mjs +47 -0
- package/src/lib/edit-operations.mjs +316 -0
- package/src/lib/edit-state.mjs +1244 -0
- package/src/lib/edit-transitions.mjs +120 -0
- package/src/lib/extensions.mjs +501 -0
- package/src/lib/image-assets.mjs +465 -0
- package/src/lib/image-derivatives.mjs +234 -0
- package/src/lib/image-intent-fingerprint.mjs +70 -0
- package/src/lib/image-intent-tags.mjs +176 -0
- package/src/lib/image-intent.mjs +802 -0
- package/src/lib/image-use-overrides.mjs +279 -0
- package/src/lib/inline-components.mjs +613 -0
- package/src/lib/lint-converted.mjs +68 -4
- package/src/lib/merge-json.mjs +163 -0
- package/src/lib/overwrite-guard.mjs +3 -3
- package/src/lib/presentation-context.mjs +531 -0
- package/src/lib/preservation-tags.mjs +157 -0
- package/src/lib/preservation-verdict.mjs +1342 -0
- package/src/lib/publish.mjs +1557 -0
- package/src/lib/rebase.mjs +945 -0
- package/src/lib/reconcile-ack.mjs +4 -6
- package/src/lib/reconcile.mjs +62 -24
- package/src/lib/record-client.mjs +175 -0
- package/src/lib/record-state.mjs +151 -0
- package/src/lib/remote.mjs +661 -0
- package/src/lib/revision.mjs +1210 -0
- package/src/lib/slide-guids.mjs +4 -6
- package/src/lib/slide-preservation.mjs +423 -0
- package/src/lib/slide-selection.mjs +9 -0
- package/src/lib/workspace-transaction.mjs +117 -0
- package/src/pptx/content-digest.mjs +334 -0
- package/src/pptx/deck-assemble.mjs +1278 -0
- package/src/pptx/fill/engine.mjs +2 -3
- package/src/pptx/fill/freeform.mjs +6 -3
- package/src/pptx/ooxml.mjs +20 -0
- package/src/pptx/package-evidence.mjs +383 -0
- package/src/pptx/tags.mjs +163 -11
- package/src/update-check.mjs +22 -4
- package/src/workspace/adapters.mjs +222 -0
- package/src/workspace/auth.mjs +256 -0
- package/src/workspace/autostart.mjs +204 -0
- package/src/workspace/daemon.mjs +360 -0
- package/src/workspace/discovery.mjs +180 -0
- package/src/workspace/edit.mjs +167 -0
- package/src/workspace/events.mjs +52 -0
- package/src/workspace/gate-client.mjs +290 -0
- package/src/workspace/handback.mjs +73 -0
- package/src/workspace/lifecycle.mjs +635 -0
- package/src/workspace/operations.mjs +200 -0
- package/src/workspace/pick-folder.mjs +152 -0
- package/src/workspace/registry.mjs +359 -0
- package/src/workspace/remote-links.mjs +397 -0
- package/src/workspace/remote-status.mjs +359 -0
- package/src/workspace/routes.mjs +1331 -0
- package/src/workspace/server.mjs +159 -0
- package/src/workspace/sync.mjs +1119 -0
- package/src/workspace/task-actions.mjs +215 -0
- package/src/workspace/tasks.mjs +507 -0
- package/src/workspace/ui/app.js +1886 -0
- package/src/workspace/ui/index.html +141 -0
- package/src/workspace/ui/plan-bridge.js +120 -0
- package/src/workspace/ui/sharepoint.svg +59 -0
- package/src/workspace/ui/styles.css +423 -0
- 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
|
+
}
|