Lifetiles · Engineering-Rückblick · 11.–14.07.2026

Vom „undefinierten Kasten" zur wasserdichten Kachel

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.

Auslöser Städte auf Marathon-Kacheln unlesbar/unproduzierbar Umfang 5 Analyse-/Planungs-Runden · 14 Pull Requests · 6 Architektur-Dokumente Repro live gegen echte Overture-Maps-Daten, keine Synthetik
6.60721.310
Echte Gebäude statt Cluster-Klumpen (München Marathon, Akt 1–2)
48,9 %2,1 %
Druckbare Gebäude-Fläche (zu kleine Strukturen eliminiert, München)
1.9450
Offene Kanten in der exportierten Basis-Geometrie (manifold-by-construction, Akt 4)
81,9 %0 %
Wasser-Oberflächendefekte (Höhensprung-Dreiecke, München, Akt 6)

Ressourceneinsatz dieser Rückblick-Sitzung

997.0M
Tokens verbraucht (10.–14.07. Rückblick-Sitzung)
$493
Kostenaufwand Claude-API (alle Modelle, Prompt-Caching eingerechnet)
5
Kalendertage (drei davon Hauptarbeit, zwei Verifikation)
14
Merged Pull Requests (Korrektur + Festigung + Dokumentation)
AKT 1Ausgangslage

Städte wurden zu Kästen, sobald der Maßstab kippte

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.

München-Zoom: Convex-Hull-Cluster fressen ganze Straßenzüge
München, Maßstab 1:59.100: Ein einzelnes konvex-Hull-Cluster schluckt 6.607 Einzelgebäude und bläht sie um das bis zu Zehnfache auf — Straßen sind nicht mehr zu erkennen, die Stadtstruktur ist zerstört.
Innsbruck: konvexe Hülle vs. echte Vereinigung im Vergleich
Innsbruck im Karwendelmarsch, Maßstab 1:243.600: Ein einziges Cluster verschlingt 6.059 Gebäude zu einem amorphen Fünfeck — der berüchtigte „undefinierte Kasten". Die Ursache: die Hülle ist mathematisch einfach, aber geometrisch unvermeidlich inkorrekt — sie füllt zwangsläufig jede Straße, jeden Hof und jede Grünfläche zwischen den echten Gebäuden auf.
AKT 2Analyse & erste KorrekturAbgeschlossen

Eine Live-Analyse gegen echte Kartendaten, nicht am Reißbrett

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:

  • Die Hülle war nicht nur unschön, sie war ein Symptom. Die konvexe Hülle kaschierte eine tiefere Schwäche: extreme Maßstäbe rundeten Gebäudehöhen auf null — bei 1:243.600 waren 728 von 731 Karwendelmarsch-Gebäuden faktisch flache, kaputte Flächen.
  • Die Kachel wurde teilweise ohne Gebäude ausgeliefert — bei bestandenem Test. Diese kaputten Gebäude ließen die anschließende 3D-Verschmelzung mit dem Gelände lautlos scheitern; der automatische Test prüfte nur „Objekt vorhanden", nicht „Objekt tatsächlich enthalten" — ein Blindspot, der ohne diese Analyse vermutlich erst bei Kunden aufgefallen wäre.
TrackMaßstabGebäude vorher → nachherFläche < 0,3 mm
München Marathon 20251:59.1006.607 Klumpen → 21.310 echte Gebäude48,9 % → 2,1 %
GaPa Langstrecke1:60.600771 → 3.38463,8 % → ~2 %
Karwendelmarsch (Innsbruck)1:243.600731 → 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.

München nach dem Fix: zusammenhängende, druckbare Blöcke Karwendelmarsch nach dem Fix: Innsbruck als ruhiges Siedlungsplateau
Dasselbe München, derselbe Karwendelmarsch — nach der Korrektur. Links: zusammenhängende, echte Blockstrukturen statt Sprenkel, Straßen bleiben durchgängig offen. Rechts: Innsbruck ist jetzt eine ruhige, taktil erfassbare Fläche mit erkennbarer Talkontur statt eines Scherbenfelds.
AKT 3Die QualitätskriseBehoben, innerhalb eines Arbeitstages

