@spectratools/graphic-designer-cli 0.10.0 → 0.12.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
@@ -1005,6 +1005,35 @@ async function computeElkLayout(elements, config, safeFrame) {
1005
1005
  };
1006
1006
  }
1007
1007
 
1008
+ // src/layout/ellipse.ts
1009
+ function clampDimension(estimated, max) {
1010
+ return Math.max(1, Math.min(max, Math.floor(estimated)));
1011
+ }
1012
+ function computeEllipseLayout(elements, config, safeFrame) {
1013
+ const placeable = elements.filter((element) => element.type !== "connection");
1014
+ const positions = /* @__PURE__ */ new Map();
1015
+ if (placeable.length === 0) {
1016
+ return { positions };
1017
+ }
1018
+ const cx = config.cx ?? safeFrame.x + safeFrame.width / 2;
1019
+ const cy = config.cy ?? safeFrame.y + safeFrame.height / 2;
1020
+ const stepDegrees = 360 / placeable.length;
1021
+ for (const [index, element] of placeable.entries()) {
1022
+ const angleRadians = (config.startAngle + index * stepDegrees) * Math.PI / 180;
1023
+ const centerX = cx + config.rx * Math.cos(angleRadians);
1024
+ const centerY = cy + config.ry * Math.sin(angleRadians);
1025
+ const width = clampDimension(estimateElementWidth(element), safeFrame.width);
1026
+ const height = clampDimension(estimateElementHeight(element), safeFrame.height);
1027
+ positions.set(element.id, {
1028
+ x: Math.round(centerX - width / 2),
1029
+ y: Math.round(centerY - height / 2),
1030
+ width,
1031
+ height
1032
+ });
1033
+ }
1034
+ return { positions };
1035
+ }
1036
+
1008
1037
  // src/layout/grid.ts
