# Jitx Physical Layout

> Use when the user asks to author PCB physical layout from code: draw copper, antennas, filters, net ties, custom shapes, board outlines, custom pads, soldermask or paste openings, thermal pads with vias, code-placed vias, fanout or escape tags, direct-connect or thermal-relief tags, control points, code-based routes, escape routing, or deskew. Covers shapely geometry, Copper, OverlappableCopper, Pour, pad features, PortAttachment, explicit placement, layout-intent tags, and Route control-point APIs. Use jitx-substrate-modeler for stackups, vias, routing structures, fence-via rules, and fenced pours; use jitx-circuit-builder for net wiring, passives, and basic pours.

- Skill: `jitx-inc/jitx-physical-layout` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add jitx-inc/jitx-physical-layout`
- Raw SKILL.md: https://api.skillmd.com/api/skills/jitx-inc/jitx-physical-layout/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: JITx-Inc (https://skillmd.com/u/jitx-inc)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/jitx-inc/jitx-physical-layout

---


# JITX Physical Layout

Author physical layout geometry — copper, custom shapes, pad features, explicit
placement, code-driven vias/routes, and layout-intent tags — directly in Python.
This is the layer **between** schematic-level wiring (`jitx-circuit-builder`) and
stackup/fab definition (`jitx-substrate-modeler`).

JITX is a moving target — APIs on this page have been renamed across releases
(most recently the control-point classes in 4.2.0). Do not rely on prior JITX
knowledge — **verify every import and signature with `pyright` against the
installed package**, and build-test control-point/route geometry before
trusting it.

## Scope — what this skill owns vs neighbors

| You want to… | Skill |
|---|---|
| Draw copper shapes, antennas, filters, net-ties, custom board/pad geometry | **this skill** |
| Build a shape with shapely and feed it to any feature | **this skill** |
| Add soldermask/paste/thermal-pad features, place vias/components from code | **this skill** |
| Tag layout objects (fanout, escape, direct-connect) for selection | **this skill** (rule *mechanics* → substrate-modeler) |
| Code-based routes & control points (escape lanes, deskew) | **this skill** (advanced — see reference) |
| Wire nets, add passives, voltage dividers, basic pours | `jitx-circuit-builder` |
| Define the stackup, vias, routing structures, fence-via rules, fenced pour outlines | `jitx-substrate-modeler` |
| Author a component's package/landpattern from a datasheet | `jitx-component-modeler` |
| Topology (`>>`), timing/skew/impedance constraints | `jitx-interconnect-constraints` |

This skill covers the design-side *geometry and placement*; the substrate owns the
*rule definitions* (`design_constraint(...)`, via classes, routing structures) that
act on it. Where they meet, this skill cross-references rather than restating.

## Environment

Environment setup is handled by the base `jitx` skill — invoke it first.

## Imports

```python
# Copper & pours
from jitx import Copper, Pour, current
from jitx.feature import OverlappableCopper          # netless overlap copper
# Features (pad / surface / keepout)
from jitx.feature import Soldermask, Paste, Silkscreen, Courtyard, Custom, Cutout, KeepOut
# Shapes
from jitx.shapes import Shape
from jitx.shapes.shapely import ShapelyGeometry       # wrap a shapely geometry
from jitx.shapes.composites import rectangle, capsule, notch_rectangle, chipped_circle
from jitx.shapes.primitive import Circle, Polygon, Text
from jitx.anchor import Anchor
from jitx.layerindex import Side, LayerSet
import shapely                                          # the upstream library
# Placement & attachment
from jitx.net import Port, PortAttachment
# Tags
from jitx.constraints import Tag, Tags
# Pad config (landpattern feature generation)
from jitxlib.landpatterns.pads import SMDPadConfig
# Code-based routes / control points (stable as of 4.2; also re-exported from top-level jitx)
from jitx.circuit import Route
from jitx.controlpoint import RoutePoint, PairInsertion, PairPoint
```

**Do NOT import** (these do not exist): `jitx.copper.OverlappableCopper`
(it lives in `jitx.feature`), `jitx.shapes.Shapely`, `jitx.geometry`,
`jitx.layout`, `jitx.routes`. When unsure, search the installed source with your **Grep** tool (pattern `class OverlappableCopper|class Route|class PortAttachment`, path `.venv`, glob `*.py`); it recurses and is OS-agnostic. Shell fallback: bash `grep -rn "class OverlappableCopper\|class Route\|class PortAttachment" .venv/lib/python*/site-packages/jitx/` (macOS/Linux); on Windows use the Grep tool, or `Select-String` over `.venv\Lib\site-packages\jitx`.

## Custom shapes with shapely (general)

Shapely is general shape creation for **any** JITX feature that takes a `Shape` —
copper, pours, keepouts, the board outline, courtyards, and pad soldermask/paste.
It is not pad-specific.

Reach for a **built-in composite first** for common shapes — they stay exact
(`rectangle`, `capsule`, `Circle`, `notch_rectangle`, `chipped_circle`, `bullseye`,
`equilateral_triangle`, …). Use **shapely** when you need CSG (union / difference /
intersection), buffering, fillets, or arbitrary polygons.

```python
import shapely
from jitx.shapes.shapely import ShapelyGeometry

