Refine group placement seeding to reduce collapse and detour
Some checks are pending
CI / test (push) Waiting to run

This commit is contained in:
Rbanh 2026-02-20 00:11:01 -05:00
parent f0b952e3a2
commit fc5f2fee41
8 changed files with 124 additions and 10 deletions

View File

@ -1279,6 +1279,127 @@ function applyAutoRotation(model, placedMap, options = {}) {
}
}
function groupLaneIndex(group, laneProfiles) {
const counts = new Map();
for (const ref of group.members ?? []) {
const lane = laneProfiles.get(ref)?.laneIndex ?? 2;
counts.set(lane, (counts.get(lane) ?? 0) + 1);
}
const sorted = [...counts.entries()].sort((a, b) => {
if (a[1] !== b[1]) {
return b[1] - a[1];
}
return a[0] - b[0];
});
return sorted[0]?.[0] ?? 2;
}
function estimateGroupFootprint(group, rank, degree) {
const refs = [...(group.members ?? [])].sort((a, b) => {
const ra = rank.get(a) ?? 1;
const rb = rank.get(b) ?? 1;
if (ra !== rb) {
return ra - rb;
}
const da = degree.get(a) ?? 0;
const db = degree.get(b) ?? 0;
if (da !== db) {
return db - da;
}
return a.localeCompare(b);
});
const cols = rankColumnsForRefs(refs, rank);
const colCount = Math.max(1, cols.size);
const maxRowsInCol = Math.max(1, ...[...cols.values()].map((list) => list.length));
const width = toGrid(240 + colCount * 220);
const height = toGrid(180 + maxRowsInCol * 120);
return { width, height };
}
function groupRectsOverlap(a, b, pad = 80) {
return !(
a.x + a.w + pad <= b.x ||
b.x + b.w + pad <= a.x ||
a.y + a.h + pad <= b.y ||
b.y + b.h + pad <= a.y
);
}
function computeGroupOrigins(groups, rank, laneProfiles, degree) {
if (!groups.length) {
return new Map();
}
const globalMinRank = Math.min(
...groups.flatMap((group) => (group.members ?? []).map((ref) => rank.get(ref) ?? 1)),
0
);
const strideX = 420;
const laneStrideY = 120;
const origins = new Map();
const usedRects = [];
const planned = groups
.map((group, idx) => {
const refs = group.members ?? [];
const minRank = Math.min(...refs.map((ref) => rank.get(ref) ?? 1), globalMinRank);
const lane = groupLaneIndex(group, laneProfiles);
const laneBand = Math.max(0, Math.min(3, lane));
const footprint = estimateGroupFootprint(group, rank, degree);
return {
group,
idx,
minRank,
lane,
laneBand,
footprint,
targetX: toGrid(MARGIN_X + Math.max(0, minRank - globalMinRank) * strideX),
targetY: toGrid(MARGIN_Y + laneBand * laneStrideY)
};
})
.sort((a, b) => {
if (a.targetX !== b.targetX) {
return a.targetX - b.targetX;
}
if (a.targetY !== b.targetY) {
return a.targetY - b.targetY;
}
return a.group.name.localeCompare(b.group.name);
});
const targetColumnSlots = new Map();
for (const item of planned) {
const slotKey = `${item.targetX}`;
const slot = targetColumnSlots.get(slotKey) ?? 0;
targetColumnSlots.set(slotKey, slot + 1);
const fanoutStep = planned.length <= 3 ? Math.floor(strideX * 0.78) : Math.floor(strideX * 0.42);
const fanoutX = toGrid(slot * fanoutStep);
let x = toGrid(item.targetX + fanoutX);
let y = item.targetY;
let tries = 0;
while (tries < 28) {
const rect = { x, y, w: item.footprint.width, h: item.footprint.height };
const collides = usedRects.some((other) => groupRectsOverlap(rect, other));
if (!collides) {
usedRects.push(rect);
origins.set(item.group.name, { x, y });
break;
}
y = toGrid(y + laneStrideY);
if (tries > 0 && tries % 6 === 0) {
x = toGrid(x + Math.floor(strideX * 0.5));
y = item.targetY;
}
tries += 1;
}
if (!origins.has(item.group.name)) {
origins.set(item.group.name, { x: item.targetX, y: toGrid(item.targetY + laneStrideY * 2) });
}
}
return origins;
}
function placeInstances(model, options = {}) {
const respectLocks = options.respectLocks ?? true;
const instances = [...model.instances].sort((a, b) => a.ref.localeCompare(b.ref));
@ -1290,18 +1411,11 @@ function placeInstances(model, options = {}) {
const placed = [];
const placedMap = new Map();
const groupsPerRow = groups.length <= 3 ? groups.length : groups.length >= 6 ? 3 : 2;
const groupCellW = 620;
const groupCellH = 420;
const groupOrigins = computeGroupOrigins(groups, rank, laneProfiles, degree);
for (let i = 0; i < groups.length; i += 1) {
const group = groups[i];
const row = groupsPerRow ? Math.floor(i / groupsPerRow) : 0;
const col = groupsPerRow ? i % groupsPerRow : 0;
const origin = {
x: toGrid(MARGIN_X + col * groupCellW),
y: toGrid(MARGIN_Y + row * groupCellH)
};
const origin = groupOrigins.get(group.name) ?? { x: toGrid(MARGIN_X), y: toGrid(MARGIN_Y + i * 220) };
const out = placeGroup(model, group, origin, {
rank,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 244 KiB

After

Width:  |  Height:  |  Size: 235 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 197 KiB

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 208 KiB

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 278 KiB

After

Width:  |  Height:  |  Size: 267 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 KiB

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 KiB

After

Width:  |  Height:  |  Size: 192 KiB

View File

@ -15,5 +15,5 @@ test("render output for reference fixture remains deterministic", () => {
assert.equal(outA.ok, true);
assert.equal(outB.ok, true);
assert.equal(outA.svg, outB.svg);
assert.equal(svgHash(outA.svg), "80ab4c279caf29b2a14096346d6e993ef677f41bf1b3bc226eefa7b069b6487d");
assert.equal(svgHash(outA.svg), "a793f82594e4aff3e898db85cd7e984e86ad568d58aecd33661e8bbb1eff1856");
});