diff --git a/vis-plugins/node-table-visualizer.js b/vis-plugins/node-table-visualizer.js index 17fb4b0..d5b1ea6 100644 --- a/vis-plugins/node-table-visualizer.js +++ b/vis-plugins/node-table-visualizer.js @@ -124,6 +124,62 @@ module.exports = function (register, lib) { return positions; } + // ---- hard rectangle-overlap resolution ---- + // Repulsion/gravity alone only balance to *some* equilibrium distance; + // nothing about that equilibrium accounts for box size, and node boxes + // vary a lot in height (one row vs. MAX_VISIBLE_ROWS). So two boxes can + // settle closer than their combined half-heights and visibly overlap. + // This runs every simulation step and directly separates any pair of + // visible boxes whose padded bounding rects intersect, along whichever + // axis needs the smaller push -- a standard AABB de-overlap pass on top + // of the physics, not a replacement for it. Returns the total remaining + // overlap so callers can tell the simulation hasn't truly settled even + // if velocities alone look calm. + function resolveCollisions(nodeList, isVisible) { + var totalOverlap = 0; + for (var i = 0; i < nodeList.length; i++) { + for (var j = i + 1; j < nodeList.length; j++) { + var a = nodeList[i]; + var b = nodeList[j]; + if (!isVisible(a.id) || !isVisible(b.id)) continue; + if (a.fixed && b.fixed) continue; + + var minDX = NODE_WIDTH + H_GAP; + var minDY = (a.height + b.height) / 2 + V_GAP; + var dx = b.x - a.x; + var dy = b.y - a.y; + var overlapX = minDX - Math.abs(dx); + var overlapY = minDY - Math.abs(dy); + if (overlapX <= 0 || overlapY <= 0) continue; + + totalOverlap += Math.min(overlapX, overlapY); + + var pushX = 0, pushY = 0; + if (dx === 0 && dy === 0) { + pushX = minDX; // exactly coincident: nudge apart deterministically + } else if (overlapX < overlapY) { + pushX = (dx < 0 ? -1 : 1) * overlapX; + } else { + pushY = (dy < 0 ? -1 : 1) * overlapY; + } + + if (a.fixed) { + b.x += pushX; + b.y += pushY; + } else if (b.fixed) { + a.x -= pushX; + a.y -= pushY; + } else { + a.x -= pushX / 2; + a.y -= pushY / 2; + b.x += pushX / 2; + b.y += pushY / 2; + } + } + } + return totalOverlap; + } + // ---- simple O(n^2) force-directed layout, vis-network-style ---- // isVisible(id) lets collapsed-subtree nodes sit out of the simulation // entirely (frozen, not just hidden) so they don't invisibly shove @@ -200,7 +256,12 @@ module.exports = function (register, lib) { n.y += n.vy; kinetic += n.vx * n.vx + n.vy * n.vy; }); - return kinetic; + + // Folded into the same "settled" signal as velocity: as long as + // boxes still overlap, the simulation isn't done, even if forces + // alone have gone quiet. + var overlap = resolveCollisions(nodeList, isVisible); + return kinetic + overlap; } return { nodes: nodeList, byId: byId, step: step };