From 11f6aa76feeb7502999cf240a653a8e9070ba3d8 Mon Sep 17 00:00:00 2001 From: KNSO Date: Sat, 12 Sep 2026 15:27:37 +0200 Subject: [PATCH] docs: Palmenpunkte in Task 5 korrigieren (Rulings 20-23) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die erste Palme startet bei halber Teilung, weil die Mittellinien seit Task 4 an Kreuzungen getrennt sind und zwei gegenüberliegende Äste sonst zwei Palmen in denselben Punkt setzen. Der feste Versatz von 4 m weicht einer Suche vom Fahrbahnrand: am echten Mosaik stünden damit 60 % der Palmen auf der Fahrbahn. Dazu zwei Tests, die es bisher nicht gab, und die korrigierte Signatur im Interfaces-Block. Co-Authored-By: Claude Opus 5 --- .../plans/2026-09-12-leonida-vice-slice.md | 88 ++++++++++++++++--- 1 file changed, 74 insertions(+), 14 deletions(-) diff --git a/docs/superpowers/plans/2026-09-12-leonida-vice-slice.md b/docs/superpowers/plans/2026-09-12-leonida-vice-slice.md index 74e70f4..49646cb 100644 --- a/docs/superpowers/plans/2026-09-12-leonida-vice-slice.md +++ b/docs/superpowers/plans/2026-09-12-leonida-vice-slice.md @@ -893,7 +893,7 @@ Co-Authored-By: Claude Opus 5 " **Interfaces:** - Consumes: `mask_polygons`, `centerlines` aus Task 4, `CLASSES` aus Task 3 -- Produces: `regularize(poly) -> Polygon | None`, `building_polygons(labels) -> list[tuple[Polygon, bool]]` (Polygon, `spekulativ`), `assign_heights(polys, landmarks, districts, seed) -> list[float]`, `height_sources(...) -> list[str]`, `palm_points(labels, lines, seed) -> list[tuple[float, float]]` +- Produces: `regularize(poly) -> Polygon | None`, `building_polygons(labels) -> list[tuple[Polygon, bool]]` (Polygon, `spekulativ`), `assign_heights(polys, landmarks, districts, seed) -> list[float]`, `height_sources(...) -> list[str]`, `palm_points(sand_polys, park_polys, road_lines, building_polys, road_polys, seed) -> list[tuple[float, float]]` - [ ] **Step 1: Den fehlschlagenden Test für Gebäude schreiben** @@ -1068,34 +1068,89 @@ def assign_heights(polys: list[Polygon], landmarks: list[dict], Run: `cd tools/extract && uv run pytest -v` Expected: PASS, alle Tests -- [ ] **Step 9: Palmenpunkte ergänzen** +- [ ] **Step 9: Den fehlschlagenden Test für Palmenpunkte schreiben** + +An `tools/extract/tests/test_heights.py` anfügen: + +```python +from shapely.geometry import LineString, Point +from leonida.heights import PALM_KERB, palm_points + +def test_no_palm_stands_on_the_carriageway(): + """Der Versatz muss vom Fahrbahnrand her gedacht werden, nicht von einem festen Wert. + + Am echten Mosaik hat die Fahrbahn an den Mittellinienpunkten p50 5,4 m, p90 11,7 m + und p95 14,0 m Halbbreite. Ein fester Versatz von 4 m setzt 60 % der Palmen auf die + Fahrbahn, einer von 11 m immer noch 12,5 % – und auf der Hälfte der Straßen stünde + die Palme dann im Nachbargebäude. + """ + road = Polygon([(0, -9), (200, -9), (200, 9), (0, 9)]) # 18 m breite Fahrbahn + line = LineString([(0, 0), (200, 0)]) + sand = Polygon([(0, 20), (200, 20), (200, 40), (0, 40)]) + pts = palm_points([sand], [], [line], [], [road], seed=1) + assert pts + assert all(road.distance(Point(x, y)) >= PALM_KERB for x, y in pts) + +def test_palms_do_not_pile_up_where_centerlines_meet(): + """Mittellinien sind an Kreuzungen getrennt (Task 4). Ein Start bei Position 0 setzt + auf zwei gegenüberliegenden Ästen zwei Palmen in genau denselben Punkt.""" + sand = Polygon([(-80, 20), (80, 20), (80, 40), (-80, 40)]) + arms = [LineString([(0, 0), (60, 0)]), LineString([(0, 0), (-60, 0)])] + pts = palm_points([sand], [], arms, [], [], seed=1) + assert pts + assert len(set(pts)) == len(pts) +``` + +- [ ] **Step 10: Palmenpunkte ergänzen** An `leonida/heights.py` anfügen: ```python PALM_SPACING = 12.0 # Meter entlang der Mittellinie PALM_BEACH_RANGE = 40.0 # Meter Abstand zur nächsten Sandfläche -PALM_CLEARANCE = 6.0 # Meter Abstand zu Gebäuden und Straßen im Park +PALM_CLEARANCE = 6.0 # Meter Abstand zu Gebäuden +PALM_KERB = 1.5 # Meter Abstand zum Fahrbahnrand +# Versatzkandidaten von innen nach außen; genommen wird der erste, der Fahrbahn und +# Gebäude freihält. Ein fester Wert geht nicht: die Fahrbahn ist an den Mittellinien- +# punkten p50 5,4 m, p90 11,7 m, p95 14,0 m halbbreit (am echten Mosaik gemessen). +PALM_VERGE = (6.0, 8.0, 10.0, 12.0, 14.0, 16.0) + + +def _verge_point(line, p, side: str, roads, buildings): + """Erster Versatz von innen nach außen, der Fahrbahn und Gebäude freihält.""" + for dist in PALM_VERGE: + off = line.parallel_offset(dist, side) + if off.is_empty: + continue + q = off.interpolate(off.project(p)) + if roads is not None and q.distance(roads) < PALM_KERB: + continue + if buildings is not None and q.distance(buildings) < PALM_CLEARANCE: + continue + return [(round(q.x, 2), round(q.y, 2))] + return [] def palm_points(sand_polys, park_polys, road_lines, building_polys, road_polys, seed: int): """Palmen entlang strandnaher Mittellinien und verstreut in Parks.""" from shapely.ops import unary_union sand = unary_union(sand_polys) if sand_polys else None - blocked = unary_union(list(building_polys) + list(road_polys)) if (building_polys or road_polys) else None + roads = unary_union(list(road_polys)) if road_polys else None + buildings = unary_union(list(building_polys)) if building_polys else None + blocked = unary_union([g for g in (roads, buildings) if g is not None]) \ + if (roads is not None or buildings is not None) else None points = [] for line in road_lines: if sand is None or line.distance(sand) > PALM_BEACH_RANGE: continue - steps = max(1, int(line.length // PALM_SPACING)) - for k in range(steps + 1): - p = line.interpolate(k * PALM_SPACING) - for side in (-1, 1): - off = line.parallel_offset(4.0 * side, "left" if side > 0 else "right") - if off.is_empty: - continue - q = off.interpolate(off.project(p)) - points.append((round(q.x, 2), round(q.y, 2))) + # Start bei halber Teilung: die Mittellinien sind an Kreuzungen getrennt, ein + # Start bei 0 setzt je Ast eine Palme in denselben Punkt. + pos = min(PALM_SPACING / 2, line.length / 2) + while pos <= line.length: + p = line.interpolate(pos) + for side in ("left", "right"): + points.extend(_verge_point(line, p, side, roads, buildings)) + pos += PALM_SPACING for park in park_polys: minx, miny, maxx, maxy = park.bounds n = int(park.area // 250) @@ -1108,7 +1163,12 @@ def palm_points(sand_polys, park_polys, road_lines, building_polys, road_polys, return points ``` -- [ ] **Step 10: Commit** +- [ ] **Step 11: Test laufen lassen, er muss bestehen** + +Run: `cd tools/extract && uv run pytest -v` +Expected: PASS, alle Tests + +- [ ] **Step 12: Commit** ```bash git add tools/extract/leonida/buildings.py tools/extract/leonida/heights.py tools/extract/tests