# Build geometry with shapely, then wrap the result for JITX:
ring = shapely.box(-5, -5, 5, 5).difference(shapely.box(-4, -4, 4, 4))
shape = ShapelyGeometry(ring)          # a jitx Shape — feed it to ANY feature

# Round-trip an existing JITX shape into shapely for an operation:
from jitx.shapes.composites import rectangle
expanded = rectangle(2, 1).to_shapely().buffer(0.1, cap_style="square", join_style="mitre")
# `.to_shapely()` and `.buffer(...)` return ShapelyGeometry; pass straight to a feature.
```

`ShapelyGeometry` supports set operators `&` (intersection) `|` (union) `-`
(difference) `^` (symmetric difference) and `.buffer()`. A **morphological open**
(`buffer(-r).buffer(r)`) rounds sharp inside corners — useful for paste cells.

**Validity caveat — guard before feeding a fab feature.** JITX serializes only
**non-empty `Polygon` / `MultiPolygon`** geometries. Shapely operations can produce
empty geometries, `LineString`s, or `GeometryCollection`s — those raise at build
time. After CSG, confirm the result is a non-empty polygon:

```python
g = ring  # a raw shapely geometry
assert not g.is_empty and g.geom_type in ("Polygon", "MultiPolygon"), g.geom_type
```

Arcs are polygonized at a tolerance when converted to/from shapely; keep an eye on
vertex counts for large numbers of circular features.

## Copper: Pour vs Copper vs OverlappableCopper

Three ways to put copper on a layer — pick by **net membership** and whether the
copper is allowed to **overlap** other copper:

| Construct | On a net? | Overlap-exempt? | Use for |
|---|---|---|---|
| `Pour(shape, layer, *, rank=0, orphans=True)` | yes (`net += Pour(...)`) | no | filled planes / shaped fills |
| `Copper(shape, layer)` | yes (`net += Copper(...)` or `a + Copper(...)`) | no | an explicit copper shape on one net |
| `OverlappableCopper(shape, layer)` | **no** (netless) | **yes** | net-tie copper bridging two nets' pads, antenna radiators, filter copper — ignored by the router and overlap checks |

`Copper(..., exempt=True)` was **removed in 4.2.0** — there is no on-net,
overlap-exempt copper anymore. Overlap-tolerant copper is `OverlappableCopper`,
which is netless: its connectivity comes from the pads it overlaps.

`Copper` lives in `jitx` (top-level / `jitx.copper`); `OverlappableCopper` lives in
`jitx.feature`.

```python
from jitx import Copper, Pour
from jitx.feature import OverlappableCopper

