@ngroznykh/papirus 0.9.3 → 0.9.5

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/papirus.js CHANGED
@@ -1255,6 +1255,9 @@ var DragManager = class extends EventEmitter {
1255
1255
  };
1256
1256
  //#endregion
1257
1257
  //#region src/core/ResizeManager.ts
1258
+ function cursorForResizeHandle(handle) {
1259
+ return handle === "nw" || handle === "se" ? "nwse-resize" : "nesw-resize";
1260
+ }
1258
1261
  /**
1259
1262
  * Manages node resizing via corner handles
1260
1263
  */
@@ -1320,6 +1323,16 @@ var ResizeManager = class extends EventEmitter {
1320
1323
  return true;
1321
1324
  }
1322
1325
  /**
1326
+ * Set canvas cursor when the pointer is over a selected node's resize handle.
1327
+ * Call after badge/connection hover so the resize cursor wins on corners.
1328
+ */
1329
+ updateHoverCursor(event) {
1330
+ if (this.renderer.blocksDiagramPointerAtScreen(event.screenX, event.screenY)) return;
1331
+ const handle = this.isResizing ? this.handle : this.hitTestSelectedHandle(event);
1332
+ if (!handle) return;
1333
+ this.setCursor(cursorForResizeHandle(handle));
1334
+ }
1335
+ /**
1323
1336
  * Handle mouse move - update resize
1324
1337
  */
1325
1338
  handleMouseMove(event) {
@@ -1436,6 +1449,20 @@ var ResizeManager = class extends EventEmitter {
1436
1449
  height
1437
1450
  };
1438
1451
  }
