@ngroznykh/papirus 0.1.0 → 0.3.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
@@ -1968,23 +1968,55 @@ class TextLabel {
1968
1968
  return this._maxWidth;
1969
1969
  }
1970
1970
  set maxWidth(value) {
1971
+ if (this._maxWidth === value) {
1972
+ return;
1973
+ }
1971
1974
  this._maxWidth = value;
1972
1975
  this._lines = [];
1973
1976
  this._measureDirty = true;
1974
1977
  this._onChange?.();
1975
1978
  }
1979
+ /**
1980
+ * Internal max width used for automatic wrapping by container elements.
1981
+ */
1982
+ setAutoMaxWidth(value) {
1983
+ if (this._autoMaxWidth === value) {
1984
+ return;
1985
+ }
1986
+ this._autoMaxWidth = value;
1987
+ this._lines = [];
1988
+ this._measureDirty = true;
1989
+ }
1976
1990
  /**
1977
1991
  * Label padding
1978
1992
  */
1979
1993
  get padding() {
1980
1994
  return this._padding;
1981
1995
  }
1996
+ set padding(value) {
1997
+ const next = Number.isFinite(value) ? Math.max(0, value) : this._padding;
1998
+ if (this._padding !== next) {
1999
+ this._padding = next;
2000
+ this._lines = [];
2001
+ this._measureDirty = true;
2002
+ this._onChange?.();
2003
+ }
2004
+ }
1982
2005
  /**
1983
2006
  * Label margin
1984
2007
  */
1985
2008
  get margin() {
1986
2009
  return this._margin;
1987
2010
  }
