living-documentation 7.35.0 → 7.36.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.
@@ -44,6 +44,12 @@ function setTool(tool, shape) {
44
44
  }
45
45
  }
46
46
 
47
+ window.addEventListener('diagram:setTool', (e) => {
48
+ const detail = e.detail || {};
49
+ if (!detail.tool) return;
50
+ setTool(detail.tool, detail.shape);
51
+ });
52
+
47
53
  function selectAll() {
48
54
  if (!st.network || !st.nodes || !st.edges) return;
49
55
  const ids = st.nodes.getIds();
@@ -34,6 +34,7 @@ let _draggingAnchorIds = new Set();
34
34
  let _rehookEdgeId = null;
35
35
  let _rehookHoveredNodeId = null;
36
36
  let _rehookHoveredPortKey = null;
37
+ let _pointerDownSelection = { nodeIds: [], edgeIds: [] };
37
38
 
38
39
  // Returns true when an edge can enter rehook mode (at least one non-anchor endpoint).
39
40
  // Works for both port edges and native vis-network edges.
@@ -126,7 +127,11 @@ export function initNetwork(savedNodes, savedEdges, edgesStraight = false) {
126
127
  callback(data);
127
128
  markDirty();
128
129
  _addEdgeFromPort = null;
129
- setTimeout(() => { if (st.currentTool === 'addEdge') st.network.addEdgeMode(); }, 0);
130
+ setTimeout(() => {
131
+ if (st.currentTool === 'addEdge') {
132
+ window.dispatchEvent(new CustomEvent('diagram:setTool', { detail: { tool: 'select' } }));
133
+ }
134
+ }, 0);
130
135
  },
131
136
  },
132
137
  };