1452
+ hitTestSelectedHandle(event) {
1453
+ const selectedIds = Array.from(this.selectionManager.selectedIds);
1454
+ if (selectedIds.length !== 1) return null;
1455
+ const node = this.renderer.getNode(selectedIds[0]);
1456
+ if (!node) return null;
1457
+ return node.hitTestResizeHandle({
1458
+ x: event.worldX,
1459
+ y: event.worldY
1460
+ });
1461
+ }
1462
+ setCursor(cursor) {
1463
+ const canvas = this.renderer.getCanvas();
1464
+ if (canvas.style.cursor !== cursor) canvas.style.cursor = cursor;
1465
+ }
1439
1466
  reset() {
1440
1467
  this.isResizing = false;
1441
1468
  this.resizedNodeId = null;
@@ -1456,7 +1483,7 @@ function getContentBounds({ nodes, edges, groups, includeInvisible = false }) {
1456
1483
  let hasContent = false;
1457
1484
  const consider = (source) => {
1458
1485
  if (!includeInvisible && !source.visible) return;
1459
- const bounds = source.getBounds();
1486
+ const bounds = source.getVisualBounds?.() ?? source.getBounds();
1460
1487
  minX = Math.min(minX, bounds.x);
1461
1488
  minY = Math.min(minY, bounds.y);
1462
1489
  maxX = Math.max(maxX, bounds.x + bounds.width);
@@ -1671,6 +1698,133 @@ var NavigationManager = class extends EventEmitter {
1671
1698
  }
1672
1699
  };
1673
1700
  //#endregion
1701
+ //#region src/utils/edgePath.ts
1702
+ /**
1703
+ * Point and tangent angle at a normalized position along an edge path (0..1 by length).
1704
+ */
1705
+ function getPathPointAt(path, type, position) {
1706
+ if (type !== "bezier" || path.length < 4) return getPointAlongPolyline(path, position);
1707
+ const samples = [];
1708
+ const steps = 20;
1709
+ for (let i = 1; i + 2 < path.length; i += 3) {
1710
+ const start = path[i - 1];
1711
+ const control1 = path[i];
1712
+ const control2 = path[i + 1];
1713
+ const end = path[i + 2];
1714
+ for (let sample = 0; sample <= steps; sample++) {
1715
+ const t = sample / steps;
1716
+ if (samples.length > 0 && t === 0) continue;
1717
+ samples.push(bezierPoint(start, control1, control2, end, t));
1718
+ }
1719
+ }
1720
+ return getPointAlongPolyline(samples, position);
1721
+ }
1722
+ function getPointAlongPolyline(path, position) {
1723
+ if (path.length === 0) return {
1724
+ point: {
1725
+ x: 0,
1726
+ y: 0
1727
+ },
1728
+ angle: 0
1729
+ };
1730
+ if (path.length === 1) return {
1731
+ point: path[0],
1732
+ angle: 0
1733
+ };
1734
+ let totalLength = 0;
1735
+ const segments = [];
1736
+ for (let i = 1; i < path.length; i++) {
1737
+ const start = path[i - 1];
1738
+ const end = path[i];
1739
+ const length = Math.hypot(end.x - start.x, end.y - start.y);
1740
+ segments.push({
1741
+ start,
1742
+ end,
1743
+ length
1744
+ });
1745
+ totalLength += length;
1746
+ }
1747
+ const targetLength = totalLength * Math.max(0, Math.min(1, position));
1748
+ let accumulated = 0;
1749
+ for (const segment of segments) {
1750
+ if (accumulated + segment.length >= targetLength) {
1751
+ const segmentPosition = segment.length > 0 ? (targetLength - accumulated) / segment.length : 0;
1752
+ return {
1753
+ point: {
1754
+ x: segment.start.x + segmentPosition * (segment.end.x - segment.start.x),
1755
+ y: segment.start.y + segmentPosition * (segment.end.y - segment.start.y)
1756
+ },
1757
+ angle: Math.atan2(segment.end.y - segment.start.y, segment.end.x - segment.start.x)
1758
+ };
1759
+ }
1760
+ accumulated += segment.length;
1761
+ }
1762
+ const lastSegment = segments[segments.length - 1];
1763
+ return {
1764
+ point: lastSegment.end,
1765
+ angle: Math.atan2(lastSegment.end.y - lastSegment.start.y, lastSegment.end.x - lastSegment.start.x)
1766
+ };
1767
+ }
1768
+ /** Map tangent angle to a cardinal direction used by polyline routing. */
1769
+ function directionFromAngle(angle) {
1770
+ const twoPi = Math.PI * 2;
1771
+ const a = (angle % twoPi + twoPi) % twoPi;
1772
+ if (a >= Math.PI * .25 && a < Math.PI * .75) return "bottom";
1773
+ if (a >= Math.PI * .75 && a < Math.PI * 1.25) return "left";
1774
+ if (a >= Math.PI * 1.25 && a < Math.PI * 1.75) return "top";
1775
+ return "right";
1776
+ }
1777
+ function closestPointOnPolyline(polyline, point) {
1778
+ if (polyline.length < 2) return null;
1779
+ let totalLength = 0;
1780
+ for (let i = 1; i < polyline.length; i++) {
1781
+ const start = polyline[i - 1];
1782
+ const end = polyline[i];
1783
+ totalLength += Math.hypot(end.x - start.x, end.y - start.y);
1784
+ }
1785
+ let bestPoint = polyline[0];
1786
+ let bestParam = 0;
1787
+ let bestDist = Infinity;
1788
+ let accumulated = 0;
1789
+ for (let i = 1; i < polyline.length; i++) {
1790
+ const start = polyline[i - 1];
1791
+ const end = polyline[i];
1792
+ const dx = end.x - start.x;
1793
+ const dy = end.y - start.y;
1794
+ const lengthSq = dx * dx + dy * dy;
1795
+ const segLen = Math.sqrt(lengthSq);
1796
+ const t = lengthSq === 0 ? 0 : Math.max(0, Math.min(1, ((point.x - start.x) * dx + (point.y - start.y) * dy) / lengthSq));
1797
+ const proj = {
1798
+ x: start.x + t * dx,
1799
+ y: start.y + t * dy
1800
+ };
1801
+ const dist = Math.hypot(point.x - proj.x, point.y - proj.y);
1802
+ if (dist < bestDist) {
1803
+ bestDist = dist;
1804
+ bestPoint = proj;
1805
+ bestParam = totalLength > 0 ? (accumulated + t * segLen) / totalLength : 0;
1806
+ }
1807
+ accumulated += segLen;
1808
+ }
1809
+ return {
1810
+ point: bestPoint,
1811
+ pathParam: bestParam,
1812
+ distance: bestDist
1813
+ };
1814
+ }
1815
+ /**
1816
+ * Nearest point on an edge path to `point`.
1817
+ * Polylines project onto real segments so a click on a long orthogonal stroke
1818
+ * is not lost between length samples. Bezier paths are densified, then projected.
1819
+ */
1820
+ function getClosestPointOnPath(path, type, point, samples = 48) {
1821
+ if (path.length < 2) return null;
1822
+ if (type !== "bezier" || path.length < 4) return closestPointOnPolyline(path, point);
1823
+ const densified = [];
1824
+ for (let i = 0; i <= samples; i++) densified.push(getPathPointAt(path, type, i / samples).point);
1825
+ return closestPointOnPolyline(densified, point);
1826
+ }
1827
+ //#endregion
1674
1828
  //#region src/utils/direction.ts
1675
1829
  /**
1676
1830
  * Map outline param [0,1) to cardinal direction.
@@ -2048,7 +2202,18 @@ var ConnectionManager = class extends EventEmitter {
2048
2202
  let snappedPoint = point;
2049
2203
  let snappedDir;
2050
2204
  let targetNode = this.getNodeAtPoint(point, true);
2051
- if (this.attachToOutline) {
2205
+ const edgeDrop = this.originalEdgeEndpoint?.edgeId ? this.findEdgeDropTarget(point, {
2206
+ maxScreenPx: 40,
2207
+ skipEdgeIds: new Set([edge.id])
2208
+ }) : null;
2209
+ if (edgeDrop) {
2210
+ const at = edgeDrop.edge.getPointAt(edgeDrop.pathParam);
2211
+ snappedPoint = edgeDrop.point;
2212
+ snappedDir = at ? directionFromAngle(at.angle) : void 0;
2213
+ targetNode = null;
2214
+ this.reconnectingOutlineParam = null;
2215
+ this.reconnectingTargetNodeId = null;
2216
+ } else if (this.attachToOutline) {
2052
2217
  const tolerance = 40 / Math.max(this.renderer.zoom, 1e-4);
2053
2218
  const toleranceSq = tolerance * tolerance;
2054
2219
  let bestDistSq = Infinity;
@@ -2215,6 +2380,9 @@ var ConnectionManager = class extends EventEmitter {
2215
2380
  this.previewTargetNodeId = null;
2216
2381
  this.previewTargetEdgeId = null;
2217
2382
  this.previewTargetPathParam = null;
2383
+ const rawNode = this.getRawNodeAtPoint(cursorPoint);
2384
+ const forbiddenFill = !!this._connectionValidator && !!this.sourceNode && !!rawNode && !this._connectionValidator(this.sourceNode.id, rawNode.id);
2385
+ const searchGroupFill = !!rawNode && (forbiddenFill || this.nodeContainsSource(rawNode) || this.nodeLooksLikeContainer(rawNode));
2218
2386
  let targetNode = this.getNodeAtPoint(cursorPoint, false);
2219
2387
  if (this.attachToOutline) {
2220
2388
  const tolerance = 40 / Math.max(this.renderer.zoom, 1e-4);
@@ -2258,7 +2426,7 @@ var ConnectionManager = class extends EventEmitter {
2258
2426
  }
2259
2427
  }
2260
2428
  }
2261
- const edgeDrop = this.findEdgeDropTarget(cursorPoint);
2429
+ const edgeDrop = this.findEdgeDropTarget(cursorPoint, searchGroupFill ? { maxScreenPx: 40 } : void 0);
2262
2430
  if (edgeDrop) {
2263
2431
  let nodeDistance = Infinity;
2264
2432
  if (targetNode) {
@@ -2266,7 +2434,7 @@ var ConnectionManager = class extends EventEmitter {
2266
2434
  const dy = cursorPoint.y - snappedPoint.y;
2267
2435
  nodeDistance = Math.sqrt(dx * dx + dy * dy);
2268
2436
  }
2269
- if (!targetNode || edgeDrop.distance < nodeDistance) {
2437
+ if (searchGroupFill || !targetNode || edgeDrop.distance <= nodeDistance) {
2270
2438
  targetNode = null;
2271
2439
  this.previewTargetNodeId = null;
2272
2440
  this.previewTargetOutlineParam = null;
@@ -2275,6 +2443,12 @@ var ConnectionManager = class extends EventEmitter {
2275
2443
  this.previewTargetEdgeId = edgeDrop.edge.id;
2276
2444
  this.previewTargetPathParam = edgeDrop.pathParam;
2277
2445
  }
2446
+ } else if (searchGroupFill) {
2447
+ targetNode = null;
2448
+ this.previewTargetNodeId = null;
2449
+ this.previewTargetOutlineParam = null;
2450
+ this.previewTargetAnchorId = null;
2451
+ snappedPoint = cursorPoint;
2278
2452
  }
2279
2453
  if (targetNode) {
2280
2454
  const forbidden = this._connectionValidator && this.sourceNode && !this._connectionValidator(this.sourceNode.id, targetNode.id);
@@ -2336,9 +2510,23 @@ var ConnectionManager = class extends EventEmitter {
2336
2510
  y: event.worldY
2337
2511
  };
2338
2512
  let reconnected = false;
2513
+ const edgeDrop = this.originalEdgeEndpoint?.edgeId ? this.findEdgeDropTarget(point, {
2514
+ maxScreenPx: 40,
2515
+ skipEdgeIds: new Set([this.reconnectingEdge.id])
2516
+ }) : null;
2517
+ if (edgeDrop) {
2518
+ const endpoint = {
2519
+ edgeId: edgeDrop.edgeId,
2520
+ pathParam: edgeDrop.pathParam
2521
+ };
2522
+ if (this.reconnectingEndpoint === "start") this.reconnectingEdge.from = endpoint;
2523
+ else this.reconnectingEdge.to = endpoint;
2524
+ reconnected = true;
2525
+ this.emit("edgeReconnect", this.reconnectingEdge, this.reconnectingEndpoint);
2526
+ }
2339
2527
  let node = this.getNodeAtPoint(point, true);
2340
2528
  if (!node && this.attachToOutline && this.reconnectingTargetNodeId) node = this.renderer.getNode(this.reconnectingTargetNodeId) ?? null;
2341
- if (node) {
2529
+ if (!reconnected && node) {
2342
2530
  if (this.attachToOutline && this.reconnectingOutlineParam !== null) {
2343
2531
  const endpoint = {
2344
2532
  nodeId: node.id,
@@ -2543,11 +2731,12 @@ var ConnectionManager = class extends EventEmitter {
2543
2731
  /**
2544
2732
  * Find the nearest edge under the cursor for junction / note→relation drops.
2545
2733
  */
2546
- findEdgeDropTarget(point) {
2547
- const tolerance = Math.max(14, 20 / Math.max(this.renderer.zoom, 1e-4));
2734
+ findEdgeDropTarget(point, options) {
2735
+ const tolerance = options?.maxScreenPx === void 0 ? Math.max(14, 20 / Math.max(this.renderer.zoom, 1e-4)) : options.maxScreenPx / Math.max(this.renderer.zoom, 1e-4);
2548
2736
  let best = null;
2549
2737
  for (const edge of this.renderer.edges.values()) {
2550
2738
  if (!edge.visible || edge.path.length < 2) continue;
2739
+ if (options?.skipEdgeIds?.has(edge.id)) continue;
2551
2740
  const closest = edge.getClosestPointOnPath(point);
2552
2741
  if (!closest || closest.distance > tolerance) continue;
2553
2742
  if (!best || closest.distance < best.distance) best = {
@@ -2806,6 +2995,26 @@ var ConnectionManager = class extends EventEmitter {
2806
2995
  }
2807
2996
  return null;
2808
2997
  }
2998
+ getRawNodeAtPoint(point) {
2999
+ const nodes = Array.from(this.renderer.nodes.values());
3000
+ for (let i = nodes.length - 1; i >= 0; i--) {
3001
+ const node = nodes[i];
3002
+ if (node.visible && node.hitTest(point)) return node;
3003
+ }
3004
+ return null;
3005
+ }
3006
+ nodeLooksLikeContainer(node, source = this.sourceNode) {
3007
+ if (!source || source.id === node.id) return false;
3008
+ return node.width * node.height >= Math.max(source.width * source.height, 1) * 4;
3009
+ }
3010
+ nodeContainsSource(node, source = this.sourceNode) {
3011
+ if (!source || source.id === node.id) return false;
3012
+ const sourceBounds = source.getBounds();
3013
+ const nodeBounds = node.getBounds();
3014
+ const cx = sourceBounds.x + sourceBounds.width / 2;
3015
+ const cy = sourceBounds.y + sourceBounds.height / 2;
3016
+ return cx >= nodeBounds.x && cx <= nodeBounds.x + nodeBounds.width && cy >= nodeBounds.y && cy <= nodeBounds.y + nodeBounds.height;
3017
+ }
2809
3018
  startOutlineConnection(node, attach) {
2810
3019
  const { point: outlinePoint, param } = node.getClosestPointOnOutline(attach);
2811
3020
  this.sourceNode = node;
@@ -2940,6 +3149,9 @@ function getHistoryShortcut(event, keys = DEFAULT_HISTORY_SHORTCUT_KEYS) {
2940
3149
  }
2941
3150
  //#endregion
2942
3151
  //#region src/utils/style.ts
3152
+ function canvasFontFromTextStyle(style) {
3153
+ return `${style.fontStyle ?? "normal"} ${style.fontWeight ?? "normal"} ${style.fontSize ?? 14}px ${style.fontFamily ?? "sans-serif"}`;
3154
+ }
2943
3155
  /**
2944
3156
  * Shallow compare two objects
2945
3157
  */
@@ -3039,6 +3251,7 @@ var DEFAULT_STYLE = {
3039
3251
  fontSize: 14,
3040
3252
  fontFamily: "sans-serif",
3041
3253
  fontWeight: "normal",
3254
+ fontStyle: "normal",
3042
3255
  color: "#000000",
3043
3256
  opacity: 1,
3044
3257
  align: "center",
@@ -3283,9 +3496,7 @@ var TextLabel = class {
3283
3496
  this.render(ctx, bounds);
3284
3497
  }
3285
3498
  applyStyle(ctx) {
3286
- const fontSize = this._style.fontSize ?? 14;
3287
- const fontFamily = this._style.fontFamily ?? "sans-serif";
3288
- ctx.font = `${this._style.fontWeight ?? "normal"} ${fontSize}px ${fontFamily}`;
3499
+ ctx.font = canvasFontFromTextStyle(this._style);
3289
3500
  ctx.textAlign = this._style.align ?? "center";
3290
3501
  ctx.textBaseline = this._style.baseline ?? "middle";
3291
3502
  }
@@ -3919,109 +4130,6 @@ var Element = class {
3919
4130
  }
3920
4131
  };
3921
4132
  //#endregion
3922
- //#region src/utils/edgePath.ts
3923
- /**
3924
- * Point and tangent angle at a normalized position along an edge path (0..1 by length).
3925
- */
3926
- function getPathPointAt(path, type, position) {
3927
- if (type !== "bezier" || path.length < 4) return getPointAlongPolyline(path, position);
3928
- const samples = [];
3929
- const steps = 20;
3930
- for (let i = 1; i + 2 < path.length; i += 3) {
3931
- const start = path[i - 1];
3932
- const control1 = path[i];
3933
- const control2 = path[i + 1];
3934
- const end = path[i + 2];
3935
- for (let sample = 0; sample <= steps; sample++) {
3936
- const t = sample / steps;
3937
- if (samples.length > 0 && t === 0) continue;
3938
- samples.push(bezierPoint(start, control1, control2, end, t));
3939
- }
3940
- }
3941
- return getPointAlongPolyline(samples, position);
3942
- }
3943
- function getPointAlongPolyline(path, position) {
3944
- if (path.length === 0) return {
3945
- point: {
3946
- x: 0,
3947
- y: 0
3948
- },
3949
- angle: 0
3950
- };
3951
- if (path.length === 1) return {
3952
- point: path[0],
3953
- angle: 0
3954
- };
3955
- let totalLength = 0;
3956
- const segments = [];
3957
- for (let i = 1; i < path.length; i++) {
3958
- const start = path[i - 1];
3959
- const end = path[i];
3960
- const length = Math.hypot(end.x - start.x, end.y - start.y);
3961
- segments.push({
3962
- start,
3963
- end,
3964
- length
3965
- });
3966
- totalLength += length;
3967
- }
3968
- const targetLength = totalLength * Math.max(0, Math.min(1, position));
3969
- let accumulated = 0;
3970
- for (const segment of segments) {
3971
- if (accumulated + segment.length >= targetLength) {
3972
- const segmentPosition = segment.length > 0 ? (targetLength - accumulated) / segment.length : 0;
3973
- return {
3974
- point: {
3975
- x: segment.start.x + segmentPosition * (segment.end.x - segment.start.x),
3976
- y: segment.start.y + segmentPosition * (segment.end.y - segment.start.y)
3977
- },
3978
- angle: Math.atan2(segment.end.y - segment.start.y, segment.end.x - segment.start.x)
3979
- };
3980
- }
3981
- accumulated += segment.length;
3982
- }
3983
- const lastSegment = segments[segments.length - 1];
3984
- return {
3985
- point: lastSegment.end,
3986
- angle: Math.atan2(lastSegment.end.y - lastSegment.start.y, lastSegment.end.x - lastSegment.start.x)
3987
- };
3988
- }
3989
- /** Map tangent angle to a cardinal direction used by polyline routing. */
3990
- function directionFromAngle(angle) {
3991
- const twoPi = Math.PI * 2;
3992
- const a = (angle % twoPi + twoPi) % twoPi;
3993
- if (a >= Math.PI * .25 && a < Math.PI * .75) return "bottom";
3994
- if (a >= Math.PI * .75 && a < Math.PI * 1.25) return "left";
3995
- if (a >= Math.PI * 1.25 && a < Math.PI * 1.75) return "top";
3996
- return "right";
3997
- }
3998
- /**
3999
- * Nearest point on an edge path to `point`, by sampling along normalized length.
4000
- */
4001
- function getClosestPointOnPath(path, type, point, samples = 48) {
4002
- if (path.length < 2) return null;
4003
- let bestPoint = getPathPointAt(path, type, 0).point;
4004
- let bestParam = 0;
4005
- let bestDistSq = Infinity;
4006
- for (let i = 0; i <= samples; i++) {
4007
- const pathParam = i / samples;
4008
- const at = getPathPointAt(path, type, pathParam).point;
4009
- const dx = at.x - point.x;
4010
- const dy = at.y - point.y;
4011
- const distSq = dx * dx + dy * dy;
4012
- if (distSq < bestDistSq) {
4013
- bestDistSq = distSq;
4014
- bestPoint = at;
4015
- bestParam = pathParam;
4016
- }
4017
- }
4018
- return {
4019
- point: bestPoint,
4020
- pathParam: bestParam,
4021
- distance: Math.sqrt(bestDistSq)
4022
- };
4023
- }
4024
- //#endregion
4025
4133
  //#region src/elements/paths/StraightPathStrategy.ts
4026
4134
  /**
4027
4135
  * Straight line path strategy
@@ -7083,7 +7191,12 @@ var Edge = class extends Element {
7083
7191
  * Nearest point on this edge's path to `point` (world coordinates).
7084
7192
  */
7085
7193
  getClosestPointOnPath(point) {
7086
- return getClosestPointOnPath(this._path, this._type, point);
7194
+ const onPath = getClosestPointOnPath(this._path, this._type, point);
7195
+ if (onPath) return onPath;
7196
+ const start = this._fromPoint;
7197
+ const end = this._toPoint;
7198
+ if (start.x === end.x && start.y === end.y) return null;
7199
+ return getClosestPointOnPath([start, end], "straight", point);
7087
7200
  }
7088
7201
  /**
7089
7202
  * Get world position of label center along path.
@@ -7839,9 +7952,13 @@ var InteractionManager = class {
7839
7952
  if (overOverlayPointerSink && !this.connectionManager.connecting && !this.connectionManager.isEditingEdgeControlPoint) this.connectionManager.clearPointerHover();
7840
7953
  if (overScrollbar || blockDiagramRoutesUnderOverlay) return;
7841
7954
  if (!this.navigationOnly) {
7842
- if (this.resizeManager.handleMouseMove(event)) return;
7955
+ if (this.resizeManager.handleMouseMove(event)) {
7956
+ this.resizeManager.updateHoverCursor(event);
7957
+ return;
7958
+ }
7843
7959
  if (this.connectionManager.handleMouseMove(event)) return;
7844
7960
  if (this.dragManager.handleMouseMove(event)) return;
7961
+ this.resizeManager.updateHoverCursor(event);
7845
7962
  }
7846
7963
  if (this.selectionManager.selectionRectangle !== null) {
7847
7964
  this.selectionManager.updateSelectionRect({
@@ -7940,7 +8057,13 @@ var InteractionManager = class {
7940
8057
  }
7941
8058
  if ("typeName" in hitElement) {
7942
8059
  if (hitElement.typeName === "composite") {
7943
- this.startCompositeNameEdit(hitElement);
8060
+ const composite = hitElement;
8061
+ if (composite.usesExternalLabel()) {
8062
+ const editText = composite.label?.editableText ?? composite.label?.text ?? "";
8063
+ this.startLabelEdit("node", composite.id, editText, composite.getLabelPosition());
8064
+ return;
8065
+ }
8066
+ this.startCompositeNameEdit(composite);
7944
8067
  return;
7945
8068
  }
7946
8069
  const editText = hitElement.label?.editableText ?? hitElement.label?.text ?? "";
@@ -9607,19 +9730,27 @@ var DiagramRenderer = class extends EventEmitter {
9607
9730
  return this._groups.get(groupId);
9608
9731
  }
9609
9732
  /**
9610
- * Find element at a point (nodes, then edges, then groups)
9733
+ * Find element at a point. Edges are painted above nodes, so a stroke over a
9734
+ * nested pool/lane fill is hit before the covering container.
9611
9735
  */
9612
9736
  getElementAtPoint(worldPoint) {
9737
+ let bestEdge;
9738
+ let bestDist = Infinity;
9739
+ for (const edge of this._edges.values()) {
9740
+ if (!edge.visible) continue;
9741
+ const tolerance = Math.max((edge.style.strokeWidth ?? 2) * 2, 8) / Math.max(this._zoom, 1e-4);
9742
+ const closest = edge.getClosestPointOnPath(worldPoint);
9743
+ if (closest && closest.distance <= tolerance && closest.distance < bestDist) {
9744
+ bestDist = closest.distance;
9745
+ bestEdge = edge;
9746
+ }
9747
+ }
9748
+ if (bestEdge) return bestEdge;
9613
9749
  const nodesArray = Array.from(this._nodes.values());
9614
9750
  for (let i = nodesArray.length - 1; i >= 0; i--) {
9615
9751
  const node = nodesArray[i];
9616
9752
  if (node.visible && node.hitTest(worldPoint)) return node;
9617
9753
  }
9618
- for (const edge of this._edges.values()) {
9619
- if (!edge.visible) continue;
9620
- const tolerance = Math.max((edge.style.strokeWidth ?? 2) * 2, 8) / Math.max(this._zoom, 1e-4);
9621
- if (edge.hitTestWithTolerance(worldPoint, tolerance)) return edge;
9622
- }
9623
9754
  for (const group of this._groups.values()) if (group.visible && group.hitTest(worldPoint)) return group;
9624
9755
  }
9625
9756
  /**
@@ -10635,6 +10766,52 @@ var NodeImage = class {
10635
10766
  }
10636
10767
  };
10637
10768
  //#endregion
10769
+ //#region src/utils/labelPlacement.ts
10770
+ var DEFAULT_LABEL_GAP = 4;
10771
+ function isExternalLabelPlacement(placement) {
10772
+ return placement === "top" || placement === "bottom" || placement === "left" || placement === "right";
10773
+ }
10774
+ function externalLabelWrapWidth(shapeWidth) {
10775
+ return Math.max(shapeWidth, 80);
10776
+ }
10777
+ function resolveExternalLabelBounds(shape, placement, gap, measured) {
10778
+ const width = Math.max(0, measured.width);
10779
+ const height = Math.max(0, measured.height);
10780
+ const safeGap = Number.isFinite(gap) ? gap : 4;
10781
+ switch (placement) {
10782
+ case "top": return {
10783
+ x: shape.x + (shape.width - width) / 2,
10784
+ y: shape.y - safeGap - height,
10785
+ width,
10786
+ height
10787
+ };
10788
+ case "bottom": return {
10789
+ x: shape.x + (shape.width - width) / 2,
10790
+ y: shape.y + shape.height + safeGap,
10791
+ width,
10792
+ height
10793
+ };
10794
+ case "left": return {
10795
+ x: shape.x - safeGap - width,
10796
+ y: shape.y + (shape.height - height) / 2,
10797
+ width,
10798
+ height
10799
+ };
10800
+ case "right": return {
10801
+ x: shape.x + shape.width + safeGap,
10802
+ y: shape.y + (shape.height - height) / 2,
10803
+ width,
10804
+ height
10805
+ };
10806
+ }
10807
+ }
10808
+ function unionBounds(a, b) {
10809
+ return rectUnion(a, b);
10810
+ }
10811
+ function pointInBounds(point, bounds) {
10812
+ return pointInRect(point, bounds);
10813
+ }
10814
+ //#endregion
10638
10815
  //#region src/utils/resolveContentInset.ts
10639
10816
  function axisFactor(node, base) {
10640
10817
  if (base === void 0 || !(base > 0) || !Number.isFinite(base)) return 1;
@@ -10707,6 +10884,8 @@ var Node = class extends Element {
10707
10884
  this._contentInset = this.normalizeContentInset(options.contentInset);
10708
10885
  this._contentInsetScale = this.normalizeContentInsetScale(options.contentInsetScale);
10709
10886
  this._contentInsetBaseSize = this.normalizeContentInsetBaseSize(options.contentInsetBaseSize);
10887
+ this._labelPlacement = options.labelPlacement ?? "center";
10888
+ this._labelGap = options.labelGap !== void 0 && Number.isFinite(options.labelGap) ? Math.max(0, options.labelGap) : 4;
10710
10889
  if (options.label !== void 0) if (typeof options.label === "string") this._label = new TextLabel({
10711
10890
  text: options.label,
10712
10891
  onChange: () => this.markDirty()
@@ -10757,6 +10936,33 @@ var Node = class extends Element {
10757
10936
  get ports() {
10758
10937
  return this._ports;
10759
10938
  }
10939
+ get labelPlacement() {
10940
+ return this._labelPlacement;
10941
+ }
10942
+ set labelPlacement(value) {
10943
+ if (this._labelPlacement !== value) {
10944
+ this._labelPlacement = value;
10945
+ this.markDirty();
10946
+ }
10947
+ }
10948
+ get labelGap() {
10949
+ return this._labelGap;
10950
+ }
10951
+ set labelGap(value) {
10952
+ const next = Number.isFinite(value) ? Math.max(0, value) : 4;
10953
+ if (this._labelGap !== next) {
10954
+ this._labelGap = next;
10955
+ this.markDirty();
10956
+ }
10957
+ }
10958
+ usesExternalLabel() {
10959
+ return isExternalLabelPlacement(this._labelPlacement) && this._label !== void 0;
10960
+ }
10961
+ getVisualBounds() {
10962
+ const shape = this.getBounds();
10963
+ if (!this.usesExternalLabel()) return shape;
10964
+ return unionBounds(shape, this.getResolvedLabelBounds());
10965
+ }
10760
10966
  /**
10761
10967
  * Text label
10762
10968
  */
@@ -10980,13 +11186,15 @@ var Node = class extends Element {
10980
11186
  * Calculate layout for icon and label.
10981
11187
  * Label always gets full content area; icon is placed within the same content area.
10982
11188
  */
10983
- calculateContentLayout(bounds, iconBoxSize, _labelSize) {
11189
+ calculateContentLayout(bounds, iconBoxSize, labelSize) {
11190
+ const iconContainer = this.getIconContainerBounds(bounds);
10984
11191
  const contentBounds = this.getLabelContainerBounds(bounds);
10985
- let iconBounds = contentBounds;
10986
- if (this._icon && iconBoxSize) iconBounds = this.getIconBounds(contentBounds, iconBoxSize, this._icon.placement);
11192
+ let iconBounds = iconContainer;
11193
+ if (this._icon && iconBoxSize) iconBounds = this.getIconBounds(iconContainer, iconBoxSize, this._icon.placement);
11194
+ const labelBounds = isExternalLabelPlacement(this._labelPlacement) && this._label && labelSize ? resolveExternalLabelBounds(bounds, this._labelPlacement, this._labelGap, labelSize) : contentBounds;
10987
11195
  return {
10988
11196
  iconBounds,
10989
- labelBounds: contentBounds
11197
+ labelBounds
10990
11198
  };
10991
11199
  }
10992
11200
  /**
@@ -10996,14 +11204,14 @@ var Node = class extends Element {
10996
11204
  getLabelBoundsForExport(ctx) {
10997
11205
  if (!this._label) return null;
10998
11206
  const bounds = this.getBounds();
10999
- const contentBounds = this.getLabelContainerBounds(bounds);
11207
+ const wrapWidth = this.getLabelWrapWidth(bounds);
11000
11208
  const iconBoxSize = this._icon ? this.getIconBoxSize() : void 0;
11001
- this._label.setAutoMaxWidth(contentBounds.width);
11209
+ this._label.setAutoMaxWidth(wrapWidth);
11002
11210
  const labelSize = this._label.measure(ctx);
11003
11211
  const { labelBounds } = this.calculateContentLayout(bounds, iconBoxSize, labelSize);
11004
11212
  return {
11005
11213
  bounds: labelBounds,
11006
- lines: this._label.getWrappedLines(ctx, Math.max(0, contentBounds.width))
11214
+ lines: this._label.getWrappedLines(ctx, Math.max(0, wrapWidth))
11007
11215
  };
11008
11216
  }
11009
11217
  /**
@@ -11016,10 +11224,10 @@ var Node = class extends Element {
11016
11224
  ctx.globalAlpha = 1;
11017
11225
  }
11018
11226
  /**
11019
- * Get world position of label center (center of content area).
11227
+ * Get world position of label center (content area or external label box).
11020
11228
  */
11021
11229
  getLabelPosition() {
11022
- const bounds = this.getLabelContainerBounds(this.getBounds());
11230
+ const bounds = this.getResolvedLabelBounds();
11023
11231
  return {
11024
11232
  x: bounds.x + bounds.width / 2,
11025
11233
  y: bounds.y + bounds.height / 2
@@ -11034,13 +11242,14 @@ var Node = class extends Element {
11034
11242
  let bounds = this.getBounds();
11035
11243
  this.renderBadges(ctx, bounds);
11036
11244
  const iconBoxSize = this._icon ? this.getIconBoxSize() : void 0;
11037
- if (this._label) this._label.setAutoMaxWidth(this.getLabelContainerBounds(bounds).width);
11245
+ const externalLabel = this.usesExternalLabel();
11246
+ if (this._label) this._label.setAutoMaxWidth(this.getLabelWrapWidth(bounds));
11038
11247
  const labelSize = this._label ? this._label.measure(ctx) : void 0;
11039
- if (labelSize || iconBoxSize) {
11248
+ if ((labelSize || iconBoxSize) && !externalLabel) {
11040
11249
  this.ensureContentsFit(labelSize, iconBoxSize);
11041
11250
  if (this._label && iconBoxSize) {
11042
11251
  bounds = this.getBounds();
11043
- this._label.setAutoMaxWidth(this.getLabelContainerBounds(bounds).width);
11252
+ this._label.setAutoMaxWidth(this.getLabelWrapWidth(bounds));
11044
11253
  }
11045
11254
  }
11046
11255
  const { iconBounds, labelBounds } = this.calculateContentLayout(this.getBounds(), iconBoxSize, labelSize);
@@ -11117,10 +11326,10 @@ var Node = class extends Element {
11117
11326
  return this.calculateContentMinSize(labelSize, iconBoxSize, bounds);
11118
11327
  }
11119
11328
  /**
11120
- * Render resize handles when selected
11329
+ * Render the selection frame when selected, and resize handles when they are enabled.
11121
11330
  */
11122
11331
  renderResizeHandles(ctx) {
11123
- if (this._state !== "selected" || !this._resizeHandlesEnabled) return;
11332
+ if (this._state !== "selected") return;
11124
11333
  const bounds = this.getBounds();
11125
11334
  const offset = 6;
11126
11335
  const size = 8;
@@ -11130,6 +11339,10 @@ var Node = class extends Element {
11130
11339
  ctx.lineWidth = 1;
11131
11340
  ctx.setLineDash([4, 4]);
11132
11341
  ctx.strokeRect(bounds.x - offset, bounds.y - offset, bounds.width + offset * 2, bounds.height + offset * 2);
11342
+ if (!this._resizeHandlesEnabled) {
11343
+ ctx.restore();
11344
+ return;
11345
+ }
11133
11346
  ctx.fillStyle = "#ffffff";
11134
11347
  ctx.strokeStyle = DEFAULT_SELECTION_COLOR;
11135
11348
  ctx.lineWidth = 1.5;
@@ -11146,7 +11359,7 @@ var Node = class extends Element {
11146
11359
  * Hit test a resize handle
11147
11360
  */
11148
11361
  hitTestResizeHandle(point) {
11149
- if (this._state !== "selected") return null;
11362
+ if (this._state !== "selected" || !this._resizeHandlesEnabled) return null;
11150
11363
  const half = 8 / 2;
11151
11364
  for (const handle of this.getResizeHandlePositions()) if (point.x >= handle.x - half && point.x <= handle.x + half && point.y >= handle.y - half && point.y <= handle.y + half) return handle.type;
11152
11365
  return null;
@@ -11157,7 +11370,25 @@ var Node = class extends Element {
11157
11370
  hitTest(point) {
11158
11371
  const bounds = this.getBounds();
11159
11372
  const padding = 10;
11160
- return point.x >= bounds.x - padding && point.x <= bounds.x + bounds.width + padding && point.y >= bounds.y - padding && point.y <= bounds.y + bounds.height + padding;
11373
+ return point.x >= bounds.x - padding && point.x <= bounds.x + bounds.width + padding && point.y >= bounds.y - padding && point.y <= bounds.y + bounds.height + padding || this.hitTestExternalLabel(point);
11374
+ }
11375
+ hitTestExternalLabel(point) {
11376
+ if (!this.usesExternalLabel()) return false;
11377
+ return pointInBounds(point, this.getResolvedLabelBounds());
11378
+ }
11379
+ getLabelWrapWidth(bounds = this.getBounds()) {
11380
+ if (this.usesExternalLabel()) return externalLabelWrapWidth(bounds.width);
11381
+ return this.getLabelContainerBounds(bounds).width;
11382
+ }
11383
+ getResolvedLabelBounds(measured) {
11384
+ const shape = this.getBounds();
11385
+ if (!this.usesExternalLabel() || !this._label) return this.getLabelContainerBounds(shape);
11386
+ const size = measured ?? {
11387
+ width: this._label.measuredWidth || this.getLabelWrapWidth(shape),
11388
+ height: this._label.measuredHeight || (this._label.style.fontSize ?? 14) * 1.2
11389
+ };
11390
+ if (!isExternalLabelPlacement(this._labelPlacement)) return this.getLabelContainerBounds(shape);
11391
+ return resolveExternalLabelBounds(shape, this._labelPlacement, this._labelGap, size);
11161
11392
  }
11162
11393
  /**
11163
11394
  * Get a connection point on the node boundary toward a target point,
@@ -11391,16 +11622,28 @@ var Node = class extends Element {
11391
11622
  }
11392
11623
  calculateContentMinSize(labelSize, iconBoxSize, bounds) {
11393
11624
  const labelContainer = this.getLabelContainerBounds(bounds);
11394
- const widthFactor = labelContainer.width > 0 ? bounds.width / labelContainer.width : 1;
11395
- const heightFactor = labelContainer.height > 0 ? bounds.height / labelContainer.height : 1;
11396
- const minContentWidth = Math.max(labelSize?.width ?? 0, iconBoxSize?.width ?? 0);
11397
- const minContentHeight = Math.max(labelSize?.height ?? 0, iconBoxSize?.height ?? 0);
11625
+ const iconContainer = this.getIconContainerBounds(bounds);
11626
+ const labelWidthFactor = labelContainer.width > 0 ? bounds.width / labelContainer.width : 1;
11627
+ const labelHeightFactor = labelContainer.height > 0 ? bounds.height / labelContainer.height : 1;
11628
+ const iconWidthFactor = iconContainer.width > 0 ? bounds.width / iconContainer.width : 1;
11629
+ const iconHeightFactor = iconContainer.height > 0 ? bounds.height / iconContainer.height : 1;
11398
11630
  return {
11399
- width: minContentWidth * widthFactor,
11400
- height: minContentHeight * heightFactor
11631
+ width: Math.max((labelSize?.width ?? 0) * labelWidthFactor, (iconBoxSize?.width ?? 0) * iconWidthFactor),
11632
+ height: Math.max((labelSize?.height ?? 0) * labelHeightFactor, (iconBoxSize?.height ?? 0) * iconHeightFactor)
11401
11633
  };
11402
11634
  }
11635
+ /**
11636
+ * Content box for the node icon: AABB minus contentInset.
11637
+ * Unlike {@link getLabelContainerBounds}, this is not shrunk to the
11638
+ * inscribed rectangle of a circle/diamond, so a centered icon can fill the shape.
11639
+ */
11640
+ getIconContainerBounds(bounds) {
11641
+ return this.applyContentInset(bounds);
11642
+ }
11403
11643
  getLabelContainerBounds(bounds) {
11644
+ return this.applyContentInset(bounds);
11645
+ }
11646
+ applyContentInset(bounds) {
11404
11647
  const { top, right, bottom, left } = resolveContentInset(this._contentInset, this._contentInsetScale, {
11405
11648
  width: bounds.width,
11406
11649
  height: bounds.height
@@ -11797,7 +12040,7 @@ var CircleNode = class extends Node {
11797
12040
  const ry = this._height / 2 + padding;
11798
12041
  const dx = point.x - center.x;
11799
12042
  const dy = point.y - center.y;
11800
- return dx * dx / (rx * rx) + dy * dy / (ry * ry) <= 1;
12043
+ return dx * dx / (rx * rx) + dy * dy / (ry * ry) <= 1 || this.hitTestExternalLabel(point);
11801
12044
  }
11802
12045
  getOutlineDirection(param) {
11803
12046
  return this.outlineDirectionFromCenter(param);
@@ -11886,7 +12129,7 @@ var DiamondNode = class extends Node {
11886
12129
  const hh = this._height / 2 + padding;
11887
12130
  const dx = Math.abs(point.x - center.x);
11888
12131
  const dy = Math.abs(point.y - center.y);
11889
- return dx / hw + dy / hh <= 1;
12132
+ return dx / hw + dy / hh <= 1 || this.hitTestExternalLabel(point);
11890
12133
  }
11891
12134
  getOutlineDirection(param) {
11892
12135
  return this.outlineDirectionFromCenter(param);
@@ -12287,7 +12530,7 @@ var CustomShapeNode = class extends Node {
12287
12530
  const path = this.getPath();
12288
12531
  if (ctx.isPointInPath(path, localX, localY)) return true;
12289
12532
  ctx.lineWidth = 20;
12290
- return ctx.isPointInStroke(path, localX, localY);
12533
+ return ctx.isPointInStroke(path, localX, localY) || this.hitTestExternalLabel(point);
12291
12534
  }
12292
12535
  render(ctx) {
12293
12536
  const style = this.style;
@@ -13117,6 +13360,9 @@ function isCContainer(component) {
13117
13360
  function isCShape(component) {
13118
13361
  return component.type === "shape";
13119
13362
  }
13363
+ function isCText(component) {
13364
+ return component.type === "text";
13365
+ }
13120
13366
  /**
13121
13367
  * Flex container component for CompositeNode.
13122
13368
  * Lays out children using a flexbox-like algorithm.
@@ -13127,6 +13373,8 @@ var CContainer = class extends CompositeComponentBase {
13127
13373
  this.type = "container";
13128
13374
  this._cachedBounds = null;
13129
13375
  this._cachedContainerBounds = null;
13376
+ this._layoutChildren = null;
13377
+ this._suppressBoundName = false;
13130
13378
  this.id = options.id;
13131
13379
  this._direction = options.direction ?? "column";
13132
13380
  this._justifyContent = options.justifyContent ?? "start";
@@ -13155,6 +13403,27 @@ var CContainer = class extends CompositeComponentBase {
13155
13403
  get children() {
13156
13404
  return this._children;
13157
13405
  }
13406
+ /**
13407
+ * When true, CText bound to `__name__` is omitted from measure/render/hit/SVG.
13408
+ * Used when CompositeNode draws the name as an external Node.label.
13409
+ */
13410
+ setSuppressBoundName(value) {
13411
+ if (this._suppressBoundName === value) return;
13412
+ this._suppressBoundName = value;
13413
+ this._cachedBounds = null;
13414
+ this._layoutChildren = null;
13415
+ for (const child of this._children) {
13416
+ if (isCContainer(child)) child.setSuppressBoundName(value);
13417
+ if (isCShape(child) && child.content) child.content.setSuppressBoundName(value);
13418
+ }
13419
+ }
13420
+ isLayoutSkipped(child) {
13421
+ if (child.style.visible === false) return true;
13422
+ return this._suppressBoundName && isCText(child) && child.bindToProperty === "__name__";
13423
+ }
13424
+ getLayoutChildren() {
13425
+ return this._children.filter((child) => !this.isLayoutSkipped(child));
13426
+ }
13158
13427
  addChild(child) {
13159
13428
  child.setOnChange(() => this.handleChildChange());
13160
13429
  this._children.push(child);
@@ -13186,8 +13455,8 @@ var CContainer = class extends CompositeComponentBase {
13186
13455
  padding: this._padding
13187
13456
  };
13188
13457
  }
13189
- buildFlexChildren(ctx) {
13190
- return this._children.map((child) => {
13458
+ buildFlexChildren(ctx, children = this.getLayoutChildren()) {
13459
+ return children.map((child) => {
13191
13460
  const s = child.style;
13192
13461
  return {
13193
13462
  measure: child.measure(ctx),
@@ -13220,11 +13489,13 @@ var CContainer = class extends CompositeComponentBase {
13220
13489
  }
13221
13490
  render(ctx, bounds) {
13222
13491
  if (this.style.visible === false) return;
13223
- const flexChildren = this.buildFlexChildren(ctx);
13492
+ const layoutChildren = this.getLayoutChildren();
13493
+ const flexChildren = this.buildFlexChildren(ctx, layoutChildren);
13224
13494
  const result = flexLayout({
13225
13495
  width: bounds.width,
13226
13496
  height: bounds.height
13227
13497
  }, this.getFlexConfig(), flexChildren);
13498
+ this._layoutChildren = layoutChildren;
13228
13499
  this._cachedBounds = result.childBounds;
13229
13500
  this._cachedContainerBounds = bounds;
13230
13501
  const opacity = this.style.opacity ?? 1;
@@ -13232,9 +13503,8 @@ var CContainer = class extends CompositeComponentBase {
13232
13503
  ctx.save();
13233
13504
  ctx.globalAlpha *= opacity;
13234
13505
  }
13235
- for (let i = 0; i < this._children.length; i++) {
13236
- const child = this._children[i];
13237
- if (child.style.visible === false) continue;
13506
+ for (let i = 0; i < layoutChildren.length; i++) {
13507
+ const child = layoutChildren[i];
13238
13508
  const cb = result.childBounds[i];
13239
13509
  child.render(ctx, {
13240
13510
  x: bounds.x + cb.x,
@@ -13247,10 +13517,9 @@ var CContainer = class extends CompositeComponentBase {
13247
13517
  }
13248
13518
  hitTest(point, bounds) {
13249
13519
  if (this.style.visible === false) return null;
13250
- if (!this._cachedBounds) return null;
13251
- for (let i = this._children.length - 1; i >= 0; i--) {
13252
- const child = this._children[i];
13253
- if (child.style.visible === false) continue;
13520
+ if (!this._cachedBounds || !this._layoutChildren) return null;
13521
+ for (let i = this._layoutChildren.length - 1; i >= 0; i--) {
13522
+ const child = this._layoutChildren[i];
13254
13523
  const cb = this._cachedBounds[i];
13255
13524
  const absBounds = {
13256
13525
  x: bounds.x + cb.x,
@@ -13264,6 +13533,22 @@ var CContainer = class extends CompositeComponentBase {
13264
13533
  return null;
13265
13534
  }
13266
13535
  /**
13536
+ * First CText bound to `__name__`, including suppressed name text.
13537
+ */
13538
+ findBoundNameText() {
13539
+ for (const child of this._children) {
13540
+ if (isCText(child) && child.bindToProperty === "__name__") return child;
13541
+ if (isCContainer(child)) {
13542
+ const found = child.findBoundNameText();
13543
+ if (found) return found;
13544
+ }
13545
+ if (isCShape(child) && child.content) {
13546
+ const found = child.content.findBoundNameText();
13547
+ if (found) return found;
13548
+ }
13549
+ }
13550
+ }
13551
+ /**
13267
13552
  * Find a component by id recursively.
13268
13553
  */
13269
13554
  findById(id) {
@@ -13300,8 +13585,8 @@ var CContainer = class extends CompositeComponentBase {
13300
13585
  if (this.style.visible === false) return "";
13301
13586
  const cachedBounds = this._cachedBounds;
13302
13587
  const cachedContainer = this._cachedContainerBounds;
13303
- return `<g>${this._children.map((child, i) => {
13304
- if (child.style.visible === false) return "";
13588
+ const layoutChildren = this._layoutChildren ?? this.getLayoutChildren();
13589
+ return `<g>${layoutChildren.map((child, i) => {
13305
13590
  if (cachedBounds && cachedContainer) {
13306
13591
  const cb = cachedBounds[i];
13307
13592
  const dx = bounds.x - cachedContainer.x;
@@ -13313,7 +13598,7 @@ var CContainer = class extends CompositeComponentBase {
13313
13598
  height: cb.height
13314
13599
  });
13315
13600
  }
13316
- const h = bounds.height / Math.max(1, this._children.length);
13601
+ const h = bounds.height / Math.max(1, layoutChildren.length);
13317
13602
  return child.toSVG({
13318
13603
  x: bounds.x,
13319
13604
  y: bounds.y + i * h,
@@ -13620,8 +13905,8 @@ var CompositeNode = class extends Node {
13620
13905
  return this.getLabelContainerBounds(this.getBounds());
13621
13906
  }
13622
13907
  hitTest(point) {
13623
- if (this._shapeType === "circle") return this.hitTestEllipse(point);
13624
- if (this._shapeType === "diamond") return this.hitTestDiamond(point);
13908
+ if (this._shapeType === "circle") return this.hitTestEllipse(point) || this.hitTestExternalLabel(point);
13909
+ if (this._shapeType === "diamond") return this.hitTestDiamond(point) || this.hitTestExternalLabel(point);
13625
13910
  return super.hitTest(point);
13626
13911
  }
13627
13912
  hitTestEllipse(point) {
@@ -13642,7 +13927,27 @@ var CompositeNode = class extends Node {
13642
13927
  const dy = Math.abs(point.y - center.y);
13643
13928
  return dx / hw + dy / hh <= 1;
13644
13929
  }
13930
+ /**
13931
+ * Copy color/font from the suppressed `__name__` CText onto the external Node.label.
13932
+ */
13933
+ syncExternalBoundNameLabelStyle() {
13934
+ if (!this.usesExternalLabel() || !this._label) return;
13935
+ const nameText = this._content.findBoundNameText();
13936
+ if (!nameText) return;
13937
+ const current = this._label.styleOverrides;
13938
+ if (current.color === nameText.color && current.fontFamily === nameText.fontFamily && current.fontWeight === nameText.fontWeight && current.fontStyle === nameText.fontStyle && current.fontSize === nameText.fontSize) return;
13939
+ this._label.style = {
13940
+ ...current,
13941
+ color: nameText.color,
13942
+ fontFamily: nameText.fontFamily,
13943
+ fontWeight: nameText.fontWeight,
13944
+ fontStyle: nameText.fontStyle,
13945
+ fontSize: nameText.fontSize
13946
+ };
13947
+ }
13645
13948
  render(ctx) {
13949
+ this._content.setSuppressBoundName(this.usesExternalLabel());
13950
+ this.syncExternalBoundNameLabelStyle();
13646
13951
  if (this._autoSize) this.applyAutoSize(ctx);
13647
13952
  const { x, y, width, height } = this.getBounds();
13648
13953
  const style = this.style;
@@ -13715,6 +14020,11 @@ var CompositeNode = class extends Node {
13715
14020
  this.renderBadges(ctx, bounds);
13716
14021
  const contentBounds = this.getContentBounds();
13717
14022
  this._content.render(ctx, contentBounds);
14023
+ if (this.usesExternalLabel() && this._label) {
14024
+ this._label.setAutoMaxWidth(this.getLabelWrapWidth(bounds));
14025
+ const labelSize = this._label.measure(ctx);
14026
+ this.renderLabel(ctx, this.getResolvedLabelBounds(labelSize));
14027
+ }
13718
14028
  this.renderPorts(ctx);
13719
14029
  }
13720
14030
  applyAutoSize(ctx) {
@@ -14348,6 +14658,8 @@ var Serializer = class {
14348
14658
  styleClass: node.styleClass,
14349
14659
  label,
14350
14660
  labelStyleClass: typeof label === "string" ? node.label?.styleClass : void 0,
14661
+ labelPlacement: node.labelPlacement !== "center" ? node.labelPlacement : void 0,
14662
+ labelGap: node.labelGap !== 4 ? node.labelGap : void 0,
14351
14663
  icon,
14352
14664
  contentInset: hasContentInset ? contentInset : void 0,
14353
14665
  anchorPoints,
@@ -14654,9 +14966,16 @@ var SvgExporter = class {
14654
14966
  break;
14655
14967
  }
14656
14968
  }
14969
+ cn.content.setSuppressBoundName(cn.usesExternalLabel());
14970
+ cn.syncExternalBoundNameLabelStyle();
14657
14971
  const contentBounds = cn.getLabelContainerBounds(bounds);
14658
14972
  const contentSvg = cn.content.toSVG(contentBounds);
14659
- return [`${shape} fill="${fill}" fill-opacity="${fillOpacity}" stroke="${stroke}" stroke-width="${strokeWidth}" stroke-opacity="${strokeOpacity}"${dash}${dashOffset}/>`, contentSvg].join("");
14973
+ const labelSvg = cn.usesExternalLabel() ? this.renderNodeLabel(cn, bounds) : "";
14974
+ return [
14975
+ `${shape} fill="${fill}" fill-opacity="${fillOpacity}" stroke="${stroke}" stroke-width="${strokeWidth}" stroke-opacity="${strokeOpacity}"${dash}${dashOffset}/>`,
14976
+ contentSvg,
14977
+ labelSvg
14978
+ ].join("");
14660
14979
  }
14661
14980
  default: shape = `<rect x="${bounds.x}" y="${bounds.y}" width="${bounds.width}" height="${bounds.height}"`;
14662
14981
  }
@@ -14729,15 +15048,13 @@ var SvgExporter = class {
14729
15048
  }
14730
15049
  measureTextLabel(text, style = {}, inset = 8) {
14731
15050
  const fontSize = style.fontSize ?? 14;
14732
- const fontFamily = style.fontFamily ?? "sans-serif";
14733
- const fontWeight = style.fontWeight ?? "normal";
14734
15051
  const lineHeight = fontSize * 1.2;
14735
15052
  const lines = text.split("\n");
14736
15053
  let maxWidth = 0;
14737
15054
  if (typeof document !== "undefined") {
14738
15055
  const ctx = document.createElement("canvas").getContext("2d");
14739
15056
  if (ctx) {
14740
- ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
15057
+ ctx.font = canvasFontFromTextStyle(style);
14741
15058
  for (const line of lines) maxWidth = Math.max(maxWidth, ctx.measureText(line).width);
14742
15059
  }
14743
15060
  }
@@ -14848,16 +15165,18 @@ var SvgExporter = class {
14848
15165
  const fontSize = style.fontSize ?? 14;
14849
15166
  const fontFamily = style.fontFamily ?? "sans-serif";
14850
15167
  const fontWeight = style.fontWeight ?? "normal";
15168
+ const fontStyle = style.fontStyle ?? "normal";
15169
+ const fontStyleAttr = fontStyle !== "normal" ? ` font-style="${fontStyle}"` : "";
14851
15170
  const opacity = style.opacity ?? 1;
14852
15171
  const anchor = style.align === "left" ? "start" : style.align === "right" ? "end" : "middle";
14853
15172
  const baseline = style.baseline === "top" ? "text-before-edge" : style.baseline === "bottom" ? "text-after-edge" : "middle";
14854
15173
  const transform = rotation !== 0 ? ` transform="rotate(${rotation * 180 / Math.PI} ${point.x} ${point.y})"` : "";
14855
15174
  const lines = text.split("\n");
14856
- if (lines.length <= 1) return `<text x="${point.x}" y="${point.y}" fill="${fill}" fill-opacity="${opacity}" font-size="${fontSize}" font-family="${fontFamily}" font-weight="${fontWeight}" text-anchor="${anchor}" dominant-baseline="${baseline}"${transform}>${this.escapeText(text)}</text>`;
15175
+ if (lines.length <= 1) return `<text x="${point.x}" y="${point.y}" fill="${fill}" fill-opacity="${opacity}" font-size="${fontSize}" font-family="${fontFamily}" font-weight="${fontWeight}"${fontStyleAttr} text-anchor="${anchor}" dominant-baseline="${baseline}"${transform}>${this.escapeText(text)}</text>`;
14857
15176
  const lineHeight = fontSize * 1.2;
14858
15177
  const startY = point.y - (lines.length - 1) * lineHeight / 2;
14859
15178
  const tspans = lines.map((line, index) => `<tspan x="${point.x}" y="${startY + index * lineHeight}">${this.escapeText(line)}</tspan>`).join("");
14860
- return `<text x="${point.x}" y="${point.y}" fill="${fill}" fill-opacity="${opacity}" font-size="${fontSize}" font-family="${fontFamily}" font-weight="${fontWeight}" text-anchor="${anchor}" dominant-baseline="${baseline}"${transform}>${tspans}</text>`;
15179
+ return `<text x="${point.x}" y="${point.y}" fill="${fill}" fill-opacity="${opacity}" font-size="${fontSize}" font-family="${fontFamily}" font-weight="${fontWeight}"${fontStyleAttr} text-anchor="${anchor}" dominant-baseline="${baseline}"${transform}>${tspans}</text>`;
14861
15180
  }
14862
15181
  buildEdgePath(edge, edgeLabelOffset) {
14863
15182
  if (!edge.label || !edge.labelLineGap) return this.buildPath(edge);
@@ -15572,6 +15891,6 @@ function applyEdgeStyle(ctx, style) {
15572
15891
  return baseOpacity * strokeOpacity;
15573
15892
  }
15574
15893
  //#endregion
15575
- export { AddNodeCommand, AnimationManager, AutoLayout, AutoRouting, BezierPathStrategy, CContainer, CDivider, CIcon, CShape, CText, ChangeEdgePropertiesCommand, ChangeEditablePolylineControlPointsCommand, ChangeGroupPropertiesCommand, ChangeNodePropertiesCommand, CircleNode, ClipboardManager, CompositeCommand, CompositeNode, ConnectionManager, ContextMenuManager, CustomShapeNode, DARK_THEME, DEFAULT_THEME, DiagramRenderer, DiamondNode, DragManager, Edge, Element, EventEmitter, GridOverlay, Group, GuidesOverlay, HistoryManager, ImageExporter, InputHandler, InteractionManager, MiniMap, MoveNodesCommand, NavigationManager, Node, NodeImage, PolylinePathStrategy, Port, PropertyChangeBatcher, RectangleNode, RemoveNodeCommand, ResizeManager, ResizeNodesCommand, RulersOverlay, SearchManager, SelectionManager, Serializer, ShapeFactories, StraightPathStrategy, StyleManager, SvgExporter, TextLabel, alignNodes, angle, applyEdgeStyle, applyNodeStyle, bezierPoint, boundsCenter, calculateBezierControlPoints, clamp, clonePoints, container, deserializeCComponent, directionFromAngle, distance, distanceToSegment, distributeNodes, divider, drawRoundedRectPath, expandBounds, flexLayout, generateId, getClosestPointOnPath, getPathPointAt, icon, isCornerPlacement, isEdgeEdgeEndpoint, isNodeEdgeEndpoint, lerp, mergeBounds, normalizeSides, pointInEllipse, pointInRect, rectIntersection, rectUnion, rectsIntersect, renderFillAndStroke, resetIdCounter, resetPortIdCounter, resolveContentInset, rotatePoint, segmentRectIntersections, shape, snapPointToGrid, snapToGrid, text };
15894
+ export { AddNodeCommand, AnimationManager, AutoLayout, AutoRouting, BezierPathStrategy, CContainer, CDivider, CIcon, CShape, CText, ChangeEdgePropertiesCommand, ChangeEditablePolylineControlPointsCommand, ChangeGroupPropertiesCommand, ChangeNodePropertiesCommand, CircleNode, ClipboardManager, CompositeCommand, CompositeNode, ConnectionManager, ContextMenuManager, CustomShapeNode, DARK_THEME, DEFAULT_LABEL_GAP, DEFAULT_THEME, DiagramRenderer, DiamondNode, DragManager, Edge, Element, EventEmitter, GridOverlay, Group, GuidesOverlay, HistoryManager, ImageExporter, InputHandler, InteractionManager, MiniMap, MoveNodesCommand, NavigationManager, Node, NodeImage, PolylinePathStrategy, Port, PropertyChangeBatcher, RectangleNode, RemoveNodeCommand, ResizeManager, ResizeNodesCommand, RulersOverlay, SearchManager, SelectionManager, Serializer, ShapeFactories, StraightPathStrategy, StyleManager, SvgExporter, TextLabel, alignNodes, angle, applyEdgeStyle, applyNodeStyle, bezierPoint, boundsCenter, calculateBezierControlPoints, clamp, clonePoints, container, deserializeCComponent, directionFromAngle, distance, distanceToSegment, distributeNodes, divider, drawRoundedRectPath, expandBounds, flexLayout, generateId, getClosestPointOnPath, getPathPointAt, icon, isCornerPlacement, isEdgeEdgeEndpoint, isExternalLabelPlacement, isNodeEdgeEndpoint, lerp, mergeBounds, normalizeSides, pointInEllipse, pointInRect, rectIntersection, rectUnion, rectsIntersect, renderFillAndStroke, resetIdCounter, resetPortIdCounter, resolveContentInset, rotatePoint, segmentRectIntersections, shape, snapPointToGrid, snapToGrid, text };
15576
15895
 
15577
15896
  //# sourceMappingURL=papirus.js.map