living-ai-documentation 1.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.fr.md +344 -0
- package/README.md +344 -0
- package/dist/bin/cli.d.ts +3 -0
- package/dist/bin/cli.d.ts.map +1 -0
- package/dist/bin/cli.js +262 -0
- package/dist/bin/cli.js.map +1 -0
- package/dist/src/frontend/accuracy-gauge.js +70 -0
- package/dist/src/frontend/admin.html +1532 -0
- package/dist/src/frontend/annotations.js +585 -0
- package/dist/src/frontend/boot.js +101 -0
- package/dist/src/frontend/config.js +29 -0
- package/dist/src/frontend/confirm-modal.js +82 -0
- package/dist/src/frontend/context.html +1252 -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 +187 -0
- package/dist/src/frontend/diagram/constants.js +109 -0
- package/dist/src/frontend/diagram/custom-shapes.js +104 -0
- package/dist/src/frontend/diagram/debug.js +43 -0
- package/dist/src/frontend/diagram/drawio-export.js +649 -0
- package/dist/src/frontend/diagram/edge-panel.js +293 -0
- package/dist/src/frontend/diagram/edge-rendering.js +12 -0
- package/dist/src/frontend/diagram/evidence.js +146 -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 +157 -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 +364 -0
- package/dist/src/frontend/diagram/network.js +2214 -0
- package/dist/src/frontend/diagram/node-panel.js +389 -0
- package/dist/src/frontend/diagram/node-rendering.js +964 -0
- package/dist/src/frontend/diagram/persistence.js +168 -0
- package/dist/src/frontend/diagram/ports.js +421 -0
- package/dist/src/frontend/diagram/selection-overlay.js +387 -0
- package/dist/src/frontend/diagram/state.js +43 -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 +206 -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 +1494 -0
- package/dist/src/frontend/documents.js +479 -0
- package/dist/src/frontend/export.js +338 -0
- package/dist/src/frontend/file-attach.js +178 -0
- package/dist/src/frontend/files-modal.js +243 -0
- package/dist/src/frontend/i18n/en.json +624 -0
- package/dist/src/frontend/i18n/fr.json +624 -0
- package/dist/src/frontend/i18n.js +32 -0
- package/dist/src/frontend/image-paste.js +126 -0
- package/dist/src/frontend/index.html +2806 -0
- package/dist/src/frontend/local-search.js +476 -0
- package/dist/src/frontend/metadata.js +318 -0
- package/dist/src/frontend/misc.js +92 -0
- package/dist/src/frontend/new-doc-modal.js +285 -0
- package/dist/src/frontend/new-folder-modal.js +169 -0
- package/dist/src/frontend/search.js +194 -0
- package/dist/src/frontend/shape-editor.html +685 -0
- package/dist/src/frontend/sidebar-helpers.js +96 -0
- package/dist/src/frontend/sidebar-resize.js +98 -0
- package/dist/src/frontend/sidebar.js +351 -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 +1146 -0
- package/dist/src/frontend/state.js +46 -0
- package/dist/src/frontend/utils.js +21 -0
- package/dist/src/frontend/validate.js +107 -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 +26 -0
- package/dist/src/lib/config.d.ts.map +1 -0
- package/dist/src/lib/config.js +195 -0
- package/dist/src/lib/config.js.map +1 -0
- package/dist/src/lib/hash.d.ts +2 -0
- package/dist/src/lib/hash.d.ts.map +1 -0
- package/dist/src/lib/hash.js +18 -0
- package/dist/src/lib/hash.js.map +1 -0
- package/dist/src/lib/metadata.d.ts +31 -0
- package/dist/src/lib/metadata.d.ts.map +1 -0
- package/dist/src/lib/metadata.js +128 -0
- package/dist/src/lib/metadata.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/lib/status.d.ts +9 -0
- package/dist/src/lib/status.d.ts.map +1 -0
- package/dist/src/lib/status.js +72 -0
- package/dist/src/lib/status.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 +2046 -0
- package/dist/src/mcp/server.js.map +1 -0
- package/dist/src/mcp/tools/diagrams.d.ts +82 -0
- package/dist/src/mcp/tools/diagrams.d.ts.map +1 -0
- package/dist/src/mcp/tools/diagrams.js +594 -0
- package/dist/src/mcp/tools/diagrams.js.map +1 -0
- package/dist/src/mcp/tools/documents.d.ts +44 -0
- package/dist/src/mcp/tools/documents.d.ts.map +1 -0
- package/dist/src/mcp/tools/documents.js +186 -0
- package/dist/src/mcp/tools/documents.js.map +1 -0
- package/dist/src/mcp/tools/git.d.ts +10 -0
- package/dist/src/mcp/tools/git.d.ts.map +1 -0
- package/dist/src/mcp/tools/git.js +217 -0
- package/dist/src/mcp/tools/git.js.map +1 -0
- package/dist/src/mcp/tools/metadata.d.ts +57 -0
- package/dist/src/mcp/tools/metadata.d.ts.map +1 -0
- package/dist/src/mcp/tools/metadata.js +222 -0
- package/dist/src/mcp/tools/metadata.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 +196 -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-source.d.ts +3 -0
- package/dist/src/routes/browse-source.d.ts.map +1 -0
- package/dist/src/routes/browse-source.js +79 -0
- package/dist/src/routes/browse-source.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 +91 -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 +145 -0
- package/dist/src/routes/config.js.map +1 -0
- package/dist/src/routes/context.d.ts +3 -0
- package/dist/src/routes/context.d.ts.map +1 -0
- package/dist/src/routes/context.js +287 -0
- package/dist/src/routes/context.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 +11 -0
- package/dist/src/routes/documents.d.ts.map +1 -0
- package/dist/src/routes/documents.js +450 -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 +280 -0
- package/dist/src/routes/export.js.map +1 -0
- package/dist/src/routes/files.d.ts +3 -0
- package/dist/src/routes/files.d.ts.map +1 -0
- package/dist/src/routes/files.js +180 -0
- package/dist/src/routes/files.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/metadata.d.ts +3 -0
- package/dist/src/routes/metadata.d.ts.map +1 -0
- package/dist/src/routes/metadata.js +131 -0
- package/dist/src/routes/metadata.js.map +1 -0
- package/dist/src/routes/shape-libraries.d.ts +3 -0
- package/dist/src/routes/shape-libraries.d.ts.map +1 -0
- package/dist/src/routes/shape-libraries.js +118 -0
- package/dist/src/routes/shape-libraries.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 +93 -0
- package/dist/src/server.js.map +1 -0
- package/dist/starter-doc/.living-doc.json +52 -0
- package/dist/starter-doc/ADRS/2026_01_01_[ADR]_example_architecture_decision.md +59 -0
- package/dist/starter-doc/AI/2026_01_01_how_to.md +112 -0
- package/dist/starter-doc/AI/PROJECT-INSTRUCTIONS.md +172 -0
- package/dist/starter-doc/AI/PROJECT-STACK.md +77 -0
- package/dist/starter-doc/AI/PROJECT-USEFUL-COMMANDS.md +80 -0
- package/dist/starter-doc/AI/default/AGENTS.md +31 -0
- package/dist/starter-doc/AI/default/CLAUDE.md +31 -0
- package/dist/starter-doc/AI/default/MEMORY.md +24 -0
- package/dist/starter-doc/AI/rules/no-magic-numbers.md +18 -0
- package/dist/starter-doc/AI/rules/track-current-work.md +23 -0
- package/dist/starter-doc/WORKLOG/current-task.md +57 -0
- package/dist/starter-doc-fr/.living-doc.json +52 -0
- package/dist/starter-doc-fr/ADRS/2026_01_01_[ADR]_example_architecture_decision.md +59 -0
- package/dist/starter-doc-fr/AI/2026_01_01_how_to.md +100 -0
- package/dist/starter-doc-fr/AI/PROJECT-INSTRUCTIONS.md +172 -0
- package/dist/starter-doc-fr/AI/PROJECT-STACK.md +77 -0
- package/dist/starter-doc-fr/AI/PROJECT-USEFUL-COMMANDS.md +80 -0
- package/dist/starter-doc-fr/AI/default/AGENTS.md +31 -0
- package/dist/starter-doc-fr/AI/default/CLAUDE.md +31 -0
- package/dist/starter-doc-fr/AI/default/MEMORY.md +24 -0
- package/dist/starter-doc-fr/AI/rules/no-magic-numbers.md +18 -0
- package/dist/starter-doc-fr/AI/rules/track-current-work.md +23 -0
- package/dist/starter-doc-fr/WORKLOG/current-task.md +57 -0
- package/images/living_documentation.jpg +0 -0
- package/images/readme-extra-files.png +0 -0
- package/images/readme-filename-pattern.png +0 -0
- package/images/readme-intelligent-search-demo.jpg +0 -0
- package/images/readme-sidebar.png +0 -0
- package/package.json +72 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
// ── Node panel ────────────────────────────────────────────────────────────────
|
|
2
|
+
// Floating formatting toolbar for selected nodes (color, font, alignment, z-order).
|
|
3
|
+
|
|
4
|
+
import { st, markDirty } from './state.js';
|
|
5
|
+
import { SHAPE_DEFAULTS } from './node-rendering.js';
|
|
6
|
+
import { CUSTOM_SHAPE_TYPE, getCustomShapeLabelPlacement } from './custom-shapes.js';
|
|
7
|
+
import { pushSnapshot } from './history.js';
|
|
8
|
+
import { t } from './t.js';
|
|
9
|
+
|
|
10
|
+
const CUSTOM_LABEL_PLACEMENTS = ['below', 'above', 'right', 'left', 'center'];
|
|
11
|
+
|
|
12
|
+
// ── Last-used style persistence (per shape type) ──────────────────────────────
|
|
13
|
+
// Saves colorKey/fontSize/textAlign/textValign per shapeType to localStorage so
|
|
14
|
+
// the next shape of that type is created with the same style.
|
|
15
|
+
|
|
16
|
+
function persistNodeStyle() {
|
|
17
|
+
st.selectedNodeIds.forEach((id) => {
|
|
18
|
+
const n = st.nodes && st.nodes.get(id);
|
|
19
|
+
if (!n || !n.shapeType || n.shapeType === 'anchor') return;
|
|
20
|
+
localStorage.setItem('ld-node-style-' + n.shapeType, JSON.stringify({
|
|
21
|
+
colorKey: n.colorKey || 'c-gray',
|
|
22
|
+
fontSize: n.fontSize || null,
|
|
23
|
+
textAlign: n.textAlign || null,
|
|
24
|
+
textValign: n.textValign || null,
|
|
25
|
+
}));
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getLastNodeStyle(shapeType) {
|
|
30
|
+
try { return JSON.parse(localStorage.getItem('ld-node-style-' + shapeType)) || {}; }
|
|
31
|
+
catch { return {}; }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// All shapes are ctxRenderers — vis-network never re-reads the closure after
|
|
35
|
+
// nodes.update(). Force refreshNeeded + redraw so the new colorKey/fontSize/
|
|
36
|
+
// textAlign/textValign values are picked up on the next draw call via st.nodes.get(id).
|
|
37
|
+
function forceRedraw() {
|
|
38
|
+
st.selectedNodeIds.forEach((id) => {
|
|
39
|
+
const bn = st.network && st.network.body.nodes[id];
|
|
40
|
+
if (bn) bn.refreshNeeded = true;
|
|
41
|
+
});
|
|
42
|
+
if (st.network) st.network.redraw();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isEdgeLocked(edge) {
|
|
46
|
+
if (!edge) return false;
|
|
47
|
+
const fromN = st.nodes && st.nodes.get(edge.from);
|
|
48
|
+
const toN = st.nodes && st.nodes.get(edge.to);
|
|
49
|
+
const isFreeArrow = fromN && fromN.shapeType === 'anchor' && toN && toN.shapeType === 'anchor';
|
|
50
|
+
return isFreeArrow ? !!(fromN.locked && toN.locked) : !!(edge.edgeLocked || (fromN && fromN.locked && toN && toN.locked));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function selectedLockState() {
|
|
54
|
+
const nodeIds = (st.selectedNodeIds || []).filter((id) => {
|
|
55
|
+
const n = st.nodes && st.nodes.get(id);
|
|
56
|
+
return n && n.shapeType !== 'anchor';
|
|
57
|
+
});
|
|
58
|
+
const edgeIds = (st.selectedEdgeIds || []).filter((id) => st.edges && st.edges.get(id));
|
|
59
|
+
const total = nodeIds.length + edgeIds.length;
|
|
60
|
+
if (!total) return { allLocked: false, nodeIds, edgeIds };
|
|
61
|
+
|
|
62
|
+
const nodesLocked = nodeIds.every((id) => {
|
|
63
|
+
const n = st.nodes.get(id);
|
|
64
|
+
return !!(n && n.locked);
|
|
65
|
+
});
|
|
66
|
+
const edgesLocked = edgeIds.every((id) => isEdgeLocked(st.edges.get(id)));
|
|
67
|
+
return { allLocked: nodesLocked && edgesLocked, nodeIds, edgeIds };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function syncNodeLockButton() {
|
|
71
|
+
const btn = document.getElementById('btnNodeLock');
|
|
72
|
+
if (!btn) return;
|
|
73
|
+
const { allLocked } = selectedLockState();
|
|
74
|
+
btn.textContent = allLocked ? '🔓' : '🔒';
|
|
75
|
+
btn.title = t(allLocked ? 'diagram.node_panel.unlock' : 'diagram.node_panel.lock');
|
|
76
|
+
btn.setAttribute('aria-label', btn.title);
|
|
77
|
+
btn.classList.toggle('tool-active', allLocked);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function syncNodeFontSizeValue() {
|
|
81
|
+
const el = document.getElementById('nodeFontSizeValue');
|
|
82
|
+
if (!el) return;
|
|
83
|
+
const sizes = (st.selectedNodeIds || []).map((id) => {
|
|
84
|
+
const n = st.nodes && st.nodes.get(id);
|
|
85
|
+
return n && n.shapeType !== 'anchor' ? (n.fontSize || 13) : null;
|
|
86
|
+
}).filter((size) => size !== null);
|
|
87
|
+
|
|
88
|
+
if (!sizes.length) {
|
|
89
|
+
el.textContent = '–';
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const first = sizes[0];
|
|
93
|
+
el.textContent = sizes.every((size) => size === first) ? String(first) : '–';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function selectedCustomShapeIds() {
|
|
97
|
+
return (st.selectedNodeIds || []).filter((id) => {
|
|
98
|
+
const n = st.nodes && st.nodes.get(id);
|
|
99
|
+
return n && n.shapeType === CUSTOM_SHAPE_TYPE;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function effectiveCustomShapeLabelPlacement(node) {
|
|
104
|
+
return CUSTOM_LABEL_PLACEMENTS.includes(node && node.labelPlacement)
|
|
105
|
+
? node.labelPlacement
|
|
106
|
+
: getCustomShapeLabelPlacement(node && node.customShapeId);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function syncCustomShapeLabelPlacementControls() {
|
|
110
|
+
const controls = document.getElementById('customShapeLabelPlacementControls');
|
|
111
|
+
if (!controls) return;
|
|
112
|
+
const customIds = selectedCustomShapeIds();
|
|
113
|
+
controls.classList.toggle('hidden', customIds.length === 0);
|
|
114
|
+
if (!customIds.length) return;
|
|
115
|
+
|
|
116
|
+
const placements = customIds
|
|
117
|
+
.map((id) => effectiveCustomShapeLabelPlacement(st.nodes.get(id)))
|
|
118
|
+
.filter(Boolean);
|
|
119
|
+
const first = placements[0];
|
|
120
|
+
const shared = placements.length && placements.every((placement) => placement === first) ? first : null;
|
|
121
|
+
controls.querySelectorAll('[data-label-placement]').forEach((btn) => {
|
|
122
|
+
btn.classList.toggle('tool-active', !!shared && btn.dataset.labelPlacement === shared);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function setEdgeLocked(edge, locked) {
|
|
127
|
+
if (!edge) return;
|
|
128
|
+
const fromN = st.nodes && st.nodes.get(edge.from);
|
|
129
|
+
const toN = st.nodes && st.nodes.get(edge.to);
|
|
130
|
+
const isFreeArrow = fromN && fromN.shapeType === 'anchor' && toN && toN.shapeType === 'anchor';
|
|
131
|
+
if (isFreeArrow) {
|
|
132
|
+
[edge.from, edge.to].forEach((nodeId) => {
|
|
133
|
+
st.nodes.update({ id: nodeId, locked, fixed: locked ? { x: true, y: true } : false, draggable: !locked });
|
|
134
|
+
const bn = st.network && st.network.body.nodes[nodeId];
|
|
135
|
+
if (bn) bn.refreshNeeded = true;
|
|
136
|
+
});
|
|
137
|
+
} else {
|
|
138
|
+
st.edges.update({ id: edge.id, edgeLocked: locked });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function showNodePanel() {
|
|
143
|
+
document.getElementById('nodePanel').classList.remove('hidden');
|
|
144
|
+
document.getElementById('nodePanelControls').classList.remove('hidden');
|
|
145
|
+
syncNodeLockButton();
|
|
146
|
+
syncNodeFontSizeValue();
|
|
147
|
+
syncCustomShapeLabelPlacementControls();
|
|
148
|
+
// Sync the opacity slider with the first selected node's current value so the
|
|
149
|
+
// slider reflects the live state rather than whatever position it was left at.
|
|
150
|
+
const slider = document.getElementById('nodeBgOpacity');
|
|
151
|
+
if (slider && st.selectedNodeIds.length) {
|
|
152
|
+
const first = st.nodes.get(st.selectedNodeIds[0]);
|
|
153
|
+
const op = first && typeof first.bgOpacity === 'number' ? first.bgOpacity : 1;
|
|
154
|
+
slider.value = String(Math.round(op * 100));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function hideNodePanel() {
|
|
159
|
+
document.getElementById('nodePanel').classList.add('hidden');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function toggleNodeLock() {
|
|
163
|
+
const { allLocked, nodeIds, edgeIds } = selectedLockState();
|
|
164
|
+
if (!nodeIds.length && !edgeIds.length) return;
|
|
165
|
+
const nextLocked = !allLocked;
|
|
166
|
+
pushSnapshot();
|
|
167
|
+
nodeIds.forEach((id) => {
|
|
168
|
+
st.nodes.update({ id, locked: nextLocked, fixed: nextLocked ? { x: true, y: true } : false, draggable: !nextLocked });
|
|
169
|
+
const bn = st.network && st.network.body.nodes[id];
|
|
170
|
+
if (bn) bn.refreshNeeded = true;
|
|
171
|
+
});
|
|
172
|
+
edgeIds.forEach((id) => setEdgeLocked(st.edges.get(id), nextLocked));
|
|
173
|
+
if (st.network) {
|
|
174
|
+
st.network.redraw();
|
|
175
|
+
if (nextLocked) st.network.unselectAll();
|
|
176
|
+
}
|
|
177
|
+
if (nextLocked) {
|
|
178
|
+
st.selectedNodeIds = [];
|
|
179
|
+
st.selectedEdgeIds = [];
|
|
180
|
+
hideNodePanel();
|
|
181
|
+
} else {
|
|
182
|
+
syncNodeLockButton();
|
|
183
|
+
}
|
|
184
|
+
markDirty();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function setNodeColor(colorKey) {
|
|
188
|
+
if (!st.selectedNodeIds.length) return;
|
|
189
|
+
pushSnapshot();
|
|
190
|
+
st.selectedNodeIds.forEach((id) => {
|
|
191
|
+
const n = st.nodes.get(id);
|
|
192
|
+
if (!n) return;
|
|
193
|
+
st.nodes.update({ id, colorKey });
|
|
194
|
+
});
|
|
195
|
+
persistNodeStyle();
|
|
196
|
+
forceRedraw();
|
|
197
|
+
markDirty();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// The slider fires `input` on every step during a drag. The caller is expected
|
|
201
|
+
// to push a single snapshot on pointerdown (gesture start) so the whole drag
|
|
202
|
+
// collapses into one undoable action instead of one per step.
|
|
203
|
+
export function setNodeBgOpacity(opacity) {
|
|
204
|
+
if (!st.selectedNodeIds.length) return;
|
|
205
|
+
const clamped = Math.max(0, Math.min(1, opacity));
|
|
206
|
+
st.selectedNodeIds.forEach((id) => {
|
|
207
|
+
const n = st.nodes.get(id);
|
|
208
|
+
if (!n) return;
|
|
209
|
+
st.nodes.update({ id, bgOpacity: clamped });
|
|
210
|
+
});
|
|
211
|
+
forceRedraw();
|
|
212
|
+
markDirty();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function changeNodeFontSize(delta) {
|
|
216
|
+
if (!st.selectedNodeIds.length) return;
|
|
217
|
+
pushSnapshot();
|
|
218
|
+
st.selectedNodeIds.forEach((id) => {
|
|
219
|
+
const n = st.nodes.get(id);
|
|
220
|
+
if (!n) return;
|
|
221
|
+
const newSize = Math.max(8, Math.min(48, (n.fontSize || 13) + delta));
|
|
222
|
+
st.nodes.update({ id, fontSize: newSize });
|
|
223
|
+
});
|
|
224
|
+
persistNodeStyle();
|
|
225
|
+
syncNodeFontSizeValue();
|
|
226
|
+
forceRedraw();
|
|
227
|
+
markDirty();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function setTextAlign(align) {
|
|
231
|
+
if (!st.selectedNodeIds.length) return;
|
|
232
|
+
pushSnapshot();
|
|
233
|
+
st.selectedNodeIds.forEach((id) => {
|
|
234
|
+
const n = st.nodes.get(id);
|
|
235
|
+
if (!n) return;
|
|
236
|
+
st.nodes.update({ id, textAlign: align });
|
|
237
|
+
});
|
|
238
|
+
persistNodeStyle();
|
|
239
|
+
forceRedraw();
|
|
240
|
+
markDirty();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function setTextValign(valign) {
|
|
244
|
+
if (!st.selectedNodeIds.length) return;
|
|
245
|
+
pushSnapshot();
|
|
246
|
+
st.selectedNodeIds.forEach((id) => {
|
|
247
|
+
const n = st.nodes.get(id);
|
|
248
|
+
if (!n) return;
|
|
249
|
+
st.nodes.update({ id, textValign: valign });
|
|
250
|
+
});
|
|
251
|
+
persistNodeStyle();
|
|
252
|
+
forceRedraw();
|
|
253
|
+
markDirty();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function setCustomShapeLabelPlacement(placement) {
|
|
257
|
+
if (!CUSTOM_LABEL_PLACEMENTS.includes(placement)) return;
|
|
258
|
+
const ids = selectedCustomShapeIds();
|
|
259
|
+
if (!ids.length) return;
|
|
260
|
+
pushSnapshot();
|
|
261
|
+
ids.forEach((id) => {
|
|
262
|
+
st.nodes.update({ id, labelPlacement: placement });
|
|
263
|
+
});
|
|
264
|
+
syncCustomShapeLabelPlacementControls();
|
|
265
|
+
forceRedraw();
|
|
266
|
+
markDirty();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ── Stamp (format painter) ────────────────────────────────────────────────────
|
|
270
|
+
// Uses a transparent DOM overlay (#stampOverlay) that intercepts canvas clicks
|
|
271
|
+
// during stamp mode. This bypasses vis-network's event system entirely, avoiding
|
|
272
|
+
// the deselectNode/click ordering problems that make st.activeStamp unreliable.
|
|
273
|
+
|
|
274
|
+
const STAMP_BTNS = { color: 'btnStampColor', fontSize: 'btnStampFontSize', size: 'btnStampSize' };
|
|
275
|
+
|
|
276
|
+
export function activateStamp(type) {
|
|
277
|
+
if (!st.stampTargetIds.length) return; // targets were saved on mousedown
|
|
278
|
+
st.activeStamp = type;
|
|
279
|
+
const overlay = document.getElementById('stampOverlay');
|
|
280
|
+
overlay.style.display = 'block';
|
|
281
|
+
Object.entries(STAMP_BTNS).forEach(([t, id]) =>
|
|
282
|
+
document.getElementById(id).classList.toggle('tool-active', t === type)
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function cancelStamp() {
|
|
287
|
+
st.activeStamp = null;
|
|
288
|
+
st.stampTargetIds = [];
|
|
289
|
+
document.getElementById('stampOverlay').style.display = 'none';
|
|
290
|
+
Object.values(STAMP_BTNS).forEach((id) =>
|
|
291
|
+
document.getElementById(id).classList.remove('tool-active')
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function applyStamp(sourceId) {
|
|
296
|
+
const source = st.nodes.get(sourceId);
|
|
297
|
+
if (!source || !st.activeStamp || !st.stampTargetIds.length) return;
|
|
298
|
+
const type = st.activeStamp;
|
|
299
|
+
const targets = [...st.stampTargetIds]; // snapshot before cancelStamp clears the array
|
|
300
|
+
cancelStamp();
|
|
301
|
+
pushSnapshot();
|
|
302
|
+
|
|
303
|
+
targets.forEach((id) => {
|
|
304
|
+
if (id === sourceId) return;
|
|
305
|
+
const target = st.nodes.get(id);
|
|
306
|
+
if (!target) return;
|
|
307
|
+
if (type === 'color') st.nodes.update({ id, colorKey: source.colorKey || 'c-gray' });
|
|
308
|
+
if (type === 'rotation') st.nodes.update({ id, rotation: source.rotation || 0 });
|
|
309
|
+
if (type === 'fontSize') st.nodes.update({ id, fontSize: source.fontSize || 13 });
|
|
310
|
+
if (type === 'size') st.nodes.update({ id, nodeWidth: source.nodeWidth || null, nodeHeight: source.nodeHeight || null });
|
|
311
|
+
const bn = st.network && st.network.body.nodes[id];
|
|
312
|
+
if (bn) bn.refreshNeeded = true;
|
|
313
|
+
});
|
|
314
|
+
if (st.network) st.network.redraw();
|
|
315
|
+
markDirty();
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// getNodeAt() is unreliable for shape:'custom' (bounding box near-zero).
|
|
319
|
+
// Manual AABB hit test using DOMtoCanvas + node dimensions, topmost node first.
|
|
320
|
+
function getNodeAtDOMPoint(domX, domY) {
|
|
321
|
+
if (!st.network || !st.nodes) return undefined;
|
|
322
|
+
const cp = st.network.DOMtoCanvas({ x: domX, y: domY });
|
|
323
|
+
for (let i = st.canonicalOrder.length - 1; i >= 0; i--) {
|
|
324
|
+
const id = st.canonicalOrder[i];
|
|
325
|
+
const n = st.nodes.get(id);
|
|
326
|
+
const bn = st.network.body.nodes[id];
|
|
327
|
+
if (!n || !bn) continue;
|
|
328
|
+
const defaults = SHAPE_DEFAULTS[n.shapeType] || [100, 40];
|
|
329
|
+
const w = n.nodeWidth || defaults[0];
|
|
330
|
+
const h = n.nodeHeight || defaults[1];
|
|
331
|
+
const rot = n.rotation || 0;
|
|
332
|
+
let hw, hh;
|
|
333
|
+
if (rot === 0) {
|
|
334
|
+
hw = w / 2; hh = h / 2;
|
|
335
|
+
} else {
|
|
336
|
+
const cos = Math.abs(Math.cos(rot)); const sin = Math.abs(Math.sin(rot));
|
|
337
|
+
hw = (w * cos + h * sin) / 2;
|
|
338
|
+
hh = (w * sin + h * cos) / 2;
|
|
339
|
+
}
|
|
340
|
+
if (Math.abs(cp.x - bn.x) <= hw && Math.abs(cp.y - bn.y) <= hh) return id;
|
|
341
|
+
}
|
|
342
|
+
return undefined;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Wire the stamp overlay click.
|
|
346
|
+
document.getElementById('stampOverlay').addEventListener('click', (e) => {
|
|
347
|
+
if (!st.activeStamp || !st.network) return;
|
|
348
|
+
const rect = document.getElementById('vis-canvas').getBoundingClientRect();
|
|
349
|
+
const nodeId = getNodeAtDOMPoint(e.clientX - rect.left, e.clientY - rect.top);
|
|
350
|
+
if (nodeId !== undefined) {
|
|
351
|
+
applyStamp(nodeId);
|
|
352
|
+
} else {
|
|
353
|
+
cancelStamp();
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// ── Step rotation ─────────────────────────────────────────────────────────────
|
|
358
|
+
|
|
359
|
+
export function stepRotate(degrees) {
|
|
360
|
+
if (!st.selectedNodeIds.length) return;
|
|
361
|
+
pushSnapshot();
|
|
362
|
+
const delta = degrees * (Math.PI / 180);
|
|
363
|
+
st.selectedNodeIds.forEach((id) => {
|
|
364
|
+
const n = st.nodes.get(id);
|
|
365
|
+
if (!n) return;
|
|
366
|
+
st.nodes.update({ id, rotation: (n.rotation || 0) + delta });
|
|
367
|
+
const bn = st.network && st.network.body.nodes[id];
|
|
368
|
+
if (bn) bn.refreshNeeded = true;
|
|
369
|
+
});
|
|
370
|
+
if (st.network) st.network.redraw();
|
|
371
|
+
markDirty();
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export function changeZOrder(direction) {
|
|
375
|
+
// direction: +1 = bring to front (last in canonicalOrder = drawn on top)
|
|
376
|
+
// -1 = send to back (first in canonicalOrder = drawn below)
|
|
377
|
+
if (!st.selectedNodeIds.length) return;
|
|
378
|
+
pushSnapshot();
|
|
379
|
+
st.selectedNodeIds.forEach((id) => {
|
|
380
|
+
const idx = st.canonicalOrder.indexOf(id);
|
|
381
|
+
if (idx === -1) return;
|
|
382
|
+
st.canonicalOrder.splice(idx, 1);
|
|
383
|
+
if (direction > 0) st.canonicalOrder.push(id);
|
|
384
|
+
else st.canonicalOrder.unshift(id);
|
|
385
|
+
});
|
|
386
|
+
st.network.redraw();
|
|
387
|
+
st.network.selectNodes(st.selectedNodeIds);
|
|
388
|
+
markDirty();
|
|
389
|
+
}
|