@spectratools/graphic-designer-cli 0.9.0 → 0.11.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 +321 -135
- package/dist/index.d.ts +48 -8
- package/dist/index.js +325 -85
- package/dist/qa.d.ts +1 -1
- package/dist/qa.js +47 -7
- package/dist/renderer.d.ts +1 -1
- package/dist/renderer.js +262 -133
- package/dist/{spec.schema-B_Z-KNqt.d.ts → spec.schema-CYlOLxmK.d.ts} +573 -49
- package/dist/spec.schema.d.ts +1 -1
- package/dist/spec.schema.js +47 -7
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -820,6 +820,21 @@ var drawLineSchema = z2.object({
|
|
|
820
820
|
opacity: z2.number().min(0).max(1).default(1),
|
|
821
821
|
shadow: drawShadowSchema.optional()
|
|
822
822
|
}).strict();
|
|
823
|
+
var drawArcSchema = z2.object({
|
|
824
|
+
type: z2.literal("arc"),
|
|
825
|
+
center: z2.object({
|
|
826
|
+
x: z2.number(),
|
|
827
|
+
y: z2.number()
|
|
828
|
+
}).strict(),
|
|
829
|
+
radius: z2.number().positive(),
|
|
830
|
+
startAngle: z2.number(),
|
|
831
|
+
endAngle: z2.number(),
|
|
832
|
+
color: colorHexSchema2.default("#FFFFFF"),
|
|
833
|
+
width: z2.number().min(0.5).max(32).default(2),
|
|
834
|
+
dash: z2.array(z2.number()).max(6).optional(),
|
|
835
|
+
opacity: z2.number().min(0).max(1).default(1),
|
|
836
|
+
shadow: drawShadowSchema.optional()
|
|
837
|
+
}).strict();
|
|
823
838
|
var drawPointSchema = z2.object({
|
|
824
839
|
x: z2.number(),
|
|
825
840
|
y: z2.number()
|
|
@@ -904,6 +919,7 @@ var drawCommandSchema = z2.discriminatedUnion("type", [
|
|
|
904
919
|
drawCircleSchema,
|
|
905
920
|
drawTextSchema,
|
|
906
921
|
drawLineSchema,
|
|
922
|
+
drawArcSchema,
|
|
907
923
|
drawBezierSchema,
|
|
908
924
|
drawPathSchema,
|
|
909
925
|
drawBadgeSchema,
|
|
@@ -1034,17 +1050,21 @@ var connectionElementSchema = z2.object({
|
|
|
1034
1050
|
from: z2.string().min(1).max(120),
|
|
1035
1051
|
to: z2.string().min(1).max(120),
|
|
1036
1052
|
style: z2.enum(["solid", "dashed", "dotted"]).default("solid"),
|
|
1037
|
-
|
|
1053
|
+
/** @deprecated Use `style` instead. */
|
|
1054
|
+
strokeStyle: z2.enum(["solid", "dashed", "dotted"]).optional(),
|
|
1038
1055
|
arrow: z2.enum(["end", "start", "both", "none"]).default("end"),
|
|
1039
1056
|
label: z2.string().min(1).max(200).optional(),
|
|
1040
1057
|
labelPosition: z2.enum(["start", "middle", "end"]).default("middle"),
|
|
1041
1058
|
color: colorHexSchema2.optional(),
|
|
1059
|
+
fromColor: colorHexSchema2.optional(),
|
|
1060
|
+
toColor: colorHexSchema2.optional(),
|
|
1042
1061
|
width: z2.number().min(0.5).max(10).optional(),
|
|
1043
1062
|
strokeWidth: z2.number().min(0.5).max(10).default(2),
|
|
1044
1063
|
arrowSize: z2.number().min(4).max(32).optional(),
|
|
1045
1064
|
arrowPlacement: z2.enum(["endpoint", "boundary"]).default("endpoint"),
|
|
1046
1065
|
opacity: z2.number().min(0).max(1).default(1),
|
|
1047
|
-
routing: z2.enum(["auto", "orthogonal", "curve", "arc"]).default("auto"),
|
|
1066
|
+
routing: z2.enum(["auto", "orthogonal", "curve", "arc", "straight"]).default("auto"),
|
|
1067
|
+
curveMode: z2.enum(["normal", "ellipse"]).default("normal"),
|
|
1048
1068
|
tension: z2.number().min(0.1).max(0.8).default(0.35),
|
|
1049
1069
|
fromAnchor: anchorHintSchema.optional(),
|
|
1050
1070
|
toAnchor: anchorHintSchema.optional()
|
|
@@ -1137,7 +1157,11 @@ var autoLayoutConfigSchema = z2.object({
|
|
|
1137
1157
|
/** Sort strategy for radial layout node ordering. Only relevant when algorithm is 'radial'. */
|
|
1138
1158
|
radialSortBy: z2.enum(["id", "connections"]).optional(),
|
|
1139
1159
|
/** Explicit center used by curve/arc connection routing. */
|
|
1140
|
-
diagramCenter: diagramCenterSchema.optional()
|
|
1160
|
+
diagramCenter: diagramCenterSchema.optional(),
|
|
1161
|
+
/** Horizontal radius for shared ellipse used by curveMode: 'ellipse'. */
|
|
1162
|
+
ellipseRx: z2.number().positive().optional(),
|
|
1163
|
+
/** Vertical radius for shared ellipse used by curveMode: 'ellipse'. */
|
|
1164
|
+
ellipseRy: z2.number().positive().optional()
|
|
1141
1165
|
}).strict();
|
|
1142
1166
|
var gridLayoutConfigSchema = z2.object({
|
|
1143
1167
|
mode: z2.literal("grid"),
|
|
@@ -1147,7 +1171,11 @@ var gridLayoutConfigSchema = z2.object({
|
|
|
1147
1171
|
cardMaxHeight: z2.number().int().min(32).max(4096).optional(),
|
|
1148
1172
|
equalHeight: z2.boolean().default(false),
|
|
1149
1173
|
/** Explicit center used by curve/arc connection routing. */
|
|
1150
|
-
diagramCenter: diagramCenterSchema.optional()
|
|
1174
|
+
diagramCenter: diagramCenterSchema.optional(),
|
|
1175
|
+
/** Horizontal radius for shared ellipse used by curveMode: 'ellipse'. */
|
|
1176
|
+
ellipseRx: z2.number().positive().optional(),
|
|
1177
|
+
/** Vertical radius for shared ellipse used by curveMode: 'ellipse'. */
|
|
1178
|
+
ellipseRy: z2.number().positive().optional()
|
|
1151
1179
|
}).strict();
|
|
1152
1180
|
var stackLayoutConfigSchema = z2.object({
|
|
1153
1181
|
mode: z2.literal("stack"),
|
|
@@ -1155,7 +1183,11 @@ var stackLayoutConfigSchema = z2.object({
|
|
|
1155
1183
|
gap: z2.number().int().min(0).max(256).default(24),
|
|
1156
1184
|
alignment: z2.enum(["start", "center", "end", "stretch"]).default("stretch"),
|
|
1157
1185
|
/** Explicit center used by curve/arc connection routing. */
|
|
1158
|
-
diagramCenter: diagramCenterSchema.optional()
|
|
1186
|
+
diagramCenter: diagramCenterSchema.optional(),
|
|
1187
|
+
/** Horizontal radius for shared ellipse used by curveMode: 'ellipse'. */
|
|
1188
|
+
ellipseRx: z2.number().positive().optional(),
|
|
1189
|
+
/** Vertical radius for shared ellipse used by curveMode: 'ellipse'. */
|
|
1190
|
+
ellipseRy: z2.number().positive().optional()
|
|
1159
1191
|
}).strict();
|
|
1160
1192
|
var manualPositionSchema = z2.object({
|
|
1161
1193
|
x: z2.number().int(),
|
|
@@ -1167,7 +1199,11 @@ var manualLayoutConfigSchema = z2.object({
|
|
|
1167
1199
|
mode: z2.literal("manual"),
|
|
1168
1200
|
positions: z2.record(z2.string().min(1), manualPositionSchema).default({}),
|
|
1169
1201
|
/** Explicit center used by curve/arc connection routing. */
|
|
1170
|
-
diagramCenter: diagramCenterSchema.optional()
|
|
1202
|
+
diagramCenter: diagramCenterSchema.optional(),
|
|
1203
|
+
/** Horizontal radius for shared ellipse used by curveMode: 'ellipse'. */
|
|
1204
|
+
ellipseRx: z2.number().positive().optional(),
|
|
1205
|
+
/** Vertical radius for shared ellipse used by curveMode: 'ellipse'. */
|
|
1206
|
+
ellipseRy: z2.number().positive().optional()
|
|
1171
1207
|
}).strict();
|
|
1172
1208
|
var layoutConfigSchema = z2.discriminatedUnion("mode", [
|
|
1173
1209
|
autoLayoutConfigSchema,
|
|
@@ -1232,7 +1268,11 @@ var diagramElementSchema = z2.discriminatedUnion("type", [
|
|
|
1232
1268
|
var diagramLayoutSchema = z2.object({
|
|
1233
1269
|
mode: z2.enum(["manual", "auto"]).default("manual"),
|
|
1234
1270
|
positions: z2.record(z2.string(), diagramPositionSchema).optional(),
|
|
1235
|
-
diagramCenter: diagramCenterSchema.optional()
|
|
1271
|
+
diagramCenter: diagramCenterSchema.optional(),
|
|
1272
|
+
/** Horizontal radius for shared ellipse used by curveMode: 'ellipse'. */
|
|
1273
|
+
ellipseRx: z2.number().positive().optional(),
|
|
1274
|
+
/** Vertical radius for shared ellipse used by curveMode: 'ellipse'. */
|
|
1275
|
+
ellipseRy: z2.number().positive().optional()
|
|
1236
1276
|
}).strict();
|
|
1237
1277
|
var diagramSpecSchema = z2.object({
|
|
1238
1278
|
version: z2.literal(1),
|
|
@@ -2760,12 +2800,12 @@ var MACOS_DOTS = [
|
|
|
2760
2800
|
{ fill: "#27C93F", stroke: "#1AAB29" }
|
|
2761
2801
|
];
|
|
2762
2802
|
function drawMacosDots(ctx, x, y) {
|
|
2763
|
-
for (const [index,
|
|
2803
|
+
for (const [index, dot] of MACOS_DOTS.entries()) {
|
|
2764
2804
|
ctx.beginPath();
|
|
2765
2805
|
ctx.arc(x + index * DOT_SPACING, y, DOT_RADIUS, 0, Math.PI * 2);
|
|
2766
2806
|
ctx.closePath();
|
|
2767
|
-
ctx.fillStyle =
|
|
2768
|
-
ctx.strokeStyle =
|
|
2807
|
+
ctx.fillStyle = dot.fill;
|
|
2808
|
+
ctx.strokeStyle = dot.stroke;
|
|
2769
2809
|
ctx.lineWidth = DOT_STROKE_WIDTH;
|
|
2770
2810
|
ctx.fill();
|
|
2771
2811
|
ctx.stroke();
|
|
@@ -3170,16 +3210,6 @@ function drawBezier(ctx, points, style) {
|
|
|
3170
3210
|
ctx.quadraticCurveTo(penultimate.x, penultimate.y, last.x, last.y);
|
|
3171
3211
|
ctx.stroke();
|
|
3172
3212
|
}
|
|
3173
|
-
function drawOrthogonalPath(ctx, from, to, style) {
|
|
3174
|
-
const midX = (from.x + to.x) / 2;
|
|
3175
|
-
applyLineStyle(ctx, style);
|
|
3176
|
-
ctx.beginPath();
|
|
3177
|
-
ctx.moveTo(from.x, from.y);
|
|
3178
|
-
ctx.lineTo(midX, from.y);
|
|
3179
|
-
ctx.lineTo(midX, to.y);
|
|
3180
|
-
ctx.lineTo(to.x, to.y);
|
|
3181
|
-
ctx.stroke();
|
|
3182
|
-
}
|
|
3183
3213
|
|
|
3184
3214
|
// src/renderers/connection.ts
|
|
3185
3215
|
var ELLIPSE_KAPPA = 4 * (Math.sqrt(2) - 1) / 3;
|
|
@@ -3262,56 +3292,71 @@ function curveRoute(fromBounds, toBounds, diagramCenter, tension, fromAnchor, to
|
|
|
3262
3292
|
const cp2 = { x: p3.x + n3.x * offset, y: p3.y + n3.y * offset };
|
|
3263
3293
|
return [p0, cp1, cp2, p3];
|
|
3264
3294
|
}
|
|
3265
|
-
function
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3295
|
+
function inferEllipseParams(nodeBounds, explicitCenter, explicitRx, explicitRy) {
|
|
3296
|
+
if (nodeBounds.length === 0) {
|
|
3297
|
+
return {
|
|
3298
|
+
cx: explicitCenter?.x ?? 0,
|
|
3299
|
+
cy: explicitCenter?.y ?? 0,
|
|
3300
|
+
rx: explicitRx ?? 1,
|
|
3301
|
+
ry: explicitRy ?? 1
|
|
3302
|
+
};
|
|
3303
|
+
}
|
|
3304
|
+
const centers = nodeBounds.map(rectCenter);
|
|
3305
|
+
const cx = explicitCenter?.x ?? centers.reduce((sum, c) => sum + c.x, 0) / centers.length;
|
|
3306
|
+
const cy = explicitCenter?.y ?? centers.reduce((sum, c) => sum + c.y, 0) / centers.length;
|
|
3307
|
+
const rx = explicitRx ?? Math.max(1, ...centers.map((c) => Math.abs(c.x - cx)));
|
|
3308
|
+
const ry = explicitRy ?? Math.max(1, ...centers.map((c) => Math.abs(c.y - cy)));
|
|
3309
|
+
return { cx, cy, rx, ry };
|
|
3273
3310
|
}
|
|
3274
|
-
function
|
|
3311
|
+
function ellipseRoute(fromBounds, toBounds, ellipse, fromAnchor, toAnchor) {
|
|
3275
3312
|
const fromCenter = rectCenter(fromBounds);
|
|
3276
3313
|
const toCenter = rectCenter(toBounds);
|
|
3277
|
-
const
|
|
3278
|
-
const
|
|
3279
|
-
const
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
const
|
|
3291
|
-
const
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
const
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
const
|
|
3301
|
-
const
|
|
3302
|
-
const
|
|
3303
|
-
const
|
|
3304
|
-
const
|
|
3305
|
-
const
|
|
3306
|
-
const
|
|
3307
|
-
const
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3314
|
+
const p0 = resolveAnchor(fromBounds, fromAnchor, toCenter);
|
|
3315
|
+
const p3 = resolveAnchor(toBounds, toAnchor, fromCenter);
|
|
3316
|
+
const theta1 = Math.atan2(
|
|
3317
|
+
(fromCenter.y - ellipse.cy) / ellipse.ry,
|
|
3318
|
+
(fromCenter.x - ellipse.cx) / ellipse.rx
|
|
3319
|
+
);
|
|
3320
|
+
const theta2 = Math.atan2(
|
|
3321
|
+
(toCenter.y - ellipse.cy) / ellipse.ry,
|
|
3322
|
+
(toCenter.x - ellipse.cx) / ellipse.rx
|
|
3323
|
+
);
|
|
3324
|
+
let angularSpan = theta2 - theta1;
|
|
3325
|
+
while (angularSpan > Math.PI) angularSpan -= 2 * Math.PI;
|
|
3326
|
+
while (angularSpan <= -Math.PI) angularSpan += 2 * Math.PI;
|
|
3327
|
+
const absSpan = Math.abs(angularSpan);
|
|
3328
|
+
const kappa = absSpan < 1e-6 ? 0 : 4 / 3 * Math.tan(absSpan / 4);
|
|
3329
|
+
const tangent1 = {
|
|
3330
|
+
x: -ellipse.rx * Math.sin(theta1),
|
|
3331
|
+
y: ellipse.ry * Math.cos(theta1)
|
|
3332
|
+
};
|
|
3333
|
+
const tangent2 = {
|
|
3334
|
+
x: -ellipse.rx * Math.sin(theta2),
|
|
3335
|
+
y: ellipse.ry * Math.cos(theta2)
|
|
3336
|
+
};
|
|
3337
|
+
const len1 = Math.hypot(tangent1.x, tangent1.y) || 1;
|
|
3338
|
+
const len2 = Math.hypot(tangent2.x, tangent2.y) || 1;
|
|
3339
|
+
const norm1 = { x: tangent1.x / len1, y: tangent1.y / len1 };
|
|
3340
|
+
const norm2 = { x: tangent2.x / len2, y: tangent2.y / len2 };
|
|
3341
|
+
const chordLength = Math.hypot(p3.x - p0.x, p3.y - p0.y);
|
|
3342
|
+
const cpDistance = chordLength * kappa * 0.5;
|
|
3343
|
+
const sign = angularSpan >= 0 ? 1 : -1;
|
|
3344
|
+
const cp1 = {
|
|
3345
|
+
x: p0.x + norm1.x * cpDistance * sign,
|
|
3346
|
+
y: p0.y + norm1.y * cpDistance * sign
|
|
3347
|
+
};
|
|
3348
|
+
const cp2 = {
|
|
3349
|
+
x: p3.x - norm2.x * cpDistance * sign,
|
|
3350
|
+
y: p3.y - norm2.y * cpDistance * sign
|
|
3351
|
+
};
|
|
3352
|
+
return [p0, cp1, cp2, p3];
|
|
3353
|
+
}
|
|
3354
|
+
function straightRoute(fromBounds, toBounds, fromAnchor, toAnchor) {
|
|
3355
|
+
const fromC = rectCenter(fromBounds);
|
|
3356
|
+
const toC = rectCenter(toBounds);
|
|
3357
|
+
const p0 = resolveAnchor(fromBounds, fromAnchor, toC);
|
|
3358
|
+
const p3 = resolveAnchor(toBounds, toAnchor, fromC);
|
|
3359
|
+
return [p0, p3];
|
|
3315
3360
|
}
|
|
3316
3361
|
function orthogonalRoute(fromBounds, toBounds, fromAnchor, toAnchor) {
|
|
3317
3362
|
const fromC = rectCenter(fromBounds);
|
|
@@ -3357,15 +3402,6 @@ function findBoundaryIntersection(p0, cp1, cp2, p3, targetRect, searchFromEnd) {
|
|
|
3357
3402
|
}
|
|
3358
3403
|
return void 0;
|
|
3359
3404
|
}
|
|
3360
|
-
function pointAlongArc(route, t) {
|
|
3361
|
-
const [first, second] = route;
|
|
3362
|
-
if (t <= 0.5) {
|
|
3363
|
-
const localT2 = Math.max(0, Math.min(1, t * 2));
|
|
3364
|
-
return bezierPointAt(first[0], first[1], first[2], first[3], localT2);
|
|
3365
|
-
}
|
|
3366
|
-
const localT = Math.max(0, Math.min(1, (t - 0.5) * 2));
|
|
3367
|
-
return bezierPointAt(second[0], second[1], second[2], second[3], localT);
|
|
3368
|
-
}
|
|
3369
3405
|
function computeDiagramCenter(nodeBounds, canvasCenter) {
|
|
3370
3406
|
if (nodeBounds.length === 0) {
|
|
3371
3407
|
return canvasCenter ?? { x: 0, y: 0 };
|
|
@@ -3419,11 +3455,36 @@ function pointAlongPolyline(points, t) {
|
|
|
3419
3455
|
}
|
|
3420
3456
|
return points[points.length - 1];
|
|
3421
3457
|
}
|
|
3422
|
-
function
|
|
3458
|
+
function createConnectionGradient(ctx, start, end, fromColor, baseColor, toColor) {
|
|
3459
|
+
const gradient = ctx.createLinearGradient(start.x, start.y, end.x, end.y);
|
|
3460
|
+
gradient.addColorStop(0, fromColor);
|
|
3461
|
+
gradient.addColorStop(0.5, baseColor);
|
|
3462
|
+
gradient.addColorStop(1, toColor);
|
|
3463
|
+
return gradient;
|
|
3464
|
+
}
|
|
3465
|
+
function resolveConnectionStroke(ctx, start, end, fromColor, baseColor, toColor) {
|
|
3466
|
+
if (!fromColor || !toColor) {
|
|
3467
|
+
return baseColor;
|
|
3468
|
+
}
|
|
3469
|
+
return createConnectionGradient(ctx, start, end, fromColor, baseColor, toColor);
|
|
3470
|
+
}
|
|
3471
|
+
function drawOrthogonalPathWithStroke(ctx, from, to, style, stroke) {
|
|
3472
|
+
const midX = (from.x + to.x) / 2;
|
|
3473
|
+
ctx.strokeStyle = stroke;
|
|
3474
|
+
ctx.lineWidth = style.width;
|
|
3475
|
+
ctx.setLineDash(style.dash ?? []);
|
|
3476
|
+
ctx.beginPath();
|
|
3477
|
+
ctx.moveTo(from.x, from.y);
|
|
3478
|
+
ctx.lineTo(midX, from.y);
|
|
3479
|
+
ctx.lineTo(midX, to.y);
|
|
3480
|
+
ctx.lineTo(to.x, to.y);
|
|
3481
|
+
ctx.stroke();
|
|
3482
|
+
}
|
|
3483
|
+
function drawCubicInterpolatedPath(ctx, points, style, stroke) {
|
|
3423
3484
|
if (points.length < 2) {
|
|
3424
3485
|
return;
|
|
3425
3486
|
}
|
|
3426
|
-
ctx.strokeStyle =
|
|
3487
|
+
ctx.strokeStyle = stroke;
|
|
3427
3488
|
ctx.lineWidth = style.width;
|
|
3428
3489
|
ctx.setLineDash(style.dash ?? []);
|
|
3429
3490
|
ctx.beginPath();
|
|
@@ -3463,8 +3524,19 @@ function polylineBounds(points) {
|
|
|
3463
3524
|
};
|
|
3464
3525
|
}
|
|
3465
3526
|
function renderConnection(ctx, conn, fromBounds, toBounds, theme, edgeRoute, options) {
|
|
3466
|
-
|
|
3467
|
-
|
|
3527
|
+
let routing = conn.routing ?? "auto";
|
|
3528
|
+
let curveMode = conn.curveMode ?? "normal";
|
|
3529
|
+
if (conn.strokeStyle !== void 0) {
|
|
3530
|
+
console.warn("connection.strokeStyle is deprecated, use style instead");
|
|
3531
|
+
}
|
|
3532
|
+
if (routing === "arc") {
|
|
3533
|
+
console.warn(
|
|
3534
|
+
"connection routing: 'arc' is deprecated. Use routing: 'curve' with curveMode: 'ellipse' instead."
|
|
3535
|
+
);
|
|
3536
|
+
routing = "curve";
|
|
3537
|
+
curveMode = "ellipse";
|
|
3538
|
+
}
|
|
3539
|
+
const strokeStyle = conn.style ?? conn.strokeStyle ?? "solid";
|
|
3468
3540
|
const strokeWidth = conn.width ?? conn.strokeWidth ?? 2;
|
|
3469
3541
|
const tension = conn.tension ?? 0.35;
|
|
3470
3542
|
const dash = dashFromStyle(strokeStyle);
|
|
@@ -3486,15 +3558,31 @@ function renderConnection(ctx, conn, fromBounds, toBounds, theme, edgeRoute, opt
|
|
|
3486
3558
|
ctx.globalAlpha = conn.opacity;
|
|
3487
3559
|
const arrowPlacement = conn.arrowPlacement ?? "endpoint";
|
|
3488
3560
|
if (routing === "curve") {
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3561
|
+
let p0;
|
|
3562
|
+
let cp1;
|
|
3563
|
+
let cp2;
|
|
3564
|
+
let p3;
|
|
3565
|
+
if (curveMode === "ellipse") {
|
|
3566
|
+
const ellipse = options?.ellipseParams ?? inferEllipseParams([fromBounds, toBounds]);
|
|
3567
|
+
[p0, cp1, cp2, p3] = ellipseRoute(
|
|
3568
|
+
fromBounds,
|
|
3569
|
+
toBounds,
|
|
3570
|
+
ellipse,
|
|
3571
|
+
conn.fromAnchor,
|
|
3572
|
+
conn.toAnchor
|
|
3573
|
+
);
|
|
3574
|
+
} else {
|
|
3575
|
+
[p0, cp1, cp2, p3] = curveRoute(
|
|
3576
|
+
fromBounds,
|
|
3577
|
+
toBounds,
|
|
3578
|
+
diagramCenter,
|
|
3579
|
+
tension,
|
|
3580
|
+
conn.fromAnchor,
|
|
3581
|
+
conn.toAnchor
|
|
3582
|
+
);
|
|
3583
|
+
}
|
|
3584
|
+
const stroke = resolveConnectionStroke(ctx, p0, p3, conn.fromColor, style.color, conn.toColor);
|
|
3585
|
+
ctx.strokeStyle = stroke;
|
|
3498
3586
|
ctx.lineWidth = style.width;
|
|
3499
3587
|
ctx.setLineDash(style.dash ?? []);
|
|
3500
3588
|
ctx.beginPath();
|
|
@@ -3525,50 +3613,22 @@ function renderConnection(ctx, conn, fromBounds, toBounds, theme, edgeRoute, opt
|
|
|
3525
3613
|
}
|
|
3526
3614
|
}
|
|
3527
3615
|
}
|
|
3528
|
-
} else if (routing === "
|
|
3529
|
-
const [
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
diagramCenter,
|
|
3533
|
-
tension,
|
|
3534
|
-
conn.fromAnchor,
|
|
3535
|
-
conn.toAnchor
|
|
3536
|
-
);
|
|
3537
|
-
const [p0, cp1, cp2, pMid] = first;
|
|
3538
|
-
const [, cp3, cp4, p3] = second;
|
|
3539
|
-
ctx.strokeStyle = style.color;
|
|
3616
|
+
} else if (routing === "straight") {
|
|
3617
|
+
const [p0, p3] = straightRoute(fromBounds, toBounds, conn.fromAnchor, conn.toAnchor);
|
|
3618
|
+
const stroke = resolveConnectionStroke(ctx, p0, p3, conn.fromColor, style.color, conn.toColor);
|
|
3619
|
+
ctx.strokeStyle = stroke;
|
|
3540
3620
|
ctx.lineWidth = style.width;
|
|
3541
3621
|
ctx.setLineDash(style.dash ?? []);
|
|
3542
3622
|
ctx.beginPath();
|
|
3543
3623
|
ctx.moveTo(p0.x, p0.y);
|
|
3544
|
-
ctx.
|
|
3545
|
-
ctx.bezierCurveTo(cp3.x, cp3.y, cp4.x, cp4.y, p3.x, p3.y);
|
|
3624
|
+
ctx.lineTo(p3.x, p3.y);
|
|
3546
3625
|
ctx.stroke();
|
|
3547
|
-
linePoints = [p0,
|
|
3626
|
+
linePoints = [p0, p3];
|
|
3548
3627
|
startPoint = p0;
|
|
3549
3628
|
endPoint = p3;
|
|
3550
|
-
startAngle = Math.atan2(p0.y -
|
|
3551
|
-
endAngle = Math.atan2(p3.y -
|
|
3552
|
-
labelPoint =
|
|
3553
|
-
if (arrowPlacement === "boundary") {
|
|
3554
|
-
if (conn.arrow === "end" || conn.arrow === "both") {
|
|
3555
|
-
const [, s_cp3, s_cp4, s_p3] = second;
|
|
3556
|
-
const tEnd = findBoundaryIntersection(pMid, s_cp3, s_cp4, s_p3, toBounds, true);
|
|
3557
|
-
if (tEnd !== void 0) {
|
|
3558
|
-
endPoint = bezierPointAt(pMid, s_cp3, s_cp4, s_p3, tEnd);
|
|
3559
|
-
const tangent = bezierTangentAt(pMid, s_cp3, s_cp4, s_p3, tEnd);
|
|
3560
|
-
endAngle = Math.atan2(tangent.y, tangent.x);
|
|
3561
|
-
}
|
|
3562
|
-
}
|
|
3563
|
-
if (conn.arrow === "start" || conn.arrow === "both") {
|
|
3564
|
-
const tStart = findBoundaryIntersection(p0, cp1, cp2, pMid, fromBounds, false);
|
|
3565
|
-
if (tStart !== void 0) {
|
|
3566
|
-
startPoint = bezierPointAt(p0, cp1, cp2, pMid, tStart);
|
|
3567
|
-
const tangent = bezierTangentAt(p0, cp1, cp2, pMid, tStart);
|
|
3568
|
-
startAngle = Math.atan2(tangent.y, tangent.x) + Math.PI;
|
|
3569
|
-
}
|
|
3570
|
-
}
|
|
3571
|
-
}
|
|
3629
|
+
startAngle = Math.atan2(p0.y - p3.y, p0.x - p3.x);
|
|
3630
|
+
endAngle = Math.atan2(p3.y - p0.y, p3.x - p0.x);
|
|
3631
|
+
labelPoint = pointAlongPolyline(linePoints, labelT);
|
|
3572
3632
|
} else {
|
|
3573
3633
|
const hasAnchorHints = conn.fromAnchor !== void 0 || conn.toAnchor !== void 0;
|
|
3574
3634
|
const useElkRoute = routing === "auto" && !hasAnchorHints && (edgeRoute?.points.length ?? 0) >= 2;
|
|
@@ -3579,10 +3639,18 @@ function renderConnection(ctx, conn, fromBounds, toBounds, theme, edgeRoute, opt
|
|
|
3579
3639
|
endPoint = linePoints[linePoints.length - 1] ?? linePoints[0];
|
|
3580
3640
|
startAngle = Math.atan2(startSegment.y - linePoints[0].y, startSegment.x - linePoints[0].x) + Math.PI;
|
|
3581
3641
|
endAngle = Math.atan2(endPoint.y - endStart.y, endPoint.x - endStart.x);
|
|
3642
|
+
const stroke = resolveConnectionStroke(
|
|
3643
|
+
ctx,
|
|
3644
|
+
startPoint,
|
|
3645
|
+
endPoint,
|
|
3646
|
+
conn.fromColor,
|
|
3647
|
+
style.color,
|
|
3648
|
+
conn.toColor
|
|
3649
|
+
);
|
|
3582
3650
|
if (useElkRoute) {
|
|
3583
|
-
drawCubicInterpolatedPath(ctx, linePoints, style);
|
|
3651
|
+
drawCubicInterpolatedPath(ctx, linePoints, style, stroke);
|
|
3584
3652
|
} else {
|
|
3585
|
-
|
|
3653
|
+
drawOrthogonalPathWithStroke(ctx, startPoint, endPoint, style, stroke);
|
|
3586
3654
|
}
|
|
3587
3655
|
labelPoint = pointAlongPolyline(linePoints, labelT);
|
|
3588
3656
|
}
|
|
@@ -3887,6 +3955,9 @@ function measureTextBounds(ctx, options) {
|
|
|
3887
3955
|
function angleBetween(from, to) {
|
|
3888
3956
|
return Math.atan2(to.y - from.y, to.x - from.x);
|
|
3889
3957
|
}
|
|
3958
|
+
function degreesToRadians(angle) {
|
|
3959
|
+
return angle * Math.PI / 180;
|
|
3960
|
+
}
|
|
3890
3961
|
function pathBounds(operations) {
|
|
3891
3962
|
let minX = Number.POSITIVE_INFINITY;
|
|
3892
3963
|
let minY = Number.POSITIVE_INFINITY;
|
|
@@ -4124,6 +4195,34 @@ function renderDrawCommands(ctx, commands, theme) {
|
|
|
4124
4195
|
});
|
|
4125
4196
|
break;
|
|
4126
4197
|
}
|
|
4198
|
+
case "arc": {
|
|
4199
|
+
const startAngle = degreesToRadians(command.startAngle);
|
|
4200
|
+
const endAngle = degreesToRadians(command.endAngle);
|
|
4201
|
+
withOpacity(ctx, command.opacity, () => {
|
|
4202
|
+
applyDrawShadow(ctx, command.shadow);
|
|
4203
|
+
ctx.beginPath();
|
|
4204
|
+
ctx.setLineDash(command.dash ?? []);
|
|
4205
|
+
ctx.lineWidth = command.width;
|
|
4206
|
+
ctx.strokeStyle = command.color;
|
|
4207
|
+
ctx.arc(command.center.x, command.center.y, command.radius, startAngle, endAngle);
|
|
4208
|
+
ctx.stroke();
|
|
4209
|
+
});
|
|
4210
|
+
rendered.push({
|
|
4211
|
+
id,
|
|
4212
|
+
kind: "draw",
|
|
4213
|
+
bounds: expandRect(
|
|
4214
|
+
{
|
|
4215
|
+
x: command.center.x - command.radius,
|
|
4216
|
+
y: command.center.y - command.radius,
|
|
4217
|
+
width: command.radius * 2,
|
|
4218
|
+
height: command.radius * 2
|
|
4219
|
+
},
|
|
4220
|
+
command.width / 2
|
|
4221
|
+
),
|
|
4222
|
+
foregroundColor: command.color
|
|
4223
|
+
});
|
|
4224
|
+
break;
|
|
4225
|
+
}
|
|
4127
4226
|
case "bezier": {
|
|
4128
4227
|
const points = command.points;
|
|
4129
4228
|
withOpacity(ctx, command.opacity, () => {
|
|
@@ -4767,6 +4866,18 @@ async function renderDesign(input, options = {}) {
|
|
|
4767
4866
|
const specHash = computeSpecHash(spec);
|
|
4768
4867
|
const generatorVersion = options.generatorVersion ?? DEFAULT_GENERATOR_VERSION;
|
|
4769
4868
|
const renderedAt = options.renderedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
4869
|
+
const iteration = options.iteration;
|
|
4870
|
+
if (iteration) {
|
|
4871
|
+
if (!Number.isInteger(iteration.iteration) || iteration.iteration <= 0) {
|
|
4872
|
+
throw new Error("Iteration metadata requires iteration to be a positive integer.");
|
|
4873
|
+
}
|
|
4874
|
+
if (iteration.maxIterations != null && (!Number.isInteger(iteration.maxIterations) || iteration.maxIterations <= 0)) {
|
|
4875
|
+
throw new Error("Iteration metadata requires maxIterations to be a positive integer.");
|
|
4876
|
+
}
|
|
4877
|
+
if (iteration.maxIterations != null && iteration.maxIterations < iteration.iteration) {
|
|
4878
|
+
throw new Error("Iteration metadata requires maxIterations to be >= iteration.");
|
|
4879
|
+
}
|
|
4880
|
+
}
|
|
4770
4881
|
const renderScale = resolveRenderScale(spec);
|
|
4771
4882
|
const canvas = createCanvas(spec.canvas.width * renderScale, spec.canvas.height * renderScale);
|
|
4772
4883
|
const ctx = canvas.getContext("2d");
|
|
@@ -4946,10 +5057,19 @@ async function renderDesign(input, options = {}) {
|
|
|
4946
5057
|
break;
|
|
4947
5058
|
}
|
|
4948
5059
|
}
|
|
4949
|
-
const
|
|
4950
|
-
|
|
4951
|
-
|
|
5060
|
+
const nodeBounds = spec.elements.filter((element) => element.type !== "connection").map((element) => elementRects.get(element.id)).filter((rect) => rect != null);
|
|
5061
|
+
const diagramCenter = spec.layout.diagramCenter ?? computeDiagramCenter(nodeBounds, { x: spec.canvas.width / 2, y: spec.canvas.height / 2 });
|
|
5062
|
+
const layoutEllipseRx = "ellipseRx" in spec.layout ? spec.layout.ellipseRx : void 0;
|
|
5063
|
+
const layoutEllipseRy = "ellipseRy" in spec.layout ? spec.layout.ellipseRy : void 0;
|
|
5064
|
+
const hasEllipseConnections = spec.elements.some(
|
|
5065
|
+
(el) => el.type === "connection" && (el.curveMode === "ellipse" || el.routing === "arc")
|
|
4952
5066
|
);
|
|
5067
|
+
const ellipseParams = hasEllipseConnections ? inferEllipseParams(
|
|
5068
|
+
nodeBounds,
|
|
5069
|
+
spec.layout.diagramCenter ?? diagramCenter,
|
|
5070
|
+
layoutEllipseRx,
|
|
5071
|
+
layoutEllipseRy
|
|
5072
|
+
) : void 0;
|
|
4953
5073
|
for (const element of spec.elements) {
|
|
4954
5074
|
if (element.type !== "connection") {
|
|
4955
5075
|
continue;
|
|
@@ -4963,7 +5083,15 @@ async function renderDesign(input, options = {}) {
|
|
|
4963
5083
|
}
|
|
4964
5084
|
const edgeRoute = edgeRoutes?.get(`${element.from}-${element.to}`);
|
|
4965
5085
|
elements.push(
|
|
4966
|
-
...renderConnection(
|
|
5086
|
+
...renderConnection(
|
|
5087
|
+
ctx,
|
|
5088
|
+
element,
|
|
5089
|
+
fromRect,
|
|
5090
|
+
toRect,
|
|
5091
|
+
theme,
|
|
5092
|
+
edgeRoute,
|
|
5093
|
+
ellipseParams ? { diagramCenter, ellipseParams } : { diagramCenter }
|
|
5094
|
+
)
|
|
4967
5095
|
);
|
|
4968
5096
|
}
|
|
4969
5097
|
if (footerRect && spec.footer) {
|
|
@@ -5007,7 +5135,8 @@ async function renderDesign(input, options = {}) {
|
|
|
5007
5135
|
layout: {
|
|
5008
5136
|
safeFrame,
|
|
5009
5137
|
elements
|
|
5010
|
-
}
|
|
5138
|
+
},
|
|
5139
|
+
...iteration ? { iteration } : {}
|
|
5011
5140
|
};
|
|
5012
5141
|
return {
|
|
5013
5142
|
png: pngBuffer,
|
|
@@ -5314,6 +5443,12 @@ var renderOutputSchema = z3.object({
|
|
|
5314
5443
|
artifactHash: z3.string(),
|
|
5315
5444
|
specHash: z3.string(),
|
|
5316
5445
|
layoutMode: z3.string(),
|
|
5446
|
+
iteration: z3.object({
|
|
5447
|
+
current: z3.number().int().positive(),
|
|
5448
|
+
max: z3.number().int().positive(),
|
|
5449
|
+
isLast: z3.boolean(),
|
|
5450
|
+
notes: z3.string().optional()
|
|
5451
|
+
}).optional(),
|
|
5317
5452
|
qa: z3.object({
|
|
5318
5453
|
pass: z3.boolean(),
|
|
5319
5454
|
issueCount: z3.number(),
|
|
@@ -5400,8 +5535,30 @@ function readCodeRange(code, start, end) {
|
|
|
5400
5535
|
const lines = code.split(/\r?\n/u);
|
|
5401
5536
|
return lines.slice(start - 1, end).join("\n");
|
|
5402
5537
|
}
|
|
5538
|
+
function parseIterationMeta(options) {
|
|
5539
|
+
if (options.iteration == null) {
|
|
5540
|
+
if (options.maxIterations != null || options.iterationNotes || options.previousHash) {
|
|
5541
|
+
throw new Error(
|
|
5542
|
+
"--iteration is required when using --max-iterations, --iteration-notes, or --previous-hash."
|
|
5543
|
+
);
|
|
5544
|
+
}
|
|
5545
|
+
return void 0;
|
|
5546
|
+
}
|
|
5547
|
+
if (options.maxIterations != null && options.maxIterations < options.iteration) {
|
|
5548
|
+
throw new Error("--max-iterations must be greater than or equal to --iteration.");
|
|
5549
|
+
}
|
|
5550
|
+
return {
|
|
5551
|
+
iteration: options.iteration,
|
|
5552
|
+
...options.maxIterations != null ? { maxIterations: options.maxIterations } : {},
|
|
5553
|
+
...options.iterationNotes ? { notes: options.iterationNotes } : {},
|
|
5554
|
+
...options.previousHash ? { previousHash: options.previousHash } : {}
|
|
5555
|
+
};
|
|
5556
|
+
}
|
|
5403
5557
|
async function runRenderPipeline(spec, options) {
|
|
5404
|
-
const renderResult = await renderDesign(spec, {
|
|
5558
|
+
const renderResult = await renderDesign(spec, {
|
|
5559
|
+
generatorVersion: pkg.version,
|
|
5560
|
+
...options.iteration ? { iteration: options.iteration } : {}
|
|
5561
|
+
});
|
|
5405
5562
|
const written = await writeRenderArtifacts(renderResult, options.out);
|
|
5406
5563
|
const specPath = options.specOut ? resolve4(options.specOut) : specPathFor(written.metadataPath);
|
|
5407
5564
|
await mkdir2(dirname3(specPath), { recursive: true });
|
|
@@ -5418,6 +5575,14 @@ async function runRenderPipeline(spec, options) {
|
|
|
5418
5575
|
artifactHash: written.metadata.artifactHash,
|
|
5419
5576
|
specHash: written.metadata.specHash,
|
|
5420
5577
|
layoutMode: spec.layout.mode,
|
|
5578
|
+
...written.metadata.iteration ? {
|
|
5579
|
+
iteration: {
|
|
5580
|
+
current: written.metadata.iteration.iteration,
|
|
5581
|
+
max: written.metadata.iteration.maxIterations ?? written.metadata.iteration.iteration,
|
|
5582
|
+
isLast: (written.metadata.iteration.maxIterations ?? written.metadata.iteration.iteration) === written.metadata.iteration.iteration,
|
|
5583
|
+
...written.metadata.iteration.notes ? { notes: written.metadata.iteration.notes } : {}
|
|
5584
|
+
}
|
|
5585
|
+
} : {},
|
|
5421
5586
|
qa: {
|
|
5422
5587
|
pass: qa.pass,
|
|
5423
5588
|
issueCount: qa.issues.length,
|
|
@@ -5431,6 +5596,10 @@ cli.command("render", {
|
|
|
5431
5596
|
spec: z3.string().describe('Path to DesignSpec JSON file (or "-" to read JSON from stdin)'),
|
|
5432
5597
|
out: z3.string().describe("Output file path (.png) or output directory"),
|
|
5433
5598
|
specOut: z3.string().optional().describe("Optional explicit output path for normalized spec JSON"),
|
|
5599
|
+
iteration: z3.number().int().positive().optional().describe("Optional iteration number for iterative workflows (1-indexed)"),
|
|
5600
|
+
iterationNotes: z3.string().optional().describe("Optional notes for the current iteration metadata"),
|
|
5601
|
+
maxIterations: z3.number().int().positive().optional().describe("Optional maximum planned iteration count"),
|
|
5602
|
+
previousHash: z3.string().optional().describe("Optional artifact hash from the previous iteration"),
|
|
5434
5603
|
allowQaFail: z3.boolean().default(false).describe("Allow render success even if QA fails")
|
|
5435
5604
|
}),
|
|
5436
5605
|
output: renderOutputSchema,
|
|
@@ -5445,9 +5614,26 @@ cli.command("render", {
|
|
|
5445
5614
|
],
|
|
5446
5615
|
async run(c) {
|
|
5447
5616
|
const spec = parseDesignSpec(await readJson(c.options.spec));
|
|
5617
|
+
let iteration;
|
|
5618
|
+
try {
|
|
5619
|
+
iteration = parseIterationMeta({
|
|
5620
|
+
...c.options.iteration != null ? { iteration: c.options.iteration } : {},
|
|
5621
|
+
...c.options.maxIterations != null ? { maxIterations: c.options.maxIterations } : {},
|
|
5622
|
+
...c.options.iterationNotes ? { iterationNotes: c.options.iterationNotes } : {},
|
|
5623
|
+
...c.options.previousHash ? { previousHash: c.options.previousHash } : {}
|
|
5624
|
+
});
|
|
5625
|
+
} catch (error) {
|
|
5626
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5627
|
+
return c.error({
|
|
5628
|
+
code: "INVALID_ITERATION_OPTIONS",
|
|
5629
|
+
message,
|
|
5630
|
+
retryable: false
|
|
5631
|
+
});
|
|
5632
|
+
}
|
|
5448
5633
|
const runReport = await runRenderPipeline(spec, {
|
|
5449
5634
|
out: c.options.out,
|
|
5450
|
-
...c.options.specOut ? { specOut: c.options.specOut } : {}
|
|
5635
|
+
...c.options.specOut ? { specOut: c.options.specOut } : {},
|
|
5636
|
+
...iteration ? { iteration } : {}
|
|
5451
5637
|
});
|
|
5452
5638
|
if (!runReport.qa.pass && !c.options.allowQaFail) {
|
|
5453
5639
|
return c.error({
|