1009
1038
  function computeGridLayout(elements, config, safeFrame) {
1010
1039
  const placeable = elements.filter((element) => element.type !== "connection");
@@ -1100,6 +1129,8 @@ async function computeLayout(elements, layout, safeFrame) {
1100
1129
  return computeGridLayout(elements, layout, safeFrame);
1101
1130
  case "stack":
1102
1131
  return computeStackLayout(elements, layout, safeFrame);
1132
+ case "ellipse":
1133
+ return computeEllipseLayout(elements, layout, safeFrame);
1103
1134
  case "manual":
1104
1135
  return computeManualLayout(elements, layout, safeFrame);
1105
1136
  default:
@@ -1366,12 +1397,12 @@ var MACOS_DOTS = [
1366
1397
  { fill: "#27C93F", stroke: "#1AAB29" }
1367
1398
  ];
1368
1399
  function drawMacosDots(ctx, x, y) {
1369
- for (const [index, dot2] of MACOS_DOTS.entries()) {
1400
+ for (const [index, dot] of MACOS_DOTS.entries()) {
1370
1401
  ctx.beginPath();
1371
1402
  ctx.arc(x + index * DOT_SPACING, y, DOT_RADIUS, 0, Math.PI * 2);
1372
1403
  ctx.closePath();
1373
- ctx.fillStyle = dot2.fill;
1374
- ctx.strokeStyle = dot2.stroke;
1404
+ ctx.fillStyle = dot.fill;
1405
+ ctx.strokeStyle = dot.stroke;
1375
1406
  ctx.lineWidth = DOT_STROKE_WIDTH;
1376
1407
  ctx.fill();
1377
1408
  ctx.stroke();
@@ -2090,56 +2121,71 @@ function curveRoute(fromBounds, toBounds, diagramCenter, tension, fromAnchor, to
2090
2121
  const cp2 = { x: p3.x + n3.x * offset, y: p3.y + n3.y * offset };
2091
2122
  return [p0, cp1, cp2, p3];
2092
2123
  }
2093
- function dot(a, b) {
2094
- return a.x * b.x + a.y * b.y;
2095
- }
2096
- function localToWorld(origin, axisX, axisY, local) {
2097
- return {
2098
- x: origin.x + axisX.x * local.x + axisY.x * local.y,
2099
- y: origin.y + axisX.y * local.x + axisY.y * local.y
2100
- };
2124
+ function inferEllipseParams(nodeBounds, explicitCenter, explicitRx, explicitRy) {
2125
+ if (nodeBounds.length === 0) {
2126
+ return {
2127
+ cx: explicitCenter?.x ?? 0,
2128
+ cy: explicitCenter?.y ?? 0,
2129
+ rx: explicitRx ?? 1,
2130
+ ry: explicitRy ?? 1
2131
+ };
2132
+ }
2133
+ const centers = nodeBounds.map(rectCenter);
2134
+ const cx = explicitCenter?.x ?? centers.reduce((sum, c) => sum + c.x, 0) / centers.length;
2135
+ const cy = explicitCenter?.y ?? centers.reduce((sum, c) => sum + c.y, 0) / centers.length;
2136
+ const rx = explicitRx ?? Math.max(1, ...centers.map((c) => Math.abs(c.x - cx)));
2137
+ const ry = explicitRy ?? Math.max(1, ...centers.map((c) => Math.abs(c.y - cy)));
2138
+ return { cx, cy, rx, ry };
2101
2139
  }
2102
- function arcRoute(fromBounds, toBounds, diagramCenter, tension, fromAnchor, toAnchor) {
2140
+ function ellipseRoute(fromBounds, toBounds, ellipse, fromAnchor, toAnchor) {
2103
2141
  const fromCenter = rectCenter(fromBounds);
2104
2142
  const toCenter = rectCenter(toBounds);
2105
- const start = resolveAnchor(fromBounds, fromAnchor, toCenter);
2106
- const end = resolveAnchor(toBounds, toAnchor, fromCenter);
2107
- const chord = { x: end.x - start.x, y: end.y - start.y };
2108
- const chordLength = Math.hypot(chord.x, chord.y);
2109
- if (chordLength < 1e-6) {
2110
- const mid = { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 };
2111
- return [
2112
- [start, start, mid, mid],
2113
- [mid, mid, end, end]
2114
- ];
2115
- }
2116
- const axisX = { x: chord.x / chordLength, y: chord.y / chordLength };
2117
- let axisY = { x: -axisX.y, y: axisX.x };
2118
- const midpoint = { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 };
2119
- const outwardHint = outwardNormal(midpoint, diagramCenter);
2120
- if (dot(axisY, outwardHint) < 0) {
2121
- axisY = { x: -axisY.x, y: -axisY.y };
2122
- }
2123
- const semiMajor = chordLength / 2;
2124
- const semiMinor = Math.max(12, chordLength * tension * 0.75);
2125
- const p0Local = { x: -semiMajor, y: 0 };
2126
- const cp1Local = { x: -semiMajor, y: ELLIPSE_KAPPA * semiMinor };
2127
- const cp2Local = { x: -ELLIPSE_KAPPA * semiMajor, y: semiMinor };
2128
- const pMidLocal = { x: 0, y: semiMinor };
2129
- const cp3Local = { x: ELLIPSE_KAPPA * semiMajor, y: semiMinor };
2130
- const cp4Local = { x: semiMajor, y: ELLIPSE_KAPPA * semiMinor };
2131
- const p3Local = { x: semiMajor, y: 0 };
2132
- const p0 = localToWorld(midpoint, axisX, axisY, p0Local);
2133
- const cp1 = localToWorld(midpoint, axisX, axisY, cp1Local);
2134
- const cp2 = localToWorld(midpoint, axisX, axisY, cp2Local);
2135
- const pMid = localToWorld(midpoint, axisX, axisY, pMidLocal);
2136
- const cp3 = localToWorld(midpoint, axisX, axisY, cp3Local);
2137
- const cp4 = localToWorld(midpoint, axisX, axisY, cp4Local);
2138
- const p3 = localToWorld(midpoint, axisX, axisY, p3Local);
2139
- return [
2140
- [p0, cp1, cp2, pMid],
2141
- [pMid, cp3, cp4, p3]
2142
- ];
2143
+ const p0 = resolveAnchor(fromBounds, fromAnchor, toCenter);
2144
+ const p3 = resolveAnchor(toBounds, toAnchor, fromCenter);
2145
+ const theta1 = Math.atan2(
2146
+ (fromCenter.y - ellipse.cy) / ellipse.ry,
2147
+ (fromCenter.x - ellipse.cx) / ellipse.rx
2148
+ );
2149
+ const theta2 = Math.atan2(
2150
+ (toCenter.y - ellipse.cy) / ellipse.ry,
2151
+ (toCenter.x - ellipse.cx) / ellipse.rx
2152
+ );
2153
+ let angularSpan = theta2 - theta1;
2154
+ while (angularSpan > Math.PI) angularSpan -= 2 * Math.PI;
2155
+ while (angularSpan <= -Math.PI) angularSpan += 2 * Math.PI;
2156
+ const absSpan = Math.abs(angularSpan);
2157
+ const kappa = absSpan < 1e-6 ? 0 : 4 / 3 * Math.tan(absSpan / 4);
2158
+ const tangent1 = {
2159
+ x: -ellipse.rx * Math.sin(theta1),
2160
+ y: ellipse.ry * Math.cos(theta1)
2161
+ };
2162
+ const tangent2 = {
2163
+ x: -ellipse.rx * Math.sin(theta2),
2164
+ y: ellipse.ry * Math.cos(theta2)
2165
+ };
2166
+ const len1 = Math.hypot(tangent1.x, tangent1.y) || 1;
2167
+ const len2 = Math.hypot(tangent2.x, tangent2.y) || 1;
2168
+ const norm1 = { x: tangent1.x / len1, y: tangent1.y / len1 };
2169
+ const norm2 = { x: tangent2.x / len2, y: tangent2.y / len2 };
2170
+ const chordLength = Math.hypot(p3.x - p0.x, p3.y - p0.y);
2171
+ const cpDistance = chordLength * kappa * 0.5;
2172
+ const sign = angularSpan >= 0 ? 1 : -1;
2173
+ const cp1 = {
2174
+ x: p0.x + norm1.x * cpDistance * sign,
2175
+ y: p0.y + norm1.y * cpDistance * sign
2176
+ };
2177
+ const cp2 = {
2178
+ x: p3.x - norm2.x * cpDistance * sign,
2179
+ y: p3.y - norm2.y * cpDistance * sign
2180
+ };
2181
+ return [p0, cp1, cp2, p3];
2182
+ }
2183
+ function straightRoute(fromBounds, toBounds, fromAnchor, toAnchor) {
2184
+ const fromC = rectCenter(fromBounds);
2185
+ const toC = rectCenter(toBounds);
2186
+ const p0 = resolveAnchor(fromBounds, fromAnchor, toC);
2187
+ const p3 = resolveAnchor(toBounds, toAnchor, fromC);
2188
+ return [p0, p3];
2143
2189
  }
2144
2190
  function orthogonalRoute(fromBounds, toBounds, fromAnchor, toAnchor) {
2145
2191
  const fromC = rectCenter(fromBounds);
@@ -2185,15 +2231,6 @@ function findBoundaryIntersection(p0, cp1, cp2, p3, targetRect, searchFromEnd) {
2185
2231
  }
2186
2232
  return void 0;
2187
2233
  }
2188
- function pointAlongArc(route, t) {
2189
- const [first, second] = route;
2190
- if (t <= 0.5) {
2191
- const localT2 = Math.max(0, Math.min(1, t * 2));
2192
- return bezierPointAt(first[0], first[1], first[2], first[3], localT2);
2193
- }
2194
- const localT = Math.max(0, Math.min(1, (t - 0.5) * 2));
2195
- return bezierPointAt(second[0], second[1], second[2], second[3], localT);
2196
- }
2197
2234
  function computeDiagramCenter(nodeBounds, canvasCenter) {
2198
2235
  if (nodeBounds.length === 0) {
2199
2236
  return canvasCenter ?? { x: 0, y: 0 };
@@ -2316,8 +2353,19 @@ function polylineBounds(points) {
2316
2353
  };
2317
2354
  }
2318
2355
  function renderConnection(ctx, conn, fromBounds, toBounds, theme, edgeRoute, options) {
2319
- const routing = conn.routing ?? "auto";
2320
- const strokeStyle = conn.strokeStyle ?? conn.style ?? "solid";
2356
+ let routing = conn.routing ?? "auto";
2357
+ let curveMode = conn.curveMode ?? "normal";
2358
+ if (conn.strokeStyle !== void 0) {
2359
+ console.warn("connection.strokeStyle is deprecated, use style instead");
2360
+ }
2361
+ if (routing === "arc") {
2362
+ console.warn(
2363
+ "connection routing: 'arc' is deprecated. Use routing: 'curve' with curveMode: 'ellipse' instead."
2364
+ );
2365
+ routing = "curve";
2366
+ curveMode = "ellipse";
2367
+ }
2368
+ const strokeStyle = conn.style ?? conn.strokeStyle ?? "solid";
2321
2369
  const strokeWidth = conn.width ?? conn.strokeWidth ?? 2;
2322
2370
  const tension = conn.tension ?? 0.35;
2323
2371
  const dash = dashFromStyle(strokeStyle);
@@ -2339,14 +2387,29 @@ function renderConnection(ctx, conn, fromBounds, toBounds, theme, edgeRoute, opt
2339
2387
  ctx.globalAlpha = conn.opacity;
2340
2388
  const arrowPlacement = conn.arrowPlacement ?? "endpoint";
2341
2389
  if (routing === "curve") {
2342
- const [p0, cp1, cp2, p3] = curveRoute(
2343
- fromBounds,
2344
- toBounds,
2345
- diagramCenter,
2346
- tension,
2347
- conn.fromAnchor,
2348
- conn.toAnchor
2349
- );
2390
+ let p0;
2391
+ let cp1;
2392
+ let cp2;
2393
+ let p3;
2394
+ if (curveMode === "ellipse") {
2395
+ const ellipse = options?.ellipseParams ?? inferEllipseParams([fromBounds, toBounds]);
2396
+ [p0, cp1, cp2, p3] = ellipseRoute(
2397
+ fromBounds,
2398
+ toBounds,
2399
+ ellipse,
2400
+ conn.fromAnchor,
2401
+ conn.toAnchor
2402
+ );
2403
+ } else {
2404
+ [p0, cp1, cp2, p3] = curveRoute(
2405
+ fromBounds,
2406
+ toBounds,
2407
+ diagramCenter,
2408
+ tension,
2409
+ conn.fromAnchor,
2410
+ conn.toAnchor
2411
+ );
2412
+ }
2350
2413
  const stroke = resolveConnectionStroke(ctx, p0, p3, conn.fromColor, style.color, conn.toColor);
2351
2414
  ctx.strokeStyle = stroke;
2352
2415
  ctx.lineWidth = style.width;
@@ -2379,51 +2442,22 @@ function renderConnection(ctx, conn, fromBounds, toBounds, theme, edgeRoute, opt
2379
2442
  }
2380
2443
  }
2381
2444
  }
2382
- } else if (routing === "arc") {
2383
- const [first, second] = arcRoute(
2384
- fromBounds,
2385
- toBounds,
2386
- diagramCenter,
2387
- tension,
2388
- conn.fromAnchor,
2389
- conn.toAnchor
2390
- );
2391
- const [p0, cp1, cp2, pMid] = first;
2392
- const [, cp3, cp4, p3] = second;
2445
+ } else if (routing === "straight") {
2446
+ const [p0, p3] = straightRoute(fromBounds, toBounds, conn.fromAnchor, conn.toAnchor);
2393
2447
  const stroke = resolveConnectionStroke(ctx, p0, p3, conn.fromColor, style.color, conn.toColor);
2394
2448
  ctx.strokeStyle = stroke;
2395
2449
  ctx.lineWidth = style.width;
2396
2450
  ctx.setLineDash(style.dash ?? []);
2397
2451
  ctx.beginPath();
2398
2452
  ctx.moveTo(p0.x, p0.y);
2399
- ctx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, pMid.x, pMid.y);
2400
- ctx.bezierCurveTo(cp3.x, cp3.y, cp4.x, cp4.y, p3.x, p3.y);
2453
+ ctx.lineTo(p3.x, p3.y);
2401
2454
  ctx.stroke();
2402
- linePoints = [p0, cp1, cp2, pMid, cp3, cp4, p3];
2455
+ linePoints = [p0, p3];
2403
2456
  startPoint = p0;
2404
2457
  endPoint = p3;
2405
- startAngle = Math.atan2(p0.y - cp1.y, p0.x - cp1.x);
2406
- endAngle = Math.atan2(p3.y - cp4.y, p3.x - cp4.x);
2407
- labelPoint = pointAlongArc([first, second], labelT);
2408
- if (arrowPlacement === "boundary") {
2409
- if (conn.arrow === "end" || conn.arrow === "both") {
2410
- const [, s_cp3, s_cp4, s_p3] = second;
2411
- const tEnd = findBoundaryIntersection(pMid, s_cp3, s_cp4, s_p3, toBounds, true);
2412
- if (tEnd !== void 0) {
2413
- endPoint = bezierPointAt(pMid, s_cp3, s_cp4, s_p3, tEnd);
2414
- const tangent = bezierTangentAt(pMid, s_cp3, s_cp4, s_p3, tEnd);
2415
- endAngle = Math.atan2(tangent.y, tangent.x);
2416
- }
2417
- }
2418
- if (conn.arrow === "start" || conn.arrow === "both") {
2419
- const tStart = findBoundaryIntersection(p0, cp1, cp2, pMid, fromBounds, false);
2420
- if (tStart !== void 0) {
2421
- startPoint = bezierPointAt(p0, cp1, cp2, pMid, tStart);
2422
- const tangent = bezierTangentAt(p0, cp1, cp2, pMid, tStart);
2423
- startAngle = Math.atan2(tangent.y, tangent.x) + Math.PI;
2424
- }
2425
- }
2426
- }
2458
+ startAngle = Math.atan2(p0.y - p3.y, p0.x - p3.x);
2459
+ endAngle = Math.atan2(p3.y - p0.y, p3.x - p0.x);
2460
+ labelPoint = pointAlongPolyline(linePoints, labelT);
2427
2461
  } else {
2428
2462
  const hasAnchorHints = conn.fromAnchor !== void 0 || conn.toAnchor !== void 0;
2429
2463
  const useElkRoute = routing === "auto" && !hasAnchorHints && (edgeRoute?.points.length ?? 0) >= 2;
@@ -2689,6 +2723,24 @@ function fromPoints(points) {
2689
2723
  function resolveDrawFont(theme, family) {
2690
2724
  return resolveFont(theme.fonts[family], family);
2691
2725
  }
2726
+ function createDrawStrokeGradient(ctx, start, end, strokeGradient) {
2727
+ const gradient = ctx.createLinearGradient(start.x, start.y, end.x, end.y);
2728
+ gradient.addColorStop(0, strokeGradient.from);
2729
+ gradient.addColorStop(1, strokeGradient.to);
2730
+ return gradient;
2731
+ }
2732
+ function resolveDrawStroke(ctx, start, end, color, strokeGradient) {
2733
+ if (!strokeGradient) {
2734
+ return color;
2735
+ }
2736
+ return createDrawStrokeGradient(ctx, start, end, strokeGradient);
2737
+ }
2738
+ function resolveArrowFill(color, strokeGradient, position) {
2739
+ if (!strokeGradient) {
2740
+ return color;
2741
+ }
2742
+ return position === "start" ? strokeGradient.from : strokeGradient.to;
2743
+ }
2692
2744
  function measureSpacedTextWidth(ctx, text, letterSpacing) {
2693
2745
  const chars = [...text];
2694
2746
  if (chars.length === 0) {
@@ -2967,18 +3019,31 @@ function renderDrawCommands(ctx, commands, theme) {
2967
3019
  const from = { x: command.x1, y: command.y1 };
2968
3020
  const to = { x: command.x2, y: command.y2 };
2969
3021
  const lineAngle = angleBetween(from, to);
3022
+ const stroke = resolveDrawStroke(ctx, from, to, command.color, command.strokeGradient);
2970
3023
  withOpacity(ctx, command.opacity, () => {
2971
3024
  applyDrawShadow(ctx, command.shadow);
2972
3025
  drawLine(ctx, from, to, {
2973
- color: command.color,
3026
+ color: stroke,
2974
3027
  width: command.width,
2975
3028
  ...command.dash ? { dash: command.dash } : {}
2976
3029
  });
2977
3030
  if (command.arrow === "end" || command.arrow === "both") {
2978
- drawArrowhead(ctx, to, lineAngle, command.arrowSize, command.color);
3031
+ drawArrowhead(
3032
+ ctx,
3033
+ to,
3034
+ lineAngle,
3035
+ command.arrowSize,
3036
+ resolveArrowFill(command.color, command.strokeGradient, "end")
3037
+ );
2979
3038
  }
2980
3039
  if (command.arrow === "start" || command.arrow === "both") {
2981
- drawArrowhead(ctx, from, lineAngle + Math.PI, command.arrowSize, command.color);
3040
+ drawArrowhead(
3041
+ ctx,
3042
+ from,
3043
+ lineAngle + Math.PI,
3044
+ command.arrowSize,
3045
+ resolveArrowFill(command.color, command.strokeGradient, "start")
3046
+ );
2982
3047
  }
2983
3048
  });
2984
3049
  const arrowPadding = command.arrow === "none" ? 0 : command.arrowSize;
@@ -2986,7 +3051,7 @@ function renderDrawCommands(ctx, commands, theme) {
2986
3051
  id,
2987
3052
  kind: "draw",
2988
3053
  bounds: expandRect(fromPoints([from, to]), Math.max(command.width / 2, arrowPadding)),
2989
- foregroundColor: command.color
3054
+ foregroundColor: command.strokeGradient?.from ?? command.color
2990
3055
  });
2991
3056
  break;
2992
3057
  }
@@ -3020,10 +3085,17 @@ function renderDrawCommands(ctx, commands, theme) {
3020
3085
  }
3021
3086
  case "bezier": {
3022
3087
  const points = command.points;
3088
+ const stroke = resolveDrawStroke(
3089
+ ctx,
3090
+ points[0],
3091
+ points[points.length - 1],
3092
+ command.color,
3093
+ command.strokeGradient
3094
+ );
3023
3095
  withOpacity(ctx, command.opacity, () => {
3024
3096
  applyDrawShadow(ctx, command.shadow);
3025
3097
  drawBezier(ctx, points, {
3026
- color: command.color,
3098
+ color: stroke,
3027
3099
  width: command.width,
3028
3100
  ...command.dash ? { dash: command.dash } : {}
3029
3101
  });
@@ -3035,11 +3107,17 @@ function renderDrawCommands(ctx, commands, theme) {
3035
3107
  points[points.length - 1],
3036
3108
  endAngle,
3037
3109
  command.arrowSize,
3038
- command.color
3110
+ resolveArrowFill(command.color, command.strokeGradient, "end")
3039
3111
  );
3040
3112
  }
3041
3113
  if (command.arrow === "start" || command.arrow === "both") {
3042
- drawArrowhead(ctx, points[0], startAngle + Math.PI, command.arrowSize, command.color);
3114
+ drawArrowhead(
3115
+ ctx,
3116
+ points[0],
3117
+ startAngle + Math.PI,
3118
+ command.arrowSize,
3119
+ resolveArrowFill(command.color, command.strokeGradient, "start")
3120
+ );
3043
3121
  }
3044
3122
  });
3045
3123
  const arrowPadding = command.arrow === "none" ? 0 : command.arrowSize;
@@ -3047,7 +3125,7 @@ function renderDrawCommands(ctx, commands, theme) {
3047
3125
  id,
3048
3126
  kind: "draw",
3049
3127
  bounds: expandRect(fromPoints(points), Math.max(command.width / 2, arrowPadding)),
3050
- foregroundColor: command.color
3128
+ foregroundColor: command.strokeGradient?.from ?? command.color
3051
3129
  });
3052
3130
  break;
3053
3131
  }
@@ -3547,6 +3625,10 @@ var drawShadowSchema = z2.object({
3547
3625
  offsetY: z2.number().default(4)
3548
3626
  }).strict();
3549
3627
  var drawFontFamilySchema = z2.enum(["heading", "body", "mono"]);
3628
+ var strokeGradientSchema = z2.object({
3629
+ from: colorHexSchema2,
3630
+ to: colorHexSchema2
3631
+ }).strict();
3550
3632
  var drawRectSchema = z2.object({
3551
3633
  type: z2.literal("rect"),
3552
3634
  x: z2.number(),
@@ -3594,6 +3676,7 @@ var drawLineSchema = z2.object({
3594
3676
  x2: z2.number(),
3595
3677
  y2: z2.number(),
3596
3678
  color: colorHexSchema2.default("#FFFFFF"),
3679
+ strokeGradient: strokeGradientSchema.optional(),
3597
3680
  width: z2.number().min(0.5).max(32).default(2),
3598
3681
  dash: z2.array(z2.number()).max(6).optional(),
3599
3682
  arrow: z2.enum(["none", "end", "start", "both"]).default("none"),
@@ -3624,6 +3707,7 @@ var drawBezierSchema = z2.object({
3624
3707
  type: z2.literal("bezier"),
3625
3708
  points: z2.array(drawPointSchema).min(2).max(20),
3626
3709
  color: colorHexSchema2.default("#FFFFFF"),
3710
+ strokeGradient: strokeGradientSchema.optional(),
3627
3711
  width: z2.number().min(0.5).max(32).default(2),
3628
3712
  dash: z2.array(z2.number()).max(6).optional(),
3629
3713
  arrow: z2.enum(["none", "end", "start", "both"]).default("none"),
@@ -3831,7 +3915,8 @@ var connectionElementSchema = z2.object({
3831
3915
  from: z2.string().min(1).max(120),
3832
3916
  to: z2.string().min(1).max(120),
3833
3917
  style: z2.enum(["solid", "dashed", "dotted"]).default("solid"),
3834
- strokeStyle: z2.enum(["solid", "dashed", "dotted"]).default("solid"),
3918
+ /** @deprecated Use `style` instead. */
3919
+ strokeStyle: z2.enum(["solid", "dashed", "dotted"]).optional(),
3835
3920
  arrow: z2.enum(["end", "start", "both", "none"]).default("end"),
3836
3921
  label: z2.string().min(1).max(200).optional(),
3837
3922
  labelPosition: z2.enum(["start", "middle", "end"]).default("middle"),
@@ -3843,7 +3928,8 @@ var connectionElementSchema = z2.object({
3843
3928
  arrowSize: z2.number().min(4).max(32).optional(),
3844
3929
  arrowPlacement: z2.enum(["endpoint", "boundary"]).default("endpoint"),
3845
3930
  opacity: z2.number().min(0).max(1).default(1),
3846
- routing: z2.enum(["auto", "orthogonal", "curve", "arc"]).default("auto"),
3931
+ routing: z2.enum(["auto", "orthogonal", "curve", "arc", "straight"]).default("auto"),
3932
+ curveMode: z2.enum(["normal", "ellipse"]).default("normal"),
3847
3933
  tension: z2.number().min(0.1).max(0.8).default(0.35),
3848
3934
  fromAnchor: anchorHintSchema.optional(),
3849
3935
  toAnchor: anchorHintSchema.optional()
@@ -3936,7 +4022,11 @@ var autoLayoutConfigSchema = z2.object({
3936
4022
  /** Sort strategy for radial layout node ordering. Only relevant when algorithm is 'radial'. */
3937
4023
  radialSortBy: z2.enum(["id", "connections"]).optional(),
3938
4024
  /** Explicit center used by curve/arc connection routing. */
3939
- diagramCenter: diagramCenterSchema.optional()
4025
+ diagramCenter: diagramCenterSchema.optional(),
4026
+ /** Horizontal radius for shared ellipse used by curveMode: 'ellipse'. */
4027
+ ellipseRx: z2.number().positive().optional(),
4028
+ /** Vertical radius for shared ellipse used by curveMode: 'ellipse'. */
4029
+ ellipseRy: z2.number().positive().optional()
3940
4030
  }).strict();
3941
4031
  var gridLayoutConfigSchema = z2.object({
3942
4032
  mode: z2.literal("grid"),
@@ -3946,7 +4036,11 @@ var gridLayoutConfigSchema = z2.object({
3946
4036
  cardMaxHeight: z2.number().int().min(32).max(4096).optional(),
3947
4037
  equalHeight: z2.boolean().default(false),
3948
4038
  /** Explicit center used by curve/arc connection routing. */
3949
- diagramCenter: diagramCenterSchema.optional()
4039
+ diagramCenter: diagramCenterSchema.optional(),
4040
+ /** Horizontal radius for shared ellipse used by curveMode: 'ellipse'. */
4041
+ ellipseRx: z2.number().positive().optional(),
4042
+ /** Vertical radius for shared ellipse used by curveMode: 'ellipse'. */
4043
+ ellipseRy: z2.number().positive().optional()
3950
4044
  }).strict();
3951
4045
  var stackLayoutConfigSchema = z2.object({
3952
4046
  mode: z2.literal("stack"),
@@ -3954,7 +4048,25 @@ var stackLayoutConfigSchema = z2.object({
3954
4048
  gap: z2.number().int().min(0).max(256).default(24),
3955
4049
  alignment: z2.enum(["start", "center", "end", "stretch"]).default("stretch"),
3956
4050
  /** Explicit center used by curve/arc connection routing. */
3957
- diagramCenter: diagramCenterSchema.optional()
4051
+ diagramCenter: diagramCenterSchema.optional(),
4052
+ /** Horizontal radius for shared ellipse used by curveMode: 'ellipse'. */
4053
+ ellipseRx: z2.number().positive().optional(),
4054
+ /** Vertical radius for shared ellipse used by curveMode: 'ellipse'. */
4055
+ ellipseRy: z2.number().positive().optional()
4056
+ }).strict();
4057
+ var ellipseLayoutConfigSchema = z2.object({
4058
+ mode: z2.literal("ellipse"),
4059
+ cx: z2.number().optional(),
4060
+ cy: z2.number().optional(),
4061
+ rx: z2.number().positive(),
4062
+ ry: z2.number().positive(),
4063
+ startAngle: z2.number().default(-90),
4064
+ /** Explicit center used by curve/arc connection routing. */
4065
+ diagramCenter: diagramCenterSchema.optional(),
4066
+ /** Horizontal radius for shared ellipse used by curveMode: 'ellipse'. */
4067
+ ellipseRx: z2.number().positive().optional(),
4068
+ /** Vertical radius for shared ellipse used by curveMode: 'ellipse'. */
4069
+ ellipseRy: z2.number().positive().optional()
3958
4070
  }).strict();
3959
4071
  var manualPositionSchema = z2.object({
3960
4072
  x: z2.number().int(),
@@ -3966,12 +4078,17 @@ var manualLayoutConfigSchema = z2.object({
3966
4078
  mode: z2.literal("manual"),
3967
4079
  positions: z2.record(z2.string().min(1), manualPositionSchema).default({}),
3968
4080
  /** Explicit center used by curve/arc connection routing. */
3969
- diagramCenter: diagramCenterSchema.optional()
4081
+ diagramCenter: diagramCenterSchema.optional(),
4082
+ /** Horizontal radius for shared ellipse used by curveMode: 'ellipse'. */
4083
+ ellipseRx: z2.number().positive().optional(),
4084
+ /** Vertical radius for shared ellipse used by curveMode: 'ellipse'. */
4085
+ ellipseRy: z2.number().positive().optional()
3970
4086
  }).strict();
3971
4087
  var layoutConfigSchema = z2.discriminatedUnion("mode", [
3972
4088
  autoLayoutConfigSchema,
3973
4089
  gridLayoutConfigSchema,
3974
4090
  stackLayoutConfigSchema,
4091
+ ellipseLayoutConfigSchema,
3975
4092
  manualLayoutConfigSchema
3976
4093
  ]);
3977
4094
  var constraintsSchema = z2.object({
@@ -4031,7 +4148,11 @@ var diagramElementSchema = z2.discriminatedUnion("type", [
4031
4148
  var diagramLayoutSchema = z2.object({
4032
4149
  mode: z2.enum(["manual", "auto"]).default("manual"),
4033
4150
  positions: z2.record(z2.string(), diagramPositionSchema).optional(),
4034
- diagramCenter: diagramCenterSchema.optional()
4151
+ diagramCenter: diagramCenterSchema.optional(),
4152
+ /** Horizontal radius for shared ellipse used by curveMode: 'ellipse'. */
4153
+ ellipseRx: z2.number().positive().optional(),
4154
+ /** Vertical radius for shared ellipse used by curveMode: 'ellipse'. */
4155
+ ellipseRy: z2.number().positive().optional()
4035
4156
  }).strict();
4036
4157
  var diagramSpecSchema = z2.object({
4037
4158
  version: z2.literal(1),
@@ -4414,10 +4535,19 @@ async function renderDesign(input, options = {}) {
4414
4535
  break;
4415
4536
  }
4416
4537
  }
4417
- const diagramCenter = spec.layout.diagramCenter ?? computeDiagramCenter(
4418
- spec.elements.filter((element) => element.type !== "connection").map((element) => elementRects.get(element.id)).filter((rect) => rect != null),
4419
- { x: spec.canvas.width / 2, y: spec.canvas.height / 2 }
4538
+ const nodeBounds = spec.elements.filter((element) => element.type !== "connection").map((element) => elementRects.get(element.id)).filter((rect) => rect != null);
4539
+ const diagramCenter = spec.layout.diagramCenter ?? computeDiagramCenter(nodeBounds, { x: spec.canvas.width / 2, y: spec.canvas.height / 2 });
4540
+ const layoutEllipseRx = "ellipseRx" in spec.layout ? spec.layout.ellipseRx : void 0;
4541
+ const layoutEllipseRy = "ellipseRy" in spec.layout ? spec.layout.ellipseRy : void 0;
4542
+ const hasEllipseConnections = spec.elements.some(
4543
+ (el) => el.type === "connection" && (el.curveMode === "ellipse" || el.routing === "arc")
4420
4544
  );
4545
+ const ellipseParams = hasEllipseConnections ? inferEllipseParams(
4546
+ nodeBounds,
4547
+ spec.layout.diagramCenter ?? diagramCenter,
4548
+ layoutEllipseRx,
4549
+ layoutEllipseRy
4550
+ ) : void 0;
4421
4551
  for (const element of spec.elements) {
4422
4552
  if (element.type !== "connection") {
4423
4553
  continue;
@@ -4431,7 +4561,15 @@ async function renderDesign(input, options = {}) {
4431
4561
  }
4432
4562
  const edgeRoute = edgeRoutes?.get(`${element.from}-${element.to}`);
4433
4563
  elements.push(
4434
- ...renderConnection(ctx, element, fromRect, toRect, theme, edgeRoute, { diagramCenter })
4564
+ ...renderConnection(
4565
+ ctx,
4566
+ element,
4567
+ fromRect,
4568
+ toRect,
4569
+ theme,
4570
+ edgeRoute,
4571
+ ellipseParams ? { diagramCenter, ellipseParams } : { diagramCenter }
4572
+ )
4435
4573
  );
4436
4574
  }
4437
4575
  if (footerRect && spec.footer) {