# FlywheelCAD — Scripting Guide for AI Agents

FlywheelCAD is a macOS CAD app whose documents are Python scripts. This file is
everything you need to write one, render it headlessly and check the result.
Read it once, top to bottom. It is self-contained: nothing else is required.

For a call's exact behaviour, read its docstring in the API module the app ships:

```sh
API=/Applications/FlywheelCAD.app/Contents/Resources/flywheelcad.py
grep -n "def sweep" -A40 "$API"
```

**Error messages name the fix.** Every refused or renamed spelling raises with the
current one in the message. Read it instead of guessing, and do not wrap `cad.*`
calls in `try/except`.

---

## A. Deliver a `.fwcad` bundle

A document is a folder named `<Name>.fwcad` containing a UTF-8 `main.py`. There is
no manifest or other required file. Start `main.py` with the preamble in §2.

- **First open asks for consent**, once, because a document is executable code.
  Tell the user to expect the prompt.
- **If the user has a document open** and wants to see the result there, write
  that bundle's `main.py`: the app watches it and reloads on change. Otherwise
  create a new bundle and tell the user to open it (`open Name.fwcad`). Do not
  write any other file inside a bundle that is open in the app.
- **The app rewrites `main.py`** on GUI edits, in one canonical spelling with
  default kwargs omitted. Hand formatting may not survive; other files in the
  bundle are left alone.
- **Sibling modules** in the bundle are importable (`import helper`). Do not name
  one `flywheelcad`, `sitecustomize`, `component_shape`,
  `component_export_rewrite`, `document_setting_rewrite`, `script_source_lines`,
  `script_symbol_rewrite`, `test` or a stdlib module. No `__init__.py` anywhere.
- **Library parts** (fasteners, bearings, steppers, …) ship in
  `/Applications/FlywheelCAD.app/Contents/Resources/Library/`. To use one, copy the
  module into the bundle as `lib/standard/<module>.py` (plus any sibling it imports
  with `from .x import`), then `from lib.standard.bearings import bearing`. An
  un-copied import fails with `No module named 'lib'`.

---

## 1. Build, render, verify

The app binary has a headless mode. It works while the GUI is open.

```sh
FWC=/Applications/FlywheelCAD.app/Contents/MacOS/FlywheelCAD
export LLVM_PROFILE_FILE=/dev/null        # stops a stray default.profraw in the cwd
"$FWC" render Part.fwcad -o /tmp/part.obj  # or .3mf (default) .stl .glb .gltf
"$FWC" snapshot Part.fwcad -o /tmp/part.png
```

| `render` exit | meaning |
|---|---|
| 0 | clean: every exported body is watertight and consistently wound |
| 1 | the script raised, or export failed or was refused (the message says why) |
| 2 | no bodies were produced, or every body is hidden |
| 3 | exported, but the sketch solver did not converge (the warning names the constraint) |
| 4 | exported, but a mesh is torn, non-manifold or mis-wound (the warning names the body) |

`--quality preview|standard|high|ultra|precision` overrides the document's mesh
tier for one render, but not a per-body `quality=` or `cad.set_cell_size`.
Without `-o`, the output is `<Name>.3mf` beside the input; write into a temp
directory instead, to keep the user's folder clean.

`snapshot` takes `--size 1200`, `--bg "#F4F6F9"`, `--view-dir x,y,z` (the direction
the camera looks *from*; default `0.9,-1,0.62`), `--look-at x,y,z --look-radius r`
(frame a detail), `--view sketch` (head-on 2D linework) and `--show-sketch`.
Unknown flags are ignored without a warning.

**Exit 0 does not prove the shape is right.** Check each body's extent. The OBJ
export starts each body with an `o <name>` line:

```sh
python3 - /tmp/part.obj <<'EOF'
import sys
bodies, cur = {}, None
for line in open(sys.argv[1]):
    if line.startswith("o "): cur = line[2:].strip(); bodies[cur] = []
    elif line.startswith("v "): bodies[cur].append([float(t) for t in line.split()[1:4]])
for name, vs in bodies.items():
    if not vs: print(name, "EMPTY"); continue
    print(name, [(round(min(v[i] for v in vs), 2), round(max(v[i] for v in vs), 2)) for i in range(3)])
EOF
```

