@ngroznykh/papirus 0.5.10 → 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 (42) 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/HistoryManager.d.ts +1 -1
  4. package/dist/core/HistoryManager.d.ts.map +1 -1
  5. package/dist/core/InteractionManager.d.ts +3 -10
  6. package/dist/core/InteractionManager.d.ts.map +1 -1
  7. package/dist/core/history/commands.d.ts +0 -13
  8. package/dist/core/history/commands.d.ts.map +1 -1
  9. package/dist/elements/Edge.d.ts +29 -2
  10. package/dist/elements/Edge.d.ts.map +1 -1
  11. package/dist/elements/NodeImage.d.ts.map +1 -1
  12. package/dist/elements/composite/CComponent.d.ts +109 -0
  13. package/dist/elements/composite/CComponent.d.ts.map +1 -0
  14. package/dist/elements/composite/CContainer.d.ts +58 -0
  15. package/dist/elements/composite/CContainer.d.ts.map +1 -0
  16. package/dist/elements/composite/CDivider.d.ts +37 -0
  17. package/dist/elements/composite/CDivider.d.ts.map +1 -0
  18. package/dist/elements/composite/CIcon.d.ts +58 -0
  19. package/dist/elements/composite/CIcon.d.ts.map +1 -0
  20. package/dist/elements/composite/CShape.d.ts +51 -0
  21. package/dist/elements/composite/CShape.d.ts.map +1 -0
  22. package/dist/elements/composite/CText.d.ts +82 -0
  23. package/dist/elements/composite/CText.d.ts.map +1 -0
  24. package/dist/elements/composite/CompositeNode.d.ts +60 -0
  25. package/dist/elements/composite/CompositeNode.d.ts.map +1 -0
  26. package/dist/elements/composite/FlexLayout.d.ts +49 -0
  27. package/dist/elements/composite/FlexLayout.d.ts.map +1 -0
  28. package/dist/elements/composite/deserialize.d.ts +6 -0
  29. package/dist/elements/composite/deserialize.d.ts.map +1 -0
  30. package/dist/elements/composite/index.d.ts +26 -0
  31. package/dist/elements/composite/index.d.ts.map +1 -0
  32. package/dist/index.d.ts +4 -2
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/papirus.js +1866 -338
  35. package/dist/papirus.js.map +1 -1
  36. package/dist/types.d.ts +21 -0
  37. package/dist/types.d.ts.map +1 -1
  38. package/dist/utils/Serializer.d.ts.map +1 -1
  39. package/dist/utils/SvgExporter.d.ts.map +1 -1
  40. package/dist/utils/svgTint.d.ts +20 -0
  41. package/dist/utils/svgTint.d.ts.map +1 -0
  42. 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 = "";
@@ -3125,33 +3125,6 @@ class MoveNodesCommand {
3125
3125
  }
3126
3126
  }
3127
3127
  }
