sketchmark 1.3.7 → 1.4.1

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/index.d.ts CHANGED
@@ -12,7 +12,7 @@ export { AnimationController, ANIMATION_CSS } from "./animation";
12
12
  export type { AnimationEvent, AnimationEventType } from "./animation";
13
13
  export { exportSVG, exportPNG, exportCanvasPNG, exportHTML, exportGIF, exportMP4, getSVGBlob, svgToPNGDataURL, } from "./export";
14
14
  export type { ExportFormat, ExportOptions } from "./export";
15
- export type { NodeShape, EdgeConnector, EdgeAnchor, LayoutType, AlignItems, JustifyContent, AnimationAction, AnimationTrigger, StyleProps, StepPace, ASTNode, ASTEdge, ASTGroup, ASTStep, ASTBeat, ASTStepItem, ASTChart, ASTTable, GroupChildRef, RootItemRef, ASTMarkdown, } from "./ast/types";
15
+ export type { NodeShape, EdgeConnector, EdgeAnchor, EdgeRoute, EdgePoint, LayoutType, AlignItems, JustifyContent, AnimationAction, AnimationTrigger, StyleProps, StepPace, ASTNode, ASTEdge, ASTGroup, ASTStep, ASTBeat, ASTStepItem, ASTChart, ASTTable, GroupChildRef, RootItemRef, ASTMarkdown, } from "./ast/types";
16
16
  export { hashStr, clamp, lerp, parseHex, sleep, throttle, debounce, EventEmitter, } from "./utils";
17
17
  export { render } from "./render";
18
18
  export type { RenderOptions, DiagramInstance } from "./render";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC7C,YAAY,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,YAAY,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAChE,OAAO,EAAE,eAAe,EAAE,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC1E,YAAY,EACV,UAAU,EACV,SAAS,EACT,SAAS,EACT,UAAU,EACV,aAAa,GACd,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAG7C,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC1D,YAAY,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACzD,OAAO,EACL,cAAc,EACd,eAAe,EACf,kBAAkB,GACnB,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAG/D,OAAO,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjE,YAAY,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAGtE,OAAO,EACL,SAAS,EACT,SAAS,EACT,eAAe,EACf,UAAU,EACV,SAAS,EACT,SAAS,EACT,UAAU,EACV,eAAe,GAChB,MAAM,UAAU,CAAC;AAClB,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAG5D,YAAY,EACV,SAAS,EACT,aAAa,EACb,UAAU,EACV,UAAU,EACV,UAAU,EACV,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,UAAU,EACV,QAAQ,EACR,OAAO,EACP,OAAO,EACP,QAAQ,EACR,OAAO,EACP,OAAO,EACP,WAAW,EACX,QAAQ,EACR,QAAQ,EACR,aAAa,EACb,WAAW,EACX,WAAW,GACZ,MAAM,aAAa,CAAC;AAGrB,OAAO,EACL,OAAO,EACP,KAAK,EACL,IAAI,EACJ,QAAQ,EACR,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,YAAY,GACb,MAAM,SAAS,CAAC;AAGjB,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAG/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,YAAY,EACV,uBAAuB,EACvB,sBAAsB,EACtB,iCAAiC,GAClC,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,YAAY,EACV,uBAAuB,EACvB,sBAAsB,GACvB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC7C,YAAY,EACV,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,YAAY,CAAC;AAGpB,OAAO,EACL,QAAQ,EACR,cAAc,EACd,gBAAgB,EAChB,UAAU,EACV,WAAW,GACZ,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC7C,YAAY,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,YAAY,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAChE,OAAO,EAAE,eAAe,EAAE,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC1E,YAAY,EACV,UAAU,EACV,SAAS,EACT,SAAS,EACT,UAAU,EACV,aAAa,GACd,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAG7C,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC1D,YAAY,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACzD,OAAO,EACL,cAAc,EACd,eAAe,EACf,kBAAkB,GACnB,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAG/D,OAAO,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjE,YAAY,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAGtE,OAAO,EACL,SAAS,EACT,SAAS,EACT,eAAe,EACf,UAAU,EACV,SAAS,EACT,SAAS,EACT,UAAU,EACV,eAAe,GAChB,MAAM,UAAU,CAAC;AAClB,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAG5D,YAAY,EACV,SAAS,EACT,aAAa,EACb,UAAU,EACV,SAAS,EACT,SAAS,EACT,UAAU,EACV,UAAU,EACV,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,UAAU,EACV,QAAQ,EACR,OAAO,EACP,OAAO,EACP,QAAQ,EACR,OAAO,EACP,OAAO,EACP,WAAW,EACX,QAAQ,EACR,QAAQ,EACR,aAAa,EACb,WAAW,EACX,WAAW,GACZ,MAAM,aAAa,CAAC;AAGrB,OAAO,EACL,OAAO,EACP,KAAK,EACL,IAAI,EACJ,QAAQ,EACR,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,YAAY,GACb,MAAM,SAAS,CAAC;AAGjB,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAG/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,YAAY,EACV,uBAAuB,EACvB,sBAAsB,EACtB,iCAAiC,GAClC,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,YAAY,EACV,uBAAuB,EACvB,sBAAsB,GACvB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC7C,YAAY,EACV,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,YAAY,CAAC;AAGpB,OAAO,EACL,QAAQ,EACR,cAAc,EACd,gBAAgB,EAChB,UAAU,EACV,WAAW,GACZ,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC"}