Then look at a snapshot or two, from more than one direction.

**Known issues in 0.35**, all silent:

- **A `cad.fillet(...)` call empties every body created before it**, with no error (the render exits 0 and counts the body).
  Do every sketch that uses `cad.fillet` before the first body op. Run the extent check above.
- `snapshot --view-dir 0,0,-1` (straight up from below) renders a blank image. Tilt
  it slightly: `0,-0.05,-1`.

---

## 2. The model and the preamble

Every script starts with exactly this:

```python
# flywheelcad-format: 1
from flywheelcad import *
cad = FlywheelCAD()
```

- **The handle must be named `cad`, at module level.** The app inserts settings
  after that line when the user changes them. Put every `import` of a component or library module *below* it.
- **The script is the document.** Opening re-runs it, and there is no other model
  file. It must rebuild the design deterministically: no `random`, clock or
  external state. Loops and helper functions are fine.
- Each `cad.*` call emits a command. The pipeline is always **2D sketch → region →
  3D body**: draw constrained geometry on a plane, identify a closed region,
  extrude/revolve/loft/sweep it, combine bodies with booleans.
- Units are **millimetres**, angles **degrees**. **Numeric arguments** accept
  `int`, `float`, `Decimal`, `Fraction` and numpy scalars. Strings and booleans
  are refused, so convert CSV or JSON values with `float(...)`.
- `from flywheelcad import *` also provides `region`, `rev`, `ref`,
  `sweep_section`, `ring`, `polar`, `grid`, and one origin point per plane:
  `origin_xy`, `origin_yz`, `origin_zx`, plus `origin_<name>` for each custom plane.

---

## 3. Rules

1. **Assign every created element to its own variable.** Its name comes from the
   left-hand side: `l1 = cad.line2d(...)` is named `l1`. Never reuse a variable for
   a different element. `name=` overrides it on any Ref-producing call; a clashing
   name raises. `shaft = cad.bool_difference(shaft, tool)` works but numbers both
   bodies (`shaft_1`, `shaft_2`), so the string `"shaft"` then names nothing.
   Prefer a new variable for each result.
2. **Emit in dependency order**: points, then lines, then regions, then bodies,
   then booleans over them.
3. **Call `cad.with_sketch(plane)` before drawing.** `point2d` and
   `region(inside=...)` use 2D coordinates on that plane, mapped like this:

   | plane | sketch x | sketch y | extrude along |
   |---|---|---|---|
   | `"xy"` | +X | +Y | +Z |
   | `"yz"` | +Y | +Z | +X |
   | `"zx"` | **−X** | +Z | +Y |

4. **Always name custom planes**: `cad.create_sketch_plane(p1, p2, p3, name="top")`,
   then refer to `"top"`. The normal is `(p2 − p1) × (p3 − p1)`. Sketch y is world
   +Z projected into the plane (+Y if the plane is horizontal), and sketch x is
   y × normal. So `(0,0,0), (1,0,0), (0,0,1)` gives x = +X, y = +Z, normal −Y.
   Unnamed planes auto-number, so adding one renumbers every later reference.
5. **Never put a construction element in a region loop.** Construction geometry is
   excluded from region detection, so the loop never closes.
6. **Model each body as one profile where the shape allows.** Holes and slots go
   in `holes=`. An L, T or stepped outline *is* the profile. A stack of round
   sections is one `revolve` of the half-profile. A boolean between separately
   built bodies can leave non-manifold edges (exit 4). Keep booleans for what one
   profile cannot express, such as a counterbore, a cross hole or a pocket, and make
   cutting tools overshoot the faces they pass through.
7. **A closed loop inside a region's outline, on the same sketch, must be in
   `holes=`** (or claimed as material by an `inside=` point within it), or the
   region is refused. To put a second profile over a first, draw it on its own
   plane.
