@spectratools/graphic-designer-cli 0.8.0 → 0.10.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/cli.js +273 -19
- package/dist/index.d.ts +6 -6
- package/dist/index.js +273 -19
- package/dist/qa.d.ts +1 -1
- package/dist/qa.js +18 -0
- package/dist/renderer.d.ts +1 -1
- package/dist/renderer.js +214 -17
- package/dist/{spec.schema-B_Z-KNqt.d.ts → spec.schema-B6sXTTou.d.ts} +1664 -1314
- package/dist/spec.schema.d.ts +1 -1
- package/dist/spec.schema.js +18 -0
- package/package.json +1 -1
package/dist/renderer.js
CHANGED
|
@@ -473,6 +473,38 @@ function renderFlowNode(ctx, node, bounds, theme) {
|
|
|
473
473
|
ctx.shadowOffsetX = 0;
|
|
474
474
|
ctx.shadowOffsetY = 0;
|
|
475
475
|
}
|
|
476
|
+
if (node.accentColor) {
|
|
477
|
+
const barWidth = node.accentBarWidth ?? 3;
|
|
478
|
+
const effectiveRadius = node.shape === "box" ? 0 : cornerRadius;
|
|
479
|
+
ctx.save();
|
|
480
|
+
ctx.beginPath();
|
|
481
|
+
ctx.roundRect(bounds.x, bounds.y, bounds.width, bounds.height, effectiveRadius);
|
|
482
|
+
ctx.clip();
|
|
483
|
+
ctx.fillStyle = node.accentColor;
|
|
484
|
+
ctx.fillRect(bounds.x, bounds.y, barWidth, bounds.height);
|
|
485
|
+
ctx.restore();
|
|
486
|
+
}
|
|
487
|
+
if (node.glowColor) {
|
|
488
|
+
const glowW = node.glowWidth ?? 16;
|
|
489
|
+
const glowOp = node.glowOpacity ?? 0.15;
|
|
490
|
+
ctx.save();
|
|
491
|
+
ctx.beginPath();
|
|
492
|
+
ctx.roundRect(bounds.x, bounds.y, bounds.width, bounds.height, cornerRadius);
|
|
493
|
+
ctx.clip();
|
|
494
|
+
const barOffset = node.accentColor ? node.accentBarWidth ?? 3 : 0;
|
|
495
|
+
const gradient = ctx.createLinearGradient(
|
|
496
|
+
bounds.x + barOffset,
|
|
497
|
+
bounds.y,
|
|
498
|
+
bounds.x + barOffset + glowW,
|
|
499
|
+
bounds.y
|
|
500
|
+
);
|
|
501
|
+
gradient.addColorStop(0, node.glowColor);
|
|
502
|
+
gradient.addColorStop(1, "rgba(0,0,0,0)");
|
|
503
|
+
ctx.globalAlpha = glowOp;
|
|
504
|
+
ctx.fillStyle = gradient;
|
|
505
|
+
ctx.fillRect(bounds.x, bounds.y, bounds.width, bounds.height);
|
|
506
|
+
ctx.restore();
|
|
507
|
+
}
|
|
476
508
|
const headingFont = resolveFont(theme.fonts.heading, "heading");
|
|
477
509
|
const bodyFont = resolveFont(theme.fonts.body, "body");
|
|
478
510
|
const monoFont = resolveFont(theme.fonts.mono, "mono");
|
|
@@ -1976,16 +2008,6 @@ function drawBezier(ctx, points, style) {
|
|
|
1976
2008
|
ctx.quadraticCurveTo(penultimate.x, penultimate.y, last.x, last.y);
|
|
1977
2009
|
ctx.stroke();
|
|
1978
2010
|
}
|
|
1979
|
-
function drawOrthogonalPath(ctx, from, to, style) {
|
|
1980
|
-
const midX = (from.x + to.x) / 2;
|
|
1981
|
-
applyLineStyle(ctx, style);
|
|
1982
|
-
ctx.beginPath();
|
|
1983
|
-
ctx.moveTo(from.x, from.y);
|
|
1984
|
-
ctx.lineTo(midX, from.y);
|
|
1985
|
-
ctx.lineTo(midX, to.y);
|
|
1986
|
-
ctx.lineTo(to.x, to.y);
|
|
1987
|
-
ctx.stroke();
|
|
1988
|
-
}
|
|
1989
2011
|
|
|
1990
2012
|
// src/renderers/connection.ts
|
|
1991
2013
|
var ELLIPSE_KAPPA = 4 * (Math.sqrt(2) - 1) / 3;
|
|
@@ -2225,11 +2247,36 @@ function pointAlongPolyline(points, t) {
|
|
|
2225
2247
|
}
|
|
2226
2248
|
return points[points.length - 1];
|
|
2227
2249
|
}
|
|
2228
|
-
function
|
|
2250
|
+
function createConnectionGradient(ctx, start, end, fromColor, baseColor, toColor) {
|
|
2251
|
+
const gradient = ctx.createLinearGradient(start.x, start.y, end.x, end.y);
|
|
2252
|
+
gradient.addColorStop(0, fromColor);
|
|
2253
|
+
gradient.addColorStop(0.5, baseColor);
|
|
2254
|
+
gradient.addColorStop(1, toColor);
|
|
2255
|
+
return gradient;
|
|
2256
|
+
}
|
|
2257
|
+
function resolveConnectionStroke(ctx, start, end, fromColor, baseColor, toColor) {
|
|
2258
|
+
if (!fromColor || !toColor) {
|
|
2259
|
+
return baseColor;
|
|
2260
|
+
}
|
|
2261
|
+
return createConnectionGradient(ctx, start, end, fromColor, baseColor, toColor);
|
|
2262
|
+
}
|
|
2263
|
+
function drawOrthogonalPathWithStroke(ctx, from, to, style, stroke) {
|
|
2264
|
+
const midX = (from.x + to.x) / 2;
|
|
2265
|
+
ctx.strokeStyle = stroke;
|
|
2266
|
+
ctx.lineWidth = style.width;
|
|
2267
|
+
ctx.setLineDash(style.dash ?? []);
|
|
2268
|
+
ctx.beginPath();
|
|
2269
|
+
ctx.moveTo(from.x, from.y);
|
|
2270
|
+
ctx.lineTo(midX, from.y);
|
|
2271
|
+
ctx.lineTo(midX, to.y);
|
|
2272
|
+
ctx.lineTo(to.x, to.y);
|
|
2273
|
+
ctx.stroke();
|
|
2274
|
+
}
|
|
2275
|
+
function drawCubicInterpolatedPath(ctx, points, style, stroke) {
|
|
2229
2276
|
if (points.length < 2) {
|
|
2230
2277
|
return;
|
|
2231
2278
|
}
|
|
2232
|
-
ctx.strokeStyle =
|
|
2279
|
+
ctx.strokeStyle = stroke;
|
|
2233
2280
|
ctx.lineWidth = style.width;
|
|
2234
2281
|
ctx.setLineDash(style.dash ?? []);
|
|
2235
2282
|
ctx.beginPath();
|
|
@@ -2300,7 +2347,8 @@ function renderConnection(ctx, conn, fromBounds, toBounds, theme, edgeRoute, opt
|
|
|
2300
2347
|
conn.fromAnchor,
|
|
2301
2348
|
conn.toAnchor
|
|
2302
2349
|
);
|
|
2303
|
-
ctx.
|
|
2350
|
+
const stroke = resolveConnectionStroke(ctx, p0, p3, conn.fromColor, style.color, conn.toColor);
|
|
2351
|
+
ctx.strokeStyle = stroke;
|
|
2304
2352
|
ctx.lineWidth = style.width;
|
|
2305
2353
|
ctx.setLineDash(style.dash ?? []);
|
|
2306
2354
|
ctx.beginPath();
|
|
@@ -2342,7 +2390,8 @@ function renderConnection(ctx, conn, fromBounds, toBounds, theme, edgeRoute, opt
|
|
|
2342
2390
|
);
|
|
2343
2391
|
const [p0, cp1, cp2, pMid] = first;
|
|
2344
2392
|
const [, cp3, cp4, p3] = second;
|
|
2345
|
-
ctx.
|
|
2393
|
+
const stroke = resolveConnectionStroke(ctx, p0, p3, conn.fromColor, style.color, conn.toColor);
|
|
2394
|
+
ctx.strokeStyle = stroke;
|
|
2346
2395
|
ctx.lineWidth = style.width;
|
|
2347
2396
|
ctx.setLineDash(style.dash ?? []);
|
|
2348
2397
|
ctx.beginPath();
|
|
@@ -2385,10 +2434,18 @@ function renderConnection(ctx, conn, fromBounds, toBounds, theme, edgeRoute, opt
|
|
|
2385
2434
|
endPoint = linePoints[linePoints.length - 1] ?? linePoints[0];
|
|
2386
2435
|
startAngle = Math.atan2(startSegment.y - linePoints[0].y, startSegment.x - linePoints[0].x) + Math.PI;
|
|
2387
2436
|
endAngle = Math.atan2(endPoint.y - endStart.y, endPoint.x - endStart.x);
|
|
2437
|
+
const stroke = resolveConnectionStroke(
|
|
2438
|
+
ctx,
|
|
2439
|
+
startPoint,
|
|
2440
|
+
endPoint,
|
|
2441
|
+
conn.fromColor,
|
|
2442
|
+
style.color,
|
|
2443
|
+
conn.toColor
|
|
2444
|
+
);
|
|
2388
2445
|
if (useElkRoute) {
|
|
2389
|
-
drawCubicInterpolatedPath(ctx, linePoints, style);
|
|
2446
|
+
drawCubicInterpolatedPath(ctx, linePoints, style, stroke);
|
|
2390
2447
|
} else {
|
|
2391
|
-
|
|
2448
|
+
drawOrthogonalPathWithStroke(ctx, startPoint, endPoint, style, stroke);
|
|
2392
2449
|
}
|
|
2393
2450
|
labelPoint = pointAlongPolyline(linePoints, labelT);
|
|
2394
2451
|
}
|
|
@@ -2693,6 +2750,9 @@ function measureTextBounds(ctx, options) {
|
|
|
2693
2750
|
function angleBetween(from, to) {
|
|
2694
2751
|
return Math.atan2(to.y - from.y, to.x - from.x);
|
|
2695
2752
|
}
|
|
2753
|
+
function degreesToRadians(angle) {
|
|
2754
|
+
return angle * Math.PI / 180;
|
|
2755
|
+
}
|
|
2696
2756
|
function pathBounds(operations) {
|
|
2697
2757
|
let minX = Number.POSITIVE_INFINITY;
|
|
2698
2758
|
let minY = Number.POSITIVE_INFINITY;
|
|
@@ -2930,6 +2990,34 @@ function renderDrawCommands(ctx, commands, theme) {
|
|
|
2930
2990
|
});
|
|
2931
2991
|
break;
|
|
2932
2992
|
}
|
|
2993
|
+
case "arc": {
|
|
2994
|
+
const startAngle = degreesToRadians(command.startAngle);
|
|
2995
|
+
const endAngle = degreesToRadians(command.endAngle);
|
|
2996
|
+
withOpacity(ctx, command.opacity, () => {
|
|
2997
|
+
applyDrawShadow(ctx, command.shadow);
|
|
2998
|
+
ctx.beginPath();
|
|
2999
|
+
ctx.setLineDash(command.dash ?? []);
|
|
3000
|
+
ctx.lineWidth = command.width;
|
|
3001
|
+
ctx.strokeStyle = command.color;
|
|
3002
|
+
ctx.arc(command.center.x, command.center.y, command.radius, startAngle, endAngle);
|
|
3003
|
+
ctx.stroke();
|
|
3004
|
+
});
|
|
3005
|
+
rendered.push({
|
|
3006
|
+
id,
|
|
3007
|
+
kind: "draw",
|
|
3008
|
+
bounds: expandRect(
|
|
3009
|
+
{
|
|
3010
|
+
x: command.center.x - command.radius,
|
|
3011
|
+
y: command.center.y - command.radius,
|
|
3012
|
+
width: command.radius * 2,
|
|
3013
|
+
height: command.radius * 2
|
|
3014
|
+
},
|
|
3015
|
+
command.width / 2
|
|
3016
|
+
),
|
|
3017
|
+
foregroundColor: command.color
|
|
3018
|
+
});
|
|
3019
|
+
break;
|
|
3020
|
+
}
|
|
2933
3021
|
case "bezier": {
|
|
2934
3022
|
const points = command.points;
|
|
2935
3023
|
withOpacity(ctx, command.opacity, () => {
|
|
@@ -3086,6 +3174,84 @@ function renderDrawCommands(ctx, commands, theme) {
|
|
|
3086
3174
|
});
|
|
3087
3175
|
break;
|
|
3088
3176
|
}
|
|
3177
|
+
case "text-row": {
|
|
3178
|
+
const segments = command.segments;
|
|
3179
|
+
if (segments.length === 0) break;
|
|
3180
|
+
const resolveSegment = (seg) => ({
|
|
3181
|
+
text: seg.text,
|
|
3182
|
+
fontSize: seg.fontSize ?? command.defaultFontSize,
|
|
3183
|
+
fontWeight: seg.fontWeight ?? command.defaultFontWeight,
|
|
3184
|
+
fontFamily: resolveDrawFont(theme, seg.fontFamily ?? command.defaultFontFamily),
|
|
3185
|
+
color: seg.color ?? command.defaultColor
|
|
3186
|
+
});
|
|
3187
|
+
const measured = [];
|
|
3188
|
+
let totalWidth = 0;
|
|
3189
|
+
let maxAscent = 0;
|
|
3190
|
+
let maxDescent = 0;
|
|
3191
|
+
for (const seg of segments) {
|
|
3192
|
+
const resolved = resolveSegment(seg);
|
|
3193
|
+
applyFont(ctx, {
|
|
3194
|
+
size: resolved.fontSize,
|
|
3195
|
+
weight: resolved.fontWeight,
|
|
3196
|
+
family: resolved.fontFamily
|
|
3197
|
+
});
|
|
3198
|
+
const metrics = ctx.measureText(resolved.text);
|
|
3199
|
+
const width = metrics.width;
|
|
3200
|
+
const ascent = metrics.actualBoundingBoxAscent || 0;
|
|
3201
|
+
const descent = metrics.actualBoundingBoxDescent || 0;
|
|
3202
|
+
totalWidth += width;
|
|
3203
|
+
maxAscent = Math.max(maxAscent, ascent);
|
|
3204
|
+
maxDescent = Math.max(maxDescent, descent);
|
|
3205
|
+
measured.push({ width, resolved });
|
|
3206
|
+
}
|
|
3207
|
+
let cursorX;
|
|
3208
|
+
if (command.align === "center") {
|
|
3209
|
+
cursorX = command.x - totalWidth / 2;
|
|
3210
|
+
} else if (command.align === "right") {
|
|
3211
|
+
cursorX = command.x - totalWidth;
|
|
3212
|
+
} else {
|
|
3213
|
+
cursorX = command.x;
|
|
3214
|
+
}
|
|
3215
|
+
const startX = cursorX;
|
|
3216
|
+
withOpacity(ctx, command.opacity, () => {
|
|
3217
|
+
ctx.textBaseline = command.baseline;
|
|
3218
|
+
for (const { width, resolved } of measured) {
|
|
3219
|
+
applyFont(ctx, {
|
|
3220
|
+
size: resolved.fontSize,
|
|
3221
|
+
weight: resolved.fontWeight,
|
|
3222
|
+
family: resolved.fontFamily
|
|
3223
|
+
});
|
|
3224
|
+
ctx.fillStyle = resolved.color;
|
|
3225
|
+
ctx.textAlign = "left";
|
|
3226
|
+
ctx.fillText(resolved.text, cursorX, command.y);
|
|
3227
|
+
cursorX += width;
|
|
3228
|
+
}
|
|
3229
|
+
});
|
|
3230
|
+
const height = Math.max(1, maxAscent + maxDescent);
|
|
3231
|
+
let topY;
|
|
3232
|
+
if (command.baseline === "top") {
|
|
3233
|
+
topY = command.y;
|
|
3234
|
+
} else if (command.baseline === "middle") {
|
|
3235
|
+
topY = command.y - height / 2;
|
|
3236
|
+
} else if (command.baseline === "bottom") {
|
|
3237
|
+
topY = command.y - height;
|
|
3238
|
+
} else {
|
|
3239
|
+
topY = command.y - maxAscent;
|
|
3240
|
+
}
|
|
3241
|
+
rendered.push({
|
|
3242
|
+
id,
|
|
3243
|
+
kind: "draw",
|
|
3244
|
+
bounds: {
|
|
3245
|
+
x: startX,
|
|
3246
|
+
y: topY,
|
|
3247
|
+
width: Math.max(1, totalWidth),
|
|
3248
|
+
height
|
|
3249
|
+
},
|
|
3250
|
+
foregroundColor: command.defaultColor,
|
|
3251
|
+
backgroundColor: theme.background
|
|
3252
|
+
});
|
|
3253
|
+
break;
|
|
3254
|
+
}
|
|
3089
3255
|
}
|
|
3090
3256
|
}
|
|
3091
3257
|
return rendered;
|
|
@@ -3435,6 +3601,21 @@ var drawLineSchema = z2.object({
|
|
|
3435
3601
|
opacity: z2.number().min(0).max(1).default(1),
|
|
3436
3602
|
shadow: drawShadowSchema.optional()
|
|
3437
3603
|
}).strict();
|
|
3604
|
+
var drawArcSchema = z2.object({
|
|
3605
|
+
type: z2.literal("arc"),
|
|
3606
|
+
center: z2.object({
|
|
3607
|
+
x: z2.number(),
|
|
3608
|
+
y: z2.number()
|
|
3609
|
+
}).strict(),
|
|
3610
|
+
radius: z2.number().positive(),
|
|
3611
|
+
startAngle: z2.number(),
|
|
3612
|
+
endAngle: z2.number(),
|
|
3613
|
+
color: colorHexSchema2.default("#FFFFFF"),
|
|
3614
|
+
width: z2.number().min(0.5).max(32).default(2),
|
|
3615
|
+
dash: z2.array(z2.number()).max(6).optional(),
|
|
3616
|
+
opacity: z2.number().min(0).max(1).default(1),
|
|
3617
|
+
shadow: drawShadowSchema.optional()
|
|
3618
|
+
}).strict();
|
|
3438
3619
|
var drawPointSchema = z2.object({
|
|
3439
3620
|
x: z2.number(),
|
|
3440
3621
|
y: z2.number()
|
|
@@ -3519,6 +3700,7 @@ var drawCommandSchema = z2.discriminatedUnion("type", [
|
|
|
3519
3700
|
drawCircleSchema,
|
|
3520
3701
|
drawTextSchema,
|
|
3521
3702
|
drawLineSchema,
|
|
3703
|
+
drawArcSchema,
|
|
3522
3704
|
drawBezierSchema,
|
|
3523
3705
|
drawPathSchema,
|
|
3524
3706
|
drawBadgeSchema,
|
|
@@ -3654,6 +3836,8 @@ var connectionElementSchema = z2.object({
|
|
|
3654
3836
|
label: z2.string().min(1).max(200).optional(),
|
|
3655
3837
|
labelPosition: z2.enum(["start", "middle", "end"]).default("middle"),
|
|
3656
3838
|
color: colorHexSchema2.optional(),
|
|
3839
|
+
fromColor: colorHexSchema2.optional(),
|
|
3840
|
+
toColor: colorHexSchema2.optional(),
|
|
3657
3841
|
width: z2.number().min(0.5).max(10).optional(),
|
|
3658
3842
|
strokeWidth: z2.number().min(0.5).max(10).default(2),
|
|
3659
3843
|
arrowSize: z2.number().min(4).max(32).optional(),
|
|
@@ -4039,6 +4223,18 @@ async function renderDesign(input, options = {}) {
|
|
|
4039
4223
|
const specHash = computeSpecHash(spec);
|
|
4040
4224
|
const generatorVersion = options.generatorVersion ?? DEFAULT_GENERATOR_VERSION;
|
|
4041
4225
|
const renderedAt = options.renderedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
4226
|
+
const iteration = options.iteration;
|
|
4227
|
+
if (iteration) {
|
|
4228
|
+
if (!Number.isInteger(iteration.iteration) || iteration.iteration <= 0) {
|
|
4229
|
+
throw new Error("Iteration metadata requires iteration to be a positive integer.");
|
|
4230
|
+
}
|
|
4231
|
+
if (iteration.maxIterations != null && (!Number.isInteger(iteration.maxIterations) || iteration.maxIterations <= 0)) {
|
|
4232
|
+
throw new Error("Iteration metadata requires maxIterations to be a positive integer.");
|
|
4233
|
+
}
|
|
4234
|
+
if (iteration.maxIterations != null && iteration.maxIterations < iteration.iteration) {
|
|
4235
|
+
throw new Error("Iteration metadata requires maxIterations to be >= iteration.");
|
|
4236
|
+
}
|
|
4237
|
+
}
|
|
4042
4238
|
const renderScale = resolveRenderScale(spec);
|
|
4043
4239
|
const canvas = createCanvas(spec.canvas.width * renderScale, spec.canvas.height * renderScale);
|
|
4044
4240
|
const ctx = canvas.getContext("2d");
|
|
@@ -4279,7 +4475,8 @@ async function renderDesign(input, options = {}) {
|
|
|
4279
4475
|
layout: {
|
|
4280
4476
|
safeFrame,
|
|
4281
4477
|
elements
|
|
4282
|
-
}
|
|
4478
|
+
},
|
|
4479
|
+
...iteration ? { iteration } : {}
|
|
4283
4480
|
};
|
|
4284
4481
|
return {
|
|
4285
4482
|
png: pngBuffer,
|