"""Build the 3D model from rack_params.py and export STL + GLB.""" import os import numpy as np import trimesh from shapely.geometry import Point, box as shp_box import rack_params as P OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "model") PANEL_DIR = os.path.join(OUT, "panels") os.makedirs(PANEL_DIR, exist_ok=True) def hex_to_rgba(h, a=255): h = h.lstrip("#") return [int(h[i:i + 2], 16) for i in (0, 2, 4)] + [a] def slab(u0, u1, v0, v1, thick, holes): """Extrude a rectangle (minus circular holes) `thick` deep along local +Z.""" foot = shp_box(u0, v0, u1, v1) for cu, cv, dia in holes: foot = foot.difference(Point(cu, cv).buffer(dia / 2.0, resolution=48)) return trimesh.creation.extrude_polygon(foot, height=thick) def panel_mesh(p): """Return a trimesh for one panel, holes subtracted, in world coordinates.""" x0, y0, z0, x1, y1, z1 = p["box"] if p["axis"] == "z": # flat panel: shape lives in XY, thickness along Z m = slab(x0, x1, y0, y1, z1 - z0, p["holes"]) m.apply_translation([0, 0, z0]) elif p["axis"] == "y": # vertical panel facing front/back: shape lives in XZ, thickness along Y m = slab(x0, x1, z0, z1, y1 - y0, p["holes"]) m.apply_transform(trimesh.transformations.rotation_matrix(np.pi / 2, [1, 0, 0])) m.apply_translation([0, y1, 0]) else: # axis == "x" — end panel, shape lives in YZ, thickness along X m = slab(y0, y1, z0, z1, x1 - x0, p["holes"]) m.apply_transform(trimesh.transformations.rotation_matrix(np.pi / 2, [1, 0, 0])) m.apply_transform(trimesh.transformations.rotation_matrix(np.pi / 2, [0, 0, 1])) m.apply_translation([x0, 0, 0]) m.visual.face_colors = hex_to_rgba(p["color"]) return m def main(): scene = trimesh.Scene() parts, report = [], [] for p in P.PANELS: m = panel_mesh(p) parts.append(m) scene.add_geometry(m, node_name=p["key"], geom_name=p["key"]) m.export(os.path.join(PANEL_DIR, f"{p['key']}.stl")) b = m.bounds report.append((p["key"], m.is_watertight, round(m.volume, 2), [round(v, 3) for v in (b[1] - b[0])])) assembly = trimesh.util.concatenate(parts) assembly.export(os.path.join(OUT, "underdesk-rack.stl")) scene.export(os.path.join(OUT, "underdesk-rack.glb")) print(f"material: {P.MATERIAL} T = {P.T}\"") print(f"{'panel':<12} {'tight':<6} {'vol in^3':>9} bbox (w x d x h)") for k, wt, v, ext in report: print(f" {k:<10} {str(wt):<6} {v:>9} {ext}") bb = assembly.bounds print("\nassembly: %.3f W x %.3f D x %.3f H" % (bb[1][0] - bb[0][0], bb[1][1] - bb[0][1], bb[1][2] - bb[0][2])) vol = sum(r[2] for r in report) print("plywood volume %.1f in^3 -> %.2f sq ft of sheet -> %.1f lb" % (vol, vol / P.T / 144.0, vol / P.T / 144.0 * P.PLY_LB_PER_SQFT)) nh = sum(len(p["holes"]) for p in P.PANELS) print(f"holes drilled: {nh} " f"({len(P.GROMMET_X)} shelf grommets, {len(P.VENT_X) * len(P.VENT_Y)} vents, " f"{len(P.REAR_HOLE_X)} rear pass-throughs)") if __name__ == "__main__": main()