"""Crop an OBJ to the faces fully inside a sphere, so clay renders can frame the hand instead of the whole arm. Keeps vertex order stable across files (same face set in/out) only when the inputs share topology, so pass --like to reuse a reference file's face mask. usage: obj_crop.py in.obj out.obj CX CY CZ R (metres) obj_crop.py in.obj out.obj --mask mask.txt obj_crop.py in.obj --write-mask mask.txt CX CY CZ R """ import sys, math from pathlib import Path def load(path): vs, faces = [], [] for line in Path(path).read_text().splitlines(): if line.startswith("v "): p = line.split() vs.append((float(p[1]), float(p[2]), float(p[3]))) elif line.startswith("f "): faces.append([int(t.split("/")[0]) - 1 for t in line.split()[1:]]) return vs, faces args = sys.argv[1:] src = args[0] vs, faces = load(src) if "--write-mask" in args: maskfile = args[args.index("--write-mask") + 1] cx, cy, cz, r = (float(x) for x in args[-4:]) keep = [i for i, f in enumerate(faces) if all(math.dist(vs[k], (cx, cy, cz)) <= r for k in f)] Path(maskfile).write_text("\n".join(map(str, keep))) print(f"mask {len(keep)}/{len(faces)} faces -> {maskfile}") sys.exit() dst = args[1] if "--mask" in args: keep = [int(x) for x in Path(args[args.index("--mask") + 1]).read_text().split()] else: cx, cy, cz, r = (float(x) for x in args[-4:]) keep = [i for i, f in enumerate(faces) if all(math.dist(vs[k], (cx, cy, cz)) <= r for k in f)] used = sorted({k for i in keep for k in faces[i]}) remap = {old: n + 1 for n, old in enumerate(used)} out = [f"v {vs[o][0]:.6f} {vs[o][1]:.6f} {vs[o][2]:.6f}" for o in used] out += ["f " + " ".join(str(remap[k]) for k in faces[i]) for i in keep] Path(dst).write_text("\n".join(out) + "\n") print(f"{Path(dst).name}: {len(used)} verts, {len(keep)} faces")