8. **A sweep profile's sketch coordinates are its offset from the path.** Draw it
   around the sketch origin; the path supplies the position (`cad.helix(radius=...)`),
   so never put the coil radius in both. At each station sketch x points along the
   path normal (radially outward from a helix axis) and sketch y along the
   binormal, so a thread tooth drawn toward −x points inward.
9. **Assign `cad.circle_tangent_line(...)` and `cad.ellipse_tangent_line(...)`.**
   They return the tangency point and raise if unassigned.
10. **Some parameters take only a plain number.** A python float is fine; a
    `cad.variable` raises. These are `cad.fillet(radius=)`, `edge_radius=`, `draft=`,
    `twist=`, and `cad.helix`'s `radius`, `pitch` and `turns`. Dimension values, `distance=` and `offset=` accept variables and
    expressions.
11. **Topology attributes are unchecked.** `body.v9999` returns a ref and fails much
    later. For reference geometry, prefer `cad.point(x, y, z)` and
    `create_sketch_plane`.
12. **Inside a `with cad.component(...)` block, pass `plane=` explicitly** or call
    `cad.with_sketch` inside the block.
13. **Leave the app's settings alone** (§8).

---

## 4. Sketching

A constrained, dimensioned rectangle with a rounded corner and a hole:

```python
cad.with_sketch("xy")
w = cad.variable(40, driving=True)          # a number the user will want to change
h = cad.variable(25, driving=True)
a = cad.point2d(0, 0)
b = cad.point2d(40, 0)                      # coordinates are the solver's start guess
c = cad.point2d(40, 25)
d = cad.point2d(0, 25)
l1 = cad.line2d(a, b)
l2 = cad.line2d(b, c)
l3 = cad.line2d(c, d)
l4 = cad.line2d(d, a)
cad.coincident(p0=a, p1=origin_xy)          # pin the corner to the plane origin
cad.horizontal(line=l1)
cad.horizontal(line=l3)
cad.vertical(line=l2)
cad.vertical(line=l4)
cad.length(line=l1, length=w)
cad.length(line=l2, length=h)
f1 = cad.fillet(l1, l2, radius=4)           # returns the arc; it goes in the loop
ctr = cad.point2d(20, 12.5)
hole = cad.circle2d(ctr, 5)
cad.radius(circle=hole, radius=5)
plate = cad.extrude("xy", region(loop=[l1, f1, l2, l3, l4], holes=[[hole]], inside=(3, 3)), 8)
```

**Geometry**, on the current sketch plane. The curve primitives take
`construction=True`, and every Ref-producing call takes `name=`:

| Call | Notes |
|---|---|
| `cad.point2d(x, y)` | 2D point. A coordinate is a start guess, not a constraint |
| `cad.point(x, y, z)` | 3D reference point, world coordinates |
| `cad.line2d(p1, p2)` | |
| `cad.circle2d(center, radius)` | |
| `cad.arc2d(center, start, end, clockwise=False)` | both endpoints must lie on the circle |
| `cad.ellipse2d(focus1, focus2, point)` | two foci and a point on the rim |
| `cad.spline2d(points, closed=False)` | cubic spline through the points |
| `cad.fillet(line1, line2, radius)` | rounds a shared-endpoint corner (lines or arcs); returns the arc, which goes in the loop between them |
| `cad.trim(element, near=(x, y))` | returns `(element, split_piece)` |
| `cad.mirror_elements([els], symmetry_line)` | copies are named `<source>_m` |
| `cad.text(string, size=10, font="Helvetica", at=(0, 0), align="left", distance=None, plane=None)` | one Ref per character. Outline splines by default; `distance=` makes one solid body per glyph (not unioned) |
| `cad.origin("xy")` | the plane's origin point, same as `origin_xy` |

**Constraints** return nothing, except the two tangent-line constraints (rule 9).
Wherever one takes a point, a circle or arc means its centre. Keyword form is what
the app writes; positional also works.

