@ngroznykh/papirus 0.3.22 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/papirus.js CHANGED
@@ -457,7 +457,6 @@ const MARKER_SIZES = {
457
457
  diamond: 14,
458
458
  circle: 6
459
459
  };
460
- const EDGE_LABEL_BACKGROUND_PADDING = 4;
461
460
  const EDGE_LABEL_BACKGROUND_RADIUS = 2;
462
461
  const RESIZE_HANDLE_SIZE = 8;
463
462
  const RESIZE_HANDLE_OFFSET = 6;
@@ -2600,6 +2599,23 @@ function applyStyleManagerToElements(styleManager, groups, edges, nodes) {
2600
2599
  node.applyStyleManager(styleManager);
2601
2600
  }
2602
2601
  }
2602
+ const DEFAULT_INSET = 8;
2603
+ function normalizeTextInset(value, fallback) {
2604
+ const n = (v) => v !== void 0 && Number.isFinite(v) ? Math.max(0, v) : fallback;
2605
+ if (value === void 0) {
2606
+ return { top: fallback, right: fallback, bottom: fallback, left: fallback };
2607
+ }
2608
+ if (typeof value === "number") {
2609
+ const v = n(value);
2610
+ return { top: v, right: v, bottom: v, left: v };
2611
+ }
2612
+ return {
2613
+ top: n(value.top),
2614
+ right: n(value.right),
2615
+ bottom: n(value.bottom),
2616
+ left: n(value.left)
2617
+ };
2618
+ }
2603
2619
  const DEFAULT_STYLE = {
2604
2620
  font: "14px sans-serif",
2605
2621
  fontSize: 14,
@@ -2608,7 +2624,8 @@ const DEFAULT_STYLE = {
2608
2624
  color: "#000000",
2609
2625
  opacity: 1,
2610
2626
  align: "center",
2611
- baseline: "middle"
2627
+ baseline: "middle",
2628
+ verticalAlign: "middle"
2612
2629
  };
2613
2630
  class TextLabel {
2614
2631
  constructor(options) {
@@ -2621,8 +2638,10 @@ class TextLabel {
2621
2638
  this._localStyle = { ...options.style };
2622
2639
  this._style = { ...DEFAULT_STYLE, ...options.style };
2623
2640
  this._maxWidth = options.maxWidth;
2624
- this._padding = options.padding ?? 8;
2625
- this._margin = Number.isFinite(options.margin) ? Math.max(0, options.margin ?? 0) : 0;
2641
+ this._inset = normalizeTextInset(
2642
+ options.inset ?? options.margin ?? options.padding,
2643
+ DEFAULT_INSET
2644
+ );
2626
2645
  this._styleClass = options.styleClass;
2627
2646
  this._onChange = options.onChange;
2628
2647
  }
@@ -2667,6 +2686,12 @@ class TextLabel {
2667
2686
  this._measureDirty = true;
2668
2687
  this._onChange?.();
2669
2688
  }
2689
+ /**
2690
+ * Raw text style overrides (without StyleManager merge)
2691
+ */
2692
+ get styleOverrides() {
2693
+ return { ...this._localStyle };
2694
+ }
2670
2695
  /**
2671
2696
  * Style class name for StyleManager
2672
2697
  */
@@ -2708,30 +2733,15 @@ class TextLabel {
2708
2733
  this._measureDirty = true;
2709
2734
  }
2710
2735
  /**
2711
- * Label padding
2736
+ * Inset from bounds edge to text (per side: top, right, bottom, left)
2712
2737
  */
2713
- get padding() {
2714
- return this._padding;
2738
+ get inset() {
2739
+ return { ...this._inset };
2715
2740
  }
2716
- set padding(value) {
2717
- const next = Number.isFinite(value) ? Math.max(0, value) : this._padding;
2718
- if (this._padding !== next) {
2719
- this._padding = next;
2720
- this._lines = [];
2721
- this._measureDirty = true;
2722
- this._onChange?.();
2723
- }
2724
- }
2725
- /**
2726
- * Label margin
2727
- */
2728
- get margin() {
2729
- return this._margin;
2730
- }
2731
- set margin(value) {
2732
- const next = Number.isFinite(value) ? Math.max(0, value) : this._margin;
2733
- if (this._margin !== next) {
2734
- this._margin = next;
2741
+ set inset(value) {
2742
+ const next = normalizeTextInset(value, DEFAULT_INSET);
2743
+ if (this._inset.top !== next.top || this._inset.right !== next.right || this._inset.bottom !== next.bottom || this._inset.left !== next.left) {
2744
+ this._inset = next;
2735
2745
  this._lines = [];
2736
2746
  this._measureDirty = true;
2737
2747
  this._onChange?.();
@@ -2764,7 +2774,7 @@ class TextLabel {
2764
2774
  }
2765
2775
  /**
2766
2776
  * Get wrapped lines for export. Uses ctx to measure and wrap by words.
2767
- * Call with maxWidth = inner bounds width (e.g. bounds.width - margin*2).
2777
+ * Call with maxWidth = inner bounds width (e.g. bounds.width - inset*2).
2768
2778
  */
2769
2779
  getWrappedLines(ctx, maxWidth) {
2770
2780
  this.setAutoMaxWidth(maxWidth);
@@ -2786,8 +2796,11 @@ class TextLabel {
2786
2796
  const effectiveMaxWidth = this._maxWidth ?? this._autoMaxWidth;
2787
2797
  const text = this._text ?? "";
2788
2798
  if (effectiveMaxWidth !== void 0) {
2789
- const maxWidth = Math.max(0, effectiveMaxWidth - this._margin * 2);
2790
- this._lines = this.wrapText(ctx, text, Math.max(0, maxWidth - this._padding * 2));
2799
+ const maxWidth = Math.max(
2800
+ 0,
2801
+ effectiveMaxWidth - this._inset.left - this._inset.right
2802
+ );
2803
+ this._lines = this.wrapText(ctx, text, maxWidth);
2791
2804
  } else {
2792
2805
  this._lines = text.split("\n");
2793
2806
  }
@@ -2796,8 +2809,8 @@ class TextLabel {
2796
2809
  const metrics = ctx.measureText(line);
2797
2810
  maxLineWidth = Math.max(maxLineWidth, metrics.width);
2798
2811
  }
2799
- this._measuredWidth = maxLineWidth + this._padding * 2 + this._margin * 2;
2800
- this._measuredHeight = this._lines.length * lineHeight + this._padding * 2 + this._margin * 2;
2812
+ this._measuredWidth = maxLineWidth + this._inset.left + this._inset.right;
2813
+ this._measuredHeight = this._lines.length * lineHeight + this._inset.top + this._inset.bottom;
2801
2814
  this._measureDirty = false;
2802
2815
  return {
2803
2816
  width: this._measuredWidth,
@@ -2818,25 +2831,41 @@ class TextLabel {
2818
2831
  }
2819
2832
  const lineHeight = (this._style.fontSize ?? 14) * 1.2;
2820
2833
  const totalHeight = this._lines.length * lineHeight;
2821
- const margin = this._margin;
2822
2834
  const innerBounds = {
2823
- x: bounds.x + margin,
2824
- y: bounds.y + margin,
2825
- width: Math.max(0, bounds.width - margin * 2),
2826
- height: Math.max(0, bounds.height - margin * 2)
2835
+ x: bounds.x + this._inset.left,
2836
+ y: bounds.y + this._inset.top,
2837
+ width: Math.max(
2838
+ 0,
2839
+ bounds.width - this._inset.left - this._inset.right
2840
+ ),
2841
+ height: Math.max(
2842
+ 0,
2843
+ bounds.height - this._inset.top - this._inset.bottom
2844
+ )
2827
2845
  };
2828
2846
  let x;
2829
2847
  switch (align) {
2830
2848
  case "left":
2831
- x = innerBounds.x + this._padding;
2849
+ x = innerBounds.x;
2832
2850
  break;
2833
2851
  case "right":
2834
- x = innerBounds.x + innerBounds.width - this._padding;
2852
+ x = innerBounds.x + innerBounds.width;
2835
2853
  break;
2836
2854
  default:
2837
2855
  x = innerBounds.x + innerBounds.width / 2;
2838
2856
  }
2839
- const startY = innerBounds.y + (innerBounds.height - totalHeight) / 2 + lineHeight / 2;
2857
+ const verticalAlign = this._style.verticalAlign ?? "middle";
2858
+ let startY;
2859
+ switch (verticalAlign) {
2860
+ case "top":
2861
+ startY = innerBounds.y + lineHeight / 2;
2862
+ break;
2863
+ case "bottom":
2864
+ startY = innerBounds.y + innerBounds.height - totalHeight + lineHeight / 2;
2865
+ break;
2866
+ default:
2867
+ startY = innerBounds.y + (innerBounds.height - totalHeight) / 2 + lineHeight / 2;
2868
+ }
2840
2869
  ctx.fillStyle = this._style.color ?? "#000000";
2841
2870
  for (let i = 0; i < this._lines.length; i++) {
2842
2871
  const line = this._lines[i];
@@ -2851,16 +2880,13 @@ class TextLabel {
2851
2880
  if (this._lines.length === 0) {
2852
2881
  this.measure(ctx);
2853
2882
  }
2854
- this.applyStyle(ctx);
2855
- const lineHeight = (this._style.fontSize ?? 14) * 1.2;
2856
- const totalHeight = this._lines.length * lineHeight;
2857
- const startY = point.y - totalHeight / 2 + lineHeight / 2;
2858
- ctx.fillStyle = this._style.color ?? "#000000";
2859
- for (let i = 0; i < this._lines.length; i++) {
2860
- const line = this._lines[i];
2861
- const y = startY + i * lineHeight;
2862
- ctx.fillText(line, point.x, y);
2863
- }
2883
+ const bounds = {
2884
+ x: point.x - this._measuredWidth / 2,
2885
+ y: point.y - this._measuredHeight / 2,
2886
+ width: this._measuredWidth,
2887
+ height: this._measuredHeight
2888
+ };
2889
+ this.render(ctx, bounds);
2864
2890
  }
2865
2891
  applyStyle(ctx) {
2866
2892
  const fontSize = this._style.fontSize ?? 14;
@@ -2906,7 +2932,9 @@ const snapshotLabel = (label) => {
2906
2932
  return {
2907
2933
  text: label.text,
2908
2934
  editableText: label.editableText,
2909
- style: cloneValue(label.style),
2935
+ // Keep only local text overrides; computed style contains theme/class values
2936
+ // and would overwrite class text color on debounced history replay.
2937
+ style: cloneValue(label.styleOverrides),
2910
2938
  styleClass: label.styleClass,
2911
2939
  maxWidth: label.maxWidth
2912
2940
  };
@@ -4732,14 +4760,13 @@ class Edge extends Element {
4732
4760
  this._label.measure(ctx);
4733
4761
  const labelWidth = this._label.measuredWidth;
4734
4762
  const labelHeight = this._label.measuredHeight;
4735
- const bgPadding = this._labelBackground?.padding ?? EDGE_LABEL_BACKGROUND_PADDING;
4736
4763
  const bgColor = this._labelBackground?.color ?? "#ffffff";
4737
4764
  const bgOpacity = this._labelBackground?.opacity ?? 1;
4738
4765
  const bgRadius = this._labelBackground?.borderRadius ?? EDGE_LABEL_BACKGROUND_RADIUS;
4739
- const bgX = labelPosition.x - labelWidth / 2 - bgPadding;
4740
- const bgY = labelPosition.y - labelHeight / 2 - bgPadding;
4741
- const bgWidth = labelWidth + bgPadding * 2;
4742
- const bgHeight = labelHeight + bgPadding * 2;
4766
+ const bgX = labelPosition.x - labelWidth / 2;
4767
+ const bgY = labelPosition.y - labelHeight / 2;
4768
+ const bgWidth = labelWidth;
4769
+ const bgHeight = labelHeight;
4743
4770
  ctx.fillStyle = bgColor;
4744
4771
  ctx.globalAlpha = bgOpacity;
4745
4772
  if (bgRadius > 0) {
@@ -5044,6 +5071,7 @@ class InteractionManager {
5044
5071
  if (shallowEqual(before, after)) {
5045
5072
  return;
5046
5073
  }
5074
+ this.renderer.markStyleDirty();
5047
5075
  this.queuePropertyChange("node", nodeId, before, after);
5048
5076
  }
5049
5077
  changeEdgeProperties(edgeId, apply) {
@@ -5057,6 +5085,7 @@ class InteractionManager {
5057
5085
  if (shallowEqual(before, after)) {
5058
5086
  return;
5059
5087
  }
5088
+ this.renderer.markStyleDirty();
5060
5089
  this.queuePropertyChange("edge", edgeId, before, after);
5061
5090
  }
5062
5091
  changeGroupProperties(groupId, apply) {
@@ -5070,6 +5099,7 @@ class InteractionManager {
5070
5099
  if (shallowEqual(before, after)) {
5071
5100
  return;
5072
5101
  }
5102
+ this.renderer.markStyleDirty();
5073
5103
  this.queuePropertyChange("group", groupId, before, after);
5074
5104
  }
5075
5105
  removeNodeFromGroups(nodeId, groupIds) {
@@ -5560,6 +5590,7 @@ class InteractionManager {
5560
5590
  pending.after
5561
5591
  )
5562
5592
  );
5593
+ this.renderer.markStyleDirty();
5563
5594
  break;
5564
5595
  case "edge":
5565
5596
  this.historyManager.execute(
@@ -5570,6 +5601,7 @@ class InteractionManager {
5570
5601
  pending.after
5571
5602
  )
5572
5603
  );
5604
+ this.renderer.markStyleDirty();
5573
5605
  break;
5574
5606
  case "group":
5575
5607
  this.historyManager.execute(
@@ -5580,6 +5612,7 @@ class InteractionManager {
5580
5612
  pending.after
5581
5613
  )
5582
5614
  );
5615
+ this.renderer.markStyleDirty();
5583
5616
  break;
5584
5617
  }
5585
5618
  }
@@ -8045,8 +8078,10 @@ class NodeImage {
8045
8078
  get placement() {
8046
8079
  return this._options.placement ?? "center";
8047
8080
  }
8048
- get gap() {
8049
- return this._options.gap ?? 6;
8081
+ /** Inset from edge of icon zone to image (single value for all sides) */
8082
+ get inset() {
8083
+ const v = this._options.inset ?? this._options.margin ?? this._options.padding;
8084
+ return v !== void 0 && Number.isFinite(v) ? Math.max(0, v) : 6;
8050
8085
  }
8051
8086
  setSource(source) {
8052
8087
  this._loaded = false;
@@ -8061,23 +8096,18 @@ class NodeImage {
8061
8096
  if (!this._loaded) {
8062
8097
  return;
8063
8098
  }
8064
- const padding = this._options.padding ?? 8;
8065
- const margin = Math.max(0, this._options.margin ?? 0);
8099
+ const ins = this.inset;
8066
8100
  const fit = this._options.fit ?? "none";
8067
8101
  const scaleWithBounds = this._options.scaleWithBounds ?? false;
8068
8102
  const opacity = this._options.opacity ?? 1;
8069
- const align = this._options.align ?? "center";
8070
- const verticalAlign = this._options.verticalAlign ?? "center";
8071
- const offsetX = this._options.offsetX ?? 0;
8072
- const offsetY = this._options.offsetY ?? 0;
8073
8103
  const innerBounds = {
8074
- x: bounds.x + margin,
8075
- y: bounds.y + margin,
8076
- width: Math.max(0, bounds.width - margin * 2),
8077
- height: Math.max(0, bounds.height - margin * 2)
8104
+ x: bounds.x + ins,
8105
+ y: bounds.y + ins,
8106
+ width: Math.max(0, bounds.width - ins * 2),
8107
+ height: Math.max(0, bounds.height - ins * 2)
8078
8108
  };
8079
- const availableWidth = Math.max(0, innerBounds.width - padding * 2);
8080
- const availableHeight = Math.max(0, innerBounds.height - padding * 2);
8109
+ const availableWidth = Math.max(0, innerBounds.width);
8110
+ const availableHeight = Math.max(0, innerBounds.height);
8081
8111
  let drawWidth = this._options.width ?? this._naturalWidth;
8082
8112
  let drawHeight = this._options.height ?? this._naturalHeight;
8083
8113
  if (scaleWithBounds) {
@@ -8096,21 +8126,13 @@ class NodeImage {
8096
8126
  const maxHeight = Math.max(0, availableHeight);
8097
8127
  drawWidth = Math.min(drawWidth, maxWidth);
8098
8128
  drawHeight = Math.min(drawHeight, maxHeight);
8099
- let x = innerBounds.x + padding;
8100
- let y = innerBounds.y + padding;
8101
- if (align === "center") {
8102
- x = innerBounds.x + (innerBounds.width - drawWidth) / 2;
8103
- } else if (align === "right") {
8104
- x = innerBounds.x + innerBounds.width - drawWidth - padding;
8105
- }
8106
- if (verticalAlign === "center") {
8107
- y = innerBounds.y + (innerBounds.height - drawHeight) / 2;
8108
- } else if (verticalAlign === "bottom") {
8109
- y = innerBounds.y + innerBounds.height - drawHeight - padding;
8110
- }
8129
+ let x = innerBounds.x;
8130
+ let y = innerBounds.y;
8131
+ x = innerBounds.x + (innerBounds.width - drawWidth) / 2;
8132
+ y = innerBounds.y + (innerBounds.height - drawHeight) / 2;
8111
8133
  ctx.save();
8112
8134
  ctx.globalAlpha = opacity;
8113
- ctx.drawImage(this._image, x + offsetX, y + offsetY, drawWidth, drawHeight);
8135
+ ctx.drawImage(this._image, x, y, drawWidth, drawHeight);
8114
8136
  ctx.restore();
8115
8137
  }
8116
8138
  getSize() {
@@ -8200,8 +8222,8 @@ class Node extends Element {
8200
8222
  this._nodeStyle = { ...DEFAULT_NODE_STYLE, ...options.style };
8201
8223
  this._showPortsAlways = options.showPortsAlways ?? false;
8202
8224
  this._anchorPoints = this.normalizeAnchorPoints(options.anchorPoints);
8203
- this._labelPlacement = options.labelPlacement ?? "auto";
8204
8225
  this._resizeHandlesEnabled = options.resizeHandlesEnabled ?? true;
8226
+ this._contentInset = this.normalizeContentInset(options.contentInset);
8205
8227
  if (options.label !== void 0) {
8206
8228
  if (typeof options.label === "string") {
8207
8229
  this._label = new TextLabel({
@@ -8398,24 +8420,22 @@ class Node extends Element {
8398
8420
  this._anchorPoints = this.normalizeAnchorPoints(value);
8399
8421
  this.markDirty();
8400
8422
  }
8401
- /**
8402
- * Label placement inside the node
8403
- */
8404
- get labelPlacement() {
8405
- return this._labelPlacement;
8406
- }
8407
- set labelPlacement(value) {
8408
- if (this._labelPlacement !== value) {
8409
- this._labelPlacement = value;
8410
- this.markDirty();
8411
- }
8412
- }
8413
8423
  /**
8414
8424
  * Default size (initial width/height)
8415
8425
  */
8416
8426
  get defaultSize() {
8417
8427
  return { ...this._defaultSize };
8418
8428
  }
8429
+ /**
8430
+ * Content area insets (per side). When all zero, content area = node bounds.
8431
+ */
8432
+ get contentInset() {
8433
+ return { ...this._contentInset };
8434
+ }
8435
+ set contentInset(value) {
8436
+ this._contentInset = this.normalizeContentInset(value);
8437
+ this.markDirty();
8438
+ }
8419
8439
  /**
8420
8440
  * Set getter for attachToOutline (called by DiagramRenderer when node is added)
8421
8441
  */
@@ -8438,87 +8458,20 @@ class Node extends Element {
8438
8458
  }
8439
8459
  }
8440
8460
  /**
8441
- * Calculate layout for icon and label
8442
- * Returns computed bounds for both elements
8461
+ * Calculate layout for icon and label.
8462
+ * Label always gets full content area; icon is placed within the same content area.
8443
8463
  */
8444
- calculateContentLayout(bounds, iconBoxSize, labelSize) {
8445
- let iconBounds = bounds;
8446
- let labelBounds = this.getLabelContainerBounds(bounds);
8464
+ calculateContentLayout(bounds, iconBoxSize, _labelSize) {
8465
+ const contentBounds = this.getLabelContainerBounds(bounds);
8466
+ let iconBounds = contentBounds;
8447
8467
  if (this._icon && iconBoxSize) {
8448
- iconBounds = this.getIconBounds(bounds, iconBoxSize, this._icon.placement);
8449
- }
8450
- if (this._icon && this._label && iconBoxSize && labelSize && this._labelPlacement === "auto" && this._icon.placement !== "center") {
8451
- const gap = this._icon.gap;
8452
- const placement = this._icon.placement;
8453
- if (isCornerPlacement(placement)) {
8454
- iconBounds = this.getIconBounds(bounds, iconBoxSize, placement);
8455
- labelBounds = this.getLabelContainerBounds(bounds);
8456
- } else {
8457
- const contentBounds = this.getLabelContainerBounds(bounds);
8458
- switch (placement) {
8459
- case "top":
8460
- iconBounds = {
8461
- x: bounds.x,
8462
- y: bounds.y,
8463
- width: bounds.width,
8464
- height: iconBoxSize.height
8465
- };
8466
- labelBounds = {
8467
- x: contentBounds.x,
8468
- y: contentBounds.y + iconBoxSize.height + gap,
8469
- width: contentBounds.width,
8470
- height: Math.max(0, contentBounds.height - iconBoxSize.height - gap)
8471
- };
8472
- break;
8473
- case "bottom":
8474
- iconBounds = {
8475
- x: bounds.x,
8476
- y: bounds.y + bounds.height - iconBoxSize.height,
8477
- width: bounds.width,
8478
- height: iconBoxSize.height
8479
- };
8480
- labelBounds = {
8481
- x: contentBounds.x,
8482
- y: contentBounds.y,
8483
- width: contentBounds.width,
8484
- height: Math.max(0, contentBounds.height - iconBoxSize.height - gap)
8485
- };
8486
- break;
8487
- case "left":
8488
- iconBounds = {
8489
- x: bounds.x,
8490
- y: bounds.y,
8491
- width: iconBoxSize.width,
8492
- height: bounds.height
8493
- };
8494
- labelBounds = {
8495
- x: contentBounds.x + iconBoxSize.width + gap,
8496
- y: contentBounds.y,
8497
- width: Math.max(0, contentBounds.width - iconBoxSize.width - gap),
8498
- height: contentBounds.height
8499
- };
8500
- break;
8501
- case "right":
8502
- iconBounds = {
8503
- x: bounds.x + bounds.width - iconBoxSize.width,
8504
- y: bounds.y,
8505
- width: iconBoxSize.width,
8506
- height: bounds.height
8507
- };
8508
- labelBounds = {
8509
- x: contentBounds.x,
8510
- y: contentBounds.y,
8511
- width: Math.max(0, contentBounds.width - iconBoxSize.width - gap),
8512
- height: contentBounds.height
8513
- };
8514
- break;
8515
- }
8516
- }
8517
- } else if (this._label && labelSize) {
8518
- const autoBounds = this.getAutoLabelBounds(bounds, iconBoxSize);
8519
- labelBounds = this.getLabelBounds(autoBounds, labelSize, this._labelPlacement);
8468
+ iconBounds = this.getIconBounds(
8469
+ contentBounds,
8470
+ iconBoxSize,
8471
+ this._icon.placement
8472
+ );
8520
8473
  }
8521
- return { iconBounds, labelBounds };
8474
+ return { iconBounds, labelBounds: contentBounds };
8522
8475
  }
8523
8476
  /**
8524
8477
  * Get label bounds and wrapped lines for SVG export. Replicates renderContents layout logic.
@@ -8529,12 +8482,19 @@ class Node extends Element {
8529
8482
  return null;
8530
8483
  }
8531
8484
  const bounds = this.getBounds();
8485
+ const contentBounds = this.getLabelContainerBounds(bounds);
8532
8486
  const iconBoxSize = this._icon ? this.getIconBoxSize() : void 0;
8533
- const autoBounds = this.getAutoLabelBounds(bounds, iconBoxSize);
8534
- this._label.setAutoMaxWidth(autoBounds.width);
8487
+ this._label.setAutoMaxWidth(contentBounds.width);
8535
8488
  const labelSize = this._label.measure(ctx);
8536
- const { labelBounds } = this.calculateContentLayout(bounds, iconBoxSize, labelSize);
8537
- const lines = this._label.getWrappedLines(ctx, Math.max(0, autoBounds.width));
8489
+ const { labelBounds } = this.calculateContentLayout(
8490
+ bounds,
8491
+ iconBoxSize,
8492
+ labelSize
8493
+ );
8494
+ const lines = this._label.getWrappedLines(
8495
+ ctx,
8496
+ Math.max(0, contentBounds.width)
8497
+ );
8538
8498
  return { bounds: labelBounds, lines };
8539
8499
  }
8540
8500
  /**
@@ -8550,23 +8510,14 @@ class Node extends Element {
8550
8510
  ctx.globalAlpha = 1;
8551
8511
  }
8552
8512
  /**
8553
- * Get world position of label center.
8513
+ * Get world position of label center (center of content area).
8554
8514
  */
8555
8515
  getLabelPosition() {
8556
- const bounds = this.getBounds();
8557
- const placement = this._labelPlacement === "auto" ? "center" : this._labelPlacement;
8558
- switch (placement) {
8559
- case "top":
8560
- return { x: bounds.x + bounds.width / 2, y: bounds.y };
8561
- case "bottom":
8562
- return { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height };
8563
- case "left":
8564
- return { x: bounds.x, y: bounds.y + bounds.height / 2 };
8565
- case "right":
8566
- return { x: bounds.x + bounds.width, y: bounds.y + bounds.height / 2 };
8567
- default:
8568
- return { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 };
8569
- }
8516
+ const bounds = this.getLabelContainerBounds(this.getBounds());
8517
+ return {
8518
+ x: bounds.x + bounds.width / 2,
8519
+ y: bounds.y + bounds.height / 2
8520
+ };
8570
8521
  }
8571
8522
  /**
8572
8523
  * Render icon, label, and ports
@@ -8577,14 +8528,14 @@ class Node extends Element {
8577
8528
  let bounds = this.getBounds();
8578
8529
  const iconBoxSize = this._icon ? this.getIconBoxSize() : void 0;
8579
8530
  if (this._label) {
8580
- this._label.setAutoMaxWidth(this.getAutoLabelBounds(bounds, iconBoxSize).width);
8531
+ this._label.setAutoMaxWidth(this.getLabelContainerBounds(bounds).width);
8581
8532
  }
8582
8533
  const labelSize = this._label ? this._label.measure(ctx) : void 0;
8583
8534
  if (labelSize || iconBoxSize) {
8584
8535
  this.ensureContentsFit(labelSize, iconBoxSize);
8585
8536
  if (this._label && iconBoxSize) {
8586
8537
  bounds = this.getBounds();
8587
- this._label.setAutoMaxWidth(this.getAutoLabelBounds(bounds, iconBoxSize).width);
8538
+ this._label.setAutoMaxWidth(this.getLabelContainerBounds(bounds).width);
8588
8539
  }
8589
8540
  }
8590
8541
  const { iconBounds, labelBounds } = this.calculateContentLayout(
@@ -8869,124 +8820,45 @@ class Node extends Element {
8869
8820
  }
8870
8821
  }
8871
8822
  calculateContentMinSize(labelSize, iconBoxSize, bounds) {
8872
- let minWidth = 0;
8873
- let minHeight = 0;
8874
8823
  const labelContainer = this.getLabelContainerBounds(bounds);
8875
8824
  const widthFactor = labelContainer.width > 0 ? bounds.width / labelContainer.width : 1;
8876
8825
  const heightFactor = labelContainer.height > 0 ? bounds.height / labelContainer.height : 1;
8877
- if (labelSize) {
8878
- minWidth = Math.max(minWidth, labelSize.width * widthFactor);
8879
- minHeight = Math.max(minHeight, labelSize.height * heightFactor);
8880
- }
8881
- if (iconBoxSize) {
8882
- minWidth = Math.max(minWidth, iconBoxSize.width);
8883
- minHeight = Math.max(minHeight, iconBoxSize.height);
8884
- }
8885
- if (labelSize && iconBoxSize && this._icon) {
8886
- const gap = this._icon.gap;
8887
- const labelPlacement = this._labelPlacement === "auto" ? "center" : this._labelPlacement;
8888
- const iconPlacement = this._icon.placement;
8889
- if (this._labelPlacement === "auto" && iconPlacement !== "center") {
8890
- if (isCornerPlacement(iconPlacement)) {
8891
- minWidth = Math.max(minWidth, iconBoxSize.width, labelSize.width);
8892
- minHeight = Math.max(minHeight, iconBoxSize.height, labelSize.height);
8893
- } else if (iconPlacement === "top" || iconPlacement === "bottom") {
8894
- minHeight = Math.max(minHeight, iconBoxSize.height + gap + labelSize.height);
8895
- minWidth = Math.max(minWidth, iconBoxSize.width, labelSize.width);
8896
- } else if (iconPlacement === "left" || iconPlacement === "right") {
8897
- minWidth = Math.max(minWidth, iconBoxSize.width + gap + labelSize.width);
8898
- minHeight = Math.max(minHeight, iconBoxSize.height, labelSize.height);
8899
- }
8900
- } else {
8901
- const labelHorizontal = labelPlacement === "left" || labelPlacement === "right";
8902
- const labelVertical = labelPlacement === "top" || labelPlacement === "bottom";
8903
- const iconHorizontal = iconPlacement === "left" || iconPlacement === "right";
8904
- const iconVertical = iconPlacement === "top" || iconPlacement === "bottom";
8905
- const labelSharesIconAxis = iconHorizontal && (labelPlacement === "center" || labelPlacement === iconPlacement) || iconVertical && (labelPlacement === "center" || labelPlacement === iconPlacement);
8906
- if (labelHorizontal && iconHorizontal && labelPlacement !== iconPlacement) {
8907
- minWidth = Math.max(minWidth, iconBoxSize.width + gap + labelSize.width);
8908
- }
8909
- if (labelVertical && iconVertical && labelPlacement !== iconPlacement) {
8910
- minHeight = Math.max(minHeight, iconBoxSize.height + gap + labelSize.height);
8911
- }
8912
- if (labelSharesIconAxis) {
8913
- if (iconHorizontal) {
8914
- minWidth = Math.max(minWidth, iconBoxSize.width + gap + labelSize.width);
8915
- }
8916
- if (iconVertical) {
8917
- minHeight = Math.max(minHeight, iconBoxSize.height + gap + labelSize.height);
8918
- }
8919
- }
8920
- }
8921
- }
8922
- return { width: minWidth, height: minHeight };
8826
+ const minContentWidth = Math.max(
8827
+ labelSize?.width ?? 0,
8828
+ iconBoxSize?.width ?? 0
8829
+ );
8830
+ const minContentHeight = Math.max(
8831
+ labelSize?.height ?? 0,
8832
+ iconBoxSize?.height ?? 0
8833
+ );
8834
+ return {
8835
+ width: minContentWidth * widthFactor,
8836
+ height: minContentHeight * heightFactor
8837
+ };
8923
8838
  }
8924
8839
  getLabelContainerBounds(bounds) {
8925
- return bounds;
8840
+ const { top, right, bottom, left } = this._contentInset;
8841
+ const x = bounds.x + left;
8842
+ const y = bounds.y + top;
8843
+ const width = Math.max(0, bounds.width - left - right);
8844
+ const height = Math.max(0, bounds.height - top - bottom);
8845
+ return { x, y, width, height };
8926
8846
  }
8927
- getAutoLabelBounds(bounds, iconBoxSize) {
8928
- const contentBounds = this.getLabelContainerBounds(bounds);
8929
- if (!this._icon || !iconBoxSize || this._icon.placement === "center") {
8930
- return contentBounds;
8931
- }
8932
- const gap = this._icon.gap;
8933
- if (isCornerPlacement(this._icon.placement)) {
8934
- return contentBounds;
8935
- }
8936
- switch (this._icon.placement) {
8937
- case "left":
8938
- return {
8939
- x: contentBounds.x + iconBoxSize.width + gap,
8940
- y: contentBounds.y,
8941
- width: Math.max(0, contentBounds.width - iconBoxSize.width - gap),
8942
- height: contentBounds.height
8943
- };
8944
- case "right":
8945
- return {
8946
- x: contentBounds.x,
8947
- y: contentBounds.y,
8948
- width: Math.max(0, contentBounds.width - iconBoxSize.width - gap),
8949
- height: contentBounds.height
8950
- };
8951
- case "top":
8952
- return {
8953
- x: contentBounds.x,
8954
- y: contentBounds.y + iconBoxSize.height + gap,
8955
- width: contentBounds.width,
8956
- height: Math.max(0, contentBounds.height - iconBoxSize.height - gap)
8957
- };
8958
- case "bottom":
8959
- return {
8960
- x: contentBounds.x,
8961
- y: contentBounds.y,
8962
- width: contentBounds.width,
8963
- height: Math.max(0, contentBounds.height - iconBoxSize.height - gap)
8964
- };
8965
- default:
8966
- return contentBounds;
8847
+ normalizeContentInset(value) {
8848
+ const n = (v) => v !== void 0 && Number.isFinite(v) ? Math.max(0, v) : 0;
8849
+ if (value === void 0) {
8850
+ return { top: 0, right: 0, bottom: 0, left: 0 };
8967
8851
  }
8968
- }
8969
- getLabelBounds(bounds, labelSize, placement) {
8970
- const normalizedPlacement = placement === "auto" ? "center" : placement;
8971
- const width = Math.min(labelSize.width, bounds.width);
8972
- const height = Math.min(labelSize.height, bounds.height);
8973
- let x = bounds.x + (bounds.width - width) / 2;
8974
- let y = bounds.y + (bounds.height - height) / 2;
8975
- switch (normalizedPlacement) {
8976
- case "top":
8977
- y = bounds.y;
8978
- break;
8979
- case "bottom":
8980
- y = bounds.y + bounds.height - height;
8981
- break;
8982
- case "left":
8983
- x = bounds.x;
8984
- break;
8985
- case "right":
8986
- x = bounds.x + bounds.width - width;
8987
- break;
8852
+ if (typeof value === "number") {
8853
+ const v = n(value);
8854
+ return { top: v, right: v, bottom: v, left: v };
8988
8855
  }
8989
- return { x, y, width, height };
8856
+ return {
8857
+ top: n(value.top),
8858
+ right: n(value.right),
8859
+ bottom: n(value.bottom),
8860
+ left: n(value.left)
8861
+ };
8990
8862
  }
8991
8863
  getIconBoxSize() {
8992
8864
  if (!this._icon) {
@@ -8996,14 +8868,14 @@ class Node extends Element {
8996
8868
  if (width <= 0 || height <= 0) {
8997
8869
  return void 0;
8998
8870
  }
8999
- const padding = this._icon.options.padding ?? 8;
9000
- const margin = Math.max(0, this._icon.options.margin ?? 0);
8871
+ const ins = this._icon.inset;
9001
8872
  return {
9002
- width: width + padding * 2 + margin * 2,
9003
- height: height + padding * 2 + margin * 2
8873
+ width: width + ins * 2,
8874
+ height: height + ins * 2
9004
8875
  };
9005
8876
  }
9006
8877
  getIconBounds(bounds, iconBoxSize, placement) {
8878
+ const ins = this._icon?.inset ?? 0;
9007
8879
  switch (placement) {
9008
8880
  case "top":
9009
8881
  return {
@@ -9035,29 +8907,29 @@ class Node extends Element {
9035
8907
  };
9036
8908
  case "top-left":
9037
8909
  return {
9038
- x: bounds.x,
9039
- y: bounds.y,
8910
+ x: bounds.x + ins,
8911
+ y: bounds.y + ins,
9040
8912
  width: iconBoxSize.width,
9041
8913
  height: iconBoxSize.height
9042
8914
  };
9043
8915
  case "top-right":
9044
8916
  return {
9045
- x: bounds.x + bounds.width - iconBoxSize.width,
9046
- y: bounds.y,
8917
+ x: bounds.x + bounds.width - iconBoxSize.width - ins,
8918
+ y: bounds.y + ins,
9047
8919
  width: iconBoxSize.width,
9048
8920
  height: iconBoxSize.height
9049
8921
  };
9050
8922
  case "bottom-left":
9051
8923
  return {
9052
- x: bounds.x,
9053
- y: bounds.y + bounds.height - iconBoxSize.height,
8924
+ x: bounds.x + ins,
8925
+ y: bounds.y + bounds.height - iconBoxSize.height - ins,
9054
8926
  width: iconBoxSize.width,
9055
8927
  height: iconBoxSize.height
9056
8928
  };
9057
8929
  case "bottom-right":
9058
8930
  return {
9059
- x: bounds.x + bounds.width - iconBoxSize.width,
9060
- y: bounds.y + bounds.height - iconBoxSize.height,
8931
+ x: bounds.x + bounds.width - iconBoxSize.width - ins,
8932
+ y: bounds.y + bounds.height - iconBoxSize.height - ins,
9061
8933
  width: iconBoxSize.width,
9062
8934
  height: iconBoxSize.height
9063
8935
  };
@@ -9956,18 +9828,14 @@ const DEFAULT_THEME = {
9956
9828
  cornerRadius: 4
9957
9829
  },
9958
9830
  selected: {
9959
- fillColor: "#ffffff",
9960
- strokeColor: "#333333",
9831
+ strokeColor: "#3b82f6",
9961
9832
  strokeWidth: 2,
9962
- opacity: 1,
9963
- cornerRadius: 4
9833
+ opacity: 1
9964
9834
  },
9965
9835
  dragging: {
9966
- fillColor: "#f0f0f0",
9967
9836
  strokeColor: "#333333",
9968
9837
  strokeWidth: 2,
9969
- opacity: 0.8,
9970
- cornerRadius: 4
9838
+ opacity: 0.8
9971
9839
  }
9972
9840
  },
9973
9841
  edge: {
@@ -10228,15 +10096,16 @@ class StyleManager {
10228
10096
  return baseStyle;
10229
10097
  }
10230
10098
  getBaseNodeStyle(state) {
10099
+ const d = this._theme.node.default;
10231
10100
  switch (state) {
10232
10101
  case "hover":
10233
- return this._theme.node.hover;
10102
+ return { ...d, ...this._theme.node.hover };
10234
10103
  case "selected":
10235
- return this._theme.node.selected;
10104
+ return { ...d, ...this._theme.node.selected };
10236
10105
  case "dragging":
10237
- return this._theme.node.dragging;
10106
+ return { ...d, ...this._theme.node.dragging };
10238
10107
  default:
10239
- return this._theme.node.default;
10108
+ return d;
10240
10109
  }
10241
10110
  }
10242
10111
  getBaseEdgeStyle(state) {
@@ -10434,13 +10303,16 @@ class Serializer {
10434
10303
  }));
10435
10304
  let label;
10436
10305
  if (node.label) {
10437
- const labelDefaults = { padding: 8, margin: 0, style: {}, maxWidth: void 0, styleClass: void 0 };
10306
+ const ins = node.label.inset;
10307
+ const defaultInset = 8;
10308
+ const insetAllDefault = ins.top === defaultInset && ins.right === defaultInset && ins.bottom === defaultInset && ins.left === defaultInset;
10309
+ const serializedInset = insetAllDefault ? void 0 : ins.top === ins.right && ins.top === ins.bottom && ins.top === ins.left ? ins.top : { top: ins.top, right: ins.right, bottom: ins.bottom, left: ins.left };
10310
+ const labelDefaults = { inset: void 0, style: {}, maxWidth: void 0, styleClass: void 0 };
10438
10311
  const labelData = {
10439
10312
  text: node.label.text,
10440
10313
  style: node.label.style,
10441
10314
  maxWidth: node.label.maxWidth,
10442
- padding: node.label.padding,
10443
- margin: node.label.margin,
10315
+ inset: serializedInset ?? void 0,
10444
10316
  styleClass: node.label.styleClass
10445
10317
  };
10446
10318
  const hasExtendedOptions = hasNonDefaultValues(labelData, labelDefaults);
@@ -10449,8 +10321,7 @@ class Serializer {
10449
10321
  text: node.label.text,
10450
10322
  style: Object.keys(node.label.style).length > 0 ? node.label.style : void 0,
10451
10323
  maxWidth: node.label.maxWidth,
10452
- padding: node.label.padding !== 8 ? node.label.padding : void 0,
10453
- margin: node.label.margin !== 0 ? node.label.margin : void 0,
10324
+ inset: serializedInset,
10454
10325
  styleClass: node.label.styleClass
10455
10326
  });
10456
10327
  } else {
@@ -10462,22 +10333,16 @@ class Serializer {
10462
10333
  const opts = node.icon.options;
10463
10334
  const source = typeof opts.source === "string" ? opts.source : void 0;
10464
10335
  if (source) {
10465
- icon = {
10336
+ icon = omitEmptyValues({
10466
10337
  source,
10467
10338
  width: opts.width,
10468
10339
  height: opts.height,
10469
10340
  fit: opts.fit,
10470
10341
  placement: opts.placement,
10471
10342
  scaleWithBounds: opts.scaleWithBounds,
10472
- padding: opts.padding,
10473
- margin: opts.margin,
10474
- gap: opts.gap,
10475
- opacity: opts.opacity,
10476
- align: opts.align,
10477
- verticalAlign: opts.verticalAlign,
10478
- offsetX: opts.offsetX,
10479
- offsetY: opts.offsetY
10480
- };
10343
+ inset: node.icon.inset !== 6 ? node.icon.inset : void 0,
10344
+ opacity: opts.opacity
10345
+ });
10481
10346
  }
10482
10347
  }
10483
10348
  let anchorPoints;
@@ -10487,7 +10352,9 @@ class Serializer {
10487
10352
  if (hasNonDefaultValues(anchorData, anchorDefaults)) {
10488
10353
  anchorPoints = omitDefaultValues(anchorData, anchorDefaults);
10489
10354
  }
10490
- return {
10355
+ const contentInset = node.contentInset;
10356
+ const hasContentInset = contentInset.top !== 0 || contentInset.right !== 0 || contentInset.bottom !== 0 || contentInset.left !== 0;
10357
+ return omitEmptyValues({
10491
10358
  id: node.id,
10492
10359
  type: node.typeName,
10493
10360
  x: node.x,
@@ -10498,12 +10365,12 @@ class Serializer {
10498
10365
  styleClass: node.styleClass,
10499
10366
  label,
10500
10367
  labelStyleClass: typeof label === "string" ? node.label?.styleClass : void 0,
10501
- labelPlacement: node.labelPlacement !== "auto" ? node.labelPlacement : void 0,
10502
10368
  icon,
10369
+ contentInset: hasContentInset ? contentInset : void 0,
10503
10370
  anchorPoints,
10504
10371
  ports: ports.length > 0 ? ports : void 0,
10505
10372
  data: Object.keys(node.data).length > 0 ? node.data : void 0
10506
- };
10373
+ });
10507
10374
  }
10508
10375
  serializeEdge(edge) {
10509
10376
  return {
@@ -10814,7 +10681,7 @@ class SvgExporter {
10814
10681
  const parts = [];
10815
10682
  const renderedEdges = [];
10816
10683
  parts.push(
10817
- `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">`
10684
+ `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">`
10818
10685
  );
10819
10686
  for (const edge of this.renderer.edges.values()) {
10820
10687
  if (edge.visible) {
@@ -10955,22 +10822,37 @@ class SvgExporter {
10955
10822
  if (!edge.label) {
10956
10823
  return "";
10957
10824
  }
10958
- const metrics = this.measureTextLabel(edge.label.text, edge.label.style, edge.label.padding, edge.label.margin);
10959
- const bgPadding = edge.labelBackground?.padding ?? EDGE_LABEL_BACKGROUND_PADDING;
10825
+ const metrics = this.measureTextLabel(edge.label.text, edge.label.style, edge.label.inset);
10960
10826
  const bgColor = edge.labelBackground?.color ?? "#ffffff";
10961
10827
  const bgOpacity = edge.labelBackground?.opacity ?? 1;
10962
10828
  const bgRadius = edge.labelBackground?.borderRadius ?? EDGE_LABEL_BACKGROUND_RADIUS;
10963
- const x = point.x - metrics.width / 2 - bgPadding;
10964
- const y = point.y - metrics.height / 2 - bgPadding;
10965
- const width = metrics.width + bgPadding * 2;
10966
- const height = metrics.height + bgPadding * 2;
10829
+ const x = point.x - metrics.width / 2;
10830
+ const y = point.y - metrics.height / 2;
10831
+ const width = metrics.width;
10832
+ const height = metrics.height;
10967
10833
  const radius = Math.max(0, Math.min(bgRadius, width / 2, height / 2));
10968
10834
  if (radius <= 0) {
10969
10835
  return `<rect x="${x}" y="${y}" width="${width}" height="${height}" fill="${bgColor}" fill-opacity="${bgOpacity}"/>`;
10970
10836
  }
10971
10837
  return `<rect x="${x}" y="${y}" width="${width}" height="${height}" rx="${radius}" ry="${radius}" fill="${bgColor}" fill-opacity="${bgOpacity}"/>`;
10972
10838
  }
10973
- measureTextLabel(text, style = {}, padding = 8, margin = 0) {
10839
+ normalizeLabelInset(value, defaultVal) {
10840
+ const n = (v) => v !== void 0 && Number.isFinite(v) ? Math.max(0, v) : defaultVal;
10841
+ if (value === void 0) {
10842
+ return { top: defaultVal, right: defaultVal, bottom: defaultVal, left: defaultVal };
10843
+ }
10844
+ if (typeof value === "number") {
10845
+ const v = n(value);
10846
+ return { top: v, right: v, bottom: v, left: v };
10847
+ }
10848
+ return {
10849
+ top: n(value.top),
10850
+ right: n(value.right),
10851
+ bottom: n(value.bottom),
10852
+ left: n(value.left)
10853
+ };
10854
+ }
10855
+ measureTextLabel(text, style = {}, inset = 8) {
10974
10856
  const fontSize = style.fontSize ?? 14;
10975
10857
  const fontFamily = style.fontFamily ?? "sans-serif";
10976
10858
  const fontWeight = style.fontWeight ?? "normal";
@@ -10991,11 +10873,10 @@ class SvgExporter {
10991
10873
  const longestLine = lines.reduce((max, line) => line.length > max.length ? line : max, "");
10992
10874
  maxWidth = longestLine.length * fontSize * 0.6;
10993
10875
  }
10994
- const resolvedPadding = Math.max(0, padding);
10995
- const resolvedMargin = Math.max(0, margin);
10876
+ const ins = this.normalizeLabelInset(inset, 8);
10996
10877
  return {
10997
- width: maxWidth + resolvedPadding * 2 + resolvedMargin * 2,
10998
- height: lines.length * lineHeight + resolvedPadding * 2 + resolvedMargin * 2
10878
+ width: maxWidth + ins.left + ins.right,
10879
+ height: lines.length * lineHeight + ins.top + ins.bottom
10999
10880
  };
11000
10881
  }
11001
10882
  resolveMarkerConfig(edge, side) {
@@ -11120,9 +11001,9 @@ class SvgExporter {
11120
11001
  return "";
11121
11002
  }
11122
11003
  const style = label.style;
11123
- const padding = Math.max(0, label.padding);
11124
- const margin = Math.max(0, label.margin);
11004
+ const ins = this.normalizeLabelInset(label.inset, 8);
11125
11005
  const align = style.align ?? "center";
11006
+ const verticalAlign = style.verticalAlign ?? "middle";
11126
11007
  const ctx = this.getMeasurementContext();
11127
11008
  let bounds;
11128
11009
  let lines;
@@ -11140,18 +11021,27 @@ class SvgExporter {
11140
11021
  lines = label.text.split("\n");
11141
11022
  }
11142
11023
  const inner = {
11143
- x: bounds.x + margin,
11144
- y: bounds.y + margin,
11145
- width: Math.max(0, bounds.width - margin * 2),
11146
- height: Math.max(0, bounds.height - margin * 2)
11024
+ x: bounds.x + ins.left,
11025
+ y: bounds.y + ins.top,
11026
+ width: Math.max(0, bounds.width - ins.left - ins.right),
11027
+ height: Math.max(0, bounds.height - ins.top - ins.bottom)
11147
11028
  };
11148
11029
  let x = inner.x + inner.width / 2;
11149
11030
  if (align === "left") {
11150
- x = inner.x + padding;
11031
+ x = inner.x;
11151
11032
  } else if (align === "right") {
11152
- x = inner.x + inner.width - padding;
11033
+ x = inner.x + inner.width;
11034
+ }
11035
+ const lineHeight = (style.fontSize ?? 14) * 1.2;
11036
+ const totalHeight = lines.length * lineHeight;
11037
+ let y;
11038
+ if (verticalAlign === "top") {
11039
+ y = inner.y + totalHeight / 2;
11040
+ } else if (verticalAlign === "bottom") {
11041
+ y = inner.y + inner.height - totalHeight / 2;
11042
+ } else {
11043
+ y = inner.y + inner.height / 2;
11153
11044
  }
11154
- const y = inner.y + inner.height / 2;
11155
11045
  return this.renderTextLabel(lines.join("\n"), { x, y }, style);
11156
11046
  }
11157
11047
  getNodeCornerRadius(node, bounds) {
@@ -11168,9 +11058,15 @@ class SvgExporter {
11168
11058
  if (iconSize.width <= 0 || iconSize.height <= 0) {
11169
11059
  return "";
11170
11060
  }
11171
- const iconBoxSize = this.getIconBoxSize(opts, iconSize);
11172
- const iconBounds = this.getIconBounds(nodeBounds, iconBoxSize, opts.placement ?? "center");
11173
- const drawRect = this.getIconDrawRect(iconBounds, opts, iconSize);
11061
+ const iconInset = icon.inset;
11062
+ const iconBoxSize = this.getIconBoxSize(iconSize, iconInset);
11063
+ const iconBounds = this.getIconBounds(
11064
+ nodeBounds,
11065
+ iconBoxSize,
11066
+ opts.placement ?? "center",
11067
+ iconInset
11068
+ );
11069
+ const drawRect = this.getIconDrawRect(iconBounds, opts, iconSize, iconInset);
11174
11070
  if (drawRect.width <= 0 || drawRect.height <= 0) {
11175
11071
  return "";
11176
11072
  }
@@ -11179,17 +11075,16 @@ class SvgExporter {
11179
11075
  return "";
11180
11076
  }
11181
11077
  const opacity = opts.opacity ?? 1;
11182
- return `<image href="${this.escapeAttribute(href)}" x="${drawRect.x}" y="${drawRect.y}" width="${drawRect.width}" height="${drawRect.height}" opacity="${opacity}" preserveAspectRatio="none"/>`;
11078
+ const escapedHref = this.escapeAttribute(href);
11079
+ return `<image href="${escapedHref}" xlink:href="${escapedHref}" x="${drawRect.x}" y="${drawRect.y}" width="${drawRect.width}" height="${drawRect.height}" opacity="${opacity}" preserveAspectRatio="none"/>`;
11183
11080
  }
11184
- getIconBoxSize(opts, imageSize) {
11185
- const padding = opts.padding ?? 8;
11186
- const margin = Math.max(0, opts.margin ?? 0);
11081
+ getIconBoxSize(imageSize, inset) {
11187
11082
  return {
11188
- width: imageSize.width + padding * 2 + margin * 2,
11189
- height: imageSize.height + padding * 2 + margin * 2
11083
+ width: imageSize.width + inset * 2,
11084
+ height: imageSize.height + inset * 2
11190
11085
  };
11191
11086
  }
11192
- getIconBounds(bounds, iconBoxSize, placement) {
11087
+ getIconBounds(bounds, iconBoxSize, placement, inset) {
11193
11088
  switch (placement) {
11194
11089
  case "top":
11195
11090
  return { x: bounds.x, y: bounds.y, width: bounds.width, height: iconBoxSize.height };
@@ -11210,25 +11105,30 @@ class SvgExporter {
11210
11105
  height: bounds.height
11211
11106
  };
11212
11107
  case "top-left":
11213
- return { x: bounds.x, y: bounds.y, width: iconBoxSize.width, height: iconBoxSize.height };
11108
+ return {
11109
+ x: bounds.x + inset,
11110
+ y: bounds.y + inset,
11111
+ width: iconBoxSize.width,
11112
+ height: iconBoxSize.height
11113
+ };
11214
11114
  case "top-right":
11215
11115
  return {
11216
- x: bounds.x + bounds.width - iconBoxSize.width,
11217
- y: bounds.y,
11116
+ x: bounds.x + bounds.width - iconBoxSize.width - inset,
11117
+ y: bounds.y + inset,
11218
11118
  width: iconBoxSize.width,
11219
11119
  height: iconBoxSize.height
11220
11120
  };
11221
11121
  case "bottom-left":
11222
11122
  return {
11223
- x: bounds.x,
11224
- y: bounds.y + bounds.height - iconBoxSize.height,
11123
+ x: bounds.x + inset,
11124
+ y: bounds.y + bounds.height - iconBoxSize.height - inset,
11225
11125
  width: iconBoxSize.width,
11226
11126
  height: iconBoxSize.height
11227
11127
  };
11228
11128
  case "bottom-right":
11229
11129
  return {
11230
- x: bounds.x + bounds.width - iconBoxSize.width,
11231
- y: bounds.y + bounds.height - iconBoxSize.height,
11130
+ x: bounds.x + bounds.width - iconBoxSize.width - inset,
11131
+ y: bounds.y + bounds.height - iconBoxSize.height - inset,
11232
11132
  width: iconBoxSize.width,
11233
11133
  height: iconBoxSize.height
11234
11134
  };
@@ -11237,23 +11137,17 @@ class SvgExporter {
11237
11137
  return bounds;
11238
11138
  }
11239
11139
  }
11240
- getIconDrawRect(bounds, opts, imageSize) {
11241
- const padding = opts.padding ?? 8;
11242
- const margin = Math.max(0, opts.margin ?? 0);
11140
+ getIconDrawRect(bounds, opts, imageSize, inset) {
11243
11141
  const fit = opts.fit ?? "none";
11244
11142
  const scaleWithBounds = opts.scaleWithBounds ?? false;
11245
- const align = opts.align ?? "center";
11246
- const verticalAlign = opts.verticalAlign ?? "center";
11247
- const offsetX = opts.offsetX ?? 0;
11248
- const offsetY = opts.offsetY ?? 0;
11249
11143
  const innerBounds = {
11250
- x: bounds.x + margin,
11251
- y: bounds.y + margin,
11252
- width: Math.max(0, bounds.width - margin * 2),
11253
- height: Math.max(0, bounds.height - margin * 2)
11144
+ x: bounds.x + inset,
11145
+ y: bounds.y + inset,
11146
+ width: Math.max(0, bounds.width - inset * 2),
11147
+ height: Math.max(0, bounds.height - inset * 2)
11254
11148
  };
11255
- const availableWidth = Math.max(0, innerBounds.width - padding * 2);
11256
- const availableHeight = Math.max(0, innerBounds.height - padding * 2);
11149
+ const availableWidth = Math.max(0, innerBounds.width);
11150
+ const availableHeight = Math.max(0, innerBounds.height);
11257
11151
  let drawWidth = opts.width ?? imageSize.width;
11258
11152
  let drawHeight = opts.height ?? imageSize.height;
11259
11153
  if (scaleWithBounds) {
@@ -11270,21 +11164,13 @@ class SvgExporter {
11270
11164
  }
11271
11165
  drawWidth = Math.min(Math.max(0, drawWidth), Math.max(0, availableWidth));
11272
11166
  drawHeight = Math.min(Math.max(0, drawHeight), Math.max(0, availableHeight));
11273
- let x = innerBounds.x + padding;
11274
- let y = innerBounds.y + padding;
11275
- if (align === "center") {
11276
- x = innerBounds.x + (innerBounds.width - drawWidth) / 2;
11277
- } else if (align === "right") {
11278
- x = innerBounds.x + innerBounds.width - drawWidth - padding;
11279
- }
11280
- if (verticalAlign === "center") {
11281
- y = innerBounds.y + (innerBounds.height - drawHeight) / 2;
11282
- } else if (verticalAlign === "bottom") {
11283
- y = innerBounds.y + innerBounds.height - drawHeight - padding;
11284
- }
11167
+ let x = innerBounds.x;
11168
+ let y = innerBounds.y;
11169
+ x = innerBounds.x + (innerBounds.width - drawWidth) / 2;
11170
+ y = innerBounds.y + (innerBounds.height - drawHeight) / 2;
11285
11171
  return {
11286
- x: x + offsetX,
11287
- y: y + offsetY,
11172
+ x,
11173
+ y,
11288
11174
  width: drawWidth,
11289
11175
  height: drawHeight
11290
11176
  };
@@ -11379,7 +11265,7 @@ class SvgExporter {
11379
11265
  }
11380
11266
  createEmptySvg(width, height, backgroundColor, includeBackground) {
11381
11267
  return [
11382
- `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">`,
11268
+ `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">`,
11383
11269
  includeBackground ? `<rect width="100%" height="100%" fill="${backgroundColor}"/>` : "",
11384
11270
  "</svg>"
11385
11271
  ].join("");