Der Fix selbst enthielt einen zweiten, versteckten Fehler

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.

Am echten Export gemessen, nicht vermutet

Ü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 gemeldeten Artefakte, direkt aus der exportierten Druckdatei gerendert
Derselbe Ausschnitt nach dem Wechsel auf korrekte Dreiecks-Zerlegung
Fächer-Bug
Behoben
Derselbe Kachel-Ausschnitt, dieselbe Geometrie — nur das Dreiecks-Verfahren beim Export geändert. Links das gemeldete Problem, direkt aus der echten Druckdatei gerendert. Rechts nach dem Wechsel auf ein robustes Verfahren (Blenders eigene, konkavitätssichere Zerlegung), noch am selben Tag ausgeliefert. Mit dem Slider oder durch Ziehen des Griffs vergleichen.

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.

AKT 4Der DurchbruchIn main, Version 0.32.0

Von „hoffentlich wasserdicht" zu „beweisbar wasserdicht"

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-GeometrieHistorische ProduktionHeute
Offene Kanten (unvollständig geschlossene Flächen)1.9450
Mehrfach belegte Kanten (Geometrie-Duplikate)6.7030
Stille Fehlschläge bei Brücken/Wasserunbemerkt möglichwerden gemeldet
Der stärkste Beweis: die neue Schranke fand Fehler ganz von selbst

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ß.

AKT 5BrückenAlle 4 Phasen abgeschlossen

„Etwas muss über den Rhein führen" — jetzt beweisbar statt geglaubt

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.

Düsseldorf: Marathon-Strecke quert den Rhein über eine durchgängige Causeway-Brücke, Gebäude und Wasser als ein Objekt München: Marathon-Strecke quert die Isar mehrfach über durchgängige Causeway-Brücken
Echte Blender-Cycles-Renderings, beide Städte, nach allen vier Phasen. Links Düsseldorf: die Strecke (rot) quert den Rhein (blau) auf einer durchgängigen, wasserdichten Brücke — keine Lücke, kein Schweben. Rechts München: dieselbe Strecke quert die Isar mehrfach; alle Übergänge fusionieren korrekt mit der Basis-Geometrie.
01
Topologie
Der 500 m-Sprung war ein Symptom, keine Anomalie

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.

02
Regime A
Causeway: die Brücke, die nie überhängen kann

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.

03
Regime B
Druckbares Hohlraum-Deck für Wahrzeichen mit echter Durchfahrtshöhe

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.

Regime B Brücke (Hofgartenrampe, Düsseldorf Hafen): flaches Deck auf Stützwänden über einem echten, offenen Wasser-Hohlraum
Regime B in echt: die Hofgartenrampe im Düsseldorfer Hafen, Maßstab 1:7.000. Flaches Deck, innere Stützwände, echter offener Hohlraum — das Wasser (blau) und die Strecke (rot) verlaufen sichtbar unter dem Deck statt in massivem Damm. Bei diesem kleinen Kachelausschnitt reicht die reale Durchfahrtshöhe erstmals aus, um die Regime-B-Schwelle zu überschreiten — genau der Fall, den Akt 5 vorgesehen hat. Die Verifikation dieses Falls deckte einen zweiten echten Fehler auf: die Kachel-Prüfung ging bis dahin davon aus, jede Brücke müsse als Damm in der Basis landen — eine Brücke, die korrekt Regime B wurde, ließ die Prüfung deshalb fälschlich fehlschlagen. In CI gefunden, noch am selben Tag behoben.
04
Verifikation
Echte Zahlen statt Vermutungen — und ein echter Fund dabei

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.

AKT 6FlüsseUmgesetzt, ADR-0039

Wasser ist die letzte Fläche, die nie manifold-by-construction wurde

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.

Ursache: Ear-Clipping auf langen, dünnen, konkaven Konturen

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.

Sprünge im Verlauf: vor dem Umbau gar nicht modelliert

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.

Ist die Neigung vernachlässigbar? Kommt auf den Maßstab an — quantifizierbar

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.

