sketchmark 1.3.6 → 1.4.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/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;
@@ -8278,6 +8529,36 @@ function setParentGroupData(el, groupId) {
8278
8529
  if (groupId)
8279
8530
  el.dataset.parentGroup = groupId;
8280
8531
  }
8532
+ function resolveEdgeEndpointKind(id, nm, tm, gm, cm) {
8533
+ if (nm.has(id))
8534
+ return "node";
8535
+ if (gm.has(id))
8536
+ return "group";
8537
+ if (tm.has(id))
8538
+ return "table";
8539
+ if (cm.has(id))
8540
+ return "chart";
8541
+ return null;
8542
+ }
8543
+ function collectEdgeGroupLineage(endpointId, endpointKind, parentGroups) {
8544
+ const lineage = [];
8545
+ let groupId = endpointKind === "group"
8546
+ ? endpointId
8547
+ : parentGroups.get(`${endpointKind}:${endpointId}`);
8548
+ while (groupId) {
8549
+ lineage.push(groupId);
8550
+ groupId = parentGroups.get(`group:${groupId}`);
8551
+ }
8552
+ return lineage;
8553
+ }
8554
+ function resolveEdgeParentGroupId(fromId, toId, nm, tm, gm, cm, parentGroups) {
8555
+ const fromKind = resolveEdgeEndpointKind(fromId, nm, tm, gm, cm);
8556
+ const toKind = resolveEdgeEndpointKind(toId, nm, tm, gm, cm);
8557
+ if (!fromKind || !toKind)
8558
+ return undefined;
8559
+ const toLineage = new Set(collectEdgeGroupLineage(toId, toKind, parentGroups));
8560
+ return collectEdgeGroupLineage(fromId, fromKind, parentGroups).find((groupId) => toLineage.has(groupId));
8561
+ }
8281
8562
  // ── Node shapes ───────────────────────────────────────────────────────────
8282
8563
  function renderShape$1(rc, n, palette) {
8283
8564
  const s = n.style ?? {};
@@ -8434,19 +8715,16 @@ function renderToSVG(sg, container, options = {}) {
8434
8715
  const srcCX = src.x + src.w / 2, srcCY = src.y + src.h / 2;
8435
8716
  const [x1, y1] = getConnPoint(src, dstCX, dstCY, e.fromAnchor);
8436
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]]);
8437
8719
  const eg = mkGroup(`edge-${e.from}-${e.to}`, "eg");
8720
+ setParentGroupData(eg, resolveEdgeParentGroupId(e.from, e.to, nm, tm, gmMap, cm, parentGroups));
8438
8721
  if (e.style?.opacity != null)
8439
8722
  eg.setAttribute("opacity", String(e.style.opacity));
8440
- const len = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) || 1;
8441
- const nx = (x2 - x1) / len, ny = (y2 - y1) / len;
8442
8723
  const ecol = String(e.style?.stroke ?? palette.edgeStroke);
8443
8724
  const { arrowAt, dashed } = connMeta(e.connector);
8444
8725
  const HEAD = EDGE.headInset;
