@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.
- package/dist/core/DiagramRenderer.d.ts +1 -0
- package/dist/core/DiagramRenderer.d.ts.map +1 -1
- package/dist/core/HistoryManager.d.ts +1 -1
- package/dist/core/HistoryManager.d.ts.map +1 -1
- package/dist/core/InteractionManager.d.ts +3 -10
- package/dist/core/InteractionManager.d.ts.map +1 -1
- package/dist/core/history/commands.d.ts +0 -13
- package/dist/core/history/commands.d.ts.map +1 -1
- package/dist/elements/Edge.d.ts +29 -2
- package/dist/elements/Edge.d.ts.map +1 -1
- package/dist/elements/NodeImage.d.ts.map +1 -1
- package/dist/elements/composite/CComponent.d.ts +109 -0
- package/dist/elements/composite/CComponent.d.ts.map +1 -0
- package/dist/elements/composite/CContainer.d.ts +58 -0
- package/dist/elements/composite/CContainer.d.ts.map +1 -0
- package/dist/elements/composite/CDivider.d.ts +37 -0
- package/dist/elements/composite/CDivider.d.ts.map +1 -0
- package/dist/elements/composite/CIcon.d.ts +58 -0
- package/dist/elements/composite/CIcon.d.ts.map +1 -0
- package/dist/elements/composite/CShape.d.ts +51 -0
- package/dist/elements/composite/CShape.d.ts.map +1 -0
- package/dist/elements/composite/CText.d.ts +82 -0
- package/dist/elements/composite/CText.d.ts.map +1 -0
- package/dist/elements/composite/CompositeNode.d.ts +60 -0
- package/dist/elements/composite/CompositeNode.d.ts.map +1 -0
- package/dist/elements/composite/FlexLayout.d.ts +49 -0
- package/dist/elements/composite/FlexLayout.d.ts.map +1 -0
- package/dist/elements/composite/deserialize.d.ts +6 -0
- package/dist/elements/composite/deserialize.d.ts.map +1 -0
- package/dist/elements/composite/index.d.ts +26 -0
- package/dist/elements/composite/index.d.ts.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/papirus.js +1866 -338
- package/dist/papirus.js.map +1 -1
- package/dist/types.d.ts +21 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/Serializer.d.ts.map +1 -1
- package/dist/utils/SvgExporter.d.ts.map +1 -1
- package/dist/utils/svgTint.d.ts +20 -0
- package/dist/utils/svgTint.d.ts.map +1 -0
- 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
|
|
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,
|
|
2912
|
+
this._lines = this.wrapText(ctx, text2, maxWidth);
|
|
2913
2913
|
} else {
|
|
2914
|
-
this._lines =
|
|
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,
|
|
3008
|
+
wrapText(ctx, text2, maxWidth) {
|
|
3009
3009
|
const lines = [];
|
|
3010
|
-
const paragraphs =
|
|
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 -
|
|
4799
|
-
y: labelCenter.y -
|
|
4800
|
-
width:
|
|
4801
|
-
height:
|
|
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
|
|
5076
|
-
const
|
|
5077
|
-
|
|
5078
|
-
|
|
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
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
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,
|
|
5109
|
+
this.drawRoundedRect(ctx, bgX, bgY, labelWidth, labelHeight, bgRadius);
|
|
5095
5110
|
ctx.fill();
|
|
5096
5111
|
} else {
|
|
5097
|
-
ctx.fillRect(bgX, bgY,
|
|
5112
|
+
ctx.fillRect(bgX, bgY, labelWidth, labelHeight);
|
|
5098
5113
|
}
|
|
5099
5114
|
ctx.globalAlpha = labelOpacity;
|
|
5100
|
-
this._label.renderAt(ctx,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
5118
|
-
if (samples.length > 0 &&
|
|
5119
|
-
|
|
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
|
-
|
|
5143
|
+
} else {
|
|
5144
|
+
samples = path;
|
|
5125
5145
|
}
|
|
5126
|
-
return this.
|
|
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
|
|
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:
|
|
5138
|
-
y:
|
|
5158
|
+
x: point.x + this._labelOffset * Math.cos(perpAngle),
|
|
5159
|
+
y: point.y + this._labelOffset * Math.sin(perpAngle)
|
|
5139
5160
|
};
|
|
5140
5161
|
}
|
|
5141
|
-
|
|
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
|
|
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 >=
|
|
5161
|
-
const
|
|
5162
|
-
|
|
5163
|
-
x: seg.start.x +
|
|
5164
|
-
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
|
-
|
|
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,
|
|
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(
|
|
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(
|
|
5288
|
+
createTextarea(text2, screenPoint) {
|
|
5245
5289
|
const textarea = document.createElement("textarea");
|
|
5246
|
-
textarea.value =
|
|
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
|
|
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
|
-
|
|
5602
|
-
|
|
5603
|
-
|
|
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,
|
|
5947
|
+
startLabelEdit(kind, id, text2, worldPosition) {
|
|
5960
5948
|
this.labelEditor.start(
|
|
5961
5949
|
kind,
|
|
5962
5950
|
id,
|
|
5963
|
-
|
|
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
|
|
6504
|
-
|
|
6505
|
-
|
|
6506
|
-
|
|
6507
|
-
|
|
6508
|
-
|
|
6509
|
-
|
|
6510
|
-
|
|
6511
|
-
|
|
6512
|
-
|
|
6513
|
-
|
|
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
|
-
|
|
6516
|
-
return
|
|
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(
|
|
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 (!
|
|
6665
|
+
if (!icon2) {
|
|
6628
6666
|
iconEl.textContent = "";
|
|
6629
6667
|
return iconEl;
|
|
6630
6668
|
}
|
|
6631
|
-
if (typeof
|
|
6632
|
-
if (this.isSvgString(
|
|
6633
|
-
iconEl.innerHTML =
|
|
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(
|
|
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 =
|
|
6683
|
+
iconEl.textContent = icon2;
|
|
6646
6684
|
}
|
|
6647
6685
|
return iconEl;
|
|
6648
6686
|
}
|
|
6649
|
-
if (
|
|
6650
|
-
iconEl.innerHTML =
|
|
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(
|
|
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 =
|
|
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
|
|
10458
|
-
|
|
10459
|
-
|
|
10460
|
-
|
|
10461
|
-
|
|
10462
|
-
|
|
10463
|
-
|
|
10464
|
-
|
|
10465
|
-
|
|
10466
|
-
|
|
10467
|
-
|
|
10468
|
-
|
|
10469
|
-
|
|
10470
|
-
|
|
10471
|
-
|
|
10472
|
-
|
|
10473
|
-
|
|
10474
|
-
|
|
10475
|
-
|
|
10476
|
-
|
|
10477
|
-
|
|
10478
|
-
|
|
10479
|
-
|
|
10480
|
-
|
|
10481
|
-
|
|
10482
|
-
|
|
10483
|
-
|
|
10484
|
-
|
|
10485
|
-
|
|
10486
|
-
|
|
10487
|
-
|
|
10488
|
-
|
|
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
|
-
|
|
10492
|
-
|
|
10493
|
-
|
|
10494
|
-
|
|
10495
|
-
|
|
10496
|
-
|
|
10497
|
-
|
|
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
|
-
|
|
10509
|
-
|
|
10510
|
-
|
|
10511
|
-
|
|
10512
|
-
|
|
10513
|
-
|
|
10514
|
-
|
|
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
|
-
|
|
10537
|
-
|
|
10538
|
-
|
|
10539
|
-
|
|
10540
|
-
|
|
10541
|
-
|
|
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
|
-
|
|
10575
|
-
|
|
10576
|
-
|
|
10577
|
-
|
|
10578
|
-
|
|
10579
|
-
|
|
10580
|
-
|
|
10581
|
-
|
|
10582
|
-
|
|
10583
|
-
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
12879
|
+
let shape2;
|
|
11399
12880
|
switch (node.typeName) {
|
|
11400
12881
|
case "rectangle": {
|
|
11401
12882
|
const radius = this.getNodeCornerRadius(node, bounds);
|
|
11402
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12907
|
+
shape2 = `<path d="${this.escapeAttribute(svgPath)}" transform="translate(${bounds.x}, ${bounds.y})"`;
|
|
11427
12908
|
} else {
|
|
11428
|
-
|
|
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
|
-
|
|
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
|
|
12952
|
+
const icon2 = this.renderNodeIcon(node, bounds);
|
|
11438
12953
|
return [
|
|
11439
|
-
`${
|
|
11440
|
-
|
|
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
|
|
12983
|
+
const text2 = this.renderTextLabel(edge.label.text, labelPoint, edge.label.style);
|
|
11469
12984
|
const bg = this.renderEdgeLabelBackground(edge, labelPoint);
|
|
11470
|
-
return `${bg}${
|
|
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(
|
|
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 =
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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
|
|
11705
|
-
if (!
|
|
13219
|
+
const icon2 = node.icon;
|
|
13220
|
+
if (!icon2) {
|
|
11706
13221
|
return "";
|
|
11707
13222
|
}
|
|
11708
|
-
const opts =
|
|
11709
|
-
const iconSize =
|
|
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 =
|
|
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(
|
|
11926
|
-
return
|
|
13440
|
+
escapeText(text2) {
|
|
13441
|
+
return text2.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
11927
13442
|
}
|
|
11928
13443
|
}
|
|
11929
13444
|
class AutoLayout {
|
|
@@ -12475,9 +13990,14 @@ export {
|
|
|
12475
13990
|
AnimationManager,
|
|
12476
13991
|
AutoLayout,
|
|
12477
13992
|
AutoRouting,
|
|
12478
|
-
|
|
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
|