mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-10 01:09:43 +02:00
fix(tabs): wrap case clusters box by box and never route a lineage line through a tab
With tabArrangement 'case' at 1440px (nine tabs in four cases), the lineage routes ran horizontally through the middle of tabs, cluster labels and box borders. #538's clusters are `max-width: 100%` boxes that may shrink, so in the one-line strip every box squeezed and wrapped inside itself (a one-tab case's swatch alone on a line above its tab). The strip then never overflowed, so updateTabOverflowMode() never wrapped it, and #544's routing room (`.lineage-tree.tabs-auto-wrap`: row gap and spine channel) never applied. computeLineageRows grouped the squeezed tabs by top alone into rows that overlap (10-40, 20-50, 42-74), and gapUnder put the first row's gap at y 30, inside the tabs below it. - On the desktop header strip a cluster keeps its width, so clusters that do not fit overflow and the strip wraps box by box; the boxes' own gaps read --lineage-row-gap, so the routes run between box rows and through the spine channel. A case wider than the whole strip still wraps inside its box, and now wraps the strip too (`_tabClustersWrapInside`, the new `innerWrap` input of shouldAutoWrapTabs), so its inner rows get the gap. - computeLineageRows never returns overlapping rows: tabs whose spans overlap are one row. gapUnder returns null when there is no real gap, and a route with no gap is not drawn. A final pass drops any route with a segment inside a tab, so no arrangement can draw a line through a tab (a layout without the room loses lines instead). Pinned with the squeezed rects measured live. At 1920px, where the clusters fit on one line, nothing changes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -380,6 +380,87 @@ describe('lineage tree geometry: header strip', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('lineage tree geometry: never through a tab, in any arrangement', () => {
|
||||
// Measured live (1440x900, tabArrangement 'case', nine tabs in four cases) before
|
||||
// the case boxes kept their width: every box squeezed and wrapped inside itself,
|
||||
// so tabs sat at tops 10, 20, 42 and 43 with spans that overlap. The old rows
|
||||
// (grouped by top alone) put the "gap" under the first row at y 30, inside the
|
||||
// tabs, and the routes ran through them.
|
||||
const strip: Rect = { left: 94, top: 6, width: 980, height: 85 };
|
||||
const squeezed: Record<string, Rect> = {
|
||||
'w1-webshop': { left: 201, top: 10, width: 99, height: 30 },
|
||||
'w2-webshop': { left: 109, top: 42, width: 101, height: 30 },
|
||||
'w3-webshop': { left: 212, top: 42, width: 101, height: 30 },
|
||||
'w1-api-gateway': { left: 518, top: 10, width: 99, height: 30 },
|
||||
'w2-api-gateway': { left: 407, top: 42, width: 143, height: 32 },
|
||||
'w3-api-gateway': { left: 552, top: 43, width: 101, height: 30 },
|
||||
'w1-notes': { left: 744, top: 20, width: 136, height: 30 },
|
||||
'w1-docs-site': { left: 968, top: 10, width: 99, height: 30 },
|
||||
'w2-docs-site': { left: 870, top: 42, width: 101, height: 30 },
|
||||
};
|
||||
const all = Object.values(squeezed);
|
||||
|
||||
it('makes overlapping spans one row, so no gap is ever inside a tab', () => {
|
||||
const helper = loadLineageHelper();
|
||||
// 10-40 and 20-50 overlap, and 42-74 overlaps 20-50: one row, no gap inside.
|
||||
expect(helper.computeRows(all)).toEqual([{ top: 10, bottom: 74 }]);
|
||||
// Rows that only share a top (the active tab is taller) stay one row, and rows
|
||||
// with a real gap between them stay apart.
|
||||
expect(helper.computeRows([tab(0, 4), tab(130, 4, 120, 32), tab(0, 46)])).toEqual([
|
||||
{ top: 4, bottom: 36 },
|
||||
{ top: 46, bottom: 76 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('draws no segment inside a tab, whichever tab is the parent or the child', () => {
|
||||
const helper = loadLineageHelper();
|
||||
const names = Object.keys(squeezed);
|
||||
let drawn = 0;
|
||||
for (const parentName of names) {
|
||||
for (const lane of [0, 1, 2]) {
|
||||
const children = names.filter((n) => n !== parentName).map((n) => ({ id: n, rect: squeezed[n] }));
|
||||
const geom = helper.computeTree({
|
||||
parent: squeezed[parentName],
|
||||
children,
|
||||
strip,
|
||||
tabs: all,
|
||||
lane,
|
||||
laneCount: 3,
|
||||
});
|
||||
for (const route of geom?.routes ?? []) {
|
||||
drawn++;
|
||||
for (let i = 1; i < route.points.length; i++) {
|
||||
for (const r of all) {
|
||||
expect(crossesRect(route.points[i - 1], route.points[i], r), `${parentName} -> ${route.id}`).toBe(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Some routes still have a clean way (a tab with nothing under it), so the
|
||||
// check above is not vacuous.
|
||||
expect(drawn).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('draws nothing into rows with no gap between them', () => {
|
||||
const helper = loadLineageHelper();
|
||||
// Two rows touching (no row gap at all): the only place a route could run is
|
||||
// through the tabs of the other row.
|
||||
const upper = [tab(0, 4), tab(130, 4)];
|
||||
const lower = [tab(0, 34), tab(130, 34)];
|
||||
const geom = helper.computeTree({
|
||||
parent: upper[0],
|
||||
children: [
|
||||
{ id: 'below', rect: lower[1] },
|
||||
{ id: 'beside', rect: upper[1] },
|
||||
],
|
||||
strip: { left: 0, top: 0, width: 600, height: 80 },
|
||||
tabs: [...upper, ...lower],
|
||||
})!;
|
||||
expect(geom.routes).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('lineage tree geometry: vertical rail', () => {
|
||||
const strip: Rect = { left: 100, top: 20, width: 320, height: 320 };
|
||||
const parent: Rect = { left: 132, top: 40, width: 260, height: 40 };
|
||||
|
||||
Reference in New Issue
Block a user