@nodius/layouting 0.1.3 → 0.1.4

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.mjs CHANGED
@@ -1473,17 +1473,23 @@ function applyRelocateProposals(input, preview, options) {
1473
1473
  const previewIndex = new Map(preview.nodes.map((n) => [n.id, n]));
1474
1474
  const inputIndex = new Map(input.nodes.map((n) => [n.id, n]));
1475
1475
  const newNodes = input.nodes.map((node) => {
1476
- const proposal = computeRelocateProposal(node, input.edges, inputIndex, previewIndex);
1476
+ const proposal = computeRelocateProposal(node, input.edges, inputIndex, previewIndex, options.direction);
1477
1477
  if (!proposal) return node;
1478
1478
  const accepted = options.onProposal(proposal);
1479
1479
  return accepted ?? node;
1480
1480
  });
1481
1481
  return { nodes: newNodes, edges: input.edges };
1482
1482
  }
1483
- function computeRelocateProposal(node, edges, inputIndex, previewIndex) {
1483
+ function computeRelocateProposal(node, edges, inputIndex, previewIndex, direction) {
1484
1484
  const selfPreview = previewIndex.get(node.id);
1485
1485
  if (!selfPreview) return null;
1486
1486
  if (node.handles.length === 0) return null;
1487
+ const isHorizontal = direction === "LR" || direction === "RL";
1488
+ const rankMin = isHorizontal ? selfPreview.x : selfPreview.y;
1489
+ const rankMax = isHorizontal ? selfPreview.x + selfPreview.width : selfPreview.y + selfPreview.height;
1490
+ const orderMin = isHorizontal ? selfPreview.y : selfPreview.x;
1491
+ const orderMax = isHorizontal ? selfPreview.y + selfPreview.height : selfPreview.x + selfPreview.width;
1492
+ const orderCenter = (orderMin + orderMax) / 2;
1487
1493
  const usedHandleIds = /* @__PURE__ */ new Set();
1488
1494
  for (const e of edges) {
1489
1495
  if (e.from === node.id) usedHandleIds.add(e.fromHandle);
@@ -1491,54 +1497,125 @@ function computeRelocateProposal(node, edges, inputIndex, previewIndex) {
1491
1497
  }
1492
1498
  if (usedHandleIds.size === 0) return null;
1493
1499
  const votesByHandle = /* @__PURE__ */ new Map();
1494
- const selfCenter = {
1495
- x: selfPreview.x + selfPreview.width / 2,
1496
- y: selfPreview.y + selfPreview.height / 2
1497
- };
1498
1500
  for (const e of edges) {
1499
1501
  const isFrom = e.from === node.id;
1500
1502
  const isTo = e.to === node.id;
1501
1503
  if (!isFrom && !isTo) continue;
1502
1504
  const handleId = isFrom ? e.fromHandle : e.toHandle;
1503
1505
  const neighborId = isFrom ? e.to : e.from;
1504
- const neighborPreview = previewIndex.get(neighborId);
1505
- if (!neighborPreview) continue;
1506
- const neighborCenter = {
1507
- x: neighborPreview.x + neighborPreview.width / 2,
1508
- y: neighborPreview.y + neighborPreview.height / 2
1509
- };
1510
- const dx = neighborCenter.x - selfCenter.x;
1511
- const dy = neighborCenter.y - selfCenter.y;
1512
- const side = Math.abs(dx) >= Math.abs(dy) ? dx >= 0 ? "right" : "left" : dy >= 0 ? "bottom" : "top";
1513
- const dist = Math.hypot(dx, dy);
1506
+ const np = previewIndex.get(neighborId);
1507
+ if (!np) continue;
1508
+ const nbrRankMin = isHorizontal ? np.x : np.y;
1509
+ const nbrRankMax = isHorizontal ? np.x + np.width : np.y + np.height;
1510
+ const overlap = Math.min(rankMax, nbrRankMax) - Math.max(rankMin, nbrRankMin);
1511
+ const isFlow = overlap <= 0;
1512
+ const nbrOrderC = isHorizontal ? np.y + np.height / 2 : np.x + np.width / 2;
1513
+ const nbrRankC = isHorizontal ? np.x + np.width / 2 : np.y + np.height / 2;
1514
+ const selfRankC = isHorizontal ? selfPreview.x + selfPreview.width / 2 : selfPreview.y + selfPreview.height / 2;
1515
+ const nbrIsBefore = nbrRankMax <= rankMin || overlap <= 0 && nbrRankC < selfRankC;
1516
+ const flowSide = isHorizontal ? nbrIsBefore ? "left" : "right" : nbrIsBefore ? "top" : "bottom";
1517
+ const d = nbrOrderC - orderCenter;
1518
+ const perpSide = isHorizontal ? d >= 0 ? "bottom" : "top" : d >= 0 ? "right" : "left";
1519
+ const beyond = nbrOrderC > orderMax ? 1 : nbrOrderC < orderMin ? -1 : 0;
1520
+ const dist = Math.hypot(nbrRankC - selfRankC, nbrOrderC - orderCenter);
1514
1521
  const weight = dist > 0 ? 1 / dist : 1;
1515
1522
  const arr = votesByHandle.get(handleId) ?? [];
1516
- arr.push({ side, weight });
1523
+ arr.push({ weight, isFlow, flowSide, perpSide, orderCoord: nbrOrderC, rankCoord: nbrRankC, beyond });
1517
1524
  votesByHandle.set(handleId, arr);
1518
1525
  }
1519
- const changes = {};
1520
- const newHandles = node.handles.map((h) => {
1521
- const votes = votesByHandle.get(h.id);
1522
- if (!votes || votes.length === 0) return h;
1526
+ const resolved = /* @__PURE__ */ new Map();
1527
+ for (const [handleId, votes] of votesByHandle) {
1528
+ if (votes.length === 0) continue;
1523
1529
  const tallies = { top: 0, right: 0, bottom: 0, left: 0 };
1524
- for (const v of votes) tallies[v.side] += v.weight;
1525
- const best = Object.entries(tallies).reduce((acc, cur) => cur[1] > acc[1] ? cur : acc, ["top", -Infinity])[0];
1526
- if (best !== h.position) {
1527
- changes[h.id] = { from: h.position, to: best };
1528
- return { ...h, position: best };
1530
+ let dom = votes[0];
1531
+ for (const v of votes) {
1532
+ tallies[v.isFlow ? v.flowSide : v.perpSide] += v.weight;
1533
+ if (v.weight > dom.weight) dom = v;
1534
+ }
1535
+ const side = Object.entries(tallies).reduce((acc, cur) => cur[1] > acc[1] ? cur : acc, ["top", -Infinity])[0];
1536
+ resolved.set(handleId, {
1537
+ side,
1538
+ isFlow: dom.isFlow,
1539
+ flowSide: dom.flowSide,
1540
+ perpSide: dom.perpSide,
1541
+ orderCoord: dom.orderCoord,
1542
+ rankCoord: dom.rankCoord,
1543
+ beyond: dom.beyond
1544
+ });
1545
+ }
1546
+ for (const flowSide of ["top", "bottom", "left", "right"]) {
1547
+ const group = [...resolved.entries()].filter(([, r]) => r.isFlow && r.side === flowSide);
1548
+ if (group.length < 2) continue;
1549
+ let hasLeft = false, hasRight = false;
1550
+ for (const [, r] of group) {
1551
+ if (r.orderCoord < orderCenter) hasLeft = true;
1552
+ else if (r.orderCoord > orderCenter) hasRight = true;
1553
+ }
1554
+ if (hasLeft && hasRight) continue;
1555
+ const distOf = (oc) => oc > orderMax ? oc - orderMax : orderMin - oc;
1556
+ const distinctCoords = [...new Set(group.filter(([, r]) => r.beyond !== 0).map(([, r]) => r.orderCoord))].sort((a, b) => distOf(a) - distOf(b));
1557
+ if (distinctCoords.length >= 2) {
1558
+ const keep = distinctCoords[0];
1559
+ for (const [, r] of group) {
1560
+ if (r.beyond !== 0 && r.orderCoord !== keep) r.side = r.perpSide;
1561
+ }
1562
+ }
1563
+ }
1564
+ const sideChanges = {};
1565
+ const newSideById = /* @__PURE__ */ new Map();
1566
+ for (const h of node.handles) {
1567
+ const r = resolved.get(h.id);
1568
+ if (!r) continue;
1569
+ newSideById.set(h.id, r.side);
1570
+ if (r.side !== h.position) sideChanges[h.id] = { from: h.position, to: r.side };
1571
+ }
1572
+ const newOffsetById = /* @__PURE__ */ new Map();
1573
+ const bySide = /* @__PURE__ */ new Map();
1574
+ for (const [hid, r] of resolved) {
1575
+ const arr = bySide.get(r.side) ?? [];
1576
+ arr.push(hid);
1577
+ bySide.set(r.side, arr);
1578
+ }
1579
+ for (const [side, handleIds] of bySide) {
1580
+ if (handleIds.length < 2) continue;
1581
+ const offsets = handleIds.map((hid) => node.handles.find((h) => h.id === hid).offset ?? 0.5).sort((a, b) => a - b);
1582
+ const horizontalSide = side === "top" || side === "bottom";
1583
+ const sortKey = (hid) => {
1584
+ const r = resolved.get(hid);
1585
+ if (isHorizontal) {
1586
+ return horizontalSide ? r.rankCoord : r.orderCoord;
1587
+ }
1588
+ return horizontalSide ? r.orderCoord : r.rankCoord;
1589
+ };
1590
+ const sortedHandles = [...handleIds].sort((a, b) => {
1591
+ const ra = sortKey(a), rb = sortKey(b);
1592
+ return ra - rb || (a < b ? -1 : a > b ? 1 : 0);
1593
+ });
1594
+ for (let i = 0; i < sortedHandles.length; i++) {
1595
+ newOffsetById.set(sortedHandles[i], offsets[i]);
1596
+ }
1597
+ }
1598
+ let changed = false;
1599
+ const newHandles = node.handles.map((h) => {
1600
+ const side = newSideById.get(h.id) ?? h.position;
1601
+ const offset = newOffsetById.has(h.id) ? newOffsetById.get(h.id) : h.offset ?? 0.5;
1602
+ if (side !== h.position || offset !== (h.offset ?? 0.5)) {
1603
+ changed = true;
1604
+ return { ...h, position: side, offset };
1529
1605
  }
1530
1606
  return h;
1531
1607
  });
1532
- if (Object.keys(changes).length === 0) return null;
1608
+ if (!changed) return null;
1533
1609
  const proposed = { ...node, handles: newHandles };
1534
- const summary = Object.entries(changes).map(([id, c]) => `${id}: ${c.from}\u2192${c.to}`).join(", ");
1610
+ const sideSummary = Object.entries(sideChanges).map(([id, c]) => `${id}: ${c.from}\u2192${c.to}`).join(", ");
1611
+ const reason = sideSummary ? `${Object.keys(sideChanges).length} handle(s) repositioned toward their neighbor (${sideSummary})` : `handles on a shared side reordered to match neighbor order`;
1535
1612
  return {
1536
1613
  type: "relocate-handles",
1537
1614
  nodeId: node.id,
1538
1615
  current: node,
1539
1616
  proposed,
1540
- changes,
1541
- reason: `${Object.keys(changes).length} handle(s) point away from their neighbor (${summary})`
1617
+ changes: sideChanges,
1618
+ reason
1542
1619
  };
1543
1620
  }
1544
1621
  function rotateHandles(handles, rot) {