Add `woodshop render <file.png>` for headless viewing

Saves an off-screen PNG of the scene (labels, grid, isometric) so the model can
be inspected without an interactive GUI window — useful over SSH or when
woodshop-view can't open a display. 44 tests passing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
rob 2026-05-29 02:15:17 -03:00
parent 7b5c58902c
commit 17e7554ff1
2 changed files with 29 additions and 1 deletions

View File

@ -146,6 +146,14 @@ def cmd_cutlist(scene: Scene, args) -> str:
return format_cutlist(scene)
def cmd_render(scene: Scene, args) -> str:
from .viewer import render_to_file # lazy: pulls in pyvista
if not scene.parts:
return "Nothing to render — the scene is empty."
path = render_to_file(scene, args.path)
return f"Rendered {len(scene.parts)} part(s) to {path}."
def _describe_part(p) -> str:
bits = [f"{_fmt_len(p.length_in)} {p.stock}"]
if p.name:
@ -257,6 +265,10 @@ def build_parser() -> argparse.ArgumentParser:
sp.add_argument("path", help="Output file, e.g. table.stl or table.step")
sp.set_defaults(func=cmd_export)
sp = sub.add_parser("render", help="Save a PNG image of the scene (works headless)")
sp.add_argument("path", help="Output image, e.g. table.png")
sp.set_defaults(func=cmd_render)
sub.add_parser("cutlist", help="Show the cut list / bill of materials").set_defaults(func=cmd_cutlist)
sub.add_parser("undo", help="Undo the last operation").set_defaults(func=cmd_undo)
sub.add_parser("clear", help="Clear the scene").set_defaults(func=cmd_clear)
@ -272,7 +284,7 @@ def main(argv: list[str] | None = None) -> int:
except (SceneError, ValueError, KeyError) as exc:
print(str(exc).strip('"'), file=sys.stderr)
return 1
if args.command not in ("status", "export", "cutlist", "save", "projects"):
if args.command not in ("status", "export", "cutlist", "render", "save", "projects"):
scene.save(args.scene)
print(message)
return 0

View File

@ -77,6 +77,22 @@ def _render(plotter, scene: Scene) -> None:
plotter.add_axes()
def render_to_file(scene: Scene, path, window_size=(1100, 800)) -> str:
"""Render the scene to a PNG (off-screen) — works headless / over SSH."""
import pyvista as pv
_quiet_vtk()
pv.OFF_SCREEN = True
plotter = pv.Plotter(off_screen=True, window_size=window_size)
plotter.set_background("#2b2b2b")
plotter.enable_parallel_projection()
_render(plotter, scene)
plotter.view_isometric()
plotter.screenshot(str(path))
plotter.close()
return str(path)
def run(scene_path: Path | None = None, poll_s: float = 0.3) -> None:
import pyvista as pv