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.
- package/diagram/diagrams.js +240 -0
- package/diagram/events.js +603 -0
- package/diagram/globals.js +137 -0
- package/diagram/links.js +141 -0
- package/diagram/persistence.js +295 -0
- package/diagram/render.js +633 -0
- package/diagram/shape-ops.js +236 -0
- package/diagram/text-edit.js +292 -0
- package/diagram.html +8 -1
- package/package.json +8 -8
- package/diagram.js +0 -2556
|
@@ -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
|
+
}
|