Batch screenshots¶
tcv_screenshots renders CAD models to PNG files without a viewer window: it runs your example files, collects the screenshots they request and draws each one in a headless Chromium with the CAD Viewer. It is meant for documentation images and CI jobs - every screenshot is reproducible from code, and nothing has to be open or clicked.
It draws through the same pipeline as the viewers, so screenshot takes the same objects and keywords as show and gives the same picture: colors, alphas, materials and studio mode, joints, clipping, camera settings. Unlike the headless screenshots of OCP Viewer, there is no server to start and no browser to manage: one command does everything.
Installation¶
pip install tcv-screenshots
playwright install chromium # only needed without an installed Chrome or Edge
tcv_screenshots renders in an installed Google Chrome or Microsoft Edge when there is one; otherwise playwright install chromium downloads Playwright's own Chromium, see Browser. Install the CAD library your examples use (build123d, CadQuery) into the same environment.
Version
This page describes tcv_screenshots 1.0.0. 0.3.0 used a config dict of renderer names instead of the show keywords; see the migration notes in the README.
Writing examples¶
An example is a Python file with a main() function that requests screenshots with screenshot and returns get_screenshots():
from build123d import *
part = fillet(Box(30, 20, 10).edges().filter_by(Axis.Z), 3) - Cylinder(4, 10)
def main():
from tcv_screenshots import screenshot, get_screenshots
screenshot(part, name="part", cad_width=800, height=600)
return get_screenshots()
This writes part.png. An example can request several screenshots, e.g. the steps of an incremental construction, each under its own name:
from build123d import *
from tcv_screenshots import Render, screenshot, get_screenshots
size = {"cad_width": 500, "height": 375}
box = Box(10, 10, 10)
screenshot(box, name="step1_box", **size)
filleted = fillet(box.edges(), 1)
screenshot(filleted, name="step2_fillet", **size)
final = filleted - Cylinder(2, 10)
screenshot(final, name="step3_hole", modes=[Render.FACES], **size)
def main():
return get_screenshots()
screenshot(*cad_objs, name, **kwargs):
cad_objs- the objects to draw, like the positional arguments ofshow. Several objects are separate arguments; a list is drawn as one objectname- the PNG file name, without.pngkwargs- any keyword ofshow, e.g.colors,alphas,modes,materials,render_joints,reset_camera,position/quaternion/target/zoom,tab="studio"and thestudio_*options. The enums (Camera,Render,Collapse, ...) can be imported fromtcv_screenshots
The viewer-specific keywords port, viewer, anchor and pinning address a viewer window, which a screenshot does not have; they are ignored with a warning. cad_width and height set the image size.
Defaults of a screenshot¶
| Keyword | Default |
|---|---|
cad_width |
1200 (minimum 450) |
height |
800 |
theme |
"light" |
glass |
True |
tools |
False |
ambient_intensity |
1.0 |
direct_intensity |
1.1 |
metalness |
0.3 |
roughness |
0.65 |
default_edgecolor |
"#707070" |
ortho |
True |
up |
"Z" |
reset_camera |
Camera.RESET |
Every screenshot starts with a fresh viewer: no setting of one screenshot (clipping, transparency, camera, tab) carries over to the next. The PNG's background is transparent.
Running¶
# a single example, PNGs into the current directory
tcv_screenshots -f example.py
# a directory of examples
tcv_screenshots -d examples -o screenshots
Installing the package adds the tcv_screenshots command to the environment's bin folder (macOS, Linux) or Scripts folder (Windows, tcv_screenshots.exe). python -m tcv_screenshots does the same, e.g. when that folder is not on the PATH.
| Option | Meaning |
|---|---|
-f, --file FILE |
A single example file |
-d, --directory DIR |
All *.py files of a directory, sorted by name (files starting with _ or . are left out); requires -o |
-o, --output-folder DIR |
Where the PNG files go |
--no-gpu |
Render with the software renderer SwiftShader, see GPU or SwiftShader |
--browser BROWSER |
Use only chrome, msedge or chromium (Playwright's own), see Browser |
--check |
Show which browsers work, with version and renderer, and which one a run would use; renders nothing |
--no-headless |
Show the browser window (for debugging) |
--pause |
Wait for Enter before each screenshot and print the camera values |
--debug MODELS_DIR |
Save each model's data as JSON, show the browser console and full error messages |
Files that do not import tcv_screenshots are skipped. A failing example is reported with its traceback, starting at the line of the example that failed, and the other examples and models go on. The command exits with status 1 if any example or model failed, so a CI job fails as it should.
Finding a camera position
Run tcv_screenshots -f example.py --no-headless --pause, turn the model into position with the mouse, then press Enter. The printed position, quaternion, target and zoom can be passed to screenshot as they are.
Browser¶
Every run uses the first browser that works: Microsoft Edge, Google Chrome, Playwright's Chromium on Windows (where Edge is always installed), and Google Chrome, Microsoft Edge, Playwright's Chromium on macOS and Linux. A browser that is not installed is skipped within milliseconds, so nothing is stored between runs. On Linux, Playwright looks for Chrome and Edge only at /opt/google/chrome/chrome and /opt/microsoft/msedge/msedge, where e.g. Google's .deb package installs Chrome; a Flatpak install (flatpak run com.google.Chrome) is not found, and the run goes on with the next browser. The browser in use is printed (Browser: chrome 155.0...).
tcv_screenshots --check
lists every candidate with its version and renderer and names the one a run would use; if none works, it says to run playwright install chromium. --browser chrome|msedge|chromium uses exactly one browser. An installed Chrome or Edge updates itself, so screenshots can change slightly with its versions; for reproducible screenshots, e.g. in CI, use --browser chromium, whose version only changes with Playwright's. Firefox and Safari cannot be used.
GPU or SwiftShader¶
By default the browser renders with the GPU: Metal on macOS, Vulkan on Linux, Direct3D 11 on Windows (preferring a discrete GPU over the integrated one). The renderer in use is printed (GPU rendering: ...). --no-gpu renders with SwiftShader, a software renderer, which works without a GPU but is much slower for large models:
Rendering a heat exchanger (149 filleted tubes) and an assembly of 100 screws:
| Machine | SwiftShader | GPU |
|---|---|---|
| Apple M1 Max, macOS | 112.8 s | 2.5 s (Metal) |
| Ryzen 9 7900 + RTX 5060, Linux | 73.6 s | 2.5 s (Vulkan) |
| Ryzen 9 7900 + RTX 5060, Windows 11 | 65.9 s | 2.4 s (Direct3D 11) |
GPU and SwiftShader images differ in anti-aliasing only, and different machines differ slightly too. A comparison against stored reference images should therefore always use the same renderer.
Without a usable GPU, Chromium falls back to SwiftShader and a warning says so. On CI runners, which have no GPU, pass --no-gpu to choose SwiftShader explicitly, and --browser chromium for a pinned browser version:
- name: Generate screenshots
run: |
playwright install chromium
tcv_screenshots -d examples -o screenshots --browser chromium --no-gpu
The repository's workflow runs this on Linux, macOS and Windows.
Scripted use¶
The two steps of the command line are functions, for build scripts that need more control, e.g. the build of a documentation site. A build script and its examples could be laid out like this:
build_screenshots.py
examples/
box.py
cylinder.py
more/
extra_example.py
Each example is a file with a main() as in Writing examples. examples/box.py:
from build123d import Box
from tcv_screenshots import get_screenshots, screenshot
def main():
screenshot(Box(10, 20, 30), name="box")
return get_screenshots()
examples/cylinder.py:
from build123d import Cylinder
from tcv_screenshots import get_screenshots, screenshot
def main():
screenshot(Cylinder(10, 30), name="cylinder")
return get_screenshots()
more/extra_example.py, with two screenshots of the same part:
from build123d import Box, fillet
from tcv_screenshots import Render, get_screenshots, screenshot
rounded = fillet(Box(20, 20, 20).edges(), 3)
def main():
screenshot(rounded, name="rounded_box")
screenshot(rounded, name="rounded_box_edges", modes=[Render.EDGES])
return get_screenshots()
build_screenshots.py runs them, adds a screenshot of its own and renders everything:
from build123d import Box
from tcv_screenshots import (
collect_models,
get_failed_models,
get_screenshots,
render_models,
screenshot,
set_defaults,
)
# Defaults for every following screenshot call, the keywords of `show`
set_defaults(cad_width=1000, height=1000)
# Run example files and directories of examples
collected = collect_models("examples", "more/extra_example.py")
# Or request screenshots directly, without example files
screenshot(Box(1, 2, 3), name="direct_box")
failed_screenshots = [name for name, _traceback in get_failed_models()]
rendered = render_models(collected.models + get_screenshots(), "screenshots")
print(rendered.written) # the PNG files
print(collected.failed + failed_screenshots + rendered.failed) # names of what failed
python build_screenshots.py writes box.png, cylinder.png, rounded_box.png, rounded_box_edges.png and direct_box.png into screenshots/, all 1000 x 1000 pixels, and prints an empty list of failures.
collect_models(*paths, models_dir=None)returnsmodelsandfailed. Each example runs with the defaults set before the call; aset_defaultsinside one example does not reach the next. Screenshots requested before the call stay available toget_screenshots()render_models(models, output_dir, *, headless=True, pause=False, debug=False, gpu=True, browser=None)writes<output_dir>/<name>.pngand returnswrittenandfailed. Where an event loop is already running (e.g. Jupyter), useawait render_models_async(...)with the same argumentsscreenshotdoes not raise: a screenshot it cannot create is printed asFAILUREwith its traceback, andget_failed_models()returns it. Neither function exits; use the returned names for the exit status of your scriptset_defaults,get_defaultsandreset_defaultswork as in the viewers, see The config system
Repeated parts
A part that appears in several screenshots of one run, also moved or rotated, is tessellated once and taken from the cache afterwards (ocp-tessellate 3.6.3 or newer, which tcv_screenshots 1.0.0 requires).