@ngroznykh/papirus 0.9.2 → 0.9.4

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
@@ -1456,7 +1456,7 @@ function getContentBounds({ nodes, edges, groups, includeInvisible = false }) {
1456
1456
  let hasContent = false;
1457
1457
  const consider = (source) => {
1458
1458
  if (!includeInvisible && !source.visible) return;
1459
- const bounds = source.getBounds();
1459
+ const bounds = source.getVisualBounds?.() ?? source.getBounds();
1460
1460
  minX = Math.min(minX, bounds.x);
1461
1461
  minY = Math.min(minY, bounds.y);
1462
1462
  maxX = Math.max(maxX, bounds.x + bounds.width);
@@ -1671,6 +1671,133 @@ var NavigationManager = class extends EventEmitter {
1671
1671
  }
1672
1672
  };
1673
1673
  //#endregion
1674
+ //#region src/utils/edgePath.ts
1675
+ /**
1676
+ * Point and tangent angle at a normalized position along an edge path (0..1 by length).
1677
+ */
1678
+ function getPathPointAt(path, type, position) {
1679
+ if (type !== "bezier" || path.length < 4) return getPointAlongPolyline(path, position);
1680
+ const samples = [];
1681
+ const steps = 20;
1682
+ for (let i = 1; i + 2 < path.length; i += 3) {
1683
+ const start = path[i - 1];
1684
+ const control1 = path[i];
1685
+ const control2 = path[i + 1];
1686
+ const end = path[i + 2];
1687
+ for (let sample = 0; sample <= steps; sample++) {
1688
+ const t = sample / steps;
1689
+ if (samples.length > 0 && t === 0) continue;
1690
+ samples.push(bezierPoint(start, control1, control2, end, t));
1691
+ }
1692
+ }
1693
+ return getPointAlongPolyline(samples, position);
1694
+ }
1695
+ function getPointAlongPolyline(path, position) {
1696
+ if (path.length === 0) return {
1697
+ point: {
1698
+ x: 0,
1699
+ y: 0
1700
+ },
1701
+ angle: 0
1702
+ };
1703
+ if (path.length === 1) return {
1704
+ point: path[0],
1705
+ angle: 0
1706
+ };
1707
+ let totalLength = 0;
1708
+ const segments = [];
1709
+ for (let i = 1; i < path.length; i++) {
1710
+ const start = path[i - 1];
1711
+ const end = path[i];
1712
+ const length = Math.hypot(end.x - start.x, end.y - start.y);
1713
+ segments.push({
1714
+ start,
1715
+ end,
1716
+ length
1717
+ });
1718
+ totalLength += length;
1719
+ }
1720
+ const targetLength = totalLength * Math.max(0, Math.min(1, position));
1721
+ let accumulated = 0;
1722
+ for (const segment of segments) {
1723
+ if (accumulated + segment.length >= targetLength) {
1724
+ const segmentPosition = segment.length > 0 ? (targetLength - accumulated) / segment.length : 0;
1725
+ return {
1726
+ point: {
1727
+ x: segment.start.x + segmentPosition * (segment.end.x - segment.start.x),
1728
+ y: segment.start.y + segmentPosition * (segment.end.y - segment.start.y)
1729
+ },
1730
+ angle: Math.atan2(segment.end.y - segment.start.y, segment.end.x - segment.start.x)
1731
+ };
1732
+ }
1733
+ accumulated += segment.length;
1734
+ }
1735
+ const lastSegment = segments[segments.length - 1];
1736
+ return {
1737
+ point: lastSegment.end,
1738
+ angle: Math.atan2(lastSegment.end.y - lastSegment.start.y, lastSegment.end.x - lastSegment.start.x)
1739
+ };
1740
+ }
1741
+ /** Map tangent angle to a cardinal direction used by polyline routing. */
1742
+ function directionFromAngle(angle) {
1743
+ const twoPi = Math.PI * 2;
1744
+ const a = (angle % twoPi + twoPi) % twoPi;
1745
+ if (a >= Math.PI * .25 && a < Math.PI * .75) return "bottom";
1746
+ if (a >= Math.PI * .75 && a < Math.PI * 1.25) return "left";
1747
+ if (a >= Math.PI * 1.25 && a < Math.PI * 1.75) return "top";
1748
+ return "right";
1749
+ }
1750
+ function closestPointOnPolyline(polyline, point) {
1751
+ if (polyline.length < 2) return null;
1752
+ let totalLength = 0;
1753
+ for (let i = 1; i < polyline.length; i++) {
1754
+ const start = polyline[i - 1];
1755
+ const end = polyline[i];
1756
+ totalLength += Math.hypot(end.x - start.x, end.y - start.y);
1757
+ }
1758
+ let bestPoint = polyline[0];
1759
+ let bestParam = 0;
1760
+ let bestDist = Infinity;
1761
+ let accumulated = 0;
1762
+ for (let i = 1; i < polyline.length; i++) {
1763
+ const start = polyline[i - 1];
1764
+ const end = polyline[i];
1765
+ const dx = end.x - start.x;
1766
+ const dy = end.y - start.y;
1767
+ const lengthSq = dx * dx + dy * dy;
1768
+ const segLen = Math.sqrt(lengthSq);
1769
+ const t = lengthSq === 0 ? 0 : Math.max(0, Math.min(1, ((point.x - start.x) * dx + (point.y - start.y) * dy) / lengthSq));
1770
+ const proj = {
1771
+ x: start.x + t * dx,
1772
+ y: start.y + t * dy
1773
+ };
1774
+ const dist = Math.hypot(point.x - proj.x, point.y - proj.y);
1775
+ if (dist < bestDist) {
1776
+ bestDist = dist;
1777
+ bestPoint = proj;
1778
+ bestParam = totalLength > 0 ? (accumulated + t * segLen) / totalLength : 0;
1779
+ }
1780
+ accumulated += segLen;
1781
+ }
1782
+ return {
1783
+ point: bestPoint,
1784
+ pathParam: bestParam,
1785
+ distance: bestDist
1786
+ };
1787
+ }
1788
+ /**
1789
+ * Nearest point on an edge path to `point`.
1790
+ * Polylines project onto real segments so a click on a long orthogonal stroke
1791
+ * is not lost between length samples. Bezier paths are densified, then projected.
1792
+ */
1793
+ function getClosestPointOnPath(path, type, point, samples = 48) {
1794
+ if (path.length < 2) return null;
1795
+ if (type !== "bezier" || path.length < 4) return closestPointOnPolyline(path, point);
1796
+ const densified = [];
1797
+ for (let i = 0; i <= samples; i++) densified.push(getPathPointAt(path, type, i / samples).point);
1798
+ return closestPointOnPolyline(densified, point);
1799
+ }
1800
+ //#endregion
1674
1801
  //#region src/utils/direction.ts
1675
1802
  /**
1676
1803
  * Map outline param [0,1) to cardinal direction.
@@ -1983,6 +2110,7 @@ var ConnectionManager = class extends EventEmitter {
1983
2110
  this.startOutlineConnection(node, outlinePoint);
1984
2111
  return true;
1985
2112
  }
2113
+ if (this.attachToOutline) return false;
1986
2114
  const hover = this.getAnchorAtPoint(node, point);
1987
2115
  if (hover) {
1988
2116
  this.sourceNode = node;
@@ -2047,7 +2175,18 @@ var ConnectionManager = class extends EventEmitter {
2047
2175
  let snappedPoint = point;
2048
2176
  let snappedDir;
2049
2177
  let targetNode = this.getNodeAtPoint(point, true);
2050
- if (this.attachToOutline) {
2178
+ const edgeDrop = this.originalEdgeEndpoint?.edgeId ? this.findEdgeDropTarget(point, {
2179
+ maxScreenPx: 40,
2180
+ skipEdgeIds: new Set([edge.id])
2181
+ }) : null;
2182
+ if (edgeDrop) {
2183
+ const at = edgeDrop.edge.getPointAt(edgeDrop.pathParam);
2184
+ snappedPoint = edgeDrop.point;
2185
+ snappedDir = at ? directionFromAngle(at.angle) : void 0;
2186
+ targetNode = null;
2187
+ this.reconnectingOutlineParam = null;
2188
+ this.reconnectingTargetNodeId = null;
2189
+ } else if (this.attachToOutline) {
2051
2190
  const tolerance = 40 / Math.max(this.renderer.zoom, 1e-4);
2052
2191
  const toleranceSq = tolerance * tolerance;
2053
2192
  let bestDistSq = Infinity;
@@ -2214,6 +2353,9 @@ var ConnectionManager = class extends EventEmitter {
2214
2353
  this.previewTargetNodeId = null;
2215
2354
  this.previewTargetEdgeId = null;
2216
2355
  this.previewTargetPathParam = null;
2356
+ const rawNode = this.getRawNodeAtPoint(cursorPoint);
2357
+ const forbiddenFill = !!this._connectionValidator && !!this.sourceNode && !!rawNode && !this._connectionValidator(this.sourceNode.id, rawNode.id);
2358
+ const searchGroupFill = !!rawNode && (forbiddenFill || this.nodeContainsSource(rawNode) || this.nodeLooksLikeContainer(rawNode));
2217
2359
  let targetNode = this.getNodeAtPoint(cursorPoint, false);
2218
2360
  if (this.attachToOutline) {
2219
2361
  const tolerance = 40 / Math.max(this.renderer.zoom, 1e-4);
@@ -2257,7 +2399,7 @@ var ConnectionManager = class extends EventEmitter {
2257
2399
  }
2258
2400
  }
2259
2401
  }
2260
- const edgeDrop = this.findEdgeDropTarget(cursorPoint);
2402
+ const edgeDrop = this.findEdgeDropTarget(cursorPoint, searchGroupFill ? { maxScreenPx: 40 } : void 0);
2261
2403
  if (edgeDrop) {
2262
2404
  let nodeDistance = Infinity;
2263
2405
  if (targetNode) {
@@ -2265,7 +2407,7 @@ var ConnectionManager = class extends EventEmitter {
2265
2407
  const dy = cursorPoint.y - snappedPoint.y;
2266
2408
  nodeDistance = Math.sqrt(dx * dx + dy * dy);
2267
2409
  }
2268
- if (!targetNode || edgeDrop.distance < nodeDistance) {
2410
+ if (searchGroupFill || !targetNode || edgeDrop.distance <= nodeDistance) {
2269
2411
  targetNode = null;
2270
2412
  this.previewTargetNodeId = null;
2271
2413
  this.previewTargetOutlineParam = null;
@@ -2274,6 +2416,12 @@ var ConnectionManager = class extends EventEmitter {
2274
2416
  this.previewTargetEdgeId = edgeDrop.edge.id;
2275
2417
  this.previewTargetPathParam = edgeDrop.pathParam;
2276
2418
  }
2419
+ } else if (searchGroupFill) {
2420
+ targetNode = null;
2421
+ this.previewTargetNodeId = null;
2422
+ this.previewTargetOutlineParam = null;
2423
+ this.previewTargetAnchorId = null;
2424
+ snappedPoint = cursorPoint;
2277
2425
  }
2278
2426
  if (targetNode) {
2279
2427
  const forbidden = this._connectionValidator && this.sourceNode && !this._connectionValidator(this.sourceNode.id, targetNode.id);
@@ -2335,9 +2483,23 @@ var ConnectionManager = class extends EventEmitter {
2335
2483
  y: event.worldY
2336
2484
  };
2337
2485
  let reconnected = false;
2486
+ const edgeDrop = this.originalEdgeEndpoint?.edgeId ? this.findEdgeDropTarget(point, {
2487
+ maxScreenPx: 40,
2488
+ skipEdgeIds: new Set([this.reconnectingEdge.id])
2489
+ }) : null;
2490
+ if (edgeDrop) {
2491
+ const endpoint = {
2492
+ edgeId: edgeDrop.edgeId,
2493
+ pathParam: edgeDrop.pathParam
2494
+ };
2495
+ if (this.reconnectingEndpoint === "start") this.reconnectingEdge.from = endpoint;
2496
+ else this.reconnectingEdge.to = endpoint;
2497
+ reconnected = true;
2498
+ this.emit("edgeReconnect", this.reconnectingEdge, this.reconnectingEndpoint);
2499
+ }
2338
2500
  let node = this.getNodeAtPoint(point, true);
2339
2501
  if (!node && this.attachToOutline && this.reconnectingTargetNodeId) node = this.renderer.getNode(this.reconnectingTargetNodeId) ?? null;
2340
- if (node) {
2502
+ if (!reconnected && node) {
2341
2503
  if (this.attachToOutline && this.reconnectingOutlineParam !== null) {
2342
2504
  const endpoint = {
2343
2505
  nodeId: node.id,
@@ -2542,11 +2704,12 @@ var ConnectionManager = class extends EventEmitter {
2542
2704
  /**
2543
2705
  * Find the nearest edge under the cursor for junction / note→relation drops.
2544
2706
  */
2545
- findEdgeDropTarget(point) {
2546
- const tolerance = Math.max(14, 20 / Math.max(this.renderer.zoom, 1e-4));
2707
+ findEdgeDropTarget(point, options) {
2708
+ 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);
2547
2709
  let best = null;
2548
2710
  for (const edge of this.renderer.edges.values()) {
2549
2711
  if (!edge.visible || edge.path.length < 2) continue;
2712
+ if (options?.skipEdgeIds?.has(edge.id)) continue;
2550
2713
  const closest = edge.getClosestPointOnPath(point);
2551
2714
  if (!closest || closest.distance > tolerance) continue;
2552
2715
  if (!best || closest.distance < best.distance) best = {
@@ -2805,6 +2968,26 @@ var ConnectionManager = class extends EventEmitter {
2805
2968
  }
2806
2969
  return null;
2807
2970
  }
2971
+ getRawNodeAtPoint(point) {
2972
+ const nodes = Array.from(this.renderer.nodes.values());
2973
+ for (let i = nodes.length - 1; i >= 0; i--) {
2974
+ const node = nodes[i];
2975
+ if (node.visible && node.hitTest(point)) return node;
2976
+ }
2977
+ return null;
2978
+ }
2979
+ nodeLooksLikeContainer(node, source = this.sourceNode) {
2980
+ if (!source || source.id === node.id) return false;
2981
+ return node.width * node.height >= Math.max(source.width * source.height, 1) * 4;
2982
+ }
2983
+ nodeContainsSource(node, source = this.sourceNode) {
2984
+ if (!source || source.id === node.id) return false;
2985
+ const sourceBounds = source.getBounds();
2986
+ const nodeBounds = node.getBounds();
2987
+ const cx = sourceBounds.x + sourceBounds.width / 2;
2988
+ const cy = sourceBounds.y + sourceBounds.height / 2;
2989
+ return cx >= nodeBounds.x && cx <= nodeBounds.x + nodeBounds.width && cy >= nodeBounds.y && cy <= nodeBounds.y + nodeBounds.height;
2990
+ }
2808
2991
  startOutlineConnection(node, attach) {
2809
2992
  const { point: outlinePoint, param } = node.getClosestPointOnOutline(attach);
2810
2993
  this.sourceNode = node;
@@ -2820,17 +3003,16 @@ var ConnectionManager = class extends EventEmitter {
2820
3003
  return 8 / Math.max(this.renderer.zoom, 1e-4);
2821
3004
  }
2822
3005
  outlineConnectHandleFor(node, pointer) {
3006
+ const { point: onOutline } = node.getClosestPointOnOutline(pointer);
2823
3007
  const prevSide = node.id === this.hoverNodeId ? this.hoverOutlineSide : null;
2824
- const geom = getOutlineConnectHandle(node.getBounds(), pointer, 0, prevSide, this.outlineHandleHysteresis());
2825
- const { point: onOutline } = node.getClosestPointOnOutline(geom.attach);
2826
3008
  return {
2827
3009
  attach: onOutline,
2828
3010
  handle: onOutline,
2829
- side: geom.side
3011
+ side: getOutlineConnectHandle(node.getBounds(), pointer, 0, prevSide, this.outlineHandleHysteresis()).side
2830
3012
  };
2831
3013
  }
2832
3014
  outlineHandleHitRadius() {
2833
- return 10 / Math.max(this.renderer.zoom, 1e-4);
3015
+ return 4 / Math.max(this.renderer.zoom, 1e-4);
2834
3016
  }
2835
3017
  findOutlineConnectHandleAt(point) {
2836
3018
  if (!this.attachToOutline) return null;
@@ -2839,6 +3021,7 @@ var ConnectionManager = class extends EventEmitter {
2839
3021
  for (let i = nodes.length - 1; i >= 0; i--) {
2840
3022
  const node = nodes[i];
2841
3023
  if (!node.visible || !this.isCompatibleTarget(node)) continue;
3024
+ if (node.hitTestResizeHandle(point)) continue;
2842
3025
  const { attach, handle } = this.outlineConnectHandleFor(node, point);
2843
3026
  const dx = point.x - handle.x;
2844
3027
  const dy = point.y - handle.y;
@@ -2851,7 +3034,7 @@ var ConnectionManager = class extends EventEmitter {
2851
3034
  return null;
2852
3035
  }
2853
3036
  renderOutlineConnectHandle(ctx) {
2854
- if (!this.hoverNodeId || !this.hoverPoint) return;
3037
+ if (!this.hoverOutlineHandle || !this.hoverNodeId || !this.hoverPoint) return;
2855
3038
  const node = this.renderer.getNode(this.hoverNodeId);
2856
3039
  if (!node) return;
2857
3040
  const { handle } = this.outlineConnectHandleFor(node, this.hoverPoint);
@@ -2939,6 +3122,9 @@ function getHistoryShortcut(event, keys = DEFAULT_HISTORY_SHORTCUT_KEYS) {
2939
3122
  }
2940
3123
  //#endregion
2941
3124
  //#region src/utils/style.ts
3125
+ function canvasFontFromTextStyle(style) {
3126
+ return `${style.fontStyle ?? "normal"} ${style.fontWeight ?? "normal"} ${style.fontSize ?? 14}px ${style.fontFamily ?? "sans-serif"}`;
3127
+ }
2942
3128
  /**
2943
3129
  * Shallow compare two objects
2944
3130
  */
@@ -3038,6 +3224,7 @@ var DEFAULT_STYLE = {
3038
3224
  fontSize: 14,
3039
3225
  fontFamily: "sans-serif",
3040
3226
  fontWeight: "normal",
3227
+ fontStyle: "normal",
3041
3228
  color: "#000000",
3042
3229
  opacity: 1,
3043
3230
  align: "center",
@@ -3282,9 +3469,7 @@ var TextLabel = class {
3282
3469
  this.render(ctx, bounds);
3283
3470
  }
3284
3471
  applyStyle(ctx) {
3285
- const fontSize = this._style.fontSize ?? 14;
3286
- const fontFamily = this._style.fontFamily ?? "sans-serif";
3287
- ctx.font = `${this._style.fontWeight ?? "normal"} ${fontSize}px ${fontFamily}`;
3472
+ ctx.font = canvasFontFromTextStyle(this._style);
3288
3473
  ctx.textAlign = this._style.align ?? "center";
3289
3474
  ctx.textBaseline = this._style.baseline ?? "middle";
3290
3475
  }
@@ -3918,109 +4103,6 @@ var Element = class {
3918
4103
  }
3919
4104
  };
3920
4105
  //#endregion
3921
- //#region src/utils/edgePath.ts
3922
- /**
3923
- * Point and tangent angle at a normalized position along an edge path (0..1 by length).
3924
- */
3925
- function getPathPointAt(path, type, position) {
3926
- if (type !== "bezier" || path.length < 4) return getPointAlongPolyline(path, position);
3927
- const samples = [];
3928
- const steps = 20;
3929
- for (let i = 1; i + 2 < path.length; i += 3) {
3930
- const start = path[i - 1];
3931
- const control1 = path[i];
3932
- const control2 = path[i + 1];
3933
- const end = path[i + 2];
3934
- for (let sample = 0; sample <= steps; sample++) {
3935
- const t = sample / steps;
3936
- if (samples.length > 0 && t === 0) continue;
3937
- samples.push(bezierPoint(start, control1, control2, end, t));
3938
- }
3939
- }
3940
- return getPointAlongPolyline(samples, position);
3941
- }
3942
- function getPointAlongPolyline(path, position) {
3943
- if (path.length === 0) return {
3944
- point: {
3945
- x: 0,
3946
- y: 0
3947
- },
3948
- angle: 0
3949
- };
3950
- if (path.length === 1) return {
3951
- point: path[0],
3952
- angle: 0
3953
- };
3954
- let totalLength = 0;
3955
- const segments = [];
3956
- for (let i = 1; i < path.length; i++) {
3957
- const start = path[i - 1];
3958
- const end = path[i];
3959
- const length = Math.hypot(end.x - start.x, end.y - start.y);
3960
- segments.push({
3961
- start,
3962
- end,
3963
- length
3964
- });
3965
- totalLength += length;
3966
- }
3967
- const targetLength = totalLength * Math.max(0, Math.min(1, position));
3968
- let accumulated = 0;
3969
- for (const segment of segments) {
3970
- if (accumulated + segment.length >= targetLength) {
3971
- const segmentPosition = segment.length > 0 ? (targetLength - accumulated) / segment.length : 0;
3972
- return {
3973
- point: {
3974
- x: segment.start.x + segmentPosition * (segment.end.x - segment.start.x),
3975
- y: segment.start.y + segmentPosition * (segment.end.y - segment.start.y)
3976
- },
3977
- angle: Math.atan2(segment.end.y - segment.start.y, segment.end.x - segment.start.x)
3978
- };
3979
- }
3980
- accumulated += segment.length;
3981
- }
3982
- const lastSegment = segments[segments.length - 1];
3983
- return {
3984
- point: lastSegment.end,
3985
- angle: Math.atan2(lastSegment.end.y - lastSegment.start.y, lastSegment.end.x - lastSegment.start.x)
3986
- };
3987
- }
3988
- /** Map tangent angle to a cardinal direction used by polyline routing. */
3989
- function directionFromAngle(angle) {
3990
- const twoPi = Math.PI * 2;
3991
- const a = (angle % twoPi + twoPi) % twoPi;
3992
- if (a >= Math.PI * .25 && a < Math.PI * .75) return "bottom";
3993
- if (a >= Math.PI * .75 && a < Math.PI * 1.25) return "left";
3994
- if (a >= Math.PI * 1.25 && a < Math.PI * 1.75) return "top";
3995
- return "right";
3996
- }
3997
- /**
3998
- * Nearest point on an edge path to `point`, by sampling along normalized length.
3999
- */
4000
- function getClosestPointOnPath(path, type, point, samples = 48) {
4001
- if (path.length < 2) return null;
4002
- let bestPoint = getPathPointAt(path, type, 0).point;
4003
- let bestParam = 0;
4004
- let bestDistSq = Infinity;
4005
- for (let i = 0; i <= samples; i++) {
4006
- const pathParam = i / samples;
4007
- const at = getPathPointAt(path, type, pathParam).point;
4008
- const dx = at.x - point.x;
4009
- const dy = at.y - point.y;
4010
- const distSq = dx * dx + dy * dy;
4011
- if (distSq < bestDistSq) {
4012
- bestDistSq = distSq;
4013
- bestPoint = at;
4014
- bestParam = pathParam;
4015
- }
4016
- }
4017
- return {
4018
- point: bestPoint,
4019
- pathParam: bestParam,
4020
- distance: Math.sqrt(bestDistSq)
4021
- };
4022
- }
4023
- //#endregion
4024
4106
  //#region src/elements/paths/StraightPathStrategy.ts
4025
4107
  /**
4026
4108
  * Straight line path strategy
@@ -7082,7 +7164,12 @@ var Edge = class extends Element {
7082
7164
  * Nearest point on this edge's path to `point` (world coordinates).
7083
7165
  */
7084
7166
  getClosestPointOnPath(point) {
7085
- return getClosestPointOnPath(this._path, this._type, point);
7167
+ const onPath = getClosestPointOnPath(this._path, this._type, point);
7168
+ if (onPath) return onPath;
7169
+ const start = this._fromPoint;
7170
+ const end = this._toPoint;
7171
+ if (start.x === end.x && start.y === end.y) return null;
7172
+ return getClosestPointOnPath([start, end], "straight", point);
7086
7173
  }
7087
7174
  /**
7088
7175
  * Get world position of label center along path.
@@ -7939,7 +8026,13 @@ var InteractionManager = class {
7939
8026
  }
7940
8027
  if ("typeName" in hitElement) {
7941
8028
  if (hitElement.typeName === "composite") {
7942
- this.startCompositeNameEdit(hitElement);
8029
+ const composite = hitElement;
8030
+ if (composite.usesExternalLabel()) {
8031
+ const editText = composite.label?.editableText ?? composite.label?.text ?? "";
8032
+ this.startLabelEdit("node", composite.id, editText, composite.getLabelPosition());
8033
+ return;
8034
+ }
8035
+ this.startCompositeNameEdit(composite);
7943
8036
  return;
7944
8037
  }
7945
8038
  const editText = hitElement.label?.editableText ?? hitElement.label?.text ?? "";
@@ -9606,19 +9699,27 @@ var DiagramRenderer = class extends EventEmitter {
9606
9699
  return this._groups.get(groupId);
9607
9700
  }
9608
9701
  /**
9609
- * Find element at a point (nodes, then edges, then groups)
9702
+ * Find element at a point. Edges are painted above nodes, so a stroke over a
9703
+ * nested pool/lane fill is hit before the covering container.
9610
9704
  */
9611
9705
  getElementAtPoint(worldPoint) {
9706
+ let bestEdge;
9707
+ let bestDist = Infinity;
9708
+ for (const edge of this._edges.values()) {
9709
+ if (!edge.visible) continue;
9710
+ const tolerance = Math.max((edge.style.strokeWidth ?? 2) * 2, 8) / Math.max(this._zoom, 1e-4);
9711
+ const closest = edge.getClosestPointOnPath(worldPoint);
9712
+ if (closest && closest.distance <= tolerance && closest.distance < bestDist) {
9713
+ bestDist = closest.distance;
9714
+ bestEdge = edge;
9715
+ }
9716
+ }
9717
+ if (bestEdge) return bestEdge;
9612
9718
  const nodesArray = Array.from(this._nodes.values());
9613
9719
  for (let i = nodesArray.length - 1; i >= 0; i--) {
9614
9720
  const node = nodesArray[i];
9615
9721
  if (node.visible && node.hitTest(worldPoint)) return node;
9616
9722
  }
9617
- for (const edge of this._edges.values()) {
9618
- if (!edge.visible) continue;
9619
- const tolerance = Math.max((edge.style.strokeWidth ?? 2) * 2, 8) / Math.max(this._zoom, 1e-4);
9620
- if (edge.hitTestWithTolerance(worldPoint, tolerance)) return edge;
9621
- }
9622
9723
  for (const group of this._groups.values()) if (group.visible && group.hitTest(worldPoint)) return group;
9623
9724
  }
9624
9725
  /**
@@ -10634,6 +10735,52 @@ var NodeImage = class {
10634
10735
  }
10635
10736
  };
10636
10737
  //#endregion
10738
+ //#region src/utils/labelPlacement.ts
10739
+ var DEFAULT_LABEL_GAP = 4;
10740
+ function isExternalLabelPlacement(placement) {
10741
+ return placement === "top" || placement === "bottom" || placement === "left" || placement === "right";
10742
+ }
10743
+ function externalLabelWrapWidth(shapeWidth) {
10744
+ return Math.max(shapeWidth, 80);
10745
+ }
10746
+ function resolveExternalLabelBounds(shape, placement, gap, measured) {
10747
+ const width = Math.max(0, measured.width);
10748
+ const height = Math.max(0, measured.height);
10749
+ const safeGap = Number.isFinite(gap) ? gap : 4;
10750
+ switch (placement) {
10751
+ case "top": return {
10752
+ x: shape.x + (shape.width - width) / 2,
10753
+ y: shape.y - safeGap - height,
10754
+ width,
10755
+ height
10756
+ };
10757
+ case "bottom": return {
10758
+ x: shape.x + (shape.width - width) / 2,
10759
+ y: shape.y + shape.height + safeGap,
10760
+ width,
10761
+ height
10762
+ };
10763
+ case "left": return {
10764
+ x: shape.x - safeGap - width,
10765
+ y: shape.y + (shape.height - height) / 2,
10766
+ width,
10767
+ height
10768
+ };
10769
+ case "right": return {
10770
+ x: shape.x + shape.width + safeGap,
10771
+ y: shape.y + (shape.height - height) / 2,
10772
+ width,
10773
+ height
10774
+ };
10775
+ }
10776
+ }
10777
+ function unionBounds(a, b) {
10778
+ return rectUnion(a, b);
10779
+ }
10780
+ function pointInBounds(point, bounds) {
10781
+ return pointInRect(point, bounds);
10782
+ }
10783
+ //#endregion
10637
10784
  //#region src/utils/resolveContentInset.ts
10638
10785
  function axisFactor(node, base) {
10639
10786
  if (base === void 0 || !(base > 0) || !Number.isFinite(base)) return 1;
@@ -10706,6 +10853,8 @@ var Node = class extends Element {
10706
10853
  this._contentInset = this.normalizeContentInset(options.contentInset);
10707
10854
  this._contentInsetScale = this.normalizeContentInsetScale(options.contentInsetScale);
10708
10855
  this._contentInsetBaseSize = this.normalizeContentInsetBaseSize(options.contentInsetBaseSize);
10856
+ this._labelPlacement = options.labelPlacement ?? "center";
10857
+ this._labelGap = options.labelGap !== void 0 && Number.isFinite(options.labelGap) ? Math.max(0, options.labelGap) : 4;
10709
10858
  if (options.label !== void 0) if (typeof options.label === "string") this._label = new TextLabel({
10710
10859
  text: options.label,
10711
10860
  onChange: () => this.markDirty()
@@ -10756,6 +10905,33 @@ var Node = class extends Element {
10756
10905
  get ports() {
10757
10906
  return this._ports;
10758
10907
  }
10908
+ get labelPlacement() {
10909
+ return this._labelPlacement;
10910
+ }
10911
+ set labelPlacement(value) {
10912
+ if (this._labelPlacement !== value) {
10913
+ this._labelPlacement = value;
10914
+ this.markDirty();
10915
+ }
10916
+ }
10917
+ get labelGap() {
10918
+ return this._labelGap;
10919
+ }
10920
+ set labelGap(value) {
10921
+ const next = Number.isFinite(value) ? Math.max(0, value) : 4;
10922
+ if (this._labelGap !== next) {
10923
+ this._labelGap = next;
10924
+ this.markDirty();
10925
+ }
10926
+ }
10927
+ usesExternalLabel() {
10928
+ return isExternalLabelPlacement(this._labelPlacement) && this._label !== void 0;
10929
+ }
10930
+ getVisualBounds() {
10931
+ const shape = this.getBounds();
10932
+ if (!this.usesExternalLabel()) return shape;
10933
+ return unionBounds(shape, this.getResolvedLabelBounds());
10934
+ }
10759
10935
  /**
10760
10936
  * Text label
10761
10937
  */
@@ -10979,13 +11155,15 @@ var Node = class extends Element {
10979
11155
  * Calculate layout for icon and label.
10980
11156
  * Label always gets full content area; icon is placed within the same content area.
10981
11157
  */
10982
- calculateContentLayout(bounds, iconBoxSize, _labelSize) {
11158
+ calculateContentLayout(bounds, iconBoxSize, labelSize) {
11159
+ const iconContainer = this.getIconContainerBounds(bounds);
10983
11160
  const contentBounds = this.getLabelContainerBounds(bounds);
10984
- let iconBounds = contentBounds;
10985
- if (this._icon && iconBoxSize) iconBounds = this.getIconBounds(contentBounds, iconBoxSize, this._icon.placement);
11161
+ let iconBounds = iconContainer;
11162
+ if (this._icon && iconBoxSize) iconBounds = this.getIconBounds(iconContainer, iconBoxSize, this._icon.placement);
11163
+ const labelBounds = isExternalLabelPlacement(this._labelPlacement) && this._label && labelSize ? resolveExternalLabelBounds(bounds, this._labelPlacement, this._labelGap, labelSize) : contentBounds;
10986
11164
  return {
10987
11165
  iconBounds,
10988
- labelBounds: contentBounds
11166
+ labelBounds
10989
11167
  };
10990
11168
  }
10991
11169
  /**
@@ -10995,14 +11173,14 @@ var Node = class extends Element {
10995
11173
  getLabelBoundsForExport(ctx) {
10996
11174
  if (!this._label) return null;
10997
11175
  const bounds = this.getBounds();
10998
- const contentBounds = this.getLabelContainerBounds(bounds);
11176
+ const wrapWidth = this.getLabelWrapWidth(bounds);
10999
11177
  const iconBoxSize = this._icon ? this.getIconBoxSize() : void 0;
11000
- this._label.setAutoMaxWidth(contentBounds.width);
11178
+ this._label.setAutoMaxWidth(wrapWidth);
11001
11179
  const labelSize = this._label.measure(ctx);
11002
11180
  const { labelBounds } = this.calculateContentLayout(bounds, iconBoxSize, labelSize);
11003
11181
  return {
11004
11182
  bounds: labelBounds,
11005
- lines: this._label.getWrappedLines(ctx, Math.max(0, contentBounds.width))
11183
+ lines: this._label.getWrappedLines(ctx, Math.max(0, wrapWidth))
11006
11184
  };
11007
11185
  }
11008
11186
  /**
@@ -11015,10 +11193,10 @@ var Node = class extends Element {
11015
11193
  ctx.globalAlpha = 1;
11016
11194
  }
11017
11195
  /**
11018
- * Get world position of label center (center of content area).
11196
+ * Get world position of label center (content area or external label box).
11019
11197
  */
11020
11198
  getLabelPosition() {
11021
- const bounds = this.getLabelContainerBounds(this.getBounds());
11199
+ const bounds = this.getResolvedLabelBounds();
11022
11200
  return {
11023
11201
  x: bounds.x + bounds.width / 2,
11024
11202
  y: bounds.y + bounds.height / 2
@@ -11033,13 +11211,14 @@ var Node = class extends Element {
11033
11211
  let bounds = this.getBounds();
11034
11212
  this.renderBadges(ctx, bounds);
11035
11213
  const iconBoxSize = this._icon ? this.getIconBoxSize() : void 0;
11036
- if (this._label) this._label.setAutoMaxWidth(this.getLabelContainerBounds(bounds).width);
11214
+ const externalLabel = this.usesExternalLabel();
11215
+ if (this._label) this._label.setAutoMaxWidth(this.getLabelWrapWidth(bounds));
11037
11216
  const labelSize = this._label ? this._label.measure(ctx) : void 0;
11038
- if (labelSize || iconBoxSize) {
11217
+ if ((labelSize || iconBoxSize) && !externalLabel) {
11039
11218
  this.ensureContentsFit(labelSize, iconBoxSize);
11040
11219
  if (this._label && iconBoxSize) {
11041
11220
  bounds = this.getBounds();
11042
- this._label.setAutoMaxWidth(this.getLabelContainerBounds(bounds).width);
11221
+ this._label.setAutoMaxWidth(this.getLabelWrapWidth(bounds));
11043
11222
  }
11044
11223
  }
11045
11224
  const { iconBounds, labelBounds } = this.calculateContentLayout(this.getBounds(), iconBoxSize, labelSize);
@@ -11116,10 +11295,10 @@ var Node = class extends Element {
11116
11295
  return this.calculateContentMinSize(labelSize, iconBoxSize, bounds);
11117
11296
  }
11118
11297
  /**
11119
- * Render resize handles when selected
11298
+ * Render the selection frame when selected, and resize handles when they are enabled.
11120
11299
  */
11121
11300
  renderResizeHandles(ctx) {
11122
- if (this._state !== "selected" || !this._resizeHandlesEnabled) return;
11301
+ if (this._state !== "selected") return;
11123
11302
  const bounds = this.getBounds();
11124
11303
  const offset = 6;
11125
11304
  const size = 8;
@@ -11129,6 +11308,10 @@ var Node = class extends Element {
11129
11308
  ctx.lineWidth = 1;
11130
11309
  ctx.setLineDash([4, 4]);
11131
11310
  ctx.strokeRect(bounds.x - offset, bounds.y - offset, bounds.width + offset * 2, bounds.height + offset * 2);
11311
+ if (!this._resizeHandlesEnabled) {
11312
+ ctx.restore();
11313
+ return;
11314
+ }
11132
11315
  ctx.fillStyle = "#ffffff";
11133
11316
  ctx.strokeStyle = DEFAULT_SELECTION_COLOR;
11134
11317
  ctx.lineWidth = 1.5;
@@ -11145,7 +11328,7 @@ var Node = class extends Element {
11145
11328
  * Hit test a resize handle
11146
11329
  */
11147
11330
  hitTestResizeHandle(point) {
11148
- if (this._state !== "selected") return null;
11331
+ if (this._state !== "selected" || !this._resizeHandlesEnabled) return null;
11149
11332
  const half = 8 / 2;
11150
11333
  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;
11151
11334
  return null;
@@ -11156,7 +11339,25 @@ var Node = class extends Element {
11156
11339
  hitTest(point) {
11157
11340
  const bounds = this.getBounds();
11158
11341
  const padding = 10;
11159
- 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;
11342
+ 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);
11343
+ }
11344
+ hitTestExternalLabel(point) {
11345
+ if (!this.usesExternalLabel()) return false;
11346
+ return pointInBounds(point, this.getResolvedLabelBounds());
11347
+ }
11348
+ getLabelWrapWidth(bounds = this.getBounds()) {
11349
+ if (this.usesExternalLabel()) return externalLabelWrapWidth(bounds.width);
11350
+ return this.getLabelContainerBounds(bounds).width;
11351
+ }
11352
+ getResolvedLabelBounds(measured) {
11353
+ const shape = this.getBounds();
11354
+ if (!this.usesExternalLabel() || !this._label) return this.getLabelContainerBounds(shape);
11355
+ const size = measured ?? {
11356
+ width: this._label.measuredWidth || this.getLabelWrapWidth(shape),
11357
+ height: this._label.measuredHeight || (this._label.style.fontSize ?? 14) * 1.2
11358
+ };
11359
+ if (!isExternalLabelPlacement(this._labelPlacement)) return this.getLabelContainerBounds(shape);
11360
+ return resolveExternalLabelBounds(shape, this._labelPlacement, this._labelGap, size);
11160
11361
  }
11161
11362
  /**
11162
11363
  * Get a connection point on the node boundary toward a target point,
@@ -11390,16 +11591,28 @@ var Node = class extends Element {
11390
11591
  }
11391
11592
  calculateContentMinSize(labelSize, iconBoxSize, bounds) {
11392
11593
  const labelContainer = this.getLabelContainerBounds(bounds);
11393
- const widthFactor = labelContainer.width > 0 ? bounds.width / labelContainer.width : 1;
11394
- const heightFactor = labelContainer.height > 0 ? bounds.height / labelContainer.height : 1;
11395
- const minContentWidth = Math.max(labelSize?.width ?? 0, iconBoxSize?.width ?? 0);
11396
- const minContentHeight = Math.max(labelSize?.height ?? 0, iconBoxSize?.height ?? 0);
11594
+ const iconContainer = this.getIconContainerBounds(bounds);
11595
+ const labelWidthFactor = labelContainer.width > 0 ? bounds.width / labelContainer.width : 1;
11596
+ const labelHeightFactor = labelContainer.height > 0 ? bounds.height / labelContainer.height : 1;
11597
+ const iconWidthFactor = iconContainer.width > 0 ? bounds.width / iconContainer.width : 1;
11598
+ const iconHeightFactor = iconContainer.height > 0 ? bounds.height / iconContainer.height : 1;
11397
11599
  return {
11398
- width: minContentWidth * widthFactor,
11399
- height: minContentHeight * heightFactor
11600
+ width: Math.max((labelSize?.width ?? 0) * labelWidthFactor, (iconBoxSize?.width ?? 0) * iconWidthFactor),
11601
+ height: Math.max((labelSize?.height ?? 0) * labelHeightFactor, (iconBoxSize?.height ?? 0) * iconHeightFactor)
11400
11602
  };
11401
11603
  }
11604
+ /**
11605
+ * Content box for the node icon: AABB minus contentInset.
11606
+ * Unlike {@link getLabelContainerBounds}, this is not shrunk to the
11607
+ * inscribed rectangle of a circle/diamond, so a centered icon can fill the shape.
11608
+ */
11609
+ getIconContainerBounds(bounds) {
11610
+ return this.applyContentInset(bounds);
11611
+ }
11402
11612
  getLabelContainerBounds(bounds) {
11613
+ return this.applyContentInset(bounds);
11614
+ }
11615
+ applyContentInset(bounds) {
11403
11616
  const { top, right, bottom, left } = resolveContentInset(this._contentInset, this._contentInsetScale, {
11404
11617
  width: bounds.width,
11405
11618
  height: bounds.height
@@ -11796,7 +12009,7 @@ var CircleNode = class extends Node {
11796
12009
  const ry = this._height / 2 + padding;
11797
12010
  const dx = point.x - center.x;
11798
12011
  const dy = point.y - center.y;
11799
- return dx * dx / (rx * rx) + dy * dy / (ry * ry) <= 1;
12012
+ return dx * dx / (rx * rx) + dy * dy / (ry * ry) <= 1 || this.hitTestExternalLabel(point);
11800
12013
  }
11801
12014
  getOutlineDirection(param) {
11802
12015
  return this.outlineDirectionFromCenter(param);
@@ -11885,7 +12098,7 @@ var DiamondNode = class extends Node {
11885
12098
  const hh = this._height / 2 + padding;
11886
12099
  const dx = Math.abs(point.x - center.x);
11887
12100
  const dy = Math.abs(point.y - center.y);
11888
- return dx / hw + dy / hh <= 1;
12101
+ return dx / hw + dy / hh <= 1 || this.hitTestExternalLabel(point);
11889
12102
  }
11890
12103
  getOutlineDirection(param) {
11891
12104
  return this.outlineDirectionFromCenter(param);
@@ -12286,7 +12499,7 @@ var CustomShapeNode = class extends Node {
12286
12499
  const path = this.getPath();
12287
12500
  if (ctx.isPointInPath(path, localX, localY)) return true;
12288
12501
  ctx.lineWidth = 20;
12289
- return ctx.isPointInStroke(path, localX, localY);
12502
+ return ctx.isPointInStroke(path, localX, localY) || this.hitTestExternalLabel(point);
12290
12503
  }
12291
12504
  render(ctx) {
12292
12505
  const style = this.style;
@@ -13116,6 +13329,9 @@ function isCContainer(component) {
13116
13329
  function isCShape(component) {
13117
13330
  return component.type === "shape";
13118
13331
  }
13332
+ function isCText(component) {
13333
+ return component.type === "text";
13334
+ }
13119
13335
  /**
13120
13336
  * Flex container component for CompositeNode.
13121
13337
  * Lays out children using a flexbox-like algorithm.
@@ -13126,6 +13342,8 @@ var CContainer = class extends CompositeComponentBase {
13126
13342
  this.type = "container";
13127
13343
  this._cachedBounds = null;
13128
13344
  this._cachedContainerBounds = null;
13345
+ this._layoutChildren = null;
13346
+ this._suppressBoundName = false;
13129
13347
  this.id = options.id;
13130
13348
  this._direction = options.direction ?? "column";
13131
13349
  this._justifyContent = options.justifyContent ?? "start";
@@ -13154,6 +13372,27 @@ var CContainer = class extends CompositeComponentBase {
13154
13372
  get children() {
13155
13373
  return this._children;
13156
13374
  }
13375
+ /**
13376
+ * When true, CText bound to `__name__` is omitted from measure/render/hit/SVG.
13377
+ * Used when CompositeNode draws the name as an external Node.label.
13378
+ */
13379
+ setSuppressBoundName(value) {
13380
+ if (this._suppressBoundName === value) return;
13381
+ this._suppressBoundName = value;
13382
+ this._cachedBounds = null;
13383
+ this._layoutChildren = null;
13384
+ for (const child of this._children) {
13385
+ if (isCContainer(child)) child.setSuppressBoundName(value);
13386
+ if (isCShape(child) && child.content) child.content.setSuppressBoundName(value);
13387
+ }
13388
+ }
13389
+ isLayoutSkipped(child) {
13390
+ if (child.style.visible === false) return true;
13391
+ return this._suppressBoundName && isCText(child) && child.bindToProperty === "__name__";
13392
+ }
13393
+ getLayoutChildren() {
13394
+ return this._children.filter((child) => !this.isLayoutSkipped(child));
13395
+ }
13157
13396
  addChild(child) {
13158
13397
  child.setOnChange(() => this.handleChildChange());
13159
13398
  this._children.push(child);
@@ -13185,8 +13424,8 @@ var CContainer = class extends CompositeComponentBase {
13185
13424
  padding: this._padding
13186
13425
  };
13187
13426
  }
13188
- buildFlexChildren(ctx) {
13189
- return this._children.map((child) => {
13427
+ buildFlexChildren(ctx, children = this.getLayoutChildren()) {
13428
+ return children.map((child) => {
13190
13429
  const s = child.style;
13191
13430
  return {
13192
13431
  measure: child.measure(ctx),
@@ -13219,11 +13458,13 @@ var CContainer = class extends CompositeComponentBase {
13219
13458
  }
13220
13459
  render(ctx, bounds) {
13221
13460
  if (this.style.visible === false) return;
13222
- const flexChildren = this.buildFlexChildren(ctx);
13461
+ const layoutChildren = this.getLayoutChildren();
13462
+ const flexChildren = this.buildFlexChildren(ctx, layoutChildren);
13223
13463
  const result = flexLayout({
13224
13464
  width: bounds.width,
13225
13465
  height: bounds.height
13226
13466
  }, this.getFlexConfig(), flexChildren);
13467
+ this._layoutChildren = layoutChildren;
13227
13468
  this._cachedBounds = result.childBounds;
13228
13469
  this._cachedContainerBounds = bounds;
13229
13470
  const opacity = this.style.opacity ?? 1;
@@ -13231,9 +13472,8 @@ var CContainer = class extends CompositeComponentBase {
13231
13472
  ctx.save();
13232
13473
  ctx.globalAlpha *= opacity;
13233
13474
  }
13234
- for (let i = 0; i < this._children.length; i++) {
13235
- const child = this._children[i];
13236
- if (child.style.visible === false) continue;
13475
+ for (let i = 0; i < layoutChildren.length; i++) {
13476
+ const child = layoutChildren[i];
13237
13477
  const cb = result.childBounds[i];
13238
13478
  child.render(ctx, {
13239
13479
  x: bounds.x + cb.x,
@@ -13246,10 +13486,9 @@ var CContainer = class extends CompositeComponentBase {
13246
13486
  }
13247
13487
  hitTest(point, bounds) {
13248
13488
  if (this.style.visible === false) return null;
13249
- if (!this._cachedBounds) return null;
13250
- for (let i = this._children.length - 1; i >= 0; i--) {
13251
- const child = this._children[i];
13252
- if (child.style.visible === false) continue;
13489
+ if (!this._cachedBounds || !this._layoutChildren) return null;
13490
+ for (let i = this._layoutChildren.length - 1; i >= 0; i--) {
13491
+ const child = this._layoutChildren[i];
13253
13492
  const cb = this._cachedBounds[i];
13254
13493
  const absBounds = {
13255
13494
  x: bounds.x + cb.x,
@@ -13263,6 +13502,22 @@ var CContainer = class extends CompositeComponentBase {
13263
13502
  return null;
13264
13503
  }
13265
13504
  /**
13505
+ * First CText bound to `__name__`, including suppressed name text.
13506
+ */
13507
+ findBoundNameText() {
13508
+ for (const child of this._children) {
13509
+ if (isCText(child) && child.bindToProperty === "__name__") return child;
13510
+ if (isCContainer(child)) {
13511
+ const found = child.findBoundNameText();
13512
+ if (found) return found;
13513
+ }
13514
+ if (isCShape(child) && child.content) {
13515
+ const found = child.content.findBoundNameText();
13516
+ if (found) return found;
13517
+ }
13518
+ }
13519
+ }
13520
+ /**
13266
13521
  * Find a component by id recursively.
13267
13522
  */
13268
13523
  findById(id) {
@@ -13299,8 +13554,8 @@ var CContainer = class extends CompositeComponentBase {
13299
13554
  if (this.style.visible === false) return "";
13300
13555
  const cachedBounds = this._cachedBounds;
13301
13556
  const cachedContainer = this._cachedContainerBounds;
13302
- return `<g>${this._children.map((child, i) => {
13303
- if (child.style.visible === false) return "";
13557
+ const layoutChildren = this._layoutChildren ?? this.getLayoutChildren();
13558
+ return `<g>${layoutChildren.map((child, i) => {
13304
13559
  if (cachedBounds && cachedContainer) {
13305
13560
  const cb = cachedBounds[i];
13306
13561
  const dx = bounds.x - cachedContainer.x;
@@ -13312,7 +13567,7 @@ var CContainer = class extends CompositeComponentBase {
13312
13567
  height: cb.height
13313
13568
  });
13314
13569
  }
13315
- const h = bounds.height / Math.max(1, this._children.length);
13570
+ const h = bounds.height / Math.max(1, layoutChildren.length);
13316
13571
  return child.toSVG({
13317
13572
  x: bounds.x,
13318
13573
  y: bounds.y + i * h,
@@ -13619,8 +13874,8 @@ var CompositeNode = class extends Node {
13619
13874
  return this.getLabelContainerBounds(this.getBounds());
13620
13875
  }
13621
13876
  hitTest(point) {
13622
- if (this._shapeType === "circle") return this.hitTestEllipse(point);
13623
- if (this._shapeType === "diamond") return this.hitTestDiamond(point);
13877
+ if (this._shapeType === "circle") return this.hitTestEllipse(point) || this.hitTestExternalLabel(point);
13878
+ if (this._shapeType === "diamond") return this.hitTestDiamond(point) || this.hitTestExternalLabel(point);
13624
13879
  return super.hitTest(point);
13625
13880
  }
13626
13881
  hitTestEllipse(point) {
@@ -13641,7 +13896,27 @@ var CompositeNode = class extends Node {
13641
13896
  const dy = Math.abs(point.y - center.y);
13642
13897
  return dx / hw + dy / hh <= 1;
13643
13898
  }
13899
+ /**
13900
+ * Copy color/font from the suppressed `__name__` CText onto the external Node.label.
13901
+ */
13902
+ syncExternalBoundNameLabelStyle() {
13903
+ if (!this.usesExternalLabel() || !this._label) return;
13904
+ const nameText = this._content.findBoundNameText();
13905
+ if (!nameText) return;
13906
+ const current = this._label.styleOverrides;
13907
+ if (current.color === nameText.color && current.fontFamily === nameText.fontFamily && current.fontWeight === nameText.fontWeight && current.fontStyle === nameText.fontStyle && current.fontSize === nameText.fontSize) return;
13908
+ this._label.style = {
13909
+ ...current,
13910
+ color: nameText.color,
13911
+ fontFamily: nameText.fontFamily,
13912
+ fontWeight: nameText.fontWeight,
13913
+ fontStyle: nameText.fontStyle,
13914
+ fontSize: nameText.fontSize
13915
+ };
13916
+ }
13644
13917
  render(ctx) {
13918
+ this._content.setSuppressBoundName(this.usesExternalLabel());
13919
+ this.syncExternalBoundNameLabelStyle();
13645
13920
  if (this._autoSize) this.applyAutoSize(ctx);
13646
13921
  const { x, y, width, height } = this.getBounds();
13647
13922
  const style = this.style;
@@ -13714,6 +13989,11 @@ var CompositeNode = class extends Node {
13714
13989
  this.renderBadges(ctx, bounds);
13715
13990
  const contentBounds = this.getContentBounds();
13716
13991
  this._content.render(ctx, contentBounds);
13992
+ if (this.usesExternalLabel() && this._label) {
13993
+ this._label.setAutoMaxWidth(this.getLabelWrapWidth(bounds));
13994
+ const labelSize = this._label.measure(ctx);
13995
+ this.renderLabel(ctx, this.getResolvedLabelBounds(labelSize));
13996
+ }
13717
13997
  this.renderPorts(ctx);
13718
13998
  }
13719
13999
  applyAutoSize(ctx) {
@@ -14347,6 +14627,8 @@ var Serializer = class {
14347
14627
  styleClass: node.styleClass,
14348
14628
  label,
14349
14629
  labelStyleClass: typeof label === "string" ? node.label?.styleClass : void 0,
14630
+ labelPlacement: node.labelPlacement !== "center" ? node.labelPlacement : void 0,
14631
+ labelGap: node.labelGap !== 4 ? node.labelGap : void 0,
14350
14632
  icon,
14351
14633
  contentInset: hasContentInset ? contentInset : void 0,
14352
14634
  anchorPoints,
@@ -14653,9 +14935,16 @@ var SvgExporter = class {
14653
14935
  break;
14654
14936
  }
14655
14937
  }
14938
+ cn.content.setSuppressBoundName(cn.usesExternalLabel());
14939
+ cn.syncExternalBoundNameLabelStyle();
14656
14940
  const contentBounds = cn.getLabelContainerBounds(bounds);
14657
14941
  const contentSvg = cn.content.toSVG(contentBounds);
14658
- return [`${shape} fill="${fill}" fill-opacity="${fillOpacity}" stroke="${stroke}" stroke-width="${strokeWidth}" stroke-opacity="${strokeOpacity}"${dash}${dashOffset}/>`, contentSvg].join("");
14942
+ const labelSvg = cn.usesExternalLabel() ? this.renderNodeLabel(cn, bounds) : "";
14943
+ return [
14944
+ `${shape} fill="${fill}" fill-opacity="${fillOpacity}" stroke="${stroke}" stroke-width="${strokeWidth}" stroke-opacity="${strokeOpacity}"${dash}${dashOffset}/>`,
14945
+ contentSvg,
14946
+ labelSvg
14947
+ ].join("");
14659
14948
  }
14660
14949
  default: shape = `<rect x="${bounds.x}" y="${bounds.y}" width="${bounds.width}" height="${bounds.height}"`;
14661
14950
  }
@@ -14728,15 +15017,13 @@ var SvgExporter = class {
14728
15017
  }
14729
15018
  measureTextLabel(text, style = {}, inset = 8) {
14730
15019
  const fontSize = style.fontSize ?? 14;
14731
- const fontFamily = style.fontFamily ?? "sans-serif";
14732
- const fontWeight = style.fontWeight ?? "normal";
14733
15020
  const lineHeight = fontSize * 1.2;
14734
15021
  const lines = text.split("\n");
14735
15022
  let maxWidth = 0;
14736
15023
  if (typeof document !== "undefined") {
14737
15024
  const ctx = document.createElement("canvas").getContext("2d");
14738
15025
  if (ctx) {
14739
- ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
15026
+ ctx.font = canvasFontFromTextStyle(style);
14740
15027
  for (const line of lines) maxWidth = Math.max(maxWidth, ctx.measureText(line).width);
14741
15028
  }
14742
15029
  }
@@ -14847,16 +15134,18 @@ var SvgExporter = class {
14847
15134
  const fontSize = style.fontSize ?? 14;
14848
15135
  const fontFamily = style.fontFamily ?? "sans-serif";
14849
15136
  const fontWeight = style.fontWeight ?? "normal";
15137
+ const fontStyle = style.fontStyle ?? "normal";
15138
+ const fontStyleAttr = fontStyle !== "normal" ? ` font-style="${fontStyle}"` : "";
14850
15139
  const opacity = style.opacity ?? 1;
14851
15140
  const anchor = style.align === "left" ? "start" : style.align === "right" ? "end" : "middle";
14852
15141
  const baseline = style.baseline === "top" ? "text-before-edge" : style.baseline === "bottom" ? "text-after-edge" : "middle";
14853
15142
  const transform = rotation !== 0 ? ` transform="rotate(${rotation * 180 / Math.PI} ${point.x} ${point.y})"` : "";
14854
15143
  const lines = text.split("\n");
14855
- 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>`;
15144
+ 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>`;
14856
15145
  const lineHeight = fontSize * 1.2;
14857
15146
  const startY = point.y - (lines.length - 1) * lineHeight / 2;
14858
15147
  const tspans = lines.map((line, index) => `<tspan x="${point.x}" y="${startY + index * lineHeight}">${this.escapeText(line)}</tspan>`).join("");
14859
- 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>`;
15148
+ 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>`;
14860
15149
  }
14861
15150
  buildEdgePath(edge, edgeLabelOffset) {
14862
15151
  if (!edge.label || !edge.labelLineGap) return this.buildPath(edge);
@@ -15571,6 +15860,6 @@ function applyEdgeStyle(ctx, style) {
15571
15860
  return baseOpacity * strokeOpacity;
15572
15861
  }
15573
15862
  //#endregion
15574
- 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 };
15863
+ 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 };
15575
15864
 
15576
15865
  //# sourceMappingURL=papirus.js.map