This commit is contained in:
FURRO404
2026-06-21 01:34:35 -07:00
parent 9465ee05d1
commit b3bcaef57a
+74 -26
View File
@@ -167,46 +167,94 @@ export function layoutKey(columnIndex, match) {
} }
// Position every match vertically so each one sits centred between the matches // Position every match vertically so each one sits centred between the matches
// that feed into it — the classic bracket look. Column 0 is stacked top-to-bottom // that feed into it — the classic bracket look. Pure: takes measured card heights,
// by card height; later columns take the mean of their feeders' centres (and are // returns absolute `top` offsets keyed by `layoutKey`.
// nudged down only as far as needed to avoid overlap). Pure: takes measured card //
// heights, returns absolute `top` offsets keyed by `layoutKey`. // Column 0 stacks top-to-bottom. Later columns centre each match on the mean of
// its feeders' centres. Matches whose feeders were all hidden byes (no anchor)
// are *extrapolated* from the nearest anchored sibling using the column's slot
// pitch — including above y=0 — rather than stacked at the top, which would shove
// the anchored matches away from their feeders and make the connectors fan and
// cross. A final per-side shift pulls any negative offsets back on-screen.
export function computeBracketLayout(columns, heights, gap = 16) { export function computeBracketLayout(columns, heights, gap = 16) {
const heightOf = (c, m) => heights.get(layoutKey(c, m)) || 92 const heightOf = (c, m) => heights.get(layoutKey(c, m)) || 92
const centers = columns.map(() => new Map()) const centers = columns.map(() => new Map())
const tops = new Map()
const columnHeights = []
columns.forEach((column, c) => { columns.forEach((column, c) => {
let childrenCenters = null const matches = column.matches
const hs = matches.map((m) => heightOf(c, m))
const pitch = (hs.reduce((a, b) => a + b, 0) / Math.max(1, hs.length)) + gap
// Anchor = mean of this match's feeders' centres (null when all were byes).
const childCenters = new Map()
if (c > 0) { if (c > 0) {
childrenCenters = new Map()
for (const child of columns[c - 1].matches) { for (const child of columns[c - 1].matches) {
const parent = feederParent(child, c - 1, columns) const parent = feederParent(child, c - 1, columns)
if (!parent) continue if (!parent) continue
const center = centers[c - 1].get(child.match_id) const center = centers[c - 1].get(child.match_id)
if (center == null) continue if (center == null) continue
if (!childrenCenters.has(parent.match_id)) childrenCenters.set(parent.match_id, []) if (!childCenters.has(parent.match_id)) childCenters.set(parent.match_id, [])
childrenCenters.get(parent.match_id).push(center) childCenters.get(parent.match_id).push(center)
}
}
const center = matches.map((m) => {
const kids = childCenters.get(m.match_id)
return kids && kids.length ? kids.reduce((a, b) => a + b, 0) / kids.length : null
})
const firstAnchor = center.findIndex((v) => v != null)
if (firstAnchor === -1) {
// No anchors (column 0, or a fully-bye column): plain stack.
let cursor = 0
matches.forEach((m, i) => {
center[i] = cursor + hs[i] / 2
cursor += hs[i] + gap
})
} else {
// Leading byes: extrapolate upward (may go negative — fixed by the shift).
for (let i = firstAnchor - 1; i >= 0; i -= 1) center[i] = center[i + 1] - pitch
// Trailing / interior byes: extend down / interpolate between anchors.
let lastAnchor = firstAnchor
for (let i = firstAnchor + 1; i < matches.length; i += 1) {
if (center[i] != null) { lastAnchor = i; continue }
let next = i
while (next < matches.length && center[next] == null) next += 1
if (next >= matches.length) {
center[i] = center[lastAnchor] + (i - lastAnchor) * pitch
} else {
const span = next - lastAnchor
center[i] = center[lastAnchor] + ((center[next] - center[lastAnchor]) * (i - lastAnchor)) / span
}
}
// Resolve any residual overlap by nudging downward (rare; honours heights).
for (let i = 1; i < matches.length; i += 1) {
const minC = center[i - 1] + (hs[i - 1] + hs[i]) / 2 + gap
if (center[i] < minC) center[i] = minC
} }
} }
let cursor = 0 matches.forEach((m, i) => centers[c].set(m.match_id, center[i]))
column.matches.forEach((match) => {
const h = heightOf(c, match)
const kids = childrenCenters && childrenCenters.get(match.match_id)
let center = kids && kids.length
? kids.reduce((a, b) => a + b, 0) / kids.length
: cursor + h / 2
let top = center - h / 2
if (top < cursor) top = cursor // never overlap the node above
center = top + h / 2
tops.set(layoutKey(c, match), top)
centers[c].set(match.match_id, center)
cursor = top + h + gap
})
columnHeights.push(cursor - gap)
}) })
return { tops, centers, height: Math.max(0, ...columnHeights) } // Shift the whole side so the highest match sits at y=0, then size to content.
let minTop = Infinity
columns.forEach((column, c) => {
column.matches.forEach((m) => {
minTop = Math.min(minTop, centers[c].get(m.match_id) - heightOf(c, m) / 2)
})
})
const shift = Number.isFinite(minTop) && minTop < 0 ? -minTop : 0
const tops = new Map()
let height = 0
columns.forEach((column, c) => {
column.matches.forEach((m) => {
const h = heightOf(c, m)
const top = centers[c].get(m.match_id) - h / 2 + shift
tops.set(layoutKey(c, m), top)
height = Math.max(height, top + h)
})
})
return { tops, centers, height }
} }