slidemind 0.3.20 → 0.4.1
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 +592 -16
- package/LICENSE +4 -0
- package/README.md +203 -8
- package/THIRD-PARTY-NOTICES.md +201 -0
- package/bin/slidemind.mjs +50 -2
- package/package.json +5 -3
- 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,540 @@
|
|
|
1
|
+
// Repeated organisms, built from one template. The concept image is a sketch:
|
|
2
|
+
// four step cards come out 105, 115, 113 and 115 px tall, their discs and
|
|
3
|
+
// texts a few px apart, the ring 181 px above its hub and 197 px below it.
|
|
4
|
+
// Near-equal geometry is never snapped on a guess (bb782591): the READER
|
|
5
|
+
// declares which containers are one organism and how they are arranged
|
|
6
|
+
// (`spec.repeats`, concept-repeats.ts), and this module builds what it means —
|
|
7
|
+
// - one size for every member (the members' median),
|
|
8
|
+
// - one inner layout: corresponding children at the median offset and size
|
|
9
|
+
// (a disc, its icon, the number badge on the corner, the text block),
|
|
10
|
+
// - exact placement: a row or column on one axis with equal gaps, a grid on
|
|
11
|
+
// equal pitches, a ring on one ellipse around its hub at equal angles —
|
|
12
|
+
// mirror-symmetric about the hub's axes.
|
|
13
|
+
// Pure spec in, spec out; a set that cannot be built stays as drawn, noted.
|
|
14
|
+
import { translateElement } from "./concept-geometry.mjs";
|
|
15
|
+
|
|
16
|
+
const GAP = 4; // px between neighbours when a row or column has to open up
|
|
17
|
+
const ANGLE_SNAP = 15; // degrees: a ring starts on the nearest 15° (top, right…)
|
|
18
|
+
|
|
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
|
+
const centre = (e) => ({ x: e.x + e.w / 2, y: e.y + e.h / 2 });
|
|
25
|
+
const half = (v) => Math.round(v * 2) / 2;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The declared sets whose members are on this slide: `[{ id, members,
|
|
29
|
+
* arrangement, hub, built }]` with elements. A member is a container (a card),
|
|
30
|
+
* or — a row the design draws without one: its disc, its label, its text — the
|
|
31
|
+
* loose elements that make it, as a virtual member `{ id, ids, virtual }` over
|
|
32
|
+
* their box. `built`: buildRepeats made this set one template.
|
|
33
|
+
*/
|
|
34
|
+
export function repeatGroups(spec) {
|
|
35
|
+
const byId = new Map((spec.elements ?? []).map((e) => [e.id, e]));
|
|
36
|
+
// A part may be a rule that became a line (remapRepeats): it spans the box,
|
|
37
|
+
// so the member's own lines (linesWithin) move with it.
|
|
38
|
+
const lineById = new Map((spec.connectors ?? []).filter((c) => c.id).map((c) => [c.id, c]));
|
|
39
|
+
const out = [];
|
|
40
|
+
for (const g of spec.repeats ?? []) {
|
|
41
|
+
const members = (g.members ?? []).map((m, i) => {
|
|
42
|
+
if (!Array.isArray(m) || m.length === 1) return byId.get(Array.isArray(m) ? m[0] : m);
|
|
43
|
+
const parts = m.filter((id) => byId.has(id)).map((id) => byId.get(id));
|
|
44
|
+
const lines = m.filter((id) => !byId.has(id)).map((id) => lineById.get(id));
|
|
45
|
+
if (!parts.length || lines.some((c) => !c)) return null;
|
|
46
|
+
const pts = [...parts.flatMap((e) => [{ x: e.x, y: e.y }, { x: e.x + e.w, y: e.y + e.h }]), ...lines.flatMap(linePoints)];
|
|
47
|
+
const x = Math.min(...pts.map((p) => p.x));
|
|
48
|
+
const y = Math.min(...pts.map((p) => p.y));
|
|
49
|
+
return { id: `${g.id}-${i + 1}`, ids: parts.map((e) => e.id), virtual: true, kind: "group", parent: null,
|
|
50
|
+
x, y, w: Math.max(...pts.map((p) => p.x)) - x, h: Math.max(...pts.map((p) => p.y)) - y };
|
|
51
|
+
}).filter(Boolean);
|
|
52
|
+
if (members.length < 2) continue;
|
|
53
|
+
out.push({ id: g.id, members, arrangement: g.arrangement, hub: g.hub ? byId.get(g.hub) ?? null : null, built: g.built === true });
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The reader declares its sets on the ids it read; normalization then folds
|
|
60
|
+
* some of them (a title and its body into one block, a label into its shape)
|
|
61
|
+
* and turns rules into lines. A member names what survived: an absorbed id
|
|
62
|
+
* the element that took it in, a rule its line; an id that is gone leaves its
|
|
63
|
+
* member. A set that lost a member, or whose members no longer hold like
|
|
64
|
+
* parts, is dropped: kept as drawn, never built from parts that differ.
|
|
65
|
+
* `before`: the elements by id before normalization. Returns the notes.
|
|
66
|
+
*/
|
|
67
|
+
export function remapRepeats(spec, before) {
|
|
68
|
+
if (!spec.repeats?.length) return [];
|
|
69
|
+
const byId = new Map((spec.elements ?? []).map((e) => [e.id, e]));
|
|
70
|
+
const lines = new Set((spec.connectors ?? []).map((c) => c.id).filter(Boolean));
|
|
71
|
+
const into = new Map();
|
|
72
|
+
for (const e of spec.elements ?? []) for (const p of e.paragraphs ?? []) if (p.id && p.id !== e.id) into.set(p.id, e.id);
|
|
73
|
+
const resolve = (id) => {
|
|
74
|
+
if (byId.has(id) || lines.has(id)) return id;
|
|
75
|
+
if (into.has(id)) return into.get(id);
|
|
76
|
+
// A label folded into its shape: the shape now carries its words.
|
|
77
|
+
const old = before.get(id);
|
|
78
|
+
const words = String(old?.text ?? "").trim();
|
|
79
|
+
const p = old?.parent ? byId.get(old.parent) : null;
|
|
80
|
+
return words && p && String(p.text ?? "").includes(words) ? p.id : null;
|
|
81
|
+
};
|
|
82
|
+
const kinds = (m) => (Array.isArray(m) ? m : [m]).map((id) => byId.get(id)?.kind ?? "line").sort().join(",");
|
|
83
|
+
const notes = [];
|
|
84
|
+
spec.repeats = spec.repeats.flatMap((g) => {
|
|
85
|
+
let members = g.members.map((m) => (Array.isArray(m) ? [...new Set(m.map(resolve).filter(Boolean))] : resolve(m)));
|
|
86
|
+
if (members.every((m) => Array.isArray(m) && m.length === 1 && byId.has(m[0]))) members = members.map((m) => m[0]);
|
|
87
|
+
const label = `${g.id} (${g.members.map((m) => (Array.isArray(m) ? `(${m.join(" + ")})` : m)).join(", ")})`;
|
|
88
|
+
const why = members.some((m) => !(Array.isArray(m) ? m.some((id) => byId.has(id)) : m)) ? "a member did not survive normalization"
|
|
89
|
+
: new Set(members.map(kinds)).size > 1 ? "its members no longer hold like parts once texts were joined" : null;
|
|
90
|
+
if (why) { notes.push(`${label}: ${why} — kept as drawn`); return []; }
|
|
91
|
+
// Rows of a dot and its line that became the items of one list: the set
|
|
92
|
+
// IS that one text now, built as a list, not as copies of it.
|
|
93
|
+
if (members.length > 1 && new Set(members.map((m) => JSON.stringify(Array.isArray(m) ? [...m].sort() : [m]))).size === 1) {
|
|
94
|
+
notes.push(`${label}: its members became one list`);
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
return [{ ...g, members }];
|
|
98
|
+
});
|
|
99
|
+
return notes;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** What a member holds: its children (or, a virtual member, its parts) and theirs, by depth. */
|
|
103
|
+
export function descendants(elements, root) {
|
|
104
|
+
const out = [];
|
|
105
|
+
const seen = new Set();
|
|
106
|
+
const walk = (id, depth) => {
|
|
107
|
+
for (const e of elements) if (e.parent === id && !seen.has(e.id)) { seen.add(e.id); out.push({ e, depth }); walk(e.id, depth + 1); }
|
|
108
|
+
};
|
|
109
|
+
if (!root.ids) {
|
|
110
|
+
walk(root.id, 1);
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
const byId = new Map(elements.map((e) => [e.id, e]));
|
|
114
|
+
const parts = new Set(root.ids);
|
|
115
|
+
// A part inside another part is that part's child, not a second root.
|
|
116
|
+
const nested = (e) => {
|
|
117
|
+
for (let p = e.parent, g = 0; p && g < 32; p = byId.get(p)?.parent ?? null, g++) if (parts.has(p)) return true;
|
|
118
|
+
return false;
|
|
119
|
+
};
|
|
120
|
+
for (const id of root.ids) {
|
|
121
|
+
const e = byId.get(id);
|
|
122
|
+
if (!e || nested(e) || seen.has(id)) continue;
|
|
123
|
+
seen.add(id);
|
|
124
|
+
out.push({ e, depth: 1 });
|
|
125
|
+
walk(id, 2);
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Corresponding children across members: each member's descendants keyed by
|
|
132
|
+
* depth, kind and — a text — its role (a title answers a title, never a
|
|
133
|
+
* body), then in reading order — rows first (children whose centres lie within
|
|
134
|
+
* half the smaller height share a row), left to right inside a row. A key the
|
|
135
|
+
* members share corresponds as far as every member has one; what only some
|
|
136
|
+
* members hold rides along with its member. null when they share nothing.
|
|
137
|
+
*/
|
|
138
|
+
export function correspond(elements, members) {
|
|
139
|
+
const reading = (items) => {
|
|
140
|
+
const byY = [...items].sort((a, b) => a.cy - b.cy);
|
|
141
|
+
const rows = [];
|
|
142
|
+
for (const it of byY) {
|
|
143
|
+
const row = rows.at(-1);
|
|
144
|
+
if (row && Math.abs(it.cy - row[0].cy) < Math.min(it.e.h, row[0].e.h) / 2) row.push(it);
|
|
145
|
+
else rows.push([it]);
|
|
146
|
+
}
|
|
147
|
+
return rows.flatMap((r) => r.sort((a, b) => a.cx - b.cx));
|
|
148
|
+
};
|
|
149
|
+
const keyOf = (it) => `${it.depth}:${it.e.kind}${it.e.kind === "text" || it.e.kind === "number" ? `:${it.e.role ?? ""}` : ""}`;
|
|
150
|
+
const lists = members.map((m) => {
|
|
151
|
+
const groups = new Map();
|
|
152
|
+
for (const { e, depth } of descendants(elements, m)) {
|
|
153
|
+
const it = { e, depth, cy: e.y + e.h / 2 - m.y, cx: e.x + e.w / 2 - m.x };
|
|
154
|
+
groups.set(keyOf(it), [...(groups.get(keyOf(it)) ?? []), it]);
|
|
155
|
+
}
|
|
156
|
+
for (const [k, items] of groups) groups.set(k, reading(items));
|
|
157
|
+
return groups;
|
|
158
|
+
});
|
|
159
|
+
const keys = [...lists[0].keys()].filter((k) => lists.every((l) => l.has(k))).sort();
|
|
160
|
+
const counts = keys.map((k) => Math.min(...lists.map((l) => l.get(k).length)));
|
|
161
|
+
const out = lists.map((l) => keys.flatMap((k, j) => l.get(k).slice(0, counts[j]).map((c) => c.e)));
|
|
162
|
+
return out[0].length ? out : null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Member centres for an arrangement at template size W×H, from where the
|
|
167
|
+
* members are now. null when the arrangement cannot hold them.
|
|
168
|
+
*/
|
|
169
|
+
export function arrange(group, W, H) {
|
|
170
|
+
const { members, arrangement } = group;
|
|
171
|
+
const c = members.map(centre);
|
|
172
|
+
const n = members.length;
|
|
173
|
+
const line = (axis, size) => {
|
|
174
|
+
const cross = axis === "x" ? "y" : "x";
|
|
175
|
+
const order = members.map((_, i) => i).sort((a, b) => c[a][axis] - c[b][axis]);
|
|
176
|
+
const lo = Math.min(...members.map((m) => m[axis]));
|
|
177
|
+
const hi = Math.max(...members.map((m) => m[axis] + (axis === "x" ? m.w : m.h)));
|
|
178
|
+
let gap = (hi - lo - n * size) / (n - 1);
|
|
179
|
+
let start = lo;
|
|
180
|
+
if (gap < GAP) {
|
|
181
|
+
gap = GAP;
|
|
182
|
+
start = (lo + hi) / 2 - (n * size + (n - 1) * GAP) / 2;
|
|
183
|
+
}
|
|
184
|
+
const at = median(c.map((p) => p[cross]));
|
|
185
|
+
const out = new Array(n);
|
|
186
|
+
order.forEach((i, k) => { out[i] = { [axis]: start + size / 2 + k * (size + gap), [cross]: at }; });
|
|
187
|
+
return out;
|
|
188
|
+
};
|
|
189
|
+
if (arrangement === "row") return line("x", W);
|
|
190
|
+
if (arrangement === "column") return line("y", H);
|
|
191
|
+
if (arrangement === "grid") {
|
|
192
|
+
const cluster = (axis, size) => {
|
|
193
|
+
const order = members.map((_, i) => i).sort((a, b) => c[a][axis] - c[b][axis]);
|
|
194
|
+
const bands = [];
|
|
195
|
+
for (const i of order) {
|
|
196
|
+
const last = bands.at(-1);
|
|
197
|
+
if (last && c[i][axis] - c[last[0]][axis] <= size / 2) last.push(i);
|
|
198
|
+
else bands.push([i]);
|
|
199
|
+
}
|
|
200
|
+
const means = bands.map((b) => b.reduce((s, i) => s + c[i][axis], 0) / b.length);
|
|
201
|
+
let pitch = bands.length > 1 ? (means.at(-1) - means[0]) / (bands.length - 1) : 0;
|
|
202
|
+
let first = means[0];
|
|
203
|
+
if (bands.length > 1 && pitch < size + GAP) {
|
|
204
|
+
pitch = size + GAP;
|
|
205
|
+
first = (means[0] + means.at(-1)) / 2 - (pitch * (bands.length - 1)) / 2;
|
|
206
|
+
}
|
|
207
|
+
const at = new Array(n);
|
|
208
|
+
bands.forEach((b, k) => { for (const i of b) at[i] = first + k * pitch; });
|
|
209
|
+
return at;
|
|
210
|
+
};
|
|
211
|
+
const ys = cluster("y", H);
|
|
212
|
+
const xs = cluster("x", W);
|
|
213
|
+
const cells = new Set(members.map((_, i) => `${xs[i]},${ys[i]}`));
|
|
214
|
+
return cells.size === n ? members.map((_, i) => ({ x: xs[i], y: ys[i] })) : null;
|
|
215
|
+
}
|
|
216
|
+
if (arrangement === "ring") {
|
|
217
|
+
const C = group.hub ? centre(group.hub) : { x: c.reduce((s, p) => s + p.x, 0) / n, y: c.reduce((s, p) => s + p.y, 0) / n };
|
|
218
|
+
const deg = (i) => (Math.atan2(c[i].y - C.y, c[i].x - C.x) * 180) / Math.PI;
|
|
219
|
+
const order = members.map((_, i) => i).sort((a, b) => deg(a) - deg(b));
|
|
220
|
+
const step = 360 / n;
|
|
221
|
+
// The start angle every member agrees on (circular mean), on the 15° grid.
|
|
222
|
+
let sx = 0;
|
|
223
|
+
let sy = 0;
|
|
224
|
+
order.forEach((i, k) => {
|
|
225
|
+
const a = ((deg(i) - k * step) * Math.PI) / 180;
|
|
226
|
+
sx += Math.cos(a);
|
|
227
|
+
sy += Math.sin(a);
|
|
228
|
+
});
|
|
229
|
+
const start = Math.round(((Math.atan2(sy, sx) * 180) / Math.PI) / ANGLE_SNAP) * ANGLE_SNAP;
|
|
230
|
+
const phi = new Array(n);
|
|
231
|
+
order.forEach((i, k) => { phi[i] = ((start + k * step) * Math.PI) / 180; });
|
|
232
|
+
// One ellipse through them all (least squares per axis).
|
|
233
|
+
const fit = (axis, f) => {
|
|
234
|
+
const den = members.reduce((s, _, i) => s + f(phi[i]) ** 2, 0);
|
|
235
|
+
return den > 1e-6 ? Math.abs(members.reduce((s, _, i) => s + (c[i][axis] - C[axis]) * f(phi[i]), 0) / den) : 0;
|
|
236
|
+
};
|
|
237
|
+
const rx = fit("x", Math.cos);
|
|
238
|
+
const ry = fit("y", Math.sin);
|
|
239
|
+
const snap = (v) => (Math.abs(v) < 1e-9 ? 0 : v);
|
|
240
|
+
return members.map((_, i) => ({ x: C.x + snap(rx * Math.cos(phi[i])), y: C.y + snap(ry * Math.sin(phi[i])) }));
|
|
241
|
+
}
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const inside = (r, p, tol = 2) => p.x >= r.x - tol && p.y >= r.y - tol && p.x <= r.x + r.w + tol && p.y <= r.y + r.h + tol;
|
|
246
|
+
// Every point a connector carries — its ends, its trace, and the cached native
|
|
247
|
+
// line (`line.ends[].at`, `line.path.points`) the directive is written from.
|
|
248
|
+
// Deduplicated by identity: the trace array is shared between them at times.
|
|
249
|
+
export const linePoints = (c) => [...new Set([c.fromPoint, c.toPoint, ...(c.points ?? []), ...(c.line?.ends ?? []).map((e) => e.at), ...(c.line?.path?.points ?? [])])]
|
|
250
|
+
.filter((p) => p && Number.isFinite(p.x) && Number.isFinite(p.y));
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* A member's own lines: connectors drawn wholly inside it (a divider under a
|
|
254
|
+
* card title) whose glued ends, if any, are the member or its content. They are
|
|
255
|
+
* not elements, so they move with the member explicitly.
|
|
256
|
+
*/
|
|
257
|
+
export function linesWithin(spec, member) {
|
|
258
|
+
const own = new Set([member.id, ...descendants(spec.elements ?? [], member).map(({ e }) => e.id)]);
|
|
259
|
+
return (spec.connectors ?? []).filter((c) => {
|
|
260
|
+
const pts = linePoints(c);
|
|
261
|
+
return pts.length >= 2 && pts.every((p) => inside(member, p)) && [c.from, c.to].every((id) => !id || own.has(id));
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** The elements a connector is glued to: its ends, and the cached native line's. */
|
|
266
|
+
export const gluedIds = (c) => [c.from, c.to, ...(c.line?.ends ?? []).map((e) => e?.node)].filter(Boolean);
|
|
267
|
+
|
|
268
|
+
/** A connector's drawn polyline: the cached native path, its trace, or its two ends. */
|
|
269
|
+
export const lineTrace = (c) => [c.line?.path?.points, c.points, [c.fromPoint, c.toPoint]]
|
|
270
|
+
.find((p) => p?.length >= 2 && p.every((q) => Number.isFinite(q?.x) && Number.isFinite(q?.y))) ?? null;
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Where the slide's lines cross the rows y0..y1: `{ x, w, line }` per segment of a
|
|
274
|
+
* connector's trace inside that band, padded by half its stroke. A text grown
|
|
275
|
+
* sideways stops at these as at any neighbour (a loop around a panel, the
|
|
276
|
+
* rule beside a title) — a line is not an element, so an element-only room
|
|
277
|
+
* let a widened text run into it (pm-03 after-bullet-2 over its loop).
|
|
278
|
+
* A line glued to one of `ignore` (the text itself, its card) follows that
|
|
279
|
+
* element when it grows, so it is no neighbour of it.
|
|
280
|
+
*/
|
|
281
|
+
export const lineSpans = (spec, y0, y1, ignore = new Set()) => band(spec, y0, y1, ignore, "y", "x").map(({ at, len, line }) => ({ x: at, w: len, line }));
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* The same crossing the columns x0..x1: `{ y, h, line }` per segment of a
|
|
285
|
+
* trace there — what lies above or under a text in its own columns (the rule
|
|
286
|
+
* over a title, the timeline over a step number).
|
|
287
|
+
*/
|
|
288
|
+
export const lineRows = (spec, x0, x1, ignore = new Set()) => band(spec, x0, x1, ignore, "x", "y").map(({ at, len, line }) => ({ y: at, h: len, line }));
|
|
289
|
+
|
|
290
|
+
// Each segment's extent along `v` where it crosses lo..hi along `u`.
|
|
291
|
+
function band(spec, lo, hi, ignore, u, v) {
|
|
292
|
+
const out = [];
|
|
293
|
+
for (const c of spec.connectors ?? []) {
|
|
294
|
+
const trace = gluedIds(c).some((id) => ignore.has(id)) ? null : lineTrace(c);
|
|
295
|
+
if (!trace) continue;
|
|
296
|
+
const pad = (Number.isFinite(c.strokeWidth) ? c.strokeWidth : 1) / 2;
|
|
297
|
+
for (let i = 1; i < trace.length; i++) {
|
|
298
|
+
const a = trace[i - 1];
|
|
299
|
+
const b = trace[i];
|
|
300
|
+
let t0 = 0;
|
|
301
|
+
let t1 = 1;
|
|
302
|
+
if (a[u] === b[u]) {
|
|
303
|
+
if (a[u] + pad <= lo || a[u] - pad >= hi) continue;
|
|
304
|
+
} else {
|
|
305
|
+
[t0, t1] = [(lo - pad - a[u]) / (b[u] - a[u]), (hi + pad - a[u]) / (b[u] - a[u])].sort((p, q) => p - q);
|
|
306
|
+
t0 = Math.max(0, t0);
|
|
307
|
+
t1 = Math.min(1, t1);
|
|
308
|
+
if (t0 >= t1) continue;
|
|
309
|
+
}
|
|
310
|
+
const va = a[v] + (b[v] - a[v]) * t0;
|
|
311
|
+
const vb = a[v] + (b[v] - a[v]) * t1;
|
|
312
|
+
out.push({ at: Math.min(va, vb) - pad, len: Math.abs(vb - va) + 2 * pad, line: c });
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return out;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export function moveLine(c, dx, dy) {
|
|
319
|
+
for (const p of linePoints(c)) { p.x += dx; p.y += dy; }
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* A member holding a sketched figure (spec.sketch, absolute coordinates keyed
|
|
324
|
+
* by id) or its own drawn geometry: a drawing moves as a whole, never resized.
|
|
325
|
+
*/
|
|
326
|
+
export function holdsDrawing(spec, member) {
|
|
327
|
+
const figures = new Set((spec.sketch?.figures ?? []).map((f) => f.id));
|
|
328
|
+
return [member, ...descendants(spec.elements ?? [], member).map(({ e }) => e)]
|
|
329
|
+
.some((e) => figures.has(e.id) || e.geometry || e.points?.length || e.outline?.length);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const isDrawn = (figures, e) => figures.has(e.id) || Boolean(e.geometry || e.points?.length || e.outline?.length);
|
|
333
|
+
// The circles a figure's arcs ride on (an edge names its circle only as an arc).
|
|
334
|
+
const usedCircles = (f) => [...(f.outline ?? []), ...(f.holes ?? []).flat()]
|
|
335
|
+
.filter((p) => p?.next === "arc").map((p) => p.circle).concat(f.copyOf?.about).filter((c) => typeof c === "string" && c !== ".");
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* The circles a moved figure rides on must move with it, so they may belong
|
|
339
|
+
* to the drawings of ONE member only: a ring piece in one card arcing on the
|
|
340
|
+
* circle of another, or of a drawing outside the set, pins the set as drawn.
|
|
341
|
+
*/
|
|
342
|
+
function circlesShared(sketch, owner) {
|
|
343
|
+
// Only a circle the sketch draws moves; a reader's empty or ":null"
|
|
344
|
+
// stand-in names none (x87 cloud deck: three card sets pinned by ":null").
|
|
345
|
+
const drawn = new Set((sketch?.circles ?? []).map((c) => c.id));
|
|
346
|
+
const users = new Map();
|
|
347
|
+
for (const f of sketch?.figures ?? []) for (const c of usedCircles(f)) if (drawn.has(c)) users.set(c, [...(users.get(c) ?? []), f.id]);
|
|
348
|
+
return [...users.values()].some((ids) => {
|
|
349
|
+
const owners = new Set(ids.map((id) => owner.get(id) ?? null));
|
|
350
|
+
return owners.size > 1 && [...owners].some((o) => o != null);
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Move a sketched figure with its element: its outline, holes, mirror axis and
|
|
356
|
+
* the circles it rides on. A copy is derived from its source, so only its
|
|
357
|
+
* offset changes — after the whole set moved (fixCopies).
|
|
358
|
+
*/
|
|
359
|
+
export function translateFigure(sketch, id, dx, dy, deltas, movedCircles) {
|
|
360
|
+
const f = (sketch?.figures ?? []).find((x) => x.id === id);
|
|
361
|
+
if (!f) return;
|
|
362
|
+
deltas.set(id, { dx, dy });
|
|
363
|
+
if (f.copyOf?.id) return;
|
|
364
|
+
for (const p of [...(f.outline ?? []), ...(f.holes ?? []).flat()]) { p.x += dx; p.y += dy; }
|
|
365
|
+
const m = f.mirror;
|
|
366
|
+
if (m?.axis === "vertical") m.at += dx;
|
|
367
|
+
else if (m?.axis === "horizontal") m.at += dy;
|
|
368
|
+
else if (m?.axis === "line" && m.through) {
|
|
369
|
+
if (Array.isArray(m.through)) m.through = [m.through[0] + dx, m.through[1] + dy];
|
|
370
|
+
else m.through = { ...m.through, x: m.through.x + dx, y: m.through.y + dy };
|
|
371
|
+
}
|
|
372
|
+
for (const c of sketch.circles ?? []) {
|
|
373
|
+
if (usedCircles(f).includes(c.id) && !movedCircles.has(c.id)) { c.cx += dx; c.cy += dy; movedCircles.add(c.id); }
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// A copy sits at its source moved by its offset: it moves by what it should
|
|
378
|
+
// move less what its source moved.
|
|
379
|
+
export function fixCopies(sketch, deltas) {
|
|
380
|
+
for (const f of sketch?.figures ?? []) {
|
|
381
|
+
if (!f.copyOf?.id) continue;
|
|
382
|
+
const own = deltas.get(f.id) ?? { dx: 0, dy: 0 };
|
|
383
|
+
const src = deltas.get(f.copyOf.id) ?? { dx: 0, dy: 0 };
|
|
384
|
+
if (own.dx === src.dx && own.dy === src.dy) continue;
|
|
385
|
+
f.copyOf.dx = (Number(f.copyOf.dx) || 0) + own.dx - src.dx;
|
|
386
|
+
f.copyOf.dy = (Number(f.copyOf.dy) || 0) + own.dy - src.dy;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const overlaps = (a, b) => a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h;
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Keep a built set inside the slot without breaking it. A ring keeps its hub
|
|
394
|
+
* where it is — the hub anchors it to the rest of the slide — and closes in:
|
|
395
|
+
* the members on both sides of the hub move toward it by the same amount.
|
|
396
|
+
* Only when that would run them into the hub does the ring move as one, hub
|
|
397
|
+
* included; a row, column or grid moves as one. Moves the plans in place;
|
|
398
|
+
* returns the hub's shift, or null when the set cannot fit.
|
|
399
|
+
*/
|
|
400
|
+
function fitSlot(plans, hub, hubTree, slot) {
|
|
401
|
+
const rects = () => plans.flatMap((p) => [p.box, ...p.content.map((c) => c.to)]);
|
|
402
|
+
const saved = rects().map((r) => ({ ...r }));
|
|
403
|
+
const restore = () => rects().forEach((r, k) => Object.assign(r, saved[k]));
|
|
404
|
+
const move = (p, axis, d) => { for (const r of [p.box, ...p.content.map((c) => c.to)]) r[axis] += d; };
|
|
405
|
+
const axes = [["x", "w", slot.w], ["y", "h", slot.h]];
|
|
406
|
+
const extent = (axis, size, extra = []) => {
|
|
407
|
+
const rs = [...rects(), ...extra];
|
|
408
|
+
return [Math.min(...rs.map((r) => r[axis])), Math.max(...rs.map((r) => r[axis] + r[size]))];
|
|
409
|
+
};
|
|
410
|
+
const closeIn = (axis, size, inward) => {
|
|
411
|
+
const c = hub[axis] + hub[size] / 2;
|
|
412
|
+
for (const p of plans) {
|
|
413
|
+
const pc = p.box[axis] + p.box[size] / 2;
|
|
414
|
+
move(p, axis, pc < c - 1 ? inward : pc > c + 1 ? -inward : 0);
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
if (hub) {
|
|
418
|
+
for (const [axis, size, limit] of axes) {
|
|
419
|
+
const c = hub[axis] + hub[size] / 2;
|
|
420
|
+
const [lo, hi] = extent(axis, size);
|
|
421
|
+
const inward = Math.max(0, -lo, hi - limit);
|
|
422
|
+
if (inward > 0 && c - lo > inward && hi - c > inward) closeIn(axis, size, inward);
|
|
423
|
+
}
|
|
424
|
+
const inside = axes.every(([axis, size, limit]) => { const [lo, hi] = extent(axis, size); return lo >= -0.5 && hi <= limit + 0.5; });
|
|
425
|
+
if (inside && plans.every((p) => !overlaps(p.box, hub))) return { dx: 0, dy: 0 };
|
|
426
|
+
restore();
|
|
427
|
+
}
|
|
428
|
+
const out = { dx: 0, dy: 0 };
|
|
429
|
+
for (const [axis, size, limit] of axes) {
|
|
430
|
+
let [lo, hi] = extent(axis, size, hubTree);
|
|
431
|
+
if (hi - lo > limit && hub) {
|
|
432
|
+
closeIn(axis, size, (hi - lo - limit) / 2);
|
|
433
|
+
[lo, hi] = extent(axis, size, hubTree);
|
|
434
|
+
}
|
|
435
|
+
if (hi - lo > limit + 0.5) return null;
|
|
436
|
+
const d = lo < 0 ? -lo : hi > limit ? limit - hi : 0;
|
|
437
|
+
for (const p of plans) move(p, axis, d);
|
|
438
|
+
out[axis === "x" ? "dx" : "dy"] = d;
|
|
439
|
+
}
|
|
440
|
+
return out;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Build every declared set from one template, in place. Returns one line per
|
|
445
|
+
* set built or kept as drawn.
|
|
446
|
+
*/
|
|
447
|
+
export function buildRepeats(spec) {
|
|
448
|
+
const elements = spec.elements ?? [];
|
|
449
|
+
const slot = spec.slot;
|
|
450
|
+
const figures = new Set((spec.sketch?.figures ?? []).map((f) => f.id));
|
|
451
|
+
const drawn = (e) => isDrawn(figures, e);
|
|
452
|
+
const notes = [];
|
|
453
|
+
for (const g of repeatGroups(spec)) {
|
|
454
|
+
const { members } = g;
|
|
455
|
+
const label = `${g.id} (${members.map((m) => (m.ids ? `(${m.ids.join(" + ")})` : m.id)).join(", ")})`;
|
|
456
|
+
// A drawing moves whole with its member (never resized); the circles it
|
|
457
|
+
// rides on move with it, so none may be another member's or the slide's.
|
|
458
|
+
const owner = new Map(members.flatMap((m) => [m, ...descendants(elements, m).map(({ e }) => e)].filter((e) => figures.has(e.id)).map((e) => [e.id, m.id])));
|
|
459
|
+
if (owner.size && circlesShared(spec.sketch, owner)) { notes.push(`${label}: a drawing shares a circle with one outside its member — kept as drawn`); continue; }
|
|
460
|
+
const W = Math.round(median(members.map((m) => m.w)));
|
|
461
|
+
const H = Math.round(median(members.map((m) => m.h)));
|
|
462
|
+
const at = arrange(g, W, H);
|
|
463
|
+
if (!at) { notes.push(`${label}: its ${g.arrangement} could not be laid out — kept as drawn`); continue; }
|
|
464
|
+
const kids = correspond(elements, members);
|
|
465
|
+
// Each member's own lines, in reading order; alike across the set they
|
|
466
|
+
// share one template (a divider at one offset and length), else ride along.
|
|
467
|
+
const lines = members.map((m) => linesWithin(spec, m).sort((a, b) => linePoints(a)[0].y - linePoints(b)[0].y || linePoints(a)[0].x - linePoints(b)[0].x));
|
|
468
|
+
const lineTemplate = lines.every((l) => l.length === lines[0].length) && lines.flat().every((c) => linePoints(c).length === 2 || (c.points?.length === 2 && c.fromPoint && c.toPoint))
|
|
469
|
+
? lines[0].map((_, k) => ["fromPoint", "toPoint"].map((end) => ({
|
|
470
|
+
x: median(lines.map((l, i) => (l[k][end] ?? l[k].points[end === "fromPoint" ? 0 : 1]).x - members[i].x)),
|
|
471
|
+
y: median(lines.map((l, i) => (l[k][end] ?? l[k].points[end === "fromPoint" ? 0 : 1]).y - members[i].y)),
|
|
472
|
+
})))
|
|
473
|
+
: null;
|
|
474
|
+
const template = kids?.[0].map((_, k) => ({
|
|
475
|
+
x: median(kids.map((l, i) => l[k].x - members[i].x)),
|
|
476
|
+
y: median(kids.map((l, i) => l[k].y - members[i].y)),
|
|
477
|
+
w: median(kids.map((l) => l[k].w)),
|
|
478
|
+
h: median(kids.map((l) => l[k].h)),
|
|
479
|
+
}));
|
|
480
|
+
// Where everything would go, before anything moves. A drawn member keeps
|
|
481
|
+
// its own size about the arranged centre; a drawn child keeps its size
|
|
482
|
+
// about its template centre; what only some members hold rides along.
|
|
483
|
+
const plans = members.map((m, i) => {
|
|
484
|
+
// Half-pixel grid: a hub centred on x.5 keeps its members mirror-exact.
|
|
485
|
+
const [bw, bh] = drawn(m) ? [m.w, m.h] : [W, H];
|
|
486
|
+
const box = { x: half(at[i].x - bw / 2), y: half(at[i].y - bh / 2), w: bw, h: bh };
|
|
487
|
+
const ride = (e) => ({ e, to: { x: half(e.x + box.x - m.x + (bw - m.w) / 2), y: half(e.y + box.y - m.y + (bh - m.h) / 2), w: e.w, h: e.h } });
|
|
488
|
+
const mine = kids?.[i] ?? [];
|
|
489
|
+
const content = [
|
|
490
|
+
...mine.map((e, k) => {
|
|
491
|
+
const t = template[k];
|
|
492
|
+
return drawn(e)
|
|
493
|
+
? { e, to: { x: half(box.x + t.x + t.w / 2 - e.w / 2), y: half(box.y + t.y + t.h / 2 - e.h / 2), w: e.w, h: e.h } }
|
|
494
|
+
: { e, to: { x: half(box.x + t.x), y: half(box.y + t.y), w: half(t.w), h: half(t.h) } };
|
|
495
|
+
}),
|
|
496
|
+
...descendants(elements, m).map(({ e }) => e).filter((e) => !mine.includes(e)).map(ride),
|
|
497
|
+
];
|
|
498
|
+
return { m, box, content, lines: lines[i] };
|
|
499
|
+
});
|
|
500
|
+
const hub = g.hub ? [g.hub, ...descendants(elements, g.hub).map(({ e }) => e)] : [];
|
|
501
|
+
const shift = fitSlot(plans, g.arrangement === "ring" ? g.hub : null, hub, slot);
|
|
502
|
+
if (!shift) { notes.push(`${label}: one template would leave the slot — kept as drawn`); continue; }
|
|
503
|
+
const moved = new Set(plans.flatMap((p) => [p.m, ...p.content.map((c) => c.e)]));
|
|
504
|
+
const hubAt = hub.map((h) => ({ x: h.x + shift.dx, y: h.y + shift.dy, w: h.w, h: h.h }));
|
|
505
|
+
const collides = plans.some((p, i) => plans.some((q, j) => j > i && overlaps(p.box, q.box)))
|
|
506
|
+
|| plans.some((p) => hub.some((h, k) => !moved.has(h) && overlaps(p.box, hubAt[k])));
|
|
507
|
+
if (collides) { notes.push(`${label}: one template would overlap — kept as drawn`); continue; }
|
|
508
|
+
const deltas = new Map();
|
|
509
|
+
const movedCircles = new Set();
|
|
510
|
+
const move = (e, dx, dy) => {
|
|
511
|
+
translateElement(e, dx, dy);
|
|
512
|
+
if (figures.has(e.id)) translateFigure(spec.sketch, e.id, dx, dy, deltas, movedCircles);
|
|
513
|
+
};
|
|
514
|
+
for (const { m, box, content, lines: own } of plans) {
|
|
515
|
+
own.forEach((c, k) => {
|
|
516
|
+
if (!lineTemplate) return moveLine(c, box.x - m.x, box.y - m.y);
|
|
517
|
+
const [a, b] = lineTemplate[k].map((t) => ({ x: half(box.x + t.x), y: half(box.y + t.y) }));
|
|
518
|
+
if (c.fromPoint) c.fromPoint = { ...a };
|
|
519
|
+
if (c.toPoint) c.toPoint = { ...b };
|
|
520
|
+
if (c.points?.length === 2) c.points = [{ ...a }, { ...b }];
|
|
521
|
+
if (c.line?.ends?.length === 2) { c.line.ends[0].at = { ...a }; c.line.ends[1].at = { ...b }; }
|
|
522
|
+
if (c.line?.path?.points?.length === 2) c.line.path.points = [{ ...a }, { ...b }];
|
|
523
|
+
});
|
|
524
|
+
for (const { e, to } of content) {
|
|
525
|
+
move(e, to.x - e.x, to.y - e.y);
|
|
526
|
+
e.w = to.w;
|
|
527
|
+
e.h = to.h;
|
|
528
|
+
}
|
|
529
|
+
move(m, box.x - m.x, box.y - m.y);
|
|
530
|
+
m.w = box.w;
|
|
531
|
+
m.h = box.h;
|
|
532
|
+
}
|
|
533
|
+
if (shift.dx || shift.dy) for (const h of hub) move(h, shift.dx, shift.dy);
|
|
534
|
+
fixCopies(spec.sketch, deltas);
|
|
535
|
+
const declared = (spec.repeats ?? []).find((r) => r.id === g.id);
|
|
536
|
+
if (declared) declared.built = true;
|
|
537
|
+
notes.push(`${label}: one ${W}×${H}px template${kids ? " with one inner layout" : ""}, ${g.arrangement}${g.hub ? ` around ${g.hub.id}` : ""}`);
|
|
538
|
+
}
|
|
539
|
+
return notes;
|
|
540
|
+
}
|
|
@@ -286,10 +286,11 @@ export function resolveSkill(skillName, planMeta = {}, opts = {}) {
|
|
|
286
286
|
// and the per-layout chrome backgrounds (`assets/chrome-*`). They must be
|
|
287
287
|
// resolved INDEPENDENTLY of the deck — `assetsDir` above prefers the deck's own
|
|
288
288
|
// `assets/` (engine downloads), so if `brand.assetsDir` mirrored it, a deck that
|
|
289
|
-
// already has any local asset would collapse both layers to the deck dir and
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
//
|
|
289
|
+
// already has any local asset would collapse both layers to the deck dir and the
|
|
290
|
+
// preview would lose the bundle's brand assets (chrome included). Keeping the
|
|
291
|
+
// brand layer on the bundle lets the deck's assets layer ON TOP and override by
|
|
292
|
+
// name, without suppressing the brand chrome. build.mjs points the page at this
|
|
293
|
+
// directory directly; nothing is copied into the deck.
|
|
293
294
|
const brandAssetsDir = firstExistingOr(
|
|
294
295
|
designRoots.map((r) => path.join(r, "assets")),
|
|
295
296
|
path.join(designRoots[0], "assets"),
|