3128
- class ChangeEditablePolylineControlPointsCommand {
3129
- constructor(getEdge, changes) {
3130
- this.getEdge = getEdge;
3131
- this.changes = new Map(
3132
- Array.from(changes.entries()).map(([id, v]) => [
3133
- id,
3134
- { before: clonePoints(v.before), after: clonePoints(v.after) }
3135
- ])
3136
- );
3137
- }
3138
- execute() {
3139
- for (const [id, { after }] of this.changes) {
3140
- const edge = this.getEdge(id);
3141
- if (edge?.isEditablePolyline()) {
3142
- edge.controlPoints = clonePoints(after);
3143
- }
3144
- }
3145
- }
3146
- undo() {
3147
- for (const [id, { before }] of this.changes) {
3148
- const edge = this.getEdge(id);
3149
- if (edge?.isEditablePolyline()) {
3150
- edge.controlPoints = clonePoints(before);
3151
- }
3152
- }
3153
- }
3154
- }
3155
3128
  class AddNodeCommand {
3156
3129
  constructor(nodeId, addNode, removeNode) {
3157
3130
  this.nodeId = nodeId;
@@ -4266,6 +4239,8 @@ class Edge extends Element {
4266
4239
  this._pathStrategy = this.getPathStrategy(this._type);
4267
4240
  this._lockAnchors = options.lockAnchors ?? true;
4268
4241
  this._labelOffset = options.labelOffset ?? 0;
4242
+ this._labelPosition = options.labelPosition ?? 0.5;
4243
+ this._labelFollowPath = options.labelFollowPath ?? false;
4269
4244
  this._labelBackground = options.labelBackground;
4270
4245
  this._labelLineGap = options.labelLineGap ?? false;
4271
4246
  if (options.label !== void 0) {
@@ -4359,6 +4334,31 @@ class Edge extends Element {
4359
4334
  this.markDirty();
4360
4335
  }
4361
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
+ }
4362
4362
  /**
4363
4363
  * Label background configuration
4364
4364
  */
@@ -4794,11 +4794,20 @@ class Edge extends Element {
4794
4794
  }
4795
4795
  const labelWidth = this._label.measuredWidth;
4796
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
+ }
4797
4806
  const labelRect = {
4798
- x: labelCenter.x - labelWidth / 2,
4799
- y: labelCenter.y - labelHeight / 2,
4800
- width: labelWidth,
4801
- height: labelHeight
4807
+ x: labelCenter.x - effectiveWidth / 2,
4808
+ y: labelCenter.y - effectiveHeight / 2,
4809
+ width: effectiveWidth,
4810
+ height: effectiveHeight
4802
4811
  };
4803
4812
  const segmentIntersections = [];
4804
4813
  for (let i = 0; i < polyline.length - 1; i++) {
@@ -5072,10 +5081,12 @@ class Edge extends Element {
5072
5081
  if (this._label === void 0 || this._path.length < 2) {
5073
5082
  return;
5074
5083
  }
5075
- const midpoint = this.getPathMidpoint();
5076
- const labelPosition = {
5077
- x: midpoint.x,
5078
- 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)
5079
5090
  };
5080
5091
  const labelOpacity = this._label.style.opacity ?? 1;
5081
5092
  this._label.measure(ctx);
@@ -5084,29 +5095,39 @@ class Edge extends Element {
5084
5095
  const bgColor = this._labelBackground?.color ?? "#ffffff";
5085
5096
  const bgOpacity = this._labelBackground?.opacity ?? 1;
5086
5097
  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;
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;
5091
5106
  ctx.fillStyle = bgColor;
5092
5107
  ctx.globalAlpha = bgOpacity;
5093
5108
  if (bgRadius > 0) {
5094
- this.drawRoundedRect(ctx, bgX, bgY, bgWidth, bgHeight, bgRadius);
5109
+ this.drawRoundedRect(ctx, bgX, bgY, labelWidth, labelHeight, bgRadius);
5095
5110
  ctx.fill();
5096
5111
  } else {
5097
- ctx.fillRect(bgX, bgY, bgWidth, bgHeight);
5112
+ ctx.fillRect(bgX, bgY, labelWidth, labelHeight);
5098
5113
  }
5099
5114
  ctx.globalAlpha = labelOpacity;
5100
- this._label.renderAt(ctx, labelPosition);
5115
+ this._label.renderAt(ctx, labelCenter);
5101
5116
  ctx.globalAlpha = 1;
5117
+ ctx.restore();
5102
5118
  }
5103
5119
  drawRoundedRect(ctx, x, y, width, height, radius) {
5104
5120
  drawRoundedRectPath(ctx, x, y, width, height, radius);
5105
5121
  }
5106
- 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) {
5107
5127
  const path = this._path;
5128
+ let samples;
5108
5129
  if (this._type === "bezier" && path.length >= 4) {
5109
- const samples = [];
5130
+ samples = [];
5110
5131
  const steps = 20;
5111
5132
  for (let i = 1; i + 2 < path.length; i += 3) {
5112
5133
  const p0 = path[i - 1];
@@ -5114,16 +5135,15 @@ class Edge extends Element {
5114
5135
  const p2 = path[i + 1];
5115
5136
  const p3 = path[i + 2];
5116
5137
  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));
5138
+ const st = s / steps;
5139
+ if (samples.length > 0 && st === 0) continue;
5140
+ samples.push(bezierPoint(p0, p1, p2, p3, st));
5122
5141
  }
5123
5142
  }
5124
- return this.getPolylineMidpoint(samples);
5143
+ } else {
5144
+ samples = path;
5125
5145
  }
5126
- return this.getPolylineMidpoint(path);
5146
+ return this.getPointAlongPolyline(samples, t);
5127
5147
  }