package/dist/index.js CHANGED
@@ -357,6 +357,39 @@ function isValueToken(t) {
357
357
  function isPropKeyToken(t) {
358
358
  return !!t && (t.type === "IDENT" || t.type === "KEYWORD");
359
359
  }
360
+ const NUMBER_RE = /[-+]?(?:\d*\.\d+|\d+)(?:[eE][-+]?\d+)?/g;
361
+ function parseEdgeWaypoints(value, token) {
362
+ if (!value)
363
+ return undefined;
364
+ const numbers = (value.match(NUMBER_RE) ?? []).map((part) => Number(part));
365
+ if (!numbers.length)
366
+ return undefined;
367
+ if (numbers.length % 2 !== 0) {
368
+ throw new ParseError(`Edge via must contain x,y coordinate pairs`, token.line, token.col);
369
+ }
370
+ const points = [];
371
+ for (let index = 0; index < numbers.length; index += 2) {
372
+ const x = numbers[index];
373
+ const y = numbers[index + 1];
374
+ if (!Number.isFinite(x) || !Number.isFinite(y)) {
375
+ throw new ParseError(`Edge via contains a non-numeric coordinate`, token.line, token.col);
376
+ }
377
+ points.push([x, y]);
378
+ }
379
+ return points.length ? points : undefined;
380
+ }
381
+ function normalizeEdgeRoute(value, token) {
382
+ if (!value)
383
+ return undefined;
384
+ const normalized = value.toLowerCase();
385
+ if (normalized === "straight" || normalized === "polyline" || normalized === "orthogonal") {
386
+ return normalized;
387
+ }
388
+ if (normalized === "ortho" || normalized === "elbow") {
389
+ return "orthogonal";
390
+ }
391
+ throw new ParseError(`Unsupported edge route "${value}"; use straight, orthogonal, or polyline`, token.line, token.col);
392
+ }
360
393
  function parse(src, options = {}) {
361
394
  resetUid();
362
395
  const preparedSource = applyPluginPreprocessors(src, options.plugins);
@@ -715,28 +748,63 @@ function parse(src, options = {}) {
715
748
  height: props.height !== undefined ? parseFloat(props.height) : undefined,
716
749
  };
717
750
  }
718
- function parseEdge(fromId, connector, rest) {
719
- const toTok = rest.shift();
720
- if (!toTok)
721
- throw new ParseError("Expected edge target", 0, 0);
751
+ function parseEdgeProps(toks) {
722
752
  const props = {};
723
753
  let j = 0;
724
- while (j < rest.length) {
725
- const t = rest[j];
726
- if ((t.type === "IDENT" || t.type === "KEYWORD") &&
727
- j + 1 < rest.length &&
728
- rest[j + 1].type === "EQUALS") {
729
- props[t.value] = rest[j + 2]?.value ?? "";
730
- j += 3;
754
+ while (j < toks.length) {
755
+ const key = toks[j];
756
+ const eq = toks[j + 1];
757
+ if (!isPropKeyToken(key) || eq?.type !== "EQUALS") {
758
+ j++;
759
+ continue;
731
760
  }
732
- else {
761
+ const value = toks[j + 2];
762
+ if (!value) {
733
763
  j++;
764
+ continue;
734
765
  }
766
+ if (value.type === "LBRACKET") {
767
+ const parts = [];
768
+ let depth = 1;
769
+ j += 3;
770
+ while (j < toks.length && depth > 0) {
771
+ const tok = toks[j];
772
+ if (tok.type === "LBRACKET") {
773
+ depth++;
774
+ }
775
+ else if (tok.type === "RBRACKET") {
776
+ depth--;
777
+ if (depth === 0) {
778
+ j++;
779
+ break;
780
+ }
781
+ }
782
+ if (depth > 0)
783
+ parts.push(tok.value);
784
+ j++;
785
+ }
786
+ if (depth > 0) {
787
+ throw new ParseError(`Unterminated edge property list; expected ']'`, key.line, key.col);
788
+ }
789
+ props[key.value] = parts.join(" ");
790
+ continue;
791
+ }
792
+ props[key.value] = value.value;
793
+ j += 3;
735
794
  }
795
+ return props;
796
+ }
797
+ function parseEdge(fromId, connector, rest) {
798
+ const toTok = rest.shift();
799
+ if (!toTok)
800
+ throw new ParseError("Expected edge target", 0, 0);
801
+ const props = parseEdgeProps(rest);
736
802
  const dashed = connector.includes("--") ||
737
803
  connector.includes(".-") ||
738
804
  connector.includes("-.");
739
805
  const bidirectional = connector.includes("<") && connector.includes(">");
806
+ const via = parseEdgeWaypoints(props.via, toTok);
807
+ const route = normalizeEdgeRoute(props.route, toTok) ?? (via?.length ? "polyline" : undefined);
740
808
  return {
741
809
  kind: "edge",
742
810
  id: uid("edge"),
@@ -748,6 +816,8 @@ function parse(src, options = {}) {
748
816
  labelDy: props["label-dy"] !== undefined ? parseFloat(props["label-dy"]) : undefined,
749
817
  fromAnchor: props["anchor-from"],
750
818
  toAnchor: props["anchor-to"],
819
+ route,
820
+ via,
751
821
  dashed,
752
822
  bidirectional,
753
823
  style: propsToStyle(props),
@@ -3681,6 +3751,8 @@ function buildSceneGraph(ast) {
3681
3751
  labelDy: e.labelDy,
3682
3752
  fromAnchor: e.fromAnchor,
3683
3753
  toAnchor: e.toAnchor,
3754
+ route: e.route,
3755
+ via: e.via,
3684
3756
  dashed: e.dashed ?? false,
3685
3757
  bidirectional: e.bidirectional ?? false,
3686
3758
  style: e.style ?? {},
@@ -4282,6 +4354,151 @@ function getConnPoint(src, dstCX, dstCY, anchor) {
4282
4354
  return anchoredConnPoint(src, anchor, dstCX, dstCY);
4283
4355
  }
4284
4356
  // ── Group depth (for paint order) ────────────────────────────────────────
4357
+ function segmentLength(a, b) {
4358
+ return Math.hypot(b[0] - a[0], b[1] - a[1]);
4359
+ }
4360
+ function compactPolylinePoints(points) {
4361
+ const compacted = [];
4362
+ for (const point of points) {
4363
+ const previous = compacted[compacted.length - 1];
4364
+ if (!previous || segmentLength(previous, point) > 0.01) {
4365
+ compacted.push(point);
4366
+ }
4367
+ }
4368
+ return compacted;
4369
+ }
4370
+ function polylinePathData(points) {
4371
+ return points
4372
+ .map(([x, y], index) => `${index === 0 ? "M" : "L"} ${x} ${y}`)
4373
+ .join(" ");
4374
+ }
4375
+ function polylineEndpointDirection(points, end) {
4376
+ const step = end === "start" ? 1 : -1;
4377
+ let index = end === "start" ? 0 : points.length - 1;
4378
+ while (index + step >= 0 && index + step < points.length) {
4379
+ const from = points[index];
4380
+ const to = points[index + step];
4381
+ const dx = to[0] - from[0];
4382
+ const dy = to[1] - from[1];
4383
+ const len = Math.hypot(dx, dy);
4384
+ if (len > 0.01) {
4385
+ return end === "start" ? [dx / len, dy / len] : [-dx / len, -dy / len];
4386
+ }
4387
+ index += step;
4388
+ }
4389
+ return [1, 0];
4390
+ }
4391
+ function insetPolylineEndpoints(points, arrowAt, inset) {
4392
+ const next = points.map((point) => [point[0], point[1]]);
4393
+ if (next.length < 2)
4394
+ return next;
4395
+ if (arrowAt === "start" || arrowAt === "both") {
4396
+ const [dx, dy] = polylineEndpointDirection(next, "start");
4397
+ next[0] = [next[0][0] + dx * inset, next[0][1] + dy * inset];
4398
+ }
4399
+ if (arrowAt === "end" || arrowAt === "both") {
4400
+ const [dx, dy] = polylineEndpointDirection(next, "end");
4401
+ const last = next.length - 1;
4402
+ next[last] = [next[last][0] - dx * inset, next[last][1] - dy * inset];
4403
+ }
4404
+ return compactPolylinePoints(next);
4405
+ }
4406
+ function polylineLabelPosition(points, offset, dx = 0, dy = 0) {
4407
+ if (points.length < 2) {
4408
+ const [x, y] = points[0] ?? [0, 0];
4409
+ return { x: x + dx, y: y + dy };
4410
+ }
4411
+ const lengths = points.slice(1).map((point, index) => segmentLength(points[index], point));
4412
+ const total = lengths.reduce((sum, value) => sum + value, 0);
4413
+ if (total <= 0.01) {
4414
+ const [x, y] = points[0];
4415
+ return { x: x + dx, y: y + dy };
4416
+ }
4417
+ let travelled = 0;
4418
+ const target = total / 2;
4419
+ for (let index = 0; index < lengths.length; index += 1) {
4420
+ const length = lengths[index];
4421
+ if (travelled + length >= target) {
4422
+ const from = points[index];
4423
+ const to = points[index + 1];
4424
+ const t = length > 0 ? (target - travelled) / length : 0;
4425
+ const ux = (to[0] - from[0]) / length;
4426
+ const uy = (to[1] - from[1]) / length;
4427
+ return {
4428
+ x: from[0] + (to[0] - from[0]) * t - uy * offset + dx,
4429
+ y: from[1] + (to[1] - from[1]) * t + ux * offset + dy,
4430
+ };
4431
+ }
4432
+ travelled += length;
4433
+ }
4434
+ const [x, y] = points[points.length - 1];
4435
+ return { x: x + dx, y: y + dy };
4436
+ }
4437
+ function rectBoundaryPoint(entity, point, direction) {
4438
+ const [px, py] = point;
4439
+ const [dx, dy] = direction;
4440
+ const candidates = [];
4441
+ const minX = entity.x;
4442
+ const maxX = entity.x + entity.w;
4443
+ const minY = entity.y;
4444
+ const maxY = entity.y + entity.h;
4445
+ const epsilon = 0.01;
4446
+ if (Math.abs(dx) > epsilon) {
4447
+ candidates.push((minX - px) / dx, (maxX - px) / dx);
4448
+ }
4449
+ if (Math.abs(dy) > epsilon) {
4450
+ candidates.push((minY - py) / dy, (maxY - py) / dy);
4451
+ }
4452
+ const valid = candidates
4453
+ .filter((t) => t >= -epsilon)
4454
+ .map((t) => ({
4455
+ t: Math.max(0, t),
4456
+ x: px + dx * t,
4457
+ y: py + dy * t,
4458
+ }))
4459
+ .filter(({ x, y }) => x >= minX - epsilon &&
4460
+ x <= maxX + epsilon &&
4461
+ y >= minY - epsilon &&
4462
+ y <= maxY + epsilon)
4463
+ .sort((a, b) => a.t - b.t);
4464
+ const hit = valid[0];
4465
+ return hit ? [hit.x, hit.y] : point;
4466
+ }
4467
+ function ellipseBoundaryPoint(entity, point, direction) {
4468
+ const [px, py] = point;
4469
+ const [dx, dy] = direction;
4470
+ const cx = entity.x + entity.w / 2;
4471
+ const cy = entity.y + entity.h / 2;
4472
+ const rx = Math.max(1, entity.w * 0.44);
4473
+ const ry = Math.max(1, entity.h * 0.44);
4474
+ const x0 = px - cx;
4475
+ const y0 = py - cy;
4476
+ const a = (dx * dx) / (rx * rx) + (dy * dy) / (ry * ry);
4477
+ const b = 2 * ((x0 * dx) / (rx * rx) + (y0 * dy) / (ry * ry));
4478
+ const c = (x0 * x0) / (rx * rx) + (y0 * y0) / (ry * ry) - 1;
4479
+ const disc = b * b - 4 * a * c;
4480
+ if (a <= 0 || disc < 0)
4481
+ return point;
4482
+ const sqrt = Math.sqrt(disc);
4483
+ const hits = [(-b - sqrt) / (2 * a), (-b + sqrt) / (2 * a)]
4484
+ .filter((t) => t >= -0.01)
4485
+ .sort((left, right) => left - right);
4486
+ const t = Math.max(0, hits[0] ?? 0);
4487
+ return [px + dx * t, py + dy * t];
4488
+ }
4489
+ function polylineArrowTipPoint(entity, points, end) {
4490
+ const point = end === "start" ? points[0] : points[points.length - 1];
4491
+ if (!point)
4492
+ return [0, 0];
4493
+ const [dx, dy] = polylineEndpointDirection(points, end);
4494
+ const outward = end === "start" ? [dx, dy] : [-dx, -dy];
4495
+ if (Math.hypot(outward[0], outward[1]) <= 0.01)
4496
+ return point;
4497
+ if (entity.shape === "circle") {
4498
+ return ellipseBoundaryPoint(entity, point, outward);
4499
+ }
4500
+ return rectBoundaryPoint(entity, point, outward);
4501
+ }
4285
4502
  function groupDepth(g, gm) {
4286
4503
  let d = 0;
4287
4504
  let cur = g;
@@ -5242,6 +5459,31 @@ function rectConnPoint(rx, ry, rw, rh, ox, oy) {
5242
5459
  const t = Math.min(tx, ty);
5243
5460
  return [cx + t * dx, cy + t * dy];
5244
5461
  }
5462
+ function distance$1(a, b) {
5463
+ return Math.hypot(b[0] - a[0], b[1] - a[1]);
5464
+ }
5465
+ function compactEdgePoints(points) {
5466
+ const compacted = [];
5467
+ for (const point of points) {
5468
+ const previous = compacted[compacted.length - 1];
5469
+ if (!previous || distance$1(previous, point) > 0.01) {
5470
+ compacted.push(point);
5471
+ }
5472
+ }
5473
+ return compacted;
5474
+ }
5475
+ function orthogonalEdgePoints(start, end) {
5476
+ if (Math.abs(start[0] - end[0]) < 0.01 || Math.abs(start[1] - end[1]) < 0.01) {
5477
+ return [start, end];
5478
+ }
5479
+ const midX = (start[0] + end[0]) / 2;
5480
+ return compactEdgePoints([
5481
+ start,
5482
+ [midX, start[1]],
5483
+ [midX, end[1]],
5484
+ end,
5485
+ ]);
5486
+ }
5245
5487
  function routeEdges(sg) {
5246
5488
  const nm = nodeMap(sg);
5247
5489
  const tm = tableMap(sg);
@@ -5271,10 +5513,17 @@ function routeEdges(sg) {
5271
5513
  }
5272
5514
  const dstCX = dst.x + dst.w / 2, dstCY = dst.y + dst.h / 2;
5273
5515
  const srcCX = src.x + src.w / 2, srcCY = src.y + src.h / 2;
5274
- e.points = [
5275
- anchoredConnPoint(src, e.fromAnchor, dstCX, dstCY),
5276
- anchoredConnPoint(dst, e.toAnchor, srcCX, srcCY),
5277
- ];
5516
+ const start = anchoredConnPoint(src, e.fromAnchor, dstCX, dstCY);
5517
+ const end = anchoredConnPoint(dst, e.toAnchor, srcCX, srcCY);
5518
+ if (e.via?.length) {
5519
+ e.points = compactEdgePoints([start, ...e.via, end]);
5520
+ }
5521
+ else if (e.route === "orthogonal") {
5522
+ e.points = orthogonalEdgePoints(start, end);
5523
+ }
5524
+ else {
5525
+ e.points = [start, end];
5526
+ }
5278
5527
  }
5279
5528
  }
5280
5529
  function computeBounds(sg, margin) {
@@ -5284,6 +5533,7 @@ function computeBounds(sg, margin) {
5284
5533
  ...sg.tables.map((t) => t.x + t.w),
5285
5534
  ...sg.charts.map((c) => c.x + c.w),
5286
5535
  ...sg.markdowns.map((m) => m.x + m.w),
5536
+ ...sg.edges.flatMap((e) => (e.points ?? []).map(([x]) => x)),
5287
5537
  ];
5288
5538
  const allY = [
5289
5539
  ...sg.nodes.map((n) => n.y + n.h),
@@ -5291,6 +5541,7 @@ function computeBounds(sg, margin) {
5291
5541
  ...sg.tables.map((t) => t.y + t.h),
5292
5542
  ...sg.charts.map((c) => c.y + c.h),
5293
5543
  ...sg.markdowns.map((m) => m.y + m.h),
5544
+ ...sg.edges.flatMap((e) => (e.points ?? []).map(([, y]) => y)),
5294
5545
  ];
5295
5546
  const autoWidth = (allX.length ? Math.max(...allX) : 400) + margin;
5296
5547
  const autoHeight = (allY.length ? Math.max(...allY) : 300) + margin;
@@ -8464,20 +8715,16 @@ function renderToSVG(sg, container, options = {}) {
8464
8715
  const srcCX = src.x + src.w / 2, srcCY = src.y + src.h / 2;
8465
8716
  const [x1, y1] = getConnPoint(src, dstCX, dstCY, e.fromAnchor);
8466
8717
  const [x2, y2] = getConnPoint(dst, srcCX, srcCY, e.toAnchor);
8718
+ const points = compactPolylinePoints(e.points?.length && e.points.length >= 2 ? e.points : [[x1, y1], [x2, y2]]);
8467
8719
  const eg = mkGroup(`edge-${e.from}-${e.to}`, "eg");
8468
8720
  setParentGroupData(eg, resolveEdgeParentGroupId(e.from, e.to, nm, tm, gmMap, cm, parentGroups));
8469
8721
  if (e.style?.opacity != null)
8470
8722
  eg.setAttribute("opacity", String(e.style.opacity));
8471
- const len = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) || 1;
8472
- const nx = (x2 - x1) / len, ny = (y2 - y1) / len;
8473
8723
  const ecol = String(e.style?.stroke ?? palette.edgeStroke);
8474
8724
  const { arrowAt, dashed } = connMeta(e.connector);
8475
8725
  const HEAD = EDGE.headInset;
8476
- const sx1 = arrowAt === "start" || arrowAt === "both" ? x1 + nx * HEAD : x1;
8477
- const sy1 = arrowAt === "start" || arrowAt === "both" ? y1 + ny * HEAD : y1;
8478
- const sx2 = arrowAt === "end" || arrowAt === "both" ? x2 - nx * HEAD : x2;
8479
- const sy2 = arrowAt === "end" || arrowAt === "both" ? y2 - ny * HEAD : y2;
8480
- const shaft = rc.line(sx1, sy1, sx2, sy2, {
8726
+ const shaftPoints = insetPolylineEndpoints(points, arrowAt, HEAD);
8727
+ const shaft = rc.path(polylinePathData(shaftPoints), {
8481
8728
  ...BASE_ROUGH,
8482
8729
  roughness: 0.9,
8483
8730
  seed: hashStr$3(e.from + e.to),
@@ -8488,18 +8735,21 @@ function renderToSVG(sg, container, options = {}) {
8488
8735
  shaft.setAttribute("data-edge-role", "shaft");
8489
8736
  eg.appendChild(shaft);
8490
8737
  if (arrowAt === "end" || arrowAt === "both") {
8491
- const endHead = arrowHead(rc, x2, y2, Math.atan2(y2 - y1, x2 - x1), ecol, hashStr$3(e.to));
8738
+ const [endDx, endDy] = polylineEndpointDirection(points, "end");
8739
+ const [endX, endY] = polylineArrowTipPoint(dst, points, "end");
8740
+ const endHead = arrowHead(rc, endX, endY, Math.atan2(endDy, endDx), ecol, hashStr$3(e.to));
8492
8741
  endHead.setAttribute("data-edge-role", "head");
8493
8742
  eg.appendChild(endHead);
8494
8743
  }
8495
8744
  if (arrowAt === "start" || arrowAt === "both") {
8496
- const startHead = arrowHead(rc, x1, y1, Math.atan2(y1 - y2, x1 - x2), ecol, hashStr$3(e.from + "back"));
8745
+ const [startDx, startDy] = polylineEndpointDirection(points, "start");
8746
+ const [startX, startY] = polylineArrowTipPoint(src, points, "start");
8747
+ const startHead = arrowHead(rc, startX, startY, Math.atan2(-startDy, -startDx), ecol, hashStr$3(e.from + "back"));
8497
8748
  startHead.setAttribute("data-edge-role", "head");
8498
8749
  eg.appendChild(startHead);
8499
8750
  }
8500
8751
  if (e.label) {
8501
- const mx = (x1 + x2) / 2 - ny * EDGE.labelOffset + (e.labelDx ?? 0);
8502
- const my = (y1 + y2) / 2 + nx * EDGE.labelOffset + (e.labelDy ?? 0);
8752
+ const { x: mx, y: my } = polylineLabelPosition(points, EDGE.labelOffset, e.labelDx ?? 0, e.labelDy ?? 0);
8503
8753
  const tw = Math.max(e.label.length * 7 + 12, 36);
8504
8754
  const bg = se("rect");
8505
8755
  bg.setAttribute("x", String(mx - tw / 2));
@@ -9211,31 +9461,31 @@ function renderToCanvas(sg, canvas, options = {}) {
9211
9461
  const srcCX = src.x + src.w / 2, srcCY = src.y + src.h / 2;
9212
9462
  const [x1, y1] = getConnPoint(src, dstCX, dstCY, e.fromAnchor);
9213
9463
  const [x2, y2] = getConnPoint(dst, srcCX, srcCY, e.toAnchor);
9464
+ const points = compactPolylinePoints(e.points?.length && e.points.length >= 2 ? e.points : [[x1, y1], [x2, y2]]);
9214
9465
  if (e.style?.opacity != null)
9215
9466
  ctx.globalAlpha = Number(e.style.opacity);
9216
9467
  const ecol = String(e.style?.stroke ?? palette.edgeStroke);
9217
9468
  const { arrowAt, dashed } = connMeta(e.connector);
9218
- const len = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) || 1;
9219
- const nx = (x2 - x1) / len, ny = (y2 - y1) / len;
9220
9469
  const HEAD = EDGE.headInset;
9221
- const sx1 = arrowAt === 'start' || arrowAt === 'both' ? x1 + nx * HEAD : x1;
9222
- const sy1 = arrowAt === 'start' || arrowAt === 'both' ? y1 + ny * HEAD : y1;
9223
- const sx2 = arrowAt === 'end' || arrowAt === 'both' ? x2 - nx * HEAD : x2;
9224
- const sy2 = arrowAt === 'end' || arrowAt === 'both' ? y2 - ny * HEAD : y2;
9225
- rc.line(sx1, sy1, sx2, sy2, {
9470
+ const shaftPoints = insetPolylineEndpoints(points, arrowAt, HEAD);
9471
+ rc.path(polylinePathData(shaftPoints), {
9226
9472
  ...R, roughness: 0.9, seed: hashStr$3(e.from + e.to),
9227
9473
  stroke: ecol,
9228
9474
  strokeWidth: Number(e.style?.strokeWidth ?? 1.6),
9229
9475
  ...(dashed ? { strokeLineDash: EDGE.dashPattern } : {}),
9230
9476
  });
9231
- const ang = Math.atan2(y2 - y1, x2 - x1);
9232
- if (arrowAt === 'end' || arrowAt === 'both')
9233
- drawArrowHead(rc, x2, y2, ang, ecol, hashStr$3(e.to));
9234
- if (arrowAt === 'start' || arrowAt === 'both')
9235
- drawArrowHead(rc, x1, y1, Math.atan2(y1 - y2, x1 - x2), ecol, hashStr$3(e.from + 'back'));
9477
+ if (arrowAt === 'end' || arrowAt === 'both') {
9478
+ const [endDx, endDy] = polylineEndpointDirection(points, 'end');
9479
+ const [endX, endY] = polylineArrowTipPoint(dst, points, 'end');
9480
+ drawArrowHead(rc, endX, endY, Math.atan2(endDy, endDx), ecol, hashStr$3(e.to));
9481
+ }
9482
+ if (arrowAt === 'start' || arrowAt === 'both') {
9483
+ const [startDx, startDy] = polylineEndpointDirection(points, 'start');
9484
+ const [startX, startY] = polylineArrowTipPoint(src, points, 'start');
9485
+ drawArrowHead(rc, startX, startY, Math.atan2(-startDy, -startDx), ecol, hashStr$3(e.from + 'back'));
9486
+ }
9236
9487
  if (e.label) {
9237
- const mx = (x1 + x2) / 2 - ny * EDGE.labelOffset + (e.labelDx ?? 0);
9238
- const my = (y1 + y2) / 2 + nx * EDGE.labelOffset + (e.labelDy ?? 0);
9488
+ const { x: mx, y: my } = polylineLabelPosition(points, EDGE.labelOffset, e.labelDx ?? 0, e.labelDy ?? 0);
9239
9489
  // ── Edge label: font, font-size, letter-spacing ──
9240
9490
  // always center-anchored (single line)
9241
9491
  const eFontSize = Number(e.style?.fontSize ?? EDGE.labelFontSize);
@@ -9932,7 +10182,7 @@ function animateShapeDraw(el, strokeDur = ANIMATION.nodeStrokeDur, stag = ANIMAT
9932
10182
  }));
9933
10183
  }
9934
10184
  // ── Edge draw helpers ─────────────────────────────────────
9935
- const EDGE_SHAFT_SELECTOR = '[data-edge-role="shaft"] path';
10185
+ const EDGE_SHAFT_SELECTOR = '[data-edge-role="shaft"] path, path[data-edge-role="shaft"]';
9936
10186
  const EDGE_DECOR_SELECTOR = '[data-edge-role="head"], [data-edge-role="label"], [data-edge-role="label-bg"]';
9937
10187
  function edgeShaftPaths(el) {
9938
10188
  return Array.from(el.querySelectorAll(EDGE_SHAFT_SELECTOR));
@@ -11149,8 +11399,11 @@ class AnimationController {
11149
11399
  * 4. After guide finishes → fade in rough.js element, remove guide
11150
11400
  */
11151
11401
  _animateAnnotation(roughEl, guideD, silent) {
11152
- if (silent)
11402
+ if (silent) {
11403
+ roughEl.style.opacity = "1";
11404
+ roughEl.style.transition = "none";
11153
11405
  return;
11406
+ }
11154
11407
  // Hide rough.js element — will be revealed after guide draws
11155
11408
  roughEl.style.opacity = "0";
11156
11409
  roughEl.style.transition = "none";
@@ -11440,10 +11693,74 @@ const ANIMATION_CSS = `
11440
11693
  .skm-caption { pointer-events: none; user-select: none; }
11441
11694
  `;
11442
11695
 
11696
+ const exportAnimationState = new WeakMap();
11697
+ function bindExportAnimationState(svg, state) {
11698
+ exportAnimationState.set(svg, state);
11699
+ }
11700
+ function getExportAnimationState(svg) {
11701
+ return exportAnimationState.get(svg);
11702
+ }
11703
+
11443
11704
  // ============================================================
11444
11705
  // sketchmark — Export System
11445
11706
  // SVG, PNG, Canvas, GIF (stub), MP4 (stub)
11446
11707
  // ============================================================
11708
+ const EXPORT_SVG_STYLE_ID = "sketchmark-export-state";
11709
+ const EXPORT_SVG_STATE_CSS = `
11710
+ .ng, .gg, .tg, .ntg, .cg, .eg, .mdg {
11711
+ animation: none !important;
11712
+ transition: none !important;
11713
+ }
11714
+
11715
+ .ng.hidden { opacity: 0 !important; pointer-events: none !important; }
11716
+ .gg.gg-hidden,
11717
+ .tg.gg-hidden,
11718
+ .ntg.gg-hidden,
11719
+ .cg.gg-hidden,
11720
+ .eg.gg-hidden,
11721
+ .mdg.gg-hidden { opacity: 0 !important; }
11722
+
11723
+ .ng.faded,
11724
+ .gg.faded,
11725
+ .tg.faded,
11726
+ .ntg.faded,
11727
+ .cg.faded,
11728
+ .eg.faded,
11729
+ .mdg.faded { opacity: 0.22 !important; }
11730
+
11731
+ .ng.hl path, .ng.hl rect, .ng.hl ellipse, .ng.hl polygon,
11732
+ .tg.hl path, .tg.hl rect,
11733
+ .ntg.hl path, .ntg.hl polygon,
11734
+ .cg.hl path, .cg.hl rect,
11735
+ .mdg.hl text,
11736
+ .eg.hl path, .eg.hl line, .eg.hl polygon { stroke-width: 2.8 !important; }
11737
+ `;
11738
+ function buildExportSnapshot(svg) {
11739
+ const snapshot = svg.cloneNode(true);
11740
+ const animationState = getExportAnimationState(svg);
11741
+ if (animationState?.steps.length) {
11742
+ snapshot.querySelector("#annotation-layer")?.remove();
11743
+ let rc = null;
11744
+ try {
11745
+ rc = rough.svg(snapshot);
11746
+ }
11747
+ catch {
11748
+ rc = null;
11749
+ }
11750
+ const anim = new AnimationController(snapshot, animationState.steps, undefined, rc, animationState.config);
11751
+ anim.goTo(animationState.steps.length - 1);
11752
+ }
11753
+ injectExportStyles(snapshot);
11754
+ return snapshot;
11755
+ }
11756
+ function injectExportStyles(svg) {
11757
+ if (svg.querySelector(`#${EXPORT_SVG_STYLE_ID}`))
11758
+ return;
11759
+ const style = document.createElementNS(SVG_NS$1, "style");
11760
+ style.setAttribute("id", EXPORT_SVG_STYLE_ID);
11761
+ style.textContent = EXPORT_SVG_STATE_CSS;
11762
+ svg.insertBefore(style, svg.firstChild);
11763
+ }
11447
11764
  // ── Trigger browser download ──────────────────────────────
11448
11765
  function download(blob, filename) {
11449
11766
  const url = URL.createObjectURL(blob);
@@ -11457,15 +11774,15 @@ function download(blob, filename) {
11457
11774
  }
11458
11775
  // ── SVG export ────────────────────────────────────────────
11459
11776
  function exportSVG(svg, opts = {}) {
11460
- const str = svgToString(svg);
11777
+ const str = svgToString(buildExportSnapshot(svg));
11461
11778
  const blob = new Blob([str], { type: 'image/svg+xml;charset=utf-8' });
11462
11779
  download(blob, opts.filename ?? 'diagram.svg');
11463
11780
  }
11464
11781
  function getSVGString(svg) {
11465
- return svgToString(svg);
11782
+ return svgToString(buildExportSnapshot(svg));
11466
11783
  }
11467
11784
  function getSVGBlob(svg) {
11468
- return new Blob([svgToString(svg)], { type: 'image/svg+xml;charset=utf-8' });
11785
+ return new Blob([svgToString(buildExportSnapshot(svg))], { type: 'image/svg+xml;charset=utf-8' });
11469
11786
  }
11470
11787
  // ── PNG export (from SVG via Canvas) ─────────────────────
11471
11788
  async function exportPNG(svg, opts = {}) {
@@ -11491,7 +11808,7 @@ async function svgToPNGDataURL(svg, opts = {}) {
11491
11808
  ctx.fillStyle = EXPORT.fallbackBg;
11492
11809
  ctx.fillRect(0, 0, w, h);
11493
11810
  }
11494
- const svgStr = svgToString(svg);
11811
+ const svgStr = svgToString(buildExportSnapshot(svg));
11495
11812
  const blob = new Blob([svgStr], { type: 'image/svg+xml;charset=utf-8' });
11496
11813
  const url = URL.createObjectURL(blob);
11497
11814
  await new Promise((resolve, reject) => {
@@ -11509,7 +11826,7 @@ async function exportCanvasPNG(canvas, opts = {}) {
11509
11826
  }
11510
11827
  // ── HTML export (self-contained) ──────────────────────────
11511
11828
  function exportHTML(svg, dslSource, opts = {}) {
11512
- const svgStr = svgToString(svg);
11829
+ const svgStr = svgToString(buildExportSnapshot(svg));
11513
11830
  const html = `<!DOCTYPE html>
11514
11831
  <html lang="en">
11515
11832
  <head>
@@ -11690,6 +12007,7 @@ function render(options) {
11690
12007
  }
11691
12008
  const containerEl = el instanceof SVGSVGElement ? undefined : el;
11692
12009
  anim = new AnimationController(svg, ast.steps, containerEl, rc, ast.config);
12010
+ bindExportAnimationState(svg, { steps: ast.steps, config: ast.config });
11693
12011
  }
11694
12012
  if (typeof tts === "boolean") {
11695
12013
  anim.tts = tts;