@ngroznykh/papirus 0.9.3 → 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.
@@ -2048,7 +2175,18 @@ var ConnectionManager = class extends EventEmitter {
2048
2175
  let snappedPoint = point;
2049
2176
  let snappedDir;
2050
2177
  let targetNode = this.getNodeAtPoint(point, true);
2051
- 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) {
2052
2190
  const tolerance = 40 / Math.max(this.renderer.zoom, 1e-4);
2053
2191
  const toleranceSq = tolerance * tolerance;
2054
2192
  let bestDistSq = Infinity;
@@ -2215,6 +2353,9 @@ var ConnectionManager = class extends EventEmitter {
2215
2353
  this.previewTargetNodeId = null;
2216
2354
  this.previewTargetEdgeId = null;
2217
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));
2218
2359
  let targetNode = this.getNodeAtPoint(cursorPoint, false);
2219
2360
  if (this.attachToOutline) {
2220
2361
  const tolerance = 40 / Math.max(this.renderer.zoom, 1e-4);
@@ -2258,7 +2399,7 @@ var ConnectionManager = class extends EventEmitter {
2258
2399
  }
2259
2400
  }
2260
2401
  }
2261
- const edgeDrop = this.findEdgeDropTarget(cursorPoint);
2402
+ const edgeDrop = this.findEdgeDropTarget(cursorPoint, searchGroupFill ? { maxScreenPx: 40 } : void 0);
2262
2403
  if (edgeDrop) {
2263
2404
  let nodeDistance = Infinity;
2264
2405
  if (targetNode) {
@@ -2266,7 +2407,7 @@ var ConnectionManager = class extends EventEmitter {
2266
2407
  const dy = cursorPoint.y - snappedPoint.y;
2267
2408
  nodeDistance = Math.sqrt(dx * dx + dy * dy);
2268
2409
  }
2269
- if (!targetNode || edgeDrop.distance < nodeDistance) {
2410
+ if (searchGroupFill || !targetNode || edgeDrop.distance <= nodeDistance) {
2270
2411
  targetNode = null;
2271
2412
  this.previewTargetNodeId = null;
2272
2413
  this.previewTargetOutlineParam = null;
@@ -2275,6 +2416,12 @@ var ConnectionManager = class extends EventEmitter {
2275
2416
  this.previewTargetEdgeId = edgeDrop.edge.id;
2276
2417
  this.previewTargetPathParam = edgeDrop.pathParam;
2277
2418
  }
2419
+ } else if (searchGroupFill) {
2420
+ targetNode = null;
2421
+ this.previewTargetNodeId = null;
2422
+ this.previewTargetOutlineParam = null;
2423
+ this.previewTargetAnchorId = null;
2424
+ snappedPoint = cursorPoint;
2278
2425
  }
2279
2426
  if (targetNode) {
2280
2427
  const forbidden = this._connectionValidator && this.sourceNode && !this._connectionValidator(this.sourceNode.id, targetNode.id);
@@ -2336,9 +2483,23 @@ var ConnectionManager = class extends EventEmitter {
2336
2483
  y: event.worldY
2337
2484
  };
2338
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
+ }
2339
2500
  let node = this.getNodeAtPoint(point, true);
2340
2501
  if (!node && this.attachToOutline && this.reconnectingTargetNodeId) node = this.renderer.getNode(this.reconnectingTargetNodeId) ?? null;
2341
- if (node) {
2502
+ if (!reconnected && node) {
2342
2503
  if (this.attachToOutline && this.reconnectingOutlineParam !== null) {
2343
2504
  const endpoint = {
2344
2505
  nodeId: node.id,
@@ -2543,11 +2704,12 @@ var ConnectionManager = class extends EventEmitter {
2543
2704
  /**
2544
2705
  * Find the nearest edge under the cursor for junction / note→relation drops.
2545
2706
  */
2546
- findEdgeDropTarget(point) {
2547
- 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);
2548
2709
  let best = null;
2549
2710
  for (const edge of this.renderer.edges.values()) {
2550
2711
  if (!edge.visible || edge.path.length < 2) continue;
2712
+ if (options?.skipEdgeIds?.has(edge.id)) continue;
2551
2713
  const closest = edge.getClosestPointOnPath(point);
2552
2714
  if (!closest || closest.distance > tolerance) continue;
2553
2715
  if (!best || closest.distance < best.distance) best = {
@@ -2806,6 +2968,26 @@ var ConnectionManager = class extends EventEmitter {
2806
2968
  }
2807
2969
  return null;
2808
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
+ }
2809
2991
  startOutlineConnection(node, attach) {
2810
2992
  const { point: outlinePoint, param } = node.getClosestPointOnOutline(attach);
2811
2993
  this.sourceNode = node;
@@ -2940,6 +3122,9 @@ function getHistoryShortcut(event, keys = DEFAULT_HISTORY_SHORTCUT_KEYS) {
2940
3122
  }
2941
3123
  //#endregion
2942
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
+ }
2943
3128
  /**
2944
3129
  * Shallow compare two objects
2945
3130
  */
@@ -3039,6 +3224,7 @@ var DEFAULT_STYLE = {
3039
3224
  fontSize: 14,
3040
3225
  fontFamily: "sans-serif",
3041
3226
  fontWeight: "normal",
3227
+ fontStyle: "normal",
3042
3228
  color: "#000000",
3043
3229
  opacity: 1,
3044
3230
  align: "center",
@@ -3283,9 +3469,7 @@ var TextLabel = class {
3283
3469
  this.render(ctx, bounds);
3284
3470
  }
3285
3471
  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}`;
3472
+ ctx.font = canvasFontFromTextStyle(this._style);
3289
3473
  ctx.textAlign = this._style.align ?? "center";
3290
3474
  ctx.textBaseline = this._style.baseline ?? "middle";
3291
3475
  }
@@ -3919,109 +4103,6 @@ var Element = class {
3919
4103
  }
3920
4104
  };
3921
4105
  //#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
4106
  //#region src/elements/paths/StraightPathStrategy.ts
4026
4107
  /**
4027
4108
  * Straight line path strategy
@@ -7083,7 +7164,12 @@ var Edge = class extends Element {
7083
7164
  * Nearest point on this edge's path to `point` (world coordinates).
7084
7165
  */
7085
7166
  getClosestPointOnPath(point) {
7086
- 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);
7087
7173
  }
7088
7174
  /**
7089
7175
  * Get world position of label center along path.
@@ -7940,7 +8026,13 @@ var InteractionManager = class {
7940
8026
  }
7941
8027
  if ("typeName" in hitElement) {
7942
8028
  if (hitElement.typeName === "composite") {
7943
- 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);
7944
8036
  return;
7945
8037
  }
7946
8038
  const editText = hitElement.label?.editableText ?? hitElement.label?.text ?? "";
@@ -9607,19 +9699,27 @@ var DiagramRenderer = class extends EventEmitter {
9607
9699
  return this._groups.get(groupId);
9608
9700
  }
9609
9701
  /**
9610
- * 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.
9611
9704
  */
9612
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;
9613
9718
  const nodesArray = Array.from(this._nodes.values());
9614
9719
  for (let i = nodesArray.length - 1; i >= 0; i--) {
9615
9720
  const node = nodesArray[i];
9616
9721
  if (node.visible && node.hitTest(worldPoint)) return node;
9617
9722
  }
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
9723
  for (const group of this._groups.values()) if (group.visible && group.hitTest(worldPoint)) return group;
9624
9724
  }
9625
9725
  /**
@@ -10635,6 +10735,52 @@ var NodeImage = class {
10635
10735
  }
10636
10736
  };
10637
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
10638
10784
  //#region src/utils/resolveContentInset.ts
10639
10785
  function axisFactor(node, base) {
10640
10786
  if (base === void 0 || !(base > 0) || !Number.isFinite(base)) return 1;
@@ -10707,6 +10853,8 @@ var Node = class extends Element {
10707
10853
  this._contentInset = this.normalizeContentInset(options.contentInset);
10708
10854
  this._contentInsetScale = this.normalizeContentInsetScale(options.contentInsetScale);
10709
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;
10710
10858
  if (options.label !== void 0) if (typeof options.label === "string") this._label = new TextLabel({
10711
10859
  text: options.label,
10712
10860
  onChange: () => this.markDirty()
@@ -10757,6 +10905,33 @@ var Node = class extends Element {
10757
10905
  get ports() {
10758
10906
  return this._ports;
10759
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
+ }
10760
10935
  /**
10761
10936
  * Text label
10762
10937
  */
@@ -10980,13 +11155,15 @@ var Node = class extends Element {
10980
11155
  * Calculate layout for icon and label.
10981
11156
  * Label always gets full content area; icon is placed within the same content area.
10982
11157
  */
10983
- calculateContentLayout(bounds, iconBoxSize, _labelSize) {
11158
+ calculateContentLayout(bounds, iconBoxSize, labelSize) {
11159
+ const iconContainer = this.getIconContainerBounds(bounds);
10984
11160
  const contentBounds = this.getLabelContainerBounds(bounds);
10985
- let iconBounds = contentBounds;
10986
- 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;
10987
11164
  return {
10988
11165
  iconBounds,
10989
- labelBounds: contentBounds
11166
+ labelBounds
10990
11167
  };
10991
11168
  }
10992
11169
  /**
@@ -10996,14 +11173,14 @@ var Node = class extends Element {
10996
11173
  getLabelBoundsForExport(ctx) {
10997
11174
  if (!this._label) return null;
10998
11175
  const bounds = this.getBounds();
10999
- const contentBounds = this.getLabelContainerBounds(bounds);
11176
+ const wrapWidth = this.getLabelWrapWidth(bounds);
11000
11177
  const iconBoxSize = this._icon ? this.getIconBoxSize() : void 0;
11001
- this._label.setAutoMaxWidth(contentBounds.width);
11178
+ this._label.setAutoMaxWidth(wrapWidth);
11002
11179
  const labelSize = this._label.measure(ctx);
11003
11180
  const { labelBounds } = this.calculateContentLayout(bounds, iconBoxSize, labelSize);
11004
11181
  return {
11005
11182
  bounds: labelBounds,
11006
- lines: this._label.getWrappedLines(ctx, Math.max(0, contentBounds.width))
11183
+ lines: this._label.getWrappedLines(ctx, Math.max(0, wrapWidth))
11007
11184
  };
11008
11185
  }
11009
11186
  /**
@@ -11016,10 +11193,10 @@ var Node = class extends Element {
11016
11193
  ctx.globalAlpha = 1;
11017
11194
  }
11018
11195
  /**
11019
- * Get world position of label center (center of content area).
11196
+ * Get world position of label center (content area or external label box).
11020
11197
  */
11021
11198
  getLabelPosition() {
11022
- const bounds = this.getLabelContainerBounds(this.getBounds());
11199
+ const bounds = this.getResolvedLabelBounds();
11023
11200
  return {
11024
11201
  x: bounds.x + bounds.width / 2,
11025
11202
  y: bounds.y + bounds.height / 2
@@ -11034,13 +11211,14 @@ var Node = class extends Element {
11034
11211
  let bounds = this.getBounds();
11035
11212
  this.renderBadges(ctx, bounds);
11036
11213
  const iconBoxSize = this._icon ? this.getIconBoxSize() : void 0;
11037
- 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));
11038
11216
  const labelSize = this._label ? this._label.measure(ctx) : void 0;
11039
- if (labelSize || iconBoxSize) {
11217
+ if ((labelSize || iconBoxSize) && !externalLabel) {
11040
11218
  this.ensureContentsFit(labelSize, iconBoxSize);
11041
11219
  if (this._label && iconBoxSize) {
11042
11220
  bounds = this.getBounds();
11043
- this._label.setAutoMaxWidth(this.getLabelContainerBounds(bounds).width);
11221
+ this._label.setAutoMaxWidth(this.getLabelWrapWidth(bounds));
11044
11222
  }
11045
11223
  }
11046
11224
  const { iconBounds, labelBounds } = this.calculateContentLayout(this.getBounds(), iconBoxSize, labelSize);
@@ -11117,10 +11295,10 @@ var Node = class extends Element {
11117
11295
  return this.calculateContentMinSize(labelSize, iconBoxSize, bounds);
11118
11296
  }
11119
11297
  /**
11120
- * Render resize handles when selected
11298
+ * Render the selection frame when selected, and resize handles when they are enabled.
11121
11299
  */
11122
11300
  renderResizeHandles(ctx) {
11123
- if (this._state !== "selected" || !this._resizeHandlesEnabled) return;
11301
+ if (this._state !== "selected") return;
11124
11302
  const bounds = this.getBounds();
11125
11303
  const offset = 6;
11126
11304
  const size = 8;
@@ -11130,6 +11308,10 @@ var Node = class extends Element {
11130
11308
  ctx.lineWidth = 1;
11131
11309
  ctx.setLineDash([4, 4]);
11132
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
+ }
11133
11315
  ctx.fillStyle = "#ffffff";
11134
11316
  ctx.strokeStyle = DEFAULT_SELECTION_COLOR;
11135
11317
  ctx.lineWidth = 1.5;
@@ -11146,7 +11328,7 @@ var Node = class extends Element {
11146
11328
  * Hit test a resize handle
11147
11329
  */
11148
11330
  hitTestResizeHandle(point) {
11149
- if (this._state !== "selected") return null;
11331
+ if (this._state !== "selected" || !this._resizeHandlesEnabled) return null;
11150
11332
  const half = 8 / 2;
11151
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;
11152
11334
  return null;
@@ -11157,7 +11339,25 @@ var Node = class extends Element {
11157
11339
  hitTest(point) {
11158
11340
  const bounds = this.getBounds();
11159
11341
  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;
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);
11161
11361
  }
11162
11362
  /**
11163
11363
  * Get a connection point on the node boundary toward a target point,
@@ -11391,16 +11591,28 @@ var Node = class extends Element {
11391
11591
  }
11392
11592
  calculateContentMinSize(labelSize, iconBoxSize, bounds) {
11393
11593
  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);
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;
11398
11599
  return {
11399
- width: minContentWidth * widthFactor,
11400
- 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)
11401
11602
  };
11402
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
+ }
11403
11612
  getLabelContainerBounds(bounds) {
11613
+ return this.applyContentInset(bounds);
11614
+ }
11615
+ applyContentInset(bounds) {
11404
11616
  const { top, right, bottom, left } = resolveContentInset(this._contentInset, this._contentInsetScale, {
11405
11617
  width: bounds.width,
11406
11618
  height: bounds.height
@@ -11797,7 +12009,7 @@ var CircleNode = class extends Node {
11797
12009
  const ry = this._height / 2 + padding;
11798
12010
  const dx = point.x - center.x;
11799
12011
  const dy = point.y - center.y;
11800
- return dx * dx / (rx * rx) + dy * dy / (ry * ry) <= 1;
12012
+ return dx * dx / (rx * rx) + dy * dy / (ry * ry) <= 1 || this.hitTestExternalLabel(point);
11801
12013
  }
11802
12014
  getOutlineDirection(param) {
11803
12015
  return this.outlineDirectionFromCenter(param);
@@ -11886,7 +12098,7 @@ var DiamondNode = class extends Node {
11886
12098
  const hh = this._height / 2 + padding;
11887
12099
  const dx = Math.abs(point.x - center.x);
11888
12100
  const dy = Math.abs(point.y - center.y);
11889
- return dx / hw + dy / hh <= 1;
12101
+ return dx / hw + dy / hh <= 1 || this.hitTestExternalLabel(point);
11890
12102
  }
11891
12103
  getOutlineDirection(param) {
11892
12104
  return this.outlineDirectionFromCenter(param);
@@ -12287,7 +12499,7 @@ var CustomShapeNode = class extends Node {
12287
12499
  const path = this.getPath();
12288
12500
  if (ctx.isPointInPath(path, localX, localY)) return true;
12289
12501
  ctx.lineWidth = 20;
12290
- return ctx.isPointInStroke(path, localX, localY);
12502
+ return ctx.isPointInStroke(path, localX, localY) || this.hitTestExternalLabel(point);
12291
12503
  }
12292
12504
  render(ctx) {
12293
12505
  const style = this.style;
@@ -13117,6 +13329,9 @@ function isCContainer(component) {
13117
13329
  function isCShape(component) {
13118
13330
  return component.type === "shape";
13119
13331
  }
13332
+ function isCText(component) {
13333
+ return component.type === "text";
13334
+ }
13120
13335
  /**
13121
13336
  * Flex container component for CompositeNode.
13122
13337
  * Lays out children using a flexbox-like algorithm.
@@ -13127,6 +13342,8 @@ var CContainer = class extends CompositeComponentBase {
13127
13342
  this.type = "container";
13128
13343
  this._cachedBounds = null;
13129
13344
  this._cachedContainerBounds = null;
13345
+ this._layoutChildren = null;
13346
+ this._suppressBoundName = false;
13130
13347
  this.id = options.id;
13131
13348
  this._direction = options.direction ?? "column";
13132
13349
  this._justifyContent = options.justifyContent ?? "start";
@@ -13155,6 +13372,27 @@ var CContainer = class extends CompositeComponentBase {
13155
13372
  get children() {
13156
13373
  return this._children;
13157
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
+ }
13158
13396
  addChild(child) {
13159
13397
  child.setOnChange(() => this.handleChildChange());
13160
13398
  this._children.push(child);
@@ -13186,8 +13424,8 @@ var CContainer = class extends CompositeComponentBase {
13186
13424
  padding: this._padding
13187
13425
  };
13188
13426
  }
13189
- buildFlexChildren(ctx) {
13190
- return this._children.map((child) => {
13427
+ buildFlexChildren(ctx, children = this.getLayoutChildren()) {
13428
+ return children.map((child) => {
13191
13429
  const s = child.style;
13192
13430
  return {
13193
13431
  measure: child.measure(ctx),
@@ -13220,11 +13458,13 @@ var CContainer = class extends CompositeComponentBase {
13220
13458
  }
13221
13459
  render(ctx, bounds) {
13222
13460
  if (this.style.visible === false) return;
13223
- const flexChildren = this.buildFlexChildren(ctx);
13461
+ const layoutChildren = this.getLayoutChildren();
13462
+ const flexChildren = this.buildFlexChildren(ctx, layoutChildren);
13224
13463
  const result = flexLayout({
13225
13464
  width: bounds.width,
13226
13465
  height: bounds.height
13227
13466
  }, this.getFlexConfig(), flexChildren);
13467
+ this._layoutChildren = layoutChildren;
13228
13468
  this._cachedBounds = result.childBounds;
13229
13469
  this._cachedContainerBounds = bounds;
13230
13470
  const opacity = this.style.opacity ?? 1;
@@ -13232,9 +13472,8 @@ var CContainer = class extends CompositeComponentBase {
13232
13472
  ctx.save();
13233
13473
  ctx.globalAlpha *= opacity;
13234
13474
  }
13235
- for (let i = 0; i < this._children.length; i++) {
13236
- const child = this._children[i];
13237
- if (child.style.visible === false) continue;
13475
+ for (let i = 0; i < layoutChildren.length; i++) {
13476
+ const child = layoutChildren[i];
13238
13477
  const cb = result.childBounds[i];
13239
13478
  child.render(ctx, {
13240
13479
  x: bounds.x + cb.x,
@@ -13247,10 +13486,9 @@ var CContainer = class extends CompositeComponentBase {
13247
13486
  }
13248
13487
  hitTest(point, bounds) {
13249
13488
  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;
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];
13254
13492
  const cb = this._cachedBounds[i];
13255
13493
  const absBounds = {
13256
13494
  x: bounds.x + cb.x,
@@ -13264,6 +13502,22 @@ var CContainer = class extends CompositeComponentBase {
13264
13502
  return null;
13265
13503
  }
13266
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
+ /**
13267
13521
  * Find a component by id recursively.
13268
13522
  */
13269
13523
  findById(id) {
@@ -13300,8 +13554,8 @@ var CContainer = class extends CompositeComponentBase {
13300
13554
  if (this.style.visible === false) return "";
13301
13555
  const cachedBounds = this._cachedBounds;
13302
13556
  const cachedContainer = this._cachedContainerBounds;
13303
- return `<g>${this._children.map((child, i) => {
13304
- if (child.style.visible === false) return "";
13557
+ const layoutChildren = this._layoutChildren ?? this.getLayoutChildren();
13558
+ return `<g>${layoutChildren.map((child, i) => {
13305
13559
  if (cachedBounds && cachedContainer) {
13306
13560
  const cb = cachedBounds[i];
13307
13561
  const dx = bounds.x - cachedContainer.x;
@@ -13313,7 +13567,7 @@ var CContainer = class extends CompositeComponentBase {
13313
13567
  height: cb.height
13314
13568
  });
13315
13569
  }
13316
- const h = bounds.height / Math.max(1, this._children.length);
13570
+ const h = bounds.height / Math.max(1, layoutChildren.length);
13317
13571
  return child.toSVG({
13318
13572
  x: bounds.x,
13319
13573
  y: bounds.y + i * h,
@@ -13620,8 +13874,8 @@ var CompositeNode = class extends Node {
13620
13874
  return this.getLabelContainerBounds(this.getBounds());
13621
13875
  }
13622
13876
  hitTest(point) {
13623
- if (this._shapeType === "circle") return this.hitTestEllipse(point);
13624
- 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);
13625
13879
  return super.hitTest(point);
13626
13880
  }
13627
13881
  hitTestEllipse(point) {
@@ -13642,7 +13896,27 @@ var CompositeNode = class extends Node {
13642
13896
  const dy = Math.abs(point.y - center.y);
13643
13897
  return dx / hw + dy / hh <= 1;
13644
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
+ }
13645
13917
  render(ctx) {
13918
+ this._content.setSuppressBoundName(this.usesExternalLabel());
13919
+ this.syncExternalBoundNameLabelStyle();
13646
13920
  if (this._autoSize) this.applyAutoSize(ctx);
13647
13921
  const { x, y, width, height } = this.getBounds();
13648
13922
  const style = this.style;
@@ -13715,6 +13989,11 @@ var CompositeNode = class extends Node {
13715
13989
  this.renderBadges(ctx, bounds);
13716
13990
  const contentBounds = this.getContentBounds();
13717
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
+ }
13718
13997
  this.renderPorts(ctx);
13719
13998
  }
13720
13999
  applyAutoSize(ctx) {
@@ -14348,6 +14627,8 @@ var Serializer = class {
14348
14627
  styleClass: node.styleClass,
14349
14628
  label,
14350
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,
14351
14632
  icon,
14352
14633
  contentInset: hasContentInset ? contentInset : void 0,
14353
14634
  anchorPoints,
@@ -14654,9 +14935,16 @@ var SvgExporter = class {
14654
14935
  break;
14655
14936
  }
14656
14937
  }
14938
+ cn.content.setSuppressBoundName(cn.usesExternalLabel());
14939
+ cn.syncExternalBoundNameLabelStyle();
14657
14940
  const contentBounds = cn.getLabelContainerBounds(bounds);
14658
14941
  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("");
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("");
14660
14948
  }
14661
14949
  default: shape = `<rect x="${bounds.x}" y="${bounds.y}" width="${bounds.width}" height="${bounds.height}"`;
14662
14950
  }
@@ -14729,15 +15017,13 @@ var SvgExporter = class {
14729
15017
  }
14730
15018
  measureTextLabel(text, style = {}, inset = 8) {
14731
15019
  const fontSize = style.fontSize ?? 14;
14732
- const fontFamily = style.fontFamily ?? "sans-serif";
14733
- const fontWeight = style.fontWeight ?? "normal";
14734
15020
  const lineHeight = fontSize * 1.2;
14735
15021
  const lines = text.split("\n");
14736
15022
  let maxWidth = 0;
14737
15023
  if (typeof document !== "undefined") {
14738
15024
  const ctx = document.createElement("canvas").getContext("2d");
14739
15025
  if (ctx) {
14740
- ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
15026
+ ctx.font = canvasFontFromTextStyle(style);
14741
15027
  for (const line of lines) maxWidth = Math.max(maxWidth, ctx.measureText(line).width);
14742
15028
  }
14743
15029
  }
@@ -14848,16 +15134,18 @@ var SvgExporter = class {
14848
15134
  const fontSize = style.fontSize ?? 14;
14849
15135
  const fontFamily = style.fontFamily ?? "sans-serif";
14850
15136
  const fontWeight = style.fontWeight ?? "normal";
15137
+ const fontStyle = style.fontStyle ?? "normal";
15138
+ const fontStyleAttr = fontStyle !== "normal" ? ` font-style="${fontStyle}"` : "";
14851
15139
  const opacity = style.opacity ?? 1;
14852
15140
  const anchor = style.align === "left" ? "start" : style.align === "right" ? "end" : "middle";
14853
15141
  const baseline = style.baseline === "top" ? "text-before-edge" : style.baseline === "bottom" ? "text-after-edge" : "middle";
14854
15142
  const transform = rotation !== 0 ? ` transform="rotate(${rotation * 180 / Math.PI} ${point.x} ${point.y})"` : "";
14855
15143
  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>`;
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>`;
14857
15145
  const lineHeight = fontSize * 1.2;
14858
15146
  const startY = point.y - (lines.length - 1) * lineHeight / 2;
14859
15147
  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>`;
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>`;
14861
15149
  }
14862
15150
  buildEdgePath(edge, edgeLabelOffset) {
14863
15151
  if (!edge.label || !edge.labelLineGap) return this.buildPath(edge);
@@ -15572,6 +15860,6 @@ function applyEdgeStyle(ctx, style) {
15572
15860
  return baseOpacity * strokeOpacity;
15573
15861
  }
15574
15862
  //#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 };
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 };
15576
15864
 
15577
15865
  //# sourceMappingURL=papirus.js.map