Geo/tools/ is now every geographic script, with Geo/README.md documenting each one; the Blender-side builders moved beside their outputs in Art/blender/*/ and import their helpers from Geo/tools/osm_common.py. That module gains to_unreal_local() and to_blender_local(): the only place in the repository that multiplies metres by 100, so a second silent scale cannot be introduced. block_height_report.py adds height provenance for the block — 3 of 15 buildings have no height data and render at a placeholder 6.4 m. asset_metadata.py writes and audits <asset>.geo.json sidecars; four now exist (hero-facade, terrain, street, blockout) and the audit recomputes each centre from the origin, failing anything that disagrees.
76 lines
2.7 KiB
Python
Executable File
76 lines
2.7 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Report which block buildings have real height data and which are guesses.
|
|
|
|
Usage:
|
|
Geo/tools/block_height_report.py Geo/raw/osm-api-map-2026-11-06.osm
|
|
|
|
`height` and `building:levels` are the only height evidence OSM carries. Where
|
|
neither exists, Blosm falls back to `defaultLevels * levelHeight`, which is a
|
|
placeholder: the demo renders a constant-height block instead of a real skyline.
|
|
This tool lists every building in the demo bounds with its height source, so the
|
|
substitutions are visible and measurable rather than buried in the model.
|
|
|
|
Exit code is 0 either way; the count of guessed buildings is the deliverable.
|
|
"""
|
|
|
|
import argparse
|
|
import math
|
|
import sys
|
|
|
|
import osm_common as oc
|
|
|
|
LEVEL_HEIGHT = 3.2
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("osm")
|
|
args = ap.parse_args()
|
|
nodes, ways = oc.load(args.osm)
|
|
|
|
line, lat0, lon0, lengths, block_len = oc.block_extent(
|
|
nodes, ways, "Hollywood Boulevard", "North Orange Drive", "North Highland Avenue")
|
|
|
|
def in_block(pts):
|
|
if min(p[1] for p in pts) > line[-1][1] or max(p[1] for p in pts) < line[0][1]:
|
|
return False
|
|
return True
|
|
|
|
rows = []
|
|
for wid, tags, refs in ways:
|
|
if "building" not in tags:
|
|
continue
|
|
pts = oc.way_points(nodes, refs)
|
|
if not pts or not in_block(pts):
|
|
continue
|
|
tag_height = tags.get("height")
|
|
levels = tags.get("building:levels")
|
|
if tag_height:
|
|
height, source = float(tag_height), "height tag"
|
|
elif levels:
|
|
height, source = float(levels) * LEVEL_HEIGHT, f"levels x {LEVEL_HEIGHT} m"
|
|
else:
|
|
height, source = 2 * LEVEL_HEIGHT, f"GUESS: default 2 levels x {LEVEL_HEIGHT} m"
|
|
nearest = min(oc.project(p, line, lat0, lon0, lengths)[0] for p in pts)
|
|
side = "S" if oc.project(pts[0], line, lat0, lon0, lengths)[2] < 0 else "N"
|
|
rows.append((side, nearest, wid, tags.get("name", "") or tags.get("addr:housenumber", ""),
|
|
height, source))
|
|
|
|
rows.sort(key=lambda r: (r[0], r[1]))
|
|
guessed = [r for r in rows if r[5].startswith("GUESS")]
|
|
print(f"{len(rows)} buildings in the block, {len(guessed)} with a guessed height\n")
|
|
for side, setback, wid, name, height, source in rows:
|
|
flag = " <-- guessed" if source.startswith("GUESS") else ""
|
|
print(f"{side} way {wid:>10} setback {setback:5.1f} m height {height:5.1f} m "
|
|
f"({source:26}) {name[:34]}{flag}")
|
|
|
|
if guessed:
|
|
print("\nGuessed heights to replace from the reference photography:")
|
|
for side, setback, wid, name, height, source in guessed:
|
|
print(f" {side} way {wid} {name}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|