@ngroznykh/papirus 0.5.10 → 0.6.1

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 (40) hide show
  1. package/README.md +42 -0
  2. package/README.ru.md +42 -0
  3. package/dist/core/DiagramRenderer.d.ts +1 -0
  4. package/dist/core/DiagramRenderer.d.ts.map +1 -1
  5. package/dist/core/InteractionManager.d.ts +3 -0
  6. package/dist/core/InteractionManager.d.ts.map +1 -1
  7. package/dist/elements/Edge.d.ts +29 -2
  8. package/dist/elements/Edge.d.ts.map +1 -1
  9. package/dist/elements/NodeImage.d.ts.map +1 -1
  10. package/dist/elements/composite/CComponent.d.ts +109 -0
  11. package/dist/elements/composite/CComponent.d.ts.map +1 -0
  12. package/dist/elements/composite/CContainer.d.ts +58 -0
  13. package/dist/elements/composite/CContainer.d.ts.map +1 -0
  14. package/dist/elements/composite/CDivider.d.ts +37 -0
  15. package/dist/elements/composite/CDivider.d.ts.map +1 -0
  16. package/dist/elements/composite/CIcon.d.ts +58 -0
  17. package/dist/elements/composite/CIcon.d.ts.map +1 -0
  18. package/dist/elements/composite/CShape.d.ts +51 -0
  19. package/dist/elements/composite/CShape.d.ts.map +1 -0
  20. package/dist/elements/composite/CText.d.ts +86 -0
  21. package/dist/elements/composite/CText.d.ts.map +1 -0
  22. package/dist/elements/composite/CompositeNode.d.ts +60 -0
  23. package/dist/elements/composite/CompositeNode.d.ts.map +1 -0
  24. package/dist/elements/composite/FlexLayout.d.ts +49 -0
  25. package/dist/elements/composite/FlexLayout.d.ts.map +1 -0
  26. package/dist/elements/composite/deserialize.d.ts +6 -0
  27. package/dist/elements/composite/deserialize.d.ts.map +1 -0
  28. package/dist/elements/composite/index.d.ts +26 -0
  29. package/dist/elements/composite/index.d.ts.map +1 -0
  30. package/dist/index.d.ts +3 -1
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/papirus.js +1915 -274
  33. package/dist/papirus.js.map +1 -1
  34. package/dist/types.d.ts +21 -0
  35. package/dist/types.d.ts.map +1 -1
  36. package/dist/utils/Serializer.d.ts.map +1 -1
  37. package/dist/utils/SvgExporter.d.ts.map +1 -1
  38. package/dist/utils/svgTint.d.ts +20 -0
  39. package/dist/utils/svgTint.d.ts.map +1 -0
  40. 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 = "";