8445
- const sx1 = arrowAt === "start" || arrowAt === "both" ? x1 + nx * HEAD : x1;
8446
- const sy1 = arrowAt === "start" || arrowAt === "both" ? y1 + ny * HEAD : y1;
8447
- const sx2 = arrowAt === "end" || arrowAt === "both" ? x2 - nx * HEAD : x2;
8448
- const sy2 = arrowAt === "end" || arrowAt === "both" ? y2 - ny * HEAD : y2;
8449
- const shaft = rc.line(sx1, sy1, sx2, sy2, {
8726
+ const shaftPoints = insetPolylineEndpoints(points, arrowAt, HEAD);
8727
+ const shaft = rc.path(polylinePathData(shaftPoints), {
8450
8728
  ...BASE_ROUGH,
8451
8729
  roughness: 0.9,
8452
8730
  seed: hashStr$3(e.from + e.to),
@@ -8457,18 +8735,21 @@ function renderToSVG(sg, container, options = {}) {
8457
8735
  shaft.setAttribute("data-edge-role", "shaft");
8458
8736
  eg.appendChild(shaft);
8459
8737
  if (arrowAt === "end" || arrowAt === "both") {
8460
- 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));
8461
8741
  endHead.setAttribute("data-edge-role", "head");
8462
8742
  eg.appendChild(endHead);
8463
8743
  }
8464
8744
  if (arrowAt === "start" || arrowAt === "both") {
8465
- 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"));
8466
8748
  startHead.setAttribute("data-edge-role", "head");
8467
8749
  eg.appendChild(startHead);
8468
8750
  }
8469
8751
  if (e.label) {
8470
- const mx = (x1 + x2) / 2 - ny * EDGE.labelOffset + (e.labelDx ?? 0);
8471
- 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);
8472
8753
  const tw = Math.max(e.label.length * 7 + 12, 36);
8473
8754
  const bg = se("rect");
8474
8755
  bg.setAttribute("x", String(mx - tw / 2));
@@ -9180,31 +9461,31 @@ function renderToCanvas(sg, canvas, options = {}) {
9180
9461
  const srcCX = src.x + src.w / 2, srcCY = src.y + src.h / 2;
9181
9462
  const [x1, y1] = getConnPoint(src, dstCX, dstCY, e.fromAnchor);
9182
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]]);
9183
9465
  if (e.style?.opacity != null)
9184
9466
  ctx.globalAlpha = Number(e.style.opacity);
9185
9467
  const ecol = String(e.style?.stroke ?? palette.edgeStroke);
9186
9468
  const { arrowAt, dashed } = connMeta(e.connector);
9187
- const len = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) || 1;
9188
- const nx = (x2 - x1) / len, ny = (y2 - y1) / len;
9189
9469
  const HEAD = EDGE.headInset;
9190
- const sx1 = arrowAt === 'start' || arrowAt === 'both' ? x1 + nx * HEAD : x1;
9191
- const sy1 = arrowAt === 'start' || arrowAt === 'both' ? y1 + ny * HEAD : y1;
9192
- const sx2 = arrowAt === 'end' || arrowAt === 'both' ? x2 - nx * HEAD : x2;
9193
- const sy2 = arrowAt === 'end' || arrowAt === 'both' ? y2 - ny * HEAD : y2;
9194
- rc.line(sx1, sy1, sx2, sy2, {
9470
+ const shaftPoints = insetPolylineEndpoints(points, arrowAt, HEAD);
9471
+ rc.path(polylinePathData(shaftPoints), {
9195
9472
  ...R, roughness: 0.9, seed: hashStr$3(e.from + e.to),
9196
9473
  stroke: ecol,
9197
9474
  strokeWidth: Number(e.style?.strokeWidth ?? 1.6),
9198
9475
  ...(dashed ? { strokeLineDash: EDGE.dashPattern } : {}),
9199
9476
  });
9200
- const ang = Math.atan2(y2 - y1, x2 - x1);
9201
- if (arrowAt === 'end' || arrowAt === 'both')
9202
- drawArrowHead(rc, x2, y2, ang, ecol, hashStr$3(e.to));
9203
- if (arrowAt === 'start' || arrowAt === 'both')
9204
- 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
+ }
9205
9487
  if (e.label) {
9206
- const mx = (x1 + x2) / 2 - ny * EDGE.labelOffset + (e.labelDx ?? 0);
9207
- 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);
9208
9489
  // ── Edge label: font, font-size, letter-spacing ──
9209
9490
  // always center-anchored (single line)
9210
9491
  const eFontSize = Number(e.style?.fontSize ?? EDGE.labelFontSize);
@@ -9481,7 +9762,7 @@ const getTableEl = (svg, id) => getEl(svg, `table-${id}`);
9481
9762
  const getNoteEl = (svg, id) => getEl(svg, `note-${id}`);
9482
9763
  const getChartEl = (svg, id) => getEl(svg, `chart-${id}`);
9483
9764
  const getMarkdownEl = (svg, id) => getEl(svg, `markdown-${id}`);
9484
- const POSITIONABLE_SELECTOR = ".ng, .gg, .tg, .ntg, .cg, .mdg";
9765
+ const POSITIONABLE_SELECTOR = ".ng, .gg, .tg, .ntg, .cg, .eg, .mdg";
9485
9766
  function resolveNonEdgeDrawEl(svg, target) {
9486
9767
  return (getGroupEl(svg, target) ??
9487
9768
  getTableEl(svg, target) ??
@@ -9901,7 +10182,7 @@ function animateShapeDraw(el, strokeDur = ANIMATION.nodeStrokeDur, stag = ANIMAT
9901
10182
  }));
9902
10183
  }
