From dabf3478ecaf8601edfa18da8cc8dc092347ff9c Mon Sep 17 00:00:00 2001 From: Erol Haagenrud Date: Mon, 17 Aug 2026 17:19:26 +0200 Subject: [PATCH] V0-integrering: polert hull-diagram MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Integrerer V0-eksporten for hole-diagram-view.tsx. Traff props- kontrakten i prompten eksakt -- ingen integreringsjustering nødvendig, tsc rent på første forsøk. Egne tillegg fra V0 utover prompten: full aria-label-oppsummering av hele diagrammet for skjermlesere, lett kollisjons-unngåelse for nære hindringsetiketter. Re-verifisert i scratch mot samme kjente syntetiske geometri som den midlertidige hånd-kodede versjonen -- identiske tall/posisjoner. Lys+mørk bekreftet. Co-Authored-By: Claude Sonnet 5 --- ARCHITECTURE_DECISIONS.md | 33 +- CHANGELOG.md | 14 +- frontend/components/hole-diagram-view.tsx | 380 ++++++++++++++-------- 3 files changed, 286 insertions(+), 141 deletions(-) diff --git a/ARCHITECTURE_DECISIONS.md b/ARCHITECTURE_DECISIONS.md index 45103b2..2c97216 100644 --- a/ARCHITECTURE_DECISIONS.md +++ b/ARCHITECTURE_DECISIONS.md @@ -8331,12 +8331,35 @@ allerede deler samme `HoleTargetDistance`-datalag (ADR-081). (target-points-endepunktene returnerte allerede `tee_front`/`tee_back` uendret, bare ikke brukt av frontend før nå). -**Gjenstår:** V0-prompt for `hole-diagram-view.tsx` sin polerte visning -skrives og sendes til bruker; midlertidig hånd-kodet versjon er allerede -bevist geometrisk korrekt og fungerer, men er visuelt uferdig. +**V0-eksport mottatt og integrert samme dag** (samme +`tee-cup-login-screen.zip`-prosjektnavn V0 alltid re-eksporterer med, +kun `components/hole-diagram.tsx` var relevant -- kopiert til +`hole-diagram-view.tsx`, ingen andre filer fra eksporten rørt). Traff +props-kontrakten i prompten eksakt, ingen integreringsfeil å rette +(til forskjell fra forrige V0-runde/kjønn-mismatchen i ADR-082) -- +`tsc --noEmit` rent på FØRSTE forsøk. Egne, ikke eksplisitt etterspurte +kvalitetstillegg fra V0: `role="img"` + en full tekstlig `aria-label`- +oppsummering av HELE diagrammet (`buildDiagramAria`) for skjermlesere, +og en lett kollisjons-unngåelse for hindringsetiketter som havner nær +hverandre vertikalt (sortert etikett-nudge, ikke bedt om i prompten, +men matcher "ikke la det se ødelagt ut hvis to hindringer er nære"- +kravet godt). -**Rullet ut: ikke ennå** -- geometri+datalag+fallback er klare og -scratch-bevist, venter på V0-integrering før samlet utrulling. +**Verifisert på nytt med den polerte visningen:** samme kjente, +syntetiske hull-fixture som den midlertidige versjonen -- IDENTISKE tall +og posisjoner bekreftet (front 140/senter 150/bak 160/hindringer 54 m +til høyre midt på hullet, 131 m til venstre nær green, spiller 25% ut +fra tee), nå med V0s poleringer (varseltrekant-ikoner, "Deg"-etikett +over spillermarkøren, ryddig etikett-plassering). Lys+mørk bekreftet. +Scratch-stacken revet ned. + +**Ingen migrasjon** -- ren frontend-endring, ingen backend-endring +(target-points-endepunktene returnerte allerede `tee_front`/`tee_back` +uendret, bare ikke brukt av frontend før nå). + +**Rullet ut: ikke ennå** -- klar, venter på bekreftelse før `docker +compose build teecup_frontend && up -d` (kun frontend, ingen +backend-endring i denne runden, ingen migrasjon). --- diff --git a/CHANGELOG.md b/CHANGELOG.md index 69f3a20..35697b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12029,5 +12029,15 @@ Neste steg: **Ingen migrasjon** -- ren frontend, backend uendret (target-points returnerte allerede tee_front/tee_back, bare ikke brukt før nå). - **Rullet ut: ikke ennå** -- venter på V0-eksport for den polerte - visningen. + **V0-eksport mottatt og integrert samme dag.** Traff props- + kontrakten eksakt -- ingen integreringsfeil å rette denne gangen + (til forskjell fra ADR-082/spillerimportens kjønn-mismatch), `tsc` + rent på første forsøk. To ekstra kvalitetstillegg fra V0 utover det + som ble bedt om: full `aria-label`-oppsummering av hele diagrammet + for skjermlesere, og en lett kollisjons-unngåelse for hindrings- + etiketter som havner nær hverandre. Re-verifisert i scratch mot + samme kjente fixture -- identiske tall/posisjoner som den + midlertidige versjonen. Lys+mørk bekreftet. + + **Rullet ut: ikke ennå** -- klar, venter på bekreftelse før + `docker compose build teecup_frontend && up -d`. diff --git a/frontend/components/hole-diagram-view.tsx b/frontend/components/hole-diagram-view.tsx index 3813cce..787f977 100644 --- a/frontend/components/hole-diagram-view.tsx +++ b/frontend/components/hole-diagram-view.tsx @@ -1,161 +1,244 @@ "use client" -// MIDLERTIDIG hånd-kodet hull-diagram (ADR-083, 2026-08-17) -- bygget for -// å bevise geometrikjeden (frontend/lib/hole-geometry.ts) fungerer riktig -// ende-til-ende FØR en V0-prompt skrives, samme rekkefølge som -// FlagPlantSheet/PlayerImportPanel tidligere denne økten. Erstattes med en -// V0-eksportert visning senere -- selve geometri-utregningen (i -// hole-target-distance.tsx) endres ikke ved bytte, kun denne rene -// visningen. -// -// Tee vises alltid nederst, green (senter) alltid øverst -- fast akse -// (IKKE spillerens gangretning), se hole-target-distance.tsx sin -// teeRef-uthenting. alongPercent/crossMeters kommer ferdigregnet inn, -// klippes KUN her (ved selve tegningen), ikke i dataen. +// V0-eksportert visning (2026-08-17, ADR-083) -- ren kontrollert +// komponent, ingen egen GPS-/geometrilogikk. All geometri-utregning +// (projectOntoAxis) bor i hole-target-distance.tsx, allerede bevist +// riktig med enhetstester + scratch-verifisering FØR denne visningen +// ble bestilt. Erstattet en midlertidig hånd-kodet stand-in med samme +// props-kontrakt -- samme mønster som FlagPlantSheet/PlayerImportPanel. -import { Flag, MapPin } from "lucide-react" +import { Flag, MapPin, TriangleAlert } from "lucide-react" import { cn } from "@/lib/utils" -type DiagramHazard = { +export type DiagramHazard = { label: string + /** Live distance FROM THE PLAYER, always accurate. */ distanceMeters: number + /** 0 = at tee, 100 = at green. May be <0 or >100. */ alongPercent: number + /** Positive = right of the tee->green axis, negative = left. */ crossMeters: number } +type Props = { + distances: { front: number; middle: number; back: number } + playerPosition: { alongPercent: number; crossMeters: number } + /** Already sorted nearest-to-player first. */ + hazards: DiagramHazard[] + size?: "compact" | "full" + className?: string +} + +// --- Diagram geometry (percentages of the field box) --------------------- +// Fixed endpoints: green stays at the top, tee stays at the bottom, always. +const TRACK_TOP = 12 // where alongPercent === 100 lands (leaves room for the flag) +const TRACK_BOTTOM = 88 // where alongPercent === 0 lands (leaves room for the tee dot) +// Sideways scale: this many meters of cross offset maps to the max visual swing. +const MAX_CROSS_METERS = 30 +const MAX_OFFSET_PCT = 40 // ±40% of the field width, per spec +// Label collision handling: minimum vertical gap between stacked labels. +const LABEL_MIN_GAP_PCT = 11 + +function clamp(n: number, lo: number, hi: number) { + return Math.min(hi, Math.max(lo, n)) +} + +function alongToTopPct(alongPercent: number) { + const a = clamp(alongPercent, 0, 100) + return TRACK_TOP + ((100 - a) / 100) * (TRACK_BOTTOM - TRACK_TOP) +} + +function crossToLeftPct(crossMeters: number) { + const offset = clamp( + (crossMeters / MAX_CROSS_METERS) * MAX_OFFSET_PCT, + -MAX_OFFSET_PCT, + MAX_OFFSET_PCT, + ) + return 50 + offset +} + +function formatMeters(m: number) { + return `${Math.round(m)} m` +} + export function HoleDiagram({ distances, playerPosition, hazards, size = "full", className, -}: { - distances: { front: number; middle: number; back: number } - playerPosition: { alongPercent: number; crossMeters: number } - hazards: DiagramHazard[] - size?: "compact" | "full" - className?: string -}) { - // Diagrammet trenger plass -- kompakt-visningen (badge ved GIR-merket) - // viser fortsatt kun tallene + full hindringsliste, ikke selve tegningen. +}: Props) { if (size === "compact") { return (
-
- - - +
+ + +
- + {hazards.length > 0 && ( +
    + {hazards.map((h, i) => ( +
  • +
  • + ))} +
+ )}
) } + // --- Full variant ------------------------------------------------------- + // Precompute placements and a light label-collision nudge (top -> bottom). + const placed = hazards.map((h, i) => ({ + ...h, + key: `${h.label}-${i}`, + topPct: alongToTopPct(h.alongPercent), + leftPct: crossToLeftPct(h.crossMeters), + })) + + const byTop = [...placed].sort((a, b) => a.topPct - b.topPct) + const labelTopByKey = new Map() + let lastLabelTop = Number.NEGATIVE_INFINITY + for (const p of byTop) { + const desired = p.topPct + const labelTop = Math.max(desired, lastLabelTop + LABEL_MIN_GAP_PCT) + labelTopByKey.set(p.key, labelTop) + lastLabelTop = labelTop + } + + const playerTop = alongToTopPct(playerPosition.alongPercent) + const playerLeft = crossToLeftPct(playerPosition.crossMeters) + return ( -
-
- - - +
+ {/* Distance readout: center emphasized + largest, in the tee-green color. */} +
+ + +
- - - {hazards.length > 0 && ( -
- -
- )} -
- ) -} - -const LANE_HEIGHT_PX = 260 -// Faktisk fairway-bredde er ikke kjent fra dataen -- klipper visuelt til -// ±30 m fra midtlinjen, en fornuftig typisk verdi (se ADR-083). Selve -// TALLENE (distanceMeters) er alltid nøyaktige uansett -- kun selve -// PLASSERINGEN i tegningen klippes. -const MAX_CROSS_METERS = 30 - -function clampAlong(percent: number): number { - return Math.min(100, Math.max(0, percent)) -} - -function crossToOffsetPercent(crossMeters: number): number { - const clamped = Math.min(MAX_CROSS_METERS, Math.max(-MAX_CROSS_METERS, crossMeters)) - return (clamped / MAX_CROSS_METERS) * 42 // maks ±42% fra senter, litt luft i kantene -} - -function Lane({ - playerPosition, - hazards, -}: { - playerPosition: { alongPercent: number; crossMeters: number } - hazards: DiagramHazard[] -}) { - return ( -
- {/* Midtlinje -- ren visuell referanse, ikke en ekte fairway-kant. */} -