living-documentation 7.0.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.
- package/LICENSE +661 -0
- package/README.md +329 -0
- package/dist/bin/cli.d.ts +3 -0
- package/dist/bin/cli.d.ts.map +1 -0
- package/dist/bin/cli.js +62 -0
- package/dist/bin/cli.js.map +1 -0
- package/dist/src/frontend/admin.html +1073 -0
- package/dist/src/frontend/annotations.js +546 -0
- package/dist/src/frontend/boot.js +90 -0
- package/dist/src/frontend/config.js +19 -0
- package/dist/src/frontend/dark-mode.js +20 -0
- package/dist/src/frontend/diagram/alignment.js +161 -0
- package/dist/src/frontend/diagram/clipboard.js +172 -0
- package/dist/src/frontend/diagram/constants.js +109 -0
- package/dist/src/frontend/diagram/debug.js +43 -0
- package/dist/src/frontend/diagram/edge-panel.js +260 -0
- package/dist/src/frontend/diagram/edge-rendering.js +12 -0
- package/dist/src/frontend/diagram/grid.js +78 -0
- package/dist/src/frontend/diagram/groups.js +102 -0
- package/dist/src/frontend/diagram/history.js +153 -0
- package/dist/src/frontend/diagram/image-name-modal.js +48 -0
- package/dist/src/frontend/diagram/image-upload.js +36 -0
- package/dist/src/frontend/diagram/label-editor.js +115 -0
- package/dist/src/frontend/diagram/link-panel.js +144 -0
- package/dist/src/frontend/diagram/main.js +299 -0
- package/dist/src/frontend/diagram/network.js +1473 -0
- package/dist/src/frontend/diagram/node-panel.js +267 -0
- package/dist/src/frontend/diagram/node-rendering.js +773 -0
- package/dist/src/frontend/diagram/persistence.js +161 -0
- package/dist/src/frontend/diagram/ports.js +386 -0
- package/dist/src/frontend/diagram/selection-overlay.js +336 -0
- package/dist/src/frontend/diagram/state.js +39 -0
- package/dist/src/frontend/diagram/t.js +3 -0
- package/dist/src/frontend/diagram/toast.js +21 -0
- package/dist/src/frontend/diagram/unlock-hold.js +182 -0
- package/dist/src/frontend/diagram/zoom.js +20 -0
- package/dist/src/frontend/diagram-link-modal.js +137 -0
- package/dist/src/frontend/diagram.html +1279 -0
- package/dist/src/frontend/documents.js +373 -0
- package/dist/src/frontend/export.js +338 -0
- package/dist/src/frontend/i18n/en.json +406 -0
- package/dist/src/frontend/i18n/fr.json +406 -0
- package/dist/src/frontend/i18n.js +32 -0
- package/dist/src/frontend/image-paste.js +101 -0
- package/dist/src/frontend/index.html +2314 -0
- package/dist/src/frontend/misc.js +25 -0
- package/dist/src/frontend/new-doc-modal.js +260 -0
- package/dist/src/frontend/new-folder-modal.js +174 -0
- package/dist/src/frontend/search.js +157 -0
- package/dist/src/frontend/sidebar-helpers.js +58 -0
- package/dist/src/frontend/sidebar.js +182 -0
- package/dist/src/frontend/snippet-detect.js +25 -0
- package/dist/src/frontend/snippet-table.js +85 -0
- package/dist/src/frontend/snippet-tree.js +94 -0
- package/dist/src/frontend/snippets.js +534 -0
- package/dist/src/frontend/state.js +28 -0
- package/dist/src/frontend/utils.js +21 -0
- package/dist/src/frontend/vendor/wordcloud2.js +1187 -0
- package/dist/src/frontend/wordcloud.js +693 -0
- package/dist/src/lib/config.d.ts +17 -0
- package/dist/src/lib/config.d.ts.map +1 -0
- package/dist/src/lib/config.js +79 -0
- package/dist/src/lib/config.js.map +1 -0
- package/dist/src/lib/parser.d.ts +11 -0
- package/dist/src/lib/parser.d.ts.map +1 -0
- package/dist/src/lib/parser.js +111 -0
- package/dist/src/lib/parser.js.map +1 -0
- package/dist/src/mcp/server.d.ts +3 -0
- package/dist/src/mcp/server.d.ts.map +1 -0
- package/dist/src/mcp/server.js +986 -0
- package/dist/src/mcp/server.js.map +1 -0
- package/dist/src/mcp/tools/diagrams.d.ts +44 -0
- package/dist/src/mcp/tools/diagrams.d.ts.map +1 -0
- package/dist/src/mcp/tools/diagrams.js +245 -0
- package/dist/src/mcp/tools/diagrams.js.map +1 -0
- package/dist/src/mcp/tools/documents.d.ts +26 -0
- package/dist/src/mcp/tools/documents.d.ts.map +1 -0
- package/dist/src/mcp/tools/documents.js +127 -0
- package/dist/src/mcp/tools/documents.js.map +1 -0
- package/dist/src/mcp/tools/source.d.ts +29 -0
- package/dist/src/mcp/tools/source.d.ts.map +1 -0
- package/dist/src/mcp/tools/source.js +200 -0
- package/dist/src/mcp/tools/source.js.map +1 -0
- package/dist/src/routes/annotations.d.ts +3 -0
- package/dist/src/routes/annotations.d.ts.map +1 -0
- package/dist/src/routes/annotations.js +83 -0
- package/dist/src/routes/annotations.js.map +1 -0
- package/dist/src/routes/browse.d.ts +3 -0
- package/dist/src/routes/browse.d.ts.map +1 -0
- package/dist/src/routes/browse.js +75 -0
- package/dist/src/routes/browse.js.map +1 -0
- package/dist/src/routes/config.d.ts +3 -0
- package/dist/src/routes/config.d.ts.map +1 -0
- package/dist/src/routes/config.js +97 -0
- package/dist/src/routes/config.js.map +1 -0
- package/dist/src/routes/diagrams.d.ts +3 -0
- package/dist/src/routes/diagrams.d.ts.map +1 -0
- package/dist/src/routes/diagrams.js +69 -0
- package/dist/src/routes/diagrams.js.map +1 -0
- package/dist/src/routes/documents.d.ts +8 -0
- package/dist/src/routes/documents.d.ts.map +1 -0
- package/dist/src/routes/documents.js +332 -0
- package/dist/src/routes/documents.js.map +1 -0
- package/dist/src/routes/export.d.ts +3 -0
- package/dist/src/routes/export.d.ts.map +1 -0
- package/dist/src/routes/export.js +277 -0
- package/dist/src/routes/export.js.map +1 -0
- package/dist/src/routes/images.d.ts +3 -0
- package/dist/src/routes/images.d.ts.map +1 -0
- package/dist/src/routes/images.js +49 -0
- package/dist/src/routes/images.js.map +1 -0
- package/dist/src/routes/wordcloud.d.ts +3 -0
- package/dist/src/routes/wordcloud.d.ts.map +1 -0
- package/dist/src/routes/wordcloud.js +95 -0
- package/dist/src/routes/wordcloud.js.map +1 -0
- package/dist/src/server.d.ts +7 -0
- package/dist/src/server.d.ts.map +1 -0
- package/dist/src/server.js +76 -0
- package/dist/src/server.js.map +1 -0
- package/dist/starting-doc/.annotations.json +3 -0
- package/dist/starting-doc/.diagrams.json +1884 -0
- package/dist/starting-doc/.living-doc.json +39 -0
- package/dist/starting-doc/1_tutorial/2026_04_11_13_25_[General]_crer_vos_dossiers.md +16 -0
- package/dist/starting-doc/1_tutorial/2026_04_11_18_58_[General]_creer_un_document_dans_un_dossier.md +9 -0
- package/dist/starting-doc/1_tutorial/2026_04_12_09_00_[General]_editer_et_sauvegarder.md +39 -0
- package/dist/starting-doc/1_tutorial/2026_04_12_10_00_[General]_utiliser_les_snippets.md +71 -0
- package/dist/starting-doc/2026_04_08_20_52_[General]_welcome.md +17 -0
- package/dist/starting-doc/2026_04_11_12_55_[General]_premiers_pas.md +271 -0
- package/dist/starting-doc/2_guide/2026_04_08_00_04_[DOCUMENT]_utilisation_des_images_plein_ecran_lien_clickable.md +40 -0
- package/dist/starting-doc/2_guide/2026_04_08_23_38_[Configuration]_demarrage_de_living_documentation.md +32 -0
- package/dist/starting-doc/2_guide/2026_04_09_09_00_[NAVIGATION]_recherche_plein_texte.md +65 -0
- package/dist/starting-doc/2_guide/2026_04_09_10_00_[EXPORT]_exporter_en_pdf.md +43 -0
- package/dist/starting-doc/2_guide/2026_04_09_11_00_[Configuration]_configurer_le_panneau_admin.md +55 -0
- package/dist/starting-doc/2_guide/2026_04_09_12_00_[Configuration]_extra_files.md +68 -0
- package/dist/starting-doc/2_guide/2026_04_09_13_00_[WORDCLOUD]_word_cloud.md +54 -0
- package/dist/starting-doc/2_guide/2026_04_09_14_00_[DIAGRAM]_creer_et_lier_un_diagramme.md +77 -0
- package/dist/starting-doc/3_concept/2026_04_08_20_58_[DOCUMENTING]_ADRS.md +20 -0
- package/dist/starting-doc/3_concept/2026_04_08_22_15_[DOCUMENTING]_living_documentation.md +17 -0
- package/dist/starting-doc/3_concept/2026_04_08_22_46_[METHODOLOGY]_diataxis_architecture_du_contenu.md +16 -0
- package/dist/starting-doc/4_reference/2026_04_08_23_14_[FUNDAMENTALS]_the_living_documentation_tool.md +41 -0
- package/dist/starting-doc/4_reference/2026_04_09_01_00_[REFERENCE]_raccourcis_clavier.md +61 -0
- package/dist/starting-doc/4_reference/2026_04_09_02_00_[REFERENCE]_tokens_pattern_nommage.md +75 -0
- package/dist/starting-doc/4_reference/2026_04_09_03_00_[REFERENCE]_types_de_snippets.md +68 -0
- package/dist/starting-doc/4_reference/2026_04_11_17_31_[FUNDAMENTALS]_architecturer_une_documentation.md +12 -0
- package/dist/starting-doc/4_reference/2026_04_12_14_07_[FUNDAMENTALS]_dossiers_et_catgories.md +89 -0
- package/dist/starting-doc/images/admin_screenshot.png +0 -0
- package/dist/starting-doc/images/ajout-document.png +0 -0
- package/dist/starting-doc/images/ajouter-document-categorie.png +0 -0
- package/dist/starting-doc/images/ajouter_un_document_dans_un_dossier.png +0 -0
- package/dist/starting-doc/images/architecturer_une_documentation_reference.png +0 -0
- package/dist/starting-doc/images/cr_er_un_document.png +0 -0
- package/dist/starting-doc/images/creation-nouveau-dossier.png +0 -0
- package/dist/starting-doc/images/creer-document-context-engineering.png +0 -0
- package/dist/starting-doc/images/creer-dossier-only-tutoriel.png +0 -0
- package/dist/starting-doc/images/creer-dossier-tutoriel.png +0 -0
- package/dist/starting-doc/images/creer-dossiers-done.png +0 -0
- package/dist/starting-doc/images/creer-un-document.png +0 -0
- package/dist/starting-doc/images/creer-vos-dossiers-tutoriel.png +0 -0
- package/dist/starting-doc/images/creer-vos-dossiers.png +0 -0
- package/dist/starting-doc/images/decouverte_adrs.png +0 -0
- package/dist/starting-doc/images/diataxis.png +0 -0
- package/dist/starting-doc/images/diataxis_callout.png +0 -0
- package/dist/starting-doc/images/document-cree.png +0 -0
- package/dist/starting-doc/images/liens_snippets.png +0 -0
- package/dist/starting-doc/images/living_documentation.png +0 -0
- package/dist/starting-doc/images/npm_logo.png +0 -0
- package/dist/starting-doc/images/popup-creer-document.png +0 -0
- package/dist/starting-doc/images/popup-creer-dossier.png +0 -0
- package/dist/starting-doc/images/popup-dossier-cree.png +0 -0
- package/dist/starting-doc/images/quatre-dossiers-crees.png +0 -0
- package/dist/starting-doc/images/screenshot-living-doc.png +0 -0
- package/dist/starting-doc/images/the_living_documentation_tool.png +0 -0
- package/package.json +49 -0
|
@@ -0,0 +1,773 @@
|
|
|
1
|
+
// ── Node rendering ────────────────────────────────────────────────────────────
|
|
2
|
+
// All shapes are ctxRenderers so rotation (ctx.rotate) works uniformly.
|
|
3
|
+
// Each renderer reads live node data from st.nodes.get(id) on every draw call
|
|
4
|
+
// (vis-network caches the ctxRenderer reference and never re-reads it from the
|
|
5
|
+
// DataSet, so dimensions/rotation/alignment must be fetched at draw time).
|
|
6
|
+
|
|
7
|
+
import { NODE_COLORS } from "./constants.js";
|
|
8
|
+
import { st } from "./state.js";
|
|
9
|
+
|
|
10
|
+
// Returns the color object for a key, checking runtime overrides first.
|
|
11
|
+
function getNodeColor(colorKey) {
|
|
12
|
+
return st.nodeColorOverrides[colorKey] || NODE_COLORS[colorKey] || NODE_COLORS['c-gray'];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Convert a "#rrggbb" hex string into an "rgba(r,g,b,a)" string.
|
|
16
|
+
// Used by renderers to apply a per-node background opacity without touching
|
|
17
|
+
// border/text colors (which stay fully opaque for readability).
|
|
18
|
+
export function hexToRgba(hex, alpha) {
|
|
19
|
+
if (typeof hex !== 'string' || hex.charAt(0) !== '#' || hex.length !== 7) return hex;
|
|
20
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
21
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
22
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
23
|
+
return `rgba(${r},${g},${b},${alpha})`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ── Link indicator ────────────────────────────────────────────────────────────
|
|
27
|
+
// Small chain icon drawn at bottom-right of any node that has a nodeLink.
|
|
28
|
+
function drawLinkIndicator(ctx, id, W, H) {
|
|
29
|
+
const n = st.nodes && st.nodes.get(id);
|
|
30
|
+
if (!n || !n.nodeLink) return;
|
|
31
|
+
const r = 7;
|
|
32
|
+
const bx = W / 2 - r;
|
|
33
|
+
const by = H / 2 - r;
|
|
34
|
+
ctx.save();
|
|
35
|
+
ctx.fillStyle = n.nodeLink.type === "url" ? "#3b82f6" : "#f97316";
|
|
36
|
+
ctx.strokeStyle = "#fff";
|
|
37
|
+
ctx.lineWidth = 1;
|
|
38
|
+
ctx.beginPath();
|
|
39
|
+
ctx.arc(bx, by, r, 0, Math.PI * 2);
|
|
40
|
+
ctx.fill();
|
|
41
|
+
ctx.stroke();
|
|
42
|
+
ctx.strokeStyle = "#fff";
|
|
43
|
+
ctx.lineWidth = 1.2;
|
|
44
|
+
ctx.lineCap = "round";
|
|
45
|
+
// Tiny link icon inside the badge
|
|
46
|
+
ctx.beginPath();
|
|
47
|
+
ctx.moveTo(bx - 1.5, by + 1.5);
|
|
48
|
+
ctx.lineTo(bx + 1.5, by - 1.5);
|
|
49
|
+
ctx.moveTo(bx - 2.5, by - 0.5);
|
|
50
|
+
ctx.lineTo(bx - 0.5, by - 2.5);
|
|
51
|
+
ctx.moveTo(bx + 0.5, by + 2.5);
|
|
52
|
+
ctx.lineTo(bx + 2.5, by + 0.5);
|
|
53
|
+
ctx.stroke();
|
|
54
|
+
ctx.restore();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── Drawing helpers ───────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
// Break a single text line into wrapped sub-lines that fit within maxWidth pixels.
|
|
60
|
+
// Requires ctx.font to be set before calling.
|
|
61
|
+
function wrapWords(ctx, text, maxWidth) {
|
|
62
|
+
if (!text) return [""];
|
|
63
|
+
const words = text.split(" ");
|
|
64
|
+
const result = [];
|
|
65
|
+
let current = "";
|
|
66
|
+
for (const word of words) {
|
|
67
|
+
const candidate = current ? current + " " + word : word;
|
|
68
|
+
if (current && ctx.measureText(candidate).width > maxWidth) {
|
|
69
|
+
result.push(current);
|
|
70
|
+
current = word;
|
|
71
|
+
} else {
|
|
72
|
+
current = candidate;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (current) result.push(current);
|
|
76
|
+
return result.length ? result : [""];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Draw multi-line label centred at (0,0) in the current (possibly rotated) ctx.
|
|
80
|
+
// labelRotation is applied around the label's own centre (independent of shape rotation).
|
|
81
|
+
function drawLabel(
|
|
82
|
+
ctx,
|
|
83
|
+
label,
|
|
84
|
+
fontSize,
|
|
85
|
+
color,
|
|
86
|
+
textAlign,
|
|
87
|
+
textValign,
|
|
88
|
+
W,
|
|
89
|
+
H,
|
|
90
|
+
labelRotation,
|
|
91
|
+
) {
|
|
92
|
+
if (!label) return;
|
|
93
|
+
const pad = 8;
|
|
94
|
+
ctx.save();
|
|
95
|
+
ctx.font = `${fontSize}px system-ui,-apple-system,sans-serif`;
|
|
96
|
+
ctx.fillStyle = color;
|
|
97
|
+
ctx.textBaseline = "middle";
|
|
98
|
+
|
|
99
|
+
let xPos = 0;
|
|
100
|
+
if (textAlign === "left") {
|
|
101
|
+
ctx.textAlign = "left";
|
|
102
|
+
xPos = W ? -W / 2 + pad : -40;
|
|
103
|
+
} else if (textAlign === "right") {
|
|
104
|
+
ctx.textAlign = "right";
|
|
105
|
+
xPos = W ? W / 2 - pad : 40;
|
|
106
|
+
} else {
|
|
107
|
+
ctx.textAlign = "center";
|
|
108
|
+
xPos = 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Auto-wrap first — line count is needed to anchor the block correctly.
|
|
112
|
+
const maxW = W ? W - pad * 2 : Infinity;
|
|
113
|
+
const lines = String(label)
|
|
114
|
+
.split("\n")
|
|
115
|
+
.flatMap((l) => wrapWords(ctx, l, maxW));
|
|
116
|
+
const lineH = fontSize * 1.3;
|
|
117
|
+
|
|
118
|
+
// startY = canvas-y of the CENTRE of the FIRST line.
|
|
119
|
+
// For 'top': first line centre sits at the top + padding.
|
|
120
|
+
// For 'bottom': last line centre sits at the bottom − padding.
|
|
121
|
+
// For 'middle': block is centred at 0.
|
|
122
|
+
let startY;
|
|
123
|
+
if (W && H) {
|
|
124
|
+
if (textValign === "top") {
|
|
125
|
+
startY = -(H / 2 - fontSize / 2 - pad);
|
|
126
|
+
} else if (textValign === "bottom") {
|
|
127
|
+
startY = H / 2 - fontSize / 2 - pad - (lines.length - 1) * lineH;
|
|
128
|
+
} else {
|
|
129
|
+
startY = -((lines.length - 1) * lineH) / 2;
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
startY = -((lines.length - 1) * lineH) / 2;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Apply independent label rotation around the label centre.
|
|
136
|
+
if (labelRotation) ctx.rotate(labelRotation);
|
|
137
|
+
|
|
138
|
+
lines.forEach((line, i) => ctx.fillText(line, xPos, startY + i * lineH));
|
|
139
|
+
ctx.restore();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Polyfill-safe rounded rectangle (ctx.roundRect not universally available).
|
|
143
|
+
function roundRect(ctx, x, y, w, h, r) {
|
|
144
|
+
r = Math.min(r, w / 2, h / 2);
|
|
145
|
+
ctx.beginPath();
|
|
146
|
+
ctx.moveTo(x + r, y);
|
|
147
|
+
ctx.lineTo(x + w - r, y);
|
|
148
|
+
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
|
|
149
|
+
ctx.lineTo(x + w, y + h - r);
|
|
150
|
+
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
|
|
151
|
+
ctx.lineTo(x + r, y + h);
|
|
152
|
+
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
|
|
153
|
+
ctx.lineTo(x, y + r);
|
|
154
|
+
ctx.quadraticCurveTo(x, y, x + r, y);
|
|
155
|
+
ctx.closePath();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Read ALL live node data from the DataSet on every draw call.
|
|
159
|
+
// vis-network caches the ctxRenderer closure and never re-reads it after
|
|
160
|
+
// nodes.update(), so colour, font size, dimensions, rotation, and alignment
|
|
161
|
+
// must all be fetched here to reflect the latest values.
|
|
162
|
+
function nodeData(id, defaultW, defaultH, defaultColorKey) {
|
|
163
|
+
const n = st.nodes && st.nodes.get(id);
|
|
164
|
+
const colorKey = (n && n.colorKey) || defaultColorKey || "c-gray";
|
|
165
|
+
const rawOp = n && n.bgOpacity;
|
|
166
|
+
const bgOpacity = typeof rawOp === "number" ? Math.max(0, Math.min(1, rawOp)) : 1;
|
|
167
|
+
const baseColors = getNodeColor(colorKey);
|
|
168
|
+
const c = bgOpacity < 1
|
|
169
|
+
? { ...baseColors, bg: hexToRgba(baseColors.bg, bgOpacity), hbg: hexToRgba(baseColors.hbg, bgOpacity) }
|
|
170
|
+
: baseColors;
|
|
171
|
+
return {
|
|
172
|
+
W: (n && n.nodeWidth) || defaultW,
|
|
173
|
+
H: (n && n.nodeHeight) || defaultH,
|
|
174
|
+
rotation: (n && n.rotation) || 0,
|
|
175
|
+
labelRotation: (n && n.labelRotation) || 0,
|
|
176
|
+
textAlign: (n && n.textAlign) || "center",
|
|
177
|
+
textValign: (n && n.textValign) || "middle",
|
|
178
|
+
fontSize: (n && n.fontSize) || 13,
|
|
179
|
+
bgOpacity,
|
|
180
|
+
colorKey,
|
|
181
|
+
c,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ── Shape renderers ───────────────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
export function makeBoxRenderer(colorKey) {
|
|
188
|
+
return function ({ ctx, x, y, id, state: visState, label }) {
|
|
189
|
+
const {
|
|
190
|
+
W,
|
|
191
|
+
H,
|
|
192
|
+
rotation,
|
|
193
|
+
labelRotation,
|
|
194
|
+
textAlign,
|
|
195
|
+
textValign,
|
|
196
|
+
fontSize,
|
|
197
|
+
c,
|
|
198
|
+
} = nodeData(id, 100, 40, colorKey);
|
|
199
|
+
return {
|
|
200
|
+
drawNode() {
|
|
201
|
+
ctx.save();
|
|
202
|
+
ctx.translate(x, y);
|
|
203
|
+
ctx.rotate(rotation);
|
|
204
|
+
ctx.strokeStyle = visState.selected ? "#f97316" : c.border;
|
|
205
|
+
ctx.fillStyle = visState.selected ? c.hbg : c.bg;
|
|
206
|
+
ctx.lineWidth = 1.5;
|
|
207
|
+
roundRect(ctx, -W / 2, -H / 2, W, H, 4);
|
|
208
|
+
ctx.fill();
|
|
209
|
+
ctx.stroke();
|
|
210
|
+
drawLabel(
|
|
211
|
+
ctx,
|
|
212
|
+
label,
|
|
213
|
+
fontSize,
|
|
214
|
+
c.font,
|
|
215
|
+
textAlign,
|
|
216
|
+
textValign,
|
|
217
|
+
W,
|
|
218
|
+
H,
|
|
219
|
+
labelRotation,
|
|
220
|
+
);
|
|
221
|
+
drawLinkIndicator(ctx, id, W, H);
|
|
222
|
+
ctx.restore();
|
|
223
|
+
},
|
|
224
|
+
nodeDimensions: { width: W, height: H },
|
|
225
|
+
};
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function makeEllipseRenderer(colorKey) {
|
|
230
|
+
return function ({ ctx, x, y, id, state: visState, label }) {
|
|
231
|
+
const {
|
|
232
|
+
W,
|
|
233
|
+
H,
|
|
234
|
+
rotation,
|
|
235
|
+
labelRotation,
|
|
236
|
+
textAlign,
|
|
237
|
+
textValign,
|
|
238
|
+
fontSize,
|
|
239
|
+
c,
|
|
240
|
+
} = nodeData(id, 110, 50, colorKey);
|
|
241
|
+
return {
|
|
242
|
+
drawNode() {
|
|
243
|
+
ctx.save();
|
|
244
|
+
ctx.translate(x, y);
|
|
245
|
+
ctx.rotate(rotation);
|
|
246
|
+
ctx.strokeStyle = visState.selected ? "#f97316" : c.border;
|
|
247
|
+
ctx.fillStyle = visState.selected ? c.hbg : c.bg;
|
|
248
|
+
ctx.lineWidth = 1.5;
|
|
249
|
+
ctx.beginPath();
|
|
250
|
+
ctx.ellipse(0, 0, W / 2, H / 2, 0, 0, Math.PI * 2);
|
|
251
|
+
ctx.fill();
|
|
252
|
+
ctx.stroke();
|
|
253
|
+
drawLabel(
|
|
254
|
+
ctx,
|
|
255
|
+
label,
|
|
256
|
+
fontSize,
|
|
257
|
+
c.font,
|
|
258
|
+
textAlign,
|
|
259
|
+
textValign,
|
|
260
|
+
W,
|
|
261
|
+
H,
|
|
262
|
+
labelRotation,
|
|
263
|
+
);
|
|
264
|
+
drawLinkIndicator(ctx, id, W, H);
|
|
265
|
+
ctx.restore();
|
|
266
|
+
},
|
|
267
|
+
nodeDimensions: { width: W, height: H },
|
|
268
|
+
};
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function makeCircleRenderer(colorKey) {
|
|
273
|
+
return function ({ ctx, x, y, id, state: visState, label }) {
|
|
274
|
+
const { W, rotation, labelRotation, textAlign, textValign, fontSize, c } =
|
|
275
|
+
nodeData(id, 55, 55, colorKey);
|
|
276
|
+
const R = W / 2;
|
|
277
|
+
return {
|
|
278
|
+
drawNode() {
|
|
279
|
+
ctx.save();
|
|
280
|
+
ctx.translate(x, y);
|
|
281
|
+
ctx.rotate(rotation);
|
|
282
|
+
ctx.strokeStyle = visState.selected ? "#f97316" : c.border;
|
|
283
|
+
ctx.fillStyle = visState.selected ? c.hbg : c.bg;
|
|
284
|
+
ctx.lineWidth = 1.5;
|
|
285
|
+
ctx.beginPath();
|
|
286
|
+
ctx.arc(0, 0, R, 0, Math.PI * 2);
|
|
287
|
+
ctx.fill();
|
|
288
|
+
ctx.stroke();
|
|
289
|
+
drawLabel(
|
|
290
|
+
ctx,
|
|
291
|
+
label,
|
|
292
|
+
fontSize,
|
|
293
|
+
c.font,
|
|
294
|
+
textAlign,
|
|
295
|
+
textValign,
|
|
296
|
+
W,
|
|
297
|
+
W,
|
|
298
|
+
labelRotation,
|
|
299
|
+
);
|
|
300
|
+
drawLinkIndicator(ctx, id, W, W);
|
|
301
|
+
ctx.restore();
|
|
302
|
+
},
|
|
303
|
+
nodeDimensions: { width: W, height: W },
|
|
304
|
+
};
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export function makeDatabaseRenderer(colorKey) {
|
|
309
|
+
return function ({ ctx, x, y, id, state: visState, label }) {
|
|
310
|
+
const {
|
|
311
|
+
W,
|
|
312
|
+
H,
|
|
313
|
+
rotation,
|
|
314
|
+
labelRotation,
|
|
315
|
+
textAlign,
|
|
316
|
+
textValign,
|
|
317
|
+
fontSize,
|
|
318
|
+
c,
|
|
319
|
+
} = nodeData(id, 50, 70, colorKey);
|
|
320
|
+
const rx = W / 2;
|
|
321
|
+
const ry = Math.max(H * 0.12, 6);
|
|
322
|
+
const bodyTop = -H / 2 + ry;
|
|
323
|
+
const bodyBottom = H / 2 - ry;
|
|
324
|
+
return {
|
|
325
|
+
drawNode() {
|
|
326
|
+
ctx.save();
|
|
327
|
+
ctx.translate(x, y);
|
|
328
|
+
ctx.rotate(rotation);
|
|
329
|
+
ctx.strokeStyle = visState.selected ? "#f97316" : c.border;
|
|
330
|
+
ctx.fillStyle = visState.selected ? c.hbg : c.bg;
|
|
331
|
+
ctx.lineWidth = 1.5;
|
|
332
|
+
ctx.fillRect(-rx, bodyTop, W, bodyBottom - bodyTop);
|
|
333
|
+
ctx.beginPath();
|
|
334
|
+
ctx.ellipse(0, bodyBottom, rx, ry, 0, 0, Math.PI * 2);
|
|
335
|
+
ctx.fill();
|
|
336
|
+
ctx.stroke();
|
|
337
|
+
ctx.beginPath();
|
|
338
|
+
ctx.moveTo(-rx, bodyTop);
|
|
339
|
+
ctx.lineTo(-rx, bodyBottom);
|
|
340
|
+
ctx.moveTo(rx, bodyTop);
|
|
341
|
+
ctx.lineTo(rx, bodyBottom);
|
|
342
|
+
ctx.stroke();
|
|
343
|
+
ctx.beginPath();
|
|
344
|
+
ctx.ellipse(0, bodyTop, rx, ry, 0, 0, Math.PI * 2);
|
|
345
|
+
ctx.fill();
|
|
346
|
+
ctx.stroke();
|
|
347
|
+
drawLabel(
|
|
348
|
+
ctx,
|
|
349
|
+
label,
|
|
350
|
+
fontSize,
|
|
351
|
+
c.font,
|
|
352
|
+
textAlign,
|
|
353
|
+
textValign,
|
|
354
|
+
W,
|
|
355
|
+
H,
|
|
356
|
+
labelRotation,
|
|
357
|
+
);
|
|
358
|
+
drawLinkIndicator(ctx, id, W, H);
|
|
359
|
+
ctx.restore();
|
|
360
|
+
},
|
|
361
|
+
nodeDimensions: { width: W, height: H },
|
|
362
|
+
};
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Post-IT: sticky note with folded top-right corner.
|
|
367
|
+
export function makePostItRenderer(colorKey) {
|
|
368
|
+
return function ({ ctx, x, y, id, state: visState, label }) {
|
|
369
|
+
const {
|
|
370
|
+
W,
|
|
371
|
+
H,
|
|
372
|
+
rotation,
|
|
373
|
+
labelRotation,
|
|
374
|
+
textAlign,
|
|
375
|
+
textValign,
|
|
376
|
+
fontSize,
|
|
377
|
+
c,
|
|
378
|
+
} = nodeData(id, 120, 100, colorKey || "c-amber");
|
|
379
|
+
const fold = Math.min(W, H) * 0.18;
|
|
380
|
+
return {
|
|
381
|
+
drawNode() {
|
|
382
|
+
ctx.save();
|
|
383
|
+
ctx.translate(x, y);
|
|
384
|
+
ctx.rotate(rotation);
|
|
385
|
+
ctx.strokeStyle = visState.selected ? "#f97316" : c.border;
|
|
386
|
+
ctx.fillStyle = visState.selected ? c.hbg : c.bg;
|
|
387
|
+
ctx.lineWidth = 1.5;
|
|
388
|
+
ctx.beginPath();
|
|
389
|
+
ctx.moveTo(-W / 2, -H / 2);
|
|
390
|
+
ctx.lineTo(W / 2 - fold, -H / 2);
|
|
391
|
+
ctx.lineTo(W / 2, -H / 2 + fold);
|
|
392
|
+
ctx.lineTo(W / 2, H / 2);
|
|
393
|
+
ctx.lineTo(-W / 2, H / 2);
|
|
394
|
+
ctx.closePath();
|
|
395
|
+
ctx.fill();
|
|
396
|
+
ctx.stroke();
|
|
397
|
+
ctx.globalAlpha = 0.3;
|
|
398
|
+
ctx.fillStyle = c.border;
|
|
399
|
+
ctx.beginPath();
|
|
400
|
+
ctx.moveTo(W / 2 - fold, -H / 2);
|
|
401
|
+
ctx.lineTo(W / 2, -H / 2 + fold);
|
|
402
|
+
ctx.lineTo(W / 2 - fold, -H / 2 + fold);
|
|
403
|
+
ctx.closePath();
|
|
404
|
+
ctx.fill();
|
|
405
|
+
ctx.globalAlpha = 1;
|
|
406
|
+
ctx.beginPath();
|
|
407
|
+
ctx.moveTo(W / 2 - fold, -H / 2);
|
|
408
|
+
ctx.lineTo(W / 2 - fold, -H / 2 + fold);
|
|
409
|
+
ctx.lineTo(W / 2, -H / 2 + fold);
|
|
410
|
+
ctx.stroke();
|
|
411
|
+
drawLabel(
|
|
412
|
+
ctx,
|
|
413
|
+
label,
|
|
414
|
+
fontSize,
|
|
415
|
+
c.font,
|
|
416
|
+
textAlign,
|
|
417
|
+
textValign,
|
|
418
|
+
W,
|
|
419
|
+
H,
|
|
420
|
+
labelRotation,
|
|
421
|
+
);
|
|
422
|
+
drawLinkIndicator(ctx, id, W, H);
|
|
423
|
+
ctx.restore();
|
|
424
|
+
},
|
|
425
|
+
nodeDimensions: { width: W, height: H },
|
|
426
|
+
};
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Free Text: no visible border or background — just the label.
|
|
431
|
+
export function makeTextFreeRenderer(colorKey) {
|
|
432
|
+
return function ({ ctx, x, y, id, state: visState, label }) {
|
|
433
|
+
const {
|
|
434
|
+
W,
|
|
435
|
+
H,
|
|
436
|
+
rotation,
|
|
437
|
+
labelRotation,
|
|
438
|
+
textAlign,
|
|
439
|
+
textValign,
|
|
440
|
+
fontSize,
|
|
441
|
+
c,
|
|
442
|
+
} = nodeData(id, 80, 30, colorKey);
|
|
443
|
+
return {
|
|
444
|
+
drawNode() {
|
|
445
|
+
ctx.save();
|
|
446
|
+
ctx.translate(x, y);
|
|
447
|
+
ctx.rotate(rotation);
|
|
448
|
+
if (visState.selected || visState.hover) {
|
|
449
|
+
ctx.strokeStyle = "#f97316";
|
|
450
|
+
ctx.lineWidth = 1;
|
|
451
|
+
ctx.setLineDash([4, 3]);
|
|
452
|
+
roundRect(ctx, -W / 2, -H / 2, W, H, 3);
|
|
453
|
+
ctx.stroke();
|
|
454
|
+
ctx.setLineDash([]);
|
|
455
|
+
}
|
|
456
|
+
drawLabel(
|
|
457
|
+
ctx,
|
|
458
|
+
label,
|
|
459
|
+
fontSize,
|
|
460
|
+
c.font,
|
|
461
|
+
textAlign,
|
|
462
|
+
textValign,
|
|
463
|
+
W,
|
|
464
|
+
H,
|
|
465
|
+
labelRotation,
|
|
466
|
+
);
|
|
467
|
+
drawLinkIndicator(ctx, id, W, H);
|
|
468
|
+
ctx.restore();
|
|
469
|
+
},
|
|
470
|
+
nodeDimensions: { width: W, height: H },
|
|
471
|
+
};
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// ── Actor (stick figure) ──────────────────────────────────────────────────────
|
|
476
|
+
const ACTOR_W0 = 30;
|
|
477
|
+
const ACTOR_H0 = 52;
|
|
478
|
+
|
|
479
|
+
export function makeActorRenderer(colorKey) {
|
|
480
|
+
return function ({ ctx, x, y, id, state: visState, label }) {
|
|
481
|
+
const { W, H, rotation, labelRotation, fontSize, c } = nodeData(
|
|
482
|
+
id,
|
|
483
|
+
ACTOR_W0,
|
|
484
|
+
ACTOR_H0,
|
|
485
|
+
colorKey,
|
|
486
|
+
);
|
|
487
|
+
const sx = W / ACTOR_W0;
|
|
488
|
+
const sy = H / ACTOR_H0;
|
|
489
|
+
return {
|
|
490
|
+
drawNode() {
|
|
491
|
+
ctx.save();
|
|
492
|
+
ctx.translate(x, y);
|
|
493
|
+
ctx.rotate(rotation);
|
|
494
|
+
ctx.strokeStyle = visState.selected ? "#f97316" : c.border;
|
|
495
|
+
ctx.fillStyle = visState.selected ? c.hbg : c.bg;
|
|
496
|
+
ctx.lineWidth = 2;
|
|
497
|
+
ctx.lineCap = "round";
|
|
498
|
+
ctx.beginPath();
|
|
499
|
+
ctx.arc(0, -20 * sy, 8 * sy, 0, Math.PI * 2);
|
|
500
|
+
ctx.fill();
|
|
501
|
+
ctx.stroke();
|
|
502
|
+
ctx.beginPath();
|
|
503
|
+
ctx.moveTo(0, -12 * sy);
|
|
504
|
+
ctx.lineTo(0, 8 * sy);
|
|
505
|
+
ctx.stroke();
|
|
506
|
+
ctx.beginPath();
|
|
507
|
+
ctx.moveTo(-13 * sx, 0 * sy);
|
|
508
|
+
ctx.lineTo(13 * sx, 0 * sy);
|
|
509
|
+
ctx.stroke();
|
|
510
|
+
ctx.beginPath();
|
|
511
|
+
ctx.moveTo(0, 8 * sy);
|
|
512
|
+
ctx.lineTo(-10 * sx, 24 * sy);
|
|
513
|
+
ctx.stroke();
|
|
514
|
+
ctx.beginPath();
|
|
515
|
+
ctx.moveTo(0, 8 * sy);
|
|
516
|
+
ctx.lineTo(10 * sx, 24 * sy);
|
|
517
|
+
ctx.stroke();
|
|
518
|
+
// Label below figure (rotates with the actor)
|
|
519
|
+
if (label) {
|
|
520
|
+
ctx.save();
|
|
521
|
+
if (labelRotation) ctx.rotate(labelRotation);
|
|
522
|
+
ctx.font = `${fontSize}px system-ui,-apple-system,sans-serif`;
|
|
523
|
+
ctx.fillStyle = c.font;
|
|
524
|
+
ctx.textAlign = "center";
|
|
525
|
+
ctx.textBaseline = "top";
|
|
526
|
+
const lines = String(label).split("\n");
|
|
527
|
+
const lineH = fontSize * 1.3;
|
|
528
|
+
const startY = 24 * sy + 4;
|
|
529
|
+
lines.forEach((line, i) => ctx.fillText(line, 0, startY + i * lineH));
|
|
530
|
+
ctx.restore();
|
|
531
|
+
}
|
|
532
|
+
drawLinkIndicator(ctx, id, W, H);
|
|
533
|
+
ctx.restore();
|
|
534
|
+
},
|
|
535
|
+
nodeDimensions: { width: W, height: H },
|
|
536
|
+
};
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// ── Image node ────────────────────────────────────────────────────────────────
|
|
541
|
+
// Images are loaded once and cached. When still loading a placeholder is drawn;
|
|
542
|
+
// once the image is ready network.redraw() is called so the frame updates.
|
|
543
|
+
|
|
544
|
+
const _imgCache = new Map(); // src → HTMLImageElement | 'loading' | 'error'
|
|
545
|
+
|
|
546
|
+
function getCachedImage(src, redrawFn) {
|
|
547
|
+
if (!src) return null;
|
|
548
|
+
const cached = _imgCache.get(src);
|
|
549
|
+
if (cached === "loading" || cached === "error") return null;
|
|
550
|
+
if (cached) return cached;
|
|
551
|
+
_imgCache.set(src, "loading");
|
|
552
|
+
const img = new Image();
|
|
553
|
+
img.onload = () => {
|
|
554
|
+
_imgCache.set(src, img);
|
|
555
|
+
redrawFn && redrawFn();
|
|
556
|
+
};
|
|
557
|
+
img.onerror = () => {
|
|
558
|
+
_imgCache.set(src, "error");
|
|
559
|
+
};
|
|
560
|
+
img.src = src;
|
|
561
|
+
return null;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
export function makeImageRenderer(colorKey) {
|
|
565
|
+
return function ({ ctx, x, y, id, state: visState, label }) {
|
|
566
|
+
const {
|
|
567
|
+
W,
|
|
568
|
+
H,
|
|
569
|
+
rotation,
|
|
570
|
+
labelRotation,
|
|
571
|
+
textAlign,
|
|
572
|
+
textValign,
|
|
573
|
+
fontSize,
|
|
574
|
+
c,
|
|
575
|
+
} = nodeData(id, 160, 120, colorKey || "c-gray");
|
|
576
|
+
const n = st.nodes && st.nodes.get(id);
|
|
577
|
+
const src = n && n.imageSrc;
|
|
578
|
+
const img = getCachedImage(src, () => st.network && st.network.redraw());
|
|
579
|
+
return {
|
|
580
|
+
drawNode() {
|
|
581
|
+
ctx.save();
|
|
582
|
+
ctx.translate(x, y);
|
|
583
|
+
ctx.rotate(rotation);
|
|
584
|
+
// Border (always visible, orange when selected)
|
|
585
|
+
ctx.strokeStyle = visState.selected ? "#f97316" : c.border;
|
|
586
|
+
ctx.lineWidth = visState.selected ? 2 : 1;
|
|
587
|
+
roundRect(ctx, -W / 2, -H / 2, W, H, 4);
|
|
588
|
+
ctx.stroke();
|
|
589
|
+
|
|
590
|
+
if (img) {
|
|
591
|
+
// Clip to rounded rect then draw image
|
|
592
|
+
ctx.save();
|
|
593
|
+
roundRect(ctx, -W / 2, -H / 2, W, H, 4);
|
|
594
|
+
ctx.clip();
|
|
595
|
+
ctx.drawImage(img, -W / 2, -H / 2, W, H);
|
|
596
|
+
ctx.restore();
|
|
597
|
+
} else {
|
|
598
|
+
// Placeholder: light fill + icon
|
|
599
|
+
ctx.fillStyle = visState.selected ? c.hbg : c.bg;
|
|
600
|
+
roundRect(ctx, -W / 2, -H / 2, W, H, 4);
|
|
601
|
+
ctx.fill();
|
|
602
|
+
ctx.fillStyle = c.border;
|
|
603
|
+
ctx.font = `${Math.round(Math.min(W, H) * 0.25)}px system-ui`;
|
|
604
|
+
ctx.textAlign = "center";
|
|
605
|
+
ctx.textBaseline = "middle";
|
|
606
|
+
ctx.fillText(src ? "…" : "🖼", 0, 0);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (label) {
|
|
610
|
+
const lines = String(label).split("\n");
|
|
611
|
+
const lineH = fontSize * 1.3;
|
|
612
|
+
const pad = 6;
|
|
613
|
+
const stripH = lines.length * lineH + pad * 2 - (lineH - fontSize);
|
|
614
|
+
ctx.save();
|
|
615
|
+
ctx.font = `${fontSize}px system-ui,-apple-system,sans-serif`;
|
|
616
|
+
const maxTextW = Math.max(
|
|
617
|
+
...lines.map((l) => ctx.measureText(l).width),
|
|
618
|
+
);
|
|
619
|
+
const stripW = maxTextW + pad * 2;
|
|
620
|
+
const M = 5; // margin from image edge
|
|
621
|
+
// Horizontal position based on textAlign
|
|
622
|
+
let stripX;
|
|
623
|
+
if (textAlign === "left") stripX = -W / 2 + M;
|
|
624
|
+
else if (textAlign === "right") stripX = W / 2 - stripW - M;
|
|
625
|
+
else stripX = -stripW / 2;
|
|
626
|
+
// Vertical position based on textValign
|
|
627
|
+
let stripY;
|
|
628
|
+
if (textValign === "top") stripY = -H / 2 + M;
|
|
629
|
+
else if (textValign === "bottom") stripY = H / 2 - stripH - M;
|
|
630
|
+
else stripY = -stripH / 2;
|
|
631
|
+
ctx.globalAlpha = 0.7;
|
|
632
|
+
ctx.fillStyle = "#000";
|
|
633
|
+
roundRect(ctx, stripX, stripY, stripW, stripH, 4);
|
|
634
|
+
ctx.fill();
|
|
635
|
+
ctx.globalAlpha = 1;
|
|
636
|
+
// Draw text centered inside the box
|
|
637
|
+
if (labelRotation) ctx.rotate(labelRotation);
|
|
638
|
+
ctx.fillStyle = "#fff";
|
|
639
|
+
ctx.textAlign = "center";
|
|
640
|
+
ctx.textBaseline = "middle";
|
|
641
|
+
const textCX = stripX + stripW / 2;
|
|
642
|
+
const startY = stripY + pad + fontSize / 2;
|
|
643
|
+
lines.forEach((line, i) =>
|
|
644
|
+
ctx.fillText(line, textCX, startY + i * lineH),
|
|
645
|
+
);
|
|
646
|
+
ctx.restore();
|
|
647
|
+
}
|
|
648
|
+
drawLinkIndicator(ctx, id, W, H);
|
|
649
|
+
ctx.restore();
|
|
650
|
+
},
|
|
651
|
+
nodeDimensions: { width: W, height: H },
|
|
652
|
+
};
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// ── Public API ────────────────────────────────────────────────────────────────
|
|
657
|
+
|
|
658
|
+
// Returns the rendered height from vis-network internals (used by node-panel
|
|
659
|
+
// for vadjust calculations — legacy, kept for API compat).
|
|
660
|
+
export function getActualNodeHeight(id) {
|
|
661
|
+
if (!st.network) return null;
|
|
662
|
+
const bn = st.network.body.nodes[id];
|
|
663
|
+
return bn && bn.shape && bn.shape.height ? bn.shape.height : null;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Kept for backward compat (called by node-panel but irrelevant for ctxRenderers).
|
|
667
|
+
export function computeVadjust() {
|
|
668
|
+
return 0;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// ── Anchor renderer — small dot endpoint for free-floating edges ──────────────
|
|
672
|
+
// nodeDimensions: 16×16 gives vis-network a proper hit area so anchors are
|
|
673
|
+
// natively clickable and draggable without custom proximity hacks.
|
|
674
|
+
// The visual dot (r=4) is only drawn on hover/select; the node is otherwise
|
|
675
|
+
// invisible. The 8-px boundary offset on arrowheads is imperceptible at normal
|
|
676
|
+
// zoom levels and is an acceptable trade-off for native drag support.
|
|
677
|
+
function makeAnchorRenderer() {
|
|
678
|
+
return ({ ctx, x, y, state: { selected, hover } }) => {
|
|
679
|
+
const r = 4;
|
|
680
|
+
return {
|
|
681
|
+
drawNode() {
|
|
682
|
+
if (!selected && !hover) return; // invisible at rest
|
|
683
|
+
ctx.save();
|
|
684
|
+
ctx.translate(x, y);
|
|
685
|
+
ctx.beginPath();
|
|
686
|
+
ctx.arc(0, 0, r, 0, Math.PI * 2);
|
|
687
|
+
ctx.fillStyle = "#f97316";
|
|
688
|
+
ctx.fill();
|
|
689
|
+
ctx.restore();
|
|
690
|
+
},
|
|
691
|
+
nodeDimensions: { width: 16, height: 16 },
|
|
692
|
+
};
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
const RENDERER_MAP = {
|
|
697
|
+
box: makeBoxRenderer,
|
|
698
|
+
ellipse: makeEllipseRenderer,
|
|
699
|
+
circle: makeCircleRenderer,
|
|
700
|
+
database: makeDatabaseRenderer,
|
|
701
|
+
"post-it": makePostItRenderer,
|
|
702
|
+
"text-free": makeTextFreeRenderer,
|
|
703
|
+
actor: makeActorRenderer,
|
|
704
|
+
image: makeImageRenderer,
|
|
705
|
+
anchor: makeAnchorRenderer,
|
|
706
|
+
};
|
|
707
|
+
|
|
708
|
+
// Default dimensions per shape type (used when nodeWidth/nodeHeight are null).
|
|
709
|
+
export const SHAPE_DEFAULTS = {
|
|
710
|
+
box: [100, 40],
|
|
711
|
+
ellipse: [110, 50],
|
|
712
|
+
circle: [55, 55],
|
|
713
|
+
database: [50, 70],
|
|
714
|
+
actor: [30, 52],
|
|
715
|
+
"post-it": [120, 100],
|
|
716
|
+
"text-free": [80, 30],
|
|
717
|
+
image: [160, 120],
|
|
718
|
+
anchor: [8, 8],
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
// Builds the full vis.js node property object.
|
|
722
|
+
// All shapes are rendered via ctxRenderer so rotation works uniformly.
|
|
723
|
+
export function visNodeProps(
|
|
724
|
+
shapeType,
|
|
725
|
+
colorKey,
|
|
726
|
+
nodeWidth,
|
|
727
|
+
nodeHeight,
|
|
728
|
+
fontSize,
|
|
729
|
+
textAlign,
|
|
730
|
+
_textValign,
|
|
731
|
+
) {
|
|
732
|
+
const c = getNodeColor(colorKey);
|
|
733
|
+
const size = fontSize || 13;
|
|
734
|
+
const align = textAlign || "center";
|
|
735
|
+
|
|
736
|
+
const colorP = {
|
|
737
|
+
color: {
|
|
738
|
+
background: c.bg,
|
|
739
|
+
border: c.border,
|
|
740
|
+
highlight: { background: c.hbg, border: c.hborder },
|
|
741
|
+
hover: { background: c.hbg, border: c.hborder },
|
|
742
|
+
},
|
|
743
|
+
font: {
|
|
744
|
+
color: c.font,
|
|
745
|
+
size,
|
|
746
|
+
face: "system-ui,-apple-system,sans-serif",
|
|
747
|
+
align,
|
|
748
|
+
},
|
|
749
|
+
};
|
|
750
|
+
|
|
751
|
+
const sizeP = {};
|
|
752
|
+
if (nodeWidth)
|
|
753
|
+
sizeP.widthConstraint = { minimum: nodeWidth, maximum: nodeWidth };
|
|
754
|
+
if (nodeHeight)
|
|
755
|
+
sizeP.heightConstraint = { minimum: nodeHeight, maximum: nodeHeight };
|
|
756
|
+
|
|
757
|
+
const factory = RENDERER_MAP[shapeType];
|
|
758
|
+
if (factory) {
|
|
759
|
+
return {
|
|
760
|
+
shape: "custom",
|
|
761
|
+
ctxRenderer: factory(colorKey),
|
|
762
|
+
...colorP,
|
|
763
|
+
...sizeP,
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
// Unknown shape — fall back to box
|
|
767
|
+
return {
|
|
768
|
+
shape: "custom",
|
|
769
|
+
ctxRenderer: makeBoxRenderer(colorKey),
|
|
770
|
+
...colorP,
|
|
771
|
+
...sizeP,
|
|
772
|
+
};
|
|
773
|
+
}
|