@ngroznykh/papirus 0.3.21 → 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) {
@@ -2616,13 +2633,15 @@ class TextLabel {
2616
2633
  this._measuredWidth = 0;
2617
2634
  this._measuredHeight = 0;
2618
2635
  this._measureDirty = true;
2619
- this._text = options.text;
2636
+ this._text = options.text ?? "";
2620
2637
  this._editableText = options.editableText;
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
  }
@@ -2633,8 +2652,9 @@ class TextLabel {
2633
2652
  return this._text;
2634
2653
  }
2635
2654
  set text(value) {
2636
- if (this._text !== value) {
2637
- this._text = value;
2655
+ const next = value ?? "";
2656
+ if (this._text !== next) {
2657
+ this._text = next;
2638
2658
  this._lines = [];
2639
2659
  this._measureDirty = true;
2640
2660
  this._onChange?.();
@@ -2666,6 +2686,12 @@ class TextLabel {
2666
2686
  this._measureDirty = true;
2667
2687
  this._onChange?.();
2668
2688
  }
2689
+ /**
2690
+ * Raw text style overrides (without StyleManager merge)
2691
+ */
2692
+ get styleOverrides() {
2693
+ return { ...this._localStyle };
2694
+ }
2669
2695
  /**
2670
2696
  * Style class name for StyleManager
2671
2697
  */
@@ -2707,30 +2733,15 @@ class TextLabel {
2707
2733
  this._measureDirty = true;
2708
2734
  }
2709
2735
  /**
2710
- * Label padding
2736
+ * Inset from bounds edge to text (per side: top, right, bottom, left)
2711
2737
  */
2712
- get padding() {
2713
- return this._padding;
2738
+ get inset() {
2739
+ return { ...this._inset };
2714
2740
  }
2715
- set padding(value) {
2716
- const next = Number.isFinite(value) ? Math.max(0, value) : this._padding;
2717
- if (this._padding !== next) {
2718
- this._padding = next;
2719
- this._lines = [];
2720
- this._measureDirty = true;
2721
- this._onChange?.();
2722
- }
2723
- }
2724
- /**
2725
- * Label margin
2726
- */
2727
- get margin() {
2728
- return this._margin;
2729
- }
2730
- set margin(value) {
2731
- const next = Number.isFinite(value) ? Math.max(0, value) : this._margin;
2732
- if (this._margin !== next) {
2733
- 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;
2734
2745
  this._lines = [];
2735
2746
  this._measureDirty = true;
2736
2747
  this._onChange?.();
@@ -2763,7 +2774,7 @@ class TextLabel {
2763
2774
  }
2764
2775
  /**
2765
2776
  * Get wrapped lines for export. Uses ctx to measure and wrap by words.
2766
- * 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).
2767
2778
  */
2768
2779
  getWrappedLines(ctx, maxWidth) {
2769
2780
  this.setAutoMaxWidth(maxWidth);
@@ -2783,19 +2794,23 @@ class TextLabel {
2783
2794
  this.applyStyle(ctx);
2784
2795
  const lineHeight = (this._style.fontSize ?? 14) * 1.2;
2785
2796
  const effectiveMaxWidth = this._maxWidth ?? this._autoMaxWidth;
2797
+ const text = this._text ?? "";
2786
2798
  if (effectiveMaxWidth !== void 0) {
2787
- const maxWidth = Math.max(0, effectiveMaxWidth - this._margin * 2);
2788
- this._lines = this.wrapText(ctx, this._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);
2789
2804
  } else {
2790
- this._lines = this._text.split("\n");
2805
+ this._lines = text.split("\n");
2791
2806
  }
2792
2807
  let maxLineWidth = 0;
2793
2808
  for (const line of this._lines) {
2794
2809
  const metrics = ctx.measureText(line);
2795
2810
  maxLineWidth = Math.max(maxLineWidth, metrics.width);
2796
2811
  }
2797
- this._measuredWidth = maxLineWidth + this._padding * 2 + this._margin * 2;
2798
- 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;
2799
2814
  this._measureDirty = false;
2800
2815
  return {
2801
2816
  width: this._measuredWidth,
@@ -2816,25 +2831,41 @@ class TextLabel {
2816
2831
  }
2817
2832
  const lineHeight = (this._style.fontSize ?? 14) * 1.2;
2818
2833
  const totalHeight = this._lines.length * lineHeight;
2819
- const margin = this._margin;
2820
2834
  const innerBounds = {
2821
- x: bounds.x + margin,
2822
- y: bounds.y + margin,
2823
- width: Math.max(0, bounds.width - margin * 2),
2824
- 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
+ )
2825
2845
  };
2826
2846
  let x;
2827
2847
  switch (align) {
2828
2848
  case "left":
2829
- x = innerBounds.x + this._padding;
2849
+ x = innerBounds.x;
2830
2850
  break;
2831
2851
  case "right":
2832
- x = innerBounds.x + innerBounds.width - this._padding;
2852
+ x = innerBounds.x + innerBounds.width;
2833
2853
  break;
2834
2854
  default:
2835
2855
  x = innerBounds.x + innerBounds.width / 2;
2836
2856
  }
2837
- 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
+ }
2838
2869
  ctx.fillStyle = this._style.color ?? "#000000";
2839
2870
  for (let i = 0; i < this._lines.length; i++) {
2840
2871
  const line = this._lines[i];
@@ -2849,16 +2880,13 @@ class TextLabel {
2849
2880
  if (this._lines.length === 0) {
2850
2881
  this.measure(ctx);
2851
2882
  }
2852
- this.applyStyle(ctx);
2853
- const lineHeight = (this._style.fontSize ?? 14) * 1.2;
2854
- const totalHeight = this._lines.length * lineHeight;
2855
- const startY = point.y - totalHeight / 2 + lineHeight / 2;
2856
- ctx.fillStyle = this._style.color ?? "#000000";
2857
- for (let i = 0; i < this._lines.length; i++) {
2858
- const line = this._lines[i];
2859
- const y = startY + i * lineHeight;
2860
- ctx.fillText(line, point.x, y);
2861
- }
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);
2862
2890
  }
2863
2891
  applyStyle(ctx) {
2864
2892
  const fontSize = this._style.fontSize ?? 14;
@@ -2904,7 +2932,9 @@ const snapshotLabel = (label) => {
2904
2932
  return {
2905
2933
  text: label.text,
2906
2934
  editableText: label.editableText,
2907
- 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),
2908
2938
  styleClass: label.styleClass,
2909
2939
  maxWidth: label.maxWidth
2910
2940
  };
@@ -4730,14 +4760,13 @@ class Edge extends Element {
4730
4760
  this._label.measure(ctx);
4731
4761
  const labelWidth = this._label.measuredWidth;
4732
4762
  const labelHeight = this._label.measuredHeight;
4733
- const bgPadding = this._labelBackground?.padding ?? EDGE_LABEL_BACKGROUND_PADDING;
4734
4763
  const bgColor = this._labelBackground?.color ?? "#ffffff";
4735
4764
  const bgOpacity = this._labelBackground?.opacity ?? 1;
4736
4765
  const bgRadius = this._labelBackground?.borderRadius ?? EDGE_LABEL_BACKGROUND_RADIUS;
4737
- const bgX = labelPosition.x - labelWidth / 2 - bgPadding;
4738
- const bgY = labelPosition.y - labelHeight / 2 - bgPadding;
4739
- const bgWidth = labelWidth + bgPadding * 2;
4740
- 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;
4741
4770
  ctx.fillStyle = bgColor;
4742
4771
  ctx.globalAlpha = bgOpacity;
4743
4772
  if (bgRadius > 0) {
@@ -5042,6 +5071,7 @@ class InteractionManager {
5042
5071
  if (shallowEqual(before, after)) {
5043
5072
  return;
5044
5073
  }
5074
+ this.renderer.markStyleDirty();
5045
5075
  this.queuePropertyChange("node", nodeId, before, after);
5046
5076
  }
5047
5077
  changeEdgeProperties(edgeId, apply) {
@@ -5055,6 +5085,7 @@ class InteractionManager {
5055
5085
  if (shallowEqual(before, after)) {
5056
5086
  return;
5057
5087
  }
5088
+ this.renderer.markStyleDirty();
5058
5089
  this.queuePropertyChange("edge", edgeId, before, after);
5059
5090
  }
5060
5091
  changeGroupProperties(groupId, apply) {
@@ -5068,6 +5099,7 @@ class InteractionManager {
5068
5099
  if (shallowEqual(before, after)) {
5069
5100
  return;
5070
5101
  }
5102
+ this.renderer.markStyleDirty();
5071
5103
  this.queuePropertyChange("group", groupId, before, after);
5072
5104
  }
5073
5105
  removeNodeFromGroups(nodeId, groupIds) {
@@ -5558,6 +5590,7 @@ class InteractionManager {
5558
5590
  pending.after
5559
5591
  )
5560
5592
  );
5593
+ this.renderer.markStyleDirty();
5561
5594
  break;
5562
5595
  case "edge":
5563
5596
  this.historyManager.execute(
@@ -5568,6 +5601,7 @@ class InteractionManager {
5568
5601
  pending.after
5569
5602
  )
5570
5603
  );
5604
+ this.renderer.markStyleDirty();
5571
5605
  break;
5572
5606
  case "group":
5573
5607
  this.historyManager.execute(
@@ -5578,6 +5612,7 @@ class InteractionManager {
5578
5612
  pending.after
5579
5613
  )
5580
5614
  );
5615
+ this.renderer.markStyleDirty();
5581
5616
  break;
5582
5617
  }
5583
5618
  }
@@ -8043,8 +8078,10 @@ class NodeImage {
8043
8078
  get placement() {
8044
8079
  return this._options.placement ?? "center";
8045
8080
  }
8046
- get gap() {
8047
- 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;
8048
8085
  }
8049
8086
  setSource(source) {
8050
8087
  this._loaded = false;
@@ -8059,23 +8096,18 @@ class NodeImage {
8059
8096
  if (!this._loaded) {
8060
8097
  return;
8061
8098
  }
8062
- const padding = this._options.padding ?? 8;
8063
- const margin = Math.max(0, this._options.margin ?? 0);
8099
+ const ins = this.inset;
8064
8100
  const fit = this._options.fit ?? "none";
8065
8101
  const scaleWithBounds = this._options.scaleWithBounds ?? false;
8066
8102
  const opacity = this._options.opacity ?? 1;
8067
- const align = this._options.align ?? "center";
8068
- const verticalAlign = this._options.verticalAlign ?? "center";
8069
- const offsetX = this._options.offsetX ?? 0;
8070
- const offsetY = this._options.offsetY ?? 0;
8071
8103
  const innerBounds = {
8072
- x: bounds.x + margin,
8073
- y: bounds.y + margin,
8074
- width: Math.max(0, bounds.width - margin * 2),
8075
- 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)
8076
8108
  };
8077
- const availableWidth = Math.max(0, innerBounds.width - padding * 2);
8078
- 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);
8079
8111
  let drawWidth = this._options.width ?? this._naturalWidth;
8080
8112
  let drawHeight = this._options.height ?? this._naturalHeight;
8081
8113
  if (scaleWithBounds) {
@@ -8094,21 +8126,13 @@ class NodeImage {
8094
8126
  const maxHeight = Math.max(0, availableHeight);
8095
8127
  drawWidth = Math.min(drawWidth, maxWidth);
8096
8128
  drawHeight = Math.min(drawHeight, maxHeight);
8097
- let x = innerBounds.x + padding;
8098
- let y = innerBounds.y + padding;
8099
- if (align === "center") {
8100
- x = innerBounds.x + (innerBounds.width - drawWidth) / 2;
8101
- } else if (align === "right") {
8102
- x = innerBounds.x + innerBounds.width - drawWidth - padding;
8103
- }
8104
- if (verticalAlign === "center") {
8105
- y = innerBounds.y + (innerBounds.height - drawHeight) / 2;
8106
- } else if (verticalAlign === "bottom") {
8107
- y = innerBounds.y + innerBounds.height - drawHeight - padding;
8108
- }
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;
8109
8133
  ctx.save();
8110
8134
  ctx.globalAlpha = opacity;
8111
- ctx.drawImage(this._image, x + offsetX, y + offsetY, drawWidth, drawHeight);
8135
+ ctx.drawImage(this._image, x, y, drawWidth, drawHeight);
8112
8136
  ctx.restore();
8113
8137
  }
8114
8138
  getSize() {
@@ -8198,8 +8222,8 @@ class Node extends Element {
8198
8222
  this._nodeStyle = { ...DEFAULT_NODE_STYLE, ...options.style };
8199
8223
  this._showPortsAlways = options.showPortsAlways ?? false;
8200
8224
  this._anchorPoints = this.normalizeAnchorPoints(options.anchorPoints);
8201
- this._labelPlacement = options.labelPlacement ?? "auto";
8202
8225
  this._resizeHandlesEnabled = options.resizeHandlesEnabled ?? true;
8226
+ this._contentInset = this.normalizeContentInset(options.contentInset);
8203
8227
  if (options.label !== void 0) {
8204
8228
  if (typeof options.label === "string") {
8205
8229
  this._label = new TextLabel({
@@ -8396,24 +8420,22 @@ class Node extends Element {
8396
8420
  this._anchorPoints = this.normalizeAnchorPoints(value);
8397
8421
  this.markDirty();
8398
8422
  }
8399
- /**
8400
- * Label placement inside the node
8401
- */
8402
- get labelPlacement() {
8403
- return this._labelPlacement;
8404
- }
8405
- set labelPlacement(value) {
8406
- if (this._labelPlacement !== value) {
8407
- this._labelPlacement = value;
8408
- this.markDirty();
8409
- }
8410
- }
8411
8423
  /**
8412
8424
  * Default size (initial width/height)
8413
8425
  */
8414
8426
  get defaultSize() {
8415
8427
  return { ...this._defaultSize };
8416
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
+ }
8417
8439
  /**
8418
8440
  * Set getter for attachToOutline (called by DiagramRenderer when node is added)
8419
8441
  */
@@ -8436,87 +8458,20 @@ class Node extends Element {
8436
8458
  }
8437
8459
  }
8438
8460
  /**
8439
- * Calculate layout for icon and label
8440
- * 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.
8441
8463
  */
8442
- calculateContentLayout(bounds, iconBoxSize, labelSize) {
8443
- let iconBounds = bounds;
8444
- let labelBounds = this.getLabelContainerBounds(bounds);
8464
+ calculateContentLayout(bounds, iconBoxSize, _labelSize) {
8465
+ const contentBounds = this.getLabelContainerBounds(bounds);
8466
+ let iconBounds = contentBounds;
8445
8467
  if (this._icon && iconBoxSize) {
8446
- iconBounds = this.getIconBounds(bounds, iconBoxSize, this._icon.placement);
8447
- }
8448
- if (this._icon && this._label && iconBoxSize && labelSize && this._labelPlacement === "auto" && this._icon.placement !== "center") {
8449
- const gap = this._icon.gap;
8450
- const placement = this._icon.placement;
8451
- if (isCornerPlacement(placement)) {
8452
- iconBounds = this.getIconBounds(bounds, iconBoxSize, placement);
8453
- labelBounds = this.getLabelContainerBounds(bounds);
8454
- } else {
8455
- const contentBounds = this.getLabelContainerBounds(bounds);
8456
- switch (placement) {
8457
- case "top":
8458
- iconBounds = {
8459
- x: bounds.x,
8460
- y: bounds.y,
8461
- width: bounds.width,
8462
- height: iconBoxSize.height
8463
- };
8464
- labelBounds = {
8465
- x: contentBounds.x,
8466
- y: contentBounds.y + iconBoxSize.height + gap,
8467
- width: contentBounds.width,
8468
- height: Math.max(0, contentBounds.height - iconBoxSize.height - gap)
8469
- };
8470
- break;
8471
- case "bottom":
8472
- iconBounds = {
8473
- x: bounds.x,
8474
- y: bounds.y + bounds.height - iconBoxSize.height,
8475
- width: bounds.width,
8476
- height: iconBoxSize.height
8477
- };
8478
- labelBounds = {
8479
- x: contentBounds.x,
8480
- y: contentBounds.y,
8481
- width: contentBounds.width,
8482
- height: Math.max(0, contentBounds.height - iconBoxSize.height - gap)
8483
- };
8484
- break;
8485
- case "left":
8486
- iconBounds = {
8487
- x: bounds.x,
8488
- y: bounds.y,
8489
- width: iconBoxSize.width,
8490
- height: bounds.height
8491
- };
8492
- labelBounds = {
8493
- x: contentBounds.x + iconBoxSize.width + gap,
8494
- y: contentBounds.y,
8495
- width: Math.max(0, contentBounds.width - iconBoxSize.width - gap),
8496
- height: contentBounds.height
8497
- };
8498
- break;
8499
- case "right":
8500
- iconBounds = {
8501
- x: bounds.x + bounds.width - iconBoxSize.width,
8502
- y: bounds.y,
8503
- width: iconBoxSize.width,
8504
- height: bounds.height
8505
- };
8506
- labelBounds = {
8507
- x: contentBounds.x,
8508
- y: contentBounds.y,
8509
- width: Math.max(0, contentBounds.width - iconBoxSize.width - gap),
8510
- height: contentBounds.height
8511
- };
8512
- break;
8513
- }
8514
- }
8515
- } else if (this._label && labelSize) {
8516
- const autoBounds = this.getAutoLabelBounds(bounds, iconBoxSize);
8517
- labelBounds = this.getLabelBounds(autoBounds, labelSize, this._labelPlacement);
8468
+ iconBounds = this.getIconBounds(
8469
+ contentBounds,
8470
+ iconBoxSize,
8471
+ this._icon.placement
8472
+ );
8518
8473
  }
8519
- return { iconBounds, labelBounds };
8474
+ return { iconBounds, labelBounds: contentBounds };
8520
8475
  }
8521
8476
  /**
8522
8477
  * Get label bounds and wrapped lines for SVG export. Replicates renderContents layout logic.
@@ -8527,12 +8482,19 @@ class Node extends Element {
8527
8482
  return null;
8528
8483
  }
8529
8484
  const bounds = this.getBounds();
8485
+ const contentBounds = this.getLabelContainerBounds(bounds);
8530
8486
  const iconBoxSize = this._icon ? this.getIconBoxSize() : void 0;
8531
- const autoBounds = this.getAutoLabelBounds(bounds, iconBoxSize);
8532
- this._label.setAutoMaxWidth(autoBounds.width);
8487
+ this._label.setAutoMaxWidth(contentBounds.width);
8533
8488
  const labelSize = this._label.measure(ctx);
8534
- const { labelBounds } = this.calculateContentLayout(bounds, iconBoxSize, labelSize);
8535
- 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
+ );
8536
8498
  return { bounds: labelBounds, lines };
8537
8499
  }
8538
8500
  /**
@@ -8548,23 +8510,14 @@ class Node extends Element {
8548
8510
  ctx.globalAlpha = 1;
8549
8511
  }
8550
8512
  /**
8551
- * Get world position of label center.
8513
+ * Get world position of label center (center of content area).
8552
8514
  */
8553
8515
  getLabelPosition() {
8554
- const bounds = this.getBounds();
8555
- const placement = this._labelPlacement === "auto" ? "center" : this._labelPlacement;
8556
- switch (placement) {
8557
- case "top":
8558
- return { x: bounds.x + bounds.width / 2, y: bounds.y };
8559
- case "bottom":
8560
- return { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height };
8561
- case "left":
8562
- return { x: bounds.x, y: bounds.y + bounds.height / 2 };
8563
- case "right":
8564
- return { x: bounds.x + bounds.width, y: bounds.y + bounds.height / 2 };
8565
- default:
8566
- return { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 };
8567
- }
8516
+ const bounds = this.getLabelContainerBounds(this.getBounds());
8517
+ return {
8518
+ x: bounds.x + bounds.width / 2,
8519
+ y: bounds.y + bounds.height / 2
8520
+ };
8568
8521
  }
8569
8522
  /**
8570
8523
  * Render icon, label, and ports
@@ -8575,14 +8528,14 @@ class Node extends Element {
8575
8528
  let bounds = this.getBounds();
8576
8529
  const iconBoxSize = this._icon ? this.getIconBoxSize() : void 0;
8577
8530
  if (this._label) {
8578
- this._label.setAutoMaxWidth(this.getAutoLabelBounds(bounds, iconBoxSize).width);
8531
+ this._label.setAutoMaxWidth(this.getLabelContainerBounds(bounds).width);
8579
8532
  }
8580
8533
  const labelSize = this._label ? this._label.measure(ctx) : void 0;
8581
8534
  if (labelSize || iconBoxSize) {
8582
8535
  this.ensureContentsFit(labelSize, iconBoxSize);
8583
8536
  if (this._label && iconBoxSize) {
8584
8537
  bounds = this.getBounds();
8585
- this._label.setAutoMaxWidth(this.getAutoLabelBounds(bounds, iconBoxSize).width);
8538
+ this._label.setAutoMaxWidth(this.getLabelContainerBounds(bounds).width);
8586
8539
  }
8587
8540
  }
8588
8541
  const { iconBounds, labelBounds } = this.calculateContentLayout(
@@ -8867,124 +8820,45 @@ class Node extends Element {
8867
8820
  }
8868
8821
  }
8869
8822
  calculateContentMinSize(labelSize, iconBoxSize, bounds) {
8870
- let minWidth = 0;
8871
- let minHeight = 0;
8872
8823
  const labelContainer = this.getLabelContainerBounds(bounds);
8873
8824
  const widthFactor = labelContainer.width > 0 ? bounds.width / labelContainer.width : 1;
8874
8825
  const heightFactor = labelContainer.height > 0 ? bounds.height / labelContainer.height : 1;
8875
- if (labelSize) {
8876
- minWidth = Math.max(minWidth, labelSize.width * widthFactor);
8877
- minHeight = Math.max(minHeight, labelSize.height * heightFactor);
8878
- }
8879
- if (iconBoxSize) {
8880
- minWidth = Math.max(minWidth, iconBoxSize.width);
8881
- minHeight = Math.max(minHeight, iconBoxSize.height);
8882
- }
8883
- if (labelSize && iconBoxSize && this._icon) {
8884
- const gap = this._icon.gap;
8885
- const labelPlacement = this._labelPlacement === "auto" ? "center" : this._labelPlacement;
8886
- const iconPlacement = this._icon.placement;
8887
- if (this._labelPlacement === "auto" && iconPlacement !== "center") {
8888
- if (isCornerPlacement(iconPlacement)) {
8889
- minWidth = Math.max(minWidth, iconBoxSize.width, labelSize.width);
8890
- minHeight = Math.max(minHeight, iconBoxSize.height, labelSize.height);
8891
- } else if (iconPlacement === "top" || iconPlacement === "bottom") {
8892
- minHeight = Math.max(minHeight, iconBoxSize.height + gap + labelSize.height);
8893
- minWidth = Math.max(minWidth, iconBoxSize.width, labelSize.width);
8894
- } else if (iconPlacement === "left" || iconPlacement === "right") {
8895
- minWidth = Math.max(minWidth, iconBoxSize.width + gap + labelSize.width);
8896
- minHeight = Math.max(minHeight, iconBoxSize.height, labelSize.height);
8897
- }
8898
- } else {
8899
- const labelHorizontal = labelPlacement === "left" || labelPlacement === "right";
8900
- const labelVertical = labelPlacement === "top" || labelPlacement === "bottom";
8901
- const iconHorizontal = iconPlacement === "left" || iconPlacement === "right";
8902
- const iconVertical = iconPlacement === "top" || iconPlacement === "bottom";
8903
- const labelSharesIconAxis = iconHorizontal && (labelPlacement === "center" || labelPlacement === iconPlacement) || iconVertical && (labelPlacement === "center" || labelPlacement === iconPlacement);
8904
- if (labelHorizontal && iconHorizontal && labelPlacement !== iconPlacement) {
8905
- minWidth = Math.max(minWidth, iconBoxSize.width + gap + labelSize.width);
8906
- }
8907
- if (labelVertical && iconVertical && labelPlacement !== iconPlacement) {
8908
- minHeight = Math.max(minHeight, iconBoxSize.height + gap + labelSize.height);
8909
- }
8910
- if (labelSharesIconAxis) {
8911
- if (iconHorizontal) {
8912
- minWidth = Math.max(minWidth, iconBoxSize.width + gap + labelSize.width);
8913
- }
8914
- if (iconVertical) {
8915
- minHeight = Math.max(minHeight, iconBoxSize.height + gap + labelSize.height);
8916
- }
8917
- }
8918
- }
8919
- }
8920
- 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
+ };
8921
8838
  }
8922
8839
  getLabelContainerBounds(bounds) {
8923
- 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 };
8924
8846
  }
8925
- getAutoLabelBounds(bounds, iconBoxSize) {
8926
- const contentBounds = this.getLabelContainerBounds(bounds);
8927
- if (!this._icon || !iconBoxSize || this._icon.placement === "center") {
8928
- return contentBounds;
8929
- }
8930
- const gap = this._icon.gap;
8931
- if (isCornerPlacement(this._icon.placement)) {
8932
- return contentBounds;
8933
- }
8934
- switch (this._icon.placement) {
8935
- case "left":
8936
- return {
8937
- x: contentBounds.x + iconBoxSize.width + gap,
8938
- y: contentBounds.y,
8939
- width: Math.max(0, contentBounds.width - iconBoxSize.width - gap),
8940
- height: contentBounds.height
8941
- };
8942
- case "right":
8943
- return {
8944
- x: contentBounds.x,
8945
- y: contentBounds.y,
8946
- width: Math.max(0, contentBounds.width - iconBoxSize.width - gap),
8947
- height: contentBounds.height
8948
- };
8949
- case "top":
8950
- return {
8951
- x: contentBounds.x,
8952
- y: contentBounds.y + iconBoxSize.height + gap,
8953
- width: contentBounds.width,
8954
- height: Math.max(0, contentBounds.height - iconBoxSize.height - gap)
8955
- };
8956
- case "bottom":
8957
- return {
8958
- x: contentBounds.x,
8959
- y: contentBounds.y,
8960
- width: contentBounds.width,
8961
- height: Math.max(0, contentBounds.height - iconBoxSize.height - gap)
8962
- };
8963
- default:
8964
- 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 };
8965
8851
  }
8966
- }
8967
- getLabelBounds(bounds, labelSize, placement) {
8968
- const normalizedPlacement = placement === "auto" ? "center" : placement;
8969
- const width = Math.min(labelSize.width, bounds.width);
8970
- const height = Math.min(labelSize.height, bounds.height);
8971
- let x = bounds.x + (bounds.width - width) / 2;
8972
- let y = bounds.y + (bounds.height - height) / 2;
8973
- switch (normalizedPlacement) {
8974
- case "top":
8975
- y = bounds.y;
8976
- break;
8977
- case "bottom":
8978
- y = bounds.y + bounds.height - height;
8979
- break;
8980
- case "left":
8981
- x = bounds.x;
8982
- break;
8983
- case "right":
8984
- x = bounds.x + bounds.width - width;
8985
- break;
8852
+ if (typeof value === "number") {
8853
+ const v = n(value);
8854
+ return { top: v, right: v, bottom: v, left: v };
8986
8855
  }
8987
- 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
+ };
8988
8862
  }
8989
8863
  getIconBoxSize() {
8990
8864
  if (!this._icon) {
@@ -8994,14 +8868,14 @@ class Node extends Element {
8994
8868
  if (width <= 0 || height <= 0) {
8995
8869
  return void 0;
8996
8870
  }
8997
- const padding = this._icon.options.padding ?? 8;
8998
- const margin = Math.max(0, this._icon.options.margin ?? 0);
8871
+ const ins = this._icon.inset;
8999
8872
  return {
9000
- width: width + padding * 2 + margin * 2,
9001
- height: height + padding * 2 + margin * 2
8873
+ width: width + ins * 2,
8874
+ height: height + ins * 2
9002
8875
  };
9003
8876
  }
9004
8877
  getIconBounds(bounds, iconBoxSize, placement) {
8878
+ const ins = this._icon?.inset ?? 0;
9005
8879
  switch (placement) {
9006
8880
  case "top":
9007
8881
  return {
@@ -9033,29 +8907,29 @@ class Node extends Element {
9033
8907
  };
9034
8908
  case "top-left":
9035
8909
  return {
9036
- x: bounds.x,
9037
- y: bounds.y,
8910
+ x: bounds.x + ins,
8911
+ y: bounds.y + ins,
9038
8912
  width: iconBoxSize.width,
9039
8913
  height: iconBoxSize.height
9040
8914
  };
9041
8915
  case "top-right":
9042
8916
  return {
9043
- x: bounds.x + bounds.width - iconBoxSize.width,
9044
- y: bounds.y,
8917
+ x: bounds.x + bounds.width - iconBoxSize.width - ins,
8918
+ y: bounds.y + ins,
9045
8919
  width: iconBoxSize.width,
9046
8920
  height: iconBoxSize.height
9047
8921
  };
9048
8922
  case "bottom-left":
9049
8923
  return {
9050
- x: bounds.x,
9051
- y: bounds.y + bounds.height - iconBoxSize.height,
8924
+ x: bounds.x + ins,
8925
+ y: bounds.y + bounds.height - iconBoxSize.height - ins,
9052
8926
  width: iconBoxSize.width,
9053
8927
  height: iconBoxSize.height
9054
8928
  };
9055
8929
  case "bottom-right":
9056
8930
  return {
9057
- x: bounds.x + bounds.width - iconBoxSize.width,
9058
- 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,
9059
8933
  width: iconBoxSize.width,
9060
8934
  height: iconBoxSize.height
9061
8935
  };
@@ -9954,18 +9828,14 @@ const DEFAULT_THEME = {
9954
9828
  cornerRadius: 4
9955
9829
  },
9956
9830
  selected: {
9957
- fillColor: "#ffffff",
9958
- strokeColor: "#333333",
9831
+ strokeColor: "#3b82f6",
9959
9832
  strokeWidth: 2,
9960
- opacity: 1,
9961
- cornerRadius: 4
9833
+ opacity: 1
9962
9834
  },
9963
9835
  dragging: {
9964
- fillColor: "#f0f0f0",
9965
9836
  strokeColor: "#333333",
9966
9837
  strokeWidth: 2,
9967
- opacity: 0.8,
9968
- cornerRadius: 4
9838
+ opacity: 0.8
9969
9839
  }
9970
9840
  },
9971
9841
  edge: {
@@ -10226,15 +10096,16 @@ class StyleManager {
10226
10096
  return baseStyle;
10227
10097
  }
10228
10098
  getBaseNodeStyle(state) {
10099
+ const d = this._theme.node.default;
10229
10100
  switch (state) {
10230
10101
  case "hover":
10231
- return this._theme.node.hover;
10102
+ return { ...d, ...this._theme.node.hover };
10232
10103
  case "selected":
10233
- return this._theme.node.selected;
10104
+ return { ...d, ...this._theme.node.selected };
10234
10105
  case "dragging":
10235
- return this._theme.node.dragging;
10106
+ return { ...d, ...this._theme.node.dragging };
10236
10107
  default:
10237
- return this._theme.node.default;
10108
+ return d;
10238
10109
  }
10239
10110
  }
10240
10111
  getBaseEdgeStyle(state) {
@@ -10432,13 +10303,16 @@ class Serializer {
10432
10303
  }));
10433
10304
  let label;
10434
10305
  if (node.label) {
10435
- 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 };
10436
10311
  const labelData = {
10437
10312
  text: node.label.text,
10438
10313
  style: node.label.style,
10439
10314
  maxWidth: node.label.maxWidth,
10440
- padding: node.label.padding,
10441
- margin: node.label.margin,
10315
+ inset: serializedInset ?? void 0,
10442
10316
  styleClass: node.label.styleClass
10443
10317
  };
10444
10318
  const hasExtendedOptions = hasNonDefaultValues(labelData, labelDefaults);
@@ -10447,8 +10321,7 @@ class Serializer {
10447
10321
  text: node.label.text,
10448
10322
  style: Object.keys(node.label.style).length > 0 ? node.label.style : void 0,
10449
10323
  maxWidth: node.label.maxWidth,
10450
- padding: node.label.padding !== 8 ? node.label.padding : void 0,
10451
- margin: node.label.margin !== 0 ? node.label.margin : void 0,
10324
+ inset: serializedInset,
10452
10325
  styleClass: node.label.styleClass
10453
10326
  });
10454
10327
  } else {
@@ -10460,22 +10333,16 @@ class Serializer {
10460
10333
  const opts = node.icon.options;
10461
10334
  const source = typeof opts.source === "string" ? opts.source : void 0;
10462
10335
  if (source) {
10463
- icon = {
10336
+ icon = omitEmptyValues({
10464
10337
  source,
10465
10338
  width: opts.width,
10466
10339
  height: opts.height,
10467
10340
  fit: opts.fit,
10468
10341
  placement: opts.placement,
10469
10342
  scaleWithBounds: opts.scaleWithBounds,
10470
- padding: opts.padding,
10471
- margin: opts.margin,
10472
- gap: opts.gap,
10473
- opacity: opts.opacity,
10474
- align: opts.align,
10475
- verticalAlign: opts.verticalAlign,
10476
- offsetX: opts.offsetX,
10477
- offsetY: opts.offsetY
10478
- };
10343
+ inset: node.icon.inset !== 6 ? node.icon.inset : void 0,
10344
+ opacity: opts.opacity
10345
+ });
10479
10346
  }
10480
10347
  }
10481
10348
  let anchorPoints;
@@ -10485,7 +10352,9 @@ class Serializer {
10485
10352
  if (hasNonDefaultValues(anchorData, anchorDefaults)) {
10486
10353
  anchorPoints = omitDefaultValues(anchorData, anchorDefaults);
10487
10354
  }
10488
- return {
10355
+ const contentInset = node.contentInset;
10356
+ const hasContentInset = contentInset.top !== 0 || contentInset.right !== 0 || contentInset.bottom !== 0 || contentInset.left !== 0;
10357
+ return omitEmptyValues({
10489
10358
  id: node.id,
10490
10359
  type: node.typeName,
10491
10360
  x: node.x,
@@ -10496,12 +10365,12 @@ class Serializer {
10496
10365
  styleClass: node.styleClass,
10497
10366
  label,
10498
10367
  labelStyleClass: typeof label === "string" ? node.label?.styleClass : void 0,
10499
- labelPlacement: node.labelPlacement !== "auto" ? node.labelPlacement : void 0,
10500
10368
  icon,
10369
+ contentInset: hasContentInset ? contentInset : void 0,
10501
10370
  anchorPoints,
10502
10371
  ports: ports.length > 0 ? ports : void 0,
10503
10372
  data: Object.keys(node.data).length > 0 ? node.data : void 0
10504
- };
10373
+ });
10505
10374
  }
10506
10375
  serializeEdge(edge) {
10507
10376
  return {
@@ -10812,7 +10681,7 @@ class SvgExporter {
10812
10681
  const parts = [];
10813
10682
  const renderedEdges = [];
10814
10683
  parts.push(
10815
- `<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}">`
10816
10685
  );
10817
10686
  for (const edge of this.renderer.edges.values()) {
10818
10687
  if (edge.visible) {
@@ -10953,22 +10822,37 @@ class SvgExporter {
10953
10822
  if (!edge.label) {
10954
10823
  return "";
10955
10824
  }
10956
- const metrics = this.measureTextLabel(edge.label.text, edge.label.style, edge.label.padding, edge.label.margin);
10957
- const bgPadding = edge.labelBackground?.padding ?? EDGE_LABEL_BACKGROUND_PADDING;
10825
+ const metrics = this.measureTextLabel(edge.label.text, edge.label.style, edge.label.inset);
10958
10826
  const bgColor = edge.labelBackground?.color ?? "#ffffff";
10959
10827
  const bgOpacity = edge.labelBackground?.opacity ?? 1;
10960
10828
  const bgRadius = edge.labelBackground?.borderRadius ?? EDGE_LABEL_BACKGROUND_RADIUS;
10961
- const x = point.x - metrics.width / 2 - bgPadding;
10962
- const y = point.y - metrics.height / 2 - bgPadding;
10963
- const width = metrics.width + bgPadding * 2;
10964
- 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;
10965
10833
  const radius = Math.max(0, Math.min(bgRadius, width / 2, height / 2));
10966
10834
  if (radius <= 0) {
10967
10835
  return `<rect x="${x}" y="${y}" width="${width}" height="${height}" fill="${bgColor}" fill-opacity="${bgOpacity}"/>`;
10968
10836
  }
10969
10837
  return `<rect x="${x}" y="${y}" width="${width}" height="${height}" rx="${radius}" ry="${radius}" fill="${bgColor}" fill-opacity="${bgOpacity}"/>`;
10970
10838
  }
10971
- 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) {
10972
10856
  const fontSize = style.fontSize ?? 14;
10973
10857
  const fontFamily = style.fontFamily ?? "sans-serif";
10974
10858
  const fontWeight = style.fontWeight ?? "normal";
@@ -10989,11 +10873,10 @@ class SvgExporter {
10989
10873
  const longestLine = lines.reduce((max, line) => line.length > max.length ? line : max, "");
10990
10874
  maxWidth = longestLine.length * fontSize * 0.6;
10991
10875
  }
10992
- const resolvedPadding = Math.max(0, padding);
10993
- const resolvedMargin = Math.max(0, margin);
10876
+ const ins = this.normalizeLabelInset(inset, 8);
10994
10877
  return {
10995
- width: maxWidth + resolvedPadding * 2 + resolvedMargin * 2,
10996
- height: lines.length * lineHeight + resolvedPadding * 2 + resolvedMargin * 2
10878
+ width: maxWidth + ins.left + ins.right,
10879
+ height: lines.length * lineHeight + ins.top + ins.bottom
10997
10880
  };
10998
10881
  }
10999
10882
  resolveMarkerConfig(edge, side) {
@@ -11118,9 +11001,9 @@ class SvgExporter {
11118
11001
  return "";
11119
11002
  }
11120
11003
  const style = label.style;
11121
- const padding = Math.max(0, label.padding);
11122
- const margin = Math.max(0, label.margin);
11004
+ const ins = this.normalizeLabelInset(label.inset, 8);
11123
11005
  const align = style.align ?? "center";
11006
+ const verticalAlign = style.verticalAlign ?? "middle";
11124
11007
  const ctx = this.getMeasurementContext();
11125
11008
  let bounds;
11126
11009
  let lines;
@@ -11138,18 +11021,27 @@ class SvgExporter {
11138
11021
  lines = label.text.split("\n");
11139
11022
  }
11140
11023
  const inner = {
11141
- x: bounds.x + margin,
11142
- y: bounds.y + margin,
11143
- width: Math.max(0, bounds.width - margin * 2),
11144
- 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)
11145
11028
  };
11146
11029
  let x = inner.x + inner.width / 2;
11147
11030
  if (align === "left") {
11148
- x = inner.x + padding;
11031
+ x = inner.x;
11149
11032
  } else if (align === "right") {
11150
- 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;
11151
11044
  }
11152
- const y = inner.y + inner.height / 2;
11153
11045
  return this.renderTextLabel(lines.join("\n"), { x, y }, style);
11154
11046
  }
11155
11047
  getNodeCornerRadius(node, bounds) {
@@ -11166,9 +11058,15 @@ class SvgExporter {
11166
11058
  if (iconSize.width <= 0 || iconSize.height <= 0) {
11167
11059
  return "";
11168
11060
  }
11169
- const iconBoxSize = this.getIconBoxSize(opts, iconSize);
11170
- const iconBounds = this.getIconBounds(nodeBounds, iconBoxSize, opts.placement ?? "center");
11171
- 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);
11172
11070
  if (drawRect.width <= 0 || drawRect.height <= 0) {
11173
11071
  return "";
11174
11072
  }
@@ -11177,17 +11075,16 @@ class SvgExporter {
11177
11075
  return "";
11178
11076
  }
11179
11077
  const opacity = opts.opacity ?? 1;
11180
- 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"/>`;
11181
11080
  }
11182
- getIconBoxSize(opts, imageSize) {
11183
- const padding = opts.padding ?? 8;
11184
- const margin = Math.max(0, opts.margin ?? 0);
11081
+ getIconBoxSize(imageSize, inset) {
11185
11082
  return {
11186
- width: imageSize.width + padding * 2 + margin * 2,
11187
- height: imageSize.height + padding * 2 + margin * 2
11083
+ width: imageSize.width + inset * 2,
11084
+ height: imageSize.height + inset * 2
11188
11085
  };
11189
11086
  }
11190
- getIconBounds(bounds, iconBoxSize, placement) {
11087
+ getIconBounds(bounds, iconBoxSize, placement, inset) {
11191
11088
  switch (placement) {
11192
11089
  case "top":
11193
11090
  return { x: bounds.x, y: bounds.y, width: bounds.width, height: iconBoxSize.height };
@@ -11208,25 +11105,30 @@ class SvgExporter {
11208
11105
  height: bounds.height
11209
11106
  };
11210
11107
  case "top-left":
11211
- 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
+ };
11212
11114
  case "top-right":
11213
11115
  return {
11214
- x: bounds.x + bounds.width - iconBoxSize.width,
11215
- y: bounds.y,
11116
+ x: bounds.x + bounds.width - iconBoxSize.width - inset,
11117
+ y: bounds.y + inset,
11216
11118
  width: iconBoxSize.width,
11217
11119
  height: iconBoxSize.height
11218
11120
  };
11219
11121
  case "bottom-left":
11220
11122
  return {
11221
- x: bounds.x,
11222
- y: bounds.y + bounds.height - iconBoxSize.height,
11123
+ x: bounds.x + inset,
11124
+ y: bounds.y + bounds.height - iconBoxSize.height - inset,
11223
11125
  width: iconBoxSize.width,
11224
11126
  height: iconBoxSize.height
11225
11127
  };
11226
11128
  case "bottom-right":
11227
11129
  return {
11228
- x: bounds.x + bounds.width - iconBoxSize.width,
11229
- 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,
11230
11132
  width: iconBoxSize.width,
11231
11133
  height: iconBoxSize.height
11232
11134
  };
@@ -11235,23 +11137,17 @@ class SvgExporter {
11235
11137
  return bounds;
11236
11138
  }
11237
11139
  }
11238
- getIconDrawRect(bounds, opts, imageSize) {
11239
- const padding = opts.padding ?? 8;
11240
- const margin = Math.max(0, opts.margin ?? 0);
11140
+ getIconDrawRect(bounds, opts, imageSize, inset) {
11241
11141
  const fit = opts.fit ?? "none";
11242
11142
  const scaleWithBounds = opts.scaleWithBounds ?? false;
11243
- const align = opts.align ?? "center";
11244
- const verticalAlign = opts.verticalAlign ?? "center";
11245
- const offsetX = opts.offsetX ?? 0;
11246
- const offsetY = opts.offsetY ?? 0;
11247
11143
  const innerBounds = {
11248
- x: bounds.x + margin,
11249
- y: bounds.y + margin,
11250
- width: Math.max(0, bounds.width - margin * 2),
11251
- 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)
11252
11148
  };
11253
- const availableWidth = Math.max(0, innerBounds.width - padding * 2);
11254
- 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);
11255
11151
  let drawWidth = opts.width ?? imageSize.width;
11256
11152
  let drawHeight = opts.height ?? imageSize.height;
11257
11153
  if (scaleWithBounds) {
@@ -11268,21 +11164,13 @@ class SvgExporter {
11268
11164
  }
11269
11165
  drawWidth = Math.min(Math.max(0, drawWidth), Math.max(0, availableWidth));
11270
11166
  drawHeight = Math.min(Math.max(0, drawHeight), Math.max(0, availableHeight));
11271
- let x = innerBounds.x + padding;
11272
- let y = innerBounds.y + padding;
11273
- if (align === "center") {
11274
- x = innerBounds.x + (innerBounds.width - drawWidth) / 2;
11275
- } else if (align === "right") {
11276
- x = innerBounds.x + innerBounds.width - drawWidth - padding;
11277
- }
11278
- if (verticalAlign === "center") {
11279
- y = innerBounds.y + (innerBounds.height - drawHeight) / 2;
11280
- } else if (verticalAlign === "bottom") {
11281
- y = innerBounds.y + innerBounds.height - drawHeight - padding;
11282
- }
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;
11283
11171
  return {
11284
- x: x + offsetX,
11285
- y: y + offsetY,
11172
+ x,
11173
+ y,
11286
11174
  width: drawWidth,
11287
11175
  height: drawHeight
11288
11176
  };
@@ -11377,7 +11265,7 @@ class SvgExporter {
11377
11265
  }
11378
11266
  createEmptySvg(width, height, backgroundColor, includeBackground) {
11379
11267
  return [
11380
- `<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}">`,
11381
11269
  includeBackground ? `<rect width="100%" height="100%" fill="${backgroundColor}"/>` : "",
11382
11270
  "</svg>"
11383
11271
  ].join("");