9903
10184
  // ── Edge draw helpers ─────────────────────────────────────
9904
- const EDGE_SHAFT_SELECTOR = '[data-edge-role="shaft"] path';
10185
+ const EDGE_SHAFT_SELECTOR = '[data-edge-role="shaft"] path, path[data-edge-role="shaft"]';
9905
10186
  const EDGE_DECOR_SELECTOR = '[data-edge-role="head"], [data-edge-role="label"], [data-edge-role="label-bg"]';
9906
10187
  function edgeShaftPaths(el) {
9907
10188
  return Array.from(el.querySelectorAll(EDGE_SHAFT_SELECTOR));
@@ -10075,8 +10356,16 @@ class AnimationController {
10075
10356
  _buildDrawStepIndex() {
10076
10357
  const drawStepIndexByElementId = new Map();
10077
10358
  forEachPlaybackStep(this.steps, (step, stepIndex) => {
10078
- if (step.action !== "draw" || parseEdgeTarget(step.target))
10359
+ if (step.action !== "draw")
10360
+ return;
10361
+ const edge = parseEdgeTarget(step.target);
10362
+ if (edge) {
10363
+ const edgeEl = getEdgeEl(this.svg, edge.from, edge.to);
10364
+ if (edgeEl && !drawStepIndexByElementId.has(edgeEl.id)) {
10365
+ drawStepIndexByElementId.set(edgeEl.id, stepIndex);
10366
+ }
10079
10367
  return;
10368
+ }
10080
10369
  const el = resolveNonEdgeDrawEl(this.svg, step.target);
10081
10370
  if (el && !drawStepIndexByElementId.has(el.id)) {
10082
10371
  drawStepIndexByElementId.set(el.id, stepIndex);
@@ -10776,6 +11065,7 @@ class AnimationController {
10776
11065
  const el = getEdgeEl(this.svg, edge.from, edge.to);
10777
11066
  if (!el)
10778
11067
  return;
11068
+ showDrawEl(el);
10779
11069
  if (silent) {
10780
11070
  revealEdgeInstant(el);
10781
11071
  requestAnimationFrame(() => requestAnimationFrame(() => {
@@ -11389,11 +11679,12 @@ const ANIMATION_CSS = `
11389
11679
  .cg.faded, .eg.faded, .mdg.faded { opacity: 0.22; }
11390
11680
 
11391
11681
  .ng.hidden { opacity: 0; pointer-events: none; }
11392
- .gg.gg-hidden { opacity: 0; }
11393
- .tg.gg-hidden { opacity: 0; }
11394
- .ntg.gg-hidden { opacity: 0; }
11395
- .cg.gg-hidden { opacity: 0; }
11396
- .mdg.gg-hidden { opacity: 0; }
11682
+ .gg.gg-hidden { opacity: 0; }
11683
+ .tg.gg-hidden { opacity: 0; }
11684
+ .ntg.gg-hidden { opacity: 0; }
11685
+ .cg.gg-hidden { opacity: 0; }
11686
+ .eg.gg-hidden { opacity: 0; }
11687
+ .mdg.gg-hidden { opacity: 0; }
11397
11688
 
11398
11689
  /* narration caption */
11399
11690
  .skm-caption { pointer-events: none; user-select: none; }