Wie eine dreitägige Geometrie-Analyse eine akute Qualitätskrise aufdeckte, eine noch tiefere strukturelle Schwäche freilegte — und am Ende zu einer fundamental robusteren Produktionspipeline führte, die ihre eigenen Fehler jetzt selbst findet, bevor sie drucken.
Lifetiles druckt Marathon-Strecken als taktile Hexagon-Kacheln: Terrain, Gewässer, Brücken, Gebäude — als ein einziges, physisches Objekt. Solange der Kurs kompakt ist (ein Stadtmarathon), passt genug Maßstab für erkennbare Straßenzüge auf die Kachel. Bei lang gezogenen Strecken (Trail-Läufe, Karwendelmarsch, alles über ~15 km Ausdehnung) zwingt die Geometrie einen deutlich kleineren Maßstab — und genau dort brach das bisherige Gebäude-Rendering zusammen: Städte am Streckenrand (Innsbruck beim Karwendelmarsch, Garmisch-Partenkirchen beim GaPa Trail) wurden zu einem einzigen, formlosen schwarzen Klumpen.
Statt am Code zu raten, wurde das Addon in einem reproduzierbaren Blender-Container gegen drei reale Strecken laufen gelassen — München Marathon 2025 (kompakt), GaPa Trail Langstrecke (Stadt am Rand) und Karwendelmarsch (extrem lang gezogen) — mit echten, aktuellen Overture-Maps-Daten. Das deckte fünf zusammenhängende Konstruktionsfehler auf, von denen zwei überraschend gravierend waren:
| Track | Maßstab | Gebäude vorher → nachher | Fläche < 0,3 mm |
|---|---|---|---|
| München Marathon 2025 | 1:59.100 | 6.607 Klumpen → 21.310 echte Gebäude | 48,9 % → 2,1 % |
| GaPa Langstrecke | 1:60.600 | 771 → 3.384 | 63,8 % → ~2 % |
| Karwendelmarsch (Innsbruck) | 1:243.600 | 731 → 2.811 (als Siedlungsplateau) | 36,7 % → 1,0 % |
Die Lösung: eine echte geometrische Vereinigung statt der Hülle, eine Mindest-Wandstärke/-Spaltbreite als globale Druckbarkeits-Garantie (statt Hoffnung), und drei explizite Darstellungsstufen je nach Maßstab — Einzelgebäude in der Stadt, Blockebene im Übergang, ruhiges Siedlungsplateau bei extremen Trail-Maßstäben.
Kurz nachdem die Korrektur aus Akt 2 produktiv war, meldete die Qualitätskontrolle: die exportierten Druckdateien zeigten extrem dünne, gebrochene Wände und fehlende Dächer — schlechter als vor der Korrektur. Das war kein Rückfall in Akt 1, sondern ein neuer, eigenständiger Fehler: Der 3D-Druck-Export zerlegt jede Gebäude-Dachfläche in Dreiecke für den Drucker. Das bisherige Verfahren dafür (ein simpler „Fächer" von einer Ecke aus) war für die alten, immer konvexen Hüllen unsichtbar harmlos — funktioniert aber bei den neuen, realistisch geformten (und damit oft konkaven) Gebäudeumrissen aus Akt 2 nicht mehr zuverlässig.
Über alle 3.214 großen Dach-Flächen im München-Export erzeugte das alte Fächer-Verfahren 19 % reine Phantom-Fläche außerhalb der wahren Gebäudeumrisse — ein Einzelfall bis zu 830 % seiner echten Fläche. Direktes Rendern der tatsächlich exportierten Datei reproduzierte exakt die gemeldeten schwarzen, sternförmigen Dreiecks-Fetzen.
Die ehrliche Nachmessung zeigte zugleich: der Fix beseitigte die akute Katastrophe, brachte die Druckqualität aber noch nicht vollständig auf das alte Niveau zurück — ein Slice-Test zeigte trotz 22 % weniger Flächen eine 40 % höhere Fehlerdichte als die historische Basis. Diese Ehrlichkeit gegenüber den eigenen Zahlen führte direkt zu Akt 4: statt den Symptomdruck weiter zu jagen, wurde die gesamte Geometrie-Pipeline noch einmal grundsätzlich infrage gestellt.
Die Nachfrage aus Akt 3 — „können wir das grundsätzlich neu denken, jetzt wo wir eh dabei sind?" — löste eine vollständige Architektur-Untersuchung der Geometrie-Erzeugung aus. Ergebnis: die exportierte Kachel-Basis war noch nie im strengen Sinn wasserdicht gewesen — schon die Produktionsversion von vor dieser ganzen Analyse-Runde enthielt 1.945 offene Kanten und 6.703 mehrfach belegte Kanten, weil Terrain und Hexagon-Grundplatte sich an ihren Nahtstellen absichtlich überlappten und Blenders eingebautes 3D-Verschmelzungswerkzeug bei nicht perfekt „sauberer" Geometrie lautlos versagen konnte, statt einen Fehler zu melden. Slicer haben das bisher toleriert — aus Glück, nicht aus Garantie.
Die Lösung ersetzt Blenders bisherige Boolesche Verschmelzungs-Kaskade durch eine exakte CSG-Komposition auf Basis der Bibliothek manifold3d — derselben Technologie, mit der auch professionelle CAD-Systeme Volumenkörper verschneiden. Der gesamte Kompositions-Schritt läuft jetzt in einer eigenständigen, von Blender unabhängigen Codeschicht, die sich ohne die grafische Oberfläche automatisiert testen lässt. Jede exportierte Kachel-Basis ist dadurch nicht mehr „wahrscheinlich in Ordnung", sondern nachweisbar: null offene Kanten, null mehrfach belegte Kanten, geschlossenes Volumen — geprüft bei jedem einzelnen Export, mit hartem Abbruch statt stillem Weiterlaufen bei einem Defekt.
| Kennzahl der Basis-Geometrie | Historische Produktion | Heute |
|---|---|---|
| Offene Kanten (unvollständig geschlossene Flächen) | 1.945 | 0 |
| Mehrfach belegte Kanten (Geometrie-Duplikate) | 6.703 | 0 |
| Stille Fehlschläge bei Brücken/Wasser | unbemerkt möglich | werden gemeldet |
Beim ersten realen Volllauf mit der neuen, strengeren Prüfung fielen sofort zwei bereits vorher existierende Fehler auf, die das alte, tolerantere Verfahren einfach durchgewunken hatte: Gebäude, die den Boden nur knapp berührten statt richtig einzusinken (erzeugte hauchdünne Geisterflächen an der Kontaktstelle), und Brücken-Geometrie, die komplett „von innen nach außen" gewickelt war, ohne dass es je aufgefallen war. Beides wurde noch am selben Tag behoben — nicht weil ein Kunde es gemeldet hat, sondern weil das System es selbst nicht mehr durchließ.
Dieselbe Disziplin aus Akt 4 wurde konsequent auf Brücken angewendet: die alte, unbestimmt „gute" BMesh-Brückengeometrie (Deck + Bogen + Pfeiler, von Hand entworfen) wurde komplett durch eine deterministische, manifold-by-construction-Pipeline ersetzt — dieselbe CSG-Bauweise, mit der Akt 4 bereits Gebäude wasserdicht gemacht hat. Vier Phasen, jede unabhängig testbar: (1) deterministische Brücken-Topologie, (2) Regime A — die sichere Standard-Brücke, (3) Regime B — druckbare Hohlräume für Wahrzeichen-Brücken mit echter Durchfahrtshöhe, (4) Verifikation gegen echte Düsseldorfer und Münchner Daten.
Die echte Rheinkniebrücke bestand aus mehreren, eigentlich unzusammenhängenden Overture-Liniensegmenten, die zu einer Mittellinie verklebt wurden. Eine neue, bpy-freie Topologie-Stufe (Segmentgraph, kollineares Verketten, geometrisches Parallel-Merge) ersetzt das alte Ad-hoc-Verfahren; ein Sprung-Gate lässt nur noch echte, kurze Verbindungsnähte durch. Nebenbefund: die Wasserquerungs-Erkennung verglich Groß-/Kleinschreibung falsch und kannte Flüsse als Linien (statt Polygone) gar nicht — der Rhein selbst ist bei Overture eine LINESTRING, keine Fläche. Beides gefixt.
Jede Brücke wird standardmäßig als massiver, gepufferter Mittellinien-Damm vom Kachelboden bis zur höchsten Deckpunkt-Höhe extrudiert und in die Basis-Union fusioniert — 0 % unterstützungsfreier Überhang, weil geometrisch gar nicht anders möglich. Der alte, handentworfene BMesh-Brückenbauer (Deck+Bogen+Pfeiler) ist komplett entfernt.
Für als Wahrzeichen markierte Brücken mit ausreichend echtem Freiraum (Deck-Gradiente minus lokales Terrain, abzüglich der Widerlager-Zone an beiden Enden) baut eine zweite Geometrie-Regel ein flaches Deck + massive Widerlager + innere Stützwände — separates, eigenständig wasserdichtes 3MF-Objekt statt Fusion in die Basis.
Alle 9 echten Düsseldorfer und alle 4 verbleibenden Münchner Brücken bleiben bei ihrem Produktions-Maßstab (1:44.200 / 1:59.100) korrekt Regime A, mit Modell-Freiraum durchgängig innerhalb ±0,3 mm um null — der Causeway ist bei realistischen Maßstäben tatsächlich die richtige, sichere Standardwahl. Die Verifikation selbst deckte einen echten Fehler auf: die Mindest-Freiraum-Berechnung wurde von einem einzelnen Messpunkt direkt am Widerlager dominiert (wo Deck und Terrain sich konstruktionsbedingt auf ~0 annähern) statt vom eigentlichen Flussquerschnitt — behoben mit einer 8 % Rand-Ausschlusszone an jedem Ende.
Der Blick auf echte Renderings von Düsseldorf- und München-Marathon-Kacheln zeigt ein bislang unadressiertes Problem: an Flussufern, Biegungen und Mündungen erscheinen unruhige, scharfkantige Dreiecks-Fetzen statt einer glatten Wasserfläche — sichtbar dieselbe Handschrift wie die „sternförmigen Dreiecks-Fetzen" aus Akt 3, nur an Wasser statt an Dächern. Der Grund ist derselbe strukturelle: Wasser ist die einzige verbleibende Geometrie-Klasse, die die manifold-by-construction-Disziplin aus Akt 4 (Gebäude) und Akt 5 (Brücken) nie bekommen hat.
WaterBuilder.create_water_surface triangulierte die Fluss- und Seepolygone bislang mit Blenders eingebautem Ear-Clipping-Verfahren (tessellate_polygon) statt mit der manifold3d-CrossSection-Extrusion, die Gebäude und Brücken längst nutzen — und prüfte das Ergebnis nur auf grobe Nicht-Mannigfaltigkeit, nicht auf Qualität. Beim 3MF-Export wird dieselbe Fläche ein zweites Mal trianguliert, diesmal mit mapbox_earcut. Zwei unabhängige Ear-Clipper auf derselben Kontur, keiner davon manifold-garantiert.
Overture liefert Flüsse als lange, schmale, stark konkave Polygone mit Löchern (Inseln, unter Brücken hindurchgeschnittene Abschnitte) und vielen Stützpunkten. Genau dieses Konturprofil ist der Schwachpunkt jedes Ear-Clipping-Verfahrens: an Biegungen, Verengungen und Mündungen — dort, wo buffer()/simplify() beim Optimieren zusätzlich leicht verzerrt — entstehen fast-entartete, extrem spitze Dreiecke statt sauberer Flächen. Die grobe Mannigfaltigkeits-Prüfung im Builder erkennt das nicht: eine Kante ohne doppelte Belegung gilt als „in Ordnung", auch wenn ihr gegenüberliegendes Dreieck praktisch keine Fläche mehr hat. Nach der CSG-Verschneidung mit dem Terrain und der zweiten Re-Triangulierung beim 3MF-Export werden genau diese Splitter im Rendering sichtbar.
Die ehrliche Ausgangslage: Wasserfälle oder abrupte Höhensprünge im Flussverlauf wurden vor diesem Umbau nicht erkannt und nicht dargestellt. Jeder Wasser-Vertex bekam seine Höhe aus dem Geländemodell, aber statt dem tatsächlichen Verlauf zu folgen, wurde pro Polygon Richtung eines einzigen Mittelwerts geglättet (Ausreißer per Interquartilsabstand gekappt). Flüsse bekamen dabei nur 60 % der Glättungsstärke von Seen — sie behielten also etwas Gefälle gegenüber der reinen Ebene, aber das war eine Nebenwirkung der Glättungsstärke, kein Modell des tatsächlichen Gefälles entlang der Fließrichtung. Ein realer Sprung (Wehr, Kaskade, Wasserfall) wurde von diesem Verfahren einfach mit in den Mittelwert gezogen und als sanfte Diagonal-Rampe über die ganze Flussbreite verschmiert — die Stufe verschwand, statt gedruckt zu werden. Die Terrassierung unten (Roadmap-Schritt 03) behebt genau das.
Hier lohnt sich dieselbe Rechnung wie bei der Gebäudehöhen-Quantisierung aus Akt 2: Die Druckerauflösung erzwingt einen Z-Rasterschritt von Schichthöhe × Maßstab. Jede reale Höhenänderung, die kleiner als dieser Schritt ist, kann der Drucker ohnehin nicht auflösen — sie ist in diesem Sinn vernachlässigbar. Alles darüber ist echte, verlorene taktile Information.
| Track | Maßstab | Z-Rasterschritt | Typisches Flussgefälle | Bewertung |
|---|---|---|---|---|
| Düsseldorf Marathon (Rhein) | 1:44.200 | 4,4 m | Tiefland, Größenordnung Dezimeter pro Kilometer | Real weit unter einer Schicht — Glättung ist die richtige Vereinfachung. |
| München Marathon (Isar) | 1:59.100 | 5,9 m | Voralpin, Größenordnung Dezimeter bis niedrige Meter pro Kilometer | Ebenfalls unter einer Schicht auf Kachel-Distanz — vernachlässigbar korrekt. |
| Karwendelmarsch (Gebirgsbäche) | 1:243.600 | 24,4 m | Alpin, kann über wenige hundert Meter zweistellige Meterbeträge erreichen (Kaskaden, Wehre) | Klärt mehrere Schichten — real druckbar, wird aber aktuell weggemittelt. |
Für die beiden gemeldeten Städte ist die Antwort also: ja, die Neigung ist bei der aktuellen Glättung zurecht vernachlässigbar — das ist nicht die Ursache der Dreiecks-Artefakte, die liegt allein im Ear-Clipping. Für alpine Trail-Strecken ist die Vernachlässigung dagegen ein echter Informationsverlust und gehört mit ins Konzept, nicht nur die Kosmetik der Wasserfläche selbst.
Statt die von Overture gelieferte, oft unruhige Uferpolygon-Kontur direkt zu triangulieren, wird der Fluss wie schon die Brücke in Akt 5 zuerst als geordnete, geglättete Mittellinie mit Breitenprofil rekonstruiert und erst daraus eine saubere Bandfläche gepuffert. Konfluenzen und Verengungen — die Hauptquelle der Splitter — entstehen dann kontrolliert statt als Nebenprodukt von buffer()/simplify() auf einer bereits unsauberen Kontur.
WaterBuilder wechselt von tessellate_polygon + manuellem bmesh-Extrude auf dieselbe manifold3d-CrossSection-Extrusion, die Akt 4 für Gebäude etabliert hat. Damit entfällt die doppelte, unabhängige Ear-Clipper-Triangulierung (einmal im Builder, einmal beim 3MF-Export) zugunsten eines einzigen, manifold-garantierten Pfads — und ein zu dünnes oder entartetes Dreieck wird ein harter CI-Fehler statt eines stillen Renderartefakts, exakt das Muster aus Akt 4.
Z wird jetzt monoton entlang der geordneten Flussachse interpoliert (bergauf → talwärts) statt pro Polygon auf einen Mittelwert geglättet zu werden. Ein Gefälle-Gate über der halben Z-Rasterschritt-Schwelle je Segment (siehe Tabelle oben) erkennt echte Sprünge und modelliert sie als kurze, gedruckte Stufe statt als Diagonal-Rampe — dieselbe Idee wie das Sprung-Gate, das Akt 5 bereits für unzusammenhängende Brückensegmente eingeführt hat, nur angewendet auf Höhen- statt auf Lückensprünge.
Erfolgskriterium ist kein subjektiver Blick mehr, sondern eine harte Kennzahl: entartete Dreiecksfläche < ein fest definierter Schwellwert über alle Wasser-Meshes, gemessen direkt an der exportierten 3MF-Datei — nach demselben Muster wie die Slice-Nachmessung in Akt 3. Der Karwendelmarsch kommt als Regressionsfall dazu, weil er der einzige der vier Referenz-Tracks ist, auf dem ein echter Höhensprung den Z-Rasterschritt überhaupt überschreitet.
Alle fünf Schritte (D1–D5) sind umgesetzt und in main-fähigem Zustand: Fetcher, Optimizer und Builder wurden umgebaut, 70 neue/angepasste Unit-Tests grün, und beide gemeldeten Städte wurden im selben headless-Blender-Container aus Akt 2 zweimal generiert — einmal mit dem Stand vor diesem Akt, einmal danach, bei identischen Kamera-Koordinaten für den direkten Vergleich.
| Kennzahl (exportiertes Wasser-Mesh) | Düsseldorf, vorher | Düsseldorf, nachher | München, vorher | München, nachher |
|---|---|---|---|---|
| Deckflächen-Dreiecke mit Höhensprung | 72,9 % | 0 % | 81,9 % | 0 % |
| component_status[water] | success | success | success | success |
| Wasser-Shells beim 3MF-Export | wasserdicht | wasserdicht | wasserdicht | wasserdicht |
Gemessen direkt an den Dreiecken der exportierten Wasserfläche (nicht geschätzt): eine Deckflächen-Kappe zählt als „Höhensprung", wenn ihre drei Eckpunkte nicht auf derselben Ebene liegen — der exakte Mechanismus hinter den gemeldeten Artefakten. Nach dem Umbau ist dieser Anteil an beiden Kacheln exakt null, weil jede Wasserfläche jetzt buchstäblich eine Ebene ist statt es zufällig zu sein.
Der volle Übersichts-Render (nicht zugeschnitten) zeigt einen zweiten, unabhängig bestätigten Effekt von D2: ein Teich in der Nähe der Isar erscheint nach dem Umbau mit einer sichtbaren Innenkontur (Insel) statt als geschlossener Klecks — der Fetcher liest jetzt die Innenringe, die vorher beim Parsen stillschweigend verworfen wurden.
Terrain/Basis (Akt 4), Gebäude (Akt 2/4) und Brücken (Akt 5) teilen sich bereits eine gemeinsame, manifold-by-construction-Bauweise. Wasser ist die letzte Ausnahme mit einer eigenständigen, älteren Triangulierungs-Logik. Akt 6 ist kein neues Feature, sondern schließt die letzte Lücke in einer bereits etablierten Architektur-Entscheidung.
Akt 1 hat das settlement_footprint-Regime eingeführt: bei sehr gestreckten Strecken (Karwendelmarsch, 1:243.600) wird die Stadt nicht mehr einzeln, sondern als ruhiges, zweilagiges Plateau gerendert — Innsbruck als taktile Fläche statt als schwarzer Klumpen. Das Plateau entstand bis dahin aus der Vereinigung aller Gebäude-Footprints in der Kachel. Das druckt zuverlässig, ist aber die falsche Datenquelle: ein Gebäude-Footprint beantwortet „wo steht ein Haus", nicht „wo ist Siedlung" — und Overture liefert die eigentlich richtige Antwort längst mit, klassifiziert nach Nutzungsart, im land_use-Datensatz. Issue #287 macht genau diesen Punkt und bringt bereits eine eigene Live-Recherche gegen Innsbruck/Karwendelmarsch mit.
Anders als zunächst geplant blieb es nicht bei der reinen Vorbereitung: ein neuer land_use_fetcher.py (gleiche Bauweise wie Wasser/Brücken), die Anbindung ans settlement_footprint-Regime samt automatischem Fallback auf die alte Gebäude-Union, und ein togglebares SSOT-Setting (settlement_use_land_use, Blender-UI und Web-Wizard) wurden noch am selben Tag umgesetzt — und gegen den echten Karwendelmarsch-Track verifiziert, der als GPX-Fixture zum ersten Mal überhaupt im Repo existiert.
Die bestehende REGIME_SETTLEMENT-Verarbeitung liefert schon seit Akt 1 eine reine Datenstruktur (Grundriss + Löcher + Höhe) an die Geometrie-Pipeline — dieselbe Form, die Akt 2/4 für einzelne Gebäude und Akt 5 für Brücken etabliert haben. Die manifold3d-Komposition (core/geometry/csg.py) weiß gar nicht, ob diese Form von einem Gebäude oder einer Siedlungsfläche stammt. Der Umbau blieb dadurch fast vollständig auf die Fetch-Ebene beschränkt — die watertight-by-construction-Garantie aus Akt 4 gilt automatisch mit, ohne dass die Komposition selbst angefasst werden musste.
Derselbe reale Track, derselbe Maßstab (1:243.600), zweimal generiert — einmal mit der alten Gebäude-Footprint-Union, einmal mit der neuen land_use-Quelle. Der Zoom zentriert automatisch auf die Bounding Box des Building-Objekts — bei diesem Track ist das praktisch deckungsgleich mit Innsbruck am südlichen Streckenrand, dem einzigen nennenswerten Siedlungsgebiet der ganzen Strecke.
| Kennzahl | Vorher (Gebäude-Union) | Nachher (land_use) |
|---|---|---|
| Plateau-Quelle | 370 Building-Solids | 930 land_use-Kandidatenteile |
| Base (komponiert) | 53.949 verts / 107.910 tris | 52.261 verts / 104.526 tris |
| Base-Genus (Tunnel/Löcher durch den Körper) | 4 | 2 |
Korrektur: Genus zählt topologische Tunnel/Löcher durch einen einzelnen zusammenhängenden Körper (wie das Loch im Donut), nicht die Anzahl getrennter Objekte — die ursprüngliche Lesart hier war falsch. Genus 4 → 2 heißt: die Gebäude-Union-Base hat 4 echte Löcher, die land_use-Base hat 2 — weniger Löcher mit der neuen Quelle ist weiterhin ein positives Signal, aber beide Varianten liefern noch echte Löcher, nicht null.
1. Cache-Key-Bug (im eigenen neuen Code, noch am selben Tag behoben): der Optimierungs-Cache-Schlüssel für Gebäude berücksichtigte settlement_use_land_use zunächst nicht — beim Umschalten der Einstellung auf eine bereits optimierte Bbox wurde stillschweigend das Ergebnis des ersten Laufs weiterserviert. Beide Vorher/Nachher-Läufe lieferten dadurch zunächst identische Gebäudezahlen, bis der Schlüssel korrigiert wurde. Regressionstest ergänzt.
2. Ein vorbestehender Terrain/CSG-Defekt (nicht hier behoben, als #297 gemeldet): der 3MF-Export scheitert bei Karwendelmarsch an einer entarteten Base-Shell (64 Faces, is_volume=False) — identisch in beiden Läufen trotz komplett unterschiedlicher Gebäude-/land_use-Eingabe, was beweist: die Ursache liegt im gemeinsamen Terrain/Kompositions-Code, nicht im Fetcher dieses Akts. Aufgefallen, weil vor diesem Akt nie ein settlement_footprint-Fixture mit echtem Alpin-Gelände existierte. Bewusst nicht in diesem Akt mitgelöst.
Beim Sichten der Renderings fielen in einem rein internen Analyse-Werkzeug (nicht im Export, nicht im gedruckten Modell) vereinzelt schwarze Flecken auf. Ausführlich geprüft und zurückverfolgt: ein reines Anzeigeproblem des Debug-Werkzeugs selbst (zwei übereinanderliegende Vorschau-Ebenen), ohne jede Auswirkung auf exportierte Dateien oder gedruckte Kacheln. Zur Sicherheit trotzdem behoben, damit das Werkzeug auch für zukünftige Analysen sauber bleibt.
Dieselbe DuckDB/httpfs-Abfrageform wie water_fetcher.py und bridge_fetcher.py, gefiltert auf urbane Klassen (residential, developed/commercial|industrial|retail, optional Bildung/Medizin/Religion). Straßen- und Bahnkorridore werden mit der bereits für Brücken vorhandenen Klassen-Breitentabelle gepuffert und vom Siedlungs-Polygon abgezogen — keine neue Breiten-Heuristik nötig.
Wohngebiet, Gewerbe/Zentrum und Industrie werden getrennte, disjunkte Höhenstufen (2/3/2 Druckschichten) statt der vorherigen einheitlichen Zwei-Schicht-Platte — mehr taktile Information bei gleicher Druckbarkeits-Garantie.
settlement_use_land_use (Default: an) folgt ADR-0017 — einmal im Schema definiert, propagiert es automatisch in beide Oberflächen. Fällt explizit protokolliert auf die alte Gebäude-Union zurück, wenn land_use für eine Bbox keine Daten liefert oder der Fetch fehlschlägt.
benchmarks/fixtures/Karwendelmarsch.gpx (4.158 Punkte, deckt sich mit ADR-0036s gemessener 32,9×6,5 km-Bbox) ist der erste jemals eingecheckte Regressionsfall für dieses Regime — vom Nutzer selbst beigesteuert und persistiert, nachdem die Planungsrunde die Lücke identifiziert hatte.
ADR-0040 begann als reine Planungs-/Explorationsrunde, wie von der Issue gefordert. Der Nutzer entschied sich anschließend, Phase 1 direkt umzusetzen und als togglebares Feature auszuliefern statt nur zu dokumentieren — inklusive echtem Vorher/Nachher gegen reale Daten, zwei gefundenen Fehlern (einer im neuen Code behoben, einer als eigenständiges Issue #297 sauber abgetrennt) und einem neuen, dauerhaften Regressions-Fixture. Phase 2 (Dichte-Modulation „Innenstadt vs. Randbezirk") bleibt bewusst ausgeklammert.
Akt 7 endete mit einer offenen Frage: derselbe entartete Shell (64 Faces, is_volume=False, Volumen −0,127) im exportierten Karwendelmarsch-Export, byte-identisch in zwei Läufen mit komplett unterschiedlicher Gebäude-/land_use-Eingabe — also musste die Ursache im gemeinsamen Terrain- oder Kompositions-Code liegen, nicht im Fetcher. Diese Sitzung hat den gemeinsamen Code isoliert nachgestellt (dieselbe to_manifold/compose_tile-Kette, aber ohne Blender, mit einem synthetischen Cutter) und den echten Verursacher gefunden: ein Wasser-Schnitt, der bei diesem Maßstab nie die Geländeoberfläche durchbricht.
Wasser wird bewusst zweimal verwendet — einmal von der Basis-Geometrie subtrahiert (schneidet eine Vertiefung ins Gelände), einmal als eigenständiges, eingefärbtes Objekt in genau diese Vertiefung gesetzt. Ohne den Schnitt läge das blaue Wasser-Objekt nur lose auf dem Gelände und würde beim Slicen mit der Landoberfläche kollidieren — der Schnitt selbst ist also die richtige Absicht. Die feste Druck-Extrusion (water_extrusion_up/down_mm, 0,2/0,4 mm) wird aber über den Maßstab in reale Meter zurückgerechnet: bei 1:243.600 werden daraus ~146 m reale Schnitttiefe. Auf dem steilen Alpenprofil des Karwendelmarsches bricht dieser Schnitt die geglättete Geländeoberfläche nie — die Subtraktion schneidet dadurch keine offene Rinne, sondern eine vollständig eingeschlossene, unsichtbare Blase.
Ein eigenständiges Repro (Base-Solid minus vergrabener Wasser-Quader, exakt derselbe manifold3d-Pfad wie die Produktions-Pipeline) liefert dieselbe Signatur wie der Original-Fehler: watertight=True, winding_consistent=True, is_volume=False, negatives Volumen — genus=-1 bestätigt die eingeschlossene Kavität. Ein direkter Abgleich der Bounding-Box im echten Export bestätigte zusätzlich, welches der elf Wasser-Objekte es war: Water_Surface_024, Zentrum [7899, 10290, 2363] — auf drei Meter genau derselbe Punkt, den die Original-Meldung als center=[7899.2, 10290.0, 2362.9] nannte.
Der Fix: compose_tile (core/geometry/csg.py) erkennt nach der Komposition jede vollständig eingeschlossene Shell mit negativem Volumen und füllt sie — eine gefangene, unerreichbare Kavität trägt ohnehin keine druckbare Information, das Auffüllen ist die geometrisch korrekte Auflösung, kein Datenverlust. Echte getrennte Volumen (disjunkte Gebäude-Shells, mehrteiliges Wasser) bleiben unangetastet; offene oder inkonsistent gewickelte Shells fallen weiterhin durch die Prüfung. Ein Regressionstest (test_buried_water_cutter_fills_enclosed_void) hält den Fall fest.
Nebenfund, gleich mitbehoben: terrain_builder.pys Seitenwände waren als Dreiecks-Fächer von einer einzigen Bodenecke konstruiert — eine Konstruktion, die nur auf einem „sternförmigen" Höhenprofil überschneidungsfrei bleibt. Auf einem echten, nicht-monotonen Alpenprofil (Grat–Tal–Grat) überlappt sich der Fächer selbst. In der Praxis unsichtbar (das rohe Terrain-Objekt wird nach der CSG-Vereinigung versteckt, und die Überschneidung blieb zufällig exakt planar, erzeugte also nie einen 3D-Hohlraum) — aber grundsätzlich falsch. Ersetzt durch einen Quad-Strip mit einem Bodenpunkt direkt unter jedem Kanten-Vertex, exakt nach demselben Muster wie die Top-Fläche ihre eigenen Zellen baut — dadurch beweisbar überschneidungsfrei, unabhängig vom Höhenprofil.
| Test | Vorher | Nachher |
|---|---|---|
| Adversarielles Grat/Tal-Höhenprofil, alte Fächer-Seitenwand | 21 überlappende Dreieckspaare | 0 (Quad-Strip, beweisbar) |
| .3mf-Export, echter Karwendelmarsch-Track im Container | Validation error, Shell 7: is_volume=False, vol=−0,127 | Erfolgreich: 7 Shells, alle wasserdicht |
| Integrationstest (headless Blender, echte Overture-Daten) | ✗ FAILED | ✓ PASSED |
A/B im echten Container, nicht nur im Unit-Test: derselbe Karwendelmarsch-Lauf, einmal mit dem alten, einmal mit dem neuen Code, bei identischem Datencache. Der alte Code reproduzierte den Original-Fehler byte-genau (volume=-0.127036, 64 Faces); der neue Code exportiert dieselbe Kachel sauber.
Auslöser war ein Nutzer-Screenshot vom München-Marathon-Tile: rund um den Marienplatz stand ein auffällig hoher, uniformer Block, deutlich über der Umgebungshöhe — in der echten Innenstadt gibt es dort so etwas nicht. Die erste Vermutung (explizite num_floors-Werte in den Overture-Daten) ließ sich schnell widerlegen: vier Gebäude nahe der vermuteten Stelle hatten tatsächlich eine explizite height von 40–41 m — aber ohne num_floors, also lief die vermutete num_floors × 3.5-Fallback-Kette gar nicht.
Der eigentliche Mechanismus lag eine Stufe tiefer, in _printability_pass() (core/fetchers/building_fetcher.py, ADR-0036): Im city_detail-Regime werden alle Gebäude-Footprints global auf Lücken < min_gap_lines × Düsenbreite geprüft und verschmolzen — in der dicht bebauten Altstadt (Gassenbreite oft < 18 m bei diesem Maßstab) verschmilzt praktisch der gesamte Block zu einem einzigen Teil. Für diesen Teil gilt dann height = max(heights) — eine hohe Höhe reicht, um alle Nachbarn mitzuziehen.
Eine direkte DuckDB-Abfrage über den Marienplatz-Bereich zeigte: im Baseline-Verfahren verschmelzen 532 von 637 Gebäuden zu einem einzigen Teil und übernehmen die 91 m Höhe von St. Peter — nicht nur die ursprünglich vermuteten 40 m. Overture liefert für viele Gebäude auch ein names.primary-Feld; unter den benannten Gebäuden fand sich exakt der ursprüngliche Verdächtige wieder: „Talbrucktor", 40 m, ein schmales Torbogen-Gebäude ohne num_floors.
Vier Ansätze lokal gegeneinander getestet (echte Marienplatz-Geometrie, echte Druckbarkeits-Parameter bei 1:59.100): globales max() pro verschmolzenem Teil (Baseline), Höhen-Ratio-Clustering, diskrete Höhen-Bänder, und schließlich — nach dem Hinweis, dass kleine Kioske/Nebengebäude gar nicht individuell erhalten werden müssen — die Idee, nur benannte Gebäude aus dem Merge-Pool auszuschließen und individuell zu verarbeiten, während der Rest unverändert durch die bestehende Logik läuft.
| Ansatz | Max. Höhen-Ratio je Teil | Größtes Cluster | Talbrucktor korrekt? |
|---|---|---|---|
| Baseline (aktuell in Prod) | 28,7× | 532 Gebäude | Nein — auf 91 m gezogen |
| Höhen-Ratio-Clustering | 6,6× | 121 | Teilweise — Ketteneffekt |
| Diskrete Höhen-Bänder | 2,2× | 20 | Verschwindet (zu schmal, unprintable) |
| Named-Exclusion (gewählt) | 7,0×(regulärer Pool, ohne Landmarks) | 133(regulärer Pool) | Ja — 40 m, eigener Footprint |
Der Fix: Gebäude mit einem Overture-Namen werden jetzt aus dem DBSCAN-Clustering und aus dem globalen Gap-Closing-Pool des city_detail-Regimes ausgeschlossen und einzeln verarbeitet — eigene Höhe bleibt erhalten, ein zu schmaler Footprint (wie das Torbogen-Gebäude) wird per Closing-Operator auf Mindestwandbreite aufgeblasen statt verworfen. Der reguläre Gebäudebestand (92 % aller Gebäude in dieser Kachel sind unbenannt) durchläuft exakt dieselbe, unveränderte Logik wie zuvor — kein Verhaltensunterschied, keine zusätzlichen Kosten für den Großteil des Bestands.
Nebenfund beim echten Blender-Lauf: der Fix legte einen zweiten, unabhängigen Bug in der CSG-Komposition offen. Ein einzeln verarbeitetes, aufgeblähtes Namens-Gebäude direkt neben einem regulär verschmolzenen Nachbarn erzeugte beim Boolean-Union eine hauchdünne, deckungsgleiche Artefakt-Shell (4 Faces, Volumen exakt 0 m³) — wasserdicht, aber kein echter Körper, und damit vom Export-Gate zurückgewiesen. _fill_enclosed_voids() (core/geometry/csg.py, bereits seit Akt 8 für eingeschlossene Hohlräume zuständig) erkennt jetzt zusätzlich exakt diese Nullvolumen-Flachschale und verwirft sie — dieselbe „trägt keine druckbare Information"-Logik, nur für einen zweiten Degenerations-Fall.
| München-Kachel (1:59.100) | Vorher | Nachher |
|---|---|---|
| Gebäude-Objekte gesamt | 4.093 | 5.597 |
| Davon einzeln (benannt) | 0alle im Merge-Pool | 1.923 |
| 3MF-Export | —(vor dem Fix erfolgreich) | Erst ✗ (Nullvolumen-Shell) → ✓ nach CSG-Fix |
| Integrationstest (headless Blender, echte Overture-Daten) | ✓ PASSED | ✓ PASSED |
Auslöser war wieder ein reales Rendering, diesmal vom Nutzer selbst geschickt: an einer Straßenkreuzung verschwand die eigene Streckenführung — das emotionale Zentrum jeder Kachel — sichtbar unter einem zusammenhängenden Gebäudeblock. Der Mechanismus dahinter ist derselbe globale Gap-Closing-Schritt aus ADR-0036/Akt 9: Lücken unter min_gap_lines × Düsenbreite werden geschlossen, weil ein Slicer sie ohnehin verschmelzen würde. Für eine schmale Gasse ohne Strecke ist das genau richtig. Läuft aber die eigene GPX-Strecke exakt durch diese Gasse, wird sie vom selben Schritt einfach mitverschluckt — nichts in der Pipeline kannte bis dahin den Unterschied zwischen „irgendeine Gasse" und „die Gasse, die die Marathonstrecke selbst ist".
Der erste Entwurf hätte den Schnitt direkt in BuildingFetcher.process_raw_buildings() eingebaut und den Optimierungs-Cache-Schlüssel um Track-Punkte erweitert — dieselbe Stelle, an der Akt 7 bereits settlement_use_land_use ergänzt hatte. Der Nutzer stoppte das während der Umsetzung selbst mit einer einfachen Frage: „wieso muss ich denn anders fetchen, wenn ich nur track carven möchte?" — zu Recht: der Fetch/Cluster-Schritt bleibt unverändert, der Schnitt ist eine reine, günstige 2D-Nachbearbeitung der bereits fertigen Footprints. Der Cache wird für teure Overture-Abfragen gebraucht, nicht für eine Shapely-difference, die pro Lauf Millisekunden kostet — sie dort hineinzuziehen hätte nur zusätzliche Cache-Key-Fläche geschaffen, ohne jeden Nutzen.
Ein neues, reines Modul (core/geometry/track_corridor.py) puffert die GPX-Mittellinie zu einem Korridor (ein konfigurierbares Vielfaches der Track-Eigenbreite) und zieht ihn per Polygon.difference von jedem bereits fertigen Gebäude-/Plateau-Footprint ab — dieselbe „Linie puffern, von Flächen abziehen"-Technik, die land_use_fetcher.py seit Akt 7 für Autobahn-/Bahn-Barrieren nutzt, hier nur generisch nach jedem Maßstab-Regime statt nur im Siedlungsplateau-Zweig. Der Aufruf sitzt im Operator, direkt nach dem bestehenden, unveränderten process_raw_buildings()-Aufruf — BuildingFetcher selbst, sein Optimierungs-Cache und core/geometry/csg.py bleiben komplett unangetastet.
Die reale München-Marathon-Kachel (1:59.100, city_detail-Regime) zeigt: das ist kein Einzelfall. Über die gesamte Strecke hinweg schneidet der Korridor 145 Gebäude-/Blockteile zurück und verwirft 150 weitere vollständig — das sind 150 Stellen, an denen vor dieser Korrektur ein Gebäudeblock die Strecke komplett unter sich begraben hätte, nicht nur angeschnitten. Mehrere davon liegen im selben Altstadt-Ausschnitt, den schon Akt 9 am Marienplatz untersucht hat — derselbe dichte Gassenraster, nur diesmal mit der eigenen Streckenführung mittendurch.
| Kennzahl (ganze Kachel, München 1:59.100) | Wert |
|---|---|
| Korridor-Gesamtbreite (Track-Eigenbreite × Faktor) | 59,1 m × 1,5 = 88,65 m |
| Gebäude-/Blockteile zurückgeschnitten | 145 |
| Gebäude-/Blockteile vollständig verworfen (Strecke wäre komplett begraben) | 150 |
| davon zusätzlich durch die Mindestwandbreiten-Schranke bereinigt | 79 |
| Gebäudeteile gesamt | 5.422 → 5.278 |
| Base (komponiert) | 151.520 verts / 303.108 tris → 146.392 verts / 292.852 tris |
Zahlen direkt aus dem echten, headless generierten Debug-Log (core.track_corridor), nicht geschätzt — inklusive der beiden Nachschärfungen unten (Korridor-Faktor 1,5× statt eines festen Sicherheitsabstands, Mindestwandbreiten-Schranke). Track-Eigenbreite bei diesem Maßstab: 1,0 mm Modellbreite × 59.100 Skalierung = 59,1 m real.
Akt 9 korrigierte, welche Höhe ein verschmolzener Block bekommt. Dieser Akt ändert nicht, ob und wie Gebäude verschmelzen dürfen — eine Gasse ohne Strecke darf weiterhin ganz regulär zu einem Block schließen, das ist gewollt und bleibt unverändert. Er garantiert nur zusätzlich: wo auch immer die eigene Strecke verläuft, bleibt sie frei, unabhängig davon, ob der überdeckende Teil ein regulär verschmolzenes Blockstück oder — wie mehrere der betroffenen Teile — ein einzeln verarbeitetes, benanntes Landmark-Gebäude ist.
Zwei berechtigte Einwände kamen direkt nach dem ersten Entwurf zurück, beide noch am selben Tag umgesetzt:
Beide Nachschärfungen zusammen erklären, warum die Zahlen oben (145/150/79) deutlich einschneidender ausfallen als im allerersten Durchlauf (193/33) — der Korridor ist mit Faktor 1,5 breiter als der ursprüngliche feste 1 m-Sicherheitsabstand, und die Mindestwandbreiten-Schranke räumt zusätzlich 79 Reste weg, die vorher als knapp-druckbare Splitter durchgerutscht wären.
Anstoß war eine ganz praktische Frage: Wie druckt man Düsseldorfs Hochhäuser — Rheinturm, ARAG-Hochhaus, Mannesmann-Hochhaus — ohne dass sie als dünne, verzugsanfällige Stifte scheitern? Eine echte Overture-Analyse der Düsseldorf-Marathon-Kachel (43.283 Gebäude, 1:44.200 — der tatsächliche Auto-Maßstab dieser Strecke) fand die exakte Ursache, keine Vermutung: gedrucktes Aspektverhältnis = reales Aspektverhältnis × z_scale. Die Höhenüberhöhung (nötig, damit Geländerelief überhaupt sichtbar wird) multipliziert die Schlankheit jedes Gebäudes 1:1 mit sich selbst — bei einem echten, mäßig schlanken Turm (Rheinturm, reales Aspektverhältnis 4,7) macht z_scale = 2,5 daraus beim Druck ein Aspektverhältnis von 11,9 — ein 4,7 mm hoher, 0,86 mm breiter Stift.
Von 43.283 Gebäuden in der Kachel überschreiten nur 62 (0,1 %) ein Aspektverhältnis von 5 — aber genau das sind die Wahrzeichen, an denen man sich in einer Stadt orientiert: Rheinturm, ARAG-Hochhaus, Mannesmann-Hochhaus, Stadttor, Sign!. Eine pauschale Lösung (z. B. z_scale für alle Gebäude senken) hätte 99,9 % aller Gebäude unnötig mitgetroffen und die ohnehin schon niedrigen Standardhäuser weiter Richtung Sichtbarkeitsgrenze gedrückt.
Der Fix — zwei unabhängige Maßnahmen, beide nur oberhalb einer Knick-Höhe aktiv: Erstens wird die Höhe oberhalb von landmark_height_knee_m (Default 30 m) logarithmisch statt linear skaliert — C1-stetig am Knick, damit kein sichtbarer Sprung zwischen „normalem Gebäude" und „Landmark" entsteht; unterhalb von 30 m ist die Formel exakt identisch zur bisherigen linearen Skalierung. Zweitens weitet sich das untere landmark_taper_fraction (Default 30 %) der (bereits gedämpften) Höhe zu einem verbreiterten Sockel auf — eine Smoothstep-Kurve (nullsteigend an beiden Enden, damit weder am Boden noch am Übergang zur wahren Silhouette eine Kante entsteht), dimensioniert, damit das Aspektverhältnis am Sockel nie landmark_max_base_aspect (Default 4,0) überschreitet. Downward-widening heißt außerdem: jede Schicht ist gleich breit oder breiter als die darüber — von Natur aus überhangfrei, keine Stützen nötig.
| Landmark (real, Düsseldorf 1:44.200) | Höhe real | Aspekt vorher | Aspekt nachher | Sockel-Verbreiterung |
|---|---|---|---|---|
| Mannesmann-Hochhaus | 88 m | 14,1× | 4,0× | 2,47× |
| ARAG-Hochhaus | 125 m | 13,2× | 4,0× | 1,90× |
| Rheinturm | 179 m | 11,9× | 4,0× | 1,38× |
| Hyatt Regency Düsseldorf | 70 m | 9,7× | 4,0× | 1,88× |
| Colorium | 62 m | 9,0× | 4,0× | 1,86× |
| Alle 28 Landmarks > 30 m (max.) | — | 14,1× | ≤ 4,0× | ≤ 2,47× |
Ein zweiter, unabhängiger Bug beim echten Blender-Lauf gefunden: der Sockel wird intern aus 8 gestapelten Kegelstumpf-Segmenten plus einem geraden Aufsatz zusammengesetzt (Boolean-Union, manifold3d). Bei realen — auch konvexen! — Düsseldorf-Footprints kam dabei nicht ein zusammenhängender Turm heraus, sondern mehrere disjunkte, einzeln wasserdichte Teilschalen, unsichtbar für jede reine „ist es wasserdicht"-Prüfung, nur durch Zählen der zusammenhängenden Komponenten auffindbar. Direkt am realen Colorium-Footprint reproduziert und eingegrenzt auf zwei unabhängige Ursachen: Segmente, die sich nur exakt berühren statt echt zu überlappen, verschmilzt manifold3ds exakte Boolesche Union nicht zuverlässig — und dieselbe Union verliert bei den realen Kachel-Weltkoordinaten (mehrere tausend Meter vom Ursprung entfernt) selbst mit Überlapp noch genug Präzision, um Segmente getrennt zu lassen. Behoben, indem der komplette Sockel in lokalen, zentroid-relativen Koordinaten aufgebaut und erst das fertig verschmolzene Ergebnis an seine Weltposition verschoben wird.
| Reale Düsseldorf-Landmarks (28 Türme > 30 m) | Vorher (exaktes Berühren, Weltkoordinaten) | Nachher (lokale Koordinaten + Überlapp) |
|---|---|---|
| Türme mit > 1 disjunkter Schale | 14 / 28 | 0 / 28 |
| Shells im exportierten Landmarks-Objekt (echter Container-Lauf) | 120(aus nur 24 Solids) | 22(aus 24 Solids — plausibel) |
| Watertight-Gate beim Export | ✓ bestanden(Einzel-Schalen waren je für sich wasserdicht — genau das Tückische) | ✓ bestanden |
Die Fetcher-Logik, die Landmarks aus dem Merge-Pool heraushält, war schon vorher da (Akt 9) und schützt jedes benannte Gebäude vor fremdbestimmter Höhe — nicht nur hohe Türme. Erst diese Session fügte die Härtung (Höhendämpfung + Sockel) hinzu, die aber nur für Gebäude über der Knick-Höhe überhaupt etwas verändert. Ursprünglich landeten deshalb alle 506 benannten Gebäude der Kachel im separaten Landmarks-3MF-Objekt — auch ein zweistöckiges, namentlich erfasstes Café. Das ist geometrisch harmlos (Sockel-Verbreiterung fällt bei kurzen Gebäuden auf 1,0× zurück), aber unnötig: ein Nutzer, der in Bambu Studio ein eigenes Druckprofil auf Landmarks legt (höhere Mindest-Schichtzeit, volle Kühlung), würde das unabsichtlich auf 506 Objekte anwenden und die ganze Kachel verlangsamen, obwohl nur 24 davon die Härtung überhaupt brauchen. Jetzt läuft nur, wer die Knick-Höhe tatsächlich überschreitet, über den separaten Pfad — alle anderen (auch weiterhin vor Merge geschützt) exakt wie zuvor durch build_building_solids, byte-identisches Ergebnis.
Anstoß war die Nutzerfrage zu einem selbst hochgeladenen 60 km-Ultramarathon-Track (11,9 × 8,7 km Bbox) — bei diesem Zuschnitt landet der Auto-Maßstab bei 1:96.300, deutlich im block_level-Regime (ADR-0036). Der Verdacht: „gefühlt sind die Gebäude so klein, dass sie beim Slicen rausfliegen." Eine echte Regenerierung der Kachel (98.076 rohe Overture/OSM-Footprints → 1.418 druckbare Teile) bestätigte das mit Zahlen statt Gefühl: 68,8 % der 573 Landmark-Gebäude druckten unter einem einzigen 0,2 mm-Layer, 97,9 % unter zwei.
Genau genommen sind die übrigen 845 Teile keine Einzelgebäude, sondern Cluster aus im Schnitt drei, maximal 60 verschmolzenen Rohgebäuden (cluster_size) — das block_level-Regime flacht sie ohnehin auf eine gemeinsame Höhe ab (Akt 9). Nur die 573 „Landmarks" sind wirklich einzelne, unveränderte Gebäude: jedes mit einem Overture-name-Tag, plus die preserve_landmarks_per_tile-höchsten pro Kachel — Akt 9s Sicherheitsnetz gegen Höhen-Ausreißer, hier zum ersten Mal an einer Kachel mit diesem Regime und dieser Größe durchgerechnet.
_printability_pass_merge() floort jeden verschmolzenen Cluster im block_level-Regime bereits seit ADR-0036 auf min_flat_height_raw = 2,02 × Layer-Schritt / z_scale — bei dieser Kachel 15,56 m real, gedruckt exakt 0,404 mm (≥ 2 Layer bei 0,2 mm Layerhöhe). _printability_pass_standalone() — der Pfad, den jedes benannte Gebäude seit Akt 9 nimmt, um nicht in die Höhe seiner Nachbarn gezogen zu werden — weitet zwar den Footprint auf Mindestwandbreite auf, rührt die Höhe aber überhaupt nicht an. Ein x-beliebiger namentlich erfasster Schuppen mit dem OSM-Default von 7 m Höhe blieb bei 0,182 mm — weniger als die Hälfte eines einzelnen Layers.
Der Fix: _printability_pass() berechnet dieselbe Floor jetzt auch für den Standalone-Pfad — aber ausdrücklich nur im block_level-Regime. Im city_detail-Regime bleibt die echte Höhendifferenzierung pro Landmark unangetastet, exakt die Eigenschaft, die Akt 9 am Marienplatz-Fall erst erkämpft hat (Talbrucktor 40 m, zwei Kirchtürme, echte Höhenvarianz) — dort würde dieselbe Floor die gerade gewonnene Differenzierung wieder verwaschen. settlement_footprint erreicht diesen Codepfad ohnehin nie (eigener Dispatch-Zweig, siehe Akt 7).
| Ultramarathon-Kachel (1:96.300, block_level) | Vorher | Nachher |
|---|---|---|
| Landmarks unter 0,2 mm (≤ 1 Layer bei 0,2 mm) | 68,8 %(n=394) | 0 % |
| Landmarks unter 0,4 mm (≤ 2 Layer bei 0,2 mm) | 97,9 %(n=561) | 0 % |
| Gemergte Cluster betroffen | 0 %(schon vorher floor-geschützt) | 0 %(unverändert) |
| Von der Floor angehobene Landmarks | — | 561 / 573 |
Parallel zur Floor kam die Frage auf, ob Gebäude nicht generell etwas prominenter wirken sollten — ohne dafür z_scale_factor hochzudrehen, was zwangsläufig auch das Gelände überhöht (derselbe Faktor treibt terrain_builder.py und die Gebäude-Extrusion gemeinsam). Die naheliegende erste Idee — den neuen Faktor einfach in BuildingFetchers eigenes z_scale mit einrechnen — hätte sich für die floor-dominierten Cluster selbst aufgehoben: min_flat_height_raw ist bewusst invers zu z_scale definiert, damit die geflootete Druckhöhe unabhängig vom gewählten z_scale konstant bei zwei Layern bleibt — ein zusätzlicher Faktor an derselben Stelle hätte sich exakt herausgekürzt und genau die Mehrheit der Gebäude (845 von 1.418) komplett ignoriert. Der Boost sitzt stattdessen ausschließlich an der Extrusionsstelle (build_building_solids/build_landmark_solids, core/geometry/solids.py) — building_height_boost (Default 1,0, Schritte von 0,1) multipliziert dort auf den bereits gefloorten Rohwert und wirkt dadurch gleichmäßig auf Cluster und Landmarks in allen drei Maßstabs-Regimes, während z_scale_factor allein für das Gelände zuständig bleibt.
Zwei neue Regressionstests (test_tiny_named_building_floored_in_block_level_regime, test_tiny_named_building_unaffected_in_city_detail_regime) bauen genau diesen Fall nach und belegen beide Regime-Grenzen; die komplette Addon-Testsuite (735 Tests, reine Python-Fetcher-/Schema-/Geometrie-Logik ohne Blender) bleibt grün.
Auslöser war wieder ein echter Nutzer-Track, kein Testfall: die Düsseldorf-Marathon-Kachel zeigte das Medienhafen-Becken im Wasser-Layer sauber auf einer Höhe, aber sobald die Base-Schicht dazugeschaltet wurde, verschwand genau dieses Becken komplett unter dem Terrain — der Rheinbogen selbst blieb offen. Ein direktes Vermessen an der hochgeladenen 3MF (Ray-Cast: Base-Oberflächenhöhe gegen die eigene Z-Spanne jedes Wasser-Features, an jedem Punkt des Hafenbecken-Footprints) bestätigte es ohne Vermutung: 82 % des Rheinbogens waren korrekt bis auf Wassertiefe geschnitten — aber praktisch das gesamte Hafenbecken lag 20 bis 35 m real zu hoch unter unsichtbarem Terrain.
WaterOptimizer._merge_overlapping_polygons verschmilzt sich berührende Wasser-Polygone (der Rhein und das direkt angeschlossene Hafenbecken gehören geometrisch zusammen) und stempelte das Ergebnis pauschal auf class='water'/subtype='merged' — unabhängig davon, was die Eingabe-Features vorher waren. detect_water_type fällt für alles ohne erkannten Subtype auf LAKE zurück, was den Fluss-Terrassierungs-Pfad (_terrace_plan_for_river, entlang der Fließachse) für jedes jemals gemergte Feature dauerhaft abschaltet — nicht nur für diesen einen Hafen-Fall. Die eine verbleibende flache Quantil-Höhe für das komplette Rhein+Hafen-Polygon wurde dadurch von den zahlenmäßig weit überlegenen Fluss-Randvertices dominiert; das Hafenbecken bekam nie sein eigenes, real höheres Niveau.
Der Fix: ein gemergtes Feature erbt jetzt den Typ des flächenmäßig dominant überlappenden Eingabe-Features statt eines pauschalen Stempels — stellt den Terrassierungs-Pfad für jedes gemergte Gewässer wieder her. Zusätzlich erkennt eine neue _spatial_lobe_split-Prüfung einen echten Höhensprung in den Randproben, bevor der Fluss-/Seen-Pfad überhaupt greift, und teilt ein verschmolzenes Mehrkörper-Polygon in unabhängige Lappen — als Absicherung für Fälle, die nicht entlang einer einzigen Fließachse liegen (z. B. ein seitlich abzweigendes Becken).
Zusätzlich zum Lappen-Split kam als Backup ein Mechanismus dazu, der den Wasser-Schnitt zusätzlich bis über die tatsächliche lokale Terrain-Höhe anhebt — falls die Level-Zuweisung trotzdem irgendwo zu niedrig ausfällt. Kaum gemergt (derselbe Sitzungsablauf, direkt im Anschluss), meldete derselbe Nutzer: das Wasser schwebt jetzt 1,2 bis 1,8 mm über dem umgebenden Terrain — im Slicer nachgemessen (Distance 1.771 mm).
Die naheliegende erste Erklärung war eine Abtast-Ungenauigkeit: die neue Absicherung hat die Bounding Box jedes Wasser-Polygons abgetastet statt dessen tatsächlichen Footprint, und bei länglichen Flusspolygonen reicht die Bbox kilometerweit in viel höheres Uferland hinein (gemessen am Rheinbogen-Feature: zugewiesenes Niveau 20,6 m, lokales Maximum in der Bbox 47,7 m — der Cutter damit 27,1 m real zu hoch). Das ist zwar real und messbar, aber nicht die eigentliche Lehre — und, wichtiger noch: das großzügige Bemessen des Cutters war für sich genommen nie das Problem. Ein Cutter darf beliebig aggressiv sein, notfalls bis +∞ auf der Z-Achse: Base − Cutter entfernt dann im Wasser-Footprint einfach jedes Material oberhalb des Wasserbodens, egal wie hoch — in derselben PR fusionieren Brücken-Causeways jetzt ohnehin erst nach der Wasser-Subtraktion in die Base statt davor, ein noch so großzügiger Cutter kann sie also gar nicht mehr erreichen, und dass Wasser Gebäude in seinem Footprint mitschneidet, ist seit Akt 6/#286 sogar ausdrücklich gewollt. Die eigentliche Ursache liegt eine Ebene tiefer: Schneiden (Base großzügig freischneiden, damit die CSG-Subtraktion garantiert eine offene Rinne statt einer eingeschlossenen Blase erzeugt, Akt 8) und Extrudieren (die sichtbare, exportierte Wasserfläche exakt bauen) sind zwei konzeptionell getrennte Operationen mit entgegengesetzten Zielen — die eine darf/soll großzügig sein, die andere muss exakt sein — und der Code hat sie nie als getrennte Operationen behandelt. WaterBuilder.create_water_surface baut pro Feature genau ein Objekt und benutzt es unverändert für beide Rollen: einmal als Cutter für compose_tile, danach (nur auf das Hexagon geclippt) als das exportierte „Water"-Objekt selbst. Selbst eine korrekt am tatsächlichen Footprint statt an der Bbox gemessene Absicherung hätte den Fehler nicht verhindert — jede zusätzliche Höhe, die der Cutter zum sicheren Durchschlagen bekommt, wäre unweigerlich auch zur sichtbaren Wasserfläche geworden, weil beide Rollen am selben Objekt hingen. Die Bbox-Ungenauigkeit war ein echter, aber sekundärer Fehler; der Sizing-Mechanismus selbst war als Cutter-Strategie durchaus vertretbar, nur eben nie dafür gedacht, auch die Extrusion zu bestimmen.
Die Entscheidung: die Absicherung komplett zurückgenommen, statt Cutter und Extrusion sauber zu trennen (ein separates, nie exportiertes Cutter-Manifold ausschließlich für compose_tile, während create_water_surface weiterhin nur die festen Druck-mm-Margins verwendet) — beide Wege führen zum selben Ergebnis. Der Lappen-Split von oben behebt den eigentlich gemeldeten Fehler auf echten Daten bereits nachweislich korrekt, ganz ohne diesen zusätzlichen Mechanismus; eine sauber getrennte Cutter-Logik wäre ein spekulatives Sicherheitsnetz ohne belegten Bedarf gewesen — nicht wert, dauerhaft eine zweite Objekt-Rolle zu pflegen.
Bei der Rücknahme blieb eine tiefere Frage offen, die der Nutzer selbst aufwarf: liegt water_level_z überhaupt dort, wo wir das Terrain sehen? water_level_z ist ein einziger flacher Wert pro Feature — ein niedriges (15.) Perzentil über alle Rand-Vertices, auf das Druck-Layer-Raster gerundet. Genau das round() war der zweite, unabhängige Fehler: an echten Daten gemessen sackte ein Teich, dessen kompletter Rand bei 37,5–39,9 m real lag (roher 15.-Perzentil-Wert ~37,6 m), durch die Rundung auf ein 10,3 m-Raster auf 41,2 m — höher als jeder einzelne Randpunkt dieses Features. Das widerspricht dem im Code selbst dokumentierten Ziel „water never stands above its banks" nicht nur im Detail, sondern systematisch: round() rundet per Konstruktion in ~50 % der Fälle nach oben.
_water_level_for_component und _terrace_plan_for_river runden beide jetzt nach unten statt zur nächsten Rasterlinie — ein bereits sicher niedriger Wert kann dadurch nie mehr über die eigene Rasterlinie hinausgeschoben werden. Zusätzlich sinkt water_extrusion_up_mm im Schema-Default von 0,2 mm auf 0 mm: die sichtbare Wasserfläche soll so tief/eingesenkt liegen, wie die (jetzt korrekte) Level-Zuweisung erlaubt, statt zusätzlich einen festen Aufschlag obendrauf zu bekommen — water_extrusion_down_mm allein garantiert bereits ein echtes, nicht-entartetes Cutter-Volumen für die Boolean-Differenz.
| Kennzahl (alle 41 Wasser-Features, echte Düsseldorf-Bbox) | Vorher (round + 0,2 mm) | Nachher (floor + 0 mm) |
|---|---|---|
| Ø Rand-Anteil, der unter der sichtbaren Wasserfläche liegt | 100 % | 0,6 % |
| Größte gemessene Klippe (Wasser über lokalem Terrain) | 16,7 m real (0,32 mm Export) | 0,3 m real (0,01 mm, Rauschniveau) |
| Vom Nutzer im Slicer gemessene Kante | 1,771 mm | — (Mechanismus behoben) |
706 Unit-Tests grün (705 Baseline + 1 neuer Regressionstest für die floor()/round()-Divergenz, mit den exakten realen Zahlen aus der Messung oben), reine Python-Fetcher-/Optimizer-/Geometrie-Logik ohne Blender. Alle drei Funde wurden an derselben echten Bbox aus der Original-Meldung verifiziert — aber, wie der vierte Fund gleich zeigt, immer noch nur gegen die Heightmap, nie gegen die tatsächlich komponierte Base-Mesh.
Der dritte Fund wurde gemergt. Der Nutzer generierte eine neue Kachel und meldete: das Terrain bedeckt den Rhein jetzt fast vollständig — fast das Gegenteil des ursprünglich gemeldeten Problems. Drei berechtigte Fragen kamen zurück: Wofür die Bedenken zu Cutten vs. Extrudieren äußern, wenn am Ende doch wieder dasselbe Objekt für beides benutzt wird? Wieso eine Heightmap-Analyse als Beleg verwenden, die genau diese Klasse von Fehler prinzipiell nicht aufdecken kann? Und was ist jetzt eigentlich der Root Cause?
Alle drei trafen zu. water_extrusion_up_mm=0 plus floor() hatten der sichtbaren Wasserfläche jeden Puffer nach oben genommen — und weil water_level_z bewusst ein niedriges 15.-Perzentil ist, liegt sie damit fast überall unter dem real schwankenden Terrain, nicht mehr darüber. Überkorrektur, keine Korrektur. Und die Validierung aus dem dritten Fund hatte water_level_z nur gegen Heightmap-Randstichproben geprüft — nie gegen die tatsächlich komponierte Base-Mesh nach Terrain-Optimierung/-Dezimierung und CSG-Union. Genau die Methode, die den allerersten Fund dieses Akts überhaupt aufgedeckt hat (Ray-Cast auf die echte 3MF), wurde für die eigene Fix-Verifikation nicht wiederholt.
Ein Cutter darf beliebig großzügig sein (siehe Fund 2) — ihn zu vergrößern kostet nichts. Die sichtbare Wasserfläche höher zu ziehen, um Terrain-Unebenheiten auszugleichen, kostet dagegen genau die Klippe aus Fund 2. Die eigentlich richtige Idee: wenn der Cutter das Terrain im gesamten Wasser-Footprint großzügig genug wegschneidet, bleibt dort gar kein Terrain mehr übrig, das die Wasserfläche bedecken könnte — die Extrusion selbst braucht dann überhaupt keine Terrain-Kenntnis, sie bleibt exakt so niedrig/eingesenkt, wie water_level_z es ohnehin vorgibt.
Der Fix: WaterBuilder.build_water_cutters() baut jetzt pro Feature ein separates, nie exportiertes Cutter-Manifold, dessen Deckfläche bis über das reale lokale Terrain-Maximum im tatsächlichen Footprint angehoben wird (nicht mehr die Bounding Box — der ursprüngliche Fehler aus Fund 2, dieses Mal per shapely.contains_xy vermieden). create_water_surface() — das exportierte, sichtbare Objekt — bleibt dagegen komplett unverändert und bekommt gar keine Terrain-Logik: sobald der Cutter seinen Job gemacht hat, gibt es dort nichts mehr, das die unveränderte, korrekt niedrige Fläche bedecken könnte.
Ein frischer Blender-Analyse-Container, echter Overture-Fetch, dieselbe Düsseldorf-GPX, kompletter Lauf bis zum 3MF-Export — keine Abkürzung über Python-Fetcher allein. Der Ray-Cast (dieselbe Methode wie im allerersten Fund dieses Akts) läuft jetzt gegen die tatsächlich komponierte Base-Mesh, nicht gegen die Heightmap: von den abgetasteten Punkten über dem größten Wasser-Shell (Rhein + Medienhafen zusammenhängend) liegt Base bei 0,5 % noch über der Wasseroberfläche — und das sind exakt die zwei Stellen, an denen Brücken den Fluss queren, nicht neue Defekte.
714 Unit-Tests grün (706 + 8 neue für build_water_cutters/_footprint_terrain_max, siehe test_water_builder.py), plus die zwei Container-Läufe oben (echte Kachel-Generierung + Ray-Cast/Render gegen den Blender-Output) gegen echte Overture-Daten. Kein Python-Fetcher-Proxy mehr als alleiniger Beleg für ein Blender-Compose-Ergebnis.
Auslöser war ein direkter Vorher/Nachher-Vergleich vom Nutzer selbst: ein älteres, vor dem großen Rework gedrucktes Tile gegen ein aktuelles. Die Beobachtung war präzise — die Gebäudeumrisse auf dem neuen Druck lasen sich schlechter, und kleine Innenhöfe tauchten als offene Löcher auf, die wie Druckfehler statt wie Absicht wirkten. Die Nachfrage, die die eigentliche Arbeit auslöste, kam gleich hinterher: „Du solltest alle Akte der Success Story gelesen haben, WARUM wir die einzelnen Änderungen gemacht haben — die Frage ist, ob wir das weiter optimieren können, ohne die Vorteile zu verlieren." Jede vorherige Entscheidung — Akt 1s echte Vereinigung statt Hülle, Akt 4s beweisbare Wasserdichtigkeit, Akt 9s Schutz vor Höhen-Leck durch benannte Gebäude, Akt 10s Freihaltung des Track-Korridors — musste unangetastet bleiben.
Die Ursache lag in BuildingFetcher._printability_pass_merge (Akt 2/ADR-0036): eine einzige morphologische Öffnung — union.buffer(-wall/2).buffer(+wall/2) — musste gleichzeitig vier verschiedene Fragen beantworten: Ist diese Wand druckbar? Ist dieser Innenhof groß genug, um als Loch zu bleiben? Ist dieses Detail über der Slicer-Auflösung? Und — separat davon — eine einzelne max()-Höhe pro zusammengeführtem Teil verschluckte jede Dachhöhen-Variation innerhalb eines Blocks. Eine Öffnung erodiert die gesamte Fläche mit demselben Radius und dilatiert danach nur die erodierte, nicht die ursprüngliche Form zurück — jede echte Ecke, jede Innenhof-Wand, die die Erosion berührt, aber nicht tatsächlich durchtrennt hat, kommt gerundet und geschrumpft zurück, obwohl sie nie unprintbar war.
printable_silhouette() (neu, core/geometry/polygons.py) findet per Erosion dieselben zu schmalen Stellen wie bisher — behält aber für jede überlebende Zusammenhangskomponente ihre ursprüngliche, nicht die erodierte Form. Nur ein Steg, der wirklich zu dünn zum Drucken ist, wird durchtrennt (iterativ, bis zu 8 Durchgänge — das Durchtrennen eines Stegs kann einen neuen, dahinterliegenden freilegen); alles andere behält seine echte Kontur. relief_partition() gruppiert die Mitglieder eines zusammengeführten Teils nach quantisierter Höhe und rekonstruiert jede Höhenstufe einzeln, statt allen Gebäuden dieselbe max()-Höhe zu geben. Und drei neue, einzeln abgestimmte Schwellenwerte ersetzen den einen überladenen: building_min_feature_lines (0,25 Düsenbreiten, reiner Rauschfilter unterhalb dessen, was der Slicer ohnehin nie druckt), building_relief_min_lines (1,0 Düsenbreiten — eine Dachrelief-Stufe steht auf tragendem Untergrund, kann also feiner sein als eine freistehende Wand) und building_courtyard_min_mm (1,0 mm — in gedruckten Millimetern, nicht Düsenbreiten, weil die Wahrnehmbarkeit eines Lochs eine Eigenschaft des fertigen Drucks ist, nicht der Düse).
| Kennzahl (identischer 154-Gebäude-Cluster, echtes München) | ALT (Öffnung + max()) | NEU (Silhouette + Relief) |
|---|---|---|
| Druckbare Teile | 9 | 10 → 33 Relief-Stücke |
| Erhaltene Fläche ggü. Rohdaten (nach gemeinsamem Spalt-Schließen) | 156 % | 172 % |
| Teile mit >1 echter gedruckter Höhe | 0 / 9 (immer flach) | 8 / 10 |
| Innenhöfe: als Loch erhalten / bewusst gefüllt | 1 / 0 (Zufallsprodukt des Spalt-Schwellenwerts) | 0 / 1 (nach 1 mm-Wahrnehmungsschwelle) |
Der Nutzer stellte die richtige Frage, bevor Code geschrieben wurde: „Ich habe die Sorge, dass wenn wir zu kleinteilig werden, wir wieder Details im Slicer verlieren?" Statt das anzunehmen, wurde real gesliced und im erzeugten G-Code nachgemessen, welche Balkenbreiten (0,10 bis 1,00 mm getestet) ein 0,2 mm-Düsenprofil tatsächlich noch als eigene Extrusion reproduziert — nicht nur theoretisch, sondern als reale, extrudierende Werkzeugbahn im G-Code. Ergebnis, direkt in building_min_feature_lines' eigener Schema-Beschreibung dokumentiert: ein 0,2 mm-Düsenprofil reproduziert noch eine freistehende 0,10 mm-Struktur (~0,5 Düsenbreiten) — der Default (0,25 Düsenbreiten) liegt bewusst unterhalb dieser gemessenen Grenze, damit er ein reiner Rauschfilter bleibt statt aktiv Form zu verändern.
Der reale München-Datensatz crashte beim ersten vollständigen Lauf mit einer Selbstüberschneidung an einem einzelnen Feature (printable_1174_3). Drei unabhängige, jeweils für sich korrekte Reparaturversuche änderten nichts: (1) Prüfung des Live-Objekts, (2) eine round-trip-bewusste Prüfung (Shapely kann is_valid=True am Live-Objekt melden, während ein aus genau denselben, extrahierten Koordinaten neu gebautes Polygon False meldet — real reproduzierbar, kein hypothetischer Fall), (3) ein Leck im covered-Akkumulator von relief_partition. Derselbe Crash, dieselbe Koordinate, jedes Mal.
Erst ein Vorher/Nachher-Dump desselben Feature an zwei Stellen der Pipeline in einem einzigen Lauf fand die eigentliche Ursache — nicht in diesem Akts eigenem neuen Code: dedupe_ring, eine bereits bestehende, unabhängige Funktion in core/geometry/solids.py (Epsilon 1e-9), entfernte einen Punkt aus einem 11-Punkte-Ring, der davor nachweislich gültig UND einfach war (is_valid=True, is_simple=True) — und machte ihn dadurch selbst zu einem sich selbst schneidenden Ring. Der entfernte Punkt lag nur 8,87e-10 von seinem Nachbarn entfernt, unter dem Epsilon der Funktion. Erreichbar wurde das erst durch diesen Akt: relief_partition erzeugt spürbar feinteiligere, dichter aneinanderliegende Geometrie als die alte flache max()-Ausgabe je hatte — der Fehler ist alt, seine Sichtbarkeit ist neu.
_dedupe_and_repair() repariert nur dann, wenn dedupe_ring selbst einen zuvor gültigen Ring gebrochen hat — ein von Anfang an ungültiger Ring (der vorbestehende Bowtie-Regressionstest) muss weiterhin einen harten Fehler auslösen, nicht stillschweigend repariert werden. Regressionstest test_dedupe_ring_induced_self_intersection_is_repaired_not_raised nutzt die exakten, real erfassten Koordinaten aus diesem Lauf.
Nach dem dedupe_ring-Fix lief die komplette München-Kachel durch die Geometrie-Erzeugung und die eigene In-Szene-Prüfung fehlerfrei — scheiterte aber beim 3MF-Export an sechs neuen, hauchdünnen entarteten Schalen (Volumen zwischen 1e-9 und 1e-12 m³), obwohl compose_tiles eigene Aufräum-Stufe (_fill_enclosed_voids, ursprünglich für #297s vergrabenen Wasser-Schnitt gebaut) an genau diesem Mesh nichts zu bereinigen fand. Ursache: Blender speichert Vertex-Koordinaten intern als float32. Ein Mesh, das im Moment der CSG-Komposition nachweislich sauber ist, kann nach dem Schreiben in ein Blender-Objekt und dem erneuten Auslesen beim Export neue, hauchdünne entartete Schalen entwickeln — weil relief_partition erheblich mehr aneinandergrenzende, fast deckungsgleiche Flächen erzeugt (benachbarte Dachrelief-Stufen) als die alte flache Geometrie je hatte, und der float64→float32→float64-Roundtrip genau präzise genug ist, um gelegentlich das Vorzeichen einer dieser Grenzflächen zu kippen.
Die Schalen-Klassifikation aus _fill_enclosed_voids wurde in eine geteilte Funktion extrahiert (drop_degenerate_shells(), core/geometry/validation.py) und läuft jetzt zweimal: einmal wie bisher direkt nach der CSG-Komposition, ein zweites Mal in export_3mf.py auf dem tatsächlichen, skalierten Export-Mesh, unmittelbar vor der Wasserdichtigkeits-Prüfung.
| Kennzahl (volle München-Marathon-2025-Kachel, echter End-zu-End-Lauf) | Wert |
|---|---|
| Gebäude → druckbare/Relief-Teile (city_detail) | 42.985 → 21.992 |
| Benannte/höchste Gebäude standalone verarbeitet (Akt 9-Schutz aktiv) | 1.956 |
| Fläche nach Spalt-Schließen | 1.555 ha → 1.885 ha |
| Innenhöfe: als Loch erhalten / gefüllt (<59 m Schwelle bei diesem Maßstab) | 24 / 1.467 |
| Teile nach Akt-10-Track-Korridor-Carving | 23.913 → 23.151 |
| Entartete Schalen beim Komponieren / erneut beim Export entfernt | 110 / 26 |
| 3MF-Export | erfolgreich, wasserdicht |
225 neue/geänderte Unit-Tests grün, plus ein echter, lokaler (nicht containerisierter) Blender-5.1-End-zu-End-Lauf gegen die reale München-Marathon-2025-Strecke. Ehrlich mitgezählt gehört zu diesem Akt auch, was er gekostet hat: die feinteiligere, echte Detailtiefe hat zwei neue, real reproduzierbare Wasserdichtigkeits-Fehlerklassen erst reproduzierbar gemacht (dedupe_ring-Pinch-Punkt, float32-Export-Roundtrip) — beide waren vorher nicht erreichbar, weil die alte, flache Geometrie nie genug aneinandergrenzende Flächen erzeugte, um sie auszulösen. Beide sind jetzt durch je einen Regressionstest und (beim zweiten Fund) eine zweite, geteilte Verteidigungsschicht abgesichert statt nur für diesen einen Fall gepatcht — mehr Detailtreue kam nicht kostenlos, aber nachweisbar korrekt.