# Solid modifiers and edge selection

[Start here](https://pegmatite.dev/guide/start.md). Fetch this topic when you transform, hollow, slice, round or bevel a solid, or need to select particular edges.

## Pipeline

Modifiers follow a solid after `|` and run left to right; the whole pipeline stays one solid for placement and booleans. A swept solid's pipeline starts with its profile and its [2D operations](https://pegmatite.dev/guide/shape-operations.md). `rotate`, `scale` and `flip` transform; `shell`, `cut`, `fillet` and `chamfer` reshape. All run before placement, which reads the resulting bounding box. After a transform the solid is normalized again: centered on X and Y, base at Z 0, so no pivot is needed.

Order matters: coordinate selectors read the frame left by the step before them.

```pegm
cube 20 | fillet 2 at parallel z | rotate 45 about x
```

```pegm
/* Expected warning: the edge selector matches nothing. */
cube 20 | rotate 45 about x | fillet 2 at parallel z
```

After the turn no edge is parallel to Z, so the second script warns and leaves the cube unrounded. Select edges in the solid's own frame and rotate last. Face references follow the solid through a turn instead; see [placement](https://pegmatite.dev/guide/placement.md).

## Transforms

### rotate

**Syntax:** **`| rotate`** _angle_ **`about`** _axis_

Degrees, counter-clockwise seen from the positive end of `x`, `y` or `z` of the solid's frame. One axis per step; steps apply in the order written. A flat side may name the axis by its normal.

```pegm
cube 10 | rotate 90 about x | rotate 45 about z
@right block 20 10 5 | rotate 90 about #front
```

A turned solid is placed by its turned bounding box: `cube 10 | rotate 45 about z` is as wide as its diagonal. Faces follow the turn; build on `#bounds.top` for the level top of the box, see [placement](https://pegmatite.dev/guide/placement.md).

### scale

**Syntax:** **`| scale`** _factor_ [_y-factor_] [_z-factor_]

One factor scales every axis; three scale X, Y and Z apart. Use it where the size is not a plain argument: a macro call, a compound, an extrusion, a revolve. A factor of `0` is an error. A negative factor also reflects along that axis. Scaling after a fillet scales the built radius; scaling before leaves the later radius as written.

```pegm
path "M0,0 H10 V10 H0 Z" | revolve | scale 1 1 2
@right cube 20 | fillet 2 at parallel z | scale 2   // radius 4
@right cube 20 | scale 2 | fillet 2 at parallel z   // radius 2
```

### flip

**Syntax:** **`| flip`** _axis_

Reflects across the plane normal to the axis, keeping the size; same as `scale -1 1 1` for `x`. Twice returns the original. To keep the original and add its mirror image, use `mirror` from [patterns](https://pegmatite.dev/guide/patterns.md).

```pegm
path "M0,0 H20 L20,10 Z" | extrude 4 | flip x
```

## Reshaping

### shell

**Syntax:** **`| shell`** _thickness_ [**`open`** _face_ | **`[`**_face_ …**`]`**] [**`closed`**]

Hollows the solid into an open vessel: every face moves inward by the thickness, the bottom stays and the flat top face is removed. Sloped and curved walls stay parallel to the outside, and a neck narrower than the body closes in around the cavity. Works on fused compounds.

```pegm
{ cylinder 40 40 on #top cylinder 16 15 } | shell 2
```

- The opening is the part of the surface in the plane of the highest point, read where `shell` stands: after `rotate 90 about x` the former front opens.
- A solid with no flat top, such as a sphere or an obliquely turned cube, is an error; `cut` it first: `sphere 40 | cut 0 80% | shell 2`.
- A thickness that leaves no cavity is an error.

`open` names the faces to leave open instead, one or a list; the list opens them all in one shell. `open` and `closed` exclude each other. Faces are named as `on` names them, read where `shell` stands, so a slope `#f1` or an inner side can open. `closed` opens none and leaves a sealed hollow, to subtract from or build into later.

```pegm
cube 40 | shell 2 open [#top #front]
x 60 pyramid 4 50 40 | shell 2 open #f1
```

- A face must be one the solid really has; a sphere's `#top` or `#bounds.top` has no surface to open.
- An opening other than the top may need the slow general computation, which the CLI leaves out without `--slow`.

### cut

**Syntax:** **`| cut`** _lower-level_ _upper-level_

Keeps the slab between two levels on the solid's own Z axis. A level is a height above the base or a percentage of the height, `0%` the base, `100%` the top, measured on the solid as the step before left it.

```pegm
sphere 70 | cut 10 100% on #bottom cylinder 20 5
```

- The lower level comes first; the other order is an error. A pair that leaves nothing is an error; levels clear of the solid remove nothing.
- The result is a new solid with its base back at Z 0 and the faces of its new bounding box, so the cut face can be built on.

## Fillet and chamfer

**Syntax:** **`| fillet`** _radius_ [**`at`** _edge-selector_]

**Syntax:** **`| chamfer`** _distance_ [_distance_] [**`at`** _edge-selector_]

Without `at` every edge is treated. They work on blocks, cylinders, extrusions, tapers, polyhedra and fused compounds. A solid with no edge to treat, such as a sphere, is an error. A size of `0` warns and the modifier is skipped; a negative size is an error.

- A chamfer distance is measured along both faces beside the edge, whatever their angle. A fillet meets both faces tangentially; where three rounded edges meet the corner becomes a sphere.
- A fillet on a concave edge adds material and fills the inner corner of a compound.
- Later modifiers work on the treated result. An edge a fillet smoothed away can no longer be selected; the curved edges it leaves can.
- Face selectors need axis-aligned faces. A right-angle turn keeps them; an oblique one does not.

```pegm
/* Expected error: the selected face is not axis aligned. */
cube 20 | rotate 30 about x | chamfer 2 at #top
```

### Size limit

The cuts must not cross. Each face beside an edge limits it to the distance from the edge to the border facing it, shared between selected edges facing each other. On `block 20 20 5`, `#top` may take 5, `#top or #bottom` 2.5 each; after `chamfer 2 at #bottom` the top has 3 left. Exactly half is allowed, so the cuts meet:

```pegm
block 4 10 30 | fillet 2
```

Only borders running alongside an edge count; where a pointed face's sides close in at a slant, the cuts meet at a mitred corner.

### Two distances

`chamfer d1 d2` measures the first distance along one face and the second along the other. Each edge gives the first distance to the face that looks further up; on a tie, further to the front; then further left. `fillet` takes one radius.

```pegm
block 40 30 20
  | chamfer 3 1 at #top
  | chamfer 1 3 at #bottom
```

`#top` takes 3 across the top and 1 down the sides; on the bottom ring the walls look further up, so a matching cut swaps the numbers. On an upright edge the face nearer the front takes the first. A tangent run of edges keeps the first distance on one side all along. A slight turn before chamfering can change the order. Each face is checked for the distance landing on it: on `block 20 20 5`, `chamfer 4 1 at #top or #bottom` fits, `chamfer 4 2` does not.

## Edge selectors

A selector picks logical edges, such as a block edge or a cylinder rim, not drawing triangles. Coordinates and face names are the solid's own frame before placement: on `block 40 30 20` x runs −20..20, y −15..15, z 0..20; `#front` faces −Y.

```pegm
#top
#top and #front
parallel z
z > 10
nearest 0 0 10
tangent parallel z
```

| Selector                            | Selects                                                             |
| ----------------------------------- | ------------------------------------------------------------------- |
| `#face`                             | the edges around that face                                          |
| `#a and #b`                         | on adjacent faces, the edge they share                              |
| `parallel x\|y\|z`                  | straight edges along the axis; four on a block                      |
| `x\|y\|z` `< <= > >= == !=` _value_ | edges lying **wholly** in the range; the value may be an expression |
| `nearest` _x_ _y_ _z_               | the edge closest to that place                                      |
| `tangent` _selector_                | the selection grown along edges that carry it on smoothly           |
| `not`, `and`, `or`, `( )`           | combine; `not` and `tangent` bind tightest, then `and`, then `or`   |

- A coordinate test needs every point of the edge to pass: on `block 40 30 20`, `z > 10` takes the top ring, not the uprights. A small tolerance makes `z == 20` pick a face's edges.
- On a compound a face name can match more edges, since several faces point the same way.
- A cylinder has two edges, its rims. `#top`, `#bottom`, `z == 30` and `nearest` pick one; `parallel` matches none, `x > 0` matches none because a rim spans both signs, and its side faces are rejected.
- **A selector that matches nothing warns, and the modifier is skipped**; the rest builds. `#top and #bottom` is such a selector.

```pegm
block 40 30 20
  | chamfer 5 at #top or parallel z            // top ring and uprights
  | chamfer 1 at #right and not parallel z     // two horizontal edges of the right face
  | chamfer 1 at (z == 0 or z == 20) and x > 0 // both rings, right side only
```

Each operand is decided over all edges before combining. So `z < 10 or z >= 10` leaves out an edge spanning 10, `not z < 10` keeps it, and `#bottom and nearest 0 0 20` is empty unless the nearest edge lies on the bottom.

### nearest

Picks the edge whose closest point lies nearest a place, for edges no coordinate separates, such as a bore rim inside an outline rim. The place may lie anywhere and needs no accuracy. An edge carries on through vertices where it runs straight on, so a bore rim is one edge and a block has twelve. A place equally close to several edges selects them all: the middle of a cube's top selects its four top edges. It always matches something; a place meant for another edge silently treats the one it lands on.

```pegm
{
  path "M0,0 H40 V30 H0 Z" | extrude 10
  -cylinder 13 10
} | chamfer 2 at nearest 0 0 10
```

### tangent

Grows each selected edge along the edges that continue it without a corner, until the run turns a corner or closes. Use it after a fillet, whose curved edges run along no axis. An edge carries another on when they meet end to end, share a face, bend the same way and turn at most 15 degrees; a convex edge never continues into a concave one. Where every selected edge ends in a corner, it adds nothing: on a block `tangent parallel z` is `parallel z`. It applies to the whole operand after it: `tangent #top and z > 10` grows before `and` filters, `tangent (#top and z > 10)` grows only what both keep.

```pegm
{
  block 4 8 27 | fillet 2 at parallel x
  z top:top -8 block 4 24 8 | fillet 2 at parallel x
} | fillet 1 at tangent parallel z
```

Without `tangent`, `parallel z` would round only the straight uprights and leave a lip at every arc.
