td2-visualizer/tools/glb_bounds.py
izawartka 82c3c7cfbb Updated glb_bounds.py
*Changed optional params of glb_bounds.py to use dashes instead of underscores in optional params
*Added missing glb_bounds.py header
2025-08-02 00:46:32 +02:00

169 lines
4.9 KiB
Python

import argparse
import os
import json
import logging
from tqdm import tqdm
import trimesh
#
# [ GLB BOUNDS EXTRACTOR ]
# by masuo
# v1.1
#
def parse_args():
parser = argparse.ArgumentParser(
description="Extract bounding boxes from GLB mesh files and output JSON summary."
)
parser.add_argument(
'input',
help='Input file or directory containing .glb files'
)
parser.add_argument(
'output',
help='Output JSON file path'
)
parser.add_argument(
'--decimal-places', '-d',
type=int,
default=3,
help='Number of decimal places for output coordinates (default: 3)'
)
parser.add_argument(
'--no-aliases', '-a',
action='store_false',
help='Disable aliases in the output JSON (default: false)'
)
return parser.parse_args()
def round_floats(obj, prec=3):
if isinstance(obj, float):
return round(obj, prec)
elif isinstance(obj, dict):
return {k: round_floats(v, prec) for k, v in obj.items()}
elif isinstance(obj, list):
return [round_floats(v, prec) for v in obj]
else:
return obj
def collect_glb_files(path):
"""
Given a path, return a list of .glb files.
If path is a directory, walk it; if file, return single-element list.
"""
glb_files = []
if os.path.isdir(path):
for root, _, files in os.walk(path):
for name in files:
if name.lower().endswith('.glb'):
glb_files.append(os.path.join(root, name))
elif os.path.isfile(path) and path.lower().endswith('.glb'):
glb_files = [path]
else:
logging.error(f"Input path '{path}' is neither a .glb file nor a directory containing them.")
return glb_files
def process_file(filepath):
"""
Load a GLB file, compute its center and size, return a dict entry.
Returns None if fails.
"""
try:
mesh = trimesh.load(filepath, force='mesh')
if mesh.is_empty:
logging.warning(f"Mesh in '{filepath}' is empty, skipping.")
return None
except Exception as e:
logging.warning(f"Failed to load '{filepath}' as a mesh: {e}")
return None
# Compute axis-aligned bounding box
bounds = mesh.bounds # shape (2,3): [min, max]
min_pt, max_pt = bounds
center = (min_pt + max_pt) / 2.0
size = max_pt - min_pt
# y is the top axis by convention
entry = {
'center': {
'x': float(center[0]),
'y': float(center[1]),
'z': float(center[2]),
},
'size': {
'x': float(size[0]),
'y': float(size[1]),
'z': float(size[2]),
}
}
return entry
def items_equal(item1, item2):
if isinstance(item1, dict) and isinstance(item2, dict):
if item1.keys() != item2.keys():
return False
return all(items_equal(item1[k], item2[k]) for k in item1)
elif isinstance(item1, list) and isinstance(item2, list):
if len(item1) != len(item2):
return False
return all(items_equal(i1, i2) for i1, i2 in zip(item1, item2))
elif isinstance(item1, (int, float)) and isinstance(item2, (int, float)):
return round(item1, 6) == round(item2, 6)
else:
return item1 == item2
def find_apply_aliases(results):
logging.info("Applying aliases...")
final_results = {}
for name, entry in results.items():
if not entry:
continue
found_alias = False
for existing_name, existing_entry in final_results.items():
if 'alias' in existing_entry:
continue
if not items_equal(entry, existing_entry):
continue
final_results[name] = {
'alias': existing_name
}
found_alias = True
break
if not found_alias:
final_results[name] = entry
return final_results
def main():
logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
args = parse_args()
glb_files = collect_glb_files(args.input)
if not glb_files:
logging.error("No .glb files found to process.")
return
results = {}
for filepath in tqdm(glb_files, desc="Processing GLB files"):
entry = process_file(filepath)
if entry:
prefab_name = os.path.splitext(os.path.basename(filepath))[0]
results[prefab_name] = entry
# Write output JSON
try:
with open(args.output, 'w') as f:
results_final = find_apply_aliases(results) if args.no_aliases else results
json.dump(round_floats(results_final, args.decimal_places), f, indent=4)
logging.info(f"Successfully wrote {len(results)} entries to '{args.output}'")
except Exception as e:
logging.error(f"Failed to write output JSON: {e}")
if __name__ == '__main__':
main()