2011
+ set margin(value) {
2012
+ const next = Number.isFinite(value) ? Math.max(0, value) : this._margin;
2013
+ if (this._margin !== next) {
2014
+ this._margin = next;
2015
+ this._lines = [];
2016
+ this._measureDirty = true;
2017
+ this._onChange?.();
2018
+ }
2019
+ }
1988
2020
  applyStyleManager(styleManager) {
1989
2021
  const baseStyle = styleManager.getTextStyle(this._styleClass);
1990
2022
  const mergedStyle = { ...baseStyle, ...this._localStyle };
@@ -2022,8 +2054,9 @@ class TextLabel {
2022
2054
  }
2023
2055
  this.applyStyle(ctx);
2024
2056
  const lineHeight = (this._style.fontSize ?? 14) * 1.2;
2025
- if (this._maxWidth !== void 0) {
2026
- const maxWidth = Math.max(0, this._maxWidth - this._margin * 2);
2057
+ const effectiveMaxWidth = this._maxWidth ?? this._autoMaxWidth;
2058
+ if (effectiveMaxWidth !== void 0) {
2059
+ const maxWidth = Math.max(0, effectiveMaxWidth - this._margin * 2);
2027
2060
  this._lines = this.wrapText(ctx, this._text, Math.max(0, maxWidth - this._padding * 2));
2028
2061
  } else {
2029
2062
  this._lines = this._text.split("\n");
@@ -3508,6 +3541,19 @@ class Edge extends Element {
3508
3541
  }
3509
3542
  return this.getPolylineMidpoint(path);
3510
3543
  }
3544
+ /**
3545
+ * Get world position of label center along path.
3546
+ */
3547
+ getLabelPosition() {
3548
+ if (this._path.length < 2) {
3549
+ return null;
3550
+ }
3551
+ const midpoint = this.getPathMidpoint();
3552
+ return {
3553
+ x: midpoint.x,
3554
+ y: midpoint.y + this._labelOffset
3555
+ };
3556
+ }
3511
3557
  getPolylineMidpoint(path) {
3512
3558
  if (path.length === 0) {
3513
3559
  return { x: 0, y: 0 };
@@ -3578,6 +3624,7 @@ class InteractionManager {
3578
3624
  this.clipboard = null;
3579
3625
  this.pendingPropertyChanges = /* @__PURE__ */ new Map();
3580
3626
  this.propertyChangeDebounceMs = 350;
3627
+ this.activeLabelEditor = null;
3581
3628
  this.renderer = options.renderer;
3582
3629
  this.inputHandler = new InputHandler({
3583
3630
  canvas: this.renderer.getCanvas(),
@@ -3704,6 +3751,7 @@ class InteractionManager {
3704
3751
  }
3705
3752
  }
3706
3753
  destroy() {
3754
+ this.finishInlineLabelEdit(false);
3707
3755
  this.inputHandler.destroy();
3708
3756
  this.overlayCleanup?.();
3709
3757
  this.overlayCleanup = null;
@@ -3724,6 +3772,7 @@ class InteractionManager {
3724
3772
  this.inputHandler.on("mousemove", (event) => this.handleMouseMove(event));
3725
3773
  this.inputHandler.on("mouseup", (event) => this.handleMouseUp(event));
3726
3774
  this.inputHandler.on("click", (event) => this.handleClick(event));
3775
+ this.inputHandler.on("dblclick", (event) => this.handleDoubleClick(event));
3727
3776
  this.inputHandler.on("wheel", (event) => this.handleWheel(event));
3728
3777
  this.inputHandler.on("pan", (event) => this.handlePan(event));
3729
3778
  this.inputHandler.on("pinch", (event) => this.handlePinch(event));
@@ -3861,6 +3910,56 @@ class InteractionManager {
3861
3910
  }
3862
3911
  this.selectionManager.handleClick(event);
3863
3912
  }
3913
+ handleDoubleClick(event) {
3914
+ if (this.dragManager.handledMouseDown || this.resizeManager.handledMouseDown || this.connectionManager.connecting) {
3915
+ return;
3916
+ }
3917
+ const point = { x: event.worldX, y: event.worldY };
3918
+ const edgeByLabel = this.getEdgeLabelAtPoint(point);
3919
+ if (edgeByLabel) {
3920
+ const labelPosition = edgeByLabel.getLabelPosition() ?? point;
3921
+ this.startInlineLabelEdit("edge", edgeByLabel.id, edgeByLabel.label?.text ?? "", labelPosition);
3922
+ return;
3923
+ }
3924
+ const hitElement = this.renderer.getElementAtPoint(point);
3925
+ if (!hitElement) {
3926
+ this.finishInlineLabelEdit(true);
3927
+ return;
3928
+ }
3929
+ if ("typeName" in hitElement) {
3930
+ this.startInlineLabelEdit("node", hitElement.id, hitElement.label?.text ?? "", hitElement.getLabelPosition());
3931
+ return;
3932
+ }
3933
+ if ("from" in hitElement && "to" in hitElement) {
3934
+ const labelPosition = hitElement.getLabelPosition() ?? point;
3935
+ this.startInlineLabelEdit("edge", hitElement.id, hitElement.label?.text ?? "", labelPosition);
3936
+ }
3937
+ }
3938
+ getEdgeLabelAtPoint(point) {
3939
+ const zoom = Math.max(this.renderer.zoom, 1e-4);
3940
+ const maxDistance = 28 / zoom;
3941
+ const maxDistanceSq = maxDistance * maxDistance;
3942
+ let closestEdge = null;
3943
+ let closestDistanceSq = Infinity;
3944
+ for (const edge of this.renderer.edges.values()) {
3945
+ if (!edge.visible || !edge.label) {
3946
+ continue;
3947
+ }
3948
+ const labelPosition = edge.getLabelPosition();
3949
+ if (!labelPosition) {
3950
+ continue;
3951
+ }
3952
+ const dx = point.x - labelPosition.x;
3953
+ const dy = point.y - labelPosition.y;
3954
+ const distanceSq = dx * dx + dy * dy;
3955
+ if (distanceSq > maxDistanceSq || distanceSq >= closestDistanceSq) {
3956
+ continue;
3957
+ }
3958
+ closestDistanceSq = distanceSq;
3959
+ closestEdge = edge;
3960
+ }
3961
+ return closestEdge;
3962
+ }
3864
3963
  handleWheel(event) {
3865
3964
  this.navigationManager.handleWheel(event);
3866
3965
  }
@@ -3899,6 +3998,109 @@ class InteractionManager {
3899
3998
  handleKeyUp(event) {
3900
3999
  this.navigationManager.handleKeyUp(event);
3901
4000
  }
4001
+ startInlineLabelEdit(kind, id, text, worldPosition) {
4002
+ this.finishInlineLabelEdit(true);
4003
+ const screenPoint = this.renderer.worldToScreen(worldPosition.x, worldPosition.y);
4004
+ const textarea = document.createElement("textarea");
4005
+ textarea.value = text;
4006
+ textarea.rows = 1;
4007
+ textarea.spellcheck = false;
4008
+ textarea.setAttribute("aria-label", "Edit label");
4009
+ textarea.style.position = "fixed";
4010
+ textarea.style.left = `${screenPoint.x}px`;
4011
+ textarea.style.top = `${screenPoint.y}px`;
4012
+ textarea.style.transform = "translate(-50%, -50%)";
4013
+ textarea.style.zIndex = "10001";
4014
+ textarea.style.minWidth = "120px";
4015
+ textarea.style.maxWidth = "420px";
4016
+ textarea.style.minHeight = "30px";
4017
+ textarea.style.padding = "6px 8px";
4018
+ textarea.style.border = "1px solid #6366f1";
4019
+ textarea.style.borderRadius = "6px";
4020
+ textarea.style.boxShadow = "0 8px 18px rgba(15, 23, 42, 0.15)";
4021
+ textarea.style.background = "#ffffff";
4022
+ textarea.style.color = "#0f172a";
4023
+ textarea.style.font = "14px sans-serif";
4024
+ textarea.style.lineHeight = "1.4";
4025
+ textarea.style.resize = "none";
4026
+ textarea.style.overflow = "hidden";
4027
+ const resizeTextarea = () => {
4028
+ textarea.style.height = "auto";
4029
+ textarea.style.height = `${Math.max(30, textarea.scrollHeight)}px`;
4030
+ };
4031
+ resizeTextarea();
4032
+ const commitAndClose = () => this.finishInlineLabelEdit(true);
4033
+ const cancelAndClose = () => this.finishInlineLabelEdit(false);
4034
+ const onKeyDown = (e) => {
4035
+ if (e.key === "Escape") {
4036
+ e.preventDefault();
4037
+ cancelAndClose();
4038
+ return;
4039
+ }
4040
+ if (e.key === "Enter" && !e.shiftKey) {
4041
+ e.preventDefault();
4042
+ commitAndClose();
4043
+ }
4044
+ };
4045
+ const onBlur = () => commitAndClose();
4046
+ const onInput = () => resizeTextarea();
4047
+ textarea.addEventListener("keydown", onKeyDown);
4048
+ textarea.addEventListener("blur", onBlur);
4049
+ textarea.addEventListener("input", onInput);
4050
+ document.body.appendChild(textarea);
4051
+ textarea.focus();
4052
+ textarea.select();
4053
+ this.activeLabelEditor = {
4054
+ kind,
4055
+ id,
4056
+ textarea,
4057
+ cleanup: () => {
4058
+ textarea.removeEventListener("keydown", onKeyDown);
4059
+ textarea.removeEventListener("blur", onBlur);
4060
+ textarea.removeEventListener("input", onInput);
4061
+ textarea.remove();
4062
+ }
4063
+ };
4064
+ }
4065
+ finishInlineLabelEdit(commit) {
4066
+ if (!this.activeLabelEditor) {
4067
+ return;
4068
+ }
4069
+ const { kind, id, textarea, cleanup } = this.activeLabelEditor;
4070
+ this.activeLabelEditor = null;
4071
+ const nextValue = textarea.value;
4072
+ cleanup();
4073
+ if (!commit) {
4074
+ return;
4075
+ }
4076
+ if (kind === "node") {
4077
+ this.changeNodeProperties(id, (node) => {
4078
+ const value = nextValue.trim();
4079
+ if (value.length === 0) {
4080
+ node.label = void 0;
4081
+ return;
4082
+ }
4083
+ if (!node.label) {
4084
+ node.label = value;
4085
+ return;
4086
+ }
4087
+ node.label.text = value;
4088
+ });
4089
+ return;
4090
+ }
4091
+ this.changeEdgeProperties(id, (edge) => {
4092
+ const value = nextValue.trim();
4093
+ if (value.length === 0) {
4094
+ edge.label = void 0;
4095
+ return;
4096
+ }
4097
+ if (!edge.label) {
4098
+ edge.label = value;
4099
+ return;
4100
+ }
4101
+ edge.label.text = value;
4102
+ });
4103
+ }
3902
4104
  queuePropertyChange(kind, id, before, after) {
3903
4105
  const key = `${kind}:${id}`;
3904
4106
  const existing = this.pendingPropertyChanges.get(key);
@@ -5950,6 +6152,7 @@ class Node extends Element {
5950
6152
  this._showPortsAlways = options.showPortsAlways ?? false;
5951
6153
  this._anchorPoints = this.normalizeAnchorPoints(options.anchorPoints);
5952
6154
  this._labelPlacement = options.labelPlacement ?? "auto";
6155
+ this._resizeHandlesEnabled = options.resizeHandlesEnabled ?? true;
5953
6156
  if (options.label !== void 0) {
5954
6157
  if (typeof options.label === "string") {
5955
6158
  this._label = new TextLabel({ text: options.label, onChange: () => this.markDirty() });
@@ -6111,6 +6314,15 @@ class Node extends Element {
6111
6314
  this.markDirty();
6112
6315
  }
6113
6316
  }
6317
+ get resizeHandlesEnabled() {
6318
+ return this._resizeHandlesEnabled;
6319
+ }
6320
+ set resizeHandlesEnabled(value) {
6321
+ if (this._resizeHandlesEnabled !== value) {
6322
+ this._resizeHandlesEnabled = value;
6323
+ this.markDirty();
6324
+ }
6325
+ }
6114
6326
  /**
6115
6327
  * Anchor points configuration (per side)
6116
6328
  */
@@ -6163,6 +6375,25 @@ class Node extends Element {
6163
6375
  this._label.render(ctx, bounds);
6164
6376
  ctx.globalAlpha = 1;
6165
6377
  }
6378
+ /**
6379
+ * Get world position of label center.
6380
+ */
6381
+ getLabelPosition() {
6382
+ const bounds = this.getBounds();
6383
+ const placement = this._labelPlacement === "auto" ? "center" : this._labelPlacement;
6384
+ switch (placement) {
6385
+ case "top":
6386
+ return { x: bounds.x + bounds.width / 2, y: bounds.y };
6387
+ case "bottom":
6388
+ return { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height };
6389
+ case "left":
6390
+ return { x: bounds.x, y: bounds.y + bounds.height / 2 };
6391
+ case "right":
6392
+ return { x: bounds.x + bounds.width, y: bounds.y + bounds.height / 2 };
6393
+ default:
6394
+ return { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 };
6395
+ }
6396
+ }
6166
6397
  /**
6167
6398
  * Render icon, label, and ports
6168
6399
  */
@@ -6170,14 +6401,22 @@ class Node extends Element {
6170
6401
  ctx.setLineDash([]);
6171
6402
  ctx.lineDashOffset = 0;
6172
6403
  let bounds = this.getBounds();
6173
- const labelSize = this._label ? this._label.measure(ctx) : void 0;
6174
- const iconBoxSize = this._icon ? this.getIconBoxSize() : void 0;
6404
+ let iconBoxSize = this._icon ? this.getIconBoxSize() : void 0;
6405
+ if (this._label) {
6406
+ this._label.setAutoMaxWidth(this.getAutoLabelBounds(bounds, iconBoxSize).width);
6407
+ }
6408
+ let labelSize = this._label ? this._label.measure(ctx) : void 0;
6175
6409
  if (labelSize || iconBoxSize) {
6176
6410
  this.ensureContentsFit(labelSize, iconBoxSize);
6177
6411
  bounds = this.getBounds();
6412
+ iconBoxSize = this._icon ? this.getIconBoxSize() : void 0;
6413
+ if (this._label) {
6414
+ this._label.setAutoMaxWidth(this.getAutoLabelBounds(bounds, iconBoxSize).width);
6415
+ labelSize = this._label.measure(ctx);
6416
+ }
6178
6417
  }
6179
6418
  let iconBounds = bounds;
6180
- let labelBounds = bounds;
6419
+ let labelBounds = this.getLabelContainerBounds(bounds);
6181
6420
  if (this._icon && iconBoxSize) {
6182
6421
  iconBounds = this.getIconBounds(bounds, iconBoxSize, this._icon.placement);
6183
6422
  }
@@ -6186,8 +6425,9 @@ class Node extends Element {
6186
6425
  const placement = this._icon.placement;
6187
6426
  if (isCornerPlacement(placement)) {
6188
6427
  iconBounds = this.getIconBounds(bounds, iconBoxSize, placement);
6189
- labelBounds = bounds;
6428
+ labelBounds = this.getLabelContainerBounds(bounds);
6190
6429
  } else {
6430
+ const contentBounds = this.getLabelContainerBounds(bounds);
6191
6431
  switch (placement) {
6192
6432
  case "top":
6193
6433
  iconBounds = {
@@ -6197,10 +6437,10 @@ class Node extends Element {
6197
6437
  height: iconBoxSize.height
6198
6438
  };
6199
6439
  labelBounds = {
6200
- x: bounds.x,
6201
- y: bounds.y + iconBoxSize.height + gap,
6202
- width: bounds.width,
6203
- height: Math.max(0, bounds.height - iconBoxSize.height - gap)
6440
+ x: contentBounds.x,
6441
+ y: contentBounds.y + iconBoxSize.height + gap,
6442
+ width: contentBounds.width,
6443
+ height: Math.max(0, contentBounds.height - iconBoxSize.height - gap)
6204
6444
  };
6205
6445
  break;
6206
6446
  case "bottom":
@@ -6211,10 +6451,10 @@ class Node extends Element {
6211
6451
  height: iconBoxSize.height
6212
6452
  };
6213
6453
  labelBounds = {
6214
- x: bounds.x,
6215
- y: bounds.y,
6216
- width: bounds.width,
6217
- height: Math.max(0, bounds.height - iconBoxSize.height - gap)
6454
+ x: contentBounds.x,
6455
+ y: contentBounds.y,
6456
+ width: contentBounds.width,
6457
+ height: Math.max(0, contentBounds.height - iconBoxSize.height - gap)
6218
6458
  };
6219
6459
  break;
6220
6460
  case "left":
@@ -6225,10 +6465,10 @@ class Node extends Element {
6225
6465
  height: bounds.height
6226
6466
  };
6227
6467
  labelBounds = {
6228
- x: bounds.x + iconBoxSize.width + gap,
6229
- y: bounds.y,
6230
- width: Math.max(0, bounds.width - iconBoxSize.width - gap),
6231
- height: bounds.height
6468
+ x: contentBounds.x + iconBoxSize.width + gap,
6469
+ y: contentBounds.y,
6470
+ width: Math.max(0, contentBounds.width - iconBoxSize.width - gap),
6471
+ height: contentBounds.height
6232
6472
  };
6233
6473
  break;
6234
6474
  case "right":
@@ -6239,16 +6479,16 @@ class Node extends Element {
6239
6479
  height: bounds.height
6240
6480
  };
6241
6481
  labelBounds = {
6242
- x: bounds.x,
6243
- y: bounds.y,
6244
- width: Math.max(0, bounds.width - iconBoxSize.width - gap),
6245
- height: bounds.height
6482
+ x: contentBounds.x,
6483
+ y: contentBounds.y,
6484
+ width: Math.max(0, contentBounds.width - iconBoxSize.width - gap),
6485
+ height: contentBounds.height
6246
6486
  };
6247
6487
  break;
6248
6488
  }
6249
6489
  }
6250
6490
  } else if (this._label && labelSize) {
6251
- labelBounds = this.getLabelBounds(bounds, labelSize, this._labelPlacement);
6491
+ labelBounds = this.getLabelBounds(this.getAutoLabelBounds(bounds, iconBoxSize), labelSize, this._labelPlacement);
6252
6492
  }
6253
6493
  if (this._icon) {
6254
6494
  this._icon.render(ctx, iconBounds);
@@ -6260,15 +6500,16 @@ class Node extends Element {
6260
6500
  * Minimal size required to fit current contents
6261
6501
  */
6262
6502
  getContentMinSize(ctx) {
6503
+ const bounds = this.getBounds();
6263
6504
  const labelSize = this._label ? this._label.measure(ctx) : void 0;
6264
6505
  const iconBoxSize = this._icon ? this.getIconBoxSize() : void 0;
6265
- return this.calculateContentMinSize(labelSize, iconBoxSize);
6506
+ return this.calculateContentMinSize(labelSize, iconBoxSize, bounds);
6266
6507
  }
6267
6508
  /**
6268
6509
  * Render resize handles when selected
6269
6510
  */
6270
6511
  renderResizeHandles(ctx) {
6271
- if (this._state !== "selected") {
6512
+ if (this._state !== "selected" || !this._resizeHandlesEnabled) {
6272
6513
  return;
6273
6514
  }
6274
6515
  const bounds = this.getBounds();
@@ -6429,7 +6670,7 @@ class Node extends Element {
6429
6670
  }
6430
6671
  ensureContentsFit(labelSize, iconBoxSize) {
6431
6672
  const bounds = this.getBounds();
6432
- const minSize = this.calculateContentMinSize(labelSize, iconBoxSize);
6673
+ const minSize = this.calculateContentMinSize(labelSize, iconBoxSize, bounds);
6433
6674
  if (minSize.width > bounds.width) {
6434
6675
  this.width = minSize.width;
6435
6676
  }
@@ -6437,12 +6678,15 @@ class Node extends Element {
6437
6678
  this.height = minSize.height;
6438
6679
  }
6439
6680
  }
6440
- calculateContentMinSize(labelSize, iconBoxSize) {
6681
+ calculateContentMinSize(labelSize, iconBoxSize, bounds) {
6441
6682
  let minWidth = 0;
6442
6683
  let minHeight = 0;
6684
+ const labelContainer = this.getLabelContainerBounds(bounds);
6685
+ const widthFactor = labelContainer.width > 0 ? bounds.width / labelContainer.width : 1;
6686
+ const heightFactor = labelContainer.height > 0 ? bounds.height / labelContainer.height : 1;
6443
6687
  if (labelSize) {
6444
- minWidth = Math.max(minWidth, labelSize.width);
6445
- minHeight = Math.max(minHeight, labelSize.height);
6688
+ minWidth = Math.max(minWidth, labelSize.width * widthFactor);
6689
+ minHeight = Math.max(minHeight, labelSize.height * heightFactor);
6446
6690
  }
6447
6691
  if (iconBoxSize) {
6448
6692
  minWidth = Math.max(minWidth, iconBoxSize.width);
@@ -6468,16 +6712,70 @@ class Node extends Element {
6468
6712
  const labelVertical = labelPlacement === "top" || labelPlacement === "bottom";
6469
6713
  const iconHorizontal = iconPlacement === "left" || iconPlacement === "right";
6470
6714
  const iconVertical = iconPlacement === "top" || iconPlacement === "bottom";
6715
+ const labelSharesIconAxis = iconHorizontal && (labelPlacement === "center" || labelPlacement === iconPlacement) || iconVertical && (labelPlacement === "center" || labelPlacement === iconPlacement);
6471
6716
  if (labelHorizontal && iconHorizontal && labelPlacement !== iconPlacement) {
6472
6717
  minWidth = Math.max(minWidth, iconBoxSize.width + gap + labelSize.width);
6473
6718
  }
6474
6719
  if (labelVertical && iconVertical && labelPlacement !== iconPlacement) {
6475
6720
  minHeight = Math.max(minHeight, iconBoxSize.height + gap + labelSize.height);
6476
6721
  }
6722
+ if (labelSharesIconAxis) {
6723
+ if (iconHorizontal) {
6724
+ minWidth = Math.max(minWidth, iconBoxSize.width + gap + labelSize.width);
6725
+ }
6726
+ if (iconVertical) {
6727
+ minHeight = Math.max(minHeight, iconBoxSize.height + gap + labelSize.height);
6728
+ }
6729
+ }
6477
6730
  }
6478
6731
  }
6479
6732
  return { width: minWidth, height: minHeight };
6480
6733
  }
6734
+ getLabelContainerBounds(bounds) {
6735
+ return bounds;
6736
+ }
6737
+ getAutoLabelBounds(bounds, iconBoxSize) {
6738
+ const contentBounds = this.getLabelContainerBounds(bounds);
6739
+ if (!this._icon || !iconBoxSize || this._icon.placement === "center") {
6740
+ return contentBounds;
6741
+ }
6742
+ const gap = this._icon.gap;
6743
+ if (isCornerPlacement(this._icon.placement)) {
6744
+ return contentBounds;
6745
+ }
6746
+ switch (this._icon.placement) {
6747
+ case "left":
6748
+ return {
6749
+ x: contentBounds.x + iconBoxSize.width + gap,
6750
+ y: contentBounds.y,
6751
+ width: Math.max(0, contentBounds.width - iconBoxSize.width - gap),
6752
+ height: contentBounds.height
6753
+ };
6754
+ case "right":
6755
+ return {
6756
+ x: contentBounds.x,
6757
+ y: contentBounds.y,
6758
+ width: Math.max(0, contentBounds.width - iconBoxSize.width - gap),
6759
+ height: contentBounds.height
6760
+ };
6761
+ case "top":
6762
+ return {
6763
+ x: contentBounds.x,
6764
+ y: contentBounds.y + iconBoxSize.height + gap,
6765
+ width: contentBounds.width,
6766
+ height: Math.max(0, contentBounds.height - iconBoxSize.height - gap)
6767
+ };
6768
+ case "bottom":
6769
+ return {
6770
+ x: contentBounds.x,
6771
+ y: contentBounds.y,
6772
+ width: contentBounds.width,
6773
+ height: Math.max(0, contentBounds.height - iconBoxSize.height - gap)
6774
+ };
6775
+ default:
6776
+ return contentBounds;
6777
+ }
6778
+ }
6481
6779
  getLabelBounds(bounds, labelSize, placement) {
6482
6780
  const normalizedPlacement = placement === "auto" ? "center" : placement;
6483
6781
  const width = Math.min(labelSize.width, bounds.width);
@@ -6901,6 +7199,16 @@ class CircleNode extends Node {
6901
7199
  y: center.y + dy * scale
6902
7200
  };
6903
7201
  }
7202
+ getLabelContainerBounds(bounds) {
7203
+ const width = bounds.width / Math.SQRT2;
7204
+ const height = bounds.height / Math.SQRT2;
7205
+ return {
7206
+ x: bounds.x + (bounds.width - width) / 2,
7207
+ y: bounds.y + (bounds.height - height) / 2,
7208
+ width,
7209
+ height
7210
+ };
7211
+ }
6904
7212
  render(ctx) {
6905
7213
  const center = this.getCenter();
6906
7214
  const rx = this._width / 2;
@@ -6952,6 +7260,16 @@ class DiamondNode extends Node {
6952
7260
  y: center.y + dy * t
6953
7261
  };
6954
7262
  }
7263
+ getLabelContainerBounds(bounds) {
7264
+ const width = bounds.width / 2;
7265
+ const height = bounds.height / 2;
7266
+ return {
7267
+ x: bounds.x + (bounds.width - width) / 2,
7268
+ y: bounds.y + (bounds.height - height) / 2,
7269
+ width,
7270
+ height
7271
+ };
7272
+ }
6955
7273
  render(ctx) {
6956
7274
  const center = this.getCenter();
6957
7275
  const hw = this._width / 2;
@@ -6981,10 +7299,20 @@ class CustomShapeNode extends Node {
6981
7299
  } else {
6982
7300
  this._pathFactory = options.path;
6983
7301
  }
7302
+ this._shapeType = options.shapeType;
6984
7303
  }
6985
7304
  get typeName() {
6986
7305
  return "custom";
6987
7306
  }
7307
+ get shapeType() {
7308
+ return this._shapeType;
7309
+ }
7310
+ set shapeType(value) {
7311
+ if (this._shapeType !== value) {
7312
+ this._shapeType = value;
7313
+ this.markDirty();
7314
+ }
7315
+ }
6988
7316
  /**
6989
7317
  * Set a new path factory
6990
7318
  */