TrackMaßstabZ-RasterschrittTypisches FlussgefälleBewertung
Düsseldorf Marathon (Rhein)1:44.2004,4 mTiefland, Größenordnung Dezimeter pro KilometerReal weit unter einer Schicht — Glättung ist die richtige Vereinfachung.
München Marathon (Isar)1:59.1005,9 mVoralpin, Größenordnung Dezimeter bis niedrige Meter pro KilometerEbenfalls unter einer Schicht auf Kachel-Distanz — vernachlässigbar korrekt.
Karwendelmarsch (Gebirgsbäche)1:243.60024,4 mAlpin, 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.

01
Mittellinie
Fließrichtung statt Rohpolygon als Ausgangsform

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.

02
Manifold-by-construction
Dieselbe Extrusions-Primitive wie Gebäude und Brücken

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.

03
Gefälle & Terrassen
Höhe entlang der Fließrichtung statt Mittelwert pro Polygon

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.

04
Verifikation
Düsseldorf/München erneut rendern, Karwendelmarsch als neuer Regressionsfall

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.

Ergebnis: gegen echte Kacheln gemessen, nicht am Reißbrett behauptet

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, vorherDüsseldorf, nachherMünchen, vorherMünchen, nachher
Deckflächen-Dreiecke mit Höhensprung72,9 %0 %81,9 %0 %
component_status[water]successsuccesssuccesssuccess
Wasser-Shells beim 3MF-Exportwasserdichtwasserdichtwasserdichtwasserdicht

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.

München, vor dem Umbau: Isar-Oberfläche mit sichtbarem diagonalem Knick und gezacktem Rand
München, nach dem Umbau: dieselbe Isar-Stelle als glatte, ruhige Fläche
Vorher
Nachher
Dieselbe Isar-Stelle, echtes Blender-Cycles-Rendering, identische Kamera. Links der Ist-Zustand vor Akt 6: der diagonale Knick quer durch die Wasserfläche und der gezackte linke Rand sind exakt die vom Nutzer gemeldeten Dreiecks-Artefakte — Folge der pro-Vertex gesnappten Rohhöhe. Rechts nach dem Umbau: eine ruhige, ebene Fläche, weil die Isar hier unter einem Print-Layer Gefälle bleibt und als eine einzige Ebene gebaut wird (siehe Tabelle oben in diesem Akt). Mit dem Slider oder durch Ziehen vergleichen.
Nebenbefund beim Vergleichs-Rendering: Inseln sind zurück

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.

Damit wäre die Pipeline durchgängig

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 7SiedlungsflächenPhase 1 umgesetzt, ADR-0040

Die Siedlungsfläche ist eine Krücke aus Gebäude-Resten — jetzt gegen den echten Karwendelmarsch-Track gemessen

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.

Der Fund, der den Umbau risikoarm gemacht hat

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.

Vorher/Nachher am echten Karwendelmarsch, Innsbruck-Zoom

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.

Vorher: Gebäude-Union, Innsbruck-Zoom Nachher: land_use, Innsbruck-Zoom
Links vorher (Gebäude-Footprint-Union), rechts nachher (land_use, ADR-0040). Echtes CPU-Cycles-Rendering, identischer Zoom-Ausschnitt (Building-Bounds), identischer Track und Maßstab. Das Gebäude-Objekt selbst schrumpft von 12.956 auf 7.684 Vertices bei praktisch gleicher Flächenabdeckung — weniger, sauberere Polygone statt Tausender Gebäude-Reste, genau wie die Issue vorhergesagt hat.
Vorher: volle Kachel-Übersicht, Gebäude-Union
Nachher: volle Kachel-Übersicht, land_use-Siedlungsfläche
Vorher
Nachher
Zum Vergleich die volle Kachel-Übersicht, vorher/nachher — zum Ziehen und mit Hervorhebung. Die 32,9×6,5 km lange, schmale Karwendelmarsch-Strecke nutzt nur einen schmalen Streifen des Hexagons — Innsbruck sitzt als einziges Siedlungsgebiet fast am Kachelrand, exakt wie ADR-0036 es beschreibt. Die Checkbox blendet die Siedlungsfläche grün ein, damit sie auf einen Blick erkennbar ist.
KennzahlVorher (Gebäude-Union)Nachher (land_use)
Plateau-Quelle370 Building-Solids930 land_use-Kandidatenteile
Base (komponiert)53.949 verts / 107.910 tris52.261 verts / 104.526 tris
Base-Genus (Tunnel/Löcher durch den Körper)42

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.