`cad.horizontal(line=)` · `cad.vertical(line=)` · `cad.parallel(line1=, line2=)` ·
`cad.perpendicular(line1=, line2=)` · `cad.collinear(line1=, line2=)` ·
`cad.coincident(p0=, p1=)` (merges `p0` into `p1`) · `cad.merge_points(source, target)` ·
`cad.point_on_line(point=, line=)` · `cad.point_on_circle(point=, circle=)` ·
`cad.point_on_ellipse(point=, ellipse=)` · `cad.point_on_plane(point=, plane=)` ·
`cad.fixed(element=)` · `cad.concentric(circle1=, circle2=)` ·
`cad.equal_lines(line1=, line2=)` · `cad.equal_radius(circle1=, circle2=)` ·
`cad.circle_tangent_circle(circle1=, circle2=)` ·
`tp = cad.circle_tangent_line(circle=, line=, anchor=None)` ·
`tp = cad.ellipse_tangent_line(ellipse=, line=, anchor=None)` ·
`cad.spline_tangent_line(spline=, line=)` ·
`cad.symmetry_line(p1=, p2=, symmetry_line=)` · `cad.symmetry_plane(point1=, point2=, plane=)`

**Dimensions** take a number, a `cad.variable`, or an expression over variables
(`w * 0.5`; `+ - * /` only, since `**` resolves to a plain number):

`cad.length(line=, length=)` · `cad.distance(p0=, p1=, distance=)` (Euclidean) ·
`cad.radius(circle=, radius=)` · `cad.angle(line1=, line2=, angle=)` ·
`cad.point_line_distance(point=, line=, distance=)`

**Variables**: `v = cad.variable(40, driving=True)` is a user-editable parameter,
and `fixed=True` is a constant. With neither flag it is a free unknown that the
solver may move (40 can solve to 32.5), so use `driving=True` for parameters.
`fixed` and `driving` are mutually exclusive. `cad.fraction(a, b, 0.25)` holds `a = 0.25 * b`. A variable
is a symbol, so `v.value` raises, but arithmetic on it (`w - wall`, `h / 2`) works
wherever a variable does (so not in the rule 10 parameters). A constructor stores numbers: `cad.point2d(w - 2, 0)`
resolves when the script runs and adds no constraint, so the link lives only in
dimensions. (`cad.circle2d(ctr, d / 2)` also emits the matching `cad.radius`
dimension, for a linear expression.)
To test that a variable really drives the shape, change its declared value in a scratch copy and re-render.
`cad.update(...)` is a low-level command, and in 0.35 a `{"value": …}` update
does not change the geometry.

**Placement helpers** emit nothing and return plain numbers: `polar(r, angle_deg,
center=(0, 0))` → `(x, y)`, `ring(count, step=None, start=0)` → angles,
`grid(nx, ny, dx, dy)` → offsets. There is no sketch pattern op, so write the loop.

---

## 5. Regions

```python
r = region(loop=[l1, l2, rev(a3), l4],   # closed, in traversal order
           holes=[[hole1], [s1, s2, s3, s4]],   # one list per hole loop
           inside=(5, 5))                # a 2D point inside the material
```

- `loop` is directed. Wrap an element that runs backwards in `rev(...)`.
- Give `inside=` whenever there are holes or one outline could enclose two areas
  (concentric circles). An `inside=` point placed in a hole fills that hole.
- Reuse the same circle when a hole in one region is the outline of another. A
  second identical circle is dropped as a duplicate, so any region that names the
  dropped copy never resolves.
- `region(...)` and `cad.region(...)` are the same. Its `name=` is only a label.
- Glyph outlines from `cad.text` carry their counters: `region(loop=[glyphs[0]])`.

---

## 6. Bodies

Every body op returns a body ref and accepts `name=` and `quality=` (§8). All but
`cad.offset` also take `export=`, which must be a real `bool`.

