Not for assembler/world: the 8 m -> 4 m strate chunk change moves every stratum, so the SurfaceWorld spawn camera floats in a void and the L1 mesh export shrank 58.7 -> 31.7 MB. Kept for its diagnostics, the boundarySeal finding and the readable capture shading. See Linux17/REPORT.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CSFYgsdW8ejZSyiMf4qRSX
90 lines
3.6 KiB
Python
90 lines
3.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Measure upward mesh faces around the FI/SW stratum interface."""
|
|
import array
|
|
import collections
|
|
import math
|
|
import struct
|
|
import sys
|
|
|
|
|
|
def read_exact(stream, size):
|
|
data = stream.read(size)
|
|
if len(data) != size:
|
|
raise EOFError(f"wanted {size} bytes, got {len(data)}")
|
|
return data
|
|
|
|
|
|
path = sys.argv[1]
|
|
low_z = float(sys.argv[2]) if len(sys.argv) > 2 else -80.0
|
|
high_z = float(sys.argv[3]) if len(sys.argv) > 3 else -45.0
|
|
with open(path, "rb") as stream:
|
|
if read_exact(stream, 8) != b"VFMSH001":
|
|
raise SystemExit("bad VFMSH001 header")
|
|
version, voxel_um, tile_count = struct.unpack("<3I", read_exact(stream, 12))
|
|
camera = struct.unpack("<3d", read_exact(stream, 24))
|
|
yaw, pitch = struct.unpack("<2d", read_exact(stream, 16))
|
|
per_lod = collections.defaultdict(lambda: {"tiles": 0, "faces": [], "spans": []})
|
|
near_tiles = collections.defaultdict(list)
|
|
for _ in range(tile_count):
|
|
key = struct.unpack("<4i", read_exact(stream, 16))
|
|
surf_vertices, skirt_count, vertex_count, index_count, mask_count = struct.unpack(
|
|
"<5I", read_exact(stream, 20))
|
|
p = array.array("f")
|
|
p.frombytes(read_exact(stream, vertex_count * 12))
|
|
n = array.array("f")
|
|
n.frombytes(read_exact(stream, vertex_count * 12))
|
|
if sys.byteorder != "little":
|
|
p.byteswap(); n.byteswap()
|
|
indices = array.array("I")
|
|
indices.frombytes(read_exact(stream, index_count * 4))
|
|
masks = read_exact(stream, mask_count)
|
|
if sys.byteorder != "little":
|
|
indices.byteswap()
|
|
lod = key[3]
|
|
z_values = []
|
|
for tri in range(index_count // 3):
|
|
if masks and masks[tri] != 0:
|
|
continue
|
|
a, b, c = indices[tri * 3:tri * 3 + 3]
|
|
if max(a, b, c) >= surf_vertices:
|
|
continue
|
|
normal_z = (n[a * 3 + 2] + n[b * 3 + 2] + n[c * 3 + 2]) / 3.0
|
|
if normal_z < 0.65:
|
|
continue
|
|
z = (p[a * 3 + 2] + p[b * 3 + 2] + p[c * 3 + 2]) / 3.0
|
|
if low_z <= z <= high_z:
|
|
z_values.append(z)
|
|
if not z_values:
|
|
continue
|
|
z_values.sort()
|
|
row = per_lod[lod]
|
|
row["tiles"] += 1
|
|
row["faces"].extend(z_values)
|
|
span = z_values[math.floor(.95 * (len(z_values) - 1))] - z_values[math.floor(.05 * (len(z_values) - 1))]
|
|
row["spans"].append(span)
|
|
extent = 32 * (1 << lod) * voxel_um / 1_000_000.0
|
|
cx = (key[0] + 0.5) * extent
|
|
cy = (key[1] + 0.5) * extent
|
|
near_tiles[lod].append((math.hypot(cx - camera[0], cy - camera[1]), key,
|
|
z_values[len(z_values) // 2], span, len(z_values)))
|
|
|
|
|
|
def q(values, fraction):
|
|
return values[math.floor(fraction * (len(values) - 1))] if values else math.nan
|
|
|
|
|
|
print(f"camera_m={camera} yaw={yaw} pitch={pitch} boundary_band_m={low_z}..{high_z}")
|
|
print("lod tiles_with_boundary_faces faces z_m_p05/median/p95 tile_span_m_p50/p95/max")
|
|
for lod in sorted(per_lod):
|
|
row = per_lod[lod]
|
|
spans = sorted(row["spans"])
|
|
print(f"{lod} {row['tiles']} {len(row['faces'])} "
|
|
f"{q(sorted(row['faces']), .05):.4f}/{q(sorted(row['faces']), .5):.4f}/"
|
|
f"{q(sorted(row['faces']), .95):.4f} "
|
|
f"{q(spans, .5):.4f}/{q(spans, .95):.4f}/{max(spans):.4f}")
|
|
print("nearest_xy_boundary_tiles_by_lod:")
|
|
for lod in sorted(near_tiles):
|
|
for distance, key, median, span, faces in sorted(near_tiles[lod])[:3]:
|
|
print(f"lod={lod} key={key} xy_distance_m={distance:.2f} "
|
|
f"boundary_median_z={median:.4f} within_tile_p05_p95_m={span:.4f} faces={faces}")
|