@spectratools/graphic-designer-cli 0.6.0 → 0.7.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 CHANGED
@@ -852,6 +852,15 @@ var drawGradientRectSchema = z2.object({
852
852
  radius: z2.number().min(0).max(256).default(0),
853
853
  opacity: z2.number().min(0).max(1).default(1)
854
854
  }).strict();
855
+ var drawGridSchema = z2.object({
856
+ type: z2.literal("grid"),
857
+ spacing: z2.number().min(5).max(200).default(40),
858
+ color: colorHexSchema2.default("#1E2D4A"),
859
+ width: z2.number().min(0.1).max(4).default(0.5),
860
+ opacity: z2.number().min(0).max(1).default(0.2),
861
+ offsetX: z2.number().default(0),
862
+ offsetY: z2.number().default(0)
863
+ }).strict();
855
864
  var drawCommandSchema = z2.discriminatedUnion("type", [
856
865
  drawRectSchema,
857
866
  drawCircleSchema,
@@ -860,7 +869,8 @@ var drawCommandSchema = z2.discriminatedUnion("type", [
860
869
  drawBezierSchema,
861
870
  drawPathSchema,
862
871
  drawBadgeSchema,
863
- drawGradientRectSchema
872
+ drawGradientRectSchema,
873
+ drawGridSchema
864
874
  ]);
865
875
  var defaultCanvas = {
866
876
  width: 1200,
@@ -966,6 +976,13 @@ var flowNodeElementSchema = z2.object({
966
976
  badgePosition: z2.enum(["top", "inside-top"]).default("inside-top"),
967
977
  shadow: flowNodeShadowSchema.optional()
968
978
  }).strict();
979
+ var anchorHintSchema = z2.union([
980
+ z2.enum(["top", "bottom", "left", "right", "center"]),
981
+ z2.object({
982
+ x: z2.number().min(-1).max(1),
983
+ y: z2.number().min(-1).max(1)
984
+ }).strict()
985
+ ]);
969
986
  var connectionElementSchema = z2.object({
970
987
  type: z2.literal("connection"),
971
988
  from: z2.string().min(1).max(120),
@@ -979,9 +996,12 @@ var connectionElementSchema = z2.object({
979
996
  width: z2.number().min(0.5).max(10).optional(),
980
997
  strokeWidth: z2.number().min(0.5).max(10).default(2),
981
998
  arrowSize: z2.number().min(4).max(32).optional(),
999
+ arrowPlacement: z2.enum(["endpoint", "boundary"]).default("endpoint"),
982
1000
  opacity: z2.number().min(0).max(1).default(1),
983
1001
  routing: z2.enum(["auto", "orthogonal", "curve", "arc"]).default("auto"),
984
- tension: z2.number().min(0.1).max(0.8).default(0.35)
1002
+ tension: z2.number().min(0.1).max(0.8).default(0.35),
1003
+ fromAnchor: anchorHintSchema.optional(),
1004
+ toAnchor: anchorHintSchema.optional()
985
1005
  }).strict();
986
1006
  var codeBlockStyleSchema = z2.object({
987
1007
  paddingVertical: z2.number().min(0).max(128).default(56),
@@ -3105,21 +3125,61 @@ function edgeAnchor(bounds, target) {
3105
3125
  const t = absDx * hh > absDy * hw ? hw / absDx : hh / absDy;
3106
3126
  return { x: c.x + dx * t, y: c.y + dy * t };
3107
3127
  }
3128
+ function resolveAnchor(bounds, anchor, fallbackTarget) {
3129
+ if (!anchor) return edgeAnchor(bounds, fallbackTarget);
3130
+ if (typeof anchor === "string") {
3131
+ const c2 = rectCenter(bounds);
3132
+ switch (anchor) {
3133
+ case "top":
3134
+ return { x: c2.x, y: bounds.y };
3135
+ case "bottom":
3136
+ return { x: c2.x, y: bounds.y + bounds.height };
3137
+ case "left":
3138
+ return { x: bounds.x, y: c2.y };
3139
+ case "right":
3140
+ return { x: bounds.x + bounds.width, y: c2.y };
3141
+ case "center":
3142
+ return c2;
3143
+ }
3144
+ }
3145
+ const c = rectCenter(bounds);
3146
+ return {
3147
+ x: c.x + anchor.x * (bounds.width / 2),
3148
+ y: c.y + anchor.y * (bounds.height / 2)
3149
+ };
3150
+ }
3151
+ function anchorNormal(anchor, point, diagramCenter) {
3152
+ if (typeof anchor === "string") {
3153
+ switch (anchor) {
3154
+ case "top":
3155
+ return { x: 0, y: -1 };
3156
+ case "bottom":
3157
+ return { x: 0, y: 1 };
3158
+ case "left":
3159
+ return { x: -1, y: 0 };
3160
+ case "right":
3161
+ return { x: 1, y: 0 };
3162
+ case "center":
3163
+ return outwardNormal(point, diagramCenter);
3164
+ }
3165
+ }
3166
+ return outwardNormal(point, diagramCenter);
3167
+ }
3108
3168
  function outwardNormal(point, diagramCenter) {
3109
3169
  const dx = point.x - diagramCenter.x;
3110
3170
  const dy = point.y - diagramCenter.y;
3111
3171
  const len = Math.hypot(dx, dy) || 1;
3112
3172
  return { x: dx / len, y: dy / len };
3113
3173
  }
3114
- function curveRoute(fromBounds, toBounds, diagramCenter, tension) {
3174
+ function curveRoute(fromBounds, toBounds, diagramCenter, tension, fromAnchor, toAnchor) {
3115
3175
  const fromCenter = rectCenter(fromBounds);
3116
3176
  const toCenter = rectCenter(toBounds);
3117
- const p0 = edgeAnchor(fromBounds, toCenter);
3118
- const p3 = edgeAnchor(toBounds, fromCenter);
3177
+ const p0 = resolveAnchor(fromBounds, fromAnchor, toCenter);
3178
+ const p3 = resolveAnchor(toBounds, toAnchor, fromCenter);
3119
3179
  const dist = Math.hypot(p3.x - p0.x, p3.y - p0.y);
3120
3180
  const offset = dist * tension;
3121
- const n0 = outwardNormal(p0, diagramCenter);
3122
- const n3 = outwardNormal(p3, diagramCenter);
3181
+ const n0 = anchorNormal(fromAnchor, p0, diagramCenter);
3182
+ const n3 = anchorNormal(toAnchor, p3, diagramCenter);
3123
3183
  const cp1 = { x: p0.x + n0.x * offset, y: p0.y + n0.y * offset };
3124
3184
  const cp2 = { x: p3.x + n3.x * offset, y: p3.y + n3.y * offset };
3125
3185
  return [p0, cp1, cp2, p3];
@@ -3133,11 +3193,11 @@ function localToWorld(origin, axisX, axisY, local) {
3133
3193
  y: origin.y + axisX.y * local.x + axisY.y * local.y
3134
3194
  };
3135
3195
  }
3136
- function arcRoute(fromBounds, toBounds, diagramCenter, tension) {
3196
+ function arcRoute(fromBounds, toBounds, diagramCenter, tension, fromAnchor, toAnchor) {
3137
3197
  const fromCenter = rectCenter(fromBounds);
3138
3198
  const toCenter = rectCenter(toBounds);
3139
- const start = edgeAnchor(fromBounds, toCenter);
3140
- const end = edgeAnchor(toBounds, fromCenter);
3199
+ const start = resolveAnchor(fromBounds, fromAnchor, toCenter);
3200
+ const end = resolveAnchor(toBounds, toAnchor, fromCenter);
3141
3201
  const chord = { x: end.x - start.x, y: end.y - start.y };
3142
3202
  const chordLength = Math.hypot(chord.x, chord.y);
3143
3203
  if (chordLength < 1e-6) {
@@ -3175,11 +3235,11 @@ function arcRoute(fromBounds, toBounds, diagramCenter, tension) {
3175
3235
  [pMid, cp3, cp4, p3]
3176
3236
  ];
3177
3237
  }
3178
- function orthogonalRoute(fromBounds, toBounds) {
3238
+ function orthogonalRoute(fromBounds, toBounds, fromAnchor, toAnchor) {
3179
3239
  const fromC = rectCenter(fromBounds);
3180
3240
  const toC = rectCenter(toBounds);
3181
- const p0 = edgeAnchor(fromBounds, toC);
3182
- const p3 = edgeAnchor(toBounds, fromC);
3241
+ const p0 = resolveAnchor(fromBounds, fromAnchor, toC);
3242
+ const p3 = resolveAnchor(toBounds, toAnchor, fromC);
3183
3243
  const midX = (p0.x + p3.x) / 2;
3184
3244
  return [p0, { x: midX, y: p0.y }, { x: midX, y: p3.y }, p3];
3185
3245
  }
@@ -3190,6 +3250,35 @@ function bezierPointAt(p0, cp1, cp2, p3, t) {
3190
3250
  y: mt * mt * mt * p0.y + 3 * mt * mt * t * cp1.y + 3 * mt * t * t * cp2.y + t * t * t * p3.y
3191
3251
  };
3192
3252
  }
3253
+ function bezierTangentAt(p0, cp1, cp2, p3, t) {
3254
+ const mt = 1 - t;
3255
+ return {
3256
+ x: 3 * mt * mt * (cp1.x - p0.x) + 6 * mt * t * (cp2.x - cp1.x) + 3 * t * t * (p3.x - cp2.x),
3257
+ y: 3 * mt * mt * (cp1.y - p0.y) + 6 * mt * t * (cp2.y - cp1.y) + 3 * t * t * (p3.y - cp2.y)
3258
+ };
3259
+ }
3260
+ function isInsideRect(point, rect) {
3261
+ return point.x >= rect.x && point.x <= rect.x + rect.width && point.y >= rect.y && point.y <= rect.y + rect.height;
3262
+ }
3263
+ function findBoundaryIntersection(p0, cp1, cp2, p3, targetRect, searchFromEnd) {
3264
+ const step = 5e-3;
3265
+ if (searchFromEnd) {
3266
+ for (let t = 0.95; t >= 0.5; t -= step) {
3267
+ const pt = bezierPointAt(p0, cp1, cp2, p3, t);
3268
+ if (!isInsideRect(pt, targetRect)) {
3269
+ return t;
3270
+ }
3271
+ }
3272
+ } else {
3273
+ for (let t = 0.05; t <= 0.5; t += step) {
3274
+ const pt = bezierPointAt(p0, cp1, cp2, p3, t);
3275
+ if (!isInsideRect(pt, targetRect)) {
3276
+ return t;
3277
+ }
3278
+ }
3279
+ }
3280
+ return void 0;
3281
+ }
3193
3282
  function pointAlongArc(route, t) {
3194
3283
  const [first, second] = route;
3195
3284
  if (t <= 0.5) {
@@ -3317,8 +3406,16 @@ function renderConnection(ctx, conn, fromBounds, toBounds, theme, edgeRoute, opt
3317
3406
  let labelPoint;
3318
3407
  ctx.save();
3319
3408
  ctx.globalAlpha = conn.opacity;
3409
+ const arrowPlacement = conn.arrowPlacement ?? "endpoint";
3320
3410
  if (routing === "curve") {
3321
- const [p0, cp1, cp2, p3] = curveRoute(fromBounds, toBounds, diagramCenter, tension);
3411
+ const [p0, cp1, cp2, p3] = curveRoute(
3412
+ fromBounds,
3413
+ toBounds,
3414
+ diagramCenter,
3415
+ tension,
3416
+ conn.fromAnchor,
3417
+ conn.toAnchor
3418
+ );
3322
3419
  ctx.strokeStyle = style.color;
3323
3420
  ctx.lineWidth = style.width;
3324
3421
  ctx.setLineDash(style.dash ?? []);
@@ -3332,8 +3429,33 @@ function renderConnection(ctx, conn, fromBounds, toBounds, theme, edgeRoute, opt
3332
3429
  startAngle = Math.atan2(p0.y - cp1.y, p0.x - cp1.x);
3333
3430
  endAngle = Math.atan2(p3.y - cp2.y, p3.x - cp2.x);
3334
3431
  labelPoint = bezierPointAt(p0, cp1, cp2, p3, labelT);
3432
+ if (arrowPlacement === "boundary") {
3433
+ if (conn.arrow === "end" || conn.arrow === "both") {
3434
+ const tEnd = findBoundaryIntersection(p0, cp1, cp2, p3, toBounds, true);
3435
+ if (tEnd !== void 0) {
3436
+ endPoint = bezierPointAt(p0, cp1, cp2, p3, tEnd);
3437
+ const tangent = bezierTangentAt(p0, cp1, cp2, p3, tEnd);
3438
+ endAngle = Math.atan2(tangent.y, tangent.x);
3439
+ }
3440
+ }
3441
+ if (conn.arrow === "start" || conn.arrow === "both") {
3442
+ const tStart = findBoundaryIntersection(p0, cp1, cp2, p3, fromBounds, false);
3443
+ if (tStart !== void 0) {
3444
+ startPoint = bezierPointAt(p0, cp1, cp2, p3, tStart);
3445
+ const tangent = bezierTangentAt(p0, cp1, cp2, p3, tStart);
3446
+ startAngle = Math.atan2(tangent.y, tangent.x) + Math.PI;
3447
+ }
3448
+ }
3449
+ }
3335
3450
  } else if (routing === "arc") {
3336
- const [first, second] = arcRoute(fromBounds, toBounds, diagramCenter, tension);
3451
+ const [first, second] = arcRoute(
3452
+ fromBounds,
3453
+ toBounds,
3454
+ diagramCenter,
3455
+ tension,
3456
+ conn.fromAnchor,
3457
+ conn.toAnchor
3458
+ );
3337
3459
  const [p0, cp1, cp2, pMid] = first;
3338
3460
  const [, cp3, cp4, p3] = second;
3339
3461
  ctx.strokeStyle = style.color;
@@ -3350,9 +3472,29 @@ function renderConnection(ctx, conn, fromBounds, toBounds, theme, edgeRoute, opt
3350
3472
  startAngle = Math.atan2(p0.y - cp1.y, p0.x - cp1.x);
3351
3473
  endAngle = Math.atan2(p3.y - cp4.y, p3.x - cp4.x);
3352
3474
  labelPoint = pointAlongArc([first, second], labelT);
3475
+ if (arrowPlacement === "boundary") {
3476
+ if (conn.arrow === "end" || conn.arrow === "both") {
3477
+ const [, s_cp3, s_cp4, s_p3] = second;
3478
+ const tEnd = findBoundaryIntersection(pMid, s_cp3, s_cp4, s_p3, toBounds, true);
3479
+ if (tEnd !== void 0) {
3480
+ endPoint = bezierPointAt(pMid, s_cp3, s_cp4, s_p3, tEnd);
3481
+ const tangent = bezierTangentAt(pMid, s_cp3, s_cp4, s_p3, tEnd);
3482
+ endAngle = Math.atan2(tangent.y, tangent.x);
3483
+ }
3484
+ }
3485
+ if (conn.arrow === "start" || conn.arrow === "both") {
3486
+ const tStart = findBoundaryIntersection(p0, cp1, cp2, pMid, fromBounds, false);
3487
+ if (tStart !== void 0) {
3488
+ startPoint = bezierPointAt(p0, cp1, cp2, pMid, tStart);
3489
+ const tangent = bezierTangentAt(p0, cp1, cp2, pMid, tStart);
3490
+ startAngle = Math.atan2(tangent.y, tangent.x) + Math.PI;
3491
+ }
3492
+ }
3493
+ }
3353
3494
  } else {
3354
- const useElkRoute = routing === "auto" && (edgeRoute?.points.length ?? 0) >= 2;
3355
- linePoints = useElkRoute ? edgeRoute?.points ?? orthogonalRoute(fromBounds, toBounds) : orthogonalRoute(fromBounds, toBounds);
3495
+ const hasAnchorHints = conn.fromAnchor !== void 0 || conn.toAnchor !== void 0;
3496
+ const useElkRoute = routing === "auto" && !hasAnchorHints && (edgeRoute?.points.length ?? 0) >= 2;
3497
+ linePoints = useElkRoute ? edgeRoute?.points ?? orthogonalRoute(fromBounds, toBounds, conn.fromAnchor, conn.toAnchor) : orthogonalRoute(fromBounds, toBounds, conn.fromAnchor, conn.toAnchor);
3356
3498
  startPoint = linePoints[0];
3357
3499
  const startSegment = linePoints[1] ?? linePoints[0];
3358
3500
  const endStart = linePoints[linePoints.length - 2] ?? linePoints[0];
@@ -4009,6 +4151,36 @@ function renderDrawCommands(ctx, commands, theme) {
4009
4151
  });
4010
4152
  break;
4011
4153
  }
4154
+ case "grid": {
4155
+ const canvasWidth = ctx.canvas.width;
4156
+ const canvasHeight = ctx.canvas.height;
4157
+ withOpacity(ctx, command.opacity, () => {
4158
+ ctx.strokeStyle = command.color;
4159
+ ctx.lineWidth = command.width;
4160
+ const startX = command.offsetX % command.spacing;
4161
+ for (let x = startX; x <= canvasWidth; x += command.spacing) {
4162
+ ctx.beginPath();
4163
+ ctx.moveTo(x, 0);
4164
+ ctx.lineTo(x, canvasHeight);
4165
+ ctx.stroke();
4166
+ }
4167
+ const startY = command.offsetY % command.spacing;
4168
+ for (let y = startY; y <= canvasHeight; y += command.spacing) {
4169
+ ctx.beginPath();
4170
+ ctx.moveTo(0, y);
4171
+ ctx.lineTo(canvasWidth, y);
4172
+ ctx.stroke();
4173
+ }
4174
+ });
4175
+ rendered.push({
4176
+ id,
4177
+ kind: "draw",
4178
+ bounds: { x: 0, y: 0, width: canvasWidth, height: canvasHeight },
4179
+ foregroundColor: command.color,
4180
+ allowOverlap: true
4181
+ });
4182
+ break;
4183
+ }
4012
4184
  }
4013
4185
  }
4014
4186
  return rendered;
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Cli } from 'incur';
2
- import { T as ThemeInput, D as DesignSpec, a as Rect$1, b as DrawCommand, c as Theme, d as RenderedElement, C as ConnectionElement } from './spec.schema-Dm_wOLTd.js';
3
- export { A as AutoLayoutConfig, B as BuiltInTheme, e as CardElement, f as CodeBlockElement, g as ConstraintSpec, h as DEFAULT_GENERATOR_VERSION, i as DEFAULT_RAINBOW_COLORS, j as Decorator, k as DesignCardSpec, l as DesignSafeFrame, m as DesignTheme, n as DiagramElement, o as DiagramLayout, p as DiagramSpec, q as DrawBadge, r as DrawBezier, s as DrawCircle, t as DrawFontFamily, u as DrawGradientRect, v as DrawLine, w as DrawPath, x as DrawPoint, y as DrawRect, z as DrawText, E as Element, F as FlowNodeElement, G as Gradient, H as GradientOverlayDecorator, I as GradientSpec, J as GradientStop, K as GridLayoutConfig, L as ImageElement, M as LayoutConfig, N as LayoutSnapshot, O as ManualLayoutConfig, P as RainbowRuleDecorator, R as RenderMetadata, Q as RenderResult, S as ShapeElement, U as StackLayoutConfig, V as TerminalElement, W as TextElement, X as ThemeInput, Y as VignetteDecorator, Z as WrittenArtifacts, _ as builtInThemeBackgrounds, $ as builtInThemes, a0 as computeSpecHash, a1 as connectionElementSchema, a2 as defaultAutoLayout, a3 as defaultCanvas, a4 as defaultConstraints, a5 as defaultGridLayout, a6 as defaultLayout, a7 as defaultStackLayout, a8 as defaultTheme, a9 as deriveSafeFrame, aa as designSpecSchema, ab as diagramElementSchema, ac as diagramLayoutSchema, ad as diagramSpecSchema, ae as drawGradientRect, af as drawRainbowRule, ag as drawVignette, ah as flowNodeElementSchema, ai as inferLayout, aj as inferSidecarPath, ak as parseDesignSpec, al as parseDiagramSpec, am as renderDesign, an as resolveTheme, ao as writeRenderArtifacts } from './spec.schema-Dm_wOLTd.js';
2
+ import { T as ThemeInput, D as DesignSpec, a as Rect$1, b as DrawCommand, c as Theme, d as RenderedElement, A as AnchorHint, C as ConnectionElement } from './spec.schema-BeFz_nk1.js';
3
+ export { e as AutoLayoutConfig, B as BuiltInTheme, f as CardElement, g as CodeBlockElement, h as ConstraintSpec, i as DEFAULT_GENERATOR_VERSION, j as DEFAULT_RAINBOW_COLORS, k as Decorator, l as DesignCardSpec, m as DesignSafeFrame, n as DesignTheme, o as DiagramElement, p as DiagramLayout, q as DiagramSpec, r as DrawBadge, s as DrawBezier, t as DrawCircle, u as DrawFontFamily, v as DrawGradientRect, w as DrawLine, x as DrawPath, y as DrawPoint, z as DrawRect, E as DrawText, F as Element, G as FlowNodeElement, H as Gradient, I as GradientOverlayDecorator, J as GradientSpec, K as GradientStop, L as GridLayoutConfig, M as ImageElement, N as LayoutConfig, O as LayoutSnapshot, P as ManualLayoutConfig, Q as RainbowRuleDecorator, R as RenderMetadata, S as RenderResult, U as ShapeElement, V as StackLayoutConfig, W as TerminalElement, X as TextElement, Y as ThemeInput, Z as VignetteDecorator, _ as WrittenArtifacts, $ as builtInThemeBackgrounds, a0 as builtInThemes, a1 as computeSpecHash, a2 as connectionElementSchema, a3 as defaultAutoLayout, a4 as defaultCanvas, a5 as defaultConstraints, a6 as defaultGridLayout, a7 as defaultLayout, a8 as defaultStackLayout, a9 as defaultTheme, aa as deriveSafeFrame, ab as designSpecSchema, ac as diagramElementSchema, ad as diagramLayoutSchema, ae as diagramSpecSchema, af as drawGradientRect, ag as drawRainbowRule, ah as drawVignette, ai as flowNodeElementSchema, aj as inferLayout, ak as inferSidecarPath, al as parseDesignSpec, am as parseDiagramSpec, an as renderDesign, ao as resolveTheme, ap as writeRenderArtifacts } from './spec.schema-BeFz_nk1.js';
4
4
  import { SKRSContext2D } from '@napi-rs/canvas';
5
5
  export { QaIssue, QaReferenceResult, QaReport, QaSeverity, readMetadata, runQa } from './qa.js';
6
6
  import { Highlighter } from 'shiki';
@@ -275,21 +275,31 @@ declare function outwardNormal(point: Point, diagramCenter: Point): Point;
275
275
  /**
276
276
  * Compute a cubic bezier curve that bows outward from the diagram center.
277
277
  * Returns `[startPoint, controlPoint1, controlPoint2, endPoint]`.
278
+ *
279
+ * When `fromAnchor` or `toAnchor` hints are provided, they override the
280
+ * automatic edge anchor calculation and the outward normal is derived from
281
+ * the hint direction instead of from the diagram center.
278
282
  */
279
- declare function curveRoute(fromBounds: Rect, toBounds: Rect, diagramCenter: Point, tension: number): [Point, Point, Point, Point];
283
+ declare function curveRoute(fromBounds: Rect, toBounds: Rect, diagramCenter: Point, tension: number, fromAnchor?: AnchorHint, toAnchor?: AnchorHint): [Point, Point, Point, Point];
280
284
  /**
281
285
  * Approximate an outward-bowing half-ellipse with two cubic bezier segments.
282
286
  *
283
287
  * Uses the classic kappa constant (`4 * (sqrt(2) - 1) / 3`) for quarter-ellipse
284
288
  * control points, producing a stable arc from source edge anchor to target edge
285
289
  * anchor.
290
+ *
291
+ * When `fromAnchor` or `toAnchor` hints are provided, they override the
292
+ * automatic edge anchor calculation.
286
293
  */
287
- declare function arcRoute(fromBounds: Rect, toBounds: Rect, diagramCenter: Point, tension: number): [CubicBezierSegment, CubicBezierSegment];
294
+ declare function arcRoute(fromBounds: Rect, toBounds: Rect, diagramCenter: Point, tension: number, fromAnchor?: AnchorHint, toAnchor?: AnchorHint): [CubicBezierSegment, CubicBezierSegment];
288
295
  /**
289
296
  * Compute an orthogonal (right-angle) path between two rectangles.
290
297
  * Returns an array of waypoints forming a 3-segment path.
298
+ *
299
+ * When `fromAnchor` or `toAnchor` hints are provided, they override the
300
+ * automatic edge anchor calculation.
291
301
  */
292
- declare function orthogonalRoute(fromBounds: Rect, toBounds: Rect): Point[];
302
+ declare function orthogonalRoute(fromBounds: Rect, toBounds: Rect, fromAnchor?: AnchorHint, toAnchor?: AnchorHint): Point[];
293
303
  /** Evaluate cubic bezier at parameter `t`. */
294
304
  declare function bezierPointAt(p0: Point, cp1: Point, cp2: Point, p3: Point, t: number): Point;
295
305
  /**
package/dist/index.js CHANGED
@@ -861,6 +861,15 @@ var drawGradientRectSchema = z2.object({
861
861
  radius: z2.number().min(0).max(256).default(0),
862
862
  opacity: z2.number().min(0).max(1).default(1)
863
863
  }).strict();
864
+ var drawGridSchema = z2.object({
865
+ type: z2.literal("grid"),
866
+ spacing: z2.number().min(5).max(200).default(40),
867
+ color: colorHexSchema2.default("#1E2D4A"),
868
+ width: z2.number().min(0.1).max(4).default(0.5),
869
+ opacity: z2.number().min(0).max(1).default(0.2),
870
+ offsetX: z2.number().default(0),
871
+ offsetY: z2.number().default(0)
872
+ }).strict();
864
873
  var drawCommandSchema = z2.discriminatedUnion("type", [
865
874
  drawRectSchema,
866
875
  drawCircleSchema,
@@ -869,7 +878,8 @@ var drawCommandSchema = z2.discriminatedUnion("type", [
869
878
  drawBezierSchema,
870
879
  drawPathSchema,
871
880
  drawBadgeSchema,
872
- drawGradientRectSchema
881
+ drawGradientRectSchema,
882
+ drawGridSchema
873
883
  ]);
874
884
  var defaultCanvas = {
875
885
  width: 1200,
@@ -976,6 +986,13 @@ var flowNodeElementSchema = z2.object({
976
986
  badgePosition: z2.enum(["top", "inside-top"]).default("inside-top"),
977
987
  shadow: flowNodeShadowSchema.optional()
978
988
  }).strict();
989
+ var anchorHintSchema = z2.union([
990
+ z2.enum(["top", "bottom", "left", "right", "center"]),
991
+ z2.object({
992
+ x: z2.number().min(-1).max(1),
993
+ y: z2.number().min(-1).max(1)
994
+ }).strict()
995
+ ]);
979
996
  var connectionElementSchema = z2.object({
980
997
  type: z2.literal("connection"),
981
998
  from: z2.string().min(1).max(120),
@@ -989,9 +1006,12 @@ var connectionElementSchema = z2.object({
989
1006
  width: z2.number().min(0.5).max(10).optional(),
990
1007
  strokeWidth: z2.number().min(0.5).max(10).default(2),
991
1008
  arrowSize: z2.number().min(4).max(32).optional(),
1009
+ arrowPlacement: z2.enum(["endpoint", "boundary"]).default("endpoint"),
992
1010
  opacity: z2.number().min(0).max(1).default(1),
993
1011
  routing: z2.enum(["auto", "orthogonal", "curve", "arc"]).default("auto"),
994
- tension: z2.number().min(0.1).max(0.8).default(0.35)
1012
+ tension: z2.number().min(0.1).max(0.8).default(0.35),
1013
+ fromAnchor: anchorHintSchema.optional(),
1014
+ toAnchor: anchorHintSchema.optional()
995
1015
  }).strict();
996
1016
  var codeBlockStyleSchema = z2.object({
997
1017
  paddingVertical: z2.number().min(0).max(128).default(56),
@@ -3122,21 +3142,61 @@ function edgeAnchor(bounds, target) {
3122
3142
  const t = absDx * hh > absDy * hw ? hw / absDx : hh / absDy;
3123
3143
  return { x: c.x + dx * t, y: c.y + dy * t };
3124
3144
  }
3145
+ function resolveAnchor(bounds, anchor, fallbackTarget) {
3146
+ if (!anchor) return edgeAnchor(bounds, fallbackTarget);
3147
+ if (typeof anchor === "string") {
3148
+ const c2 = rectCenter(bounds);
3149
+ switch (anchor) {
3150
+ case "top":
3151
+ return { x: c2.x, y: bounds.y };
3152
+ case "bottom":
3153
+ return { x: c2.x, y: bounds.y + bounds.height };
3154
+ case "left":
3155
+ return { x: bounds.x, y: c2.y };
3156
+ case "right":
3157
+ return { x: bounds.x + bounds.width, y: c2.y };
3158
+ case "center":
3159
+ return c2;
3160
+ }
3161
+ }
3162
+ const c = rectCenter(bounds);
3163
+ return {
3164
+ x: c.x + anchor.x * (bounds.width / 2),
3165
+ y: c.y + anchor.y * (bounds.height / 2)
3166
+ };
3167
+ }
3168
+ function anchorNormal(anchor, point, diagramCenter) {
3169
+ if (typeof anchor === "string") {
3170
+ switch (anchor) {
3171
+ case "top":
3172
+ return { x: 0, y: -1 };
3173
+ case "bottom":
3174
+ return { x: 0, y: 1 };
3175
+ case "left":
3176
+ return { x: -1, y: 0 };
3177
+ case "right":
3178
+ return { x: 1, y: 0 };
3179
+ case "center":
3180
+ return outwardNormal(point, diagramCenter);
3181
+ }
3182
+ }
3183
+ return outwardNormal(point, diagramCenter);
3184
+ }
3125
3185
  function outwardNormal(point, diagramCenter) {
3126
3186
  const dx = point.x - diagramCenter.x;
3127
3187
  const dy = point.y - diagramCenter.y;
3128
3188
  const len = Math.hypot(dx, dy) || 1;
3129
3189
  return { x: dx / len, y: dy / len };
3130
3190
  }
3131
- function curveRoute(fromBounds, toBounds, diagramCenter, tension) {
3191
+ function curveRoute(fromBounds, toBounds, diagramCenter, tension, fromAnchor, toAnchor) {
3132
3192
  const fromCenter = rectCenter(fromBounds);
3133
3193
  const toCenter = rectCenter(toBounds);
3134
- const p0 = edgeAnchor(fromBounds, toCenter);
3135
- const p3 = edgeAnchor(toBounds, fromCenter);
3194
+ const p0 = resolveAnchor(fromBounds, fromAnchor, toCenter);
3195
+ const p3 = resolveAnchor(toBounds, toAnchor, fromCenter);
3136
3196
  const dist = Math.hypot(p3.x - p0.x, p3.y - p0.y);
3137
3197
  const offset = dist * tension;
3138
- const n0 = outwardNormal(p0, diagramCenter);
3139
- const n3 = outwardNormal(p3, diagramCenter);
3198
+ const n0 = anchorNormal(fromAnchor, p0, diagramCenter);
3199
+ const n3 = anchorNormal(toAnchor, p3, diagramCenter);
3140
3200
  const cp1 = { x: p0.x + n0.x * offset, y: p0.y + n0.y * offset };
3141
3201
  const cp2 = { x: p3.x + n3.x * offset, y: p3.y + n3.y * offset };
3142
3202
  return [p0, cp1, cp2, p3];
@@ -3150,11 +3210,11 @@ function localToWorld(origin, axisX, axisY, local) {
3150
3210
  y: origin.y + axisX.y * local.x + axisY.y * local.y
3151
3211
  };
3152
3212
  }
3153
- function arcRoute(fromBounds, toBounds, diagramCenter, tension) {
3213
+ function arcRoute(fromBounds, toBounds, diagramCenter, tension, fromAnchor, toAnchor) {
3154
3214
  const fromCenter = rectCenter(fromBounds);
3155
3215
  const toCenter = rectCenter(toBounds);
3156
- const start = edgeAnchor(fromBounds, toCenter);
3157
- const end = edgeAnchor(toBounds, fromCenter);
3216
+ const start = resolveAnchor(fromBounds, fromAnchor, toCenter);
3217
+ const end = resolveAnchor(toBounds, toAnchor, fromCenter);
3158
3218
  const chord = { x: end.x - start.x, y: end.y - start.y };
3159
3219
  const chordLength = Math.hypot(chord.x, chord.y);
3160
3220
  if (chordLength < 1e-6) {
@@ -3192,11 +3252,11 @@ function arcRoute(fromBounds, toBounds, diagramCenter, tension) {
3192
3252
  [pMid, cp3, cp4, p3]
3193
3253
  ];
3194
3254
  }
3195
- function orthogonalRoute(fromBounds, toBounds) {
3255
+ function orthogonalRoute(fromBounds, toBounds, fromAnchor, toAnchor) {
3196
3256
  const fromC = rectCenter(fromBounds);
3197
3257
  const toC = rectCenter(toBounds);
3198
- const p0 = edgeAnchor(fromBounds, toC);
3199
- const p3 = edgeAnchor(toBounds, fromC);
3258
+ const p0 = resolveAnchor(fromBounds, fromAnchor, toC);
3259
+ const p3 = resolveAnchor(toBounds, toAnchor, fromC);
3200
3260
  const midX = (p0.x + p3.x) / 2;
3201
3261
  return [p0, { x: midX, y: p0.y }, { x: midX, y: p3.y }, p3];
3202
3262
  }
@@ -3207,6 +3267,35 @@ function bezierPointAt(p0, cp1, cp2, p3, t) {
3207
3267
  y: mt * mt * mt * p0.y + 3 * mt * mt * t * cp1.y + 3 * mt * t * t * cp2.y + t * t * t * p3.y
3208
3268
  };
3209
3269
  }
3270
+ function bezierTangentAt(p0, cp1, cp2, p3, t) {
3271
+ const mt = 1 - t;
3272
+ return {
3273
+ x: 3 * mt * mt * (cp1.x - p0.x) + 6 * mt * t * (cp2.x - cp1.x) + 3 * t * t * (p3.x - cp2.x),
3274
+ y: 3 * mt * mt * (cp1.y - p0.y) + 6 * mt * t * (cp2.y - cp1.y) + 3 * t * t * (p3.y - cp2.y)
3275
+ };
3276
+ }
3277
+ function isInsideRect(point, rect) {
3278
+ return point.x >= rect.x && point.x <= rect.x + rect.width && point.y >= rect.y && point.y <= rect.y + rect.height;
3279
+ }
3280
+ function findBoundaryIntersection(p0, cp1, cp2, p3, targetRect, searchFromEnd) {
3281
+ const step = 5e-3;
3282
+ if (searchFromEnd) {
3283
+ for (let t = 0.95; t >= 0.5; t -= step) {
3284
+ const pt = bezierPointAt(p0, cp1, cp2, p3, t);
3285
+ if (!isInsideRect(pt, targetRect)) {
3286
+ return t;
3287
+ }
3288
+ }
3289
+ } else {
3290
+ for (let t = 0.05; t <= 0.5; t += step) {
3291
+ const pt = bezierPointAt(p0, cp1, cp2, p3, t);
3292
+ if (!isInsideRect(pt, targetRect)) {
3293
+ return t;
3294
+ }
3295
+ }
3296
+ }
3297
+ return void 0;
3298
+ }
3210
3299
  function pointAlongArc(route, t) {
3211
3300
  const [first, second] = route;
3212
3301
  if (t <= 0.5) {
@@ -3334,8 +3423,16 @@ function renderConnection(ctx, conn, fromBounds, toBounds, theme, edgeRoute, opt
3334
3423
  let labelPoint;
3335
3424
  ctx.save();
3336
3425
  ctx.globalAlpha = conn.opacity;
3426
+ const arrowPlacement = conn.arrowPlacement ?? "endpoint";
3337
3427
  if (routing === "curve") {
3338
- const [p0, cp1, cp2, p3] = curveRoute(fromBounds, toBounds, diagramCenter, tension);
3428
+ const [p0, cp1, cp2, p3] = curveRoute(
3429
+ fromBounds,
3430
+ toBounds,
3431
+ diagramCenter,
3432
+ tension,
3433
+ conn.fromAnchor,
3434
+ conn.toAnchor
3435
+ );
3339
3436
  ctx.strokeStyle = style.color;
3340
3437
  ctx.lineWidth = style.width;
3341
3438
  ctx.setLineDash(style.dash ?? []);
@@ -3349,8 +3446,33 @@ function renderConnection(ctx, conn, fromBounds, toBounds, theme, edgeRoute, opt
3349
3446
  startAngle = Math.atan2(p0.y - cp1.y, p0.x - cp1.x);
3350
3447
  endAngle = Math.atan2(p3.y - cp2.y, p3.x - cp2.x);
3351
3448
  labelPoint = bezierPointAt(p0, cp1, cp2, p3, labelT);
3449
+ if (arrowPlacement === "boundary") {
3450
+ if (conn.arrow === "end" || conn.arrow === "both") {
3451
+ const tEnd = findBoundaryIntersection(p0, cp1, cp2, p3, toBounds, true);
3452
+ if (tEnd !== void 0) {
3453
+ endPoint = bezierPointAt(p0, cp1, cp2, p3, tEnd);
3454
+ const tangent = bezierTangentAt(p0, cp1, cp2, p3, tEnd);
3455
+ endAngle = Math.atan2(tangent.y, tangent.x);
3456
+ }
3457
+ }
3458
+ if (conn.arrow === "start" || conn.arrow === "both") {
3459
+ const tStart = findBoundaryIntersection(p0, cp1, cp2, p3, fromBounds, false);
3460
+ if (tStart !== void 0) {
3461
+ startPoint = bezierPointAt(p0, cp1, cp2, p3, tStart);
3462
+ const tangent = bezierTangentAt(p0, cp1, cp2, p3, tStart);
3463
+ startAngle = Math.atan2(tangent.y, tangent.x) + Math.PI;
3464
+ }
3465
+ }
3466
+ }
3352
3467
  } else if (routing === "arc") {
3353
- const [first, second] = arcRoute(fromBounds, toBounds, diagramCenter, tension);
3468
+ const [first, second] = arcRoute(
3469
+ fromBounds,
3470
+ toBounds,
3471
+ diagramCenter,
3472
+ tension,
3473
+ conn.fromAnchor,
3474
+ conn.toAnchor
3475
+ );
3354
3476
  const [p0, cp1, cp2, pMid] = first;
3355
3477
  const [, cp3, cp4, p3] = second;
3356
3478
  ctx.strokeStyle = style.color;
@@ -3367,9 +3489,29 @@ function renderConnection(ctx, conn, fromBounds, toBounds, theme, edgeRoute, opt
3367
3489
  startAngle = Math.atan2(p0.y - cp1.y, p0.x - cp1.x);
3368
3490
  endAngle = Math.atan2(p3.y - cp4.y, p3.x - cp4.x);
3369
3491
  labelPoint = pointAlongArc([first, second], labelT);
3492
+ if (arrowPlacement === "boundary") {
3493
+ if (conn.arrow === "end" || conn.arrow === "both") {
3494
+ const [, s_cp3, s_cp4, s_p3] = second;
3495
+ const tEnd = findBoundaryIntersection(pMid, s_cp3, s_cp4, s_p3, toBounds, true);
3496
+ if (tEnd !== void 0) {
3497
+ endPoint = bezierPointAt(pMid, s_cp3, s_cp4, s_p3, tEnd);
3498
+ const tangent = bezierTangentAt(pMid, s_cp3, s_cp4, s_p3, tEnd);
3499
+ endAngle = Math.atan2(tangent.y, tangent.x);
3500
+ }
3501
+ }
3502
+ if (conn.arrow === "start" || conn.arrow === "both") {
3503
+ const tStart = findBoundaryIntersection(p0, cp1, cp2, pMid, fromBounds, false);
3504
+ if (tStart !== void 0) {
3505
+ startPoint = bezierPointAt(p0, cp1, cp2, pMid, tStart);
3506
+ const tangent = bezierTangentAt(p0, cp1, cp2, pMid, tStart);
3507
+ startAngle = Math.atan2(tangent.y, tangent.x) + Math.PI;
3508
+ }
3509
+ }
3510
+ }
3370
3511
  } else {
3371
- const useElkRoute = routing === "auto" && (edgeRoute?.points.length ?? 0) >= 2;
3372
- linePoints = useElkRoute ? edgeRoute?.points ?? orthogonalRoute(fromBounds, toBounds) : orthogonalRoute(fromBounds, toBounds);
3512
+ const hasAnchorHints = conn.fromAnchor !== void 0 || conn.toAnchor !== void 0;
3513
+ const useElkRoute = routing === "auto" && !hasAnchorHints && (edgeRoute?.points.length ?? 0) >= 2;
3514
+ linePoints = useElkRoute ? edgeRoute?.points ?? orthogonalRoute(fromBounds, toBounds, conn.fromAnchor, conn.toAnchor) : orthogonalRoute(fromBounds, toBounds, conn.fromAnchor, conn.toAnchor);
3373
3515
  startPoint = linePoints[0];
3374
3516
  const startSegment = linePoints[1] ?? linePoints[0];
3375
3517
  const endStart = linePoints[linePoints.length - 2] ?? linePoints[0];
@@ -4026,6 +4168,36 @@ function renderDrawCommands(ctx, commands, theme) {
4026
4168
  });
4027
4169
  break;
4028
4170
  }
4171
+ case "grid": {
4172
+ const canvasWidth = ctx.canvas.width;
4173
+ const canvasHeight = ctx.canvas.height;
4174
+ withOpacity(ctx, command.opacity, () => {
4175
+ ctx.strokeStyle = command.color;
4176
+ ctx.lineWidth = command.width;
4177
+ const startX = command.offsetX % command.spacing;
4178
+ for (let x = startX; x <= canvasWidth; x += command.spacing) {
4179
+ ctx.beginPath();
4180
+ ctx.moveTo(x, 0);
4181
+ ctx.lineTo(x, canvasHeight);
4182
+ ctx.stroke();
4183
+ }
4184
+ const startY = command.offsetY % command.spacing;
4185
+ for (let y = startY; y <= canvasHeight; y += command.spacing) {
4186
+ ctx.beginPath();
4187
+ ctx.moveTo(0, y);
4188
+ ctx.lineTo(canvasWidth, y);
4189
+ ctx.stroke();
4190
+ }
4191
+ });
4192
+ rendered.push({
4193
+ id,
4194
+ kind: "draw",
4195
+ bounds: { x: 0, y: 0, width: canvasWidth, height: canvasHeight },
4196
+ foregroundColor: command.color,
4197
+ allowOverlap: true
4198
+ });
4199
+ break;
4200
+ }
4029
4201
  }
4030
4202
  }
4031
4203
  return rendered;
package/dist/qa.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { R as RenderMetadata, D as DesignSpec } from './spec.schema-Dm_wOLTd.js';
1
+ import { R as RenderMetadata, D as DesignSpec } from './spec.schema-BeFz_nk1.js';
2
2
  import 'zod';
3
3
  import '@napi-rs/canvas';
4
4