@ngroznykh/papirus 0.3.22 → 0.5.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;
@@ -571,6 +570,14 @@ class SelectionManager extends EventEmitter {
571
570
  }
572
571
  return;
573
572
  }
573
+ const node = element;
574
+ if (typeof node.getBadgeAtPoint === "function") {
575
+ const badge = node.getBadgeAtPoint(point);
576
+ if (badge !== null) {
577
+ this.renderer.emit("nodeBadgeClick", element.id, badge.id);
578
+ return;
579
+ }
580
+ }
574
581
  if (event.ctrlKey || event.metaKey) {
575
582
  this.toggleSelection(element.id);
576
583
  } else {
@@ -2600,6 +2607,23 @@ function applyStyleManagerToElements(styleManager, groups, edges, nodes) {
2600
2607
  node.applyStyleManager(styleManager);
2601
2608
  }
2602
2609
  }
2610
+ const DEFAULT_INSET = 8;
2611
+ function normalizeTextInset(value, fallback) {
2612
+ const n = (v) => v !== void 0 && Number.isFinite(v) ? Math.max(0, v) : fallback;
2613
+ if (value === void 0) {
2614
+ return { top: fallback, right: fallback, bottom: fallback, left: fallback };
2615
+ }
2616
+ if (typeof value === "number") {
2617
+ const v = n(value);
2618
+ return { top: v, right: v, bottom: v, left: v };
2619
+ }
2620
+ return {
2621
+ top: n(value.top),
2622
+ right: n(value.right),
2623
+ bottom: n(value.bottom),
2624
+ left: n(value.left)
2625
+ };
2626
+ }
2603
2627
  const DEFAULT_STYLE = {
2604
2628
  font: "14px sans-serif",
2605
2629
  fontSize: 14,
@@ -2608,7 +2632,8 @@ const DEFAULT_STYLE = {
2608
2632
  color: "#000000",
2609
2633
  opacity: 1,
2610
2634
  align: "center",
2611
- baseline: "middle"
2635
+ baseline: "middle",
2636
+ verticalAlign: "middle"
2612
2637
  };
2613
2638
  class TextLabel {
2614
2639
  constructor(options) {
@@ -2621,8 +2646,10 @@ class TextLabel {
2621
2646
  this._localStyle = { ...options.style };
2622
2647
  this._style = { ...DEFAULT_STYLE, ...options.style };
2623
2648
  this._maxWidth = options.maxWidth;
2624
- this._padding = options.padding ?? 8;
2625
- this._margin = Number.isFinite(options.margin) ? Math.max(0, options.margin ?? 0) : 0;
2649
+ this._inset = normalizeTextInset(
2650
+ options.inset ?? options.margin ?? options.padding,
2651
+ DEFAULT_INSET
2652
+ );
2626
2653
  this._styleClass = options.styleClass;
2627
2654
  this._onChange = options.onChange;
2628
2655
  }
@@ -2667,6 +2694,12 @@ class TextLabel {
2667
2694
  this._measureDirty = true;
2668
2695
  this._onChange?.();
2669
2696
  }
2697
+ /**
2698
+ * Raw text style overrides (without StyleManager merge)
2699
+ */
2700
+ get styleOverrides() {
2701
+ return { ...this._localStyle };
2702
+ }
2670
2703
  /**
2671
2704
  * Style class name for StyleManager
2672
2705
  */
@@ -2708,30 +2741,15 @@ class TextLabel {
2708
2741
  this._measureDirty = true;
2709
2742
  }
2710
2743
  /**
2711
- * Label padding
2744
+ * Inset from bounds edge to text (per side: top, right, bottom, left)
2712
2745
  */
2713
- get padding() {
2714
- return this._padding;
2746
+ get inset() {
2747
+ return { ...this._inset };
2715
2748
  }
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;
2749
+ set inset(value) {
2750
+ const next = normalizeTextInset(value, DEFAULT_INSET);
2751
+ if (this._inset.top !== next.top || this._inset.right !== next.right || this._inset.bottom !== next.bottom || this._inset.left !== next.left) {
2752
+ this._inset = next;
2735
2753
  this._lines = [];
2736
2754
  this._measureDirty = true;
2737
2755
  this._onChange?.();
@@ -2764,7 +2782,7 @@ class TextLabel {
2764
2782
  }
2765
2783
  /**
2766
2784
  * 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).
2785
+ * Call with maxWidth = inner bounds width (e.g. bounds.width - inset*2).
2768
2786
  */
2769
2787
  getWrappedLines(ctx, maxWidth) {
2770
2788
  this.setAutoMaxWidth(maxWidth);
@@ -2786,8 +2804,11 @@ class TextLabel {
2786
2804
  const effectiveMaxWidth = this._maxWidth ?? this._autoMaxWidth;
2787
2805
  const text = this._text ?? "";
2788
2806
  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));
2807
+ const maxWidth = Math.max(
2808
+ 0,
2809
+ effectiveMaxWidth - this._inset.left - this._inset.right
2810
+ );
2811
+ this._lines = this.wrapText(ctx, text, maxWidth);
2791
2812
  } else {
2792
2813
  this._lines = text.split("\n");
2793
2814
  }
@@ -2796,8 +2817,8 @@ class TextLabel {
2796
2817
  const metrics = ctx.measureText(line);
2797
2818
  maxLineWidth = Math.max(maxLineWidth, metrics.width);
2798
2819
  }
2799
- this._measuredWidth = maxLineWidth + this._padding * 2 + this._margin * 2;
2800
- this._measuredHeight = this._lines.length * lineHeight + this._padding * 2 + this._margin * 2;
2820
+ this._measuredWidth = maxLineWidth + this._inset.left + this._inset.right;
2821
+ this._measuredHeight = this._lines.length * lineHeight + this._inset.top + this._inset.bottom;
2801
2822
  this._measureDirty = false;
2802
2823
  return {
2803
2824
  width: this._measuredWidth,
@@ -2818,25 +2839,41 @@ class TextLabel {
2818
2839
  }
2819
2840
  const lineHeight = (this._style.fontSize ?? 14) * 1.2;
2820
2841
  const totalHeight = this._lines.length * lineHeight;
2821
- const margin = this._margin;
2822
2842
  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)
2843
+ x: bounds.x + this._inset.left,
2844
+ y: bounds.y + this._inset.top,
2845
+ width: Math.max(
2846
+ 0,
2847
+ bounds.width - this._inset.left - this._inset.right
2848
+ ),
2849
+ height: Math.max(
2850
+ 0,
2851
+ bounds.height - this._inset.top - this._inset.bottom
2852
+ )
2827
2853
  };
2828
2854
  let x;
2829
2855
  switch (align) {
2830
2856
  case "left":
2831
- x = innerBounds.x + this._padding;
2857
+ x = innerBounds.x;
2832
2858
  break;
2833
2859
  case "right":
2834
- x = innerBounds.x + innerBounds.width - this._padding;
2860
+ x = innerBounds.x + innerBounds.width;
2835
2861
  break;
2836
2862
  default:
2837
2863
  x = innerBounds.x + innerBounds.width / 2;
2838
2864
  }
2839
- const startY = innerBounds.y + (innerBounds.height - totalHeight) / 2 + lineHeight / 2;
2865
+ const verticalAlign = this._style.verticalAlign ?? "middle";
2866
+ let startY;
2867
+ switch (verticalAlign) {
2868
+ case "top":
2869
+ startY = innerBounds.y + lineHeight / 2;
2870
+ break;
2871
+ case "bottom":
2872
+ startY = innerBounds.y + innerBounds.height - totalHeight + lineHeight / 2;
2873
+ break;
2874
+ default:
2875
+ startY = innerBounds.y + (innerBounds.height - totalHeight) / 2 + lineHeight / 2;
2876
+ }
2840
2877
  ctx.fillStyle = this._style.color ?? "#000000";
2841
2878
  for (let i = 0; i < this._lines.length; i++) {
2842
2879
  const line = this._lines[i];
@@ -2851,16 +2888,13 @@ class TextLabel {
2851
2888
  if (this._lines.length === 0) {
2852
2889
  this.measure(ctx);
2853
2890
  }
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
- }
2891
+ const bounds = {
2892
+ x: point.x - this._measuredWidth / 2,
2893
+ y: point.y - this._measuredHeight / 2,
2894
+ width: this._measuredWidth,
2895
+ height: this._measuredHeight
2896
+ };
2897
+ this.render(ctx, bounds);
2864
2898
  }
2865
2899
  applyStyle(ctx) {
2866
2900
  const fontSize = this._style.fontSize ?? 14;
@@ -2906,7 +2940,9 @@ const snapshotLabel = (label) => {
2906
2940
  return {
2907
2941
  text: label.text,
2908
2942
  editableText: label.editableText,
2909
- style: cloneValue(label.style),
2943
+ // Keep only local text overrides; computed style contains theme/class values
2944
+ // and would overwrite class text color on debounced history replay.
2945
+ style: cloneValue(label.styleOverrides),
2910
2946
  styleClass: label.styleClass,
2911
2947
  maxWidth: label.maxWidth
2912
2948
  };
@@ -4732,14 +4768,13 @@ class Edge extends Element {
4732
4768
  this._label.measure(ctx);
4733
4769
  const labelWidth = this._label.measuredWidth;
4734
4770
  const labelHeight = this._label.measuredHeight;
4735
- const bgPadding = this._labelBackground?.padding ?? EDGE_LABEL_BACKGROUND_PADDING;
4736
4771
  const bgColor = this._labelBackground?.color ?? "#ffffff";
4737
4772
  const bgOpacity = this._labelBackground?.opacity ?? 1;
4738
4773
  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;
4774
+ const bgX = labelPosition.x - labelWidth / 2;
4775
+ const bgY = labelPosition.y - labelHeight / 2;
4776
+ const bgWidth = labelWidth;
4777
+ const bgHeight = labelHeight;
4743
4778
  ctx.fillStyle = bgColor;
4744
4779
  ctx.globalAlpha = bgOpacity;
4745
4780
  if (bgRadius > 0) {
@@ -4975,6 +5010,7 @@ class InteractionManager {
4975
5010
  this.overlayDragSession = null;
4976
5011
  this.handledOverlayMouseDown = false;
4977
5012
  this.renderer = options.renderer;
5013
+ this.navigationOnly = options.navigationOnly ?? false;
4978
5014
  this.inputHandler = new InputHandler({
4979
5015
  canvas: this.renderer.getCanvas(),
4980
5016
  screenToWorld: (x, y) => this.renderer.screenToWorld(x, y)
@@ -5044,6 +5080,7 @@ class InteractionManager {
5044
5080
  if (shallowEqual(before, after)) {
5045
5081
  return;
5046
5082
  }
5083
+ this.renderer.markStyleDirty();
5047
5084
  this.queuePropertyChange("node", nodeId, before, after);
5048
5085
  }
5049
5086
  changeEdgeProperties(edgeId, apply) {
@@ -5057,6 +5094,7 @@ class InteractionManager {
5057
5094
  if (shallowEqual(before, after)) {
5058
5095
  return;
5059
5096
  }
5097
+ this.renderer.markStyleDirty();
5060
5098
  this.queuePropertyChange("edge", edgeId, before, after);
5061
5099
  }
5062
5100
  changeGroupProperties(groupId, apply) {
@@ -5070,6 +5108,7 @@ class InteractionManager {
5070
5108
  if (shallowEqual(before, after)) {
5071
5109
  return;
5072
5110
  }
5111
+ this.renderer.markStyleDirty();
5073
5112
  this.queuePropertyChange("group", groupId, before, after);
5074
5113
  }
5075
5114
  removeNodeFromGroups(nodeId, groupIds) {
@@ -5123,13 +5162,15 @@ class InteractionManager {
5123
5162
  setupEvents(options) {
5124
5163
  this.overlayCleanup = this.renderer.addOverlayRenderer((ctx) => {
5125
5164
  this.selectionManager.renderSelectionRect(ctx);
5126
- this.dragManager.renderAlignmentGuides(ctx);
5127
- this.connectionManager.renderPreview(ctx);
5128
- this.connectionManager.renderHoverAnchors(ctx);
5129
- for (const id of this.selectionManager.selectedIds) {
5130
- const node = this.renderer.getNode(id);
5131
- if (node) {
5132
- node.renderResizeHandles(ctx);
5165
+ if (!this.navigationOnly) {
5166
+ this.dragManager.renderAlignmentGuides(ctx);
5167
+ this.connectionManager.renderPreview(ctx);
5168
+ this.connectionManager.renderHoverAnchors(ctx);
5169
+ for (const id of this.selectionManager.selectedIds) {
5170
+ const node = this.renderer.getNode(id);
5171
+ if (node) {
5172
+ node.renderResizeHandles(ctx);
5173
+ }
5133
5174
  }
5134
5175
  }
5135
5176
  });
@@ -5143,68 +5184,70 @@ class InteractionManager {
5143
5184
  this.inputHandler.on("pinch", (event) => this.handlePinch(event));
5144
5185
  this.inputHandler.on("keydown", (event) => this.handleKeyDown(event, options));
5145
5186
  this.inputHandler.on("keyup", (event) => this.handleKeyUp(event));
5146
- this.dragManager.on("dragstart", (nodeIds) => {
5147
- this.connectionManager.disableHover();
5148
- this.dragStartPositions.clear();
5149
- for (const id of nodeIds) {
5150
- const node = this.renderer.getNode(id);
5151
- if (node) {
5152
- this.dragStartPositions.set(id, { x: node.x, y: node.y });
5153
- }
5154
- }
5155
- });
5156
- this.dragManager.on("dragend", (nodeIds) => {
5157
- this.connectionManager.enableHover();
5158
- const nodePositions = /* @__PURE__ */ new Map();
5159
- for (const id of nodeIds) {
5160
- const node = this.renderer.getNode(id);
5161
- const before = this.dragStartPositions.get(id);
5162
- if (!node || !before) continue;
5163
- const after = { x: node.x, y: node.y };
5164
- if (before.x !== after.x || before.y !== after.y) {
5165
- nodePositions.set(id, { before, after });
5166
- }
5167
- }
5168
- if (nodePositions.size > 0) {
5169
- this.historyManager.execute(
5170
- new MoveNodesCommand((id) => this.renderer.getNode(id), nodePositions)
5171
- );
5172
- }
5173
- });
5174
- this.connectionManager.on("edgeReconnectStart", (edge, endpoint, original) => {
5175
- this.reconnectOrigins.set(edge.id, { endpoint, original: { ...original } });
5176
- });
5177
- this.connectionManager.on("edgeReconnect", (edge, endpoint) => {
5178
- const origin = this.reconnectOrigins.get(edge.id);
5179
- if (!origin || origin.endpoint !== endpoint) {
5180
- return;
5181
- }
5182
- const before = origin.original;
5183
- const after = endpoint === "start" ? edge.from : edge.to;
5184
- if (this.endpointsEqual(before, after)) {
5185
- this.reconnectOrigins.delete(edge.id);
5186
- return;
5187
- }
5188
- this.historyManager.execute({
5189
- execute: () => {
5190
- if (endpoint === "start") {
5191
- edge.from = { ...after };
5192
- } else {
5193
- edge.to = { ...after };
5187
+ if (!this.navigationOnly) {
5188
+ this.dragManager.on("dragstart", (nodeIds) => {
5189
+ this.connectionManager.disableHover();
5190
+ this.dragStartPositions.clear();
5191
+ for (const id of nodeIds) {
5192
+ const node = this.renderer.getNode(id);
5193
+ if (node) {
5194
+ this.dragStartPositions.set(id, { x: node.x, y: node.y });
5194
5195
  }
5195
- this.renderer.markDirty();
5196
- },
5197
- undo: () => {
5198
- if (endpoint === "start") {
5199
- edge.from = { ...before };
5200
- } else {
5201
- edge.to = { ...before };
5196
+ }
5197
+ });
5198
+ this.dragManager.on("dragend", (nodeIds) => {
5199
+ this.connectionManager.enableHover();
5200
+ const nodePositions = /* @__PURE__ */ new Map();
5201
+ for (const id of nodeIds) {
5202
+ const node = this.renderer.getNode(id);
5203
+ const before = this.dragStartPositions.get(id);
5204
+ if (!node || !before) continue;
5205
+ const after = { x: node.x, y: node.y };
5206
+ if (before.x !== after.x || before.y !== after.y) {
5207
+ nodePositions.set(id, { before, after });
5202
5208
  }
5203
- this.renderer.markDirty();
5209
+ }
5210
+ if (nodePositions.size > 0) {
5211
+ this.historyManager.execute(
5212
+ new MoveNodesCommand((id) => this.renderer.getNode(id), nodePositions)
5213
+ );
5204
5214
  }
5205
5215
  });
5206
- this.reconnectOrigins.delete(edge.id);
5207
- });
5216
+ this.connectionManager.on("edgeReconnectStart", (edge, endpoint, original) => {
5217
+ this.reconnectOrigins.set(edge.id, { endpoint, original: { ...original } });
5218
+ });
5219
+ this.connectionManager.on("edgeReconnect", (edge, endpoint) => {
5220
+ const origin = this.reconnectOrigins.get(edge.id);
5221
+ if (!origin || origin.endpoint !== endpoint) {
5222
+ return;
5223
+ }
5224
+ const before = origin.original;
5225
+ const after = endpoint === "start" ? edge.from : edge.to;
5226
+ if (this.endpointsEqual(before, after)) {
5227
+ this.reconnectOrigins.delete(edge.id);
5228
+ return;
5229
+ }
5230
+ this.historyManager.execute({
5231
+ execute: () => {
5232
+ if (endpoint === "start") {
5233
+ edge.from = { ...after };
5234
+ } else {
5235
+ edge.to = { ...after };
5236
+ }
5237
+ this.renderer.markDirty();
5238
+ },
5239
+ undo: () => {
5240
+ if (endpoint === "start") {
5241
+ edge.from = { ...before };
5242
+ } else {
5243
+ edge.to = { ...before };
5244
+ }
5245
+ this.renderer.markDirty();
5246
+ }
5247
+ });
5248
+ this.reconnectOrigins.delete(edge.id);
5249
+ });
5250
+ }
5208
5251
  this.historyManager.on("change", () => {
5209
5252
  this.renderer.markDirty();
5210
5253
  });
@@ -5212,14 +5255,16 @@ class InteractionManager {
5212
5255
  handleMouseDown(event) {
5213
5256
  this.handledScrollbarMouseDown = false;
5214
5257
  this.handledOverlayMouseDown = false;
5215
- if (this.resizeManager.handleMouseDown(event)) {
5216
- return;
5217
- }
5218
- if (this.connectionManager.tryStartReconnection(event)) {
5219
- return;
5220
- }
5221
- if (this.connectionManager.tryStartConnectionAtPoint(event)) {
5222
- return;
5258
+ if (!this.navigationOnly) {
5259
+ if (this.resizeManager.handleMouseDown(event)) {
5260
+ return;
5261
+ }
5262
+ if (this.connectionManager.tryStartReconnection(event)) {
5263
+ return;
5264
+ }
5265
+ if (this.connectionManager.tryStartConnectionAtPoint(event)) {
5266
+ return;
5267
+ }
5223
5268
  }
5224
5269
  const overlayDrag = this.renderer.beginOverlayDrag(event.screenX, event.screenY);
5225
5270
  if (overlayDrag) {
@@ -5253,6 +5298,9 @@ class InteractionManager {
5253
5298
  if (this.navigationManager.handleMouseDown(event)) {
5254
5299
  return;
5255
5300
  }
5301
+ if (this.navigationOnly) {
5302
+ return;
5303
+ }
5256
5304
  if (this.dragManager.handleMouseDown(event)) {
5257
5305
  return;
5258
5306
  }
@@ -5272,6 +5320,9 @@ class InteractionManager {
5272
5320
  }
5273
5321
  }
5274
5322
  const overScrollbar = this.renderer.updateScrollbarHover(event.screenX, event.screenY);
5323
+ this.renderer.updateBadgeHover(
5324
+ overScrollbar ? { x: -1e9, y: -1e9 } : { x: event.worldX, y: event.worldY }
5325
+ );
5275
5326
  if (this.overlayDragSession) {
5276
5327
  const moved = this.renderer.updateOverlayDrag(
5277
5328
  this.overlayDragSession,
@@ -5300,14 +5351,16 @@ class InteractionManager {
5300
5351
  if (overScrollbar) {
5301
5352
  return;
5302
5353
  }
5303
- if (this.resizeManager.handleMouseMove(event)) {
5304
- return;
5305
- }
5306
- if (this.connectionManager.handleMouseMove(event)) {
5307
- return;
5308
- }
5309
- if (this.dragManager.handleMouseMove(event)) {
5310
- return;
5354
+ if (!this.navigationOnly) {
5355
+ if (this.resizeManager.handleMouseMove(event)) {
5356
+ return;
5357
+ }
5358
+ if (this.connectionManager.handleMouseMove(event)) {
5359
+ return;
5360
+ }
5361
+ if (this.dragManager.handleMouseMove(event)) {
5362
+ return;
5363
+ }
5311
5364
  }
5312
5365
  if (this.selectionManager.selectionRectangle !== null) {
5313
5366
  this.selectionManager.updateSelectionRect({ x: event.worldX, y: event.worldY });
@@ -5326,14 +5379,16 @@ class InteractionManager {
5326
5379
  this.renderer.setScrollbarActiveAxis(null);
5327
5380
  return;
5328
5381
  }
5329
- if (this.resizeManager.handleMouseUp()) {
5330
- return;
5331
- }
5332
- if (this.connectionManager.handleMouseUp(event)) {
5333
- return;
5334
- }
5335
- if (this.dragManager.handleMouseUp(event)) {
5336
- return;
5382
+ if (!this.navigationOnly) {
5383
+ if (this.resizeManager.handleMouseUp()) {
5384
+ return;
5385
+ }
5386
+ if (this.connectionManager.handleMouseUp(event)) {
5387
+ return;
5388
+ }
5389
+ if (this.dragManager.handleMouseUp(event)) {
5390
+ return;
5391
+ }
5337
5392
  }
5338
5393
  if (this.selectionManager.selectionRectangle !== null) {
5339
5394
  this.selectionManager.endSelectionRect();
@@ -5359,6 +5414,9 @@ class InteractionManager {
5359
5414
  if (this.dragManager.handledMouseDown || this.resizeManager.handledMouseDown || this.connectionManager.connecting) {
5360
5415
  return;
5361
5416
  }
5417
+ if (this.navigationOnly) {
5418
+ return;
5419
+ }
5362
5420
  if (this.connectionManager.handleDoubleClick(event)) {
5363
5421
  return;
5364
5422
  }
@@ -5421,16 +5479,19 @@ class InteractionManager {
5421
5479
  handleKeyDown(event, options) {
5422
5480
  const isCtrlOrMeta = event.ctrlKey || event.metaKey;
5423
5481
  const key = event.code.startsWith("Key") ? event.code.slice(3).toLowerCase() : event.key.toLowerCase();
5482
+ this.navigationManager.handleKeyDown(event);
5483
+ if (this.handleViewportNavigationKey(event)) {
5484
+ return;
5485
+ }
5486
+ if (this.navigationOnly) {
5487
+ return;
5488
+ }
5424
5489
  if (isCtrlOrMeta && (key === "z" || key === "y")) {
5425
5490
  this.flushPendingPropertyChanges();
5426
5491
  }
5427
5492
  if (this.historyManager.handleKeyDown(event)) {
5428
5493
  return;
5429
5494
  }
5430
- this.navigationManager.handleKeyDown(event);
5431
- if (this.handleViewportNavigationKey(event)) {
5432
- return;
5433
- }
5434
5495
  if (this.keymap.deleteKeys.includes(event.key)) {
5435
5496
  event.preventDefault();
5436
5497
  this.deleteSelection();
@@ -5560,6 +5621,7 @@ class InteractionManager {
5560
5621
  pending.after
5561
5622
  )
5562
5623
  );
5624
+ this.renderer.markStyleDirty();
5563
5625
  break;
5564
5626
  case "edge":
5565
5627
  this.historyManager.execute(
@@ -5570,6 +5632,7 @@ class InteractionManager {
5570
5632
  pending.after
5571
5633
  )
5572
5634
  );
5635
+ this.renderer.markStyleDirty();
5573
5636
  break;
5574
5637
  case "group":
5575
5638
  this.historyManager.execute(
@@ -5580,6 +5643,7 @@ class InteractionManager {
5580
5643
  pending.after
5581
5644
  )
5582
5645
  );
5646
+ this.renderer.markStyleDirty();
5583
5647
  break;
5584
5648
  }
5585
5649
  }
@@ -6680,6 +6744,30 @@ class DiagramRenderer extends EventEmitter {
6680
6744
  }
6681
6745
  return void 0;
6682
6746
  }
6747
+ /**
6748
+ * Update badge hover state and canvas cursor based on pointer position.
6749
+ * Call from mousemove to show hover highlight and pointer cursor over badges.
6750
+ */
6751
+ updateBadgeHover(worldPoint) {
6752
+ const element = this.getElementAtPoint(worldPoint);
6753
+ let hoveredNodeId = null;
6754
+ let hoveredIndex = -1;
6755
+ const node = element;
6756
+ if (element && typeof node.getBadgeAtPoint === "function") {
6757
+ const badge = node.getBadgeAtPoint(worldPoint);
6758
+ if (badge !== null) {
6759
+ hoveredNodeId = node.id;
6760
+ hoveredIndex = badge.index;
6761
+ }
6762
+ }
6763
+ const cursor = hoveredNodeId !== null ? "pointer" : "";
6764
+ if (this.canvas.style.cursor !== cursor) {
6765
+ this.canvas.style.cursor = cursor;
6766
+ }
6767
+ for (const n of this._nodes.values()) {
6768
+ n.setBadgeHover(n.id === hoveredNodeId ? hoveredIndex : -1);
6769
+ }
6770
+ }
6683
6771
  /**
6684
6772
  * Mark the diagram as needing re-render
6685
6773
  */
@@ -7992,11 +8080,11 @@ function tintSvg(svgText, strokeColor, fillColor) {
7992
8080
  const all = [root, ...Array.from(root.querySelectorAll("*"))];
7993
8081
  for (const el of all) {
7994
8082
  const stroke = el.getAttribute("stroke");
7995
- if (strokeColor && stroke !== null && stroke.toLowerCase() !== "none") {
8083
+ const fill = el.getAttribute("fill");
8084
+ if (strokeColor && (stroke === null || stroke.toLowerCase() !== "none")) {
7996
8085
  el.setAttribute("stroke", strokeColor);
7997
8086
  }
7998
- const fill = el.getAttribute("fill");
7999
- if (fillColor && fill !== null && fill.toLowerCase() !== "none") {
8087
+ if (fillColor && (fill === null || fill.toLowerCase() !== "none")) {
8000
8088
  el.setAttribute("fill", fillColor);
8001
8089
  }
8002
8090
  const style = el.getAttribute("style");
@@ -8045,8 +8133,10 @@ class NodeImage {
8045
8133
  get placement() {
8046
8134
  return this._options.placement ?? "center";
8047
8135
  }
8048
- get gap() {
8049
- return this._options.gap ?? 6;
8136
+ /** Inset from edge of icon zone to image (single value for all sides) */
8137
+ get inset() {
8138
+ const v = this._options.inset ?? this._options.margin ?? this._options.padding;
8139
+ return v !== void 0 && Number.isFinite(v) ? Math.max(0, v) : 6;
8050
8140
  }
8051
8141
  setSource(source) {
8052
8142
  this._loaded = false;
@@ -8061,23 +8151,18 @@ class NodeImage {
8061
8151
  if (!this._loaded) {
8062
8152
  return;
8063
8153
  }
8064
- const padding = this._options.padding ?? 8;
8065
- const margin = Math.max(0, this._options.margin ?? 0);
8154
+ const ins = this.inset;
8066
8155
  const fit = this._options.fit ?? "none";
8067
8156
  const scaleWithBounds = this._options.scaleWithBounds ?? false;
8068
8157
  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
8158
  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)
8159
+ x: bounds.x + ins,
8160
+ y: bounds.y + ins,
8161
+ width: Math.max(0, bounds.width - ins * 2),
8162
+ height: Math.max(0, bounds.height - ins * 2)
8078
8163
  };
8079
- const availableWidth = Math.max(0, innerBounds.width - padding * 2);
8080
- const availableHeight = Math.max(0, innerBounds.height - padding * 2);
8164
+ const availableWidth = Math.max(0, innerBounds.width);
8165
+ const availableHeight = Math.max(0, innerBounds.height);
8081
8166
  let drawWidth = this._options.width ?? this._naturalWidth;
8082
8167
  let drawHeight = this._options.height ?? this._naturalHeight;
8083
8168
  if (scaleWithBounds) {
@@ -8096,21 +8181,13 @@ class NodeImage {
8096
8181
  const maxHeight = Math.max(0, availableHeight);
8097
8182
  drawWidth = Math.min(drawWidth, maxWidth);
8098
8183
  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
- }
8184
+ let x = innerBounds.x;
8185
+ let y = innerBounds.y;
8186
+ x = innerBounds.x + (innerBounds.width - drawWidth) / 2;
8187
+ y = innerBounds.y + (innerBounds.height - drawHeight) / 2;
8111
8188
  ctx.save();
8112
8189
  ctx.globalAlpha = opacity;
8113
- ctx.drawImage(this._image, x + offsetX, y + offsetY, drawWidth, drawHeight);
8190
+ ctx.drawImage(this._image, x, y, drawWidth, drawHeight);
8114
8191
  ctx.restore();
8115
8192
  }
8116
8193
  getSize() {
@@ -8174,6 +8251,9 @@ class NodeImage {
8174
8251
  };
8175
8252
  }
8176
8253
  }
8254
+ const BADGE_SIZE = 15;
8255
+ const BADGE_OFFSET = 4;
8256
+ const BADGE_GAP = 4;
8177
8257
  function isValidAnchorId(id) {
8178
8258
  return /^(top|right|bottom|left):\d+$/.test(id);
8179
8259
  }
@@ -8195,13 +8275,16 @@ class Node extends Element {
8195
8275
  styleClass: options.styleClass
8196
8276
  });
8197
8277
  this._ports = [];
8278
+ this._badges = [];
8198
8279
  this._anchorCache = null;
8280
+ this._badgeImageCache = /* @__PURE__ */ new Map();
8281
+ this._hoveredBadgeIndex = -1;
8199
8282
  this._defaultSize = { width: options.width, height: options.height };
8200
8283
  this._nodeStyle = { ...DEFAULT_NODE_STYLE, ...options.style };
8201
8284
  this._showPortsAlways = options.showPortsAlways ?? false;
8202
8285
  this._anchorPoints = this.normalizeAnchorPoints(options.anchorPoints);
8203
- this._labelPlacement = options.labelPlacement ?? "auto";
8204
8286
  this._resizeHandlesEnabled = options.resizeHandlesEnabled ?? true;
8287
+ this._contentInset = this.normalizeContentInset(options.contentInset);
8205
8288
  if (options.label !== void 0) {
8206
8289
  if (typeof options.label === "string") {
8207
8290
  this._label = new TextLabel({
@@ -8220,6 +8303,10 @@ class Node extends Element {
8220
8303
  this.addPort(portOptions);
8221
8304
  }
8222
8305
  }
8306
+ if (options.badges !== void 0 && options.badges.length > 0) {
8307
+ this._badges = options.badges.map((b) => ({ id: b.id, iconUrl: b.iconUrl }));
8308
+ this.ensureBadgeImagesLoaded();
8309
+ }
8223
8310
  }
8224
8311
  /**
8225
8312
  * Node style
@@ -8285,6 +8372,46 @@ class Node extends Element {
8285
8372
  }
8286
8373
  this.markDirty();
8287
8374
  }
8375
+ /**
8376
+ * Badges shown in top-left corner of node (e.g. interactive property icons)
8377
+ */
8378
+ get badges() {
8379
+ return this._badges;
8380
+ }
8381
+ set badges(value) {
8382
+ this._badges = Array.isArray(value) ? value.map((b) => ({ id: b.id, iconUrl: b.iconUrl })) : [];
8383
+ this.ensureBadgeImagesLoaded();
8384
+ this.markDirty();
8385
+ }
8386
+ /**
8387
+ * Set which badge index is under the pointer (-1 for none). Used for hover highlight and cursor.
8388
+ */
8389
+ setBadgeHover(index) {
8390
+ if (this._hoveredBadgeIndex === index) return;
8391
+ this._hoveredBadgeIndex = index;
8392
+ this.markDirty();
8393
+ }
8394
+ /**
8395
+ * Return badge at world point, or null if point is not over a badge.
8396
+ */
8397
+ getBadgeAtPoint(worldPoint) {
8398
+ if (this._badges.length === 0) {
8399
+ return null;
8400
+ }
8401
+ const bounds = this.getBounds();
8402
+ const contentBounds = this.getLabelContainerBounds(bounds);
8403
+ const localX = worldPoint.x - (contentBounds.x + BADGE_OFFSET);
8404
+ const localY = worldPoint.y - (contentBounds.y + BADGE_OFFSET);
8405
+ for (let i = 0; i < this._badges.length; i++) {
8406
+ const badge = this._badges[i];
8407
+ if (badge === void 0) continue;
8408
+ const x = i * (BADGE_SIZE + BADGE_GAP);
8409
+ if (localX >= x && localX <= x + BADGE_SIZE && localY >= 0 && localY <= BADGE_SIZE) {
8410
+ return { id: badge.id, index: i };
8411
+ }
8412
+ }
8413
+ return null;
8414
+ }
8288
8415
  /**
8289
8416
  * Add a port to this node
8290
8417
  */
@@ -8398,24 +8525,22 @@ class Node extends Element {
8398
8525
  this._anchorPoints = this.normalizeAnchorPoints(value);
8399
8526
  this.markDirty();
8400
8527
  }
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
8528
  /**
8414
8529
  * Default size (initial width/height)
8415
8530
  */
8416
8531
  get defaultSize() {
8417
8532
  return { ...this._defaultSize };
8418
8533
  }
8534
+ /**
8535
+ * Content area insets (per side). When all zero, content area = node bounds.
8536
+ */
8537
+ get contentInset() {
8538
+ return { ...this._contentInset };
8539
+ }
8540
+ set contentInset(value) {
8541
+ this._contentInset = this.normalizeContentInset(value);
8542
+ this.markDirty();
8543
+ }
8419
8544
  /**
8420
8545
  * Set getter for attachToOutline (called by DiagramRenderer when node is added)
8421
8546
  */
@@ -8438,87 +8563,20 @@ class Node extends Element {
8438
8563
  }
8439
8564
  }
8440
8565
  /**
8441
- * Calculate layout for icon and label
8442
- * Returns computed bounds for both elements
8566
+ * Calculate layout for icon and label.
8567
+ * Label always gets full content area; icon is placed within the same content area.
8443
8568
  */
8444
- calculateContentLayout(bounds, iconBoxSize, labelSize) {
8445
- let iconBounds = bounds;
8446
- let labelBounds = this.getLabelContainerBounds(bounds);
8569
+ calculateContentLayout(bounds, iconBoxSize, _labelSize) {
8570
+ const contentBounds = this.getLabelContainerBounds(bounds);
8571
+ let iconBounds = contentBounds;
8447
8572
  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);
8573
+ iconBounds = this.getIconBounds(
8574
+ contentBounds,
8575
+ iconBoxSize,
8576
+ this._icon.placement
8577
+ );
8520
8578
  }
8521
- return { iconBounds, labelBounds };
8579
+ return { iconBounds, labelBounds: contentBounds };
8522
8580
  }
8523
8581
  /**
8524
8582
  * Get label bounds and wrapped lines for SVG export. Replicates renderContents layout logic.
@@ -8529,12 +8587,19 @@ class Node extends Element {
8529
8587
  return null;
8530
8588
  }
8531
8589
  const bounds = this.getBounds();
8590
+ const contentBounds = this.getLabelContainerBounds(bounds);
8532
8591
  const iconBoxSize = this._icon ? this.getIconBoxSize() : void 0;
8533
- const autoBounds = this.getAutoLabelBounds(bounds, iconBoxSize);
8534
- this._label.setAutoMaxWidth(autoBounds.width);
8592
+ this._label.setAutoMaxWidth(contentBounds.width);
8535
8593
  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));
8594
+ const { labelBounds } = this.calculateContentLayout(
8595
+ bounds,
8596
+ iconBoxSize,
8597
+ labelSize
8598
+ );
8599
+ const lines = this._label.getWrappedLines(
8600
+ ctx,
8601
+ Math.max(0, contentBounds.width)
8602
+ );
8538
8603
  return { bounds: labelBounds, lines };
8539
8604
  }
8540
8605
  /**
@@ -8550,23 +8615,14 @@ class Node extends Element {
8550
8615
  ctx.globalAlpha = 1;
8551
8616
  }
8552
8617
  /**
8553
- * Get world position of label center.
8618
+ * Get world position of label center (center of content area).
8554
8619
  */
8555
8620
  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
- }
8621
+ const bounds = this.getLabelContainerBounds(this.getBounds());
8622
+ return {
8623
+ x: bounds.x + bounds.width / 2,
8624
+ y: bounds.y + bounds.height / 2
8625
+ };
8570
8626
  }
8571
8627
  /**
8572
8628
  * Render icon, label, and ports
@@ -8575,16 +8631,17 @@ class Node extends Element {
8575
8631
  ctx.setLineDash([]);
8576
8632
  ctx.lineDashOffset = 0;
8577
8633
  let bounds = this.getBounds();
8634
+ this.renderBadges(ctx, bounds);
8578
8635
  const iconBoxSize = this._icon ? this.getIconBoxSize() : void 0;
8579
8636
  if (this._label) {
8580
- this._label.setAutoMaxWidth(this.getAutoLabelBounds(bounds, iconBoxSize).width);
8637
+ this._label.setAutoMaxWidth(this.getLabelContainerBounds(bounds).width);
8581
8638
  }
8582
8639
  const labelSize = this._label ? this._label.measure(ctx) : void 0;
8583
8640
  if (labelSize || iconBoxSize) {
8584
8641
  this.ensureContentsFit(labelSize, iconBoxSize);
8585
8642
  if (this._label && iconBoxSize) {
8586
8643
  bounds = this.getBounds();
8587
- this._label.setAutoMaxWidth(this.getAutoLabelBounds(bounds, iconBoxSize).width);
8644
+ this._label.setAutoMaxWidth(this.getLabelContainerBounds(bounds).width);
8588
8645
  }
8589
8646
  }
8590
8647
  const { iconBounds, labelBounds } = this.calculateContentLayout(
@@ -8598,6 +8655,67 @@ class Node extends Element {
8598
8655
  this.renderLabel(ctx, labelBounds);
8599
8656
  this.renderPorts(ctx);
8600
8657
  }
8658
+ ensureBadgeImagesLoaded() {
8659
+ for (const badge of this._badges) {
8660
+ const url = badge.iconUrl;
8661
+ if (!url || this._badgeImageCache.has(url)) {
8662
+ continue;
8663
+ }
8664
+ const img = new Image();
8665
+ img.decoding = "async";
8666
+ this._badgeImageCache.set(url, { img, loaded: false });
8667
+ img.onload = () => {
8668
+ const entry = this._badgeImageCache.get(url);
8669
+ if (entry) {
8670
+ entry.loaded = true;
8671
+ this.markDirty();
8672
+ }
8673
+ };
8674
+ img.onerror = () => {
8675
+ this.markDirty();
8676
+ };
8677
+ img.src = url;
8678
+ }
8679
+ }
8680
+ renderBadges(ctx, bounds) {
8681
+ if (this._badges.length === 0) {
8682
+ return;
8683
+ }
8684
+ const contentBounds = this.getLabelContainerBounds(bounds);
8685
+ const x0 = contentBounds.x + BADGE_OFFSET;
8686
+ const y0 = contentBounds.y + BADGE_OFFSET;
8687
+ const radius = 2;
8688
+ for (let i = 0; i < this._badges.length; i++) {
8689
+ const badge = this._badges[i];
8690
+ if (badge === void 0) continue;
8691
+ const x = x0 + i * (BADGE_SIZE + BADGE_GAP);
8692
+ const isHovered = this._hoveredBadgeIndex === i;
8693
+ if (isHovered) {
8694
+ ctx.save();
8695
+ ctx.fillStyle = "rgba(0, 0, 0, 0.08)";
8696
+ ctx.beginPath();
8697
+ ctx.roundRect(x, y0, BADGE_SIZE, BADGE_SIZE, radius);
8698
+ ctx.fill();
8699
+ ctx.restore();
8700
+ }
8701
+ const entry = this._badgeImageCache.get(badge.iconUrl);
8702
+ if (entry?.loaded && entry.img.naturalWidth > 0) {
8703
+ ctx.save();
8704
+ const img = entry.img;
8705
+ const sw = img.naturalWidth;
8706
+ const sh = img.naturalHeight;
8707
+ const scale = Math.min(BADGE_SIZE / sw, BADGE_SIZE / sh, 1);
8708
+ const dw = sw * scale;
8709
+ const dh = sh * scale;
8710
+ const dx = x + (BADGE_SIZE - dw) / 2;
8711
+ const dy = y0 + (BADGE_SIZE - dh) / 2;
8712
+ ctx.imageSmoothingEnabled = true;
8713
+ ctx.imageSmoothingQuality = "high";
8714
+ ctx.drawImage(img, 0, 0, sw, sh, dx, dy, dw, dh);
8715
+ ctx.restore();
8716
+ }
8717
+ }
8718
+ }
8601
8719
  /**
8602
8720
  * Minimal size required to fit current contents
8603
8721
  */
@@ -8869,124 +8987,45 @@ class Node extends Element {
8869
8987
  }
8870
8988
  }
8871
8989
  calculateContentMinSize(labelSize, iconBoxSize, bounds) {
8872
- let minWidth = 0;
8873
- let minHeight = 0;
8874
8990
  const labelContainer = this.getLabelContainerBounds(bounds);
8875
8991
  const widthFactor = labelContainer.width > 0 ? bounds.width / labelContainer.width : 1;
8876
8992
  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 };
8993
+ const minContentWidth = Math.max(
8994
+ labelSize?.width ?? 0,
8995
+ iconBoxSize?.width ?? 0
8996
+ );
8997
+ const minContentHeight = Math.max(
8998
+ labelSize?.height ?? 0,
8999
+ iconBoxSize?.height ?? 0
9000
+ );
9001
+ return {
9002
+ width: minContentWidth * widthFactor,
9003
+ height: minContentHeight * heightFactor
9004
+ };
8923
9005
  }
8924
9006
  getLabelContainerBounds(bounds) {
8925
- return bounds;
9007
+ const { top, right, bottom, left } = this._contentInset;
9008
+ const x = bounds.x + left;
9009
+ const y = bounds.y + top;
9010
+ const width = Math.max(0, bounds.width - left - right);
9011
+ const height = Math.max(0, bounds.height - top - bottom);
9012
+ return { x, y, width, height };
8926
9013
  }
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;
9014
+ normalizeContentInset(value) {
9015
+ const n = (v) => v !== void 0 && Number.isFinite(v) ? Math.max(0, v) : 0;
9016
+ if (value === void 0) {
9017
+ return { top: 0, right: 0, bottom: 0, left: 0 };
8967
9018
  }
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;
9019
+ if (typeof value === "number") {
9020
+ const v = n(value);
9021
+ return { top: v, right: v, bottom: v, left: v };
8988
9022
  }
8989
- return { x, y, width, height };
9023
+ return {
9024
+ top: n(value.top),
9025
+ right: n(value.right),
9026
+ bottom: n(value.bottom),
9027
+ left: n(value.left)
9028
+ };
8990
9029
  }
8991
9030
  getIconBoxSize() {
8992
9031
  if (!this._icon) {
@@ -8996,14 +9035,14 @@ class Node extends Element {
8996
9035
  if (width <= 0 || height <= 0) {
8997
9036
  return void 0;
8998
9037
  }
8999
- const padding = this._icon.options.padding ?? 8;
9000
- const margin = Math.max(0, this._icon.options.margin ?? 0);
9038
+ const ins = this._icon.inset;
9001
9039
  return {
9002
- width: width + padding * 2 + margin * 2,
9003
- height: height + padding * 2 + margin * 2
9040
+ width: width + ins * 2,
9041
+ height: height + ins * 2
9004
9042
  };
9005
9043
  }
9006
9044
  getIconBounds(bounds, iconBoxSize, placement) {
9045
+ const ins = this._icon?.inset ?? 0;
9007
9046
  switch (placement) {
9008
9047
  case "top":
9009
9048
  return {
@@ -9035,29 +9074,29 @@ class Node extends Element {
9035
9074
  };
9036
9075
  case "top-left":
9037
9076
  return {
9038
- x: bounds.x,
9039
- y: bounds.y,
9077
+ x: bounds.x + ins,
9078
+ y: bounds.y + ins,
9040
9079
  width: iconBoxSize.width,
9041
9080
  height: iconBoxSize.height
9042
9081
  };
9043
9082
  case "top-right":
9044
9083
  return {
9045
- x: bounds.x + bounds.width - iconBoxSize.width,
9046
- y: bounds.y,
9084
+ x: bounds.x + bounds.width - iconBoxSize.width - ins,
9085
+ y: bounds.y + ins,
9047
9086
  width: iconBoxSize.width,
9048
9087
  height: iconBoxSize.height
9049
9088
  };
9050
9089
  case "bottom-left":
9051
9090
  return {
9052
- x: bounds.x,
9053
- y: bounds.y + bounds.height - iconBoxSize.height,
9091
+ x: bounds.x + ins,
9092
+ y: bounds.y + bounds.height - iconBoxSize.height - ins,
9054
9093
  width: iconBoxSize.width,
9055
9094
  height: iconBoxSize.height
9056
9095
  };
9057
9096
  case "bottom-right":
9058
9097
  return {
9059
- x: bounds.x + bounds.width - iconBoxSize.width,
9060
- y: bounds.y + bounds.height - iconBoxSize.height,
9098
+ x: bounds.x + bounds.width - iconBoxSize.width - ins,
9099
+ y: bounds.y + bounds.height - iconBoxSize.height - ins,
9061
9100
  width: iconBoxSize.width,
9062
9101
  height: iconBoxSize.height
9063
9102
  };
@@ -9956,18 +9995,14 @@ const DEFAULT_THEME = {
9956
9995
  cornerRadius: 4
9957
9996
  },
9958
9997
  selected: {
9959
- fillColor: "#ffffff",
9960
- strokeColor: "#333333",
9998
+ strokeColor: "#3b82f6",
9961
9999
  strokeWidth: 2,
9962
- opacity: 1,
9963
- cornerRadius: 4
10000
+ opacity: 1
9964
10001
  },
9965
10002
  dragging: {
9966
- fillColor: "#f0f0f0",
9967
10003
  strokeColor: "#333333",
9968
10004
  strokeWidth: 2,
9969
- opacity: 0.8,
9970
- cornerRadius: 4
10005
+ opacity: 0.8
9971
10006
  }
9972
10007
  },
9973
10008
  edge: {
@@ -10228,15 +10263,16 @@ class StyleManager {
10228
10263
  return baseStyle;
10229
10264
  }
10230
10265
  getBaseNodeStyle(state) {
10266
+ const d = this._theme.node.default;
10231
10267
  switch (state) {
10232
10268
  case "hover":
10233
- return this._theme.node.hover;
10269
+ return { ...d, ...this._theme.node.hover };
10234
10270
  case "selected":
10235
- return this._theme.node.selected;
10271
+ return { ...d, ...this._theme.node.selected };
10236
10272
  case "dragging":
10237
- return this._theme.node.dragging;
10273
+ return { ...d, ...this._theme.node.dragging };
10238
10274
  default:
10239
- return this._theme.node.default;
10275
+ return d;
10240
10276
  }
10241
10277
  }
10242
10278
  getBaseEdgeStyle(state) {
@@ -10434,13 +10470,16 @@ class Serializer {
10434
10470
  }));
10435
10471
  let label;
10436
10472
  if (node.label) {
10437
- const labelDefaults = { padding: 8, margin: 0, style: {}, maxWidth: void 0, styleClass: void 0 };
10473
+ const ins = node.label.inset;
10474
+ const defaultInset = 8;
10475
+ const insetAllDefault = ins.top === defaultInset && ins.right === defaultInset && ins.bottom === defaultInset && ins.left === defaultInset;
10476
+ 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 };
10477
+ const labelDefaults = { inset: void 0, style: {}, maxWidth: void 0, styleClass: void 0 };
10438
10478
  const labelData = {
10439
10479
  text: node.label.text,
10440
10480
  style: node.label.style,
10441
10481
  maxWidth: node.label.maxWidth,
10442
- padding: node.label.padding,
10443
- margin: node.label.margin,
10482
+ inset: serializedInset ?? void 0,
10444
10483
  styleClass: node.label.styleClass
10445
10484
  };
10446
10485
  const hasExtendedOptions = hasNonDefaultValues(labelData, labelDefaults);
@@ -10449,8 +10488,7 @@ class Serializer {
10449
10488
  text: node.label.text,
10450
10489
  style: Object.keys(node.label.style).length > 0 ? node.label.style : void 0,
10451
10490
  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,
10491
+ inset: serializedInset,
10454
10492
  styleClass: node.label.styleClass
10455
10493
  });
10456
10494
  } else {
@@ -10462,22 +10500,16 @@ class Serializer {
10462
10500
  const opts = node.icon.options;
10463
10501
  const source = typeof opts.source === "string" ? opts.source : void 0;
10464
10502
  if (source) {
10465
- icon = {
10503
+ icon = omitEmptyValues({
10466
10504
  source,
10467
10505
  width: opts.width,
10468
10506
  height: opts.height,
10469
10507
  fit: opts.fit,
10470
10508
  placement: opts.placement,
10471
10509
  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
- };
10510
+ inset: node.icon.inset !== 6 ? node.icon.inset : void 0,
10511
+ opacity: opts.opacity
10512
+ });
10481
10513
  }
10482
10514
  }
10483
10515
  let anchorPoints;
@@ -10487,7 +10519,9 @@ class Serializer {
10487
10519
  if (hasNonDefaultValues(anchorData, anchorDefaults)) {
10488
10520
  anchorPoints = omitDefaultValues(anchorData, anchorDefaults);
10489
10521
  }
10490
- return {
10522
+ const contentInset = node.contentInset;
10523
+ const hasContentInset = contentInset.top !== 0 || contentInset.right !== 0 || contentInset.bottom !== 0 || contentInset.left !== 0;
10524
+ return omitEmptyValues({
10491
10525
  id: node.id,
10492
10526
  type: node.typeName,
10493
10527
  x: node.x,
@@ -10498,12 +10532,12 @@ class Serializer {
10498
10532
  styleClass: node.styleClass,
10499
10533
  label,
10500
10534
  labelStyleClass: typeof label === "string" ? node.label?.styleClass : void 0,
10501
- labelPlacement: node.labelPlacement !== "auto" ? node.labelPlacement : void 0,
10502
10535
  icon,
10536
+ contentInset: hasContentInset ? contentInset : void 0,
10503
10537
  anchorPoints,
10504
10538
  ports: ports.length > 0 ? ports : void 0,
10505
10539
  data: Object.keys(node.data).length > 0 ? node.data : void 0
10506
- };
10540
+ });
10507
10541
  }
10508
10542
  serializeEdge(edge) {
10509
10543
  return {
@@ -10814,7 +10848,7 @@ class SvgExporter {
10814
10848
  const parts = [];
10815
10849
  const renderedEdges = [];
10816
10850
  parts.push(
10817
- `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">`
10851
+ `<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
10852
  );
10819
10853
  for (const edge of this.renderer.edges.values()) {
10820
10854
  if (edge.visible) {
@@ -10955,22 +10989,37 @@ class SvgExporter {
10955
10989
  if (!edge.label) {
10956
10990
  return "";
10957
10991
  }
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;
10992
+ const metrics = this.measureTextLabel(edge.label.text, edge.label.style, edge.label.inset);
10960
10993
  const bgColor = edge.labelBackground?.color ?? "#ffffff";
10961
10994
  const bgOpacity = edge.labelBackground?.opacity ?? 1;
10962
10995
  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;
10996
+ const x = point.x - metrics.width / 2;
10997
+ const y = point.y - metrics.height / 2;
10998
+ const width = metrics.width;
10999
+ const height = metrics.height;
10967
11000
  const radius = Math.max(0, Math.min(bgRadius, width / 2, height / 2));
10968
11001
  if (radius <= 0) {
10969
11002
  return `<rect x="${x}" y="${y}" width="${width}" height="${height}" fill="${bgColor}" fill-opacity="${bgOpacity}"/>`;
10970
11003
  }
10971
11004
  return `<rect x="${x}" y="${y}" width="${width}" height="${height}" rx="${radius}" ry="${radius}" fill="${bgColor}" fill-opacity="${bgOpacity}"/>`;
10972
11005
  }
10973
- measureTextLabel(text, style = {}, padding = 8, margin = 0) {
11006
+ normalizeLabelInset(value, defaultVal) {
11007
+ const n = (v) => v !== void 0 && Number.isFinite(v) ? Math.max(0, v) : defaultVal;
11008
+ if (value === void 0) {
11009
+ return { top: defaultVal, right: defaultVal, bottom: defaultVal, left: defaultVal };
11010
+ }
11011
+ if (typeof value === "number") {
11012
+ const v = n(value);
11013
+ return { top: v, right: v, bottom: v, left: v };
11014
+ }
11015
+ return {
11016
+ top: n(value.top),
11017
+ right: n(value.right),
11018
+ bottom: n(value.bottom),
11019
+ left: n(value.left)
11020
+ };
11021
+ }
11022
+ measureTextLabel(text, style = {}, inset = 8) {
10974
11023
  const fontSize = style.fontSize ?? 14;
10975
11024
  const fontFamily = style.fontFamily ?? "sans-serif";
10976
11025
  const fontWeight = style.fontWeight ?? "normal";
@@ -10991,11 +11040,10 @@ class SvgExporter {
10991
11040
  const longestLine = lines.reduce((max, line) => line.length > max.length ? line : max, "");
10992
11041
  maxWidth = longestLine.length * fontSize * 0.6;
10993
11042
  }
10994
- const resolvedPadding = Math.max(0, padding);
10995
- const resolvedMargin = Math.max(0, margin);
11043
+ const ins = this.normalizeLabelInset(inset, 8);
10996
11044
  return {
10997
- width: maxWidth + resolvedPadding * 2 + resolvedMargin * 2,
10998
- height: lines.length * lineHeight + resolvedPadding * 2 + resolvedMargin * 2
11045
+ width: maxWidth + ins.left + ins.right,
11046
+ height: lines.length * lineHeight + ins.top + ins.bottom
10999
11047
  };
11000
11048
  }
11001
11049
  resolveMarkerConfig(edge, side) {
@@ -11120,9 +11168,9 @@ class SvgExporter {
11120
11168
  return "";
11121
11169
  }
11122
11170
  const style = label.style;
11123
- const padding = Math.max(0, label.padding);
11124
- const margin = Math.max(0, label.margin);
11171
+ const ins = this.normalizeLabelInset(label.inset, 8);
11125
11172
  const align = style.align ?? "center";
11173
+ const verticalAlign = style.verticalAlign ?? "middle";
11126
11174
  const ctx = this.getMeasurementContext();
11127
11175
  let bounds;
11128
11176
  let lines;
@@ -11140,18 +11188,27 @@ class SvgExporter {
11140
11188
  lines = label.text.split("\n");
11141
11189
  }
11142
11190
  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)
11191
+ x: bounds.x + ins.left,
11192
+ y: bounds.y + ins.top,
11193
+ width: Math.max(0, bounds.width - ins.left - ins.right),
11194
+ height: Math.max(0, bounds.height - ins.top - ins.bottom)
11147
11195
  };
11148
11196
  let x = inner.x + inner.width / 2;
11149
11197
  if (align === "left") {
11150
- x = inner.x + padding;
11198
+ x = inner.x;
11151
11199
  } else if (align === "right") {
11152
- x = inner.x + inner.width - padding;
11200
+ x = inner.x + inner.width;
11201
+ }
11202
+ const lineHeight = (style.fontSize ?? 14) * 1.2;
11203
+ const totalHeight = lines.length * lineHeight;
11204
+ let y;
11205
+ if (verticalAlign === "top") {
11206
+ y = inner.y + totalHeight / 2;
11207
+ } else if (verticalAlign === "bottom") {
11208
+ y = inner.y + inner.height - totalHeight / 2;
11209
+ } else {
11210
+ y = inner.y + inner.height / 2;
11153
11211
  }
11154
- const y = inner.y + inner.height / 2;
11155
11212
  return this.renderTextLabel(lines.join("\n"), { x, y }, style);
11156
11213
  }
11157
11214
  getNodeCornerRadius(node, bounds) {
@@ -11168,9 +11225,15 @@ class SvgExporter {
11168
11225
  if (iconSize.width <= 0 || iconSize.height <= 0) {
11169
11226
  return "";
11170
11227
  }
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);
11228
+ const iconInset = icon.inset;
11229
+ const iconBoxSize = this.getIconBoxSize(iconSize, iconInset);
11230
+ const iconBounds = this.getIconBounds(
11231
+ nodeBounds,
11232
+ iconBoxSize,
11233
+ opts.placement ?? "center",
11234
+ iconInset
11235
+ );
11236
+ const drawRect = this.getIconDrawRect(iconBounds, opts, iconSize, iconInset);
11174
11237
  if (drawRect.width <= 0 || drawRect.height <= 0) {
11175
11238
  return "";
11176
11239
  }
@@ -11179,17 +11242,16 @@ class SvgExporter {
11179
11242
  return "";
11180
11243
  }
11181
11244
  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"/>`;
11245
+ const escapedHref = this.escapeAttribute(href);
11246
+ return `<image href="${escapedHref}" xlink:href="${escapedHref}" x="${drawRect.x}" y="${drawRect.y}" width="${drawRect.width}" height="${drawRect.height}" opacity="${opacity}" preserveAspectRatio="none"/>`;
11183
11247
  }
11184
- getIconBoxSize(opts, imageSize) {
11185
- const padding = opts.padding ?? 8;
11186
- const margin = Math.max(0, opts.margin ?? 0);
11248
+ getIconBoxSize(imageSize, inset) {
11187
11249
  return {
11188
- width: imageSize.width + padding * 2 + margin * 2,
11189
- height: imageSize.height + padding * 2 + margin * 2
11250
+ width: imageSize.width + inset * 2,
11251
+ height: imageSize.height + inset * 2
11190
11252
  };
11191
11253
  }
11192
- getIconBounds(bounds, iconBoxSize, placement) {
11254
+ getIconBounds(bounds, iconBoxSize, placement, inset) {
11193
11255
  switch (placement) {
11194
11256
  case "top":
11195
11257
  return { x: bounds.x, y: bounds.y, width: bounds.width, height: iconBoxSize.height };
@@ -11210,25 +11272,30 @@ class SvgExporter {
11210
11272
  height: bounds.height
11211
11273
  };
11212
11274
  case "top-left":
11213
- return { x: bounds.x, y: bounds.y, width: iconBoxSize.width, height: iconBoxSize.height };
11275
+ return {
11276
+ x: bounds.x + inset,
11277
+ y: bounds.y + inset,
11278
+ width: iconBoxSize.width,
11279
+ height: iconBoxSize.height
11280
+ };
11214
11281
  case "top-right":
11215
11282
  return {
11216
- x: bounds.x + bounds.width - iconBoxSize.width,
11217
- y: bounds.y,
11283
+ x: bounds.x + bounds.width - iconBoxSize.width - inset,
11284
+ y: bounds.y + inset,
11218
11285
  width: iconBoxSize.width,
11219
11286
  height: iconBoxSize.height
11220
11287
  };
11221
11288
  case "bottom-left":
11222
11289
  return {
11223
- x: bounds.x,
11224
- y: bounds.y + bounds.height - iconBoxSize.height,
11290
+ x: bounds.x + inset,
11291
+ y: bounds.y + bounds.height - iconBoxSize.height - inset,
11225
11292
  width: iconBoxSize.width,
11226
11293
  height: iconBoxSize.height
11227
11294
  };
11228
11295
  case "bottom-right":
11229
11296
  return {
11230
- x: bounds.x + bounds.width - iconBoxSize.width,
11231
- y: bounds.y + bounds.height - iconBoxSize.height,
11297
+ x: bounds.x + bounds.width - iconBoxSize.width - inset,
11298
+ y: bounds.y + bounds.height - iconBoxSize.height - inset,
11232
11299
  width: iconBoxSize.width,
11233
11300
  height: iconBoxSize.height
11234
11301
  };
@@ -11237,23 +11304,17 @@ class SvgExporter {
11237
11304
  return bounds;
11238
11305
  }
11239
11306
  }
11240
- getIconDrawRect(bounds, opts, imageSize) {
11241
- const padding = opts.padding ?? 8;
11242
- const margin = Math.max(0, opts.margin ?? 0);
11307
+ getIconDrawRect(bounds, opts, imageSize, inset) {
11243
11308
  const fit = opts.fit ?? "none";
11244
11309
  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
11310
  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)
11311
+ x: bounds.x + inset,
11312
+ y: bounds.y + inset,
11313
+ width: Math.max(0, bounds.width - inset * 2),
11314
+ height: Math.max(0, bounds.height - inset * 2)
11254
11315
  };
11255
- const availableWidth = Math.max(0, innerBounds.width - padding * 2);
11256
- const availableHeight = Math.max(0, innerBounds.height - padding * 2);
11316
+ const availableWidth = Math.max(0, innerBounds.width);
11317
+ const availableHeight = Math.max(0, innerBounds.height);
11257
11318
  let drawWidth = opts.width ?? imageSize.width;
11258
11319
  let drawHeight = opts.height ?? imageSize.height;
11259
11320
  if (scaleWithBounds) {
@@ -11270,21 +11331,13 @@ class SvgExporter {
11270
11331
  }
11271
11332
  drawWidth = Math.min(Math.max(0, drawWidth), Math.max(0, availableWidth));
11272
11333
  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
- }
11334
+ let x = innerBounds.x;
11335
+ let y = innerBounds.y;
11336
+ x = innerBounds.x + (innerBounds.width - drawWidth) / 2;
11337
+ y = innerBounds.y + (innerBounds.height - drawHeight) / 2;
11285
11338
  return {
11286
- x: x + offsetX,
11287
- y: y + offsetY,
11339
+ x,
11340
+ y,
11288
11341
  width: drawWidth,
11289
11342
  height: drawHeight
11290
11343
  };
@@ -11379,7 +11432,7 @@ class SvgExporter {
11379
11432
  }
11380
11433
  createEmptySvg(width, height, backgroundColor, includeBackground) {
11381
11434
  return [
11382
- `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">`,
11435
+ `<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
11436
  includeBackground ? `<rect width="100%" height="100%" fill="${backgroundColor}"/>` : "",
11384
11437
  "</svg>"
11385
11438
  ].join("");