| Call | Notes |
|---|---|
| `cad.extrude(plane, region, distance, direction=None, offset=None, edge_radius=None, draft=None, twist=None, twist_center=None)` | `direction`: `None` or `"positive"` (+normal), `"negative"`, `"symmetric"`. `offset` shifts the start along the normal |
| `cad.revolve(plane, region, axis_line, angle)` | `angle=360` for a full solid. The axis may be a profile edge or a construction line that the loop does not use |
| `cad.loft(start_plane=, start_region=, end_plane=, end_region=)` | use the keyword form. Both profiles need the same number of holes |
| `cad.multi_loft([planes], [regions])` | one smooth solid through N ≥ 2 sections. It bulges between equal sections, so use `revolve` for straight-walled round stacks |
| `cad.sweep(plane, region, path, twist=0.0)` | profile centred on the sketch origin (rule 8) |
| `cad.sweep(path=, sections=[sweep_section(fraction, plane, region), ...])` | morphing profile. Fractions run from exactly 0.0 to exactly 1.0, strictly increasing; no holes. Put same-origin sections on different planes |
| `cad.helix(radius, pitch, turns, axis_origin=(0,0,0), axis_dir=(0,0,1), start_angle=0, left_handed=False)` | returns a path, not a body |
| `cad.path(plane, chain=[l1, rev(a2), l3])` | open planar rail. Keep it clear of the profile's elements |
| `cad.path3d(points=[p1, p2, p3], up=None)` | 3D rail through named `cad.point` refs, not tuples |
| `cad.bool_union(a, b, ...)` · `cad.bool_difference(a, b, ...)` · `cad.bool_intersection(a, b, ...)` | ≥ 2 bodies; difference is first minus the rest. `blend="smooth"` or `"chamfer"` needs exactly 2 bodies and `radius > 0` |
| `cad.offset(body, distance)` | + grows, − shrinks |
| `cad.move(body, translate=(x, y, z))` | `translate=` is keyword-only |
| `cad.rotate(body, axis=(0,0,1), angle=0, center=(0,0,0))` · `cad.scale(body, factor=)` or `sx=/sy=/sz=` · `cad.mirror_body(body, plane="yz")` · `cad.copy(body)` | transforms keep the source and return a new body |
| `cad.pattern_linear(peg, direction=(1,0,0), count=4, spacing=10, name="row")` | returns `[seed, copy_1, ...]`. `count` includes the seed (2–1000). Needs an assignment or `name=`; copies are named `row_1`, `row_2`, … |
| `cad.pattern_circular(bolt, axis=(0,0,1), count=6, angle=360, center=(0,0,0), name="ring")` | `angle` is the total sweep, stepping `angle / count` for 360 and `angle / (count - 1)` for a partial fan |
| `cad.delete_body(body)` | cascades to consumers |
| `cad.section(body, plane)` · `cad.project(body, plane)` · `cad.project_point(point, plane)` | analysis curves in a sketch |

- **Booleans consume their operands.** Consumed and hidden bodies are left out of
  the export, so only the final body ships. Do not `cad.hide` an operand yourself.
- **A plain extrude or revolve meshes exactly** (no `edge_radius`, no `draft`).
  Booleans, offsets, lofts, sweeps and rounded extrudes are sampled on a grid, so
  tiny features can need a finer tier (§8).
- **Vertices of an extruded body** are available as `box.v0`, `box.v1`, … (holes:
  `box.h0_v0`; symmetric extrudes: `box.pos_v0`, `box.neg_v0`). Use them to define
  a plane: `cad.create_sketch_plane(box.v0, box.v1, box.v2, name="face")`.
- **A plane parallel to XY at height z**, for a second profile on top of a body:

  ```python
  q0 = cad.point(0, 0, 8)
  qx = cad.point(1, 0, 8)
  qy = cad.point(0, 1, 8)
  top = cad.create_sketch_plane(q0, qx, qy, name="top")
  cad.with_sketch("top")                    # sketch x = +X, sketch y = +Y
  ```

**Appearance and export** take a body ref or its name as a string. The string form
also reaches bodies with no python variable, such as pattern
copies and instance parts.

- `cad.set_color(body, color, finish=None)`. The color can be `"#RRGGBB"`,
  `"#RRGGBBAA"`, `"#RGB"`, a name (`"red"`, `"steel"`, `"gold"`, …) or an
  `(r, g, b[, a])` tuple. The finish is `"matte"`, `"glossy"` (the default),
  `"metallic"` or `"glass"`. It is display-only, and is written into 3MF and
  glTF/GLB exports.
