@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
package/README.md
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# @imenam/mcp-design
|
|
2
|
+
|
|
3
|
+
Un canvas de design pilotable par un agent, avec une interface graphique où l'utilisateur reprend la main.
|
|
4
|
+
|
|
5
|
+
L'agent compose une planche (formes, texte) via des outils MCP. L'utilisateur ouvre la GUI, déplace, retaille, recolore, réécrit. L'agent relit ensuite **exactement ce qui a changé** et l'intègre dans le code de l'application.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Agent ──add_nodes/update_nodes──▶ Design ◀──glisser-déposer── Utilisateur (GUI)
|
|
9
|
+
▲ │
|
|
10
|
+
└──────────── get_changes ──── diff depuis la dernière baseline ──────┘
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install -g @imenam/mcp-design
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Ou directement via `npx`, sans installation.
|
|
22
|
+
|
|
23
|
+
### Déclaration dans un projet
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx -y @imenam/mcp-design --claude-setup-mcp
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
La commande crée (ou complète) le `.mcp.json` du répertoire courant avec une entrée `mcp-design` dont les variables sont pré-remplies. Ajoutez `--force` pour remplacer une entrée existante ; les autres serveurs sont préservés.
|
|
30
|
+
|
|
31
|
+
Exemple d'entrée générée :
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"mcpServers": {
|
|
36
|
+
"mcp-design": {
|
|
37
|
+
"command": "npx",
|
|
38
|
+
"args": ["-y", "@imenam/mcp-design"],
|
|
39
|
+
"env": {
|
|
40
|
+
"PROXY_URL": "http://localhost:3000",
|
|
41
|
+
"APP_GROUP": "Mon Projet",
|
|
42
|
+
"APP_NAME": "Design Mon Projet",
|
|
43
|
+
"APP_PATH": "/design-mon-projet",
|
|
44
|
+
"MCP_DESIGN_DATA_DIR": "C:/dev/mon-projet/.design-data",
|
|
45
|
+
"MCP_LOG_DIR": ""
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Variables d'environnement
|
|
53
|
+
|
|
54
|
+
| Variable | Rôle |
|
|
55
|
+
|---|---|
|
|
56
|
+
| `PROXY_URL` | URL du [proxy central](https://github.com/imenam/proxy-setup). **Sans elle, la GUI est simplement désactivée** — les outils MCP continuent de fonctionner. |
|
|
57
|
+
| `APP_PATH` | Chemin de montage de la GUI derrière le proxy (défaut `/design-gui`). |
|
|
58
|
+
| `APP_NAME` | Nom affiché dans le dashboard du proxy. |
|
|
59
|
+
| `APP_GROUP` | Section repliable du dashboard du proxy (facultatif). |
|
|
60
|
+
| `MCP_DESIGN_DATA_DIR` | Répertoire de stockage des designs. `MCP_DATA_DIR` est accepté en repli. Défaut : `<package>/.design-data`. |
|
|
61
|
+
| `MCP_LOG_DIR` | Répertoire des logs. Défaut : `<data>/logs`. |
|
|
62
|
+
|
|
63
|
+
Un design est un simple fichier JSON dans `<data>/designs/<id>.json` — lisible, diffable, versionnable.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Les outils
|
|
68
|
+
|
|
69
|
+
### Planches
|
|
70
|
+
|
|
71
|
+
| Outil | Effet |
|
|
72
|
+
|---|---|
|
|
73
|
+
| `list_designs` | Liste les planches, leur taille, leur nombre d'éléments et le nombre de modifications non encore récupérées. |
|
|
74
|
+
| `create_design` | Crée une planche vide (`name`, et facultativement `width`, `height`, `background`). |
|
|
75
|
+
| `get_design` | Lit une planche : `format: "summary"` (inventaire compact, défaut) ou `format: "json"` (valeurs exactes). |
|
|
76
|
+
| `update_design` | Renomme la planche, change ses dimensions ou son fond. |
|
|
77
|
+
| `delete_design` | Supprime définitivement une planche. |
|
|
78
|
+
|
|
79
|
+
### Éléments
|
|
80
|
+
|
|
81
|
+
| Outil | Effet |
|
|
82
|
+
|---|---|
|
|
83
|
+
| `add_nodes` | Ajoute un ou plusieurs éléments par-dessus les autres. |
|
|
84
|
+
| `update_nodes` | Patche des éléments existants — seuls les champs fournis changent. |
|
|
85
|
+
| `delete_nodes` | Supprime des éléments par identifiant. Supprimer un groupe emporte son contenu. |
|
|
86
|
+
| `group_nodes` | Enveloppe des éléments dans un nouveau groupe, sans rien déplacer à l'écran. |
|
|
87
|
+
| `ungroup_nodes` | Dissout des groupes : leur contenu remonte d'un niveau, toujours sans bouger. |
|
|
88
|
+
|
|
89
|
+
Cinq types d'éléments : `rect`, `ellipse`, `line`, `text` et `group`. Seul `type` est obligatoire ; tout le reste tombe sur des valeurs par défaut visibles, donc `{ "type": "rect" }` suffit à créer quelque chose qu'on voit à l'écran.
|
|
90
|
+
|
|
91
|
+
**Champs communs** — `name`, `parentId`, `x`, `y`, `width`, `height` (en pixels, origine en haut à gauche), `rotation` (degrés, autour du centre de la boîte), `opacity` (0–1).
|
|
92
|
+
|
|
93
|
+
**Toute longueur est en pixels**, sans exception : `x`, `y`, `width`, `height`, `strokeWidth`, `radius`, `fontSize` et `lineHeight`. Chaque champ porte sa description et son unité dans [`shared/node-fields.ts`](shared/node-fields.ts), d'où le schéma zod **et** l'`inputSchema` MCP les tirent tous les deux — une unité n'est écrite qu'à un seul endroit.
|
|
94
|
+
|
|
95
|
+
| Type | Champs propres |
|
|
96
|
+
|---|---|
|
|
97
|
+
| `rect` | `fill`, `stroke`, `strokeWidth`, `radius` |
|
|
98
|
+
| `ellipse` | `fill`, `stroke`, `strokeWidth` |
|
|
99
|
+
| `line` | `stroke`, `strokeWidth` — le trait va de `(x, y)` à `(x + width, y + height)`, donc les deltas négatifs encodent la direction |
|
|
100
|
+
| `text` | `text`, `color`, `fontSize`, `fontFamily`, `fontWeight`, `fontStyle`, `textAlign`, `lineHeight` (en px) |
|
|
101
|
+
| `group` | aucun — c'est un pur conteneur |
|
|
102
|
+
|
|
103
|
+
### Hiérarchie
|
|
104
|
+
|
|
105
|
+
Chaque élément porte un `parentId` : `null` à la racine de la planche, sinon l'identifiant d'un `group`. **Les coordonnées sont exprimées dans l'espace du parent** : un élément à la racine se positionne par rapport à la planche, un élément dans un groupe par rapport à l'origine de ce groupe.
|
|
106
|
+
|
|
107
|
+
C'est ce qui rend un déplacement de groupe lisible côté agent — un seul champ change, pas les coordonnées de chaque membre :
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
- g_a1b2c3 — "Carte produit" (group)
|
|
111
|
+
x: 100 -> 260
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
La taille d'un groupe est **déduite de son contenu** : `width` et `height` sont recalculés à chaque mutation, ne peuvent pas être écrits, et n'apparaissent jamais dans un diff. Le stockage reste une liste plate, ordonnée en parcours préfixe — chaque parent suivi de son sous-arbre — ce qui garde le fichier JSON lisible et diffable.
|
|
115
|
+
|
|
116
|
+
Le texte n'est **jamais re-wrappé automatiquement** : seuls les `\n` explicites créent une nouvelle ligne. Le comportement est ainsi identique à l'écran et à l'export.
|
|
117
|
+
|
|
118
|
+
`lineHeight` est une **distance en pixels entre deux lignes de base**, pas un multiplicateur de `fontSize` : avec `fontSize: 64`, on écrit `76`, pas `1.2`. C'est l'unité que les agents emploient spontanément, et la seule qui se lise dans la même échelle que le reste. Une valeur sous 4 est donc refusée à l'écriture avec un message explicite, et les fichiers écrits avant ce changement sont convertis à la lecture (`ratio x fontSize`) par [`src/core/migrate.ts`](src/core/migrate.ts). Champ omis = `fontSize x 1.35`.
|
|
119
|
+
|
|
120
|
+
### Ordre d'empilement
|
|
121
|
+
|
|
122
|
+
L'index dans la liste **est** l'ordre d'empilement : le dernier élément d'une fratrie est celui du dessus. `add_nodes` empile au-dessus, et `reorder_nodes` déplace ensuite ce qui existe déjà.
|
|
123
|
+
|
|
124
|
+
| Outil | Effet |
|
|
125
|
+
|---|---|
|
|
126
|
+
| `reorder_nodes` | `position: front / back / forward / backward` sur un ou plusieurs éléments. Un groupe emporte son contenu ; plusieurs éléments gardent leur ordre relatif. |
|
|
127
|
+
|
|
128
|
+
L'empilement est **relatif à la fratrie** : un élément ne passe jamais devant le contenu d'un autre groupe — c'est ce groupe qu'il faut alors réordonner, ou l'élément qu'il faut en sortir via `parentId`. La logique vit dans [`shared/tree.ts`](shared/tree.ts) et sert aussi bien l'outil que le panneau de calques de la GUI : les deux ne peuvent pas diverger.
|
|
129
|
+
|
|
130
|
+
Un lot est intégralement validé avant la moindre écriture : une entrée invalide ne laisse jamais un design à moitié modifié.
|
|
131
|
+
|
|
132
|
+
### Récupérer le travail de l'utilisateur
|
|
133
|
+
|
|
134
|
+
| Outil | Effet |
|
|
135
|
+
|---|---|
|
|
136
|
+
| `get_changes` | Liste tout ce qui a été ajouté, modifié, supprimé ou réordonné depuis la dernière baseline, champ par champ (`x: 40 -> 240`). |
|
|
137
|
+
| `mark_synced` | Fige l'état courant comme nouvelle baseline. |
|
|
138
|
+
| `export_design` | Rend la planche en `html`, `jsx`, `svg`, `json` ou `summary`. `output_path` écrit dans un fichier au lieu de renvoyer le contenu. |
|
|
139
|
+
| `screenshot_design` | Rasterise la planche en PNG et le renvoie **en image** à l'agent. `node_id` cadre la capture sur un groupe (ou n'importe quel élément), `scale` zoome, `padding` ajoute de la marge, `output_path` écrit un `.png` au lieu de renvoyer l'image. |
|
|
140
|
+
|
|
141
|
+
**`get_changes` n'acquitte jamais ce qu'il montre.** Seul `mark_synced` déplace la baseline, et il ne doit être appelé qu'une fois les modifications réellement reportées dans le code. Un simple appel de lecture ne peut donc pas faire disparaître un diff que l'agent n'a pas encore traité.
|
|
142
|
+
|
|
143
|
+
### GUI
|
|
144
|
+
|
|
145
|
+
| Outil | Effet |
|
|
146
|
+
|---|---|
|
|
147
|
+
| `reconnect_gui` | Relance le worker GUI et le réenregistre auprès du proxy. `force: true` redémarre même s'il semble vivant. |
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Boucle de travail type
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
create_design { name: "Landing" }
|
|
155
|
+
add_nodes { design_id: "landing", nodes: [...] }
|
|
156
|
+
mark_synced { design_id: "landing" } ← point de départ de la comparaison
|
|
157
|
+
|
|
158
|
+
… l'utilisateur ouvre la GUI et retouche la maquette …
|
|
159
|
+
|
|
160
|
+
get_changes { design_id: "landing" } ← ce qu'il a changé, champ par champ
|
|
161
|
+
screenshot_design { design_id: "landing" } ← ce que ça donne réellement, en image
|
|
162
|
+
export_design { design_id: "landing", format: "html" }
|
|
163
|
+
… l'agent reporte les valeurs dans le code de l'application …
|
|
164
|
+
mark_synced { design_id: "landing" } ← acquittement
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## L'interface
|
|
170
|
+
|
|
171
|
+
Trois colonnes : la liste des planches, le canvas, l'inspecteur et les calques.
|
|
172
|
+
|
|
173
|
+
- **Outils** — sélection, rectangle, ellipse, ligne, texte. Glisser pour dimensionner, cliquer pour poser à la taille par défaut.
|
|
174
|
+
- **Édition** — déplacement au glisser, 8 poignées de redimensionnement (2 extrémités pour une ligne), sélection multiple au rectangle d'encadrement ou au Maj+clic.
|
|
175
|
+
- **Groupes** — `Ctrl+G` groupe la sélection, `Ctrl+Maj+G` la dissout. Un clic dans le canvas sélectionne le **groupe le plus haut** ; `Alt+clic` vise l'élément exact à l'intérieur. Déplacer un groupe déplace tout son contenu.
|
|
176
|
+
- **Texte** — double-clic pour éditer en place, avec la même métrique qu'au rendu final.
|
|
177
|
+
- **Inspecteur** — position, taille, rotation, opacité, couleurs, épaisseur, arrondi, contenu, taille et graisse de police, interligne (en px), alignement. Pour un groupe, la taille est affichée en lecture seule puisqu'elle suit son contenu.
|
|
178
|
+
- **Calques** — arbre repliable, ordre d'empilement, sélection, montée/descente d'un cran.
|
|
179
|
+
|
|
180
|
+
### Naviguer dans la planche
|
|
181
|
+
|
|
182
|
+
- **Zoom** : `Ctrl`/`Cmd` + molette, ancré sous le curseur — le point visé ne bouge pas.
|
|
183
|
+
- **Déplacement** : molette seule, ou **barre espace maintenue + glisser**. Tant que la barre espace est enfoncée, aucun geste ne peut déplacer un élément, même en partant de l'un d'eux. Le clic molette fait la même chose.
|
|
184
|
+
- Le sélecteur de zoom de la barre d'outils reste disponible pour revenir à une valeur ronde.
|
|
185
|
+
|
|
186
|
+
Raccourcis : `V` `R` `O` `L` `T` (outils), `Ctrl+Z` / `Ctrl+Y` (annuler/rétablir), `Ctrl+G` / `Ctrl+Maj+G` (grouper / dissoudre), `Ctrl+D` (dupliquer), `Ctrl+A` (tout sélectionner), `Suppr`, flèches (déplacer d'1 px, 10 px avec `Maj`), `Échap`.
|
|
187
|
+
|
|
188
|
+
La sauvegarde est automatique et différée. Une modification faite par l'agent pendant que la GUI est ouverte y apparaît en direct, via SSE ; une modification faite dans la GUI est enregistrée comme venant de l'utilisateur, et c'est ce que `get_changes` restitue.
|
|
189
|
+
|
|
190
|
+
Le badge « n à synchroniser » d'une planche indique le nombre de modifications que l'agent n'a pas encore acquittées.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Architecture
|
|
195
|
+
|
|
196
|
+
Deux processus, conformément au standard de l'écosystème (`@imenam/mcp-gui-interface`) :
|
|
197
|
+
|
|
198
|
+
- **MCP Master** (`src/index.ts`) — protocole JSON-RPC sur stdio, stockage des designs, cycle de vie du worker via `GuiLauncher`.
|
|
199
|
+
- **GUI Worker** (`src/gui-worker.ts`) — serveur Hono, enregistrement auprès du proxy via `ProxyClient`, API REST + SSE, sert la SPA React.
|
|
200
|
+
|
|
201
|
+
Les deux communiquent par IPC. Le worker se termine proprement quand le master disparaît (canal IPC fermé, PID parent absent) et quand une autre instance est déjà enregistrée (HTTP 409), ce qui évite les GUI orphelines.
|
|
202
|
+
|
|
203
|
+
`shared/render.ts` contient le rendu SVG d'un élément, `shared/tree.ts` la résolution de la hiérarchie en coordonnées document, et `shared/node-defaults.ts` les valeurs par défaut. Les trois sont importés **à la fois** par l'exporteur côté serveur et par le canvas côté navigateur : ce que l'utilisateur voit est exactement ce que `export_design` — et donc `screenshot_design`, qui rasterise ce même SVG via resvg — produit.
|
|
204
|
+
|
|
205
|
+
Les invariants de hiérarchie — parent existant et de type `group`, absence de cycle, taille des groupes recalculée, ordre préfixe — sont appliqués dans `DesignStore.mutate`, donc sur **tout** chemin d'écriture : outil MCP comme sauvegarde depuis la GUI.
|
|
206
|
+
|
|
207
|
+
**Limite assumée de cette version** : un groupe se déplace, se pivote et change d'opacité, mais ne se redimensionne pas — mettre son contenu à l'échelle demanderait de propager un facteur dans tout le sous-arbre, ce qui dépasse le cadre d'un premier jet sobre.
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Développement
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
npm install
|
|
215
|
+
npm run build # backend (tsc) + GUI (vite)
|
|
216
|
+
npm test # suite complète
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
La suite comprend :
|
|
220
|
+
|
|
221
|
+
- des tests unitaires du store, de la fabrique d'éléments, du moteur de diff et de l'export ;
|
|
222
|
+
- des tests des outils MCP appelés directement ;
|
|
223
|
+
- une suite de bout en bout (`test/e2e.test.ts`) qui lance un vrai serveur MCP sur stdio, attend l'enregistrement de son worker auprès du proxy, puis vérifie l'aller-retour complet agent → GUI → agent.
|
|
224
|
+
|
|
225
|
+
La suite de bout en bout a besoin du proxy (`http://localhost:3000`, ou `E2E_PROXY_URL`). S'il est absent, elle est ignorée plutôt qu'en échec.
|
|
226
|
+
|
|
227
|
+
Pour inspecter le worker sans traverser le proxy, ajoutez `?__direct=1` à son port local : les assets ne sont alors pas préfixés par le chemin de montage.
|
|
228
|
+
|
|
229
|
+
### Méta-configurations du proxy
|
|
230
|
+
|
|
231
|
+
`mcp-design` n'expose pas `GET /api/configurations` : il n'a pas de configuration métier (ni token, ni identifiant de ressource) qui justifierait des profils. Le proxy ignore simplement les MCPs qui n'implémentent pas ces routes.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { RenderNode } from "./render.js";
|
|
2
|
+
/**
|
|
3
|
+
* Valeurs par défaut et champs autorisés par type de noeud.
|
|
4
|
+
*
|
|
5
|
+
* Partagé entre le serveur (validation des appels d'outils) et la GUI (création
|
|
6
|
+
* à la souris) : une forme créée par l'agent et une forme dessinée par
|
|
7
|
+
* l'utilisateur partent exactement du même état.
|
|
8
|
+
*
|
|
9
|
+
* Comme `render.ts`, ce module ne dépend d'aucune librairie.
|
|
10
|
+
*/
|
|
11
|
+
export type NodeKind = RenderNode["type"];
|
|
12
|
+
export declare const NODE_KINDS: readonly NodeKind[];
|
|
13
|
+
/** Types que l'utilisateur peut dessiner directement ; un groupe se cree a partir d'une selection. */
|
|
14
|
+
export declare const DRAWABLE_KINDS: readonly NodeKind[];
|
|
15
|
+
export declare const NODE_DEFAULTS: Record<NodeKind, Partial<RenderNode>>;
|
|
16
|
+
/** Champs stylistiques acceptés par type — tout le reste est refusé à la création comme au patch. */
|
|
17
|
+
export declare const NODE_STYLE_FIELDS: Record<NodeKind, ReadonlyArray<keyof RenderNode>>;
|
|
18
|
+
/**
|
|
19
|
+
* Champs deduits du contenu d'un groupe. Ils sont recalcules a chaque mutation
|
|
20
|
+
* et ne peuvent pas etre ecrits directement — ni par l'agent, ni par la GUI.
|
|
21
|
+
*/
|
|
22
|
+
export declare const GROUP_DERIVED_FIELDS: ReadonlyArray<keyof RenderNode>;
|
|
23
|
+
export declare const NODE_GEOMETRY_FIELDS: ReadonlyArray<keyof RenderNode>;
|
|
24
|
+
export declare function defaultNodeName(type: NodeKind, index: number): string;
|
|
25
|
+
export declare function nodeKindLabel(type: NodeKind): string;
|
|
26
|
+
/** Taille minimale garantissant qu'un noeud reste sélectionnable à la souris. */
|
|
27
|
+
export declare function clampNodeSize(type: NodeKind, width: number, height: number): {
|
|
28
|
+
width: number;
|
|
29
|
+
height: number;
|
|
30
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export const NODE_KINDS = ["rect", "ellipse", "line", "text", "group"];
|
|
2
|
+
/** Types que l'utilisateur peut dessiner directement ; un groupe se cree a partir d'une selection. */
|
|
3
|
+
export const DRAWABLE_KINDS = ["rect", "ellipse", "line", "text"];
|
|
4
|
+
export const NODE_DEFAULTS = {
|
|
5
|
+
rect: { width: 200, height: 120, fill: "#e8e8e6", stroke: "#1a1a18", strokeWidth: 1, radius: 8 },
|
|
6
|
+
ellipse: { width: 160, height: 160, fill: "#e8e8e6", stroke: "#1a1a18", strokeWidth: 1 },
|
|
7
|
+
line: { width: 200, height: 0, stroke: "#1a1a18", strokeWidth: 2 },
|
|
8
|
+
text: {
|
|
9
|
+
width: 260,
|
|
10
|
+
height: 40,
|
|
11
|
+
text: "Texte",
|
|
12
|
+
color: "#1a1a18",
|
|
13
|
+
fontSize: 24,
|
|
14
|
+
fontFamily: "Inter, system-ui, sans-serif",
|
|
15
|
+
fontWeight: 400,
|
|
16
|
+
fontStyle: "normal",
|
|
17
|
+
textAlign: "left",
|
|
18
|
+
// En pixels, comme x/y/fontSize — jamais un multiplicateur.
|
|
19
|
+
lineHeight: 32,
|
|
20
|
+
},
|
|
21
|
+
// Un groupe n'a pas de taille propre : elle est deduite de son contenu.
|
|
22
|
+
group: { width: 0, height: 0 },
|
|
23
|
+
};
|
|
24
|
+
/** Champs stylistiques acceptés par type — tout le reste est refusé à la création comme au patch. */
|
|
25
|
+
export const NODE_STYLE_FIELDS = {
|
|
26
|
+
rect: ["fill", "stroke", "strokeWidth", "radius"],
|
|
27
|
+
ellipse: ["fill", "stroke", "strokeWidth"],
|
|
28
|
+
line: ["stroke", "strokeWidth"],
|
|
29
|
+
text: ["text", "color", "fontSize", "fontFamily", "fontWeight", "fontStyle", "textAlign", "lineHeight"],
|
|
30
|
+
group: [],
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Champs deduits du contenu d'un groupe. Ils sont recalcules a chaque mutation
|
|
34
|
+
* et ne peuvent pas etre ecrits directement — ni par l'agent, ni par la GUI.
|
|
35
|
+
*/
|
|
36
|
+
export const GROUP_DERIVED_FIELDS = ["width", "height"];
|
|
37
|
+
export const NODE_GEOMETRY_FIELDS = [
|
|
38
|
+
"x",
|
|
39
|
+
"y",
|
|
40
|
+
"width",
|
|
41
|
+
"height",
|
|
42
|
+
"rotation",
|
|
43
|
+
"opacity",
|
|
44
|
+
];
|
|
45
|
+
const LABELS = {
|
|
46
|
+
rect: "Rectangle",
|
|
47
|
+
ellipse: "Ellipse",
|
|
48
|
+
line: "Ligne",
|
|
49
|
+
text: "Texte",
|
|
50
|
+
group: "Groupe",
|
|
51
|
+
};
|
|
52
|
+
export function defaultNodeName(type, index) {
|
|
53
|
+
return `${LABELS[type]} ${index}`;
|
|
54
|
+
}
|
|
55
|
+
export function nodeKindLabel(type) {
|
|
56
|
+
return LABELS[type];
|
|
57
|
+
}
|
|
58
|
+
/** Taille minimale garantissant qu'un noeud reste sélectionnable à la souris. */
|
|
59
|
+
export function clampNodeSize(type, width, height) {
|
|
60
|
+
if (type === "group")
|
|
61
|
+
return { width, height }; // taille deduite du contenu
|
|
62
|
+
if (type === "line")
|
|
63
|
+
return { width, height }; // les deltas négatifs encodent la direction
|
|
64
|
+
if (type === "text")
|
|
65
|
+
return { width: Math.max(20, Math.abs(width)), height: Math.max(16, Math.abs(height)) };
|
|
66
|
+
return { width: Math.max(1, Math.abs(width)), height: Math.max(1, Math.abs(height)) };
|
|
67
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Description de chaque champ d'un noeud, **avec son unité**.
|
|
3
|
+
*
|
|
4
|
+
* Source unique : le schéma zod l'attache via `.describe()`, et le serveur MCP
|
|
5
|
+
* en construit l'`inputSchema` exposé à l'agent. Une unité n'a donc qu'un seul
|
|
6
|
+
* endroit où être écrite — c'est précisément l'ambiguïté qui a fait interpréter
|
|
7
|
+
* `lineHeight` comme un multiplicateur d'un côté et des pixels de l'autre.
|
|
8
|
+
*/
|
|
9
|
+
export declare const NODE_FIELD_DOCS: {
|
|
10
|
+
id: string;
|
|
11
|
+
type: string;
|
|
12
|
+
name: string;
|
|
13
|
+
parentId: string;
|
|
14
|
+
x: string;
|
|
15
|
+
y: string;
|
|
16
|
+
width: string;
|
|
17
|
+
height: string;
|
|
18
|
+
rotation: string;
|
|
19
|
+
opacity: string;
|
|
20
|
+
fill: string;
|
|
21
|
+
stroke: string;
|
|
22
|
+
strokeWidth: string;
|
|
23
|
+
radius: string;
|
|
24
|
+
text: string;
|
|
25
|
+
color: string;
|
|
26
|
+
fontSize: string;
|
|
27
|
+
fontFamily: string;
|
|
28
|
+
fontWeight: string;
|
|
29
|
+
fontStyle: string;
|
|
30
|
+
textAlign: string;
|
|
31
|
+
lineHeight: string;
|
|
32
|
+
};
|
|
33
|
+
export type NodeFieldName = keyof typeof NODE_FIELD_DOCS;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Description de chaque champ d'un noeud, **avec son unité**.
|
|
3
|
+
*
|
|
4
|
+
* Source unique : le schéma zod l'attache via `.describe()`, et le serveur MCP
|
|
5
|
+
* en construit l'`inputSchema` exposé à l'agent. Une unité n'a donc qu'un seul
|
|
6
|
+
* endroit où être écrite — c'est précisément l'ambiguïté qui a fait interpréter
|
|
7
|
+
* `lineHeight` comme un multiplicateur d'un côté et des pixels de l'autre.
|
|
8
|
+
*/
|
|
9
|
+
export const NODE_FIELD_DOCS = {
|
|
10
|
+
id: "Explicit id. Generated when omitted; must be unique within the design.",
|
|
11
|
+
type: "Kind of node: rect, ellipse, line, text or group.",
|
|
12
|
+
name: "Layer name shown in the GUI. Purely descriptive.",
|
|
13
|
+
parentId: "Id of the containing group, or null for the root level of the artboard. " +
|
|
14
|
+
"Changing it reparents the node — its x/y are then read in the new parent's space, so it moves on screen.",
|
|
15
|
+
x: "Horizontal position of the bounding box's left edge, in pixels, in the PARENT's coordinate space " +
|
|
16
|
+
"(the artboard's top-left corner when parentId is null, the group's origin otherwise). " +
|
|
17
|
+
"On a line, this is the start point.",
|
|
18
|
+
y: "Vertical position of the bounding box's top edge, in pixels, in the PARENT's coordinate space, " +
|
|
19
|
+
"measured downwards. On a line, this is the start point.",
|
|
20
|
+
width: "Width of the bounding box, in pixels. On a line, the horizontal delta from start to end — it may be negative. " +
|
|
21
|
+
"On a group, derived from the content and read-only.",
|
|
22
|
+
height: "Height of the bounding box, in pixels. On a line, the vertical delta from start to end — it may be negative. " +
|
|
23
|
+
"On a group, derived from the content and read-only.",
|
|
24
|
+
rotation: "Rotation in degrees, clockwise, around the centre of the bounding box. 0 = upright.",
|
|
25
|
+
opacity: "Opacity, from 0 (fully transparent) to 1 (fully opaque).",
|
|
26
|
+
fill: 'CSS colour filling the interior, e.g. "#ffffff" or "rgba(0,0,0,0.4)". rect and ellipse only.',
|
|
27
|
+
stroke: "CSS colour of the outline — of the stroke itself on a line node.",
|
|
28
|
+
strokeWidth: "Thickness of the outline, in pixels. 0 or omitted means no outline.",
|
|
29
|
+
radius: "Corner radius, in pixels. rect only; clamped to half of the shortest side.",
|
|
30
|
+
text: "Text content. It is NEVER auto-wrapped: only explicit \\n characters start a new line.",
|
|
31
|
+
color: "CSS colour of the text.",
|
|
32
|
+
fontSize: "Font size, in pixels (same meaning as the CSS font-size property).",
|
|
33
|
+
fontFamily: 'CSS font stack, e.g. "Inter, system-ui, sans-serif".',
|
|
34
|
+
fontWeight: "CSS numeric font weight, from 100 to 900. 400 = regular, 700 = bold.",
|
|
35
|
+
fontStyle: "normal or italic.",
|
|
36
|
+
textAlign: "Horizontal alignment of each line within the node's width.",
|
|
37
|
+
lineHeight: "Distance between two consecutive baselines, IN PIXELS — NOT a multiplier of fontSize. " +
|
|
38
|
+
"With fontSize 64, a comfortable value is 76, not 1.2. " +
|
|
39
|
+
"Defaults to fontSize x 1.35 when omitted, which is usually what you want.",
|
|
40
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
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 { normalizedBox } from "./tree.js";
|
|
10
|
+
export { normalizedBox };
|
|
11
|
+
export type { Box, Point } from "./tree.js";
|
|
12
|
+
export interface RenderNode {
|
|
13
|
+
id: string;
|
|
14
|
+
type: "rect" | "ellipse" | "line" | "text" | "group";
|
|
15
|
+
name: string;
|
|
16
|
+
/** Groupe conteneur, ou null a la racine. Les coordonnees sont relatives a ce parent. */
|
|
17
|
+
parentId: string | null;
|
|
18
|
+
x: number;
|
|
19
|
+
y: number;
|
|
20
|
+
width: number;
|
|
21
|
+
height: number;
|
|
22
|
+
rotation: number;
|
|
23
|
+
opacity: number;
|
|
24
|
+
fill?: string;
|
|
25
|
+
stroke?: string;
|
|
26
|
+
strokeWidth?: number;
|
|
27
|
+
radius?: number;
|
|
28
|
+
text?: string;
|
|
29
|
+
color?: string;
|
|
30
|
+
fontSize?: number;
|
|
31
|
+
fontFamily?: string;
|
|
32
|
+
fontWeight?: number;
|
|
33
|
+
fontStyle?: "normal" | "italic";
|
|
34
|
+
textAlign?: "left" | "center" | "right";
|
|
35
|
+
lineHeight?: number;
|
|
36
|
+
}
|
|
37
|
+
export declare function escapeXml(value: string): string;
|
|
38
|
+
export declare function nodeCenter(node: RenderNode): {
|
|
39
|
+
cx: number;
|
|
40
|
+
cy: number;
|
|
41
|
+
};
|
|
42
|
+
/** Transform SVG de rotation autour du centre de la boîte, ou "" si pas de rotation. */
|
|
43
|
+
export declare function rotationTransform(node: RenderNode): string;
|
|
44
|
+
/**
|
|
45
|
+
* Le texte n'est pas re-wrappé automatiquement : seuls les retours à la ligne
|
|
46
|
+
* explicites créent une nouvelle ligne. Comportement prévisible pour l'agent
|
|
47
|
+
* comme pour l'utilisateur, et identique à l'export.
|
|
48
|
+
*/
|
|
49
|
+
export declare function textLines(node: RenderNode): string[];
|
|
50
|
+
export declare const DEFAULT_FONT_FAMILY = "Inter, system-ui, sans-serif";
|
|
51
|
+
export declare const DEFAULT_FONT_SIZE = 24;
|
|
52
|
+
/**
|
|
53
|
+
* Interligne appliqué quand `lineHeight` est absent, exprimé en multiple de la
|
|
54
|
+
* taille de police. C'est le seul endroit où un ratio subsiste : le champ
|
|
55
|
+
* stocké, lui, est toujours en pixels.
|
|
56
|
+
*/
|
|
57
|
+
export declare const DEFAULT_LINE_HEIGHT_RATIO = 1.35;
|
|
58
|
+
/**
|
|
59
|
+
* En dessous de ce seuil, une valeur d'interligne ne peut pas être des pixels :
|
|
60
|
+
* 4px d'interligne écraserait n'importe quel texte lisible. C'est donc un ratio
|
|
61
|
+
* — refusé à l'écriture, converti à la lecture des anciens fichiers.
|
|
62
|
+
*/
|
|
63
|
+
export declare const MIN_LINE_HEIGHT_PX = 4;
|
|
64
|
+
/** Interligne par défaut, en pixels, pour une taille de police donnée. */
|
|
65
|
+
export declare function defaultLineHeight(fontSize: number): number;
|
|
66
|
+
export declare function textMetrics(node: RenderNode): {
|
|
67
|
+
fontSize: number;
|
|
68
|
+
lineStep: number;
|
|
69
|
+
/** Ligne de base de la première ligne, mesurée depuis le haut de la boîte. */
|
|
70
|
+
firstBaseline: number;
|
|
71
|
+
anchor: string;
|
|
72
|
+
anchorX: number;
|
|
73
|
+
};
|
|
74
|
+
/** Hauteur minimale nécessaire pour afficher tout le texte du noeud. */
|
|
75
|
+
export declare function measuredTextHeight(node: RenderNode): number;
|
|
76
|
+
/**
|
|
77
|
+
* Transform SVG d'un groupe : translation vers son origine, puis rotation
|
|
78
|
+
* autour du centre de son contenu (exprime dans l'espace local du groupe).
|
|
79
|
+
*/
|
|
80
|
+
export declare function groupTransform(node: RenderNode, localCenter: {
|
|
81
|
+
cx: number;
|
|
82
|
+
cy: number;
|
|
83
|
+
}): string;
|
|
84
|
+
/**
|
|
85
|
+
* Markup SVG d'un seul noeud, sans wrapper — utilisé par l'export et par le canvas.
|
|
86
|
+
* Un groupe ne produit rien par lui-meme : c'est un conteneur, rendu par
|
|
87
|
+
* `documentToSvg` (serveur) ou par le canvas (navigateur).
|
|
88
|
+
*/
|
|
89
|
+
export declare function nodeToSvg(node: RenderNode): string;
|
|
90
|
+
export interface RenderDocument {
|
|
91
|
+
name: string;
|
|
92
|
+
width: number;
|
|
93
|
+
height: number;
|
|
94
|
+
background: string;
|
|
95
|
+
nodes: RenderNode[];
|
|
96
|
+
}
|
|
97
|
+
export declare function documentToSvg(doc: RenderDocument): string;
|
|
98
|
+
/**
|
|
99
|
+
* SVG cadré sur un seul élément — l'élément et son contenu, rien d'autre.
|
|
100
|
+
*
|
|
101
|
+
* Le sous-arbre est rendu dans ses coordonnées locales, puis translaté vers sa
|
|
102
|
+
* position document : le viewBox peut alors être exprimé dans le même repère
|
|
103
|
+
* que `computeLayout`, donc directement sur la boîte englobante de l'élément.
|
|
104
|
+
*/
|
|
105
|
+
export declare function subtreeToSvg(doc: RenderDocument, nodeId: string, padding?: number): string;
|