@ngroznykh/papirus 0.5.9 → 0.6.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.
Files changed (38) hide show
  1. package/dist/core/DiagramRenderer.d.ts +1 -0
  2. package/dist/core/DiagramRenderer.d.ts.map +1 -1
  3. package/dist/core/InteractionManager.d.ts +3 -0
  4. package/dist/core/InteractionManager.d.ts.map +1 -1
  5. package/dist/elements/Edge.d.ts +29 -2
  6. package/dist/elements/Edge.d.ts.map +1 -1
  7. package/dist/elements/NodeImage.d.ts.map +1 -1
  8. package/dist/elements/composite/CComponent.d.ts +109 -0
  9. package/dist/elements/composite/CComponent.d.ts.map +1 -0
  10. package/dist/elements/composite/CContainer.d.ts +58 -0
  11. package/dist/elements/composite/CContainer.d.ts.map +1 -0
  12. package/dist/elements/composite/CDivider.d.ts +37 -0
  13. package/dist/elements/composite/CDivider.d.ts.map +1 -0
  14. package/dist/elements/composite/CIcon.d.ts +58 -0
  15. package/dist/elements/composite/CIcon.d.ts.map +1 -0
  16. package/dist/elements/composite/CShape.d.ts +51 -0
  17. package/dist/elements/composite/CShape.d.ts.map +1 -0
  18. package/dist/elements/composite/CText.d.ts +82 -0
  19. package/dist/elements/composite/CText.d.ts.map +1 -0
  20. package/dist/elements/composite/CompositeNode.d.ts +60 -0
  21. package/dist/elements/composite/CompositeNode.d.ts.map +1 -0
  22. package/dist/elements/composite/FlexLayout.d.ts +49 -0
  23. package/dist/elements/composite/FlexLayout.d.ts.map +1 -0
  24. package/dist/elements/composite/deserialize.d.ts +6 -0
  25. package/dist/elements/composite/deserialize.d.ts.map +1 -0
  26. package/dist/elements/composite/index.d.ts +26 -0
  27. package/dist/elements/composite/index.d.ts.map +1 -0
  28. package/dist/index.d.ts +3 -1
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/papirus.js +1908 -274
  31. package/dist/papirus.js.map +1 -1
  32. package/dist/types.d.ts +21 -0
  33. package/dist/types.d.ts.map +1 -1
  34. package/dist/utils/Serializer.d.ts.map +1 -1
  35. package/dist/utils/SvgExporter.d.ts.map +1 -1
  36. package/dist/utils/svgTint.d.ts +20 -0
  37. package/dist/utils/svgTint.d.ts.map +1 -0
  38. package/package.json +1 -1
