diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 454783a..d0403f7 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -2,7 +2,7 @@ import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' import Tree, { prewarmTreeCanvas } from '../Tree/Tree' import FallingLeaves from '../Tree/FallingLeaves' import ReplayCanvasPanel from './ReplayCanvas' -import { buildBracket, parentPosition } from './bracket' +import { buildBracket, computeBracketLayout, feederParent, layoutKey } from './bracket' const numberFormat = new Intl.NumberFormat('en-GB') const dateFormat = new Intl.DateTimeFormat('en-GB', { @@ -3840,66 +3840,59 @@ function TournamentMatchCard({ match, navigate }) { ) } -// Map each match to the match it feeds into in the next column, so we can draw a -// connector between them. Parent is found by slot position (faithful to the real -// tree even with byes hidden); falls back to proportional index if the exact -// parent slot was itself a hidden bye. -function feederParent(match, columnIndex, columns) { - const cur = columns[columnIndex] - const next = columns[columnIndex + 1] - if (!next) return null - const pPos = parentPosition(Number(match.position), cur.matches.length, next.matches.length) - const exact = next.matches.find((m) => Number(m.position) === pPos) - if (exact) return exact - const order = cur.matches.indexOf(match) - const idx = Math.min(next.matches.length - 1, Math.floor((order * next.matches.length) / cur.matches.length)) - return next.matches[idx] || null -} - function TournamentBracketSide({ side, navigate }) { const gridRef = useRef(null) const nodeRefs = useRef(new Map()) + // `tops` places each match; once placed we read back the DOM to draw connectors. + const [layout, setLayout] = useState({ tops: new Map(), height: 0 }) const [connectors, setConnectors] = useState({ width: 0, height: 0, paths: [] }) - const nodeKey = (columnIndex, match) => `${columnIndex}:${match.match_id}` - + // Pass 1: measure card heights, then position every match centred on its feeders. useLayoutEffect(() => { const grid = gridRef.current if (!grid) return undefined - - const measure = () => { - const gridBox = grid.getBoundingClientRect() - const boxOf = (key) => { - const el = nodeRefs.current.get(key) - if (!el) return null - const b = el.getBoundingClientRect() - return { - left: b.left - gridBox.left, - right: b.right - gridBox.left, - mid: b.top - gridBox.top + b.height / 2, - } - } - const paths = [] - for (let c = 0; c < side.columns.length - 1; c += 1) { - for (const match of side.columns[c].matches) { - const parent = feederParent(match, c, side.columns) - if (!parent) continue - const from = boxOf(nodeKey(c, match)) - const to = boxOf(nodeKey(c + 1, parent)) - if (!from || !to) continue - const midX = (from.right + to.left) / 2 - paths.push(`M ${from.right} ${from.mid} H ${midX} V ${to.mid} H ${to.left}`) - } - } - setConnectors({ width: grid.scrollWidth, height: grid.scrollHeight, paths }) + const relayout = () => { + const heights = new Map() + for (const [key, el] of nodeRefs.current) heights.set(key, el.offsetHeight) + const next = computeBracketLayout(side.columns, heights) + setLayout({ tops: next.tops, height: next.height }) } - - measure() - const observer = new ResizeObserver(measure) + relayout() + const observer = new ResizeObserver(relayout) observer.observe(grid) return () => observer.disconnect() }, [side]) + // Pass 2: with matches positioned, read real edges and draw elbow connectors. + useLayoutEffect(() => { + const grid = gridRef.current + if (!grid) return + const gridBox = grid.getBoundingClientRect() + const boxOf = (key) => { + const el = nodeRefs.current.get(key) + if (!el) return null + const b = el.getBoundingClientRect() + return { + left: b.left - gridBox.left, + right: b.right - gridBox.left, + mid: b.top - gridBox.top + b.height / 2, + } + } + const paths = [] + for (let c = 0; c < side.columns.length - 1; c += 1) { + for (const match of side.columns[c].matches) { + const parent = feederParent(match, c, side.columns) + if (!parent) continue + const from = boxOf(layoutKey(c, match)) + const to = boxOf(layoutKey(c + 1, parent)) + if (!from || !to) continue + const midX = (from.right + to.left) / 2 + paths.push(`M ${from.right} ${from.mid} H ${midX} V ${to.mid} H ${to.left}`) + } + } + setConnectors({ width: grid.scrollWidth, height: grid.scrollHeight, paths }) + }, [side, layout]) + return (
+
{column.label}
-