Zwei echte Fehler beim ersten scharfen Testlauf gefunden — einer davon überraschend

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.

Randnotiz: schwarze Flecken in einer internen Vorschau — kein Produkt-Thema

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.

01
Neuer Fetcher
land_use_fetcher.py, gebaut wie Wasser/Brücken

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.

02
Höhenstufen
Nutzungsart statt einer flachen Ebene

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.

03
Togglebar per SSOT
Ein Setting, automatisch in Blender-UI und Web-Wizard

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.

04
Verifikation
Die fehlende Lücke geschlossen: echter Karwendelmarsch-Track jetzt im Repo

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.

Vom Bauplan zur verifizierten Phase 1 — in derselben Sitzung

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 8Der vergrabene SchnittExport behoben & im Container verifiziert

#297 geschlossen: kein Terrain-Defekt, sondern ein Wasser-Schnitt, der nie an die Oberfläche kam

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.

Reproduziert, nicht vermutet

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.

TestVorherNachher
Adversarielles Grat/Tal-Höhenprofil, alte Fächer-Seitenwand21 überlappende Dreieckspaare0 (Quad-Strip, beweisbar)
.3mf-Export, echter Karwendelmarsch-Track im ContainerValidation error, Shell 7: is_volume=False, vol=−0,127Erfolgreich: 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.

Karwendelmarsch, Top-Down-Rendering nach dem Fix Karwendelmarsch, Oblique-Rendering nach dem Fix
Der reale Karwendelmarsch-Track, gerendert nach dem Fix. Optisch identisch zum Vorher-Zustand — der Defekt war ein vollständig eingeschlossener, unsichtbarer Hohlraum, keine sichtbare Geländeverformung. Der Unterschied zeigt sich nicht im Rendering, sondern erst am 3MF-Export selbst: vorher hart abgebrochen, jetzt erfolgreich.
AKT 9Der Talbrucktor-BlockBehoben & im echten Blender-Lauf verifiziert

Ein einzelnes benanntes Gebäude zog einen ganzen Altstadt-Block auf seine Höhe hoch

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.

Reproduziert mit echten Overture-Daten, nicht vermutet

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.

AnsatzMax. Höhen-Ratio je TeilGrößtes ClusterTalbrucktor korrekt?
Baseline (aktuell in Prod)28,7×532 GebäudeNein — auf 91 m gezogen
Höhen-Ratio-Clustering6,6×121Teilweise — Ketteneffekt
Diskrete Höhen-Bänder2,2×20Verschwindet (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)VorherNachher
Gebäude-Objekte gesamt4.0935.597
Davon einzeln (benannt)0alle im Merge-Pool1.923
3MF-Export(vor dem Fix erfolgreich)Erst ✗ (Nullvolumen-Shell) → ✓ nach CSG-Fix
Integrationstest (headless Blender, echte Overture-Daten)✓ PASSED✓ PASSED
Marienplatz-Bereich vorher: ein einzelner, uniform hoher Block
Marienplatz-Bereich nachher: differenzierte Gebäudehöhen, zwei Landmark-Türme sichtbar
Vorher
Nachher
Derselbe Ausschnitt, echter Blender-Lauf auf der realen München-Marathon-Kachel. Links vorher: die Altstadt ist ein einzelner, flacher, uniform hoher Klotz. Rechts nachher: einzelne Gebäude in plausibler Höhenvarianz, zwei Kirchtürme (St. Peter, Frauenkirche) heben sich jetzt korrekt als individuelle Landmarks ab, statt die ganze Nachbarschaft mit hochzuziehen. Mit dem Slider oder durch Ziehen des Griffs vergleichen.
Datenanalyse: Baseline vs. Named-Exclusion, eingefärbt nach Gebäudehöhe
Die lokale Analyse hinter der Entscheidung — echte Overture-Footprints um den Marienplatz, eingefärbt nach Höhe (dunkel=niedrig, hell=hoch), bei den realen Druckbarkeits-Parametern der Kachel. Baseline (Mitte) verschmilzt fast alles zu einem 91 m-Block; Named-Exclusion (rechts) hält sich nah am Original (links) und lässt trotzdem nur druckbare Teile übrig.
AKT 10Der freigelegte TrackBehoben & gegen die reale München-Kachel verifiziert