- `cad.set_export(body, False)` drops a visible body from the export. Pass a
  real `bool`.
- `cad.hide(*bodies)` and `cad.show(*bodies)` take one or more bodies. Hidden
  bodies are not exported.
- `set_color`, `set_export` and `set_cell_size` also chain:
  `cad.extrude(...).set_color("red")`.

---

## 7. Components (reusable parts, assemblies)

```python
with cad.component("peg"):                   # define flat; never nest `with` blocks
    cad.with_sketch("xy")
    pc = cad.point2d(0, 0)
    pcirc = cad.circle2d(pc, 4)
    peg_body = cad.extrude("xy", region(loop=[pcirc], inside=(0, 0)), 12)
    cad.component_export(peg_body)                       # a visible part of the component
    cad.component_export_point("base", cad.point(0, 0, 0))   # named anchors
    cad.component_export_point("top", cad.point(0, 0, 12))

left = cad.instance("peg", translate=(-15, 0, 0))
right = cad.instance("peg", translate=(15, 0, 0))    # also mirror_plane=, scale=, axis=/angle=/center=
stacked = cad.instance("peg")
cad.mate_coincident(stacked, "base", left.top)       # stacked sits on top of left
```

- A component that is never instanced builds straight into the document.
- Anchors are addressed as `<instance>.<anchor>` (`left.top`). With several
  exported bodies, an instance's parts are named `<instance>_<body name>`.
- The mates are:
  - `cad.mate_coincident(inst, anchor, target, offset=)`: snap one point onto
    another.
  - `cad.mate_align(inst, a1, a2, t1, t2, offset=)`: snap and orient.
  - `cad.mate(inst, anchor, target)`: solved; three of these fix a pose.
  - `cad.mate_axis(inst, a1, a2, t1, t2)`: solved; the axes are made collinear
    and the part stays free to slide and spin.
  - Invalid mate combinations fail the run with an error that names them.
- `cad.instance(..., quality=)` is dropped with only a warning on stderr: an
  instance takes its component's quality.
- `cad.parametric(prefix, build, label=None, **params)` memoizes a component per
  parameter set and returns its name. `cad.component_name(...)` and
  `cad.has_component(...)` support it. A library factory such as
  `bearing(cad, ...)` returns a component name, so place it with `cad.instance`.
- Each call's docstring in `flywheelcad.py` has the details.

---

## 8. Settings that belong to the app

The user sets mesh quality, units and default cell size in the app, and **the app
changes them by rewriting your script**. A call in a shape it cannot rewrite
permanently disables that control. So:

- **Never write `cad.set_quality(...)`, `cad.set_units(...)` or
  `cad.set_default_cell_size(...)`.** If you leave them out, the document uses
  `standard` quality, `mm` units and the tier's own cell size.
- **Write a per-body `quality=` only on a body whose feature needs it**, such as a
  real thread tooth or a sub-millimetre groove. Say why in a comment. Spell it as a
  double-quoted literal on a single-line call:
  `cad.sweep("xy", tooth, tpath, quality="high")`. The tiers, fastest first, are
  `"preview"`, `"standard"`, `"high"`, `"ultra"` and `"precision"`. Never use
  `"precision"` as a blanket setting: it can take minutes to hours.
- **`cad.set_cell_size(body, mm)`** sets an absolute grid cell for one sampled body
  whose small features are lost. It has no effect on an exactly meshed body. Write
  one line per body, never in a loop, using a variable assigned exactly once or the
  body's name string. A rebound variable blocks the Body Inspector's Clear for
  every body.
- **Never hand-write `cad.label_offset(...)`.** The GUI records dragged dimension
  labels with it; leave existing ones in place.

---

## 9. Author for the person who opens it

The file is a document someone will open, inspect and edit, not a build artifact.

- **Sketch, don't compute.** Build profiles from points, lines and arcs with
  constraints (`horizontal`, `perpendicular`, `equal_lines`, `concentric`,
  `symmetry_line`, …) rather than computing every vertex in python. Four points
  that merely sit in a rectangle stop being one the first time someone drags a
  point.
