living-documentation 7.43.0 → 7.44.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/dist/src/frontend/diagram/custom-shapes.js +11 -3
- package/dist/src/frontend/diagram/main.js +3 -1
- package/dist/src/frontend/diagram/network.js +1000 -497
- package/dist/src/frontend/diagram/node-panel.js +47 -0
- package/dist/src/frontend/diagram/node-rendering.js +117 -23
- package/dist/src/frontend/diagram/persistence.js +1 -0
- package/dist/src/frontend/diagram/ports.js +7 -5
- package/dist/src/frontend/diagram/selection-overlay.js +64 -13
- package/dist/src/frontend/diagram.html +34 -0
- package/dist/src/frontend/i18n/en.json +14 -1
- package/dist/src/frontend/i18n/fr.json +14 -1
- package/dist/src/frontend/shape-editor.html +385 -165
- package/dist/src/routes/shape-libraries.d.ts +1 -1
- package/dist/src/routes/shape-libraries.d.ts.map +1 -1
- package/dist/src/routes/shape-libraries.js +42 -24
- package/dist/src/routes/shape-libraries.js.map +1 -1
- package/package.json +1 -1
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
|
|
4
4
|
import { st, markDirty } from './state.js';
|
|
5
5
|
import { SHAPE_DEFAULTS } from './node-rendering.js';
|
|
6
|
+
import { CUSTOM_SHAPE_TYPE, getCustomShapeLabelPlacement } from './custom-shapes.js';
|
|
6
7
|
import { pushSnapshot } from './history.js';
|
|
7
8
|
import { t } from './t.js';
|
|
8
9
|
|
|
10
|
+
const CUSTOM_LABEL_PLACEMENTS = ['below', 'above', 'right', 'left', 'center'];
|
|
11
|
+
|
|
9
12
|
// ── Last-used style persistence (per shape type) ──────────────────────────────
|
|
10
13
|
// Saves colorKey/fontSize/textAlign/textValign per shapeType to localStorage so
|
|
11
14
|
// the next shape of that type is created with the same style.
|
|
@@ -90,6 +93,36 @@ function syncNodeFontSizeValue() {
|
|
|
90
93
|
el.textContent = sizes.every((size) => size === first) ? String(first) : '–';
|
|
91
94
|
}
|
|
92
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
|
+
|
|
93
126
|
function setEdgeLocked(edge, locked) {
|
|
94
127
|
if (!edge) return;
|
|
95
128
|
const fromN = st.nodes && st.nodes.get(edge.from);
|
|
@@ -111,6 +144,7 @@ export function showNodePanel() {
|
|
|
111
144
|
document.getElementById('nodePanelControls').classList.remove('hidden');
|
|
112
145
|
syncNodeLockButton();
|
|
113
146
|
syncNodeFontSizeValue();
|
|
147
|
+
syncCustomShapeLabelPlacementControls();
|
|
114
148
|
// Sync the opacity slider with the first selected node's current value so the
|
|
115
149
|
// slider reflects the live state rather than whatever position it was left at.
|
|
116
150
|
const slider = document.getElementById('nodeBgOpacity');
|
|
@@ -219,6 +253,19 @@ export function setTextValign(valign) {
|
|
|
219
253
|
markDirty();
|
|
220
254
|
}
|
|
221
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
|
+
|
|
222
269
|
// ── Stamp (format painter) ────────────────────────────────────────────────────
|
|
223
270
|
// Uses a transparent DOM overlay (#stampOverlay) that intercepts canvas clicks
|
|
224
271
|
// during stamp mode. This bypasses vis-network's event system entirely, avoiding
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { NODE_COLORS } from "./constants.js";
|
|
8
8
|
import { st } from "./state.js";
|
|
9
|
-
import { CUSTOM_SHAPE_TYPE, getCustomShapeDefinition } from "./custom-shapes.js";
|
|
9
|
+
import { CUSTOM_SHAPE_TYPE, getCustomShapeDefinition, CUSTOM_SHAPE_DEFAULT_SIZE } from "./custom-shapes.js";
|
|
10
10
|
|
|
11
11
|
// Returns the color object for a key, checking runtime overrides first.
|
|
12
12
|
function getNodeColor(colorKey) {
|
|
@@ -187,13 +187,87 @@ function textLines(label) {
|
|
|
187
187
|
return label ? String(label).split("\n") : [];
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
-
|
|
190
|
+
function normalizeCustomLabelPlacement(value) {
|
|
191
|
+
return ["center", "below", "above", "right", "left"].includes(value) ? value : null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function isExternalCustomLabelPlacement(placement) {
|
|
195
|
+
return ["above", "below", "right", "left"].includes(placement);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function measureTextWidth(line, fontSize, ctx) {
|
|
199
|
+
const font = `${fontSize}px system-ui,-apple-system,sans-serif`;
|
|
200
|
+
if (ctx) {
|
|
201
|
+
const prevFont = ctx.font;
|
|
202
|
+
ctx.font = font;
|
|
203
|
+
const width = ctx.measureText(line).width;
|
|
204
|
+
ctx.font = prevFont;
|
|
205
|
+
return width;
|
|
206
|
+
}
|
|
207
|
+
if (typeof document === "undefined") return String(line).length * fontSize * 0.58;
|
|
208
|
+
if (!measureTextWidth._ctx) {
|
|
209
|
+
measureTextWidth._ctx = document.createElement("canvas").getContext("2d");
|
|
210
|
+
}
|
|
211
|
+
measureTextWidth._ctx.font = font;
|
|
212
|
+
return measureTextWidth._ctx.measureText(line).width;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function customShapeLayout(node, label, ctx) {
|
|
191
216
|
const def = getCustomShapeDefinition(node && node.customShapeId);
|
|
192
|
-
|
|
217
|
+
const W =
|
|
218
|
+
(node && node.nodeWidth) || (def && def.width) || CUSTOM_SHAPE_DEFAULT_SIZE;
|
|
219
|
+
const H =
|
|
220
|
+
(node && node.nodeHeight) ||
|
|
221
|
+
(def && def.height) ||
|
|
222
|
+
CUSTOM_SHAPE_DEFAULT_SIZE;
|
|
223
|
+
const placement = normalizeCustomLabelPlacement(node && node.labelPlacement)
|
|
224
|
+
|| normalizeCustomLabelPlacement(def && def.labelPlacement)
|
|
225
|
+
|| "below";
|
|
193
226
|
const fontSize = (node && node.fontSize) || 13;
|
|
194
227
|
const lines = textLines(label);
|
|
195
|
-
|
|
196
|
-
|
|
228
|
+
const lineH = fontSize * 1.3;
|
|
229
|
+
if (!lines.length || !isExternalCustomLabelPlacement(placement)) {
|
|
230
|
+
return {
|
|
231
|
+
placement,
|
|
232
|
+
lines,
|
|
233
|
+
lineH,
|
|
234
|
+
imageOffsetX: 0,
|
|
235
|
+
imageOffsetY: 0,
|
|
236
|
+
labelBlockW: 0,
|
|
237
|
+
labelBlockH: 0,
|
|
238
|
+
totalW: W,
|
|
239
|
+
totalH: H,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const maxTextW = Math.max(...lines.map((line) => measureTextWidth(line, fontSize, ctx)));
|
|
244
|
+
const labelBlockW = maxTextW + 16;
|
|
245
|
+
const labelBlockH = lines.length * lineH + 8;
|
|
246
|
+
const horizontal = placement === "left" || placement === "right";
|
|
247
|
+
const totalW = horizontal ? W + labelBlockW : Math.max(W, labelBlockW);
|
|
248
|
+
const totalH = horizontal ? Math.max(H, labelBlockH) : H + labelBlockH;
|
|
249
|
+
const imageOffsetX = placement === "right" ? -labelBlockW / 2
|
|
250
|
+
: placement === "left" ? labelBlockW / 2
|
|
251
|
+
: 0;
|
|
252
|
+
const imageOffsetY = placement === "below" ? -labelBlockH / 2
|
|
253
|
+
: placement === "above" ? labelBlockH / 2
|
|
254
|
+
: 0;
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
placement,
|
|
258
|
+
lines,
|
|
259
|
+
lineH,
|
|
260
|
+
imageOffsetX,
|
|
261
|
+
imageOffsetY,
|
|
262
|
+
labelBlockW,
|
|
263
|
+
labelBlockH,
|
|
264
|
+
totalW,
|
|
265
|
+
totalH,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function customShapeImageOffsetY(node, label) {
|
|
270
|
+
return customShapeLayout(node, label).imageOffsetY;
|
|
197
271
|
}
|
|
198
272
|
|
|
199
273
|
// ── Shape renderers ───────────────────────────────────────────────────────────
|
|
@@ -671,8 +745,8 @@ export function makeCustomShapeRenderer(colorKey) {
|
|
|
671
745
|
return function ({ ctx, x, y, id, state: visState, label }) {
|
|
672
746
|
const n = st.nodes && st.nodes.get(id);
|
|
673
747
|
const def = getCustomShapeDefinition(n && n.customShapeId);
|
|
674
|
-
const defaultW = def && def.width ||
|
|
675
|
-
const defaultH = def && def.height ||
|
|
748
|
+
const defaultW = (def && def.width) || CUSTOM_SHAPE_DEFAULT_SIZE;
|
|
749
|
+
const defaultH = (def && def.height) || CUSTOM_SHAPE_DEFAULT_SIZE;
|
|
676
750
|
const {
|
|
677
751
|
W,
|
|
678
752
|
H,
|
|
@@ -684,11 +758,17 @@ export function makeCustomShapeRenderer(colorKey) {
|
|
|
684
758
|
c,
|
|
685
759
|
} = nodeData(id, defaultW, defaultH, colorKey || "c-gray");
|
|
686
760
|
const img = getCachedImage(def && def.imageSrc, () => st.network && st.network.redraw());
|
|
687
|
-
const
|
|
688
|
-
const
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
761
|
+
const layout = customShapeLayout({ ...(n || {}), nodeWidth: W, nodeHeight: H, fontSize }, label, ctx);
|
|
762
|
+
const {
|
|
763
|
+
placement,
|
|
764
|
+
lines,
|
|
765
|
+
lineH,
|
|
766
|
+
imageOffsetX,
|
|
767
|
+
imageOffsetY,
|
|
768
|
+
totalW,
|
|
769
|
+
totalH,
|
|
770
|
+
} = layout;
|
|
771
|
+
const externalLabel = isExternalCustomLabelPlacement(placement) && lines.length;
|
|
692
772
|
return {
|
|
693
773
|
drawNode() {
|
|
694
774
|
ctx.save();
|
|
@@ -696,12 +776,12 @@ export function makeCustomShapeRenderer(colorKey) {
|
|
|
696
776
|
ctx.rotate(rotation);
|
|
697
777
|
|
|
698
778
|
if (img) {
|
|
699
|
-
ctx.drawImage(img, -W / 2, imageOffsetY - H / 2, W, H);
|
|
779
|
+
ctx.drawImage(img, imageOffsetX - W / 2, imageOffsetY - H / 2, W, H);
|
|
700
780
|
} else {
|
|
701
781
|
ctx.fillStyle = visState.selected ? c.hbg : c.bg;
|
|
702
782
|
ctx.strokeStyle = visState.selected ? "#f97316" : c.border;
|
|
703
783
|
ctx.lineWidth = 1.5;
|
|
704
|
-
roundRect(ctx, -W / 2, imageOffsetY - H / 2, W, H, 4);
|
|
784
|
+
roundRect(ctx, imageOffsetX - W / 2, imageOffsetY - H / 2, W, H, 4);
|
|
705
785
|
ctx.fill();
|
|
706
786
|
ctx.stroke();
|
|
707
787
|
}
|
|
@@ -710,21 +790,32 @@ export function makeCustomShapeRenderer(colorKey) {
|
|
|
710
790
|
ctx.strokeStyle = visState.selected ? "#f97316" : c.border;
|
|
711
791
|
ctx.lineWidth = visState.selected ? 2 : 1;
|
|
712
792
|
ctx.setLineDash([4, 3]);
|
|
713
|
-
roundRect(ctx, -W / 2, -
|
|
793
|
+
roundRect(ctx, imageOffsetX - W / 2, imageOffsetY - H / 2, W, H, 4);
|
|
714
794
|
ctx.stroke();
|
|
715
795
|
ctx.setLineDash([]);
|
|
716
796
|
}
|
|
717
797
|
|
|
718
|
-
if (
|
|
798
|
+
if (externalLabel) {
|
|
719
799
|
ctx.save();
|
|
720
800
|
if (labelRotation) ctx.rotate(labelRotation);
|
|
721
801
|
ctx.font = `${fontSize}px system-ui,-apple-system,sans-serif`;
|
|
722
802
|
ctx.fillStyle = c.font;
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
803
|
+
if (placement === "right" || placement === "left") {
|
|
804
|
+
const textX = placement === "right"
|
|
805
|
+
? imageOffsetX + W / 2 + 8
|
|
806
|
+
: imageOffsetX - W / 2 - 8;
|
|
807
|
+
const startY = -((lines.length - 1) * lineH) / 2;
|
|
808
|
+
ctx.textAlign = placement === "right" ? "left" : "right";
|
|
809
|
+
ctx.textBaseline = "middle";
|
|
810
|
+
lines.forEach((line, i) => ctx.fillText(line, textX, startY + i * lineH));
|
|
811
|
+
} else {
|
|
812
|
+
ctx.textAlign = "center";
|
|
813
|
+
ctx.textBaseline = "top";
|
|
814
|
+
const startY = placement === "below"
|
|
815
|
+
? imageOffsetY + H / 2 + 4
|
|
816
|
+
: -totalH / 2 + 4;
|
|
817
|
+
lines.forEach((line, i) => ctx.fillText(line, 0, startY + i * lineH));
|
|
818
|
+
}
|
|
728
819
|
ctx.restore();
|
|
729
820
|
} else {
|
|
730
821
|
drawLabel(
|
|
@@ -739,10 +830,13 @@ export function makeCustomShapeRenderer(colorKey) {
|
|
|
739
830
|
labelRotation,
|
|
740
831
|
);
|
|
741
832
|
}
|
|
833
|
+
ctx.save();
|
|
834
|
+
ctx.translate(imageOffsetX, imageOffsetY);
|
|
742
835
|
drawLinkIndicator(ctx, id, W, H);
|
|
743
836
|
ctx.restore();
|
|
837
|
+
ctx.restore();
|
|
744
838
|
},
|
|
745
|
-
nodeDimensions: { width:
|
|
839
|
+
nodeDimensions: { width: totalW, height: totalH },
|
|
746
840
|
};
|
|
747
841
|
};
|
|
748
842
|
}
|
|
@@ -811,7 +905,7 @@ export const SHAPE_DEFAULTS = {
|
|
|
811
905
|
"post-it": [120, 100],
|
|
812
906
|
"text-free": [80, 30],
|
|
813
907
|
image: [160, 120],
|
|
814
|
-
[CUSTOM_SHAPE_TYPE]: [
|
|
908
|
+
[CUSTOM_SHAPE_TYPE]: [CUSTOM_SHAPE_DEFAULT_SIZE, CUSTOM_SHAPE_DEFAULT_SIZE],
|
|
815
909
|
anchor: [8, 8],
|
|
816
910
|
};
|
|
817
911
|
|
|
@@ -127,6 +127,7 @@ export async function saveDiagram() {
|
|
|
127
127
|
id: n.id, label: n.label,
|
|
128
128
|
shapeType: n.shapeType || 'box', colorKey: n.colorKey || 'c-gray',
|
|
129
129
|
customShapeId: n.customShapeId || null,
|
|
130
|
+
labelPlacement: n.labelPlacement || null,
|
|
130
131
|
kind: n.kind || null, renderAs: n.renderAs || null,
|
|
131
132
|
description: n.description || null,
|
|
132
133
|
evidence: Array.isArray(n.evidence) ? n.evidence : null,
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// vis-network's native rendering unchanged.
|
|
8
8
|
|
|
9
9
|
import { st } from './state.js';
|
|
10
|
-
import {
|
|
10
|
+
import { customShapeLayout, SHAPE_DEFAULTS } from './node-rendering.js';
|
|
11
11
|
import { CUSTOM_SHAPE_TYPE, getCustomShapeAnchors } from './custom-shapes.js';
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -84,8 +84,10 @@ function nodeGeometry(nodeId) {
|
|
|
84
84
|
const W = n.nodeWidth || defaults[0];
|
|
85
85
|
// 'circle' is always square (H = W); 'ellipse' uses its own height.
|
|
86
86
|
const H = shapeType === 'circle' ? W : (n.nodeHeight || defaults[1]);
|
|
87
|
-
const
|
|
88
|
-
|
|
87
|
+
const customLayout = shapeType === CUSTOM_SHAPE_TYPE ? customShapeLayout(n, n.label) : null;
|
|
88
|
+
const imageOffsetX = customLayout ? customLayout.imageOffsetX : 0;
|
|
89
|
+
const imageOffsetY = customLayout ? customLayout.imageOffsetY : 0;
|
|
90
|
+
return { cx: pos.x, cy: pos.y, W, H, rotation: n.rotation || 0, shapeType, customShapeId: n.customShapeId || null, imageOffsetX, imageOffsetY };
|
|
89
91
|
}
|
|
90
92
|
|
|
91
93
|
export function getPortKeys(nodeId) {
|
|
@@ -120,7 +122,7 @@ function controlNormalForEdge(nodeId, portKey) {
|
|
|
120
122
|
export function getPortPosition(nodeId, portKey) {
|
|
121
123
|
const geo = nodeGeometry(nodeId);
|
|
122
124
|
if (!geo) return null;
|
|
123
|
-
const { cx, cy, W, H, rotation, shapeType, customShapeId, imageOffsetY } = geo;
|
|
125
|
+
const { cx, cy, W, H, rotation, shapeType, customShapeId, imageOffsetX, imageOffsetY } = geo;
|
|
124
126
|
const offsets = shapeType === CUSTOM_SHAPE_TYPE ? null
|
|
125
127
|
: shapeType === 'database' ? PORT_OFFSETS_DATABASE
|
|
126
128
|
: CIRCULAR_SHAPES.has(shapeType) ? PORT_OFFSETS_CIRC
|
|
@@ -128,7 +130,7 @@ export function getPortPosition(nodeId, portKey) {
|
|
|
128
130
|
const [ox, oy] = shapeType === CUSTOM_SHAPE_TYPE
|
|
129
131
|
? customPortOffset(customShapeId, portKey) || [0, 0]
|
|
130
132
|
: offsets[portKey] || [0, 0];
|
|
131
|
-
let dx = ox * W / 2;
|
|
133
|
+
let dx = ox * W / 2 + imageOffsetX;
|
|
132
134
|
let dy = oy * H / 2 + imageOffsetY;
|
|
133
135
|
if (rotation) {
|
|
134
136
|
const cos = Math.cos(rotation), sin = Math.sin(rotation);
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// Dashed selection box, corner resize handles, and top-centre rotation handle.
|
|
3
3
|
|
|
4
4
|
import { st, markDirty } from './state.js';
|
|
5
|
-
import { visNodeProps, SHAPE_DEFAULTS } from './node-rendering.js';
|
|
5
|
+
import { customShapeLayout, visNodeProps, SHAPE_DEFAULTS } from './node-rendering.js';
|
|
6
|
+
import { CUSTOM_SHAPE_TYPE } from './custom-shapes.js';
|
|
6
7
|
import { t } from './t.js';
|
|
7
8
|
import { pushSnapshot } from './history.js';
|
|
8
9
|
import { snapToGrid } from './grid.js';
|
|
@@ -31,12 +32,71 @@ function nodeBounds(id) {
|
|
|
31
32
|
return { minX: cx - hw, minY: cy - hh - headExtra, maxX: cx + hw, maxY: cy + hh };
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
function customShapeImageFrame(id) {
|
|
36
|
+
const n = st.nodes.get(id);
|
|
37
|
+
const bodyNode = st.network.body.nodes[id];
|
|
38
|
+
if (!n || !bodyNode || n.shapeType !== CUSTOM_SHAPE_TYPE) return null;
|
|
39
|
+
const defaults = SHAPE_DEFAULTS[CUSTOM_SHAPE_TYPE] || [96, 96];
|
|
40
|
+
const W = n.nodeWidth || defaults[0];
|
|
41
|
+
const H = n.nodeHeight || defaults[1];
|
|
42
|
+
const rotation = n.rotation || 0;
|
|
43
|
+
const layout = customShapeLayout(n, n.label);
|
|
44
|
+
const cos = Math.cos(rotation);
|
|
45
|
+
const sin = Math.sin(rotation);
|
|
46
|
+
const cx = bodyNode.x + layout.imageOffsetX * cos - layout.imageOffsetY * sin;
|
|
47
|
+
const cy = bodyNode.y + layout.imageOffsetX * sin + layout.imageOffsetY * cos;
|
|
48
|
+
return { cx, cy, W, H, rotation };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function syncOverlayHandles(width, height, pad) {
|
|
52
|
+
const rh = document.getElementById('rh-rotate');
|
|
53
|
+
rh.style.left = width / 2 + pad - 8 + 'px';
|
|
54
|
+
rh.style.top = '-28px';
|
|
55
|
+
|
|
56
|
+
const lrh = document.getElementById('rh-label-rotate');
|
|
57
|
+
lrh.style.left = width / 2 + pad - 8 - 24 + 'px';
|
|
58
|
+
lrh.style.top = '-28px';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function updateSingleCustomShapeOverlay(id) {
|
|
62
|
+
const frame = customShapeImageFrame(id);
|
|
63
|
+
if (!frame) return false;
|
|
64
|
+
|
|
65
|
+
const PAD = 10;
|
|
66
|
+
const scale = st.network.getScale();
|
|
67
|
+
const center = st.network.canvasToDOM({ x: frame.cx, y: frame.cy });
|
|
68
|
+
const width = frame.W * scale;
|
|
69
|
+
const height = frame.H * scale;
|
|
70
|
+
const ov = document.getElementById('selectionOverlay');
|
|
71
|
+
ov.style.display = 'block';
|
|
72
|
+
ov.style.left = center.x - width / 2 - PAD + 'px';
|
|
73
|
+
ov.style.top = center.y - height / 2 - PAD + 'px';
|
|
74
|
+
ov.style.width = width + PAD * 2 + 'px';
|
|
75
|
+
ov.style.height = height + PAD * 2 + 'px';
|
|
76
|
+
ov.style.transformOrigin = '50% 50%';
|
|
77
|
+
ov.style.transform = `rotate(${frame.rotation}rad)`;
|
|
78
|
+
syncOverlayHandles(width, height, PAD);
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
|
|
34
82
|
// ── Overlay position ──────────────────────────────────────────────────────────
|
|
35
83
|
export function updateSelectionOverlay() {
|
|
36
84
|
if (!st.network || !st.selectedNodeIds.length) { hideSelectionOverlay(); return; }
|
|
37
85
|
// Anchor-only selections (free arrow drag) have no meaningful bounding box.
|
|
38
|
-
const
|
|
39
|
-
if (!
|
|
86
|
+
const nonAnchorIds = st.selectedNodeIds.filter((id) => { const n = st.nodes && st.nodes.get(id); return !(n && n.shapeType === 'anchor'); });
|
|
87
|
+
if (!nonAnchorIds.length) { hideSelectionOverlay(); return; }
|
|
88
|
+
|
|
89
|
+
const ov = document.getElementById('selectionOverlay');
|
|
90
|
+
ov.style.transform = '';
|
|
91
|
+
ov.style.transformOrigin = '';
|
|
92
|
+
|
|
93
|
+
if (nonAnchorIds.length === 1 && updateSingleCustomShapeOverlay(nonAnchorIds[0])) {
|
|
94
|
+
const anyLocked = st.selectedNodeIds.some((id) => { const n = st.nodes && st.nodes.get(id); return n && n.locked; });
|
|
95
|
+
['rh-tl','rh-tr','rh-bl','rh-br','rh-rotate','rh-label-rotate'].forEach((handleId) => {
|
|
96
|
+
document.getElementById(handleId).style.display = anyLocked ? 'none' : '';
|
|
97
|
+
});
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
40
100
|
|
|
41
101
|
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
42
102
|
for (const id of st.selectedNodeIds) {
|
|
@@ -52,7 +112,6 @@ export function updateSelectionOverlay() {
|
|
|
52
112
|
const PAD = 10;
|
|
53
113
|
const tl = st.network.canvasToDOM({ x: minX, y: minY });
|
|
54
114
|
const br = st.network.canvasToDOM({ x: maxX, y: maxY });
|
|
55
|
-
const ov = document.getElementById('selectionOverlay');
|
|
56
115
|
ov.style.display = 'block';
|
|
57
116
|
ov.style.left = tl.x - PAD + 'px';
|
|
58
117
|
ov.style.top = tl.y - PAD + 'px';
|
|
@@ -65,15 +124,7 @@ export function updateSelectionOverlay() {
|
|
|
65
124
|
document.getElementById(id).style.display = anyLocked ? 'none' : '';
|
|
66
125
|
});
|
|
67
126
|
|
|
68
|
-
|
|
69
|
-
const rh = document.getElementById('rh-rotate');
|
|
70
|
-
rh.style.left = (br.x - tl.x) / 2 + PAD - 8 + 'px';
|
|
71
|
-
rh.style.top = '-28px';
|
|
72
|
-
|
|
73
|
-
// Position label rotation handle: top-centre offset left by 24px to avoid overlap.
|
|
74
|
-
const lrh = document.getElementById('rh-label-rotate');
|
|
75
|
-
lrh.style.left = (br.x - tl.x) / 2 + PAD - 8 - 24 + 'px';
|
|
76
|
-
lrh.style.top = '-28px';
|
|
127
|
+
syncOverlayHandles(br.x - tl.x, br.y - tl.y, PAD);
|
|
77
128
|
}
|
|
78
129
|
|
|
79
130
|
export function hideSelectionOverlay() {
|
|
@@ -85,6 +85,12 @@
|
|
|
85
85
|
.dark .panel-sep {
|
|
86
86
|
background: #374151;
|
|
87
87
|
}
|
|
88
|
+
#customShapeLabelPlacementControls {
|
|
89
|
+
display: contents;
|
|
90
|
+
}
|
|
91
|
+
#customShapeLabelPlacementControls.hidden {
|
|
92
|
+
display: none;
|
|
93
|
+
}
|
|
88
94
|
.edge-btn-active {
|
|
89
95
|
background: #fff7ed !important;
|
|
90
96
|
color: #ea580c !important;
|
|
@@ -942,6 +948,34 @@
|
|
|
942
948
|
</svg>
|
|
943
949
|
</button>
|
|
944
950
|
<div class="panel-sep"></div>
|
|
951
|
+
<div id="customShapeLabelPlacementControls" class="hidden">
|
|
952
|
+
<button
|
|
953
|
+
class="tool-btn !w-7 !h-6 font-mono text-[10px]"
|
|
954
|
+
data-label-placement="above"
|
|
955
|
+
data-i18n-title="diagram.node_panel.custom_label_above"
|
|
956
|
+
>T↑</button>
|
|
957
|
+
<button
|
|
958
|
+
class="tool-btn !w-7 !h-6 font-mono text-[10px]"
|
|
959
|
+
data-label-placement="below"
|
|
960
|
+
data-i18n-title="diagram.node_panel.custom_label_below"
|
|
961
|
+
>T↓</button>
|
|
962
|
+
<button
|
|
963
|
+
class="tool-btn !w-7 !h-6 font-mono text-[10px]"
|
|
964
|
+
data-label-placement="left"
|
|
965
|
+
data-i18n-title="diagram.node_panel.custom_label_left"
|
|
966
|
+
>←T</button>
|
|
967
|
+
<button
|
|
968
|
+
class="tool-btn !w-7 !h-6 font-mono text-[10px]"
|
|
969
|
+
data-label-placement="right"
|
|
970
|
+
data-i18n-title="diagram.node_panel.custom_label_right"
|
|
971
|
+
>T→</button>
|
|
972
|
+
<button
|
|
973
|
+
class="tool-btn !w-7 !h-6 font-mono text-[10px]"
|
|
974
|
+
data-label-placement="center"
|
|
975
|
+
data-i18n-title="diagram.node_panel.custom_label_center"
|
|
976
|
+
>T□</button>
|
|
977
|
+
<div class="panel-sep"></div>
|
|
978
|
+
</div>
|
|
945
979
|
<button
|
|
946
980
|
id="btnZOrderBack"
|
|
947
981
|
class="tool-btn !w-7 !h-6"
|
|
@@ -443,6 +443,11 @@
|
|
|
443
443
|
"diagram.node_panel.align_top": "Align top",
|
|
444
444
|
"diagram.node_panel.align_middle": "Align middle",
|
|
445
445
|
"diagram.node_panel.align_bottom": "Align bottom",
|
|
446
|
+
"diagram.node_panel.custom_label_above": "Place custom shape label above this instance",
|
|
447
|
+
"diagram.node_panel.custom_label_below": "Place custom shape label below this instance",
|
|
448
|
+
"diagram.node_panel.custom_label_left": "Place custom shape label to the left of this instance",
|
|
449
|
+
"diagram.node_panel.custom_label_right": "Place custom shape label to the right of this instance",
|
|
450
|
+
"diagram.node_panel.custom_label_center": "Center custom shape label in this instance",
|
|
446
451
|
"diagram.node_panel.send_back": "Send to back",
|
|
447
452
|
"diagram.node_panel.bring_front": "Bring to front",
|
|
448
453
|
"diagram.node_panel.stamp_color": "Color stamp — select targets, click here, then click source",
|
|
@@ -517,5 +522,13 @@
|
|
|
517
522
|
"diagram.toast.confirm_delete": "Delete this diagram?",
|
|
518
523
|
"diagram.toast.new_diagram_title": "New diagram",
|
|
519
524
|
"diagram.toast.untitled": "Untitled",
|
|
520
|
-
"diagram.toast.diagram_linked": "Diagram \"{title}\" created and linked"
|
|
525
|
+
"diagram.toast.diagram_linked": "Diagram \"{title}\" created and linked",
|
|
526
|
+
|
|
527
|
+
"shape_editor.show_in_diagram_label": "Show in diagram palette",
|
|
528
|
+
"shape_editor.show_in_diagram_hint": "When disabled, existing diagram nodes still render but this shape is hidden from the bottom palette.",
|
|
529
|
+
"shape_editor.label_placement.below": "Below the shape",
|
|
530
|
+
"shape_editor.label_placement.above": "Above the shape",
|
|
531
|
+
"shape_editor.label_placement.right": "To the right",
|
|
532
|
+
"shape_editor.label_placement.left": "To the left",
|
|
533
|
+
"shape_editor.label_placement.center": "Centered in the shape"
|
|
521
534
|
}
|
|
@@ -443,6 +443,11 @@
|
|
|
443
443
|
"diagram.node_panel.align_top": "Aligner en haut",
|
|
444
444
|
"diagram.node_panel.align_middle": "Centrer verticalement",
|
|
445
445
|
"diagram.node_panel.align_bottom": "Aligner en bas",
|
|
446
|
+
"diagram.node_panel.custom_label_above": "Placer le libellé de cette instance au-dessus",
|
|
447
|
+
"diagram.node_panel.custom_label_below": "Placer le libellé de cette instance dessous",
|
|
448
|
+
"diagram.node_panel.custom_label_left": "Placer le libellé de cette instance à gauche",
|
|
449
|
+
"diagram.node_panel.custom_label_right": "Placer le libellé de cette instance à droite",
|
|
450
|
+
"diagram.node_panel.custom_label_center": "Centrer le libellé dans cette instance",
|
|
446
451
|
"diagram.node_panel.send_back": "Envoyer en arrière",
|
|
447
452
|
"diagram.node_panel.bring_front": "Amener au premier plan",
|
|
448
453
|
"diagram.node_panel.stamp_color": "Tampon couleur — sélectionner les cibles, cliquer ici, puis cliquer la source",
|
|
@@ -517,5 +522,13 @@
|
|
|
517
522
|
"diagram.toast.confirm_delete": "Supprimer ce diagramme ?",
|
|
518
523
|
"diagram.toast.new_diagram_title": "Nouveau diagramme",
|
|
519
524
|
"diagram.toast.untitled": "Sans titre",
|
|
520
|
-
"diagram.toast.diagram_linked": "Diagramme \"{title}\" créé et lié"
|
|
525
|
+
"diagram.toast.diagram_linked": "Diagramme \"{title}\" créé et lié",
|
|
526
|
+
|
|
527
|
+
"shape_editor.show_in_diagram_label": "Afficher dans la palette du diagramme",
|
|
528
|
+
"shape_editor.show_in_diagram_hint": "Si désactivé, les nœuds existants restent rendus mais cette forme est masquée dans la palette du bas.",
|
|
529
|
+
"shape_editor.label_placement.below": "Sous la forme",
|
|
530
|
+
"shape_editor.label_placement.above": "Au-dessus de la forme",
|
|
531
|
+
"shape_editor.label_placement.right": "À droite",
|
|
532
|
+
"shape_editor.label_placement.left": "À gauche",
|
|
533
|
+
"shape_editor.label_placement.center": "Centré dans la forme"
|
|
521
534
|
}
|