@imenam/mcp-design 1.0.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.
Files changed (71) hide show
  1. package/README.md +231 -0
  2. package/dist/shared/node-defaults.d.ts +30 -0
  3. package/dist/shared/node-defaults.js +67 -0
  4. package/dist/shared/node-fields.d.ts +33 -0
  5. package/dist/shared/node-fields.js +40 -0
  6. package/dist/shared/render.d.ts +105 -0
  7. package/dist/shared/render.js +215 -0
  8. package/dist/shared/schemas/design.schema.d.ts +231 -0
  9. package/dist/shared/schemas/design.schema.js +98 -0
  10. package/dist/shared/tree.d.ts +93 -0
  11. package/dist/shared/tree.js +223 -0
  12. package/dist/src/cli/setup-mcp.d.ts +5 -0
  13. package/dist/src/cli/setup-mcp.js +87 -0
  14. package/dist/src/core/design-store.d.ts +61 -0
  15. package/dist/src/core/design-store.js +294 -0
  16. package/dist/src/core/diff.d.ts +22 -0
  17. package/dist/src/core/diff.js +97 -0
  18. package/dist/src/core/export.d.ts +6 -0
  19. package/dist/src/core/export.js +251 -0
  20. package/dist/src/core/hierarchy.d.ts +18 -0
  21. package/dist/src/core/hierarchy.js +68 -0
  22. package/dist/src/core/ids.d.ts +4 -0
  23. package/dist/src/core/ids.js +21 -0
  24. package/dist/src/core/log-dir.d.ts +6 -0
  25. package/dist/src/core/log-dir.js +12 -0
  26. package/dist/src/core/migrate.d.ts +18 -0
  27. package/dist/src/core/migrate.js +54 -0
  28. package/dist/src/core/node-factory.d.ts +19 -0
  29. package/dist/src/core/node-factory.js +103 -0
  30. package/dist/src/core/screenshot.d.ts +31 -0
  31. package/dist/src/core/screenshot.js +53 -0
  32. package/dist/src/gui-worker.d.ts +1 -0
  33. package/dist/src/gui-worker.js +334 -0
  34. package/dist/src/index.d.ts +2 -0
  35. package/dist/src/index.js +673 -0
  36. package/dist/src/tools/add-nodes.d.ts +7 -0
  37. package/dist/src/tools/add-nodes.js +26 -0
  38. package/dist/src/tools/create-design.d.ts +7 -0
  39. package/dist/src/tools/create-design.js +15 -0
  40. package/dist/src/tools/delete-design.d.ts +3 -0
  41. package/dist/src/tools/delete-design.js +7 -0
  42. package/dist/src/tools/delete-nodes.d.ts +3 -0
  43. package/dist/src/tools/delete-nodes.js +29 -0
  44. package/dist/src/tools/export-design.d.ts +3 -0
  45. package/dist/src/tools/export-design.js +21 -0
  46. package/dist/src/tools/get-changes.d.ts +11 -0
  47. package/dist/src/tools/get-changes.js +27 -0
  48. package/dist/src/tools/get-design.d.ts +7 -0
  49. package/dist/src/tools/get-design.js +29 -0
  50. package/dist/src/tools/group-nodes.d.ts +10 -0
  51. package/dist/src/tools/group-nodes.js +83 -0
  52. package/dist/src/tools/list-designs.d.ts +3 -0
  53. package/dist/src/tools/list-designs.js +13 -0
  54. package/dist/src/tools/mark-synced.d.ts +7 -0
  55. package/dist/src/tools/mark-synced.js +19 -0
  56. package/dist/src/tools/reorder-nodes.d.ts +7 -0
  57. package/dist/src/tools/reorder-nodes.js +60 -0
  58. package/dist/src/tools/screenshot-design.d.ts +3 -0
  59. package/dist/src/tools/screenshot-design.js +36 -0
  60. package/dist/src/tools/types.d.ts +25 -0
  61. package/dist/src/tools/types.js +13 -0
  62. package/dist/src/tools/ungroup-nodes.d.ts +7 -0
  63. package/dist/src/tools/ungroup-nodes.js +64 -0
  64. package/dist/src/tools/update-design.d.ts +4 -0
  65. package/dist/src/tools/update-design.js +33 -0
  66. package/dist/src/tools/update-nodes.d.ts +3 -0
  67. package/dist/src/tools/update-nodes.js +35 -0
  68. package/dist/src/version.d.ts +1 -0
  69. package/dist/src/version.js +2 -0
  70. package/package.json +58 -0
  71. package/scripts/generate-version.js +20 -0
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Rendu SVG d'un noeud, partagé entre l'exporteur (côté serveur) et le canvas
3
+ * de la GUI (côté navigateur). Une seule implémentation : ce que l'utilisateur
4
+ * voit à l'écran est exactement ce que `export_design` produit.
5
+ *
6
+ * Ce module ne dépend volontairement d'aucune librairie (pas de zod) pour rester
7
+ * importable depuis le bundle GUI.
8
+ */
9
+ import { boundsOf, childrenByParent, computeLayout, normalizedBox, spaceOriginOf } from "./tree.js";
10
+ // La géométrie de base vit dans tree.ts (module bas-niveau, sans dépendance vers
11
+ // celui-ci) ; elle reste exposée ici pour tous les appelants existants.
12
+ export { normalizedBox };
13
+ export function escapeXml(value) {
14
+ return value
15
+ .replace(/&/g, "&")
16
+ .replace(/</g, "&lt;")
17
+ .replace(/>/g, "&gt;")
18
+ .replace(/"/g, "&quot;")
19
+ .replace(/'/g, "&apos;");
20
+ }
21
+ function round(value) {
22
+ return Math.round(value * 100) / 100;
23
+ }
24
+ export function nodeCenter(node) {
25
+ const box = normalizedBox(node);
26
+ return { cx: box.x + box.width / 2, cy: box.y + box.height / 2 };
27
+ }
28
+ /** Transform SVG de rotation autour du centre de la boîte, ou "" si pas de rotation. */
29
+ export function rotationTransform(node) {
30
+ if (!node.rotation)
31
+ return "";
32
+ const { cx, cy } = nodeCenter(node);
33
+ return `rotate(${round(node.rotation)} ${round(cx)} ${round(cy)})`;
34
+ }
35
+ /**
36
+ * Le texte n'est pas re-wrappé automatiquement : seuls les retours à la ligne
37
+ * explicites créent une nouvelle ligne. Comportement prévisible pour l'agent
38
+ * comme pour l'utilisateur, et identique à l'export.
39
+ */
40
+ export function textLines(node) {
41
+ return (node.text ?? "").split("\n");
42
+ }
43
+ export const DEFAULT_FONT_FAMILY = "Inter, system-ui, sans-serif";
44
+ export const DEFAULT_FONT_SIZE = 24;
45
+ /**
46
+ * Interligne appliqué quand `lineHeight` est absent, exprimé en multiple de la
47
+ * taille de police. C'est le seul endroit où un ratio subsiste : le champ
48
+ * stocké, lui, est toujours en pixels.
49
+ */
50
+ export const DEFAULT_LINE_HEIGHT_RATIO = 1.35;
51
+ /**
52
+ * En dessous de ce seuil, une valeur d'interligne ne peut pas être des pixels :
53
+ * 4px d'interligne écraserait n'importe quel texte lisible. C'est donc un ratio
54
+ * — refusé à l'écriture, converti à la lecture des anciens fichiers.
55
+ */
56
+ export const MIN_LINE_HEIGHT_PX = 4;
57
+ /** Interligne par défaut, en pixels, pour une taille de police donnée. */
58
+ export function defaultLineHeight(fontSize) {
59
+ return Math.round(fontSize * DEFAULT_LINE_HEIGHT_RATIO * 100) / 100;
60
+ }
61
+ export function textMetrics(node) {
62
+ const fontSize = node.fontSize ?? DEFAULT_FONT_SIZE;
63
+ // `lineHeight` est une distance en pixels entre deux lignes de base, pas un
64
+ // multiplicateur : c'est l'unité que l'agent emploie spontanément, et la
65
+ // seule qui se lise directement dans la même échelle que x, y et fontSize.
66
+ const lineStep = node.lineHeight ?? defaultLineHeight(fontSize);
67
+ return {
68
+ fontSize,
69
+ lineStep,
70
+ /** Ligne de base de la première ligne, mesurée depuis le haut de la boîte. */
71
+ firstBaseline: lineStep * 0.5 + fontSize * 0.36,
72
+ anchor: node.textAlign === "center" ? "middle" : node.textAlign === "right" ? "end" : "start",
73
+ anchorX: node.textAlign === "center"
74
+ ? node.x + node.width / 2
75
+ : node.textAlign === "right"
76
+ ? node.x + node.width
77
+ : node.x,
78
+ };
79
+ }
80
+ /** Hauteur minimale nécessaire pour afficher tout le texte du noeud. */
81
+ export function measuredTextHeight(node) {
82
+ const { lineStep } = textMetrics(node);
83
+ return Math.max(lineStep, textLines(node).length * lineStep);
84
+ }
85
+ function shapeAttributes(node) {
86
+ const parts = [];
87
+ if (node.type !== "line" && node.type !== "text") {
88
+ parts.push(`fill="${escapeXml(node.fill ?? "none")}"`);
89
+ }
90
+ if (node.stroke && (node.strokeWidth ?? 0) > 0) {
91
+ parts.push(`stroke="${escapeXml(node.stroke)}"`, `stroke-width="${round(node.strokeWidth ?? 1)}"`);
92
+ }
93
+ return parts.join(" ");
94
+ }
95
+ /**
96
+ * Transform SVG d'un groupe : translation vers son origine, puis rotation
97
+ * autour du centre de son contenu (exprime dans l'espace local du groupe).
98
+ */
99
+ export function groupTransform(node, localCenter) {
100
+ const parts = [`translate(${round(node.x)} ${round(node.y)})`];
101
+ if (node.rotation)
102
+ parts.push(`rotate(${round(node.rotation)} ${round(localCenter.cx)} ${round(localCenter.cy)})`);
103
+ return parts.join(" ");
104
+ }
105
+ /**
106
+ * Markup SVG d'un seul noeud, sans wrapper — utilisé par l'export et par le canvas.
107
+ * Un groupe ne produit rien par lui-meme : c'est un conteneur, rendu par
108
+ * `documentToSvg` (serveur) ou par le canvas (navigateur).
109
+ */
110
+ export function nodeToSvg(node) {
111
+ if (node.type === "group")
112
+ return "";
113
+ const transform = rotationTransform(node);
114
+ const transformAttr = transform ? ` transform="${transform}"` : "";
115
+ const opacityAttr = node.opacity >= 1 ? "" : ` opacity="${round(node.opacity)}"`;
116
+ const box = normalizedBox(node);
117
+ switch (node.type) {
118
+ case "rect": {
119
+ const radius = Math.max(0, Math.min(node.radius ?? 0, Math.min(box.width, box.height) / 2));
120
+ return (`<rect x="${round(box.x)}" y="${round(box.y)}" width="${round(box.width)}" height="${round(box.height)}"` +
121
+ (radius > 0 ? ` rx="${round(radius)}" ry="${round(radius)}"` : "") +
122
+ ` ${shapeAttributes(node)}${opacityAttr}${transformAttr} />`);
123
+ }
124
+ case "ellipse": {
125
+ return (`<ellipse cx="${round(box.x + box.width / 2)}" cy="${round(box.y + box.height / 2)}"` +
126
+ ` rx="${round(box.width / 2)}" ry="${round(box.height / 2)}"` +
127
+ ` ${shapeAttributes(node)}${opacityAttr}${transformAttr} />`);
128
+ }
129
+ case "line": {
130
+ return (`<line x1="${round(node.x)}" y1="${round(node.y)}" x2="${round(node.x + node.width)}" y2="${round(node.y + node.height)}"` +
131
+ ` ${shapeAttributes(node)} stroke-linecap="round"${opacityAttr}${transformAttr} />`);
132
+ }
133
+ case "text": {
134
+ const m = textMetrics(node);
135
+ const lines = textLines(node);
136
+ const tspans = lines
137
+ .map((line, index) => `<tspan x="${round(m.anchorX)}" y="${round(node.y + m.firstBaseline + index * m.lineStep)}">` +
138
+ `${escapeXml(line === "" ? " " : line)}</tspan>`)
139
+ .join("");
140
+ return (`<text font-family="${escapeXml(node.fontFamily ?? DEFAULT_FONT_FAMILY)}" font-size="${round(m.fontSize)}"` +
141
+ ` font-weight="${node.fontWeight ?? 400}" font-style="${node.fontStyle ?? "normal"}"` +
142
+ ` fill="${escapeXml(node.color ?? "#000000")}" text-anchor="${m.anchor}"` +
143
+ ` xml:space="preserve"${opacityAttr}${transformAttr}>${tspans}</text>`);
144
+ }
145
+ }
146
+ }
147
+ /**
148
+ * Rend une liste de noeuds frères, en descendant récursivement dans les groupes.
149
+ * Partagé par le rendu plein document et le rendu cadré sur un seul élément.
150
+ */
151
+ function renderSiblings(nodes, children, layout, depth) {
152
+ const pad = " ".repeat(depth + 1);
153
+ return nodes.flatMap((node) => {
154
+ if (node.type !== "group")
155
+ return [`${pad}${nodeToSvg(node)}`];
156
+ const bounds = boundsOf(layout, node.id);
157
+ const origin = layout.origin.get(node.id) ?? { x: 0, y: 0 };
158
+ const center = { cx: bounds.x + bounds.width / 2 - origin.x, cy: bounds.y + bounds.height / 2 - origin.y };
159
+ const opacity = node.opacity >= 1 ? "" : ` opacity="${round(node.opacity)}"`;
160
+ return [
161
+ `${pad}<g transform="${groupTransform(node, center)}"${opacity}>`,
162
+ ...renderSiblings(children.get(node.id) ?? [], children, layout, depth + 1),
163
+ `${pad}</g>`,
164
+ ];
165
+ });
166
+ }
167
+ export function documentToSvg(doc) {
168
+ const layout = computeLayout(doc.nodes);
169
+ const children = childrenByParent(doc.nodes);
170
+ // Un groupe devient un <g> : ses enfants gardent leurs coordonnées locales,
171
+ // c'est la transformation du conteneur qui les place.
172
+ const body = renderSiblings(children.get(null) ?? [], children, layout, 0).join("\n");
173
+ return (`<svg xmlns="http://www.w3.org/2000/svg" width="${doc.width}" height="${doc.height}" ` +
174
+ `viewBox="0 0 ${doc.width} ${doc.height}">\n` +
175
+ ` <rect x="0" y="0" width="${doc.width}" height="${doc.height}" fill="${escapeXml(doc.background)}" />\n` +
176
+ (body ? `${body}\n` : "") +
177
+ `</svg>`);
178
+ }
179
+ /**
180
+ * SVG cadré sur un seul élément — l'élément et son contenu, rien d'autre.
181
+ *
182
+ * Le sous-arbre est rendu dans ses coordonnées locales, puis translaté vers sa
183
+ * position document : le viewBox peut alors être exprimé dans le même repère
184
+ * que `computeLayout`, donc directement sur la boîte englobante de l'élément.
185
+ */
186
+ export function subtreeToSvg(doc, nodeId, padding = 0) {
187
+ const node = doc.nodes.find((candidate) => candidate.id === nodeId);
188
+ if (!node)
189
+ throw new Error(`Node "${nodeId}" not found in design "${doc.name}".`);
190
+ const layout = computeLayout(doc.nodes);
191
+ const children = childrenByParent(doc.nodes);
192
+ const bounds = boundsOf(layout, nodeId);
193
+ const region = {
194
+ x: bounds.x - padding,
195
+ y: bounds.y - padding,
196
+ // Un groupe vide (ou un trait purement horizontal) a une dimension nulle :
197
+ // on garde au moins 1px pour que le PNG produit reste valide.
198
+ width: Math.max(1, bounds.width + padding * 2),
199
+ height: Math.max(1, bounds.height + padding * 2),
200
+ };
201
+ // Un groupe est rendu par son contenu, dans le repère du groupe ; tout autre
202
+ // noeud est rendu tel quel, dans le repère de son parent.
203
+ const origin = node.type === "group" ? (layout.origin.get(nodeId) ?? { x: 0, y: 0 }) : spaceOriginOf(layout, node.parentId);
204
+ const inner = node.type === "group"
205
+ ? renderSiblings([{ ...node, x: 0, y: 0 }], children, layout, 1).join("\n")
206
+ : ` ${nodeToSvg(node)}`;
207
+ return (`<svg xmlns="http://www.w3.org/2000/svg" width="${round(region.width)}" height="${round(region.height)}" ` +
208
+ `viewBox="${round(region.x)} ${round(region.y)} ${round(region.width)} ${round(region.height)}">\n` +
209
+ ` <rect x="${round(region.x)}" y="${round(region.y)}" width="${round(region.width)}" ` +
210
+ `height="${round(region.height)}" fill="${escapeXml(doc.background)}" />\n` +
211
+ ` <g transform="translate(${round(origin.x)} ${round(origin.y)})">\n` +
212
+ (inner ? `${inner}\n` : "") +
213
+ ` </g>\n` +
214
+ `</svg>`);
215
+ }
@@ -0,0 +1,231 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Modèle de données du canvas.
4
+ *
5
+ * Un `Design` est une planche (artboard) unique contenant une liste plate de
6
+ * noeuds. La liste est ordonnée : l'index = l'ordre d'empilement parmi les
7
+ * noeuds de meme parent (le dernier est au-dessus). C'est `reorder_nodes` — et
8
+ * le panneau de calques de la GUI — qui manipule cet ordre.
9
+ *
10
+ * **Toutes les longueurs sont en pixels** (x, y, width, height, strokeWidth,
11
+ * radius, fontSize, lineHeight), les angles en degres et l'opacite entre 0 et 1.
12
+ *
13
+ * La hierarchie est portee par `parentId` plutot que par de l'imbrication : le
14
+ * stockage reste plat, donc diffable et lisible, tandis que les coordonnees
15
+ * d'un noeud sont exprimees dans l'espace de son parent.
16
+ */
17
+ export declare const NODE_TYPES: readonly ["rect", "ellipse", "line", "text", "group"];
18
+ export declare const NodeTypeSchema: z.ZodEnum<{
19
+ ellipse: "ellipse";
20
+ group: "group";
21
+ line: "line";
22
+ rect: "rect";
23
+ text: "text";
24
+ }>;
25
+ export type NodeType = z.infer<typeof NodeTypeSchema>;
26
+ export declare const TEXT_ALIGNS: readonly ["left", "center", "right"];
27
+ export declare const TextAlignSchema: z.ZodEnum<{
28
+ center: "center";
29
+ left: "left";
30
+ right: "right";
31
+ }>;
32
+ /**
33
+ * Un seul schéma pour tous les types de noeuds : les champs non pertinents pour
34
+ * un type donné sont simplement absents. Un schéma discriminé obligerait l'agent
35
+ * à connaître quatre formes différentes pour un gain nul côté rendu.
36
+ */
37
+ export declare const DesignNodeSchema: z.ZodObject<{
38
+ id: z.ZodString;
39
+ type: z.ZodEnum<{
40
+ ellipse: "ellipse";
41
+ group: "group";
42
+ line: "line";
43
+ rect: "rect";
44
+ text: "text";
45
+ }>;
46
+ name: z.ZodString;
47
+ parentId: z.ZodDefault<z.ZodNullable<z.ZodString>>;
48
+ x: z.ZodNumber;
49
+ y: z.ZodNumber;
50
+ width: z.ZodNumber;
51
+ height: z.ZodNumber;
52
+ rotation: z.ZodDefault<z.ZodNumber>;
53
+ opacity: z.ZodDefault<z.ZodNumber>;
54
+ fill: z.ZodOptional<z.ZodString>;
55
+ stroke: z.ZodOptional<z.ZodString>;
56
+ strokeWidth: z.ZodOptional<z.ZodNumber>;
57
+ radius: z.ZodOptional<z.ZodNumber>;
58
+ text: z.ZodOptional<z.ZodString>;
59
+ color: z.ZodOptional<z.ZodString>;
60
+ fontSize: z.ZodOptional<z.ZodNumber>;
61
+ fontFamily: z.ZodOptional<z.ZodString>;
62
+ fontWeight: z.ZodOptional<z.ZodNumber>;
63
+ fontStyle: z.ZodOptional<z.ZodEnum<{
64
+ italic: "italic";
65
+ normal: "normal";
66
+ }>>;
67
+ textAlign: z.ZodOptional<z.ZodEnum<{
68
+ center: "center";
69
+ left: "left";
70
+ right: "right";
71
+ }>>;
72
+ lineHeight: z.ZodOptional<z.ZodNumber>;
73
+ }, z.core.$strip>;
74
+ export type DesignNode = z.infer<typeof DesignNodeSchema>;
75
+ /** Snapshot servant de référence au calcul de `get_changes`. */
76
+ export declare const BaselineSchema: z.ZodObject<{
77
+ revision: z.ZodNumber;
78
+ at: z.ZodString;
79
+ nodes: z.ZodArray<z.ZodObject<{
80
+ id: z.ZodString;
81
+ type: z.ZodEnum<{
82
+ ellipse: "ellipse";
83
+ group: "group";
84
+ line: "line";
85
+ rect: "rect";
86
+ text: "text";
87
+ }>;
88
+ name: z.ZodString;
89
+ parentId: z.ZodDefault<z.ZodNullable<z.ZodString>>;
90
+ x: z.ZodNumber;
91
+ y: z.ZodNumber;
92
+ width: z.ZodNumber;
93
+ height: z.ZodNumber;
94
+ rotation: z.ZodDefault<z.ZodNumber>;
95
+ opacity: z.ZodDefault<z.ZodNumber>;
96
+ fill: z.ZodOptional<z.ZodString>;
97
+ stroke: z.ZodOptional<z.ZodString>;
98
+ strokeWidth: z.ZodOptional<z.ZodNumber>;
99
+ radius: z.ZodOptional<z.ZodNumber>;
100
+ text: z.ZodOptional<z.ZodString>;
101
+ color: z.ZodOptional<z.ZodString>;
102
+ fontSize: z.ZodOptional<z.ZodNumber>;
103
+ fontFamily: z.ZodOptional<z.ZodString>;
104
+ fontWeight: z.ZodOptional<z.ZodNumber>;
105
+ fontStyle: z.ZodOptional<z.ZodEnum<{
106
+ italic: "italic";
107
+ normal: "normal";
108
+ }>>;
109
+ textAlign: z.ZodOptional<z.ZodEnum<{
110
+ center: "center";
111
+ left: "left";
112
+ right: "right";
113
+ }>>;
114
+ lineHeight: z.ZodOptional<z.ZodNumber>;
115
+ }, z.core.$strip>>;
116
+ }, z.core.$strip>;
117
+ export type Baseline = z.infer<typeof BaselineSchema>;
118
+ export declare const DesignSchema: z.ZodObject<{
119
+ id: z.ZodString;
120
+ name: z.ZodString;
121
+ width: z.ZodNumber;
122
+ height: z.ZodNumber;
123
+ background: z.ZodString;
124
+ nodes: z.ZodArray<z.ZodObject<{
125
+ id: z.ZodString;
126
+ type: z.ZodEnum<{
127
+ ellipse: "ellipse";
128
+ group: "group";
129
+ line: "line";
130
+ rect: "rect";
131
+ text: "text";
132
+ }>;
133
+ name: z.ZodString;
134
+ parentId: z.ZodDefault<z.ZodNullable<z.ZodString>>;
135
+ x: z.ZodNumber;
136
+ y: z.ZodNumber;
137
+ width: z.ZodNumber;
138
+ height: z.ZodNumber;
139
+ rotation: z.ZodDefault<z.ZodNumber>;
140
+ opacity: z.ZodDefault<z.ZodNumber>;
141
+ fill: z.ZodOptional<z.ZodString>;
142
+ stroke: z.ZodOptional<z.ZodString>;
143
+ strokeWidth: z.ZodOptional<z.ZodNumber>;
144
+ radius: z.ZodOptional<z.ZodNumber>;
145
+ text: z.ZodOptional<z.ZodString>;
146
+ color: z.ZodOptional<z.ZodString>;
147
+ fontSize: z.ZodOptional<z.ZodNumber>;
148
+ fontFamily: z.ZodOptional<z.ZodString>;
149
+ fontWeight: z.ZodOptional<z.ZodNumber>;
150
+ fontStyle: z.ZodOptional<z.ZodEnum<{
151
+ italic: "italic";
152
+ normal: "normal";
153
+ }>>;
154
+ textAlign: z.ZodOptional<z.ZodEnum<{
155
+ center: "center";
156
+ left: "left";
157
+ right: "right";
158
+ }>>;
159
+ lineHeight: z.ZodOptional<z.ZodNumber>;
160
+ }, z.core.$strip>>;
161
+ createdAt: z.ZodString;
162
+ updatedAt: z.ZodString;
163
+ revision: z.ZodNumber;
164
+ lastEditor: z.ZodEnum<{
165
+ agent: "agent";
166
+ user: "user";
167
+ }>;
168
+ baseline: z.ZodObject<{
169
+ revision: z.ZodNumber;
170
+ at: z.ZodString;
171
+ nodes: z.ZodArray<z.ZodObject<{
172
+ id: z.ZodString;
173
+ type: z.ZodEnum<{
174
+ ellipse: "ellipse";
175
+ group: "group";
176
+ line: "line";
177
+ rect: "rect";
178
+ text: "text";
179
+ }>;
180
+ name: z.ZodString;
181
+ parentId: z.ZodDefault<z.ZodNullable<z.ZodString>>;
182
+ x: z.ZodNumber;
183
+ y: z.ZodNumber;
184
+ width: z.ZodNumber;
185
+ height: z.ZodNumber;
186
+ rotation: z.ZodDefault<z.ZodNumber>;
187
+ opacity: z.ZodDefault<z.ZodNumber>;
188
+ fill: z.ZodOptional<z.ZodString>;
189
+ stroke: z.ZodOptional<z.ZodString>;
190
+ strokeWidth: z.ZodOptional<z.ZodNumber>;
191
+ radius: z.ZodOptional<z.ZodNumber>;
192
+ text: z.ZodOptional<z.ZodString>;
193
+ color: z.ZodOptional<z.ZodString>;
194
+ fontSize: z.ZodOptional<z.ZodNumber>;
195
+ fontFamily: z.ZodOptional<z.ZodString>;
196
+ fontWeight: z.ZodOptional<z.ZodNumber>;
197
+ fontStyle: z.ZodOptional<z.ZodEnum<{
198
+ italic: "italic";
199
+ normal: "normal";
200
+ }>>;
201
+ textAlign: z.ZodOptional<z.ZodEnum<{
202
+ center: "center";
203
+ left: "left";
204
+ right: "right";
205
+ }>>;
206
+ lineHeight: z.ZodOptional<z.ZodNumber>;
207
+ }, z.core.$strip>>;
208
+ }, z.core.$strip>;
209
+ }, z.core.$strip>;
210
+ export type Design = z.infer<typeof DesignSchema>;
211
+ export interface DesignSummary {
212
+ id: string;
213
+ name: string;
214
+ width: number;
215
+ height: number;
216
+ nodeCount: number;
217
+ updatedAt: string;
218
+ revision: number;
219
+ lastEditor: "agent" | "user";
220
+ /** Nombre de noeuds ajoutés/modifiés/supprimés depuis le dernier mark_synced. */
221
+ pendingChanges: number;
222
+ }
223
+ /** Enveloppe IPC commune Master <-> GUI Worker (même contrat que les autres MCPs). */
224
+ export declare const IPCMessageSchema: z.ZodObject<{
225
+ type: z.ZodString;
226
+ correlationId: z.ZodOptional<z.ZodString>;
227
+ data: z.ZodOptional<z.ZodAny>;
228
+ error: z.ZodOptional<z.ZodString>;
229
+ timestamp: z.ZodString;
230
+ }, z.core.$strip>;
231
+ export type IPCMessage = z.infer<typeof IPCMessageSchema>;
@@ -0,0 +1,98 @@
1
+ import { z } from "zod";
2
+ import { MIN_LINE_HEIGHT_PX } from "../render.js";
3
+ import { NODE_FIELD_DOCS } from "../node-fields.js";
4
+ /**
5
+ * Modèle de données du canvas.
6
+ *
7
+ * Un `Design` est une planche (artboard) unique contenant une liste plate de
8
+ * noeuds. La liste est ordonnée : l'index = l'ordre d'empilement parmi les
9
+ * noeuds de meme parent (le dernier est au-dessus). C'est `reorder_nodes` — et
10
+ * le panneau de calques de la GUI — qui manipule cet ordre.
11
+ *
12
+ * **Toutes les longueurs sont en pixels** (x, y, width, height, strokeWidth,
13
+ * radius, fontSize, lineHeight), les angles en degres et l'opacite entre 0 et 1.
14
+ *
15
+ * La hierarchie est portee par `parentId` plutot que par de l'imbrication : le
16
+ * stockage reste plat, donc diffable et lisible, tandis que les coordonnees
17
+ * d'un noeud sont exprimees dans l'espace de son parent.
18
+ */
19
+ export const NODE_TYPES = ["rect", "ellipse", "line", "text", "group"];
20
+ export const NodeTypeSchema = z.enum(NODE_TYPES);
21
+ export const TEXT_ALIGNS = ["left", "center", "right"];
22
+ export const TextAlignSchema = z.enum(TEXT_ALIGNS);
23
+ /**
24
+ * Un seul schéma pour tous les types de noeuds : les champs non pertinents pour
25
+ * un type donné sont simplement absents. Un schéma discriminé obligerait l'agent
26
+ * à connaître quatre formes différentes pour un gain nul côté rendu.
27
+ */
28
+ export const DesignNodeSchema = z.object({
29
+ id: z.string().describe(NODE_FIELD_DOCS.id),
30
+ type: NodeTypeSchema.describe(NODE_FIELD_DOCS.type),
31
+ name: z.string().describe(NODE_FIELD_DOCS.name),
32
+ /** Groupe conteneur, ou null a la racine de la planche. */
33
+ parentId: z.string().nullable().default(null).describe(NODE_FIELD_DOCS.parentId),
34
+ // Boîte englobante, en PIXELS, exprimée dans l'espace du parent. Pour `line`, le trait va de (x, y) à
35
+ // (x + width, y + height), ce qui autorise des largeurs/hauteurs négatives sur ce type uniquement.
36
+ x: z.number().describe(NODE_FIELD_DOCS.x),
37
+ y: z.number().describe(NODE_FIELD_DOCS.y),
38
+ width: z.number().describe(NODE_FIELD_DOCS.width),
39
+ height: z.number().describe(NODE_FIELD_DOCS.height),
40
+ /** Degrés, sens horaire, autour du centre de la boîte. */
41
+ rotation: z.number().default(0).describe(NODE_FIELD_DOCS.rotation),
42
+ opacity: z.number().min(0).max(1).default(1).describe(NODE_FIELD_DOCS.opacity),
43
+ // Formes — couleurs CSS, épaisseurs et rayons en pixels.
44
+ fill: z.string().describe(NODE_FIELD_DOCS.fill).optional(),
45
+ stroke: z.string().describe(NODE_FIELD_DOCS.stroke).optional(),
46
+ strokeWidth: z.number().min(0).describe(NODE_FIELD_DOCS.strokeWidth).optional(),
47
+ radius: z.number().min(0).describe(NODE_FIELD_DOCS.radius).optional(),
48
+ // Texte — fontSize et lineHeight sont tous deux en PIXELS.
49
+ text: z.string().describe(NODE_FIELD_DOCS.text).optional(),
50
+ color: z.string().describe(NODE_FIELD_DOCS.color).optional(),
51
+ fontSize: z.number().positive().describe(NODE_FIELD_DOCS.fontSize).optional(),
52
+ fontFamily: z.string().describe(NODE_FIELD_DOCS.fontFamily).optional(),
53
+ fontWeight: z.number().int().min(100).max(900).describe(NODE_FIELD_DOCS.fontWeight).optional(),
54
+ fontStyle: z.enum(["normal", "italic"]).describe(NODE_FIELD_DOCS.fontStyle).optional(),
55
+ textAlign: TextAlignSchema.describe(NODE_FIELD_DOCS.textAlign).optional(),
56
+ /**
57
+ * Distance entre deux lignes de base, **en pixels** — jamais un multiplicateur.
58
+ * Le plancher rejette a l'ecriture les valeurs en ratio (1.2, 1.35...) plutot
59
+ * que de rendre un paragraphe ecrase ; les anciens fichiers, eux, sont
60
+ * convertis a la lecture par src/core/migrate.ts.
61
+ */
62
+ lineHeight: z
63
+ .number()
64
+ .min(MIN_LINE_HEIGHT_PX, `lineHeight is a distance in PIXELS between baselines, not a multiplier of fontSize. ` +
65
+ `A value under ${MIN_LINE_HEIGHT_PX} is almost certainly a ratio: with fontSize 24 use about 32, ` +
66
+ `with fontSize 64 use about 86. Omit the field to get fontSize x 1.35 automatically.`)
67
+ .describe(NODE_FIELD_DOCS.lineHeight)
68
+ .optional(),
69
+ });
70
+ /** Snapshot servant de référence au calcul de `get_changes`. */
71
+ export const BaselineSchema = z.object({
72
+ revision: z.number().int().nonnegative(),
73
+ at: z.string(),
74
+ nodes: z.array(DesignNodeSchema),
75
+ });
76
+ export const DesignSchema = z.object({
77
+ id: z.string(),
78
+ name: z.string(),
79
+ width: z.number().positive(),
80
+ height: z.number().positive(),
81
+ background: z.string(),
82
+ nodes: z.array(DesignNodeSchema),
83
+ createdAt: z.string(),
84
+ updatedAt: z.string(),
85
+ /** Incrémentée à chaque mutation, quelle qu'en soit l'origine. */
86
+ revision: z.number().int().nonnegative(),
87
+ /** Qui a produit la dernière mutation — sert à expliquer un diff. */
88
+ lastEditor: z.enum(["agent", "user"]),
89
+ baseline: BaselineSchema,
90
+ });
91
+ /** Enveloppe IPC commune Master <-> GUI Worker (même contrat que les autres MCPs). */
92
+ export const IPCMessageSchema = z.object({
93
+ type: z.string(),
94
+ correlationId: z.string().optional(),
95
+ data: z.any().optional(),
96
+ error: z.string().optional(),
97
+ timestamp: z.string(),
98
+ });
@@ -0,0 +1,93 @@
1
+ import type { RenderNode } from "./render.js";
2
+ /**
3
+ * Hiérarchie des éléments.
4
+ *
5
+ * Le stockage reste une liste plate — c'est ce qui garde le diff lisible et le
6
+ * fichier JSON diffable — mais chaque élément porte un `parentId`. Les
7
+ * coordonnées d'un élément sont exprimées **dans l'espace de son parent** :
8
+ * déplacer un groupe ne change donc qu'un seul champ, et l'export HTML devient
9
+ * une simple imbrication de div positionnées.
10
+ *
11
+ * Ce module ne dépend d'aucune librairie : il est importé aussi bien par le
12
+ * serveur MCP que par le canvas dans le navigateur.
13
+ */
14
+ export interface Box {
15
+ x: number;
16
+ y: number;
17
+ width: number;
18
+ height: number;
19
+ }
20
+ export interface Point {
21
+ x: number;
22
+ y: number;
23
+ }
24
+ /** Boîte englobante normalisée (largeur/hauteur toujours positives). */
25
+ export declare function normalizedBox(node: {
26
+ x: number;
27
+ y: number;
28
+ width: number;
29
+ height: number;
30
+ }): Box;
31
+ export interface HierarchyNode {
32
+ id: string;
33
+ parentId: string | null;
34
+ }
35
+ /** Enfants directs de chaque parent, dans l'ordre du tableau (donc l'ordre d'empilement). */
36
+ export declare function childrenByParent<T extends HierarchyNode>(nodes: T[]): Map<string | null, T[]>;
37
+ export declare function rootNodes<T extends HierarchyNode>(nodes: T[]): T[];
38
+ /** Chaîne des ancêtres, du parent direct jusqu'à la racine. */
39
+ export declare function ancestorsOf<T extends HierarchyNode>(nodes: T[], id: string): T[];
40
+ /** Ancêtre le plus haut d'un élément, ou l'élément lui-même s'il est à la racine. */
41
+ export declare function outermostAncestor<T extends HierarchyNode>(nodes: T[], id: string): T | undefined;
42
+ /** Identifiants de tous les descendants d'un élément (lui-même exclu). */
43
+ export declare function descendantIds<T extends HierarchyNode>(nodes: T[], id: string): Set<string>;
44
+ /**
45
+ * Réécrit la liste à plat en parcours préfixe : chaque parent est suivi de son
46
+ * sous-arbre. L'ordre relatif entre frères — donc l'empilement — est préservé.
47
+ *
48
+ * Le stockage garde ainsi une forme unique pour un même arbre, ce qui évite au
49
+ * diff de signaler un réordonnancement là où rien n'a bougé visuellement.
50
+ */
51
+ export declare function flattenPreOrder<T extends HierarchyNode>(nodes: T[]): T[];
52
+ export interface Layout {
53
+ /** Position, en coordonnées document, de l'origine de l'espace d'un groupe. */
54
+ origin: Map<string, Point>;
55
+ /** Boîte englobante, en coordonnées document, de chaque élément. */
56
+ bounds: Map<string, Box>;
57
+ }
58
+ /**
59
+ * Résout toute la hiérarchie en coordonnées document, en une passe.
60
+ *
61
+ * La rotation n'entre pas dans le calcul des boîtes : comme pour un rectangle
62
+ * pivoté, la boîte d'un groupe pivoté reste sa boîte non pivotée. C'est ce que
63
+ * le cadre de sélection affiche déjà, et le rendu, lui, applique bien la rotation.
64
+ */
65
+ export declare function computeLayout(nodes: RenderNode[]): Layout;
66
+ /** Origine de l'espace de coordonnées dans lequel vit un élément. */
67
+ export declare function spaceOriginOf(layout: Layout, parentId: string | null): Point;
68
+ export declare function boundsOf(layout: Layout, id: string): Box;
69
+ /** Boîte englobante commune à plusieurs éléments, en coordonnées document. */
70
+ export declare function unionBoundsOf(layout: Layout, ids: string[]): Box | null;
71
+ export declare const REORDER_DIRECTIONS: readonly ["front", "back", "forward", "backward"];
72
+ export type ReorderDirection = (typeof REORDER_DIRECTIONS)[number];
73
+ /**
74
+ * Retire d'une sélection les éléments dont un ancêtre est déjà sélectionné.
75
+ *
76
+ * Déplacer un enfant en même temps que son groupe n'a pas de sens : le groupe
77
+ * emporte déjà son contenu, et les deux gestes s'appliqueraient dans des
78
+ * fratries différentes.
79
+ */
80
+ export declare function withoutRedundantDescendants<T extends HierarchyNode>(nodes: T[], ids: string[]): string[];
81
+ /**
82
+ * Change l'ordre d'empilement d'un ou plusieurs éléments.
83
+ *
84
+ * L'empilement est **relatif à la fratrie** : un élément ne peut passer devant
85
+ * que ses frères, jamais devant le contenu d'un autre groupe — c'est le groupe
86
+ * lui-même qu'il faut alors déplacer. Chaque fratrie concernée est réordonnée
87
+ * indépendamment, puis la liste plate est reconstruite en parcours préfixe.
88
+ *
89
+ * `forward`/`backward` décalent d'un cran ; `front`/`back` vont au bout. Un
90
+ * élément déjà au bout de sa fratrie ne bouge pas, et un lot d'éléments garde
91
+ * son ordre relatif d'origine.
92
+ */
93
+ export declare function reorderNodes<T extends HierarchyNode>(nodes: T[], ids: string[], direction: ReorderDirection): T[];