@@ -4266,6 +4266,8 @@ class Edge extends Element {
4266
4266
  this._pathStrategy = this.getPathStrategy(this._type);
4267
4267
  this._lockAnchors = options.lockAnchors ?? true;
4268
4268
  this._labelOffset = options.labelOffset ?? 0;
4269
+ this._labelPosition = options.labelPosition ?? 0.5;
4270
+ this._labelFollowPath = options.labelFollowPath ?? false;
4269
4271
  this._labelBackground = options.labelBackground;
4270
4272
  this._labelLineGap = options.labelLineGap ?? false;
4271
4273
  if (options.label !== void 0) {
@@ -4359,6 +4361,31 @@ class Edge extends Element {
4359
4361
  this.markDirty();
4360
4362
  }
4361
4363
  }
4364
+ /**
4365
+ * Position along path (0 = source, 0.5 = midpoint, 1 = target)
4366
+ */
4367
+ get labelPosition() {
4368
+ return this._labelPosition;
4369
+ }
4370
+ set labelPosition(value) {
4371
+ const clamped = Math.max(0, Math.min(1, value));
4372
+ if (this._labelPosition !== clamped) {
4373
+ this._labelPosition = clamped;
4374
+ this.markDirty();
4375
+ }
4376
+ }
4377
+ /**
4378
+ * Whether label text rotates to follow the path tangent
4379
+ */
4380
+ get labelFollowPath() {
4381
+ return this._labelFollowPath;
4382
+ }
4383
+ set labelFollowPath(value) {
4384
+ if (this._labelFollowPath !== value) {
4385
+ this._labelFollowPath = value;
4386
+ this.markDirty();
4387
+ }
4388
+ }
4362
4389
  /**
4363
4390
  * Label background configuration
4364
4391
  */
@@ -4794,11 +4821,20 @@ class Edge extends Element {
4794
4821
  }
4795
4822
  const labelWidth = this._label.measuredWidth;
4796
4823
  const labelHeight = this._label.measuredHeight;
4824
+ const rot = this.getLabelRotation();
4825
+ let effectiveWidth = labelWidth;
4826
+ let effectiveHeight = labelHeight;
4827
+ if (rot !== 0) {
4828
+ const cosR = Math.abs(Math.cos(rot));
4829
+ const sinR = Math.abs(Math.sin(rot));
4830
+ effectiveWidth = labelWidth * cosR + labelHeight * sinR;
4831
+ effectiveHeight = labelWidth * sinR + labelHeight * cosR;
4832
+ }
4797
4833
  const labelRect = {
4798
- x: labelCenter.x - labelWidth / 2,
4799
- y: labelCenter.y - labelHeight / 2,
4800
- width: labelWidth,
4801
- height: labelHeight
4834
+ x: labelCenter.x - effectiveWidth / 2,
4835
+ y: labelCenter.y - effectiveHeight / 2,
4836
+ width: effectiveWidth,
4837
+ height: effectiveHeight
4802
4838
  };
4803
4839
  const segmentIntersections = [];
4804
4840
  for (let i = 0; i < polyline.length - 1; i++) {
@@ -5072,10 +5108,12 @@ class Edge extends Element {
5072
5108
  if (this._label === void 0 || this._path.length < 2) {
5073
5109
  return;
5074
5110
  }
5075
- const midpoint = this.getPathMidpoint();
5076
- const labelPosition = {
5077
- x: midpoint.x,
5078
- y: midpoint.y + this._labelOffset
5111
+ const { point, angle: pathAngle } = this.getPathPointAt(this._labelPosition);
5112
+ const rotation = this.getLabelRotation();
5113
+ const perpAngle = this._labelFollowPath ? pathAngle + Math.PI / 2 : Math.PI / 2;
5114
+ const labelCenter = {
5115
+ x: point.x + this._labelOffset * Math.cos(perpAngle),
5116
+ y: point.y + this._labelOffset * Math.sin(perpAngle)
5079
5117
  };
5080
5118
  const labelOpacity = this._label.style.opacity ?? 1;
5081
5119
  this._label.measure(ctx);
@@ -5084,29 +5122,39 @@ class Edge extends Element {
5084
5122
  const bgColor = this._labelBackground?.color ?? "#ffffff";
5085
5123
  const bgOpacity = this._labelBackground?.opacity ?? 1;
5086
5124
  const bgRadius = this._labelBackground?.borderRadius ?? EDGE_LABEL_BACKGROUND_RADIUS;
5087
- const bgX = labelPosition.x - labelWidth / 2;
5088
- const bgY = labelPosition.y - labelHeight / 2;
5089
- const bgWidth = labelWidth;
5090
- const bgHeight = labelHeight;
5125
+ ctx.save();
5126
+ if (rotation !== 0) {
5127
+ ctx.translate(labelCenter.x, labelCenter.y);
5128
+ ctx.rotate(rotation);
5129
+ ctx.translate(-labelCenter.x, -labelCenter.y);
5130
+ }
5131
+ const bgX = labelCenter.x - labelWidth / 2;
5132
+ const bgY = labelCenter.y - labelHeight / 2;
5091
5133
  ctx.fillStyle = bgColor;
5092
5134
  ctx.globalAlpha = bgOpacity;
5093
5135
  if (bgRadius > 0) {
5094
- this.drawRoundedRect(ctx, bgX, bgY, bgWidth, bgHeight, bgRadius);
5136
+ this.drawRoundedRect(ctx, bgX, bgY, labelWidth, labelHeight, bgRadius);
5095
5137
  ctx.fill();
5096
5138
  } else {
5097
- ctx.fillRect(bgX, bgY, bgWidth, bgHeight);
5139
+ ctx.fillRect(bgX, bgY, labelWidth, labelHeight);
5098
5140
  }
5099
5141
  ctx.globalAlpha = labelOpacity;
5100
- this._label.renderAt(ctx, labelPosition);
5142
+ this._label.renderAt(ctx, labelCenter);
5101
5143
  ctx.globalAlpha = 1;
5144
+ ctx.restore();
5102
5145
  }
5103
5146
  drawRoundedRect(ctx, x, y, width, height, radius) {
5104
5147
  drawRoundedRectPath(ctx, x, y, width, height, radius);
5105
5148
  }
5106
- getPathMidpoint() {
5149
+ /**
5150
+ * Get a point along the sampled path at parameter t (0..1).
5151
+ * Also returns the tangent angle in radians at that point.
5152
+ */
5153
+ getPathPointAt(t) {
5107
5154
  const path = this._path;
5155
+ let samples;
5108
5156
  if (this._type === "bezier" && path.length >= 4) {
5109
- const samples = [];
5157
+ samples = [];
5110
5158
  const steps = 20;
5111
5159
  for (let i = 1; i + 2 < path.length; i += 3) {
5112
5160
  const p0 = path[i - 1];
@@ -5114,16 +5162,15 @@ class Edge extends Element {
5114
5162
  const p2 = path[i + 1];
5115
5163
  const p3 = path[i + 2];
5116
5164
  for (let s = 0; s <= steps; s++) {
5117
- const t = s / steps;
5118
- if (samples.length > 0 && t === 0) {
5119
- continue;
5120
- }
5121
- samples.push(bezierPoint(p0, p1, p2, p3, t));
5165
+ const st = s / steps;
5166
+ if (samples.length > 0 && st === 0) continue;
5167
+ samples.push(bezierPoint(p0, p1, p2, p3, st));
5122
5168
  }
5123
5169
  }
5124
- return this.getPolylineMidpoint(samples);
5170
+ } else {
5171
+ samples = path;
5125
5172
  }
5126
- return this.getPolylineMidpoint(path);
5173
+ return this.getPointAlongPolyline(samples, t);
5127
5174
  }
5128
5175
  /**
5129
5176
  * Get world position of label center along path.
@@ -5132,18 +5179,33 @@ class Edge extends Element {
5132
5179
  if (this._path.length < 2) {
5133
5180
  return null;
5134
5181
  }
5135
- const midpoint = this.getPathMidpoint();
5182
+ const { point, angle: pathAngle } = this.getPathPointAt(this._labelPosition);
5183
+ const perpAngle = this._labelFollowPath ? pathAngle + Math.PI / 2 : Math.PI / 2;
5136
5184
  return {
5137
- x: midpoint.x,
5138
- y: midpoint.y + this._labelOffset
5185
+ x: point.x + this._labelOffset * Math.cos(perpAngle),
5186
+ y: point.y + this._labelOffset * Math.sin(perpAngle)
5139
5187
  };
5140
5188
  }
5141
- getPolylineMidpoint(path) {
5189
+ /**
5190
+ * Get label rotation angle in radians (if labelFollowPath is true).
5191
+ */
5192
+ getLabelRotation() {
5193
+ if (!this._labelFollowPath || this._path.length < 2) return 0;
5194
+ const { angle: angle2 } = this.getPathPointAt(this._labelPosition);
5195
+ let a = angle2;
5196
+ if (a > Math.PI / 2) a -= Math.PI;
5197
+ if (a < -Math.PI / 2) a += Math.PI;
5198
+ return a;
5199
+ }
5200
+ /**
5201
+ * Get point and tangent angle at parameter t along a polyline.
5202
+ */
5203
+ getPointAlongPolyline(path, t) {
5142
5204
  if (path.length === 0) {
5143
- return { x: 0, y: 0 };
5205
+ return { point: { x: 0, y: 0 }, angle: 0 };
5144
5206
  }
5145
5207
  if (path.length === 1) {
5146
- return path[0];
5208
+ return { point: path[0], angle: 0 };
5147
5209
  }
5148
5210
  let totalLength = 0;
5149
5211
  const segments = [];
@@ -5154,19 +5216,28 @@ class Edge extends Element {
5154
5216
  segments.push({ start, end, length });
5155
5217
  totalLength += length;
5156
5218
  }
5157
- const halfLength = totalLength / 2;
5219
+ const targetLength = totalLength * Math.max(0, Math.min(1, t));
5158
5220
  let accumulated = 0;
5159
5221
  for (const seg of segments) {
5160
- if (accumulated + seg.length >= halfLength) {
5161
- const t = (halfLength - accumulated) / seg.length;
5162
- return {
5163
- x: seg.start.x + t * (seg.end.x - seg.start.x),
5164
- y: seg.start.y + t * (seg.end.y - seg.start.y)
5222
+ if (accumulated + seg.length >= targetLength) {
5223
+ const segT = seg.length > 0 ? (targetLength - accumulated) / seg.length : 0;
5224
+ const point = {
5225
+ x: seg.start.x + segT * (seg.end.x - seg.start.x),
5226
+ y: seg.start.y + segT * (seg.end.y - seg.start.y)
5165
5227
  };
5228
+ const angle2 = Math.atan2(seg.end.y - seg.start.y, seg.end.x - seg.start.x);
5229
+ return { point, angle: angle2 };
5166
5230
  }
5167
5231
  accumulated += seg.length;
5168
5232
  }
5169
- return path[0];
5233
+ const lastSeg = segments[segments.length - 1];
5234
+ return {
5235
+ point: lastSeg.end,
5236
+ angle: Math.atan2(
5237
+ lastSeg.end.y - lastSeg.start.y,
5238
+ lastSeg.end.x - lastSeg.start.x
5239
+ )
5240
+ };
5170
5241
  }
5171
5242
  getPathStrategy(type) {
5172
5243
  switch (type) {
@@ -5214,10 +5285,10 @@ class LabelEditor {
5214
5285
  /**
5215
5286
  * Start editing a label
5216
5287
  */
5217
- start(kind, id, text, worldPosition, worldToScreen, onCommit) {
5288
+ start(kind, id, text2, worldPosition, worldToScreen, onCommit) {
5218
5289
  this.finish(true);
5219
5290
  const screenPoint = worldToScreen(worldPosition.x, worldPosition.y);
5220
- const textarea = this.createTextarea(text, screenPoint);
5291
+ const textarea = this.createTextarea(text2, screenPoint);
5221
5292
  const { cleanup } = this.setupEventHandlers(textarea, onCommit);
5222
5293
  document.body.appendChild(textarea);
5223
5294
  textarea.focus();
@@ -5241,9 +5312,9 @@ class LabelEditor {
5241
5312
  onCommit(kind, id, value);
5242
5313
  }
5243
5314
  }
5244
- createTextarea(text, screenPoint) {
5315
+ createTextarea(text2, screenPoint) {
5245
5316
  const textarea = document.createElement("textarea");
5246
- textarea.value = text;
5317
+ textarea.value = text2;
5247
5318
  textarea.rows = 1;
5248
5319
  textarea.spellcheck = false;
5249
5320
  textarea.setAttribute("aria-label", "Edit label");
@@ -5820,6 +5891,24 @@ class InteractionManager {
5820
5891
  return;
5821
5892
  }
5822
5893
  this.selectionManager.handleClick(event);
5894
+ this.emitComponentClick(event);
5895
+ }
5896
+ emitComponentClick(event) {
5897
+ const point = { x: event.worldX, y: event.worldY };
5898
+ const hitElement = this.renderer.getInteractableElementAtPoint(point, event.screenX, event.screenY);
5899
+ if (!hitElement || !("typeName" in hitElement)) return;
5900
+ const node = hitElement;
5901
+ if (node.typeName !== "composite") return;
5902
+ const compositeNode = node;
5903
+ const component = compositeNode.getComponentAtPoint(point);
5904
+ if (!component) return;
5905
+ if (component.id !== void 0) {
5906
+ this.renderer.emit("componentClick", node.id, component, point);
5907
+ }
5908
+ const asAny = component;
5909
+ if (typeof asAny["onClick"] === "function") {
5910
+ asAny["onClick"](component);
5911
+ }
5823
5912
  }
5824
5913
  handleDoubleClick(event) {
5825
5914
  if (this.renderer.blocksDiagramPointerAtScreen(event.screenX, event.screenY)) {
@@ -5847,6 +5936,10 @@ class InteractionManager {
5847
5936
  return;
5848
5937
  }
5849
5938
  if ("typeName" in hitElement) {
5939
+ if (hitElement.typeName === "composite") {
5940
+ this.startCompositeNameEdit(hitElement);
5941
+ return;
5942
+ }
5850
5943
  const editText = hitElement.label?.editableText ?? hitElement.label?.text ?? "";
5851
5944
  this.startLabelEdit("node", hitElement.id, editText, hitElement.getLabelPosition());
5852
5945
  return;
@@ -5956,16 +6049,67 @@ class InteractionManager {
5956
6049
  return false;
5957
6050
  }
5958
6051
  }
5959
- startLabelEdit(kind, id, text, worldPosition) {
6052
+ startLabelEdit(kind, id, text2, worldPosition) {
5960
6053
  this.labelEditor.start(
5961
6054
  kind,
5962
6055
  id,
5963
- text,
6056
+ text2,
5964
6057
  worldPosition,
5965
6058
  (x, y) => this.renderer.worldToScreen(x, y),
5966
6059
  (k, i, value) => this.handleLabelCommit(k, i, value)
5967
6060
  );
5968
6061
  }
6062
+ startCompositeNameEdit(node) {
6063
+ const nameText = this.findNameComponent(node.content);
6064
+ if (!nameText) return;
6065
+ const currentText = nameText.text;
6066
+ const worldPos = node.getLabelPosition();
6067
+ this.labelEditor.start(
6068
+ "node",
6069
+ node.id,
6070
+ currentText,
6071
+ worldPos,
6072
+ (x, y) => this.renderer.worldToScreen(x, y),
6073
+ (_kind, _id, value) => {
6074
+ const trimmed = value.trim();
6075
+ if (trimmed.length > 0 && trimmed !== currentText) {
6076
+ const before = currentText;
6077
+ nameText.text = trimmed;
6078
+ this.historyManager.execute({
6079
+ execute: () => {
6080
+ nameText.text = trimmed;
6081
+ },
6082
+ undo: () => {
6083
+ nameText.text = before;
6084
+ }
6085
+ });
6086
+ }
6087
+ }
6088
+ );
6089
+ }
6090
+ findNameComponent(container2) {
6091
+ const bindToDisplayName = "__name__";
6092
+ for (const child of container2.children) {
6093
+ if (child.type === "text") {
6094
+ const ctext = child;
6095
+ if (ctext.bindToProperty === bindToDisplayName) return ctext;
6096
+ }
6097
+ if (child.type === "container") {
6098
+ const found = this.findNameComponent(
6099
+ child
6100
+ );
6101
+ if (found) return found;
6102
+ }
6103
+ if (child.type === "shape") {
6104
+ const content = child.content;
6105
+ if (content) {
6106
+ const found = this.findNameComponent(content);
6107
+ if (found) return found;
6108
+ }
6109
+ }
6110
+ }
6111
+ return null;
6112
+ }
5969
6113
  handleLabelCommit(kind, id, nextValue) {
5970
6114
  if (kind === "node") {
5971
6115
  this.changeNodeProperties(id, (node) => {
@@ -6500,20 +6644,20 @@ class ContextMenuManager extends EventEmitter {
6500
6644
  }
6501
6645
  }
6502
6646
  buildMenu(items, target) {
6503
- const container = document.createElement("div");
6504
- container.style.position = "fixed";
6505
- container.style.zIndex = "10000";
6506
- container.style.background = "#ffffff";
6507
- container.style.border = "1px solid #e5e7eb";
6508
- container.style.borderRadius = "8px";
6509
- container.style.padding = "6px";
6510
- container.style.boxShadow = "0 8px 20px rgba(15, 23, 42, 0.18)";
6511
- container.style.fontFamily = "system-ui, -apple-system, Segoe UI, sans-serif";
6512
- container.style.fontSize = "14px";
6513
- container.style.color = "#0f172a";
6647
+ const container2 = document.createElement("div");
6648
+ container2.style.position = "fixed";
6649
+ container2.style.zIndex = "10000";
6650
+ container2.style.background = "#ffffff";
6651
+ container2.style.border = "1px solid #e5e7eb";
6652
+ container2.style.borderRadius = "8px";
6653
+ container2.style.padding = "6px";
6654
+ container2.style.boxShadow = "0 8px 20px rgba(15, 23, 42, 0.18)";
6655
+ container2.style.fontFamily = "system-ui, -apple-system, Segoe UI, sans-serif";
6656
+ container2.style.fontSize = "14px";
6657
+ container2.style.color = "#0f172a";
6514
6658
  const list = this.buildList(items, target);
6515
- container.appendChild(list);
6516
- return container;
6659
+ container2.appendChild(list);
6660
+ return container2;
6517
6661
  }
6518
6662
  buildList(items, target) {
6519
6663
  const list = document.createElement("ul");
@@ -6615,7 +6759,7 @@ class ContextMenuManager extends EventEmitter {
6615
6759
  }
6616
6760
  return list;
6617
6761
  }
6618
- createIconElement(icon) {
6762
+ createIconElement(icon2) {
6619
6763
  const iconEl = document.createElement("span");
6620
6764
  iconEl.style.width = "16px";
6621
6765
  iconEl.style.height = "16px";
@@ -6624,16 +6768,16 @@ class ContextMenuManager extends EventEmitter {
6624
6768
  iconEl.style.justifyContent = "center";
6625
6769
  iconEl.style.textAlign = "center";
6626
6770
  iconEl.style.flexShrink = "0";
6627
- if (!icon) {
6771
+ if (!icon2) {
6628
6772
  iconEl.textContent = "";
6629
6773
  return iconEl;
6630
6774
  }
6631
- if (typeof icon === "string") {
6632
- if (this.isSvgString(icon)) {
6633
- iconEl.innerHTML = icon;
6775
+ if (typeof icon2 === "string") {
6776
+ if (this.isSvgString(icon2)) {
6777
+ iconEl.innerHTML = icon2;
6634
6778
  } else if (this.options.iconToUrl) {
6635
6779
  const img = document.createElement("img");
6636
- img.src = this.options.iconToUrl(icon);
6780
+ img.src = this.options.iconToUrl(icon2);
6637
6781
  img.alt = "";
6638
6782
  img.style.width = "16px";
6639
6783
  img.style.height = "16px";
@@ -6642,15 +6786,15 @@ class ContextMenuManager extends EventEmitter {
6642
6786
  } else {
6643
6787
  iconEl.classList.add("material-symbols-outlined");
6644
6788
  iconEl.style.fontSize = "16px";
6645
- iconEl.textContent = icon;
6789
+ iconEl.textContent = icon2;
6646
6790
  }
6647
6791
  return iconEl;
6648
6792
  }
6649
- if (icon.type === "svg" || icon.type === "html") {
6650
- iconEl.innerHTML = icon.value;
6793
+ if (icon2.type === "svg" || icon2.type === "html") {
6794
+ iconEl.innerHTML = icon2.value;
6651
6795
  } else if (this.options.iconToUrl) {
6652
6796
  const img = document.createElement("img");
6653
- img.src = this.options.iconToUrl(icon.value);
6797
+ img.src = this.options.iconToUrl(icon2.value);
6654
6798
  img.alt = "";
6655
6799
  img.style.width = "16px";
6656
6800
  img.style.height = "16px";
@@ -6659,7 +6803,7 @@ class ContextMenuManager extends EventEmitter {
6659
6803
  } else {
6660
6804
  iconEl.classList.add("material-symbols-outlined");
6661
6805
  iconEl.style.fontSize = "16px";
6662
- iconEl.textContent = icon.value;
6806
+ iconEl.textContent = icon2.value;
6663
6807
  }
6664
6808
  return iconEl;
6665
6809
  }
@@ -8540,10 +8684,6 @@ class LRUCache {
8540
8684
  return this.maxSize;
8541
8685
  }
8542
8686
  }
8543
- function isCornerPlacement(placement) {
8544
- return placement === "top-left" || placement === "top-right" || placement === "bottom-left" || placement === "bottom-right";
8545
- }
8546
- const svgTextCache = new LRUCache(100);
8547
8687
  function styleSetColor(style, key, color) {
8548
8688
  const hasKey = new RegExp(`${key}\\s*:`).test(style);
8549
8689
  if (hasKey) {
@@ -8594,6 +8734,18 @@ function svgToDataUrl(svg) {
8594
8734
  const encoded = encodeURIComponent(svg).replace(/%0A/g, "").replace(/%0D/g, "").replace(/%09/g, " ").replace(/%20/g, " ");
8595
8735
  return `data:image/svg+xml;utf8,${encoded}`;
8596
8736
  }
8737
+ function isSvgUrl(url) {
8738
+ try {
8739
+ const path = new URL(url, "http://localhost").pathname;
8740
+ return path.endsWith(".svg");
8741
+ } catch {
8742
+ return url.endsWith(".svg");
8743
+ }
8744
+ }
8745
+ function isCornerPlacement(placement) {
8746
+ return placement === "top-left" || placement === "top-right" || placement === "bottom-left" || placement === "bottom-right";
8747
+ }
8748
+ const svgTextCache = new LRUCache(100);
8597
8749
  class NodeImage {
8598
8750
  constructor(options, onChange) {
8599
8751
  this._loaded = false;
@@ -10454,181 +10606,1609 @@ const ShapeFactories = {
10454
10606
  }
10455
10607
  }
10456
10608
  };
10457
- const DEFAULT_THEME = {
10458
- name: "default",
10459
- colors: {
10460
- background: "#ffffff",
10461
- grid: "#e5e5e5",
10462
- selection: "rgba(59, 130, 246, 0.1)",
10463
- connectionPreview: "#3b82f6"
10464
- },
10465
- node: {
10466
- default: {
10467
- fillColor: "#ffffff",
10468
- strokeColor: "#333333",
10469
- strokeWidth: 2,
10470
- opacity: 1,
10471
- cornerRadius: 4
10472
- },
10473
- hover: {
10474
- fillColor: "#f5f5f5",
10475
- strokeColor: "#6366f1",
10476
- strokeWidth: 2,
10477
- opacity: 1,
10478
- cornerRadius: 4
10479
- },
10480
- selected: {
10481
- strokeColor: "#3b82f6",
10482
- strokeWidth: 2,
10483
- opacity: 1
10484
- },
10485
- dragging: {
10486
- strokeColor: "#333333",
10487
- strokeWidth: 2,
10488
- opacity: 0.8
10489
- }
10490
- },
10491
- edge: {
10492
- default: {
10493
- strokeColor: "#666666",
10494
- strokeWidth: 2,
10495
- opacity: 1
10496
- },
10497
- hover: {
10498
- strokeColor: "#6366f1",
10499
- strokeWidth: 2,
10500
- opacity: 1
10501
- },
10502
- selected: {
10503
- strokeColor: "#3b82f6",
10504
- strokeWidth: 3,
10505
- opacity: 1
10506
- }
10507
- },
10508
- text: {
10509
- font: "14px sans-serif",
10510
- fontSize: 14,
10511
- fontFamily: "sans-serif",
10512
- fontWeight: "normal",
10513
- color: "#333333",
10514
- align: "center",
10515
- baseline: "middle"
10516
- },
10517
- port: {
10518
- default: { color: "#666666", radius: 6 },
10519
- hover: { color: "#3b82f6", radius: 7 }
10520
- },
10521
- group: {
10522
- default: {
10523
- fillColor: "rgba(200, 200, 200, 0.2)",
10524
- strokeColor: "#999999",
10525
- strokeWidth: 1,
10526
- opacity: 1
10527
- },
10528
- selected: {
10529
- fillColor: "rgba(59, 130, 246, 0.1)",
10530
- strokeColor: "#3b82f6",
10531
- strokeWidth: 2,
10532
- opacity: 1
10609
+ const DEFAULT_FONT_FAMILY = "sans-serif";
10610
+ const DEFAULT_FONT_SIZE = 14;
10611
+ const DEFAULT_LINE_HEIGHT = 1.2;
10612
+ const DEFAULT_COLOR$1 = "#000000";
10613
+ class CText {
10614
+ constructor(options) {
10615
+ this.type = "text";
10616
+ this._cachedLines = null;
10617
+ this.id = options.id;
10618
+ this._text = options.text;
10619
+ this._fontFamily = options.fontFamily ?? DEFAULT_FONT_FAMILY;
10620
+ this._fontWeight = options.fontWeight ?? "normal";
10621
+ this._fontStyle = options.fontStyle ?? "normal";
10622
+ this._fontSize = options.fontSize ?? DEFAULT_FONT_SIZE;
10623
+ this._color = options.color ?? DEFAULT_COLOR$1;
10624
+ this._align = options.align ?? "center";
10625
+ this._verticalAlign = options.verticalAlign ?? "middle";
10626
+ this._maxLines = options.maxLines;
10627
+ this._lineHeight = options.lineHeight ?? DEFAULT_LINE_HEIGHT;
10628
+ this._role = options.role;
10629
+ this._bindToProperty = options.bindToProperty;
10630
+ this._rotation = options.rotation ?? 0;
10631
+ this.style = options.style ?? {};
10632
+ }
10633
+ // --- Property accessors with dirty notification ---
10634
+ get text() {
10635
+ return this._text;
10636
+ }
10637
+ set text(value) {
10638
+ if (this._text !== value) {
10639
+ this._text = value;
10640
+ this._cachedLines = null;
10641
+ this._onChange?.();
10533
10642
  }
10534
10643
  }
10535
- };
10536
- const DARK_THEME = {
10537
- name: "dark",
10538
- colors: {
10539
- background: "#1a1a1a",
10540
- grid: "#333333",
10541
- selection: "rgba(99, 102, 241, 0.2)",
10542
- connectionPreview: "#6366f1"
10543
- },
10544
- node: {
10545
- default: {
10546
- fillColor: "#2d2d2d",
10547
- strokeColor: "#555555",
10548
- strokeWidth: 2,
10549
- opacity: 1,
10550
- cornerRadius: 4
10551
- },
10552
- hover: {
10553
- fillColor: "#3d3d3d",
10554
- strokeColor: "#6366f1",
10555
- strokeWidth: 2,
10556
- opacity: 1,
10557
- cornerRadius: 4
10558
- },
10559
- selected: {
10560
- fillColor: "#2d2d2d",
10561
- strokeColor: "#555555",
10562
- strokeWidth: 2,
10563
- opacity: 1,
10564
- cornerRadius: 4
10565
- },
10566
- dragging: {
10567
- fillColor: "#404040",
10568
- strokeColor: "#555555",
10569
- strokeWidth: 2,
10570
- opacity: 0.8,
10571
- cornerRadius: 4
10644
+ get fontFamily() {
10645
+ return this._fontFamily;
10646
+ }
10647
+ set fontFamily(value) {
10648
+ if (this._fontFamily !== value) {
10649
+ this._fontFamily = value;
10650
+ this._onChange?.();
10572
10651
  }
10573
- },
10574
- edge: {
10575
- default: {
10576
- strokeColor: "#888888",
10577
- strokeWidth: 2,
10578
- opacity: 1
10579
- },
10580
- hover: {
10581
- strokeColor: "#6366f1",
10582
- strokeWidth: 2,
10583
- opacity: 1
10584
- },
10585
- selected: {
10586
- strokeColor: "#818cf8",
10587
- strokeWidth: 3,
10588
- opacity: 1
10652
+ }
10653
+ get fontWeight() {
10654
+ return this._fontWeight;
10655
+ }
10656
+ set fontWeight(value) {
10657
+ if (this._fontWeight !== value) {
10658
+ this._fontWeight = value;
10659
+ this._onChange?.();
10589
10660
  }
10590
- },
10591
- text: {
10592
- font: "14px sans-serif",
10593
- fontSize: 14,
10594
- fontFamily: "sans-serif",
10595
- fontWeight: "normal",
10596
- color: "#e0e0e0",
10597
- align: "center",
10598
- baseline: "middle"
10599
- },
10600
- port: {
10601
- default: { color: "#888888", radius: 6 },
10602
- hover: { color: "#6366f1", radius: 7 }
10603
- },
10604
- group: {
10605
- default: {
10606
- fillColor: "rgba(100, 100, 100, 0.2)",
10607
- strokeColor: "#666666",
10608
- strokeWidth: 1,
10609
- opacity: 1
10610
- },
10611
- selected: {
10612
- fillColor: "rgba(99, 102, 241, 0.2)",
10613
- strokeColor: "#6366f1",
10614
- strokeWidth: 2,
10615
- opacity: 1
10661
+ }
10662
+ get fontStyle() {
10663
+ return this._fontStyle;
10664
+ }
10665
+ set fontStyle(value) {
10666
+ if (this._fontStyle !== value) {
10667
+ this._fontStyle = value;
10668
+ this._onChange?.();
10616
10669
  }
10617
10670
  }
10618
- };
10619
- class StyleManager {
10620
- constructor(theme) {
10621
- this._classes = /* @__PURE__ */ new Map();
10622
- this._builtInThemes = /* @__PURE__ */ new Map([
10623
- ["default", DEFAULT_THEME],
10624
- ["dark", DARK_THEME]
10625
- ]);
10626
- if (theme === void 0) {
10627
- this._theme = DEFAULT_THEME;
10628
- } else if (typeof theme === "string") {
10629
- this._theme = this._builtInThemes.get(theme) ?? DEFAULT_THEME;
10630
- } else {
10631
- this._theme = theme;
10671
+ get fontSize() {
10672
+ return this._fontSize;
10673
+ }
10674
+ set fontSize(value) {
10675
+ if (this._fontSize !== value) {
10676
+ this._fontSize = value;
10677
+ this._onChange?.();
10678
+ }
10679
+ }
10680
+ get color() {
10681
+ return this._color;
10682
+ }
10683
+ set color(value) {
10684
+ if (this._color !== value) {
10685
+ this._color = value;
10686
+ this._onChange?.();
10687
+ }
10688
+ }
10689
+ get align() {
10690
+ return this._align;
10691
+ }
10692
+ set align(value) {
10693
+ if (this._align !== value) {
10694
+ this._align = value;
10695
+ this._onChange?.();
10696
+ }
10697
+ }
10698
+ get verticalAlign() {
10699
+ return this._verticalAlign;
10700
+ }
10701
+ set verticalAlign(value) {
10702
+ if (this._verticalAlign !== value) {
10703
+ this._verticalAlign = value;
10704
+ this._onChange?.();
10705
+ }
10706
+ }
10707
+ get maxLines() {
10708
+ return this._maxLines;
10709
+ }
10710
+ set maxLines(value) {
10711
+ if (this._maxLines !== value) {
10712
+ this._maxLines = value;
10713
+ this._onChange?.();
10714
+ }
10715
+ }
10716
+ get lineHeight() {
10717
+ return this._lineHeight;
10718
+ }
10719
+ get role() {
10720
+ return this._role;
10721
+ }
10722
+ get bindToProperty() {
10723
+ return this._bindToProperty;
10724
+ }
10725
+ get rotation() {
10726
+ return this._rotation;
10727
+ }
10728
+ set rotation(value) {
10729
+ if (this._rotation !== value) {
10730
+ this._rotation = value;
10731
+ this._onChange?.();
10732
+ }
10733
+ }
10734
+ setOnChange(cb) {
10735
+ this._onChange = cb;
10736
+ }
10737
+ getFont() {
10738
+ return `${this._fontStyle} ${this._fontWeight} ${this._fontSize}px ${this._fontFamily}`;
10739
+ }
10740
+ getLineHeightPx() {
10741
+ return this._fontSize * this._lineHeight;
10742
+ }
10743
+ /**
10744
+ * Word-wrap text to fit within maxWidth.
10745
+ */
10746
+ wrapText(ctx, maxWidth) {
10747
+ ctx.font = this.getFont();
10748
+ const words = this._text.split(" ");
10749
+ const lines = [];
10750
+ let currentLine = "";
10751
+ for (const word of words) {
10752
+ const testLine = currentLine ? `${currentLine} ${word}` : word;
10753
+ const metrics = ctx.measureText(testLine);
10754
+ if (metrics.width > maxWidth && currentLine) {
10755
+ lines.push(currentLine);
10756
+ currentLine = word;
10757
+ } else {
10758
+ currentLine = testLine;
10759
+ }
10760
+ }
10761
+ if (currentLine) {
10762
+ lines.push(currentLine);
10763
+ }
10764
+ if (this._maxLines !== void 0 && lines.length > this._maxLines) {
10765
+ const truncated = lines.slice(0, this._maxLines);
10766
+ const lastLine = truncated[truncated.length - 1];
10767
+ if (lastLine !== void 0) {
10768
+ truncated[truncated.length - 1] = lastLine + "…";
10769
+ }
10770
+ return truncated;
10771
+ }
10772
+ return lines;
10773
+ }
10774
+ measure(ctx) {
10775
+ ctx.font = this.getFont();
10776
+ const lineHeightPx = this.getLineHeightPx();
10777
+ const rawLines = this._text.split("\n");
10778
+ let maxWidth = 0;
10779
+ for (const line of rawLines) {
10780
+ const metrics = ctx.measureText(line);
10781
+ if (metrics.width > maxWidth) {
10782
+ maxWidth = metrics.width;
10783
+ }
10784
+ }
10785
+ let lineCount = rawLines.length;
10786
+ if (this._maxLines !== void 0 && lineCount > this._maxLines) {
10787
+ lineCount = this._maxLines;
10788
+ }
10789
+ const width = maxWidth;
10790
+ const height = lineCount * lineHeightPx;
10791
+ if (this._rotation === 90 || this._rotation === -90) {
10792
+ return { width: height, height: width };
10793
+ }
10794
+ return { width, height };
10795
+ }
10796
+ render(ctx, bounds) {
10797
+ if (this.style.visible === false) return;
10798
+ const opacity = this.style.opacity ?? 1;
10799
+ if (opacity <= 0) return;
10800
+ ctx.save();
10801
+ ctx.globalAlpha *= opacity;
10802
+ ctx.font = this.getFont();
10803
+ ctx.fillStyle = this._color;
10804
+ const isRotated = this._rotation === 90 || this._rotation === -90;
10805
+ const textBounds = isRotated ? { x: bounds.x, y: bounds.y, width: bounds.height, height: bounds.width } : bounds;
10806
+ const lines = this.wrapText(ctx, textBounds.width);
10807
+ this._cachedLines = lines;
10808
+ const lineHeightPx = this.getLineHeightPx();
10809
+ const totalTextHeight = lines.length * lineHeightPx;
10810
+ let textAlign;
10811
+ let xBase;
10812
+ switch (this._align) {
10813
+ case "left":
10814
+ textAlign = "left";
10815
+ xBase = textBounds.x;
10816
+ break;
10817
+ case "right":
10818
+ textAlign = "right";
10819
+ xBase = textBounds.x + textBounds.width;
10820
+ break;
10821
+ default:
10822
+ textAlign = "center";
10823
+ xBase = textBounds.x + textBounds.width / 2;
10824
+ break;
10825
+ }
10826
+ let yStart;
10827
+ switch (this._verticalAlign) {
10828
+ case "top":
10829
+ yStart = textBounds.y + lineHeightPx / 2;
10830
+ break;
10831
+ case "bottom":
10832
+ yStart = textBounds.y + textBounds.height - totalTextHeight + lineHeightPx / 2;
10833
+ break;
10834
+ default:
10835
+ yStart = textBounds.y + (textBounds.height - totalTextHeight) / 2 + lineHeightPx / 2;
10836
+ break;
10837
+ }
10838
+ ctx.textAlign = textAlign;
10839
+ ctx.textBaseline = "middle";
10840
+ if (isRotated) {
10841
+ const cx = bounds.x + bounds.width / 2;
10842
+ const cy = bounds.y + bounds.height / 2;
10843
+ ctx.translate(cx, cy);
10844
+ ctx.rotate(this._rotation * Math.PI / 180);
10845
+ const offsetX = xBase - (textBounds.x + textBounds.width / 2);
10846
+ const offsetY = yStart - (textBounds.y + textBounds.height / 2);
10847
+ for (let i = 0; i < lines.length; i++) {
10848
+ ctx.fillText(lines[i], offsetX, offsetY + i * lineHeightPx);
10849
+ }
10850
+ } else {
10851
+ for (let i = 0; i < lines.length; i++) {
10852
+ ctx.fillText(lines[i], xBase, yStart + i * lineHeightPx);
10853
+ }
10854
+ }
10855
+ ctx.restore();
10856
+ }
10857
+ hitTest(point, bounds) {
10858
+ if (this.style.visible === false) return null;
10859
+ if (point.x >= bounds.x && point.x <= bounds.x + bounds.width && point.y >= bounds.y && point.y <= bounds.y + bounds.height) {
10860
+ return this;
10861
+ }
10862
+ return null;
10863
+ }
10864
+ serialize() {
10865
+ const data = {
10866
+ type: "text",
10867
+ text: this._text
10868
+ };
10869
+ if (this.id !== void 0) data.id = this.id;
10870
+ if (this.style && Object.keys(this.style).length > 0) data.style = this.style;
10871
+ if (this._fontFamily !== DEFAULT_FONT_FAMILY) data.fontFamily = this._fontFamily;
10872
+ if (this._fontWeight !== "normal") data.fontWeight = this._fontWeight;
10873
+ if (this._fontStyle !== "normal") data.fontStyle = this._fontStyle;
10874
+ if (this._fontSize !== DEFAULT_FONT_SIZE) data.fontSize = this._fontSize;
10875
+ if (this._color !== DEFAULT_COLOR$1) data.color = this._color;
10876
+ if (this._align !== "center") data.align = this._align;
10877
+ if (this._verticalAlign !== "middle") data.verticalAlign = this._verticalAlign;
10878
+ if (this._maxLines !== void 0) data.maxLines = this._maxLines;
10879
+ if (this._lineHeight !== DEFAULT_LINE_HEIGHT) data.lineHeight = this._lineHeight;
10880
+ if (this._bindToProperty !== void 0) data.bindToProperty = this._bindToProperty;
10881
+ if (this._role !== void 0) data.role = this._role;
10882
+ if (this._rotation !== 0) data.rotation = this._rotation;
10883
+ return data;
10884
+ }
10885
+ toSVG(bounds) {
10886
+ if (this.style.visible === false) return "";
10887
+ const lineHeightPx = this.getLineHeightPx();
10888
+ const displayLines = this._cachedLines ?? this._text.split("\n");
10889
+ const totalTextHeight = displayLines.length * lineHeightPx;
10890
+ let anchor;
10891
+ let xBase;
10892
+ switch (this._align) {
10893
+ case "left":
10894
+ anchor = "start";
10895
+ xBase = bounds.x;
10896
+ break;
10897
+ case "right":
10898
+ anchor = "end";
10899
+ xBase = bounds.x + bounds.width;
10900
+ break;
10901
+ default:
10902
+ anchor = "middle";
10903
+ xBase = bounds.x + bounds.width / 2;
10904
+ break;
10905
+ }
10906
+ let yStart;
10907
+ switch (this._verticalAlign) {
10908
+ case "top":
10909
+ yStart = bounds.y + lineHeightPx * 0.75;
10910
+ break;
10911
+ case "bottom":
10912
+ yStart = bounds.y + bounds.height - totalTextHeight + lineHeightPx * 0.75;
10913
+ break;
10914
+ default:
10915
+ yStart = bounds.y + (bounds.height - totalTextHeight) / 2 + lineHeightPx * 0.75;
10916
+ break;
10917
+ }
10918
+ const fontAttrs = [
10919
+ `font-family="${this._fontFamily}"`,
10920
+ `font-size="${this._fontSize}"`,
10921
+ this._fontWeight !== "normal" ? `font-weight="${this._fontWeight}"` : "",
10922
+ this._fontStyle !== "normal" ? `font-style="${this._fontStyle}"` : ""
10923
+ ].filter(Boolean).join(" ");
10924
+ const transform = this._rotation !== 0 ? ` transform="rotate(${this._rotation}, ${bounds.x + bounds.width / 2}, ${bounds.y + bounds.height / 2})"` : "";
10925
+ const tspans = displayLines.map(
10926
+ (line, i) => `<tspan x="${xBase}" dy="${i === 0 ? 0 : lineHeightPx}">${escapeXml(line)}</tspan>`
10927
+ ).join("");
10928
+ return `<text x="${xBase}" y="${yStart}" ${fontAttrs} fill="${this._color}" text-anchor="${anchor}"${transform}>${tspans}</text>`;
10929
+ }
10930
+ }
10931
+ function escapeXml(str) {
10932
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
10933
+ }
10934
+ const DEFAULT_ICON_SIZE = 24;
10935
+ const svgFetchCache = new LRUCache(100);
10936
+ class CIcon {
10937
+ constructor(options) {
10938
+ this.type = "icon";
10939
+ this._loaded = false;
10940
+ this.id = options.id;
10941
+ this._source = options.source;
10942
+ this._width = options.width ?? DEFAULT_ICON_SIZE;
10943
+ this._height = options.height ?? DEFAULT_ICON_SIZE;
10944
+ this._backgroundColor = options.backgroundColor;
10945
+ this._fillColor = options.fillColor;
10946
+ this._bindsNotationIcon = options.bindsNotationIcon ?? false;
10947
+ this.onClick = options.onClick;
10948
+ this.style = options.style ?? {};
10949
+ if (options.visible === false) {
10950
+ this.style.visible = false;
10951
+ }
10952
+ this._image = new Image();
10953
+ this._image.onload = () => {
10954
+ this._loaded = true;
10955
+ this._onChange?.();
10956
+ };
10957
+ this._image.onerror = () => {
10958
+ this._onChange?.();
10959
+ };
10960
+ void this.loadSource(this._source);
10961
+ }
10962
+ get source() {
10963
+ return this._source;
10964
+ }
10965
+ set source(value) {
10966
+ if (this._source !== value) {
10967
+ this._source = value;
10968
+ this._loaded = false;
10969
+ void this.loadSource(value);
10970
+ this._onChange?.();
10971
+ }
10972
+ }
10973
+ get width() {
10974
+ return this._width;
10975
+ }
10976
+ set width(value) {
10977
+ if (this._width !== value) {
10978
+ this._width = value;
10979
+ this._onChange?.();
10980
+ }
10981
+ }
10982
+ get height() {
10983
+ return this._height;
10984
+ }
10985
+ set height(value) {
10986
+ if (this._height !== value) {
10987
+ this._height = value;
10988
+ this._onChange?.();
10989
+ }
10990
+ }
10991
+ get backgroundColor() {
10992
+ return this._backgroundColor;
10993
+ }
10994
+ set backgroundColor(value) {
10995
+ if (this._backgroundColor !== value) {
10996
+ this._backgroundColor = value;
10997
+ this._onChange?.();
10998
+ }
10999
+ }
11000
+ get fillColor() {
11001
+ return this._fillColor;
11002
+ }
11003
+ set fillColor(value) {
11004
+ if (this._fillColor !== value) {
11005
+ this._fillColor = value;
11006
+ void this.loadSource(this._source);
11007
+ this._onChange?.();
11008
+ }
11009
+ }
11010
+ get loaded() {
11011
+ return this._loaded;
11012
+ }
11013
+ get bindsNotationIcon() {
11014
+ return this._bindsNotationIcon;
11015
+ }
11016
+ set bindsNotationIcon(value) {
11017
+ if (this._bindsNotationIcon !== value) {
11018
+ this._bindsNotationIcon = value;
11019
+ this._onChange?.();
11020
+ }
11021
+ }
11022
+ setOnChange(cb) {
11023
+ this._onChange = cb;
11024
+ }
11025
+ async loadSource(source) {
11026
+ if (isSvgMarkup(source)) {
11027
+ const tinted = this._fillColor ? tintSvg(source, void 0, this._fillColor) : source;
11028
+ this._image.src = svgToDataUrl(tinted);
11029
+ } else if (isSvgUrl(source) && this._fillColor) {
11030
+ try {
11031
+ const svgText = await this.fetchSvg(source);
11032
+ const tinted = tintSvg(svgText, void 0, this._fillColor);
11033
+ this._image.src = svgToDataUrl(tinted);
11034
+ } catch {
11035
+ this._image.src = source;
11036
+ }
11037
+ } else {
11038
+ this._image.src = source;
11039
+ }
11040
+ }
11041
+ async fetchSvg(url) {
11042
+ let promise = svgFetchCache.get(url);
11043
+ if (!promise) {
11044
+ promise = fetch(url).then((r) => r.text());
11045
+ svgFetchCache.set(url, promise);
11046
+ }
11047
+ return promise;
11048
+ }
11049
+ measure(_ctx) {
11050
+ return { width: this._width, height: this._height };
11051
+ }
11052
+ render(ctx, bounds) {
11053
+ if (this.style.visible === false) return;
11054
+ const opacity = this.style.opacity ?? 1;
11055
+ if (opacity <= 0) return;
11056
+ ctx.save();
11057
+ ctx.globalAlpha *= opacity;
11058
+ if (this._backgroundColor) {
11059
+ ctx.fillStyle = this._backgroundColor;
11060
+ ctx.fillRect(bounds.x, bounds.y, bounds.width, bounds.height);
11061
+ }
11062
+ if (this._loaded) {
11063
+ const drawWidth = Math.min(this._width, bounds.width);
11064
+ const drawHeight = Math.min(this._height, bounds.height);
11065
+ const dx = bounds.x + (bounds.width - drawWidth) / 2;
11066
+ const dy = bounds.y + (bounds.height - drawHeight) / 2;
11067
+ ctx.drawImage(this._image, dx, dy, drawWidth, drawHeight);
11068
+ }
11069
+ ctx.restore();
11070
+ }
11071
+ hitTest(point, bounds) {
11072
+ if (this.style.visible === false) return null;
11073
+ if (point.x >= bounds.x && point.x <= bounds.x + bounds.width && point.y >= bounds.y && point.y <= bounds.y + bounds.height) {
11074
+ return this;
11075
+ }
11076
+ return null;
11077
+ }
11078
+ serialize() {
11079
+ const data = {
11080
+ type: "icon",
11081
+ source: this._source,
11082
+ width: this._width,
11083
+ height: this._height
11084
+ };
11085
+ if (this.id !== void 0) data.id = this.id;
11086
+ if (this.style && Object.keys(this.style).length > 0) data.style = this.style;
11087
+ if (this._backgroundColor) data.backgroundColor = this._backgroundColor;
11088
+ if (this._fillColor) data.fillColor = this._fillColor;
11089
+ if (this._bindsNotationIcon) data.bindsNotationIcon = true;
11090
+ return data;
11091
+ }
11092
+ toSVG(bounds) {
11093
+ if (this.style.visible === false) return "";
11094
+ let svg = "";
11095
+ if (this._backgroundColor) {
11096
+ svg += `<rect x="${bounds.x}" y="${bounds.y}" width="${bounds.width}" height="${bounds.height}" fill="${this._backgroundColor}" />`;
11097
+ }
11098
+ const drawWidth = Math.min(this._width, bounds.width);
11099
+ const drawHeight = Math.min(this._height, bounds.height);
11100
+ const dx = bounds.x + (bounds.width - drawWidth) / 2;
11101
+ const dy = bounds.y + (bounds.height - drawHeight) / 2;
11102
+ svg += `<image href="${escapeXmlAttr(this._source)}" x="${dx}" y="${dy}" width="${drawWidth}" height="${drawHeight}" />`;
11103
+ return svg;
11104
+ }
11105
+ }
11106
+ function escapeXmlAttr(str) {
11107
+ return str.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
11108
+ }
11109
+ const DEFAULT_COLOR = "#cccccc";
11110
+ const DEFAULT_THICKNESS = 1;
11111
+ class CDivider {
11112
+ constructor(options = {}) {
11113
+ this.type = "divider";
11114
+ this.id = options.id;
11115
+ this._color = options.color ?? DEFAULT_COLOR;
11116
+ this._thickness = options.thickness ?? DEFAULT_THICKNESS;
11117
+ this.style = options.style ?? {};
11118
+ }
11119
+ get color() {
11120
+ return this._color;
11121
+ }
11122
+ set color(value) {
11123
+ if (this._color !== value) {
11124
+ this._color = value;
11125
+ this._onChange?.();
11126
+ }
11127
+ }
11128
+ get thickness() {
11129
+ return this._thickness;
11130
+ }
11131
+ set thickness(value) {
11132
+ if (this._thickness !== value) {
11133
+ this._thickness = value;
11134
+ this._onChange?.();
11135
+ }
11136
+ }
11137
+ setOnChange(cb) {
11138
+ this._onChange = cb;
11139
+ }
11140
+ measure(_ctx) {
11141
+ return { width: this._thickness, height: this._thickness };
11142
+ }
11143
+ render(ctx, bounds) {
11144
+ if (this.style.visible === false) return;
11145
+ ctx.save();
11146
+ ctx.strokeStyle = this._color;
11147
+ ctx.lineWidth = this._thickness;
11148
+ ctx.globalAlpha *= this.style.opacity ?? 1;
11149
+ ctx.beginPath();
11150
+ if (bounds.width >= bounds.height) {
11151
+ const y = bounds.y + bounds.height / 2;
11152
+ ctx.moveTo(bounds.x, y);
11153
+ ctx.lineTo(bounds.x + bounds.width, y);
11154
+ } else {
11155
+ const x = bounds.x + bounds.width / 2;
11156
+ ctx.moveTo(x, bounds.y);
11157
+ ctx.lineTo(x, bounds.y + bounds.height);
11158
+ }
11159
+ ctx.stroke();
11160
+ ctx.restore();
11161
+ }
11162
+ hitTest(_point, _bounds) {
11163
+ return null;
11164
+ }
11165
+ serialize() {
11166
+ const data = { type: "divider" };
11167
+ if (this.id !== void 0) data.id = this.id;
11168
+ if (this._color !== DEFAULT_COLOR) data.color = this._color;
11169
+ if (this._thickness !== DEFAULT_THICKNESS) data.thickness = this._thickness;
11170
+ if (this.style && Object.keys(this.style).length > 0) data.style = this.style;
11171
+ return data;
11172
+ }
11173
+ toSVG(bounds) {
11174
+ if (this.style.visible === false) return "";
11175
+ if (bounds.width >= bounds.height) {
11176
+ const y = bounds.y + bounds.height / 2;
11177
+ return `<line x1="${bounds.x}" y1="${y}" x2="${bounds.x + bounds.width}" y2="${y}" stroke="${this._color}" stroke-width="${this._thickness}" />`;
11178
+ } else {
11179
+ const x = bounds.x + bounds.width / 2;
11180
+ return `<line x1="${x}" y1="${bounds.y}" x2="${x}" y2="${bounds.y + bounds.height}" stroke="${this._color}" stroke-width="${this._thickness}" />`;
11181
+ }
11182
+ }
11183
+ }
11184
+ function normalizeSides(value, fallback = 0) {
11185
+ if (value === void 0) {
11186
+ return { top: fallback, right: fallback, bottom: fallback, left: fallback };
11187
+ }
11188
+ if (typeof value === "number") {
11189
+ return { top: value, right: value, bottom: value, left: value };
11190
+ }
11191
+ return {
11192
+ top: value.top ?? fallback,
11193
+ right: value.right ?? fallback,
11194
+ bottom: value.bottom ?? fallback,
11195
+ left: value.left ?? fallback
11196
+ };
11197
+ }
11198
+ function flexLayout(container2, config, children) {
11199
+ const pad = normalizeSides(config.padding);
11200
+ const isRow = config.direction === "row";
11201
+ const innerWidth = Math.max(0, container2.width - pad.left - pad.right);
11202
+ const innerHeight = Math.max(0, container2.height - pad.top - pad.bottom);
11203
+ const availableMain = isRow ? innerWidth : innerHeight;
11204
+ const availableCross = isRow ? innerHeight : innerWidth;
11205
+ if (children.length === 0) {
11206
+ return {
11207
+ childBounds: [],
11208
+ contentSize: { width: pad.left + pad.right, height: pad.top + pad.bottom }
11209
+ };
11210
+ }
11211
+ const baseSizes = [];
11212
+ const mainMargins = [];
11213
+ const crossMargins = [];
11214
+ for (const child of children) {
11215
+ const m = child.margin;
11216
+ if (isRow) {
11217
+ mainMargins.push({ before: m.left, after: m.right });
11218
+ crossMargins.push({ before: m.top, after: m.bottom });
11219
+ } else {
11220
+ mainMargins.push({ before: m.top, after: m.bottom });
11221
+ crossMargins.push({ before: m.left, after: m.right });
11222
+ }
11223
+ const measured = isRow ? child.measure.width : child.measure.height;
11224
+ const minMain = isRow ? child.minSize.width : child.minSize.height;
11225
+ const basis = child.flexBasis === "auto" ? measured : child.flexBasis;
11226
+ baseSizes.push(Math.max(basis, minMain));
11227
+ }
11228
+ const totalGaps = config.gap * Math.max(0, children.length - 1);
11229
+ let totalMargins = 0;
11230
+ for (const mm of mainMargins) {
11231
+ totalMargins += mm.before + mm.after;
11232
+ }
11233
+ let totalBase = 0;
11234
+ for (const s of baseSizes) {
11235
+ totalBase += s;
11236
+ }
11237
+ const totalUsed = totalBase + totalGaps + totalMargins;
11238
+ const finalSizes = [...baseSizes];
11239
+ const remaining = availableMain - totalUsed;
11240
+ if (remaining > 0) {
11241
+ let totalGrow = 0;
11242
+ for (const child of children) {
11243
+ totalGrow += child.flexGrow;
11244
+ }
11245
+ if (totalGrow > 0) {
11246
+ for (let i = 0; i < children.length; i++) {
11247
+ const child = children[i];
11248
+ finalSizes[i] = finalSizes[i] + remaining * child.flexGrow / totalGrow;
11249
+ }
11250
+ }
11251
+ } else if (remaining < 0) {
11252
+ let totalShrink = 0;
11253
+ for (let i = 0; i < children.length; i++) {
11254
+ totalShrink += children[i].flexShrink * baseSizes[i];
11255
+ }
11256
+ if (totalShrink > 0) {
11257
+ const deficit = -remaining;
11258
+ for (let i = 0; i < children.length; i++) {
11259
+ const child = children[i];
11260
+ const shrinkRatio = child.flexShrink * baseSizes[i] / totalShrink;
11261
+ const minMain = isRow ? child.minSize.width : child.minSize.height;
11262
+ finalSizes[i] = Math.max(minMain, finalSizes[i] - deficit * shrinkRatio);
11263
+ }
11264
+ }
11265
+ }
11266
+ let actualTotal = totalGaps + totalMargins;
11267
+ for (const s of finalSizes) {
11268
+ actualTotal += s;
11269
+ }
11270
+ const freeSpace = Math.max(0, availableMain - actualTotal);
11271
+ let mainOffset;
11272
+ let betweenExtra;
11273
+ switch (config.justifyContent) {
11274
+ case "center":
11275
+ mainOffset = freeSpace / 2;
11276
+ betweenExtra = 0;
11277
+ break;
11278
+ case "end":
11279
+ mainOffset = freeSpace;
11280
+ betweenExtra = 0;
11281
+ break;
11282
+ case "space-between":
11283
+ mainOffset = 0;
11284
+ betweenExtra = children.length > 1 ? freeSpace / (children.length - 1) : 0;
11285
+ break;
11286
+ case "space-around":
11287
+ betweenExtra = freeSpace / children.length;
11288
+ mainOffset = betweenExtra / 2;
11289
+ break;
11290
+ default:
11291
+ mainOffset = 0;
11292
+ betweenExtra = 0;
11293
+ break;
11294
+ }
11295
+ const childBounds = [];
11296
+ let cursor = mainOffset;
11297
+ let maxCrossContent = 0;
11298
+ for (let i = 0; i < children.length; i++) {
11299
+ const child = children[i];
11300
+ const mainSize = finalSizes[i];
11301
+ const mm = mainMargins[i];
11302
+ const cm = crossMargins[i];
11303
+ cursor += mm.before;
11304
+ const crossAvailable = availableCross - cm.before - cm.after;
11305
+ const measuredCross = isRow ? child.measure.height : child.measure.width;
11306
+ const align = child.alignSelf === "auto" ? config.alignItems : child.alignSelf;
11307
+ let crossSize;
11308
+ let crossOffset;
11309
+ if (align === "stretch") {
11310
+ crossSize = crossAvailable;
11311
+ crossOffset = cm.before;
11312
+ } else {
11313
+ crossSize = Math.min(measuredCross, crossAvailable);
11314
+ switch (align) {
11315
+ case "center":
11316
+ crossOffset = cm.before + (crossAvailable - crossSize) / 2;
11317
+ break;
11318
+ case "end":
11319
+ crossOffset = cm.before + crossAvailable - crossSize;
11320
+ break;
11321
+ default:
11322
+ crossOffset = cm.before;
11323
+ break;
11324
+ }
11325
+ }
11326
+ maxCrossContent = Math.max(maxCrossContent, crossOffset + crossSize + cm.after);
11327
+ if (isRow) {
11328
+ childBounds.push({
11329
+ x: pad.left + cursor,
11330
+ y: pad.top + crossOffset,
11331
+ width: mainSize,
11332
+ height: crossSize
11333
+ });
11334
+ } else {
11335
+ childBounds.push({
11336
+ x: pad.left + crossOffset,
11337
+ y: pad.top + cursor,
11338
+ width: crossSize,
11339
+ height: mainSize
11340
+ });
11341
+ }
11342
+ cursor += mainSize + mm.after;
11343
+ if (i < children.length - 1) {
11344
+ cursor += config.gap + betweenExtra;
11345
+ }
11346
+ }
11347
+ const mainContent = cursor;
11348
+ const contentWidth = isRow ? pad.left + mainContent + pad.right : pad.left + maxCrossContent + pad.right;
11349
+ const contentHeight = isRow ? pad.top + maxCrossContent + pad.bottom : pad.top + mainContent + pad.bottom;
11350
+ return {
11351
+ childBounds,
11352
+ contentSize: { width: contentWidth, height: contentHeight }
11353
+ };
11354
+ }
11355
+ class CContainer {
11356
+ constructor(options = {}) {
11357
+ this.type = "container";
11358
+ this._cachedBounds = null;
11359
+ this._cachedContainerBounds = null;
11360
+ this.id = options.id;
11361
+ this._direction = options.direction ?? "column";
11362
+ this._justifyContent = options.justifyContent ?? "start";
11363
+ this._alignItems = options.alignItems ?? "stretch";
11364
+ this._gap = options.gap ?? 0;
11365
+ this._padding = options.padding ?? 0;
11366
+ this._children = options.children ?? [];
11367
+ this.style = options.style ?? {};
11368
+ for (const child of this._children) {
11369
+ child.setOnChange(() => this.handleChildChange());
11370
+ }
11371
+ }
11372
+ get direction() {
11373
+ return this._direction;
11374
+ }
11375
+ get justifyContent() {
11376
+ return this._justifyContent;
11377
+ }
11378
+ get alignItems() {
11379
+ return this._alignItems;
11380
+ }
11381
+ get gap() {
11382
+ return this._gap;
11383
+ }
11384
+ get padding() {
11385
+ return this._padding;
11386
+ }
11387
+ get children() {
11388
+ return this._children;
11389
+ }
11390
+ addChild(child) {
11391
+ child.setOnChange(() => this.handleChildChange());
11392
+ this._children.push(child);
11393
+ this.handleChildChange();
11394
+ }
11395
+ removeChild(index) {
11396
+ const removed = this._children.splice(index, 1)[0];
11397
+ if (removed) {
11398
+ removed.setOnChange(void 0);
11399
+ this.handleChildChange();
11400
+ }
11401
+ return removed;
11402
+ }
11403
+ insertChild(index, child) {
11404
+ child.setOnChange(() => this.handleChildChange());
11405
+ this._children.splice(index, 0, child);
11406
+ this.handleChildChange();
11407
+ }
11408
+ setOnChange(cb) {
11409
+ this._onChange = cb;
11410
+ }
11411
+ handleChildChange() {
11412
+ this._cachedBounds = null;
11413
+ this._onChange?.();
11414
+ }
11415
+ getFlexConfig() {
11416
+ return {
11417
+ direction: this._direction,
11418
+ justifyContent: this._justifyContent,
11419
+ alignItems: this._alignItems,
11420
+ gap: this._gap,
11421
+ padding: this._padding
11422
+ };
11423
+ }
11424
+ buildFlexChildren(ctx) {
11425
+ return this._children.map((child) => {
11426
+ const s = child.style;
11427
+ const measured = child.measure(ctx);
11428
+ return {
11429
+ measure: measured,
11430
+ minSize: { width: 0, height: 0 },
11431
+ flexGrow: s.flexGrow ?? 0,
11432
+ flexShrink: s.flexShrink ?? 1,
11433
+ flexBasis: s.flexBasis ?? "auto",
11434
+ alignSelf: s.alignSelf ?? "auto",
11435
+ margin: normalizeSides(s.margin)
11436
+ };
11437
+ });
11438
+ }
11439
+ measure(ctx) {
11440
+ const flexChildren = this.buildFlexChildren(ctx);
11441
+ const measureConfig = {
11442
+ ...this.getFlexConfig(),
11443
+ justifyContent: "start",
11444
+ alignItems: "start"
11445
+ };
11446
+ const measureChildren = flexChildren.map((c) => ({
11447
+ ...c,
11448
+ flexGrow: 0,
11449
+ alignSelf: "start"
11450
+ }));
11451
+ const result = flexLayout(
11452
+ { width: 1e5, height: 1e5 },
11453
+ measureConfig,
11454
+ measureChildren
11455
+ );
11456
+ return result.contentSize;
11457
+ }
11458
+ render(ctx, bounds) {
11459
+ if (this.style.visible === false) return;
11460
+ const flexChildren = this.buildFlexChildren(ctx);
11461
+ const result = flexLayout(
11462
+ { width: bounds.width, height: bounds.height },
11463
+ this.getFlexConfig(),
11464
+ flexChildren
11465
+ );
11466
+ this._cachedBounds = result.childBounds;
11467
+ this._cachedContainerBounds = bounds;
11468
+ const opacity = this.style.opacity ?? 1;
11469
+ if (opacity < 1) {
11470
+ ctx.save();
11471
+ ctx.globalAlpha *= opacity;
11472
+ }
11473
+ for (let i = 0; i < this._children.length; i++) {
11474
+ const child = this._children[i];
11475
+ if (child.style.visible === false) continue;
11476
+ const cb = result.childBounds[i];
11477
+ child.render(ctx, {
11478
+ x: bounds.x + cb.x,
11479
+ y: bounds.y + cb.y,
11480
+ width: cb.width,
11481
+ height: cb.height
11482
+ });
11483
+ }
11484
+ if (opacity < 1) {
11485
+ ctx.restore();
11486
+ }
11487
+ }
11488
+ hitTest(point, bounds) {
11489
+ if (this.style.visible === false) return null;
11490
+ if (!this._cachedBounds) return null;
11491
+ for (let i = this._children.length - 1; i >= 0; i--) {
11492
+ const child = this._children[i];
11493
+ if (child.style.visible === false) continue;
11494
+ const cb = this._cachedBounds[i];
11495
+ const absBounds = {
11496
+ x: bounds.x + cb.x,
11497
+ y: bounds.y + cb.y,
11498
+ width: cb.width,
11499
+ height: cb.height
11500
+ };
11501
+ const hit = child.hitTest(point, absBounds);
11502
+ if (hit) return hit;
11503
+ }
11504
+ return null;
11505
+ }
11506
+ /**
11507
+ * Find a component by id recursively.
11508
+ */
11509
+ findById(id) {
11510
+ for (const child of this._children) {
11511
+ if (child.id === id) return child;
11512
+ if (child.type === "container") {
11513
+ const found = child.findById(id);
11514
+ if (found) return found;
11515
+ }
11516
+ if (child.type === "shape") {
11517
+ const content = child.content;
11518
+ if (content) {
11519
+ const found = content.findById(id);
11520
+ if (found) return found;
11521
+ }
11522
+ }
11523
+ }
11524
+ return void 0;
11525
+ }
11526
+ serialize() {
11527
+ const data = {
11528
+ type: "container",
11529
+ direction: this._direction,
11530
+ children: this._children.map((c) => c.serialize())
11531
+ };
11532
+ if (this.id !== void 0) data.id = this.id;
11533
+ if (this.style && Object.keys(this.style).length > 0) data.style = this.style;
11534
+ if (this._justifyContent !== "start") data.justifyContent = this._justifyContent;
11535
+ if (this._alignItems !== "stretch") data.alignItems = this._alignItems;
11536
+ if (this._gap !== 0) data.gap = this._gap;
11537
+ if (typeof this._padding === "number" && this._padding !== 0 || typeof this._padding === "object" && Object.values(this._padding).some((v) => v !== void 0 && v !== 0)) {
11538
+ data.padding = this._padding;
11539
+ }
11540
+ return data;
11541
+ }
11542
+ toSVG(bounds) {
11543
+ if (this.style.visible === false) return "";
11544
+ const cachedBounds = this._cachedBounds;
11545
+ const cachedContainer = this._cachedContainerBounds;
11546
+ const childSvg = this._children.map((child, i) => {
11547
+ if (child.style.visible === false) return "";
11548
+ if (cachedBounds && cachedContainer) {
11549
+ const cb = cachedBounds[i];
11550
+ const dx = bounds.x - cachedContainer.x;
11551
+ const dy = bounds.y - cachedContainer.y;
11552
+ return child.toSVG({
11553
+ x: cachedContainer.x + cb.x + dx,
11554
+ y: cachedContainer.y + cb.y + dy,
11555
+ width: cb.width,
11556
+ height: cb.height
11557
+ });
11558
+ }
11559
+ const h = bounds.height / Math.max(1, this._children.length);
11560
+ return child.toSVG({
11561
+ x: bounds.x,
11562
+ y: bounds.y + i * h,
11563
+ width: bounds.width,
11564
+ height: h
11565
+ });
11566
+ }).join("");
11567
+ return `<g>${childSvg}</g>`;
11568
+ }
11569
+ }
11570
+ class CShape {
11571
+ constructor(options = {}) {
11572
+ this.type = "shape";
11573
+ this.id = options.id;
11574
+ this._borderColor = options.borderColor;
11575
+ this._borderWidth = options.borderWidth ?? 0;
11576
+ this._backgroundColor = options.backgroundColor;
11577
+ this._cornerRadius = options.cornerRadius ?? 0;
11578
+ this._padding = options.padding ?? 0;
11579
+ this.onClick = options.onClick;
11580
+ this._content = options.content;
11581
+ this.style = options.style ?? {};
11582
+ if (this._content) {
11583
+ this._content.setOnChange(() => this._onChange?.());
11584
+ }
11585
+ }
11586
+ get borderColor() {
11587
+ return this._borderColor;
11588
+ }
11589
+ set borderColor(value) {
11590
+ if (this._borderColor !== value) {
11591
+ this._borderColor = value;
11592
+ this._onChange?.();
11593
+ }
11594
+ }
11595
+ get borderWidth() {
11596
+ return this._borderWidth;
11597
+ }
11598
+ set borderWidth(value) {
11599
+ if (this._borderWidth !== value) {
11600
+ this._borderWidth = value;
11601
+ this._onChange?.();
11602
+ }
11603
+ }
11604
+ get backgroundColor() {
11605
+ return this._backgroundColor;
11606
+ }
11607
+ set backgroundColor(value) {
11608
+ if (this._backgroundColor !== value) {
11609
+ this._backgroundColor = value;
11610
+ this._onChange?.();
11611
+ }
11612
+ }
11613
+ get cornerRadius() {
11614
+ return this._cornerRadius;
11615
+ }
11616
+ get padding() {
11617
+ return this._padding;
11618
+ }
11619
+ get content() {
11620
+ return this._content;
11621
+ }
11622
+ setOnChange(cb) {
11623
+ this._onChange = cb;
11624
+ }
11625
+ measure(ctx) {
11626
+ const pad = normalizeSides(this._padding);
11627
+ const bw = this._borderWidth;
11628
+ if (this._content) {
11629
+ const contentSize = this._content.measure(ctx);
11630
+ return {
11631
+ width: contentSize.width + pad.left + pad.right + bw * 2,
11632
+ height: contentSize.height + pad.top + pad.bottom + bw * 2
11633
+ };
11634
+ }
11635
+ return {
11636
+ width: pad.left + pad.right + bw * 2,
11637
+ height: pad.top + pad.bottom + bw * 2
11638
+ };
11639
+ }
11640
+ render(ctx, bounds) {
11641
+ if (this.style.visible === false) return;
11642
+ ctx.save();
11643
+ ctx.globalAlpha *= this.style.opacity ?? 1;
11644
+ const { x, y, width, height } = bounds;
11645
+ if (this._backgroundColor) {
11646
+ ctx.fillStyle = this._backgroundColor;
11647
+ if (this._cornerRadius > 0 && ctx.roundRect) {
11648
+ ctx.beginPath();
11649
+ ctx.roundRect(x, y, width, height, this._cornerRadius);
11650
+ ctx.fill();
11651
+ } else {
11652
+ ctx.fillRect(x, y, width, height);
11653
+ }
11654
+ }
11655
+ if (this._borderColor && this._borderWidth > 0) {
11656
+ ctx.strokeStyle = this._borderColor;
11657
+ ctx.lineWidth = this._borderWidth;
11658
+ if (this._cornerRadius > 0 && ctx.roundRect) {
11659
+ ctx.beginPath();
11660
+ ctx.roundRect(x, y, width, height, this._cornerRadius);
11661
+ ctx.stroke();
11662
+ } else {
11663
+ ctx.strokeRect(x, y, width, height);
11664
+ }
11665
+ }
11666
+ if (this._content) {
11667
+ const pad = normalizeSides(this._padding);
11668
+ const bw = this._borderWidth;
11669
+ this._content.render(ctx, {
11670
+ x: x + pad.left + bw,
11671
+ y: y + pad.top + bw,
11672
+ width: Math.max(0, width - pad.left - pad.right - bw * 2),
11673
+ height: Math.max(0, height - pad.top - pad.bottom - bw * 2)
11674
+ });
11675
+ }
11676
+ ctx.restore();
11677
+ }
11678
+ hitTest(point, bounds) {
11679
+ if (this.style.visible === false) return null;
11680
+ if (point.x < bounds.x || point.x > bounds.x + bounds.width || point.y < bounds.y || point.y > bounds.y + bounds.height) {
11681
+ return null;
11682
+ }
11683
+ if (this._content) {
11684
+ const pad = normalizeSides(this._padding);
11685
+ const bw = this._borderWidth;
11686
+ const contentBounds = {
11687
+ x: bounds.x + pad.left + bw,
11688
+ y: bounds.y + pad.top + bw,
11689
+ width: Math.max(0, bounds.width - pad.left - pad.right - bw * 2),
11690
+ height: Math.max(0, bounds.height - pad.top - pad.bottom - bw * 2)
11691
+ };
11692
+ const hit = this._content.hitTest(point, contentBounds);
11693
+ if (hit) return hit;
11694
+ }
11695
+ return this;
11696
+ }
11697
+ serialize() {
11698
+ const data = { type: "shape" };
11699
+ if (this.id !== void 0) data.id = this.id;
11700
+ if (this.style && Object.keys(this.style).length > 0) data.style = this.style;
11701
+ if (this._borderColor) data.borderColor = this._borderColor;
11702
+ if (this._borderWidth !== 0) data.borderWidth = this._borderWidth;
11703
+ if (this._backgroundColor) data.backgroundColor = this._backgroundColor;
11704
+ if (this._cornerRadius !== 0) data.cornerRadius = this._cornerRadius;
11705
+ if (typeof this._padding === "number" && this._padding !== 0 || typeof this._padding === "object" && Object.values(this._padding).some((v) => v !== void 0 && v !== 0)) {
11706
+ data.padding = this._padding;
11707
+ }
11708
+ if (this._content) {
11709
+ data.content = this._content.serialize();
11710
+ }
11711
+ return data;
11712
+ }
11713
+ toSVG(bounds) {
11714
+ if (this.style.visible === false) return "";
11715
+ let svg = "";
11716
+ const { x, y, width, height } = bounds;
11717
+ const hasFill = !!this._backgroundColor;
11718
+ const hasStroke = !!this._borderColor && this._borderWidth > 0;
11719
+ if (hasFill || hasStroke) {
11720
+ const fill = hasFill ? `fill="${this._backgroundColor}"` : 'fill="none"';
11721
+ const stroke = hasStroke ? `stroke="${this._borderColor}" stroke-width="${this._borderWidth}"` : "";
11722
+ const rx = this._cornerRadius > 0 ? ` rx="${this._cornerRadius}"` : "";
11723
+ svg += `<rect x="${x}" y="${y}" width="${width}" height="${height}" ${fill} ${stroke}${rx} />`;
11724
+ }
11725
+ if (this._content) {
11726
+ const pad = normalizeSides(this._padding);
11727
+ const bw = this._borderWidth;
11728
+ svg += this._content.toSVG({
11729
+ x: x + pad.left + bw,
11730
+ y: y + pad.top + bw,
11731
+ width: Math.max(0, width - pad.left - pad.right - bw * 2),
11732
+ height: Math.max(0, height - pad.top - pad.bottom - bw * 2)
11733
+ });
11734
+ }
11735
+ return svg;
11736
+ }
11737
+ }
11738
+ function deserializeCComponent(data) {
11739
+ switch (data.type) {
11740
+ case "text":
11741
+ return new CText({
11742
+ id: data.id,
11743
+ text: data.text ?? "",
11744
+ fontFamily: data.fontFamily,
11745
+ fontWeight: data.fontWeight,
11746
+ fontStyle: data.fontStyle,
11747
+ fontSize: data.fontSize,
11748
+ color: data.color,
11749
+ align: data.align,
11750
+ verticalAlign: data.verticalAlign,
11751
+ maxLines: data.maxLines,
11752
+ lineHeight: data.lineHeight,
11753
+ role: data.role,
11754
+ bindToProperty: data.bindToProperty,
11755
+ rotation: data.rotation,
11756
+ style: data.style
11757
+ });
11758
+ case "icon":
11759
+ return new CIcon({
11760
+ id: data.id,
11761
+ source: data.source ?? "",
11762
+ width: data.width,
11763
+ height: data.height,
11764
+ backgroundColor: data.backgroundColor,
11765
+ fillColor: data.fillColor,
11766
+ bindsNotationIcon: data.bindsNotationIcon === true,
11767
+ style: data.style
11768
+ });
11769
+ case "divider":
11770
+ return new CDivider({
11771
+ id: data.id,
11772
+ color: data.color,
11773
+ thickness: data.thickness,
11774
+ style: data.style
11775
+ });
11776
+ case "container":
11777
+ return new CContainer({
11778
+ id: data.id,
11779
+ direction: data.direction,
11780
+ justifyContent: data.justifyContent,
11781
+ alignItems: data.alignItems,
11782
+ gap: data.gap,
11783
+ padding: data.padding,
11784
+ children: data.children?.map(deserializeCComponent),
11785
+ style: data.style
11786
+ });
11787
+ case "shape":
11788
+ return new CShape({
11789
+ id: data.id,
11790
+ borderColor: data.borderColor,
11791
+ borderWidth: data.borderWidth,
11792
+ backgroundColor: data.backgroundColor,
11793
+ cornerRadius: data.cornerRadius,
11794
+ padding: data.padding,
11795
+ content: data.content ? deserializeCComponent(data.content) : void 0,
11796
+ style: data.style
11797
+ });
11798
+ default:
11799
+ throw new Error(`Unknown component type: ${String(data.type)}`);
11800
+ }
11801
+ }
11802
+ class CompositeNode extends Node {
11803
+ constructor(options) {
11804
+ super(options);
11805
+ this._content = options.content;
11806
+ this._shapeType = options.shapeType ?? "rectangle";
11807
+ this._cornerRadius = options.cornerRadius ?? 0;
11808
+ this._pathFactory = options.pathFactory;
11809
+ this._autoSize = options.autoSize ?? false;
11810
+ this._minWidth = options.minWidth ?? 0;
11811
+ this._minHeight = options.minHeight ?? 0;
11812
+ this._content.setOnChange(() => this.markDirty());
11813
+ }
11814
+ get typeName() {
11815
+ return "composite";
11816
+ }
11817
+ get content() {
11818
+ return this._content;
11819
+ }
11820
+ get shapeType() {
11821
+ return this._shapeType;
11822
+ }
11823
+ get cornerRadius() {
11824
+ return this._cornerRadius;
11825
+ }
11826
+ get autoSize() {
11827
+ return this._autoSize;
11828
+ }
11829
+ set autoSize(value) {
11830
+ if (this._autoSize !== value) {
11831
+ this._autoSize = value;
11832
+ this.markDirty();
11833
+ }
11834
+ }
11835
+ get minWidth() {
11836
+ return this._minWidth;
11837
+ }
11838
+ get minHeight() {
11839
+ return this._minHeight;
11840
+ }
11841
+ /**
11842
+ * Find a component by id in the content tree.
11843
+ */
11844
+ getComponent(id) {
11845
+ if (this._content.id === id) return this._content;
11846
+ return this._content.findById(id);
11847
+ }
11848
+ /**
11849
+ * Hit test the component tree. Returns the deepest component at the given world point.
11850
+ */
11851
+ getComponentAtPoint(worldPoint) {
11852
+ const bounds = this.getContentBounds();
11853
+ return this._content.hitTest(worldPoint, bounds);
11854
+ }
11855
+ getContentBounds() {
11856
+ return this.getLabelContainerBounds(this.getBounds());
11857
+ }
11858
+ // --- Shape rendering ---
11859
+ hitTest(point) {
11860
+ if (this._shapeType === "circle") {
11861
+ return this.hitTestEllipse(point);
11862
+ }
11863
+ if (this._shapeType === "diamond") {
11864
+ return this.hitTestDiamond(point);
11865
+ }
11866
+ return super.hitTest(point);
11867
+ }
11868
+ hitTestEllipse(point) {
11869
+ const center = this.getCenter();
11870
+ const padding = NODE_HITBOX_PADDING;
11871
+ const rx = this._width / 2 + padding;
11872
+ const ry = this._height / 2 + padding;
11873
+ const dx = point.x - center.x;
11874
+ const dy = point.y - center.y;
11875
+ return dx * dx / (rx * rx) + dy * dy / (ry * ry) <= 1;
11876
+ }
11877
+ hitTestDiamond(point) {
11878
+ const center = this.getCenter();
11879
+ const padding = NODE_HITBOX_PADDING;
11880
+ const hw = this._width / 2 + padding;
11881
+ const hh = this._height / 2 + padding;
11882
+ const dx = Math.abs(point.x - center.x);
11883
+ const dy = Math.abs(point.y - center.y);
11884
+ return dx / hw + dy / hh <= 1;
11885
+ }
11886
+ render(ctx) {
11887
+ if (this._autoSize) {
11888
+ this.applyAutoSize(ctx);
11889
+ }
11890
+ const { x, y, width, height } = this.getBounds();
11891
+ const style = this.style;
11892
+ const baseOpacity = style.opacity ?? 1;
11893
+ const fillOpacity = style.fillOpacity ?? 1;
11894
+ const strokeOpacity = style.strokeOpacity ?? 1;
11895
+ this.applyStyle(ctx);
11896
+ ctx.beginPath();
11897
+ this.buildShapePath(ctx, x, y, width, height);
11898
+ ctx.closePath();
11899
+ ctx.globalAlpha = baseOpacity * fillOpacity;
11900
+ ctx.fill();
11901
+ ctx.globalAlpha = baseOpacity * strokeOpacity;
11902
+ ctx.stroke();
11903
+ ctx.globalAlpha = 1;
11904
+ this.renderContents(ctx);
11905
+ }
11906
+ buildShapePath(ctx, x, y, w, h) {
11907
+ switch (this._shapeType) {
11908
+ case "circle": {
11909
+ const cx = x + w / 2;
11910
+ const cy = y + h / 2;
11911
+ ctx.ellipse(cx, cy, w / 2, h / 2, 0, 0, Math.PI * 2);
11912
+ break;
11913
+ }
11914
+ case "diamond": {
11915
+ const cx = x + w / 2;
11916
+ const cy = y + h / 2;
11917
+ ctx.moveTo(cx, y);
11918
+ ctx.lineTo(x + w, cy);
11919
+ ctx.lineTo(cx, y + h);
11920
+ ctx.lineTo(x, cy);
11921
+ break;
11922
+ }
11923
+ case "custom": {
11924
+ if (this._pathFactory) {
11925
+ const path = this._pathFactory(w, h);
11926
+ ctx.save();
11927
+ ctx.translate(x, y);
11928
+ ctx.fill(path);
11929
+ ctx.stroke(path);
11930
+ ctx.restore();
11931
+ return;
11932
+ }
11933
+ this.buildRectPath(ctx, x, y, w, h);
11934
+ break;
11935
+ }
11936
+ default:
11937
+ this.buildRectPath(ctx, x, y, w, h);
11938
+ break;
11939
+ }
11940
+ }
11941
+ buildRectPath(ctx, x, y, w, h) {
11942
+ const radius = Math.min(this._cornerRadius, w / 2, h / 2);
11943
+ if (radius > 0) {
11944
+ ctx.moveTo(x + radius, y);
11945
+ ctx.lineTo(x + w - radius, y);
11946
+ ctx.arcTo(x + w, y, x + w, y + radius, radius);
11947
+ ctx.lineTo(x + w, y + h - radius);
11948
+ ctx.arcTo(x + w, y + h, x + w - radius, y + h, radius);
11949
+ ctx.lineTo(x + radius, y + h);
11950
+ ctx.arcTo(x, y + h, x, y + h - radius, radius);
11951
+ ctx.lineTo(x, y + radius);
11952
+ ctx.arcTo(x, y, x + radius, y, radius);
11953
+ } else {
11954
+ ctx.rect(x, y, w, h);
11955
+ }
11956
+ }
11957
+ // --- Content rendering (overrides Node.renderContents) ---
11958
+ renderContents(ctx) {
11959
+ ctx.setLineDash([]);
11960
+ ctx.lineDashOffset = 0;
11961
+ const contentBounds = this.getContentBounds();
11962
+ this._content.render(ctx, contentBounds);
11963
+ this.renderPorts(ctx);
11964
+ }
11965
+ applyAutoSize(ctx) {
11966
+ const contentSize = this._content.measure(ctx);
11967
+ const inset = this.contentInset;
11968
+ const neededWidth = Math.max(
11969
+ this._minWidth,
11970
+ contentSize.width + inset.left + inset.right
11971
+ );
11972
+ const neededHeight = Math.max(
11973
+ this._minHeight,
11974
+ contentSize.height + inset.top + inset.bottom
11975
+ );
11976
+ if (neededWidth > this._width) {
11977
+ this._width = neededWidth;
11978
+ }
11979
+ if (neededHeight > this._height) {
11980
+ this._height = neededHeight;
11981
+ }
11982
+ }
11983
+ // --- Outline methods for connections (delegate based on shapeType) ---
11984
+ getLabelContainerBounds(bounds) {
11985
+ if (this._shapeType === "circle") {
11986
+ const factor = 1 / Math.SQRT2;
11987
+ const w = bounds.width * factor;
11988
+ const h = bounds.height * factor;
11989
+ return {
11990
+ x: bounds.x + (bounds.width - w) / 2,
11991
+ y: bounds.y + (bounds.height - h) / 2,
11992
+ width: w,
11993
+ height: h
11994
+ };
11995
+ }
11996
+ if (this._shapeType === "diamond") {
11997
+ const w = bounds.width / 2;
11998
+ const h = bounds.height / 2;
11999
+ return {
12000
+ x: bounds.x + (bounds.width - w) / 2,
12001
+ y: bounds.y + (bounds.height - h) / 2,
12002
+ width: w,
12003
+ height: h
12004
+ };
12005
+ }
12006
+ const ci = this.contentInset;
12007
+ return {
12008
+ x: bounds.x + ci.left,
12009
+ y: bounds.y + ci.top,
12010
+ width: Math.max(0, bounds.width - ci.left - ci.right),
12011
+ height: Math.max(0, bounds.height - ci.top - ci.bottom)
12012
+ };
12013
+ }
12014
+ /**
12015
+ * Get the minimum content size needed for the content tree.
12016
+ * Useful for external auto-sizing logic.
12017
+ */
12018
+ getContentMinSize(ctx) {
12019
+ return this._content.measure(ctx);
12020
+ }
12021
+ }
12022
+ function text(options) {
12023
+ return new CText(options);
12024
+ }
12025
+ function icon(options) {
12026
+ return new CIcon(options);
12027
+ }
12028
+ function divider(options) {
12029
+ return new CDivider(options);
12030
+ }
12031
+ function container(options) {
12032
+ return new CContainer(options);
12033
+ }
12034
+ function shape(options) {
12035
+ return new CShape(options);
12036
+ }
12037
+ const DEFAULT_THEME = {
12038
+ name: "default",
12039
+ colors: {
12040
+ background: "#ffffff",
12041
+ grid: "#e5e5e5",
12042
+ selection: "rgba(59, 130, 246, 0.1)",
12043
+ connectionPreview: "#3b82f6"
12044
+ },
12045
+ node: {
12046
+ default: {
12047
+ fillColor: "#ffffff",
12048
+ strokeColor: "#333333",
12049
+ strokeWidth: 2,
12050
+ opacity: 1,
12051
+ cornerRadius: 4
12052
+ },
12053
+ hover: {
12054
+ fillColor: "#f5f5f5",
12055
+ strokeColor: "#6366f1",
12056
+ strokeWidth: 2,
12057
+ opacity: 1,
12058
+ cornerRadius: 4
12059
+ },
12060
+ selected: {
12061
+ strokeColor: "#3b82f6",
12062
+ strokeWidth: 2,
12063
+ opacity: 1
12064
+ },
12065
+ dragging: {
12066
+ strokeColor: "#333333",
12067
+ strokeWidth: 2,
12068
+ opacity: 0.8
12069
+ }
12070
+ },
12071
+ edge: {
12072
+ default: {
12073
+ strokeColor: "#666666",
12074
+ strokeWidth: 2,
12075
+ opacity: 1
12076
+ },
12077
+ hover: {
12078
+ strokeColor: "#6366f1",
12079
+ strokeWidth: 2,
12080
+ opacity: 1
12081
+ },
12082
+ selected: {
12083
+ strokeColor: "#3b82f6",
12084
+ strokeWidth: 3,
12085
+ opacity: 1
12086
+ }
12087
+ },
12088
+ text: {
12089
+ font: "14px sans-serif",
12090
+ fontSize: 14,
12091
+ fontFamily: "sans-serif",
12092
+ fontWeight: "normal",
12093
+ color: "#333333",
12094
+ align: "center",
12095
+ baseline: "middle"
12096
+ },
12097
+ port: {
12098
+ default: { color: "#666666", radius: 6 },
12099
+ hover: { color: "#3b82f6", radius: 7 }
12100
+ },
12101
+ group: {
12102
+ default: {
12103
+ fillColor: "rgba(200, 200, 200, 0.2)",
12104
+ strokeColor: "#999999",
12105
+ strokeWidth: 1,
12106
+ opacity: 1
12107
+ },
12108
+ selected: {
12109
+ fillColor: "rgba(59, 130, 246, 0.1)",
12110
+ strokeColor: "#3b82f6",
12111
+ strokeWidth: 2,
12112
+ opacity: 1
12113
+ }
12114
+ }
12115
+ };
12116
+ const DARK_THEME = {
12117
+ name: "dark",
12118
+ colors: {
12119
+ background: "#1a1a1a",
12120
+ grid: "#333333",
12121
+ selection: "rgba(99, 102, 241, 0.2)",
12122
+ connectionPreview: "#6366f1"
12123
+ },
12124
+ node: {
12125
+ default: {
12126
+ fillColor: "#2d2d2d",
12127
+ strokeColor: "#555555",
12128
+ strokeWidth: 2,
12129
+ opacity: 1,
12130
+ cornerRadius: 4
12131
+ },
12132
+ hover: {
12133
+ fillColor: "#3d3d3d",
12134
+ strokeColor: "#6366f1",
12135
+ strokeWidth: 2,
12136
+ opacity: 1,
12137
+ cornerRadius: 4
12138
+ },
12139
+ selected: {
12140
+ fillColor: "#2d2d2d",
12141
+ strokeColor: "#555555",
12142
+ strokeWidth: 2,
12143
+ opacity: 1,
12144
+ cornerRadius: 4
12145
+ },
12146
+ dragging: {
12147
+ fillColor: "#404040",
12148
+ strokeColor: "#555555",
12149
+ strokeWidth: 2,
12150
+ opacity: 0.8,
12151
+ cornerRadius: 4
12152
+ }
12153
+ },
12154
+ edge: {
12155
+ default: {
12156
+ strokeColor: "#888888",
12157
+ strokeWidth: 2,
12158
+ opacity: 1
12159
+ },
12160
+ hover: {
12161
+ strokeColor: "#6366f1",
12162
+ strokeWidth: 2,
12163
+ opacity: 1
12164
+ },
12165
+ selected: {
12166
+ strokeColor: "#818cf8",
12167
+ strokeWidth: 3,
12168
+ opacity: 1
12169
+ }
12170
+ },
12171
+ text: {
12172
+ font: "14px sans-serif",
12173
+ fontSize: 14,
12174
+ fontFamily: "sans-serif",
12175
+ fontWeight: "normal",
12176
+ color: "#e0e0e0",
12177
+ align: "center",
12178
+ baseline: "middle"
12179
+ },
12180
+ port: {
12181
+ default: { color: "#888888", radius: 6 },
12182
+ hover: { color: "#6366f1", radius: 7 }
12183
+ },
12184
+ group: {
12185
+ default: {
12186
+ fillColor: "rgba(100, 100, 100, 0.2)",
12187
+ strokeColor: "#666666",
12188
+ strokeWidth: 1,
12189
+ opacity: 1
12190
+ },
12191
+ selected: {
12192
+ fillColor: "rgba(99, 102, 241, 0.2)",
12193
+ strokeColor: "#6366f1",
12194
+ strokeWidth: 2,
12195
+ opacity: 1
12196
+ }
12197
+ }
12198
+ };
12199
+ class StyleManager {
12200
+ constructor(theme) {
12201
+ this._classes = /* @__PURE__ */ new Map();
12202
+ this._builtInThemes = /* @__PURE__ */ new Map([
12203
+ ["default", DEFAULT_THEME],
12204
+ ["dark", DARK_THEME]
12205
+ ]);
12206
+ if (theme === void 0) {
12207
+ this._theme = DEFAULT_THEME;
12208
+ } else if (typeof theme === "string") {
12209
+ this._theme = this._builtInThemes.get(theme) ?? DEFAULT_THEME;
12210
+ } else {
12211
+ this._theme = theme;
10632
12212
  }
10633
12213
  }
10634
12214
  /**
@@ -10978,12 +12558,12 @@ class Serializer {
10978
12558
  label = node.label.text;
10979
12559
  }
10980
12560
  }
10981
- let icon;
12561
+ let icon2;
10982
12562
  if (node.icon) {
10983
12563
  const opts = node.icon.options;
10984
12564
  const source = typeof opts.source === "string" ? opts.source : void 0;
10985
12565
  if (source) {
10986
- icon = omitEmptyValues({
12566
+ icon2 = omitEmptyValues({
10987
12567
  source,
10988
12568
  width: opts.width,
10989
12569
  height: opts.height,
@@ -11004,7 +12584,7 @@ class Serializer {
11004
12584
  }
11005
12585
  const contentInset = node.contentInset;
11006
12586
  const hasContentInset = contentInset.top !== 0 || contentInset.right !== 0 || contentInset.bottom !== 0 || contentInset.left !== 0;
11007
- return omitEmptyValues({
12587
+ const base = omitEmptyValues({
11008
12588
  id: node.id,
11009
12589
  type: node.typeName,
11010
12590
  x: node.x,
@@ -11015,12 +12595,23 @@ class Serializer {
11015
12595
  styleClass: node.styleClass,
11016
12596
  label,
11017
12597
  labelStyleClass: typeof label === "string" ? node.label?.styleClass : void 0,
11018
- icon,
12598
+ icon: icon2,
11019
12599
  contentInset: hasContentInset ? contentInset : void 0,
11020
12600
  anchorPoints,
11021
12601
  ports: ports.length > 0 ? ports : void 0,
11022
12602
  data: Object.keys(node.data).length > 0 ? node.data : void 0
11023
12603
  });
12604
+ if (node.typeName === "composite") {
12605
+ const cn = node;
12606
+ const ext = base;
12607
+ ext.content = cn.content.serialize();
12608
+ ext.shapeType = cn.shapeType;
12609
+ if (cn.cornerRadius !== 0) ext.cornerRadius = cn.cornerRadius;
12610
+ ext.autoSize = cn.autoSize;
12611
+ if (cn.minWidth !== 0) ext.minWidth = cn.minWidth;
12612
+ if (cn.minHeight !== 0) ext.minHeight = cn.minHeight;
12613
+ }
12614
+ return base;
11024
12615
  }
11025
12616
  serializeEdge(edge) {
11026
12617
  return {
@@ -11037,6 +12628,8 @@ class Serializer {
11037
12628
  label: edge.label?.text,
11038
12629
  labelStyleClass: edge.label?.styleClass,
11039
12630
  labelOffset: edge.labelOffset !== 0 ? edge.labelOffset : void 0,
12631
+ labelPosition: edge.labelPosition !== 0.5 ? edge.labelPosition : void 0,
12632
+ labelFollowPath: edge.labelFollowPath ? true : void 0,
11040
12633
  labelBackground: edge.labelBackground,
11041
12634
  labelLineGap: edge.labelLineGap ? true : void 0,
11042
12635
  data: Object.keys(edge.data).length > 0 ? edge.data : void 0
@@ -11395,16 +12988,16 @@ class SvgExporter {
11395
12988
  const strokeOpacity = (style.strokeOpacity ?? 1) * baseOpacity;
11396
12989
  const dash = style.lineDash?.length ? ` stroke-dasharray="${style.lineDash.join(" ")}"` : "";
11397
12990
  const dashOffset = style.lineDashOffset !== void 0 ? ` stroke-dashoffset="${style.lineDashOffset}"` : "";
11398
- let shape;
12991
+ let shape2;
11399
12992
  switch (node.typeName) {
11400
12993
  case "rectangle": {
11401
12994
  const radius = this.getNodeCornerRadius(node, bounds);
11402
- shape = `<rect x="${bounds.x}" y="${bounds.y}" width="${bounds.width}" height="${bounds.height}" rx="${radius}" ry="${radius}"`;
12995
+ shape2 = `<rect x="${bounds.x}" y="${bounds.y}" width="${bounds.width}" height="${bounds.height}" rx="${radius}" ry="${radius}"`;
11403
12996
  break;
11404
12997
  }
11405
12998
  case "circle": {
11406
12999
  const center = node.getCenter();
11407
- shape = `<ellipse cx="${center.x}" cy="${center.y}" rx="${bounds.width / 2}" ry="${bounds.height / 2}"`;
13000
+ shape2 = `<ellipse cx="${center.x}" cy="${center.y}" rx="${bounds.width / 2}" ry="${bounds.height / 2}"`;
11408
13001
  break;
11409
13002
  }
11410
13003
  case "diamond": {
@@ -11417,27 +13010,61 @@ class SvgExporter {
11417
13010
  `${center.x},${center.y + hh}`,
11418
13011
  `${center.x - hw},${center.y}`
11419
13012
  ].join(" ");
11420
- shape = `<polygon points="${points}"`;
13013
+ shape2 = `<polygon points="${points}"`;
11421
13014
  break;
11422
13015
  }
11423
13016
  case "custom": {
11424
13017
  const svgPath = "getSvgPath" in node && typeof node.getSvgPath === "function" ? node.getSvgPath() : null;
11425
13018
  if (svgPath) {
11426
- shape = `<path d="${this.escapeAttribute(svgPath)}" transform="translate(${bounds.x}, ${bounds.y})"`;
13019
+ shape2 = `<path d="${this.escapeAttribute(svgPath)}" transform="translate(${bounds.x}, ${bounds.y})"`;
11427
13020
  } else {
11428
- shape = `<rect x="${bounds.x}" y="${bounds.y}" width="${bounds.width}" height="${bounds.height}"`;
13021
+ shape2 = `<rect x="${bounds.x}" y="${bounds.y}" width="${bounds.width}" height="${bounds.height}"`;
11429
13022
  }
11430
13023
  break;
11431
13024
  }
13025
+ case "composite": {
13026
+ const cn = node;
13027
+ switch (cn.shapeType) {
13028
+ case "circle": {
13029
+ const center = node.getCenter();
13030
+ shape2 = `<ellipse cx="${center.x}" cy="${center.y}" rx="${bounds.width / 2}" ry="${bounds.height / 2}"`;
13031
+ break;
13032
+ }
13033
+ case "diamond": {
13034
+ const center = node.getCenter();
13035
+ const hw = bounds.width / 2;
13036
+ const hh = bounds.height / 2;
13037
+ const points = [
13038
+ `${center.x},${center.y - hh}`,
13039
+ `${center.x + hw},${center.y}`,
13040
+ `${center.x},${center.y + hh}`,
13041
+ `${center.x - hw},${center.y}`
13042
+ ].join(" ");
13043
+ shape2 = `<polygon points="${points}"`;
13044
+ break;
13045
+ }
13046
+ default: {
13047
+ const radius = cn.cornerRadius;
13048
+ shape2 = `<rect x="${bounds.x}" y="${bounds.y}" width="${bounds.width}" height="${bounds.height}" rx="${radius}" ry="${radius}"`;
13049
+ break;
13050
+ }
13051
+ }
13052
+ const contentBounds = cn.getLabelContainerBounds(bounds);
13053
+ const contentSvg = cn.content.toSVG(contentBounds);
13054
+ return [
13055
+ `${shape2} fill="${fill}" fill-opacity="${fillOpacity}" stroke="${stroke}" stroke-width="${strokeWidth}" stroke-opacity="${strokeOpacity}"${dash}${dashOffset}/>`,
13056
+ contentSvg
13057
+ ].join("");
13058
+ }
11432
13059
  default: {
11433
- shape = `<rect x="${bounds.x}" y="${bounds.y}" width="${bounds.width}" height="${bounds.height}"`;
13060
+ shape2 = `<rect x="${bounds.x}" y="${bounds.y}" width="${bounds.width}" height="${bounds.height}"`;
11434
13061
  }
11435
13062
  }
11436
13063
  const label = this.renderNodeLabel(node, bounds);
11437
- const icon = this.renderNodeIcon(node, bounds);
13064
+ const icon2 = this.renderNodeIcon(node, bounds);
11438
13065
  return [
11439
- `${shape} fill="${fill}" fill-opacity="${fillOpacity}" stroke="${stroke}" stroke-width="${strokeWidth}" stroke-opacity="${strokeOpacity}"${dash}${dashOffset}/>`,
11440
- icon,
13066
+ `${shape2} fill="${fill}" fill-opacity="${fillOpacity}" stroke="${stroke}" stroke-width="${strokeWidth}" stroke-opacity="${strokeOpacity}"${dash}${dashOffset}/>`,
13067
+ icon2,
11441
13068
  label
11442
13069
  ].join("");
11443
13070
  }
@@ -11465,9 +13092,9 @@ class SvgExporter {
11465
13092
  return "";
11466
13093
  }
11467
13094
  const labelPoint = this.getEdgeLabelPoint(edge, edgeLabelOffset);
11468
- const text = this.renderTextLabel(edge.label.text, labelPoint, edge.label.style);
13095
+ const text2 = this.renderTextLabel(edge.label.text, labelPoint, edge.label.style);
11469
13096
  const bg = this.renderEdgeLabelBackground(edge, labelPoint);
11470
- return `${bg}${text}`;
13097
+ return `${bg}${text2}`;
11471
13098
  }
11472
13099
  renderEdgeLabelBackground(edge, point) {
11473
13100
  if (!edge.label) {
@@ -11503,12 +13130,12 @@ class SvgExporter {
11503
13130
  left: n(value.left)
11504
13131
  };
11505
13132
  }
11506
- measureTextLabel(text, style = {}, inset = 8) {
13133
+ measureTextLabel(text2, style = {}, inset = 8) {
11507
13134
  const fontSize = style.fontSize ?? 14;
11508
13135
  const fontFamily = style.fontFamily ?? "sans-serif";
11509
13136
  const fontWeight = style.fontWeight ?? "normal";
11510
13137
  const lineHeight = fontSize * 1.2;
11511
- const lines = text.split("\n");
13138
+ const lines = text2.split("\n");
11512
13139
  let maxWidth = 0;
11513
13140
  if (typeof document !== "undefined") {
11514
13141
  const canvas = document.createElement("canvas");
@@ -11621,7 +13248,7 @@ class SvgExporter {
11621
13248
  y: midpoint.y + effectiveOffset
11622
13249
  };
11623
13250
  }
11624
- renderTextLabel(text, point, style = {}) {
13251
+ renderTextLabel(text2, point, style = {}) {
11625
13252
  const fill = style.color ?? "#000000";
11626
13253
  const fontSize = style.fontSize ?? 14;
11627
13254
  const fontFamily = style.fontFamily ?? "sans-serif";
@@ -11629,10 +13256,10 @@ class SvgExporter {
11629
13256
  const opacity = style.opacity ?? 1;
11630
13257
  const anchor = style.align === "left" ? "start" : style.align === "right" ? "end" : "middle";
11631
13258
  const baseline = style.baseline === "top" ? "text-before-edge" : style.baseline === "bottom" ? "text-after-edge" : "middle";
11632
- const lines = text.split("\n");
13259
+ const lines = text2.split("\n");
11633
13260
  if (lines.length <= 1) {
11634
13261
  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(
11635
- text
13262
+ text2
11636
13263
  )}</text>`;
11637
13264
  }
11638
13265
  const lineHeight = fontSize * 1.2;
@@ -11701,16 +13328,16 @@ class SvgExporter {
11701
13328
  return Math.max(0, Math.min(rectangleRadius, bounds.width / 2, bounds.height / 2));
11702
13329
  }
11703
13330
  renderNodeIcon(node, nodeBounds) {
11704
- const icon = node.icon;
11705
- if (!icon) {
13331
+ const icon2 = node.icon;
13332
+ if (!icon2) {
11706
13333
  return "";
11707
13334
  }
11708
- const opts = icon.options;
11709
- const iconSize = icon.getSize();
13335
+ const opts = icon2.options;
13336
+ const iconSize = icon2.getSize();
11710
13337
  if (iconSize.width <= 0 || iconSize.height <= 0) {
11711
13338
  return "";
11712
13339
  }
11713
- const iconInset = icon.inset;
13340
+ const iconInset = icon2.inset;
11714
13341
  const iconBoxSize = this.getIconBoxSize(iconSize, iconInset);
11715
13342
  const iconBounds = this.getIconBounds(
11716
13343
  nodeBounds,
@@ -11922,8 +13549,8 @@ class SvgExporter {
11922
13549
  "</svg>"
11923
13550
  ].join("");
11924
13551
  }
11925
- escapeText(text) {
11926
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
13552
+ escapeText(text2) {
13553
+ return text2.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
11927
13554
  }
11928
13555
  }
11929
13556
  class AutoLayout {
@@ -12475,9 +14102,15 @@ export {
12475
14102
  AnimationManager,
12476
14103
  AutoLayout,
12477
14104
  AutoRouting,
14105
+ CContainer,
14106
+ CDivider,
14107
+ CIcon,
14108
+ CShape,
14109
+ CText,
12478
14110
  ChangeEditablePolylineControlPointsCommand,
12479
14111
  CircleNode,
12480
14112
  CompositeCommand,
14113
+ CompositeNode,
12481
14114
  ConnectionManager,
12482
14115
  ContextMenuManager,
12483
14116
  CustomShapeNode,
@@ -12522,15 +14155,21 @@ export {
12522
14155
  calculateBezierControlPoints,
12523
14156
  clamp,
12524
14157
  clonePoints,
14158
+ container,
14159
+ deserializeCComponent,
12525
14160
  distance,
12526
14161
  distanceToSegment,
12527
14162
  distributeNodes,
14163
+ divider,
12528
14164
  drawRoundedRectPath,
12529
14165
  expandBounds,
14166
+ flexLayout,
12530
14167
  generateId,
14168
+ icon,
12531
14169
  isCornerPlacement,
12532
14170
  lerp,
12533
14171
  mergeBounds,
14172
+ normalizeSides,
12534
14173
  pointInEllipse,
12535
14174
  pointInRect,
12536
14175
  rectIntersection,
@@ -12541,7 +14180,9 @@ export {
12541
14180
  resetPortIdCounter,
12542
14181
  rotatePoint,
12543
14182
  segmentRectIntersections,
14183
+ shape,
12544
14184
  snapPointToGrid,
12545
- snapToGrid
14185
+ snapToGrid,
14186
+ text
12546
14187
  };
12547
14188
  //# sourceMappingURL=papirus.js.map