Ein Nutzer-Screenshot: der eigene Lauf verschwindet unter dem Gebäude-Klotz

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.

Die Korrektur: ein eigenständiger Schritt, nach dem Fetch, nicht darin

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.

Echte Zahlen, echte Kachel — nicht nur der eine gemeldete Fall

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ückgeschnitten145
Gebäude-/Blockteile vollständig verworfen (Strecke wäre komplett begraben)150
davon zusätzlich durch die Mindestwandbreiten-Schranke bereinigt79
Gebäudeteile gesamt5.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.

Vorher: Gebäudeblock überdeckt die Marathonstrecke an einer Kreuzung nahe Marienplatz
Nachher: dieselbe Kreuzung mit freigeschnittenem Korridor, Strecke durchgängig sichtbar
Vorher
Nachher
Echtes CPU-Cycles-Top-Down-Rendering, identischer Ausschnitt (500 m Kantenlänge), zentriert exakt auf die Koordinaten des Marienplatz. Links vorher (track_corridor_enabled=False, exakt der alte Zustand): ein durchgängiger weißer Gebäudeblock legt sich am oberen Kreuzungspunkt direkt über die Streckenführung (rot) — exakt der gemeldete Effekt. Rechts nachher: derselbe Block, jetzt mit einem durchgängig freigeschnittenen Korridor — die Strecke bleibt an der gesamten Kreuzung sichtbar, ohne dass die Gebäude ringsherum verschwinden. Mit dem Slider oder durch Ziehen des Griffs vergleichen.
Warum das nicht dieselbe Baustelle wie Akt 9 ist

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 Nachschärfungen aus der Review, noch am selben Tag

Zwei berechtigte Einwände kamen direkt nach dem ersten Entwurf zurück, beide noch am selben Tag umgesetzt:

  1. „Das Feature ist sehr einschneidend — kann ich es bei Bedarf deaktivieren und die Korridorbreite im Verhältnis zur Track-Breite konfigurieren?" Ein fester Sicherheitsabstand in Metern (der ursprüngliche Entwurf) skaliert nicht mit dem Kachel-Maßstab — 1 m ist bei 1:20.000 kaum sichtbar, bei 1:243.600 unverhältnismäßig knapp. Jetzt gibt es zwei echte, in Blender-UI und Web-Wizard sichtbare Einstellungen (SSOT, ADR-0017): track_corridor_enabled (Default an — schaltet exakt auf das alte Verhalten zurück) und track_corridor_width_factor (Default 1,5×, ein Verhältnis zur Track-Eigenbreite statt eines fixen Meterwerts, damit es über alle Maßstäbe konsistent bleibt).
  2. „Gibt es ein Gate gegen 0/Mini-Artefakte nach dem Carven? Das sah im Screenshot verdächtig aus." Der reine Flächen-Filter (min_part_area_m2) lässt einen langen, dünnen Span durch, solange seine Fläche über der Schwelle liegt — 0,3 m × 20 m sind 6 m², klar über 4 m², aber als Wand unbedruckbar. Jeder verbliebene Teil läuft jetzt zusätzlich durch dieselbe morphologische Öffnungs-Schranke (Erodieren + Dilatieren), die die Gebäude-Druckbarkeitsprüfung ohnehin schon global anwendet (ADR-0036) — mit demselben building_min_wall_lines-Wert. Ein Regressionstest (TestWallGate) baut genau so einen zu dünnen Rest nach und bestätigt: er wird jetzt verworfen, ein solider Rest bleibt unangetastet.

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.