@@ -138,6 +143,13 @@ export function initNetwork(savedNodes, savedEdges, edgesStraight = false) {
138
143
  // Must be registered BEFORE any other capture-phase mousedown listeners on
139
144
  // the container so it can stopImmediatePropagation for locked targets.
140
145
  installUnlockHold(container);
146
+ container.addEventListener('mousedown', (e) => {
147
+ if (e.button !== 0) return;
148
+ _pointerDownSelection = {
149
+ nodeIds: [...(st.selectedNodeIds || [])],
150
+ edgeIds: [...(st.selectedEdgeIds || [])],
151
+ };
152
+ }, { capture: true });
141
153
 
142
154
  // ── Z-order patch ──────────────────────────────────────────────────────────
143
155
  // vis.js renders in 3 passes (normal → selected → hovered), which breaks
@@ -189,7 +201,7 @@ export function initNetwork(savedNodes, savedEdges, edgesStraight = false) {
189
201
  } else if (fromIsAnchor && !toIsAnchor) {
190
202
  level = toLevel;
191
203
  } else {
192
- level = Math.min(fromLevel, toLevel);
204
+ level = Math.max(fromLevel, toLevel);
193
205
  }
194
206
  if (!edgesByLevel.has(level)) edgesByLevel.set(level, []);
195
207
  edgesByLevel.get(level).push(edge);
@@ -1037,26 +1049,121 @@ function _distToSegment(px, py, ax, ay, bx, by) {
1037
1049
  }
1038
1050
 
1039
1051
 
1040
- // Returns the topmost (highest z-order) node whose bounding box contains canvasPos.
1052
+ function nodeContainsCanvasPoint(id, canvasPos) {
1053
+ const n = st.nodes.get(id);
1054
+ const bn = st.network && st.network.body.nodes[id];
1055
+ if (!n || !bn || n.shapeType === 'anchor') return false;
1056
+
1057
+ const shapeType = n.shapeType || 'box';
1058
+ const defaults = SHAPE_DEFAULTS[shapeType] || [60, 28];
1059
+ const W = n.nodeWidth || defaults[0];
1060
+ const H = shapeType === 'circle' ? W : (n.nodeHeight || defaults[1]);
1061
+
1062
+ const dx = canvasPos.x - bn.x;
1063
+ const dy = canvasPos.y - bn.y;
1064
+ const rot = n.rotation || 0;
1065
+ const lx = rot ? dx * Math.cos(-rot) - dy * Math.sin(-rot) : dx;
1066
+ const ly = rot ? dx * Math.sin(-rot) + dy * Math.cos(-rot) : dy;
1067
+
1068
+ if (shapeType === 'circle' || shapeType === 'ellipse') {
1069
+ const rx = W / 2;
1070
+ const ry = H / 2;
1071
+ return rx > 0 && ry > 0 && ((lx * lx) / (rx * rx) + (ly * ly) / (ry * ry)) <= 1;
1072
+ }
1073
+
1074
+ return Math.abs(lx) <= W / 2 && Math.abs(ly) <= H / 2;
1075
+ }
1076
+
1077
+ // Returns the topmost (highest z-order) node containing canvasPos.
1041
1078
  // Ignores anchor nodes and respects st.canonicalOrder.
1042
1079
  function topmostNodeAt(canvasPos) {
1043
- let topmost = null;
1044
- let topmostIdx = -1;
1045
- for (const id of st.canonicalOrder) {
1046
- const n = st.nodes.get(id);
1047
- const bn = st.network && st.network.body.nodes[id];
1048
- if (!n || !bn || n.shapeType === 'anchor') continue;
1049
- const defaults = SHAPE_DEFAULTS[n.shapeType || 'box'] || [60, 28];
1050
- const W = n.nodeWidth || defaults[0];
1051
- const H = n.nodeHeight || defaults[1];
1052
- const cx = bn.x, cy = bn.y;
1053
- if (canvasPos.x >= cx - W / 2 && canvasPos.x <= cx + W / 2 &&
1054
- canvasPos.y >= cy - H / 2 && canvasPos.y <= cy + H / 2) {
1055
- const idx = st.canonicalOrder.indexOf(id);
1056
- if (idx > topmostIdx) { topmostIdx = idx; topmost = id; }
1080
+ for (let i = st.canonicalOrder.length - 1; i >= 0; i--) {
1081
+ const id = st.canonicalOrder[i];
1082
+ if (nodeContainsCanvasPoint(id, canvasPos)) return id;
1083
+ }
1084
+ return null;
1085
+ }
1086
+
1087
+ function edgeDrawLevel(edgeData) {
1088
+ if (!edgeData) return -1;
1089
+ const fromLevel = st.canonicalOrder.indexOf(edgeData.from);
1090
+ const toLevel = st.canonicalOrder.indexOf(edgeData.to);
1091
+ if (fromLevel === -1 && toLevel === -1) return -1;
1092
+ if (fromLevel === -1) return toLevel;
1093
+ if (toLevel === -1) return fromLevel;
1094
+
1095
+ const fromNode = st.nodes.get(edgeData.from);
1096
+ const toNode = st.nodes.get(edgeData.to);
1097
+ const fromIsAnchor = fromNode && fromNode.shapeType === 'anchor';
1098
+ const toIsAnchor = toNode && toNode.shapeType === 'anchor';
1099
+ if (toIsAnchor && !fromIsAnchor) return fromLevel;
1100
+ if (fromIsAnchor && !toIsAnchor) return toLevel;
1101
+ return Math.max(fromLevel, toLevel);
1102
+ }
1103
+
1104
+ function nearestPortEdgeAt(canvasPos, threshold = 8) {
1105
+ const portEdges = st.edges.get({ filter: (e) => e.fromPort || e.toPort });
1106
+ let nearest = null;
1107
+ let nearestDist = Infinity;
1108
+ for (const edge of portEdges) {
1109
+ const d = distanceToPortEdge(edge, canvasPos);
1110
+ if (d < nearestDist) {
1111
+ nearestDist = d;
1112
+ nearest = edge;
1057
1113
  }
1058
1114
  }
1059
- return topmost;
1115
+ return nearest && nearestDist <= threshold ? nearest : null;
1116
+ }
1117
+
1118
+ function selectableEdgesForNodes(nodeIds) {
1119
+ const selectedSet = new Set(nodeIds);
1120
+ return st.edges.get().filter((e) => {
1121
+ if (!selectedSet.has(e.from) || !selectedSet.has(e.to)) return false;
1122
+ const fromN = st.nodes.get(e.from);
1123
+ const toN = st.nodes.get(e.to);
1124
+ const isFreeArrow = fromN && fromN.shapeType === 'anchor' && toN && toN.shapeType === 'anchor';
1125
+ return isFreeArrow ? !(fromN.locked && toN.locked) : !e.edgeLocked;
1126
+ }).map((e) => e.id);
1127
+ }
1128
+
1129
+ function selectNodesFromClick(nodeId, srcEvent) {
1130
+ const additive = !!(srcEvent && (srcEvent.metaKey || srcEvent.ctrlKey));
1131
+ const clicked = expandSelectionToGroup([nodeId]).filter((id) => {
1132
+ const n = st.nodes.get(id);
1133
+ return n && !n.locked && n.shapeType !== 'anchor';
1134
+ });
1135
+ if (!clicked.length) return;
1136
+
1137
+ let nodeIds;
1138
+ if (additive) {
1139
+ const next = new Set(_pointerDownSelection.nodeIds || []);
1140
+ const allAlreadySelected = clicked.every((id) => next.has(id));
1141
+ clicked.forEach((id) => {
1142
+ if (allAlreadySelected) next.delete(id);
1143
+ else next.add(id);
1144
+ });
1145
+ nodeIds = Array.from(next).filter((id) => {
1146
+ const n = st.nodes.get(id);
1147
+ return n && !n.locked && n.shapeType !== 'anchor';
1148
+ });
1149
+ } else {
1150
+ nodeIds = clicked;
1151
+ }
1152
+
1153
+ const edgeIds = selectableEdgesForNodes(nodeIds);
1154
+ _addingEdgesToSelection = true;
1155
+ st.network.setSelection({ nodes: nodeIds, edges: edgeIds });
1156
+ _addingEdgesToSelection = false;
1157
+ st.selectedNodeIds = nodeIds;
1158
+ st.selectedEdgeIds = edgeIds;
1159
+ _rehookEdgeId = _rehookHoveredNodeId = _rehookHoveredPortKey = null;
1160
+
1161
+ hideEdgePanel();
1162
+ if (nodeIds.length) showNodePanel();
1163
+ else {
1164
+ hideNodePanel();
1165
+ hideSelectionOverlay();
1166
+ }
1060
1167
  }
1061
1168
 
1062
1169
  function onDoubleClick(params) {
@@ -1229,8 +1336,8 @@ function onClickNode(params) {
1229
1336
  update.color = { color: 'rgba(0,0,0,0)', highlight: 'rgba(0,0,0,0)', hover: 'rgba(0,0,0,0)' };
1230
1337
  update.arrows = { to: { enabled: false }, from: { enabled: false } };
1231
1338
  }
1232
- st.edges.update(update);
1233
1339
  pushSnapshot();
1340
+ st.edges.update(update);
1234
1341
  markDirty();
1235
1342
  const edgeId = edgeData.id;
1236
1343
  _rehookHoveredNodeId = _rehookHoveredPortKey = null;
@@ -1244,14 +1351,34 @@ function onClickNode(params) {
1244
1351
  }
1245
1352
  }
1246
1353
 
1247
- // When a node is reported, params.edges is always empty — vis-network short-circuits
1354
+ // When a node is reported, params.edges is usually empty — vis-network short-circuits
1248
1355
  // edge detection once a node is found. Fix: call getEdgeAt() directly with CLIENT
1249
1356
  // coordinates. params.pointer.DOM is already offset-relative to the container, so
1250
1357
  // passing it to getEdgeAt() causes a double-subtraction of the container rect and
1251
1358
  // returns null. Passing clientX/Y lets vis-network do its own pixel-perfect detection.
1252
1359
  if (params.nodes.length > 0) {
1253
1360
  const clientPos = { x: params.event.srcEvent.clientX, y: params.event.srcEvent.clientY };
1254
- const edgeId = st.network.getEdgeAt(clientPos);
1361
+ const nativeEdgeId = st.network.getEdgeAt(clientPos);
1362
+ const portEdge = nearestPortEdgeAt(params.pointer.canvas);
1363
+ const edgeId = nativeEdgeId || (portEdge && portEdge.id);
1364
+
1365
+ // First honour the app-managed z-order. vis-network may report an edge or a
1366
+ // lower node at the click point; compare draw levels so the visibly top
1367
+ // element gets the click.
1368
+ const top = topmostNodeAt(params.pointer.canvas);
1369
+ if (top) {
1370
+ const topNode = st.nodes.get(top);
1371
+ const topLevel = st.canonicalOrder.indexOf(top);
1372
+ const edgeLevel = edgeId ? edgeDrawLevel(st.edges.get(edgeId)) : -1;
1373
+ if (topNode && !topNode.locked && topNode.shapeType !== 'anchor') {
1374
+ if (!edgeId || topLevel >= edgeLevel) {
1375
+ setTimeout(() => {
1376
+ selectNodesFromClick(top, params.event.srcEvent);
1377
+ }, 0);
1378
+ return;
1379
+ }
1380
+ }
1381
+ }
1255
1382
  if (edgeId) {
1256
1383
  const edge = st.edges.get(edgeId);
1257
1384
  const fromN = edge && st.nodes.get(edge.from);
@@ -1277,12 +1404,12 @@ function onClickNode(params) {
1277
1404
  return;
1278
1405
  }
1279
1406
  // No edge at click position — apply z-order correction for the topmost node.
1280
- const top = topmostNodeAt(params.pointer.canvas);
1407
+ const fallbackTop = topmostNodeAt(params.pointer.canvas);
1281
1408
  const clickable = params.nodes.filter(id => {
1282
1409
  const n = st.nodes.get(id);
1283
1410
  return n && !n.locked && n.shapeType !== 'anchor';
1284
1411
  });
1285
- if (!top || !clickable.includes(top)) {
1412
+ if (!fallbackTop || !clickable.includes(fallbackTop)) {
1286
1413
  const fallbackEdgeId = params.edges.length > 0 ? params.edges[0] : null;
1287
1414
  if (fallbackEdgeId) {
1288
1415
  setTimeout(() => {
@@ -1305,19 +1432,12 @@ function onClickNode(params) {
1305
1432
  // Also check the canvas for port-edge proximity when nothing was hit.
1306
1433
  if (params.nodes.length === 0 && params.edges.length === 0) {
1307
1434
  const cp = params.pointer.canvas;
1308
- const THRESHOLD = 8;
1309
1435
 
1310
1436
  // ── Port edge proximity check ──────────────────────────────────────────
1311
1437
  // vis-network's hit detection uses the invisible centre-to-centre ghost,
1312
1438
  // so port edges that diverge visually from that path are not selectable.
1313
- // We scan all port edges and pick the one whose bezier path is closest.
1314
- const portEdges = st.edges.get({ filter: (e) => e.fromPort || e.toPort });
1315
- let nearest = null, nearestDist = Infinity;
1316
- for (const edge of portEdges) {
1317
- const d = distanceToPortEdge(edge, cp);
1318
- if (d < nearestDist) { nearestDist = d; nearest = edge; }
1319
- }
1320
- if (nearest && nearestDist <= THRESHOLD) {
1439
+ const nearest = nearestPortEdgeAt(cp);
1440
+ if (nearest) {
1321
1441
  st.network.selectEdges([nearest.id]);
1322
1442
  st.selectedEdgeIds = [nearest.id];
1323
1443
  st.selectedNodeIds = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "living-documentation",
3
- "version": "7.35.0",
3
+ "version": "7.36.0",
4
4
  "description": "A CLI tool that serves a local Markdown documentation viewer",
5
5
  "main": "dist/src/server.js",
6
6
  "bin": {