self.GND += Pour(current.design.board.shape, layer=0)     # board-wide top pour
self.SIG += Copper(rectangle(10, 0.5).at(0, 5), layer=0)  # copper shape on a net
```

**OverlappableCopper is netless.** Its electrical connection comes from the **pads it
overlaps**, not from the copper itself. A net-tie is the minimal case: the bridging
shape is `OverlappableCopper` drawn across pads on the two nets it ties. The
antenna pattern is the same idea at full size: give the structure a
small `Component` with anchor pads that *are* on the nets (so the router has
something to land on), then draw the radiating shape as `OverlappableCopper`
overlapping those pads:

```python
self.ant = AntennaIFA().at(0, 0)        # component with feed + short anchor pads, pinned at origin
self.ANT_FEED += self.ant.feed          # pads carry the nets
self.GND      += self.ant.short
# radiator shape is netless copper overlapping the pads — no DRC overlap error:
self.copper_radiator = OverlappableCopper(radiator_shape, layer=0)
```

Note `OverlappableCopper` is **not** in the set of tag-able objects (see Tags below).
The full antenna example is in `references/layout-examples.md`.

## Pad features (soldermask / paste / thermal pad)

Custom `Pad` subclasses (KiCad-converted footprints, mechanical pads) get **no
default soldermask or paste** — the pad is unsolderable until you add them. Two
mechanisms, kept distinct:

**(a) Feature objects added to a Pad.** Surface features take `(shape, side=Side.Top)`:

```python
from jitx.feature import Soldermask, Paste
# JLCPCB convention: mask = copper expanded by solder_mask_registration (~0.05 mm);
# paste = copper exactly. See pad_features.py helpers in references for Circle-exact expansion.
mask  = Soldermask(expanded_copper_shape, side=Side.Top)
paste = Paste(copper_shape)
```

**(b) `SMDPadConfig` driving a landpattern generator.** Each of `copper` /
`soldermask` / `paste` takes a **`Shape`** (or a `float` expansion, `None` to skip,
or a `ShapeAdjustment` like paste subdivision). Omit a field (default `...`) for
standard behavior. Pass the config to `landpattern.thermal_pad(shape, config=)`:

```python
from jitxlib.landpatterns.pads import SMDPadConfig
config = SMDPadConfig(soldermask=opening_shape, paste=opening_shape)   # both are Shapes
landpattern.thermal_pad(shape=rectangle(3.45, 3.45), config=config)
```

A **soldermask-defined thermal pad** (shapely CSG webs + via dams, a cheap-fab
alternative to filled via-in-pad) is a complete worked example in
`references/layout-examples.md`. Authoring the package/landpattern itself from a
datasheet belongs to `jitx-component-modeler`; this skill is the feature mechanics.

## Explicit placement & via attachment

**Scope rule — `PortAttachment` is for signal topologies only**: binding a signal
port to a control point, or to a signal via in an escape/deskew path. For
ground/power stitching vias, thermal vias, and anything else that just joins a
net, add the placed via (or copper) to the **net** — `Net` accepts `Copper | Via`
members directly. PortAttachment use is deliberately being limited and is
expected to be deprecated; default to the net form whenever the connection is
plain net membership.

```python
# Ground / thermal vias: net membership — NOT PortAttachment.
# Source the via class from the substrate / JLCPCB library — do NOT redefine vias here:
via_cls = substrate.signal_via[layer]          # or: from jitxlib.jlcpcb.vias import JLCPCBVias
self.thermal_vias = [via_cls().at(x, y) for (x, y) in via_positions]   # list, not a string-keyed dict
for via in self.thermal_vias:
    self.GND += via