AKT 11Landmark-TürmeUmgesetzt & im echten Blender-Lauf verifiziert

Der Rheinturm wurde zum unbedruckbaren Zahnstocher — z_scale multipliziert die Schlankheit jedes Turms exakt mit sich selbst

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.

Nicht nur ein Sonderfall — aber ein kleiner, klar benennbarer

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 realAspekt vorherAspekt nachherSockel-Verbreiterung
Mannesmann-Hochhaus88 m14,1×4,0×2,47×
ARAG-Hochhaus125 m13,2×4,0×1,90×
Rheinturm179 m11,9×4,0×1,38×
Hyatt Regency Düsseldorf70 m9,7×4,0×1,88×
Colorium62 m9,0×4,0×1,86×
Alle 28 Landmarks > 30 m (max.)14,1×≤ 4,0×≤ 2,47×
Vorher: der Rheinturm als duenner Zahnstocher, schiesst oben aus dem Bild
Nachher: derselbe Turm, jetzt proportional in die Skyline eingebunden
Vorher
Nachher
Identische, fest verankerte Kamera (nicht auf das jeweilige Objekt zentriert — sonst würde die Zentrierung selbst den Höhenunterschied kaschieren), echter CPU-Cycles-Render, echte Düsseldorf-Overture-Daten. Links vorher: der Rheinturm schießt als dünne Nadel über den oberen Bildrand hinaus — exakt das Symptom, das den ganzen Akt ausgelöst hat. Rechts nachher: derselbe Turm, real gemessen 539 m → 300 m absolute Spitzenhöhe an dieser Koordinate, fügt sich jetzt proportional in die Skyline ein, bleibt aber weiterhin das höchste Gebäude der Kachel. Mit dem Slider oder durch Ziehen des Griffs vergleichen.
Isoliertes Landmarks-Objekt: alle Duesseldorf-Tuerme mit sichtbar konischem Sockel, Base ausgeblendet
Das isolierte Landmarks-Objekt der echten Düsseldorf-Kachel (Base/Track/Wasser ausgeblendet, direkt aus der Export-Geometrie gerendert) — jeder der 24 gehärteten Türme zeigt den charakteristischen, sich verjüngenden Sockel. Kein einziger reiner Zylinder/Prisma mehr.

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 Schale14 / 280 / 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
Warum das Landmarks-Objekt nicht alle 506 benannten Gebäude enthält

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.

Gesamtansicht der Duesseldorf-Kachel: Track entlang des Rheins, Skyline erkennbar, keine Regression bei den Standardgebaeuden
Die vollständige Kachel, echter Render, keine Regression. Track entlang des Rheins, alle 2.745 regulären Gebäudeteile unverändert wie zuvor, die Skyline bleibt an jeder Stelle wiedererkennbar — der Fix greift ausschließlich in den 24 Türmen über der Knick-Höhe, der Rest der Kachel ist byte-identisch zum bisherigen Verhalten.
AKT 12Landmark-ZwergeUmgesetzt & gegen einen echten Nutzer-Track verifiziert

Ausgerechnet die Gebäude, die als Landmark einzeln erhalten bleiben, waren die kleinsten auf der ganzen Kachel

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.

Die Floor existierte längst — nur nicht für die Gebäude, die sie am nötigsten hatten

_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)VorherNachher
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 betroffen0 %(schon vorher floor-geschützt)0 %(unverändert)
Von der Floor angehobene Landmarks561 / 573
Analyse der echten Ultramarathon-Kachel: Landmark-Hoehen vorher/nachher, ECDF und raeumliche Differenzkarte
Reale Daten aus dem tatsächlich generierten Track, nicht simuliert. Oben links: Verteilung der 573 Landmark-Gebäude auf vier Druck-Höhenklassen vor dem Fix. Oben rechts: kumulierte Höhenverteilung (ECDF) über Status Quo, Fix und drei Boost-Stufen — die Floor katapultiert die komplette Verteilung schlagartig über jede Druckbarkeits-Schwelle. Unten: die ganze Kachel mit den echten Footprint-Polygonen (dicker Rand = Landmark, dünner Rand = gemergter Cluster), eingefärbt nach Δ-Höhe — nur die 561 tatsächlich angehobenen Landmarks sind blau, der Rest (sowohl unveränderte Landmarks als auch die bereits geschützten Cluster) bleibt neutral grau.
Ein zweiter, unabhängiger Hebel: building_height_boost

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.

