living-documentation 4.2.0 → 4.4.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 living-documentation might be problematic. Click here for more details.
- package/README.md +7 -6
- package/dist/src/frontend/diagram/main.js +2 -1
- package/dist/src/frontend/diagram/network.js +167 -12
- package/dist/src/frontend/diagram/node-rendering.js +440 -167
- package/dist/src/frontend/diagram/persistence.js +4 -2
- package/dist/src/frontend/diagram/state.js +1 -0
- package/dist/src/frontend/diagram.html +10 -0
- package/dist/src/frontend/index.html +63 -12
- package/dist/src/frontend/wordcloud.js +581 -209
- package/dist/src/routes/browse.d.ts.map +1 -1
- package/dist/src/routes/browse.js +2 -1
- package/dist/src/routes/browse.js.map +1 -1
- package/dist/src/routes/diagrams.d.ts.map +1 -1
- package/dist/src/routes/diagrams.js +2 -1
- package/dist/src/routes/diagrams.js.map +1 -1
- package/dist/src/routes/wordcloud.d.ts.map +1 -1
- package/dist/src/routes/wordcloud.js +42 -20
- package/dist/src/routes/wordcloud.js.map +1 -1
- package/package.json +1 -1
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
// (vis-network caches the ctxRenderer reference and never re-reads it from the
|
|
5
5
|
// DataSet, so dimensions/rotation/alignment must be fetched at draw time).
|
|
6
6
|
|
|
7
|
-
import { NODE_COLORS } from
|
|
8
|
-
import { st } from
|
|
7
|
+
import { NODE_COLORS } from "./constants.js";
|
|
8
|
+
import { st } from "./state.js";
|
|
9
9
|
|
|
10
10
|
// ── Link indicator ────────────────────────────────────────────────────────────
|
|
11
11
|
// Small chain icon drawn at bottom-right of any node that has a nodeLink.
|
|
@@ -16,48 +16,95 @@ function drawLinkIndicator(ctx, id, W, H) {
|
|
|
16
16
|
const bx = W / 2 - r;
|
|
17
17
|
const by = H / 2 - r;
|
|
18
18
|
ctx.save();
|
|
19
|
-
ctx.fillStyle
|
|
20
|
-
ctx.strokeStyle =
|
|
21
|
-
ctx.lineWidth
|
|
22
|
-
ctx.beginPath();
|
|
23
|
-
ctx.
|
|
24
|
-
ctx.
|
|
25
|
-
ctx.
|
|
19
|
+
ctx.fillStyle = n.nodeLink.type === "url" ? "#3b82f6" : "#f97316";
|
|
20
|
+
ctx.strokeStyle = "#fff";
|
|
21
|
+
ctx.lineWidth = 1;
|
|
22
|
+
ctx.beginPath();
|
|
23
|
+
ctx.arc(bx, by, r, 0, Math.PI * 2);
|
|
24
|
+
ctx.fill();
|
|
25
|
+
ctx.stroke();
|
|
26
|
+
ctx.strokeStyle = "#fff";
|
|
27
|
+
ctx.lineWidth = 1.2;
|
|
28
|
+
ctx.lineCap = "round";
|
|
26
29
|
// Tiny link icon inside the badge
|
|
27
30
|
ctx.beginPath();
|
|
28
|
-
ctx.moveTo(bx - 1.5, by + 1.5);
|
|
29
|
-
ctx.
|
|
30
|
-
ctx.moveTo(bx
|
|
31
|
+
ctx.moveTo(bx - 1.5, by + 1.5);
|
|
32
|
+
ctx.lineTo(bx + 1.5, by - 1.5);
|
|
33
|
+
ctx.moveTo(bx - 2.5, by - 0.5);
|
|
34
|
+
ctx.lineTo(bx - 0.5, by - 2.5);
|
|
35
|
+
ctx.moveTo(bx + 0.5, by + 2.5);
|
|
36
|
+
ctx.lineTo(bx + 2.5, by + 0.5);
|
|
31
37
|
ctx.stroke();
|
|
32
38
|
ctx.restore();
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
// ── Drawing helpers ───────────────────────────────────────────────────────────
|
|
36
42
|
|
|
43
|
+
// Break a single text line into wrapped sub-lines that fit within maxWidth pixels.
|
|
44
|
+
// Requires ctx.font to be set before calling.
|
|
45
|
+
function wrapWords(ctx, text, maxWidth) {
|
|
46
|
+
if (!text) return [""];
|
|
47
|
+
const words = text.split(" ");
|
|
48
|
+
const result = [];
|
|
49
|
+
let current = "";
|
|
50
|
+
for (const word of words) {
|
|
51
|
+
const candidate = current ? current + " " + word : word;
|
|
52
|
+
if (current && ctx.measureText(candidate).width > maxWidth) {
|
|
53
|
+
result.push(current);
|
|
54
|
+
current = word;
|
|
55
|
+
} else {
|
|
56
|
+
current = candidate;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (current) result.push(current);
|
|
60
|
+
return result.length ? result : [""];
|
|
61
|
+
}
|
|
62
|
+
|
|
37
63
|
// Draw multi-line label centred at (0,0) in the current (possibly rotated) ctx.
|
|
38
64
|
// labelRotation is applied around the label's own centre (independent of shape rotation).
|
|
39
|
-
function drawLabel(
|
|
65
|
+
function drawLabel(
|
|
66
|
+
ctx,
|
|
67
|
+
label,
|
|
68
|
+
fontSize,
|
|
69
|
+
color,
|
|
70
|
+
textAlign,
|
|
71
|
+
textValign,
|
|
72
|
+
W,
|
|
73
|
+
H,
|
|
74
|
+
labelRotation,
|
|
75
|
+
) {
|
|
40
76
|
if (!label) return;
|
|
41
77
|
const pad = 8;
|
|
42
78
|
ctx.save();
|
|
43
|
-
ctx.font
|
|
44
|
-
ctx.fillStyle
|
|
45
|
-
ctx.textBaseline =
|
|
79
|
+
ctx.font = `${fontSize}px system-ui,-apple-system,sans-serif`;
|
|
80
|
+
ctx.fillStyle = color;
|
|
81
|
+
ctx.textBaseline = "middle";
|
|
46
82
|
|
|
47
83
|
let xPos = 0;
|
|
48
|
-
if (textAlign ===
|
|
49
|
-
|
|
50
|
-
|
|
84
|
+
if (textAlign === "left") {
|
|
85
|
+
ctx.textAlign = "left";
|
|
86
|
+
xPos = W ? -W / 2 + pad : -40;
|
|
87
|
+
} else if (textAlign === "right") {
|
|
88
|
+
ctx.textAlign = "right";
|
|
89
|
+
xPos = W ? W / 2 - pad : 40;
|
|
90
|
+
} else {
|
|
91
|
+
ctx.textAlign = "center";
|
|
92
|
+
xPos = 0;
|
|
93
|
+
}
|
|
51
94
|
|
|
52
95
|
let yOff = 0;
|
|
53
96
|
if (W && H) {
|
|
54
|
-
if (textValign ===
|
|
55
|
-
else if (textValign ===
|
|
97
|
+
if (textValign === "top") yOff = -(H / 2 - fontSize / 2 - pad);
|
|
98
|
+
else if (textValign === "bottom") yOff = H / 2 - fontSize / 2 - pad;
|
|
56
99
|
}
|
|
57
100
|
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
const
|
|
101
|
+
// Auto-wrap: split each explicit \n line further if it overflows the shape width.
|
|
102
|
+
const maxW = W ? W - pad * 2 : Infinity;
|
|
103
|
+
const lines = String(label)
|
|
104
|
+
.split("\n")
|
|
105
|
+
.flatMap((l) => wrapWords(ctx, l, maxW));
|
|
106
|
+
const lineH = fontSize * 1.3;
|
|
107
|
+
const startY = yOff - ((lines.length - 1) * lineH) / 2;
|
|
61
108
|
|
|
62
109
|
// Apply independent label rotation around the label centre.
|
|
63
110
|
if (labelRotation) ctx.rotate(labelRotation);
|
|
@@ -87,18 +134,18 @@ function roundRect(ctx, x, y, w, h, r) {
|
|
|
87
134
|
// nodes.update(), so colour, font size, dimensions, rotation, and alignment
|
|
88
135
|
// must all be fetched here to reflect the latest values.
|
|
89
136
|
function nodeData(id, defaultW, defaultH, defaultColorKey) {
|
|
90
|
-
const n
|
|
91
|
-
const colorKey = (n && n.colorKey) || defaultColorKey ||
|
|
137
|
+
const n = st.nodes && st.nodes.get(id);
|
|
138
|
+
const colorKey = (n && n.colorKey) || defaultColorKey || "c-gray";
|
|
92
139
|
return {
|
|
93
|
-
W:
|
|
94
|
-
H:
|
|
95
|
-
rotation:
|
|
96
|
-
labelRotation: (n && n.labelRotation)
|
|
97
|
-
textAlign:
|
|
98
|
-
textValign:
|
|
99
|
-
fontSize:
|
|
140
|
+
W: (n && n.nodeWidth) || defaultW,
|
|
141
|
+
H: (n && n.nodeHeight) || defaultH,
|
|
142
|
+
rotation: (n && n.rotation) || 0,
|
|
143
|
+
labelRotation: (n && n.labelRotation) || 0,
|
|
144
|
+
textAlign: (n && n.textAlign) || "center",
|
|
145
|
+
textValign: (n && n.textValign) || "middle",
|
|
146
|
+
fontSize: (n && n.fontSize) || 13,
|
|
100
147
|
colorKey,
|
|
101
|
-
c: NODE_COLORS[colorKey] || NODE_COLORS[
|
|
148
|
+
c: NODE_COLORS[colorKey] || NODE_COLORS["c-gray"],
|
|
102
149
|
};
|
|
103
150
|
}
|
|
104
151
|
|
|
@@ -106,16 +153,38 @@ function nodeData(id, defaultW, defaultH, defaultColorKey) {
|
|
|
106
153
|
|
|
107
154
|
export function makeBoxRenderer(colorKey) {
|
|
108
155
|
return function ({ ctx, x, y, id, state: visState, label }) {
|
|
109
|
-
const {
|
|
156
|
+
const {
|
|
157
|
+
W,
|
|
158
|
+
H,
|
|
159
|
+
rotation,
|
|
160
|
+
labelRotation,
|
|
161
|
+
textAlign,
|
|
162
|
+
textValign,
|
|
163
|
+
fontSize,
|
|
164
|
+
c,
|
|
165
|
+
} = nodeData(id, 100, 40, colorKey);
|
|
110
166
|
return {
|
|
111
167
|
drawNode() {
|
|
112
|
-
ctx.save();
|
|
113
|
-
ctx.
|
|
114
|
-
ctx.
|
|
115
|
-
ctx.
|
|
168
|
+
ctx.save();
|
|
169
|
+
ctx.translate(x, y);
|
|
170
|
+
ctx.rotate(rotation);
|
|
171
|
+
ctx.strokeStyle = visState.selected ? "#f97316" : c.border;
|
|
172
|
+
ctx.fillStyle = visState.selected ? c.hbg : c.bg;
|
|
173
|
+
ctx.lineWidth = 1.5;
|
|
116
174
|
roundRect(ctx, -W / 2, -H / 2, W, H, 4);
|
|
117
|
-
ctx.fill();
|
|
118
|
-
|
|
175
|
+
ctx.fill();
|
|
176
|
+
ctx.stroke();
|
|
177
|
+
drawLabel(
|
|
178
|
+
ctx,
|
|
179
|
+
label,
|
|
180
|
+
fontSize,
|
|
181
|
+
c.font,
|
|
182
|
+
textAlign,
|
|
183
|
+
textValign,
|
|
184
|
+
W,
|
|
185
|
+
H,
|
|
186
|
+
labelRotation,
|
|
187
|
+
);
|
|
119
188
|
drawLinkIndicator(ctx, id, W, H);
|
|
120
189
|
ctx.restore();
|
|
121
190
|
},
|
|
@@ -126,16 +195,39 @@ export function makeBoxRenderer(colorKey) {
|
|
|
126
195
|
|
|
127
196
|
export function makeEllipseRenderer(colorKey) {
|
|
128
197
|
return function ({ ctx, x, y, id, state: visState, label }) {
|
|
129
|
-
const {
|
|
198
|
+
const {
|
|
199
|
+
W,
|
|
200
|
+
H,
|
|
201
|
+
rotation,
|
|
202
|
+
labelRotation,
|
|
203
|
+
textAlign,
|
|
204
|
+
textValign,
|
|
205
|
+
fontSize,
|
|
206
|
+
c,
|
|
207
|
+
} = nodeData(id, 110, 50, colorKey);
|
|
130
208
|
return {
|
|
131
209
|
drawNode() {
|
|
132
|
-
ctx.save();
|
|
133
|
-
ctx.
|
|
134
|
-
ctx.
|
|
135
|
-
ctx.
|
|
136
|
-
ctx.
|
|
137
|
-
ctx.
|
|
138
|
-
|
|
210
|
+
ctx.save();
|
|
211
|
+
ctx.translate(x, y);
|
|
212
|
+
ctx.rotate(rotation);
|
|
213
|
+
ctx.strokeStyle = visState.selected ? "#f97316" : c.border;
|
|
214
|
+
ctx.fillStyle = visState.selected ? c.hbg : c.bg;
|
|
215
|
+
ctx.lineWidth = 1.5;
|
|
216
|
+
ctx.beginPath();
|
|
217
|
+
ctx.ellipse(0, 0, W / 2, H / 2, 0, 0, Math.PI * 2);
|
|
218
|
+
ctx.fill();
|
|
219
|
+
ctx.stroke();
|
|
220
|
+
drawLabel(
|
|
221
|
+
ctx,
|
|
222
|
+
label,
|
|
223
|
+
fontSize,
|
|
224
|
+
c.font,
|
|
225
|
+
textAlign,
|
|
226
|
+
textValign,
|
|
227
|
+
W,
|
|
228
|
+
H,
|
|
229
|
+
labelRotation,
|
|
230
|
+
);
|
|
139
231
|
drawLinkIndicator(ctx, id, W, H);
|
|
140
232
|
ctx.restore();
|
|
141
233
|
},
|
|
@@ -146,17 +238,32 @@ export function makeEllipseRenderer(colorKey) {
|
|
|
146
238
|
|
|
147
239
|
export function makeCircleRenderer(colorKey) {
|
|
148
240
|
return function ({ ctx, x, y, id, state: visState, label }) {
|
|
149
|
-
const { W, rotation, labelRotation, textAlign, textValign, fontSize, c } =
|
|
241
|
+
const { W, rotation, labelRotation, textAlign, textValign, fontSize, c } =
|
|
242
|
+
nodeData(id, 55, 55, colorKey);
|
|
150
243
|
const R = W / 2;
|
|
151
244
|
return {
|
|
152
245
|
drawNode() {
|
|
153
|
-
ctx.save();
|
|
154
|
-
ctx.
|
|
155
|
-
ctx.
|
|
156
|
-
ctx.
|
|
157
|
-
ctx.
|
|
158
|
-
ctx.
|
|
159
|
-
|
|
246
|
+
ctx.save();
|
|
247
|
+
ctx.translate(x, y);
|
|
248
|
+
ctx.rotate(rotation);
|
|
249
|
+
ctx.strokeStyle = visState.selected ? "#f97316" : c.border;
|
|
250
|
+
ctx.fillStyle = visState.selected ? c.hbg : c.bg;
|
|
251
|
+
ctx.lineWidth = 1.5;
|
|
252
|
+
ctx.beginPath();
|
|
253
|
+
ctx.arc(0, 0, R, 0, Math.PI * 2);
|
|
254
|
+
ctx.fill();
|
|
255
|
+
ctx.stroke();
|
|
256
|
+
drawLabel(
|
|
257
|
+
ctx,
|
|
258
|
+
label,
|
|
259
|
+
fontSize,
|
|
260
|
+
c.font,
|
|
261
|
+
textAlign,
|
|
262
|
+
textValign,
|
|
263
|
+
W,
|
|
264
|
+
W,
|
|
265
|
+
labelRotation,
|
|
266
|
+
);
|
|
160
267
|
drawLinkIndicator(ctx, id, W, W);
|
|
161
268
|
ctx.restore();
|
|
162
269
|
},
|
|
@@ -167,27 +274,54 @@ export function makeCircleRenderer(colorKey) {
|
|
|
167
274
|
|
|
168
275
|
export function makeDatabaseRenderer(colorKey) {
|
|
169
276
|
return function ({ ctx, x, y, id, state: visState, label }) {
|
|
170
|
-
const {
|
|
277
|
+
const {
|
|
278
|
+
W,
|
|
279
|
+
H,
|
|
280
|
+
rotation,
|
|
281
|
+
labelRotation,
|
|
282
|
+
textAlign,
|
|
283
|
+
textValign,
|
|
284
|
+
fontSize,
|
|
285
|
+
c,
|
|
286
|
+
} = nodeData(id, 50, 70, colorKey);
|
|
171
287
|
const rx = W / 2;
|
|
172
288
|
const ry = Math.max(H * 0.12, 6);
|
|
173
|
-
const bodyTop
|
|
174
|
-
const bodyBottom =
|
|
289
|
+
const bodyTop = -H / 2 + ry;
|
|
290
|
+
const bodyBottom = H / 2 - ry;
|
|
175
291
|
return {
|
|
176
292
|
drawNode() {
|
|
177
|
-
ctx.save();
|
|
178
|
-
ctx.
|
|
179
|
-
ctx.
|
|
180
|
-
ctx.
|
|
293
|
+
ctx.save();
|
|
294
|
+
ctx.translate(x, y);
|
|
295
|
+
ctx.rotate(rotation);
|
|
296
|
+
ctx.strokeStyle = visState.selected ? "#f97316" : c.border;
|
|
297
|
+
ctx.fillStyle = visState.selected ? c.hbg : c.bg;
|
|
298
|
+
ctx.lineWidth = 1.5;
|
|
181
299
|
ctx.fillRect(-rx, bodyTop, W, bodyBottom - bodyTop);
|
|
182
|
-
ctx.beginPath(); ctx.ellipse(0, bodyBottom, rx, ry, 0, 0, Math.PI * 2);
|
|
183
|
-
ctx.fill(); ctx.stroke();
|
|
184
300
|
ctx.beginPath();
|
|
185
|
-
ctx.
|
|
186
|
-
ctx.
|
|
301
|
+
ctx.ellipse(0, bodyBottom, rx, ry, 0, 0, Math.PI * 2);
|
|
302
|
+
ctx.fill();
|
|
303
|
+
ctx.stroke();
|
|
304
|
+
ctx.beginPath();
|
|
305
|
+
ctx.moveTo(-rx, bodyTop);
|
|
306
|
+
ctx.lineTo(-rx, bodyBottom);
|
|
307
|
+
ctx.moveTo(rx, bodyTop);
|
|
308
|
+
ctx.lineTo(rx, bodyBottom);
|
|
309
|
+
ctx.stroke();
|
|
310
|
+
ctx.beginPath();
|
|
311
|
+
ctx.ellipse(0, bodyTop, rx, ry, 0, 0, Math.PI * 2);
|
|
312
|
+
ctx.fill();
|
|
187
313
|
ctx.stroke();
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
314
|
+
drawLabel(
|
|
315
|
+
ctx,
|
|
316
|
+
label,
|
|
317
|
+
fontSize,
|
|
318
|
+
c.font,
|
|
319
|
+
textAlign,
|
|
320
|
+
textValign,
|
|
321
|
+
W,
|
|
322
|
+
H,
|
|
323
|
+
labelRotation,
|
|
324
|
+
);
|
|
191
325
|
drawLinkIndicator(ctx, id, W, H);
|
|
192
326
|
ctx.restore();
|
|
193
327
|
},
|
|
@@ -199,27 +333,39 @@ export function makeDatabaseRenderer(colorKey) {
|
|
|
199
333
|
// Post-IT: sticky note with folded top-right corner.
|
|
200
334
|
export function makePostItRenderer(colorKey) {
|
|
201
335
|
return function ({ ctx, x, y, id, state: visState, label }) {
|
|
202
|
-
const {
|
|
336
|
+
const {
|
|
337
|
+
W,
|
|
338
|
+
H,
|
|
339
|
+
rotation,
|
|
340
|
+
labelRotation,
|
|
341
|
+
textAlign,
|
|
342
|
+
textValign,
|
|
343
|
+
fontSize,
|
|
344
|
+
c,
|
|
345
|
+
} = nodeData(id, 120, 100, colorKey || "c-amber");
|
|
203
346
|
const fold = Math.min(W, H) * 0.18;
|
|
204
347
|
return {
|
|
205
348
|
drawNode() {
|
|
206
|
-
ctx.save();
|
|
207
|
-
ctx.
|
|
208
|
-
ctx.
|
|
209
|
-
ctx.
|
|
349
|
+
ctx.save();
|
|
350
|
+
ctx.translate(x, y);
|
|
351
|
+
ctx.rotate(rotation);
|
|
352
|
+
ctx.strokeStyle = visState.selected ? "#f97316" : c.border;
|
|
353
|
+
ctx.fillStyle = visState.selected ? c.hbg : c.bg;
|
|
354
|
+
ctx.lineWidth = 1.5;
|
|
210
355
|
ctx.beginPath();
|
|
211
|
-
ctx.moveTo(-W / 2,
|
|
212
|
-
ctx.lineTo(
|
|
213
|
-
ctx.lineTo(
|
|
214
|
-
ctx.lineTo(
|
|
215
|
-
ctx.lineTo(-W / 2,
|
|
356
|
+
ctx.moveTo(-W / 2, -H / 2);
|
|
357
|
+
ctx.lineTo(W / 2 - fold, -H / 2);
|
|
358
|
+
ctx.lineTo(W / 2, -H / 2 + fold);
|
|
359
|
+
ctx.lineTo(W / 2, H / 2);
|
|
360
|
+
ctx.lineTo(-W / 2, H / 2);
|
|
216
361
|
ctx.closePath();
|
|
217
|
-
ctx.fill();
|
|
362
|
+
ctx.fill();
|
|
363
|
+
ctx.stroke();
|
|
218
364
|
ctx.globalAlpha = 0.3;
|
|
219
|
-
ctx.fillStyle
|
|
365
|
+
ctx.fillStyle = c.border;
|
|
220
366
|
ctx.beginPath();
|
|
221
367
|
ctx.moveTo(W / 2 - fold, -H / 2);
|
|
222
|
-
ctx.lineTo(W / 2,
|
|
368
|
+
ctx.lineTo(W / 2, -H / 2 + fold);
|
|
223
369
|
ctx.lineTo(W / 2 - fold, -H / 2 + fold);
|
|
224
370
|
ctx.closePath();
|
|
225
371
|
ctx.fill();
|
|
@@ -227,9 +373,19 @@ export function makePostItRenderer(colorKey) {
|
|
|
227
373
|
ctx.beginPath();
|
|
228
374
|
ctx.moveTo(W / 2 - fold, -H / 2);
|
|
229
375
|
ctx.lineTo(W / 2 - fold, -H / 2 + fold);
|
|
230
|
-
ctx.lineTo(W / 2,
|
|
376
|
+
ctx.lineTo(W / 2, -H / 2 + fold);
|
|
231
377
|
ctx.stroke();
|
|
232
|
-
drawLabel(
|
|
378
|
+
drawLabel(
|
|
379
|
+
ctx,
|
|
380
|
+
label,
|
|
381
|
+
fontSize,
|
|
382
|
+
c.font,
|
|
383
|
+
textAlign,
|
|
384
|
+
textValign,
|
|
385
|
+
W,
|
|
386
|
+
H,
|
|
387
|
+
labelRotation,
|
|
388
|
+
);
|
|
233
389
|
drawLinkIndicator(ctx, id, W, H);
|
|
234
390
|
ctx.restore();
|
|
235
391
|
},
|
|
@@ -241,19 +397,40 @@ export function makePostItRenderer(colorKey) {
|
|
|
241
397
|
// Free Text: no visible border or background — just the label.
|
|
242
398
|
export function makeTextFreeRenderer(colorKey) {
|
|
243
399
|
return function ({ ctx, x, y, id, state: visState, label }) {
|
|
244
|
-
const {
|
|
400
|
+
const {
|
|
401
|
+
W,
|
|
402
|
+
H,
|
|
403
|
+
rotation,
|
|
404
|
+
labelRotation,
|
|
405
|
+
textAlign,
|
|
406
|
+
textValign,
|
|
407
|
+
fontSize,
|
|
408
|
+
c,
|
|
409
|
+
} = nodeData(id, 80, 30, colorKey);
|
|
245
410
|
return {
|
|
246
411
|
drawNode() {
|
|
247
|
-
ctx.save();
|
|
412
|
+
ctx.save();
|
|
413
|
+
ctx.translate(x, y);
|
|
414
|
+
ctx.rotate(rotation);
|
|
248
415
|
if (visState.selected || visState.hover) {
|
|
249
|
-
ctx.strokeStyle =
|
|
250
|
-
ctx.lineWidth
|
|
416
|
+
ctx.strokeStyle = "#f97316";
|
|
417
|
+
ctx.lineWidth = 1;
|
|
251
418
|
ctx.setLineDash([4, 3]);
|
|
252
419
|
roundRect(ctx, -W / 2, -H / 2, W, H, 3);
|
|
253
420
|
ctx.stroke();
|
|
254
421
|
ctx.setLineDash([]);
|
|
255
422
|
}
|
|
256
|
-
drawLabel(
|
|
423
|
+
drawLabel(
|
|
424
|
+
ctx,
|
|
425
|
+
label,
|
|
426
|
+
fontSize,
|
|
427
|
+
c.font,
|
|
428
|
+
textAlign,
|
|
429
|
+
textValign,
|
|
430
|
+
W,
|
|
431
|
+
H,
|
|
432
|
+
labelRotation,
|
|
433
|
+
);
|
|
257
434
|
drawLinkIndicator(ctx, id, W, H);
|
|
258
435
|
ctx.restore();
|
|
259
436
|
},
|
|
@@ -268,31 +445,53 @@ const ACTOR_H0 = 52;
|
|
|
268
445
|
|
|
269
446
|
export function makeActorRenderer(colorKey) {
|
|
270
447
|
return function ({ ctx, x, y, id, state: visState, label }) {
|
|
271
|
-
const { W, H, rotation, labelRotation, fontSize, c } = nodeData(
|
|
448
|
+
const { W, H, rotation, labelRotation, fontSize, c } = nodeData(
|
|
449
|
+
id,
|
|
450
|
+
ACTOR_W0,
|
|
451
|
+
ACTOR_H0,
|
|
452
|
+
colorKey,
|
|
453
|
+
);
|
|
272
454
|
const sx = W / ACTOR_W0;
|
|
273
455
|
const sy = H / ACTOR_H0;
|
|
274
456
|
return {
|
|
275
457
|
drawNode() {
|
|
276
|
-
ctx.save();
|
|
277
|
-
ctx.
|
|
278
|
-
ctx.
|
|
279
|
-
ctx.
|
|
280
|
-
ctx.
|
|
281
|
-
ctx.
|
|
282
|
-
ctx.
|
|
283
|
-
ctx.beginPath();
|
|
284
|
-
ctx.
|
|
285
|
-
ctx.
|
|
458
|
+
ctx.save();
|
|
459
|
+
ctx.translate(x, y);
|
|
460
|
+
ctx.rotate(rotation);
|
|
461
|
+
ctx.strokeStyle = visState.selected ? "#f97316" : c.border;
|
|
462
|
+
ctx.fillStyle = visState.selected ? c.hbg : c.bg;
|
|
463
|
+
ctx.lineWidth = 2;
|
|
464
|
+
ctx.lineCap = "round";
|
|
465
|
+
ctx.beginPath();
|
|
466
|
+
ctx.arc(0, -20 * sy, 8 * sy, 0, Math.PI * 2);
|
|
467
|
+
ctx.fill();
|
|
468
|
+
ctx.stroke();
|
|
469
|
+
ctx.beginPath();
|
|
470
|
+
ctx.moveTo(0, -12 * sy);
|
|
471
|
+
ctx.lineTo(0, 8 * sy);
|
|
472
|
+
ctx.stroke();
|
|
473
|
+
ctx.beginPath();
|
|
474
|
+
ctx.moveTo(-13 * sx, 0 * sy);
|
|
475
|
+
ctx.lineTo(13 * sx, 0 * sy);
|
|
476
|
+
ctx.stroke();
|
|
477
|
+
ctx.beginPath();
|
|
478
|
+
ctx.moveTo(0, 8 * sy);
|
|
479
|
+
ctx.lineTo(-10 * sx, 24 * sy);
|
|
480
|
+
ctx.stroke();
|
|
481
|
+
ctx.beginPath();
|
|
482
|
+
ctx.moveTo(0, 8 * sy);
|
|
483
|
+
ctx.lineTo(10 * sx, 24 * sy);
|
|
484
|
+
ctx.stroke();
|
|
286
485
|
// Label below figure (rotates with the actor)
|
|
287
486
|
if (label) {
|
|
288
487
|
ctx.save();
|
|
289
488
|
if (labelRotation) ctx.rotate(labelRotation);
|
|
290
|
-
ctx.font
|
|
291
|
-
ctx.fillStyle
|
|
292
|
-
ctx.textAlign
|
|
293
|
-
ctx.textBaseline =
|
|
294
|
-
const lines
|
|
295
|
-
const lineH
|
|
489
|
+
ctx.font = `${fontSize}px system-ui,-apple-system,sans-serif`;
|
|
490
|
+
ctx.fillStyle = c.font;
|
|
491
|
+
ctx.textAlign = "center";
|
|
492
|
+
ctx.textBaseline = "top";
|
|
493
|
+
const lines = String(label).split("\n");
|
|
494
|
+
const lineH = fontSize * 1.3;
|
|
296
495
|
const startY = 24 * sy + 4;
|
|
297
496
|
lines.forEach((line, i) => ctx.fillText(line, 0, startY + i * lineH));
|
|
298
497
|
ctx.restore();
|
|
@@ -314,28 +513,44 @@ const _imgCache = new Map(); // src → HTMLImageElement | 'loading' | 'error'
|
|
|
314
513
|
function getCachedImage(src, redrawFn) {
|
|
315
514
|
if (!src) return null;
|
|
316
515
|
const cached = _imgCache.get(src);
|
|
317
|
-
if (cached ===
|
|
516
|
+
if (cached === "loading" || cached === "error") return null;
|
|
318
517
|
if (cached) return cached;
|
|
319
|
-
_imgCache.set(src,
|
|
518
|
+
_imgCache.set(src, "loading");
|
|
320
519
|
const img = new Image();
|
|
321
|
-
img.onload
|
|
322
|
-
|
|
520
|
+
img.onload = () => {
|
|
521
|
+
_imgCache.set(src, img);
|
|
522
|
+
redrawFn && redrawFn();
|
|
523
|
+
};
|
|
524
|
+
img.onerror = () => {
|
|
525
|
+
_imgCache.set(src, "error");
|
|
526
|
+
};
|
|
323
527
|
img.src = src;
|
|
324
528
|
return null;
|
|
325
529
|
}
|
|
326
530
|
|
|
327
531
|
export function makeImageRenderer(colorKey) {
|
|
328
532
|
return function ({ ctx, x, y, id, state: visState, label }) {
|
|
329
|
-
const {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
533
|
+
const {
|
|
534
|
+
W,
|
|
535
|
+
H,
|
|
536
|
+
rotation,
|
|
537
|
+
labelRotation,
|
|
538
|
+
textAlign,
|
|
539
|
+
textValign,
|
|
540
|
+
fontSize,
|
|
541
|
+
c,
|
|
542
|
+
} = nodeData(id, 160, 120, colorKey || "c-gray");
|
|
543
|
+
const n = st.nodes && st.nodes.get(id);
|
|
544
|
+
const src = n && n.imageSrc;
|
|
545
|
+
const img = getCachedImage(src, () => st.network && st.network.redraw());
|
|
333
546
|
return {
|
|
334
547
|
drawNode() {
|
|
335
|
-
ctx.save();
|
|
548
|
+
ctx.save();
|
|
549
|
+
ctx.translate(x, y);
|
|
550
|
+
ctx.rotate(rotation);
|
|
336
551
|
// Border (always visible, orange when selected)
|
|
337
|
-
ctx.strokeStyle = visState.selected ?
|
|
338
|
-
ctx.lineWidth
|
|
552
|
+
ctx.strokeStyle = visState.selected ? "#f97316" : c.border;
|
|
553
|
+
ctx.lineWidth = visState.selected ? 2 : 1;
|
|
339
554
|
roundRect(ctx, -W / 2, -H / 2, W, H, 4);
|
|
340
555
|
ctx.stroke();
|
|
341
556
|
|
|
@@ -353,44 +568,48 @@ export function makeImageRenderer(colorKey) {
|
|
|
353
568
|
ctx.fill();
|
|
354
569
|
ctx.fillStyle = c.border;
|
|
355
570
|
ctx.font = `${Math.round(Math.min(W, H) * 0.25)}px system-ui`;
|
|
356
|
-
ctx.textAlign
|
|
357
|
-
ctx.textBaseline =
|
|
358
|
-
ctx.fillText(src ?
|
|
571
|
+
ctx.textAlign = "center";
|
|
572
|
+
ctx.textBaseline = "middle";
|
|
573
|
+
ctx.fillText(src ? "…" : "🖼", 0, 0);
|
|
359
574
|
}
|
|
360
575
|
|
|
361
576
|
if (label) {
|
|
362
|
-
const lines
|
|
363
|
-
const lineH
|
|
364
|
-
const pad
|
|
577
|
+
const lines = String(label).split("\n");
|
|
578
|
+
const lineH = fontSize * 1.3;
|
|
579
|
+
const pad = 6;
|
|
365
580
|
const stripH = lines.length * lineH + pad * 2 - (lineH - fontSize);
|
|
366
581
|
ctx.save();
|
|
367
582
|
ctx.font = `${fontSize}px system-ui,-apple-system,sans-serif`;
|
|
368
|
-
const maxTextW = Math.max(
|
|
369
|
-
|
|
583
|
+
const maxTextW = Math.max(
|
|
584
|
+
...lines.map((l) => ctx.measureText(l).width),
|
|
585
|
+
);
|
|
586
|
+
const stripW = maxTextW + pad * 2;
|
|
370
587
|
const M = 5; // margin from image edge
|
|
371
588
|
// Horizontal position based on textAlign
|
|
372
589
|
let stripX;
|
|
373
|
-
if (textAlign ===
|
|
374
|
-
else if (textAlign ===
|
|
375
|
-
else
|
|
590
|
+
if (textAlign === "left") stripX = -W / 2 + M;
|
|
591
|
+
else if (textAlign === "right") stripX = W / 2 - stripW - M;
|
|
592
|
+
else stripX = -stripW / 2;
|
|
376
593
|
// Vertical position based on textValign
|
|
377
594
|
let stripY;
|
|
378
|
-
if (textValign ===
|
|
379
|
-
else if (textValign ===
|
|
380
|
-
else
|
|
595
|
+
if (textValign === "top") stripY = -H / 2 + M;
|
|
596
|
+
else if (textValign === "bottom") stripY = H / 2 - stripH - M;
|
|
597
|
+
else stripY = -stripH / 2;
|
|
381
598
|
ctx.globalAlpha = 0.7;
|
|
382
|
-
ctx.fillStyle
|
|
599
|
+
ctx.fillStyle = "#000";
|
|
383
600
|
roundRect(ctx, stripX, stripY, stripW, stripH, 4);
|
|
384
601
|
ctx.fill();
|
|
385
602
|
ctx.globalAlpha = 1;
|
|
386
603
|
// Draw text centered inside the box
|
|
387
604
|
if (labelRotation) ctx.rotate(labelRotation);
|
|
388
|
-
ctx.fillStyle
|
|
389
|
-
ctx.textAlign
|
|
390
|
-
ctx.textBaseline =
|
|
605
|
+
ctx.fillStyle = "#fff";
|
|
606
|
+
ctx.textAlign = "center";
|
|
607
|
+
ctx.textBaseline = "middle";
|
|
391
608
|
const textCX = stripX + stripW / 2;
|
|
392
609
|
const startY = stripY + pad + fontSize / 2;
|
|
393
|
-
lines.forEach((line, i) =>
|
|
610
|
+
lines.forEach((line, i) =>
|
|
611
|
+
ctx.fillText(line, textCX, startY + i * lineH),
|
|
612
|
+
);
|
|
394
613
|
ctx.restore();
|
|
395
614
|
}
|
|
396
615
|
drawLinkIndicator(ctx, id, W, H);
|
|
@@ -412,55 +631,109 @@ export function getActualNodeHeight(id) {
|
|
|
412
631
|
}
|
|
413
632
|
|
|
414
633
|
// Kept for backward compat (called by node-panel but irrelevant for ctxRenderers).
|
|
415
|
-
export function computeVadjust() {
|
|
634
|
+
export function computeVadjust() {
|
|
635
|
+
return 0;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// ── Anchor renderer — small dot endpoint for free-floating edges ──────────────
|
|
639
|
+
// nodeDimensions matches the visual radius so vis-network places the arrowhead
|
|
640
|
+
// exactly at the dot's border ("planted" effect).
|
|
641
|
+
// The dot is filled with the canvas background colour to mask the line that
|
|
642
|
+
// vis-network draws from the arrowhead to the node centre.
|
|
643
|
+
function makeAnchorRenderer() {
|
|
644
|
+
return ({ ctx, x, y, state: { selected, hover } }) => {
|
|
645
|
+
const r = 4;
|
|
646
|
+
return {
|
|
647
|
+
drawNode() {
|
|
648
|
+
if (!selected && !hover) return; // invisible at rest
|
|
649
|
+
ctx.save();
|
|
650
|
+
ctx.translate(x, y);
|
|
651
|
+
ctx.beginPath();
|
|
652
|
+
ctx.arc(0, 0, r, 0, Math.PI * 2);
|
|
653
|
+
ctx.fillStyle = "#f97316";
|
|
654
|
+
ctx.fill();
|
|
655
|
+
ctx.restore();
|
|
656
|
+
},
|
|
657
|
+
nodeDimensions: { width: 0, height: 0 },
|
|
658
|
+
};
|
|
659
|
+
};
|
|
660
|
+
}
|
|
416
661
|
|
|
417
662
|
const RENDERER_MAP = {
|
|
418
|
-
box:
|
|
419
|
-
ellipse:
|
|
420
|
-
circle:
|
|
421
|
-
database:
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
actor:
|
|
425
|
-
image:
|
|
663
|
+
box: makeBoxRenderer,
|
|
664
|
+
ellipse: makeEllipseRenderer,
|
|
665
|
+
circle: makeCircleRenderer,
|
|
666
|
+
database: makeDatabaseRenderer,
|
|
667
|
+
"post-it": makePostItRenderer,
|
|
668
|
+
"text-free": makeTextFreeRenderer,
|
|
669
|
+
actor: makeActorRenderer,
|
|
670
|
+
image: makeImageRenderer,
|
|
671
|
+
anchor: makeAnchorRenderer,
|
|
426
672
|
};
|
|
427
673
|
|
|
428
674
|
// Default dimensions per shape type (used when nodeWidth/nodeHeight are null).
|
|
429
675
|
export const SHAPE_DEFAULTS = {
|
|
430
|
-
box:
|
|
431
|
-
ellipse:
|
|
432
|
-
circle:
|
|
433
|
-
database:
|
|
434
|
-
actor:
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
image:
|
|
676
|
+
box: [100, 40],
|
|
677
|
+
ellipse: [110, 50],
|
|
678
|
+
circle: [55, 55],
|
|
679
|
+
database: [50, 70],
|
|
680
|
+
actor: [30, 52],
|
|
681
|
+
"post-it": [120, 100],
|
|
682
|
+
"text-free": [80, 30],
|
|
683
|
+
image: [160, 120],
|
|
684
|
+
anchor: [8, 8],
|
|
438
685
|
};
|
|
439
686
|
|
|
440
687
|
// Builds the full vis.js node property object.
|
|
441
688
|
// All shapes are rendered via ctxRenderer so rotation works uniformly.
|
|
442
|
-
export function visNodeProps(
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
689
|
+
export function visNodeProps(
|
|
690
|
+
shapeType,
|
|
691
|
+
colorKey,
|
|
692
|
+
nodeWidth,
|
|
693
|
+
nodeHeight,
|
|
694
|
+
fontSize,
|
|
695
|
+
textAlign,
|
|
696
|
+
_textValign,
|
|
697
|
+
) {
|
|
698
|
+
const c = NODE_COLORS[colorKey] || NODE_COLORS["c-gray"];
|
|
699
|
+
const size = fontSize || 13;
|
|
700
|
+
const align = textAlign || "center";
|
|
446
701
|
|
|
447
702
|
const colorP = {
|
|
448
703
|
color: {
|
|
449
|
-
background: c.bg,
|
|
450
|
-
|
|
451
|
-
|
|
704
|
+
background: c.bg,
|
|
705
|
+
border: c.border,
|
|
706
|
+
highlight: { background: c.hbg, border: c.hborder },
|
|
707
|
+
hover: { background: c.hbg, border: c.hborder },
|
|
708
|
+
},
|
|
709
|
+
font: {
|
|
710
|
+
color: c.font,
|
|
711
|
+
size,
|
|
712
|
+
face: "system-ui,-apple-system,sans-serif",
|
|
713
|
+
align,
|
|
452
714
|
},
|
|
453
|
-
font: { color: c.font, size, face: 'system-ui,-apple-system,sans-serif', align },
|
|
454
715
|
};
|
|
455
716
|
|
|
456
717
|
const sizeP = {};
|
|
457
|
-
if (nodeWidth)
|
|
458
|
-
|
|
718
|
+
if (nodeWidth)
|
|
719
|
+
sizeP.widthConstraint = { minimum: nodeWidth, maximum: nodeWidth };
|
|
720
|
+
if (nodeHeight)
|
|
721
|
+
sizeP.heightConstraint = { minimum: nodeHeight, maximum: nodeHeight };
|
|
459
722
|
|
|
460
723
|
const factory = RENDERER_MAP[shapeType];
|
|
461
724
|
if (factory) {
|
|
462
|
-
return {
|
|
725
|
+
return {
|
|
726
|
+
shape: "custom",
|
|
727
|
+
ctxRenderer: factory(colorKey),
|
|
728
|
+
...colorP,
|
|
729
|
+
...sizeP,
|
|
730
|
+
};
|
|
463
731
|
}
|
|
464
732
|
// Unknown shape — fall back to box
|
|
465
|
-
return {
|
|
733
|
+
return {
|
|
734
|
+
shape: "custom",
|
|
735
|
+
ctxRenderer: makeBoxRenderer(colorKey),
|
|
736
|
+
...colorP,
|
|
737
|
+
...sizeP,
|
|
738
|
+
};
|
|
466
739
|
}
|