- **Dimension what a person would change**: overall sizes, wall thickness, hole
  sizes and positions, spacing. Bind those to `cad.variable(..., driving=True)`
  and reuse the variable when two edges are meant to be equal.
- **Leave sampled geometry undimensioned**: spline points from a table, glyph
  outlines, points made in a loop. Put the editable number on the parameter that
  generated them.
- **Write dimensions at module level**, one statement per line or `;`-joined. An
  indented dimension (inside a `with` block) or one split across lines cannot be
  edited from the GUI.
- **The Edit Dimension sheet edits numbers and `cad.variable`s, not expressions.**
  A dimension bound to an expression (`w * 0.5`) or to a plain python variable is
  preserved as written, and the sheet declines to edit it.
- **The sketch badge**: green (fully constrained) is the goal for designed shapes,
  red (conflicting) is a real error, and blue (DOFs free) is honest for sampled
  geometry. Each `cad.fillet` currently adds 2 DOFs to the badge.

---

## 10. Worked example: a filleted tag with a ring hole

```python
# flywheelcad-format: 1
from flywheelcad import *
cad = FlywheelCAD()

# All sketches that use cad.fillet come before the first body (see §1).
cad.with_sketch("xy")
tag_w = cad.variable(50, driving=True)       # tag length
tag_h = cad.variable(22, driving=True)       # tag height
hole_d = cad.variable(5, driving=True)       # ring-hole diameter
thick = cad.variable(3, driving=True)        # plate thickness

a = cad.point2d(0, 0)
b = cad.point2d(50, 0)
c = cad.point2d(50, 22)
d = cad.point2d(0, 22)
bottom = cad.line2d(a, b)
right = cad.line2d(b, c)
top = cad.line2d(c, d)
left = cad.line2d(d, a)
cad.coincident(p0=a, p1=origin_xy)
cad.horizontal(line=bottom)
cad.horizontal(line=top)
cad.vertical(line=right)
cad.vertical(line=left)
cad.length(line=bottom, length=tag_w)
cad.length(line=right, length=tag_h)
r1 = cad.fillet(bottom, right, radius=4)
r2 = cad.fillet(right, top, radius=4)
r3 = cad.fillet(top, left, radius=4)
r4 = cad.fillet(left, bottom, radius=4)

hc = cad.point2d(6, 11)
ring_hole = cad.circle2d(hc, 2.5)
cad.radius(circle=ring_hole, radius=hole_d / 2)
cad.point_line_distance(point=hc, line=left, distance=6)
cad.point_line_distance(point=hc, line=bottom, distance=tag_h / 2)

tag = cad.extrude("xy", region(loop=[bottom, r1, right, r2, top, r3, left, r4],
                               holes=[[ring_hole]], inside=(25, 11)), thick)
cad.set_color(tag, "#2E7D32", finish="matte")
```

Render it, check the extent, and look at it:

```sh
"$FWC" render Tag.fwcad -o /tmp/tag.obj; echo "exit $?"   # expect 0; extent 50 × 22 × 3
"$FWC" snapshot Tag.fwcad -o /tmp/tag.png
```

To check that the dimensions drive the shape, change `cad.variable(50, ...)` to 70
in a scratch copy and re-render. The extent should become 70 × 22 × 3.

---

## 11. Checklist before you hand it over

- [ ] A `<Name>.fwcad/main.py` bundle, unless the user asked for a flat `.py`.
- [ ] The preamble, with the handle named `cad` at module level and imports below it.
- [ ] No `set_quality`, `set_units` or `set_default_cell_size`, and no blanket `quality=`.
- [ ] `with_sketch` before each sketch, with axes taken from the §3 table.
- [ ] Every element in its own variable, and regions closed, directed, with `inside=`.
- [ ] One profile per body where possible, and cutting tools that overshoot.
- [ ] Tunable numbers are `cad.variable`s bound to module-level dimensions.
- [ ] `render` exits 0, every body's extent is what you intended, and a snapshot looks right.
- [ ] Every `cad.fillet` sketch comes before the first body (§1).
