@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/README.md +198 -10
- package/dist/index.js +107 -30
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +107 -30
- package/dist/index.mjs.map +1 -1
- package/dist/proposals.d.ts.map +1 -1
- package/package.json +1 -1
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
|
|
1505
|
-
if (!
|
|
1506
|
-
const
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
const
|
|
1511
|
-
const
|
|
1512
|
-
const
|
|
1513
|
-
const
|
|
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({
|
|
1523
|
+
arr.push({ weight, isFlow, flowSide, perpSide, orderCoord: nbrOrderC, rankCoord: nbrRankC, beyond });
|
|
1517
1524
|
votesByHandle.set(handleId, arr);
|
|
1518
1525
|
}
|
|
1519
|
-
const
|
|
1520
|
-
const
|
|
1521
|
-
|
|
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
|
-
|
|
1525
|
-
const
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
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 (
|
|
1608
|
+
if (!changed) return null;
|
|
1533
1609
|
const proposed = { ...node, handles: newHandles };
|
|
1534
|
-
const
|
|
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
|
|
1617
|
+
changes: sideChanges,
|
|
1618
|
+
reason
|
|
1542
1619
|
};
|
|
1543
1620
|
}
|
|
1544
1621
|
function rotateHandles(handles, rot) {
|