diff options
| author | Valentin Popov <valentin@popov.link> | 2026-06-21 23:35:19 +0300 |
|---|---|---|
| committer | Valentin Popov <valentin@popov.link> | 2026-06-21 23:35:19 +0300 |
| commit | 50c2cf4686b53ebd2b76318223096660e92305a4 (patch) | |
| tree | 8741109102a21faaa09a013a047e5c7b74f62b12 /tools/terrain_map_preview_renderer.py | |
| parent | 96a25b6c0e39ee39bceddbc8eae5bda8f305acbf (diff) | |
| download | fparkan-50c2cf4686b53ebd2b76318223096660e92305a4.tar.xz fparkan-50c2cf4686b53ebd2b76318223096660e92305a4.zip | |
chore: remove Python tooling and resource viewer
Diffstat (limited to 'tools/terrain_map_preview_renderer.py')
| -rw-r--r-- | tools/terrain_map_preview_renderer.py | 679 |
1 files changed, 0 insertions, 679 deletions
diff --git a/tools/terrain_map_preview_renderer.py b/tools/terrain_map_preview_renderer.py deleted file mode 100644 index 86d72d7..0000000 --- a/tools/terrain_map_preview_renderer.py +++ /dev/null @@ -1,679 +0,0 @@ -#!/usr/bin/env python3 -""" -Software 3D renderer for terrain Land.msh + Land.map overlay. - -Output format: binary PPM (P6), dependency-free. -""" - -from __future__ import annotations - -import argparse -import math -import struct -from pathlib import Path -from typing import Any - -import archive_roundtrip_validator as arv - -MAGIC_NRES = b"NRes" - - -def _entry_payload(blob: bytes, entry: dict[str, Any]) -> bytes: - start = int(entry["data_offset"]) - end = start + int(entry["size"]) - return blob[start:end] - - -def _parse_nres(blob: bytes, source: str) -> dict[str, Any]: - if blob[:4] != MAGIC_NRES: - raise RuntimeError(f"{source}: not an NRes payload") - return arv.parse_nres(blob, source=source) - - -def _by_type(entries: list[dict[str, Any]]) -> dict[int, list[dict[str, Any]]]: - out: dict[int, list[dict[str, Any]]] = {} - for row in entries: - out.setdefault(int(row["type_id"]), []).append(row) - return out - - -def _get_single(by_type: dict[int, list[dict[str, Any]]], type_id: int, label: str) -> dict[str, Any]: - rows = by_type.get(type_id, []) - if not rows: - raise RuntimeError(f"missing resource type {type_id} ({label})") - return rows[0] - - -def _downsample_faces( - faces: list[tuple[int, int, int]], - max_faces: int, -) -> list[tuple[int, int, int]]: - if max_faces <= 0 or len(faces) <= max_faces: - return faces - step = len(faces) / max_faces - out: list[tuple[int, int, int]] = [] - pos = 0.0 - while len(out) < max_faces and int(pos) < len(faces): - out.append(faces[int(pos)]) - pos += step - return out - - -def load_terrain_msh( - path: Path, - *, - max_faces: int, -) -> tuple[list[tuple[float, float, float]], list[tuple[int, int, int]], dict[str, int]]: - blob = path.read_bytes() - parsed = _parse_nres(blob, str(path)) - by_type = _by_type(parsed["entries"]) - - res3 = _get_single(by_type, 3, "positions") - res21 = _get_single(by_type, 21, "terrain faces") - - pos_blob = _entry_payload(blob, res3) - if len(pos_blob) % 12 != 0: - raise RuntimeError(f"{path}: type 3 payload size is not divisible by 12") - vertex_count = len(pos_blob) // 12 - positions = [struct.unpack_from("<3f", pos_blob, i * 12) for i in range(vertex_count)] - - face_blob = _entry_payload(blob, res21) - if len(face_blob) % 28 != 0: - raise RuntimeError(f"{path}: type 21 payload size is not divisible by 28") - all_faces: list[tuple[int, int, int]] = [] - raw_face_count = len(face_blob) // 28 - dropped = 0 - for i in range(raw_face_count): - off = i * 28 - i0, i1, i2 = struct.unpack_from("<HHH", face_blob, off + 8) - if i0 >= vertex_count or i1 >= vertex_count or i2 >= vertex_count: - dropped += 1 - continue - all_faces.append((i0, i1, i2)) - - faces = _downsample_faces(all_faces, max_faces) - meta = { - "vertex_count": vertex_count, - "face_count_raw": raw_face_count, - "face_count_valid": len(all_faces), - "face_count_rendered": len(faces), - "face_dropped_invalid": dropped, - } - return positions, faces, meta - - -def load_areal_map(path: Path) -> tuple[list[dict[str, Any]], dict[str, int]]: - blob = path.read_bytes() - parsed = _parse_nres(blob, str(path)) - by_type = _by_type(parsed["entries"]) - chunk = _get_single(by_type, 12, "ArealMapGeometry") - - payload = _entry_payload(blob, chunk) - areal_count = int(chunk["attr1"]) - ptr = 0 - areals: list[dict[str, Any]] = [] - for idx in range(areal_count): - if ptr + 56 > len(payload): - raise RuntimeError(f"{path}: truncated areal header at index={idx}") - class_id = struct.unpack_from("<I", payload, ptr + 40)[0] - vertex_count, poly_count = struct.unpack_from("<II", payload, ptr + 48) - verts_off = ptr + 56 - verts_size = 12 * vertex_count - if verts_off + verts_size > len(payload): - raise RuntimeError(f"{path}: areal[{idx}] vertices out of bounds") - verts = [struct.unpack_from("<3f", payload, verts_off + 12 * i) for i in range(vertex_count)] - - links_off = verts_off + verts_size - links_size = 8 * (vertex_count + 3 * poly_count) - p = links_off + links_size - for _ in range(poly_count): - if p + 4 > len(payload): - raise RuntimeError(f"{path}: areal[{idx}] poly header out of bounds") - n = struct.unpack_from("<I", payload, p)[0] - p += 4 * (3 * n + 1) - if p > len(payload): - raise RuntimeError(f"{path}: areal[{idx}] poly data out of bounds") - - areals.append( - { - "index": idx, - "class_id": class_id, - "vertices": verts, - } - ) - ptr = p - - if ptr + 8 > len(payload): - raise RuntimeError(f"{path}: missing cells section") - cells_x, cells_y = struct.unpack_from("<II", payload, ptr) - ptr += 8 - for _x in range(cells_x): - for _y in range(cells_y): - if ptr + 2 > len(payload): - raise RuntimeError(f"{path}: cells section truncated") - hit_count = struct.unpack_from("<H", payload, ptr)[0] - ptr += 2 + 2 * hit_count - if ptr > len(payload): - raise RuntimeError(f"{path}: cells section out of bounds") - if ptr != len(payload): - raise RuntimeError(f"{path}: trailing bytes in chunk12 parse ({len(payload) - ptr})") - - meta = { - "areal_count": areal_count, - "cells_x": cells_x, - "cells_y": cells_y, - } - return areals, meta - - -def _color_for_class(class_id: int) -> tuple[int, int, int]: - x = (class_id * 1103515245 + 12345) & 0x7FFFFFFF - r = 60 + (x & 0x7F) - g = 60 + ((x >> 7) & 0x7F) - b = 60 + ((x >> 14) & 0x7F) - return r, g, b - - -def _write_ppm(path: Path, width: int, height: int, rgb: bytearray) -> None: - path.parent.mkdir(parents=True, exist_ok=True) - with path.open("wb") as handle: - handle.write(f"P6\n{width} {height}\n255\n".encode("ascii")) - handle.write(rgb) - - -def _write_obj( - path: Path, - terrain_positions: list[tuple[float, float, float]], - terrain_faces: list[tuple[int, int, int]], - areals: list[dict[str, Any]], - *, - include_areals: bool, -) -> None: - path.parent.mkdir(parents=True, exist_ok=True) - with path.open("w", encoding="utf-8", newline="\n") as out: - out.write("# Exported by terrain_map_preview_renderer.py\n") - out.write("o terrain\n") - for x, y, z in terrain_positions: - out.write(f"v {x:.9g} {y:.9g} {z:.9g}\n") - for i0, i1, i2 in terrain_faces: - # OBJ indices are 1-based. - out.write(f"f {i0 + 1} {i1 + 1} {i2 + 1}\n") - - if include_areals and areals: - base = len(terrain_positions) - area_vertex_counts: list[int] = [] - out.write("o areal_edges\n") - for area in areals: - verts = area["vertices"] - area_vertex_counts.append(len(verts)) - for x, y, z in verts: - out.write(f"v {x:.9g} {y:.9g} {z:.9g}\n") - - ptr = base - for area_idx, area in enumerate(areals): - cnt = area_vertex_counts[area_idx] - if cnt < 2: - ptr += cnt - continue - # closed polyline. - line = [str(ptr + i + 1) for i in range(cnt)] - line.append(str(ptr + 1)) - out.write("l " + " ".join(line) + "\n") - ptr += cnt - - -def _render_scene( - terrain_positions: list[tuple[float, float, float]], - terrain_faces: list[tuple[int, int, int]], - areals: list[dict[str, Any]], - *, - width: int, - height: int, - yaw_deg: float, - pitch_deg: float, - wireframe: bool, - areal_overlay: bool, -) -> bytearray: - all_positions = list(terrain_positions) - if areal_overlay: - for area in areals: - all_positions.extend(area["vertices"]) - if not all_positions: - raise RuntimeError("scene is empty") - - xs = [p[0] for p in all_positions] - ys = [p[1] for p in all_positions] - zs = [p[2] for p in all_positions] - cx = (min(xs) + max(xs)) * 0.5 - cy = (min(ys) + max(ys)) * 0.5 - cz = (min(zs) + max(zs)) * 0.5 - span = max(max(xs) - min(xs), max(ys) - min(ys), max(zs) - min(zs)) - radius = max(span * 0.5, 1e-3) - - yaw = math.radians(yaw_deg) - pitch = math.radians(pitch_deg) - cyaw = math.cos(yaw) - syaw = math.sin(yaw) - cpitch = math.cos(pitch) - spitch = math.sin(pitch) - camera_dist = radius * 3.2 - scale = min(width, height) * 0.96 - - # Terrain transform cache. - vx: list[float] = [] - vy: list[float] = [] - vz: list[float] = [] - sx: list[float] = [] - sy: list[float] = [] - for x, y, z in terrain_positions: - x0 = x - cx - y0 = y - cy - z0 = z - cz - x1 = cyaw * x0 + syaw * z0 - z1 = -syaw * x0 + cyaw * z0 - y2 = cpitch * y0 - spitch * z1 - z2 = spitch * y0 + cpitch * z1 + camera_dist - if z2 < 1e-3: - z2 = 1e-3 - vx.append(x1) - vy.append(y2) - vz.append(z2) - sx.append(width * 0.5 + (x1 / z2) * scale) - sy.append(height * 0.5 - (y2 / z2) * scale) - - def project_point(x: float, y: float, z: float) -> tuple[float, float, float]: - x0 = x - cx - y0 = y - cy - z0 = z - cz - x1 = cyaw * x0 + syaw * z0 - z1 = -syaw * x0 + cyaw * z0 - y2 = cpitch * y0 - spitch * z1 - z2 = spitch * y0 + cpitch * z1 + camera_dist - if z2 < 1e-3: - z2 = 1e-3 - px = width * 0.5 + (x1 / z2) * scale - py = height * 0.5 - (y2 / z2) * scale - return px, py, z2 - - rgb = bytearray([14, 16, 20] * (width * height)) - zbuf = [float("inf")] * (width * height) - light_dir = (0.35, 0.45, 1.0) - l_len = math.sqrt(light_dir[0] ** 2 + light_dir[1] ** 2 + light_dir[2] ** 2) - light = (light_dir[0] / l_len, light_dir[1] / l_len, light_dir[2] / l_len) - - def edge(ax: float, ay: float, bx: float, by: float, px: float, py: float) -> float: - return (px - ax) * (by - ay) - (py - ay) * (bx - ax) - - for i0, i1, i2 in terrain_faces: - x0 = sx[i0] - y0 = sy[i0] - x1 = sx[i1] - y1 = sy[i1] - x2 = sx[i2] - y2 = sy[i2] - area = edge(x0, y0, x1, y1, x2, y2) - if area == 0.0: - continue - - ux = vx[i1] - vx[i0] - uy = vy[i1] - vy[i0] - uz = vz[i1] - vz[i0] - wx = vx[i2] - vx[i0] - wy = vy[i2] - vy[i0] - wz = vz[i2] - vz[i0] - nx = uy * wz - uz * wy - ny = uz * wx - ux * wz - nz = ux * wy - uy * wx - n_len = math.sqrt(nx * nx + ny * ny + nz * nz) - if n_len > 0.0: - nx /= n_len - ny /= n_len - nz /= n_len - intensity = nx * light[0] + ny * light[1] + nz * light[2] - if intensity < 0.0: - intensity = 0.0 - shade = int(45 + 185 * intensity) - color = (min(255, shade + 6), min(255, shade + 14), min(255, shade + 28)) - - minx = int(max(0, math.floor(min(x0, x1, x2)))) - maxx = int(min(width - 1, math.ceil(max(x0, x1, x2)))) - miny = int(max(0, math.floor(min(y0, y1, y2)))) - maxy = int(min(height - 1, math.ceil(max(y0, y1, y2)))) - if minx > maxx or miny > maxy: - continue - - z0 = vz[i0] - z1 = vz[i1] - z2 = vz[i2] - inv_area = 1.0 / area - for py in range(miny, maxy + 1): - fy = py + 0.5 - row = py * width - for px in range(minx, maxx + 1): - fx = px + 0.5 - w0 = edge(x1, y1, x2, y2, fx, fy) - w1 = edge(x2, y2, x0, y0, fx, fy) - w2 = edge(x0, y0, x1, y1, fx, fy) - if area > 0: - if w0 < 0 or w1 < 0 or w2 < 0: - continue - else: - if w0 > 0 or w1 > 0 or w2 > 0: - continue - bz0 = w0 * inv_area - bz1 = w1 * inv_area - bz2 = w2 * inv_area - depth = bz0 * z0 + bz1 * z1 + bz2 * z2 - idx = row + px - if depth >= zbuf[idx]: - continue - zbuf[idx] = depth - p = idx * 3 - rgb[p + 0] = color[0] - rgb[p + 1] = color[1] - rgb[p + 2] = color[2] - - def draw_line( - xa: float, - ya: float, - xb: float, - yb: float, - color: tuple[int, int, int], - ) -> None: - x0i = int(round(xa)) - y0i = int(round(ya)) - x1i = int(round(xb)) - y1i = int(round(yb)) - dx = abs(x1i - x0i) - sx_step = 1 if x0i < x1i else -1 - dy = -abs(y1i - y0i) - sy_step = 1 if y0i < y1i else -1 - err = dx + dy - x = x0i - y = y0i - while True: - if 0 <= x < width and 0 <= y < height: - p = (y * width + x) * 3 - rgb[p + 0] = color[0] - rgb[p + 1] = color[1] - rgb[p + 2] = color[2] - if x == x1i and y == y1i: - break - e2 = 2 * err - if e2 >= dy: - err += dy - x += sx_step - if e2 <= dx: - err += dx - y += sy_step - - if wireframe: - wf = (225, 232, 246) - for i0, i1, i2 in terrain_faces: - draw_line(sx[i0], sy[i0], sx[i1], sy[i1], wf) - draw_line(sx[i1], sy[i1], sx[i2], sy[i2], wf) - draw_line(sx[i2], sy[i2], sx[i0], sy[i0], wf) - - if areal_overlay: - for area in areals: - verts = area["vertices"] - if len(verts) < 2: - continue - color = _color_for_class(int(area["class_id"])) - projected = [project_point(x, y, z + 0.35) for x, y, z in verts] - for i in range(len(projected)): - x0, y0, _ = projected[i] - x1, y1, _ = projected[(i + 1) % len(projected)] - draw_line(x0, y0, x1, y1, color) - - return rgb - - -def cmd_render(args: argparse.Namespace) -> int: - msh_path = Path(args.land_msh).resolve() - map_path = Path(args.land_map).resolve() if args.land_map else None - output_path = Path(args.output).resolve() - - positions, faces, terrain_meta = load_terrain_msh(msh_path, max_faces=int(args.max_faces)) - areals: list[dict[str, Any]] = [] - map_meta: dict[str, int] = {"areal_count": 0, "cells_x": 0, "cells_y": 0} - if map_path: - areals, map_meta = load_areal_map(map_path) - - rgb = _render_scene( - positions, - faces, - areals, - width=int(args.width), - height=int(args.height), - yaw_deg=float(args.yaw), - pitch_deg=float(args.pitch), - wireframe=bool(args.wireframe), - areal_overlay=bool(args.overlay_areals), - ) - _write_ppm(output_path, int(args.width), int(args.height), rgb) - - print(f"Rendered terrain : {msh_path}") - if map_path: - print(f"Areal overlay : {map_path}") - print(f"Output : {output_path}") - print( - "Terrain geometry : " - f"vertices={terrain_meta['vertex_count']}, " - f"faces={terrain_meta['face_count_rendered']}/{terrain_meta['face_count_valid']} " - f"(raw={terrain_meta['face_count_raw']}, dropped={terrain_meta['face_dropped_invalid']})" - ) - if map_path: - print( - "Areal map : " - f"areals={map_meta['areal_count']}, cells={map_meta['cells_x']}x{map_meta['cells_y']}" - ) - return 0 - - -def cmd_export_obj(args: argparse.Namespace) -> int: - msh_path = Path(args.land_msh).resolve() - map_path = Path(args.land_map).resolve() if args.land_map else None - output_path = Path(args.output).resolve() - - positions, faces, terrain_meta = load_terrain_msh(msh_path, max_faces=int(args.max_faces)) - areals: list[dict[str, Any]] = [] - if map_path and bool(args.include_areals): - areals, _ = load_areal_map(map_path) - - _write_obj( - output_path, - positions, - faces, - areals, - include_areals=bool(args.include_areals), - ) - - areal_vertices = sum(len(a["vertices"]) for a in areals) - print(f"Terrain source : {msh_path}") - if map_path: - print(f"Areal source : {map_path}") - print(f"OBJ output : {output_path}") - print( - "Terrain geometry : " - f"vertices={terrain_meta['vertex_count']}, " - f"faces={terrain_meta['face_count_rendered']}/{terrain_meta['face_count_valid']}" - ) - if bool(args.include_areals): - print(f"Areal edges : areals={len(areals)}, extra_vertices={areal_vertices}") - return 0 - - -def cmd_render_turntable(args: argparse.Namespace) -> int: - msh_path = Path(args.land_msh).resolve() - map_path = Path(args.land_map).resolve() if args.land_map else None - output_dir = Path(args.output_dir).resolve() - output_dir.mkdir(parents=True, exist_ok=True) - - frames = int(args.frames) - if frames <= 0: - raise RuntimeError("--frames must be > 0") - - positions, faces, terrain_meta = load_terrain_msh(msh_path, max_faces=int(args.max_faces)) - areals: list[dict[str, Any]] = [] - if map_path: - areals, _ = load_areal_map(map_path) - - yaw_start = float(args.yaw_start) - yaw_end = float(args.yaw_end) - if frames == 1: - yaws = [yaw_start] - else: - step = (yaw_end - yaw_start) / (frames - 1) - yaws = [yaw_start + i * step for i in range(frames)] - - prefix = str(args.prefix) - for i, yaw in enumerate(yaws): - rgb = _render_scene( - positions, - faces, - areals, - width=int(args.width), - height=int(args.height), - yaw_deg=yaw, - pitch_deg=float(args.pitch), - wireframe=bool(args.wireframe), - areal_overlay=bool(args.overlay_areals), - ) - out = output_dir / f"{prefix}_{i:03d}.ppm" - _write_ppm(out, int(args.width), int(args.height), rgb) - - print(f"Turntable source : {msh_path}") - if map_path: - print(f"Areal source : {map_path}") - print(f"Output dir : {output_dir}") - print(f"Frames : {frames} ({yaws[0]:.3f} -> {yaws[-1]:.3f} yaw)") - print( - "Terrain geometry : " - f"vertices={terrain_meta['vertex_count']}, faces={terrain_meta['face_count_rendered']}" - ) - return 0 - - -def cmd_render_batch(args: argparse.Namespace) -> int: - maps_root = Path(args.maps_root).resolve() - output_dir = Path(args.output_dir).resolve() - msh_paths = sorted(maps_root.rglob("Land.msh")) - if not msh_paths: - raise RuntimeError(f"no Land.msh files under {maps_root}") - - rendered = 0 - skipped = 0 - for msh_path in msh_paths: - map_path = msh_path.with_name("Land.map") - if not map_path.exists(): - skipped += 1 - continue - rel = msh_path.parent.relative_to(maps_root) - out = output_dir / f"{rel.as_posix().replace('/', '__')}.ppm" - cmd_render( - argparse.Namespace( - land_msh=str(msh_path), - land_map=str(map_path), - output=str(out), - max_faces=args.max_faces, - width=args.width, - height=args.height, - yaw=args.yaw, - pitch=args.pitch, - wireframe=args.wireframe, - overlay_areals=args.overlay_areals, - ) - ) - rendered += 1 - - print(f"Batch summary: rendered={rendered}, skipped_no_map={skipped}, output_dir={output_dir}") - return 0 - - -def build_parser() -> argparse.ArgumentParser: - parser = argparse.ArgumentParser( - description="Software 3D terrain renderer (Land.msh + optional Land.map overlay)." - ) - sub = parser.add_subparsers(dest="command", required=True) - - render = sub.add_parser("render", help="Render one terrain map to PPM.") - render.add_argument("--land-msh", required=True, help="Path to Land.msh") - render.add_argument("--land-map", help="Path to Land.map (optional)") - render.add_argument("--output", required=True, help="Output .ppm path") - render.add_argument("--max-faces", type=int, default=220000, help="Face limit (default: 220000)") - render.add_argument("--width", type=int, default=1280, help="Image width (default: 1280)") - render.add_argument("--height", type=int, default=720, help="Image height (default: 720)") - render.add_argument("--yaw", type=float, default=38.0, help="Yaw angle in degrees (default: 38)") - render.add_argument("--pitch", type=float, default=26.0, help="Pitch angle in degrees (default: 26)") - render.add_argument("--wireframe", action="store_true", help="Draw terrain wireframe overlay") - render.add_argument( - "--overlay-areals", - action="store_true", - help="Draw ArealMap polygon overlay", - ) - render.set_defaults(func=cmd_render) - - export_obj = sub.add_parser("export-obj", help="Export terrain (and optional areal edges) to OBJ.") - export_obj.add_argument("--land-msh", required=True, help="Path to Land.msh") - export_obj.add_argument("--land-map", help="Path to Land.map (optional)") - export_obj.add_argument("--output", required=True, help="Output .obj path") - export_obj.add_argument("--max-faces", type=int, default=0, help="Face limit (0 = all)") - export_obj.add_argument( - "--include-areals", - action="store_true", - help="Export areal polygons as OBJ polyline object", - ) - export_obj.set_defaults(func=cmd_export_obj) - - turn = sub.add_parser("render-turntable", help="Render turntable frame sequence to PPM.") - turn.add_argument("--land-msh", required=True, help="Path to Land.msh") - turn.add_argument("--land-map", help="Path to Land.map (optional)") - turn.add_argument("--output-dir", required=True, help="Output directory for frames") - turn.add_argument("--prefix", default="frame", help="Frame filename prefix (default: frame)") - turn.add_argument("--frames", type=int, default=36, help="Frame count (default: 36)") - turn.add_argument("--yaw-start", type=float, default=0.0, help="Start yaw in degrees (default: 0)") - turn.add_argument("--yaw-end", type=float, default=360.0, help="End yaw in degrees (default: 360)") - turn.add_argument("--pitch", type=float, default=26.0, help="Pitch angle in degrees (default: 26)") - turn.add_argument("--max-faces", type=int, default=160000, help="Face limit (default: 160000)") - turn.add_argument("--width", type=int, default=960, help="Image width (default: 960)") - turn.add_argument("--height", type=int, default=540, help="Image height (default: 540)") - turn.add_argument("--wireframe", action="store_true", help="Draw terrain wireframe overlay") - turn.add_argument( - "--overlay-areals", - action="store_true", - help="Draw ArealMap polygon overlay", - ) - turn.set_defaults(func=cmd_render_turntable) - - batch = sub.add_parser("render-batch", help="Render all MAPS/**/Land.msh under root.") - batch.add_argument( - "--maps-root", - default="tmp/gamedata/DATA/MAPS", - help="Root directory with MAPS subfolders (default: tmp/gamedata/DATA/MAPS)", - ) - batch.add_argument("--output-dir", required=True, help="Directory for output PPM files") - batch.add_argument("--max-faces", type=int, default=90000, help="Face limit per map (default: 90000)") - batch.add_argument("--width", type=int, default=960, help="Image width (default: 960)") - batch.add_argument("--height", type=int, default=540, help="Image height (default: 540)") - batch.add_argument("--yaw", type=float, default=38.0, help="Yaw angle in degrees (default: 38)") - batch.add_argument("--pitch", type=float, default=26.0, help="Pitch angle in degrees (default: 26)") - batch.add_argument("--wireframe", action="store_true", help="Draw terrain wireframe overlay") - batch.add_argument( - "--overlay-areals", - action="store_true", - help="Draw ArealMap polygon overlay", - ) - batch.set_defaults(func=cmd_render_batch) - - return parser - - -def main() -> int: - parser = build_parser() - args = parser.parse_args() - return int(args.func(args)) - - -if __name__ == "__main__": - raise SystemExit(main()) |
