@ngroznykh/papirus 0.1.0 → 0.2.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,11 +1968,25 @@ 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
  */
@@ -2022,8 +2036,9 @@ class TextLabel {
2022
2036
  }
2023
2037
  this.applyStyle(ctx);
2024
2038
  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);
2039
+ const effectiveMaxWidth = this._maxWidth ?? this._autoMaxWidth;
2040
+ if (effectiveMaxWidth !== void 0) {
2041
+ const maxWidth = Math.max(0, effectiveMaxWidth - this._margin * 2);
2027
2042
  this._lines = this.wrapText(ctx, this._text, Math.max(0, maxWidth - this._padding * 2));
2028
2043
  } else {
2029
2044
  this._lines = this._text.split("\n");
@@ -3508,6 +3523,19 @@ class Edge extends Element {
3508
3523
  }
3509
3524
  return this.getPolylineMidpoint(path);
3510
3525
  }
3526
+ /**
3527
+ * Get world position of label center along path.
3528
+ */
3529
+ getLabelPosition() {
3530
+ if (this._path.length < 2) {
3531
+ return null;
3532
+ }
3533
+ const midpoint = this.getPathMidpoint();
3534
+ return {
3535
+ x: midpoint.x,
3536
+ y: midpoint.y + this._labelOffset
3537
+ };
3538
+ }
3511
3539
  getPolylineMidpoint(path) {
3512
3540
  if (path.length === 0) {
3513
3541
  return { x: 0, y: 0 };
@@ -3578,6 +3606,7 @@ class InteractionManager {
3578
3606
  this.clipboard = null;
3579
3607
  this.pendingPropertyChanges = /* @__PURE__ */ new Map();
3580
3608
  this.propertyChangeDebounceMs = 350;
3609
+ this.activeLabelEditor = null;
3581
3610
  this.renderer = options.renderer;
3582
3611
  this.inputHandler = new InputHandler({
3583
3612
  canvas: this.renderer.getCanvas(),
@@ -3704,6 +3733,7 @@ class InteractionManager {
3704
3733
  }
3705
3734
  }
3706
3735
  destroy() {
3736
+ this.finishInlineLabelEdit(false);
3707
3737
  this.inputHandler.destroy();
3708
3738
  this.overlayCleanup?.();
3709
3739
  this.overlayCleanup = null;
@@ -3724,6 +3754,7 @@ class InteractionManager {
3724
3754
  this.inputHandler.on("mousemove", (event) => this.handleMouseMove(event));
3725
3755
  this.inputHandler.on("mouseup", (event) => this.handleMouseUp(event));
3726
3756
  this.inputHandler.on("click", (event) => this.handleClick(event));
3757
+ this.inputHandler.on("dblclick", (event) => this.handleDoubleClick(event));
3727
3758
  this.inputHandler.on("wheel", (event) => this.handleWheel(event));
3728
3759
  this.inputHandler.on("pan", (event) => this.handlePan(event));
3729
3760
  this.inputHandler.on("pinch", (event) => this.handlePinch(event));
@@ -3861,6 +3892,56 @@ class InteractionManager {
3861
3892
  }
3862
3893
  this.selectionManager.handleClick(event);
3863
3894
  }
3895
+ handleDoubleClick(event) {
3896
+ if (this.dragManager.handledMouseDown || this.resizeManager.handledMouseDown || this.connectionManager.connecting) {
3897
+ return;
3898
+ }
3899
+ const point = { x: event.worldX, y: event.worldY };
3900
+ const edgeByLabel = this.getEdgeLabelAtPoint(point);
3901
+ if (edgeByLabel) {
3902
+ const labelPosition = edgeByLabel.getLabelPosition() ?? point;
3903
+ this.startInlineLabelEdit("edge", edgeByLabel.id, edgeByLabel.label?.text ?? "", labelPosition);
3904
+ return;
3905
+ }
3906
+ const hitElement = this.renderer.getElementAtPoint(point);
3907
+ if (!hitElement) {
3908
+ this.finishInlineLabelEdit(true);
3909
+ return;
3910
+ }
3911
+ if ("typeName" in hitElement) {
3912
+ this.startInlineLabelEdit("node", hitElement.id, hitElement.label?.text ?? "", hitElement.getLabelPosition());
3913
+ return;
3914
+ }
3915
+ if ("from" in hitElement && "to" in hitElement) {
3916
+ const labelPosition = hitElement.getLabelPosition() ?? point;
3917
+ this.startInlineLabelEdit("edge", hitElement.id, hitElement.label?.text ?? "", labelPosition);
3918
+ }
3919
+ }
3920
+ getEdgeLabelAtPoint(point) {
3921
+ const zoom = Math.max(this.renderer.zoom, 1e-4);
3922
+ const maxDistance = 28 / zoom;
3923
+ const maxDistanceSq = maxDistance * maxDistance;
3924
+ let closestEdge = null;
3925
+ let closestDistanceSq = Infinity;
3926
+ for (const edge of this.renderer.edges.values()) {
3927
+ if (!edge.visible || !edge.label) {
3928
+ continue;
3929
+ }
3930
+ const labelPosition = edge.getLabelPosition();
3931
+ if (!labelPosition) {
3932
+ continue;
3933
+ }
3934
+ const dx = point.x - labelPosition.x;
3935
+ const dy = point.y - labelPosition.y;
3936
+ const distanceSq = dx * dx + dy * dy;
3937
+ if (distanceSq > maxDistanceSq || distanceSq >= closestDistanceSq) {
3938
+ continue;
3939
+ }
3940
+ closestDistanceSq = distanceSq;
3941
+ closestEdge = edge;
3942
+ }
3943
+ return closestEdge;
3944
+ }
3864
3945
  handleWheel(event) {
3865
3946
  this.navigationManager.handleWheel(event);
3866
3947
  }
@@ -3899,6 +3980,109 @@ class InteractionManager {
3899
3980
  handleKeyUp(event) {
3900
3981
  this.navigationManager.handleKeyUp(event);
3901
3982
  }
3983
+ startInlineLabelEdit(kind, id, text, worldPosition) {
3984
+ this.finishInlineLabelEdit(true);
3985
+ const screenPoint = this.renderer.worldToScreen(worldPosition.x, worldPosition.y);
3986
+ const textarea = document.createElement("textarea");
3987
+ textarea.value = text;
3988
+ textarea.rows = 1;
3989
+ textarea.spellcheck = false;
3990
+ textarea.setAttribute("aria-label", "Edit label");
3991
+ textarea.style.position = "fixed";
3992
+ textarea.style.left = `${screenPoint.x}px`;
3993
+ textarea.style.top = `${screenPoint.y}px`;
3994
+ textarea.style.transform = "translate(-50%, -50%)";
3995
+ textarea.style.zIndex = "10001";
3996
+ textarea.style.minWidth = "120px";
3997
+ textarea.style.maxWidth = "420px";
3998
+ textarea.style.minHeight = "30px";
3999
+ textarea.style.padding = "6px 8px";
4000
+ textarea.style.border = "1px solid #6366f1";
4001
+ textarea.style.borderRadius = "6px";
4002
+ textarea.style.boxShadow = "0 8px 18px rgba(15, 23, 42, 0.15)";
4003
+ textarea.style.background = "#ffffff";
4004
+ textarea.style.color = "#0f172a";
4005
+ textarea.style.font = "14px sans-serif";
4006
+ textarea.style.lineHeight = "1.4";
4007
+ textarea.style.resize = "none";
4008
+ textarea.style.overflow = "hidden";
4009
+ const resizeTextarea = () => {
4010
+ textarea.style.height = "auto";
4011
+ textarea.style.height = `${Math.max(30, textarea.scrollHeight)}px`;
4012
+ };
4013
+ resizeTextarea();
4014
+ const commitAndClose = () => this.finishInlineLabelEdit(true);
4015
+ const cancelAndClose = () => this.finishInlineLabelEdit(false);
4016
+ const onKeyDown = (e) => {
4017
+ if (e.key === "Escape") {
4018
+ e.preventDefault();
4019
+ cancelAndClose();
4020
+ return;
4021
+ }
4022
+ if (e.key === "Enter" && !e.shiftKey) {
4023
+ e.preventDefault();
4024
+ commitAndClose();
4025
+ }
4026
+ };
4027
+ const onBlur = () => commitAndClose();
4028
+ const onInput = () => resizeTextarea();
4029
+ textarea.addEventListener("keydown", onKeyDown);
4030
+ textarea.addEventListener("blur", onBlur);
4031
+ textarea.addEventListener("input", onInput);
4032
+ document.body.appendChild(textarea);
4033
+ textarea.focus();
4034
+ textarea.select();
4035
+ this.activeLabelEditor = {
4036
+ kind,
4037
+ id,
4038
+ textarea,
4039
+ cleanup: () => {
4040
+ textarea.removeEventListener("keydown", onKeyDown);
4041
+ textarea.removeEventListener("blur", onBlur);
4042
+ textarea.removeEventListener("input", onInput);
4043
+ textarea.remove();
4044
+ }
4045
+ };
4046
+ }
4047
+ finishInlineLabelEdit(commit) {
4048
+ if (!this.activeLabelEditor) {
4049
+ return;
4050
+ }
4051
+ const { kind, id, textarea, cleanup } = this.activeLabelEditor;
4052
+ this.activeLabelEditor = null;
4053
+ const nextValue = textarea.value;
4054
+ cleanup();
4055
+ if (!commit) {
4056
+ return;
4057
+ }
4058
+ if (kind === "node") {
4059
+ this.changeNodeProperties(id, (node) => {
4060
+ const value = nextValue.trim();
4061
+ if (value.length === 0) {
4062
+ node.label = void 0;
4063
+ return;
4064
+ }
4065
+ if (!node.label) {
4066
+ node.label = value;
4067
+ return;
4068
+ }
4069
+ node.label.text = value;
4070
+ });
4071
+ return;
4072
+ }
4073
+ this.changeEdgeProperties(id, (edge) => {
4074
+ const value = nextValue.trim();
4075
+ if (value.length === 0) {
4076
+ edge.label = void 0;
4077
+ return;
4078
+ }
4079
+ if (!edge.label) {
4080
+ edge.label = value;
4081
+ return;
4082
+ }
4083
+ edge.label.text = value;
4084
+ });
4085
+ }
3902
4086
  queuePropertyChange(kind, id, before, after) {
3903
4087
  const key = `${kind}:${id}`;
3904
4088
  const existing = this.pendingPropertyChanges.get(key);
@@ -6163,6 +6347,25 @@ class Node extends Element {
6163
6347
  this._label.render(ctx, bounds);
6164
6348
  ctx.globalAlpha = 1;
6165
6349
  }
6350
+ /**
6351
+ * Get world position of label center.
6352
+ */
6353
+ getLabelPosition() {
6354
+ const bounds = this.getBounds();
6355
+ const placement = this._labelPlacement === "auto" ? "center" : this._labelPlacement;
6356
+ switch (placement) {
6357
+ case "top":
6358
+ return { x: bounds.x + bounds.width / 2, y: bounds.y };
6359
+ case "bottom":
6360
+ return { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height };
6361
+ case "left":
6362
+ return { x: bounds.x, y: bounds.y + bounds.height / 2 };
6363
+ case "right":
6364
+ return { x: bounds.x + bounds.width, y: bounds.y + bounds.height / 2 };
6365
+ default:
6366
+ return { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 };
6367
+ }
6368
+ }
6166
6369
  /**
6167
6370
  * Render icon, label, and ports
6168
6371
  */
@@ -6170,14 +6373,22 @@ class Node extends Element {
6170
6373
  ctx.setLineDash([]);
6171
6374
  ctx.lineDashOffset = 0;
6172
6375
  let bounds = this.getBounds();
6173
- const labelSize = this._label ? this._label.measure(ctx) : void 0;
6174
- const iconBoxSize = this._icon ? this.getIconBoxSize() : void 0;
6376
+ let iconBoxSize = this._icon ? this.getIconBoxSize() : void 0;
6377
+ if (this._label) {
6378
+ this._label.setAutoMaxWidth(this.getAutoLabelBounds(bounds, iconBoxSize).width);
6379
+ }
6380
+ let labelSize = this._label ? this._label.measure(ctx) : void 0;
6175
6381
  if (labelSize || iconBoxSize) {
6176
6382
  this.ensureContentsFit(labelSize, iconBoxSize);
6177
6383
  bounds = this.getBounds();
6384
+ iconBoxSize = this._icon ? this.getIconBoxSize() : void 0;
6385
+ if (this._label) {
6386
+ this._label.setAutoMaxWidth(this.getAutoLabelBounds(bounds, iconBoxSize).width);
6387
+ labelSize = this._label.measure(ctx);
6388
+ }
6178
6389
  }
6179
6390
  let iconBounds = bounds;
6180
- let labelBounds = bounds;
6391
+ let labelBounds = this.getLabelContainerBounds(bounds);
6181
6392
  if (this._icon && iconBoxSize) {
6182
6393
  iconBounds = this.getIconBounds(bounds, iconBoxSize, this._icon.placement);
6183
6394
  }
@@ -6186,8 +6397,9 @@ class Node extends Element {
6186
6397
  const placement = this._icon.placement;
6187
6398
  if (isCornerPlacement(placement)) {
6188
6399
  iconBounds = this.getIconBounds(bounds, iconBoxSize, placement);
6189
- labelBounds = bounds;
6400
+ labelBounds = this.getLabelContainerBounds(bounds);
6190
6401
  } else {
6402
+ const contentBounds = this.getLabelContainerBounds(bounds);
6191
6403
  switch (placement) {
6192
6404
  case "top":
6193
6405
  iconBounds = {
@@ -6197,10 +6409,10 @@ class Node extends Element {
6197
6409
  height: iconBoxSize.height
6198
6410
  };
6199
6411
  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)
6412
+ x: contentBounds.x,
6413
+ y: contentBounds.y + iconBoxSize.height + gap,
6414
+ width: contentBounds.width,
6415
+ height: Math.max(0, contentBounds.height - iconBoxSize.height - gap)
6204
6416
  };
6205
6417
  break;
6206
6418
  case "bottom":
@@ -6211,10 +6423,10 @@ class Node extends Element {
6211
6423
  height: iconBoxSize.height
6212
6424
  };
6213
6425
  labelBounds = {
6214
- x: bounds.x,
6215
- y: bounds.y,
6216
- width: bounds.width,
6217
- height: Math.max(0, bounds.height - iconBoxSize.height - gap)
6426
+ x: contentBounds.x,
6427
+ y: contentBounds.y,
6428
+ width: contentBounds.width,
6429
+ height: Math.max(0, contentBounds.height - iconBoxSize.height - gap)
6218
6430
  };
6219
6431
  break;
6220
6432
  case "left":
@@ -6225,10 +6437,10 @@ class Node extends Element {
6225
6437
  height: bounds.height
6226
6438
  };
6227
6439
  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
6440
+ x: contentBounds.x + iconBoxSize.width + gap,
6441
+ y: contentBounds.y,
6442
+ width: Math.max(0, contentBounds.width - iconBoxSize.width - gap),
6443
+ height: contentBounds.height
6232
6444
  };
6233
6445
  break;
6234
6446
  case "right":
@@ -6239,16 +6451,16 @@ class Node extends Element {
6239
6451
  height: bounds.height
6240
6452
  };
6241
6453
  labelBounds = {
6242
- x: bounds.x,
6243
- y: bounds.y,
6244
- width: Math.max(0, bounds.width - iconBoxSize.width - gap),
6245
- height: bounds.height
6454
+ x: contentBounds.x,
6455
+ y: contentBounds.y,
6456
+ width: Math.max(0, contentBounds.width - iconBoxSize.width - gap),
6457
+ height: contentBounds.height
6246
6458
  };
6247
6459
  break;
6248
6460
  }
6249
6461
  }
6250
6462
  } else if (this._label && labelSize) {
6251
- labelBounds = this.getLabelBounds(bounds, labelSize, this._labelPlacement);
6463
+ labelBounds = this.getLabelBounds(this.getAutoLabelBounds(bounds, iconBoxSize), labelSize, this._labelPlacement);
6252
6464
  }
6253
6465
  if (this._icon) {
6254
6466
  this._icon.render(ctx, iconBounds);
@@ -6260,9 +6472,10 @@ class Node extends Element {
6260
6472
  * Minimal size required to fit current contents
6261
6473
  */
6262
6474
  getContentMinSize(ctx) {
6475
+ const bounds = this.getBounds();
6263
6476
  const labelSize = this._label ? this._label.measure(ctx) : void 0;
6264
6477
  const iconBoxSize = this._icon ? this.getIconBoxSize() : void 0;
6265
- return this.calculateContentMinSize(labelSize, iconBoxSize);
6478
+ return this.calculateContentMinSize(labelSize, iconBoxSize, bounds);
6266
6479
  }
6267
6480
  /**
6268
6481
  * Render resize handles when selected
@@ -6429,7 +6642,7 @@ class Node extends Element {
6429
6642
  }
6430
6643
  ensureContentsFit(labelSize, iconBoxSize) {
6431
6644
  const bounds = this.getBounds();
6432
- const minSize = this.calculateContentMinSize(labelSize, iconBoxSize);
6645
+ const minSize = this.calculateContentMinSize(labelSize, iconBoxSize, bounds);
6433
6646
  if (minSize.width > bounds.width) {
6434
6647
  this.width = minSize.width;
6435
6648
  }
@@ -6437,12 +6650,15 @@ class Node extends Element {
6437
6650
  this.height = minSize.height;
6438
6651
  }
6439
6652
  }
6440
- calculateContentMinSize(labelSize, iconBoxSize) {
6653
+ calculateContentMinSize(labelSize, iconBoxSize, bounds) {
6441
6654
  let minWidth = 0;
6442
6655
  let minHeight = 0;
6656
+ const labelContainer = this.getLabelContainerBounds(bounds);
6657
+ const widthFactor = labelContainer.width > 0 ? bounds.width / labelContainer.width : 1;
6658
+ const heightFactor = labelContainer.height > 0 ? bounds.height / labelContainer.height : 1;
6443
6659
  if (labelSize) {
6444
- minWidth = Math.max(minWidth, labelSize.width);
6445
- minHeight = Math.max(minHeight, labelSize.height);
6660
+ minWidth = Math.max(minWidth, labelSize.width * widthFactor);
6661
+ minHeight = Math.max(minHeight, labelSize.height * heightFactor);
6446
6662
  }
6447
6663
  if (iconBoxSize) {
6448
6664
  minWidth = Math.max(minWidth, iconBoxSize.width);
@@ -6468,16 +6684,70 @@ class Node extends Element {
6468
6684
  const labelVertical = labelPlacement === "top" || labelPlacement === "bottom";
6469
6685
  const iconHorizontal = iconPlacement === "left" || iconPlacement === "right";
6470
6686
  const iconVertical = iconPlacement === "top" || iconPlacement === "bottom";
6687
+ const labelSharesIconAxis = iconHorizontal && (labelPlacement === "center" || labelPlacement === iconPlacement) || iconVertical && (labelPlacement === "center" || labelPlacement === iconPlacement);
6471
6688
  if (labelHorizontal && iconHorizontal && labelPlacement !== iconPlacement) {
6472
6689
  minWidth = Math.max(minWidth, iconBoxSize.width + gap + labelSize.width);
6473
6690
  }
6474
6691
  if (labelVertical && iconVertical && labelPlacement !== iconPlacement) {
6475
6692
  minHeight = Math.max(minHeight, iconBoxSize.height + gap + labelSize.height);
6476
6693
  }
6694
+ if (labelSharesIconAxis) {
6695
+ if (iconHorizontal) {
6696
+ minWidth = Math.max(minWidth, iconBoxSize.width + gap + labelSize.width);
6697
+ }
6698
+ if (iconVertical) {
6699
+ minHeight = Math.max(minHeight, iconBoxSize.height + gap + labelSize.height);
6700
+ }
6701
+ }
6477
6702
  }
6478
6703
  }
6479
6704
  return { width: minWidth, height: minHeight };
6480
6705
  }
6706
+ getLabelContainerBounds(bounds) {
6707
+ return bounds;
6708
+ }
6709
+ getAutoLabelBounds(bounds, iconBoxSize) {
6710
+ const contentBounds = this.getLabelContainerBounds(bounds);
6711
+ if (!this._icon || !iconBoxSize || this._icon.placement === "center") {
6712
+ return contentBounds;
6713
+ }
6714
+ const gap = this._icon.gap;
6715
+ if (isCornerPlacement(this._icon.placement)) {
6716
+ return contentBounds;
6717
+ }
6718
+ switch (this._icon.placement) {
6719
+ case "left":
6720
+ return {
6721
+ x: contentBounds.x + iconBoxSize.width + gap,
6722
+ y: contentBounds.y,
6723
+ width: Math.max(0, contentBounds.width - iconBoxSize.width - gap),
6724
+ height: contentBounds.height
6725
+ };
6726
+ case "right":
6727
+ return {
6728
+ x: contentBounds.x,
6729
+ y: contentBounds.y,
6730
+ width: Math.max(0, contentBounds.width - iconBoxSize.width - gap),
6731
+ height: contentBounds.height
6732
+ };
6733
+ case "top":
6734
+ return {
6735
+ x: contentBounds.x,
6736
+ y: contentBounds.y + iconBoxSize.height + gap,
6737
+ width: contentBounds.width,
6738
+ height: Math.max(0, contentBounds.height - iconBoxSize.height - gap)
6739
+ };
6740
+ case "bottom":
6741
+ return {
6742
+ x: contentBounds.x,
6743
+ y: contentBounds.y,
6744
+ width: contentBounds.width,
6745
+ height: Math.max(0, contentBounds.height - iconBoxSize.height - gap)
6746
+ };
6747
+ default:
6748
+ return contentBounds;
6749
+ }
6750
+ }
6481
6751
  getLabelBounds(bounds, labelSize, placement) {
6482
6752
  const normalizedPlacement = placement === "auto" ? "center" : placement;
6483
6753
  const width = Math.min(labelSize.width, bounds.width);
@@ -6901,6 +7171,16 @@ class CircleNode extends Node {
6901
7171
  y: center.y + dy * scale
6902
7172
  };
6903
7173
  }
7174
+ getLabelContainerBounds(bounds) {
7175
+ const width = bounds.width / Math.SQRT2;
7176
+ const height = bounds.height / Math.SQRT2;
7177
+ return {
7178
+ x: bounds.x + (bounds.width - width) / 2,
7179
+ y: bounds.y + (bounds.height - height) / 2,
7180
+ width,
7181
+ height
7182
+ };
7183
+ }
6904
7184
  render(ctx) {
6905
7185
  const center = this.getCenter();
6906
7186
  const rx = this._width / 2;
@@ -6952,6 +7232,16 @@ class DiamondNode extends Node {
6952
7232
  y: center.y + dy * t
6953
7233
  };
6954
7234
  }
7235
+ getLabelContainerBounds(bounds) {
7236
+ const width = bounds.width / 2;
7237
+ const height = bounds.height / 2;
7238
+ return {
7239
+ x: bounds.x + (bounds.width - width) / 2,
7240
+ y: bounds.y + (bounds.height - height) / 2,
7241
+ width,
7242
+ height
7243
+ };
7244
+ }
6955
7245
  render(ctx) {
6956
7246
  const center = this.getCenter();
6957
7247
  const hw = this._width / 2;