5128
5148
  /**
5129
5149
  * Get world position of label center along path.
@@ -5132,18 +5152,33 @@ class Edge extends Element {
5132
5152
  if (this._path.length < 2) {
5133
5153
  return null;
5134
5154
  }
5135
- 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;
5136
5157
  return {
5137
- x: midpoint.x,
5138
- y: midpoint.y + this._labelOffset
5158
+ x: point.x + this._labelOffset * Math.cos(perpAngle),
5159
+ y: point.y + this._labelOffset * Math.sin(perpAngle)
5139
5160
  };
5140
5161
  }
5141
- 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) {
5142
5177
  if (path.length === 0) {
5143
- return { x: 0, y: 0 };
5178
+ return { point: { x: 0, y: 0 }, angle: 0 };
5144
5179
  }
5145
5180
  if (path.length === 1) {
5146
- return path[0];
5181
+ return { point: path[0], angle: 0 };
5147
5182
  }
5148
5183
  let totalLength = 0;
5149
5184
  const segments = [];
@@ -5154,19 +5189,28 @@ class Edge extends Element {
5154
5189
  segments.push({ start, end, length });
5155
5190
  totalLength += length;
5156
5191
  }
5157
- const halfLength = totalLength / 2;
5192
+ const targetLength = totalLength * Math.max(0, Math.min(1, t));
5158
5193
  let accumulated = 0;
5159
5194
  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)
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)
5165
5200
  };
5201
+ const angle2 = Math.atan2(seg.end.y - seg.start.y, seg.end.x - seg.start.x);
5202
+ return { point, angle: angle2 };
5166
5203
  }
5167
5204
  accumulated += seg.length;
5168
5205
  }
5169
- 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
+ };
5170
5214
  }
5171
5215
  getPathStrategy(type) {
5172
5216
  switch (type) {
@@ -5214,10 +5258,10 @@ class LabelEditor {
5214
5258
  /**
5215
5259
  * Start editing a label
5216
5260
  */
