"""Stand a mesh upright: our scenes have Y pointing down (like the Steam Frame's tracking). 3D programs expect Z up for PLY and STL (Blender, slicers for 3D printing) and Y up for glTF/GLB (by its standard) and OBJ (by custom) – Blender turns those two into Z up itself on import. Without this a room lies on its side or upside down. (Checked by importing each format in Blender 5.2.) Usage: python3 upright.py [--floor Y] [ ...] (.ply .obj .stl .glb .gltf; changed in place) PLY, STL: positions and normals turned so that up (-Y) becomes +Z: (x, y, z) -> (x, z, -y) OBJ: turned 180° about X, up (-Y) becomes +Y: (x, y, z) -> (x, -y, -z) GLB/glTF: the scene's root nodes go under one new node turned 180° about X: (x, y, z) -> (x, -y, -z) – nothing else in the file is touched (textures, materials, accessors stay as they are) --floor Y where the floor is in the scene (Y down): it is moved to height 0, so the room stands on the ground plane (the scene's origin is where the Frame was at the start, at eye height) --floor-from S the same, found in the scene's splats S (spirula/scene.ply) as for the floor plan """ import json import struct import sys from pathlib import Path import numpy as np Z_UP = np.array([[1, 0, 0], [0, 0, 1], [0, -1, 0]], dtype=np.float64) # proper rotation, det +1 Y_UP_QUAT = [1.0, 0.0, 0.0, 0.0] # x, y, z, w: 180° about X LIFT = 0.0 # the floor's height before (Y down) def _replace(path: Path, data: bytes): tmp = path.with_name(path.name + ".tmp") tmp.write_bytes(data) tmp.replace(path) def ply(path: Path): raw = path.read_bytes() end = raw.index(b"end_header\n") + len(b"end_header\n") header = raw[:end].decode("ascii", "replace").splitlines() if "format binary_little_endian 1.0" not in header: sys.exit(f"{path}: only binary little-endian PLY is supported") sizes = {"float": "