b123d-drawing — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited b123d-drawing (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Use this skill when asked to create or fix an engineering drawing for a build123d component. Drawings export to a project drawings/ output directory.
There are two paths — start with the automatic one.
four-view SVG + DXF with dimensions, centrelines, and an ISO 7200 title block. Use this first for almost every part.
Drawing you can edit (add/remove dimensions, add section/auxiliary views) before export. Use this when the automatic drawing needs tweaks.
for cases the builder cannot express (e.g. a true cut section). Documented at the end as a fallback.
Requires build123d-drafting-helpers >= 0.7.0 (automatic hole callouts, patterns, location dims, and section views).
Before drawing anything, build and inspect the geometry:
mcp__build123d-mcp__execute — build the part in the session
mcp__build123d-mcp__measure — confirm volume, bbox, face count
mcp__build123d-mcp__render_view (save_to='/tmp/preview.png') — visual sanity checkIn the build step, register the part under a stable name with show(part, "part") — the manual pipeline's suggest_view_layout(object_name="part") and lint step reference it by that name.
Note the bounding-box extents and whether the part is rotationally symmetric. This tells you whether the automatic drawing will capture the key features, and drives any manual layout decisions later.
In the same execute() session where you built the part:
from build123d_drafting import make_drawing
svg, dxf = make_drawing(
part, # an in-session build123d object, OR a "path/to/part.step"
out="drawings/bracket", # output stem; ".svg"/".dxf" are appended
title="BRACKET", # ISO 7200 document title
number="DWG-042", # ISO 7200 document identifier
tolerance="ISO 2768-f", # general tolerance
drawn_by="Your Name",
)make_drawing chooses the scale + ISO page size, projects front/plan/side/iso views, and annotates automatically — then lints and writes both SVG and DXF. Pass an in-memory object directly (no STEP round-trip) or a STEP path.
As of helpers v0.7.0 the automatic annotation covers prismatic parts in full: every recognised hole gets a grouped callout ("4× ø10 THRU", counterbore/depth symbols), bolt circles get "EQ SP ON øD BC" callouts with a pitch-circle centreline, linear arrays get pitch dims, every hole gets a centre mark and baseline X/Y location dims from the min-X/Y datum corner, and blind/counterbored holes trigger an automatic SECTION A–A. Turned parts get OD/length dims, centrelines, and bore leaders. Do not clear and re-place these annotations — edit only what is wrong or missing.
Then verify (Step 3). For most parts you are done here.
What make_drawing does not do: functional datum selection, GD&T, tolerances by fit, thread callouts, spiral/freeform profile dimensions, cross-axis hole location dims, pattern callouts on turned flanges, and section hatching. If the lint reports feature_not_dimensioned / feature_count_mismatch, something was skipped for page-space reasons — add just that callout in Step 2.
The default location datum is the part's minimum-X/Y corner; if the functional datum differs, re-anchor those dims in Step 2 rather than rebuilding the sheet.
When the automatic drawing is close but needs edits, swap make_drawing for build_drawing. It runs the identical pipeline but returns a live Drawing before export, so your edits land in the output. For exact signatures and keyword names of every class and function used below (Dimension, Leader, TitleBlock, Drawing methods, ...), read the build123d://drafting-api resource — it is generated from the installed library, so it always matches:
from build123d_drafting import build_drawing, Leader
dwg = build_drawing(part, out="drawings/bracket", title="BRACKET",
number="DWG-042", tolerance="ISO 2768-f", drawn_by="Your Name")
# Available on dwg:
# dwg.views {"front","plan","side","iso"} → (visible, hidden) compounds
# dwg.annotations mutable list of annotation objects
# dwg.draft / dwg.scale / dwg.page_w / dwg.page_h
# dwg.look_at / dwg.dist (scaled-space building blocks for custom cameras)
# dwg.at(view, x, y, z) → page point (px, py, 0) mapped from world coordinates
# Add a dimension/leader the automatic pass missed:
dwg.add(Leader(tip=dwg.at("front", 10, 0, 5), elbow=(8, 40, 0),
label="ø4 BORE", draft=dwg.draft), "ldr_bore")
# Drop an automatic annotation by name. Auto names: dim_height, dim_od, dim_width,
# centerline_front, centerline_side, ldr_z0…, dim_step_0…, title_block.
dwg.remove("dim_od")
# Re-lint after edits, then export:
issues = dwg.lint() # list of LintIssue; [] when clean
svg, dxf = dwg.export("drawings/bracket")make_drawing(...) is exactly build_drawing(...).export().
Add a section or auxiliary view with add_view(). Give it a (pre-cut) shape, a camera in scaled space (compose it from dwg.look_at and dwg.dist, the same convention the standard views use), an up vector, and a page position; it returns that view's coordinate helper (ViewCoordinates):
look = dwg.look_at
bottom = (look[0], look[1], look[2] - dwg.dist) # camera below the part
vc = dwg.add_view("bottom", part, bottom, (0, 1, 0), (260.0, 60.0))
px, py = vc.pp(world_x, world_y, world_z) # annotate using the helperDrop to the manual pipeline (below) only when even the builder cannot express what you need — most commonly a true cut/section where you must boolean the part against a cutting box yourself, or a fully bespoke multi-part sheet.
mcp__build123d-mcp__render_drawing(svg_path='drawings/part_name.svg', save_to='/tmp/dwg.png')Review the rendered PNG before moving on: check the views are upright and complete, dimensions are legible and not colliding, and the title block reads correctly. view_annotation_overlap warnings from the lint step usually show up here as cramped leaders — fix them with the builder (Step 2) if they matter.
Then preserve the label metadata and check the exported file itself:
mcp__build123d-mcp__save_drawing_annotations(svg_path='drawings/part_name.svg')
mcp__build123d-mcp__inspect_drawing(svg_path='drawings/part_name.svg')build123d renders text as glyph paths, so label strings are irrecoverable from a finished SVG — save_drawing_annotations writes a .dims.json sidecar that inspect_drawing(svg_path=...) reads back, letting you (or a later session) inspect page size, layers, and annotation content without rebuilding anything.
The sidecar only captures annotations registered in this session via annotate(). If the SVG was produced by a standalone script (Step 4), the annotations live in that script's process and the tool will tell you nothing was written — re-exporting the sidecar is then the script's job.
Unless the user opts out, also write a clean, committable script to scripts/drawings/<part>.py that regenerates the drawing in a single run. The drawing should live in version control as reproducible code, not only as output artifacts. This must be a tidy reproducible script — not a dump of your exploratory execute() session.
If the project already has a conflicting name at that path (e.g. a scripts/drawings.py module), do not create a scripts/drawings/ directory alongside it — pick a non-colliding path such as scripts/<part>_drawing.py and follow the project's existing script layout.
Pick the case that matches how you obtained the geometry:
A — Drawing from a STEP file → use generate_script(). It writes an editable build_drawing script (including the customise-before-export seam) that reloads the STEP from disk:
from build123d_drafting import generate_script
generate_script(
"path/to/part.step",
out="scripts/drawings/bracket", # → writes scripts/drawings/bracket.py
title="BRACKET", number="DWG-042",
tolerance="ISO 2768-f", drawn_by="Your Name",
)The generated script exports its SVG/DXF next to itself (the out=_stem line uses the script's own path), e.g. scripts/drawings/bracket.svg. If you want the outputs under drawings/ instead, edit the final dwg.export(...) line in the generated script.
B — Drawing an in-session object → generate_script() cannot embed a live object (it reloads geometry from disk and raises TypeError on a Shape). Write the script by hand so it is self-contained:
exists (preferred), otherwise inline the minimal construction code.
make_drawing / build_drawing with the same parameters you used.#!/usr/bin/env python3
"""BRACKET — regenerates drawings/bracket.svg + .dxf in one run."""
from build123d_drafting import make_drawing
from myproject.bracket import build_bracket # the part's source of truth
part = build_bracket()
make_drawing(part, out="drawings/bracket", title="BRACKET",
number="DWG-042", tolerance="ISO 2768-f", drawn_by="Your Name")If the part was built ad-hoc in the session with no importable source, export it to STEP once and use case A instead — that keeps the script reproducible without pasting scratch geometry code.
Tell the user where the script was saved.
Use this only when make_drawing / build_drawing cannot express the drawing (true cut sections, bespoke layouts). It builds every view and annotation by hand. Step 0 above still applies; the steps below replace Steps 1–3.
Standard four-view layout. Page size is chosen in Manual 2 based on part extents (A4 landscape 297 × 210 mm for most parts; A3 landscape 420 × 297 mm for large ones).
| View | Camera position (scaled space) | Up vector | Role |
|---|---|---|---|
| Front (along -Y) | (cxs, cys − DIST, czs) | (0, 0, 1) | primary dims |
| Side (along +X) | (cxs + DIST, cys, czs) | (0, 0, 1) | depth/bore |
| Plan (along +Z) | (cxs, cys, czs + DIST) | (0, 1, 0) | footprint |
| Isometric | (cxs+ID, cys+ID, czs+ID) | (0, 0, 1) | pictorial, no dims |
where cxs = cx * SCALE, cys = cy * SCALE, czs = cz * SCALE, DIST = bbox_max * SCALE + 100 (orthographic cameras always outside the scaled bbox), and ID = DIST / (3 ** 0.5) (iso camera at the same distance along the equal-axis diagonal).
Critical: view direction = look_at − camera. For a pure orthographic projection the camera's off-axis coordinates must equal the scaled centroid — using (0, -DIST, 0) instead of (cxs, cys - DIST, czs) introduces a silent tilt whenever the centroid is off-axis.
The iso camera uses equal +ID offsets on all three axes for a standard equal-axis view. Negate one axis (e.g. (cxs-ID, cys+ID, czs+ID)) to flip the pictorial orientation when a key feature is otherwise hidden.
Axis mapping verification and sheet-position layout are both done in Manual 2 once SCALE is known — see the view_axes and suggest_view_layout calls there.
Use choose_scale() from build123d_drafting — it applies the same thresholds as the automated pipeline and returns TB_W (title-block width) too:
from build123d_drafting import choose_scale
# Extract geometry from part — drives all layout decisions below.
_bb = part.bounding_box()
x_size = _bb.max.X - _bb.min.X
y_size = _bb.max.Y - _bb.min.Y
z_size = _bb.max.Z - _bb.min.Z
cx = (_bb.min.X + _bb.max.X) / 2
cy = (_bb.min.Y + _bb.max.Y) / 2
cz = (_bb.min.Z + _bb.max.Z) / 2
bbox_max = max(x_size, y_size, z_size)
SCALE, PAGE_W, PAGE_H, TB_W = choose_scale(x_size, y_size, z_size)Compute sheet positions using the suggest_view_layout MCP tool — it accounts for the title block footprint and warns when views collide with it or with each other:
# TB_W comes from choose_scale() — do NOT hardcode 150.0, it is 120.0 on A4.
mcp__build123d-mcp__suggest_view_layout(
object_name="part", # name passed to show() in Step 0
page_w=PAGE_W, page_h=PAGE_H, scale=SCALE,
title_block_w=TB_W,
title_block_h=24, # ISO 7200 title block height with revision + legal_owner rows
)If the part is not in the session (e.g. the import failed or timed out), pass extents=[x_size, y_size, z_size] (+ optional centroid=[x, y, z]) instead of object_name — the layout only needs the bounding box, not live geometry.
Check result["warnings"] — if any view overlaps the title block or another view, the tool says so and may suggest a smaller scale or larger page. Address warnings before continuing. Then extract the positions:
# Extract positions from suggest_view_layout result.
# Re-run suggest_view_layout above if the part geometry changes before Manual 2.
FV_X, FV_Y = <result["views"]["front"]["VIEW_X"]>, <result["views"]["front"]["VIEW_Y"]>
SV_X, SV_Y = <result["views"]["side"]["VIEW_X"]>, <result["views"]["side"]["VIEW_Y"]>
PV_X, PV_Y = <result["views"]["plan"]["VIEW_X"]>, <result["views"]["plan"]["VIEW_Y"]>
ISO_X, ISO_Y = <result["views"]["iso"]["VIEW_X"]>, <result["views"]["iso"]["VIEW_Y"]>Then project:
part_scaled = part.scale(SCALE)
# Scaled centroid — used as look_at AND as the camera's off-axis coordinates.
# Off-axis coords must match look_at so view direction stays pure-orthographic.
cxs, cys, czs = cx * SCALE, cy * SCALE, cz * SCALE
look_at_s = (cxs, cys, czs)
DIST = bbox_max * SCALE + 100 # camera always outside the scaled bbox
ID = DIST / (3 ** 0.5) # iso offset — same distance along equal-axis diagonal
# Per-view camera positions: only the on-axis component differs.
# Remove entries for views you don't need.
VIEWS = {
"front": ((cxs, cys - DIST, czs), (0, 0, 1)), # looking along +Y
"side": ((cxs + DIST, cys, czs), (0, 0, 1)), # looking along -X
"plan": ((cxs, cys, czs + DIST), (0, 1, 0)), # looking down -Z
}
VIEW_POS = {
"front": (FV_X, FV_Y),
"side": (SV_X, SV_Y),
"plan": (PV_X, PV_Y),
}
# Project and place each orthographic view; collect results by name.
view_proj = {} # {name: (placed_vis, placed_hid_or_None)}
for view_name, (camera_pos, up) in VIEWS.items():
vis, hid = part_scaled.project_to_viewport(camera_pos, up, look_at_s)
if not list(vis):
raise ValueError(f"project_to_viewport returned empty geometry for {view_name} camera {camera_pos}")
vx, vy = VIEW_POS[view_name]
placed = Compound(children=list(vis)).locate(Location((vx, vy, 0)))
placed_hid = Compound(children=list(hid)).locate(Location((vx, vy, 0))) if hid else None
view_proj[view_name] = (placed, placed_hid)
# Iso uses the scaled part and a distance-safe offset on all three axes.
iso_vis, iso_hid = part_scaled.project_to_viewport(
(cxs + ID, cys + ID, czs + ID), (0, 0, 1), look_at_s
)
iso = Compound(children=list(iso_vis)).locate(Location((ISO_X, ISO_Y, 0)))
iso_h = Compound(children=list(iso_hid)).locate(Location((ISO_X, ISO_Y, 0))) if iso_hid else NoneNow verify the axis mapping for each view. cxs/cys/czs are available here; substitute the actual numeric values for look_at:
mcp__build123d-mcp__view_axes(viewport_origin=[cxs, cys-DIST, czs], viewport_up=[0,0,1], look_at=[cxs,cys,czs])
mcp__build123d-mcp__view_axes(viewport_origin=[cxs+DIST, cys, czs], viewport_up=[0,0,1], look_at=[cxs,cys,czs])
mcp__build123d-mcp__view_axes(viewport_origin=[cxs, cys, czs+DIST], viewport_up=[0,1,0], look_at=[cxs,cys,czs])Copy the results into the script as comments — they are the source of truth for the coordinate helpers in Manual 3.
Write one pair of helpers per view so annotation coords are derived from world geometry, not hardcoded page numbers. Use the view_axes results from Manual 2 to get the signs right — never assume:
# Front view (camera along −Y): world_X → page_X (+1), world_Z → page_Y (+1)
def FX(x): return FV_X + (x - cx) * SCALE
def FZ(z): return FV_Y + (z - cz) * SCALE
# Side view (camera along +X): world_Y → page_X (+1), world_Z → page_Y (+1)
def SX(y): return SV_X + (y - cy) * SCALE
def SZ(z): return SV_Y + (z - cz) * SCALEFor ISO views (two world axes share one page axis), use ViewCoordinates.pp() instead:
from build123d_drafting import ViewCoordinates, view_axes
# ViewCoordinates(axes, view_x, view_y, cx, cy, cz, scale)
iso_vc = ViewCoordinates(
view_axes((cxs + ID, cys + ID, czs + ID), (0, 0, 1), look_at_s),
ISO_X, ISO_Y, cx, cy, cz, SCALE,
)
page_x, page_y = iso_vc.pp(world_x, world_y, world_z)Verify a known extent (e.g. top of part) maps to a sensible page Y before placing any annotation.
from build123d_drafting import (
Centerline, Dimension, Leader, TitleBlock,
annotate, draft_preset, lint_drawing, place_dims, set_page,
)
draft = draft_preset(font_size=2.5, decimal_precision=1)Centrelines and bore leaders for rotationally symmetric parts (Z-axis cylinders):
# Z-axis diameters, largest first (or build the list by hand):
from build123d_drafting import analyse_cylinders, dedup_diams
z_diams = dedup_diams(analyse_cylinders(part)[0])
if z_diams:
# Rotation-axis centreline in front and side views
annotate(Centerline(
(FX(cx), FZ(_bb.min.Z) - 5, 0),
(FX(cx), FZ(_bb.max.Z) + 5, 0),
), "centerline_front")
annotate(Centerline(
(SX(cy), SZ(_bb.min.Z) - 5, 0),
(SX(cy), SZ(_bb.max.Z) + 5, 0),
), "centerline_side")
# Inner bore leaders — arrowhead on bore edge, elbow to the left
left_edge = FX(_bb.min.X)
elbow_x = left_edge - 10 # ~10 mm gap to left of part outline
for i, d in enumerate(z_diams[1:4]):
tip_z = FZ(cz) + (i - 1) * 10 # stagger vertically
annotate(Leader(
tip=(FX(cx - d / 2), tip_z, 0),
elbow=(elbow_x, tip_z, 0),
label=f"ø{d:.1f}" if d != int(d) else f"ø{int(d)}",
draft=draft,
), f"bore_z{i}")Stacked dimensions (use place_dims — it handles offset stacking automatically):
dims = place_dims([
((x0, y_base, 0), (x1, y_base, 0), "below", "14.8"),
((x0, y_base, 0), (x2, y_base, 0), "below", "7.1"),
], draft)
for i, d in enumerate(dims):
annotate(d, f"dim_name_{i}")Leaders (diameter callouts, part labels):
ldr = Leader(
tip=(page_x, page_y, 0),
elbow=(elbow_x, elbow_y, 0),
label="ø4.0 BEARING",
draft=draft,
)
annotate(ldr, "ldr_bearing_d")Title block (always include — use real values for the part being drawn). ISO 7200:2004 mandatory fields:
| ISO 7200 field | Parameter |
|---|---|
| Field 1 — Legal owner | legal_owner= |
| Field 2 — Document description | part_name (first positional) |
| Field 3 — Document identifier | drawing_number (second positional) |
| Field 4 — Revision indicator | revision= |
tb = TitleBlock(
"PART NAME", # ISO 7200 field 2 — document title
"DWG-NNN", # ISO 7200 field 3 — document identifier
drawing_scale=SCALE, # syncs title block cell text with lint_drawing(drawing_scale=SCALE)
material="CZ121 BRASS",
general_tolerance="ISO 2768-f",
designed_by="Your Name",
revision="A", # ISO 7200 field 4 — revision indicator
legal_owner="COMPANY NAME", # ISO 7200 field 1 — legal owner
width=TB_W,
draft=draft,
).locate(Location((PAGE_W - TB_W - 10, 10, 0))) # right-aligned: PAGE_W − block_width − margin
annotate(tb, "title_block")Every annotation object must be passed to annotate() — otherwise lint and export will not see it.
Use the MCP tool — it reads annotations and view shapes directly from session state:
# show() each placed view compound under a stable name so the MCP tool can find them.
show(view_proj["front"][0], "front_placed")
show(view_proj["side"][0], "side_placed")
show(view_proj["plan"][0], "plan_placed")
show(iso, "iso_placed")
mcp__build123d-mcp__lint_drawing(
drawing_scale=SCALE,
view_shape_names=["front_placed", "side_placed", "plan_placed", "iso_placed"],
)The view_shape_names list enables view_annotation_overlap and view_overlap checks. Omit it if you only need label/overlap checks and don't need view-boundary detection.
# Alternatively, call the helper directly in execute() for more control:
all_anns = list(dims) + [ldr1, ldr2, tb]
set_page(PAGE_W, PAGE_H, margin=10)
view_shapes = [placed for placed, _ in view_proj.values()] + [iso]
issues = lint_drawing(all_anns, drawing_scale=SCALE, view_shapes=view_shapes)
if issues:
for iss in issues: print(f" [{iss.severity}] {iss.code}: {iss.message}")
else:
print("Lint: OK")Do not export until lint is clean (or all issues are understood and accepted).
Common lint failures and fixes:
label_axis_swap — dimension endpoints are swapped (X↔Y); check coord helper signslabel_mismatch — label string doesn't match the geometric distance; recheck scalepage_bounds — annotation is outside the 297×210 margin; adjust view positionview_annotation_overlap — an annotation's bbox overlaps a view outline; move the dimview_overlap — two view outlines overlap each other; re-run suggest_view_layout at a smaller scale or larger pagepart_color = Color(0, 0, 0)
hid_color = Color(0.5, 0.5, 0.5)
dim_color = Color(0, 0.2, 0.7)
svg_exp = ExportSVG(margin=10)
svg_exp.add_layer("part", line_color=part_color, line_weight=0.5)
svg_exp.add_layer("hidden", line_color=hid_color, line_weight=0.25,
line_type=LineType.HIDDEN)
svg_exp.add_layer("dims", line_color=dim_color, fill_color=dim_color,
line_weight=0.05)
for placed, _ in view_proj.values():
svg_exp.add_shape(placed, layer="part")
svg_exp.add_shape(iso, layer="part")
for _, placed_hid in view_proj.values():
if placed_hid:
svg_exp.add_shape(placed_hid, layer="hidden")
if iso_h:
svg_exp.add_shape(iso_h, layer="hidden")
for ann in all_anns:
svg_exp.add_shape(ann, layer="dims")
svg_exp.write(str(output_dir / "part_name.svg"))
# Mandatory: fix the SVG viewBox so the full ISO page is preserved, not cropped
# to the content bounding box (build123d ExportSVG default).
from build123d_drafting import fix_svg_page_size
fix_svg_page_size(str(output_dir / "part_name.svg"), PAGE_W, PAGE_H)Then export DXF (same layer structure; omit line_color, fill_color, and line_type args):
from build123d import ExportDXF
dxf_exp = ExportDXF()
dxf_exp.add_layer("part", line_weight=0.5)
dxf_exp.add_layer("hidden", line_weight=0.25)
dxf_exp.add_layer("dims", line_weight=0.05)
for placed, _ in view_proj.values():
dxf_exp.add_shape(placed, layer="part")
dxf_exp.add_shape(iso, layer="part")
for _, placed_hid in view_proj.values():
if placed_hid:
dxf_exp.add_shape(placed_hid, layer="hidden")
if iso_h:
dxf_exp.add_shape(iso_h, layer="hidden")
for ann in all_anns:
dxf_exp.add_shape(ann, layer="dims")
dxf_exp.write(str(output_dir / "part_name.dxf"))mcp__build123d-mcp__render_drawing(svg_path='drawings/part_name.svg', save_to='/tmp/dwg.png')Review the rendered PNG before moving on.
To assemble multiple drawing SVGs into a single multi-page PDF, rasterise each SVG at 200 DPI using resvg-py and combine pages with fpdf2.
PAGE_W and PAGE_H are the values chosen in Manual 2 (e.g. 297/210 for A4 or 420/297 for A3 landscape). Use them throughout so the PDF matches the drawing sheet.
import resvg_py
from fpdf import FPDF
# PAGE_W, PAGE_H set in Manual 2
fmt = "A4" if PAGE_W < 400 else "A3"
png_bytes = resvg_py.svg_to_bytes(svg_path=str(svg_path), dpi=200)
pdf = FPDF(orientation="L", unit="mm", format=fmt)
pdf.add_page()
pdf.image(tmp_png_path, x=0, y=0, w=PAGE_W, h=PAGE_H)
pdf.output("drawings/output.pdf")build123d ExportSVG writes Y-up coordinates; the viewBox origin encodes where content sits on the sheet. To recover the correct Y position for PDF (Y-down, top-left origin):
vb_y = float(svg_root.get("viewBox").split()[1]) # negative in Y-up drawing coords
assert vb_y <= 0, f"Unexpected positive viewBox Y: {vb_y} — check ExportSVG output"
pdf_y = PAGE_H - abs(vb_y)If the assert fires, the SVG was generated with a different coordinate convention; inspect the viewBox before placing the image.
The same default as Step 4 applies: unless the user opts out, save a clean, committable scripts/drawings/<part>.py that regenerates the drawing in one run. For the manual pipeline there is no generate_script() shortcut, so assemble the script by hand — reconstruct the part (import its source module, or load the STEP) followed by the projection / annotation / export steps above. Keep it tidy and reproducible, not a paste of the exploratory session.
view_axes output drives all coordinate helpers. Common cases:
| Camera (VIEWS key) | Up | world_X | world_Y | world_Z |
|---|---|---|---|---|
| -Y (front) | +Z | page_X (+1) | — | page_Y (+1) |
| +X (side) | +Z | — | page_X (+1) | page_Y (+1) |
| +Z (plan) | +Y | page_X (+1) | page_Y (+1) | — |
| -X (alt) | +Z | — | page_X (-1) | page_Y (+1) |
Signs flip when the camera is on the negative axis — always verify with view_axes rather than assuming.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.