@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/renderer.js CHANGED
@@ -1366,12 +1366,12 @@ var MACOS_DOTS = [
1366
1366
  { fill: "#27C93F", stroke: "#1AAB29" }
1367
1367
  ];
1368
1368
  function drawMacosDots(ctx, x, y) {
1369
- for (const [index, dot2] of MACOS_DOTS.entries()) {
1369
+ for (const [index, dot] of MACOS_DOTS.entries()) {
1370
1370
  ctx.beginPath();
1371
1371
  ctx.arc(x + index * DOT_SPACING, y, DOT_RADIUS, 0, Math.PI * 2);
1372
1372
  ctx.closePath();
1373
- ctx.fillStyle = dot2.fill;
1374
- ctx.strokeStyle = dot2.stroke;
1373
+ ctx.fillStyle = dot.fill;
1374
+ ctx.strokeStyle = dot.stroke;
1375
1375
  ctx.lineWidth = DOT_STROKE_WIDTH;
1376
1376
  ctx.fill();
1377
1377
  ctx.stroke();
@@ -2008,16 +2008,6 @@ function drawBezier(ctx, points, style) {
2008
2008
  ctx.quadraticCurveTo(penultimate.x, penultimate.y, last.x, last.y);
2009
2009
  ctx.stroke();
2010
2010
  }
2011
- function drawOrthogonalPath(ctx, from, to, style) {
2012
- const midX = (from.x + to.x) / 2;
2013
- applyLineStyle(ctx, style);
2014
- ctx.beginPath();
2015
- ctx.moveTo(from.x, from.y);
2016
- ctx.lineTo(midX, from.y);
2017
- ctx.lineTo(midX, to.y);
2018
- ctx.lineTo(to.x, to.y);
2019
- ctx.stroke();
2020
- }
2021
2011
 
2022
2012
  // src/renderers/connection.ts
2023
2013
  var ELLIPSE_KAPPA = 4 * (Math.sqrt(2) - 1) / 3;
@@ -2100,56 +2090,71 @@ function curveRoute(fromBounds, toBounds, diagramCenter, tension, fromAnchor, to
2100
2090
  const cp2 = { x: p3.x + n3.x * offset, y: p3.y + n3.y * offset };
2101
2091
  return [p0, cp1, cp2, p3];
2102
2092
  }
2103
- function dot(a, b) {
2104
- return a.x * b.x + a.y * b.y;
2105
- }
2106
- function localToWorld(origin, axisX, axisY, local) {
2107
- return {
2108
- x: origin.x + axisX.x * local.x + axisY.x * local.y,
2109
- y: origin.y + axisX.y * local.x + axisY.y * local.y
2110
- };
2093
+ function inferEllipseParams(nodeBounds, explicitCenter, explicitRx, explicitRy) {
2094
+ if (nodeBounds.length === 0) {
2095
+ return {
2096
+ cx: explicitCenter?.x ?? 0,
2097
+ cy: explicitCenter?.y ?? 0,
2098
+ rx: explicitRx ?? 1,
2099
+ ry: explicitRy ?? 1
2100
+ };
2101
+ }
2102
+ const centers = nodeBounds.map(rectCenter);
2103
+ const cx = explicitCenter?.x ?? centers.reduce((sum, c) => sum + c.x, 0) / centers.length;
2104
+ const cy = explicitCenter?.y ?? centers.reduce((sum, c) => sum + c.y, 0) / centers.length;
2105
+ const rx = explicitRx ?? Math.max(1, ...centers.map((c) => Math.abs(c.x - cx)));
2106
+ const ry = explicitRy ?? Math.max(1, ...centers.map((c) => Math.abs(c.y - cy)));
2107
+ return { cx, cy, rx, ry };
2111
2108
  }
2112
- function arcRoute(fromBounds, toBounds, diagramCenter, tension, fromAnchor, toAnchor) {
2109
+ function ellipseRoute(fromBounds, toBounds, ellipse, fromAnchor, toAnchor) {
2113
2110
  const fromCenter = rectCenter(fromBounds);
2114
2111
  const toCenter = rectCenter(toBounds);
2115
- const start = resolveAnchor(fromBounds, fromAnchor, toCenter);
2116
- const end = resolveAnchor(toBounds, toAnchor, fromCenter);
2117
- const chord = { x: end.x - start.x, y: end.y - start.y };
2118
- const chordLength = Math.hypot(chord.x, chord.y);
2119
- if (chordLength < 1e-6) {
2120
- const mid = { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 };
2121
- return [
2122
- [start, start, mid, mid],
2123
- [mid, mid, end, end]
2124
- ];
2125
- }
2126
- const axisX = { x: chord.x / chordLength, y: chord.y / chordLength };
2127
- let axisY = { x: -axisX.y, y: axisX.x };
2128
- const midpoint = { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 };
2129
- const outwardHint = outwardNormal(midpoint, diagramCenter);
2130
- if (dot(axisY, outwardHint) < 0) {
2131
- axisY = { x: -axisY.x, y: -axisY.y };
2132
- }
2133
- const semiMajor = chordLength / 2;
2134
- const semiMinor = Math.max(12, chordLength * tension * 0.75);
2135
- const p0Local = { x: -semiMajor, y: 0 };
2136
- const cp1Local = { x: -semiMajor, y: ELLIPSE_KAPPA * semiMinor };
2137
- const cp2Local = { x: -ELLIPSE_KAPPA * semiMajor, y: semiMinor };
2138
- const pMidLocal = { x: 0, y: semiMinor };
2139
- const cp3Local = { x: ELLIPSE_KAPPA * semiMajor, y: semiMinor };
2140
- const cp4Local = { x: semiMajor, y: ELLIPSE_KAPPA * semiMinor };
2141
- const p3Local = { x: semiMajor, y: 0 };
2142
- const p0 = localToWorld(midpoint, axisX, axisY, p0Local);
2143
- const cp1 = localToWorld(midpoint, axisX, axisY, cp1Local);
2144
- const cp2 = localToWorld(midpoint, axisX, axisY, cp2Local);
2145
- const pMid = localToWorld(midpoint, axisX, axisY, pMidLocal);
2146
- const cp3 = localToWorld(midpoint, axisX, axisY, cp3Local);
2147
- const cp4 = localToWorld(midpoint, axisX, axisY, cp4Local);
2148
- const p3 = localToWorld(midpoint, axisX, axisY, p3Local);
2149
- return [
2150
- [p0, cp1, cp2, pMid],
2151
- [pMid, cp3, cp4, p3]
2152
- ];
2112
+ const p0 = resolveAnchor(fromBounds, fromAnchor, toCenter);
2113
+ const p3 = resolveAnchor(toBounds, toAnchor, fromCenter);
2114
+ const theta1 = Math.atan2(
2115
+ (fromCenter.y - ellipse.cy) / ellipse.ry,
2116
+ (fromCenter.x - ellipse.cx) / ellipse.rx
2117
+ );
2118
+ const theta2 = Math.atan2(
2119
+ (toCenter.y - ellipse.cy) / ellipse.ry,
2120
+ (toCenter.x - ellipse.cx) / ellipse.rx
2121
+ );
2122
+ let angularSpan = theta2 - theta1;
2123
+ while (angularSpan > Math.PI) angularSpan -= 2 * Math.PI;
2124
+ while (angularSpan <= -Math.PI) angularSpan += 2 * Math.PI;
2125
+ const absSpan = Math.abs(angularSpan);
2126
+ const kappa = absSpan < 1e-6 ? 0 : 4 / 3 * Math.tan(absSpan / 4);
2127
+ const tangent1 = {
2128
+ x: -ellipse.rx * Math.sin(theta1),
2129
+ y: ellipse.ry * Math.cos(theta1)
2130
+ };
2131
+ const tangent2 = {
2132
+ x: -ellipse.rx * Math.sin(theta2),
2133
+ y: ellipse.ry * Math.cos(theta2)
2134
+ };
2135
+ const len1 = Math.hypot(tangent1.x, tangent1.y) || 1;
2136
+ const len2 = Math.hypot(tangent2.x, tangent2.y) || 1;
2137
+ const norm1 = { x: tangent1.x / len1, y: tangent1.y / len1 };
2138
+ const norm2 = { x: tangent2.x / len2, y: tangent2.y / len2 };
2139
+ const chordLength = Math.hypot(p3.x - p0.x, p3.y - p0.y);
2140
+ const cpDistance = chordLength * kappa * 0.5;
2141
+ const sign = angularSpan >= 0 ? 1 : -1;
2142
+ const cp1 = {
2143
+ x: p0.x + norm1.x * cpDistance * sign,
2144
+ y: p0.y + norm1.y * cpDistance * sign
2145
+ };
2146
+ const cp2 = {
2147
+ x: p3.x - norm2.x * cpDistance * sign,
2148
+ y: p3.y - norm2.y * cpDistance * sign
2149
+ };
2150
+ return [p0, cp1, cp2, p3];
2151
+ }
2152
+ function straightRoute(fromBounds, toBounds, fromAnchor, toAnchor) {
2153
+ const fromC = rectCenter(fromBounds);
2154
+ const toC = rectCenter(toBounds);
2155
+ const p0 = resolveAnchor(fromBounds, fromAnchor, toC);
2156
+ const p3 = resolveAnchor(toBounds, toAnchor, fromC);
2157
+ return [p0, p3];
2153
2158
  }
2154
2159
  function orthogonalRoute(fromBounds, toBounds, fromAnchor, toAnchor) {
2155
2160
  const fromC = rectCenter(fromBounds);
@@ -2195,15 +2200,6 @@ function findBoundaryIntersection(p0, cp1, cp2, p3, targetRect, searchFromEnd) {
2195
2200
  }
2196
2201
  return void 0;
2197
2202
  }
2198
- function pointAlongArc(route, t) {
2199
- const [first, second] = route;
2200
- if (t <= 0.5) {
2201
- const localT2 = Math.max(0, Math.min(1, t * 2));
2202
- return bezierPointAt(first[0], first[1], first[2], first[3], localT2);
2203
- }
2204
- const localT = Math.max(0, Math.min(1, (t - 0.5) * 2));
2205
- return bezierPointAt(second[0], second[1], second[2], second[3], localT);
2206
- }
2207
2203
  function computeDiagramCenter(nodeBounds, canvasCenter) {
2208
2204
  if (nodeBounds.length === 0) {
2209
2205
  return canvasCenter ?? { x: 0, y: 0 };
@@ -2257,11 +2253,36 @@ function pointAlongPolyline(points, t) {
2257
2253
  }
2258
2254
  return points[points.length - 1];
2259
2255
  }
2260
- function drawCubicInterpolatedPath(ctx, points, style) {
2256
+ function createConnectionGradient(ctx, start, end, fromColor, baseColor, toColor) {
2257
+ const gradient = ctx.createLinearGradient(start.x, start.y, end.x, end.y);
2258
+ gradient.addColorStop(0, fromColor);
2259
+ gradient.addColorStop(0.5, baseColor);
2260
+ gradient.addColorStop(1, toColor);
2261
+ return gradient;
2262
+ }
2263
+ function resolveConnectionStroke(ctx, start, end, fromColor, baseColor, toColor) {
2264
+ if (!fromColor || !toColor) {
2265
+ return baseColor;
2266
+ }
2267
+ return createConnectionGradient(ctx, start, end, fromColor, baseColor, toColor);
2268
+ }
2269
+ function drawOrthogonalPathWithStroke(ctx, from, to, style, stroke) {
2270
+ const midX = (from.x + to.x) / 2;
2271
+ ctx.strokeStyle = stroke;
2272
+ ctx.lineWidth = style.width;
2273
+ ctx.setLineDash(style.dash ?? []);
2274
+ ctx.beginPath();
2275
+ ctx.moveTo(from.x, from.y);
2276
+ ctx.lineTo(midX, from.y);
2277
+ ctx.lineTo(midX, to.y);
2278
+ ctx.lineTo(to.x, to.y);
2279
+ ctx.stroke();
2280
+ }
2281
+ function drawCubicInterpolatedPath(ctx, points, style, stroke) {
2261
2282
  if (points.length < 2) {
2262
2283
  return;
2263
2284
  }
2264
- ctx.strokeStyle = style.color;
2285
+ ctx.strokeStyle = stroke;
2265
2286
  ctx.lineWidth = style.width;
2266
2287
  ctx.setLineDash(style.dash ?? []);
2267
2288
  ctx.beginPath();
@@ -2301,8 +2322,19 @@ function polylineBounds(points) {
2301
2322
  };
2302
2323
  }
2303
2324
  function renderConnection(ctx, conn, fromBounds, toBounds, theme, edgeRoute, options) {
2304
- const routing = conn.routing ?? "auto";
2305
- const strokeStyle = conn.strokeStyle ?? conn.style ?? "solid";
2325
+ let routing = conn.routing ?? "auto";
2326
+ let curveMode = conn.curveMode ?? "normal";
2327
+ if (conn.strokeStyle !== void 0) {
2328
+ console.warn("connection.strokeStyle is deprecated, use style instead");
2329
+ }
2330
+ if (routing === "arc") {
2331
+ console.warn(
2332
+ "connection routing: 'arc' is deprecated. Use routing: 'curve' with curveMode: 'ellipse' instead."
2333
+ );
2334
+ routing = "curve";
2335
+ curveMode = "ellipse";
2336
+ }
2337
+ const strokeStyle = conn.style ?? conn.strokeStyle ?? "solid";
2306
2338
  const strokeWidth = conn.width ?? conn.strokeWidth ?? 2;
2307
2339
  const tension = conn.tension ?? 0.35;
2308
2340
  const dash = dashFromStyle(strokeStyle);
@@ -2324,15 +2356,31 @@ function renderConnection(ctx, conn, fromBounds, toBounds, theme, edgeRoute, opt
2324
2356
  ctx.globalAlpha = conn.opacity;
2325
2357
  const arrowPlacement = conn.arrowPlacement ?? "endpoint";
2326
2358
  if (routing === "curve") {
2327
- const [p0, cp1, cp2, p3] = curveRoute(
2328
- fromBounds,
2329
- toBounds,
2330
- diagramCenter,
2331
- tension,
2332
- conn.fromAnchor,
2333
- conn.toAnchor
2334
- );
2335
- ctx.strokeStyle = style.color;
2359
+ let p0;
2360
+ let cp1;
2361
+ let cp2;
2362
+ let p3;
2363
+ if (curveMode === "ellipse") {
2364
+ const ellipse = options?.ellipseParams ?? inferEllipseParams([fromBounds, toBounds]);
2365
+ [p0, cp1, cp2, p3] = ellipseRoute(
2366
+ fromBounds,
2367
+ toBounds,
2368
+ ellipse,
2369
+ conn.fromAnchor,
2370
+ conn.toAnchor
2371
+ );
2372
+ } else {
2373
+ [p0, cp1, cp2, p3] = curveRoute(
2374
+ fromBounds,
2375
+ toBounds,
2376
+ diagramCenter,
2377
+ tension,
2378
+ conn.fromAnchor,
2379
+ conn.toAnchor
2380
+ );
2381
+ }
2382
+ const stroke = resolveConnectionStroke(ctx, p0, p3, conn.fromColor, style.color, conn.toColor);
2383
+ ctx.strokeStyle = stroke;
2336
2384
  ctx.lineWidth = style.width;
2337
2385
  ctx.setLineDash(style.dash ?? []);
2338
2386
  ctx.beginPath();
@@ -2363,50 +2411,22 @@ function renderConnection(ctx, conn, fromBounds, toBounds, theme, edgeRoute, opt
2363
2411
  }
2364
2412
  }
2365
2413
  }
2366
- } else if (routing === "arc") {
2367
- const [first, second] = arcRoute(
2368
- fromBounds,
2369
- toBounds,
2370
- diagramCenter,
2371
- tension,
2372
- conn.fromAnchor,
2373
- conn.toAnchor
2374
- );
2375
- const [p0, cp1, cp2, pMid] = first;
2376
- const [, cp3, cp4, p3] = second;
2377
- ctx.strokeStyle = style.color;
2414
+ } else if (routing === "straight") {
2415
+ const [p0, p3] = straightRoute(fromBounds, toBounds, conn.fromAnchor, conn.toAnchor);
2416
+ const stroke = resolveConnectionStroke(ctx, p0, p3, conn.fromColor, style.color, conn.toColor);
2417
+ ctx.strokeStyle = stroke;
2378
2418
  ctx.lineWidth = style.width;
2379
2419
  ctx.setLineDash(style.dash ?? []);
2380
2420
  ctx.beginPath();
2381
2421
  ctx.moveTo(p0.x, p0.y);
2382
- ctx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, pMid.x, pMid.y);
2383
- ctx.bezierCurveTo(cp3.x, cp3.y, cp4.x, cp4.y, p3.x, p3.y);
2422
+ ctx.lineTo(p3.x, p3.y);
2384
2423
  ctx.stroke();
2385
- linePoints = [p0, cp1, cp2, pMid, cp3, cp4, p3];
2424
+ linePoints = [p0, p3];
2386
2425
  startPoint = p0;
2387
2426
  endPoint = p3;
2388
- startAngle = Math.atan2(p0.y - cp1.y, p0.x - cp1.x);
2389
- endAngle = Math.atan2(p3.y - cp4.y, p3.x - cp4.x);
2390
- labelPoint = pointAlongArc([first, second], labelT);
2391
- if (arrowPlacement === "boundary") {
2392
- if (conn.arrow === "end" || conn.arrow === "both") {
2393
- const [, s_cp3, s_cp4, s_p3] = second;
2394
- const tEnd = findBoundaryIntersection(pMid, s_cp3, s_cp4, s_p3, toBounds, true);
2395
- if (tEnd !== void 0) {
2396
- endPoint = bezierPointAt(pMid, s_cp3, s_cp4, s_p3, tEnd);
2397
- const tangent = bezierTangentAt(pMid, s_cp3, s_cp4, s_p3, tEnd);
2398
- endAngle = Math.atan2(tangent.y, tangent.x);
2399
- }
2400
- }
2401
- if (conn.arrow === "start" || conn.arrow === "both") {
2402
- const tStart = findBoundaryIntersection(p0, cp1, cp2, pMid, fromBounds, false);
2403
- if (tStart !== void 0) {
2404
- startPoint = bezierPointAt(p0, cp1, cp2, pMid, tStart);
2405
- const tangent = bezierTangentAt(p0, cp1, cp2, pMid, tStart);
2406
- startAngle = Math.atan2(tangent.y, tangent.x) + Math.PI;
2407
- }
2408
- }
2409
- }
2427
+ startAngle = Math.atan2(p0.y - p3.y, p0.x - p3.x);
2428
+ endAngle = Math.atan2(p3.y - p0.y, p3.x - p0.x);
2429
+ labelPoint = pointAlongPolyline(linePoints, labelT);
2410
2430
  } else {
2411
2431
  const hasAnchorHints = conn.fromAnchor !== void 0 || conn.toAnchor !== void 0;
2412
2432
  const useElkRoute = routing === "auto" && !hasAnchorHints && (edgeRoute?.points.length ?? 0) >= 2;
@@ -2417,10 +2437,18 @@ function renderConnection(ctx, conn, fromBounds, toBounds, theme, edgeRoute, opt
2417
2437
  endPoint = linePoints[linePoints.length - 1] ?? linePoints[0];
2418
2438
  startAngle = Math.atan2(startSegment.y - linePoints[0].y, startSegment.x - linePoints[0].x) + Math.PI;
2419
2439
  endAngle = Math.atan2(endPoint.y - endStart.y, endPoint.x - endStart.x);
2440
+ const stroke = resolveConnectionStroke(
2441
+ ctx,
2442
+ startPoint,
2443
+ endPoint,
2444
+ conn.fromColor,
2445
+ style.color,
2446
+ conn.toColor
2447
+ );
2420
2448
  if (useElkRoute) {
2421
- drawCubicInterpolatedPath(ctx, linePoints, style);
2449
+ drawCubicInterpolatedPath(ctx, linePoints, style, stroke);
2422
2450
  } else {
2423
- drawOrthogonalPath(ctx, startPoint, endPoint, style);
2451
+ drawOrthogonalPathWithStroke(ctx, startPoint, endPoint, style, stroke);
2424
2452
  }
2425
2453
  labelPoint = pointAlongPolyline(linePoints, labelT);
2426
2454
  }
@@ -2725,6 +2753,9 @@ function measureTextBounds(ctx, options) {
2725
2753
  function angleBetween(from, to) {
2726
2754
  return Math.atan2(to.y - from.y, to.x - from.x);
2727
2755
  }
2756
+ function degreesToRadians(angle) {
2757
+ return angle * Math.PI / 180;
2758
+ }
2728
2759
  function pathBounds(operations) {
2729
2760
  let minX = Number.POSITIVE_INFINITY;
2730
2761
  let minY = Number.POSITIVE_INFINITY;
@@ -2962,6 +2993,34 @@ function renderDrawCommands(ctx, commands, theme) {
2962
2993
  });
2963
2994
  break;
2964
2995
  }
2996
+ case "arc": {
2997
+ const startAngle = degreesToRadians(command.startAngle);
2998
+ const endAngle = degreesToRadians(command.endAngle);
2999
+ withOpacity(ctx, command.opacity, () => {
3000
+ applyDrawShadow(ctx, command.shadow);
3001
+ ctx.beginPath();
3002
+ ctx.setLineDash(command.dash ?? []);
3003
+ ctx.lineWidth = command.width;
3004
+ ctx.strokeStyle = command.color;
3005
+ ctx.arc(command.center.x, command.center.y, command.radius, startAngle, endAngle);
3006
+ ctx.stroke();
3007
+ });
3008
+ rendered.push({
3009
+ id,
3010
+ kind: "draw",
3011
+ bounds: expandRect(
3012
+ {
3013
+ x: command.center.x - command.radius,
3014
+ y: command.center.y - command.radius,
3015
+ width: command.radius * 2,
3016
+ height: command.radius * 2
3017
+ },
3018
+ command.width / 2
3019
+ ),
3020
+ foregroundColor: command.color
3021
+ });
3022
+ break;
3023
+ }
2965
3024
  case "bezier": {
2966
3025
  const points = command.points;
2967
3026
  withOpacity(ctx, command.opacity, () => {
@@ -3545,6 +3604,21 @@ var drawLineSchema = z2.object({
3545
3604
  opacity: z2.number().min(0).max(1).default(1),
3546
3605
  shadow: drawShadowSchema.optional()
3547
3606
  }).strict();
3607
+ var drawArcSchema = z2.object({
3608
+ type: z2.literal("arc"),
3609
+ center: z2.object({
3610
+ x: z2.number(),
3611
+ y: z2.number()
3612
+ }).strict(),
3613
+ radius: z2.number().positive(),
3614
+ startAngle: z2.number(),
3615
+ endAngle: z2.number(),
3616
+ color: colorHexSchema2.default("#FFFFFF"),
3617
+ width: z2.number().min(0.5).max(32).default(2),
3618
+ dash: z2.array(z2.number()).max(6).optional(),
3619
+ opacity: z2.number().min(0).max(1).default(1),
3620
+ shadow: drawShadowSchema.optional()
3621
+ }).strict();
3548
3622
  var drawPointSchema = z2.object({
3549
3623
  x: z2.number(),
3550
3624
  y: z2.number()
@@ -3629,6 +3703,7 @@ var drawCommandSchema = z2.discriminatedUnion("type", [
3629
3703
  drawCircleSchema,
3630
3704
  drawTextSchema,
3631
3705
  drawLineSchema,
3706
+ drawArcSchema,
3632
3707
  drawBezierSchema,
3633
3708
  drawPathSchema,
3634
3709
  drawBadgeSchema,
@@ -3759,17 +3834,21 @@ var connectionElementSchema = z2.object({
3759
3834
  from: z2.string().min(1).max(120),
3760
3835
  to: z2.string().min(1).max(120),
3761
3836
  style: z2.enum(["solid", "dashed", "dotted"]).default("solid"),
3762
- strokeStyle: z2.enum(["solid", "dashed", "dotted"]).default("solid"),
3837
+ /** @deprecated Use `style` instead. */
3838
+ strokeStyle: z2.enum(["solid", "dashed", "dotted"]).optional(),
3763
3839
  arrow: z2.enum(["end", "start", "both", "none"]).default("end"),
3764
3840
  label: z2.string().min(1).max(200).optional(),
3765
3841
  labelPosition: z2.enum(["start", "middle", "end"]).default("middle"),
3766
3842
  color: colorHexSchema2.optional(),
3843
+ fromColor: colorHexSchema2.optional(),
3844
+ toColor: colorHexSchema2.optional(),
3767
3845
  width: z2.number().min(0.5).max(10).optional(),
3768
3846
  strokeWidth: z2.number().min(0.5).max(10).default(2),
3769
3847
  arrowSize: z2.number().min(4).max(32).optional(),
3770
3848
  arrowPlacement: z2.enum(["endpoint", "boundary"]).default("endpoint"),
3771
3849
  opacity: z2.number().min(0).max(1).default(1),
3772
- routing: z2.enum(["auto", "orthogonal", "curve", "arc"]).default("auto"),
3850
+ routing: z2.enum(["auto", "orthogonal", "curve", "arc", "straight"]).default("auto"),
3851
+ curveMode: z2.enum(["normal", "ellipse"]).default("normal"),
3773
3852
  tension: z2.number().min(0.1).max(0.8).default(0.35),
3774
3853
  fromAnchor: anchorHintSchema.optional(),
3775
3854
  toAnchor: anchorHintSchema.optional()
@@ -3862,7 +3941,11 @@ var autoLayoutConfigSchema = z2.object({
3862
3941
  /** Sort strategy for radial layout node ordering. Only relevant when algorithm is 'radial'. */
3863
3942
  radialSortBy: z2.enum(["id", "connections"]).optional(),
3864
3943
  /** Explicit center used by curve/arc connection routing. */
3865
- diagramCenter: diagramCenterSchema.optional()
3944
+ diagramCenter: diagramCenterSchema.optional(),
3945
+ /** Horizontal radius for shared ellipse used by curveMode: 'ellipse'. */
3946
+ ellipseRx: z2.number().positive().optional(),
3947
+ /** Vertical radius for shared ellipse used by curveMode: 'ellipse'. */
3948
+ ellipseRy: z2.number().positive().optional()
3866
3949
  }).strict();
3867
3950
  var gridLayoutConfigSchema = z2.object({
3868
3951
  mode: z2.literal("grid"),
@@ -3872,7 +3955,11 @@ var gridLayoutConfigSchema = z2.object({
3872
3955
  cardMaxHeight: z2.number().int().min(32).max(4096).optional(),
3873
3956
  equalHeight: z2.boolean().default(false),
3874
3957
  /** Explicit center used by curve/arc connection routing. */
3875
- diagramCenter: diagramCenterSchema.optional()
3958
+ diagramCenter: diagramCenterSchema.optional(),
3959
+ /** Horizontal radius for shared ellipse used by curveMode: 'ellipse'. */
3960
+ ellipseRx: z2.number().positive().optional(),
3961
+ /** Vertical radius for shared ellipse used by curveMode: 'ellipse'. */
3962
+ ellipseRy: z2.number().positive().optional()
3876
3963
  }).strict();
3877
3964
  var stackLayoutConfigSchema = z2.object({
3878
3965
  mode: z2.literal("stack"),
@@ -3880,7 +3967,11 @@ var stackLayoutConfigSchema = z2.object({
3880
3967
  gap: z2.number().int().min(0).max(256).default(24),
3881
3968
  alignment: z2.enum(["start", "center", "end", "stretch"]).default("stretch"),
3882
3969
  /** Explicit center used by curve/arc connection routing. */
3883
- diagramCenter: diagramCenterSchema.optional()
3970
+ diagramCenter: diagramCenterSchema.optional(),
3971
+ /** Horizontal radius for shared ellipse used by curveMode: 'ellipse'. */
3972
+ ellipseRx: z2.number().positive().optional(),
3973
+ /** Vertical radius for shared ellipse used by curveMode: 'ellipse'. */
3974
+ ellipseRy: z2.number().positive().optional()
3884
3975
  }).strict();
3885
3976
  var manualPositionSchema = z2.object({
3886
3977
  x: z2.number().int(),
@@ -3892,7 +3983,11 @@ var manualLayoutConfigSchema = z2.object({
3892
3983
  mode: z2.literal("manual"),
3893
3984
  positions: z2.record(z2.string().min(1), manualPositionSchema).default({}),
3894
3985
  /** Explicit center used by curve/arc connection routing. */
3895
- diagramCenter: diagramCenterSchema.optional()
3986
+ diagramCenter: diagramCenterSchema.optional(),
3987
+ /** Horizontal radius for shared ellipse used by curveMode: 'ellipse'. */
3988
+ ellipseRx: z2.number().positive().optional(),
3989
+ /** Vertical radius for shared ellipse used by curveMode: 'ellipse'. */
3990
+ ellipseRy: z2.number().positive().optional()
3896
3991
  }).strict();
3897
3992
  var layoutConfigSchema = z2.discriminatedUnion("mode", [
3898
3993
  autoLayoutConfigSchema,
@@ -3957,7 +4052,11 @@ var diagramElementSchema = z2.discriminatedUnion("type", [
3957
4052
  var diagramLayoutSchema = z2.object({
3958
4053
  mode: z2.enum(["manual", "auto"]).default("manual"),
3959
4054
  positions: z2.record(z2.string(), diagramPositionSchema).optional(),
3960
- diagramCenter: diagramCenterSchema.optional()
4055
+ diagramCenter: diagramCenterSchema.optional(),
4056
+ /** Horizontal radius for shared ellipse used by curveMode: 'ellipse'. */
4057
+ ellipseRx: z2.number().positive().optional(),
4058
+ /** Vertical radius for shared ellipse used by curveMode: 'ellipse'. */
4059
+ ellipseRy: z2.number().positive().optional()
3961
4060
  }).strict();
3962
4061
  var diagramSpecSchema = z2.object({
3963
4062
  version: z2.literal(1),
@@ -4149,6 +4248,18 @@ async function renderDesign(input, options = {}) {
4149
4248
  const specHash = computeSpecHash(spec);
4150
4249
  const generatorVersion = options.generatorVersion ?? DEFAULT_GENERATOR_VERSION;
4151
4250
  const renderedAt = options.renderedAt ?? (/* @__PURE__ */ new Date()).toISOString();
4251
+ const iteration = options.iteration;
4252
+ if (iteration) {
4253
+ if (!Number.isInteger(iteration.iteration) || iteration.iteration <= 0) {
4254
+ throw new Error("Iteration metadata requires iteration to be a positive integer.");
4255
+ }
4256
+ if (iteration.maxIterations != null && (!Number.isInteger(iteration.maxIterations) || iteration.maxIterations <= 0)) {
4257
+ throw new Error("Iteration metadata requires maxIterations to be a positive integer.");
4258
+ }
4259
+ if (iteration.maxIterations != null && iteration.maxIterations < iteration.iteration) {
4260
+ throw new Error("Iteration metadata requires maxIterations to be >= iteration.");
4261
+ }
4262
+ }
4152
4263
  const renderScale = resolveRenderScale(spec);
4153
4264
  const canvas = createCanvas(spec.canvas.width * renderScale, spec.canvas.height * renderScale);
4154
4265
  const ctx = canvas.getContext("2d");
@@ -4328,10 +4439,19 @@ async function renderDesign(input, options = {}) {
4328
4439
  break;
4329
4440
  }
4330
4441
  }
4331
- const diagramCenter = spec.layout.diagramCenter ?? computeDiagramCenter(
4332
- spec.elements.filter((element) => element.type !== "connection").map((element) => elementRects.get(element.id)).filter((rect) => rect != null),
4333
- { x: spec.canvas.width / 2, y: spec.canvas.height / 2 }
4442
+ const nodeBounds = spec.elements.filter((element) => element.type !== "connection").map((element) => elementRects.get(element.id)).filter((rect) => rect != null);
4443
+ const diagramCenter = spec.layout.diagramCenter ?? computeDiagramCenter(nodeBounds, { x: spec.canvas.width / 2, y: spec.canvas.height / 2 });
4444
+ const layoutEllipseRx = "ellipseRx" in spec.layout ? spec.layout.ellipseRx : void 0;
4445
+ const layoutEllipseRy = "ellipseRy" in spec.layout ? spec.layout.ellipseRy : void 0;
4446
+ const hasEllipseConnections = spec.elements.some(
4447
+ (el) => el.type === "connection" && (el.curveMode === "ellipse" || el.routing === "arc")
4334
4448
  );
4449
+ const ellipseParams = hasEllipseConnections ? inferEllipseParams(
4450
+ nodeBounds,
4451
+ spec.layout.diagramCenter ?? diagramCenter,
4452
+ layoutEllipseRx,
4453
+ layoutEllipseRy
4454
+ ) : void 0;
4335
4455
  for (const element of spec.elements) {
4336
4456
  if (element.type !== "connection") {
4337
4457
  continue;
@@ -4345,7 +4465,15 @@ async function renderDesign(input, options = {}) {
4345
4465
  }
4346
4466
  const edgeRoute = edgeRoutes?.get(`${element.from}-${element.to}`);
4347
4467
  elements.push(
4348
- ...renderConnection(ctx, element, fromRect, toRect, theme, edgeRoute, { diagramCenter })
4468
+ ...renderConnection(
4469
+ ctx,
4470
+ element,
4471
+ fromRect,
4472
+ toRect,
4473
+ theme,
4474
+ edgeRoute,
4475
+ ellipseParams ? { diagramCenter, ellipseParams } : { diagramCenter }
4476
+ )
4349
4477
  );
4350
4478
  }
4351
4479
  if (footerRect && spec.footer) {
@@ -4389,7 +4517,8 @@ async function renderDesign(input, options = {}) {
4389
4517
  layout: {
4390
4518
  safeFrame,
4391
4519
  elements
4392
- }
4520
+ },
4521
+ ...iteration ? { iteration } : {}
4393
4522
  };
4394
4523
  return {
4395
4524
  png: pngBuffer,