Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
e88a24ce6This 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()