Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
"""Generate docs/PRINT-QUEUE.md from cad/stl/ — per-part solid volume, filament
estimate, and a sensible print order. Pure stdlib.
Volume = divergence theorem over the STL mesh (exact for closed meshes).
Filament grams = solid volume x 1.24 g/cm3 (PETG) x a 0.55 effective-density
factor (3-4 walls + 25-40% infill on chunky brackets). It's an ESTIMATE for
planning spools — the slicer's number wins.
Run: python3 scripts/print_queue.py
"""
import csv
import os
import struct
ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")
STL = os.path.join(ROOT, "cad", "stl")
OUT = os.path.join(ROOT, "docs", "PRINT-QUEUE.md")
DENSITY = 1.24 # g/cm3 PETG (ASA ~1.07 — scale accordingly)
EFFECTIVE = 0.55 # walls+infill vs solid
GRAMS_PER_HOUR = 18.0 # ~0.4mm nozzle, 0.2mm layers, moderate speeds — planning number
GROUPS = [
("Weekend 1 — brain box + safety (bench-build first)",
["equipment_plate", "upper_shelf", "box_foot", "relay_box", "relay_lid",
"estop_pedestal_a", "estop_pedestal_b", "estop_face", "badge"]),
("Weekend 2 — steering (the actuator kit)",
["lapbar_yoke_bottom", "lapbar_yoke_top", "rail_anchor_bottom", "rail_anchor_top",
"throttle_servo_bracket"]),
("Weekend 3 — sensors (masts + mounts)",
["gps_clamp_a", "gps_clamp_b", "gps_top_plate", "lidar_base_a", "lidar_base_b",
"lidar_mast_lower", "lidar_mast_upper", "lidar_top_plate", "camera_base", "camera_cradle"]),
("Phase 3 — attachment hardware (print when the attachments land)",
["boom_base_plate", "blower_cradle_top", "blower_cradle_bottom", "trimmer_motor_plate",
"duct_adapter", "dump_clevis", "tpms_cradle", "sprayer_ctrl_mount"]),
("Weekend 4 — overhead sonar + on-unit screen",
["sonar_collar_a", "sonar_collar_b", "display_hood_l", "display_hood_r",
"display_brace", "display_yoke"]),
("Upgrade — dual-antenna RTK heading (moving baseline)",
["baseline_tee", "baseline_ant_plate"]),
]
QTY = {"dump_clevis": 2, "display_brace": 2, "baseline_ant_plate": 2}
def stl_volume_mm3(path):
with open(path, "rb") as f:
head = f.read(80)
if head[:5] == b"solid" and b"facet" in head + f.read(200):
f.seek(0)
return _ascii_volume(f)
f.seek(80)
n = struct.unpack("<I", f.read(4))[0]
vol = 0.0
for _ in range(n):
f.read(12)
v = [struct.unpack("<3f", f.read(12)) for _ in range(3)]
f.read(2)
vol += _signed_tetra(v)
return abs(vol)
def _ascii_volume(f):
vol, tri = 0.0, []
for line in f.read().decode(errors="ignore").splitlines():
s = line.split()
if len(s) >= 4 and s[0] == "vertex":
tri.append(tuple(float(x) for x in s[1:4]))
if len(tri) == 3:
vol += _signed_tetra(tri)
tri = []
return abs(vol)
def _signed_tetra(v):
(ax, ay, az), (bx, by, bz), (cx, cy, cz) = v
return (ax * (by * cz - bz * cy) - ay * (bx * cz - bz * cx)
+ az * (bx * cy - by * cx)) / 6.0
def main():
rows = {}
with open(os.path.join(STL, "MANIFEST.csv")) as f:
for r in csv.DictReader(f):
rows[r["part"].replace("PRINT_", "")] = r
lines = [f"# Print queue — {sum(len(p) for _, p in GROUPS)} parts in build order",
"",
"Generated by `scripts/print_queue.py` from the real STLs. Grams are a",
f"planning estimate (solid volume × {DENSITY} g/cm³ PETG × {EFFECTIVE} walls+infill",
"factor) — **the slicer's number wins**. Print the brim-baked files in",
"`cad/stl/brim/` and do NOT add a slicer brim on top.",
""]
total_g = total_h = 0.0
for title, parts in GROUPS:
lines += [f"## {title}", "",
"| Part | Qty | Bounding box (mm) | ~grams | ~hours |",
"|---|---|---|---|---|"]
for name in parts:
r = rows.get(name)
if not r:
lines.append(f"| {name} | — | MISSING FROM MANIFEST | — | — |")
continue
qty = QTY.get(name, 1)
vol = stl_volume_mm3(os.path.join(ROOT, r["stl"] if os.path.isabs(r["stl"])
else os.path.join("cad", r["stl"])))
g = vol / 1000.0 * DENSITY * EFFECTIVE * qty
h = g / GRAMS_PER_HOUR
total_g += g; total_h += h
bbox = f'{r["bbox_x"]} × {r["bbox_y"]} × {r["bbox_z"]}'
lines.append(f"| {name} | {qty} | {bbox} | {g:.0f} g | {h:.1f} h |")
lines.append("")
lines += [f"**Totals: ≈ {total_g:.0f} g (~{total_g/1000:.1f} kg spool budget) · ≈ {total_h:.0f} printer-hours** "
f"(mostly unattended; batch small parts per plate).", ""]
with open(OUT, "w") as f:
f.write("\n".join(lines))
print(f"wrote {OUT} (≈{total_g:.0f} g, ≈{total_h:.0f} h)")
if __name__ == "__main__":
main()
b1b1ea139This file is pinned to the selected release. Download the original to open it in your design tool.