AKT 13Der schwebende HafenBehoben & gegen einen echten Blender-Container-Lauf verifiziert

Ein Nutzer meldet ein unsichtbares Hafenbecken — die erste Korrektur brachte einen zweiten, selbstgemachten Fehler mit, und der dritte Fund war der eigentlich interessante

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.

Ray-Cast-Diagnose: Base-Oberflächenhöhe über dem Wasser-Footprint, Rheinbogen dunkel (korrekt geschnitten) vs. Hafenbecken-Finger hell (Terrain nicht durchbrochen)
Direkt an der hochgeladenen 3MF gemessen, nicht simuliert. Jeder Punkt ist ein Downward-Raycast auf die Base-Oberfläche, eingefärbt nach ihrer Höhe innerhalb der eigenen Wasser-Z-Spanne jedes Features. Der Rheinbogen (oben, S-Kurve) ist fast durchgehend dunkel — korrekt bis zum Wasserboden geschnitten, bis auf zwei helle Punkte genau dort, wo Brücken bewusst über den Fluss fahren. Die Hafenbecken-Finger (unten links, dieselbe Kammform wie im Screenshot des Nutzers) sind fast vollständig hell: Terrain deckelt hier praktisch die gesamte Fläche zu.
Ursache: eine Merge-Stufe, die den Wasser-Typ wegwarf

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).

Vorher/Nachher: dieselben echten Rhein+Hafen-Polygone eingefärbt nach zugewiesenem water_level_z
Dieselbe echte Bbox aus der Nutzer-Meldung, einmal mit altem und einmal mit neuem water_optimizer.py. Links: das komplette gemergte Polygon (4.921 Randvertices, subtype='merged') bekommt ein einziges flaches Niveau — 20,6 m, dominiert vom Fluss. Rechts: derselbe Datensatz (subtype='river' erhalten) terrassiert jetzt korrekt in zwei Niveaus, 20,6 m für den Flussarm und 30,9 m für die Hafen-Seite — eine reale ~10,3 m-Korrektur, exakt in der Region, die der Ray-Cast oben als unterschnitten auswies.

Zweiter Fund: Schneiden und Extrudieren waren nie zwei getrennte Operationen — sie sollten es sein

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.

Dritter Fund: die eigentliche Frage war nie gestellt worden

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.

Der Fix: floor() statt round(), plus ein Default auf null

_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 liegt100 %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 Kante1,771 mm— (Mechanismus behoben)
Terrain-Heightmap als Fläche, Wasser-Objekte als gestrichelte Polygone, gleiche Farbskala Vorher/Nachher: Terrain-Heightmap vs. Wasser-Top, round+0,2mm gegen floor+0mm
Terrain-Heightmap und Wasser-Objekte auf derselben Farbskala (m, real-world), Wasser-Umriss gestrichelt. Links: der Endzustand — kein harter Farbsprung am Übergang von Ufer zu Wasser. Rechts direkter Vorher/Nachher-Vergleich derselben Kachel: links (0.40.1, round() + 0,2 mm) liest sich der Fluss als sichtbar hellerer/höherer Farbton als die unmittelbaren Uferpixel — das ist die Klippe; rechts (floor() + 0 mm) liegt das Wasser satt blau, klar unter dem Terrain-Ton am Rand.

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.

Vierter Fund: die eigene Validierung konnte den nächsten eigenen Fehler nicht sehen

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.

Root Cause: Schneiden ist immer noch keine eigene Operation — nur diesmal in die andere Richtung falsch

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.

Diesmal gegen den echten Blender-Output verifiziert, nicht gegen die Heightmap

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.