```

For the signal-topology cases, `PortAttachment(port_or_ports, attachment)` connects
a port (or a sequence of ports) to a placed **`Copper` | `Via` | `ControlPoint`**
at a fixed location:

```python
from jitx.net import PortAttachment
# Signal escape via at a fixed location, bound to its signal port:
self.attachments = [PortAttachment(self.serdes.TX.p, via_cls().at(x, y))]
```

**Vias are defined in the substrate**, not here (see `jitx-substrate-modeler`). This
skill *places* instances of them. Define a custom module-scope `Via`
subclass only with a fab-verified reason — e.g. the tented-unfilled thermal via in
the thermal-pad example, where JLCPCB charges nothing for tented vias inside a pad.

**Placement — place a direct descendant with `.at()`; use `Circuit.place()` sparingly.**

```python
self.led = LED().at(10.0, 5.0, rotate=90)          # x, y, rotate (deg), on=Side — default form
self.led_b = LED().at(10.0, 5.0, on=Side.Bottom)
self.subckt = MySub().at(floating=True)            # let the layout engine choose the position
# Circuit.place() — sparingly: records a DEFERRED placement request on the parent (the child's own
# transform won't reflect it until placement resolves) and force-floats a placed subcircuit. Reserve
# it for the one thing .at() can't express — placing relative to ANOTHER instance (for a
# layout-engine-chosen position, use .at(floating=True) as above):
self.x = MyChip()
self.place(self.x, (1.0, 0.0), relative_to=self.led)
```

`.at()` mutates the instance's own `transform`, so the placement is readable on the instance —
visible to introspection before the design is built. For a **direct descendant** placed in the
parent's frame, set the position with `.at()` (chained when you create it, `self.x = Comp().at(x,
y)`, or `self.x.at(x, y)` if it already exists) — **not** `self.place(self.x, (x, y))`.

When a placed component and the geometry attached to it share the **same local
frame** (pin the anchor with `.at(0, 0)`, give attachments offsets in that frame), they
move together under interactive placement. Use `.at(0, 0)` here, not `place()`: `place()` records a
deferred placement request (and force-floats a placed *subcircuit*), so the anchor is not pinned in
the parent frame alongside its copper. Store attachments/vias/lanes as **list or
dataclass attributes** — never `getattr(self, f"via_{i}")` (see Anti-string-hacking).

## Keepouts that shape pours

`KeepOut(shape, layers=LayerSet(...), pour=, via=, route=)` — **at least one** of
`pour` / `via` / `route` must be `True` or it is a no-op (the constructor warns):

- `pour=True` — keep pours out of this area
- `via=True` — block the auto-router from dropping vias here
- `route=True` — disallow auto-router traces here

A higher-`rank` `Pour` of the same shape fills back **over** a keepout — the pattern
for a deliberately-shaped local ground island (e.g. around an antenna) that the
board-wide pour is otherwise cleared from.

**Local vs global pours.** Board-wide return-path pours belong in the **top-level**
design (the convention in `jitx-circuit-builder`). The exception: a local
pour/keepout that must **track a placed sub-circuit** — like an antenna's ground
island — lives **inside that circuit** so it follows the circuit wherever it is
placed. To ring an arbitrary shape with fence vias (antipads, RF cavities, BGA
breakouts), see `jitx-substrate-modeler` "Fenced Pour Outlines".

## Layout-intent tags (object selection)

Use tags to **mark which physical objects** get special layout treatment; the
**rule** that defines the treatment (width, clearance, thermal relief, fence vias,
routing structure) is declared in the substrate or top-level design via
`design_constraint(...)` — see `jitx-substrate-modeler`.

Apply with `MyLayoutTag().assign(obj)` or `Tags(tag_a, tag_b).assign(obj1, obj2)` —
always a `Tag` *subclass you define*, never the bare `Tag` base — inside a
design/circuit context. Supported object types: **`Net`, `TopologyNet`, `Copper`,
`Pour`, `Route`, `Component`, `Circuit`, `Landpattern`, `Pad`, `Via`,
`ControlPoint`** — note `OverlappableCopper` is *not* taggable.

Tagging a **container tags the copper objects inside it** — tag a `Landpattern`
and every pad in it inherits the tag; same for a `Component` or `Circuit`. Tag
`self` in a class `__init__` to tag *all instances* of that class. Assignment
outside a design-relevant context emits a `RuntimeWarning` and has no effect, and
assigning a `BuiltinTag` raises `TypeError` (builtins are rule conditions only —
see `jitx-substrate-modeler`).

Common layout-intent tag roles (define the `Tag` subclasses in your design):

- **Fanout / escape** (`PinFanoutTag`, `PowerPinFanoutTag`, `BootstrapFanoutTag`) —
  local neckdown/escape for fine-pitch package pins, applied to short route segments
  or individual pads, overriding the board-wide width/clearance for the escape.
- **`FanoutPourKeepoutTag`** — ask pours to stay back from dense local fanout copper.
- **`DirectConnectTag`** — solid pour connection (no thermal-relief spokes) for
  high-current/high-dissipation pads; tag the component to tag all its pads.

The **"route two pads, then tag the route"** workflow — create a code-based route and
mark it for the escape rule:

```python
# Route a pin to its escape destination (both ports on the SAME net), then tag the segment:
r = Route(self.u1.SCL, self.header.SCL, layer=0)
self.routes = [r]                          # store on self so the structural walk sees it
Tags(PinFanoutTag()).assign(r)             # Route is a supported tag target
```

`Route` and the control-point types are detailed in
`references/control-points.md`.

## Control points & code-based routes (ADVANCED)

Stable as of **JITX 4.2.0**. The module is **`jitx.controlpoint`** (the three
classes are also re-exported from top-level `jitx`). The classes were **renamed in
4.2.0** — pre-release alphas called them `SingleControl` / `InsertionControl` /
`PairControl`; those names no longer import.

- `Route(source, destination, layer, sketch=None)` — a code-based route between two
  `Port`/`Pad`/`Via` endpoints (not directional); `sketch` is an optional list of
  points hinting the routing engine. No per-route width/clearance overrides — tag
  the route and write a `design_constraint(...)` rule instead.
- `RoutePoint(layer=..., shape=None, bundle=Port)` — the **single-ended** control
  point; its `.port` is the routable endpoint.
- `PairInsertion(layer=..., bundle=DiffPair)` — differential-pair **insertion**
  point (uncoupled legs on one side via `.uncoupled.{n,p}`, coupled pair on the
  other via `.coupled`); `PairPoint(layer=..., bundle=DiffPair)` — joins two
  coupled segments via `.pair`. Both are placed with `.at(point, rotate=)` and
  wired to ports via `PortAttachment([n, p], control)` — port **order sets
  chirality** for `PairInsertion`.

Full pattern, chirality rules + the real BGA escape/deskew example:
`references/control-points.md`.

## Anti-string-hacking

Geometry-heavy layout code tempts you into building a parallel string-keyed model
(`vias[f"r{r}c{c}"] = ...`) and walking it to emit JITX calls. Don't. Construct the
JITX objects directly; batch parameters with a `@dataclass(frozen=True)` or a plain
`list`; key dicts by `Port`/structural objects, never by an assembled string. If the
only key you have is a runtime-built string, the structural object you need is
missing. See the base `jitx` skill's `references/architectural-patterns.md`, and run
`jitx-code-review` as a self-critique pass on layout code.

## Verification

```bash
pyright path/to/layout.py        # verify imports/signatures against the installed package
ruff format path/to/layout.py
```

Then build-test with a `SampleDesign` harness (see `jitx-circuit-builder`
"Verification Process"); sequence builds — don't parallelize against the same
design. Validate shapely outputs (non-empty `Polygon`/`MultiPolygon`) before they
reach a fab feature.

## API Reference

Complete class definitions and parameters: [JITX Documentation](https://docs.jitx.com).
Worked examples: `references/layout-examples.md` (thermal-pad CSG, antenna) and
`references/control-points.md` (Route / control points).

