@imenam/mcp-design 1.0.10 → 1.0.11
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 +44 -3
- package/dist/shared/feedback.d.ts +128 -0
- package/dist/shared/feedback.js +165 -0
- package/dist/shared/render.d.ts +7 -1
- package/dist/shared/render.js +32 -6
- package/dist/shared/schemas/feedback.schema.d.ts +185 -0
- package/dist/shared/schemas/feedback.schema.js +87 -0
- package/dist/shared/tree.d.ts +2 -0
- package/dist/shared/tree.js +4 -0
- package/dist/src/core/design-store.d.ts +1 -5
- package/dist/src/core/design-store.js +3 -56
- package/dist/src/core/feedback-store.d.ts +73 -0
- package/dist/src/core/feedback-store.js +308 -0
- package/dist/src/core/file-lock.d.ts +8 -0
- package/dist/src/core/file-lock.js +61 -0
- package/dist/src/core/screenshot.d.ts +3 -0
- package/dist/src/core/screenshot.js +28 -3
- package/dist/src/gui-worker.js +103 -1
- package/dist/src/index.js +175 -1
- package/dist/src/tools/delete-design.d.ts +3 -1
- package/dist/src/tools/delete-design.js +4 -1
- package/dist/src/tools/get-feedback.d.ts +3 -0
- package/dist/src/tools/get-feedback.js +62 -0
- package/dist/src/tools/mark-feedback-processed.d.ts +8 -0
- package/dist/src/tools/mark-feedback-processed.js +29 -0
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/gui/dist/assets/index-BXpZGU8f.css +1 -0
- package/gui/dist/assets/index-CgZSm3aX.js +15 -0
- package/gui/dist/assets/{search-CSwiX0lv.js → search-DnrsSC4m.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/gui/dist/assets/index-DBZNXgF_.js +0 -10
- package/gui/dist/assets/index-PQqxUQLx.css +0 -1
package/README.md
CHANGED
|
@@ -4,10 +4,13 @@ Un canvas de design pilotable par un agent, avec une interface graphique où l'u
|
|
|
4
4
|
|
|
5
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
6
|
|
|
7
|
+
L'utilisateur peut aussi, sans toucher au design, **épingler des remarques** sur une page, un élément ou une zone. Il les transmet en un retour nommé que l'agent lit avec `get_feedback`, captures comprises ; l'agent déclare ensuite ce qu'il a traité avec `mark_feedback_processed`, et l'utilisateur suit l'avancement de chaque retour dans la page « Retours ».
|
|
8
|
+
|
|
7
9
|
```
|
|
8
10
|
Agent ──add_nodes/update_nodes──▶ Design ◀──glisser-déposer── Utilisateur (GUI)
|
|
9
11
|
▲ │
|
|
10
|
-
|
|
12
|
+
├──────────── get_changes ──── diff depuis la dernière baseline ──────┤
|
|
13
|
+
└──────────── get_feedback ─── remarques transmises (retour n°N) ─────┘
|
|
11
14
|
```
|
|
12
15
|
|
|
13
16
|
---
|
|
@@ -72,7 +75,7 @@ Exemple d'entrée générée :
|
|
|
72
75
|
| `APP_NAME` | Nom affiché dans le dashboard du proxy. `PROXY_APP_NAME` puis `GUI_NAME` sont acceptés en repli. |
|
|
73
76
|
| `APP_PATH_PREFIX` | Préfixe de namespace ajouté au chemin **par défaut** (ex. `/abc123` → `/abc123/design-gui`). Sert à isoler plusieurs instances sous un même proxy. |
|
|
74
77
|
| `APP_GROUP` | Section repliable du dashboard du proxy (facultatif). |
|
|
75
|
-
| `MCP_DESIGN_DATA_DIR` | Répertoire de stockage des designs. `MCP_DATA_DIR` est accepté en repli. Défaut : `<package>/.design-data`. |
|
|
78
|
+
| `MCP_DESIGN_DATA_DIR` | Répertoire de stockage des designs et des remarques. `MCP_DATA_DIR` est accepté en repli. Défaut : `<package>/.design-data`. |
|
|
76
79
|
| `MCP_LOG_DIR` | Répertoire des logs. Défaut : `<data>/logs`. |
|
|
77
80
|
|
|
78
81
|
`PROXY_URL` et `APP_PORT` s'excluent : `PROXY_URL` prime si les deux sont renseignés. **Si aucune des deux n'est définie, la GUI est simplement désactivée** — les outils MCP continuent de fonctionner.
|
|
@@ -241,6 +244,17 @@ Un lot est intégralement validé avant la moindre écriture : une entrée inval
|
|
|
241
244
|
|
|
242
245
|
**`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é.
|
|
243
246
|
|
|
247
|
+
### Lire les remarques de l'utilisateur
|
|
248
|
+
|
|
249
|
+
| Outil | Effet |
|
|
250
|
+
|---|---|
|
|
251
|
+
| `get_feedback` | Renvoie le plus ancien retour qui a encore des remarques à traiter (le dernier quand tout est traité), ou celui demandé par `round`. Pour chaque remarque : son identifiant, la page, puis la page entière, l'élément visé (id, nom, chemin, boîte) ou la zone et les éléments cités, le texte et son état de traitement. Les captures d'éléments et de zones suivent **en images**, numérotées comme dans le texte ; `captures: false` les omet. La réponse se termine par la liste de tous les retours et de leur avancement. |
|
|
252
|
+
| `mark_feedback_processed` | Déclare traitées des remarques d'un retour (`round`, `remark_ids` — toutes si omis), avec une `note` sur ce qui a été fait. C'est ce que l'utilisateur voit dans la page « Retours ». |
|
|
253
|
+
|
|
254
|
+
Un retour est **figé** au moment où l'utilisateur clique sur « Terminer les retours » : `get_feedback` peut le relire autant de fois que nécessaire, rien ne change. Seuls son nom (renommable dans la GUI) et le suivi du traitement évoluent ; **lire un retour ne le marque jamais traité**, seul `mark_feedback_processed` (ou l'utilisateur) le fait. Le texte est celui que la GUI affiche en aperçu avant l'enregistrement — même fonction de mise en forme, donc même contenu. Tant qu'aucun retour n'existe, l'outil dit combien de remarques attendent encore dans la GUI.
|
|
255
|
+
|
|
256
|
+
Les remarques vivent dans `<MCP_DESIGN_DATA_DIR>/feedback/remarks.json`, les captures PNG dans `<MCP_DESIGN_DATA_DIR>/feedback/captures/` : le même dossier de données que les designs, celui fixé par la configuration du projet.
|
|
257
|
+
|
|
244
258
|
### GUI
|
|
245
259
|
|
|
246
260
|
| Outil | Effet |
|
|
@@ -263,6 +277,13 @@ screenshot_design { design_id: "landing" } ← ce que ça donne réellement,
|
|
|
263
277
|
export_design { design_id: "landing", format: "html" }
|
|
264
278
|
… l'agent reporte les valeurs dans le code de l'application …
|
|
265
279
|
mark_synced { design_id: "landing" } ← acquittement
|
|
280
|
+
|
|
281
|
+
… l'utilisateur passe en mode Remarques, commente, clique « Terminer les retours »,
|
|
282
|
+
puis dit à l'agent qu'il a terminé …
|
|
283
|
+
|
|
284
|
+
get_feedback {} ← retour à traiter : remarques + captures
|
|
285
|
+
… l'agent applique les remarques …
|
|
286
|
+
mark_feedback_processed { round: 1, remark_ids: ["rm_…"], note: "…" } ← suivi visible dans la GUI
|
|
266
287
|
```
|
|
267
288
|
|
|
268
289
|
---
|
|
@@ -290,6 +311,26 @@ La sauvegarde est automatique et différée. Une modification faite par l'agent
|
|
|
290
311
|
|
|
291
312
|
Le badge « n à synchroniser » d'une planche indique le nombre de modifications que l'agent n'a pas encore acquittées.
|
|
292
313
|
|
|
314
|
+
### Mode Remarques
|
|
315
|
+
|
|
316
|
+
La bascule **Édition / Remarques** de la barre d'outils (raccourci `M`) passe le canvas en lecture : rien ne s'y déplace, et le rail de droite affiche les remarques à la place de l'inspecteur.
|
|
317
|
+
|
|
318
|
+
- **Remarque sur un élément** — un clic vise le **groupe le plus haut**, comme en édition ; un nouveau clic à l'intérieur descend d'un niveau, `Alt+clic` vise l'élément exact. La bulle de saisie affiche le chemin de l'élément : un clic sur un niveau vise ce parent.
|
|
319
|
+
- **Remarque sur une zone** — glisser trace un rectangle. La bulle montre l'aperçu de la zone et les **éléments détectés** : un élément entièrement compris est cité tel quel, un groupe qui déborde est ouvert pour citer ceux de ses éléments que la zone touche, un élément qui déborde est marqué « en partie » et décoché par défaut.
|
|
320
|
+
- **Remarque sur la page** — depuis le panneau, une seule remarque en cours par page.
|
|
321
|
+
- **Captures** — pour un élément ou une zone, « Joindre une capture » (coché par défaut) enregistre un PNG de ce que l'utilisateur commentait, rendu par le serveur au moment de la remarque.
|
|
322
|
+
- **Pastilles** — chaque remarque d'élément ou de zone porte un numéro sur la planche ; survoler une carte du panneau met sa cible en évidence, cliquer une pastille désigne sa carte.
|
|
323
|
+
- **Terminer les retours** — demande un **nom** pour le retour (« Retour n°N » proposé), choisit le périmètre (cette page ou toutes) et les remarques à transmettre, et montre exactement le texte que l'agent recevra. L'enregistrement fige un **retour numéroté et nommé**.
|
|
324
|
+
- **Après l'envoi** — les remarques restent visibles en lecture seule, marquées « Transmise · *nom du retour* », puis « Traitée » une fois traitées : pastille creuse, zone en pointillés. Une case du panneau les masque. Une remarque dont l'élément a disparu depuis le signale.
|
|
325
|
+
|
|
326
|
+
### Page « Retours »
|
|
327
|
+
|
|
328
|
+
Le bouton **Retours** de la barre d'outils (avec le nombre de retours pas encore entièrement traités) ouvre la liste de tous les retours, du plus récent au plus ancien, chacun avec son état — *À traiter*, *En cours* (n/total), *Traité* — et une barre d'avancement. Le détail d'un retour reprend ses remarques page par page, avec leur capture, et pour chacune : *À traiter*, ou *Traitée par l'agent / par vous* avec la date et la note de l'agent. Le nom du retour se modifie en place ; une case coche ou rouvre une remarque à la main, et « Tout marquer comme traité » (ou « Tout rouvrir ») agit sur tout le retour affiché ; le même bouton en haut de la page traite d'un coup tous les retours en attente, après confirmation. Une remarque déjà traitée par l'agent garde son auteur et sa note ; « Ouvrir la page » ramène sur la planche en mode Remarques. `Échap` ferme la page.
|
|
329
|
+
|
|
330
|
+
Les retours créés avant l'arrivée des noms reçoivent « Retour n°N », et aucune de leurs remarques n'est considérée comme traitée.
|
|
331
|
+
|
|
332
|
+
`Ctrl+Entrée` enregistre une remarque, `Échap` la ferme, puis quitte le mode. Le badge violet d'une page dans la liste compte ses remarques non transmises.
|
|
333
|
+
|
|
293
334
|
---
|
|
294
335
|
|
|
295
336
|
## Architecture
|
|
@@ -301,7 +342,7 @@ Deux processus, conformément au standard de l'écosystème (`@imenam/mcp-gui-in
|
|
|
301
342
|
|
|
302
343
|
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.
|
|
303
344
|
|
|
304
|
-
`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, `shared/instances.ts` l'expansion des instances en groupes, `shared/node-defaults.ts` les valeurs par défaut et `shared/icons/` la bibliothèque d'icônes. Tous 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.
|
|
345
|
+
`shared/feedback.ts` porte le modèle des remarques, la détection des éléments d'une zone et la mise en forme d'un retour ; le serveur et la GUI l'importent tous deux, ce qui garantit que l'aperçu affiché est le texte transmis. `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, `shared/instances.ts` l'expansion des instances en groupes, `shared/node-defaults.ts` les valeurs par défaut et `shared/icons/` la bibliothèque d'icônes. Tous 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.
|
|
305
346
|
|
|
306
347
|
Les invariants de hiérarchie — parent existant et de type `group`, absence de cycle (y compris dans le graphe composant/instance), taille des groupes et des instances recalculée, ordre préfixe — sont appliqués dans `DesignStore.mutate`, donc sur **tout** chemin d'écriture : outil MCP comme sauvegarde depuis la GUI.
|
|
307
348
|
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remarques de l'utilisateur sur un design, et retours transmis à l'agent.
|
|
3
|
+
*
|
|
4
|
+
* Une remarque vise la page entière, un élément, ou une zone rectangulaire de la
|
|
5
|
+
* planche. Tant qu'elle est un brouillon, l'utilisateur la modifie librement ;
|
|
6
|
+
* « Terminer les retours » fige les remarques cochées dans un retour numéroté,
|
|
7
|
+
* nommé, que l'agent lit avec `get_feedback`. Une remarque transmise ne se
|
|
8
|
+
* modifie plus : le retour qui la contient reste exactement ce que l'agent a
|
|
9
|
+
* reçu. Seul son traitement évolue, déclaré par l'agent avec
|
|
10
|
+
* `mark_feedback_processed` ou par l'utilisateur dans la page des retours.
|
|
11
|
+
*
|
|
12
|
+
* Comme `render.ts` et `tree.ts`, ce module ne dépend d'aucune librairie : il
|
|
13
|
+
* est importé par le serveur MCP comme par la GUI, qui s'en sert pour afficher
|
|
14
|
+
* l'aperçu exact du retour avant de l'enregistrer.
|
|
15
|
+
*/
|
|
16
|
+
import type { RenderNode } from "./render.js";
|
|
17
|
+
import { type Box } from "./tree.js";
|
|
18
|
+
export type RemarkStatus = "draft" | "sent";
|
|
19
|
+
/** Élément de la planche cité par une remarque. */
|
|
20
|
+
export interface RemarkElement {
|
|
21
|
+
id: string;
|
|
22
|
+
name: string;
|
|
23
|
+
type: RenderNode["type"];
|
|
24
|
+
}
|
|
25
|
+
export type RemarkTarget = {
|
|
26
|
+
kind: "page";
|
|
27
|
+
} | {
|
|
28
|
+
kind: "node";
|
|
29
|
+
element: RemarkElement;
|
|
30
|
+
/** Noms des groupes englobants puis de l'élément, de la racine vers lui. */
|
|
31
|
+
path: string[];
|
|
32
|
+
/** Boîte de l'élément au moment de la remarque, en coordonnées document. */
|
|
33
|
+
box: Box;
|
|
34
|
+
} | {
|
|
35
|
+
kind: "area";
|
|
36
|
+
/** Zone tracée, en coordonnées document. */
|
|
37
|
+
box: Box;
|
|
38
|
+
/** Éléments de la zone que l'utilisateur a choisi de citer. */
|
|
39
|
+
elements: RemarkElement[];
|
|
40
|
+
};
|
|
41
|
+
export type RemarkKind = RemarkTarget["kind"];
|
|
42
|
+
export interface Remark {
|
|
43
|
+
id: string;
|
|
44
|
+
designId: string;
|
|
45
|
+
/** Numéro de la pastille sur la planche ; null pour la remarque de page. */
|
|
46
|
+
number: number | null;
|
|
47
|
+
target: RemarkTarget;
|
|
48
|
+
text: string;
|
|
49
|
+
/** Nom du fichier PNG de la capture, ou null sans capture. */
|
|
50
|
+
capture: string | null;
|
|
51
|
+
status: RemarkStatus;
|
|
52
|
+
/** Retour qui contient la remarque, une fois transmise. */
|
|
53
|
+
round: number | null;
|
|
54
|
+
createdAt: string;
|
|
55
|
+
updatedAt: string;
|
|
56
|
+
}
|
|
57
|
+
/** Traitement d'une remarque transmise : qui l'a déclarée traitée, quand, et ce qui a été fait. */
|
|
58
|
+
export interface RemarkProcessing {
|
|
59
|
+
at: string;
|
|
60
|
+
by: "agent" | "user";
|
|
61
|
+
note: string | null;
|
|
62
|
+
}
|
|
63
|
+
/** Remarque telle qu'elle figure dans un retour transmis. */
|
|
64
|
+
export interface RoundRemark {
|
|
65
|
+
id: string;
|
|
66
|
+
designId: string;
|
|
67
|
+
designName: string;
|
|
68
|
+
number: number | null;
|
|
69
|
+
target: RemarkTarget;
|
|
70
|
+
text: string;
|
|
71
|
+
capture: string | null;
|
|
72
|
+
/** Null tant que la remarque n'est pas traitée. */
|
|
73
|
+
processed: RemarkProcessing | null;
|
|
74
|
+
}
|
|
75
|
+
export interface FeedbackRound {
|
|
76
|
+
number: number;
|
|
77
|
+
/** Nom donné par l'utilisateur en terminant sa revue. */
|
|
78
|
+
title: string;
|
|
79
|
+
submittedAt: string;
|
|
80
|
+
remarks: RoundRemark[];
|
|
81
|
+
}
|
|
82
|
+
export type RoundStatus = "pending" | "in_progress" | "done";
|
|
83
|
+
/** Avancement d'un retour : à traiter, en cours, ou entièrement traité. */
|
|
84
|
+
export declare function roundProgress(round: Pick<FeedbackRound, "remarks">): {
|
|
85
|
+
done: number;
|
|
86
|
+
total: number;
|
|
87
|
+
status: RoundStatus;
|
|
88
|
+
};
|
|
89
|
+
/** Nom proposé pour un retour avant que l'utilisateur ne le change. */
|
|
90
|
+
export declare function defaultRoundTitle(number: number): string;
|
|
91
|
+
/** Élément touché par une zone. `partial` : il en déborde. */
|
|
92
|
+
export interface AreaHit extends RemarkElement {
|
|
93
|
+
partial: boolean;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Éléments qu'une zone recouvre, au niveau le plus utile pour les nommer.
|
|
97
|
+
*
|
|
98
|
+
* Un élément entièrement compris dans la zone est cité tel quel, sans détailler
|
|
99
|
+
* son contenu. Un groupe qui en déborde est ouvert, pour citer ceux de ses
|
|
100
|
+
* éléments que la zone touche. Un élément sans contenu qui en déborde (forme,
|
|
101
|
+
* texte, instance) est cité comme partiel. Les instances ne sont jamais
|
|
102
|
+
* ouvertes : leur contenu n'existe pas en stockage.
|
|
103
|
+
*/
|
|
104
|
+
export declare function elementsInArea(nodes: RenderNode[], area: Box): AreaHit[];
|
|
105
|
+
/** Noms des groupes englobants puis de l'élément, de la racine vers lui. */
|
|
106
|
+
export declare function nodePath(nodes: RenderNode[], id: string): string[];
|
|
107
|
+
/** Cible d'une remarque sur un élément, calculée sur l'état courant du design. */
|
|
108
|
+
export declare function nodeTarget(nodes: RenderNode[], id: string): Extract<RemarkTarget, {
|
|
109
|
+
kind: "node";
|
|
110
|
+
}>;
|
|
111
|
+
export declare function roundBox(box: Box): Box;
|
|
112
|
+
/**
|
|
113
|
+
* Ordre de lecture d'un lot de remarques : les pages dans l'ordre où elles
|
|
114
|
+
* apparaissent, puis dans chaque page la remarque de page d'abord et les autres
|
|
115
|
+
* par numéro de pastille. Le serveur fige un retour dans cet ordre, la GUI
|
|
116
|
+
* affiche son aperçu dans le même.
|
|
117
|
+
*/
|
|
118
|
+
export declare function orderRemarks<T extends Pick<Remark, "designId" | "number">>(remarks: T[]): T[];
|
|
119
|
+
export declare const PATH_SEPARATOR = " \u203A ";
|
|
120
|
+
/**
|
|
121
|
+
* Texte d'un retour, tel que l'agent le reçoit.
|
|
122
|
+
*
|
|
123
|
+
* La GUI l'affiche en aperçu avant l'enregistrement : c'est la même fonction,
|
|
124
|
+
* donc le même texte. Chaque remarque porte son identifiant, que l'agent passe
|
|
125
|
+
* à `mark_feedback_processed`. Les captures sont numérotées dans l'ordre des
|
|
126
|
+
* remarques ; `get_feedback` joint les images dans cet ordre.
|
|
127
|
+
*/
|
|
128
|
+
export declare function formatFeedbackRound(round: Pick<FeedbackRound, "number" | "title" | "submittedAt" | "remarks">): string;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remarques de l'utilisateur sur un design, et retours transmis à l'agent.
|
|
3
|
+
*
|
|
4
|
+
* Une remarque vise la page entière, un élément, ou une zone rectangulaire de la
|
|
5
|
+
* planche. Tant qu'elle est un brouillon, l'utilisateur la modifie librement ;
|
|
6
|
+
* « Terminer les retours » fige les remarques cochées dans un retour numéroté,
|
|
7
|
+
* nommé, que l'agent lit avec `get_feedback`. Une remarque transmise ne se
|
|
8
|
+
* modifie plus : le retour qui la contient reste exactement ce que l'agent a
|
|
9
|
+
* reçu. Seul son traitement évolue, déclaré par l'agent avec
|
|
10
|
+
* `mark_feedback_processed` ou par l'utilisateur dans la page des retours.
|
|
11
|
+
*
|
|
12
|
+
* Comme `render.ts` et `tree.ts`, ce module ne dépend d'aucune librairie : il
|
|
13
|
+
* est importé par le serveur MCP comme par la GUI, qui s'en sert pour afficher
|
|
14
|
+
* l'aperçu exact du retour avant de l'enregistrer.
|
|
15
|
+
*/
|
|
16
|
+
import { expandInstances } from "./instances.js";
|
|
17
|
+
import { ancestorsOf, boundsOf, boxesIntersect, childrenByParent, computeLayout } from "./tree.js";
|
|
18
|
+
/** Avancement d'un retour : à traiter, en cours, ou entièrement traité. */
|
|
19
|
+
export function roundProgress(round) {
|
|
20
|
+
const total = round.remarks.length;
|
|
21
|
+
const done = round.remarks.filter((remark) => remark.processed !== null).length;
|
|
22
|
+
return { done, total, status: done === 0 ? "pending" : done === total ? "done" : "in_progress" };
|
|
23
|
+
}
|
|
24
|
+
/** Nom proposé pour un retour avant que l'utilisateur ne le change. */
|
|
25
|
+
export function defaultRoundTitle(number) {
|
|
26
|
+
return `Retour n°${number}`;
|
|
27
|
+
}
|
|
28
|
+
function contains(outer, inner) {
|
|
29
|
+
return (inner.x >= outer.x &&
|
|
30
|
+
inner.y >= outer.y &&
|
|
31
|
+
inner.x + inner.width <= outer.x + outer.width &&
|
|
32
|
+
inner.y + inner.height <= outer.y + outer.height);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Éléments qu'une zone recouvre, au niveau le plus utile pour les nommer.
|
|
36
|
+
*
|
|
37
|
+
* Un élément entièrement compris dans la zone est cité tel quel, sans détailler
|
|
38
|
+
* son contenu. Un groupe qui en déborde est ouvert, pour citer ceux de ses
|
|
39
|
+
* éléments que la zone touche. Un élément sans contenu qui en déborde (forme,
|
|
40
|
+
* texte, instance) est cité comme partiel. Les instances ne sont jamais
|
|
41
|
+
* ouvertes : leur contenu n'existe pas en stockage.
|
|
42
|
+
*/
|
|
43
|
+
export function elementsInArea(nodes, area) {
|
|
44
|
+
const layout = computeLayout(expandInstances(nodes));
|
|
45
|
+
const children = childrenByParent(nodes);
|
|
46
|
+
const hits = [];
|
|
47
|
+
const visit = (node) => {
|
|
48
|
+
const box = boundsOf(layout, node.id);
|
|
49
|
+
if (box.width <= 0 && box.height <= 0)
|
|
50
|
+
return;
|
|
51
|
+
if (!boxesIntersect(box, area))
|
|
52
|
+
return;
|
|
53
|
+
const element = { id: node.id, name: node.name, type: node.type };
|
|
54
|
+
if (contains(area, box)) {
|
|
55
|
+
hits.push({ ...element, partial: false });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const content = node.type === "group" ? (children.get(node.id) ?? []) : [];
|
|
59
|
+
if (content.length > 0)
|
|
60
|
+
content.forEach(visit);
|
|
61
|
+
else
|
|
62
|
+
hits.push({ ...element, partial: true });
|
|
63
|
+
};
|
|
64
|
+
(children.get(null) ?? []).forEach(visit);
|
|
65
|
+
return hits;
|
|
66
|
+
}
|
|
67
|
+
/** Noms des groupes englobants puis de l'élément, de la racine vers lui. */
|
|
68
|
+
export function nodePath(nodes, id) {
|
|
69
|
+
const node = nodes.find((candidate) => candidate.id === id);
|
|
70
|
+
if (!node)
|
|
71
|
+
return [];
|
|
72
|
+
return [...ancestorsOf(nodes, id).reverse().map((ancestor) => ancestor.name), node.name];
|
|
73
|
+
}
|
|
74
|
+
/** Cible d'une remarque sur un élément, calculée sur l'état courant du design. */
|
|
75
|
+
export function nodeTarget(nodes, id) {
|
|
76
|
+
const node = nodes.find((candidate) => candidate.id === id);
|
|
77
|
+
if (!node)
|
|
78
|
+
throw new Error(`Node "${id}" not found in the design.`);
|
|
79
|
+
const box = boundsOf(computeLayout(expandInstances(nodes)), id);
|
|
80
|
+
return {
|
|
81
|
+
kind: "node",
|
|
82
|
+
element: { id: node.id, name: node.name, type: node.type },
|
|
83
|
+
path: nodePath(nodes, id),
|
|
84
|
+
box: roundBox(box),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
export function roundBox(box) {
|
|
88
|
+
const round = (value) => Math.round(value * 100) / 100;
|
|
89
|
+
return { x: round(box.x), y: round(box.y), width: round(box.width), height: round(box.height) };
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Ordre de lecture d'un lot de remarques : les pages dans l'ordre où elles
|
|
93
|
+
* apparaissent, puis dans chaque page la remarque de page d'abord et les autres
|
|
94
|
+
* par numéro de pastille. Le serveur fige un retour dans cet ordre, la GUI
|
|
95
|
+
* affiche son aperçu dans le même.
|
|
96
|
+
*/
|
|
97
|
+
export function orderRemarks(remarks) {
|
|
98
|
+
const pages = [...new Set(remarks.map((remark) => remark.designId))];
|
|
99
|
+
return [...remarks].sort((a, b) => pages.indexOf(a.designId) - pages.indexOf(b.designId) || (a.number ?? 0) - (b.number ?? 0));
|
|
100
|
+
}
|
|
101
|
+
export const PATH_SEPARATOR = " › ";
|
|
102
|
+
function formatBox(box) {
|
|
103
|
+
const round = (value) => Math.round(value);
|
|
104
|
+
return `x ${round(box.x)}, y ${round(box.y)}, ${round(box.width)}×${round(box.height)}`;
|
|
105
|
+
}
|
|
106
|
+
function formatElement(element) {
|
|
107
|
+
return `${element.id} "${element.name}" (${element.type})`;
|
|
108
|
+
}
|
|
109
|
+
function indent(text, prefix) {
|
|
110
|
+
return text
|
|
111
|
+
.split("\n")
|
|
112
|
+
.map((line) => `${prefix}${line}`)
|
|
113
|
+
.join("\n");
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Texte d'un retour, tel que l'agent le reçoit.
|
|
117
|
+
*
|
|
118
|
+
* La GUI l'affiche en aperçu avant l'enregistrement : c'est la même fonction,
|
|
119
|
+
* donc le même texte. Chaque remarque porte son identifiant, que l'agent passe
|
|
120
|
+
* à `mark_feedback_processed`. Les captures sont numérotées dans l'ordre des
|
|
121
|
+
* remarques ; `get_feedback` joint les images dans cet ordre.
|
|
122
|
+
*/
|
|
123
|
+
export function formatFeedbackRound(round) {
|
|
124
|
+
const pages = new Map();
|
|
125
|
+
for (const remark of round.remarks) {
|
|
126
|
+
const list = pages.get(remark.designId);
|
|
127
|
+
if (list)
|
|
128
|
+
list.push(remark);
|
|
129
|
+
else
|
|
130
|
+
pages.set(remark.designId, [remark]);
|
|
131
|
+
}
|
|
132
|
+
const progress = roundProgress(round);
|
|
133
|
+
const lines = [
|
|
134
|
+
`Feedback round ${round.number} "${round.title}" — submitted ${round.submittedAt} — ` +
|
|
135
|
+
`${round.remarks.length} remark(s) on ${pages.size} page(s), ${progress.done} processed.`,
|
|
136
|
+
"Coordinates are design pixels in the artboard's space, as in get_design.",
|
|
137
|
+
];
|
|
138
|
+
let image = 0;
|
|
139
|
+
for (const [designId, remarks] of pages) {
|
|
140
|
+
lines.push("", `Page "${remarks[0].designName}" (${designId})`);
|
|
141
|
+
for (const remark of remarks) {
|
|
142
|
+
const capture = remark.capture ? ` — capture: image ${++image}` : "";
|
|
143
|
+
const target = remark.target;
|
|
144
|
+
let head;
|
|
145
|
+
if (target.kind === "page") {
|
|
146
|
+
head = `- ${remark.id} [whole page]`;
|
|
147
|
+
}
|
|
148
|
+
else if (target.kind === "node") {
|
|
149
|
+
head =
|
|
150
|
+
`- ${remark.id} [#${remark.number} · element] ${formatElement(target.element)} — ` +
|
|
151
|
+
`path: ${target.path.join(PATH_SEPARATOR)} — box ${formatBox(target.box)}${capture}`;
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
const elements = target.elements.length > 0 ? target.elements.map(formatElement).join(", ") : "no element selected";
|
|
155
|
+
head = `- ${remark.id} [#${remark.number} · area] box ${formatBox(target.box)} — elements: ${elements}${capture}`;
|
|
156
|
+
}
|
|
157
|
+
lines.push(head, indent(remark.text, " "));
|
|
158
|
+
if (remark.processed) {
|
|
159
|
+
const note = remark.processed.note ? `: ${remark.processed.note}` : "";
|
|
160
|
+
lines.push(` ✓ processed by ${remark.processed.by} at ${remark.processed.at}${note}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return lines.join("\n");
|
|
165
|
+
}
|
package/dist/shared/render.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Ce module ne dépend volontairement d'aucune librairie (pas de zod) pour rester
|
|
7
7
|
* importable depuis le bundle GUI.
|
|
8
8
|
*/
|
|
9
|
-
import { normalizedBox } from "./tree.js";
|
|
9
|
+
import { normalizedBox, type Box } from "./tree.js";
|
|
10
10
|
import { type IconElement } from "./icons/index.js";
|
|
11
11
|
export { normalizedBox };
|
|
12
12
|
export type { Box, Point } from "./tree.js";
|
|
@@ -146,6 +146,12 @@ export interface RenderDocument {
|
|
|
146
146
|
nodes: RenderNode[];
|
|
147
147
|
}
|
|
148
148
|
export declare function documentToSvg(doc: RenderDocument): string;
|
|
149
|
+
/**
|
|
150
|
+
* SVG cadré sur une zone rectangulaire de la planche, en coordonnées document.
|
|
151
|
+
* Tout ce qui recoupe la zone y figure, coupé à ses bords ; les éléments
|
|
152
|
+
* semi-transparents qui ne la recoupent pas sont omis.
|
|
153
|
+
*/
|
|
154
|
+
export declare function regionToSvg(doc: RenderDocument, region: Box): string;
|
|
149
155
|
/**
|
|
150
156
|
* SVG cadré sur un seul élément — l'élément et son contenu, rien d'autre.
|
|
151
157
|
*
|
package/dist/shared/render.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Ce module ne dépend volontairement d'aucune librairie (pas de zod) pour rester
|
|
7
7
|
* importable depuis le bundle GUI.
|
|
8
8
|
*/
|
|
9
|
-
import { boundsOf, childrenByParent, computeLayout, normalizedBox, spaceOriginOf } from "./tree.js";
|
|
9
|
+
import { boundsOf, boxesIntersect, childrenByParent, computeLayout, normalizedBox, spaceOriginOf, } from "./tree.js";
|
|
10
10
|
import { expandInstances } from "./instances.js";
|
|
11
11
|
import { DEFAULT_ICON_STROKE_WIDTH, ICON_GRID, UNKNOWN_ICON_ELEMENTS, iconElements, } from "./icons/index.js";
|
|
12
12
|
// La géométrie de base vit dans tree.ts (module bas-niveau, sans dépendance vers
|
|
@@ -238,24 +238,31 @@ export function nodeToSvg(node) {
|
|
|
238
238
|
/**
|
|
239
239
|
* Rend une liste de noeuds frères, en descendant récursivement dans les groupes.
|
|
240
240
|
* Partagé par le rendu plein document et le rendu cadré sur un seul élément.
|
|
241
|
+
*
|
|
242
|
+
* Avec `frame`, un élément semi-transparent qui ne recoupe pas le cadre est
|
|
243
|
+
* omis : il n'y serait pas visible, et resvg panique — en emportant le
|
|
244
|
+
* processus — sur un élément semi-transparent dessiné entièrement hors du cadre.
|
|
241
245
|
*/
|
|
242
|
-
function renderSiblings(nodes, children, layout, depth) {
|
|
246
|
+
function renderSiblings(nodes, children, layout, depth, frame) {
|
|
243
247
|
const pad = " ".repeat(depth + 1);
|
|
244
248
|
return nodes.flatMap((node) => {
|
|
249
|
+
const bounds = boundsOf(layout, node.id);
|
|
250
|
+
if (frame && node.opacity < 1 && !boxesIntersect(bounds, frame))
|
|
251
|
+
return [];
|
|
245
252
|
if (node.type !== "group")
|
|
246
253
|
return [`${pad}${nodeToSvg(node)}`];
|
|
247
|
-
const bounds = boundsOf(layout, node.id);
|
|
248
254
|
const origin = layout.origin.get(node.id) ?? { x: 0, y: 0 };
|
|
249
255
|
const center = { cx: bounds.x + bounds.width / 2 - origin.x, cy: bounds.y + bounds.height / 2 - origin.y };
|
|
250
256
|
const opacity = node.opacity >= 1 ? "" : ` opacity="${round(node.opacity)}"`;
|
|
251
257
|
return [
|
|
252
258
|
`${pad}<g transform="${groupTransform(node, center)}"${opacity}>`,
|
|
253
|
-
...renderSiblings(children.get(node.id) ?? [], children, layout, depth + 1),
|
|
259
|
+
...renderSiblings(children.get(node.id) ?? [], children, layout, depth + 1, frame),
|
|
254
260
|
`${pad}</g>`,
|
|
255
261
|
];
|
|
256
262
|
});
|
|
257
263
|
}
|
|
258
|
-
|
|
264
|
+
/** Contenu SVG de la planche, en coordonnées document, limité à `frame` s'il est donné. */
|
|
265
|
+
function documentBody(doc, frame) {
|
|
259
266
|
// Les instances sont resolues ici, une fois pour toutes : en dessous, il n'y
|
|
260
267
|
// a plus que des groupes ordinaires.
|
|
261
268
|
const nodes = expandInstances(doc.nodes);
|
|
@@ -263,13 +270,32 @@ export function documentToSvg(doc) {
|
|
|
263
270
|
const children = childrenByParent(nodes);
|
|
264
271
|
// Un groupe devient un <g> : ses enfants gardent leurs coordonnées locales,
|
|
265
272
|
// c'est la transformation du conteneur qui les place.
|
|
266
|
-
|
|
273
|
+
return renderSiblings(children.get(null) ?? [], children, layout, 0, frame).join("\n");
|
|
274
|
+
}
|
|
275
|
+
export function documentToSvg(doc) {
|
|
276
|
+
const body = documentBody(doc);
|
|
267
277
|
return (`<svg xmlns="http://www.w3.org/2000/svg" width="${doc.width}" height="${doc.height}" ` +
|
|
268
278
|
`viewBox="0 0 ${doc.width} ${doc.height}">\n` +
|
|
269
279
|
` <rect x="0" y="0" width="${doc.width}" height="${doc.height}" fill="${escapeXml(doc.background)}" />\n` +
|
|
270
280
|
(body ? `${body}\n` : "") +
|
|
271
281
|
`</svg>`);
|
|
272
282
|
}
|
|
283
|
+
/**
|
|
284
|
+
* SVG cadré sur une zone rectangulaire de la planche, en coordonnées document.
|
|
285
|
+
* Tout ce qui recoupe la zone y figure, coupé à ses bords ; les éléments
|
|
286
|
+
* semi-transparents qui ne la recoupent pas sont omis.
|
|
287
|
+
*/
|
|
288
|
+
export function regionToSvg(doc, region) {
|
|
289
|
+
const body = documentBody(doc, region);
|
|
290
|
+
const width = round(Math.max(1, region.width));
|
|
291
|
+
const height = round(Math.max(1, region.height));
|
|
292
|
+
return (`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" ` +
|
|
293
|
+
`viewBox="${round(region.x)} ${round(region.y)} ${width} ${height}">\n` +
|
|
294
|
+
` <rect x="${round(region.x)}" y="${round(region.y)}" width="${width}" height="${height}" ` +
|
|
295
|
+
`fill="${escapeXml(doc.background)}" />\n` +
|
|
296
|
+
(body ? `${body}\n` : "") +
|
|
297
|
+
`</svg>`);
|
|
298
|
+
}
|
|
273
299
|
/**
|
|
274
300
|
* SVG cadré sur un seul élément — l'élément et son contenu, rien d'autre.
|
|
275
301
|
*
|