package/dist/papirus.js CHANGED
@@ -2903,15 +2903,15 @@ class TextLabel {
2903
2903
  this.applyStyle(ctx);
2904
2904
  const lineHeight = (this._style.fontSize ?? 14) * 1.2;
2905
2905
  const effectiveMaxWidth = this._maxWidth ?? this._autoMaxWidth;
2906
- const text = this._text ?? "";
2906
+ const text2 = this._text ?? "";
2907
2907
  if (effectiveMaxWidth !== void 0) {
2908
2908
  const maxWidth = Math.max(
2909
2909
  0,
2910
2910
  effectiveMaxWidth - this._inset.left - this._inset.right
2911
2911
  );
2912
- this._lines = this.wrapText(ctx, text, maxWidth);
2912
+ this._lines = this.wrapText(ctx, text2, maxWidth);
2913
2913
  } else {
2914
- this._lines = text.split("\n");
2914
+ this._lines = text2.split("\n");
2915
2915
  }
2916
2916
  let maxLineWidth = 0;
2917
2917
  for (const line of this._lines) {
@@ -3005,9 +3005,9 @@ class TextLabel {
3005
3005
  ctx.textAlign = this._style.align ?? "center";
3006
3006
  ctx.textBaseline = this._style.baseline ?? "middle";
3007
3007
  }
3008
- wrapText(ctx, text, maxWidth) {
3008
+ wrapText(ctx, text2, maxWidth) {
3009
3009
  const lines = [];
3010
- const paragraphs = text.split("\n");
3010
+ const paragraphs = text2.split("\n");
3011
3011
  for (const paragraph of paragraphs) {
3012
3012
  const words = paragraph.split(" ");
3013
3013
  let currentLine = "";
@@ -4239,6 +4239,8 @@ class Edge extends Element {
4239
4239
  this._pathStrategy = this.getPathStrategy(this._type);
4240
4240
  this._lockAnchors = options.lockAnchors ?? true;
4241
4241
  this._labelOffset = options.labelOffset ?? 0;
4242
+ this._labelPosition = options.labelPosition ?? 0.5;
4243
+ this._labelFollowPath = options.labelFollowPath ?? false;
4242
4244
  this._labelBackground = options.labelBackground;
4243
4245
  this._labelLineGap = options.labelLineGap ?? false;
4244
4246
  if (options.label !== void 0) {
@@ -4332,6 +4334,31 @@ class Edge extends Element {
4332
4334
  this.markDirty();
4333
4335
  }
4334
4336
  }
4337
+ /**
4338
+ * Position along path (0 = source, 0.5 = midpoint, 1 = target)
4339
+ */
4340
+ get labelPosition() {
4341
+ return this._labelPosition;
4342
+ }
4343
+ set labelPosition(value) {
4344
+ const clamped = Math.max(0, Math.min(1, value));
4345
+ if (this._labelPosition !== clamped) {
4346
+ this._labelPosition = clamped;
4347
+ this.markDirty();
4348
+ }
4349
+ }
4350
+ /**
4351
+ * Whether label text rotates to follow the path tangent
4352
+ */
4353
+ get labelFollowPath() {
4354
+ return this._labelFollowPath;
4355
+ }
4356
+ set labelFollowPath(value) {
4357
+ if (this._labelFollowPath !== value) {
4358
+ this._labelFollowPath = value;
4359
+ this.markDirty();
4360
+ }
4361
+ }
4335
4362
  /**
4336
4363
  * Label background configuration
4337
4364
  */
@@ -4767,11 +4794,20 @@ class Edge extends Element {
4767
4794
  }
4768
4795
  const labelWidth = this._label.measuredWidth;
4769
4796
  const labelHeight = this._label.measuredHeight;
4797
+ const rot = this.getLabelRotation();
4798
+ let effectiveWidth = labelWidth;
4799
+ let effectiveHeight = labelHeight;
4800
+ if (rot !== 0) {
4801
+ const cosR = Math.abs(Math.cos(rot));
4802
+ const sinR = Math.abs(Math.sin(rot));
4803
+ effectiveWidth = labelWidth * cosR + labelHeight * sinR;
4804
+ effectiveHeight = labelWidth * sinR + labelHeight * cosR;
4805
+ }
4770
4806
  const labelRect = {
4771
- x: labelCenter.x - labelWidth / 2,
4772
- y: labelCenter.y - labelHeight / 2,
4773
- width: labelWidth,
4774
- height: labelHeight
4807
+ x: labelCenter.x - effectiveWidth / 2,
4808
+ y: labelCenter.y - effectiveHeight / 2,
4809
+ width: effectiveWidth,
4810
+ height: effectiveHeight
4775
4811
  };
4776
4812
  const segmentIntersections = [];
4777
4813
  for (let i = 0; i < polyline.length - 1; i++) {
@@ -5045,10 +5081,12 @@ class Edge extends Element {
5045
5081
  if (this._label === void 0 || this._path.length < 2) {
5046
5082
  return;
5047
5083
  }
5048
- const midpoint = this.getPathMidpoint();
5049
- const labelPosition = {
5050
- x: midpoint.x,
5051
- y: midpoint.y + this._labelOffset
5084
+ const { point, angle: pathAngle } = this.getPathPointAt(this._labelPosition);
5085
+ const rotation = this.getLabelRotation();
5086
+ const perpAngle = this._labelFollowPath ? pathAngle + Math.PI / 2 : Math.PI / 2;
5087
+ const labelCenter = {
5088
+ x: point.x + this._labelOffset * Math.cos(perpAngle),
5089
+ y: point.y + this._labelOffset * Math.sin(perpAngle)
5052
5090
  };
5053
5091
  const labelOpacity = this._label.style.opacity ?? 1;
5054
5092
  this._label.measure(ctx);
@@ -5057,29 +5095,39 @@ class Edge extends Element {
5057
5095
  const bgColor = this._labelBackground?.color ?? "#ffffff";
5058
5096
  const bgOpacity = this._labelBackground?.opacity ?? 1;
5059
5097
  const bgRadius = this._labelBackground?.borderRadius ?? EDGE_LABEL_BACKGROUND_RADIUS;
5060
- const bgX = labelPosition.x - labelWidth / 2;
5061
- const bgY = labelPosition.y - labelHeight / 2;
5062
- const bgWidth = labelWidth;
5063
- const bgHeight = labelHeight;
5098
+ ctx.save();
5099
+ if (rotation !== 0) {
5100
+ ctx.translate(labelCenter.x, labelCenter.y);
5101
+ ctx.rotate(rotation);
5102
+ ctx.translate(-labelCenter.x, -labelCenter.y);
5103
+ }
5104
+ const bgX = labelCenter.x - labelWidth / 2;
5105
+ const bgY = labelCenter.y - labelHeight / 2;
5064
5106
  ctx.fillStyle = bgColor;
5065
5107
  ctx.globalAlpha = bgOpacity;
5066
5108
  if (bgRadius > 0) {
5067
- this.drawRoundedRect(ctx, bgX, bgY, bgWidth, bgHeight, bgRadius);
5109
+ this.drawRoundedRect(ctx, bgX, bgY, labelWidth, labelHeight, bgRadius);
5068
5110
  ctx.fill();
5069
5111
  } else {
5070
- ctx.fillRect(bgX, bgY, bgWidth, bgHeight);
5112
+ ctx.fillRect(bgX, bgY, labelWidth, labelHeight);
5071
5113
  }
5072
5114
  ctx.globalAlpha = labelOpacity;
5073
- this._label.renderAt(ctx, labelPosition);
5115
+ this._label.renderAt(ctx, labelCenter);
5074
5116
  ctx.globalAlpha = 1;
5117
+ ctx.restore();
5075
5118
  }
5076
5119
  drawRoundedRect(ctx, x, y, width, height, radius) {
5077
5120
  drawRoundedRectPath(ctx, x, y, width, height, radius);
5078
5121
  }
5079
- getPathMidpoint() {
5122
+ /**
5123
+ * Get a point along the sampled path at parameter t (0..1).
5124
+ * Also returns the tangent angle in radians at that point.
5125
+ */
5126
+ getPathPointAt(t) {
5080
5127
  const path = this._path;
5128
+ let samples;
5081
5129
  if (this._type === "bezier" && path.length >= 4) {
5082
- const samples = [];
5130
+ samples = [];
5083
5131
  const steps = 20;
5084
5132
  for (let i = 1; i + 2 < path.length; i += 3) {
5085
5133
  const p0 = path[i - 1];
@@ -5087,16 +5135,15 @@ class Edge extends Element {
5087
5135
  const p2 = path[i + 1];
5088
5136
  const p3 = path[i + 2];
5089
5137
  for (let s = 0; s <= steps; s++) {
5090
- const t = s / steps;
5091
- if (samples.length > 0 && t === 0) {
5092
- continue;
5093
- }
5094
- samples.push(bezierPoint(p0, p1, p2, p3, t));
5138
+ const st = s / steps;
5139
+ if (samples.length > 0 && st === 0) continue;
5140
+ samples.push(bezierPoint(p0, p1, p2, p3, st));
5095
5141
  }
5096
5142
  }
5097
- return this.getPolylineMidpoint(samples);
5143
+ } else {
5144
+ samples = path;
5098
5145
  }
5099
- return this.getPolylineMidpoint(path);
5146
+ return this.getPointAlongPolyline(samples, t);
5100
5147
  }
5101
5148
  /**
5102
5149
  * Get world position of label center along path.
@@ -5105,18 +5152,33 @@ class Edge extends Element {
5105
5152
  if (this._path.length < 2) {
5106
5153
  return null;
5107
5154
  }
5108
- const midpoint = this.getPathMidpoint();
5155
+ const { point, angle: pathAngle } = this.getPathPointAt(this._labelPosition);
5156
+ const perpAngle = this._labelFollowPath ? pathAngle + Math.PI / 2 : Math.PI / 2;
5109
5157
  return {
5110
- x: midpoint.x,
5111
- y: midpoint.y + this._labelOffset
5158
+ x: point.x + this._labelOffset * Math.cos(perpAngle),
5159
+ y: point.y + this._labelOffset * Math.sin(perpAngle)
5112
5160
  };
5113
5161
  }
5114
- getPolylineMidpoint(path) {
5162
+ /**
5163
+ * Get label rotation angle in radians (if labelFollowPath is true).
5164
+ */
5165
+ getLabelRotation() {
5166
+ if (!this._labelFollowPath || this._path.length < 2) return 0;
5167
+ const { angle: angle2 } = this.getPathPointAt(this._labelPosition);
5168
+ let a = angle2;
5169
+ if (a > Math.PI / 2) a -= Math.PI;
5170
+ if (a < -Math.PI / 2) a += Math.PI;
5171
+ return a;
5172
+ }
5173
+ /**
5174
+ * Get point and tangent angle at parameter t along a polyline.
5175
+ */
5176
+ getPointAlongPolyline(path, t) {
5115
5177
  if (path.length === 0) {
5116
- return { x: 0, y: 0 };
5178
+ return { point: { x: 0, y: 0 }, angle: 0 };
5117
5179
  }
5118
5180
  if (path.length === 1) {
5119
- return path[0];
5181
+ return { point: path[0], angle: 0 };
5120
5182
  }
5121
5183
  let totalLength = 0;
5122
5184
  const segments = [];
@@ -5127,19 +5189,28 @@ class Edge extends Element {
5127
5189
  segments.push({ start, end, length });
5128
5190
  totalLength += length;
5129
5191
  }
5130
- const halfLength = totalLength / 2;
5192
+ const targetLength = totalLength * Math.max(0, Math.min(1, t));
5131
5193
  let accumulated = 0;
5132
5194
  for (const seg of segments) {
5133
- if (accumulated + seg.length >= halfLength) {
5134
- const t = (halfLength - accumulated) / seg.length;
5135
- return {
5136
- x: seg.start.x + t * (seg.end.x - seg.start.x),
5137
- y: seg.start.y + t * (seg.end.y - seg.start.y)
5195
+ if (accumulated + seg.length >= targetLength) {
5196
+ const segT = seg.length > 0 ? (targetLength - accumulated) / seg.length : 0;
5197
+ const point = {
5198
+ x: seg.start.x + segT * (seg.end.x - seg.start.x),
5199
+ y: seg.start.y + segT * (seg.end.y - seg.start.y)
5138
5200
  };
5201
+ const angle2 = Math.atan2(seg.end.y - seg.start.y, seg.end.x - seg.start.x);
5202
+ return { point, angle: angle2 };
5139
5203
  }
5140
5204
  accumulated += seg.length;
5141
5205
  }
5142
- return path[0];
5206
+ const lastSeg = segments[segments.length - 1];
5207
+ return {
5208
+ point: lastSeg.end,
5209
+ angle: Math.atan2(
5210
+ lastSeg.end.y - lastSeg.start.y,
5211
+ lastSeg.end.x - lastSeg.start.x
5212
+ )
5213
+ };
5143
5214
  }
5144
5215
  getPathStrategy(type) {
5145
5216
  switch (type) {
@@ -5187,10 +5258,10 @@ class LabelEditor {
5187
5258
  /**
5188
5259
  * Start editing a label
5189
5260
  */
5190
- start(kind, id, text, worldPosition, worldToScreen, onCommit) {
5261
+ start(kind, id, text2, worldPosition, worldToScreen, onCommit) {
5191
5262
  this.finish(true);
5192
5263
  const screenPoint = worldToScreen(worldPosition.x, worldPosition.y);
5193
- const textarea = this.createTextarea(text, screenPoint);
5264
+ const textarea = this.createTextarea(text2, screenPoint);
5194
5265
  const { cleanup } = this.setupEventHandlers(textarea, onCommit);
5195
5266
  document.body.appendChild(textarea);
5196
5267
  textarea.focus();
@@ -5214,9 +5285,9 @@ class LabelEditor {
5214
5285
  onCommit(kind, id, value);
5215
5286
  }
5216
5287
  }
5217
- createTextarea(text, screenPoint) {
5288
+ createTextarea(text2, screenPoint) {
5218
5289
  const textarea = document.createElement("textarea");
5219
- textarea.value = text;
5290
+ textarea.value = text2;
5220
5291
  textarea.rows = 1;
5221
5292
  textarea.spellcheck = false;
5222
5293
  textarea.setAttribute("aria-label", "Edit label");
@@ -5715,6 +5786,24 @@ class InteractionManager {
5715
5786
  return;
5716
5787
  }
5717
5788
  this.selectionManager.handleClick(event);
5789
+ this.emitComponentClick(event);
5790
+ }
5791
+ emitComponentClick(event) {
5792
+ const point = { x: event.worldX, y: event.worldY };
5793
+ const hitElement = this.renderer.getInteractableElementAtPoint(point, event.screenX, event.screenY);
5794
+ if (!hitElement || !("typeName" in hitElement)) return;
5795
+ const node = hitElement;
5796
+ if (node.typeName !== "composite") return;
5797
+ const compositeNode = node;
5798
+ const component = compositeNode.getComponentAtPoint(point);
5799
+ if (!component) return;
5800
+ if (component.id !== void 0) {
5801
+ this.renderer.emit("componentClick", node.id, component, point);
5802
+ }
5803
+ const asAny = component;
5804
+ if (typeof asAny["onClick"] === "function") {
5805
+ asAny["onClick"](component);
5806
+ }
5718
5807
  }
5719
5808
  handleDoubleClick(event) {
5720
5809
  if (this.renderer.blocksDiagramPointerAtScreen(event.screenX, event.screenY)) {
@@ -5742,6 +5831,10 @@ class InteractionManager {
5742
5831
  return;
5743
5832
  }
5744
5833
  if ("typeName" in hitElement) {
5834
+ if (hitElement.typeName === "composite") {
5835
+ this.startCompositeNameEdit(hitElement);
5836
+ return;
5837
+ }
5745
5838
  const editText = hitElement.label?.editableText ?? hitElement.label?.text ?? "";
5746
5839
  this.startLabelEdit("node", hitElement.id, editText, hitElement.getLabelPosition());
5747
5840
  return;
@@ -5851,16 +5944,66 @@ class InteractionManager {
5851
5944
  return false;
5852
5945
  }
5853
5946
  }
5854
- startLabelEdit(kind, id, text, worldPosition) {
5947
+ startLabelEdit(kind, id, text2, worldPosition) {
5855
5948
  this.labelEditor.start(
5856
5949
  kind,
5857
5950
  id,
5858
- text,
5951
+ text2,
5859
5952
  worldPosition,
5860
5953
  (x, y) => this.renderer.worldToScreen(x, y),
5861
5954
  (k, i, value) => this.handleLabelCommit(k, i, value)
5862
5955
  );
5863
5956
  }
5957
+ startCompositeNameEdit(node) {
5958
+ const nameText = this.findNameComponent(node.content);
5959
+ if (!nameText) return;
5960
+ const currentText = nameText.text;
5961
+ const worldPos = node.getLabelPosition();
5962
+ this.labelEditor.start(
5963
+ "node",
5964
+ node.id,
5965
+ currentText,
5966
+ worldPos,
5967
+ (x, y) => this.renderer.worldToScreen(x, y),
5968
+ (_kind, _id, value) => {
5969
+ const trimmed = value.trim();
5970
+ if (trimmed.length > 0 && trimmed !== currentText) {
5971
+ const before = currentText;
5972
+ nameText.text = trimmed;
5973
+ this.historyManager.execute({
5974
+ execute: () => {
5975
+ nameText.text = trimmed;
5976
+ },
5977
+ undo: () => {
5978
+ nameText.text = before;
5979
+ }
5980
+ });
5981
+ }
5982
+ }
5983
+ );
5984
+ }
5985
+ findNameComponent(container2) {
5986
+ for (const child of container2.children) {
5987
+ if (child.type === "text") {
5988
+ const ctext = child;
5989
+ if (ctext.role === "name") return ctext;
5990
+ }
5991
+ if (child.type === "container") {
5992
+ const found = this.findNameComponent(
5993
+ child
5994
+ );
5995
+ if (found) return found;
5996
+ }
5997
+ if (child.type === "shape") {
5998
+ const content = child.content;
5999
+ if (content) {
6000
+ const found = this.findNameComponent(content);
6001
+ if (found) return found;
6002
+ }
6003
+ }
6004
+ }
6005
+ return null;
6006
+ }
5864
6007
  handleLabelCommit(kind, id, nextValue) {
5865
6008
  if (kind === "node") {
5866
6009
  this.changeNodeProperties(id, (node) => {
@@ -6395,20 +6538,20 @@ class ContextMenuManager extends EventEmitter {
6395
6538
  }
6396
6539
  }
6397
6540
  buildMenu(items, target) {
6398
- const container = document.createElement("div");
6399
- container.style.position = "fixed";
6400
- container.style.zIndex = "10000";
6401
- container.style.background = "#ffffff";
6402
- container.style.border = "1px solid #e5e7eb";
6403
- container.style.borderRadius = "8px";
6404
- container.style.padding = "6px";
6405
- container.style.boxShadow = "0 8px 20px rgba(15, 23, 42, 0.18)";
6406
- container.style.fontFamily = "system-ui, -apple-system, Segoe UI, sans-serif";
6407
- container.style.fontSize = "14px";
6408
- container.style.color = "#0f172a";
6541
+ const container2 = document.createElement("div");
6542
+ container2.style.position = "fixed";
6543
+ container2.style.zIndex = "10000";
6544
+ container2.style.background = "#ffffff";
6545
+ container2.style.border = "1px solid #e5e7eb";
6546
+ container2.style.borderRadius = "8px";
6547
+ container2.style.padding = "6px";
6548
+ container2.style.boxShadow = "0 8px 20px rgba(15, 23, 42, 0.18)";
6549
+ container2.style.fontFamily = "system-ui, -apple-system, Segoe UI, sans-serif";
6550
+ container2.style.fontSize = "14px";
6551
+ container2.style.color = "#0f172a";
6409
6552
  const list = this.buildList(items, target);
6410
- container.appendChild(list);
6411
- return container;
6553
+ container2.appendChild(list);
6554
+ return container2;
6412
6555
  }
6413
6556
  buildList(items, target) {
6414
6557
  const list = document.createElement("ul");
@@ -6510,7 +6653,7 @@ class ContextMenuManager extends EventEmitter {
6510
6653
  }
6511
6654
  return list;
6512
6655
  }
6513
- createIconElement(icon) {
6656
+ createIconElement(icon2) {
6514
6657
  const iconEl = document.createElement("span");
6515
6658
  iconEl.style.width = "16px";
6516
6659
  iconEl.style.height = "16px";
@@ -6519,16 +6662,16 @@ class ContextMenuManager extends EventEmitter {
6519
6662
  iconEl.style.justifyContent = "center";
6520
6663
  iconEl.style.textAlign = "center";
6521
6664
  iconEl.style.flexShrink = "0";
6522
- if (!icon) {
6665
+ if (!icon2) {
6523
6666
  iconEl.textContent = "";
6524
6667
  return iconEl;
6525
6668
  }
6526
- if (typeof icon === "string") {
6527
- if (this.isSvgString(icon)) {
6528
- iconEl.innerHTML = icon;
6669
+ if (typeof icon2 === "string") {
6670
+ if (this.isSvgString(icon2)) {
6671
+ iconEl.innerHTML = icon2;
6529
6672
  } else if (this.options.iconToUrl) {
6530
6673
  const img = document.createElement("img");
6531
- img.src = this.options.iconToUrl(icon);
6674
+ img.src = this.options.iconToUrl(icon2);
6532
6675
  img.alt = "";
6533
6676
  img.style.width = "16px";
6534
6677
  img.style.height = "16px";
@@ -6537,15 +6680,15 @@ class ContextMenuManager extends EventEmitter {
6537
6680
  } else {
6538
6681
  iconEl.classList.add("material-symbols-outlined");
6539
6682
  iconEl.style.fontSize = "16px";
6540
- iconEl.textContent = icon;
6683
+ iconEl.textContent = icon2;
6541
6684
  }
6542
6685
  return iconEl;
6543
6686
  }
6544
- if (icon.type === "svg" || icon.type === "html") {
6545
- iconEl.innerHTML = icon.value;
6687
+ if (icon2.type === "svg" || icon2.type === "html") {
6688
+ iconEl.innerHTML = icon2.value;
6546
6689
  } else if (this.options.iconToUrl) {
6547
6690
  const img = document.createElement("img");
6548
- img.src = this.options.iconToUrl(icon.value);
6691
+ img.src = this.options.iconToUrl(icon2.value);
6549
6692
  img.alt = "";
6550
6693
  img.style.width = "16px";
6551
6694
  img.style.height = "16px";
@@ -6554,7 +6697,7 @@ class ContextMenuManager extends EventEmitter {
6554
6697
  } else {
6555
6698
  iconEl.classList.add("material-symbols-outlined");
6556
6699
  iconEl.style.fontSize = "16px";
6557
- iconEl.textContent = icon.value;
6700
+ iconEl.textContent = icon2.value;
6558
6701
  }
6559
6702
  return iconEl;
6560
6703
  }
@@ -8435,10 +8578,6 @@ class LRUCache {
8435
8578
  return this.maxSize;
8436
8579
  }
8437
8580
  }
8438
- function isCornerPlacement(placement) {
8439
- return placement === "top-left" || placement === "top-right" || placement === "bottom-left" || placement === "bottom-right";
8440
- }
8441
- const svgTextCache = new LRUCache(100);
8442
8581
  function styleSetColor(style, key, color) {
8443
8582
  const hasKey = new RegExp(`${key}\\s*:`).test(style);
8444
8583
  if (hasKey) {
@@ -8489,6 +8628,18 @@ function svgToDataUrl(svg) {
8489
8628
  const encoded = encodeURIComponent(svg).replace(/%0A/g, "").replace(/%0D/g, "").replace(/%09/g, " ").replace(/%20/g, " ");
8490
8629
  return `data:image/svg+xml;utf8,${encoded}`;
8491
8630
  }
8631
+ function isSvgUrl(url) {
8632
+ try {
8633
+ const path = new URL(url, "http://localhost").pathname;
8634
+ return path.endsWith(".svg");
8635
+ } catch {
8636
+ return url.endsWith(".svg");
8637
+ }
8638
+ }
8639
+ function isCornerPlacement(placement) {
8640
+ return placement === "top-left" || placement === "top-right" || placement === "bottom-left" || placement === "bottom-right";
8641
+ }
8642
+ const svgTextCache = new LRUCache(100);
8492
8643
  class NodeImage {
8493
8644
  constructor(options, onChange) {
8494
8645
  this._loaded = false;
@@ -10349,181 +10500,1603 @@ const ShapeFactories = {
10349
10500
  }
10350
10501
  }
10351
10502
  };
10352
- const DEFAULT_THEME = {
10353
- name: "default",
10354
- colors: {
10355
- background: "#ffffff",
10356
- grid: "#e5e5e5",
10357
- selection: "rgba(59, 130, 246, 0.1)",
10358
- connectionPreview: "#3b82f6"
10359
- },
10360
- node: {
10361
- default: {
10362
- fillColor: "#ffffff",
10363
- strokeColor: "#333333",
10364
- strokeWidth: 2,
10365
- opacity: 1,
10366
- cornerRadius: 4
10367
- },
10368
- hover: {
10369
- fillColor: "#f5f5f5",
10370
- strokeColor: "#6366f1",
10371
- strokeWidth: 2,
10372
- opacity: 1,
10373
- cornerRadius: 4
10374
- },
10375
- selected: {
10376
- strokeColor: "#3b82f6",
10377
- strokeWidth: 2,
10378
- opacity: 1
10379
- },
10380
- dragging: {
10381
- strokeColor: "#333333",
10382
- strokeWidth: 2,
10383
- opacity: 0.8
10384
- }
10385
- },
10386
- edge: {
10387
- default: {
10388
- strokeColor: "#666666",
10389
- strokeWidth: 2,
10390
- opacity: 1
10391
- },
10392
- hover: {
10393
- strokeColor: "#6366f1",
10394
- strokeWidth: 2,
10395
- opacity: 1
10396
- },
10397
- selected: {
10398
- strokeColor: "#3b82f6",
10399
- strokeWidth: 3,
10400
- opacity: 1
10401
- }
10402
- },
10403
- text: {
10404
- font: "14px sans-serif",
10405
- fontSize: 14,
10406
- fontFamily: "sans-serif",
10407
- fontWeight: "normal",
10408
- color: "#333333",
10409
- align: "center",
10410
- baseline: "middle"
10411
- },
10412
- port: {
10413
- default: { color: "#666666", radius: 6 },
10414
- hover: { color: "#3b82f6", radius: 7 }
10415
- },
10416
- group: {
10417
- default: {
10418
- fillColor: "rgba(200, 200, 200, 0.2)",
10419
- strokeColor: "#999999",
10420
- strokeWidth: 1,
10421
- opacity: 1
10422
- },
10423
- selected: {
10424
- fillColor: "rgba(59, 130, 246, 0.1)",
10425
- strokeColor: "#3b82f6",
10426
- strokeWidth: 2,
10427
- opacity: 1
10503
+ const DEFAULT_FONT_FAMILY = "sans-serif";
10504
+ const DEFAULT_FONT_SIZE = 14;
10505
+ const DEFAULT_LINE_HEIGHT = 1.2;
10506
+ const DEFAULT_COLOR$1 = "#000000";
10507
+ class CText {
10508
+ constructor(options) {
10509
+ this.type = "text";
10510
+ this._cachedLines = null;
10511
+ this.id = options.id;
10512
+ this._text = options.text;
10513
+ this._fontFamily = options.fontFamily ?? DEFAULT_FONT_FAMILY;
10514
+ this._fontWeight = options.fontWeight ?? "normal";
10515
+ this._fontStyle = options.fontStyle ?? "normal";
10516
+ this._fontSize = options.fontSize ?? DEFAULT_FONT_SIZE;
10517
+ this._color = options.color ?? DEFAULT_COLOR$1;
10518
+ this._align = options.align ?? "center";
10519
+ this._verticalAlign = options.verticalAlign ?? "middle";
10520
+ this._maxLines = options.maxLines;
10521
+ this._lineHeight = options.lineHeight ?? DEFAULT_LINE_HEIGHT;
10522
+ this._role = options.role;
10523
+ this._rotation = options.rotation ?? 0;
10524
+ this.style = options.style ?? {};
10525
+ }
10526
+ // --- Property accessors with dirty notification ---
10527
+ get text() {
10528
+ return this._text;
10529
+ }
10530
+ set text(value) {
10531
+ if (this._text !== value) {
10532
+ this._text = value;
10533
+ this._cachedLines = null;
10534
+ this._onChange?.();
10428
10535
  }
10429
10536
  }
10430
- };
10431
- const DARK_THEME = {
10432
- name: "dark",
10433
- colors: {
10434
- background: "#1a1a1a",
10435
- grid: "#333333",
10436
- selection: "rgba(99, 102, 241, 0.2)",
10437
- connectionPreview: "#6366f1"
10438
- },
10439
- node: {
10440
- default: {
10441
- fillColor: "#2d2d2d",
10442
- strokeColor: "#555555",
10443
- strokeWidth: 2,
10444
- opacity: 1,
10445
- cornerRadius: 4
10446
- },
10447
- hover: {
10448
- fillColor: "#3d3d3d",
10449
- strokeColor: "#6366f1",
10450
- strokeWidth: 2,
10451
- opacity: 1,
10452
- cornerRadius: 4
10453
- },
10454
- selected: {
10455
- fillColor: "#2d2d2d",
10456
- strokeColor: "#555555",
10457
- strokeWidth: 2,
10458
- opacity: 1,
10459
- cornerRadius: 4
10460
- },
10461
- dragging: {
10462
- fillColor: "#404040",
10463
- strokeColor: "#555555",
10464
- strokeWidth: 2,
10465
- opacity: 0.8,
10466
- cornerRadius: 4
10537
+ get fontFamily() {
10538
+ return this._fontFamily;
10539
+ }
10540
+ set fontFamily(value) {
10541
+ if (this._fontFamily !== value) {
10542
+ this._fontFamily = value;
10543
+ this._onChange?.();
10467
10544
  }
10468
- },
10469
- edge: {
10470
- default: {
10471
- strokeColor: "#888888",
10472
- strokeWidth: 2,
10473
- opacity: 1
10474
- },
10475
- hover: {
10476
- strokeColor: "#6366f1",
10477
- strokeWidth: 2,
10478
- opacity: 1
10479
- },
10480
- selected: {
10481
- strokeColor: "#818cf8",
10482
- strokeWidth: 3,
10483
- opacity: 1
10545
+ }
10546
+ get fontWeight() {
10547
+ return this._fontWeight;
10548
+ }
10549
+ set fontWeight(value) {
10550
+ if (this._fontWeight !== value) {
10551
+ this._fontWeight = value;
10552
+ this._onChange?.();
10484
10553
  }
10485
- },
10486
- text: {
10487
- font: "14px sans-serif",
10488
- fontSize: 14,
10489
- fontFamily: "sans-serif",
10490
- fontWeight: "normal",
10491
- color: "#e0e0e0",
10492
- align: "center",
10493
- baseline: "middle"
10494
- },
10495
- port: {
10496
- default: { color: "#888888", radius: 6 },
10497
- hover: { color: "#6366f1", radius: 7 }
10498
- },
10499
- group: {
10500
- default: {
10501
- fillColor: "rgba(100, 100, 100, 0.2)",
10502
- strokeColor: "#666666",
10503
- strokeWidth: 1,
10504
- opacity: 1
10505
- },
10506
- selected: {
10507
- fillColor: "rgba(99, 102, 241, 0.2)",
10508
- strokeColor: "#6366f1",
10509
- strokeWidth: 2,
10510
- opacity: 1
10554
+ }
10555
+ get fontStyle() {
10556
+ return this._fontStyle;
10557
+ }
10558
+ set fontStyle(value) {
10559
+ if (this._fontStyle !== value) {
10560
+ this._fontStyle = value;
10561
+ this._onChange?.();
10511
10562
  }
10512
10563
  }
10513
- };
10514
- class StyleManager {
10515
- constructor(theme) {
10516
- this._classes = /* @__PURE__ */ new Map();
10517
- this._builtInThemes = /* @__PURE__ */ new Map([
10518
- ["default", DEFAULT_THEME],
10519
- ["dark", DARK_THEME]
10520
- ]);
10521
- if (theme === void 0) {
10522
- this._theme = DEFAULT_THEME;
10523
- } else if (typeof theme === "string") {
10524
- this._theme = this._builtInThemes.get(theme) ?? DEFAULT_THEME;
10525
- } else {
10526
- this._theme = theme;
10564
+ get fontSize() {
10565
+ return this._fontSize;
10566
+ }
10567
+ set fontSize(value) {
10568
+ if (this._fontSize !== value) {
10569
+ this._fontSize = value;
10570
+ this._onChange?.();
10571
+ }
10572
+ }
10573
+ get color() {
10574
+ return this._color;
10575
+ }
10576
+ set color(value) {
10577
+ if (this._color !== value) {
10578
+ this._color = value;
10579
+ this._onChange?.();
10580
+ }
10581
+ }
10582
+ get align() {
10583
+ return this._align;
10584
+ }
10585
+ set align(value) {
10586
+ if (this._align !== value) {
10587
+ this._align = value;
10588
+ this._onChange?.();
10589
+ }
10590
+ }
10591
+ get verticalAlign() {
10592
+ return this._verticalAlign;
10593
+ }
10594
+ set verticalAlign(value) {
10595
+ if (this._verticalAlign !== value) {
10596
+ this._verticalAlign = value;
10597
+ this._onChange?.();
10598
+ }
10599
+ }
10600
+ get maxLines() {
10601
+ return this._maxLines;
10602
+ }
10603
+ set maxLines(value) {
10604
+ if (this._maxLines !== value) {
10605
+ this._maxLines = value;
10606
+ this._onChange?.();
10607
+ }
10608
+ }
10609
+ get lineHeight() {
10610
+ return this._lineHeight;
10611
+ }
10612
+ get role() {
10613
+ return this._role;
10614
+ }
10615
+ get rotation() {
10616
+ return this._rotation;
10617
+ }
10618
+ set rotation(value) {
10619
+ if (this._rotation !== value) {
10620
+ this._rotation = value;
10621
+ this._onChange?.();
10622
+ }
10623
+ }
10624
+ setOnChange(cb) {
10625
+ this._onChange = cb;
10626
+ }
10627
+ getFont() {
10628
+ return `${this._fontStyle} ${this._fontWeight} ${this._fontSize}px ${this._fontFamily}`;
10629
+ }
10630
+ getLineHeightPx() {
10631
+ return this._fontSize * this._lineHeight;
10632
+ }
10633
+ /**
10634
+ * Word-wrap text to fit within maxWidth.
10635
+ */
10636
+ wrapText(ctx, maxWidth) {
10637
+ ctx.font = this.getFont();
10638
+ const words = this._text.split(" ");
10639
+ const lines = [];
10640
+ let currentLine = "";
10641
+ for (const word of words) {
10642
+ const testLine = currentLine ? `${currentLine} ${word}` : word;
10643
+ const metrics = ctx.measureText(testLine);
10644
+ if (metrics.width > maxWidth && currentLine) {
10645
+ lines.push(currentLine);
10646
+ currentLine = word;
10647
+ } else {
10648
+ currentLine = testLine;
10649
+ }
10650
+ }
10651
+ if (currentLine) {
10652
+ lines.push(currentLine);
10653
+ }
10654
+ if (this._maxLines !== void 0 && lines.length > this._maxLines) {
10655
+ const truncated = lines.slice(0, this._maxLines);
10656
+ const lastLine = truncated[truncated.length - 1];
10657
+ if (lastLine !== void 0) {
10658
+ truncated[truncated.length - 1] = lastLine + "…";
10659
+ }
10660
+ return truncated;
10661
+ }
10662
+ return lines;
10663
+ }
10664
+ measure(ctx) {
10665
+ ctx.font = this.getFont();
10666
+ const lineHeightPx = this.getLineHeightPx();
10667
+ const rawLines = this._text.split("\n");
10668
+ let maxWidth = 0;
10669
+ for (const line of rawLines) {
10670
+ const metrics = ctx.measureText(line);
10671
+ if (metrics.width > maxWidth) {
10672
+ maxWidth = metrics.width;
10673
+ }
10674
+ }
10675
+ let lineCount = rawLines.length;
10676
+ if (this._maxLines !== void 0 && lineCount > this._maxLines) {
10677
+ lineCount = this._maxLines;
10678
+ }
10679
+ const width = maxWidth;
10680
+ const height = lineCount * lineHeightPx;
10681
+ if (this._rotation === 90 || this._rotation === -90) {
10682
+ return { width: height, height: width };
10683
+ }
10684
+ return { width, height };
10685
+ }
10686
+ render(ctx, bounds) {
10687
+ if (this.style.visible === false) return;
10688
+ const opacity = this.style.opacity ?? 1;
10689
+ if (opacity <= 0) return;
10690
+ ctx.save();
10691
+ ctx.globalAlpha *= opacity;
10692
+ ctx.font = this.getFont();
10693
+ ctx.fillStyle = this._color;
10694
+ const isRotated = this._rotation === 90 || this._rotation === -90;
10695
+ const textBounds = isRotated ? { x: bounds.x, y: bounds.y, width: bounds.height, height: bounds.width } : bounds;
10696
+ const lines = this.wrapText(ctx, textBounds.width);
10697
+ this._cachedLines = lines;
10698
+ const lineHeightPx = this.getLineHeightPx();
10699
+ const totalTextHeight = lines.length * lineHeightPx;
10700
+ let textAlign;
10701
+ let xBase;
10702
+ switch (this._align) {
10703
+ case "left":
10704
+ textAlign = "left";
10705
+ xBase = textBounds.x;
10706
+ break;
10707
+ case "right":
10708
+ textAlign = "right";
10709
+ xBase = textBounds.x + textBounds.width;
10710
+ break;
10711
+ default:
10712
+ textAlign = "center";
10713
+ xBase = textBounds.x + textBounds.width / 2;
10714
+ break;
10715
+ }
10716
+ let yStart;
10717
+ switch (this._verticalAlign) {
10718
+ case "top":
10719
+ yStart = textBounds.y + lineHeightPx / 2;
10720
+ break;
10721
+ case "bottom":
10722
+ yStart = textBounds.y + textBounds.height - totalTextHeight + lineHeightPx / 2;
10723
+ break;
10724
+ default:
10725
+ yStart = textBounds.y + (textBounds.height - totalTextHeight) / 2 + lineHeightPx / 2;
10726
+ break;
10727
+ }
10728
+ ctx.textAlign = textAlign;
10729
+ ctx.textBaseline = "middle";
10730
+ if (isRotated) {
10731
+ const cx = bounds.x + bounds.width / 2;
10732
+ const cy = bounds.y + bounds.height / 2;
10733
+ ctx.translate(cx, cy);
10734
+ ctx.rotate(this._rotation * Math.PI / 180);
10735
+ const offsetX = xBase - (textBounds.x + textBounds.width / 2);
10736
+ const offsetY = yStart - (textBounds.y + textBounds.height / 2);
10737
+ for (let i = 0; i < lines.length; i++) {
10738
+ ctx.fillText(lines[i], offsetX, offsetY + i * lineHeightPx);
10739
+ }
10740
+ } else {
10741
+ for (let i = 0; i < lines.length; i++) {
10742
+ ctx.fillText(lines[i], xBase, yStart + i * lineHeightPx);
10743
+ }
10744
+ }
10745
+ ctx.restore();
10746
+ }
10747
+ hitTest(point, bounds) {
10748
+ if (this.style.visible === false) return null;
10749
+ if (point.x >= bounds.x && point.x <= bounds.x + bounds.width && point.y >= bounds.y && point.y <= bounds.y + bounds.height) {
10750
+ return this;
10751
+ }
10752
+ return null;
10753
+ }
10754
+ serialize() {
10755
+ const data = {
10756
+ type: "text",
10757
+ text: this._text
10758
+ };
10759
+ if (this.id !== void 0) data.id = this.id;
10760
+ if (this.style && Object.keys(this.style).length > 0) data.style = this.style;
10761
+ if (this._fontFamily !== DEFAULT_FONT_FAMILY) data.fontFamily = this._fontFamily;
10762
+ if (this._fontWeight !== "normal") data.fontWeight = this._fontWeight;
10763
+ if (this._fontStyle !== "normal") data.fontStyle = this._fontStyle;
10764
+ if (this._fontSize !== DEFAULT_FONT_SIZE) data.fontSize = this._fontSize;
10765
+ if (this._color !== DEFAULT_COLOR$1) data.color = this._color;
10766
+ if (this._align !== "center") data.align = this._align;
10767
+ if (this._verticalAlign !== "middle") data.verticalAlign = this._verticalAlign;
10768
+ if (this._maxLines !== void 0) data.maxLines = this._maxLines;
10769
+ if (this._lineHeight !== DEFAULT_LINE_HEIGHT) data.lineHeight = this._lineHeight;
10770
+ if (this._role !== void 0) data.role = this._role;
10771
+ if (this._rotation !== 0) data.rotation = this._rotation;
10772
+ return data;
10773
+ }
10774
+ toSVG(bounds) {
10775
+ if (this.style.visible === false) return "";
10776
+ const lineHeightPx = this.getLineHeightPx();
10777
+ const displayLines = this._cachedLines ?? this._text.split("\n");
10778
+ const totalTextHeight = displayLines.length * lineHeightPx;
10779
+ let anchor;
10780
+ let xBase;
10781
+ switch (this._align) {
10782
+ case "left":
10783
+ anchor = "start";
10784
+ xBase = bounds.x;
10785
+ break;
10786
+ case "right":
10787
+ anchor = "end";
10788
+ xBase = bounds.x + bounds.width;
10789
+ break;
10790
+ default:
10791
+ anchor = "middle";
10792
+ xBase = bounds.x + bounds.width / 2;
10793
+ break;
10794
+ }
10795
+ let yStart;
10796
+ switch (this._verticalAlign) {
10797
+ case "top":
10798
+ yStart = bounds.y + lineHeightPx * 0.75;
10799
+ break;
10800
+ case "bottom":
10801
+ yStart = bounds.y + bounds.height - totalTextHeight + lineHeightPx * 0.75;
10802
+ break;
10803
+ default:
10804
+ yStart = bounds.y + (bounds.height - totalTextHeight) / 2 + lineHeightPx * 0.75;
10805
+ break;
10806
+ }
10807
+ const fontAttrs = [
10808
+ `font-family="${this._fontFamily}"`,
10809
+ `font-size="${this._fontSize}"`,
10810
+ this._fontWeight !== "normal" ? `font-weight="${this._fontWeight}"` : "",
10811
+ this._fontStyle !== "normal" ? `font-style="${this._fontStyle}"` : ""
10812
+ ].filter(Boolean).join(" ");
10813
+ const transform = this._rotation !== 0 ? ` transform="rotate(${this._rotation}, ${bounds.x + bounds.width / 2}, ${bounds.y + bounds.height / 2})"` : "";
10814
+ const tspans = displayLines.map(
10815
+ (line, i) => `<tspan x="${xBase}" dy="${i === 0 ? 0 : lineHeightPx}">${escapeXml(line)}</tspan>`
10816
+ ).join("");
10817
+ return `<text x="${xBase}" y="${yStart}" ${fontAttrs} fill="${this._color}" text-anchor="${anchor}"${transform}>${tspans}</text>`;
10818
+ }
10819
+ }
10820
+ function escapeXml(str) {
10821
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
10822
+ }
10823
+ const DEFAULT_ICON_SIZE = 24;
10824
+ const svgFetchCache = new LRUCache(100);
10825
+ class CIcon {
10826
+ constructor(options) {
10827
+ this.type = "icon";
10828
+ this._loaded = false;
10829
+ this.id = options.id;
10830
+ this._source = options.source;
10831
+ this._width = options.width ?? DEFAULT_ICON_SIZE;
10832
+ this._height = options.height ?? DEFAULT_ICON_SIZE;
10833
+ this._backgroundColor = options.backgroundColor;
10834
+ this._fillColor = options.fillColor;
10835
+ this._bindsNotationIcon = options.bindsNotationIcon ?? false;
10836
+ this.onClick = options.onClick;
10837
+ this.style = options.style ?? {};
10838
+ if (options.visible === false) {
10839
+ this.style.visible = false;
10840
+ }
10841
+ this._image = new Image();
10842
+ this._image.onload = () => {
10843
+ this._loaded = true;
10844
+ this._onChange?.();
10845
+ };
10846
+ this._image.onerror = () => {
10847
+ this._onChange?.();
10848
+ };
10849
+ void this.loadSource(this._source);
10850
+ }
10851
+ get source() {
10852
+ return this._source;
10853
+ }
10854
+ set source(value) {
10855
+ if (this._source !== value) {
10856
+ this._source = value;
10857
+ this._loaded = false;
10858
+ void this.loadSource(value);
10859
+ this._onChange?.();
10860
+ }
10861
+ }
10862
+ get width() {
10863
+ return this._width;
10864
+ }
10865
+ set width(value) {
10866
+ if (this._width !== value) {
10867
+ this._width = value;
10868
+ this._onChange?.();
10869
+ }
10870
+ }
10871
+ get height() {
10872
+ return this._height;
10873
+ }
10874
+ set height(value) {
10875
+ if (this._height !== value) {
10876
+ this._height = value;
10877
+ this._onChange?.();
10878
+ }
10879
+ }
10880
+ get backgroundColor() {
10881
+ return this._backgroundColor;
10882
+ }
10883
+ set backgroundColor(value) {
10884
+ if (this._backgroundColor !== value) {
10885
+ this._backgroundColor = value;
10886
+ this._onChange?.();
10887
+ }
10888
+ }
10889
+ get fillColor() {
10890
+ return this._fillColor;
10891
+ }
10892
+ set fillColor(value) {
10893
+ if (this._fillColor !== value) {
10894
+ this._fillColor = value;
10895
+ void this.loadSource(this._source);
10896
+ this._onChange?.();
10897
+ }
10898
+ }
10899
+ get loaded() {
10900
+ return this._loaded;
10901
+ }
10902
+ get bindsNotationIcon() {
10903
+ return this._bindsNotationIcon;
10904
+ }
10905
+ set bindsNotationIcon(value) {
10906
+ if (this._bindsNotationIcon !== value) {
10907
+ this._bindsNotationIcon = value;
10908
+ this._onChange?.();
10909
+ }
10910
+ }
10911
+ setOnChange(cb) {
10912
+ this._onChange = cb;
10913
+ }
10914
+ async loadSource(source) {
10915
+ if (isSvgMarkup(source)) {
10916
+ const tinted = this._fillColor ? tintSvg(source, void 0, this._fillColor) : source;
10917
+ this._image.src = svgToDataUrl(tinted);
10918
+ } else if (isSvgUrl(source) && this._fillColor) {
10919
+ try {
10920
+ const svgText = await this.fetchSvg(source);
10921
+ const tinted = tintSvg(svgText, void 0, this._fillColor);
10922
+ this._image.src = svgToDataUrl(tinted);
10923
+ } catch {
10924
+ this._image.src = source;
10925
+ }
10926
+ } else {
10927
+ this._image.src = source;
10928
+ }
10929
+ }
10930
+ async fetchSvg(url) {
10931
+ let promise = svgFetchCache.get(url);
10932
+ if (!promise) {
10933
+ promise = fetch(url).then((r) => r.text());
10934
+ svgFetchCache.set(url, promise);
10935
+ }
10936
+ return promise;
10937
+ }
10938
+ measure(_ctx) {
10939
+ return { width: this._width, height: this._height };
10940
+ }
10941
+ render(ctx, bounds) {
10942
+ if (this.style.visible === false) return;
10943
+ const opacity = this.style.opacity ?? 1;
10944
+ if (opacity <= 0) return;
10945
+ ctx.save();
10946
+ ctx.globalAlpha *= opacity;
10947
+ if (this._backgroundColor) {
10948
+ ctx.fillStyle = this._backgroundColor;
10949
+ ctx.fillRect(bounds.x, bounds.y, bounds.width, bounds.height);
10950
+ }
10951
+ if (this._loaded) {
10952
+ const drawWidth = Math.min(this._width, bounds.width);
10953
+ const drawHeight = Math.min(this._height, bounds.height);
10954
+ const dx = bounds.x + (bounds.width - drawWidth) / 2;
10955
+ const dy = bounds.y + (bounds.height - drawHeight) / 2;
10956
+ ctx.drawImage(this._image, dx, dy, drawWidth, drawHeight);
10957
+ }
10958
+ ctx.restore();
10959
+ }
10960
+ hitTest(point, bounds) {
10961
+ if (this.style.visible === false) return null;
10962
+ if (point.x >= bounds.x && point.x <= bounds.x + bounds.width && point.y >= bounds.y && point.y <= bounds.y + bounds.height) {
10963
+ return this;
10964
+ }
10965
+ return null;
10966
+ }
10967
+ serialize() {
10968
+ const data = {
10969
+ type: "icon",
10970
+ source: this._source,
10971
+ width: this._width,
10972
+ height: this._height
10973
+ };
10974
+ if (this.id !== void 0) data.id = this.id;
10975
+ if (this.style && Object.keys(this.style).length > 0) data.style = this.style;
10976
+ if (this._backgroundColor) data.backgroundColor = this._backgroundColor;
10977
+ if (this._fillColor) data.fillColor = this._fillColor;
10978
+ if (this._bindsNotationIcon) data.bindsNotationIcon = true;
10979
+ return data;
10980
+ }
10981
+ toSVG(bounds) {
10982
+ if (this.style.visible === false) return "";
10983
+ let svg = "";
10984
+ if (this._backgroundColor) {
10985
+ svg += `<rect x="${bounds.x}" y="${bounds.y}" width="${bounds.width}" height="${bounds.height}" fill="${this._backgroundColor}" />`;
10986
+ }
10987
+ const drawWidth = Math.min(this._width, bounds.width);
10988
+ const drawHeight = Math.min(this._height, bounds.height);
10989
+ const dx = bounds.x + (bounds.width - drawWidth) / 2;
10990
+ const dy = bounds.y + (bounds.height - drawHeight) / 2;
10991
+ svg += `<image href="${escapeXmlAttr(this._source)}" x="${dx}" y="${dy}" width="${drawWidth}" height="${drawHeight}" />`;
10992
+ return svg;
10993
+ }
10994
+ }
10995
+ function escapeXmlAttr(str) {
10996
+ return str.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
10997
+ }
10998
+ const DEFAULT_COLOR = "#cccccc";
10999
+ const DEFAULT_THICKNESS = 1;
11000
+ class CDivider {
11001
+ constructor(options = {}) {
11002
+ this.type = "divider";
11003
+ this.id = options.id;
11004
+ this._color = options.color ?? DEFAULT_COLOR;
11005
+ this._thickness = options.thickness ?? DEFAULT_THICKNESS;
11006
+ this.style = options.style ?? {};
11007
+ }
11008
+ get color() {
11009
+ return this._color;
11010
+ }
11011
+ set color(value) {
11012
+ if (this._color !== value) {
11013
+ this._color = value;
11014
+ this._onChange?.();
11015
+ }
11016
+ }
11017
+ get thickness() {
11018
+ return this._thickness;
11019
+ }
11020
+ set thickness(value) {
11021
+ if (this._thickness !== value) {
11022
+ this._thickness = value;
11023
+ this._onChange?.();
11024
+ }
11025
+ }
11026
+ setOnChange(cb) {
11027
+ this._onChange = cb;
11028
+ }
11029
+ measure(_ctx) {
11030
+ return { width: this._thickness, height: this._thickness };
11031
+ }
11032
+ render(ctx, bounds) {
11033
+ if (this.style.visible === false) return;
11034
+ ctx.save();
11035
+ ctx.strokeStyle = this._color;
11036
+ ctx.lineWidth = this._thickness;
11037
+ ctx.globalAlpha *= this.style.opacity ?? 1;
11038
+ ctx.beginPath();
11039
+ if (bounds.width >= bounds.height) {
11040
+ const y = bounds.y + bounds.height / 2;
11041
+ ctx.moveTo(bounds.x, y);
11042
+ ctx.lineTo(bounds.x + bounds.width, y);
11043
+ } else {
11044
+ const x = bounds.x + bounds.width / 2;
11045
+ ctx.moveTo(x, bounds.y);
11046
+ ctx.lineTo(x, bounds.y + bounds.height);
11047
+ }
11048
+ ctx.stroke();
11049
+ ctx.restore();
11050
+ }
11051
+ hitTest(_point, _bounds) {
11052
+ return null;
11053
+ }
11054
+ serialize() {
11055
+ const data = { type: "divider" };
11056
+ if (this.id !== void 0) data.id = this.id;
11057
+ if (this._color !== DEFAULT_COLOR) data.color = this._color;
11058
+ if (this._thickness !== DEFAULT_THICKNESS) data.thickness = this._thickness;
11059
+ if (this.style && Object.keys(this.style).length > 0) data.style = this.style;
11060
+ return data;
11061
+ }
11062
+ toSVG(bounds) {
11063
+ if (this.style.visible === false) return "";
11064
+ if (bounds.width >= bounds.height) {
11065
+ const y = bounds.y + bounds.height / 2;
11066
+ return `<line x1="${bounds.x}" y1="${y}" x2="${bounds.x + bounds.width}" y2="${y}" stroke="${this._color}" stroke-width="${this._thickness}" />`;
11067
+ } else {
11068
+ const x = bounds.x + bounds.width / 2;
11069
+ return `<line x1="${x}" y1="${bounds.y}" x2="${x}" y2="${bounds.y + bounds.height}" stroke="${this._color}" stroke-width="${this._thickness}" />`;
11070
+ }
11071
+ }
11072
+ }
11073
+ function normalizeSides(value, fallback = 0) {
11074
+ if (value === void 0) {
11075
+ return { top: fallback, right: fallback, bottom: fallback, left: fallback };
11076
+ }
11077
+ if (typeof value === "number") {
11078
+ return { top: value, right: value, bottom: value, left: value };
11079
+ }
11080
+ return {
11081
+ top: value.top ?? fallback,
11082
+ right: value.right ?? fallback,
11083
+ bottom: value.bottom ?? fallback,
11084
+ left: value.left ?? fallback
11085
+ };
11086
+ }
11087
+ function flexLayout(container2, config, children) {
11088
+ const pad = normalizeSides(config.padding);
11089
+ const isRow = config.direction === "row";
11090
+ const innerWidth = Math.max(0, container2.width - pad.left - pad.right);
11091
+ const innerHeight = Math.max(0, container2.height - pad.top - pad.bottom);
11092
+ const availableMain = isRow ? innerWidth : innerHeight;
11093
+ const availableCross = isRow ? innerHeight : innerWidth;
11094
+ if (children.length === 0) {
11095
+ return {
11096
+ childBounds: [],
11097
+ contentSize: { width: pad.left + pad.right, height: pad.top + pad.bottom }
11098
+ };
11099
+ }
11100
+ const baseSizes = [];
11101
+ const mainMargins = [];
11102
+ const crossMargins = [];
11103
+ for (const child of children) {
11104
+ const m = child.margin;
11105
+ if (isRow) {
11106
+ mainMargins.push({ before: m.left, after: m.right });
11107
+ crossMargins.push({ before: m.top, after: m.bottom });
11108
+ } else {
11109
+ mainMargins.push({ before: m.top, after: m.bottom });
11110
+ crossMargins.push({ before: m.left, after: m.right });
11111
+ }
11112
+ const measured = isRow ? child.measure.width : child.measure.height;
11113
+ const minMain = isRow ? child.minSize.width : child.minSize.height;
11114
+ const basis = child.flexBasis === "auto" ? measured : child.flexBasis;
11115
+ baseSizes.push(Math.max(basis, minMain));
11116
+ }
11117
+ const totalGaps = config.gap * Math.max(0, children.length - 1);
11118
+ let totalMargins = 0;
11119
+ for (const mm of mainMargins) {
11120
+ totalMargins += mm.before + mm.after;
11121
+ }
11122
+ let totalBase = 0;
11123
+ for (const s of baseSizes) {
11124
+ totalBase += s;
11125
+ }
11126
+ const totalUsed = totalBase + totalGaps + totalMargins;
11127
+ const finalSizes = [...baseSizes];
11128
+ const remaining = availableMain - totalUsed;
11129
+ if (remaining > 0) {
11130
+ let totalGrow = 0;
11131
+ for (const child of children) {
11132
+ totalGrow += child.flexGrow;
11133
+ }
11134
+ if (totalGrow > 0) {
11135
+ for (let i = 0; i < children.length; i++) {
11136
+ const child = children[i];
11137
+ finalSizes[i] = finalSizes[i] + remaining * child.flexGrow / totalGrow;
11138
+ }
11139
+ }
11140
+ } else if (remaining < 0) {
11141
+ let totalShrink = 0;
11142
+ for (let i = 0; i < children.length; i++) {
11143
+ totalShrink += children[i].flexShrink * baseSizes[i];
11144
+ }
11145
+ if (totalShrink > 0) {
11146
+ const deficit = -remaining;
11147
+ for (let i = 0; i < children.length; i++) {
11148
+ const child = children[i];
11149
+ const shrinkRatio = child.flexShrink * baseSizes[i] / totalShrink;
11150
+ const minMain = isRow ? child.minSize.width : child.minSize.height;
11151
+ finalSizes[i] = Math.max(minMain, finalSizes[i] - deficit * shrinkRatio);
11152
+ }
11153
+ }
11154
+ }
11155
+ let actualTotal = totalGaps + totalMargins;
11156
+ for (const s of finalSizes) {
11157
+ actualTotal += s;
11158
+ }
11159
+ const freeSpace = Math.max(0, availableMain - actualTotal);
11160
+ let mainOffset;
11161
+ let betweenExtra;
11162
+ switch (config.justifyContent) {
11163
+ case "center":
11164
+ mainOffset = freeSpace / 2;
11165
+ betweenExtra = 0;
11166
+ break;
11167
+ case "end":
11168
+ mainOffset = freeSpace;
11169
+ betweenExtra = 0;
11170
+ break;
11171
+ case "space-between":
11172
+ mainOffset = 0;
11173
+ betweenExtra = children.length > 1 ? freeSpace / (children.length - 1) : 0;
11174
+ break;
11175
+ case "space-around":
11176
+ betweenExtra = freeSpace / children.length;
11177
+ mainOffset = betweenExtra / 2;
11178
+ break;
11179
+ default:
11180
+ mainOffset = 0;
11181
+ betweenExtra = 0;
11182
+ break;
11183
+ }
11184
+ const childBounds = [];
11185
+ let cursor = mainOffset;
11186
+ let maxCrossContent = 0;
11187
+ for (let i = 0; i < children.length; i++) {
11188
+ const child = children[i];
11189
+ const mainSize = finalSizes[i];
11190
+ const mm = mainMargins[i];
11191
+ const cm = crossMargins[i];
11192
+ cursor += mm.before;
11193
+ const crossAvailable = availableCross - cm.before - cm.after;
11194
+ const measuredCross = isRow ? child.measure.height : child.measure.width;
11195
+ const align = child.alignSelf === "auto" ? config.alignItems : child.alignSelf;
11196
+ let crossSize;
11197
+ let crossOffset;
11198
+ if (align === "stretch") {
11199
+ crossSize = crossAvailable;
11200
+ crossOffset = cm.before;
11201
+ } else {
11202
+ crossSize = Math.min(measuredCross, crossAvailable);
11203
+ switch (align) {
11204
+ case "center":
11205
+ crossOffset = cm.before + (crossAvailable - crossSize) / 2;
11206
+ break;
11207
+ case "end":
11208
+ crossOffset = cm.before + crossAvailable - crossSize;
11209
+ break;
11210
+ default:
11211
+ crossOffset = cm.before;
11212
+ break;
11213
+ }
11214
+ }
11215
+ maxCrossContent = Math.max(maxCrossContent, crossOffset + crossSize + cm.after);
11216
+ if (isRow) {
11217
+ childBounds.push({
11218
+ x: pad.left + cursor,
11219
+ y: pad.top + crossOffset,
11220
+ width: mainSize,
11221
+ height: crossSize
11222
+ });
11223
+ } else {
11224
+ childBounds.push({
11225
+ x: pad.left + crossOffset,
11226
+ y: pad.top + cursor,
11227
+ width: crossSize,
11228
+ height: mainSize
11229
+ });
11230
+ }
11231
+ cursor += mainSize + mm.after;
11232
+ if (i < children.length - 1) {
11233
+ cursor += config.gap + betweenExtra;
11234
+ }
11235
+ }
11236
+ const mainContent = cursor;
11237
+ const contentWidth = isRow ? pad.left + mainContent + pad.right : pad.left + maxCrossContent + pad.right;
11238
+ const contentHeight = isRow ? pad.top + maxCrossContent + pad.bottom : pad.top + mainContent + pad.bottom;
11239
+ return {
11240
+ childBounds,
11241
+ contentSize: { width: contentWidth, height: contentHeight }
11242
+ };
11243
+ }
11244
+ class CContainer {
11245
+ constructor(options = {}) {
11246
+ this.type = "container";
11247
+ this._cachedBounds = null;
11248
+ this._cachedContainerBounds = null;
11249
+ this.id = options.id;
11250
+ this._direction = options.direction ?? "column";
11251
+ this._justifyContent = options.justifyContent ?? "start";
11252
+ this._alignItems = options.alignItems ?? "stretch";
11253
+ this._gap = options.gap ?? 0;
11254
+ this._padding = options.padding ?? 0;
11255
+ this._children = options.children ?? [];
11256
+ this.style = options.style ?? {};
11257
+ for (const child of this._children) {
11258
+ child.setOnChange(() => this.handleChildChange());
11259
+ }
11260
+ }
11261
+ get direction() {
11262
+ return this._direction;
11263
+ }
11264
+ get justifyContent() {
11265
+ return this._justifyContent;
11266
+ }
11267
+ get alignItems() {
11268
+ return this._alignItems;
11269
+ }
11270
+ get gap() {
11271
+ return this._gap;
11272
+ }
11273
+ get padding() {
11274
+ return this._padding;
11275
+ }
11276
+ get children() {
11277
+ return this._children;
11278
+ }
11279
+ addChild(child) {
11280
+ child.setOnChange(() => this.handleChildChange());
11281
+ this._children.push(child);
11282
+ this.handleChildChange();
11283
+ }
11284
+ removeChild(index) {
11285
+ const removed = this._children.splice(index, 1)[0];
11286
+ if (removed) {
11287
+ removed.setOnChange(void 0);
11288
+ this.handleChildChange();
11289
+ }
11290
+ return removed;
11291
+ }
11292
+ insertChild(index, child) {
11293
+ child.setOnChange(() => this.handleChildChange());
11294
+ this._children.splice(index, 0, child);
11295
+ this.handleChildChange();
11296
+ }
11297
+ setOnChange(cb) {
11298
+ this._onChange = cb;
11299
+ }
11300
+ handleChildChange() {
11301
+ this._cachedBounds = null;
11302
+ this._onChange?.();
11303
+ }
11304
+ getFlexConfig() {
11305
+ return {
11306
+ direction: this._direction,
11307
+ justifyContent: this._justifyContent,
11308
+ alignItems: this._alignItems,
11309
+ gap: this._gap,
11310
+ padding: this._padding
11311
+ };
11312
+ }
11313
+ buildFlexChildren(ctx) {
11314
+ return this._children.map((child) => {
11315
+ const s = child.style;
11316
+ const measured = child.measure(ctx);
11317
+ return {
11318
+ measure: measured,
11319
+ minSize: { width: 0, height: 0 },
11320
+ flexGrow: s.flexGrow ?? 0,
11321
+ flexShrink: s.flexShrink ?? 1,
11322
+ flexBasis: s.flexBasis ?? "auto",
11323
+ alignSelf: s.alignSelf ?? "auto",
11324
+ margin: normalizeSides(s.margin)
11325
+ };
11326
+ });
11327
+ }
11328
+ measure(ctx) {
11329
+ const flexChildren = this.buildFlexChildren(ctx);
11330
+ const measureConfig = {
11331
+ ...this.getFlexConfig(),
11332
+ justifyContent: "start",
11333
+ alignItems: "start"
11334
+ };
11335
+ const measureChildren = flexChildren.map((c) => ({
11336
+ ...c,
11337
+ flexGrow: 0,
11338
+ alignSelf: "start"
11339
+ }));
11340
+ const result = flexLayout(
11341
+ { width: 1e5, height: 1e5 },
11342
+ measureConfig,
11343
+ measureChildren
11344
+ );
11345
+ return result.contentSize;
11346
+ }
11347
+ render(ctx, bounds) {
11348
+ if (this.style.visible === false) return;
11349
+ const flexChildren = this.buildFlexChildren(ctx);
11350
+ const result = flexLayout(
11351
+ { width: bounds.width, height: bounds.height },
11352
+ this.getFlexConfig(),
11353
+ flexChildren
11354
+ );
11355
+ this._cachedBounds = result.childBounds;
11356
+ this._cachedContainerBounds = bounds;
11357
+ const opacity = this.style.opacity ?? 1;
11358
+ if (opacity < 1) {
11359
+ ctx.save();
11360
+ ctx.globalAlpha *= opacity;
11361
+ }
11362
+ for (let i = 0; i < this._children.length; i++) {
11363
+ const child = this._children[i];
11364
+ if (child.style.visible === false) continue;
11365
+ const cb = result.childBounds[i];
11366
+ child.render(ctx, {
11367
+ x: bounds.x + cb.x,
11368
+ y: bounds.y + cb.y,
11369
+ width: cb.width,
11370
+ height: cb.height
11371
+ });
11372
+ }
11373
+ if (opacity < 1) {
11374
+ ctx.restore();
11375
+ }
11376
+ }
11377
+ hitTest(point, bounds) {
11378
+ if (this.style.visible === false) return null;
11379
+ if (!this._cachedBounds) return null;
11380
+ for (let i = this._children.length - 1; i >= 0; i--) {
11381
+ const child = this._children[i];
11382
+ if (child.style.visible === false) continue;
11383
+ const cb = this._cachedBounds[i];
11384
+ const absBounds = {
11385
+ x: bounds.x + cb.x,
11386
+ y: bounds.y + cb.y,
11387
+ width: cb.width,
11388
+ height: cb.height
11389
+ };
11390
+ const hit = child.hitTest(point, absBounds);
11391
+ if (hit) return hit;
11392
+ }
11393
+ return null;
11394
+ }
11395
+ /**
11396
+ * Find a component by id recursively.
11397
+ */
11398
+ findById(id) {
11399
+ for (const child of this._children) {
11400
+ if (child.id === id) return child;
11401
+ if (child.type === "container") {
11402
+ const found = child.findById(id);
11403
+ if (found) return found;
11404
+ }
11405
+ if (child.type === "shape") {
11406
+ const content = child.content;
11407
+ if (content) {
11408
+ const found = content.findById(id);
11409
+ if (found) return found;
11410
+ }
11411
+ }
11412
+ }
11413
+ return void 0;
11414
+ }
11415
+ serialize() {
11416
+ const data = {
11417
+ type: "container",
11418
+ direction: this._direction,
11419
+ children: this._children.map((c) => c.serialize())
11420
+ };
11421
+ if (this.id !== void 0) data.id = this.id;
11422
+ if (this.style && Object.keys(this.style).length > 0) data.style = this.style;
11423
+ if (this._justifyContent !== "start") data.justifyContent = this._justifyContent;
11424
+ if (this._alignItems !== "stretch") data.alignItems = this._alignItems;
11425
+ if (this._gap !== 0) data.gap = this._gap;
11426
+ if (typeof this._padding === "number" && this._padding !== 0 || typeof this._padding === "object" && Object.values(this._padding).some((v) => v !== void 0 && v !== 0)) {
11427
+ data.padding = this._padding;
11428
+ }
11429
+ return data;
11430
+ }
11431
+ toSVG(bounds) {
11432
+ if (this.style.visible === false) return "";
11433
+ const cachedBounds = this._cachedBounds;
11434
+ const cachedContainer = this._cachedContainerBounds;
11435
+ const childSvg = this._children.map((child, i) => {
11436
+ if (child.style.visible === false) return "";
11437
+ if (cachedBounds && cachedContainer) {
11438
+ const cb = cachedBounds[i];
11439
+ const dx = bounds.x - cachedContainer.x;
11440
+ const dy = bounds.y - cachedContainer.y;
11441
+ return child.toSVG({
11442
+ x: cachedContainer.x + cb.x + dx,
11443
+ y: cachedContainer.y + cb.y + dy,
11444
+ width: cb.width,
11445
+ height: cb.height
11446
+ });
11447
+ }
11448
+ const h = bounds.height / Math.max(1, this._children.length);
11449
+ return child.toSVG({
11450
+ x: bounds.x,
11451
+ y: bounds.y + i * h,
11452
+ width: bounds.width,
11453
+ height: h
11454
+ });
11455
+ }).join("");
11456
+ return `<g>${childSvg}</g>`;
11457
+ }
11458
+ }
11459
+ class CShape {
11460
+ constructor(options = {}) {
11461
+ this.type = "shape";
11462
+ this.id = options.id;
11463
+ this._borderColor = options.borderColor;
11464
+ this._borderWidth = options.borderWidth ?? 0;
11465
+ this._backgroundColor = options.backgroundColor;
11466
+ this._cornerRadius = options.cornerRadius ?? 0;
11467
+ this._padding = options.padding ?? 0;
11468
+ this.onClick = options.onClick;
11469
+ this._content = options.content;
11470
+ this.style = options.style ?? {};
11471
+ if (this._content) {
11472
+ this._content.setOnChange(() => this._onChange?.());
11473
+ }
11474
+ }
11475
+ get borderColor() {
11476
+ return this._borderColor;
11477
+ }
11478
+ set borderColor(value) {
11479
+ if (this._borderColor !== value) {
11480
+ this._borderColor = value;
11481
+ this._onChange?.();
11482
+ }
11483
+ }
11484
+ get borderWidth() {
11485
+ return this._borderWidth;
11486
+ }
11487
+ set borderWidth(value) {
11488
+ if (this._borderWidth !== value) {
11489
+ this._borderWidth = value;
11490
+ this._onChange?.();
11491
+ }
11492
+ }
11493
+ get backgroundColor() {
11494
+ return this._backgroundColor;
11495
+ }
11496
+ set backgroundColor(value) {
11497
+ if (this._backgroundColor !== value) {
11498
+ this._backgroundColor = value;
11499
+ this._onChange?.();
11500
+ }
11501
+ }
11502
+ get cornerRadius() {
11503
+ return this._cornerRadius;
11504
+ }
11505
+ get padding() {
11506
+ return this._padding;
11507
+ }
11508
+ get content() {
11509
+ return this._content;
11510
+ }
11511
+ setOnChange(cb) {
11512
+ this._onChange = cb;
11513
+ }
11514
+ measure(ctx) {
11515
+ const pad = normalizeSides(this._padding);
11516
+ const bw = this._borderWidth;
11517
+ if (this._content) {
11518
+ const contentSize = this._content.measure(ctx);
11519
+ return {
11520
+ width: contentSize.width + pad.left + pad.right + bw * 2,
11521
+ height: contentSize.height + pad.top + pad.bottom + bw * 2
11522
+ };
11523
+ }
11524
+ return {
11525
+ width: pad.left + pad.right + bw * 2,
11526
+ height: pad.top + pad.bottom + bw * 2
11527
+ };
11528
+ }
11529
+ render(ctx, bounds) {
11530
+ if (this.style.visible === false) return;
11531
+ ctx.save();
11532
+ ctx.globalAlpha *= this.style.opacity ?? 1;
11533
+ const { x, y, width, height } = bounds;
11534
+ if (this._backgroundColor) {
11535
+ ctx.fillStyle = this._backgroundColor;
11536
+ if (this._cornerRadius > 0 && ctx.roundRect) {
11537
+ ctx.beginPath();
11538
+ ctx.roundRect(x, y, width, height, this._cornerRadius);
11539
+ ctx.fill();
11540
+ } else {
11541
+ ctx.fillRect(x, y, width, height);
11542
+ }
11543
+ }
11544
+ if (this._borderColor && this._borderWidth > 0) {
11545
+ ctx.strokeStyle = this._borderColor;
11546
+ ctx.lineWidth = this._borderWidth;
11547
+ if (this._cornerRadius > 0 && ctx.roundRect) {
11548
+ ctx.beginPath();
11549
+ ctx.roundRect(x, y, width, height, this._cornerRadius);
11550
+ ctx.stroke();
11551
+ } else {
11552
+ ctx.strokeRect(x, y, width, height);
11553
+ }
11554
+ }
11555
+ if (this._content) {
11556
+ const pad = normalizeSides(this._padding);
11557
+ const bw = this._borderWidth;
11558
+ this._content.render(ctx, {
11559
+ x: x + pad.left + bw,
11560
+ y: y + pad.top + bw,
11561
+ width: Math.max(0, width - pad.left - pad.right - bw * 2),
11562
+ height: Math.max(0, height - pad.top - pad.bottom - bw * 2)
11563
+ });
11564
+ }
11565
+ ctx.restore();
11566
+ }
11567
+ hitTest(point, bounds) {
11568
+ if (this.style.visible === false) return null;
11569
+ if (point.x < bounds.x || point.x > bounds.x + bounds.width || point.y < bounds.y || point.y > bounds.y + bounds.height) {
11570
+ return null;
11571
+ }
11572
+ if (this._content) {
11573
+ const pad = normalizeSides(this._padding);
11574
+ const bw = this._borderWidth;
11575
+ const contentBounds = {
11576
+ x: bounds.x + pad.left + bw,
11577
+ y: bounds.y + pad.top + bw,
11578
+ width: Math.max(0, bounds.width - pad.left - pad.right - bw * 2),
11579
+ height: Math.max(0, bounds.height - pad.top - pad.bottom - bw * 2)
11580
+ };
11581
+ const hit = this._content.hitTest(point, contentBounds);
11582
+ if (hit) return hit;
11583
+ }
11584
+ return this;
11585
+ }
11586
+ serialize() {
11587
+ const data = { type: "shape" };
11588
+ if (this.id !== void 0) data.id = this.id;
11589
+ if (this.style && Object.keys(this.style).length > 0) data.style = this.style;
11590
+ if (this._borderColor) data.borderColor = this._borderColor;
11591
+ if (this._borderWidth !== 0) data.borderWidth = this._borderWidth;
11592
+ if (this._backgroundColor) data.backgroundColor = this._backgroundColor;
11593
+ if (this._cornerRadius !== 0) data.cornerRadius = this._cornerRadius;
11594
+ if (typeof this._padding === "number" && this._padding !== 0 || typeof this._padding === "object" && Object.values(this._padding).some((v) => v !== void 0 && v !== 0)) {
11595
+ data.padding = this._padding;
11596
+ }
11597
+ if (this._content) {
11598
+ data.content = this._content.serialize();
11599
+ }
11600
+ return data;
11601
+ }
11602
+ toSVG(bounds) {
11603
+ if (this.style.visible === false) return "";
11604
+ let svg = "";
11605
+ const { x, y, width, height } = bounds;
11606
+ const hasFill = !!this._backgroundColor;
11607
+ const hasStroke = !!this._borderColor && this._borderWidth > 0;
11608
+ if (hasFill || hasStroke) {
11609
+ const fill = hasFill ? `fill="${this._backgroundColor}"` : 'fill="none"';
11610
+ const stroke = hasStroke ? `stroke="${this._borderColor}" stroke-width="${this._borderWidth}"` : "";
11611
+ const rx = this._cornerRadius > 0 ? ` rx="${this._cornerRadius}"` : "";
11612
+ svg += `<rect x="${x}" y="${y}" width="${width}" height="${height}" ${fill} ${stroke}${rx} />`;
11613
+ }
11614
+ if (this._content) {
11615
+ const pad = normalizeSides(this._padding);
11616
+ const bw = this._borderWidth;
11617
+ svg += this._content.toSVG({
11618
+ x: x + pad.left + bw,
11619
+ y: y + pad.top + bw,
11620
+ width: Math.max(0, width - pad.left - pad.right - bw * 2),
11621
+ height: Math.max(0, height - pad.top - pad.bottom - bw * 2)
11622
+ });
11623
+ }
11624
+ return svg;
11625
+ }
11626
+ }
11627
+ function deserializeCComponent(data) {
11628
+ switch (data.type) {
11629
+ case "text":
11630
+ return new CText({
11631
+ id: data.id,
11632
+ text: data.text ?? "",
11633
+ fontFamily: data.fontFamily,
11634
+ fontWeight: data.fontWeight,
11635
+ fontStyle: data.fontStyle,
11636
+ fontSize: data.fontSize,
11637
+ color: data.color,
11638
+ align: data.align,
11639
+ verticalAlign: data.verticalAlign,
11640
+ maxLines: data.maxLines,
11641
+ lineHeight: data.lineHeight,
11642
+ role: data.role,
11643
+ rotation: data.rotation,
11644
+ style: data.style
11645
+ });
11646
+ case "icon":
11647
+ return new CIcon({
11648
+ id: data.id,
11649
+ source: data.source ?? "",
11650
+ width: data.width,
11651
+ height: data.height,
11652
+ backgroundColor: data.backgroundColor,
11653
+ fillColor: data.fillColor,
11654
+ bindsNotationIcon: data.bindsNotationIcon === true,
11655
+ style: data.style
11656
+ });
11657
+ case "divider":
11658
+ return new CDivider({
11659
+ id: data.id,
11660
+ color: data.color,
11661
+ thickness: data.thickness,
11662
+ style: data.style
11663
+ });
11664
+ case "container":
11665
+ return new CContainer({
11666
+ id: data.id,
11667
+ direction: data.direction,
11668
+ justifyContent: data.justifyContent,
11669
+ alignItems: data.alignItems,
11670
+ gap: data.gap,
11671
+ padding: data.padding,
11672
+ children: data.children?.map(deserializeCComponent),
11673
+ style: data.style
11674
+ });
11675
+ case "shape":
11676
+ return new CShape({
11677
+ id: data.id,
11678
+ borderColor: data.borderColor,
11679
+ borderWidth: data.borderWidth,
11680
+ backgroundColor: data.backgroundColor,
11681
+ cornerRadius: data.cornerRadius,
11682
+ padding: data.padding,
11683
+ content: data.content ? deserializeCComponent(data.content) : void 0,
11684
+ style: data.style
11685
+ });
11686
+ default:
11687
+ throw new Error(`Unknown component type: ${String(data.type)}`);
11688
+ }
11689
+ }
11690
+ class CompositeNode extends Node {
11691
+ constructor(options) {
11692
+ super(options);
11693
+ this._content = options.content;
11694
+ this._shapeType = options.shapeType ?? "rectangle";
11695
+ this._cornerRadius = options.cornerRadius ?? 0;
11696
+ this._pathFactory = options.pathFactory;
11697
+ this._autoSize = options.autoSize ?? false;
11698
+ this._minWidth = options.minWidth ?? 0;
11699
+ this._minHeight = options.minHeight ?? 0;
11700
+ this._content.setOnChange(() => this.markDirty());
11701
+ }
11702
+ get typeName() {
11703
+ return "composite";
11704
+ }
11705
+ get content() {
11706
+ return this._content;
11707
+ }
11708
+ get shapeType() {
11709
+ return this._shapeType;
11710
+ }
11711
+ get cornerRadius() {
11712
+ return this._cornerRadius;
11713
+ }
11714
+ get autoSize() {
11715
+ return this._autoSize;
11716
+ }
11717
+ set autoSize(value) {
11718
+ if (this._autoSize !== value) {
11719
+ this._autoSize = value;
11720
+ this.markDirty();
11721
+ }
11722
+ }
11723
+ get minWidth() {
11724
+ return this._minWidth;
11725
+ }
11726
+ get minHeight() {
11727
+ return this._minHeight;
11728
+ }
11729
+ /**
11730
+ * Find a component by id in the content tree.
11731
+ */
11732
+ getComponent(id) {
11733
+ if (this._content.id === id) return this._content;
11734
+ return this._content.findById(id);
11735
+ }
11736
+ /**
11737
+ * Hit test the component tree. Returns the deepest component at the given world point.
11738
+ */
11739
+ getComponentAtPoint(worldPoint) {
11740
+ const bounds = this.getContentBounds();
11741
+ return this._content.hitTest(worldPoint, bounds);
11742
+ }
11743
+ getContentBounds() {
11744
+ return this.getLabelContainerBounds(this.getBounds());
11745
+ }
11746
+ // --- Shape rendering ---
11747
+ hitTest(point) {
11748
+ if (this._shapeType === "circle") {
11749
+ return this.hitTestEllipse(point);
11750
+ }
11751
+ if (this._shapeType === "diamond") {
11752
+ return this.hitTestDiamond(point);
11753
+ }
11754
+ return super.hitTest(point);
11755
+ }
11756
+ hitTestEllipse(point) {
11757
+ const center = this.getCenter();
11758
+ const padding = NODE_HITBOX_PADDING;
11759
+ const rx = this._width / 2 + padding;
11760
+ const ry = this._height / 2 + padding;
11761
+ const dx = point.x - center.x;
11762
+ const dy = point.y - center.y;
11763
+ return dx * dx / (rx * rx) + dy * dy / (ry * ry) <= 1;
11764
+ }
11765
+ hitTestDiamond(point) {
11766
+ const center = this.getCenter();
11767
+ const padding = NODE_HITBOX_PADDING;
11768
+ const hw = this._width / 2 + padding;
11769
+ const hh = this._height / 2 + padding;
11770
+ const dx = Math.abs(point.x - center.x);
11771
+ const dy = Math.abs(point.y - center.y);
11772
+ return dx / hw + dy / hh <= 1;
11773
+ }
11774
+ render(ctx) {
11775
+ if (this._autoSize) {
11776
+ this.applyAutoSize(ctx);
11777
+ }
11778
+ const { x, y, width, height } = this.getBounds();
11779
+ const style = this.style;
11780
+ const baseOpacity = style.opacity ?? 1;
11781
+ const fillOpacity = style.fillOpacity ?? 1;
11782
+ const strokeOpacity = style.strokeOpacity ?? 1;
11783
+ this.applyStyle(ctx);
11784
+ ctx.beginPath();
11785
+ this.buildShapePath(ctx, x, y, width, height);
11786
+ ctx.closePath();
11787
+ ctx.globalAlpha = baseOpacity * fillOpacity;
11788
+ ctx.fill();
11789
+ ctx.globalAlpha = baseOpacity * strokeOpacity;
11790
+ ctx.stroke();
11791
+ ctx.globalAlpha = 1;
11792
+ this.renderContents(ctx);
11793
+ }
11794
+ buildShapePath(ctx, x, y, w, h) {
11795
+ switch (this._shapeType) {
11796
+ case "circle": {
11797
+ const cx = x + w / 2;
11798
+ const cy = y + h / 2;
11799
+ ctx.ellipse(cx, cy, w / 2, h / 2, 0, 0, Math.PI * 2);
11800
+ break;
11801
+ }
11802
+ case "diamond": {
11803
+ const cx = x + w / 2;
11804
+ const cy = y + h / 2;
11805
+ ctx.moveTo(cx, y);
11806
+ ctx.lineTo(x + w, cy);
11807
+ ctx.lineTo(cx, y + h);
11808
+ ctx.lineTo(x, cy);
11809
+ break;
11810
+ }
11811
+ case "custom": {
11812
+ if (this._pathFactory) {
11813
+ const path = this._pathFactory(w, h);
11814
+ ctx.save();
11815
+ ctx.translate(x, y);
11816
+ ctx.fill(path);
11817
+ ctx.stroke(path);
11818
+ ctx.restore();
11819
+ return;
11820
+ }
11821
+ this.buildRectPath(ctx, x, y, w, h);
11822
+ break;
11823
+ }
11824
+ default:
11825
+ this.buildRectPath(ctx, x, y, w, h);
11826
+ break;
11827
+ }
11828
+ }
11829
+ buildRectPath(ctx, x, y, w, h) {
11830
+ const radius = Math.min(this._cornerRadius, w / 2, h / 2);
11831
+ if (radius > 0) {
11832
+ ctx.moveTo(x + radius, y);
11833
+ ctx.lineTo(x + w - radius, y);
11834
+ ctx.arcTo(x + w, y, x + w, y + radius, radius);
11835
+ ctx.lineTo(x + w, y + h - radius);
11836
+ ctx.arcTo(x + w, y + h, x + w - radius, y + h, radius);
11837
+ ctx.lineTo(x + radius, y + h);
11838
+ ctx.arcTo(x, y + h, x, y + h - radius, radius);
11839
+ ctx.lineTo(x, y + radius);
11840
+ ctx.arcTo(x, y, x + radius, y, radius);
11841
+ } else {
11842
+ ctx.rect(x, y, w, h);
11843
+ }
11844
+ }
11845
+ // --- Content rendering (overrides Node.renderContents) ---
11846
+ renderContents(ctx) {
11847
+ ctx.setLineDash([]);
11848
+ ctx.lineDashOffset = 0;
11849
+ const contentBounds = this.getContentBounds();
11850
+ this._content.render(ctx, contentBounds);
11851
+ this.renderPorts(ctx);
11852
+ }
11853
+ applyAutoSize(ctx) {
11854
+ const contentSize = this._content.measure(ctx);
11855
+ const inset = this.contentInset;
11856
+ const neededWidth = Math.max(
11857
+ this._minWidth,
11858
+ contentSize.width + inset.left + inset.right
11859
+ );
11860
+ const neededHeight = Math.max(
11861
+ this._minHeight,
11862
+ contentSize.height + inset.top + inset.bottom
11863
+ );
11864
+ if (neededWidth > this._width) {
11865
+ this._width = neededWidth;
11866
+ }
11867
+ if (neededHeight > this._height) {
11868
+ this._height = neededHeight;
11869
+ }
11870
+ }
11871
+ // --- Outline methods for connections (delegate based on shapeType) ---
11872
+ getLabelContainerBounds(bounds) {
11873
+ if (this._shapeType === "circle") {
11874
+ const factor = 1 / Math.SQRT2;
11875
+ const w = bounds.width * factor;
11876
+ const h = bounds.height * factor;
11877
+ return {
11878
+ x: bounds.x + (bounds.width - w) / 2,
11879
+ y: bounds.y + (bounds.height - h) / 2,
11880
+ width: w,
11881
+ height: h
11882
+ };
11883
+ }
11884
+ if (this._shapeType === "diamond") {
11885
+ const w = bounds.width / 2;
11886
+ const h = bounds.height / 2;
11887
+ return {
11888
+ x: bounds.x + (bounds.width - w) / 2,
11889
+ y: bounds.y + (bounds.height - h) / 2,
11890
+ width: w,
11891
+ height: h
11892
+ };
11893
+ }
11894
+ const ci = this.contentInset;
11895
+ return {
11896
+ x: bounds.x + ci.left,
11897
+ y: bounds.y + ci.top,
11898
+ width: Math.max(0, bounds.width - ci.left - ci.right),
11899
+ height: Math.max(0, bounds.height - ci.top - ci.bottom)
11900
+ };
11901
+ }
11902
+ /**
11903
+ * Get the minimum content size needed for the content tree.
11904
+ * Useful for external auto-sizing logic.
11905
+ */
11906
+ getContentMinSize(ctx) {
11907
+ return this._content.measure(ctx);
11908
+ }
11909
+ }
11910
+ function text(options) {
11911
+ return new CText(options);
11912
+ }
11913
+ function icon(options) {
11914
+ return new CIcon(options);
11915
+ }
11916
+ function divider(options) {
11917
+ return new CDivider(options);
11918
+ }
11919
+ function container(options) {
11920
+ return new CContainer(options);
11921
+ }
11922
+ function shape(options) {
11923
+ return new CShape(options);
11924
+ }
11925
+ const DEFAULT_THEME = {
11926
+ name: "default",
11927
+ colors: {
11928
+ background: "#ffffff",
11929
+ grid: "#e5e5e5",
11930
+ selection: "rgba(59, 130, 246, 0.1)",
11931
+ connectionPreview: "#3b82f6"
11932
+ },
11933
+ node: {
11934
+ default: {
11935
+ fillColor: "#ffffff",
11936
+ strokeColor: "#333333",
11937
+ strokeWidth: 2,
11938
+ opacity: 1,
11939
+ cornerRadius: 4
11940
+ },
11941
+ hover: {
11942
+ fillColor: "#f5f5f5",
11943
+ strokeColor: "#6366f1",
11944
+ strokeWidth: 2,
11945
+ opacity: 1,
11946
+ cornerRadius: 4
11947
+ },
11948
+ selected: {
11949
+ strokeColor: "#3b82f6",
11950
+ strokeWidth: 2,
11951
+ opacity: 1
11952
+ },
11953
+ dragging: {
11954
+ strokeColor: "#333333",
11955
+ strokeWidth: 2,
11956
+ opacity: 0.8
11957
+ }
11958
+ },
11959
+ edge: {
11960
+ default: {
11961
+ strokeColor: "#666666",
11962
+ strokeWidth: 2,
11963
+ opacity: 1
11964
+ },
11965
+ hover: {
11966
+ strokeColor: "#6366f1",
11967
+ strokeWidth: 2,
11968
+ opacity: 1
11969
+ },
11970
+ selected: {
11971
+ strokeColor: "#3b82f6",
11972
+ strokeWidth: 3,
11973
+ opacity: 1
11974
+ }
11975
+ },
11976
+ text: {
11977
+ font: "14px sans-serif",
11978
+ fontSize: 14,
11979
+ fontFamily: "sans-serif",
11980
+ fontWeight: "normal",
11981
+ color: "#333333",
11982
+ align: "center",
11983
+ baseline: "middle"
11984
+ },
11985
+ port: {
11986
+ default: { color: "#666666", radius: 6 },
11987
+ hover: { color: "#3b82f6", radius: 7 }
11988
+ },
11989
+ group: {
11990
+ default: {
11991
+ fillColor: "rgba(200, 200, 200, 0.2)",
11992
+ strokeColor: "#999999",
11993
+ strokeWidth: 1,
11994
+ opacity: 1
11995
+ },
11996
+ selected: {
11997
+ fillColor: "rgba(59, 130, 246, 0.1)",
11998
+ strokeColor: "#3b82f6",
11999
+ strokeWidth: 2,
12000
+ opacity: 1
12001
+ }
12002
+ }
12003
+ };
12004
+ const DARK_THEME = {
12005
+ name: "dark",
12006
+ colors: {
12007
+ background: "#1a1a1a",
12008
+ grid: "#333333",
12009
+ selection: "rgba(99, 102, 241, 0.2)",
12010
+ connectionPreview: "#6366f1"
12011
+ },
12012
+ node: {
12013
+ default: {
12014
+ fillColor: "#2d2d2d",
12015
+ strokeColor: "#555555",
12016
+ strokeWidth: 2,
12017
+ opacity: 1,
12018
+ cornerRadius: 4
12019
+ },
12020
+ hover: {
12021
+ fillColor: "#3d3d3d",
12022
+ strokeColor: "#6366f1",
12023
+ strokeWidth: 2,
12024
+ opacity: 1,
12025
+ cornerRadius: 4
12026
+ },
12027
+ selected: {
12028
+ fillColor: "#2d2d2d",
12029
+ strokeColor: "#555555",
12030
+ strokeWidth: 2,
12031
+ opacity: 1,
12032
+ cornerRadius: 4
12033
+ },
12034
+ dragging: {
12035
+ fillColor: "#404040",
12036
+ strokeColor: "#555555",
12037
+ strokeWidth: 2,
12038
+ opacity: 0.8,
12039
+ cornerRadius: 4
12040
+ }
12041
+ },
12042
+ edge: {
12043
+ default: {
12044
+ strokeColor: "#888888",
12045
+ strokeWidth: 2,
12046
+ opacity: 1
12047
+ },
12048
+ hover: {
12049
+ strokeColor: "#6366f1",
12050
+ strokeWidth: 2,
12051
+ opacity: 1
12052
+ },
12053
+ selected: {
12054
+ strokeColor: "#818cf8",
12055
+ strokeWidth: 3,
12056
+ opacity: 1
12057
+ }
12058
+ },
12059
+ text: {
12060
+ font: "14px sans-serif",
12061
+ fontSize: 14,
12062
+ fontFamily: "sans-serif",
12063
+ fontWeight: "normal",
12064
+ color: "#e0e0e0",
12065
+ align: "center",
12066
+ baseline: "middle"
12067
+ },
12068
+ port: {
12069
+ default: { color: "#888888", radius: 6 },
12070
+ hover: { color: "#6366f1", radius: 7 }
12071
+ },
12072
+ group: {
12073
+ default: {
12074
+ fillColor: "rgba(100, 100, 100, 0.2)",
12075
+ strokeColor: "#666666",
12076
+ strokeWidth: 1,
12077
+ opacity: 1
12078
+ },
12079
+ selected: {
12080
+ fillColor: "rgba(99, 102, 241, 0.2)",
12081
+ strokeColor: "#6366f1",
12082
+ strokeWidth: 2,
12083
+ opacity: 1
12084
+ }
12085
+ }
12086
+ };
12087
+ class StyleManager {
12088
+ constructor(theme) {
12089
+ this._classes = /* @__PURE__ */ new Map();
12090
+ this._builtInThemes = /* @__PURE__ */ new Map([
12091
+ ["default", DEFAULT_THEME],
12092
+ ["dark", DARK_THEME]
12093
+ ]);
12094
+ if (theme === void 0) {
12095
+ this._theme = DEFAULT_THEME;
12096
+ } else if (typeof theme === "string") {
12097
+ this._theme = this._builtInThemes.get(theme) ?? DEFAULT_THEME;
12098
+ } else {
12099
+ this._theme = theme;
10527
12100
  }
10528
12101
  }
10529
12102
  /**
@@ -10873,12 +12446,12 @@ class Serializer {
10873
12446
  label = node.label.text;
10874
12447
  }
10875
12448
  }
10876
- let icon;
12449
+ let icon2;
10877
12450
  if (node.icon) {
10878
12451
  const opts = node.icon.options;
10879
12452
  const source = typeof opts.source === "string" ? opts.source : void 0;
10880
12453
  if (source) {
10881
- icon = omitEmptyValues({
12454
+ icon2 = omitEmptyValues({
10882
12455
  source,
10883
12456
  width: opts.width,
10884
12457
  height: opts.height,
@@ -10899,7 +12472,7 @@ class Serializer {
10899
12472
  }
10900
12473
  const contentInset = node.contentInset;
10901
12474
  const hasContentInset = contentInset.top !== 0 || contentInset.right !== 0 || contentInset.bottom !== 0 || contentInset.left !== 0;
10902
- return omitEmptyValues({
12475
+ const base = omitEmptyValues({
10903
12476
  id: node.id,
10904
12477
  type: node.typeName,
10905
12478
  x: node.x,
@@ -10910,12 +12483,23 @@ class Serializer {
10910
12483
  styleClass: node.styleClass,
10911
12484
  label,
10912
12485
  labelStyleClass: typeof label === "string" ? node.label?.styleClass : void 0,
10913
- icon,
12486
+ icon: icon2,
10914
12487
  contentInset: hasContentInset ? contentInset : void 0,
10915
12488
  anchorPoints,
10916
12489
  ports: ports.length > 0 ? ports : void 0,
10917
12490
  data: Object.keys(node.data).length > 0 ? node.data : void 0
10918
12491
  });
12492
+ if (node.typeName === "composite") {
12493
+ const cn = node;
12494
+ const ext = base;
12495
+ ext.content = cn.content.serialize();
12496
+ ext.shapeType = cn.shapeType;
12497
+ if (cn.cornerRadius !== 0) ext.cornerRadius = cn.cornerRadius;
12498
+ ext.autoSize = cn.autoSize;
12499
+ if (cn.minWidth !== 0) ext.minWidth = cn.minWidth;
12500
+ if (cn.minHeight !== 0) ext.minHeight = cn.minHeight;
12501
+ }
12502
+ return base;
10919
12503
  }
10920
12504
  serializeEdge(edge) {
10921
12505
  return {
@@ -10932,6 +12516,8 @@ class Serializer {
10932
12516
  label: edge.label?.text,
10933
12517
  labelStyleClass: edge.label?.styleClass,
10934
12518
  labelOffset: edge.labelOffset !== 0 ? edge.labelOffset : void 0,
12519
+ labelPosition: edge.labelPosition !== 0.5 ? edge.labelPosition : void 0,
12520
+ labelFollowPath: edge.labelFollowPath ? true : void 0,
10935
12521
  labelBackground: edge.labelBackground,
10936
12522
  labelLineGap: edge.labelLineGap ? true : void 0,
10937
12523
  data: Object.keys(edge.data).length > 0 ? edge.data : void 0
@@ -11290,16 +12876,16 @@ class SvgExporter {
11290
12876
  const strokeOpacity = (style.strokeOpacity ?? 1) * baseOpacity;
11291
12877
  const dash = style.lineDash?.length ? ` stroke-dasharray="${style.lineDash.join(" ")}"` : "";
11292
12878
  const dashOffset = style.lineDashOffset !== void 0 ? ` stroke-dashoffset="${style.lineDashOffset}"` : "";
11293
- let shape;
12879
+ let shape2;
11294
12880
  switch (node.typeName) {
11295
12881
  case "rectangle": {
11296
12882
  const radius = this.getNodeCornerRadius(node, bounds);
11297
- shape = `<rect x="${bounds.x}" y="${bounds.y}" width="${bounds.width}" height="${bounds.height}" rx="${radius}" ry="${radius}"`;
12883
+ shape2 = `<rect x="${bounds.x}" y="${bounds.y}" width="${bounds.width}" height="${bounds.height}" rx="${radius}" ry="${radius}"`;
11298
12884
  break;
11299
12885
  }
11300
12886
  case "circle": {
11301
12887
  const center = node.getCenter();
11302
- shape = `<ellipse cx="${center.x}" cy="${center.y}" rx="${bounds.width / 2}" ry="${bounds.height / 2}"`;
12888
+ shape2 = `<ellipse cx="${center.x}" cy="${center.y}" rx="${bounds.width / 2}" ry="${bounds.height / 2}"`;
11303
12889
  break;
11304
12890
  }
11305
12891
  case "diamond": {
@@ -11312,27 +12898,61 @@ class SvgExporter {
11312
12898
  `${center.x},${center.y + hh}`,
11313
12899
  `${center.x - hw},${center.y}`
11314
12900
  ].join(" ");
11315
- shape = `<polygon points="${points}"`;
12901
+ shape2 = `<polygon points="${points}"`;
11316
12902
  break;
11317
12903
  }
11318
12904
  case "custom": {
11319
12905
  const svgPath = "getSvgPath" in node && typeof node.getSvgPath === "function" ? node.getSvgPath() : null;
11320
12906
  if (svgPath) {
11321
- shape = `<path d="${this.escapeAttribute(svgPath)}" transform="translate(${bounds.x}, ${bounds.y})"`;
12907
+ shape2 = `<path d="${this.escapeAttribute(svgPath)}" transform="translate(${bounds.x}, ${bounds.y})"`;
11322
12908
  } else {
11323
- shape = `<rect x="${bounds.x}" y="${bounds.y}" width="${bounds.width}" height="${bounds.height}"`;
12909
+ shape2 = `<rect x="${bounds.x}" y="${bounds.y}" width="${bounds.width}" height="${bounds.height}"`;
11324
12910
  }
11325
12911
  break;
11326
12912
  }
12913
+ case "composite": {
12914
+ const cn = node;
12915
+ switch (cn.shapeType) {
12916
+ case "circle": {
12917
+ const center = node.getCenter();
12918
+ shape2 = `<ellipse cx="${center.x}" cy="${center.y}" rx="${bounds.width / 2}" ry="${bounds.height / 2}"`;
12919
+ break;
12920
+ }
12921
+ case "diamond": {
12922
+ const center = node.getCenter();
12923
+ const hw = bounds.width / 2;
12924
+ const hh = bounds.height / 2;
12925
+ const points = [
12926
+ `${center.x},${center.y - hh}`,
12927
+ `${center.x + hw},${center.y}`,
12928
+ `${center.x},${center.y + hh}`,
12929
+ `${center.x - hw},${center.y}`
12930
+ ].join(" ");
12931
+ shape2 = `<polygon points="${points}"`;
12932
+ break;
12933
+ }
12934
+ default: {
12935
+ const radius = cn.cornerRadius;
12936
+ shape2 = `<rect x="${bounds.x}" y="${bounds.y}" width="${bounds.width}" height="${bounds.height}" rx="${radius}" ry="${radius}"`;
12937
+ break;
12938
+ }
12939
+ }
12940
+ const contentBounds = cn.getLabelContainerBounds(bounds);
12941
+ const contentSvg = cn.content.toSVG(contentBounds);
12942
+ return [
12943
+ `${shape2} fill="${fill}" fill-opacity="${fillOpacity}" stroke="${stroke}" stroke-width="${strokeWidth}" stroke-opacity="${strokeOpacity}"${dash}${dashOffset}/>`,
12944
+ contentSvg
12945
+ ].join("");
12946
+ }
11327
12947
  default: {
11328
- shape = `<rect x="${bounds.x}" y="${bounds.y}" width="${bounds.width}" height="${bounds.height}"`;
12948
+ shape2 = `<rect x="${bounds.x}" y="${bounds.y}" width="${bounds.width}" height="${bounds.height}"`;
11329
12949
  }
11330
12950
  }
11331
12951
  const label = this.renderNodeLabel(node, bounds);
11332
- const icon = this.renderNodeIcon(node, bounds);
12952
+ const icon2 = this.renderNodeIcon(node, bounds);
11333
12953
  return [
11334
- `${shape} fill="${fill}" fill-opacity="${fillOpacity}" stroke="${stroke}" stroke-width="${strokeWidth}" stroke-opacity="${strokeOpacity}"${dash}${dashOffset}/>`,
11335
- icon,
12954
+ `${shape2} fill="${fill}" fill-opacity="${fillOpacity}" stroke="${stroke}" stroke-width="${strokeWidth}" stroke-opacity="${strokeOpacity}"${dash}${dashOffset}/>`,
12955
+ icon2,
11336
12956
  label
11337
12957
  ].join("");
11338
12958
  }
@@ -11360,9 +12980,9 @@ class SvgExporter {
11360
12980
  return "";
11361
12981
  }
11362
12982
  const labelPoint = this.getEdgeLabelPoint(edge, edgeLabelOffset);
11363
- const text = this.renderTextLabel(edge.label.text, labelPoint, edge.label.style);
12983
+ const text2 = this.renderTextLabel(edge.label.text, labelPoint, edge.label.style);
11364
12984
  const bg = this.renderEdgeLabelBackground(edge, labelPoint);
11365
- return `${bg}${text}`;
12985
+ return `${bg}${text2}`;
11366
12986
  }
11367
12987
  renderEdgeLabelBackground(edge, point) {
11368
12988
  if (!edge.label) {
@@ -11398,12 +13018,12 @@ class SvgExporter {
11398
13018
  left: n(value.left)
11399
13019
  };
11400
13020
  }
11401
- measureTextLabel(text, style = {}, inset = 8) {
13021
+ measureTextLabel(text2, style = {}, inset = 8) {
11402
13022
  const fontSize = style.fontSize ?? 14;
11403
13023
  const fontFamily = style.fontFamily ?? "sans-serif";
11404
13024
  const fontWeight = style.fontWeight ?? "normal";
11405
13025
  const lineHeight = fontSize * 1.2;
11406
- const lines = text.split("\n");
13026
+ const lines = text2.split("\n");
11407
13027
  let maxWidth = 0;
11408
13028
  if (typeof document !== "undefined") {
11409
13029
  const canvas = document.createElement("canvas");
@@ -11516,7 +13136,7 @@ class SvgExporter {
11516
13136
  y: midpoint.y + effectiveOffset
11517
13137
  };
11518
13138
  }
11519
- renderTextLabel(text, point, style = {}) {
13139
+ renderTextLabel(text2, point, style = {}) {
11520
13140
  const fill = style.color ?? "#000000";
11521
13141
  const fontSize = style.fontSize ?? 14;
11522
13142
  const fontFamily = style.fontFamily ?? "sans-serif";
@@ -11524,10 +13144,10 @@ class SvgExporter {
11524
13144
  const opacity = style.opacity ?? 1;
11525
13145
  const anchor = style.align === "left" ? "start" : style.align === "right" ? "end" : "middle";
11526
13146
  const baseline = style.baseline === "top" ? "text-before-edge" : style.baseline === "bottom" ? "text-after-edge" : "middle";
11527
- const lines = text.split("\n");
13147
+ const lines = text2.split("\n");
11528
13148
  if (lines.length <= 1) {
11529
13149
  return `<text x="${point.x}" y="${point.y}" fill="${fill}" fill-opacity="${opacity}" font-size="${fontSize}" font-family="${fontFamily}" font-weight="${fontWeight}" text-anchor="${anchor}" dominant-baseline="${baseline}">${this.escapeText(
11530
- text
13150
+ text2
11531
13151
  )}</text>`;
11532
13152
  }
11533
13153
  const lineHeight = fontSize * 1.2;
@@ -11596,16 +13216,16 @@ class SvgExporter {
11596
13216
  return Math.max(0, Math.min(rectangleRadius, bounds.width / 2, bounds.height / 2));
11597
13217
  }
11598
13218
  renderNodeIcon(node, nodeBounds) {
11599
- const icon = node.icon;
11600
- if (!icon) {
13219
+ const icon2 = node.icon;
13220
+ if (!icon2) {
11601
13221
  return "";
11602
13222
  }
11603
- const opts = icon.options;
11604
- const iconSize = icon.getSize();
13223
+ const opts = icon2.options;
13224
+ const iconSize = icon2.getSize();
11605
13225
  if (iconSize.width <= 0 || iconSize.height <= 0) {
11606
13226
  return "";
11607
13227
  }
11608
- const iconInset = icon.inset;
13228
+ const iconInset = icon2.inset;
11609
13229
  const iconBoxSize = this.getIconBoxSize(iconSize, iconInset);
11610
13230
  const iconBounds = this.getIconBounds(
11611
13231
  nodeBounds,
@@ -11817,8 +13437,8 @@ class SvgExporter {
11817
13437
  "</svg>"
11818
13438
  ].join("");
11819
13439
  }
11820
- escapeText(text) {
11821
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
13440
+ escapeText(text2) {
13441
+ return text2.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
11822
13442
  }
11823
13443
  }
11824
13444
  class AutoLayout {
@@ -12370,8 +13990,14 @@ export {
12370
13990
  AnimationManager,
12371
13991
  AutoLayout,
12372
13992
  AutoRouting,
13993
+ CContainer,
13994
+ CDivider,
13995
+ CIcon,
13996
+ CShape,
13997
+ CText,
12373
13998
  CircleNode,
12374
13999
  CompositeCommand,
14000
+ CompositeNode,
12375
14001
  ConnectionManager,
12376
14002
  ContextMenuManager,
12377
14003
  CustomShapeNode,
@@ -12416,15 +14042,21 @@ export {
12416
14042
  calculateBezierControlPoints,
12417
14043
  clamp,
12418
14044
  clonePoints,
14045
+ container,
14046
+ deserializeCComponent,
12419
14047
  distance,
12420
14048
  distanceToSegment,
12421
14049
  distributeNodes,
14050
+ divider,
12422
14051
  drawRoundedRectPath,
12423
14052
  expandBounds,
14053
+ flexLayout,
12424
14054
  generateId,
14055
+ icon,
12425
14056
  isCornerPlacement,
12426
14057
  lerp,
12427
14058
  mergeBounds,
14059
+ normalizeSides,
12428
14060
  pointInEllipse,
12429
14061
  pointInRect,
12430
14062
  rectIntersection,
@@ -12435,7 +14067,9 @@ export {
12435
14067
  resetPortIdCounter,
12436
14068
  rotatePoint,
12437
14069
  segmentRectIntersections,
14070
+ shape,
12438
14071
  snapPointToGrid,
12439
- snapToGrid
14072
+ snapToGrid,
14073
+ text
12440
14074
  };
12441
14075
  //# sourceMappingURL=papirus.js.map