Seit dem neuen Chunk-Format werfen overview.py und geometry.js, und props.js
setzt still keine einzige Laterne mehr. Die drei Aufgaben, die das reparieren,
bekommen den genauen Bruch und die Pflicht, ihre von Hand im alten Format
gebauten Fixtures mitzuziehen. Dazu der Docstring von _line() als Bedingung
statt als Tatsache.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
chunk_payload() liefert jetzt carriageway, curb, sidewalk, markings, lots, bridges
und edges statt roads und centerlines - die Ausgabe fuer Task 12 (Pipeline) und
Task 13/14 (Zeichnen). curb ist ein Linienzug: Bordstein hat keine eigene Flaeche,
Loch und Fahrbahn teilen sich den Umriss. _line() normalisiert die Laufrichtung wie
_rings() die Wicklung - anders als bei den alten centerlines duerfen curb und edges
das, weil die Laternen ihre Seite seit Teil 2 aus Kantenrichtung und Gehwegpruefung
waehlen, nicht mehr aus der Listenposition.
Selbstpruefung zur Lauf-Stabilitaet ging ueber die zwei mitgelieferten Tests hinaus,
die bridges und edges nur leer durchreichen. Ad-hoc nachgerechnet und dabei eine
Luecke gefunden: Der Sortierschluessel von edges deckte nur (line, klasse) ab, nicht
auch breite und fahrbahn wie bei buildings ueblich (dort alle vier Felder). Bei
gleichem line+klasse, aber unterschiedlicher breite/fahrbahn, waere die Ausgabe
wieder an der Listenposition haengengeblieben - in der echten Pipeline legt
SECTION[klasse] beide Werte fest und kann so nicht divergieren, der Schluessel
deckt jetzt trotzdem alle vier Felder ab, wie bei buildings.
test_centerlines_keep_their_order_and_direction geht weg: Sie bewies die jetzt
bewusst umgekehrte Zusicherung (Reihenfolge und Richtung tragen Bedeutung);
centerlines selbst entfaellt. Die zwei neuen Tests aus dem Auftrag belegen das
Gegenteil fuer die Nachfolgefelder.
Bricht wie angekuendigt run.py (ruft chunk_payload noch mit roads=/centerlines= auf)
- das repariert Task 11/12. Anders als im Auftrag angenommen bleibt die Testsuite
komplett gruen (248/248): test_park_apply.py und test_park_street.py haengen an
park.py's eigenem World, nicht an chunk_payload, und kein anderer Test importiert
emit.py.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
reach kommt vom breitesten Arm der ganzen Kreuzung, wurde aber fuer jeden Arm
gleich verwendet. An einer Kreuzung, wo ein kurzer Arm auf einen breiten trifft,
reicht der kurze Arm dann nicht mehr bis reach (Zebrastreifen) oder bis
reach + STOP_SETBACK + STOP_DEPTH (Haltelinie) - die Markierung entstand trotzdem
und ragte ueber das Kantenende hinaus. junction_arms() in section.py schuetzt sich
dagegen laengst; junction_marks() jetzt auch, mit getrennter Pruefung je Markierung,
weil beide unterschiedlich viel Platz brauchen.
Am echten Netz: 38 von 1.508 Armstummeln sind zu kurz fuer die Haltelinie, groesster
Ueberstand 10,6 m (Fixrunde 1 des Koordinators). Auf tatsaechliche Zufahrten
umgerechnet (Schleifen zaehlen doppelt) sind es 44 von 1.536 - die Differenz zur
Kantenzahl war anfangs unklar, ist jetzt aber nachgerechnet und im Kommentar
festgehalten. markings(graph) sinkt dadurch von 22.586 auf 22.336 Flaechen (-206
Zebrabalken, -44 Haltelinien) - genau das vorher falsch platzierte Material.
Dazu zwei bisher uebersehene Stellen mit demselben zu lockeren Testfilter
(abs(centroid.y) < 20) auf die halbe Fahrbahnbreite verengt, wie schon an den zwei
Schwesterstellen der vorigen Fixrunde - die Haltelinie des Suedarms erfuellte die
lockere Bedingung ebenfalls, next() traf bisher nur zufaellig zuerst auf den Ostarm.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Zebrastreifen im Miami-Balkenmuster und Haltelinie je Zufahrt, dazu markings(graph)
als Sammelfunktion fuer Task 10. Beim Nachrechnen der Pruefkoerper zwei Fehler im
Auftrag gefunden und vor der Uebernahme berichtigt:
Die Haltelinie lag auf der falschen Fahrbahnhaelfte. "across" ist schon als links von
"along" definiert (dieselbe Konvention wie offset_curve() in _offset()), und die Umkehr
der Fahrtrichtung an der Zufahrt braucht deshalb kein zusaetzliches Vorzeichen - der
Auftrag zog eins ab und landete damit auf der Gegenspur. Eine Probe mit offset_curve()
auf der Einfahrtlinie bestaetigt +across als die rechte Seite; alle vier Arme der
Testkreuzung ergeben danach das erwartete Windradmuster.
Zwei Testfilter (|x| bzw. |y| < 20) waren zu locker: An der vierarmigen Testkreuzung
liegen Balken benachbarter Arme nahe genug am Ursprung, um faelschlich mitgezaehlt zu
werden - falsches Gruen durch Verwechslung. Auf die halbe Fahrbahnbreite verengt.
Zusaetzlich gefunden und mit eigenem Test belegt: Eine Schleifenkante haengt mit beiden
Enden am selben Knoten und stellt zwei getrennte Zufahrten dar (wie junction_arms() in
section.py); ohne die zweite Richtung fehlten Haltelinie und Zebra auf einer Seite der
Schleife. Betrifft 28 von 690 Kreuzungen am echten Netz.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Die Begruendung zur doppelten Mitte war selbst missverstaendlich: "jede Linie
frisst je die Haelfte des Achsabstands" ergibt woertlich gelesen null Luecke.
Jetzt steht die Rechnung ausgeschrieben da. Dazu tragen die Signaturen von
Task 9 nicht mehr object, sondern Polygon - sonst haette sich derselbe Fehler
in die naechste Aufgabe vererbt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der Kommentar zu gelb_doppelt nannte den Achsabstand (2*WIDTH = 20 cm) als "Abstand der
Mitten", meinte damit aber die Luecke zwischen den gemalten Linien - die entsteht erst
nach der Pufferung um je WIDTH/2 und betraegt WIDTH = 10 cm, wie vom Entwurf verlangt.
Die Geometrie war immer richtig, nur der Satz dazu nicht.
longitudinal() versprach list[tuple[object, str]] statt der zugesagten
list[tuple[Polygon, str]] - Polygon war nicht einmal importiert.
Dazu zwei Tests, die die schon richtige Lage festhalten, damit ein Kommentarfehler wie
der obige naechstes Mal auffiele: gelb_doppelt liegt symmetrisch um die Achse mit 20 cm
Achsabstand und 10 cm Luecke, der Boulevard-Spurtrenner liegt bei genau ±LANE. Kein
TDD-Fall - beide liefen beim ersten Lauf gruen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Der Kommentar nannte 10 cm Abstand der Mitten. Tatsächlich liegen die Achsen
20 cm auseinander, und erst die Pufferung lässt zwischen den gemalten Linien
die 10 cm entstehen, die der Entwurf verlangt. Dazu der zugesagte
Rückgabetyp statt object.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Gelbe Mitte, Spurtrenner und Randlinien je Kante, zwischen den Kreuzungsraendern -
Geometrie statt Textur, weil Haltelinie und Zebrastreifen (Task 9) quer zur
Fahrtrichtung liegen und in einer laengs laufenden Kachel nicht unterzubringen waeren.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Zuschneiden (Fixrunde 4) war der falsche Hebel: Egal wie breit die abgezogene
Zone, die Nische zwischen zwei Gassenstummeln entsteht erst durch die
FILLET-Rundung in junction_area() und liegt abseits der Gassenachse - ein
linearer Abzug entlang dieser Achse kann sie grundsaetzlich nicht erreichen.
Die Stummelbildung aus junction_area() in einen eigenen Helfer gezogen,
junction_arms(graph, node, kerb_only=False), mit einem Filter fuer Kanten ohne
Bordstein. junction_area() nutzt ihn ungefiltert und rundet wie bisher -
verhaelt sich dadurch identisch (913.566,7 m² in 213 Teilen am echten Netz,
bit-genau nachgemessen). _walk_source() in street.py nimmt jetzt nur noch die
rohen, ungerundeten Armstummel mit Bordstein (kerb_only=True) als Quelle,
ohne den kerbless_zone-Abzug aus Fixrunde 4 - die Nische entsteht dadurch gar
nicht erst, weil die Kreuzung ohne Rundung nur noch ueber ihre eigenen,
bordsteintragenden Arme in die Quelle eingeht.
Gemessen: an der Kreuzung mit drei Gassen und einem Boulevard verschwindet
der Rest in der Nische vollstaendig (0,0 statt der 27,14 m² aus Fixrunde 4)
- weiter als die fuer diese Runde erwartete Obergrenze von rund 6 m
Ausdehnung. Kriterium 4 damit erfuellt, keine weitere Variante noetig.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
_walk_source() nahm bisher die ganze junction_area() eines Knotens auf, sobald
irgendein anliegender Arm Bordstein hat - auch den Anteil, der zu einer dort
einmuendenden Gasse gehoert. Der Gehweg wuchs dadurch die volle Laenge des
Kreuzungsstummels die Gasse hinauf (bis zu setback+1, beim Boulevard 11 m)
statt nur um die Ecke. _kerbless_zone() bekommt den extra-Parameter zurueck
(_kerb_lines() und _walk_source() rufen sie beide mit 0.2 auf) und beschneidet
jetzt auch den Kreuzungsanteil der Gehwegquelle.
Gemessen: an einer T-Kreuzung (2 Gassen + 1 Wohnstrasse) verschwindet der
fehlerhafte Gassenstummel-Gehweg vollstaendig (53,11 -> 0,0 m2). An einer
4-Wege-Kreuzung mit 3 Gassen + 1 Boulevard reicht der Fix nicht aus: der Rest
in der Nische zwischen zwei Gassenstummeln schrumpft nur auf 27,14 statt der
erwarteten rund 2 m2, und seine maximale Ausdehnung von der Kreuzungsmitte
(11,14 m) liegt weit ueber der als Grenze genannten 3-4 m - dokumentiert als
bekannter, nicht ausreichend geloester Punkt, siehe Fixbericht.
Unabhaengig davon: MultiPolygon([result]) wirft TypeError, wenn result eine
GeometryCollection ist (intersection()/difference() liefern das gelegentlich
an Beruehrstellen, mit einer entarteten Nulllinie neben echten Polygonen).
Neuer Helfer to_multipolygon() in section.py filtert auf echte Polygone,
statt daran abzustuerzen; carriageway_area() und _sidewalk() nutzen ihn beide.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Die Reichweitenzone aus der letzten Runde (Gehweg aus der ganzen Fahrbahn wachsen
lassen, dann eine Zone um jede bordsteinlose Klasse wieder abziehen) fragte die
falsche Richtung: "liegt dieser Punkt nahe einer Gasse?" statt "gehoert die
naechste Fahrbahn zu einer Klasse mit Bordstein?". Die Zone kennt ihre
Nachbarschaft nicht und frass dabei echten Wohnstrassen-Gehweg mit, sobald der
zufaellig nah an einer Gasse lag - 250 von 500 m2 bei einer parallelen Gasse mit
nur 0,5 m Korridorabstand, 7,58 m2 systematisch an jeder Gasse-Wohnstrasse-
Einmuendung, unabhaengig von der Gassenlaenge.
_walk_source(graph) baut die Fahrbahnquelle jetzt nur aus Kanten und Kreuzungen
mit mindestens einem bordsteintragenden Arm (ribbon()/junction_area(), gefiltert
auf SECTION[...].kerb). _sidewalk() waechst aus dieser Quelle statt aus der
ganzen Fahrbahn und braucht keinen nachtraeglichen Abzug mehr. _kerbless_zone()
bleibt nur noch fuer den Bordstein-Ausschluss in _kerb_lines(), mit dem
urspruenglichen festen Zuschlag von 0,2 m.
Rechenzeit am echten Netz gemessen: _walk_source() kostet zusaetzlich 1,8 s
(carriageway_area() braucht 2,45 s), build_street() insgesamt rund 5,1-5,2 s -
weit unter der 30-s-Schranke, kein Anlass, die Kreuzungsflaechen nur einmal
statt zweimal zu berechnen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Der Entwurf nannte 0,18 km² Restfläche und 1,0-1,2 km² Fahrbahn. Gemessen
sind es 0,093 und 0,914 km²: Die Restfläche zählte den Gehweg doppelt, und
bei der Fahrbahn fehlte der Rückschnitt an 690 Kreuzungen. Dazu Gehweg
0,295 km² und Bordstein 120,3 km.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der eigene Fix aus der letzten Runde (_lots() zieht carriage.buffer(WALK_MAX) ab)
hatte einen neuen Fehler eingeführt: Der klassenunabhängige 3-m-Abzug frisst bei
der Gasse (walk=0,0, gar kein Gehweg) und bei der Wohnstraße (walk=2,5, nicht 3,0)
mehr weg, als dort tatsächlich Gehweg ist - eine echte Pflasterfläche neben einer
Gasse wurde bis zu WALK_MAX komplett als Gehweg gezählt statt als Restfläche.
An der Wurzel behoben: _kerbless_zone(graph, extra) berechnet den Korridor aller
Klassen ohne Bordstein aus der vollen Kantenmittellinie, mit extra als Parameter
statt festem Wert - _kerb_lines() ruft sie mit extra=0.2 auf (schmale Fuge an der
physischen Kante, unveraendert), _sidewalk() mit extra=WALK_MAX (muss die volle
Gehweg-Reichweite abdecken, sonst bliebe ein bis zu 2,8 m breiter Streifen
faelschlich Gehweg). Ein einzelner fester Radius fuer beide Zwecke funktioniert
nicht: zu schmal laesst echten Gehweg als Gehweg einer Klasse ohne Gehweg stehen,
zu breit schneidet er benachbarten, legitimen Bordstein weg - nachgerechnet mit
der vom Fix-Auftrag selbst vorgeschlagenen Testfixture, bevor der feste Wert
uebernommen wurde.
_lots() rechnet jetzt exakt (corridor.difference(carriage).difference(sidewalk))
statt mit einem eigenen Puffer, wie vorgegeben.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
_lots() zog bisher nur die Fahrbahn vom Korridor ab, nicht den Gehwegstreifen - weil
carriageway + 2*walk je Klasse genau den Korridor ergibt, war "Korridor minus Fahrbahn"
wörtlich der Gehweg und legte ihn ab 80 m Straßenlänge ein zweites Mal als Restfläche
darüber. _sidewalk() hielt die MultiPolygon-Zusage nicht (derselbe Fehler wie bei
carriageway_area() in Task 6): ein leeres Ergebnis kam als leeres Polygon zurück statt
als leeres MultiPolygon. _kerb_lines() nimmt jetzt die volle Kantenmittellinie statt
ribbon() für den Gassen-/Brückenausschluss, weil ribbon() an der Kreuzung endet und der
Bordstein sonst im Einmündungstrichter stehen bliebe.
Die im Fix-Auftrag genannte Test-Konfiguration für den dritten Punkt (30 m lange Gasse)
zeigte den gemeldeten Fehler nicht: ribbon() liefert dort kein None (Setback 5,5 m,
30-5,5=24,5 >= 1,0), der alte Ausschluss deckte die Gasse also schon vollständig ab. Der
Fehler braucht eine Gasse kürzer als setback+1=6,5 m - erst dann liefert trimmed_line()
None und der alte Code baut gar keinen Ausschluss. Testkonfiguration entsprechend auf
eine 5 m kurze Gasse korrigiert, siehe task-7-report.md für die vollständige Herleitung.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ein Prüfkörper korrigiert: Der Splitter in
test_kleine_reststuecke_fallen_weg_grosse_bleiben lag direkt an derselben
Korridorkante wie die Ausbuchtung und verschmolz dort schon bei unary_union()
mit ihr, bevor der LOT_MIN-Filter überhaupt etwas zu tun bekam. Mit einer
kleinen Lücke zum Rand bleibt er ein eigenständiges 2-m²-Polygon und fällt
durch den Filter - der Test prüft jetzt tatsächlich das Wegfallen kleiner
Reststücke, nicht nur, dass große bleiben.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Zwei Befunde aus der Prüfung: unary_union() vereinte eine einzelne zusammenhängende
Kreuzung zu einem einfachen Polygon statt des zugesagten MultiPolygon (bricht an kleinen
Prüfkörpern, nicht am echten Netz mit seinen 213 Teilen - deshalb bisher unbemerkt).
Normalisiert jetzt auf MultiPolygon, mit Testzusicherung an der zusammenhängenden
kreuzung()-Fixture.
test_zwei_parallele_strassen_verschmelzen_nicht bewachte nichts: alle vier Knoten hatten
Grad 1, junction_area() lief nie. Fixture bekommt jetzt an beiden Straßen einen Querarm
(Grad 3), damit das Schließen tatsächlich ausgeführt wird, während die Straßen getrennt
bleiben.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
incident() liefert eine Schleife (a == b) nur einmal, und edge.a == node ist bei ihr immer
wahr - die zweite Abgangsrichtung bekam deshalb nie einen eigenen Arm im Kreuzungsfächer.
Das war kein Schönheitsfehler, sondern ein Loch in der Fahrbahn genau dort, wo das
zurückgeschnittene Schleifenband auf die Kreuzungsfläche trifft: 179 von 196 Schleifen
sitzen an einem Knoten mit Grad >= 3 und waren betroffen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Die nodes-Liste in test_kante_ganz_innerhalb_der_kreuzung_liefert_kein_band
stand ein Zeichen zu weit links, statt wie die edges-Liste darunter unter
dem ersten Listenelement zu fluchten - beim Selbstlesen des Diffs
aufgefallen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Der Korridor, den Task 4 misst, ist auf der Karte eine einzige graue
Fläche - Fahrbahn und Gehweg tragen dort dieselbe Farbe. Der Querschnitt
teilt sie deshalb nur auf, statt sie zu vergrößern: Fahrbahn plus zweimal
Gehweg ergibt für jede Klasse genau die gemessene Breite, und der
Stadtgrundriss bleibt unberührt. Die Maße (Fahrspur 3,5 m, Parkspur
2,0-2,5 m, Gehweg 2,5-3,0 m) stehen im Entwurf; eine Assertion auf
Modulebene hält die Aufteilung für alle vier Klassen fest.
Das Fahrbahnband entsteht aus der Mittellinie: An jedem Kreuzungsende
(Grad >= 3) weicht sie um die halbe Fahrbahn der breitesten anliegenden
Kante plus 2 m zurück, freie Enden bleiben stehen. Eine Kante, die ganz
in den Kreuzungsflächen verschwindet, liefert kein Band - das füllt Task 6.
Testkorrektur: test_kante_ganz_innerhalb_der_kreuzung_liefert_kein_band
hatte an beiden "Kreuzungsknoten" nur je zwei Kanten, also Grad 2 statt
Grad 3 - keine Kreuzung nach der Definition, die trimmed_line() selbst
verwendet. Gegen die fertige Implementierung geprüft, lieferte sie eine
echte Linie statt None. Je ein zweiter Arm an beiden Knoten (wie schon in
test_kontraktion_zieht_alle_anliegenden_kanten_mit für denselben
3-m-Kreuzungsabstand) behebt das.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
_contract_short() ließ Kanten, die schon vorher am überlebenden Knoten
hingen, auf ihrer alten Koordinate stehen - nur die vom eingezogenen
Knoten umgehängten folgten dem neuen Mittelpunkt. Da Kontraktion beidseitig
Grad >= 3 voraussetzt, hat der überlebende Knoten danach immer mindestens
Grad 4, und _merge_chains() fasst nur Grad-2-Knoten an: Der Fehler konnte
sich nie von selbst heilen. Am heutigen Mosaik wirkungslos (die Funktion
greift dort kein einziges Mal), aber real für jeden Ausschnitt mit zwei
nahen Kreuzungen - genau der Fall, den die Funktion behandeln soll.
Dazu zwei Kleinigkeiten: _median_width() deckte den Kurzkanten-Fallback
(unter 2 * WIDTH_MARGIN) bisher nicht ab, und ein Kommentar in
_merge_chains() war ein Zeichen zu weit eingerückt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sackgassen unter 10 m sind Skelettfransen und fallen weg, Knoten mit nur
noch zwei Kanten lösen sich auf, Kreuzungen näher als 5 m werden eine. Die
Breite kommt aus der Distanztransformation ohne die ersten und letzten 8 m:
dort misst sie die Kreuzung, nicht die Straße.
Der Gegencheck am echten Mosaik deckte zwei Fälle auf, die kein
synthetischer Test zeigt: Ein Rohast ohne Kreuzungsanschluss liefert eine
Kante mit nur einem Stützpunkt (LineString verlangt 0 oder mehr als einen
Punkt), und ein Knoten mit Schleife plus Verbindungskante hat kein zweites
Ende zum Verketten. Beides fängt clean() jetzt ab, mit je einem
Regressionstest.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Die Fassung im Plan suchte je Kante den Grad über alle Kanten und begann
nach jedem Fund von vorn. Am echten Skelett mit rund 12.000 Rohästen wäre
das nicht langsam, sondern praktisch stehengeblieben. Jetzt wird der Grad
einmal gezählt und fortgeschrieben, ein Durchgang räumt alle Fransen auf
einmal weg, und ein Test mit über 1.500 Kanten nagelt es fest.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
centerlines() verwirft alles unter 15 m und zerlegt das Netz damit an jeder
Kreuzung: von 3.038 Linienenden treffen sich nur 81. Der neue Graph lässt
das Skelett ungefiltert, macht aus den 5.970 Pixeln mit Grad >= 3 durch
Verschmelzen im Umkreis von 8 m die 862 Kreuzungen und hängt die Kanten
dort ein.
_skeleton_graph/_to_world in vectors.py sind jetzt öffentliche Schnittstelle
für roads.py (skeleton_graph/to_world); inpaint.py zieht seinen einzigen
Aufruf von skeleton_graph mit, sonst bräche dessen Import.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
modular_sidewalk_curb_kit (Just8, CC BY 4.0) liefert 2048er Karten für
Bordstein und Gehweg. Sie werden auf 1024 verkleinert und im Namensschema
der Poly-Haven-Texturen abgelegt, damit createMaterials() sie ohne
Sonderfall lädt. Das Kit selbst bleibt außerhalb des Repos; das Skript
überspringt den Schritt, wenn der Ordner fehlt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Schritt 4 von Task 2 verschob modular_sidewalk_curb_kit nach /tmp und zurück.
Bricht der Lauf dazwischen ab, liegt ein Ordner, der uns nicht gehört, im
Temp-Verzeichnis. CURB_KIT stellt den Pfad um, ohne etwas anzufassen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fixrunde 1: Der Test „lässt kein Loch verschwinden" konnte nicht fehlschlagen,
weil er nur position.count prüfte, das vor der Triangulierung gefüllt wird.
Jetzt absichert footprintArea(geo) die tatsächliche Restfläche (18400 m²);
das console.log ist entfernt.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Erstes Risiko des Entwurfs: die Fahrbahn wird als Loch aus der Bodenplatte
geschnitten, und ShapeUtils.triangulateShape läuft beim Nachladen im
Hauptthread. Der Test hält fest, dass 100 Löcher je Chunk unter 50 ms
bleiben und keines verschwindet.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Signatur von road_graph, eine wirkungslose Hilfsfunktion, eine Zusicherung
die der Vereinfachung widerspricht, ein doppelter Aufruf und ein sorted()
über Wörterbücher ohne Schlüssel.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
16 Aufgaben mit Tests zuerst. Aufgabe 1 misst das größte Risiko des
Entwurfs, bevor irgendetwas gebaut wird: ob der Browser eine Bodenplatte
mit 100 Löchern je Chunk in unter 50 ms trianguliert. Danach Graph,
Querschnitte, Kreuzungen, Mulde, Markierungen, Chunk-Format, Flamingo
Park, Pipeline, Browser, Minimap und die Abnahme.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Teil 2 der Straßenarbeit. Gemessen statt geschätzt: 88,74 km in 1.519
Mittellinien, deren Krummheit im Median bei 1,0104 liegt — das Zackige
sitzt allein in den Rändern. Das heutige Netz zerfällt an den Kreuzungen
(nur 81 von 3.038 Enden treffen sich), weil centerlines() alles unter 15 m
verwirft; das ungefilterte Skelett hat 862 Kreuzungen.
Entwurf: Graph aus dem Skelett, vier Straßenklassen nach gemessener
Korridorbreite, Fahrbahn als 15 cm tiefe Mulde mit Bordsteinwand und
Gehweg auf Bodenhöhe, Markierungen als Geometrie in einem Material.
Im Browser gemessen: die Kapsel erklimmt 15 und 25 cm ohne Halt und
blockiert erst bei 40 cm — ein Bordstein mit Kollision sperrt niemanden ein.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Flamingo Park zeigt die echte Anlage aus OpenStreetMap: Wege, Sportfelder,
Plätze, Pool, Parkplatz und Gebäude, eingepasst in die Parkfläche des
Spiels. Die Jefferson Avenue führt vom Park geradeaus nach Süden. Tribüne
und Hood Yard bleiben, wo sie sind.
Teil 2 (Bordstein, Gehweg, amerikanische Straßenmarkierungen mit den
beiden Kits) ist ein eigener Entwurf.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
140 der 144 Chunkdateien ändern sich einmalig durch die Normalisierung in
emit.py, ohne dass sich außerhalb der acht Parkchunks in irgendeiner Ebene die
Fläche bewegt: symmetrische Differenz 0,00 m² für land, surfaces, roads,
buildings und paths über alle 136 übrigen Chunks.
In den Parkchunks ändert sich allein die Wegschicht, und zwar genau um die
runden Kappen: außerhalb des Parkvierecks liegen jetzt 0,02 statt 95,96 m² Weg
(der Rest ist die Rundung auf zwei Stellen). Wegfläche gesamt 7.999,35 ->
7.903,38 m². Palmen (2.257), Mittellinien und Gebäude bleiben unverändert;
map.png folgt der kleineren Wegfläche.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
emit.py normalisiert jede ausgegebene Fläche und sortiert die Listen nach der
Ausgabestruktur selbst. unary_union() in apply_street() gab das kartenweite
Straßenpolygon mit anderem Ringstartpunkt und an anderer Listenstelle zurück und
änderte damit 88 Chunkdateien, in denen sich geometrisch nichts bewegt hatte
(symmetrische Differenz 0,00 m²). Belagsarten und Mittellinien behalten ihre
Reihenfolge: overview.py malt die Beläge in Listenreihenfolge übereinander, und
vergeLampPoints() hängt an Reihenfolge und Laufrichtung der Mittellinien —
Sortieren verschöbe dort 1.848 von 3.921 Laternen auf die andere Straßenseite.
check_corners() misst gegen den Straßenrand statt gegen die Straßenfläche. Alle
vier Ecken liegen in der Fahrbahn, ihr Flächenabstand ist 0,000 m; die alte
Prüfung schlug deshalb nur an, wenn eine Straße sich zurückzieht. Eine nach
innen verbreiterte Straße hätte apply_park() still auf das veraltete Viereck
zurückgeschnitten.
Die Parkwege werden am Parkviereck abgeschnitten — die runden Kappen von
LineString.buffer() lagen mit 95,96 m² darüber hinaus, rund 41 m² davon sichtbar
als Betonzungen auf den umgebenden Belägen. load_elements() prüft den OSM-Abzug
so streng wie die Parkdatei, mit Dateiname statt nacktem KeyError.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Draw Calls, Dreiecke und Bildzeit für Draufsicht, Weg und Feld, der Anteil
der neuen Schichten daran, der Zustand der Konsole und die abgestimmte
Rasenfarbe.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
leafy_grass ist roh sandfarben (Mittel der Diffuse-Kachel R 151, G 131,
B 89). Seit der Park seine echte Fläche als eigene Schicht trägt (102 790
statt 16 798 m² im Parkrechteck), las er sich aus der Vogelperspektive wie
eine Brache, während ihn die Minimap grün zeichnet. PARK_TINT 0xb4dc94
tönt Rot und Blau stärker ab als Grün; die Felder behalten ihren helleren
FIELD_TINT und heben sich weiter ab.
Im Browser abgestimmt (Task 12, Schritt 7) und vom Nutzer so gewählt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Startpunkt und Fahrzeugpunkte stehen jetzt in src/world/spawn.js, damit ein
Datentest sie lesen kann.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Folge der Entscheidung aus Task 5: Sperrflächen schneiden auch Zufahrten aus
und warnen dabei. Am echten Stand quert die Zufahrt zum Parkplatz die
Sperrfläche der Tribüne, die Warnung gehört also zum gewollten Zustand.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die Erwartung "keine Warnung" im Plan war überholt: Task 5 schneidet Sperrflächen
bewusst auch aus Zufahrten aus und warnt dabei, und am echten Stand quert die
Zufahrt zum Parkplatz die Tribuenen-Sperrflaeche. Der Test prueft jetzt die
Warnungsliste wortgenau, damit er weiterhin bei jeder neuen oder anderen
Warnung anschlaegt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Zufahrtsflächen (highway=service) wurden bisher ungeschnitten ausgegeben und nicht auf
Überschneidung mit Sperrflächen geprüft. Am echten Stand überlappte eine Zufahrt die
Sperrfläche "tribuene" um 42,17 m² ohne Schnitt und ohne Warnung. build_layout wendet
difference(blocked) jetzt auch auf Zufahrten an, die Warnschleife prüft Wege und
Zufahrten getrennt ("Parkweg schneidet ..." / "Zufahrt schneidet ...").
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Die Pipeline erzeugt auf diesem Rechner nicht dieselben Daten wie auf dem Mac,
auf dem der bisherige Stand entstand: Pillow dekodiert die JPEG-Kacheln
minimal anders, bei unveränderten Paketversionen aus uv.lock. Betroffen sind
22 von 144 Chunks und die drei Kartendateien.
In neun Chunks ändert sich dabei auch die Zahl der Objekte je Ebene, weil
Flächen an Berührungspunkten anders verschmelzen oder sich teilen. Der
relative Flächenanteil je Ebene ist dabei kein verlässliches Maß und läuft bis
31.69 % hoch, weil einzelne Ebenen je Chunk nur wenige hundert m² groß sind.
Absolut liegt der größte Einzelverlust bei 1651 m² (chunk_1_3, Straße), davon
1541 m² räumlich deckungsgleich als Stadtfläche wiedergefunden; ähnlich
sauber chunk_3_0 mit 1286 m² verlorenem Gebäude gegen 1266 m² neue
Stadtfläche am selben Ort. Ausnahme: in chunk_1_1 verschwinden 66 m²
Gebäudefläche ersatzlos, ohne Entsprechung in Straße oder Stadtfläche
desselben Chunks.
Die Landplatte (`land`) bleibt in 20 von 22 Chunks unverändert. In chunk_4_11
Randrauschen (±11 m²), in chunk_3_11 schließt sich eine rund 150 m² kleine
Aussparung (netto +140 m² Land) – in keinem Chunk verliert die Landplatte per
Saldo Fläche.
Die Parkchunks (J6 bis K9) sind bis auf chunk_10_6 nicht betroffen; dort
ändern sich nur Koordinaten, weder Objektzahl je Ebene noch die Landplatte.
Ab hier ist Windows der Referenzrechner, damit spätere Läufe nur noch die
gewollten Änderungen zeigen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
_sides() und _place_rows() nehmen die ersten vier Ringpunkte eines Rechtecks
als seine Ecken. GEOS setzt nach dem Puffern je nach Plattform zusätzliche
Punkte mitten auf die Kanten (macOS 0, Windows 2), und dann messen beide die
falschen Kanten: ergänzte Gebäude bekamen unter entfernten Beschriftungen eine
falsche Größe und eine falsche Richtung. simplify(0) entfernt genau diese
Punkte und lässt die Form unverändert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
GEOS setzt bei entarteten Eingaben je nach Plattform zusätzliche Punkte auf
eine Kante, und fileURLToPath liefert unter Windows Backslashes. Beide Tests
prüfen jetzt, was sie meinen: die Ecken des Rechtecks und den Ort im Baum.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Beim Nachrechnen korrigiert: affine statt bilinearer Einpassung (bilinear
verfehlte den Hood Yard um 4,2 m), offset_u 6,25 m, gemessenes Schnitt-Vieleck
der Gasse, Mittellinienstücke erst ab 15 m, Tribüne ohne replaces, Startpunkt
als eigenes Modul, PYTHONUTF8=1 unter Windows.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Teil 1 von zwei: Der Park bekommt die echten Wege, Felder, Plätze, Pool,
Parkplatz und Gebäude, eingepasst nach Variante D, dazu die Jefferson Avenue
als gerade Verlängerung nach Süden. Umsetzung in der Extraktion. Teil 2
(Bordstein, Gehweg, amerikanische Markierungen) bekommt einen eigenen Entwurf.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Das Submodul zeigte auf GitHub, der eingetragene Stand 028c652 lag aber nur
lokal vor: GitHub-Master ac39682 plus Showcase mit Figuren und launch.json.
Frische Klone konnten den Controller deshalb nicht laden. Jetzt liegt dieser
Stand als normaler Ordner im Repo, ohne Verweis auf GitHub.
dist/ und node_modules/ im Controller bleiben ignoriert und werden wie bisher
lokal gebaut.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Tribüne im Park J6 statt des alten Gebäudekastens, mit Materialrollen,
ersetzten Grundrissen beim Chunk-Aufbau und Asset-Test je Ort.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Behebt Befund 1 aus dem Abschluss-Review: der Skill war seit den
Tribüne-Tasks veraltet - er kannte weder replaces noch die
Materialzeilen je Ort in SOLID_BY_PLACE/BUMP_BY_PLACE. Ersetzt statt
angehängt, an den drei Stellen, wo man danach sucht: Überblick
(Ein-neuer-Ort-Liste), Schritt 2 (Stelle holen) und Schritt 8 (Tests),
dazu zwei Fallstricke-Zeilen für die roten Ausgangszustände.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Behebt Befunde 2-6 aus dem Abschluss-Review: der replaces-Test prüft
jetzt zusätzlich jede Ecke des getroffenen Grundrisses gegen die
Außenmaße plus REPLACED_OVERHANG (5 m), der Verteilungstest vergleicht
als Mengen und bekommt eine Gegenrichtung je Datei, BUMP_BY_PLACE
erzwingt eine Zeile je Ort statt sie mit "?? []" zu verdecken, ein
Gebäude an einer Chunkgrenze bekommt einen Test über zwei Chunks samt
JSDoc-Hinweis, und der Rückwand-Messwert in der Spec nennt Kapselmitte,
Wandfläche und Radius einzeln.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Browserprüfung: alter Kasten weg (Strahl trifft Boden bzw. Tribünendach), acht feste Materialien als Float32 im Collider, Rückwand stoppt die Kapsel am Sockel, Tunnel samt Treppe begehbar, Sitze durchlässig, +0,1–0,3 ms je Bild, vorgewärmt (48 ms bis zum Einsetzen, größte Lücke 56 ms).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tribuene.glb mit places.mjs aufbereitet (6,3 MB -> 1,7 MB, 69 557 Dreiecke, 14 Primitive, 49 MB Grafikspeicher). PLACES-Eintrag bei (1409,9 | 1051,1), 45° gedreht, lift 0, replaces auf den 18,6 m hohen Kasten. Der Lage-Test übergeht ersetzte Grundrisse und prüft, dass jeder replaces-Punkt genau ein Gebäude unter dem Ort trifft.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fest: Sichtbeton, Estrich, Verkleidung, Stahl, Trapezblech, Riffelblech, Verglasung, utility_box_01. Nahflächen: Sichtbeton, Estrich, Verkleidung, Stahl, Sitzschale. Kabel, Kabeldosen, Wandleuchten und Kästen auf 0,25. Der Asset-Test prüft jede Datei aus PLACES gegen ihre eigene Zeile fester Materialien statt gegen die ganze globale Liste, Bump nur beim Hood Yard.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
withoutReplacedBuildings filtert die Grundrisse, in denen ein Weltpunkt aus PLACES[].replaces liegt; ChunkStreamer baut die Geometrie daraus, main.js übergibt die Punkte aller Orte. Damit entfallen Kasten, Schatten und Kollision des alten Gebäudes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Vier Tasks: ersetzte Gebäude beim Chunk-Aufbau weglassen, Materialrollen und Asset-Test je Ort, Tribüne aufbereiten und eintragen (am Prototyp gemessen: 1,7 MB, 69 557 Dreiecke, 14 Primitive, 49 MB), Prüfung im Browser mit vorberechneten Blick- und Kapselpunkten.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die Tribüne aus tribuene.glb ersetzt 1:1 den Gebäudekasten in J6:
längs mittig auf dem alten Gebäude, 45° gedreht, Front 1 m vor dessen
Feldseite (Vergleichsbild im Gespräch bestätigt), lift 0 ohne
Bodenplatte. Neu gegenüber dem Hood Yard ist replaces im Orte-Eintrag:
der ChunkStreamer lässt den Grundriss beim Aufbau weg. Dazu
Materialrollen, Budget und der Umbau des Asset-Tests auf alle Orte.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ablauf wie beim Hood Yard mit den gefundenen Fallstricken (Dreiecke je Knoten,
Bodenplatte +6 cm, globale Materiallisten, Asset-Test je Ort, launch.json nur im
Hauptordner, gl.finish-Messung, Vorwärmen prüfen) und Werkzeugen: Modell vermessen,
Kartendaten eines Weltfensters, Vergleichsbild Satellit/Spiel, Screenshot-Dekodierung,
erprobte Browser-Ausschnitte.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Wortlaut: die Radien garantieren nur, dass die Chunks unter einem Ort
angefordert sind, solange er in der Szene ist — nicht, dass ihr Boden
schon gebaut ist (ChunkStreamer baut höchstens einen Chunk je Bild).
"geladen" -> "angefordert" im Kommentar über den Radien
(src/world/places.js), über der PlaceLayer-Konstruktion (src/main.js),
im Testnamen (tests/placesSite.test.js) und in der Spec, dort mit
einem Satz zu den paar Bildern nach einem Kartensprung.
Spec zusätzlich: warmUp in den Bullets "Vorbereiten"/"Einsetzen",
robustes Einsetzen (Warnung, kein erneuter Versuch) im Bullet zum
Ladefehler, und ein neuer Messwerte-Bullet in "Fehlerfälle und
Risiken" (+0,4–0,6 ms je Bild, ~130 ms einmaliger Texturupload ohne
warmUp).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
GLTFLoader liefert quantisierte Positionen in echt als
InterleavedBufferAttribute (gemeinsamer Puffer mit Normale/UV), nicht
als eigene BufferAttribute wie im bisherigen Test. dequantizePosition
braucht dafür keine Änderung: es liest nur über das gemeinsame
Attribut-Interface (normalized, getX/getY/getZ, count), das three.js
für beide Klassen implementiert. Der Test läuft grün und schreibt das
fest.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
places.mjs schrieb bisher nach public/models/places/<id>.glb relativ
zu process.cwd() und nahm jede id ungeprüft an. Die neue, exportierte
placeTarget(id) löst den Pfad stattdessen von import.meta.url ab (egal
von wo das Skript aufgerufen wird) und lehnt IDs außerhalb von
[a-z0-9-]+ ab, bevor daraus ein Pfad wird, der aus public/models/
places/ herausführen könnte.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
#show baute bisher scene.add, bevor player.addCollider lief: wirft
addCollider (z. B. toter Worker), blieb die Wurzel ohne active-Eintrag
in der Szene stehen — sichtbar, aber nie wieder entfernbar — und die
Ausnahme entkam synchron aus update() in die Spielschleife.
Der Collider-Handle wird jetzt vor scene.add in einem try/catch
gebaut; schlägt er fehl, bleibt die Szene unverändert, der Ort landet
in this.failed (kein erneuter Versuch) und #show überspringt ihn beim
nächsten Aufruf.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
PlaceLayer nimmt jetzt ein optionales warmUp(root) entgegen und ruft es
nach prepareModel, aber vor dem Einsetzen auf. main.js nutzt das, um
Texturen hochzuladen und CSM-Shader zu übersetzen, solange der Ort noch
nicht in der Szene ist — gemessen kostete das sonst rund 130 ms auf
einen Schlag im ersten Bild, in dem der Hof erscheint. Schlägt das
Vorwärmen fehl, wird nur gewarnt; der Ort wird trotzdem eingesetzt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Prototyp am echten Modell: 653,6 MB -> 16,2 MB, 2,9 Mio. -> 658 000 Dreiecke,
Bump-Maps erhalten. Radien geändert: Der Hof erscheint erst ab 250 m und
verschwindet ab 450 m, sonst stünde er vom Startpunkt aus über ungeladenem
Boden. Vereinfachung je Material statt eines gemeinsamen Anteils.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Eigenes Modell aus three-d-stage 1:1 an die Stelle der kleinen Gebäude
zwischen Baseball- und Fußballfeld: Aufbereitung mit gltf-transform unter
Erhalt von EXT_materials_bump, Orte-Schicht mit Laden je nach Abstand,
Kollision über Materialnamen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Acht Aufgaben, Code und Liste aus dem gegen die echte Vorlage geprüften
Prototyp; in Plan-Reihenfolge in einer Repository-Kopie nachgespielt.
Spec: Start und Autos ziehen von der Buchstabenkontur auf die
wiederaufgebaute Straße am Park, Nummern im Berichtsbeispiel.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Straßenenden über das Skelett samt Maske, Korridor statt Versatzgrenze,
Weiterführen über Kreuzungen, Ziel auf der anderen Seite, keine gemalten
Sackgassen, Kartenrand; Gebäudevorlage ab 100 m² im 150-m-Fenster;
randvoll vor dem Wachsen; Kontrastregel der Suche; Probelauf mit Kennzahlen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Alle Beschriftungen der Vorlage (Schriftzüge, Marker, Schilder, Symbole,
Straßennamen, Gitternetz) werden vor der Vektorisierung aus dem Klassenbild
entfernt und darunter wiederaufgebaut: Straßen laufen durch, Flächen
schließen sich, angebissene Gebäude werden geschlossen, verdeckte Blockteile
bekommen passende, als ergänzt markierte Gebäude. Ersetzt task_999fed4a.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Himmel: 4k-HDRI von Poly Haven (CC0, tools/assets/hdri.mjs) statt Sky-Shader. Kuppel zeigt das Foto
ab 12°, darunter weiches Band und Dunst in Nebelfarbe; Umgebungslicht und Spiegelungen per PMREM;
Sonne aus dem Bild gekappt und als Licht übernommen (77°, Stärke 2,5)
- Schatten: CSM mit vier Stufen (25/100/350/1000 m, je 2048²), Schutznetz für beleuchtete Materialien,
Normal-Bias 1,5 Texel je Stufe gegen Streifen auf doppelseitigen Materialien (Figur, Autos)
- Startfigur wirft Schatten, Palmen und Laternen empfangen Schatten, Wasser als ruhige See
- Debug-Panel: Sonne, Himmelslicht, Himmelshelligkeit; Namensnennung um das HDRI ergänzt
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>