@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.
- package/README.md +231 -0
- package/dist/shared/node-defaults.d.ts +30 -0
- package/dist/shared/node-defaults.js +67 -0
- package/dist/shared/node-fields.d.ts +33 -0
- package/dist/shared/node-fields.js +40 -0
- package/dist/shared/render.d.ts +105 -0
- package/dist/shared/render.js +215 -0
- package/dist/shared/schemas/design.schema.d.ts +231 -0
- package/dist/shared/schemas/design.schema.js +98 -0
- package/dist/shared/tree.d.ts +93 -0
- package/dist/shared/tree.js +223 -0
- package/dist/src/cli/setup-mcp.d.ts +5 -0
- package/dist/src/cli/setup-mcp.js +87 -0
- package/dist/src/core/design-store.d.ts +61 -0
- package/dist/src/core/design-store.js +294 -0
- package/dist/src/core/diff.d.ts +22 -0
- package/dist/src/core/diff.js +97 -0
- package/dist/src/core/export.d.ts +6 -0
- package/dist/src/core/export.js +251 -0
- package/dist/src/core/hierarchy.d.ts +18 -0
- package/dist/src/core/hierarchy.js +68 -0
- package/dist/src/core/ids.d.ts +4 -0
- package/dist/src/core/ids.js +21 -0
- package/dist/src/core/log-dir.d.ts +6 -0
- package/dist/src/core/log-dir.js +12 -0
- package/dist/src/core/migrate.d.ts +18 -0
- package/dist/src/core/migrate.js +54 -0
- package/dist/src/core/node-factory.d.ts +19 -0
- package/dist/src/core/node-factory.js +103 -0
- package/dist/src/core/screenshot.d.ts +31 -0
- package/dist/src/core/screenshot.js +53 -0
- package/dist/src/gui-worker.d.ts +1 -0
- package/dist/src/gui-worker.js +334 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +673 -0
- package/dist/src/tools/add-nodes.d.ts +7 -0
- package/dist/src/tools/add-nodes.js +26 -0
- package/dist/src/tools/create-design.d.ts +7 -0
- package/dist/src/tools/create-design.js +15 -0
- package/dist/src/tools/delete-design.d.ts +3 -0
- package/dist/src/tools/delete-design.js +7 -0
- package/dist/src/tools/delete-nodes.d.ts +3 -0
- package/dist/src/tools/delete-nodes.js +29 -0
- package/dist/src/tools/export-design.d.ts +3 -0
- package/dist/src/tools/export-design.js +21 -0
- package/dist/src/tools/get-changes.d.ts +11 -0
- package/dist/src/tools/get-changes.js +27 -0
- package/dist/src/tools/get-design.d.ts +7 -0
- package/dist/src/tools/get-design.js +29 -0
- package/dist/src/tools/group-nodes.d.ts +10 -0
- package/dist/src/tools/group-nodes.js +83 -0
- package/dist/src/tools/list-designs.d.ts +3 -0
- package/dist/src/tools/list-designs.js +13 -0
- package/dist/src/tools/mark-synced.d.ts +7 -0
- package/dist/src/tools/mark-synced.js +19 -0
- package/dist/src/tools/reorder-nodes.d.ts +7 -0
- package/dist/src/tools/reorder-nodes.js +60 -0
- package/dist/src/tools/screenshot-design.d.ts +3 -0
- package/dist/src/tools/screenshot-design.js +36 -0
- package/dist/src/tools/types.d.ts +25 -0
- package/dist/src/tools/types.js +13 -0
- package/dist/src/tools/ungroup-nodes.d.ts +7 -0
- package/dist/src/tools/ungroup-nodes.js +64 -0
- package/dist/src/tools/update-design.d.ts +4 -0
- package/dist/src/tools/update-design.js +33 -0
- package/dist/src/tools/update-nodes.d.ts +3 -0
- package/dist/src/tools/update-nodes.js +35 -0
- package/dist/src/version.d.ts +1 -0
- package/dist/src/version.js +2 -0
- package/package.json +58 -0
- 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, "<")
|
|
17
|
+
.replace(/>/g, ">")
|
|
18
|
+
.replace(/"/g, """)
|
|
19
|
+
.replace(/'/g, "'");
|
|
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[];
|