doc-survival-kit 2.0.0 → 3.11.0

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.

Potentially problematic release.


This version of doc-survival-kit might be problematic. Click here for more details.

@@ -0,0 +1,633 @@
1
+ // ══════════════════════════════════════
2
+ // render.js — Rendu SVG, pan/zoom
3
+ // ══════════════════════════════════════
4
+
5
+ // ── Coordonnées SVG ──
6
+ function svgPoint(clientX, clientY) {
7
+ var svg = document.getElementById("canvas");
8
+ var rect = svg.getBoundingClientRect();
9
+ return {
10
+ x: (clientX - rect.left - viewTransform.x) / viewTransform.scale,
11
+ y: (clientY - rect.top - viewTransform.y) / viewTransform.scale,
12
+ };
13
+ }
14
+
15
+ function updateViewport() {
16
+ var vp = document.getElementById("viewport");
17
+ vp.setAttribute(
18
+ "transform",
19
+ "translate(" + viewTransform.x + "," + viewTransform.y + ") scale(" + viewTransform.scale + ")"
20
+ );
21
+ document.getElementById("zoomLevel").textContent =
22
+ Math.round(viewTransform.scale * 100) + "%";
23
+ updateTableOverlay();
24
+ }
25
+
26
+ // ── Zoom ──
27
+ function zoomIn() { applyZoom(1.2, null, null); }
28
+ function zoomOut() { applyZoom(1 / 1.2, null, null); }
29
+ function resetZoom() { viewTransform = { x: 60, y: 60, scale: 1 }; updateViewport(); saveCurrentZoom(); }
30
+
31
+ function applyZoom(factor, cx, cy) {
32
+ var newScale = Math.min(4, Math.max(0.15, viewTransform.scale * factor));
33
+ if (cx !== null && cy !== null) {
34
+ viewTransform.x = cx - (cx - viewTransform.x) * (newScale / viewTransform.scale);
35
+ viewTransform.y = cy - (cy - viewTransform.y) * (newScale / viewTransform.scale);
36
+ }
37
+ viewTransform.scale = newScale;
38
+ updateViewport();
39
+ saveCurrentZoom();
40
+ }
41
+
42
+ // ── Rendu des formes ──
43
+ function renderShape(shape) {
44
+ var c = COLORS[shape.color] || COLORS[DEFAULT_COLOR];
45
+ var isSel = selectedIds.indexOf(shape.id) !== -1;
46
+ var stroke = isSel ? "#f97316" : c.stroke;
47
+ var sw = isSel ? 2.5 : 1.5;
48
+
49
+ var g = createSVGEl("g");
50
+ g.setAttribute("data-id", shape.id);
51
+ g.setAttribute("data-type", "shape");
52
+ g.classList.add("shape-group");
53
+
54
+ // ── Fond selon le type ──
55
+ if (shape.type === "rect" || shape.type === "rounded") {
56
+ var rx = shape.type === "rounded" ? 12 : 3;
57
+ var r = createSVGEl("rect");
58
+ r.setAttribute("x", shape.x); r.setAttribute("y", shape.y);
59
+ r.setAttribute("width", shape.w); r.setAttribute("height", shape.h);
60
+ r.setAttribute("rx", rx);
61
+ r.setAttribute("fill", c.fill); r.setAttribute("stroke", stroke);
62
+ r.setAttribute("stroke-width", sw);
63
+ g.appendChild(r);
64
+
65
+ } else if (shape.type === "db") {
66
+ var ry = Math.min(12, shape.h * 0.2);
67
+ var cx = shape.x + shape.w / 2;
68
+ // Corps : path latéral + arc bas
69
+ var body = createSVGEl("path");
70
+ body.setAttribute("d", [
71
+ "M", shape.x, shape.y + ry,
72
+ "L", shape.x, shape.y + shape.h - ry,
73
+ "A", shape.w / 2, ry, 0, 0, 0, shape.x + shape.w, shape.y + shape.h - ry,
74
+ "L", shape.x + shape.w, shape.y + ry,
75
+ "A", shape.w / 2, ry, 0, 0, 0, shape.x, shape.y + ry,
76
+ "Z",
77
+ ].join(" "));
78
+ body.setAttribute("fill", c.fill);
79
+ body.setAttribute("stroke", stroke);
80
+ body.setAttribute("stroke-width", sw);
81
+ g.appendChild(body);
82
+ // Ellipse du haut (face visible)
83
+ var topEl = createSVGEl("ellipse");
84
+ topEl.setAttribute("cx", cx); topEl.setAttribute("cy", shape.y + ry);
85
+ topEl.setAttribute("rx", shape.w / 2); topEl.setAttribute("ry", ry);
86
+ topEl.setAttribute("fill", c.fill);
87
+ topEl.setAttribute("stroke", stroke); topEl.setAttribute("stroke-width", sw);
88
+ g.appendChild(topEl);
89
+
90
+ } else if (shape.type === "cloud") {
91
+ // Ellipse en trait continu = service externe / cloud
92
+ var el = createSVGEl("ellipse");
93
+ el.setAttribute("cx", shape.x + shape.w / 2);
94
+ el.setAttribute("cy", shape.y + shape.h / 2);
95
+ el.setAttribute("rx", shape.w / 2);
96
+ el.setAttribute("ry", shape.h / 2);
97
+ el.setAttribute("fill", c.fill);
98
+ el.setAttribute("stroke", stroke);
99
+ el.setAttribute("stroke-width", sw);
100
+ g.appendChild(el);
101
+
102
+ } else if (shape.type === "nuage") {
103
+ // Nuage — contour extérieur du Bootstrap cloud icon (viewBox 0 0 16 16)
104
+ // Le path se referme exactement : endpoint = startpoint, Z explicite
105
+ var nuageG = createSVGEl("g");
106
+ nuageG.setAttribute("transform",
107
+ "translate(" + shape.x + "," + shape.y + ") scale(" + (shape.w / 16) + "," + (shape.h / 16) + ")"
108
+ );
109
+ var nuagePathEl = createSVGEl("path");
110
+ nuagePathEl.setAttribute("d",
111
+ "M4.406 3.342A5.53 5.53 0 0 1 8 2c2.69 0 4.923 2 5.166 4.579" +
112
+ "C14.758 6.804 16 8.137 16 9.773 16 11.569 14.502 13 12.687 13" +
113
+ "H3.781C1.708 13 0 11.366 0 9.318" +
114
+ "c0-1.763 1.266-3.223 2.942-3.593.143-.863.698-1.723 1.464-2.383Z"
115
+ );
116
+ nuagePathEl.setAttribute("fill", c.fill);
117
+ nuagePathEl.setAttribute("stroke", stroke);
118
+ nuagePathEl.setAttribute("stroke-width", sw);
119
+ nuagePathEl.setAttribute("vector-effect", "non-scaling-stroke");
120
+ nuageG.appendChild(nuagePathEl);
121
+ g.appendChild(nuageG);
122
+
123
+ } else if (shape.type === "actor") {
124
+ // Bonhomme UML (stick figure) — pas de fond, juste les traits
125
+ var acx = shape.x + shape.w / 2;
126
+ var aHeadR = Math.min(shape.w * 0.20, shape.h * 0.14);
127
+ var aHeadCY = shape.y + aHeadR + shape.h * 0.02;
128
+ // Tête
129
+ var aHead = createSVGEl("circle");
130
+ aHead.setAttribute("cx", acx);
131
+ aHead.setAttribute("cy", aHeadCY);
132
+ aHead.setAttribute("r", aHeadR);
133
+ aHead.setAttribute("fill", c.fill);
134
+ aHead.setAttribute("stroke", stroke);
135
+ aHead.setAttribute("stroke-width", sw);
136
+ g.appendChild(aHead);
137
+ // Corps
138
+ var aBodyTop = aHeadCY + aHeadR;
139
+ var aBodyBot = shape.y + shape.h * 0.60;
140
+ var aBody = createSVGEl("line");
141
+ aBody.setAttribute("x1", acx); aBody.setAttribute("y1", aBodyTop);
142
+ aBody.setAttribute("x2", acx); aBody.setAttribute("y2", aBodyBot);
143
+ aBody.setAttribute("stroke", stroke); aBody.setAttribute("stroke-width", sw);
144
+ aBody.setAttribute("stroke-linecap", "round");
145
+ g.appendChild(aBody);
146
+ // Bras
147
+ var aArmY = shape.y + shape.h * 0.38;
148
+ var aArmLX = shape.x + shape.w * 0.10;
149
+ var aArmRX = shape.x + shape.w * 0.90;
150
+ var aArms = createSVGEl("line");
151
+ aArms.setAttribute("x1", aArmLX); aArms.setAttribute("y1", aArmY);
152
+ aArms.setAttribute("x2", aArmRX); aArms.setAttribute("y2", aArmY);
153
+ aArms.setAttribute("stroke", stroke); aArms.setAttribute("stroke-width", sw);
154
+ aArms.setAttribute("stroke-linecap", "round");
155
+ g.appendChild(aArms);
156
+ // Jambe gauche
157
+ var aLegLX = shape.x + shape.w * 0.18;
158
+ var aLegRX = shape.x + shape.w * 0.82;
159
+ var aLegBotY = shape.y + shape.h * 0.82;
160
+ var aLegL = createSVGEl("line");
161
+ aLegL.setAttribute("x1", acx); aLegL.setAttribute("y1", aBodyBot);
162
+ aLegL.setAttribute("x2", aLegLX); aLegL.setAttribute("y2", aLegBotY);
163
+ aLegL.setAttribute("stroke", stroke); aLegL.setAttribute("stroke-width", sw);
164
+ aLegL.setAttribute("stroke-linecap", "round");
165
+ g.appendChild(aLegL);
166
+ // Jambe droite
167
+ var aLegR = createSVGEl("line");
168
+ aLegR.setAttribute("x1", acx); aLegR.setAttribute("y1", aBodyBot);
169
+ aLegR.setAttribute("x2", aLegRX); aLegR.setAttribute("y2", aLegBotY);
170
+ aLegR.setAttribute("stroke", stroke); aLegR.setAttribute("stroke-width", sw);
171
+ aLegR.setAttribute("stroke-linecap", "round");
172
+ g.appendChild(aLegR);
173
+
174
+ } else if (shape.type === "table") {
175
+ var tRows = shape.rows || 3;
176
+ var tCols = shape.cols || 3;
177
+ var tCells = shape.cells || [];
178
+ var tColWidths = getColWidths(shape);
179
+ var tColOffsets = getColOffsets(shape);
180
+ var cellH = shape.h / tRows;
181
+ var tfs = shape.fontSize || 12;
182
+ // Fond
183
+ var tBg = createSVGEl("rect");
184
+ tBg.setAttribute("x", shape.x); tBg.setAttribute("y", shape.y);
185
+ tBg.setAttribute("width", shape.w); tBg.setAttribute("height", shape.h);
186
+ tBg.setAttribute("rx", 3);
187
+ tBg.setAttribute("fill", c.fill); tBg.setAttribute("stroke", stroke);
188
+ tBg.setAttribute("stroke-width", sw);
189
+ g.appendChild(tBg);
190
+ // Lignes horizontales internes
191
+ for (var tri = 1; tri < tRows; tri++) {
192
+ var thl = createSVGEl("line");
193
+ thl.setAttribute("x1", shape.x); thl.setAttribute("y1", shape.y + tri * cellH);
194
+ thl.setAttribute("x2", shape.x + shape.w); thl.setAttribute("y2", shape.y + tri * cellH);
195
+ thl.setAttribute("stroke", stroke); thl.setAttribute("stroke-width", sw * 0.5);
196
+ g.appendChild(thl);
197
+ }
198
+ // Lignes verticales internes
199
+ for (var tci = 1; tci < tCols; tci++) {
200
+ var tvl = createSVGEl("line");
201
+ var tvlX = shape.x + tColOffsets[tci];
202
+ tvl.setAttribute("x1", tvlX); tvl.setAttribute("y1", shape.y);
203
+ tvl.setAttribute("x2", tvlX); tvl.setAttribute("y2", shape.y + shape.h);
204
+ tvl.setAttribute("stroke", stroke); tvl.setAttribute("stroke-width", sw * 0.5);
205
+ g.appendChild(tvl);
206
+ }
207
+ // Textes des cellules avec word wrap
208
+ var clineH = Math.round(tfs * 1.33);
209
+ for (var tri2 = 0; tri2 < tRows; tri2++) {
210
+ for (var tci2 = 0; tci2 < tCols; tci2++) {
211
+ var cellVal = (tCells[tri2] && tCells[tri2][tci2]) || "";
212
+ if (!cellVal) continue;
213
+ var cellW2 = tColWidths[tci2];
214
+ var cpad = 6;
215
+ var clines = wrapPostitLines(cellVal, cellW2 - cpad * 2, tfs);
216
+ var cCenterY = shape.y + tri2 * cellH + cellH / 2;
217
+ var cfirstY = cCenterY - clines.length * clineH / 2 + clineH * 0.5 + tfs * 0.5;
218
+ var cellCenterX = shape.x + tColOffsets[tci2] + cellW2 / 2;
219
+ var ctxt = createSVGEl("text");
220
+ ctxt.setAttribute("x", cellCenterX);
221
+ ctxt.setAttribute("y", cfirstY);
222
+ ctxt.setAttribute("text-anchor", "middle");
223
+ ctxt.setAttribute("fill", c.text);
224
+ ctxt.setAttribute("font-size", tfs);
225
+ ctxt.setAttribute("data-cell", tri2 + "-" + tci2);
226
+ ctxt.setAttribute("font-family", '"Segoe UI",system-ui,sans-serif');
227
+ ctxt.setAttribute("font-weight", "600");
228
+ ctxt.setAttribute("pointer-events", "none");
229
+ (function(cx, lines) {
230
+ lines.forEach(function(line, i) {
231
+ var ts = createSVGEl("tspan");
232
+ ts.setAttribute("x", cx);
233
+ if (i > 0) ts.setAttribute("dy", clineH + "px");
234
+ ts.textContent = line || " ";
235
+ ctxt.appendChild(ts);
236
+ });
237
+ })(cellCenterX, clines);
238
+ g.appendChild(ctxt);
239
+ }
240
+ }
241
+ // Poignées de redimensionnement des colonnes (chevrons au-dessus de chaque séparateur)
242
+ if (isSel && selectedIds.length === 1) {
243
+ for (var tsi = 1; tsi < tCols; tsi++) {
244
+ var hx = shape.x + tColOffsets[tsi];
245
+ var hy = shape.y - 13;
246
+ var hg = createSVGEl("g");
247
+ hg.setAttribute("data-col-sep", tsi - 1);
248
+ hg.setAttribute("data-shape-id", shape.id);
249
+ hg.style.cursor = "col-resize";
250
+ // Zone de clic transparente
251
+ var hArea = createSVGEl("rect");
252
+ hArea.setAttribute("x", hx - 9); hArea.setAttribute("y", hy - 7);
253
+ hArea.setAttribute("width", 18); hArea.setAttribute("height", 22);
254
+ hArea.setAttribute("fill", "transparent");
255
+ hArea.setAttribute("data-col-sep", tsi - 1);
256
+ hArea.setAttribute("data-shape-id", shape.id);
257
+ hg.appendChild(hArea);
258
+ // Chevron ∨
259
+ var chev = createSVGEl("path");
260
+ chev.setAttribute("d", "M" + (hx - 5) + "," + (hy - 1) + " L" + hx + "," + (hy + 5) + " L" + (hx + 5) + "," + (hy - 1));
261
+ chev.setAttribute("fill", "none");
262
+ chev.setAttribute("stroke", "#f97316");
263
+ chev.setAttribute("stroke-width", "1.8");
264
+ chev.setAttribute("stroke-linecap", "round");
265
+ chev.setAttribute("stroke-linejoin", "round");
266
+ chev.setAttribute("pointer-events", "none");
267
+ hg.appendChild(chev);
268
+ // Trait pointillé vers la table
269
+ var hvl = createSVGEl("line");
270
+ hvl.setAttribute("x1", hx); hvl.setAttribute("y1", hy + 5);
271
+ hvl.setAttribute("x2", hx); hvl.setAttribute("y2", shape.y);
272
+ hvl.setAttribute("stroke", "#f97316");
273
+ hvl.setAttribute("stroke-width", "1");
274
+ hvl.setAttribute("stroke-dasharray", "3,2");
275
+ hvl.setAttribute("pointer-events", "none");
276
+ hg.appendChild(hvl);
277
+ g.appendChild(hg);
278
+ }
279
+ }
280
+ // Poignée de redimensionnement
281
+ if (isSel && selectedIds.length === 1) {
282
+ var tGrip = createSVGEl("rect");
283
+ tGrip.setAttribute("x", shape.x + shape.w - 5); tGrip.setAttribute("y", shape.y + shape.h - 5);
284
+ tGrip.setAttribute("width", 10); tGrip.setAttribute("height", 10);
285
+ tGrip.setAttribute("rx", 2);
286
+ tGrip.setAttribute("fill", "#f97316");
287
+ tGrip.setAttribute("stroke", "#fff"); tGrip.setAttribute("stroke-width", 1.5);
288
+ tGrip.classList.add("resize-grip");
289
+ tGrip.setAttribute("data-shape-id", shape.id);
290
+ g.appendChild(tGrip);
291
+ }
292
+ // Points de connexion
293
+ var thcx = shape.x + shape.w / 2, thcy = shape.y + shape.h / 2;
294
+ [[thcx, shape.y], [thcx, shape.y + shape.h], [shape.x, thcy], [shape.x + shape.w, thcy]]
295
+ .forEach(function (pt) {
296
+ var tdot = createSVGEl("circle");
297
+ tdot.setAttribute("cx", pt[0]); tdot.setAttribute("cy", pt[1]);
298
+ tdot.setAttribute("r", 5);
299
+ tdot.setAttribute("fill", "#f97316");
300
+ tdot.setAttribute("stroke", "#fff"); tdot.setAttribute("stroke-width", 1.5);
301
+ tdot.classList.add("conn-dot");
302
+ tdot.setAttribute("data-shape-id", shape.id);
303
+ g.appendChild(tdot);
304
+ });
305
+ return g;
306
+
307
+ } else if (shape.type === "postit") {
308
+ var fold = 18;
309
+ var body = createSVGEl("path");
310
+ body.setAttribute("d", [
311
+ "M", shape.x, shape.y,
312
+ "L", shape.x + shape.w - fold, shape.y,
313
+ "L", shape.x + shape.w, shape.y + fold,
314
+ "L", shape.x + shape.w, shape.y + shape.h,
315
+ "L", shape.x, shape.y + shape.h,
316
+ "Z",
317
+ ].join(" "));
318
+ body.setAttribute("fill", c.fill);
319
+ body.setAttribute("stroke", stroke);
320
+ body.setAttribute("stroke-width", sw);
321
+ g.appendChild(body);
322
+ // Triangle du coin replié
323
+ var foldTri = createSVGEl("path");
324
+ foldTri.setAttribute("d", [
325
+ "M", shape.x + shape.w - fold, shape.y,
326
+ "L", shape.x + shape.w, shape.y + fold,
327
+ "L", shape.x + shape.w - fold, shape.y + fold,
328
+ "Z",
329
+ ].join(" "));
330
+ foldTri.setAttribute("fill", c.stroke);
331
+ foldTri.setAttribute("fill-opacity", "0.25");
332
+ foldTri.setAttribute("stroke", stroke);
333
+ foldTri.setAttribute("stroke-width", sw * 0.7);
334
+ g.appendChild(foldTri);
335
+
336
+ } else if (shape.type === "image") {
337
+ var imgEl = createSVGEl("image");
338
+ imgEl.setAttribute("x", shape.x); imgEl.setAttribute("y", shape.y);
339
+ imgEl.setAttribute("width", shape.w); imgEl.setAttribute("height", shape.h);
340
+ imgEl.setAttribute("href", shape.src);
341
+ imgEl.setAttribute("preserveAspectRatio", "xMidYMid meet");
342
+ g.appendChild(imgEl);
343
+ // Bordure de sélection
344
+ if (isSel) {
345
+ var selRect = createSVGEl("rect");
346
+ selRect.setAttribute("x", shape.x); selRect.setAttribute("y", shape.y);
347
+ selRect.setAttribute("width", shape.w); selRect.setAttribute("height", shape.h);
348
+ selRect.setAttribute("fill", "none");
349
+ selRect.setAttribute("stroke", "#f97316"); selRect.setAttribute("stroke-width", "2");
350
+ selRect.setAttribute("stroke-dasharray", "6,3");
351
+ selRect.setAttribute("pointer-events", "none");
352
+ g.appendChild(selRect);
353
+ }
354
+
355
+ } else if (shape.type === "text") {
356
+ // pas de fond — juste le texte
357
+ }
358
+
359
+ // ── Texte ──
360
+ var textCy = shape.type === "db"
361
+ ? shape.y + shape.h * 0.62
362
+ : shape.type === "actor"
363
+ ? shape.y + shape.h - 2
364
+ : shape.y + shape.h / 2;
365
+ var ta = shape.textAlign || "center";
366
+ var textAnchor = ta === "left" ? "start" : ta === "right" ? "end" : "middle";
367
+ var txt = createSVGEl("text");
368
+ txt.setAttribute("x", shape.x + shape.w / 2);
369
+ txt.setAttribute("y", textCy);
370
+ txt.setAttribute("text-anchor", textAnchor);
371
+ txt.setAttribute("dominant-baseline", "middle");
372
+ var fs = shape.fontSize || (shape.type === "text" ? 13 : 12);
373
+ txt.setAttribute("fill", shape.type === "text" ? "#292524" : c.text);
374
+ txt.setAttribute("font-size", fs);
375
+ txt.setAttribute("font-family", '"Segoe UI",system-ui,sans-serif');
376
+ txt.setAttribute("font-weight", shape.type === "text" ? "400" : "600");
377
+ txt.setAttribute("pointer-events", "none");
378
+ var tv = shape.textValign || "middle";
379
+ if (shape.type === "postit") {
380
+ var pad = 14;
381
+ var lines = wrapPostitLines(shape.text || "", shape.w - pad * 2, fs);
382
+ var lineH = Math.round(fs * 1.42);
383
+ var firstY = tv === "top"
384
+ ? shape.y + pad + lineH * 0.5
385
+ : tv === "bottom"
386
+ ? shape.y + shape.h - pad - (lines.length - 1) * lineH - fs * 0.2
387
+ : shape.y + (shape.h - lines.length * lineH) / 2 + lineH * 0.5 + fs * 0.3;
388
+ var tspanX = ta === "left" ? shape.x + pad : ta === "right" ? shape.x + shape.w - pad : shape.x + shape.w / 2;
389
+ txt.setAttribute("y", firstY);
390
+ txt.removeAttribute("dominant-baseline");
391
+ lines.forEach(function (line, i) {
392
+ var ts = createSVGEl("tspan");
393
+ ts.setAttribute("x", tspanX);
394
+ if (i > 0) ts.setAttribute("dy", lineH + "px");
395
+ ts.textContent = line || " ";
396
+ txt.appendChild(ts);
397
+ });
398
+ } else if (shape.type === "actor") {
399
+ var alines = wrapPostitLines(shape.text || "", shape.w * 1.2, fs);
400
+ var alineH = Math.round(fs * 1.33);
401
+ txt.setAttribute("text-anchor", "middle");
402
+ txt.removeAttribute("dominant-baseline");
403
+ alines.forEach(function (line, i) {
404
+ var ts = createSVGEl("tspan");
405
+ ts.setAttribute("x", shape.x + shape.w / 2);
406
+ if (i > 0) ts.setAttribute("dy", alineH + "px");
407
+ ts.textContent = line || " ";
408
+ txt.appendChild(ts);
409
+ });
410
+ } else if (shape.type === "rect" || shape.type === "rounded" || shape.type === "db" || shape.type === "cloud" || shape.type === "nuage") {
411
+ var wpad = shape.type === "cloud" ? Math.round(shape.w * 0.2) : shape.type === "nuage" ? Math.round(shape.w * 0.12) : 12;
412
+ // Pour nuage : vpad = offset depuis le haut visible (y=2/16) + marge interne
413
+ var wvpad = shape.type === "cloud" ? Math.round(shape.h * 0.12)
414
+ : shape.type === "nuage" ? Math.round(shape.h * 2 / 16) + 6
415
+ : 8;
416
+ var wlines = wrapPostitLines(shape.text || "", shape.w - wpad * 2, fs);
417
+ var wlineH = Math.round(fs * 1.33);
418
+ // Pour nuage : centre de la forme visible = milieu entre y=2/16 et y=13/16
419
+ var wcenterY = shape.type === "db" ? shape.y + shape.h * 0.62
420
+ : shape.type === "nuage" ? shape.y + shape.h * ((2 + 13) / 2 / 16)
421
+ : shape.y + shape.h / 2;
422
+ var dbCapH = shape.type === "db" ? Math.min(12, shape.h * 0.2) * 2 : 0;
423
+ var wfirstY = tv === "top"
424
+ ? shape.y + dbCapH + wvpad + wlineH * 0.5
425
+ : tv === "bottom"
426
+ ? shape.y + shape.h - wvpad - (wlines.length - 1) * wlineH - fs * 0.2
427
+ : wcenterY - wlines.length * wlineH / 2 + wlineH * 0.5 + fs * 0.5;
428
+ var wtspanX = ta === "left" ? shape.x + wpad : ta === "right" ? shape.x + shape.w - wpad : shape.x + shape.w / 2;
429
+ txt.setAttribute("x", wtspanX);
430
+ txt.setAttribute("y", wfirstY);
431
+ txt.removeAttribute("dominant-baseline");
432
+ wlines.forEach(function (line, i) {
433
+ var ts = createSVGEl("tspan");
434
+ ts.setAttribute("x", wtspanX);
435
+ if (i > 0) ts.setAttribute("dy", wlineH + "px");
436
+ ts.textContent = line || " ";
437
+ txt.appendChild(ts);
438
+ });
439
+ } else {
440
+ txt.textContent = shape.text || "";
441
+ }
442
+ g.appendChild(txt);
443
+
444
+ // ── Poignée de redimensionnement (coin bas-droit, sélection unique seulement) ──
445
+ if (isSel && selectedIds.length === 1) {
446
+ var grip = createSVGEl("rect");
447
+ grip.setAttribute("x", shape.x + shape.w - 5);
448
+ grip.setAttribute("y", shape.y + shape.h - 5);
449
+ grip.setAttribute("width", 10); grip.setAttribute("height", 10);
450
+ grip.setAttribute("rx", 2);
451
+ grip.setAttribute("fill", "#f97316");
452
+ grip.setAttribute("stroke", "#fff"); grip.setAttribute("stroke-width", 1.5);
453
+ grip.classList.add("resize-grip");
454
+ grip.setAttribute("data-shape-id", shape.id);
455
+ g.appendChild(grip);
456
+ }
457
+
458
+ // ── Texte non rendu pour les images ──
459
+ if (shape.type === "image") return g;
460
+
461
+ // ── Indicateur de lien (diagramme enfant ou lien externe) ──
462
+ if ((shape.linkedDiagramId || shape.externalUrl) && shape.type !== "image") {
463
+ var lnkColor = shape.externalUrl ? "#0284c7" : "#f97316";
464
+ var lnkCirc = createSVGEl("circle");
465
+ lnkCirc.setAttribute("cx", shape.x + shape.w - 5);
466
+ lnkCirc.setAttribute("cy", shape.y + 5);
467
+ lnkCirc.setAttribute("r", 6);
468
+ lnkCirc.setAttribute("fill", lnkColor);
469
+ lnkCirc.setAttribute("stroke", "#fff");
470
+ lnkCirc.setAttribute("stroke-width", 1.5);
471
+ lnkCirc.setAttribute("pointer-events", "none");
472
+ g.appendChild(lnkCirc);
473
+ var lnkTxt = createSVGEl("text");
474
+ lnkTxt.setAttribute("x", shape.x + shape.w - 5);
475
+ lnkTxt.setAttribute("y", shape.y + 8.5);
476
+ lnkTxt.setAttribute("text-anchor", "middle");
477
+ lnkTxt.setAttribute("font-size", "8");
478
+ lnkTxt.setAttribute("font-weight", "bold");
479
+ lnkTxt.setAttribute("fill", "#fff");
480
+ lnkTxt.setAttribute("pointer-events", "none");
481
+ lnkTxt.textContent = "\u2197";
482
+ g.appendChild(lnkTxt);
483
+ }
484
+
485
+ // ── Rotation ──
486
+ if (shape.rotation) {
487
+ var rcx = shape.x + shape.w / 2, rcy = shape.y + shape.h / 2;
488
+ g.setAttribute("transform", "rotate(" + shape.rotation + "," + rcx + "," + rcy + ")");
489
+ }
490
+
491
+ // ── Points de connexion (visibles au hover et en mode flèche — sauf postit) ──
492
+ if (shape.type !== "postit") {
493
+ var hcx = shape.x + shape.w / 2, hcy = shape.y + shape.h / 2;
494
+ [
495
+ [hcx, shape.y],
496
+ [hcx, shape.y + shape.h],
497
+ [shape.x, hcy],
498
+ [shape.x + shape.w, hcy],
499
+ ].forEach(function (pt) {
500
+ var dot = createSVGEl("circle");
501
+ dot.setAttribute("cx", pt[0]); dot.setAttribute("cy", pt[1]);
502
+ dot.setAttribute("r", 5);
503
+ dot.setAttribute("fill", "#f97316");
504
+ dot.setAttribute("stroke", "#fff"); dot.setAttribute("stroke-width", 1.5);
505
+ dot.classList.add("conn-dot");
506
+ dot.setAttribute("data-shape-id", shape.id);
507
+ g.appendChild(dot);
508
+ });
509
+ }
510
+
511
+ return g;
512
+ }
513
+
514
+ // ── Rendu d'une flèche ──
515
+ function getEdgePoint(shape, targetX, targetY) {
516
+ var cx = shape.x + shape.w / 2;
517
+ var cy = shape.y + shape.h / 2;
518
+ var lTargetX = targetX, lTargetY = targetY;
519
+ if (shape.rotation) {
520
+ var rad = -shape.rotation * Math.PI / 180;
521
+ var cos = Math.cos(rad), sin = Math.sin(rad);
522
+ var ddx = targetX - cx, ddy = targetY - cy;
523
+ lTargetX = cx + ddx * cos - ddy * sin;
524
+ lTargetY = cy + ddx * sin + ddy * cos;
525
+ }
526
+ var dx = lTargetX - cx, dy = lTargetY - cy;
527
+ var p;
528
+ if (dx === 0 && dy === 0) {
529
+ p = { x: cx, y: shape.y };
530
+ } else {
531
+ var hw = shape.w / 2, hh = shape.h / 2;
532
+ if (Math.abs(dx) * hh > Math.abs(dy) * hw) {
533
+ var sx = dx > 0 ? 1 : -1;
534
+ p = { x: cx + sx * hw, y: cy + dy * hw / Math.abs(dx) };
535
+ } else {
536
+ var sy = dy > 0 ? 1 : -1;
537
+ p = { x: cx + dx * hh / Math.abs(dy), y: cy + sy * hh };
538
+ }
539
+ }
540
+ if (shape.rotation) {
541
+ var rad2 = shape.rotation * Math.PI / 180;
542
+ var cos2 = Math.cos(rad2), sin2 = Math.sin(rad2);
543
+ var ex = p.x - cx, ey = p.y - cy;
544
+ p = { x: cx + ex * cos2 - ey * sin2, y: cy + ex * sin2 + ey * cos2 };
545
+ }
546
+ return p;
547
+ }
548
+
549
+ function renderArrow(arrow, shapes) {
550
+ var from = shapes.find(function (s) { return s.id === arrow.from; });
551
+ var to = shapes.find(function (s) { return s.id === arrow.to; });
552
+ if (!from || !to) return null;
553
+
554
+ var toCx = to.x + to.w / 2, toCy = to.y + to.h / 2;
555
+ var frCx = from.x + from.w / 2, frCy = from.y + from.h / 2;
556
+ var fp = getEdgePoint(from, toCx, toCy);
557
+ var tp = getEdgePoint(to, frCx, frCy);
558
+
559
+ // Raccourcir légèrement la pointe pour ne pas chevauchner la forme
560
+ var dx = tp.x - fp.x, dy = tp.y - fp.y;
561
+ var len = Math.sqrt(dx * dx + dy * dy);
562
+ if (len > 16) {
563
+ var off = 7 / len;
564
+ tp = { x: tp.x - dx * off, y: tp.y - dy * off };
565
+ }
566
+
567
+ var isSel = selectedId === arrow.id && selectedType === "arrow";
568
+ var stroke = isSel ? "#f97316" : "#a8a29e";
569
+ var markerId = isSel ? "arrowhead-sel" : "arrowhead";
570
+
571
+ var g = createSVGEl("g");
572
+ g.setAttribute("data-id", arrow.id);
573
+ g.setAttribute("data-type", "arrow");
574
+ g.classList.add("arrow-group");
575
+
576
+ var line = createSVGEl("line");
577
+ line.setAttribute("x1", fp.x); line.setAttribute("y1", fp.y);
578
+ line.setAttribute("x2", tp.x); line.setAttribute("y2", tp.y);
579
+ line.setAttribute("stroke", stroke);
580
+ line.setAttribute("stroke-width", isSel ? 2 : 1.5);
581
+ line.setAttribute("marker-end", "url(#" + markerId + ")");
582
+ g.appendChild(line);
583
+
584
+ // Zone de clic élargie
585
+ var hit = createSVGEl("line");
586
+ hit.setAttribute("x1", fp.x); hit.setAttribute("y1", fp.y);
587
+ hit.setAttribute("x2", tp.x); hit.setAttribute("y2", tp.y);
588
+ hit.setAttribute("stroke", "transparent");
589
+ hit.setAttribute("stroke-width", 14);
590
+ g.appendChild(hit);
591
+
592
+ // Label
593
+ if (arrow.label) {
594
+ var mx = (fp.x + tp.x) / 2, my = (fp.y + tp.y) / 2;
595
+ var lbl = createSVGEl("text");
596
+ lbl.setAttribute("x", mx); lbl.setAttribute("y", my - 7);
597
+ lbl.setAttribute("text-anchor", "middle");
598
+ lbl.setAttribute("font-size", "10");
599
+ lbl.setAttribute("font-family", '"Cascadia Code","SF Mono",Consolas,monospace');
600
+ lbl.setAttribute("fill", "#a8a29e");
601
+ lbl.setAttribute("pointer-events", "none");
602
+ lbl.textContent = arrow.label;
603
+ g.appendChild(lbl);
604
+ }
605
+
606
+ return g;
607
+ }
608
+
609
+ // ── Rendu complet ──
610
+ function renderAll() {
611
+ var diag = getCurrentDiagram();
612
+ if (!diag) return;
613
+
614
+ document.getElementById("diagramTitle").value = diag.titre;
615
+
616
+ var arrowsLayer = document.getElementById("arrowsLayer");
617
+ arrowsLayer.innerHTML = "";
618
+ (diag.arrows || []).forEach(function (a) {
619
+ var el = renderArrow(a, diag.shapes);
620
+ if (el) arrowsLayer.appendChild(el);
621
+ });
622
+
623
+ var shapesLayer = document.getElementById("shapesLayer");
624
+ shapesLayer.innerHTML = "";
625
+ (diag.shapes || []).forEach(function (s) {
626
+ shapesLayer.appendChild(renderShape(s));
627
+ });
628
+
629
+ updateViewport();
630
+ renderDiagramList();
631
+ syncColorPanel();
632
+ updateTableOverlay();
633
+ }