5217
- start(kind, id, text, worldPosition, worldToScreen, onCommit) {
5261
+ start(kind, id, text2, worldPosition, worldToScreen, onCommit) {
5218
5262
  this.finish(true);
5219
5263
  const screenPoint = worldToScreen(worldPosition.x, worldPosition.y);
5220
- const textarea = this.createTextarea(text, screenPoint);
5264
+ const textarea = this.createTextarea(text2, screenPoint);
5221
5265
  const { cleanup } = this.setupEventHandlers(textarea, onCommit);
5222
5266
  document.body.appendChild(textarea);
5223
5267
  textarea.focus();
@@ -5241,9 +5285,9 @@ class LabelEditor {
5241
5285
  onCommit(kind, id, value);
5242
5286
  }
5243
5287
  }
5244
- createTextarea(text, screenPoint) {
5288
+ createTextarea(text2, screenPoint) {
5245
5289
  const textarea = document.createElement("textarea");
5246
- textarea.value = text;
5290
+ textarea.value = text2;
5247
5291
  textarea.rows = 1;
5248
5292
  textarea.spellcheck = false;
5249
5293
  textarea.setAttribute("aria-label", "Edit label");
@@ -5313,7 +5357,6 @@ class InteractionManager {
5313
5357
  constructor(options) {
5314
5358
  this.overlayCleanup = null;
5315
5359
  this.dragStartPositions = /* @__PURE__ */ new Map();
5316
- this.dragStartEditablePolylinePoints = /* @__PURE__ */ new Map();
5317
5360
  this.reconnectOrigins = /* @__PURE__ */ new Map();
5318
5361
  this.clipboard = null;
5319
5362
  this.pendingPropertyChanges = /* @__PURE__ */ new Map();
@@ -5385,52 +5428,6 @@ class InteractionManager {
5385
5428
  get history() {
5386
5429
  return this.historyManager;
5387
5430
  }
5388
- /**
5389
- * Records drag-start positions for node IDs moved together with the selection by the host app
5390
- * (e.g. contained nodes while Papirus only reports the container in drag events).
5391
- * Call from a `dragstart` listener after the default handler; skips IDs already captured.
5392
- * Also snapshots editable-polyline control points for edges incident to those nodes (for undo).
5393
- */
5394
- recordAdditionalDragStartPositions(nodeIds) {
5395
- for (const id of nodeIds) {
5396
- if (this.dragStartPositions.has(id)) {
5397
- continue;
5398
- }
5399
- const node = this.renderer.getNode(id);
5400
- if (node !== void 0) {
5401
- this.dragStartPositions.set(id, { x: node.x, y: node.y });
5402
- }
5403
- }
5404
- this.snapshotEditablePolylineControlPointsForEdgesTouchingNodes(nodeIds);
5405
- }
5406
- snapshotEditablePolylineControlPointsForEdgesTouchingNodes(nodeIds) {
5407
- const idSet = new Set(nodeIds);
5408
- for (const edge of this.renderer.edges.values()) {
5409
- if (!edge.hasEditableControlPoints()) {
5410
- continue;
5411
- }
5412
- if (!idSet.has(edge.from.nodeId) && !idSet.has(edge.to.nodeId)) {
5413
- continue;
5414
- }
5415
- if (this.dragStartEditablePolylinePoints.has(edge.id)) {
5416
- continue;
5417
- }
5418
- this.dragStartEditablePolylinePoints.set(edge.id, clonePoints(edge.controlPoints));
5419
- }
5420
- }
5421
- editablePolylinePointsEqual(a, b) {
5422
- if (a.length !== b.length) {
5423
- return false;
5424
- }
5425
- for (let i = 0; i < a.length; i++) {
5426
- const p = a[i];
5427
- const q = b[i];
5428
- if (p.x !== q.x || p.y !== q.y) {
5429
- return false;
5430
- }
5431
- }
5432
- return true;
5433
- }
5434
5431
  changeNodeProperties(nodeId, apply) {
5435
5432
  const node = this.renderer.getNode(nodeId);
5436
5433
  if (!node) {
@@ -5550,19 +5547,17 @@ class InteractionManager {
5550
5547
  this.dragManager.on("dragstart", (nodeIds) => {
5551
5548
  this.connectionManager.disableHover();
5552
5549
  this.dragStartPositions.clear();
5553
- this.dragStartEditablePolylinePoints.clear();
5554
5550
  for (const id of nodeIds) {
5555
5551
  const node = this.renderer.getNode(id);
5556
5552
  if (node) {
5557
5553
  this.dragStartPositions.set(id, { x: node.x, y: node.y });
5558
5554
  }
5559
5555
  }
5560
- this.snapshotEditablePolylineControlPointsForEdgesTouchingNodes(nodeIds);
5561
5556
  });
5562
- this.dragManager.on("dragend", () => {
5557
+ this.dragManager.on("dragend", (nodeIds) => {
5563
5558
  this.connectionManager.enableHover();
5564
5559
  const nodePositions = /* @__PURE__ */ new Map();
5565
- for (const id of this.dragStartPositions.keys()) {
5560
+ for (const id of nodeIds) {
5566
5561
  const node = this.renderer.getNode(id);
5567
5562
  const before = this.dragStartPositions.get(id);
5568
5563
  if (!node || !before) continue;
@@ -5571,39 +5566,10 @@ class InteractionManager {
5571
5566
  nodePositions.set(id, { before, after });
5572
5567
  }
5573
5568
  }
5574
- const polylineChanges = /* @__PURE__ */ new Map();
5575
- for (const edge of this.renderer.edges.values()) {
5576
- if (!edge.hasEditableControlPoints()) {
5577
- continue;
5578
- }
5579
- const beforePts = this.dragStartEditablePolylinePoints.get(edge.id);
5580
- if (beforePts === void 0) {
5581
- continue;
5582
- }
5583
- const afterPts = clonePoints(edge.controlPoints);
5584
- if (!this.editablePolylinePointsEqual(beforePts, afterPts)) {
5585
- polylineChanges.set(edge.id, {
5586
- before: clonePoints(beforePts),
5587
- after: afterPts
5588
- });
5589
- }
5590
- }
5591
- const parts = [];
5592
- if (polylineChanges.size > 0) {
5593
- parts.push(
5594
- new ChangeEditablePolylineControlPointsCommand(
5595
- (id) => this.renderer.getEdge(id),
5596
- polylineChanges
5597
- )
5598
- );
5599
- }
5600
5569
  if (nodePositions.size > 0) {
5601
- parts.push(new MoveNodesCommand((id) => this.renderer.getNode(id), nodePositions));
5602
- }
5603
- if (parts.length === 1) {
5604
- this.historyManager.execute(parts[0]);
5605
- } else if (parts.length > 1) {
5606
- this.historyManager.execute(new CompositeCommand(parts));
5570
+ this.historyManager.execute(
5571
+ new MoveNodesCommand((id) => this.renderer.getNode(id), nodePositions)
5572
+ );
5607
5573
  }
5608
5574
  });
5609
5575
  this.connectionManager.on("edgeReconnectStart", (edge, endpoint, original) => {
@@ -5820,6 +5786,24 @@ class InteractionManager {
5820
5786
  return;
5821
5787
  }
5822
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
+ }
5823
5807
  }
5824
5808
  handleDoubleClick(event) {
5825
5809
  if (this.renderer.blocksDiagramPointerAtScreen(event.screenX, event.screenY)) {
@@ -5847,6 +5831,10 @@ class InteractionManager {
5847
5831
  return;
5848
5832
  }
5849
5833
  if ("typeName" in hitElement) {
5834
+ if (hitElement.typeName === "composite") {
5835
+ this.startCompositeNameEdit(hitElement);
5836
+ return;
5837
+ }
5850
5838
  const editText = hitElement.label?.editableText ?? hitElement.label?.text ?? "";
5851
5839
  this.startLabelEdit("node", hitElement.id, editText, hitElement.getLabelPosition());
5852
5840
  return;
@@ -5956,16 +5944,66 @@ class InteractionManager {
5956
5944
  return false;
5957
5945
  }
5958
5946
  }
5959
- startLabelEdit(kind, id, text, worldPosition) {
5947
+ startLabelEdit(kind, id, text2, worldPosition) {
5960
5948
  this.labelEditor.start(
5961
5949
  kind,
5962
5950
  id,
5963
- text,
5951
+ text2,
5964
5952
  worldPosition,
5965
5953
  (x, y) => this.renderer.worldToScreen(x, y),
5966
5954
  (k, i, value) => this.handleLabelCommit(k, i, value)
5967
5955
  );
5968
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
+ }
5969
6007
  handleLabelCommit(kind, id, nextValue) {
5970
6008
  if (kind === "node") {
5971
6009
  this.changeNodeProperties(id, (node) => {
@@ -6500,20 +6538,20 @@ class ContextMenuManager extends EventEmitter {
6500
6538
  }
6501
6539
  }
6502
6540
  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";
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";
6514
6552
  const list = this.buildList(items, target);
6515
- container.appendChild(list);
6516
- return container;
6553
+ container2.appendChild(list);
6554
+ return container2;
6517
6555
  }
6518
6556
  buildList(items, target) {
6519
6557
  const list = document.createElement("ul");
@@ -6615,7 +6653,7 @@ class ContextMenuManager extends EventEmitter {
6615
6653
  }
6616
6654
  return list;
6617
6655
  }
6618
- createIconElement(icon) {
6656
+ createIconElement(icon2) {
6619
6657
  const iconEl = document.createElement("span");
6620
6658
  iconEl.style.width = "16px";
6621
6659
  iconEl.style.height = "16px";
@@ -6624,16 +6662,16 @@ class ContextMenuManager extends EventEmitter {
6624
6662
  iconEl.style.justifyContent = "center";
6625
6663
  iconEl.style.textAlign = "center";
6626
6664
  iconEl.style.flexShrink = "0";
6627
- if (!icon) {
6665
+ if (!icon2) {
6628
6666
  iconEl.textContent = "";
6629
6667
  return iconEl;
6630
6668
  }
6631
- if (typeof icon === "string") {
6632
- if (this.isSvgString(icon)) {
6633
- iconEl.innerHTML = icon;
6669
+ if (typeof icon2 === "string") {
6670
+ if (this.isSvgString(icon2)) {
6671
+ iconEl.innerHTML = icon2;
6634
6672
  } else if (this.options.iconToUrl) {
6635
6673
  const img = document.createElement("img");
6636
- img.src = this.options.iconToUrl(icon);
6674
+ img.src = this.options.iconToUrl(icon2);
6637
6675
  img.alt = "";
6638
6676
  img.style.width = "16px";
6639
6677
  img.style.height = "16px";
@@ -6642,15 +6680,15 @@ class ContextMenuManager extends EventEmitter {
6642
6680
  } else {
6643
6681
  iconEl.classList.add("material-symbols-outlined");
6644
6682
  iconEl.style.fontSize = "16px";
6645
- iconEl.textContent = icon;
6683
+ iconEl.textContent = icon2;
6646
6684
  }
6647
6685
  return iconEl;
6648
6686
  }
6649
- if (icon.type === "svg" || icon.type === "html") {
6650
- iconEl.innerHTML = icon.value;
6687
+ if (icon2.type === "svg" || icon2.type === "html") {
6688
+ iconEl.innerHTML = icon2.value;
6651
6689
  } else if (this.options.iconToUrl) {
6652
6690
  const img = document.createElement("img");
6653
- img.src = this.options.iconToUrl(icon.value);
6691
+ img.src = this.options.iconToUrl(icon2.value);
6654
6692
  img.alt = "";
6655
6693
  img.style.width = "16px";
6656
6694
  img.style.height = "16px";
@@ -6659,7 +6697,7 @@ class ContextMenuManager extends EventEmitter {
6659
6697
  } else {
6660
6698
  iconEl.classList.add("material-symbols-outlined");
6661
6699
  iconEl.style.fontSize = "16px";
6662
- iconEl.textContent = icon.value;
6700
+ iconEl.textContent = icon2.value;
6663
6701
  }
6664
6702
  return iconEl;
6665
6703
  }
@@ -8540,10 +8578,6 @@ class LRUCache {
8540
8578
  return this.maxSize;
8541
8579
  }
8542
8580
  }
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
8581
  function styleSetColor(style, key, color) {
8548
8582
  const hasKey = new RegExp(`${key}\\s*:`).test(style);
8549
8583
  if (hasKey) {
@@ -8594,6 +8628,18 @@ function svgToDataUrl(svg) {
8594
8628
  const encoded = encodeURIComponent(svg).replace(/%0A/g, "").replace(/%0D/g, "").replace(/%09/g, " ").replace(/%20/g, " ");
8595
8629
  return `data:image/svg+xml;utf8,${encoded}`;
8596
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);
8597
8643
  class NodeImage {
8598
8644
  constructor(options, onChange) {
8599
8645
  this._loaded = false;
@@ -10454,133 +10500,1555 @@ const ShapeFactories = {
10454
10500
  }
10455
10501
  }
10456
10502
  };
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
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?.();
10489
10535
  }
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
10536
+ }
10537
+ get fontFamily() {
10538
+ return this._fontFamily;
10539
+ }
10540
+ set fontFamily(value) {
10541
+ if (this._fontFamily !== value) {
10542
+ this._fontFamily = value;
10543
+ this._onChange?.();
10506
10544
  }
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
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?.();
10533
10553
  }
10534
10554
  }
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
10555
+ get fontStyle() {
10556
+ return this._fontStyle;
10557
+ }
10558
+ set fontStyle(value) {
10559
+ if (this._fontStyle !== value) {
10560
+ this._fontStyle = value;
10561
+ this._onChange?.();
10572
10562
  }
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
10563
+ }
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
10584
12052
  },
10585
12053
  selected: {
10586
12054
  strokeColor: "#818cf8",
@@ -10978,12 +12446,12 @@ class Serializer {
10978
12446
  label = node.label.text;
10979
12447
  }
10980
12448
  }
10981
- let icon;
12449
+ let icon2;
10982
12450
  if (node.icon) {
10983
12451
  const opts = node.icon.options;
10984
12452
  const source = typeof opts.source === "string" ? opts.source : void 0;
10985
12453
  if (source) {
10986
- icon = omitEmptyValues({
12454
+ icon2 = omitEmptyValues({
10987
12455
  source,
10988
12456
  width: opts.width,
10989
12457
  height: opts.height,
@@ -11004,7 +12472,7 @@ class Serializer {
11004
12472
  }
11005
12473
  const contentInset = node.contentInset;
11006
12474
  const hasContentInset = contentInset.top !== 0 || contentInset.right !== 0 || contentInset.bottom !== 0 || contentInset.left !== 0;
11007
- return omitEmptyValues({
12475
+ const base = omitEmptyValues({
11008
12476
  id: node.id,
11009
12477
  type: node.typeName,
11010
12478
  x: node.x,
@@ -11015,12 +12483,23 @@ class Serializer {
11015
12483
  styleClass: node.styleClass,
11016
12484
  label,
11017
12485
  labelStyleClass: typeof label === "string" ? node.label?.styleClass : void 0,
11018
- icon,
12486
+ icon: icon2,
11019
12487
  contentInset: hasContentInset ? contentInset : void 0,
11020
12488
  anchorPoints,
11021
12489
  ports: ports.length > 0 ? ports : void 0,
11022
12490
  data: Object.keys(node.data).length > 0 ? node.data : void 0
11023
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;
11024
12503
  }
11025
12504
  serializeEdge(edge) {
11026
12505
  return {
@@ -11037,6 +12516,8 @@ class Serializer {
11037
12516
  label: edge.label?.text,
11038
12517
  labelStyleClass: edge.label?.styleClass,
11039
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,
11040
12521
  labelBackground: edge.labelBackground,
11041
12522
  labelLineGap: edge.labelLineGap ? true : void 0,
11042
12523
  data: Object.keys(edge.data).length > 0 ? edge.data : void 0
@@ -11395,16 +12876,16 @@ class SvgExporter {
11395
12876
  const strokeOpacity = (style.strokeOpacity ?? 1) * baseOpacity;
11396
12877
  const dash = style.lineDash?.length ? ` stroke-dasharray="${style.lineDash.join(" ")}"` : "";
11397
12878
  const dashOffset = style.lineDashOffset !== void 0 ? ` stroke-dashoffset="${style.lineDashOffset}"` : "";
11398
- let shape;
12879
+ let shape2;
11399
12880
  switch (node.typeName) {
11400
12881
  case "rectangle": {
11401
12882
  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}"`;
12883
+ shape2 = `<rect x="${bounds.x}" y="${bounds.y}" width="${bounds.width}" height="${bounds.height}" rx="${radius}" ry="${radius}"`;
11403
12884
  break;
11404
12885
  }
11405
12886
  case "circle": {
11406
12887
  const center = node.getCenter();
11407
- 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}"`;
11408
12889
  break;
11409
12890
  }
11410
12891
  case "diamond": {
@@ -11417,27 +12898,61 @@ class SvgExporter {
11417
12898
  `${center.x},${center.y + hh}`,
11418
12899
  `${center.x - hw},${center.y}`
11419
12900
  ].join(" ");
11420
- shape = `<polygon points="${points}"`;
12901
+ shape2 = `<polygon points="${points}"`;
11421
12902
  break;
11422
12903
  }
11423
12904
  case "custom": {
11424
12905
  const svgPath = "getSvgPath" in node && typeof node.getSvgPath === "function" ? node.getSvgPath() : null;
11425
12906
  if (svgPath) {
11426
- 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})"`;
11427
12908
  } else {
11428
- 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}"`;
11429
12910
  }
11430
12911
  break;
11431
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
+ }
11432
12947
  default: {
11433
- 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}"`;
11434
12949
  }
11435
12950
  }
11436
12951
  const label = this.renderNodeLabel(node, bounds);
11437
- const icon = this.renderNodeIcon(node, bounds);
12952
+ const icon2 = this.renderNodeIcon(node, bounds);
11438
12953
  return [
11439
- `${shape} fill="${fill}" fill-opacity="${fillOpacity}" stroke="${stroke}" stroke-width="${strokeWidth}" stroke-opacity="${strokeOpacity}"${dash}${dashOffset}/>`,
11440
- icon,
12954
+ `${shape2} fill="${fill}" fill-opacity="${fillOpacity}" stroke="${stroke}" stroke-width="${strokeWidth}" stroke-opacity="${strokeOpacity}"${dash}${dashOffset}/>`,
12955
+ icon2,
11441
12956
  label
11442
12957
  ].join("");
11443
12958
  }
@@ -11465,9 +12980,9 @@ class SvgExporter {
11465
12980
  return "";
11466
12981
  }
11467
12982
  const labelPoint = this.getEdgeLabelPoint(edge, edgeLabelOffset);
11468
- const text = this.renderTextLabel(edge.label.text, labelPoint, edge.label.style);
12983
+ const text2 = this.renderTextLabel(edge.label.text, labelPoint, edge.label.style);
11469
12984
  const bg = this.renderEdgeLabelBackground(edge, labelPoint);
11470
- return `${bg}${text}`;
12985
+ return `${bg}${text2}`;
11471
12986
  }
11472
12987
  renderEdgeLabelBackground(edge, point) {
11473
12988
  if (!edge.label) {
@@ -11503,12 +13018,12 @@ class SvgExporter {
11503
13018
  left: n(value.left)
11504
13019
  };
11505
13020
  }
11506
- measureTextLabel(text, style = {}, inset = 8) {
13021
+ measureTextLabel(text2, style = {}, inset = 8) {
11507
13022
  const fontSize = style.fontSize ?? 14;
11508
13023
  const fontFamily = style.fontFamily ?? "sans-serif";
11509
13024
  const fontWeight = style.fontWeight ?? "normal";
11510
13025
  const lineHeight = fontSize * 1.2;
11511
- const lines = text.split("\n");
13026
+ const lines = text2.split("\n");
11512
13027
  let maxWidth = 0;
11513
13028
  if (typeof document !== "undefined") {
11514
13029
  const canvas = document.createElement("canvas");
@@ -11621,7 +13136,7 @@ class SvgExporter {
11621
13136
  y: midpoint.y + effectiveOffset
11622
13137
  };
11623
13138
  }
11624
- renderTextLabel(text, point, style = {}) {
13139
+ renderTextLabel(text2, point, style = {}) {
11625
13140
  const fill = style.color ?? "#000000";
11626
13141
  const fontSize = style.fontSize ?? 14;
11627
13142
  const fontFamily = style.fontFamily ?? "sans-serif";
@@ -11629,10 +13144,10 @@ class SvgExporter {
11629
13144
  const opacity = style.opacity ?? 1;
11630
13145
  const anchor = style.align === "left" ? "start" : style.align === "right" ? "end" : "middle";
11631
13146
  const baseline = style.baseline === "top" ? "text-before-edge" : style.baseline === "bottom" ? "text-after-edge" : "middle";
11632
- const lines = text.split("\n");
13147
+ const lines = text2.split("\n");
11633
13148
  if (lines.length <= 1) {
11634
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(
11635
- text
13150
+ text2
11636
13151
  )}</text>`;
11637
13152
  }
11638
13153
  const lineHeight = fontSize * 1.2;
@@ -11701,16 +13216,16 @@ class SvgExporter {
11701
13216
  return Math.max(0, Math.min(rectangleRadius, bounds.width / 2, bounds.height / 2));
11702
13217
  }
11703
13218
  renderNodeIcon(node, nodeBounds) {
11704
- const icon = node.icon;
11705
- if (!icon) {
13219
+ const icon2 = node.icon;
13220
+ if (!icon2) {
11706
13221
  return "";
11707
13222
  }
11708
- const opts = icon.options;
11709
- const iconSize = icon.getSize();
13223
+ const opts = icon2.options;
13224
+ const iconSize = icon2.getSize();
11710
13225
  if (iconSize.width <= 0 || iconSize.height <= 0) {
11711
13226
  return "";
11712
13227
  }
11713
- const iconInset = icon.inset;
13228
+ const iconInset = icon2.inset;
11714
13229
  const iconBoxSize = this.getIconBoxSize(iconSize, iconInset);
11715
13230
  const iconBounds = this.getIconBounds(
11716
13231
  nodeBounds,
@@ -11922,8 +13437,8 @@ class SvgExporter {
11922
13437
  "</svg>"
11923
13438
  ].join("");
11924
13439
  }
11925
- escapeText(text) {
11926
- 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;");
11927
13442
  }
11928
13443
  }
11929
13444
  class AutoLayout {
@@ -12475,9 +13990,14 @@ export {
12475
13990
  AnimationManager,
12476
13991
  AutoLayout,
12477
13992
  AutoRouting,
12478
- ChangeEditablePolylineControlPointsCommand,
13993
+ CContainer,
13994
+ CDivider,
13995
+ CIcon,
13996
+ CShape,
13997
+ CText,
12479
13998
  CircleNode,
12480
13999
  CompositeCommand,
14000
+ CompositeNode,
12481
14001
  ConnectionManager,
12482
14002
  ContextMenuManager,
12483
14003
  CustomShapeNode,
@@ -12522,15 +14042,21 @@ export {
12522
14042
  calculateBezierControlPoints,
12523
14043
  clamp,
12524
14044
  clonePoints,
14045
+ container,
14046
+ deserializeCComponent,
12525
14047
  distance,
12526
14048
  distanceToSegment,
12527
14049
  distributeNodes,
14050
+ divider,
12528
14051
  drawRoundedRectPath,
12529
14052
  expandBounds,
14053
+ flexLayout,
12530
14054
  generateId,
14055
+ icon,
12531
14056
  isCornerPlacement,
12532
14057
  lerp,
12533
14058
  mergeBounds,
14059
+ normalizeSides,
12534
14060
  pointInEllipse,
12535
14061
  pointInRect,
12536
14062
  rectIntersection,
@@ -12541,7 +14067,9 @@ export {
12541
14067
  resetPortIdCounter,
12542
14068
  rotatePoint,
12543
14069
  segmentRectIntersections,
14070
+ shape,
12544
14071
  snapPointToGrid,
12545
- snapToGrid
14072
+ snapToGrid,
14073
+ text
12546
14074
  };
12547
14075
  //# sourceMappingURL=papirus.js.map