@spectratools/graphic-designer-cli 0.3.2 → 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/README.md +32 -2
- package/dist/cli.js +1252 -517
- package/dist/index.d.ts +105 -5
- package/dist/index.js +1271 -513
- package/dist/qa.d.ts +14 -3
- package/dist/qa.js +361 -50
- package/dist/renderer.d.ts +1 -1
- package/dist/renderer.js +909 -431
- package/dist/{spec.schema-DhAI-tE8.d.ts → spec.schema-Dm_wOLTd.d.ts} +2028 -599
- package/dist/spec.schema.d.ts +1 -1
- package/dist/spec.schema.js +158 -10
- package/package.json +1 -1
package/dist/renderer.js
CHANGED
|
@@ -86,6 +86,485 @@ function loadFonts() {
|
|
|
86
86
|
// src/layout/elk.ts
|
|
87
87
|
import ELK from "elkjs";
|
|
88
88
|
|
|
89
|
+
// src/primitives/shapes.ts
|
|
90
|
+
function roundRectPath(ctx, rect, radius) {
|
|
91
|
+
const r = Math.max(0, Math.min(radius, rect.width / 2, rect.height / 2));
|
|
92
|
+
const right = rect.x + rect.width;
|
|
93
|
+
const bottom = rect.y + rect.height;
|
|
94
|
+
ctx.beginPath();
|
|
95
|
+
ctx.moveTo(rect.x + r, rect.y);
|
|
96
|
+
ctx.lineTo(right - r, rect.y);
|
|
97
|
+
ctx.quadraticCurveTo(right, rect.y, right, rect.y + r);
|
|
98
|
+
ctx.lineTo(right, bottom - r);
|
|
99
|
+
ctx.quadraticCurveTo(right, bottom, right - r, bottom);
|
|
100
|
+
ctx.lineTo(rect.x + r, bottom);
|
|
101
|
+
ctx.quadraticCurveTo(rect.x, bottom, rect.x, bottom - r);
|
|
102
|
+
ctx.lineTo(rect.x, rect.y + r);
|
|
103
|
+
ctx.quadraticCurveTo(rect.x, rect.y, rect.x + r, rect.y);
|
|
104
|
+
ctx.closePath();
|
|
105
|
+
}
|
|
106
|
+
function fillAndStroke(ctx, fill, stroke) {
|
|
107
|
+
ctx.fillStyle = fill;
|
|
108
|
+
ctx.fill();
|
|
109
|
+
if (stroke) {
|
|
110
|
+
ctx.strokeStyle = stroke;
|
|
111
|
+
ctx.stroke();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function drawRoundedRect(ctx, rect, radius, fill, stroke) {
|
|
115
|
+
roundRectPath(ctx, rect, radius);
|
|
116
|
+
fillAndStroke(ctx, fill, stroke);
|
|
117
|
+
}
|
|
118
|
+
function drawCircle(ctx, center, radius, fill, stroke) {
|
|
119
|
+
ctx.beginPath();
|
|
120
|
+
ctx.arc(center.x, center.y, Math.max(0, radius), 0, Math.PI * 2);
|
|
121
|
+
ctx.closePath();
|
|
122
|
+
fillAndStroke(ctx, fill, stroke);
|
|
123
|
+
}
|
|
124
|
+
function drawDiamond(ctx, bounds, fill, stroke) {
|
|
125
|
+
const cx = bounds.x + bounds.width / 2;
|
|
126
|
+
const cy = bounds.y + bounds.height / 2;
|
|
127
|
+
ctx.beginPath();
|
|
128
|
+
ctx.moveTo(cx, bounds.y);
|
|
129
|
+
ctx.lineTo(bounds.x + bounds.width, cy);
|
|
130
|
+
ctx.lineTo(cx, bounds.y + bounds.height);
|
|
131
|
+
ctx.lineTo(bounds.x, cy);
|
|
132
|
+
ctx.closePath();
|
|
133
|
+
fillAndStroke(ctx, fill, stroke);
|
|
134
|
+
}
|
|
135
|
+
function drawPill(ctx, bounds, fill, stroke) {
|
|
136
|
+
drawRoundedRect(ctx, bounds, Math.min(bounds.width, bounds.height) / 2, fill, stroke);
|
|
137
|
+
}
|
|
138
|
+
function drawEllipse(ctx, bounds, fill, stroke) {
|
|
139
|
+
const cx = bounds.x + bounds.width / 2;
|
|
140
|
+
const cy = bounds.y + bounds.height / 2;
|
|
141
|
+
ctx.beginPath();
|
|
142
|
+
ctx.ellipse(
|
|
143
|
+
cx,
|
|
144
|
+
cy,
|
|
145
|
+
Math.max(0, bounds.width / 2),
|
|
146
|
+
Math.max(0, bounds.height / 2),
|
|
147
|
+
0,
|
|
148
|
+
0,
|
|
149
|
+
Math.PI * 2
|
|
150
|
+
);
|
|
151
|
+
ctx.closePath();
|
|
152
|
+
fillAndStroke(ctx, fill, stroke);
|
|
153
|
+
}
|
|
154
|
+
function drawCylinder(ctx, bounds, fill, stroke) {
|
|
155
|
+
const rx = Math.max(2, bounds.width / 2);
|
|
156
|
+
const ry = Math.max(2, Math.min(bounds.height * 0.18, 16));
|
|
157
|
+
const cx = bounds.x + bounds.width / 2;
|
|
158
|
+
const topCy = bounds.y + ry;
|
|
159
|
+
const bottomCy = bounds.y + bounds.height - ry;
|
|
160
|
+
ctx.beginPath();
|
|
161
|
+
ctx.moveTo(bounds.x, topCy);
|
|
162
|
+
ctx.ellipse(cx, topCy, rx, ry, 0, Math.PI, 0, true);
|
|
163
|
+
ctx.lineTo(bounds.x + bounds.width, bottomCy);
|
|
164
|
+
ctx.ellipse(cx, bottomCy, rx, ry, 0, 0, Math.PI, false);
|
|
165
|
+
ctx.closePath();
|
|
166
|
+
fillAndStroke(ctx, fill, stroke);
|
|
167
|
+
if (stroke) {
|
|
168
|
+
ctx.beginPath();
|
|
169
|
+
ctx.ellipse(cx, topCy, rx, ry, 0, 0, Math.PI * 2);
|
|
170
|
+
ctx.closePath();
|
|
171
|
+
ctx.strokeStyle = stroke;
|
|
172
|
+
ctx.stroke();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function drawParallelogram(ctx, bounds, fill, stroke, skew) {
|
|
176
|
+
const maxSkew = bounds.width * 0.45;
|
|
177
|
+
const skewX = Math.max(-maxSkew, Math.min(maxSkew, skew ?? bounds.width * 0.18));
|
|
178
|
+
ctx.beginPath();
|
|
179
|
+
ctx.moveTo(bounds.x + skewX, bounds.y);
|
|
180
|
+
ctx.lineTo(bounds.x + bounds.width, bounds.y);
|
|
181
|
+
ctx.lineTo(bounds.x + bounds.width - skewX, bounds.y + bounds.height);
|
|
182
|
+
ctx.lineTo(bounds.x, bounds.y + bounds.height);
|
|
183
|
+
ctx.closePath();
|
|
184
|
+
fillAndStroke(ctx, fill, stroke);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// src/primitives/text.ts
|
|
188
|
+
var SUPPORTED_FONT_FAMILIES = /* @__PURE__ */ new Set(["Inter", "JetBrains Mono", "Space Grotesk"]);
|
|
189
|
+
function resolveFont(requested, role) {
|
|
190
|
+
if (SUPPORTED_FONT_FAMILIES.has(requested)) {
|
|
191
|
+
return requested;
|
|
192
|
+
}
|
|
193
|
+
if (role === "mono" || /mono|code|terminal|console/iu.test(requested)) {
|
|
194
|
+
return "JetBrains Mono";
|
|
195
|
+
}
|
|
196
|
+
if (role === "heading" || /display|grotesk|headline/iu.test(requested)) {
|
|
197
|
+
return "Space Grotesk";
|
|
198
|
+
}
|
|
199
|
+
return "Inter";
|
|
200
|
+
}
|
|
201
|
+
function applyFont(ctx, options) {
|
|
202
|
+
ctx.font = `${options.weight} ${options.size}px ${options.family}`;
|
|
203
|
+
}
|
|
204
|
+
function wrapText(ctx, text, maxWidth, maxLines) {
|
|
205
|
+
const trimmed = text.trim();
|
|
206
|
+
if (!trimmed) {
|
|
207
|
+
return { lines: [], truncated: false };
|
|
208
|
+
}
|
|
209
|
+
const words = trimmed.split(/\s+/u);
|
|
210
|
+
const lines = [];
|
|
211
|
+
let current = "";
|
|
212
|
+
for (const word of words) {
|
|
213
|
+
const trial = current.length > 0 ? `${current} ${word}` : word;
|
|
214
|
+
if (ctx.measureText(trial).width <= maxWidth) {
|
|
215
|
+
current = trial;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (current.length > 0) {
|
|
219
|
+
lines.push(current);
|
|
220
|
+
current = word;
|
|
221
|
+
} else {
|
|
222
|
+
lines.push(word);
|
|
223
|
+
current = "";
|
|
224
|
+
}
|
|
225
|
+
if (lines.length >= maxLines) {
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (lines.length < maxLines && current.length > 0) {
|
|
230
|
+
lines.push(current);
|
|
231
|
+
}
|
|
232
|
+
const wasTruncated = lines.length >= maxLines && words.join(" ") !== lines.join(" ");
|
|
233
|
+
if (!wasTruncated) {
|
|
234
|
+
return { lines, truncated: false };
|
|
235
|
+
}
|
|
236
|
+
const lastIndex = lines.length - 1;
|
|
237
|
+
let truncatedLine = `${lines[lastIndex]}\u2026`;
|
|
238
|
+
while (truncatedLine.length > 1 && ctx.measureText(truncatedLine).width > maxWidth) {
|
|
239
|
+
truncatedLine = `${truncatedLine.slice(0, -2)}\u2026`;
|
|
240
|
+
}
|
|
241
|
+
lines[lastIndex] = truncatedLine;
|
|
242
|
+
return { lines, truncated: true };
|
|
243
|
+
}
|
|
244
|
+
function drawTextBlock(ctx, options) {
|
|
245
|
+
applyFont(ctx, { size: options.fontSize, weight: options.fontWeight, family: options.family });
|
|
246
|
+
const wrapped = wrapText(ctx, options.text, options.maxWidth, options.maxLines);
|
|
247
|
+
ctx.fillStyle = options.color;
|
|
248
|
+
for (const [index, line] of wrapped.lines.entries()) {
|
|
249
|
+
ctx.fillText(line, options.x, options.y + index * options.lineHeight);
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
height: wrapped.lines.length * options.lineHeight,
|
|
253
|
+
truncated: wrapped.truncated
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
function drawTextLabel(ctx, text, position, options) {
|
|
257
|
+
applyFont(ctx, { size: options.fontSize, weight: 600, family: options.fontFamily });
|
|
258
|
+
const textWidth = Math.ceil(ctx.measureText(text).width);
|
|
259
|
+
const rect = {
|
|
260
|
+
x: Math.round(position.x - (textWidth + options.padding * 2) / 2),
|
|
261
|
+
y: Math.round(position.y - (options.fontSize + options.padding * 2) / 2),
|
|
262
|
+
width: textWidth + options.padding * 2,
|
|
263
|
+
height: options.fontSize + options.padding * 2
|
|
264
|
+
};
|
|
265
|
+
drawRoundedRect(ctx, rect, options.borderRadius, options.backgroundColor);
|
|
266
|
+
ctx.fillStyle = options.color;
|
|
267
|
+
ctx.fillText(text, rect.x + options.padding, rect.y + rect.height - options.padding);
|
|
268
|
+
return rect;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// src/utils/color.ts
|
|
272
|
+
function parseChannel(hex, offset) {
|
|
273
|
+
return Number.parseInt(hex.slice(offset, offset + 2), 16);
|
|
274
|
+
}
|
|
275
|
+
function parseHexColor(hexColor) {
|
|
276
|
+
const normalized = hexColor.startsWith("#") ? hexColor.slice(1) : hexColor;
|
|
277
|
+
if (normalized.length !== 6 && normalized.length !== 8) {
|
|
278
|
+
throw new Error(`Unsupported color format: ${hexColor}`);
|
|
279
|
+
}
|
|
280
|
+
return {
|
|
281
|
+
r: parseChannel(normalized, 0),
|
|
282
|
+
g: parseChannel(normalized, 2),
|
|
283
|
+
b: parseChannel(normalized, 4)
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
var rgbaRegex = /^rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*([01](?:\.\d+)?|0?\.\d+)\s*)?\)$/;
|
|
287
|
+
var hexColorRegex = /^#(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
|
|
288
|
+
function toHex(n) {
|
|
289
|
+
return n.toString(16).padStart(2, "0");
|
|
290
|
+
}
|
|
291
|
+
function parseRgbaToHex(color) {
|
|
292
|
+
const match = rgbaRegex.exec(color);
|
|
293
|
+
if (!match) {
|
|
294
|
+
throw new Error(`Invalid rgb/rgba color: ${color}`);
|
|
295
|
+
}
|
|
296
|
+
const r = Number.parseInt(match[1], 10);
|
|
297
|
+
const g = Number.parseInt(match[2], 10);
|
|
298
|
+
const b = Number.parseInt(match[3], 10);
|
|
299
|
+
if (r > 255 || g > 255 || b > 255) {
|
|
300
|
+
throw new Error(`RGB channel values must be 0-255, got: ${color}`);
|
|
301
|
+
}
|
|
302
|
+
if (match[4] !== void 0) {
|
|
303
|
+
const a = Number.parseFloat(match[4]);
|
|
304
|
+
if (a < 0 || a > 1) {
|
|
305
|
+
throw new Error(`Alpha value must be 0-1, got: ${a}`);
|
|
306
|
+
}
|
|
307
|
+
const alphaByte = Math.round(a * 255);
|
|
308
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}${toHex(alphaByte)}`;
|
|
309
|
+
}
|
|
310
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
|
311
|
+
}
|
|
312
|
+
function isRgbaColor(color) {
|
|
313
|
+
return rgbaRegex.test(color);
|
|
314
|
+
}
|
|
315
|
+
function isHexColor(color) {
|
|
316
|
+
return hexColorRegex.test(color);
|
|
317
|
+
}
|
|
318
|
+
function normalizeColor(color) {
|
|
319
|
+
if (isHexColor(color)) {
|
|
320
|
+
return color;
|
|
321
|
+
}
|
|
322
|
+
if (isRgbaColor(color)) {
|
|
323
|
+
return parseRgbaToHex(color);
|
|
324
|
+
}
|
|
325
|
+
throw new Error(`Expected #RRGGBB, #RRGGBBAA, rgb(), or rgba() color, got: ${color}`);
|
|
326
|
+
}
|
|
327
|
+
function srgbToLinear(channel) {
|
|
328
|
+
const normalized = channel / 255;
|
|
329
|
+
if (normalized <= 0.03928) {
|
|
330
|
+
return normalized / 12.92;
|
|
331
|
+
}
|
|
332
|
+
return ((normalized + 0.055) / 1.055) ** 2.4;
|
|
333
|
+
}
|
|
334
|
+
function relativeLuminance(hexColor) {
|
|
335
|
+
const normalized = isRgbaColor(hexColor) ? parseRgbaToHex(hexColor) : hexColor;
|
|
336
|
+
const rgb = parseHexColor(normalized);
|
|
337
|
+
const r = srgbToLinear(rgb.r);
|
|
338
|
+
const g = srgbToLinear(rgb.g);
|
|
339
|
+
const b = srgbToLinear(rgb.b);
|
|
340
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
341
|
+
}
|
|
342
|
+
function withAlpha(hexColor, opacity) {
|
|
343
|
+
const rgb = parseHexColor(hexColor);
|
|
344
|
+
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${opacity})`;
|
|
345
|
+
}
|
|
346
|
+
function blendColorWithOpacity(foreground, background, opacity) {
|
|
347
|
+
const fg = parseHexColor(foreground);
|
|
348
|
+
const bg = parseHexColor(background);
|
|
349
|
+
const r = Math.round(fg.r * opacity + bg.r * (1 - opacity));
|
|
350
|
+
const g = Math.round(fg.g * opacity + bg.g * (1 - opacity));
|
|
351
|
+
const b = Math.round(fg.b * opacity + bg.b * (1 - opacity));
|
|
352
|
+
return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`.toUpperCase();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// src/renderers/flow-node.ts
|
|
356
|
+
var BADGE_FONT_SIZE = 10;
|
|
357
|
+
var BADGE_FONT_WEIGHT = 600;
|
|
358
|
+
var BADGE_LETTER_SPACING = 1;
|
|
359
|
+
var BADGE_PADDING_X = 8;
|
|
360
|
+
var BADGE_PADDING_Y = 3;
|
|
361
|
+
var BADGE_BORDER_RADIUS = 12;
|
|
362
|
+
var BADGE_DEFAULT_COLOR = "#FFFFFF";
|
|
363
|
+
var BADGE_PILL_HEIGHT = BADGE_FONT_SIZE + BADGE_PADDING_Y * 2;
|
|
364
|
+
var BADGE_INSIDE_TOP_EXTRA = BADGE_PILL_HEIGHT + 6;
|
|
365
|
+
function drawNodeShape(ctx, shape, bounds, fill, stroke, cornerRadius) {
|
|
366
|
+
switch (shape) {
|
|
367
|
+
case "box":
|
|
368
|
+
drawRoundedRect(ctx, bounds, 0, fill, stroke);
|
|
369
|
+
break;
|
|
370
|
+
case "rounded-box":
|
|
371
|
+
drawRoundedRect(ctx, bounds, cornerRadius, fill, stroke);
|
|
372
|
+
break;
|
|
373
|
+
case "diamond":
|
|
374
|
+
drawDiamond(ctx, bounds, fill, stroke);
|
|
375
|
+
break;
|
|
376
|
+
case "circle": {
|
|
377
|
+
const radius = Math.min(bounds.width, bounds.height) / 2;
|
|
378
|
+
drawCircle(
|
|
379
|
+
ctx,
|
|
380
|
+
{ x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 },
|
|
381
|
+
radius,
|
|
382
|
+
fill,
|
|
383
|
+
stroke
|
|
384
|
+
);
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
case "pill":
|
|
388
|
+
drawPill(ctx, bounds, fill, stroke);
|
|
389
|
+
break;
|
|
390
|
+
case "cylinder":
|
|
391
|
+
drawCylinder(ctx, bounds, fill, stroke);
|
|
392
|
+
break;
|
|
393
|
+
case "parallelogram":
|
|
394
|
+
drawParallelogram(ctx, bounds, fill, stroke);
|
|
395
|
+
break;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
function measureSpacedText(ctx, text, letterSpacing) {
|
|
399
|
+
const base = ctx.measureText(text).width;
|
|
400
|
+
const extraChars = [...text].length - 1;
|
|
401
|
+
return extraChars > 0 ? base + extraChars * letterSpacing : base;
|
|
402
|
+
}
|
|
403
|
+
function drawSpacedText(ctx, text, centerX, centerY, letterSpacing) {
|
|
404
|
+
const chars = [...text];
|
|
405
|
+
if (chars.length === 0) return;
|
|
406
|
+
const totalWidth = measureSpacedText(ctx, text, letterSpacing);
|
|
407
|
+
let cursorX = centerX - totalWidth / 2;
|
|
408
|
+
ctx.textAlign = "left";
|
|
409
|
+
for (let i = 0; i < chars.length; i++) {
|
|
410
|
+
ctx.fillText(chars[i], cursorX, centerY);
|
|
411
|
+
cursorX += ctx.measureText(chars[i]).width + (i < chars.length - 1 ? letterSpacing : 0);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
function renderBadgePill(ctx, centerX, centerY, text, textColor, background, monoFont) {
|
|
415
|
+
ctx.save();
|
|
416
|
+
applyFont(ctx, { size: BADGE_FONT_SIZE, weight: BADGE_FONT_WEIGHT, family: monoFont });
|
|
417
|
+
const textWidth = measureSpacedText(ctx, text, BADGE_LETTER_SPACING);
|
|
418
|
+
const pillWidth = textWidth + BADGE_PADDING_X * 2;
|
|
419
|
+
const pillHeight = BADGE_PILL_HEIGHT;
|
|
420
|
+
const pillX = centerX - pillWidth / 2;
|
|
421
|
+
const pillY = centerY - pillHeight / 2;
|
|
422
|
+
ctx.fillStyle = background;
|
|
423
|
+
ctx.beginPath();
|
|
424
|
+
ctx.roundRect(pillX, pillY, pillWidth, pillHeight, BADGE_BORDER_RADIUS);
|
|
425
|
+
ctx.fill();
|
|
426
|
+
ctx.fillStyle = textColor;
|
|
427
|
+
ctx.textBaseline = "middle";
|
|
428
|
+
applyFont(ctx, { size: BADGE_FONT_SIZE, weight: BADGE_FONT_WEIGHT, family: monoFont });
|
|
429
|
+
drawSpacedText(ctx, text, centerX, centerY, BADGE_LETTER_SPACING);
|
|
430
|
+
ctx.restore();
|
|
431
|
+
return pillWidth;
|
|
432
|
+
}
|
|
433
|
+
function renderFlowNode(ctx, node, bounds, theme) {
|
|
434
|
+
const fillColor = node.color ?? theme.surfaceElevated;
|
|
435
|
+
const borderColor = node.borderColor ?? theme.border;
|
|
436
|
+
const borderWidth = node.borderWidth ?? 2;
|
|
437
|
+
const cornerRadius = node.cornerRadius ?? 16;
|
|
438
|
+
const labelColor = node.labelColor ?? theme.text;
|
|
439
|
+
const sublabelColor = node.sublabelColor ?? theme.textMuted;
|
|
440
|
+
const labelFontSize = node.labelFontSize ?? 20;
|
|
441
|
+
const fillOpacity = node.fillOpacity ?? 1;
|
|
442
|
+
const hasBadge = !!node.badgeText;
|
|
443
|
+
const badgePosition = node.badgePosition ?? "inside-top";
|
|
444
|
+
const badgeColor = node.badgeColor ?? BADGE_DEFAULT_COLOR;
|
|
445
|
+
const badgeBackground = node.badgeBackground ?? borderColor ?? theme.accent;
|
|
446
|
+
ctx.save();
|
|
447
|
+
ctx.lineWidth = borderWidth;
|
|
448
|
+
if (node.shadow) {
|
|
449
|
+
const shadowColor = node.shadow.color ?? borderColor ?? theme.accent;
|
|
450
|
+
ctx.shadowColor = withAlpha(shadowColor, node.shadow.opacity);
|
|
451
|
+
ctx.shadowBlur = node.shadow.blur;
|
|
452
|
+
ctx.shadowOffsetX = node.shadow.offsetX;
|
|
453
|
+
ctx.shadowOffsetY = node.shadow.offsetY;
|
|
454
|
+
}
|
|
455
|
+
if (fillOpacity < 1) {
|
|
456
|
+
ctx.globalAlpha = node.opacity * fillOpacity;
|
|
457
|
+
drawNodeShape(ctx, node.shape, bounds, fillColor, void 0, cornerRadius);
|
|
458
|
+
if (node.shadow) {
|
|
459
|
+
ctx.shadowColor = "transparent";
|
|
460
|
+
ctx.shadowBlur = 0;
|
|
461
|
+
ctx.shadowOffsetX = 0;
|
|
462
|
+
ctx.shadowOffsetY = 0;
|
|
463
|
+
}
|
|
464
|
+
ctx.globalAlpha = node.opacity;
|
|
465
|
+
drawNodeShape(ctx, node.shape, bounds, "rgba(0,0,0,0)", borderColor, cornerRadius);
|
|
466
|
+
} else {
|
|
467
|
+
ctx.globalAlpha = node.opacity;
|
|
468
|
+
drawNodeShape(ctx, node.shape, bounds, fillColor, borderColor, cornerRadius);
|
|
469
|
+
}
|
|
470
|
+
if (node.shadow) {
|
|
471
|
+
ctx.shadowColor = "transparent";
|
|
472
|
+
ctx.shadowBlur = 0;
|
|
473
|
+
ctx.shadowOffsetX = 0;
|
|
474
|
+
ctx.shadowOffsetY = 0;
|
|
475
|
+
}
|
|
476
|
+
const headingFont = resolveFont(theme.fonts.heading, "heading");
|
|
477
|
+
const bodyFont = resolveFont(theme.fonts.body, "body");
|
|
478
|
+
const monoFont = resolveFont(theme.fonts.mono, "mono");
|
|
479
|
+
const centerX = bounds.x + bounds.width / 2;
|
|
480
|
+
const centerY = bounds.y + bounds.height / 2;
|
|
481
|
+
const insideTopShift = hasBadge && badgePosition === "inside-top" ? BADGE_INSIDE_TOP_EXTRA / 2 : 0;
|
|
482
|
+
const sublabelFontSize = Math.max(12, Math.round(labelFontSize * 0.68));
|
|
483
|
+
const sublabel2FontSize = node.sublabel2FontSize ?? 11;
|
|
484
|
+
const sublabel2Color = node.sublabel2Color ?? sublabelColor;
|
|
485
|
+
const lineCount = node.sublabel2 ? 3 : node.sublabel ? 2 : 1;
|
|
486
|
+
const labelToSublabelGap = Math.max(20, sublabelFontSize + 6);
|
|
487
|
+
const sublabelToSublabel2Gap = sublabel2FontSize + 4;
|
|
488
|
+
let textBlockHeight;
|
|
489
|
+
if (lineCount === 1) {
|
|
490
|
+
textBlockHeight = labelFontSize;
|
|
491
|
+
} else if (lineCount === 2) {
|
|
492
|
+
textBlockHeight = labelFontSize + labelToSublabelGap;
|
|
493
|
+
} else {
|
|
494
|
+
textBlockHeight = labelFontSize + labelToSublabelGap + sublabelToSublabel2Gap;
|
|
495
|
+
}
|
|
496
|
+
const labelY = lineCount === 1 ? centerY + labelFontSize * 0.3 + insideTopShift : centerY - textBlockHeight / 2 + labelFontSize * 0.8 + insideTopShift;
|
|
497
|
+
ctx.textAlign = "center";
|
|
498
|
+
applyFont(ctx, { size: labelFontSize, weight: 700, family: headingFont });
|
|
499
|
+
ctx.fillStyle = labelColor;
|
|
500
|
+
ctx.fillText(node.label, centerX, labelY);
|
|
501
|
+
let textBoundsY = bounds.y + bounds.height / 2 - 18;
|
|
502
|
+
let textBoundsHeight = 36;
|
|
503
|
+
if (node.sublabel) {
|
|
504
|
+
applyFont(ctx, { size: sublabelFontSize, weight: 500, family: bodyFont });
|
|
505
|
+
ctx.fillStyle = sublabelColor;
|
|
506
|
+
ctx.fillText(node.sublabel, centerX, labelY + labelToSublabelGap);
|
|
507
|
+
textBoundsY = bounds.y + bounds.height / 2 - 24;
|
|
508
|
+
textBoundsHeight = 56;
|
|
509
|
+
}
|
|
510
|
+
if (node.sublabel2) {
|
|
511
|
+
applyFont(ctx, { size: sublabel2FontSize, weight: 500, family: bodyFont });
|
|
512
|
+
ctx.fillStyle = sublabel2Color;
|
|
513
|
+
const sublabel2Y = node.sublabel ? labelY + labelToSublabelGap + sublabelToSublabel2Gap : labelY + labelToSublabelGap;
|
|
514
|
+
ctx.fillText(node.sublabel2, centerX, sublabel2Y);
|
|
515
|
+
textBoundsY = bounds.y + bounds.height / 2 - 30;
|
|
516
|
+
textBoundsHeight = 72;
|
|
517
|
+
}
|
|
518
|
+
if (hasBadge && node.badgeText) {
|
|
519
|
+
if (badgePosition === "inside-top") {
|
|
520
|
+
const badgeCenterY = bounds.y + BADGE_PILL_HEIGHT / 2 + 8;
|
|
521
|
+
renderBadgePill(
|
|
522
|
+
ctx,
|
|
523
|
+
centerX,
|
|
524
|
+
badgeCenterY,
|
|
525
|
+
node.badgeText,
|
|
526
|
+
badgeColor,
|
|
527
|
+
badgeBackground,
|
|
528
|
+
monoFont
|
|
529
|
+
);
|
|
530
|
+
} else {
|
|
531
|
+
const badgeCenterY = bounds.y - BADGE_PILL_HEIGHT / 2 - 4;
|
|
532
|
+
renderBadgePill(
|
|
533
|
+
ctx,
|
|
534
|
+
centerX,
|
|
535
|
+
badgeCenterY,
|
|
536
|
+
node.badgeText,
|
|
537
|
+
badgeColor,
|
|
538
|
+
badgeBackground,
|
|
539
|
+
monoFont
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
ctx.restore();
|
|
544
|
+
const effectiveBg = fillOpacity < 1 ? blendColorWithOpacity(fillColor, theme.background, fillOpacity) : fillColor;
|
|
545
|
+
return [
|
|
546
|
+
{
|
|
547
|
+
id: `flow-node-${node.id}`,
|
|
548
|
+
kind: "flow-node",
|
|
549
|
+
bounds,
|
|
550
|
+
foregroundColor: labelColor,
|
|
551
|
+
backgroundColor: effectiveBg
|
|
552
|
+
},
|
|
553
|
+
{
|
|
554
|
+
id: `flow-node-${node.id}-label`,
|
|
555
|
+
kind: "text",
|
|
556
|
+
bounds: {
|
|
557
|
+
x: bounds.x + 8,
|
|
558
|
+
y: textBoundsY,
|
|
559
|
+
width: bounds.width - 16,
|
|
560
|
+
height: textBoundsHeight
|
|
561
|
+
},
|
|
562
|
+
foregroundColor: labelColor,
|
|
563
|
+
backgroundColor: effectiveBg
|
|
564
|
+
}
|
|
565
|
+
];
|
|
566
|
+
}
|
|
567
|
+
|
|
89
568
|
// src/layout/estimates.ts
|
|
90
569
|
function estimateElementHeight(element) {
|
|
91
570
|
switch (element.type) {
|
|
@@ -184,33 +663,37 @@ function computeStackLayout(elements, config, safeFrame) {
|
|
|
184
663
|
|
|
185
664
|
// src/layout/elk.ts
|
|
186
665
|
function estimateFlowNodeSize(node) {
|
|
666
|
+
const badgeExtra = node.badgeText && (node.badgePosition ?? "inside-top") === "inside-top" ? BADGE_INSIDE_TOP_EXTRA : 0;
|
|
667
|
+
const sublabel2Extra = node.sublabel2 ? (node.sublabel2FontSize ?? 11) + 4 : 0;
|
|
668
|
+
const extra = badgeExtra + sublabel2Extra;
|
|
187
669
|
if (node.width && node.height) {
|
|
188
|
-
return { width: node.width, height: node.height };
|
|
670
|
+
return { width: node.width, height: node.height + extra };
|
|
189
671
|
}
|
|
190
672
|
if (node.width) {
|
|
673
|
+
const baseHeight = node.shape === "diamond" || node.shape === "circle" ? node.width : 60;
|
|
191
674
|
return {
|
|
192
675
|
width: node.width,
|
|
193
|
-
height:
|
|
676
|
+
height: baseHeight + extra
|
|
194
677
|
};
|
|
195
678
|
}
|
|
196
679
|
if (node.height) {
|
|
197
680
|
return {
|
|
198
681
|
width: node.shape === "diamond" || node.shape === "circle" ? node.height : 160,
|
|
199
|
-
height: node.height
|
|
682
|
+
height: node.height + extra
|
|
200
683
|
};
|
|
201
684
|
}
|
|
202
685
|
switch (node.shape) {
|
|
203
686
|
case "diamond":
|
|
204
687
|
case "circle":
|
|
205
|
-
return { width: 100, height: 100 };
|
|
688
|
+
return { width: 100 + extra, height: 100 + extra };
|
|
206
689
|
case "pill":
|
|
207
|
-
return { width: 180, height: 56 };
|
|
690
|
+
return { width: 180, height: 56 + extra };
|
|
208
691
|
case "cylinder":
|
|
209
|
-
return { width: 140, height: 92 };
|
|
692
|
+
return { width: 140, height: 92 + extra };
|
|
210
693
|
case "parallelogram":
|
|
211
|
-
return { width: 180, height: 72 };
|
|
694
|
+
return { width: 180, height: 72 + extra };
|
|
212
695
|
default:
|
|
213
|
-
return { width: 170, height: 64 };
|
|
696
|
+
return { width: 170, height: 64 + extra };
|
|
214
697
|
}
|
|
215
698
|
}
|
|
216
699
|
function splitLayoutFrames(safeFrame, direction, hasAuxiliary) {
|
|
@@ -328,6 +811,40 @@ function directionToElk(direction) {
|
|
|
328
811
|
return "DOWN";
|
|
329
812
|
}
|
|
330
813
|
}
|
|
814
|
+
function radialCompactionToElk(compaction) {
|
|
815
|
+
switch (compaction) {
|
|
816
|
+
case "radial":
|
|
817
|
+
return "RADIAL_COMPACTION";
|
|
818
|
+
case "wedge":
|
|
819
|
+
return "WEDGE_COMPACTION";
|
|
820
|
+
default:
|
|
821
|
+
return "NONE";
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
function radialSortByToElk(sortBy) {
|
|
825
|
+
switch (sortBy) {
|
|
826
|
+
case "connections":
|
|
827
|
+
return "POLAR_COORDINATE";
|
|
828
|
+
default:
|
|
829
|
+
return "ID";
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
function buildRadialOptions(config) {
|
|
833
|
+
const options = {};
|
|
834
|
+
if (config.radialRoot) {
|
|
835
|
+
options["elk.radial.centerOnRoot"] = "true";
|
|
836
|
+
}
|
|
837
|
+
if (config.radialRadius != null) {
|
|
838
|
+
options["elk.radial.radius"] = String(config.radialRadius);
|
|
839
|
+
}
|
|
840
|
+
if (config.radialCompaction) {
|
|
841
|
+
options["elk.radial.compaction.strategy"] = radialCompactionToElk(config.radialCompaction);
|
|
842
|
+
}
|
|
843
|
+
if (config.radialSortBy) {
|
|
844
|
+
options["elk.radial.orderId"] = radialSortByToElk(config.radialSortBy);
|
|
845
|
+
}
|
|
846
|
+
return options;
|
|
847
|
+
}
|
|
331
848
|
function fallbackForNoFlowNodes(nonFlow, safeFrame) {
|
|
332
849
|
const fallbackConfig = {
|
|
333
850
|
mode: "stack",
|
|
@@ -363,6 +880,11 @@ async function computeElkLayout(elements, config, safeFrame) {
|
|
|
363
880
|
elkNodeSizes.set(node.id, estimateFlowNodeSize(node));
|
|
364
881
|
}
|
|
365
882
|
const edgeIdToRouteKey = /* @__PURE__ */ new Map();
|
|
883
|
+
const radialOptions = config.algorithm === "radial" ? buildRadialOptions(config) : {};
|
|
884
|
+
const orderedFlowNodes = config.radialRoot && config.algorithm === "radial" ? [
|
|
885
|
+
...flowNodes.filter((node) => node.id === config.radialRoot),
|
|
886
|
+
...flowNodes.filter((node) => node.id !== config.radialRoot)
|
|
887
|
+
] : flowNodes;
|
|
366
888
|
const elkGraph = {
|
|
367
889
|
id: "root",
|
|
368
890
|
layoutOptions: {
|
|
@@ -372,9 +894,10 @@ async function computeElkLayout(elements, config, safeFrame) {
|
|
|
372
894
|
"elk.layered.spacing.nodeNodeBetweenLayers": String(config.rankSpacing),
|
|
373
895
|
"elk.edgeRouting": edgeRoutingToElk(config.edgeRouting),
|
|
374
896
|
...config.aspectRatio ? { "elk.aspectRatio": String(config.aspectRatio) } : {},
|
|
375
|
-
...config.algorithm === "stress" ? { "elk.stress.desiredEdgeLength": String(config.rankSpacing + config.nodeSpacing) } : {}
|
|
897
|
+
...config.algorithm === "stress" ? { "elk.stress.desiredEdgeLength": String(config.rankSpacing + config.nodeSpacing) } : {},
|
|
898
|
+
...radialOptions
|
|
376
899
|
},
|
|
377
|
-
children:
|
|
900
|
+
children: orderedFlowNodes.map((node) => {
|
|
378
901
|
const size = elkNodeSizes.get(node.id) ?? { width: 160, height: 60 };
|
|
379
902
|
return {
|
|
380
903
|
id: node.id,
|
|
@@ -605,268 +1128,86 @@ function roundedRectPath(ctx, x, y, width, height, radius) {
|
|
|
605
1128
|
ctx.lineTo(x + safeRadius, y + height);
|
|
606
1129
|
ctx.quadraticCurveTo(x, y + height, x, y + height - safeRadius);
|
|
607
1130
|
ctx.lineTo(x, y + safeRadius);
|
|
608
|
-
ctx.quadraticCurveTo(x, y, x + safeRadius, y);
|
|
609
|
-
ctx.closePath();
|
|
610
|
-
}
|
|
611
|
-
function
|
|
612
|
-
const normalized = color.startsWith("#") ? color.slice(1) : color;
|
|
613
|
-
if (normalized.length !== 6 && normalized.length !== 8) {
|
|
614
|
-
throw new Error(`Expected #RRGGBB or #RRGGBBAA color, received ${color}`);
|
|
615
|
-
}
|
|
616
|
-
const parseChannel2 = (offset) => Number.parseInt(normalized.slice(offset, offset + 2), 16);
|
|
617
|
-
return {
|
|
618
|
-
r: parseChannel2(0),
|
|
619
|
-
g: parseChannel2(2),
|
|
620
|
-
b: parseChannel2(4),
|
|
621
|
-
a: normalized.length === 8 ? parseChannel2(6) / 255 : 1
|
|
622
|
-
};
|
|
623
|
-
}
|
|
624
|
-
function withAlpha(color, alpha) {
|
|
625
|
-
const parsed = parseHexColor(color);
|
|
626
|
-
const effectiveAlpha = clamp01(parsed.a * alpha);
|
|
627
|
-
return `rgba(${parsed.r}, ${parsed.g}, ${parsed.b}, ${effectiveAlpha})`;
|
|
628
|
-
}
|
|
629
|
-
function drawGradientRect(ctx, rect, gradient, borderRadius = 0) {
|
|
630
|
-
const fill = gradient.type === "linear" ? createLinearRectGradient(ctx, rect, gradient.angle ?? 180) : ctx.createRadialGradient(
|
|
631
|
-
rect.x + rect.width / 2,
|
|
632
|
-
rect.y + rect.height / 2,
|
|
633
|
-
0,
|
|
634
|
-
rect.x + rect.width / 2,
|
|
635
|
-
rect.y + rect.height / 2,
|
|
636
|
-
Math.max(rect.width, rect.height) / 2
|
|
637
|
-
);
|
|
638
|
-
addGradientStops(fill, gradient.stops);
|
|
639
|
-
ctx.save();
|
|
640
|
-
ctx.fillStyle = fill;
|
|
641
|
-
if (borderRadius > 0) {
|
|
642
|
-
roundedRectPath(ctx, rect.x, rect.y, rect.width, rect.height, borderRadius);
|
|
643
|
-
ctx.fill();
|
|
644
|
-
} else {
|
|
645
|
-
ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
|
|
646
|
-
}
|
|
647
|
-
ctx.restore();
|
|
648
|
-
}
|
|
649
|
-
function drawRainbowRule(ctx, x, y, width, thickness = 2, colors = [...DEFAULT_RAINBOW_COLORS], borderRadius = thickness / 2) {
|
|
650
|
-
if (width <= 0 || thickness <= 0) {
|
|
651
|
-
return;
|
|
652
|
-
}
|
|
653
|
-
const gradient = ctx.createLinearGradient(x, y, x + width, y);
|
|
654
|
-
const stops = colors.length >= 2 ? colors : [...DEFAULT_RAINBOW_COLORS];
|
|
655
|
-
for (const [index, color] of stops.entries()) {
|
|
656
|
-
gradient.addColorStop(index / (stops.length - 1), color);
|
|
657
|
-
}
|
|
658
|
-
const ruleTop = y - thickness / 2;
|
|
659
|
-
ctx.save();
|
|
660
|
-
roundedRectPath(ctx, x, ruleTop, width, thickness, borderRadius);
|
|
661
|
-
ctx.fillStyle = gradient;
|
|
662
|
-
ctx.fill();
|
|
663
|
-
ctx.restore();
|
|
664
|
-
}
|
|
665
|
-
function drawVignette(ctx, width, height, intensity = 0.3, color = "#000000") {
|
|
666
|
-
if (width <= 0 || height <= 0 || intensity <= 0) {
|
|
667
|
-
return;
|
|
668
|
-
}
|
|
669
|
-
const centerX = width / 2;
|
|
670
|
-
const centerY = height / 2;
|
|
671
|
-
const outerRadius = Math.max(width, height) / 2;
|
|
672
|
-
const innerRadius = Math.min(width, height) * 0.2;
|
|
673
|
-
const vignette = ctx.createRadialGradient(
|
|
674
|
-
centerX,
|
|
675
|
-
centerY,
|
|
676
|
-
innerRadius,
|
|
677
|
-
centerX,
|
|
678
|
-
centerY,
|
|
679
|
-
outerRadius
|
|
680
|
-
);
|
|
681
|
-
vignette.addColorStop(0, withAlpha(color, 0));
|
|
682
|
-
vignette.addColorStop(0.6, withAlpha(color, 0));
|
|
683
|
-
vignette.addColorStop(1, withAlpha(color, clamp01(intensity)));
|
|
684
|
-
ctx.save();
|
|
685
|
-
ctx.fillStyle = vignette;
|
|
686
|
-
ctx.fillRect(0, 0, width, height);
|
|
687
|
-
ctx.restore();
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
// src/primitives/shapes.ts
|
|
691
|
-
function roundRectPath(ctx, rect, radius) {
|
|
692
|
-
const r = Math.max(0, Math.min(radius, rect.width / 2, rect.height / 2));
|
|
693
|
-
const right = rect.x + rect.width;
|
|
694
|
-
const bottom = rect.y + rect.height;
|
|
695
|
-
ctx.beginPath();
|
|
696
|
-
ctx.moveTo(rect.x + r, rect.y);
|
|
697
|
-
ctx.lineTo(right - r, rect.y);
|
|
698
|
-
ctx.quadraticCurveTo(right, rect.y, right, rect.y + r);
|
|
699
|
-
ctx.lineTo(right, bottom - r);
|
|
700
|
-
ctx.quadraticCurveTo(right, bottom, right - r, bottom);
|
|
701
|
-
ctx.lineTo(rect.x + r, bottom);
|
|
702
|
-
ctx.quadraticCurveTo(rect.x, bottom, rect.x, bottom - r);
|
|
703
|
-
ctx.lineTo(rect.x, rect.y + r);
|
|
704
|
-
ctx.quadraticCurveTo(rect.x, rect.y, rect.x + r, rect.y);
|
|
705
|
-
ctx.closePath();
|
|
706
|
-
}
|
|
707
|
-
function fillAndStroke(ctx, fill, stroke) {
|
|
708
|
-
ctx.fillStyle = fill;
|
|
709
|
-
ctx.fill();
|
|
710
|
-
if (stroke) {
|
|
711
|
-
ctx.strokeStyle = stroke;
|
|
712
|
-
ctx.stroke();
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
function drawRoundedRect(ctx, rect, radius, fill, stroke) {
|
|
716
|
-
roundRectPath(ctx, rect, radius);
|
|
717
|
-
fillAndStroke(ctx, fill, stroke);
|
|
718
|
-
}
|
|
719
|
-
function drawCircle(ctx, center2, radius, fill, stroke) {
|
|
720
|
-
ctx.beginPath();
|
|
721
|
-
ctx.arc(center2.x, center2.y, Math.max(0, radius), 0, Math.PI * 2);
|
|
722
|
-
ctx.closePath();
|
|
723
|
-
fillAndStroke(ctx, fill, stroke);
|
|
724
|
-
}
|
|
725
|
-
function drawDiamond(ctx, bounds, fill, stroke) {
|
|
726
|
-
const cx = bounds.x + bounds.width / 2;
|
|
727
|
-
const cy = bounds.y + bounds.height / 2;
|
|
728
|
-
ctx.beginPath();
|
|
729
|
-
ctx.moveTo(cx, bounds.y);
|
|
730
|
-
ctx.lineTo(bounds.x + bounds.width, cy);
|
|
731
|
-
ctx.lineTo(cx, bounds.y + bounds.height);
|
|
732
|
-
ctx.lineTo(bounds.x, cy);
|
|
733
|
-
ctx.closePath();
|
|
734
|
-
fillAndStroke(ctx, fill, stroke);
|
|
735
|
-
}
|
|
736
|
-
function drawPill(ctx, bounds, fill, stroke) {
|
|
737
|
-
drawRoundedRect(ctx, bounds, Math.min(bounds.width, bounds.height) / 2, fill, stroke);
|
|
738
|
-
}
|
|
739
|
-
function drawEllipse(ctx, bounds, fill, stroke) {
|
|
740
|
-
const cx = bounds.x + bounds.width / 2;
|
|
741
|
-
const cy = bounds.y + bounds.height / 2;
|
|
742
|
-
ctx.beginPath();
|
|
743
|
-
ctx.ellipse(
|
|
744
|
-
cx,
|
|
745
|
-
cy,
|
|
746
|
-
Math.max(0, bounds.width / 2),
|
|
747
|
-
Math.max(0, bounds.height / 2),
|
|
748
|
-
0,
|
|
749
|
-
0,
|
|
750
|
-
Math.PI * 2
|
|
751
|
-
);
|
|
752
|
-
ctx.closePath();
|
|
753
|
-
fillAndStroke(ctx, fill, stroke);
|
|
754
|
-
}
|
|
755
|
-
function drawCylinder(ctx, bounds, fill, stroke) {
|
|
756
|
-
const rx = Math.max(2, bounds.width / 2);
|
|
757
|
-
const ry = Math.max(2, Math.min(bounds.height * 0.18, 16));
|
|
758
|
-
const cx = bounds.x + bounds.width / 2;
|
|
759
|
-
const topCy = bounds.y + ry;
|
|
760
|
-
const bottomCy = bounds.y + bounds.height - ry;
|
|
761
|
-
ctx.beginPath();
|
|
762
|
-
ctx.moveTo(bounds.x, topCy);
|
|
763
|
-
ctx.ellipse(cx, topCy, rx, ry, 0, Math.PI, 0, true);
|
|
764
|
-
ctx.lineTo(bounds.x + bounds.width, bottomCy);
|
|
765
|
-
ctx.ellipse(cx, bottomCy, rx, ry, 0, 0, Math.PI, false);
|
|
766
|
-
ctx.closePath();
|
|
767
|
-
fillAndStroke(ctx, fill, stroke);
|
|
768
|
-
if (stroke) {
|
|
769
|
-
ctx.beginPath();
|
|
770
|
-
ctx.ellipse(cx, topCy, rx, ry, 0, 0, Math.PI * 2);
|
|
771
|
-
ctx.closePath();
|
|
772
|
-
ctx.strokeStyle = stroke;
|
|
773
|
-
ctx.stroke();
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
function drawParallelogram(ctx, bounds, fill, stroke, skew) {
|
|
777
|
-
const maxSkew = bounds.width * 0.45;
|
|
778
|
-
const skewX = Math.max(-maxSkew, Math.min(maxSkew, skew ?? bounds.width * 0.18));
|
|
779
|
-
ctx.beginPath();
|
|
780
|
-
ctx.moveTo(bounds.x + skewX, bounds.y);
|
|
781
|
-
ctx.lineTo(bounds.x + bounds.width, bounds.y);
|
|
782
|
-
ctx.lineTo(bounds.x + bounds.width - skewX, bounds.y + bounds.height);
|
|
783
|
-
ctx.lineTo(bounds.x, bounds.y + bounds.height);
|
|
784
|
-
ctx.closePath();
|
|
785
|
-
fillAndStroke(ctx, fill, stroke);
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
// src/primitives/text.ts
|
|
789
|
-
var SUPPORTED_FONT_FAMILIES = /* @__PURE__ */ new Set(["Inter", "JetBrains Mono", "Space Grotesk"]);
|
|
790
|
-
function resolveFont(requested, role) {
|
|
791
|
-
if (SUPPORTED_FONT_FAMILIES.has(requested)) {
|
|
792
|
-
return requested;
|
|
793
|
-
}
|
|
794
|
-
if (role === "mono" || /mono|code|terminal|console/iu.test(requested)) {
|
|
795
|
-
return "JetBrains Mono";
|
|
796
|
-
}
|
|
797
|
-
if (role === "heading" || /display|grotesk|headline/iu.test(requested)) {
|
|
798
|
-
return "Space Grotesk";
|
|
799
|
-
}
|
|
800
|
-
return "Inter";
|
|
801
|
-
}
|
|
802
|
-
function applyFont(ctx, options) {
|
|
803
|
-
ctx.font = `${options.weight} ${options.size}px ${options.family}`;
|
|
804
|
-
}
|
|
805
|
-
function wrapText(ctx, text, maxWidth, maxLines) {
|
|
806
|
-
const trimmed = text.trim();
|
|
807
|
-
if (!trimmed) {
|
|
808
|
-
return { lines: [], truncated: false };
|
|
809
|
-
}
|
|
810
|
-
const words = trimmed.split(/\s+/u);
|
|
811
|
-
const lines = [];
|
|
812
|
-
let current = "";
|
|
813
|
-
for (const word of words) {
|
|
814
|
-
const trial = current.length > 0 ? `${current} ${word}` : word;
|
|
815
|
-
if (ctx.measureText(trial).width <= maxWidth) {
|
|
816
|
-
current = trial;
|
|
817
|
-
continue;
|
|
818
|
-
}
|
|
819
|
-
if (current.length > 0) {
|
|
820
|
-
lines.push(current);
|
|
821
|
-
current = word;
|
|
822
|
-
} else {
|
|
823
|
-
lines.push(word);
|
|
824
|
-
current = "";
|
|
825
|
-
}
|
|
826
|
-
if (lines.length >= maxLines) {
|
|
827
|
-
break;
|
|
828
|
-
}
|
|
1131
|
+
ctx.quadraticCurveTo(x, y, x + safeRadius, y);
|
|
1132
|
+
ctx.closePath();
|
|
1133
|
+
}
|
|
1134
|
+
function parseHexColor2(color) {
|
|
1135
|
+
const normalized = color.startsWith("#") ? color.slice(1) : color;
|
|
1136
|
+
if (normalized.length !== 6 && normalized.length !== 8) {
|
|
1137
|
+
throw new Error(`Expected #RRGGBB or #RRGGBBAA color, received ${color}`);
|
|
829
1138
|
}
|
|
830
|
-
|
|
831
|
-
|
|
1139
|
+
const parseChannel2 = (offset) => Number.parseInt(normalized.slice(offset, offset + 2), 16);
|
|
1140
|
+
return {
|
|
1141
|
+
r: parseChannel2(0),
|
|
1142
|
+
g: parseChannel2(2),
|
|
1143
|
+
b: parseChannel2(4),
|
|
1144
|
+
a: normalized.length === 8 ? parseChannel2(6) / 255 : 1
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
function withAlpha2(color, alpha) {
|
|
1148
|
+
const parsed = parseHexColor2(color);
|
|
1149
|
+
const effectiveAlpha = clamp01(parsed.a * alpha);
|
|
1150
|
+
return `rgba(${parsed.r}, ${parsed.g}, ${parsed.b}, ${effectiveAlpha})`;
|
|
1151
|
+
}
|
|
1152
|
+
function drawGradientRect(ctx, rect, gradient, borderRadius = 0) {
|
|
1153
|
+
const fill = gradient.type === "linear" ? createLinearRectGradient(ctx, rect, gradient.angle ?? 180) : ctx.createRadialGradient(
|
|
1154
|
+
rect.x + rect.width / 2,
|
|
1155
|
+
rect.y + rect.height / 2,
|
|
1156
|
+
0,
|
|
1157
|
+
rect.x + rect.width / 2,
|
|
1158
|
+
rect.y + rect.height / 2,
|
|
1159
|
+
Math.max(rect.width, rect.height) / 2
|
|
1160
|
+
);
|
|
1161
|
+
addGradientStops(fill, gradient.stops);
|
|
1162
|
+
ctx.save();
|
|
1163
|
+
ctx.fillStyle = fill;
|
|
1164
|
+
if (borderRadius > 0) {
|
|
1165
|
+
roundedRectPath(ctx, rect.x, rect.y, rect.width, rect.height, borderRadius);
|
|
1166
|
+
ctx.fill();
|
|
1167
|
+
} else {
|
|
1168
|
+
ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
|
|
832
1169
|
}
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
1170
|
+
ctx.restore();
|
|
1171
|
+
}
|
|
1172
|
+
function drawRainbowRule(ctx, x, y, width, thickness = 2, colors = [...DEFAULT_RAINBOW_COLORS], borderRadius = thickness / 2) {
|
|
1173
|
+
if (width <= 0 || thickness <= 0) {
|
|
1174
|
+
return;
|
|
836
1175
|
}
|
|
837
|
-
const
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
1176
|
+
const gradient = ctx.createLinearGradient(x, y, x + width, y);
|
|
1177
|
+
const stops = colors.length >= 2 ? colors : [...DEFAULT_RAINBOW_COLORS];
|
|
1178
|
+
for (const [index, color] of stops.entries()) {
|
|
1179
|
+
gradient.addColorStop(index / (stops.length - 1), color);
|
|
841
1180
|
}
|
|
842
|
-
|
|
843
|
-
|
|
1181
|
+
const ruleTop = y - thickness / 2;
|
|
1182
|
+
ctx.save();
|
|
1183
|
+
roundedRectPath(ctx, x, ruleTop, width, thickness, borderRadius);
|
|
1184
|
+
ctx.fillStyle = gradient;
|
|
1185
|
+
ctx.fill();
|
|
1186
|
+
ctx.restore();
|
|
844
1187
|
}
|
|
845
|
-
function
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
ctx.fillStyle = options.color;
|
|
849
|
-
for (const [index, line] of wrapped.lines.entries()) {
|
|
850
|
-
ctx.fillText(line, options.x, options.y + index * options.lineHeight);
|
|
1188
|
+
function drawVignette(ctx, width, height, intensity = 0.3, color = "#000000") {
|
|
1189
|
+
if (width <= 0 || height <= 0 || intensity <= 0) {
|
|
1190
|
+
return;
|
|
851
1191
|
}
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
ctx.
|
|
868
|
-
ctx.
|
|
869
|
-
|
|
1192
|
+
const centerX = width / 2;
|
|
1193
|
+
const centerY = height / 2;
|
|
1194
|
+
const outerRadius = Math.max(width, height) / 2;
|
|
1195
|
+
const innerRadius = Math.min(width, height) * 0.2;
|
|
1196
|
+
const vignette = ctx.createRadialGradient(
|
|
1197
|
+
centerX,
|
|
1198
|
+
centerY,
|
|
1199
|
+
innerRadius,
|
|
1200
|
+
centerX,
|
|
1201
|
+
centerY,
|
|
1202
|
+
outerRadius
|
|
1203
|
+
);
|
|
1204
|
+
vignette.addColorStop(0, withAlpha2(color, 0));
|
|
1205
|
+
vignette.addColorStop(0.6, withAlpha2(color, 0));
|
|
1206
|
+
vignette.addColorStop(1, withAlpha2(color, clamp01(intensity)));
|
|
1207
|
+
ctx.save();
|
|
1208
|
+
ctx.fillStyle = vignette;
|
|
1209
|
+
ctx.fillRect(0, 0, width, height);
|
|
1210
|
+
ctx.restore();
|
|
870
1211
|
}
|
|
871
1212
|
|
|
872
1213
|
// src/renderers/card.ts
|
|
@@ -981,36 +1322,6 @@ function renderCard(ctx, card, rect, theme) {
|
|
|
981
1322
|
return elements;
|
|
982
1323
|
}
|
|
983
1324
|
|
|
984
|
-
// src/utils/color.ts
|
|
985
|
-
function parseChannel(hex, offset) {
|
|
986
|
-
return Number.parseInt(hex.slice(offset, offset + 2), 16);
|
|
987
|
-
}
|
|
988
|
-
function parseHexColor2(hexColor) {
|
|
989
|
-
const normalized = hexColor.startsWith("#") ? hexColor.slice(1) : hexColor;
|
|
990
|
-
if (normalized.length !== 6 && normalized.length !== 8) {
|
|
991
|
-
throw new Error(`Unsupported color format: ${hexColor}`);
|
|
992
|
-
}
|
|
993
|
-
return {
|
|
994
|
-
r: parseChannel(normalized, 0),
|
|
995
|
-
g: parseChannel(normalized, 2),
|
|
996
|
-
b: parseChannel(normalized, 4)
|
|
997
|
-
};
|
|
998
|
-
}
|
|
999
|
-
function srgbToLinear(channel) {
|
|
1000
|
-
const normalized = channel / 255;
|
|
1001
|
-
if (normalized <= 0.03928) {
|
|
1002
|
-
return normalized / 12.92;
|
|
1003
|
-
}
|
|
1004
|
-
return ((normalized + 0.055) / 1.055) ** 2.4;
|
|
1005
|
-
}
|
|
1006
|
-
function relativeLuminance(hexColor) {
|
|
1007
|
-
const rgb = parseHexColor2(hexColor);
|
|
1008
|
-
const r = srgbToLinear(rgb.r);
|
|
1009
|
-
const g = srgbToLinear(rgb.g);
|
|
1010
|
-
const b = srgbToLinear(rgb.b);
|
|
1011
|
-
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
1325
|
// src/primitives/window-chrome.ts
|
|
1015
1326
|
var WINDOW_CHROME_HEIGHT = 34;
|
|
1016
1327
|
var WINDOW_CHROME_LEFT_MARGIN = 14;
|
|
@@ -1023,12 +1334,12 @@ var MACOS_DOTS = [
|
|
|
1023
1334
|
{ fill: "#27C93F", stroke: "#1AAB29" }
|
|
1024
1335
|
];
|
|
1025
1336
|
function drawMacosDots(ctx, x, y) {
|
|
1026
|
-
for (const [index,
|
|
1337
|
+
for (const [index, dot2] of MACOS_DOTS.entries()) {
|
|
1027
1338
|
ctx.beginPath();
|
|
1028
1339
|
ctx.arc(x + index * DOT_SPACING, y, DOT_RADIUS, 0, Math.PI * 2);
|
|
1029
1340
|
ctx.closePath();
|
|
1030
|
-
ctx.fillStyle =
|
|
1031
|
-
ctx.strokeStyle =
|
|
1341
|
+
ctx.fillStyle = dot2.fill;
|
|
1342
|
+
ctx.strokeStyle = dot2.stroke;
|
|
1032
1343
|
ctx.lineWidth = DOT_STROKE_WIDTH;
|
|
1033
1344
|
ctx.fill();
|
|
1034
1345
|
ctx.stroke();
|
|
@@ -1160,7 +1471,17 @@ async function highlightCode(code, language, themeName) {
|
|
|
1160
1471
|
|
|
1161
1472
|
// src/themes/builtin.ts
|
|
1162
1473
|
import { z } from "zod";
|
|
1163
|
-
var colorHexSchema = z.string().
|
|
1474
|
+
var colorHexSchema = z.string().refine(
|
|
1475
|
+
(v) => {
|
|
1476
|
+
try {
|
|
1477
|
+
normalizeColor(v);
|
|
1478
|
+
return true;
|
|
1479
|
+
} catch {
|
|
1480
|
+
return false;
|
|
1481
|
+
}
|
|
1482
|
+
},
|
|
1483
|
+
{ message: "Expected #RRGGBB, #RRGGBBAA, rgb(), or rgba() color" }
|
|
1484
|
+
).transform((v) => normalizeColor(v));
|
|
1164
1485
|
var fontFamilySchema = z.string().min(1).max(120);
|
|
1165
1486
|
var codeThemeSchema = z.object({
|
|
1166
1487
|
background: colorHexSchema,
|
|
@@ -1667,25 +1988,134 @@ function drawOrthogonalPath(ctx, from, to, style) {
|
|
|
1667
1988
|
}
|
|
1668
1989
|
|
|
1669
1990
|
// src/renderers/connection.ts
|
|
1670
|
-
|
|
1991
|
+
var ELLIPSE_KAPPA = 4 * (Math.sqrt(2) - 1) / 3;
|
|
1992
|
+
function rectCenter(rect) {
|
|
1671
1993
|
return {
|
|
1672
1994
|
x: rect.x + rect.width / 2,
|
|
1673
1995
|
y: rect.y + rect.height / 2
|
|
1674
1996
|
};
|
|
1675
1997
|
}
|
|
1676
|
-
function edgeAnchor(
|
|
1677
|
-
const c =
|
|
1998
|
+
function edgeAnchor(bounds, target) {
|
|
1999
|
+
const c = rectCenter(bounds);
|
|
1678
2000
|
const dx = target.x - c.x;
|
|
1679
2001
|
const dy = target.y - c.y;
|
|
1680
|
-
if (
|
|
1681
|
-
return {
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
2002
|
+
if (dx === 0 && dy === 0) {
|
|
2003
|
+
return { x: c.x, y: c.y - bounds.height / 2 };
|
|
2004
|
+
}
|
|
2005
|
+
const hw = bounds.width / 2;
|
|
2006
|
+
const hh = bounds.height / 2;
|
|
2007
|
+
const absDx = Math.abs(dx);
|
|
2008
|
+
const absDy = Math.abs(dy);
|
|
2009
|
+
const t = absDx * hh > absDy * hw ? hw / absDx : hh / absDy;
|
|
2010
|
+
return { x: c.x + dx * t, y: c.y + dy * t };
|
|
2011
|
+
}
|
|
2012
|
+
function outwardNormal(point, diagramCenter) {
|
|
2013
|
+
const dx = point.x - diagramCenter.x;
|
|
2014
|
+
const dy = point.y - diagramCenter.y;
|
|
2015
|
+
const len = Math.hypot(dx, dy) || 1;
|
|
2016
|
+
return { x: dx / len, y: dy / len };
|
|
2017
|
+
}
|
|
2018
|
+
function curveRoute(fromBounds, toBounds, diagramCenter, tension) {
|
|
2019
|
+
const fromCenter = rectCenter(fromBounds);
|
|
2020
|
+
const toCenter = rectCenter(toBounds);
|
|
2021
|
+
const p0 = edgeAnchor(fromBounds, toCenter);
|
|
2022
|
+
const p3 = edgeAnchor(toBounds, fromCenter);
|
|
2023
|
+
const dist = Math.hypot(p3.x - p0.x, p3.y - p0.y);
|
|
2024
|
+
const offset = dist * tension;
|
|
2025
|
+
const n0 = outwardNormal(p0, diagramCenter);
|
|
2026
|
+
const n3 = outwardNormal(p3, diagramCenter);
|
|
2027
|
+
const cp1 = { x: p0.x + n0.x * offset, y: p0.y + n0.y * offset };
|
|
2028
|
+
const cp2 = { x: p3.x + n3.x * offset, y: p3.y + n3.y * offset };
|
|
2029
|
+
return [p0, cp1, cp2, p3];
|
|
2030
|
+
}
|
|
2031
|
+
function dot(a, b) {
|
|
2032
|
+
return a.x * b.x + a.y * b.y;
|
|
2033
|
+
}
|
|
2034
|
+
function localToWorld(origin, axisX, axisY, local) {
|
|
2035
|
+
return {
|
|
2036
|
+
x: origin.x + axisX.x * local.x + axisY.x * local.y,
|
|
2037
|
+
y: origin.y + axisX.y * local.x + axisY.y * local.y
|
|
2038
|
+
};
|
|
2039
|
+
}
|
|
2040
|
+
function arcRoute(fromBounds, toBounds, diagramCenter, tension) {
|
|
2041
|
+
const fromCenter = rectCenter(fromBounds);
|
|
2042
|
+
const toCenter = rectCenter(toBounds);
|
|
2043
|
+
const start = edgeAnchor(fromBounds, toCenter);
|
|
2044
|
+
const end = edgeAnchor(toBounds, fromCenter);
|
|
2045
|
+
const chord = { x: end.x - start.x, y: end.y - start.y };
|
|
2046
|
+
const chordLength = Math.hypot(chord.x, chord.y);
|
|
2047
|
+
if (chordLength < 1e-6) {
|
|
2048
|
+
const mid = { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 };
|
|
2049
|
+
return [
|
|
2050
|
+
[start, start, mid, mid],
|
|
2051
|
+
[mid, mid, end, end]
|
|
2052
|
+
];
|
|
2053
|
+
}
|
|
2054
|
+
const axisX = { x: chord.x / chordLength, y: chord.y / chordLength };
|
|
2055
|
+
let axisY = { x: -axisX.y, y: axisX.x };
|
|
2056
|
+
const midpoint = { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 };
|
|
2057
|
+
const outwardHint = outwardNormal(midpoint, diagramCenter);
|
|
2058
|
+
if (dot(axisY, outwardHint) < 0) {
|
|
2059
|
+
axisY = { x: -axisY.x, y: -axisY.y };
|
|
2060
|
+
}
|
|
2061
|
+
const semiMajor = chordLength / 2;
|
|
2062
|
+
const semiMinor = Math.max(12, chordLength * tension * 0.75);
|
|
2063
|
+
const p0Local = { x: -semiMajor, y: 0 };
|
|
2064
|
+
const cp1Local = { x: -semiMajor, y: ELLIPSE_KAPPA * semiMinor };
|
|
2065
|
+
const cp2Local = { x: -ELLIPSE_KAPPA * semiMajor, y: semiMinor };
|
|
2066
|
+
const pMidLocal = { x: 0, y: semiMinor };
|
|
2067
|
+
const cp3Local = { x: ELLIPSE_KAPPA * semiMajor, y: semiMinor };
|
|
2068
|
+
const cp4Local = { x: semiMajor, y: ELLIPSE_KAPPA * semiMinor };
|
|
2069
|
+
const p3Local = { x: semiMajor, y: 0 };
|
|
2070
|
+
const p0 = localToWorld(midpoint, axisX, axisY, p0Local);
|
|
2071
|
+
const cp1 = localToWorld(midpoint, axisX, axisY, cp1Local);
|
|
2072
|
+
const cp2 = localToWorld(midpoint, axisX, axisY, cp2Local);
|
|
2073
|
+
const pMid = localToWorld(midpoint, axisX, axisY, pMidLocal);
|
|
2074
|
+
const cp3 = localToWorld(midpoint, axisX, axisY, cp3Local);
|
|
2075
|
+
const cp4 = localToWorld(midpoint, axisX, axisY, cp4Local);
|
|
2076
|
+
const p3 = localToWorld(midpoint, axisX, axisY, p3Local);
|
|
2077
|
+
return [
|
|
2078
|
+
[p0, cp1, cp2, pMid],
|
|
2079
|
+
[pMid, cp3, cp4, p3]
|
|
2080
|
+
];
|
|
2081
|
+
}
|
|
2082
|
+
function orthogonalRoute(fromBounds, toBounds) {
|
|
2083
|
+
const fromC = rectCenter(fromBounds);
|
|
2084
|
+
const toC = rectCenter(toBounds);
|
|
2085
|
+
const p0 = edgeAnchor(fromBounds, toC);
|
|
2086
|
+
const p3 = edgeAnchor(toBounds, fromC);
|
|
2087
|
+
const midX = (p0.x + p3.x) / 2;
|
|
2088
|
+
return [p0, { x: midX, y: p0.y }, { x: midX, y: p3.y }, p3];
|
|
2089
|
+
}
|
|
2090
|
+
function bezierPointAt(p0, cp1, cp2, p3, t) {
|
|
2091
|
+
const mt = 1 - t;
|
|
2092
|
+
return {
|
|
2093
|
+
x: mt * mt * mt * p0.x + 3 * mt * mt * t * cp1.x + 3 * mt * t * t * cp2.x + t * t * t * p3.x,
|
|
2094
|
+
y: mt * mt * mt * p0.y + 3 * mt * mt * t * cp1.y + 3 * mt * t * t * cp2.y + t * t * t * p3.y
|
|
2095
|
+
};
|
|
2096
|
+
}
|
|
2097
|
+
function pointAlongArc(route, t) {
|
|
2098
|
+
const [first, second] = route;
|
|
2099
|
+
if (t <= 0.5) {
|
|
2100
|
+
const localT2 = Math.max(0, Math.min(1, t * 2));
|
|
2101
|
+
return bezierPointAt(first[0], first[1], first[2], first[3], localT2);
|
|
2102
|
+
}
|
|
2103
|
+
const localT = Math.max(0, Math.min(1, (t - 0.5) * 2));
|
|
2104
|
+
return bezierPointAt(second[0], second[1], second[2], second[3], localT);
|
|
2105
|
+
}
|
|
2106
|
+
function computeDiagramCenter(nodeBounds, canvasCenter) {
|
|
2107
|
+
if (nodeBounds.length === 0) {
|
|
2108
|
+
return canvasCenter ?? { x: 0, y: 0 };
|
|
2109
|
+
}
|
|
2110
|
+
let totalX = 0;
|
|
2111
|
+
let totalY = 0;
|
|
2112
|
+
for (const bounds of nodeBounds) {
|
|
2113
|
+
totalX += bounds.x + bounds.width / 2;
|
|
2114
|
+
totalY += bounds.y + bounds.height / 2;
|
|
1685
2115
|
}
|
|
1686
2116
|
return {
|
|
1687
|
-
x:
|
|
1688
|
-
y:
|
|
2117
|
+
x: totalX / nodeBounds.length,
|
|
2118
|
+
y: totalY / nodeBounds.length
|
|
1689
2119
|
};
|
|
1690
2120
|
}
|
|
1691
2121
|
function dashFromStyle(style) {
|
|
@@ -1769,51 +2199,95 @@ function polylineBounds(points) {
|
|
|
1769
2199
|
height: Math.max(1, maxY - minY)
|
|
1770
2200
|
};
|
|
1771
2201
|
}
|
|
1772
|
-
function renderConnection(ctx, conn, fromBounds, toBounds, theme, edgeRoute) {
|
|
1773
|
-
const
|
|
1774
|
-
const
|
|
1775
|
-
const
|
|
1776
|
-
const
|
|
1777
|
-
const dash = dashFromStyle(
|
|
2202
|
+
function renderConnection(ctx, conn, fromBounds, toBounds, theme, edgeRoute, options) {
|
|
2203
|
+
const routing = conn.routing ?? "auto";
|
|
2204
|
+
const strokeStyle = conn.strokeStyle ?? conn.style ?? "solid";
|
|
2205
|
+
const strokeWidth = conn.width ?? conn.strokeWidth ?? 2;
|
|
2206
|
+
const tension = conn.tension ?? 0.35;
|
|
2207
|
+
const dash = dashFromStyle(strokeStyle);
|
|
1778
2208
|
const style = {
|
|
1779
2209
|
color: conn.color ?? theme.borderMuted,
|
|
1780
|
-
width:
|
|
2210
|
+
width: strokeWidth,
|
|
1781
2211
|
headSize: conn.arrowSize ?? 10,
|
|
1782
2212
|
...dash ? { dash } : {}
|
|
1783
2213
|
};
|
|
1784
|
-
const
|
|
1785
|
-
const
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
let
|
|
1789
|
-
let
|
|
2214
|
+
const labelT = conn.labelPosition === "start" ? 0.2 : conn.labelPosition === "end" ? 0.8 : 0.5;
|
|
2215
|
+
const diagramCenter = options?.diagramCenter ?? computeDiagramCenter([fromBounds, toBounds]);
|
|
2216
|
+
let linePoints;
|
|
2217
|
+
let startPoint;
|
|
2218
|
+
let endPoint;
|
|
2219
|
+
let startAngle;
|
|
2220
|
+
let endAngle;
|
|
2221
|
+
let labelPoint;
|
|
2222
|
+
ctx.save();
|
|
2223
|
+
ctx.globalAlpha = conn.opacity;
|
|
2224
|
+
if (routing === "curve") {
|
|
2225
|
+
const [p0, cp1, cp2, p3] = curveRoute(fromBounds, toBounds, diagramCenter, tension);
|
|
2226
|
+
ctx.strokeStyle = style.color;
|
|
2227
|
+
ctx.lineWidth = style.width;
|
|
2228
|
+
ctx.setLineDash(style.dash ?? []);
|
|
2229
|
+
ctx.beginPath();
|
|
2230
|
+
ctx.moveTo(p0.x, p0.y);
|
|
2231
|
+
ctx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, p3.x, p3.y);
|
|
2232
|
+
ctx.stroke();
|
|
2233
|
+
linePoints = [p0, cp1, cp2, p3];
|
|
2234
|
+
startPoint = p0;
|
|
2235
|
+
endPoint = p3;
|
|
2236
|
+
startAngle = Math.atan2(p0.y - cp1.y, p0.x - cp1.x);
|
|
2237
|
+
endAngle = Math.atan2(p3.y - cp2.y, p3.x - cp2.x);
|
|
2238
|
+
labelPoint = bezierPointAt(p0, cp1, cp2, p3, labelT);
|
|
2239
|
+
} else if (routing === "arc") {
|
|
2240
|
+
const [first, second] = arcRoute(fromBounds, toBounds, diagramCenter, tension);
|
|
2241
|
+
const [p0, cp1, cp2, pMid] = first;
|
|
2242
|
+
const [, cp3, cp4, p3] = second;
|
|
2243
|
+
ctx.strokeStyle = style.color;
|
|
2244
|
+
ctx.lineWidth = style.width;
|
|
2245
|
+
ctx.setLineDash(style.dash ?? []);
|
|
2246
|
+
ctx.beginPath();
|
|
2247
|
+
ctx.moveTo(p0.x, p0.y);
|
|
2248
|
+
ctx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, pMid.x, pMid.y);
|
|
2249
|
+
ctx.bezierCurveTo(cp3.x, cp3.y, cp4.x, cp4.y, p3.x, p3.y);
|
|
2250
|
+
ctx.stroke();
|
|
2251
|
+
linePoints = [p0, cp1, cp2, pMid, cp3, cp4, p3];
|
|
2252
|
+
startPoint = p0;
|
|
2253
|
+
endPoint = p3;
|
|
2254
|
+
startAngle = Math.atan2(p0.y - cp1.y, p0.x - cp1.x);
|
|
2255
|
+
endAngle = Math.atan2(p3.y - cp4.y, p3.x - cp4.x);
|
|
2256
|
+
labelPoint = pointAlongArc([first, second], labelT);
|
|
2257
|
+
} else {
|
|
2258
|
+
const useElkRoute = routing === "auto" && (edgeRoute?.points.length ?? 0) >= 2;
|
|
2259
|
+
linePoints = useElkRoute ? edgeRoute?.points ?? orthogonalRoute(fromBounds, toBounds) : orthogonalRoute(fromBounds, toBounds);
|
|
2260
|
+
startPoint = linePoints[0];
|
|
2261
|
+
const startSegment = linePoints[1] ?? linePoints[0];
|
|
2262
|
+
const endStart = linePoints[linePoints.length - 2] ?? linePoints[0];
|
|
2263
|
+
endPoint = linePoints[linePoints.length - 1] ?? linePoints[0];
|
|
2264
|
+
startAngle = Math.atan2(startSegment.y - linePoints[0].y, startSegment.x - linePoints[0].x) + Math.PI;
|
|
2265
|
+
endAngle = Math.atan2(endPoint.y - endStart.y, endPoint.x - endStart.x);
|
|
2266
|
+
if (useElkRoute) {
|
|
2267
|
+
drawCubicInterpolatedPath(ctx, linePoints, style);
|
|
2268
|
+
} else {
|
|
2269
|
+
drawOrthogonalPath(ctx, startPoint, endPoint, style);
|
|
2270
|
+
}
|
|
2271
|
+
labelPoint = pointAlongPolyline(linePoints, labelT);
|
|
2272
|
+
}
|
|
1790
2273
|
if (!Number.isFinite(startAngle)) {
|
|
1791
2274
|
startAngle = 0;
|
|
1792
2275
|
}
|
|
1793
2276
|
if (!Number.isFinite(endAngle)) {
|
|
1794
2277
|
endAngle = 0;
|
|
1795
2278
|
}
|
|
1796
|
-
const t = conn.labelPosition === "start" ? 0.2 : conn.labelPosition === "end" ? 0.8 : 0.5;
|
|
1797
|
-
const labelPoint = pointAlongPolyline(points, t);
|
|
1798
|
-
ctx.save();
|
|
1799
|
-
ctx.globalAlpha = conn.opacity;
|
|
1800
|
-
if (edgeRoute && edgeRoute.points.length >= 2) {
|
|
1801
|
-
drawCubicInterpolatedPath(ctx, points, style);
|
|
1802
|
-
} else {
|
|
1803
|
-
drawOrthogonalPath(ctx, points[0], points[points.length - 1], style);
|
|
1804
|
-
}
|
|
1805
2279
|
if (conn.arrow === "start" || conn.arrow === "both") {
|
|
1806
|
-
drawArrowhead(ctx,
|
|
2280
|
+
drawArrowhead(ctx, startPoint, startAngle, style.headSize, style.color);
|
|
1807
2281
|
}
|
|
1808
2282
|
if (conn.arrow === "end" || conn.arrow === "both") {
|
|
1809
|
-
drawArrowhead(ctx,
|
|
2283
|
+
drawArrowhead(ctx, endPoint, endAngle, style.headSize, style.color);
|
|
1810
2284
|
}
|
|
1811
2285
|
ctx.restore();
|
|
1812
2286
|
const elements = [
|
|
1813
2287
|
{
|
|
1814
2288
|
id: `connection-${conn.from}-${conn.to}`,
|
|
1815
2289
|
kind: "connection",
|
|
1816
|
-
bounds: polylineBounds(
|
|
2290
|
+
bounds: polylineBounds(linePoints),
|
|
1817
2291
|
foregroundColor: style.color
|
|
1818
2292
|
}
|
|
1819
2293
|
];
|
|
@@ -2444,92 +2918,6 @@ function renderDrawCommands(ctx, commands, theme) {
|
|
|
2444
2918
|
return rendered;
|
|
2445
2919
|
}
|
|
2446
2920
|
|
|
2447
|
-
// src/renderers/flow-node.ts
|
|
2448
|
-
function renderFlowNode(ctx, node, bounds, theme) {
|
|
2449
|
-
const fillColor = node.color ?? theme.surfaceElevated;
|
|
2450
|
-
const borderColor = node.borderColor ?? theme.border;
|
|
2451
|
-
const borderWidth = node.borderWidth ?? 2;
|
|
2452
|
-
const cornerRadius = node.cornerRadius ?? 16;
|
|
2453
|
-
const labelColor = node.labelColor ?? theme.text;
|
|
2454
|
-
const sublabelColor = node.sublabelColor ?? theme.textMuted;
|
|
2455
|
-
const labelFontSize = node.labelFontSize ?? 20;
|
|
2456
|
-
ctx.save();
|
|
2457
|
-
ctx.globalAlpha = node.opacity;
|
|
2458
|
-
ctx.lineWidth = borderWidth;
|
|
2459
|
-
switch (node.shape) {
|
|
2460
|
-
case "box":
|
|
2461
|
-
drawRoundedRect(ctx, bounds, 0, fillColor, borderColor);
|
|
2462
|
-
break;
|
|
2463
|
-
case "rounded-box":
|
|
2464
|
-
drawRoundedRect(ctx, bounds, cornerRadius, fillColor, borderColor);
|
|
2465
|
-
break;
|
|
2466
|
-
case "diamond":
|
|
2467
|
-
drawDiamond(ctx, bounds, fillColor, borderColor);
|
|
2468
|
-
break;
|
|
2469
|
-
case "circle": {
|
|
2470
|
-
const radius = Math.min(bounds.width, bounds.height) / 2;
|
|
2471
|
-
drawCircle(
|
|
2472
|
-
ctx,
|
|
2473
|
-
{ x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 },
|
|
2474
|
-
radius,
|
|
2475
|
-
fillColor,
|
|
2476
|
-
borderColor
|
|
2477
|
-
);
|
|
2478
|
-
break;
|
|
2479
|
-
}
|
|
2480
|
-
case "pill":
|
|
2481
|
-
drawPill(ctx, bounds, fillColor, borderColor);
|
|
2482
|
-
break;
|
|
2483
|
-
case "cylinder":
|
|
2484
|
-
drawCylinder(ctx, bounds, fillColor, borderColor);
|
|
2485
|
-
break;
|
|
2486
|
-
case "parallelogram":
|
|
2487
|
-
drawParallelogram(ctx, bounds, fillColor, borderColor);
|
|
2488
|
-
break;
|
|
2489
|
-
}
|
|
2490
|
-
const headingFont = resolveFont(theme.fonts.heading, "heading");
|
|
2491
|
-
const bodyFont = resolveFont(theme.fonts.body, "body");
|
|
2492
|
-
const centerX = bounds.x + bounds.width / 2;
|
|
2493
|
-
const centerY = bounds.y + bounds.height / 2;
|
|
2494
|
-
const labelY = node.sublabel ? centerY - Math.max(4, labelFontSize * 0.2) : centerY + labelFontSize * 0.3;
|
|
2495
|
-
ctx.textAlign = "center";
|
|
2496
|
-
applyFont(ctx, { size: labelFontSize, weight: 700, family: headingFont });
|
|
2497
|
-
ctx.fillStyle = labelColor;
|
|
2498
|
-
ctx.fillText(node.label, centerX, labelY);
|
|
2499
|
-
let textBoundsY = bounds.y + bounds.height / 2 - 18;
|
|
2500
|
-
let textBoundsHeight = 36;
|
|
2501
|
-
if (node.sublabel) {
|
|
2502
|
-
const sublabelFontSize = Math.max(12, Math.round(labelFontSize * 0.68));
|
|
2503
|
-
applyFont(ctx, { size: sublabelFontSize, weight: 500, family: bodyFont });
|
|
2504
|
-
ctx.fillStyle = sublabelColor;
|
|
2505
|
-
ctx.fillText(node.sublabel, centerX, labelY + Math.max(20, sublabelFontSize + 6));
|
|
2506
|
-
textBoundsY = bounds.y + bounds.height / 2 - 24;
|
|
2507
|
-
textBoundsHeight = 56;
|
|
2508
|
-
}
|
|
2509
|
-
ctx.restore();
|
|
2510
|
-
return [
|
|
2511
|
-
{
|
|
2512
|
-
id: `flow-node-${node.id}`,
|
|
2513
|
-
kind: "flow-node",
|
|
2514
|
-
bounds,
|
|
2515
|
-
foregroundColor: labelColor,
|
|
2516
|
-
backgroundColor: fillColor
|
|
2517
|
-
},
|
|
2518
|
-
{
|
|
2519
|
-
id: `flow-node-${node.id}-label`,
|
|
2520
|
-
kind: "text",
|
|
2521
|
-
bounds: {
|
|
2522
|
-
x: bounds.x + 8,
|
|
2523
|
-
y: textBoundsY,
|
|
2524
|
-
width: bounds.width - 16,
|
|
2525
|
-
height: textBoundsHeight
|
|
2526
|
-
},
|
|
2527
|
-
foregroundColor: labelColor,
|
|
2528
|
-
backgroundColor: fillColor
|
|
2529
|
-
}
|
|
2530
|
-
];
|
|
2531
|
-
}
|
|
2532
|
-
|
|
2533
2921
|
// src/renderers/image.ts
|
|
2534
2922
|
import { loadImage } from "@napi-rs/canvas";
|
|
2535
2923
|
function roundedRectPath2(ctx, bounds, radius) {
|
|
@@ -2784,7 +3172,17 @@ function renderTextElement(ctx, textEl, bounds, theme) {
|
|
|
2784
3172
|
|
|
2785
3173
|
// src/spec.schema.ts
|
|
2786
3174
|
import { z as z2 } from "zod";
|
|
2787
|
-
var colorHexSchema2 = z2.string().
|
|
3175
|
+
var colorHexSchema2 = z2.string().refine(
|
|
3176
|
+
(v) => {
|
|
3177
|
+
try {
|
|
3178
|
+
normalizeColor(v);
|
|
3179
|
+
return true;
|
|
3180
|
+
} catch {
|
|
3181
|
+
return false;
|
|
3182
|
+
}
|
|
3183
|
+
},
|
|
3184
|
+
{ message: "Expected #RRGGBB, #RRGGBBAA, rgb(), or rgba() color" }
|
|
3185
|
+
).transform((v) => normalizeColor(v));
|
|
2788
3186
|
var gradientStopSchema = z2.object({
|
|
2789
3187
|
offset: z2.number().min(0).max(1),
|
|
2790
3188
|
color: colorHexSchema2
|
|
@@ -2968,13 +3366,32 @@ var cardElementSchema = z2.object({
|
|
|
2968
3366
|
tone: z2.enum(["neutral", "accent", "success", "warning", "error"]).default("neutral"),
|
|
2969
3367
|
icon: z2.string().min(1).max(64).optional()
|
|
2970
3368
|
}).strict();
|
|
3369
|
+
var flowNodeShadowSchema = z2.object({
|
|
3370
|
+
color: colorHexSchema2.optional(),
|
|
3371
|
+
blur: z2.number().min(0).max(64).default(8),
|
|
3372
|
+
offsetX: z2.number().min(-32).max(32).default(0),
|
|
3373
|
+
offsetY: z2.number().min(-32).max(32).default(0),
|
|
3374
|
+
opacity: z2.number().min(0).max(1).default(0.3)
|
|
3375
|
+
}).strict();
|
|
2971
3376
|
var flowNodeElementSchema = z2.object({
|
|
2972
3377
|
type: z2.literal("flow-node"),
|
|
2973
3378
|
id: z2.string().min(1).max(120),
|
|
2974
|
-
shape: z2.enum([
|
|
3379
|
+
shape: z2.enum([
|
|
3380
|
+
"box",
|
|
3381
|
+
"rounded-box",
|
|
3382
|
+
"diamond",
|
|
3383
|
+
"circle",
|
|
3384
|
+
"pill",
|
|
3385
|
+
"cylinder",
|
|
3386
|
+
"parallelogram",
|
|
3387
|
+
"hexagon"
|
|
3388
|
+
]).default("rounded-box"),
|
|
2975
3389
|
label: z2.string().min(1).max(200),
|
|
2976
3390
|
sublabel: z2.string().min(1).max(300).optional(),
|
|
2977
3391
|
sublabelColor: colorHexSchema2.optional(),
|
|
3392
|
+
sublabel2: z2.string().min(1).max(300).optional(),
|
|
3393
|
+
sublabel2Color: colorHexSchema2.optional(),
|
|
3394
|
+
sublabel2FontSize: z2.number().min(8).max(32).optional(),
|
|
2978
3395
|
labelColor: colorHexSchema2.optional(),
|
|
2979
3396
|
labelFontSize: z2.number().min(10).max(48).optional(),
|
|
2980
3397
|
color: colorHexSchema2.optional(),
|
|
@@ -2983,20 +3400,30 @@ var flowNodeElementSchema = z2.object({
|
|
|
2983
3400
|
cornerRadius: z2.number().min(0).max(64).optional(),
|
|
2984
3401
|
width: z2.number().int().min(40).max(800).optional(),
|
|
2985
3402
|
height: z2.number().int().min(30).max(600).optional(),
|
|
2986
|
-
|
|
3403
|
+
fillOpacity: z2.number().min(0).max(1).default(1),
|
|
3404
|
+
opacity: z2.number().min(0).max(1).default(1),
|
|
3405
|
+
badgeText: z2.string().min(1).max(32).optional(),
|
|
3406
|
+
badgeColor: colorHexSchema2.optional(),
|
|
3407
|
+
badgeBackground: colorHexSchema2.optional(),
|
|
3408
|
+
badgePosition: z2.enum(["top", "inside-top"]).default("inside-top"),
|
|
3409
|
+
shadow: flowNodeShadowSchema.optional()
|
|
2987
3410
|
}).strict();
|
|
2988
3411
|
var connectionElementSchema = z2.object({
|
|
2989
3412
|
type: z2.literal("connection"),
|
|
2990
3413
|
from: z2.string().min(1).max(120),
|
|
2991
3414
|
to: z2.string().min(1).max(120),
|
|
2992
3415
|
style: z2.enum(["solid", "dashed", "dotted"]).default("solid"),
|
|
3416
|
+
strokeStyle: z2.enum(["solid", "dashed", "dotted"]).default("solid"),
|
|
2993
3417
|
arrow: z2.enum(["end", "start", "both", "none"]).default("end"),
|
|
2994
3418
|
label: z2.string().min(1).max(200).optional(),
|
|
2995
3419
|
labelPosition: z2.enum(["start", "middle", "end"]).default("middle"),
|
|
2996
3420
|
color: colorHexSchema2.optional(),
|
|
2997
|
-
width: z2.number().min(0.5).max(
|
|
3421
|
+
width: z2.number().min(0.5).max(10).optional(),
|
|
3422
|
+
strokeWidth: z2.number().min(0.5).max(10).default(2),
|
|
2998
3423
|
arrowSize: z2.number().min(4).max(32).optional(),
|
|
2999
|
-
opacity: z2.number().min(0).max(1).default(1)
|
|
3424
|
+
opacity: z2.number().min(0).max(1).default(1),
|
|
3425
|
+
routing: z2.enum(["auto", "orthogonal", "curve", "arc"]).default("auto"),
|
|
3426
|
+
tension: z2.number().min(0.1).max(0.8).default(0.35)
|
|
3000
3427
|
}).strict();
|
|
3001
3428
|
var codeBlockStyleSchema = z2.object({
|
|
3002
3429
|
paddingVertical: z2.number().min(0).max(128).default(56),
|
|
@@ -3065,6 +3492,10 @@ var elementSchema = z2.discriminatedUnion("type", [
|
|
|
3065
3492
|
shapeElementSchema,
|
|
3066
3493
|
imageElementSchema
|
|
3067
3494
|
]);
|
|
3495
|
+
var diagramCenterSchema = z2.object({
|
|
3496
|
+
x: z2.number(),
|
|
3497
|
+
y: z2.number()
|
|
3498
|
+
}).strict();
|
|
3068
3499
|
var autoLayoutConfigSchema = z2.object({
|
|
3069
3500
|
mode: z2.literal("auto"),
|
|
3070
3501
|
algorithm: z2.enum(["layered", "stress", "force", "radial", "box"]).default("layered"),
|
|
@@ -3072,7 +3503,17 @@ var autoLayoutConfigSchema = z2.object({
|
|
|
3072
3503
|
nodeSpacing: z2.number().int().min(0).max(512).default(80),
|
|
3073
3504
|
rankSpacing: z2.number().int().min(0).max(512).default(120),
|
|
3074
3505
|
edgeRouting: z2.enum(["orthogonal", "polyline", "spline"]).default("polyline"),
|
|
3075
|
-
aspectRatio: z2.number().min(0.5).max(3).optional()
|
|
3506
|
+
aspectRatio: z2.number().min(0.5).max(3).optional(),
|
|
3507
|
+
/** ID of the root node for radial layout. Only relevant when algorithm is 'radial'. */
|
|
3508
|
+
radialRoot: z2.string().min(1).max(120).optional(),
|
|
3509
|
+
/** Fixed radius in pixels for radial layout. Only relevant when algorithm is 'radial'. */
|
|
3510
|
+
radialRadius: z2.number().positive().optional(),
|
|
3511
|
+
/** Compaction strategy for radial layout. Only relevant when algorithm is 'radial'. */
|
|
3512
|
+
radialCompaction: z2.enum(["none", "radial", "wedge"]).optional(),
|
|
3513
|
+
/** Sort strategy for radial layout node ordering. Only relevant when algorithm is 'radial'. */
|
|
3514
|
+
radialSortBy: z2.enum(["id", "connections"]).optional(),
|
|
3515
|
+
/** Explicit center used by curve/arc connection routing. */
|
|
3516
|
+
diagramCenter: diagramCenterSchema.optional()
|
|
3076
3517
|
}).strict();
|
|
3077
3518
|
var gridLayoutConfigSchema = z2.object({
|
|
3078
3519
|
mode: z2.literal("grid"),
|
|
@@ -3080,13 +3521,17 @@ var gridLayoutConfigSchema = z2.object({
|
|
|
3080
3521
|
gap: z2.number().int().min(0).max(256).default(24),
|
|
3081
3522
|
cardMinHeight: z2.number().int().min(32).max(4096).optional(),
|
|
3082
3523
|
cardMaxHeight: z2.number().int().min(32).max(4096).optional(),
|
|
3083
|
-
equalHeight: z2.boolean().default(false)
|
|
3524
|
+
equalHeight: z2.boolean().default(false),
|
|
3525
|
+
/** Explicit center used by curve/arc connection routing. */
|
|
3526
|
+
diagramCenter: diagramCenterSchema.optional()
|
|
3084
3527
|
}).strict();
|
|
3085
3528
|
var stackLayoutConfigSchema = z2.object({
|
|
3086
3529
|
mode: z2.literal("stack"),
|
|
3087
3530
|
direction: z2.enum(["vertical", "horizontal"]).default("vertical"),
|
|
3088
3531
|
gap: z2.number().int().min(0).max(256).default(24),
|
|
3089
|
-
alignment: z2.enum(["start", "center", "end", "stretch"]).default("stretch")
|
|
3532
|
+
alignment: z2.enum(["start", "center", "end", "stretch"]).default("stretch"),
|
|
3533
|
+
/** Explicit center used by curve/arc connection routing. */
|
|
3534
|
+
diagramCenter: diagramCenterSchema.optional()
|
|
3090
3535
|
}).strict();
|
|
3091
3536
|
var manualPositionSchema = z2.object({
|
|
3092
3537
|
x: z2.number().int(),
|
|
@@ -3096,7 +3541,9 @@ var manualPositionSchema = z2.object({
|
|
|
3096
3541
|
}).strict();
|
|
3097
3542
|
var manualLayoutConfigSchema = z2.object({
|
|
3098
3543
|
mode: z2.literal("manual"),
|
|
3099
|
-
positions: z2.record(z2.string().min(1), manualPositionSchema).default({})
|
|
3544
|
+
positions: z2.record(z2.string().min(1), manualPositionSchema).default({}),
|
|
3545
|
+
/** Explicit center used by curve/arc connection routing. */
|
|
3546
|
+
diagramCenter: diagramCenterSchema.optional()
|
|
3100
3547
|
}).strict();
|
|
3101
3548
|
var layoutConfigSchema = z2.discriminatedUnion("mode", [
|
|
3102
3549
|
autoLayoutConfigSchema,
|
|
@@ -3148,6 +3595,31 @@ var canvasSchema = z2.object({
|
|
|
3148
3595
|
padding: z2.number().int().min(0).max(256).default(defaultCanvas.padding)
|
|
3149
3596
|
}).strict();
|
|
3150
3597
|
var themeInputSchema = z2.union([builtInThemeSchema, themeSchema]);
|
|
3598
|
+
var diagramPositionSchema = z2.object({
|
|
3599
|
+
x: z2.number(),
|
|
3600
|
+
y: z2.number(),
|
|
3601
|
+
width: z2.number().positive(),
|
|
3602
|
+
height: z2.number().positive()
|
|
3603
|
+
}).strict();
|
|
3604
|
+
var diagramElementSchema = z2.discriminatedUnion("type", [
|
|
3605
|
+
flowNodeElementSchema,
|
|
3606
|
+
connectionElementSchema
|
|
3607
|
+
]);
|
|
3608
|
+
var diagramLayoutSchema = z2.object({
|
|
3609
|
+
mode: z2.enum(["manual", "auto"]).default("manual"),
|
|
3610
|
+
positions: z2.record(z2.string(), diagramPositionSchema).optional(),
|
|
3611
|
+
diagramCenter: diagramCenterSchema.optional()
|
|
3612
|
+
}).strict();
|
|
3613
|
+
var diagramSpecSchema = z2.object({
|
|
3614
|
+
version: z2.literal(1),
|
|
3615
|
+
canvas: z2.object({
|
|
3616
|
+
width: z2.number().int().min(320).max(4096).default(1200),
|
|
3617
|
+
height: z2.number().int().min(180).max(4096).default(675)
|
|
3618
|
+
}).default({ width: 1200, height: 675 }),
|
|
3619
|
+
theme: themeSchema.optional(),
|
|
3620
|
+
elements: z2.array(diagramElementSchema).min(1),
|
|
3621
|
+
layout: diagramLayoutSchema.default({ mode: "manual" })
|
|
3622
|
+
}).strict();
|
|
3151
3623
|
var designSpecSchema = z2.object({
|
|
3152
3624
|
version: z2.literal(2).default(2),
|
|
3153
3625
|
canvas: canvasSchema.default(defaultCanvas),
|
|
@@ -3507,6 +3979,10 @@ async function renderDesign(input, options = {}) {
|
|
|
3507
3979
|
break;
|
|
3508
3980
|
}
|
|
3509
3981
|
}
|
|
3982
|
+
const diagramCenter = spec.layout.diagramCenter ?? computeDiagramCenter(
|
|
3983
|
+
spec.elements.filter((element) => element.type !== "connection").map((element) => elementRects.get(element.id)).filter((rect) => rect != null),
|
|
3984
|
+
{ x: spec.canvas.width / 2, y: spec.canvas.height / 2 }
|
|
3985
|
+
);
|
|
3510
3986
|
for (const element of spec.elements) {
|
|
3511
3987
|
if (element.type !== "connection") {
|
|
3512
3988
|
continue;
|
|
@@ -3519,7 +3995,9 @@ async function renderDesign(input, options = {}) {
|
|
|
3519
3995
|
);
|
|
3520
3996
|
}
|
|
3521
3997
|
const edgeRoute = edgeRoutes?.get(`${element.from}-${element.to}`);
|
|
3522
|
-
elements.push(
|
|
3998
|
+
elements.push(
|
|
3999
|
+
...renderConnection(ctx, element, fromRect, toRect, theme, edgeRoute, { diagramCenter })
|
|
4000
|
+
);
|
|
3523
4001
|
}
|
|
3524
4002
|
if (footerRect && spec.footer) {
|
|
3525
4003
|
const footerText = spec.footer.tagline ? `${spec.footer.text} \u2022 ${spec.footer.tagline}` : spec.footer.text;
|