Ray-Cast auf der echten, frisch generierten 3MF: Base-Oberfläche über dem größten Wasser-Shell-Footprint Echter Top-Down-Blender-Render der frisch generierten Düsseldorf-Kachel mit dem Fix
Beide Belege am echten, frisch generierten Output, nicht an Zwischendaten. Links: derselbe Ray-Cast-Mechanismus wie ganz am Anfang dieses Akts, dieses Mal gegen die tatsächliche komponierte Base-Mesh statt gegen die Heightmap — fast durchgehend dunkel (Base auf Wasserboden-Niveau), die zwei hellen Flecken sind Brückenquerungen. Rechts: echter Cycles-Top-Down-Render derselben Kachel — Rhein-Bogen und Medienhafen-Becken beide durchgehend als Wasser sichtbar, nicht unter Terrain verschwunden.

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.

AKT 14Silhouette statt ErosionBehoben & gegen einen echten München-Lauf verifiziert

Eine einzige Erosion beantwortete vier verschiedene Fragen — und wurde dabei zur destruktiven Vereinfachung

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.

Entscheidung: Rekonstruktion statt Erosion, plus getrennte Schwellenwerte

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).

Drei Panels: echte München-Gebäude, ALT mit destruktiver Öffnung, NEU mit printable_silhouette und Dachrelief, gleicher Cluster
Identischer echter Ausschnitt aus dem München-Datensatz (154 Gebäude, echte OSM/Overture-Footprints), identischer Eingabe-Cluster — nur der letzte Verarbeitungsschritt unterscheidet sich. Links: die Rohdaten. Mitte: die alte Öffnung, eine Höhe pro Teil. Rechts: printable_silhouette + relief_partition, eingefärbt nach tatsächlicher gedruckter Höhe — 8 von 10 Teilen zeigen jetzt echte Höhenstufen statt einer Einheitshöhe.
Kennzahl (identischer 154-Gebäude-Cluster, echtes München)ALT (Öffnung + max())NEU (Silhouette + Relief)
Druckbare Teile910 → 33 Relief-Stücke
Erhaltene Fläche ggü. Rohdaten (nach gemeinsamem Spalt-Schließen)156 %172 %
Teile mit >1 echter gedruckter Höhe0 / 9 (immer flach)8 / 10
Innenhöfe: als Loch erhalten / bewusst gefüllt1 / 0 (Zufallsprodukt des Spalt-Schwellenwerts)0 / 1 (nach 1 mm-Wahrnehmungsschwelle)
Zoom auf ein einzelnes reales zweigeteiltes Gebäude: von der alten Öffnung vollständig gelöscht, von der neuen Silhouette korrekt mit echter Höhe erhalten
Der krasseste Einzelfall im selben Cluster, direkt gemessen. Ein reales, zweigeteiltes Gebäude wird von der alten destruktiven Öffnung vollständig gelöscht (0 m² übrig) — die neue Silhouette-Rekonstruktion erhält es mit seiner echten, rekonstruierten Höhe. Kein Konstrukt, sondern die tatsächliche Ausgabe desselben Skripts auf denselben Koordinaten.

Der Slicer-Realitätscheck kam vor der Implementierung, nicht danach

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 Fehler, der drei Anläufe brauchte

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.

Der Fix: gezielt reparieren, nicht pauschal — und nicht stärker als nötig

_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.

Ein zweiter, unabhängiger Fund — dieses Mal erst beim Export sichtbar

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.

Der Fix: dieselbe Aufräum-Logik ein zweites Mal, direkt vor dem Export-Gate

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.

Balkendiagramm: dieselbe zusammengeführte Fläche zeigt zwei verschiedene gedruckte Höhen statt einer einheitlichen Höhe
Innerhalb eines einzigen zusammengeführten Teils desselben Clusters. ALT hätte die komplette Fläche flach bei der höchsten Mitgliedshöhe gedruckt; NEU löst sie in ihre echten, quantisierten Höhenstufen auf.
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ßen1.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-Carving23.913 → 23.151
Entartete Schalen beim Komponieren / erneut beim Export entfernt110 / 26
3MF-Exporterfolgreich, 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.