Files
gta6/tools/extract/run.py
knsoandClaude Opus 5 f226bc2c95 feat(world): Bordstein bekommt gebürsteten Beton von Poly Haven
Auf Wunsch des Nutzers: brushed_concrete_04 statt der Textur aus
modular_sidewalk_curb_kit. Die Wand ist 18 cm hoch, da trägt eine Textur mit
waagerechter Struktur mehr als das Modulmuster des Kits.

Damit ist das Kit nirgends mehr im Spiel: Die Geometrie des Bordsteins war von
Anfang an selbst gebaut, der Gehweg nimmt seit der Platzhalter-Korrektur
concrete_pavement, und der Bordstein jetzt brushed_concrete_04 - beides
Poly Haven (CC0). Der Kit-Import in tools/assets/textures.mjs entfällt samt
sharp-Abhängigkeit, die drei kopierten Kit-Texturen sind entfernt, und die
Namensnennung im Manifest nennt Just8 nicht mehr, weil nichts davon mehr
benutzt wird. Der Kit-Ordner des Nutzers bleibt unangetastet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 12:42:10 +02:00

179 lines
9.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Erzeugt aus den Kacheln die Chunk-Dateien. Aufruf: uv run python run.py"""
import json
from collections import Counter
from dataclasses import replace
from pathlib import Path
from scipy import ndimage as ndi
from shapely.geometry import LineString, Point
from shapely.ops import unary_union
from leonida.annotations import load_annotations, mark_generated, remove_annotations
from leonida.buildings import building_polygons
from leonida.classify import CLASSES, class_shares, classify, drop_map_labels
from leonida.emit import (chunk_box_of, chunk_payload, in_chunk, split_by_chunk_all,
write_chunks, write_debug_png)
from leonida.heights import PALM_KERB, assign_heights, palm_points
from leonida.marking import markings
from leonida.park import World, apply_flamingo_park
from leonida.roads import road_graph
from leonida.section import SECTION
from leonida.street import build_street, punch
from leonida.tiles import COMMIT, download_tiles, load_mosaic
from leonida.vectors import land_polygons, mask_polygons, road_mask, road_polygons
from overview import render_map
SEED = 20260912
OUT = Path(__file__).resolve().parents[2] / "public" / "data" / "vice-a"
PARK = Path("data/flamingo-park.toml")
TOLERANCE = {"wasser": (0.30, 0.36), "strasse": (0.10, 0.14), "gebaeude": (0.09, 0.13)}
MARKING_MIN_AREA = 0.001 # m² — ein Strich ist 3 m × 10 cm = 0,3 m², die Flächengrenze 0,5 m² verwürfe ihn
def _pieces(geometry) -> list:
return [g for g in getattr(geometry, "geoms", [geometry]) if not g.is_empty]
def _clip_lines(line, cell) -> list:
"""Der Anteil einer Linie in diesem Chunk, in Stücken."""
part = line.intersection(cell)
return [g for g in _pieces(part) if g.geom_type == "LineString" and g.length > 0.5]
def fit_to_carriageway(world: World, carriage) -> World:
"""Die Welt an die gebaute Fahrbahn anpassen (Entwurf, Ablauf Schritte 3 und 7).
Bodenplatte und Beläge bekommen dasselbe Loch — fehlt es in einer Schicht, liegt dort ein Belag
über der Mulde oder die Platte schließt sie. Palmen näher als PALM_KERB an der Fahrbahn fallen
weg, nach derselben Regel, mit der apply_street() sie von der Jefferson Avenue fernhält:
palm_points() hält nur Abstand zum gemessenen Korridor, und die Fahrbahn ragt an Kurven und
Kreuzungen darüber hinaus.
"""
return replace(world,
land=punch(world.land, carriage),
surfaces=[(part, kind) for poly, kind in world.surfaces
for part in punch([poly], carriage)],
palms=[p for p in world.palms if carriage.distance(Point(p)) >= PALM_KERB])
def chunk_markings(marks, i: int, j: int) -> list:
"""Die Markierungen in Chunk (i, j) — an der Chunkgrenze geteilt, nicht verworfen."""
return [(part, art) for poly, art in marks
for part in split_by_chunk_all([poly], i, j, min_area=MARKING_MIN_AREA)]
def main() -> None:
cache = Path("cache")
download_tiles(cache)
mosaic = load_mosaic(cache)
labels, unknown_share = classify(mosaic, report_unknown=True)
labels = drop_map_labels(labels)
removal = remove_annotations(mosaic, labels, load_annotations(Path("data/annotations.toml")))
labels = removal.labels
labels = drop_map_labels(labels) # Inselreste, die erst das Entfernen im Wasser hinterlässt
print(removal.report())
if unknown_share > 0.05:
raise SystemExit(f"{unknown_share:.1%} der Pixel unbekannt. Palette kalibrieren, nicht die Grenze anheben.")
shares = class_shares(labels)
for name, (lo, hi) in TOLERANCE.items():
if not lo <= shares[name] <= hi:
raise SystemExit(f"Klassenanteil {name}={shares[name]:.3f} außerhalb {lo}..{hi}. Palette prüfen.")
land = land_polygons(labels)
roads = road_polygons(labels)
# Der Graph ersetzt die nachgezeichneten Straßenränder (Entwurf Teil 2). Die
# Distanztransformation misst an jedem Straßenpixel den Abstand zum nächsten Nicht-Straßen-
# pixel; doppelt genommen ist das die Korridorbreite an dieser Stelle.
distance = ndi.distance_transform_edt(road_mask(labels))
graph = road_graph(labels, distance)
print(f"Straßennetz: {len(graph.nodes)} Knoten, {len(graph.edges)} Kanten")
sand = mask_polygons(labels == CLASSES["sand"], 50, 1.0)
parks = mask_polygons(labels == CLASSES["gruen"], 50, 1.0)
urban = mask_polygons(labels == CLASSES["stadt"], 80, 1.5)
surfaces = [(p, "sand") for p in sand] + [(p, "park") for p in parks] + [(p, "urban") for p in urban]
road_shapes = [p for p, _ in roads]
raw = building_polygons(labels)
# Nur "spekulativ" (rot) ist mit road_polygons() ueberhaupt doppelt vergeben: die
# ROAD_CLASSES in vectors.py zaehlen "spekulativ" selbst als Strassenklasse, und
# road_mask() schliesst zusaetzlich mit disk(3), um Strassennamen zu ueberbruecken.
# Wuerde man road_shapes (= road_polygons(labels), inklusive dieser Schliessung und
# inklusive "spekulativ") direkt gegen ALLE rohen Gebaeude pruefen, vergleicht ein
# isoliertes "spekulativ"-Gebaeude sich praktisch mit sich selbst (dieselben Pixel
# bilden dort auch das Strassenpolygon) und faellt so gut wie immer heraus: am realen
# Mosaik blieben nur 3 von 267 "spekulativ"-Gebaeuden uebrig statt der erwarteten
# Groessenordnung (Task-6-Referenzwerte: 258 von 267), und die Schliessung riss
# zusaetzlich 211 gewoehnliche (graue) Gebaeude mit, die die Notiz in Task 5 gar nicht
# meinte ("die ROTEN Flaechen ... werden abgezogen"). Deshalb hier ein eigenes,
# unzweideutiges Strassenmass nur aus "strasse"/"weg" (ohne "spekulativ", ohne die
# Namens-Schliessung) und die Pruefung nur auf spekulative Kandidaten begrenzt; graue
# Gebaeude durchlaufen ungeprueft. Ergebnis am realen Mosaik: 2.348 Gebaeude, davon 263
# spekulativ (Referenz: 2.352 bzw. 258) — Wahrzeichenhoehen (68, 31-248 m) und
# Palmenzahl (2.133) treffen die Referenz exakt.
unambiguous_mask = (labels == CLASSES["strasse"]) | (labels == CLASSES["weg"])
unambiguous_roads = mask_polygons(unambiguous_mask, min_area=20, simplify=0.8)
buildings = [(p, s) for p, s in raw
if not s or not any(p.intersection(r).area > 0.5 * p.area for r in unambiguous_roads)]
landmarks = json.loads(Path("data/skyline.json").read_text())["landmarks"]
districts = json.loads(Path("data/districts.json").read_text())
heights = assign_heights([p for p, _ in buildings], landmarks, districts, SEED)
marked = mark_generated([p for p, _ in buildings], heights, removal.generated)
print(f"Gebäude: {len(buildings)}, davon {marked} unter entfernten Beschriftungen ergänzt")
edge_lines = [LineString(e.points) for e in graph.edges]
palms = palm_points(sand, parks, edge_lines, [p for p, _ in buildings], road_shapes, land, SEED)
world = World(land=land, surfaces=surfaces, roads=roads, graph=graph,
buildings=[(p, h, s) for (p, s), h in zip(buildings, heights)], palms=palms)
world, messages = apply_flamingo_park(world, PARK, SEED)
for message in messages:
print(message)
corridor = unary_union([p for p, bridge in world.roads if not bridge])
bridge_polys = [p for p, bridge in world.roads if bridge]
street = build_street(world.graph, corridor, bridge_polys)
marks = markings(world.graph)
palmen = len(world.palms)
world = fit_to_carriageway(world, street.carriageway)
arten = dict(sorted(Counter(art for _, art in marks).items()))
print(f"Straßen: {len(street.kerb)} Bordsteinstücke, {len(street.lots)} Restflächen, "
f"{len(marks)} Markierungen {arten}, {palmen - len(world.palms)} Palmen an der Fahrbahn entfernt")
payloads = []
for j in range(12):
for i in range(12):
payloads.append(chunk_payload(
i, j,
land=list(split_by_chunk_all(world.land, i, j)),
surfaces=[(part, kind) for (p, kind) in world.surfaces
for part in split_by_chunk_all([p], i, j)],
carriageway=list(split_by_chunk_all(_pieces(street.carriageway), i, j)),
curb=[part for line in street.kerb
for part in _clip_lines(line, chunk_box_of(i, j))],
sidewalk=list(split_by_chunk_all(_pieces(street.sidewalk), i, j)),
markings=chunk_markings(marks, i, j),
lots=list(split_by_chunk_all(street.lots, i, j)),
bridges=list(split_by_chunk_all(bridge_polys, i, j)),
edges=[(part, e.klass, e.width, SECTION[e.klass].carriageway)
for e in world.graph.edges
for part in _clip_lines(LineString(e.points), chunk_box_of(i, j))],
buildings=[(p, h, s) for (p, h, s) in world.buildings
if p.intersects(chunk_box_of(i, j))],
palms=[pt for pt in world.palms if in_chunk(pt, i, j)],
paths=[part for p in world.paths for part in split_by_chunk_all([p], i, j)],
))
write_debug_png(Path("debug"), mosaic, payloads)
write_chunks(OUT, payloads, {
"slice": "vice-a", "source_commit": COMMIT, "seed": SEED,
"chunk_size": 250, "chunks": [p["chunk"] for p in payloads],
"class_shares": shares,
"attribution": "Kartenkacheln: GTA VI Map (CC BY 4.0) — https://map.gtadb.org, YANIS v15. "
"Wahrzeichen: rolux/gtamaplib-vc (Unlicense). "
"Flamingo Park: © OpenStreetMap-Mitwirkende (ODbL).",
})
print(render_map(OUT, OUT), "->", OUT)
if __name__ == "__main__":
main()