@markdy/core 1.4.1 → 1.4.3

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.
Files changed (2) hide show
  1. package/dist/index.js +179 -25
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1023,17 +1023,32 @@ function dedupeEdges(edges) {
1023
1023
  function assignRanks(nodeIds, edges, direction) {
1024
1024
  const ranks = /* @__PURE__ */ new Map();
1025
1025
  for (const id of nodeIds) ranks.set(id, 0);
1026
- const forward = edges.filter((e) => e.kind !== "response" && !e.selfLoop);
1027
- for (let sweep = 0; sweep < nodeIds.length; sweep++) {
1028
- let changed = false;
1029
- for (const e of forward) {
1030
- const next = (ranks.get(e.from) ?? 0) + 1;
1031
- if (next > (ranks.get(e.to) ?? 0)) {
1032
- ranks.set(e.to, next);
1033
- changed = true;
1034
- }
1026
+ const outgoing = new Map(nodeIds.map((id) => [id, []]));
1027
+ const indegree = new Map(nodeIds.map((id) => [id, 0]));
1028
+ for (const edge of edges) {
1029
+ if (edge.kind === "response" || edge.selfLoop || !outgoing.has(edge.from) || !outgoing.has(edge.to)) continue;
1030
+ outgoing.get(edge.from).push(edge.to);
1031
+ indegree.set(edge.to, indegree.get(edge.to) + 1);
1032
+ }
1033
+ const queue = nodeIds.filter((id) => indegree.get(id) === 0);
1034
+ const processed = /* @__PURE__ */ new Set();
1035
+ let cursor = 0;
1036
+ let cycleRoot = 0;
1037
+ while (processed.size < nodeIds.length) {
1038
+ if (cursor === queue.length) {
1039
+ while (processed.has(nodeIds[cycleRoot])) cycleRoot++;
1040
+ queue.push(nodeIds[cycleRoot]);
1041
+ }
1042
+ const id = queue[cursor++];
1043
+ if (processed.has(id)) continue;
1044
+ processed.add(id);
1045
+ for (const target of outgoing.get(id)) {
1046
+ if (processed.has(target)) continue;
1047
+ ranks.set(target, Math.max(ranks.get(target), ranks.get(id) + 1));
1048
+ const remaining = indegree.get(target) - 1;
1049
+ indegree.set(target, remaining);
1050
+ if (remaining === 0) queue.push(target);
1035
1051
  }
1036
- if (!changed) break;
1037
1052
  }
1038
1053
  if (direction === "RL" || direction === "BT") {
1039
1054
  const max = Math.max(...ranks.values(), 0);
@@ -1047,7 +1062,7 @@ function snapGrid(n, step = 4) {
1047
1062
  function layoutRanked(ast, edges, opts) {
1048
1063
  const nodeIds = Object.keys(ast.nodes);
1049
1064
  const dtype = diagramType(ast);
1050
- const direction = opts.forceVertical ? "TB" : ast.meta.direction;
1065
+ const direction = opts.forceVertical ? ast.meta.direction === "BT" ? "BT" : "TB" : ast.meta.direction;
1051
1066
  const ranks = assignRanks(nodeIds, edges, direction);
1052
1067
  const byRank = /* @__PURE__ */ new Map();
1053
1068
  for (const id of nodeIds) {
@@ -1072,6 +1087,7 @@ function layoutRanked(ast, edges, opts) {
1072
1087
  }
1073
1088
  const rankOrder = /* @__PURE__ */ new Map();
1074
1089
  const sortedRanks = [...byRank.keys()].sort((a, b) => a - b);
1090
+ if (direction === "RL" || direction === "BT") sortedRanks.reverse();
1075
1091
  for (const r of sortedRanks) {
1076
1092
  const ids = byRank.get(r);
1077
1093
  ids.sort((a, b) => {
@@ -1091,6 +1107,39 @@ function layoutRanked(ast, edges, opts) {
1091
1107
  rankOrder.set(id, idx);
1092
1108
  });
1093
1109
  }
1110
+ const neighbors = new Map(nodeIds.map((id) => [id, []]));
1111
+ for (const edge of edges) {
1112
+ if (edge.kind === "response" || edge.selfLoop || !neighbors.has(edge.from) || !neighbors.has(edge.to)) continue;
1113
+ neighbors.get(edge.from).push(edge.to);
1114
+ neighbors.get(edge.to).push(edge.from);
1115
+ }
1116
+ for (let sweep = 0; sweep < 4; sweep++) {
1117
+ let changed = false;
1118
+ const order = sweep % 2 === 0 ? [...sortedRanks].reverse() : sortedRanks;
1119
+ for (const rank of order) {
1120
+ const ids = byRank.get(rank);
1121
+ for (let index = 0; index < ids.length - 1; index++) {
1122
+ const left = ids[index];
1123
+ const right = ids[index + 1];
1124
+ if (nodeGroupIndex.get(left) !== nodeGroupIndex.get(right)) continue;
1125
+ let improvement = 0;
1126
+ for (const leftNeighbor of neighbors.get(left)) {
1127
+ for (const rightNeighbor of neighbors.get(right)) {
1128
+ const neighborRank = ranks.get(leftNeighbor);
1129
+ if (neighborRank === rank || neighborRank !== ranks.get(rightNeighbor)) continue;
1130
+ improvement += Math.sign(rankOrder.get(leftNeighbor) - rankOrder.get(rightNeighbor));
1131
+ }
1132
+ }
1133
+ if (improvement <= 0) continue;
1134
+ ids[index] = right;
1135
+ ids[index + 1] = left;
1136
+ rankOrder.set(right, index);
1137
+ rankOrder.set(left, index + 1);
1138
+ changed = true;
1139
+ }
1140
+ }
1141
+ if (!changed) break;
1142
+ }
1094
1143
  const isVertical = direction === "TB" || direction === "BT";
1095
1144
  const hasTitle = Boolean(ast.meta.title && ast.meta.title.trim().length > 0);
1096
1145
  const hasBeatCaptions = ast.beats.some((b) => b.label && b.label.trim().length > 0);
@@ -1225,6 +1274,7 @@ function layoutRanked(ast, edges, opts) {
1225
1274
  function layoutTree(ast, edges) {
1226
1275
  const nodeIds = Object.keys(ast.nodes);
1227
1276
  if (nodeIds.length === 0) return [];
1277
+ edges = cycleSafeEdges(nodeIds, edges);
1228
1278
  const nodeDims = /* @__PURE__ */ new Map();
1229
1279
  for (const id of nodeIds) {
1230
1280
  nodeDims.set(id, computeNodeDimensions(ast.nodes[id]));
@@ -1395,19 +1445,21 @@ function cycleSafeEdges(nodeIds, edges) {
1395
1445
  }
1396
1446
  const visited = /* @__PURE__ */ new Set();
1397
1447
  const safe = [];
1398
- const visit = (id) => {
1399
- if (visited.has(id)) return;
1400
- visited.add(id);
1401
- for (const edge of outgoing.get(id) ?? []) {
1402
- if (visited.has(edge.to)) continue;
1403
- safe.push(edge);
1404
- visit(edge.to);
1448
+ const roots = nodeIds.filter((id) => !incoming.has(id));
1449
+ for (const root of [...roots, ...nodeIds]) {
1450
+ if (visited.has(root)) continue;
1451
+ const stack = [{ id: root }];
1452
+ while (stack.length > 0) {
1453
+ const current = stack.pop();
1454
+ if (visited.has(current.id)) continue;
1455
+ visited.add(current.id);
1456
+ if (current.edge) safe.push(current.edge);
1457
+ const children = outgoing.get(current.id);
1458
+ for (let index = children.length - 1; index >= 0; index--) {
1459
+ stack.push({ id: children[index].to, edge: children[index] });
1460
+ }
1405
1461
  }
1406
- };
1407
- for (const id of nodeIds) {
1408
- if (!incoming.has(id)) visit(id);
1409
1462
  }
1410
- for (const id of nodeIds) visit(id);
1411
1463
  return safe;
1412
1464
  }
1413
1465
  function layoutConstellation(ast) {
@@ -2549,7 +2601,8 @@ function computeAdaptiveDimensions(ast, edges) {
2549
2601
  const children = /* @__PURE__ */ new Map();
2550
2602
  const hasParent = /* @__PURE__ */ new Set();
2551
2603
  for (const id of nodeIds) children.set(id, []);
2552
- const treeEdges = effectiveEdges.some((e) => e.structural) ? effectiveEdges.filter((e) => e.structural) : effectiveEdges;
2604
+ const treeCandidates = effectiveEdges.some((e) => e.structural) ? effectiveEdges.filter((e) => e.structural) : effectiveEdges;
2605
+ const treeEdges = cycleSafeEdges(nodeIds, treeCandidates);
2553
2606
  for (const e of treeEdges) {
2554
2607
  if (ast.nodes[e.from] && ast.nodes[e.to] && e.from !== e.to) {
2555
2608
  children.get(e.from)?.push(e.to);
@@ -5257,6 +5310,97 @@ function buildSmoothSvgPath(start, waypoints, end, cornerRadius = 0) {
5257
5310
  d += ` L ${end.x} ${end.y}`;
5258
5311
  return d;
5259
5312
  }
5313
+ function routeAroundObstacles(sourceBox, targetBox, points, options) {
5314
+ if (!options.obstacles?.length) return points;
5315
+ const margin = options.margin ?? 20;
5316
+ const obstacles = [sourceBox, targetBox, ...options.obstacles];
5317
+ const intersections = (route) => {
5318
+ let hits = 0;
5319
+ for (let index = 1; index < route.length; index++) {
5320
+ const start2 = route[index - 1];
5321
+ const end2 = route[index];
5322
+ if (start2.x === end2.x && start2.y === end2.y) continue;
5323
+ for (const box of obstacles) {
5324
+ if (start2.y === end2.y && start2.y > box.y && start2.y < box.y + box.height && Math.max(start2.x, end2.x) > box.x && Math.min(start2.x, end2.x) < box.x + box.width) hits++;
5325
+ else if (start2.x === end2.x && start2.x > box.x && start2.x < box.x + box.width && Math.max(start2.y, end2.y) > box.y && Math.min(start2.y, end2.y) < box.y + box.height) hits++;
5326
+ }
5327
+ }
5328
+ return hits;
5329
+ };
5330
+ if (intersections(points) === 0) return points;
5331
+ const start = points[0];
5332
+ const end = points[points.length - 1];
5333
+ const stub = (point, port) => {
5334
+ let distance = margin;
5335
+ for (const box of obstacles) {
5336
+ let clearance = Number.POSITIVE_INFINITY;
5337
+ if (point.y > box.y && point.y < box.y + box.height) {
5338
+ if (port === "right") clearance = box.x - point.x;
5339
+ if (port === "left") clearance = point.x - box.x - box.width;
5340
+ }
5341
+ if (point.x > box.x && point.x < box.x + box.width) {
5342
+ if (port === "bottom") clearance = box.y - point.y;
5343
+ if (port === "top") clearance = point.y - box.y - box.height;
5344
+ }
5345
+ if (clearance >= 0) distance = Math.min(distance, clearance / 2);
5346
+ }
5347
+ return {
5348
+ x: point.x + (port === "right" ? distance : port === "left" ? -distance : 0),
5349
+ y: point.y + (port === "bottom" ? distance : port === "top" ? -distance : 0)
5350
+ };
5351
+ };
5352
+ const sourceStub = stub(start, options.sourcePort);
5353
+ const targetStub = stub(end, options.targetPort);
5354
+ const corridorsX = /* @__PURE__ */ new Set();
5355
+ const corridorsY = /* @__PURE__ */ new Set();
5356
+ for (const box of obstacles) {
5357
+ corridorsX.add(box.x - margin);
5358
+ corridorsX.add(box.x + box.width + margin);
5359
+ corridorsY.add(box.y - margin);
5360
+ corridorsY.add(box.y + box.height + margin);
5361
+ }
5362
+ const candidates = [
5363
+ points,
5364
+ [start, sourceStub, { x: sourceStub.x, y: targetStub.y }, targetStub, end],
5365
+ [start, sourceStub, { x: targetStub.x, y: sourceStub.y }, targetStub, end]
5366
+ ];
5367
+ for (const corridor of corridorsX) {
5368
+ candidates.push([
5369
+ start,
5370
+ sourceStub,
5371
+ { x: corridor, y: sourceStub.y },
5372
+ { x: corridor, y: targetStub.y },
5373
+ targetStub,
5374
+ end
5375
+ ]);
5376
+ }
5377
+ for (const corridor of corridorsY) {
5378
+ candidates.push([
5379
+ start,
5380
+ sourceStub,
5381
+ { x: sourceStub.x, y: corridor },
5382
+ { x: targetStub.x, y: corridor },
5383
+ targetStub,
5384
+ end
5385
+ ]);
5386
+ }
5387
+ let best = points;
5388
+ let bestHits = Number.POSITIVE_INFINITY;
5389
+ let bestLength = Number.POSITIVE_INFINITY;
5390
+ for (const candidate of candidates) {
5391
+ const hits = intersections(candidate);
5392
+ let length = 0;
5393
+ for (let index = 1; index < candidate.length; index++) {
5394
+ length += Math.abs(candidate[index].x - candidate[index - 1].x) + Math.abs(candidate[index].y - candidate[index - 1].y);
5395
+ }
5396
+ if (hits < bestHits || hits === bestHits && length < bestLength) {
5397
+ best = candidate;
5398
+ bestHits = hits;
5399
+ bestLength = length;
5400
+ }
5401
+ }
5402
+ return best.filter((point, index) => index === 0 || point.x !== best[index - 1].x || point.y !== best[index - 1].y);
5403
+ }
5260
5404
  function routeOrthogonalEdge(sourceBox, targetBox, options = {}) {
5261
5405
  const isSelfLoop = sourceBox === targetBox || sourceBox.x === targetBox.x && sourceBox.y === targetBox.y && sourceBox.width === targetBox.width && sourceBox.height === targetBox.height;
5262
5406
  const MARGIN = options.margin ?? 20;
@@ -5299,6 +5443,11 @@ function routeOrthogonalEdge(sourceBox, targetBox, options = {}) {
5299
5443
  end2 = { x: rightX, y: endY };
5300
5444
  waypoints2 = [{ x: apexX, y: startY }, { x: apexX, y: endY }];
5301
5445
  }
5446
+ waypoints2 = routeAroundObstacles(sourceBox, targetBox, [start2, ...waypoints2, end2], {
5447
+ ...options,
5448
+ sourcePort: port,
5449
+ targetPort: port
5450
+ }).slice(1, -1);
5302
5451
  const svgPathData2 = buildSmoothSvgPath(start2, waypoints2, end2, options.cornerRadius ?? 6);
5303
5452
  return {
5304
5453
  sourcePort: port,
@@ -5381,13 +5530,18 @@ function routeOrthogonalEdge(sourceBox, targetBox, options = {}) {
5381
5530
  }
5382
5531
  waypoints.push(p2);
5383
5532
  }
5384
- const svgPathData = buildSmoothSvgPath(start, waypoints, end, options.cornerRadius ?? 6);
5533
+ const routedWaypoints = routeAroundObstacles(sourceBox, targetBox, [start, ...waypoints, end], {
5534
+ ...options,
5535
+ sourcePort,
5536
+ targetPort
5537
+ }).slice(1, -1);
5538
+ const svgPathData = buildSmoothSvgPath(start, routedWaypoints, end, options.cornerRadius ?? 6);
5385
5539
  return {
5386
5540
  sourcePort,
5387
5541
  targetPort,
5388
5542
  startPoint: start,
5389
5543
  endPoint: end,
5390
- waypoints,
5544
+ waypoints: routedWaypoints,
5391
5545
  svgPathData
5392
5546
  };
5393
5547
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markdy/core",
3
- "version": "1.4.1",
3
+ "version": "1.4.3",
4
4
  "description": "Diagram-native MarkdyScript parser and compiler (auto-layout, edge routing, cue scheduling) — zero runtime dependencies.",
5
5
  "license": "MIT",
6
6
  "type": "module",