Skip to content
Back to skills

Automated Engineering Drawing with draftwright

ASecurity

Use this skill when asked to create an engineering drawing automatically from a build123d solid or STEP file. It requires the **draftwright** package (AGPL-3.0) to be installed in the execution environment. > **License notice**: draftwright is AGPL-3.0. If you deploy code that uses it > as part of a network service you must make your application source available. > For Apache 2.0 annotation primitives only (`Dimension`, `Leader`, `TitleBlock`, > etc.), use `build123d-drafting-helpers` instead...

  • 104 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added November 11, 2026
developmentpythonrustexpressawsapi

Works with

  • cli
  • api
  • mcp

Security analysis

A92/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro shows the line behind each finding and how to fix it

Scanned November 11, 2026

npx -y skills add pzfreo/draftwright-python --skill skills --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Automated Engineering Drawing with draftwright?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Automated Engineering Drawing with draftwright
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/pzfreo-automated-engineering-drawing-with-draftwright-draftwright-python/badge)](https://www.skillsdirectory.com/skills/pzfreo-automated-engineering-drawing-with-draftwright-draftwright-python)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
# Automated Engineering Drawing with draftwright

Use this skill when asked to create an engineering drawing automatically from a
build123d solid or STEP file. It requires the **draftwright** package (AGPL-3.0)
to be installed in the execution environment.

> **License notice**: draftwright is AGPL-3.0. If you deploy code that uses it
> as part of a network service you must make your application source available.
> For Apache 2.0 annotation primitives only (`Dimension`, `Leader`, `TitleBlock`,
> etc.), use `build123d-drafting-helpers` instead without this obligation.

**There are two paths — start with the automatic one.**

1. **Automatic (`make_drawing`)** — one call turns a part (or STEP file) into a
   multi-view SVG + DXF with dimensions, centrelines, and an ISO 7200 title
   block.
2. **Builder (`build_drawing`)** — the same pipeline, but it hands back a live
   `Drawing` you can edit before export.

Requires `draftwright >= 0.4.0`; the current package requires
`build123d-drafting-helpers >= 0.15.5`.
Install: `pip install draftwright`.

(The old floor said 0.1.9, which cannot run this guide: none of the feature-backed
verbs existed then, and the dotted parameter ids in Step 2 are new in 0.4.0.)

**Design model (worth knowing before you edit):** the engine is *deterministic*
— no AI inside it — and you refine a drawing by **stating domain intent**
(dimension this feature, section through here) and letting the engine **place
everything automatically** (placement is constraint-based; you never compute page
coordinates). When the first pass isn't perfect, you drive a **build → critique
→ fix** loop (Step "Lint → critique → fix" below), not a hand-layout edit. The
rationale lives in the five current records in `docs/adr/`: ADR 1 (compiler pipeline),
ADR 2 (layout and view planning), ADR 3 (recognition), ADR 4 (declared intent), and
ADR 5 (trust and honest failure). Edit through the domain API; treat `Placeable`/page mechanics as
internals.

---

## Step 0 — Understand the part first

```
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 check
```

Register the part under a stable name with `show(part, "part")`.

---

## Step 1 — Generate the drawing automatically (start here)

```python
from draftwright 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, and views required by the part,
then annotates, lints, and writes both SVG and DXF.

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 with ISO 128-44 solid filled arrows and ISO 128-50 45° hatching. Turned
parts get OD/length dims, centrelines, bore leaders, and — for parts turned
about a horizontal (X) axis — their external stepped diameters as ø
leader-callouts.

Then verify (Step 3). For most parts you are done here.

---

## Step 2 — Customise with the Drawing builder

`build_drawing(...)` returns a live `Drawing`. **Edit it in *domain* vocabulary —
name a detected *feature* and the measurement you want; the engine decides the
offset, stacking, and strip slot.** You give *what*, never *where on the page*:
placement is automatic and constraint-based.

Use the feature-backed verbs (`dimension` / `locate` / `callout` / `note` /
`drop`) for edits. `Drawing.place_dim` remains a deprecated raw-coordinate
escape hatch; `Drawing.add`, `Drawing.add_view`, and the public view-coordinate
plumbing were removed in 0.5.0. See `docs/deprecations.md`.

```python
from draftwright import build_drawing

dwg = build_drawing(part, out="drawings/bracket", title="BRACKET",
                    number="DWG-042", tolerance="ISO 2768-f", drawn_by="Your Name")
```

**Inspect what the engine found and placed (read APIs):**

```python
dwg.features("plan")        # detected features in a view → [FeatureInfo(...)]
                            #   each: .type .diameter .through .depth .count .page_pos
dwg.annotations()           # {name: type} of every named annotation already on the sheet
dwg.get_annotation(name)    # the named annotation object, or None
dwg.view_bounds("front")    # (x_min, y_min, x_max, y_max) page bbox of a view, or None
dwg.items                   # the ordered, mutable list of annotation objects
dwg.views                   # selected view names → (visible, hidden) compounds
dwg.draft / dwg.scale / dwg.page_w / dwg.page_h
```

**Add a dimension by naming a feature and a measurement** — the engine derives the
value from the geometry and owns the placement:

```python
# side ∈ {"above","below","left","right"}; view ∈ {"front","plan","side"}.
env  = next(f for f in dwg.model().features if f.kind == "envelope")
hole = next(f for f in dwg.model().features if f.kind == "hole")

dwg.dimension(env, "length", role="width", side="above", view="front")
dwg.locate(hole)                         # locating dimensions for the feature
dwg.callout(hole)                        # a ⌀ callout the auto-pass missed

dwg.drop(hole)                           # stop dimensioning this feature

name = next(iter(dwg.annotations()))     # names come from annotations(); they are
dwg.remove(name)                         # engine-assigned, so don't guess one
```

**Mind the two spellings.** On `Drawing.dimension` the second argument is the
parameter *kind* (`"length"`, `"diameter"`) and `role=` discriminates between
same-kind parameters — an envelope has three `length` params with roles `width` /
`height` / `depth`, so `dimension(env, "length")` alone is ambiguous and raises,
naming them. `feature.parameters()` returns `DimParameter` objects carrying
`.kind` and `.role`, so `[(p.kind, p.role) for p in feature.parameters()]` is how
to see what a feature accepts.

On the `Sheet` facade the same measurement is one dotted **parameter id** —
`"width.length"` — and `dimension_ids()` on a handle lists them. Use the id there
rather than the bare family role (`"width"`): the bare spelling is deprecated,
because it is what let a single call silently declare two dimensions.

`place_dim(p1, p2, side, view, …)` still exists for raw page coordinates, but it is
the **escape hatch of last resort** (ADR 4) and is deprecated: it bypasses the
layout solve, so nothing re-flows around it.

**Add a callout on a hole the auto-pass missed** — inspect its semantic requirement
finding first. Equal diameters can belong to different axes, blind depths, threads
or fits; a diameter-total warning never authorises increasing a callout count.

```python
for issue in dwg.lint():
    if issue.code.startswith("hole_requirement_"):
        print(issue.message)
        if issue.suggestion:
            print(issue.suggestion)
```

For a verified missing bore on an automatic model, the suggestion targets the exact
feature with `dwg.callout(...)` through the shared solve. It applies to that Drawing;
re-run lint after rebuilding. Authored omissions, unverified ownership and conflicting
counts require inspection of the declared operation rather than an automatic count edit.

**Free text** at a chosen point is `note()` — a domain verb, not an escape hatch:

```python
dwg.note("ø4 BORE", dwg.at("front", 10, 0, 5), view="front")
```

Then re-lint and export:

```python
issues = dwg.lint()                       # list of LintIssue; [] when clean
paths = dwg.export("drawings/bracket", formats=("svg", "dxf", "pdf"))
svg, dxf = paths["svg"], paths["dxf"]
```

Pass `formats=` and read the `{format: path}` dict. `Drawing.export()` without
`formats`, with `formats=None`, or with the old `svg=`/`dxf=` booleans was removed
in 0.5.0. See `docs/deprecations.md`.

`make_drawing(...)` is unaffected and does not warn: it still returns
`(svg_path, dxf_path)`, and passes `formats=` internally to get them.

`make_drawing(...)` calls `build_drawing(...).export(formats=("svg", "dxf"))`
and returns the two paths as a tuple.

**Authored section views** use `section_view()` alongside an explicit dimension and
view set. This complete example declares the bore diameter and overall extents;
it deliberately leaves the bore's location dimensions unrequested:

<!-- example: authored-section -->
```python
from build123d import Box, Cylinder, Pos
from draftwright import Sheet

bore_tool = Pos(12, 0, 0) * Cylinder(3, 12)
part = Box(60, 40, 12) - bore_tool
sheet = Sheet(part, page="A3", scale=1, projection="third")
sheet.authored_dimensions().authored_views()

envelope = sheet.envelope(part)
for parameter_id in envelope.dimension_ids():
    sheet.dimension(envelope, parameter_id)
bore = sheet.hole(bore_tool)
print(bore.dimension_ids())  # discover this handle's Sheet measurement IDs
sheet.dimension(bore, "bore.diameter")

for name in ("front", "plan", "side"):
    sheet.view(name)
sheet.section_view("A", through=bore)  # or at=0 for an explicit model-space Y plane

drawing = sheet.build()
issues = drawing.lint()
for issue in issues:
    print(issue.severity, issue.code, issue.message)
# drawing.export("build/section", formats=("pdf", "svg"))
```

`dimension_ids()` lists the Sheet handle's dotted IDs: use `"bore.diameter"` here.
Drawing.dimension instead takes a kind and optional role, such as `"length"` with
`role="width"` for an envelope. Hole diameter/depth use Drawing.callout, not
Drawing.dimension. Inspect `issues`: authored omissions
remain visible, so a successfully built section does not prove a complete drawing.

`view()` and `section_view()` define complete authored principal and derived sets.
To augment automatic views instead, start a new Sheet, explicitly select
`auto_views()`, and use `add_section_view("A", through=...)` or `at=...`.
That mode remains supported; `auto_views()` emits a soft-deprecation notice with
no removal date. Do not switch an already-authored view set to `auto_views()`.
See the runnable augmentation example in `docs/reference/sheet.md`.

`Sheet.section()` is deprecated with removal targeted at 0.6.0. Migrate it to the
method for your view source above. `Drawing.section()` remains the automatic
post-build section operation: it may return no section when the geometry does not
warrant one. It does not replace an explicit authored section request.

Arbitrary **auxiliary** views have no public `Drawing` verb, so a custom viewing
direction is not currently part of the supported surface.

`Drawing.add_view()` and its public view-coordinate plumbing were removed in
0.5.0 (#2113). `Sheet.add_view()` is a separate supported verb for augmenting an
automatic view set; view projection itself remains engine plumbing.

---

## Step 3 — Verify

```
mcp__build123d-mcp__render_drawing(svg_path='drawings/part_name.svg', save_to='/tmp/dwg.png')
mcp__build123d-mcp__save_drawing_annotations(svg_path='drawings/part_name.svg')
mcp__build123d-mcp__inspect_drawing(svg_path='drawings/part_name.svg')
```

---

## Step 4 — Save a standalone regeneration script (default)

**A — Drawing from a STEP file**:

```python
from draftwright.sheet_emit import generate_sheet_script

generate_sheet_script(
    "path/to/part.step",
    out="scripts/drawings/bracket",
    title="BRACKET", number="DWG-042",
    tolerance="ISO 2768-f", drawn_by="Your Name",
)
```

This writes a declarative `Sheet` script: one named line per detected feature, then the
dimension set it draws. Comment a feature line out to drop that feature; edit a value
freely; re-run the file. (`generate_script`, the older imperative flavour, was retired in
#940 and deleted in 0.4.0 — importing it now fails; use `generate_sheet_script` above.) The
CLI equivalent is `draftwright part.step
--script --out scripts/drawings/bracket`.

**B — Drawing an in-session object** (hand-write the script):

```python
#!/usr/bin/env python3
"""BRACKET — regenerates drawings/bracket.svg + .dxf in one run."""
from draftwright import make_drawing
from myproject.bracket import build_bracket

part = build_bracket()
make_drawing(part, out="drawings/bracket", title="BRACKET",
             number="DWG-042", tolerance="ISO 2768-f", drawn_by="Your Name")
```

---

## Scale and page control

```python
from draftwright import choose_scale

SCALE, PAGE_W, PAGE_H, TB_W = choose_scale(x_size, y_size, z_size)
make_drawing(part, out="drawing", scale=2.0, page="A2")
```

---

## Lint → critique → fix (the loop to drive as an AI)

draftwright is built to be *iterated*: build, read a machine-readable critique,
apply a domain-level fix, re-build. This is the supported refinement model —
prefer it over eyeballing the SVG and hand-placing annotations.

```python
dwg = build_drawing(part)

# 1. Critique — the machine channel. JSON-friendly aggregate of lint().
crit = dwg.lint_summary()
#   {"passed": bool, "score": 0..1, "diagnostic_score": 0..1,
#    "errors": n, "warnings": n, "infos": n, "by_code": {code: n},
#    "issues": [{code, severity, message, suggestion?}, ...],
#    "quality": {"completeness": …, "restraint": …, "legibility": …, "fidelity": …,
#                "unscored": {"available": True, "score": None, "issues": n,
#                             "by_code": {…}, "unclassified": [...], "reason": …}}}
# Gate on the severity/code COUNTS, not the scalar score.
#
# The four `quality` components answer four different questions and none substitutes for
# another: did required content land, is there too much of it, can a reader make it out,
# and — `fidelity` — is what it says TRUE. Completeness, restraint and fidelity report
# `available: False` with a `reason` rather than a flattering number when they have no
# evidence, so read `available` first. (Legibility is always available: an empty sheet
# genuinely IS legible, which is not the same kind of 1.0 as an empty sheet being truthful.)
# `quality["unscored"]` lists the findings none of the four priced; a non-empty
# `unclassified` there means a lint code reached no component and nobody decided it should.

# 2. Each issue names the problem in DOMAIN terms and (when computable) carries a
#    ready-to-apply suggestion — a domain-API call you paste in, not page maths.
for i in dwg.lint():
    print(i.severity, i.code, i.message)
    if getattr(i, "suggestion", None):
        print("  fix:", i.suggestion)   # e.g. dwg.callout(...) / dwg.dimension(...)

# 3. Self-repair — auto-applies the mechanically-fixable issues (overlapping
#    labels pushed apart, wrong-side dims flipped). Runs by default inside
#    build_drawing; call again after manual edits. It never makes a sheet worse.
dwg.repair()

# Pin a deliberate placement so repair won't move it (the constraint solver will
# honour it too as it lands — ADR 2):
dwg.pin(name)             # name from dwg.annotations(); dwg.unpin(name) to release
```

Codes are domain-meaningful (`feature_not_dimensioned`, `feature_count_mismatch`,
`callout_dropped`, `location_ref_dropped`, `step_dim_dropped`, …), so a fix is
always expressible through the domain API (`dimension`, `locate`, `callout`,
`features`), never the page-layout internals. Loop until `passed` (or the score
plateaus).

Coverage-only check, standalone:

```python
from draftwright import build_drawing, lint_feature_coverage
issues = build_drawing(part).lint()   # geometry lint + feature-coverage check
```

---

## Using this skill with build123d-mcp

This skill requires `draftwright` to be in the server's import allowlist.
The build123d-mcp server allows it by default from v0.3.51 onwards. If you see
`ImportError: draftwright is not in the import allowlist`, pass
`--allow-imports draftwright` to the server CLI or set
`BUILD123D_ALLOW_IMPORTS=draftwright` in the environment.

For annotation primitives (`Dimension`, `Leader`, `TitleBlock`, `lint_drawing`,
`ViewCoordinates`, etc.) continue to import from `build123d_drafting`.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

View all in development →

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.