@spectratools/graphic-designer-cli 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/renderer.js CHANGED
@@ -115,9 +115,9 @@ function drawRoundedRect(ctx, rect, radius, fill, stroke) {
115
115
  roundRectPath(ctx, rect, radius);
116
116
  fillAndStroke(ctx, fill, stroke);
117
117
  }
118
- function drawCircle(ctx, center2, radius, fill, stroke) {
118
+ function drawCircle(ctx, center, radius, fill, stroke) {
119
119
  ctx.beginPath();
120
- ctx.arc(center2.x, center2.y, Math.max(0, radius), 0, Math.PI * 2);
120
+ ctx.arc(center.x, center.y, Math.max(0, radius), 0, Math.PI * 2);
121
121
  ctx.closePath();
122
122
  fillAndStroke(ctx, fill, stroke);
123
123
  }
@@ -339,6 +339,10 @@ function relativeLuminance(hexColor) {
339
339
  const b = srgbToLinear(rgb.b);
340
340
  return 0.2126 * r + 0.7152 * g + 0.0722 * b;
341
341
  }
342
+ function withAlpha(hexColor, opacity) {
343
+ const rgb = parseHexColor(hexColor);
344
+ return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${opacity})`;
345
+ }
342
346
  function blendColorWithOpacity(foreground, background, opacity) {
343
347
  const fg = parseHexColor(foreground);
344
348
  const bg = parseHexColor(background);
@@ -441,15 +445,34 @@ function renderFlowNode(ctx, node, bounds, theme) {
441
445
  const badgeBackground = node.badgeBackground ?? borderColor ?? theme.accent;
442
446
  ctx.save();
443
447
  ctx.lineWidth = borderWidth;
448
+ if (node.shadow) {
449
+ const shadowColor = node.shadow.color ?? borderColor ?? theme.accent;
450
+ ctx.shadowColor = withAlpha(shadowColor, node.shadow.opacity);
451
+ ctx.shadowBlur = node.shadow.blur;
452
+ ctx.shadowOffsetX = node.shadow.offsetX;
453
+ ctx.shadowOffsetY = node.shadow.offsetY;
454
+ }
444
455
  if (fillOpacity < 1) {
445
456
  ctx.globalAlpha = node.opacity * fillOpacity;
446
457
  drawNodeShape(ctx, node.shape, bounds, fillColor, void 0, cornerRadius);
458
+ if (node.shadow) {
459
+ ctx.shadowColor = "transparent";
460
+ ctx.shadowBlur = 0;
461
+ ctx.shadowOffsetX = 0;
462
+ ctx.shadowOffsetY = 0;
463
+ }
447
464
  ctx.globalAlpha = node.opacity;
448
465
  drawNodeShape(ctx, node.shape, bounds, "rgba(0,0,0,0)", borderColor, cornerRadius);
449
466
  } else {
450
467
  ctx.globalAlpha = node.opacity;
451
468
  drawNodeShape(ctx, node.shape, bounds, fillColor, borderColor, cornerRadius);
452
469
  }
470
+ if (node.shadow) {
471
+ ctx.shadowColor = "transparent";
472
+ ctx.shadowBlur = 0;
473
+ ctx.shadowOffsetX = 0;
474
+ ctx.shadowOffsetY = 0;
475
+ }
453
476
  const headingFont = resolveFont(theme.fonts.heading, "heading");
454
477
  const bodyFont = resolveFont(theme.fonts.body, "body");
455
478
  const monoFont = resolveFont(theme.fonts.mono, "mono");
@@ -1121,7 +1144,7 @@ function parseHexColor2(color) {
1121
1144
  a: normalized.length === 8 ? parseChannel2(6) / 255 : 1
1122
1145
  };
1123
1146
  }
1124
- function withAlpha(color, alpha) {
1147
+ function withAlpha2(color, alpha) {
1125
1148
  const parsed = parseHexColor2(color);
1126
1149
  const effectiveAlpha = clamp01(parsed.a * alpha);
1127
1150
  return `rgba(${parsed.r}, ${parsed.g}, ${parsed.b}, ${effectiveAlpha})`;
@@ -1178,9 +1201,9 @@ function drawVignette(ctx, width, height, intensity = 0.3, color = "#000000") {
1178
1201
  centerY,
1179
1202
  outerRadius
1180
1203
  );
1181
- vignette.addColorStop(0, withAlpha(color, 0));
1182
- vignette.addColorStop(0.6, withAlpha(color, 0));
1183
- vignette.addColorStop(1, withAlpha(color, clamp01(intensity)));
1204
+ vignette.addColorStop(0, withAlpha2(color, 0));
1205
+ vignette.addColorStop(0.6, withAlpha2(color, 0));
1206
+ vignette.addColorStop(1, withAlpha2(color, clamp01(intensity)));
1184
1207
  ctx.save();
1185
1208
  ctx.fillStyle = vignette;
1186
1209
  ctx.fillRect(0, 0, width, height);
@@ -1311,12 +1334,12 @@ var MACOS_DOTS = [
1311
1334
  { fill: "#27C93F", stroke: "#1AAB29" }
1312
1335
  ];
1313
1336
  function drawMacosDots(ctx, x, y) {
1314
- for (const [index, dot] of MACOS_DOTS.entries()) {
1337
+ for (const [index, dot2] of MACOS_DOTS.entries()) {
1315
1338
  ctx.beginPath();
1316
1339
  ctx.arc(x + index * DOT_SPACING, y, DOT_RADIUS, 0, Math.PI * 2);
1317
1340
  ctx.closePath();
1318
- ctx.fillStyle = dot.fill;
1319
- ctx.strokeStyle = dot.stroke;
1341
+ ctx.fillStyle = dot2.fill;
1342
+ ctx.strokeStyle = dot2.stroke;
1320
1343
  ctx.lineWidth = DOT_STROKE_WIDTH;
1321
1344
  ctx.fill();
1322
1345
  ctx.stroke();
@@ -1965,25 +1988,134 @@ function drawOrthogonalPath(ctx, from, to, style) {
1965
1988
  }
1966
1989
 
1967
1990
  // src/renderers/connection.ts
1968
- function center(rect) {
1991
+ var ELLIPSE_KAPPA = 4 * (Math.sqrt(2) - 1) / 3;
1992
+ function rectCenter(rect) {
1969
1993
  return {
1970
1994
  x: rect.x + rect.width / 2,
1971
1995
  y: rect.y + rect.height / 2
1972
1996
  };
1973
1997
  }
1974
- function edgeAnchor(rect, target) {
1975
- const c = center(rect);
1998
+ function edgeAnchor(bounds, target) {
1999
+ const c = rectCenter(bounds);
1976
2000
  const dx = target.x - c.x;
1977
2001
  const dy = target.y - c.y;
1978
- if (Math.abs(dx) >= Math.abs(dy)) {
1979
- return {
1980
- x: dx >= 0 ? rect.x + rect.width : rect.x,
1981
- y: c.y
1982
- };
2002
+ if (dx === 0 && dy === 0) {
2003
+ return { x: c.x, y: c.y - bounds.height / 2 };
2004
+ }
2005
+ const hw = bounds.width / 2;
2006
+ const hh = bounds.height / 2;
2007
+ const absDx = Math.abs(dx);
2008
+ const absDy = Math.abs(dy);
2009
+ const t = absDx * hh > absDy * hw ? hw / absDx : hh / absDy;
2010
+ return { x: c.x + dx * t, y: c.y + dy * t };
2011
+ }
2012
+ function outwardNormal(point, diagramCenter) {
2013
+ const dx = point.x - diagramCenter.x;
2014
+ const dy = point.y - diagramCenter.y;
2015
+ const len = Math.hypot(dx, dy) || 1;
2016
+ return { x: dx / len, y: dy / len };
2017
+ }
2018
+ function curveRoute(fromBounds, toBounds, diagramCenter, tension) {
2019
+ const fromCenter = rectCenter(fromBounds);
2020
+ const toCenter = rectCenter(toBounds);
2021
+ const p0 = edgeAnchor(fromBounds, toCenter);
2022
+ const p3 = edgeAnchor(toBounds, fromCenter);
2023
+ const dist = Math.hypot(p3.x - p0.x, p3.y - p0.y);
2024
+ const offset = dist * tension;
2025
+ const n0 = outwardNormal(p0, diagramCenter);
2026
+ const n3 = outwardNormal(p3, diagramCenter);
2027
+ const cp1 = { x: p0.x + n0.x * offset, y: p0.y + n0.y * offset };
2028
+ const cp2 = { x: p3.x + n3.x * offset, y: p3.y + n3.y * offset };
2029
+ return [p0, cp1, cp2, p3];
2030
+ }
2031
+ function dot(a, b) {
2032
+ return a.x * b.x + a.y * b.y;
2033
+ }
2034
+ function localToWorld(origin, axisX, axisY, local) {
2035
+ return {
2036
+ x: origin.x + axisX.x * local.x + axisY.x * local.y,
2037
+ y: origin.y + axisX.y * local.x + axisY.y * local.y
2038
+ };
2039
+ }
2040
+ function arcRoute(fromBounds, toBounds, diagramCenter, tension) {
2041
+ const fromCenter = rectCenter(fromBounds);
2042
+ const toCenter = rectCenter(toBounds);
2043
+ const start = edgeAnchor(fromBounds, toCenter);
2044
+ const end = edgeAnchor(toBounds, fromCenter);
2045
+ const chord = { x: end.x - start.x, y: end.y - start.y };
2046
+ const chordLength = Math.hypot(chord.x, chord.y);
2047
+ if (chordLength < 1e-6) {
2048
+ const mid = { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 };
2049
+ return [
2050
+ [start, start, mid, mid],
2051
+ [mid, mid, end, end]
2052
+ ];
1983
2053
  }
2054
+ const axisX = { x: chord.x / chordLength, y: chord.y / chordLength };
2055
+ let axisY = { x: -axisX.y, y: axisX.x };
2056
+ const midpoint = { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 };
2057
+ const outwardHint = outwardNormal(midpoint, diagramCenter);
2058
+ if (dot(axisY, outwardHint) < 0) {
2059
+ axisY = { x: -axisY.x, y: -axisY.y };
2060
+ }
2061
+ const semiMajor = chordLength / 2;
2062
+ const semiMinor = Math.max(12, chordLength * tension * 0.75);
2063
+ const p0Local = { x: -semiMajor, y: 0 };
2064
+ const cp1Local = { x: -semiMajor, y: ELLIPSE_KAPPA * semiMinor };
2065
+ const cp2Local = { x: -ELLIPSE_KAPPA * semiMajor, y: semiMinor };
2066
+ const pMidLocal = { x: 0, y: semiMinor };
2067
+ const cp3Local = { x: ELLIPSE_KAPPA * semiMajor, y: semiMinor };
2068
+ const cp4Local = { x: semiMajor, y: ELLIPSE_KAPPA * semiMinor };
2069
+ const p3Local = { x: semiMajor, y: 0 };
2070
+ const p0 = localToWorld(midpoint, axisX, axisY, p0Local);
2071
+ const cp1 = localToWorld(midpoint, axisX, axisY, cp1Local);
2072
+ const cp2 = localToWorld(midpoint, axisX, axisY, cp2Local);
2073
+ const pMid = localToWorld(midpoint, axisX, axisY, pMidLocal);
2074
+ const cp3 = localToWorld(midpoint, axisX, axisY, cp3Local);
2075
+ const cp4 = localToWorld(midpoint, axisX, axisY, cp4Local);
2076
+ const p3 = localToWorld(midpoint, axisX, axisY, p3Local);
2077
+ return [
2078
+ [p0, cp1, cp2, pMid],
2079
+ [pMid, cp3, cp4, p3]
2080
+ ];
2081
+ }
2082
+ function orthogonalRoute(fromBounds, toBounds) {
2083
+ const fromC = rectCenter(fromBounds);
2084
+ const toC = rectCenter(toBounds);
2085
+ const p0 = edgeAnchor(fromBounds, toC);
2086
+ const p3 = edgeAnchor(toBounds, fromC);
2087
+ const midX = (p0.x + p3.x) / 2;
2088
+ return [p0, { x: midX, y: p0.y }, { x: midX, y: p3.y }, p3];
2089
+ }
2090
+ function bezierPointAt(p0, cp1, cp2, p3, t) {
2091
+ const mt = 1 - t;
1984
2092
  return {
1985
- x: c.x,
1986
- y: dy >= 0 ? rect.y + rect.height : rect.y
2093
+ x: mt * mt * mt * p0.x + 3 * mt * mt * t * cp1.x + 3 * mt * t * t * cp2.x + t * t * t * p3.x,
2094
+ y: mt * mt * mt * p0.y + 3 * mt * mt * t * cp1.y + 3 * mt * t * t * cp2.y + t * t * t * p3.y
2095
+ };
2096
+ }
2097
+ function pointAlongArc(route, t) {
2098
+ const [first, second] = route;
2099
+ if (t <= 0.5) {
2100
+ const localT2 = Math.max(0, Math.min(1, t * 2));
2101
+ return bezierPointAt(first[0], first[1], first[2], first[3], localT2);
2102
+ }
2103
+ const localT = Math.max(0, Math.min(1, (t - 0.5) * 2));
2104
+ return bezierPointAt(second[0], second[1], second[2], second[3], localT);
2105
+ }
2106
+ function computeDiagramCenter(nodeBounds, canvasCenter) {
2107
+ if (nodeBounds.length === 0) {
2108
+ return canvasCenter ?? { x: 0, y: 0 };
2109
+ }
2110
+ let totalX = 0;
2111
+ let totalY = 0;
2112
+ for (const bounds of nodeBounds) {
2113
+ totalX += bounds.x + bounds.width / 2;
2114
+ totalY += bounds.y + bounds.height / 2;
2115
+ }
2116
+ return {
2117
+ x: totalX / nodeBounds.length,
2118
+ y: totalY / nodeBounds.length
1987
2119
  };
1988
2120
  }
1989
2121
  function dashFromStyle(style) {
@@ -2067,51 +2199,95 @@ function polylineBounds(points) {
2067
2199
  height: Math.max(1, maxY - minY)
2068
2200
  };
2069
2201
  }
2070
- function renderConnection(ctx, conn, fromBounds, toBounds, theme, edgeRoute) {
2071
- const fromCenter = center(fromBounds);
2072
- const toCenter = center(toBounds);
2073
- const from = edgeAnchor(fromBounds, toCenter);
2074
- const to = edgeAnchor(toBounds, fromCenter);
2075
- const dash = dashFromStyle(conn.style);
2202
+ function renderConnection(ctx, conn, fromBounds, toBounds, theme, edgeRoute, options) {
2203
+ const routing = conn.routing ?? "auto";
2204
+ const strokeStyle = conn.strokeStyle ?? conn.style ?? "solid";
2205
+ const strokeWidth = conn.width ?? conn.strokeWidth ?? 2;
2206
+ const tension = conn.tension ?? 0.35;
2207
+ const dash = dashFromStyle(strokeStyle);
2076
2208
  const style = {
2077
2209
  color: conn.color ?? theme.borderMuted,
2078
- width: conn.width ?? 2,
2210
+ width: strokeWidth,
2079
2211
  headSize: conn.arrowSize ?? 10,
2080
2212
  ...dash ? { dash } : {}
2081
2213
  };
2082
- const points = edgeRoute && edgeRoute.points.length >= 2 ? edgeRoute.points : [from, { x: (from.x + to.x) / 2, y: from.y }, { x: (from.x + to.x) / 2, y: to.y }, to];
2083
- const startSegment = points[1] ?? points[0];
2084
- const endStart = points[points.length - 2] ?? points[0];
2085
- const end = points[points.length - 1] ?? points[0];
2086
- let startAngle = Math.atan2(startSegment.y - points[0].y, startSegment.x - points[0].x) + Math.PI;
2087
- let endAngle = Math.atan2(end.y - endStart.y, end.x - endStart.x);
2214
+ const labelT = conn.labelPosition === "start" ? 0.2 : conn.labelPosition === "end" ? 0.8 : 0.5;
2215
+ const diagramCenter = options?.diagramCenter ?? computeDiagramCenter([fromBounds, toBounds]);
2216
+ let linePoints;
2217
+ let startPoint;
2218
+ let endPoint;
2219
+ let startAngle;
2220
+ let endAngle;
2221
+ let labelPoint;
2222
+ ctx.save();
2223
+ ctx.globalAlpha = conn.opacity;
2224
+ if (routing === "curve") {
2225
+ const [p0, cp1, cp2, p3] = curveRoute(fromBounds, toBounds, diagramCenter, tension);
2226
+ ctx.strokeStyle = style.color;
2227
+ ctx.lineWidth = style.width;
2228
+ ctx.setLineDash(style.dash ?? []);
2229
+ ctx.beginPath();
2230
+ ctx.moveTo(p0.x, p0.y);
2231
+ ctx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, p3.x, p3.y);
2232
+ ctx.stroke();
2233
+ linePoints = [p0, cp1, cp2, p3];
2234
+ startPoint = p0;
2235
+ endPoint = p3;
2236
+ startAngle = Math.atan2(p0.y - cp1.y, p0.x - cp1.x);
2237
+ endAngle = Math.atan2(p3.y - cp2.y, p3.x - cp2.x);
2238
+ labelPoint = bezierPointAt(p0, cp1, cp2, p3, labelT);
2239
+ } else if (routing === "arc") {
2240
+ const [first, second] = arcRoute(fromBounds, toBounds, diagramCenter, tension);
2241
+ const [p0, cp1, cp2, pMid] = first;
2242
+ const [, cp3, cp4, p3] = second;
2243
+ ctx.strokeStyle = style.color;
2244
+ ctx.lineWidth = style.width;
2245
+ ctx.setLineDash(style.dash ?? []);
2246
+ ctx.beginPath();
2247
+ ctx.moveTo(p0.x, p0.y);
2248
+ ctx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, pMid.x, pMid.y);
2249
+ ctx.bezierCurveTo(cp3.x, cp3.y, cp4.x, cp4.y, p3.x, p3.y);
2250
+ ctx.stroke();
2251
+ linePoints = [p0, cp1, cp2, pMid, cp3, cp4, p3];
2252
+ startPoint = p0;
2253
+ endPoint = p3;
2254
+ startAngle = Math.atan2(p0.y - cp1.y, p0.x - cp1.x);
2255
+ endAngle = Math.atan2(p3.y - cp4.y, p3.x - cp4.x);
2256
+ labelPoint = pointAlongArc([first, second], labelT);
2257
+ } else {
2258
+ const useElkRoute = routing === "auto" && (edgeRoute?.points.length ?? 0) >= 2;
2259
+ linePoints = useElkRoute ? edgeRoute?.points ?? orthogonalRoute(fromBounds, toBounds) : orthogonalRoute(fromBounds, toBounds);
2260
+ startPoint = linePoints[0];
2261
+ const startSegment = linePoints[1] ?? linePoints[0];
2262
+ const endStart = linePoints[linePoints.length - 2] ?? linePoints[0];
2263
+ endPoint = linePoints[linePoints.length - 1] ?? linePoints[0];
2264
+ startAngle = Math.atan2(startSegment.y - linePoints[0].y, startSegment.x - linePoints[0].x) + Math.PI;
2265
+ endAngle = Math.atan2(endPoint.y - endStart.y, endPoint.x - endStart.x);
2266
+ if (useElkRoute) {
2267
+ drawCubicInterpolatedPath(ctx, linePoints, style);
2268
+ } else {
2269
+ drawOrthogonalPath(ctx, startPoint, endPoint, style);
2270
+ }
2271
+ labelPoint = pointAlongPolyline(linePoints, labelT);
2272
+ }
2088
2273
  if (!Number.isFinite(startAngle)) {
2089
2274
  startAngle = 0;
2090
2275
  }
2091
2276
  if (!Number.isFinite(endAngle)) {
2092
2277
  endAngle = 0;
2093
2278
  }
2094
- const t = conn.labelPosition === "start" ? 0.2 : conn.labelPosition === "end" ? 0.8 : 0.5;
2095
- const labelPoint = pointAlongPolyline(points, t);
2096
- ctx.save();
2097
- ctx.globalAlpha = conn.opacity;
2098
- if (edgeRoute && edgeRoute.points.length >= 2) {
2099
- drawCubicInterpolatedPath(ctx, points, style);
2100
- } else {
2101
- drawOrthogonalPath(ctx, points[0], points[points.length - 1], style);
2102
- }
2103
2279
  if (conn.arrow === "start" || conn.arrow === "both") {
2104
- drawArrowhead(ctx, points[0], startAngle, style.headSize, style.color);
2280
+ drawArrowhead(ctx, startPoint, startAngle, style.headSize, style.color);
2105
2281
  }
2106
2282
  if (conn.arrow === "end" || conn.arrow === "both") {
2107
- drawArrowhead(ctx, end, endAngle, style.headSize, style.color);
2283
+ drawArrowhead(ctx, endPoint, endAngle, style.headSize, style.color);
2108
2284
  }
2109
2285
  ctx.restore();
2110
2286
  const elements = [
2111
2287
  {
2112
2288
  id: `connection-${conn.from}-${conn.to}`,
2113
2289
  kind: "connection",
2114
- bounds: polylineBounds(points),
2290
+ bounds: polylineBounds(linePoints),
2115
2291
  foregroundColor: style.color
2116
2292
  }
2117
2293
  ];
@@ -3190,10 +3366,26 @@ var cardElementSchema = z2.object({
3190
3366
  tone: z2.enum(["neutral", "accent", "success", "warning", "error"]).default("neutral"),
3191
3367
  icon: z2.string().min(1).max(64).optional()
3192
3368
  }).strict();
3369
+ var flowNodeShadowSchema = z2.object({
3370
+ color: colorHexSchema2.optional(),
3371
+ blur: z2.number().min(0).max(64).default(8),
3372
+ offsetX: z2.number().min(-32).max(32).default(0),
3373
+ offsetY: z2.number().min(-32).max(32).default(0),
3374
+ opacity: z2.number().min(0).max(1).default(0.3)
3375
+ }).strict();
3193
3376
  var flowNodeElementSchema = z2.object({
3194
3377
  type: z2.literal("flow-node"),
3195
3378
  id: z2.string().min(1).max(120),
3196
- shape: z2.enum(["box", "rounded-box", "diamond", "circle", "pill", "cylinder", "parallelogram"]),
3379
+ shape: z2.enum([
3380
+ "box",
3381
+ "rounded-box",
3382
+ "diamond",
3383
+ "circle",
3384
+ "pill",
3385
+ "cylinder",
3386
+ "parallelogram",
3387
+ "hexagon"
3388
+ ]).default("rounded-box"),
3197
3389
  label: z2.string().min(1).max(200),
3198
3390
  sublabel: z2.string().min(1).max(300).optional(),
3199
3391
  sublabelColor: colorHexSchema2.optional(),
@@ -3213,20 +3405,25 @@ var flowNodeElementSchema = z2.object({
3213
3405
  badgeText: z2.string().min(1).max(32).optional(),
3214
3406
  badgeColor: colorHexSchema2.optional(),
3215
3407
  badgeBackground: colorHexSchema2.optional(),
3216
- badgePosition: z2.enum(["top", "inside-top"]).default("inside-top")
3408
+ badgePosition: z2.enum(["top", "inside-top"]).default("inside-top"),
3409
+ shadow: flowNodeShadowSchema.optional()
3217
3410
  }).strict();
3218
3411
  var connectionElementSchema = z2.object({
3219
3412
  type: z2.literal("connection"),
3220
3413
  from: z2.string().min(1).max(120),
3221
3414
  to: z2.string().min(1).max(120),
3222
3415
  style: z2.enum(["solid", "dashed", "dotted"]).default("solid"),
3416
+ strokeStyle: z2.enum(["solid", "dashed", "dotted"]).default("solid"),
3223
3417
  arrow: z2.enum(["end", "start", "both", "none"]).default("end"),
3224
3418
  label: z2.string().min(1).max(200).optional(),
3225
3419
  labelPosition: z2.enum(["start", "middle", "end"]).default("middle"),
3226
3420
  color: colorHexSchema2.optional(),
3227
- width: z2.number().min(0.5).max(8).optional(),
3421
+ width: z2.number().min(0.5).max(10).optional(),
3422
+ strokeWidth: z2.number().min(0.5).max(10).default(2),
3228
3423
  arrowSize: z2.number().min(4).max(32).optional(),
3229
- opacity: z2.number().min(0).max(1).default(1)
3424
+ opacity: z2.number().min(0).max(1).default(1),
3425
+ routing: z2.enum(["auto", "orthogonal", "curve", "arc"]).default("auto"),
3426
+ tension: z2.number().min(0.1).max(0.8).default(0.35)
3230
3427
  }).strict();
3231
3428
  var codeBlockStyleSchema = z2.object({
3232
3429
  paddingVertical: z2.number().min(0).max(128).default(56),
@@ -3295,6 +3492,10 @@ var elementSchema = z2.discriminatedUnion("type", [
3295
3492
  shapeElementSchema,
3296
3493
  imageElementSchema
3297
3494
  ]);
3495
+ var diagramCenterSchema = z2.object({
3496
+ x: z2.number(),
3497
+ y: z2.number()
3498
+ }).strict();
3298
3499
  var autoLayoutConfigSchema = z2.object({
3299
3500
  mode: z2.literal("auto"),
3300
3501
  algorithm: z2.enum(["layered", "stress", "force", "radial", "box"]).default("layered"),
@@ -3310,7 +3511,9 @@ var autoLayoutConfigSchema = z2.object({
3310
3511
  /** Compaction strategy for radial layout. Only relevant when algorithm is 'radial'. */
3311
3512
  radialCompaction: z2.enum(["none", "radial", "wedge"]).optional(),
3312
3513
  /** Sort strategy for radial layout node ordering. Only relevant when algorithm is 'radial'. */
3313
- radialSortBy: z2.enum(["id", "connections"]).optional()
3514
+ radialSortBy: z2.enum(["id", "connections"]).optional(),
3515
+ /** Explicit center used by curve/arc connection routing. */
3516
+ diagramCenter: diagramCenterSchema.optional()
3314
3517
  }).strict();
3315
3518
  var gridLayoutConfigSchema = z2.object({
3316
3519
  mode: z2.literal("grid"),
@@ -3318,13 +3521,17 @@ var gridLayoutConfigSchema = z2.object({
3318
3521
  gap: z2.number().int().min(0).max(256).default(24),
3319
3522
  cardMinHeight: z2.number().int().min(32).max(4096).optional(),
3320
3523
  cardMaxHeight: z2.number().int().min(32).max(4096).optional(),
3321
- equalHeight: z2.boolean().default(false)
3524
+ equalHeight: z2.boolean().default(false),
3525
+ /** Explicit center used by curve/arc connection routing. */
3526
+ diagramCenter: diagramCenterSchema.optional()
3322
3527
  }).strict();
3323
3528
  var stackLayoutConfigSchema = z2.object({
3324
3529
  mode: z2.literal("stack"),
3325
3530
  direction: z2.enum(["vertical", "horizontal"]).default("vertical"),
3326
3531
  gap: z2.number().int().min(0).max(256).default(24),
3327
- alignment: z2.enum(["start", "center", "end", "stretch"]).default("stretch")
3532
+ alignment: z2.enum(["start", "center", "end", "stretch"]).default("stretch"),
3533
+ /** Explicit center used by curve/arc connection routing. */
3534
+ diagramCenter: diagramCenterSchema.optional()
3328
3535
  }).strict();
3329
3536
  var manualPositionSchema = z2.object({
3330
3537
  x: z2.number().int(),
@@ -3334,7 +3541,9 @@ var manualPositionSchema = z2.object({
3334
3541
  }).strict();
3335
3542
  var manualLayoutConfigSchema = z2.object({
3336
3543
  mode: z2.literal("manual"),
3337
- positions: z2.record(z2.string().min(1), manualPositionSchema).default({})
3544
+ positions: z2.record(z2.string().min(1), manualPositionSchema).default({}),
3545
+ /** Explicit center used by curve/arc connection routing. */
3546
+ diagramCenter: diagramCenterSchema.optional()
3338
3547
  }).strict();
3339
3548
  var layoutConfigSchema = z2.discriminatedUnion("mode", [
3340
3549
  autoLayoutConfigSchema,
@@ -3386,6 +3595,31 @@ var canvasSchema = z2.object({
3386
3595
  padding: z2.number().int().min(0).max(256).default(defaultCanvas.padding)
3387
3596
  }).strict();
3388
3597
  var themeInputSchema = z2.union([builtInThemeSchema, themeSchema]);
3598
+ var diagramPositionSchema = z2.object({
3599
+ x: z2.number(),
3600
+ y: z2.number(),
3601
+ width: z2.number().positive(),
3602
+ height: z2.number().positive()
3603
+ }).strict();
3604
+ var diagramElementSchema = z2.discriminatedUnion("type", [
3605
+ flowNodeElementSchema,
3606
+ connectionElementSchema
3607
+ ]);
3608
+ var diagramLayoutSchema = z2.object({
3609
+ mode: z2.enum(["manual", "auto"]).default("manual"),
3610
+ positions: z2.record(z2.string(), diagramPositionSchema).optional(),
3611
+ diagramCenter: diagramCenterSchema.optional()
3612
+ }).strict();
3613
+ var diagramSpecSchema = z2.object({
3614
+ version: z2.literal(1),
3615
+ canvas: z2.object({
3616
+ width: z2.number().int().min(320).max(4096).default(1200),
3617
+ height: z2.number().int().min(180).max(4096).default(675)
3618
+ }).default({ width: 1200, height: 675 }),
3619
+ theme: themeSchema.optional(),
3620
+ elements: z2.array(diagramElementSchema).min(1),
3621
+ layout: diagramLayoutSchema.default({ mode: "manual" })
3622
+ }).strict();
3389
3623
  var designSpecSchema = z2.object({
3390
3624
  version: z2.literal(2).default(2),
3391
3625
  canvas: canvasSchema.default(defaultCanvas),
@@ -3745,6 +3979,10 @@ async function renderDesign(input, options = {}) {
3745
3979
  break;
3746
3980
  }
3747
3981
  }
3982
+ const diagramCenter = spec.layout.diagramCenter ?? computeDiagramCenter(
3983
+ spec.elements.filter((element) => element.type !== "connection").map((element) => elementRects.get(element.id)).filter((rect) => rect != null),
3984
+ { x: spec.canvas.width / 2, y: spec.canvas.height / 2 }
3985
+ );
3748
3986
  for (const element of spec.elements) {
3749
3987
  if (element.type !== "connection") {
3750
3988
  continue;
@@ -3757,7 +3995,9 @@ async function renderDesign(input, options = {}) {
3757
3995
  );
3758
3996
  }
3759
3997
  const edgeRoute = edgeRoutes?.get(`${element.from}-${element.to}`);
3760
- elements.push(...renderConnection(ctx, element, fromRect, toRect, theme, edgeRoute));
3998
+ elements.push(
3999
+ ...renderConnection(ctx, element, fromRect, toRect, theme, edgeRoute, { diagramCenter })
4000
+ );
3761
4001
  }
3762
4002
  if (footerRect && spec.footer) {
3763
4003
  const footerText = spec.footer.tagline ? `${spec.footer.text} \u2022 ${spec.footer.tagline}` : spec.footer.text;