# Pegmatite Script — complete public guide > Pegmatite Script (.pegm) builds 3D models from solids placed against one another. Use the editor or npm CLI to check models, inspect previews and export 3MF or STEP. This optional download contains every topic from https://pegmatite.dev/guide. For smaller context, start with https://pegmatite.dev/guide/start.md and fetch only needed topics. All guide topics linked below are already included here. Links identify their standalone sources. Examples in pegm fences are Pegmatite Script; each starts fresh unless marked otherwise. --- # Pegmatite Script Guide Pegmatite Script (`.pegm`) builds 3D models from solids placed against other solids and their faces. This guide alone covers common printed parts: plates, boxes, brackets, knobs, hooks. The [topic index](https://pegmatite.dev/guide/start.md#topics) at the end says what every further topic adds; fetch one only when a task needs it. ## How a script works - Z is up, X right, Y back; the front is −Y. Lengths are millimeters, angles degrees. - A script is a sequence of solids. Each solid is built centered on X and Y with its base at Z 0, then placed against the previous volume: **on top of it by default**. - Build the way a part is assembled: a base, then each feature attached to the volume or face it belongs to. Prefer anchors, faces and alignment to calculated coordinates. - Comments run from `//` to the end of the line or inside `/* … */`. Whitespace separates words; indentation means nothing. Keep prefixes attached: `$width`, `%boss`, `@top`, `#front`, `!color`, `base:`. - Numbers are `10`, `0.5` or `-2.5`; `.5` and `2pi` are errors. Arithmetic needs parentheses, `($width / 2)`; a sign alone needs none, `-$width`. Functions: `sin cos tan sqrt abs min max round`, and the constant `pi`. Strings use double quotes. ## Variables ```pegm set $width 40 set $wall 2 block $width 30 ($wall * 3) ``` `set $name value` before use. Keep design dimensions as variables at the top and derive the rest in parentheses. ## Solids | Command | Makes | | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | | **`cube`** _size_ | cube | | **`block`** _width_ _depth_ _height_ | box along X, Y and Z | | **`cylinder`** _diameter_ _height_ | upright cylinder | | **`sphere`** _diameter_ | sphere | | **`tube`** _outer-diameter_ _inner-diameter_ _height_ | hollow cylinder | | **`prism`** _sides_ [**`diameter`** \| **`edge`** \| **`across`**] _size_ _height_ | regular prism, a flat facing front; `prism 6 across 13 7` is a nut pocket | | **`torus`** _outer-diameter_ _inner-diameter_ | ring lying flat | ## Placement ```pegm block 40 30 10 @right cube 10 // beside the block, touching its right side @right 5 cube 10 // beside the previous cube, with a 5 mm gap ``` Anchors `@left @right @front @back @top @bottom` name the side of the previous volume the new one goes to, with an optional gap. `@center` places it centered inside. An axis setter aligns and offsets on one axis, independently of the anchor: | Setter | Points | | ---------------------------- | ------------------------- | | **`x`** [_align_] [_offset_] | `left`, `center`, `right` | | **`y`** [_align_] [_offset_] | `front`, `center`, `back` | | **`z`** [_align_] [_offset_] | `bottom`, `center`, `top` | - `x right` makes the right sides flush. An offset runs along the positive axis whatever it follows: `x left 4` is 4 mm inward from the left edge, `x right 4` 4 mm outward past the right edge. - **An alignment lines up edges, not centers.** To place a solid's center, name both points as `reference:volume`: `x left:center 8` puts the center 8 mm in from the left edge, `x right:center -8` 8 mm in from the right edge; the same with `y front:center` and `y back:center`. This is how a dimensioned hole position is written. - A setter applies to the next solid only and stands directly before it. `!` makes it persistent, `!@right 2` builds a row; `unset` clears it, `!x unset`. `color` takes an HTML color name or a hex value such as `#ff0000` and follows the same rules. ## Faces ```pegm block 50 50 10 on #top x left 5 y front 5 cube 10 // on the top face, near its front-left corner on #front x right -4 cylinder 6 4 // standing out of the front face ``` - **`on`** _reference_ builds the next solid against a face of the previous volume, `on #top`, `#bottom`, `#left`, `#right`, `#front`, `#back`, or of a labelled one, `on base #front`. The solid stands on that face and grows outward, centered unless aligned with `x` and `y`. - On a face, `x` and `y` are the face's own axes as seen from outside: `x left` and `x right` are its left and right edges, `y front` and `y back` its lower and upper edges. - The first solid on a face takes no side anchor; align it with `x` and `y`. Solids after it follow each other as usual. - Faces are the faces of the solid's **bounding box**. On an L-shape a feature aligned on a face can land over empty space: build such parts from labelled blocks and attach each feature to the block it belongs to. ## Lists and labels ```pegm base: block 60 40 5 [ on #top x left 6 cylinder 8 20 on #top x right -6 cylinder 8 20 ] @right cube 10 // still placed against the base block on base #front cylinder 4 6 // a label reaches the base from anywhere ``` Every item of `[ … ]` starts from the volume before the bracket, and after `]` that volume is the reference again. A nested list builds on its item. A label, `base:`, names a volume so `on base #face` and the measure `base.width` can come back to it. ## Holes and cuts ```pegm block 60 20 10 on #top x left:center 10 -cylinder 5 10 // a 5 mm hole, 10 mm from the left end on #top x right:center -10 -cylinder 5 10 ``` - A sign before a solid combines it with the material built so far: `-` subtracts, `&` keeps the intersection, `^` keeps what does not overlap. A plain solid adds. - A signed solid placed on a face reaches **into** the material from that face. Make a through hole at least as long as the material is thick. - A cut applies to everything built before it in the same context; braces open a context of their own. To cut one piece only, put that piece and its cut in braces. A signed solid cannot be the first one in a script, braces or part. - After a cut, the cut's space is the reference for the next solid; return to the body with a list or a label. A subtraction that removes no material warns: the hole is in the wrong place. A hole in a side face selects the face and positions along its axes. A cylinder on a side face already points along the face's normal: ```pegm plate: block 20 4 60 on plate #front y front:center 12 -cylinder 4.5 4 // center 12 mm above the lower edge on plate #front y back:center -12 -cylinder 4.5 4 // center 12 mm below the upper edge ``` Holes set symmetrically a margin in from the edges are one pattern with `inset`. It spans the face, the outermost centers the margin in from its edges and the rest evenly between: ```pegm block 95 60 10 on #top grid 2 2 inset 9 -cylinder 5.5 10 // four corner holes, centers 9 mm from the edges ``` | Pattern | Holes | | --------------------------------------------------------- | -------------------------------------------- | | **`grid`** _columns_ _rows_ [**`inset`** _margin_] _body_ | columns × rows, corners included | | **`linear inset`** _margin_ _count_ _body_ | a row along X, the end centers the margin in | | **`mirror`** _axis_ [**`inset`** _margin_] _body_ | a pair, each center the margin from its edge | Without `inset`, the patterns space centers by a distance or gap; see [patterns](https://pegmatite.dev/guide/patterns.md). ## Compounds and parts ```pegm { block 40 30 4 on #top x left block 4 30 30 } | fillet 1 at parallel y ``` Braces fuse several solids into one, with its own placement and its own cuts, placed and modified as a whole. Use them to scope a cut and before a modifier that must apply to the whole group. `part "Lid"` divides the script into separately exported pieces, each with its own cuts; placement carries on across parts. ## Shapes and sweeps | Shape or sweep | Makes | | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | | **`circle`** _diameter_ | circle, centered on its origin | | **`rect`** _width_ _height_ | rectangle, centered on its origin | | **`polygon points [`**_x_ _y_**`]`**… | outline in mm, Y up; values may be variables or expressions | | **`path`** _SVG-path_ | SVG path string, numbers only; SVG Y points down | | **`text`** _text_ | lettering; `font-size` sets the capital height, 10 by default | | **`\| round`** _radius_ | round the corners of an outline, written before the sweep | | **`\| offset`** _distance_ | grow an outline, or shrink it by a negative distance | | _2d-shape_ **`\| extrude`** _height_ [**`wall`** _thickness_] [**`inside`** \| **`outside`**] | raise a shape; `wall` raises only its outline, an open box or frame | | _2d-shape_ **`\| revolve`** | turn a profile about Z: X is the radius, Y the height; draw it with X ≥ 0 | ```pegm set $w 40 set $h 20 polygon points [0 0] [$w 0] [$w 4] [4 4] [4 $h] [0 $h] | extrude 15 @right 10 { circle 30 -circle 10 } | extrude 4 ``` A hook or clip that is not made of blocks is drawn as its side outline with `polygon points` and extruded by its width; it then lies on its side, which prints well. A plain plate is better as `block … | fillet r at parallel z` than `rect | round | extrude`: the same look, exact in STEP. ## Modifiers Written after `|`, in order, before the solid is placed. | Modifier | Effect | | ------------------------------------------------------ | -------------------------------------------------- | | **`\| fillet`** _radius_ [**`at`** _edge-selector_] | round edges, all of them without `at` | | **`\| chamfer`** _distance_ [**`at`** _edge-selector_] | bevel edges | | **`\| shell`** _thickness_ | hollow out, keeping the bottom and opening the top | | **`\| rotate`** _angle_ **`about`** _axis_ | turn about `x`, `y` or `z`, then place | | **`\| flip`** _axis_ | mirror | | **`\| cut`** _lower-level_ _upper-level_ | keep the slab between two heights, `cut 0 50%` | Edge selectors after `at`: `#top` and the other faces select the edges around that face, `parallel z` the upright edges, `#top and #front` the one edge two faces share, `z > 5` edges above a height; combine with `and`, `or`, `not`. A selector that matches nothing warns and is skipped. ```pegm block 60 40 20 | fillet 4 at parallel z | shell 2 | fillet 0.8 at #top ``` A rounded open box: round the upright edges, hollow it with 2 mm walls, then soften the rim. Round and bevel before rotating, and shell before rounding the rim. ## Patterns | Pattern | Arrangement | | ------------------------------------------ | ---------------------------------------------------------------- | | **`repeat`** _count_ _body_ | continues the sequence: each copy is placed against the previous | | **`linear`** _distance_ _count_ _body_ | centers spaced along X | | **`circular`** _diameter_ _count_ _body_ | around a circle, each copy turned to face out | | **`grid`** _columns_ _rows_ [_gap_] _body_ | rows and columns | | **`mirror`** _axis_ [_gap_] _body_ | the body and its mirror image side by side | The body is one statement; use braces for several solids. `with index $i` before the body names the iteration number, from 0. The pattern leaves the box over all its copies as the reference for the next solid. ```pegm block 80 20 6 on #top linear 15 4 -cylinder 5 6 // 4 holes, centers 15 apart, along X ``` ```pegm cylinder 60 8 on #top circular 44 6 -cylinder 5 8 // 6 holes on a 44 mm circle ``` ## Macros and conditions ```pegm def %boss $d { cylinder $d 4 cylinder ($d / 2) 8 } block 40 30 20 [ on #top %boss 12 on #front %boss 8 ] ``` `def %name $parameter … { … }` defines a reusable compound; a one-statement body needs no braces. Call it by name with its arguments. Written upright in its own frame, a macro works on any face. `if $n == 1 statement` builds its statement when a comparison holds, `else statement` otherwise. ## Check, inspect and export With Node.js 22 or newer, check the script before rendering anything: ```sh npx --yes pegmatite --check --strict model.pegm ``` The report gives the model size and part count, then one row per **labelled** volume with its size, its X, Y and Z spans and its line. A hole's center is the middle of its spans. Label the pieces and holes whose position matters, compare their rows with the request, and fix the script before rendering. A cut that removes nothing is a warning, and strict mode fails on it. ```pegm base: block 40 20 4 on base #top x right:center -10 hole: -cylinder 4.5 4 ``` reports `hole: 4.5 × 4.5 × 4 x 7.75..12.25 …`: the hole is centered at x 10, 10 mm from the right edge at x 20. Render an image only when a question about shape or appearance remains: `-o model.png --view top`, or `front`, `iso`. Export by naming the output, `npx --yes pegmatite model.pegm -o model.3mf`; `.stl` and `.step` work the same way. ## Recipes Plate with rounded corners and four screw holes: ```pegm set $w 80 set $d 50 set $t 4 set $hole 4.5 set $inset 8 block $w $d $t | fillet 6 at parallel z on #top [ x left:center $inset y front:center $inset -cylinder $hole $t x right:center -$inset y front:center $inset -cylinder $hole $t x left:center $inset y back:center -$inset -cylinder $hole $t x right:center -$inset y back:center -$inset -cylinder $hole $t ] ``` Open box with rounded corners: ```pegm block 60 40 25 | fillet 5 at parallel z | shell 2 ``` L-bracket with a screw hole in each leg, each leg its own labelled block: ```pegm set $leg 40 set $t 4 set $width 20 base: block $leg $width $t on base #top x left upright: block $t $width ($leg - $t) on base #top x right:center -10 hole1: -cylinder 4.5 $t on upright #right y back:center -10 hole2: -cylinder 4.5 $t ``` Knob with a hex nut pocket and a bolt hole: ```pegm knob: cylinder 40 12 | fillet 2 at #top on knob #bottom -prism 6 across 13.2 6.5 on knob #bottom -cylinder 8.4 12 ``` Wall hook: a plate with two screw holes and a J-hook made from half a tube turned to lie across the width: ```pegm set $width 20 set $t 4 set $hook 30 plate: block $width $t 60 on plate #front y back:center -10 -cylinder 4.5 $t on plate #front y back:center -32 -cylinder 4.5 $t on plate @bottom y back:back tube $hook ($hook - 2 * $t) $width | rotate 90 about y | cut 0 50% ``` ## Common mistakes - Forgetting that each solid sits on the previous one: a second `block` lands on top of the first. Use a list, a label or an anchor. - A cut that reaches further than intended: everything earlier in the same context is cut. Scope it with braces. - Arithmetic without parentheses: `$w / 2` must be `($w / 2)`. - Placing a hole by its edge: `x left 8` puts the hole's edge 8 mm in; for its center write `x left:center 8`. The report shows where it landed. - Expecting `path` to take variables: use `polygon points` for sized profiles. - A `-` on the first solid of a script, braces or part. - Rounding the rim before `shell`: shell first, then fillet the top edges. ## Formatting One solid per line with its setters and modifiers. Indent the contents of `[ … ]` and `{ … }` by two spaces; the closing bracket aligns with its opening statement. ## Topics Each topic is the complete reference for its area; its row lists what it holds. | Topic | Contents | | ----------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [Syntax, settings and scope](https://pegmatite.dev/guide/syntax.md) | name and string rules, escapes, percentages, statement forms; `!setter`, `unset`, the reach of a setting through lists, braces, macros and parts; `color` names and hex, export by color | | [Placement, faces and frames](https://pegmatite.dev/guide/placement.md) | `@anchor [gap]`, `@center`; `x\|y\|z point[:point] [offset]`, `0% 50% 100%`; `on [label] #face`, `on root`, `on [#a #b]`; face axes per face; build planes; `#bounds.top`; face names after `rotate`; labels on compounds; list and compound isolation | | [Booleans, slots and parts](https://pegmatite.dev/guide/booleans.md) | `- & ^` in full, which end meets, boolean scope, the slot as next reference, `slot solid`, zero-sized volumes; `part "Name"`: own cuts, duplicate names, empty parts, export objects | | [Expressions, functions and measures](https://pegmatite.dev/guide/expressions.md) | `set`, variable scope, operators, `and or not`; `sin cos tan asin acos atan atan2 sqrt exp log abs min max round floor ceil lerp curve`, `pi`; `label.width\|depth\|height`; `width(shape)`, `height(shape)` | | [Solids reference](https://pegmatite.dev/guide/solids.md) | `cube block cylinder sphere cone tube torus`; `prism\|pyramid n [diameter\|edge\|across] size height`; `starprism spikes d1 d2 height`; `tetrahedron octahedron dodecahedron icosahedron [edge\|diameter\|across] size`; `mesh vertices [x y z]… faces [i j k]…`; numbered faces `#f1` | | [Solid modifiers and edge selection](https://pegmatite.dev/guide/modifiers.md) | `rotate angle about axis`, `scale f\|x y z`, `flip axis`, `shell t [open\|closed]`, `cut low high`; `fillet r [at sel]`, `chamfer d1 [d2] [at sel]`; selectors `#face`, `#a and #b`, `parallel axis`, `x\|y\|z op value`, `nearest x y z`, `tangent sel`, `and or not ( )` | | [Macros and conditionals](https://pegmatite.dev/guide/macros.md) | `def %name $p … { … }`, braceless bodies, shape macros, define before use, variables fixed at definition, recursion; `if test stmt [else stmt]` | | [Patterns](https://pegmatite.dev/guide/patterns.md) | `repeat n`, `linear d n`, `circular d n [angle a] [start a]`, `linear inset m n`, `grid cols rows [gap\|inset m [my]]`, `mirror axis [gap\|inset m]`, all with `with index $i`; centers not gaps; what each pattern leaves; grid cells; two mirror planes | | [2D shapes](https://pegmatite.dev/guide/shapes.md) | `circle d`, `rect w h`, `ellipse dx dy`, `star n d1 d2`, `polygon points [x y]…` or `polygon n [diameter\|edge\|across] size`, `path "svg"`; faces as shapes `#top`, `label #top` | | [2D operations and groups](https://pegmatite.dev/guide/shape-operations.md) | `offset d [round]`, `round r`, `fill`, `rotate deg`, `flip axis`, `scale f [fy]`; `{ shape … }` groups with `- & ^`, placing a shape against the previous one | | [Extrude and revolve](https://pegmatite.dev/guide/extrude.md) | `extrude h [wall t [inside\|outside]] [twist deg] [size f] [apex [x y [z]] \| to shape]`; `revolve [d] [angle a] [start a] [about axis] [shrink]` | | [Helix, sweep and varying profiles](https://pegmatite.dev/guide/sweeps.md) | `helix d [d2] [pitch p] [turns n \| height h] [start a] [about axis] [shrink]`; `sweep along trajectory [about axis] [round]`; varying profiles `%macro args \| sweep`, `% $t shape \| sweep`, `curve()` | | [Text and fonts](https://pegmatite.dev/guide/text.md) | `text "…"`, `font-size s`, `font family [weight]`, Google Fonts and font files, lettering as cutter or relief | | [Tilings](https://pegmatite.dev/guide/tilings.md) | `honeycomb\|squares\|triangles d strut [angle a] [rim w] in shape`, `stripes w strut …`, `voronoi strut cells n [seed s] [relax p] [rim w] in shape` or `voronoi strut points [x y]… …` | | [CLI](https://pegmatite.dev/guide/cli.md) | `-o --check --strict --slow --quiet --view --size`, stdin; reading the report, spans as placement checks; previews; 3MF, STL and STEP; fonts from disk; exit codes | --- # Syntax, settings and scope [Start here](https://pegmatite.dev/guide/start.md). Fetch this topic for exact rules on names, numbers, strings and statements, persistent settings and `unset`, how far a setting reaches, and colors. ## Values and names - Numbers are integers or decimals with an optional `-`: `10`, `0.5`, `-2.5`. `.5`, `1.`, `+1` and `1e3` are not numbers. `2pi` is an error; write `(2 * pi)`. - A numeric argument also takes a variable, a parenthesized [expression](https://pegmatite.dev/guide/expressions.md), a function call or a measure: `$size`, `($size / 2)`, `sqrt($area)`, `base.width`. A `-` against any of them negates it: `-$size`, `-($a + 1)`, `-base.width`. - Percentages, in alignments and `cut`, are a literal number followed directly by `%`: `25%`. A variable or expression cannot take `%`. - Strings are in double quotes and may span lines. Escapes: `\n` line break, `\"` double quote, `\\` backslash, `\u{…}` a hex code point such as `\u{B0}` for `°`. Any other escape is an error, so a literal backslash is `\\`. Strings serve `text`, `path` (with its own limits, see [shapes](https://pegmatite.dev/guide/shapes.md)), `font` and `set`. - Names are case-sensitive, start with a letter and continue with letters, digits or underscores; letters may be Unicode. Variables carry `$`, macros `%`, labels end in `:`, so no word is reserved: `cube: cube 10` labels a cube `cube`. Only `root`, which every context already has, and `pi` cannot be labels. - A word against `(` is a function call, but command and statement words never are: `repeat(` is no call. - Block comments do not nest: the first `*/` closes the comment. ## Statements A script is a sequence of statements, with [macro definitions](https://pegmatite.dev/guide/macros.md) and [parts](https://pegmatite.dev/guide/booleans.md) among them. | Statement | Example | Meaning | | ------------------- | ------------------------- | -------------------------------------------- | | Qualified solid | `color red @left cube 10` | one solid with everything qualifying it | | Persistent setter | `!color red` | a setting for every following solid | | List | `[ … ]` | each item built from the same reference | | Reference | `on #front`, `on base` | what the next solid is built against | | Pattern | `repeat 4 cube 10` | the body built several times | | Conditional | `if $ears == 1 %ears` | the body built only when the test holds | | Variable assignment | `set $size 10` | binds a value | | Sketch | `circle 10` | a 2D outline drawn without building material | **Syntax:** **`set`** _variable_ _value_ A qualified solid is one-shot setters with an optional label among them, an optional boolean sign, the solid and its `|` pipeline. Line breaks do not split it. A one-shot setter belongs to the solid or pattern after it, and may stand before the `on` leading into it: `color red on #front cube 10` is `on #front color red cube 10`. A setter standing alone, or separated from its solid by another statement, applies to nothing and warns: ```pegm /* Expected warning: the color setter has no solid. */ color red set $size 10 /* warning: "color red" applies to no solid */ cube $size ``` Before `repeat`, one-shot setters go to the first solid it builds. Before the other [patterns](https://pegmatite.dev/guide/patterns.md), placement setters position the whole pattern and appearance setters reach its first solid. ## Setters **Syntax:** **`x`** [_align_] [_offset_] **Syntax:** **`y`** [_align_] [_offset_] **Syntax:** **`z`** [_align_] [_offset_] **Syntax:** **`color`** _color_ **Syntax:** **`font-size`** _size_ **Syntax:** **`font`** _family_ [_weight_] Anchors with their gap are setters too. Alignments are in [placement](https://pegmatite.dev/guide/placement.md), fonts in [text](https://pegmatite.dev/guide/text.md). ### Persistent settings `!` attached to a setter, `!color red`, `!x 10`, `!@right 10`, applies it to every following solid until changed or its scope ends; `! color red` is an error. A persistent setter is a statement of its own, so it goes before any one-shot setter of the next solid. A one-shot setter overrides it for one solid: ```pegm !color red cube 10 color blue cube 10 /* red, blue, red */ cube 10 ``` The three axes are separate setters, so one overrides only its own axis. Naming an axis with `0` cancels its persistent offset for one solid and keeps its alignment: ```pegm !@right !y 10 block 10 10 10 y 0 block 10 10 20 /* centered on y again */ block 10 10 30 /* 10 back of center once more */ ``` ### Unset `unset` clears a setting. `!y unset` clears the persistent one for all following solids; `y unset` clears it for the next solid only, after which it resumes. `y 0` changes only the offset; `y unset` clears alignment and offset, so the anchor or the default applies. On an anchor, `unset` clears the gap and keeps the direction: `!@right unset` closes up a row spaced by `!@right 10`. `color unset` restores the default color for one solid, `!color unset` clears the persistent color. ```pegm !@right !y 10 cube 10 y unset cube 10 /* centered; the others 10 back */ cube 10 ``` ## Colors `color` takes an HTML named color or a hex `#rrggbb`: ```pegm color blue cube 10 color #ff0000 cube 20 color unset cube 5 ``` 3MF and STEP carry colors only when the model uses one. Then it exports as one object per color, the default included, for a slicer to give each its own filament. [Parts](https://pegmatite.dev/guide/booleans.md) are split by color within each part. A compound keeps the colors of the solids it fuses; where two overlap, the earlier solid keeps the shared volume. In STEP each body has one color, so a compound of several shows the color most of it is drawn in. ## Scope Settings come in two families: - **Appearance**, `color`, `font-size`, `font`: reach this context, the lists opened in it, and compound and macro bodies. - **Placement**, `x`, `y`, `z`, `@anchor`: reach this context and its lists. They position a finished body, not the solids inside it. A change inside a list or body stays there. Both towers are red, 4 apart; the gap does not separate a cylinder from its base: ```pegm def %tower $height { block 10 10 3 cylinder 5 $height } !color red !@right 4 %tower 14 %tower 14 ``` A body wanting a row of its own sets the pitch inside, `{ !@right 15 sphere 30 cube 20 }`. What each construct scopes: - **List `[ … ]`**: each item branches from the saved reference, `]` restores it. Settings are inherited, changes stay inside. Outer labels are visible, new ones stay inside. Variables and booleans are the surrounding context's. - **Compound `{ … }`**: own origin, placed as one volume. Appearance settings enter, placement settings do not; changes stay inside. Labels are isolated for `on`, but measures read outer ones; `root` is the compound's origin. Variables are inherited, assignments stay inside. Booleans affect only the body. - **Macro body**: as a compound, except labels are isolated for measures too, and variables are those set above the definition, with their values there, under the parameters. See [macros](https://pegmatite.dev/guide/macros.md). - **Pattern body**: `repeat` continues the sequence, the others start each iteration at its own point; the pattern leaves one combined slot. A single statement adds no scope and its assignments persist; a braced body is a compound. The index variable is restored afterwards. Booleans affect the surrounding context. - **Conditional**: stands for the branch it picks; with none picked, what follows is placed as if it were not written. A single statement adds no scope and the picked branch's assignments persist; a braced branch is a compound. - **`on`**: selects a volume or face and opens no scope. Axis settings stay in force in the selected frame; a persistent anchor skips the first solid against the face and resumes after it. An initial face sets the build plane, `on root #face` inside a compound. - **`part`**: starts a new boolean context and nothing else; settings, labels, variables and placement carry on as if it were not written. **Syntax:** **`on`** _reference_ **Syntax:** **`part`** _name_ A list changes placement, not which geometry a cut reaches; isolate a cut with braces, a macro, or a part. --- # Placement, faces and frames [Start here](https://pegmatite.dev/guide/start.md). Fetch this topic when a solid must land at an exact position, on a side face, in a turned frame, or against a volume built earlier. ## Reference and default direction A solid is placed against its **reference**: the previous volume of the current context, a face selected with `on`, or a labelled volume. The first solid starts at the context's origin. Placement uses a volume's **slot**, usually its bounding box; `set` and `!color` create none. Slots, cuts and what a cut leaves as reference are in [booleans](https://pegmatite.dev/guide/booleans.md). Without an anchor an ordinary solid follows local `@top`; after `-`, `&` or `^` it follows `@bottom`. ## Anchors | Axis | Towards the reference's | and its | | ---- | ----------------------- | -------- | | x | `@left` | `@right` | | y | `@front` | `@back` | | z | `@bottom` | `@top` | `@right 5` leaves a 5 mm gap. An anchor sets the alignment and gap of its one axis; the other axes keep their own settings. `@center` names no axis and centers the solid inside its reference on all three. An anchor is a setter: it precedes its solid and takes `!`. ```pegm !@right block 10 10 10 block 10 10 20 // a row along X ``` A persistent anchor also applies at the origin. It is ignored for the first solid built against a selected face and applies again to the solids after it. ## Alignment **Syntax:** **`x`** [_align_] [_offset_] **Syntax:** **`y`** [_align_] [_offset_] **Syntax:** **`z`** [_align_] [_offset_] Each axis holds two separate settings, an alignment and an offset, resolved in order: the anchor supplies its axis's alignment and its gap the offset; `x`, `y`, `z` override whichever of the two they name; the rest defaults to centered on x and y, base on z. `unset` clears both. Several setters share a line, `@top x right z top cube 10`; unnamed axes keep what is in force. An alignment pairs a point on the reference with a point on the volume, `reference:volume`. A point is a percentage of its box: | Setter | `0%` | `50%` | `100%` | | ------ | -------- | -------- | ------- | | `x` | `left` | `center` | `right` | | `y` | `front` | `center` | `back` | | `z` | `bottom` | `center` | `top` | One point applies to both boxes: `x right` is `x right:right`, `x 25%` is `x 25%:25%`. Names and percentages mix, `x right:center` equals `x 100%:50%`. ```pegm cube 20 @right x right:left cube 10 // beside it, touching @right x right:center cube 10 // straddling the previous cube's right face @right x 100%:25% cube 10 // a quarter of it inside that face ``` The offset is signed and runs along the positive axis whatever the alignment, so `x right 4` is 4 mm outward and `x left 4` 4 mm inward. An offset alone keeps the alignment, and settings from different places combine: ```pegm cube 10 !@right 5 cube 10 // gap 5 from the anchor x 10 cube 10 // gap 10: x overrides the offset, the anchor still aligns @top 5 z top cube 4 // alignment from z, offset from the anchor ``` The axes are those of the placement frame, not the world: after `on #front` of an unrotated block, `x right` points to world +X and `y back` to world +Z. ## Faces **Syntax:** **`on`** _reference_ | Reference | Builds the next solid against | | ------------------ | -------------------------------------------------------- | | `on #front` | that face of the previous volume | | `on #bounds.front` | that face of its axis-aligned bounding box | | `on base` | the volume labelled `base`, in the current frame | | `on base #front` | that face of the labelled volume | | `on root` | the origin of the current context, compound or macro | | `on root #front` | that [build plane](https://pegmatite.dev/guide/placement.md#build-planes) of the current context | A bare `on` is invalid. Faces are `#top #bottom #left #right #front #back`, the faces of the bounding box. `on` builds against any of the six, even where the box only touches the solid, so spheres stack on `on #top`. Everywhere else, in a [face shape](https://pegmatite.dev/guide/shapes.md), an edge selector or `rotate about`, a direction names a flat side the solid has in that plane, so a sphere has none and a dodecahedron only `#top` and `#bottom`; `#bounds.front` names the box's face. An unknown name is reported with the names the solid offers. Prisms and polyhedra add [numbered faces](https://pegmatite.dev/guide/solids.md) `#f1` …. A face is a frame: local X and Y run across it as seen from outside, local +Z points outward. The solid stands on it with its base and is centered unless aligned. For an unrotated parent: | Face | Local +X | Local +Y | Local +Z (outward) | | --------- | -------- | -------- | ------------------ | | `#top` | world +X | world +Y | world +Z | | `#bottom` | world +X | world −Y | world −Z | | `#front` | world +X | world +Z | world −Y | | `#back` | world −X | world +Z | world +Y | | `#left` | world −Y | world +Z | world −X | | `#right` | world +Y | world +Z | world +X | A turned parent turns its face frames. Following a face orients, an anchor does not: `@bottom cone 12 8` hangs an upright cone under a block, `on #bottom cone 12 8` points its tip down. A macro written upright therefore works on any face, and a solid after it follows its local +Z. ### On a face The first solid on a face takes no side anchor; align it with `x` and `y`. `@top` and `@bottom` are allowed, and a gap lifts it off the face, `on #top @top 5 cube 10`. Solids after the first follow each other, so an anchor there makes a row. ```pegm block 50 50 10 /* Expected error: an anchor cannot move the first solid across a selected face. */ on #top @right cube 10 ``` ```pegm block 50 50 10 on #top x left y front cube 10 @right 2 cube 10 ``` `@top` continues in the current frame; `on #top` takes the face's rotation. They agree on an unrotated box. ### Build planes Before any volume exists, `on #face` selects the plane the context grows from. `on #top` is the default ground plane; `on #front` stands a block up, its depth running upward and its height toward the viewer; `#back`, `#left`, `#right` and `#bottom` turn the other ways. The model still rests on the ground, and every solid of the context follows that plane. Once a volume exists, `on #top` names its face: ```pegm on #front cube 10 on #top cube 4 // grows toward world front, not world up ``` Inside a compound or macro body write `on root #front`; a bare face before the body's first solid is an error, since it would name the volume outside. `on root #front` also works at top level. ### Face identity A face name is an identity: it follows the solid through `rotate`, `scale` and `flip` and is never renamed. After `rotate 90 about z`, `#left` is the same facet, now facing −y; after `flip x`, `#left` is on the right. Numbered faces behave the same. An initial `on #front` makes a solid's `#front` face world down, `#back` up, `#top` front and `#bottom` back. `#bounds.` names a face of the axis-aligned box instead, for the six standard faces only: ```pegm cube 20 | rotate 30 about x [ on #top cube 10 // flat on the tilted face on #bounds.top cube 10 // level, on the box's roof ] ``` Selectors that measure, `parallel z` or `(x > 0)`, read coordinates after the pipeline. ## Labels `name:` labels the volume its statement builds. It stands among the solid's qualifiers or before the `on` leading into it: `base: @left cube 10`, `@left base: cube 10` and `base: on #front cube 10` are all valid, at most one per statement, and never before a face list. | Form | Meaning | | ---------------- | ---------------------------------------------------------- | | `on name` | build against the volume; an anchor works across it | | `on name #front` | build against that face of it | | `name.width` | [measure](https://pegmatite.dev/guide/expressions.md) its slot, also `depth`, `height` | `on name` retargets placement only; persistent settings stay. A label is visible after its solid, in its script or list and nested lists, and on compounds and macro calls as on any solid. `on` never reaches a label across compound or macro braces; a measure reads one from outside a compound, not into a macro body. ```pegm base: cube 20 [ on #top cube 4 ] on base @left cube 6 ``` ## Lists Each item of `[ … ]` starts from the reference in force at `[`, and `]` restores it with the settings and labels of that point. A list opens from what the statement before it left: a volume, a pattern's box, or a face named by `on`. With nothing before it, it opens from the context's plane, so a bare list's items share the origin. Items branch, sequences follow: a second `on #front` in a list names the parent's face, and two items with the same face and settings overlap. Persistent setters inside a list reach later items until `]`; one-shot setters do not cross items. A pattern's iterations are not items, so `repeat 4 cube 10` in a list still makes a row. A build plane chosen in an item reaches that item only. To continue from an item, open a nested list right after it: ```pegm block 40 40 10 [ on #top block 20 20 10 [ on #top cube 6 ] ] @right cube 10 // against the base block ``` `on` [_label_] **`[`**_face_ …**`]`** _statement_ writes one item per face. It is that list: each item starts from the reference at the `on`, and without a label the faces belong to the previous volume. The statement may carry its own `on` or be a list; `on box [ cube 2 ]` is `on box` followed by a list. ```pegm box: prism 6 edge 50 50 on box [#f2 #f3 #f4 #f5 #f6 #f7] -cylinder 10 3 ``` ## Compounds `{ … }` fuses its statements into one solid that takes setters and anchors like any solid and is the previous volume after `}`. Its faces come from the box over the slots that add material, including an empty [`slot`](https://pegmatite.dev/guide/booleans.md); the material keeps its place in the box. A `-` or `&` cutter adds nothing to the box, so a through hole leaves the compound the size of its solid. Macro calls follow the same rules. A color before the braces applies to the body; colors inside do not escape. ```pegm { cube 20 @center -cylinder 8 30 } @right cube 4 // beside a 20 mm compound ``` A compound has its own root context. A list closes inside the body, `on root` returns to the body's ground plane and `on` reaches no label outside; a measure does, `base: cube 20 { cylinder 4 base.height }`. `on root #front` first in the body builds the compound in its own orientation; to place the whole compound against a face write `on #front { … }`. ```pegm block 30 20 5 { on root #front block 30 20 5 // an upright panel on the block } ``` --- # Booleans, slots and parts [Start here](https://pegmatite.dev/guide/start.md). Fetch this topic when a script cuts, intersects or reserves space, or divides a model into exported parts. ## Signs | Sign | Operation | Keeps | | ---- | -------------------- | -------------------------------------- | | none | union | both, joined | | `-` | difference | what the operand does not cover | | `&` | intersection | only what the operand covers | | `^` | symmetric difference | what belongs to exactly one of the two | The sign may touch the solid or stand apart: `-cylinder 4 10` and `- cylinder 4 10` are the same. A sign against a value, `-$a`, negates the value. A compound or a macro call takes a sign too, so one operation handles several shapes: ```pegm def %hole $diameter { cylinder $diameter 10 } block 10 10 10 on #top -%hole 4 ``` A signed volume cannot be the first volume of its context: braces, a part or the script. ### `-` **Syntax:** **`-`** _solid_ Removes the operand. A `-` that misses every earlier volume or only touches one warns; the script still builds. ```pegm /* Expected warning: This negative volume removes no material */ block 30 30 20 x 40 -cylinder 14 20 ``` ### `&` **Syntax:** **`&`** _solid_ Keeps only what the operand covers, and discards every volume of the context it never reaches. With no overlap the model is empty. `-` and `^` also empty it when the operand covers or equals all material. ```pegm cube 80 &cylinder 40 100 ``` That leaves the cube's cylindrical core, full height. ### `^` **Syntax:** **`^`** _solid_ Removes the overlap and adds the part of the operand that reaches past the model. An operand entirely inside the model acts as `-`. ```pegm block 30 30 20 ^block 44 20 20 ``` The overhanging ends stay; the middle cancels. ## Placement of a signed volume A signed volume uses the same context, anchor and alignment as a plain one, except on the anchor's axis: **a plain volume starts where its reference ends, a signed one reaches back across it** and bites into the material. Explicit alignments and offsets can still move it out of the material. Default directions: `-` drills in from the top. `&` and `^` default to `@bottom`, so their bases are flush with the reference: ```pegm block 30 30 20 &cylinder 24 20 ``` A written direction overrides the default: `@top &cube 10` reaches down from the top. With `on`, the plane is the named face; with an anchor, the anchored face: `@top -cube 10` cuts a pocket where `@top cube 10` would rest a pin. Only placement changes, not shape: a signed volume is not reflected, so a tapered solid keeps its taper. Use `flip` ([modifiers](https://pegmatite.dev/guide/modifiers.md)) for the reflected shape, `mirror` ([patterns](https://pegmatite.dev/guide/patterns.md)) for a reflected copy. ### Which end meets Which end of the volume lands on the reference follows from the two signs. For an explicit `@top`: | previous | this one | `@top` stands for | result | | -------- | -------- | ----------------- | ------------------------------- | | plain | plain | `z 100%:0%` | they touch, end to end | | plain | `-` | `z 100%:100%` | the cutter reaches back into it | | `-` | `-` | `z 0%:0%` | the cutter bases align | | `-` | plain | `z 0%:0%` | the plug fills the hole's slot | Default directions are resolved separately; without a written direction, successive `-` volumes continue down into the model. ### The slot is the next reference Every volume, whatever its sign, is the reference for the next. A signed volume leaves its operand's slot, marked as subtractive; the next placement uses it, not the bounds of the remaining material. A row of pockets lays out like a row of pins: ```pegm cube 80 @top -cube 10 @right 10 -cube 10 ``` With `cube` for `-cube` this gives two pins at the same X and Y. A pattern over a signed volume steps the same way: ```pegm block 120 30 16 on #top !@right 6 repeat 5 -cylinder 8 8 ``` ## Boolean scope A signed volume combines with every earlier volume of its context, as though they were unioned first. A list does not isolate them: ```pegm !@right 5 cube 20 cube 20 on #top -block 70 4 10 ``` The groove crosses both cubes. Braces have their own context, so an operation inside never reaches out: ```pegm cube 20 { block 30 30 10 on #top -cube 12 } ``` The outer cube is untouched. A part is a context too. Scope `&` with braces: it discards everything of its context that it misses. `-` and `&` distribute over the union, so the model keeps its separate volumes and colors. `^` does not: the piece of the operand no volume covered is added once, as a volume of its own. ## Slots A volume has a **slot**, its box used for placement, and **material**. A sphere's slot is its bounding box, empty corners included. An ordinary prism's slot is the box around its faces, not its base's circumcircle. - A compound's slot encloses the slots of its adding volumes, empty slots and partial sweep bounds included. `-` and `&` volumes lead the next volume inside the body but claim no room. - Circular patterns, partial revolutions and helices under one turn use bounds mirrored about their sweep axis, so a partial ring keeps its axis; `shrink` uses the material's bounds instead. ### `slot` **Syntax:** **`slot`** _solid_ Builds and places a solid with its whole pipeline, keeping its bounds and faces but no material. It becomes the next reference. ```pegm slot block 40 40 60 on #front block 60 40 5 ``` It takes any solid, compound or macro call. The shape's own faces come with it, numbered sides included: ```pegm slot prism 6 100 20 on #f2 cube 10 ``` A modifier shapes the room: `slot block 10 20 30 | rotate 90 about x` is the turned block's room. Labels, anchors, alignment and `on` work as with a solid: ```pegm gap: slot block 60 40 20 block 60 40 5 on gap #back block 60 20 5 ``` A plate 20 above the ground on a wall at the back of the space beneath. `-slot` and the other signs are refused: a slot has no volume to combine. For a plain gap in a row, `@top 20` says the same as a 20 tall `slot block`; use `slot` when the space must be named, aligned against or built on. ### Zero-sized volumes A solid with a zero size, including an extrude's height or wall thickness, builds no material but is placed, passed through its pipeline and keeps its room like a slot: ```pegm set $lid 0 block 40 40 20 @top block 40 40 $lid @top cylinder 10 5 ``` The cylinder stands where it would on a lid of any thickness. The room of `cube 0` is a point, of `block 40 40 0` a flat rectangle; `cube 0 @right cube 5` puts the cube's left face on the point. - A sign treats it as empty: `cube 5 -cube 0` leaves the cube whole, `cube 5 &cube 0` leaves nothing, `cube 0 -cube 5` removes nothing. It still leaves its slot. - A transform such as `rotate` turns its room: `cylinder 10 0 | rotate 90 about x` stands as tall as it is wide. Other modifiers only remove material and are skipped. - It offers the six box faces and no numbered sides. - An `extrude` of height zero keeps the flat room of its outline, its wall or its `to` shape; `apex` has no height to close. A zero wall keeps the room of its outline, rising or tapering as the wall would. - A size within rounding of zero, `0.1 + 0.2 - 0.3`, counts as zero. A negative size is an error. - A literal zero, `cube 0`, warns. A zero computed from a variable or a macro argument does not. ## Parts **Syntax:** **`part`** _name_ Begins a part: the material built up to the next `part` belongs to it. ```pegm part "Box" box: block 60 40 30 | shell 2 part "Lid" on box #top block 60 40 3 on #bottom block 55 35 3 ``` - Placement, labels, persistent settings and variables carry on across parts; that is how the lid sits on the box. - A part is its own boolean context: a signed volume reaches only its own part, and a part cannot begin with one. - The name is a string, spaces allowed, `part "Left hinge"`. It names the export object and the report heading and is no reference: `on` cannot reach a part, use a label. Two parts may share a name and stay two parts. - Material before the first `part` is a part named `Unnamed`. A script without `part` is one part, and each separate body is its own export object. - `part` stands among the root statements, never inside braces, a list, an `if` branch or a macro body. ```pegm part "Box" block 40 40 10 part "Lid" on #top block 40 40 4 -cylinder 6 30 ``` The hole goes through the lid only. To cut both parts, cut in each, or call one macro in each. ```pegm /* Expected error: A negative volume needs a volume to combine with */ part "Box" block 40 40 10 part "Hole" on #top -cylinder 6 30 ``` A part that builds no material warns and is left out of the export: ```pegm /* Expected warning: part "Spacer" builds no material and is not exported */ part "Box" cube 20 part "Spacer" slot cube 10 ``` ### Report and export `--check` groups rows and diagnostics by part: a heading row with the part's name, material size, spans and line, then its labelled volumes. - 3MF: one object per part, named after it, so a slicer lists touching parts apart. Within a part, touching material fuses; separate bodies are one object of several pieces. - STEP: bodies named after their part, numbered when it has several. A model without parts names its bodies after the model. - STL drops part names. See [CLI](https://pegmatite.dev/guide/cli.md). --- # Expressions, functions and measures [Start here](https://pegmatite.dev/guide/start.md). Fetch this topic when a size is worked out: variable scope, operators, choosing a value, functions, reading back a labelled volume's or a shape's size, or what a prism's size is measured along. ## Variables **Syntax:** **`set`** _variable_ _value_ A variable holds a number or a double-quoted string. Numbers supply numeric arguments; strings supply `text` and `path`. Variables cannot supply colors or references. Types are checked at evaluation: a string used as a number is an error, and reassigning another type is allowed. `set` assigns immediately and affects only the statements after it. A persistent setting stores the value, not the variable: ```pegm set $spacing 10 !@right $spacing set $spacing 20 cube 10 cube 10 ``` The gap is 10. A variable used before it is set is an error: **Expected error:** a variable must be assigned before use. ```pegm /* Expected error: undefined variable $size. */ cube $size set $size 10 ``` This reports `Undefined variable: $size`. ### Scope A list opens no scope. A compound does: it inherits the outer variables, a `set` inside shadows the outer value, and the change ends at the closing brace: ```pegm set $size 20 { cube $size set $size 10 cube $size } cube $size ``` The cubes are 20, 10 and 20. ## Operators A numeric argument is a number, a variable, a call, a measure, `pi`, or a parenthesized expression; a compound expression always needs parentheses. A sign alone needs none and negates a variable, parentheses, a call, a measure or `pi`: `x -$inset`, `y -($inset * 2)`. The sign must touch its operand; `- $inset` is not an argument. A volume [signed for a difference](https://pegmatite.dev/guide/booleans.md) starts with a command, macro, brace or face, never a value, so `cube 10 -cube 4` is still a difference. | Operator | Meaning | | ------------------------------ | --------------------------- | | `**` | power, right to left | | `-` (unary) | negation | | `*` `/` `%` | multiply, divide, remainder | | `+` `-` | add, subtract | | `<` `<=` `>` `>=` `==` `!=` | comparison | | `not`, then `and`, then `or` | joining tests | | _test_ `?` _value_ `:` _value_ | choice | Tightest first; one row applies left to right. - `**` binds tighter than a leading minus and to the right: `-2 ** 2` is `-4`, `2 ** 3 ** 2` is `2 ** 9`. A square root is `** 0.5`. - `%` agrees with `floor`: `a` is `floor(a / b) * b + a % b`, so a remainder takes the divisor's sign, `-1 % 4` is `3`. Inside parentheses `%` is always the remainder; a percentage such as `25%` is a literal written outside any expression, in an alignment or a `cut` level. - Division or remainder by zero is an error, and so is a power with no real value, `(-8) ** 0.5`. ```pegm grid 4 3 2 with index $i cylinder 8 (4 + $i % 4 * 3 + floor($i / 4) * 6) ``` The column is `$i % 4`, the row `floor($i / 4)`. **Expected error:** a remainder needs a divisor other than zero. ```pegm /* Expected error: modulo by zero. */ cube (7 % 0) ``` ### Choosing a value _test_ `?` _value_ `:` _otherwise_ picks a value. A test is a comparison between two numbers, or comparisons joined by `and`, `or`, `not` and regrouped by parentheses. The choice binds loosest, so nothing inside needs parentheses, and a value may be another choice, read left to right: ```pegm set $n 7 cube ($n < 5 ? 4 : $n < 10 ? 8 : 12) /* 8 */ ``` Only the picked value is evaluated, and the right side of `and` or `or` only when it can change the answer, so the other may be one that could not be: ```pegm set $n 0 cube ($n == 0 or 10 / $n > 1 ? 5 : 2) /* 5 */ ``` Numbers closer than 0.000001 are equal: `(0.1 + 0.2 == 0.3 ? 1 : 2)` is `1`. A test is not a number; it stands only before `?` or after [`if`](https://pegmatite.dev/guide/macros.md). A string compares only by `==` and `!=`, equals only the same string, and never equals a number: ```pegm set $style "round" cylinder 10 ($style == "round" ? 20 : 10) ``` **Expected error:** a text has no order. ```pegm /* Expected error: a text can only be compared with == or !=. */ set $style "round" cube ($style < "square" ? 1 : 2) ``` ## Functions A call attaches `(` to the name, separates arguments with commas, returns a number, needs no extra parentheses as an argument, and nests. A name against `(` is always a call. Angles are degrees, given and returned. | Function | Value | | -------------------------------------------------------- | ---------------------------------------------------------- | | `sin(`_degrees_`)` `cos(`_degrees_`)` `tan(`_degrees_`)` | sine, cosine, tangent | | `asin(`_value_`)` `acos(`_value_`)` `atan(`_value_`)` | angle: `asin`, `atan` −90 to 90, `acos` 0 to 180 | | `atan2(`_y_`,` _x_`)` | angle of point x, y from +X, −180 to 180 | | `sqrt(`_value_`)` | square root | | `exp(`_value_`)` `log(`_value_`)` | e to the value; natural logarithm of a value above zero | | `abs(`_value_`)` | value without its sign | | `min(`_value_`,` …`)` `max(`_value_`,` …`)` | smallest or largest of one or more values | | `round(`_value_`)` | nearest whole number; a half rounds away from zero | | `floor(`_value_`)` `ceil(`_value_`)` | nearest whole number below or above, or the value if whole | | `lerp(`_from_`,` _to_`,` _t_`)` | _from_ at t 0, _to_ at 1, continuing past either end | | `curve(`_value_`,` _value_`,` …`,` _t_`)` | smooth curve through values spaced evenly from t 0 to t 1 | - A turn landing on a quarter is exact: `sin(180)` is `0`. - `curve` passes through each value, never overshoots between two, is level where values turn back, holds the end value outside 0 to 1, and with two values is `lerp`. An [extrude's `size`](https://pegmatite.dev/guide/extrude.md) list follows it. - `pi` is written bare like a number, so it cannot be a label. There is no `e`; write `exp(1)`. - Unknown functions, wrong argument counts and undefined results such as `sqrt(-1)`, `asin(2)`, `acos(2)`, `tan(90)` are errors. ```pegm set $w 14 block max($w, 10) (pi * 4) (40 * tan(30)) ``` **Expected error:** a negative number has no square root. ```pegm /* Expected error: sqrt(-1) has no value. */ cube sqrt(-1) ``` ## Measures **Syntax:** _label_**`.width`** | _label_**`.depth`** | _label_**`.height`** A measure reads the size along X, Y or Z of a labelled volume's [slot](https://pegmatite.dev/guide/booleans.md), and stands like a call: ```pegm base: block 30 20 5 on base #top cylinder base.depth (base.width / 3) ``` - It is measured in the frame the volume was placed in, in `block`'s order: `block base.width base.depth base.height` is the slot's size. A volume built on a face measures along its own axes; a rotated solid measures its silhouette. - The slot, not the material, is measured, since placement reads the slot. A partial revolve, circular pattern or helix measures its whole circle, or with `shrink` the arc it covers. A [`slot`](https://pegmatite.dev/guide/booleans.md) measures a solid without building it. A [zero-sized volume](https://pegmatite.dev/guide/booleans.md) measures 0 along its zero side. - A labelled macro call measures the part its body built. - The label must be built and visible where the measure is written; a solid cannot measure itself. Unlike `on`, a measure reads a label from outside a compound's braces, a braced pattern body included; a label of the same name inside wins. No outside label reaches a macro body; pass the size as an argument. ```pegm title: slot text "PEGMATITE" | extrude 1 @bottom block (title.width + 6) (title.depth + 6) 2 on #top text "PEGMATITE" | extrude 1 ``` **Expected error:** a measure needs its label built first. ```pegm /* Expected error: there is no volume labelled base yet. */ base: block 30 20 base.width ``` ## Measuring a shape **Syntax:** **`width(`**_2d-shape_ [**`,`** _y_]**`)`** | **`height(`**_2d-shape_ [**`,`** _x_]**`)`** `width` is how far a [2D shape](https://pegmatite.dev/guide/shapes.md) reaches along x, `height` along y. Given a y, `width` spans from the shape's leftmost to rightmost point at that y, holes and gaps included; `height` at an x likewise. The shape is written as a sweep profile, modifiers included, and ends at the comma. It is measured in its own frame: a circle is centred on 0, so `width(circle 20, 10)` is 0. A `path` stands upright as a sweep builds it, y up. A y the shape does not reach is an error. Measured along a sweep, a silhouette sizes a [varying profile](https://pegmatite.dev/guide/sweeps.md). ```pegm cube width(circle 20) /* 20 */ cube width(circle 20, 5) /* about 17.3 */ cube height(rect 30 10 | rotate 90) /* 30 */ ``` **Expected error:** a circle 20 across reaches only from y -10 to 10. ```pegm /* Expected error: width() at y 11 has no value. */ cube width(circle 20, 11) ``` ## Measuring a solid's size A word before the size of a prism, pyramid or platonic solid says what it is measured along: | Word | Measures | | ---------- | --------------------------------------------------------------- | | `diameter` | circle through the base corners, or sphere through the vertices | | `edge` | one edge, of the base for a prism or pyramid | | `across` | distance between two parallel flats | Left off, a prism or pyramid takes `diameter`, fitting the cylinder or cone of that size, and a platonic solid `edge`, as a cube does. The word changes only the measure, not what is measured: a pyramid's size is still its base. A square by `edge` or `across` has that side, so `prism 4 edge 20 10` fills `block 20 20 10`. A flat faces front, so a prism's depth is its `across`, and `across 17` is the hexagon a 17 spanner fits. A platonic solid stands on a face, so its `across` is its height: ```pegm !@right 6 prism 4 edge 20 10 prism 6 across 17 8 octahedron across 20 tetrahedron diameter 20 ``` Only an even polygon has a flat opposite every flat, and a tetrahedron has no face opposite a face, so neither takes `across`: **Expected error:** an odd prism has no distance across its flats. ```pegm /* Expected error: 5 sides leave no flat opposite a flat */ prism 5 across 20 10 ``` --- # Solids reference [Start here](https://pegmatite.dev/guide/start.md). Fetch this topic when a model needs a cone, pyramid, star, platonic solid or hand-written mesh, the rules of a primitive's sizes, or a solid's numbered faces `#f1`, `#f2`, …. Every dimension is zero or greater; a dimension of zero makes a [zero-sized volume](https://pegmatite.dev/guide/booleans.md). Width runs along X, depth along Y and height along Z in the solid's own frame. ## Basic solids **Syntax:** **`cube`** _size_ **Syntax:** **`block`** _width_ _depth_ _height_ **Syntax:** **`cylinder`** _diameter_ _height_ **Syntax:** **`sphere`** _diameter_ **Syntax:** **`cone`** _diameter_ _height_ A right circular cone standing on its base; the diameter is the base's. ```pegm cylinder 20 10 cone 20 30 ``` ### Tube and torus **Syntax:** **`tube`** _outer-diameter_ _inner-diameter_ _height_ An upright cylinder of the outer diameter with a bore of the inner one. The inner diameter must be greater than zero and no larger than the outer; equal diameters leave a wall of no thickness, a zero-sized volume. Both walls use the flats of a cylinder's wall, so a tube and a cylinder of one diameter meet facet to facet, without slivers: ```pegm tube 30 18 10 @top cylinder 30 5 ``` **Syntax:** **`torus`** _outer-diameter_ _inner-diameter_ A ring lying in the XY plane, measured by its overall diameter and its hole. The hole must be greater than zero and no larger than the outside; equal diameters leave no material. ```pegm torus 30 18 ``` ## Prisms and pyramids **Syntax:** **`prism`** _sides_ [**`diameter`** | **`edge`** | **`across`**] _size_ _height_ **Syntax:** **`pyramid`** _sides_ [**`diameter`** | **`edge`** | **`across`**] _size_ _height_ A regular prism with _sides_ upright faces, or a regular pyramid with _sides_ faces meeting at the apex, standing on its base. _sides_ is a whole number of 3 or more. - The size measures the base. By default it is the diameter of the base's circumcircle, the circle through its corners, so the solid fits inside the cylinder or cone of that diameter. A square base of diameter _d_ has a side of _d_ / √2. The word before the size changes the measure: see [measuring a solid's size](https://pegmatite.dev/guide/expressions.md). - The base is turned so that a **face**, not a corner, points front. A square prism or pyramid stands square with the axes like `block`, and its box faces run parallel to real faces. - Every upright face of a prism has a [numbered name](https://pegmatite.dev/guide/solids.md#numbered-faces), so a prism builds round something on flats. ```pegm block 20 20 6 @top pyramid 4 edge 20 20 ``` The pyramid's base covers the block it caps. ```pegm prism 6 40 20 on #f2 cube 6 ``` ### Star prism **Syntax:** **`starprism`** _spikes_ _outer-diameter_ _inner-diameter_ _height_ A star raised into a solid on its base. All four numbers are required. The spikes reach the outer circle and the notches between them the inner one; the inner diameter must be smaller than the outer, and _spikes_ is a whole number of 3 or more. As the diameters approach each other the star flattens towards a prism of _spikes_ × 2 sides. The first spike points along **+X**, where a [circular pattern](https://pegmatite.dev/guide/patterns.md) puts its first iteration and a [revolve](https://pegmatite.dev/guide/extrude.md) begins its turn, so a ring written in the same frame lands one solid on each spike: ```pegm starprism 8 60 30 6 on #top circular 60 8 @top cylinder 6 6 ``` ## Platonic solids **Syntax:** **`tetrahedron`** [**`diameter`** | **`edge`**] _size_ **Syntax:** **`octahedron`** [**`diameter`** | **`edge`** | **`across`**] _size_ **Syntax:** **`dodecahedron`** [**`diameter`** | **`edge`** | **`across`**] _size_ **Syntax:** **`icosahedron`** [**`diameter`** | **`edge`** | **`across`**] _size_ - The size is the edge length by default, the measure `cube`, the fifth platonic solid, takes. The same edge makes very different sizes: a dodecahedron of edge 10 is far bigger than a tetrahedron of edge 10. - `diameter` measures the sphere through the vertices, as `sphere` does. Only the vertices touch it, so the solid is a little smaller than the sphere, the closer the more faces it has. - `across` measures between opposite faces, which is the height the solid stands. The tetrahedron has no face opposite a face and does not take `across`. See [measuring a solid's size](https://pegmatite.dev/guide/expressions.md). - Each stands on one of its faces, so it rests flat on the ground and on any face it is placed on. Every face except the tetrahedron's has a parallel one on top, so the others stack directly. ```pegm !@right 4 sphere 20 icosahedron diameter 20 dodecahedron diameter 20 ``` ```pegm icosahedron 12 on #top icosahedron 12 ``` ## Mesh **Syntax:** **`mesh vertices [`**_x_ _y_ _z_**`]`**… **`faces [`**_i_ _j_ _k_ …**`]`**… A solid written out by hand: its corners after `vertices`, then its faces after `faces`, each the list of corners it runs through. A corner is named by its place in the list, **counted from 0**. This is a tetrahedron with three corners on the axes: ```pegm mesh vertices [0 0 0] [10 0 0] [0 10 0] [0 0 10] faces [0 1 2] [0 1 3] [0 2 3] [1 2 3] ``` - A face may have more than three corners if it is **flat** and they are listed in order around it, so a box needs six faces, not twelve triangles. - The faces must close the solid: **every edge is shared by exactly two faces**. Which way round a face is listed does not matter; the faces are turned to agree and to face out. - A face that is not flat, an edge not shared by exactly two faces, or a corner that does not exist is an error pointing at the face. - Placement normalizes the mesh's box to the X/Y centre and Z 0, so translating every vertex does not move the placed solid. - Coordinates and indices accept variables and [expressions](https://pegmatite.dev/guide/expressions.md). ```pegm mesh vertices [0 0 0] [20 0 0] [20 10 0] [0 10 0] [0 0 10] [20 0 10] [20 10 10] [0 10 10] faces [4 5 6 7] [0 1 5 4] [1 2 6 5] [2 3 7 6] [3 0 4 7] [0 1 2 3] ``` A mesh numbers its faces `#f1`, `#f2`, … **in the order they are written**, not in the order of other solids' [numbered faces](https://pegmatite.dev/guide/solids.md#numbered-faces). The six direction names cover its box as usual. Below, `#f1` is the front slope and the square base `#f5` is also `#bottom`: ```pegm set $h 30 mesh vertices [-10 -10 0] [10 -10 0] [10 10 0] [-10 10 0] [0 0 $h] faces [0 1 4] [1 2 4] [2 3 4] [3 0 4] [0 1 2 3] on #f1 cylinder 4 6 ``` ## Numbered faces Besides the six directions, a solid names every planar side it really has `#f1`, `#f2`, …, including sides no direction can name, such as a pyramid's slope or one pentagon of a dodecahedron. A solid built on a slanted face leans out with it: ```pegm pyramid 4 20 30 on #f1 cylinder 4 10 ``` The numbering runs from the side that faces most steeply up to the one that faces most steeply down; sides facing equally far up are counted around from the front, clockwise. On a cube: | Name | Same face as | | ----- | ------------ | | `#f1` | `#top` | | `#f2` | `#front` | | `#f3` | `#left` | | `#f4` | `#back` | | `#f5` | `#right` | | `#f6` | `#bottom` | On a four-sided pyramid `#f1` to `#f4` are the slopes, front first, and `#f5` is `#bottom`. A numbered face that covers a whole side of the box _is_ that box face, so both names place a solid identically. - Only planar sides are numbered, never facets on curves: a cylinder has two (its caps), a cone one (its base), a sphere none. - Outside `on`, the six directions name a side only where one of these lies in that face of the box: a dodecahedron has `#top` and `#bottom` but no `#front`. `on #front` still builds against the box's plane. - The names belong to the shape the solid was built as. Like the six directions they follow it through a transform and are never renamed. - An oblique face cannot be given to `shell`, `chamfer` or `fillet`, which work in axis-aligned coordinates. - A `cut` makes a new shape, which is numbered afresh. ```pegm cylinder 20 30 on #f1 cube 6 ``` --- # 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. --- # Macros and conditionals [Start here](https://pegmatite.dev/guide/start.md). Fetch this topic when a script repeats a feature with different sizes, names a shape, or builds something only when a test holds. ## Macros **Syntax:** **`def`** _%name_ [_$parameter_ …] _body_ A macro is a named compound solid or a named 2D shape. The `%` is part of the name in the definition and every call; `%cube` is valid. Arguments follow the name without parentheses or separators, and the next command ends the call. They are numbers, variables, negated values such as `-$size`, or parenthesized expressions. The count must match the definition. A macro with no parameters is called by its name alone. ```pegm def %pillar $size $height { block $size $size $height } set $size 10 %pillar $size (5 * $size) ``` ### Braceless bodies A body of one statement needs no braces. Setters, the solid, its whole pipeline and an `on` in front of it belong to it, so `def %pin on #front color red cylinder 4 10 | rotate 90 about x` is one body, as a [pattern](https://pegmatite.dev/guide/patterns.md) body is. The next statement is the script's own, whatever the indentation. Use braces for two solids or more. ```pegm def %b block 10 20 30 %b @right color red %b ``` ### Shape macros A body can be a [2D shape](https://pegmatite.dev/guide/shapes.md). The macro names that shape, and a call stands wherever a shape is expected: before a sweep, as its profile, or as the shape a sweep takes as an argument. ```pegm def %slot $width rect $width 10 %slot 20 | extrude 4 @right 5 %slot 30 | extrude 4 ``` - A shape body is one shape, braced only when it is a [group](https://pegmatite.dev/guide/shape-operations.md), and may end with 2D modifiers, turns, flips and scales. - A body that only calls a shape macro is a shape too: `def %wide %slot 40`, `def %lip %slot 40 | offset -1`. A body that sweeps its shape, `def %plate rect 40 30 | extrude 2`, is a solid. - Settings are read where the shape is used, so a `text` shape takes the `font-size` in force where it is swept. - Called before a sweep with one argument too few, a shape macro is a [varying profile](https://pegmatite.dev/guide/sweeps.md): the sweep supplies the last argument. - Called as a statement of its own, a shape macro is a [sketch](https://pegmatite.dev/guide/shapes.md), drawn and not built. A solid macro cannot stand where a shape is expected: ```pegm /* Expected error: macro %block is a solid, a shape is expected here. */ def %block cube 10 %block | extrude 2 ``` ### Define before use A definition stands among the root statements, wherever it reads best, never inside braces, a list or a branch of an `if`. A name is defined once; a second definition is an error. A macro is known from its definition on. A statement calls macros defined above it, and a body may call any macro whose definition has been reached when the call runs, so two macros can call each other if both are defined before the first call. ```pegm def %up $n { if $n > 0 { cube 4 on #top %down ($n - 1) } } def %down $n { if $n > 0 { cylinder 4 4 on #top %up ($n - 1) } } %up 4 ``` ```pegm /* Expected error: Macro %base is defined on line 3, below this call. */ %base def %base block 40 40 4 ``` An unknown bare command is an error with a spelling suggestion: ```pegm /* Expected error: unknown command "cilinder", did you mean "cylinder"? */ cilinder 20 10 ``` ### Macros are compounds A call is placed, anchored and snapped onto as one solid, like a `{ … }` [compound](https://pegmatite.dev/guide/start.md#compounds-and-parts). Placement setters apply to the call as a whole, and appearance settings are inherited. The body is an isolated context: a list inside it closes inside it, and `on root` returns to the macro's own ground plane. ```pegm def %tower $height { block 12 12 4 on #top cylinder 6 $height } block 60 60 6 on #top color red x left %tower 14 ``` ### Variables A body sees its parameters and the variables set above its definition, with their values at definition time. Later assignments and the caller's variables do not change them. A parameter of the same name stands over the variable. A `set` in the body does not escape. ```pegm set $size 10 def %peg { cylinder 4 $size } %peg set $size 20 on #top %peg // still 10 high ``` ```pegm /* Expected error: Undefined variable: $size, a macro sees the variables set above its definition and its parameters. */ def %peg { cylinder 4 $size } set $size 10 %peg ``` ### Recursion A macro may call itself while a test stops it. Each call is a compound placed by the caller. More than 64 nested calls is an error. ```pegm def %stack $n { cube $n if $n > 1 %stack ($n - 1) } %stack 4 // cubes 4, 3, 2, 1, each on the one before ``` ## Conditionals **Syntax:** **`if`** _test_ _body_ [**`else`** _body_] `if` builds one branch. With no `else` and a false test it does nothing. ### The test A bare test compares two arguments, each a number, variable, measure, call, text or parenthesized expression. Parenthesize arithmetic arguments: `if ($n + 1) > 2 …`, not `if $n + 1 > 2 …`. A test of several comparisons goes in parentheses with `and`, `or` and `not`, as in a [choice](https://pegmatite.dev/guide/expressions.md). ```pegm set $width 60 set $holes 1 block $width 40 4 if ($width > 50 and $holes == 1) @center -cylinder 6 4 ``` ### Branches Each branch is one statement with any `on` in front of it. A list `[ … ]` builds several statements from the same reference, and a boolean in it still reaches what was built before the `if`, which suits optional ears, bosses and holes. A compound `{ … }` builds one isolated volume. ```pegm def %case $ears { block 40 30 20 if $ears == 1 [ on #left block 10 30 3 on #right block 10 30 3 ] } %case 1 ``` An `if` is a statement, so `else if` chains: ```pegm set $mount "flange" block 40 30 20 if $mount == "ears" on #left block 10 30 3 else if $mount == "flange" on #bottom @center block 60 50 3 else on #bottom @center -cylinder 10 5 ``` An `else` belongs to the nearest `if` that can still take one: in `if $a == 1 if $b == 1 cube 4 else cube 2` it is the inner one's. To give it to the outer one, invert the test, `if $a != 1 cube 2 else if $b == 1 cube 4`, or give the inner `if` its own `else`. ### What an if leaves behind An `if` is no volume, so it opens no scope and keeps no slot. - The next statement is placed against what the branch left: the volume a solid built, the box a pattern left, or after a list the reference the list opened from. - With no branch picked, it is placed against what the `if` followed, as after `repeat 0`. - A `set` or label in a branch is visible after it. - To leave a reference whatever the test picks, write one in an `else`, such as `else slot block 40 40 0`. ```pegm set $lid 0 block 40 40 20 if $lid == 1 block 40 40 3 cylinder 10 5 // on the lid if there is one, else on the box ``` One-shot setters before an `if` bind to the branch it picks. With no branch picked they bind to nothing, and the next solid is placed without them: ```pegm set $ears 0 cube 20 @right 5 if $ears == 1 block 10 20 3 @right cube 4 // against the cube, with no gap ``` --- # Patterns [Start here](https://pegmatite.dev/guide/start.md). Fetch this topic when you repeat a statement in a row, ring, grid or mirrored pair, space holes a margin in from the edges, or need what a pattern leaves as the reference. ## Common rules Every pattern has the form `kind arguments [with index $name] body`. `repeat` continues a sequence; `linear` spaces centers along X; `circular` spaces them around a circle; `grid` fills rows and columns; `mirror` adds a reflected copy. - A count is a number, variable or expression, a whole number from 0 to 10,000; `0` builds nothing. For `grid`, _columns_ × _rows_ follows the same limit. - `with index $name` counts iterations from 0. The index is local: it is undefined after the pattern, and an outer variable of the same name keeps its value. Nested patterns name their own index. ```pegm set $i 5 repeat 3 with index $i cube (10 + $i) // cubes of 10, 11 and 12 cube $i // 5 ``` ### The body is one statement The body is one statement with its setters and pipeline, and may span lines; whatever follows is outside the pattern, whatever the indentation. An `on` in front of the solid belongs to the body: `repeat 3 on #top cube 10` builds a stack. ```pegm circular 40 4 on #front text "A" | extrude 4 ``` Braces make the body a [compound](https://pegmatite.dev/guide/start.md#compounds-and-parts): each iteration is one fused volume, placed by the setters before the brace. A setter on the first solid inside only moves it within the compound, so it warns; a statement built on `root` is the exception. A `set`, a persistent setting or a label inside the braces ends with the iteration, so every iteration starts from the same state; use the index for what changes. A cut inside the braces cuts only the iteration; to drill earlier material with several solids, negate the compound, `-{ … }`. ```pegm repeat 3 { @right 2 cube 10 /* Expected warning: this placement has no effect */ } ``` ```pegm block 60 20 10 on #top linear 15 4 -{ cylinder 9 2 @bottom cylinder 5 8 } ``` Each iteration is a counterbored hole. ### What a pattern leaves The pattern leaves the **box over everything its iterations built** as the reference; the next statement is placed against it, and `on` names its faces. The iterations are not fused: put the pattern in braces to fuse it or apply a modifier. A pattern that builds nothing leaves nothing, and the next statement follows what came before the pattern. ```pegm repeat 4 @right cube 10 on #top cube 4 /* centered on top of the whole row */ ``` ```pegm cube 10 repeat 0 cube 4 @right cube 4 /* to the right of the cube 10 */ ``` For `linear`, `circular`, `grid` and `mirror`, placement setters before the pattern place its combined slot, not single iterations. A pattern of negative volumes reaches into its reference like a negative solid, so a row of holes drills the face it was placed on. Appearance setters reach only the first solid; write them with `!` or inside the body to apply them to every iteration. ### Spanning the reference with `inset` `linear`, `grid` and `mirror` take `inset` _margin_ in place of their distance or gap. The pattern then spans what it is placed against, the face after `on` or the previous volume: its outermost centers stand the margin in from the edges, and the rest are spaced evenly between, whatever size the iterations are. A single iteration is centered. The slot is the reference's width on the spanned axes, so the pattern lands centered on it; a margin over half the width is an error. This is how holes dimensioned from the edges are written: ```pegm block 95 60 10 [ -cylinder 12 10 grid 2 2 inset 9 -cylinder 5.5 10 // four holes, centers 9 from the edges ] ``` ## repeat **Syntax:** **`repeat`** _count_ [**`with index $`**_name_] _body_ Each iteration is placed against the previous one, so the gaps are even. All one-shot setters before `repeat` go to its first solid. ```pegm !@right 2 repeat 5 cube 10 ``` ```pegm block 60 20 10 on #top !@right 6 x left 11 repeat 4 -cylinder 5 10 ``` `x left 11` places the first hole, which centers the row of four on the block. ## linear **Syntax:** **`linear`** _distance_ _count_ [**`with index $`**_name_] _body_ **Syntax:** **`linear inset`** _margin_ _count_ [**`with index $`**_name_] _body_ _distance_ runs from one iteration's center to the next; negative runs towards −X, so iteration 0 is at the +X end; `0` puts every iteration in one place. The centers are spaced, not the gaps: an iteration of any size keeps its place on the line. ```pegm linear 16 5 with index $i cylinder (4 + $i * 2) 6 ``` The cylinders grow from 4 to 12 across with centers 16 apart; `!@right 4 repeat 5` would keep the gaps at 4 instead. With `inset`, the count comes after the margin and the row spans its reference: `on #top linear inset 10 4 -cylinder 5 10` drills four holes across a block, the outer two 10 from its ends. Each iteration is centered on its point across the plane, whatever offset its body is built at, and keeps its own height above the lowest of them. Iterations branch from their own point; inside one, statements follow each other as usual. ```pegm linear 10 4 cube 6 @top block 36 6 2 /* one bar across the whole row */ ``` ## circular **Syntax:** **`circular`** _diameter_ _count_ [**`angle`** _degrees_] [**`start`** _degrees_] [**`shrink`**] [**`with index $`**_name_] _body_ | Option | Default | Rule | | ----------------- | ------- | --------------------------------------------------------- | | _diameter_ | | zero or greater | | `angle` _degrees_ | `360` | sweep shared by the iterations; zero and negative allowed | | `start` _degrees_ | `0` | angle of the first iteration, from +X | | `shrink` | off | the slot is the box over what was built | `angle` and `start` may come in either order; a repeated one keeps its last value. A full turn is closed, shared out over the iterations; any other sweep is open, with an iteration at each end when there are at least two. One iteration sits at `start`. A negative sweep runs the other way. ```pegm circular 40 4 cube 4 /* 0, 90, 180 and 270 degrees */ circular 40 4 angle 90 cube 4 /* 0, 30, 60 and 90 degrees */ circular 40 4 angle 90 start 45 cube 4 /* 45, 75, 105 and 135 degrees */ ``` Each iteration stands on its own point of the circle, turned by its share of the sweep, so a solid facing out on the first faces out on every one. Iterations branch from their point; a braced body's statements follow each other. An anchor in the body moves the solid from its point: `@bottom` hangs it under the circle, `@center` sinks it onto the point. This sets the iterations relative to each other; the ring itself goes where its slot goes. ```pegm cylinder 60 10 on #top circular 40 6 @top -cylinder 6 10 // six holes on a 40 bolt circle ``` The placement frame sets the circle's plane, and the slot its position: `sphere 30` then `@center circular 30 4 sphere 5` rings the equator; without `@center` the ring sits on top of the sphere. ### The slot is the circle The slot is the generated bounds mirrored about the circle's center. Two opposite arcs of one circle claim the same slot, an odd count is not off-center, and `angle` and `start` move the material without moving the ring: ```pegm sphere 70 | cut 30 40 [ @top 1 circular 65 19 angle 154 start 103 sphere 4 @top 1 circular 65 19 angle 154 start 283 sphere 4 ] ``` `shrink` uses the box over what was built instead, placed like any solid. A ring whose iterations cut a volume built before it stays where its circle put it, so a bolt circle drills the face it was placed on. ## grid **Syntax:** **`grid`** _columns_ _rows_ [_gap_] [**`inset`** _margin_] [_y-margin_] [**`with index $`**_name_] _body_ _columns_ run along X, _rows_ along Y. _gap_ is the distance between cells, `0` by default so cells touch; a negative gap overlaps them, `grid 4 4 -2 sphere 10`. Every cell has the largest width, depth and height of all iterations, and each iteration is centered in its cell on X and Y, so rows stay straight: ```pegm grid 3 2 2 with index $i cylinder (4 + $i * 2) 6 // cells 14 across ``` The index runs along the first row, then the next: iteration 0 is at the left front, the row runs towards +X, and each new row is further +Y; with 4 columns, iteration 5 is the second of the second row. Iterations branch from their own cell and stand on it as on the ground. The slot encloses all cells, gaps included: ```pegm block 50 50 6 on #top grid 4 4 4 -cylinder 6 6 ``` `inset` replaces the gap and spans the reference on both axes; a second value sets the margin along Y, `grid 3 2 inset 10 8`. ## mirror **Syntax:** **`mirror`** _axis_ [_gap_] [**`inset`** _margin_] [**`with index $`**_name_] _body_ _axis_ is `x`, `y` or `z`, normal to the plane between the halves: `mirror x` sets them side by side along X. The original is on the negative side, the copy on the positive side, each against the plane or half the _gap_ from it; a negative gap overlaps. A half is placed by its size like a `linear` iteration, so `mirror x x 15 cube 10` is two cubes meeting at the plane; only the gap spaces them. The index is 0 for the original and 1 for the copy, and a label in the body names the original half. The slot encloses both halves with the plane in its middle, even when the halves differ. ```pegm mirror x { block 30 24 4 [ on #top x 6 -cylinder 8 4 ] @top x left block 4 24 20 } ``` A bracket with a hole in each foot, written as one half. `inset` replaces the gap: the halves span the reference along the axis, each centered the margin in from its edge, `on #top mirror x inset 8 -cylinder 4 6`. `mirror z` takes no inset. ### The copy is reflected The copy is the reflection, with the opposite handedness: a profile is reversed, a helix turns the other way, text reads backwards. Put lettering that must read correctly outside the pattern. Each volume of the copy is reflected as it is finished, so a hole the body drills into earlier material is mirrored too: ```pegm block 60 20 4 on #top mirror x 20 -path "M0,0 H10 L10,6 Z" | extrude 4 ``` ### Two planes A mirror has one plane; for symmetry in two directions, mirror a mirrored pair: ```pegm mirror y mirror x cube 10 /* four cubes meeting at the center */ ``` --- # 2D shapes [Start here](https://pegmatite.dev/guide/start.md). Fetch this topic when a profile needs an exact outline: an ellipse, a star, a polygon, an SVG path, or the outline of an existing face. ## Shapes A 2D shape becomes a solid with `| extrude`, `| revolve`, `| helix` or `| sweep` ([extrude.md](https://pegmatite.dev/guide/extrude.md), [sweeps.md](https://pegmatite.dev/guide/sweeps.md)). Alter outlines and combine them with [2D operations and groups](https://pegmatite.dev/guide/shape-operations.md); reuse them with [shape macros](https://pegmatite.dev/guide/macros.md). `circle`, `rect`, `ellipse`, `star` and a regular `polygon` are centered on their origin; `polygon points`, `path` and `text` use drawn coordinates. Sizes take variables and expressions, and a size not greater than zero is rejected. ### Sketches A shape with no sweep after it is a sketch. It is placed exactly as the solid swept from it would be, and the preview draws its outline where that solid's base would lie. It builds nothing: no material, no slot, and the next volume is placed as though it were not there. The script builds with a warning: ```pegm /* Expected warning: the circle is a sketch, drawn and not built. */ block 40 40 10 circle 20 @right 4 cube 10 ``` The cube stands beside the block. A sketch's label and sign wait for its sweep, `-circle 20 | extrude 6`. Braces holding only shapes are a group, and so one sketch; a shape macro called as a statement is a sketch of its shape. ### `circle` **Syntax:** **`circle`** _diameter_ ### `rect` **Syntax:** **`rect`** _width_ _height_ _width_ runs across X, _height_ across Y, the shape's own axes. An extruded `rect` is a `block`. ### `ellipse` **Syntax:** **`ellipse`** _diameter-x_ _diameter-y_ Both diameters span the whole shape. ### `star` **Syntax:** **`star`** _spikes_ _outer-diameter_ _inner-diameter_ The base of a `starprism`: `star n outer inner | extrude h` is `starprism n outer inner h`. Spikes reach the outer circle, notches the inner one; _inner-diameter_ must be smaller than _outer-diameter_, and _spikes_ a whole number of 3 or more. The origin is the center of both circles, the first spike points along +X, so `revolve` turns about that point and `apex [0 0]` stands over the middle: ```pegm star 5 40 20 | extrude 20 apex [0 0] ``` ### `polygon` **Syntax:** **`polygon points [`**_x_ _y_**`]`**… **Syntax:** **`polygon`** _sides_ [**`diameter`** | **`edge`** | **`across`**] _size_ With `points`, list at least three corners in outline order, in the shape's own coordinates with Y up. Coordinates take expressions. The outline closes itself; a repeated first point is allowed, and either winding works. ```pegm set $w 30 polygon points [0 0] [$w 0] [($w / 2) 20] | extrude 4 ``` Crossing edges, self-touching outlines and edges that double back are rejected: ```pegm /* Expected error: the edges of a polygon must not cross. */ polygon points [0 0] [10 10] [10 0] [0 10] | extrude 4 ``` With a side count it is a regular polygon sized and oriented like a `prism` base: at least three sides, _size_ the diameter through the corners by default, a flat facing front, the origin at the circumcircle center. These two give the same shape and numbered faces: ```pegm polygon 6 across 24 | extrude 20 @right 10 prism 6 across 24 20 ``` ### `path` **Syntax:** **`path`** _SVG-path_ An SVG path string. SVG Y grows downward, so the path is flipped to stand as drawn: a larger Y is lower. This matters for `revolve` and `helix`, where Y becomes height; draw the profile as it looks on screen. The flange drawn at Y 0 is the top here: ```pegm path "M0,0 H10 V2 H2 V20 H0 Z" | revolve ``` The `points` of an SVG `` paste between `M` and `Z`, since pairs after a move are lines: ```pegm path "M20,0 40,20 30,20 30,40 10,40 10,20 0,20 Z" | extrude 10 ``` A literal accepts the commands `M L H V Z C S Q T A` and their lowercase forms, digits, decimal points, minus signs, commas and whitespace; it rejects `+` and exponents, which a path in a string variable accepts. Use plain decimals in both. A path is filled, so every subpath is closed before it is raised; only `extrude … wall` and a `sweep` trajectory read a subpath as the open line it was drawn as. ## Faces as shapes **Syntax:** _face_ or _label_ _face_ A [face reference](https://pegmatite.dev/guide/placement.md) where a shape is expected stands for that face's outline, drawn in the face's own frame along the directions `on` places a solid in. A bare face, alone, under outline modifiers or as a tiling's area, also places the solid on that face, so it covers the face edge to edge: ```pegm rect 40 40 | extrude 50 apex [0 0 -170] #f2 | extrude 2 ``` An `on` written out still decides where the solid goes: `on #top #front | extrude 2` lays the front's outline on the top. A bare face is read off the volume the next solid is built against, the one `on` would read; after `on base`, that is `base`. Right after `on base`, though, a face belongs to `on`, `on base #top`, so write the profile in a group there: `on base { #top } | extrude 2`. A label in front reads that volume's face wherever the shape is used: ```pegm base: cylinder 40 10 @right 10 base #top | extrude 2 ``` Through a label, the outline is the face as the labelled statement built it, pipeline and all; a later boolean cuts the model, not that outline. So a lid from a hollowed box's top covers the whole box: ```pegm box: block 40 40 20 [ -block 36 36 18 ] z 10 box #top | extrude 2 ``` To read the surface left after a cut, label a compound holding the cut: `box: { block 40 40 20 -block 36 36 18 }` makes `box #top` the rim. A bare face is always read as the volume stands now. The outline is the real surface on the face: a cylinder's top is a circle. A direction must name a flat side the solid has: a sphere's `#top` or a cylinder's `#front` is an error. Every face of a bounding box, `#bounds.top`, gives the whole face. A face shape claims the whole face as its slot, so an outline covering part of the face keeps its place within it: ```pegm { block 20 20 10 @right block 20 20 5 } on #top color red #top | extrude 2 ``` The red plate stands on the higher block only. A face shape is most useful as the area of a [tiling](https://pegmatite.dev/guide/tilings.md), fitting a pattern to a face without measuring it. --- # 2D operations and groups [Start here](https://pegmatite.dev/guide/start.md). Fetch this topic when a profile must be grown, shrunk, rounded, filled, turned, mirrored or resized before its sweep, or built from several shapes in braces. ## 2D modifiers `offset`, `round` and `fill` treat any 2D shape before its sweep. A pipeline reads left to right: steps before the sweep treat the profile, steps after it the solid. `round` here rounds the outline's corners, which become upright edges; `fillet` then rounds the solid's top edges: ```pegm rect 40 30 | round 5 | extrude 10 | fillet 1 at #top ``` A 2D modifier after the sweep, or after a solid that never was a shape, is an error: ```pegm /* Expected error: round treats a flat shape, not the swept solid. */ rect 40 30 | extrude 10 | fillet 1 at #top | round 5 ``` Steps run in written order, each on the outline the one before left. Rounding then growing gives larger corners than growing then rounding: ```pegm !@right 6 rect 40 30 | round 2 | offset 2 | extrude 4 rect 40 30 | offset 2 | round 2 | extrude 4 ``` A modifier treats the innermost shape written before it. When a shape ends another, as a [tiling](https://pegmatite.dev/guide/tilings.md)'s area does, the modifier belongs to it: here the area is rounded and the cells are cut back to its corners. Braces end the tiling, so a modifier after them treats the cells: ```pegm honeycomb 8 1.5 in rect 40 40 | round 8 | extrude 3 @right 6 { honeycomb 8 1.5 in rect 40 40 } | round 0.5 | extrude 3 ``` A treated shape stays in its frame. A measured shape stays centred on its origin, and a path or a polygon of written points keeps its coordinates, so an [`apex`](https://pegmatite.dev/guide/extrude.md) or a [`revolve`](https://pegmatite.dev/guide/extrude.md) reads it as it reads the untreated shape. A [face](https://pegmatite.dev/guide/shapes.md) or a tiling still claims the box it claimed, grown or shrunk by an offset. The output is filled, so an open path is closed first, as `fill` closes it. ### `offset` **Syntax:** **`| offset`** _distance_ [**`round`**] Positive _distance_ expands the outline and narrows holes; negative shrinks it. Corners stay sharp, so `rect 40 30 | offset 2` is `rect 44 34`; very sharp corners are squared off, as on a mitred [wall](https://pegmatite.dev/guide/extrude.md). With `round`, every point of the new outline is the same distance from the old one, which rounds the corners the outline moves away from: outer ones as it grows, inward-turning ones as it shrinks. ```pegm rect 40 30 | offset 2 round | extrude 4 ``` An inward offset that leaves nothing is an error. Offset makes fits with a clearance: this lid's lip is the box outline shrunk by the wall and 0.2 more: ```pegm def %outline rect 60 40 | round 4 { %outline | extrude 2 %outline | extrude 28 wall 2 inside } @right 10 { %outline | extrude 2 %outline | offset -2.2 | extrude 4 } ``` A [`sweep`](https://pegmatite.dev/guide/sweeps.md) trajectory takes no modifiers, so an offset trajectory goes in braces, which also end the offset: the `round` after them is the sweep's option. ```pegm circle 3 | sweep along { rect 40 40 | offset 2 } round ``` ### `round` **Syntax:** **`| round`** _radius_ Rounds outer and inner corners to _radius_, the L's inner corner included; curves gentler than the radius stay. Extruded, it rounds the upright edges a [fillet](https://pegmatite.dev/guide/modifiers.md) would. ```pegm polygon points [0 0] [40 0] [40 10] [10 10] [10 40] [0 40] | round 3 | extrude 4 ``` A bar exactly twice the radius wide keeps its width and gets round ends: `rect 40 6 | round 3` is a slot. A narrower part, such as a spike or thin bar, is lost, and a gap narrower than twice the radius closes, such as the strut between tiling cells. A radius that leaves nothing is an error; `round 0` changes nothing. Only after `|` is `round` this modifier: `round(2.5)` is the [function](https://pegmatite.dev/guide/expressions.md), and after a sweep trajectory it is the sweep's option. ### `fill` **Syntax:** **`| fill`** Fills the holes in every outline, islands within holes included. Filling a hollow box's top face, a rim, makes a lid: ```pegm box: block 40 30 20 | shell 2 on box #top box #top | fill | extrude 2 ``` ### `rotate`, `flip`, `scale` **Syntax:** **`| rotate`** _angle_ **Syntax:** **`| flip`** _axis_ **Syntax:** **`| scale`** _factor_ [_y-factor_] Before its sweep, a profile or a shape in a group is turned, mirrored or resized about its own origin, in its own plane. `rotate` turns counter-clockwise, `flip x` or `flip y` negates that axis, `scale` takes one factor for both axes or one per axis. They act before placement, which uses the transformed box, and interleave freely with outline modifiers, each acting on the result before it. ```pegm { rect 40 10 rect 40 10 | rotate 90 } | extrude 2 ``` A wedge and its mirror image side by side make a triangle: ```pegm def %half polygon points [0 0] [20 0] [0 10] { %half @left %half | flip x } | extrude 2 ``` After the sweep they treat the solid instead: `rotate` and `flip` name an axis of space and `scale` reaches the height, see [modifiers](https://pegmatite.dev/guide/modifiers.md). Here the first is a plate 20 × 20 × 5, the second the whole block grown to 10 tall: ```pegm !@right 6 rect 10 10 | scale 2 | extrude 5 rect 10 10 | extrude 5 | scale 2 ``` ## Groups **Syntax:** **`{`** _shape_ … **`}`** A group combines 2D shapes into one, as braces combine solids. It stands wherever a shape does; inside, shapes are placed and combined by the rules for solids, on the two axes a flat shape has. ### Placing shapes Each shape is placed against the one before it, centred on it unless told otherwise, so a washer or a frame needs no coordinates. [`x` and `y`](https://pegmatite.dev/guide/placement.md) align and offset against the previous shape; an anchor lays it beside, with an optional gap: ```pegm { rect 60 10 @right 2 circle 10 @right 2 circle 10 } | extrude 2 ``` `@left` and `@right` travel along X, `@front` and `@back` along Y, front towards −Y. `@top`, `@bottom`, `z` and `color` are errors inside a group; color belongs to the swept solid. `!` makes a setter persistent to the end of the group: ```pegm { !@right 2 rect 10 10 rect 10 10 rect 10 10 } | extrude 2 ``` Shapes are placed as they stand upright, Y up, so `@back` points the same way after a [`path`](https://pegmatite.dev/guide/shapes.md), which draws Y down the page, as after a `circle`. ### The first shape The first shape is placed against the group's origin and otherwise stays where its coordinates put it. A group of one shape is that shape: `{ path "M0,0 H20 L10,-20 Z" }` sweeps as the path does, apex and revolve axis included. An offset moves the first shape within its coordinates; an alignment puts that point of its box on the origin: ```pegm { x left rect 10 10 } | revolve ``` The square's left side is on the axis, so it revolves into a solid cylinder 20 across. `{ x 5 rect 10 10 }` is the same square and the same cylinder. ### Signs `-`, `&` and `^` cut, keep and cancel as for [solids](https://pegmatite.dev/guide/booleans.md), each against everything before it in the group, so the first shape cannot carry one. A signed shape is placed by the same rules, but on an anchor's axis it reaches back into the previous shape instead of standing beside it, so this notch stands inside the right edge: ```pegm { rect 40 20 @right -rect 10 10 } | extrude 2 ``` A second cut after a cut steps along beside it, as a row of holes does. ### Lists A list builds each item from the shape it opened after, and the shape after the list is placed against that shape again. A persistent setter written in a list ends with it. ```pegm { rect 40 20 [ x left 4 -circle 6 x right -4 -circle 6 ] } | extrude 2 ``` ### What a group leaves A group is one shape. It takes 2D modifiers as a whole, and is swept, used as a tiling's area or named by a [shape macro](https://pegmatite.dev/guide/macros.md) like any shape: ```pegm def %washer $d { circle $d - circle ($d / 2) } %washer 20 | offset -1 | extrude 2 ``` Braces holding only shape macro calls are a group too: `def %pair { %a @right %b }` is a shape when `%a` and `%b` are. A group claims the box of every shape that adds to the outline; a shape written with `-` or `&` claims none. A [`slot`](https://pegmatite.dev/guide/booleans.md) shape takes room and adds nothing, so a small shape can be swept into a larger box. The cube stands beside the 40 × 40 box, not the circle: ```pegm { slot rect 40 40 circle 10 } | extrude 2 @right cube 4 ``` --- # Extrude and revolve [Start here](https://pegmatite.dev/guide/start.md). Fetch this topic when you raise or turn a 2D shape into a solid: walls, tapers, twists, a top of another shape, rings and turned parts. ## Profile first Write the [2D shape](https://pegmatite.dev/guide/shapes.md), its shape modifiers, the sweep word, then solid modifiers: ```pegm rect 40 30 | round 5 | extrude 10 | fillet 1 at #top ``` Everything before the sweep word treats the flat shape, everything after it the solid. After the word, positional dimensions come first and named options follow in any order; a repeated option keeps its last value. A second shape argument, such as the top of `extrude … to`, takes no modifiers of its own. Write one that needs them in a [group](https://pegmatite.dev/guide/shape-operations.md), whose braces mark where it ends: ```pegm rect 30 30 | extrude 30 to { circle 20 | offset 2 } ``` `helix`, `sweep along` and varying profiles, shapes redrawn along the sweep, are in [sweeps](https://pegmatite.dev/guide/sweeps.md). ## Extrude **Syntax:** _2d-shape_ **`| extrude`** _height_ [**`wall`** _thickness_] [**`inside`** | **`outside`**] [**`twist`** _degrees_] [**`size`** _factor_] [**`apex [`**_x_ _y_ [_z_]**`]`**] [**`to`** _top_] | Argument or option | Default | Rule | | -------------------- | -------------- | ----------------------------------------------------------------------- | | _height_ | required | zero or greater | | `wall` _thickness_ | filled profile | zero or greater; the outlines must stay valid | | `inside` / `outside` | centered wall | only with `wall` on a closed outline | | `twist` _degrees_ | no turn | a number or a list `[`_a_ _b_ …`]` of two or more; not with `apex`/`to` | | `size` _factor_ | drawn size | as `twist`; zero or greater, zero only at the base or the top | | `apex [`_x y z_`]` | straight sides | _z_ defaults to _height_; not between 0 and _height_ | | `to` _top_ | the profile | one outline without holes, as is the profile; not with `apex` | The result is an ordinary solid, one closed, printable surface. ### Wall `wall` draws the outline with a thickness instead of filling it, open at both ends. The wall is centered on the outline: `circle 20 | extrude 10 wall 2` is `tube 22 18 10`. `inside` and `outside` put it wholly on one side, keeping the drawn outline as its outer or inner face; below, the first is 40 across, the second 44, a centered wall 42. A hole in the shape is an outline of its own and gets its own wall. ```pegm rect 40 40 | extrude 10 wall 2 inside rect 40 40 | extrude 10 wall 2 outside path "M0,0 H40 V40 H0 Z M10,10 H30 V30 H10 Z" | extrude 10 wall 2 ``` A wall thick enough to close the opening is rejected, as an over-thick `shell` is. An open path encloses no area, so filling closes it first, while `wall` keeps it open; it has no inside or outside, so its wall is always centered. Below, a filled triangle and an L-shaped wall: ```pegm path "M0,0 H30 V20" | extrude 10 path "M0,0 H30 V20" | extrude 10 wall 2 ``` [`shell`](https://pegmatite.dev/guide/modifiers.md) hollows a finished solid inwards and keeps its bottom, a vessel. `wall` never fills the shape: open at both ends, corners as sharp as the outline's, and it can follow a line that encloses nothing. ### Apex `apex` aims the sides at a point; _height_ is where they stop. Without _z_ the apex is at _height_, a pyramid. The point is in the shape's own frame, as a [mesh](https://pegmatite.dev/guide/solids.md) corner is: `circle`, `rect`, `ellipse` and regular polygons are centered on their origin, a `path` or a written `polygon` lies where its coordinates put it. An apex off the middle leans the point; the second below stands over the path's first corner. The solid is then placed by its box, so a lean moves the box, not the base. ```pegm rect 20 10 | extrude 30 apex [5 -3] path "M 0 0 L 20 0 L 15 12 L 2 10 Z" | extrude 30 apex [0 0] circle 20 | extrude 30 apex [0 0 60] ``` An apex above _height_ makes a frustum: the third stops halfway, its top half the base. A negative _z_ spreads the sides instead: `apex [0 0 -60]` makes the top of a 30 tall extrude 1.5 times the base. An apex between 0 and _height_ is refused, since the sides would cross. With `wall`, every level is the outline at that level drawn with the same thickness. Where the outline gets smaller than the wall, near a point or on a curve tighter than the wall, the wall closes over into solid, so a wall run to a point ends in a solid tip. Text or another long outline closing over all along is refused rather than built slowly; a higher apex keeps the top open. ```pegm rect 30 30 | extrude 30 wall 2 inside apex [0 0 60] rect 30 30 | extrude 30 wall 2 inside apex [0 0] ``` ### To another shape `to` joins the profile to a different top outline with straight lines. Outlines with the same number of corners pair corner to corner, each face between them a smooth twisted face. Different counts pair the points at the same fraction of each perimeter and keep every corner of both, each one becoming a point of the other outline; give both the same corner count to pair them directly. The top starts where the side twists least, its lines shortest, so a square's corners meet a circle at 45°. When both ways are as short, as with a square turned exactly 45°, the side turns counterclockwise. ```pegm rect 30 30 | extrude 30 to circle 20 rect 30 30 | extrude 50 to { rect 30 30 | rotate 45 } ``` The top uses the profile's coordinates: centered shapes align by their origins, written coordinates shift the top. Below, the rectangle lies 20 right of the circle's middle and the side runs over to it; the solid, placed by its box, is 40 wide: ```pegm circle 20 | extrude 30 to polygon points [10 -5] [30 -5] [30 5] [10 5] ``` With `wall`, both outlines are thickened and joined by straight sides, open at both ends. A wall too thick for either outline, closing the opening or pinching it in two, is refused, as is a wall on an open path. Both shapes must be single outlines without holes: text, tilings and paths with holes or several subpaths are rejected. ```pegm rect 40 40 | extrude 40 wall 2 inside to circle 12 ``` ### Twist and size `twist` turns the outline as it rises and `size` resizes it, both about the profile's own origin, the point `| rotate` and `| scale` use. A number is the top's value, reached evenly from the base: `twist 60` turns the top 60° counterclockwise seen from above, `size 0.5` makes it half the profile. `twist 720` turns twice. Size is a share of the drawn profile, above 1 it grows. An even size without twist rises in straight sides: on a 40 tall extrude `size 0.5` is the frustum of `apex [0 0 80]`, `size 0` the pyramid of `apex [0 0]`. ```pegm !@right 10 star 6 40 28 | extrude 40 twist 60 star 6 40 28 | extrude 40 size 0.5 star 6 40 28 | extrude 40 twist 60 size 0.5 ``` A list gives values at evenly spaced levels, base and top included, joined by a smooth curve that never overshoots: between two values it stays between them, and where they turn back it levels out. The first value is the base's own, so `size [0.5 1]` starts at half size and `twist [30 30 90]` starts turned. A list needs two values. A size cannot be negative and can be zero only at the base or the top, where the solid closes to a point. This vase swells, narrows and turns faster near its top: ```pegm star 9 80 64 | round 4 | extrude 160 twist [0 30 120] size [1 1.3 0.85 1] wall 2 inside ``` A `wall` keeps its thickness at every level, turned and resized, but cannot follow a size down to zero. `apex` and `to` already set the top, so neither takes `twist` or `size`. A path drawn away from its origin swings round that origin as it turns. ## Revolve **Syntax:** _2d-shape_ **`| revolve`** [_diameter_] [**`angle`** _degrees_] [**`start`** _degrees_] [**`shrink`**] [**`about`** _axis_] | Argument or option | Default | Rule | | ------------------ | ------- | ---------------------------------------------------------- | | _diameter_ | `0` | zero or greater; circle the profile's origin travels on | | `angle` _degrees_ | `360` | greater than 0, at most 360 | | `start` _degrees_ | `0` | where the turn begins round the axis | | `shrink` | off | the material's own box instead of one mirrored on the axis | | `about` _axis_ | `z` | `x`, `y` or `z` | `revolve` turns the profile about the upright axis. A point's distance from the axis becomes its radius, its position along the axis its height. A `path` keeps the way it was drawn, its top the top of the solid, its Y growing downwards: below, a 10 square held 5 off the axis becomes a tube 10 tall, outer radius 15, bore 5. The axis and turn are those of a [circular pattern](https://pegmatite.dev/guide/patterns.md), starting at +X, so a `revolve` and a `circular` written against the same face agree. ```pegm path "M5,0 H15 V10 H5 Z" | revolve ``` _diameter_ rings a shape drawn about its own origin: `circle 20 | revolve 40` is `torus 60 20`. At `0` the profile is swept at its own coordinates. A profile on the negative side of the axis is mirrored, one crossing it is clipped to the swept half, so a centered profile gives a primitive: `circle 20 | revolve` is a sphere, `rect 10 20 | revolve` a cylinder 10 across and 20 tall, `ellipse 10 20 | revolve` the spheroid between. Only the outline is swept; holes are ignored. `angle` runs from +X toward +Y and caps both ends; facets follow the sweep. A partial turn keeps the box mirrored about its axis, so arcs of one ring share an axis and `start` moves material within a stable slot; `shrink` uses the material's own box. `start` shows only on a partial turn. ```pegm circle 20 | revolve 40 angle 90 shrink path "M4,0 C 4,6 14,6 14,14 L 18,14 C 18,4 8,4 8,0 Z" | revolve angle 270 circle 20 | revolve 40 angle 90 start 45 ``` `about` names the axis. The radius lies on the axis that follows it in the cycle `x` → `y` → `z` → `x` and the turn runs toward the next: `about z` starts at +X toward +Y, `about x` starts at +Y, `about y` at +Z. `about` turns the solid itself, unlike [`on`](https://pegmatite.dev/guide/placement.md), which turns the frame the solid is built in and carries placement with it. A build plane lays it down. ```pegm circle 20 | revolve 40 about y angle 120 start 30 on #front path "M5,0 H15 V10 H5 Z" | revolve ``` --- # Helix, sweep and varying profiles [Start here](https://pegmatite.dev/guide/start.md). Fetch this topic when a part needs a thread, coil or spiral, a profile carried along a route, or a profile that changes along its sweep. ## Helix **Syntax:** _2d-shape_ **`| helix`** _diameter_ [_end-diameter_] [**`pitch`** _distance_] [**`turns`** _count_] [**`height`** _rise_] [**`start`** _degrees_] [**`shrink`**] [**`about`** _axis_] | Argument or option | Default | Rule | | ------------------ | ----------------- | ----------------------------------------------------------------------------- | | _diameter_ | Required | Nonnegative diameter of the path of the profile's origin. | | _end-diameter_ | Starting diameter | Nonnegative; written before named options. | | `pitch` _distance_ | `0` | Nonnegative rise per turn. | | `turns` _count_ | `1` | Greater than 0, at most 100; fractional turns are allowed. | | `height` _rise_ | Use `turns` | Positive path rise; requires positive pitch. Cannot be combined with `turns`. | | `start` _degrees_ | `0` | Start angle around the axis. | | `about` _axis_ | `z` | `x`, `y`, or `z`. | | `shrink` | Off | Use the material's bounds for placement. | ```pegm circle 5 | helix 40 pitch 10 turns 6 ``` A helix is a [`revolve`](https://pegmatite.dev/guide/extrude.md) that climbs by _pitch_ every turn: six coils of wire 5 thick on a mean diameter of 40. The profile is read as by revolve, X a radius and Y a height, and stays in the plane through the axis, so it is the section a cut through the axis shows, as a thread form is drawn. Unlike revolve, a helix keeps the side of the axis its profile is drawn on; a profile crossing the axis is refused. - `height` states the turns as the rise of the **path**, which the pitch divides into turns. The material reaches half a profile past each end, and the [slot](https://pegmatite.dev/guide/booleans.md) holds that overshoot: below, the thread is 62 tall, not 60, sticks out by 1 at both ends of its rod, and anything stacked on it or a compound holding it measures 62. - _end-diameter_ makes the path climb a cone, growing by an even share each turn. Growing without rising is a flat spiral. - Neither rising nor growing comes back to the start: one turn is a ring (`circle 5 | helix 40` is `circle 5 | revolve 40`); more turns warn and build one. - A helix starts at +X and rises toward +Y, a right-handed thread; [`| flip x`](https://pegmatite.dev/guide/modifiers.md) makes it left-handed. `start` and `about` read as on a revolve. - An open helix is capped with the profile at both ends. A pitch smaller than the profile's height makes coils overlap into a self-crossing surface. - A turn or more claims the box over the sweep. Less than a turn claims the box mirrored about the axis, as a partial revolve does; `shrink` claims the material's own box. ```pegm cylinder 20 60 @center path "M0,0 L2,1 L0,2 Z" | helix 20 pitch 4 height 60 ``` ```pegm rect 3 1 | helix 10 60 turns 5 @right 10 circle 6 | helix 40 80 pitch 10 turns 4 @right 10 circle 5 | helix 40 about y pitch 10 turns 3 start 45 | flip x ``` ### Thread ends To end a thread flush, build it outside any compound, run it a pitch past each end, and trim it with an [intersection](https://pegmatite.dev/guide/booleans.md) `on` a label at the part's base: ```pegm head: prism 6 30 10 cylinder 16 30 @center path "M-0.2,-1.2 L0,-1.2 L1.2,-0.3 L1.2,0.3 L0,1.2 L-0.2,1.2 Z" | helix 16 pitch 3 height 36 on head &path "M0,0 H8 L9.5,1.5 V30 H16 V40 H0 Z" | revolve ``` The bolt's thread caps lie inside the head and above the tip, where the intersection removes them, and its chamfer tapers the ridge to nothing; a helix stopped at the rod's ends would leave a blunt stub. The profile starts at X `-0.2` so thread and rod overlap rather than touch. The trimming path is drawn top first, since a path's Y grows downward. A nut is the same helix cut from a prism: `@center -{ cylinder … helix … }`, run a pitch past each face. ## Sweep along a path **Syntax:** _2d-shape_ **`| sweep along`** _trajectory_ [**`about`** _axis_] [**`round`**] | Argument or option | Default | Rule | | -------------------- | -------------- | ------------------------------------------------------------------- | | _2d-shape_ | Required | Any 2D shape with an area. It must fit round the tightest bend. | | `along` _trajectory_ | Required | Any 2D shape. Its outline is followed, and an open path stays open. | | `about` _axis_ | `z` | `x`, `y`, or `z`: the axis the trajectory's plane faces. | | `round` | Mitred corners | Rounds the outside of every corner. | ```pegm block 80 40 6 on #top -rect 4 3 | sweep along path "M0,0 H30 C40,0 40,20 50,20 H70" ``` The trajectory lies in the build plane, where an extrude's shape lies, so on a face it follows a route drawn on that face: here a channel cut into a plate. Write the options after the trajectory. The result is an ordinary solid placed by the box over its material. - The profile stands across the route: its X points to the **right** of the way an open path is drawn, its Y up off the plane. Its origin rides the line, so a measured shape is centered on it. The profile never twists. - On a closed outline the profile's X points **outward**, whichever way it was drawn: a profile right of its origin lies outside the ring, one left of it inside. - An open path's ends are capped square with the profile; a closed outline needs no caps. A [face](https://pegmatite.dev/guide/shapes.md) is followed along its real outline. - Every outline of a path with several subpaths, a text or a [tiling](https://pegmatite.dev/guide/tilings.md) is swept on its own and the solids joined. A single outline that crosses itself gives a self-crossing surface; draw it as separate subpaths. - Sharp corners are **mitred** like a wall's, and one sharper than a wall would mitre is cut off square. `round` turns the profile about the outside of every corner sharper than a circle's facet; the inside stays mitred. - A profile may reach no further into a bend than its radius; wider is refused. Reaching only outward, it can turn as tightly as it likes. - `about` names the axis the trajectory's plane faces. The trajectory's X lies on the axis that **follows** it in `x` → `y` → `z` → `x` and its Y on the next: `about x` has X along Y, `about y` has X running up Z. - `rect 2 10 | sweep along path "M0,0 H30 V20"` equals `path "M0,0 H30 V20" | extrude 10 wall 2`; `circle 20 | sweep along circle 40` equals `torus 60 20`. ```pegm path "M0,0 H5 V2 H0 Z" | sweep along circle 40 @right 10 path "M-5,0 H0 V2 H-5 Z" | sweep along circle 40 @right 10 rect 4 4 | sweep along rect 40 20 round ``` The first ring is 50 across, the second 40 with a bore of 30. ```pegm cylinder 40 50 @right -2 color gold circle 6 | sweep along path "M0,0 C0,-30 36,-30 36,0" about y ``` A mug handle: X is the height, Y the distance out. ```pegm /* Expected error: the profile is too wide for the bend */ circle 10 | sweep along circle 6 ``` ## Varying profiles **Syntax:** _shape-macro_ [_argument_ …] **`|`** _sweep_ or **`%`** **`$`**_parameter_ _2d-shape_ **`|`** _sweep_ A varying profile is drawn anew at every point of its sweep. It is a [shape macro](https://pegmatite.dev/guide/macros.md) called with every argument but its last; the sweep supplies the last, from 0 at its start to 1 at its end. | Sweep | The last argument runs over | | ------------- | --------------------------- | | `extrude` | the height | | `revolve` | the turn | | `helix` | the turns | | `sweep along` | each outline of the route | ```pegm !@right 10 def %petal $depth $t star 6 40 (36 - $depth * $t) %petal 8 | extrude 40 %petal 24 | extrude 40 ``` Used once, it is written inline with `%` and its one parameter. Everything up to the sweep's word is the shape, modifiers included. Its own `rotate` may speed up, slow down or turn back, unlike [`twist`](https://pegmatite.dev/guide/extrude.md). A [`curve`](https://pegmatite.dev/guide/expressions.md) passes through given values; a path silhouette gives a size at every height through `width` (see [measures](https://pegmatite.dev/guide/expressions.md)). ```pegm !@right 10 % $t rect 30 30 | round (1 + 13 * $t) | rotate (45 * $t) | extrude 40 % $t circle curve(20, 44, 16, $t) | extrude 60 % $t circle (8 + 12 * $t) | revolve 50 angle 270 % $t rect 3 (1 + 3 * $t) | helix 40 pitch 8 turns 4 ``` - Each outline is joined to itself from one point to the next, starting where it is drawn from, so a turning profile turns the solid. Outlines with equal point counts pair points in order; others are cut at equal shares of the way round, keeping every corner. - Size, corners and turn may change; the number of outlines and holes may not, and a change is refused naming where along the sweep it happens. - A sweep that closes, a whole revolve or a sweep along a closed outline, must end on its starting shape or is refused there; a partial turn need not. `% $t circle (16 + 4 * sin($t * 720)) | revolve 60` swells twice and closes. - Revolve and helix keep the profile to one side of the axis and sweep its outlines, passing over its holes. Extrude adds `twist` and `size` on top, and a `wall` of one thickness at every level; it takes no `apex` or `to`. - It is drawn 8 times, then again halfway between two drawings wherever the shape strays from the straight way between them. Still straying after 160 extra drawings warns and builds from those. - Written alone, it is a sketch of its starting shape. --- # Text and fonts [Start here](https://pegmatite.dev/guide/start.md). Fetch this topic when a model carries lettering or needs a particular font. ## `text` **Syntax:** **`text`** _text_ A line of text in the current font at the current font size. Text is a [2D shape](https://pegmatite.dev/guide/shapes.md): `extrude` raises it, and the result is an ordinary solid that is placed, anchored and signed like any other. The string may come from a variable. ```pegm set $label "Top" block 60 60 6 on #top x left text $label | extrude 2 ``` Lettering stands out of a face as relief, as above, or is cut into it with a sign: the extrusion reaches into the material from the face. ```pegm block 60 20 6 on #top -text "OPEN" | extrude 1 ``` A string may span several lines or break them with `\n`. Each line starts below the one before, aligned on the left: ```pegm text "OPEN DAILY" | extrude 2 text "OPEN\nDAILY" | extrude 2 ``` [Escapes](https://pegmatite.dev/guide/syntax.md) write a double quote, a backslash or a character that is hard to type: ```pegm text "\"20\u{B0}C\"" | extrude 2 /* "20°C" */ ``` Without `font`, text is drawn in a built-in sans-serif covering Latin-1 and Greek. A character the font has no glyph for is left out, with a warning. ## `font-size` **Syntax:** **`font-size`** _size_ The height of a capital letter in mm, `10` by default, in every font: another font keeps the text the same size. Accents and tall lowercase letters may rise above it. The extrusion height is independent. ```pegm font-size 12 text "Hello" | extrude 15 ``` Like `x`, `y` and `z`, it applies to the next solid only; `!` makes it persistent and `unset` restores the default: ```pegm !font-size 20 text "Big" | extrude 4 text "Also big" | extrude 4 font-size unset text "Default size" | extrude 4 ``` ## `font` **Syntax:** **`font`** _family_ [_weight_] The family is a [Google Fonts](https://fonts.google.com/) name, the `https` URL of a font file, or the path of a font file on disk. It is written literally, never as a variable, because the font loads before the script runs. ```pegm font "Isometra" text "Hello" | extrude 2 ``` The weight runs from `100`, thin, to `900`, black. A family name without one is `400`; a weight the family does not have is an error. ```pegm font "Roboto" 700 text "Bold" | extrude 2 ``` ```pegm font "https://fonts.gstatic.com/s/isometra/v3/Gg82N5ofdAjvCxbehlYclg4hLmpf.woff2" text "From a file" | extrude 2 ``` ### Font files - Formats: TrueType (`.ttf`), OpenType (`.otf`), WOFF, WOFF2; a collection (`.ttc`) gives its first font. The format is read from the content, so a URL needs no extension. - A file holds one weight, so the weight only picks one from a variable font and is otherwise left out. A file over 10 MB is an error. - A path ends in `.ttf`, `.otf`, `.woff`, `.woff2` or `.ttc` and is relative to the script's folder, or to the working directory for stdin. A backslash starts an escape, so write Windows paths with `/` or `\\`. - Only the [command-line tool](https://pegmatite.dev/guide/cli.md) reads files from disk; the web app reports an error and needs a URL. ```pegm font "fonts/Logo.otf" text "Local" | extrude 2 ``` `font` applies to the next solid only; `!` makes it persistent and `unset` returns to the built-in font: ```pegm !font "Roboto" text "Roboto" | extrude 2 font unset text "Built-in" | extrude 2 ``` --- # Tilings [Start here](https://pegmatite.dev/guide/start.md). Fetch this topic when a plate or face needs a grid of holes: honeycomb, square, triangle, stripe or Voronoi cells. ## Regular tilings **Syntax:** **`honeycomb`** _diameter_ _strut_ [**`angle`** _degrees_] [**`rim`** _width_] **`in`** _area_ **Syntax:** **`squares`** _diameter_ _strut_ [**`angle`** _degrees_] [**`rim`** _width_] **`in`** _area_ **Syntax:** **`triangles`** _diameter_ _strut_ [**`angle`** _degrees_] [**`rim`** _width_] **`in`** _area_ **Syntax:** **`stripes`** _width_ _strut_ [**`angle`** _degrees_] [**`rim`** _width_] **`in`** _area_ A tiling is a shape: it lays cells over the area of the shape written after `in`, and is swept like any shape. Neighbouring cells are _strut_ apart, so subtracting a tiling from a plate leaves struts of that width. ```pegm !@right 6 honeycomb 8 1.5 in rect 40 40 | extrude 3 squares 8 1.5 in rect 40 40 | extrude 3 triangles 8 1.5 in rect 40 40 | extrude 3 stripes 4 2 in rect 40 40 | extrude 3 ``` The _diameter_ is the cell's circumcircle and the cell faces front like a prism: `honeycomb 8 1.5` uses the footprint of `prism 6 8 3`. | Tiling | Cells | Middle of the area's box | Repeats at | | ----------- | --------------------------------------------------------------------- | ------------------------------- | ---------- | | `honeycomb` | hexagons in columns, each half a cell off its neighbours | a cell's center | 60° | | `squares` | squares in a plain grid, side `d / √2` | a cell's center | 90° | | `triangles` | triangles in rows, pointing back and front in turn, side `d * √3 / 2` | the corner where six cells meet | 60° | | `stripes` | stripes of _width_ running left to right | a stripe's center | 180° | A square of side 10 has diameter `(10 * 2 ** 0.5)`, as with a [`pyramid`](https://pegmatite.dev/guide/solids.md) of four sides. Every tiling is symmetric about both axes of its box. ### `angle` `angle` turns the cells counter-clockwise seen from above, about the area's box center, the way [`rotate`](https://pegmatite.dev/guide/modifiers.md) turns a solid. The area stays where it is and the turned cells are cut back to it; `| rotate` would turn the whole result. `angle 90` runs stripes front to back. ```pegm block 70 50 3 -squares 8 1.5 angle 45 rim 5 in rect 70 50 | extrude 3 ``` ## Shared rules These hold for the four tilings and for `voronoi`. - **Rim.** Cells stop short of the area's outline by a rim as wide as the strut unless `rim` sets it. A cell the rim crosses is cut back to it; stripes have their ends clipped. `rim 0` lets cells run out to the outline. A negative rim, or one so wide it leaves nothing of the area, is an error. - **Area.** The area may be any shape: a face, a shape macro or another tiling. Its box is the tiling's slot even where cells do not reach its edges, and cells are arranged about that box's center. - **Cell limit.** At most 10,000 cells, the most iterations a [pattern](https://pegmatite.dev/guide/patterns.md) runs. Each stripe counts as one cell. ```pegm set $outline "M0,0 H80 L100,20 V40 H0 Z" path $outline | extrude 3 -honeycomb 8 1.5 rim 3 in path $outline | extrude 3 ``` ## `voronoi` **Syntax:** **`voronoi`** _strut_ [**`cells`** _count_] [**`seed`** _seed_] [**`points [`**_x_ _y_**`]`**…] [**`relax`** _passes_] [**`rim`** _width_] **`in`** _area_ Voronoi cells divide the area by proximity to sites. Give exactly one of `cells` or `points`, right after the strut; `seed` follows `cells` and is not allowed with `points`. | Option | Default | Rule | | -------- | ----------------------------------- | --------------------------------------------------------------------------------------------- | | `cells` | | scatters _count_ random sites inside the rim | | `seed` | `0` | whole number. The same seed draws the same cells in every Pegmatite version | | `points` | | sites as `[x y]` in the area's frame, centered on its box | | `relax` | `2` with `cells`, `0` with `points` | each pass moves every site to its cell's center, evening out sizes; over 50 warns and uses 50 | ```pegm block 80 60 3 -voronoi 1.5 cells 30 seed 7 in rect 80 60 | extrude 3 ``` ```pegm !@right 6 voronoi 1.2 cells 25 relax 0 in rect 60 60 | extrude 3 voronoi 1.2 cells 25 in rect 60 60 | extrude 3 voronoi 1.2 cells 25 relax 5 in rect 60 60 | extrude 3 ``` ```pegm voronoi 1.5 points [-20 10] [15 12] [0 -10] [22 -12] [-18 -14] in rect 60 40 | extrude 3 ``` A site outside the area still takes the part of the area nearest to it. A cell narrower than the strut vanishes, leaving its material. ### On faces A tiling fits a face best when the face is its area. Each side of a vessel takes its own pattern and seed, and the rim keeps the corners standing. The label returns each cut to the vessel, which the next cut would not otherwise follow, and `on vase` is where each bare `#f` area is read from: ```pegm vase: rect 40 40 | extrude 50 apex [0 0 -170] | shell 2 on vase #f2 -voronoi 1.5 cells 20 seed 1 in #f2 | extrude 2 on vase #f3 -voronoi 1.5 cells 20 seed 2 in #f3 | extrude 2 on vase #f4 -voronoi 1.5 cells 20 seed 3 in #f4 | extrude 2 on vase #f5 -voronoi 1.5 cells 20 seed 4 in #f5 | extrude 2 ``` --- # Check, preview and export with the CLI [Start here](https://pegmatite.dev/guide/start.md). Fetch this topic when you verify a model, read a check report, render a view or export a file from the command line. ## Workflow The npm package `pegmatite` needs Node.js 22 or newer. `--check` is the agent's main inspection tool: it builds the model and prints dimensions, part bounds and labelled positions as text. 1. Record what should be true: overall and per-part sizes, feature sizes, alignment, gaps, cutter depths. Label the base and every feature whose position matters, cutters included. Label a macro call or compound itself; its inner labels are private. 2. Run `npx --yes pegmatite --check --strict model.pegm`. Read stdout for the report and stderr for diagnostics; do not use `--quiet`. 3. Fix errors, warnings and mismatched spans, then recheck. Under `--strict` a warning fails the input and can suppress the report. 4. Render a view only for what the report cannot settle, such as a hole opening, a curve or appearance: `npx --yes pegmatite model.pegm -o model-top.png --view top`. Open the image. After a fix, rerun `--check`; rerender only if the visual question remains. 5. Export after the checks pass, and state what was verified, which views were inspected and what remains uncertain. A successful exit does not prove the design correct. An empty model passes a check with zero size and zero parts, but cannot be exported to 3MF or PNG. Without the CLI, say which checks are undone and offer the script for the [editor](https://pegmatite.dev/editor). For repeated use, `npm install pegmatite` and run `npx pegmatite …`. ## Options **Syntax:** **`npx pegmatite`** [_options_] _script_… There is no `check`, `render` or `export` subcommand. | Argument or option | Meaning | | -------------------- | ---------------------------------------------------------------------------------------------------------------------- | | _script_… | One or more scripts; each writes a `.3mf` by default. | | `-` | Read UTF-8 script text from stdin; requires `-o`, even with `--check`. | | `-o`, `--output` _p_ | Output for a single input. `.3mf`, `.stl`, `.step`/`.stp` or `.png` picks the format. `-` writes binary 3MF to stdout. | | `--check` | Build at export quality without writing output; report dimensions and labelled volumes. | | `--strict` | Warnings fail; that input writes no output. | | `--slow` | Build slow steps, such as a shell needing a general computation; without it they are left out with a warning. | | `--quiet` | Suppress success summaries and check reports; diagnostics still print. | | `--view` _name_ | PNG view: `iso` (default), `front`, `back`, `left`, `right`, `top`, `bottom`. | | `--size` _pixels_ | Square PNG size, 64–2048; default 512. | | `-h`, `--help` | Show help. | | `--` | End options, so paths may begin with a dash. | Paths are relative to the working directory; output directories must exist. Without `-o`, the input's final extension becomes `.3mf`; an output with no extension is 3MF. Extensions are case-insensitive. PNG needs a named path. `--view` and `--size` require PNG output and cannot accompany `--check`. Several inputs run independently in order; a failure does not stop later ones, and generated outputs remain. Conflicting output paths and outputs that would overwrite an input are rejected. An existing output is replaced only after successful generation. ```sh npx pegmatite a.pegm b.pegm npx pegmatite model.pegm -o out/model.3mf printf 'cube 10\n' | npx pegmatite --check - -o - npx pegmatite model.pegm -o - > model.3mf ``` ## Reading the check report `--check` prints to stdout the input name, X × Y × Z size in millimeters and part count, then one row per labelled volume: slot size, span on each world axis, source line. ```pegm base: block 40 20 10 post: cylinder 6 20 cap: cylinder 12 6 ``` ```output model.pegm 40 × 20 × 36 mm, 1 part base: 40 × 20 × 10 x -20..20 y -10..10 z 0..10 line 1 post: 6 × 6 × 20 x -3..3 y -3..3 z 10..30 line 2 cap: 12 × 12 × 6 x -6..6 y -6..6 z 30..36 line 3 ``` Rows show the slot, the room used for placement and [measures](https://pegmatite.dev/guide/expressions.md), which can differ from the material. A `material` row follows when material bounds differ from the slot; a `slot` gets `no material`. Material is measured as that statement built it, before later booleans. A turned slot is reported as the box around it. Coordinates match the export: centered in X and Y, Z up. The summary uses up to six decimals, rows three. Labels in lists, patterns and parts appear in build order, one row per iteration. Labels inside compounds and macro bodies are private. With `part`, the report and diagnostics are grouped by part. Each part heading gives its material size and spans, then its labelled volumes; geometry before the first part is `Unnamed`. The exported part count reflects fusion and color splitting: disconnected pieces and different colors count separately even within one `part`. ### Spans as placement checks Above, the post's `z 10..30` starts at the base's top, `z 10`; its X and Y spans are centered in the base. The cap starts at `z 30` and the stack ends at `z 36`. That verifies the stacking without an image. - Compare each size and span with the specification. Equal span midpoints mean centered; equal min or max values mean aligned edges. Subtract spans for a gap or offset. - A through-cutter's span must cover the thickness it cuts. A cutter outside its target's bounds cannot reach it. Overlapping bounds are only a first check, especially for curves. - Read part bounds as well as the total. An unexpected part count can flag disconnected material, but deliberate parts and color splitting also change it. - Read the `material` row when actual extent matters. `no material` under a `slot` is deliberate empty space, not a missed cut. The negative-volume warning means a subtraction removed nothing from material already built in its construction context. It does not catch a positive feature that fails to touch another. Bounds cannot prove contact, wall thickness or a through-hole; use a targeted view when one of those matters. ## Previews PNG rendering needs no browser or GPU. It fits the whole model in an orthographic view with a transparent background and edge outlines. `iso` looks from front-right and above. Views use world directions: front is −Y, right is +X, top is +Z, whatever local frames the script used. ```sh npx pegmatite model.pegm -o model-front.png --view front --size 1024 ``` The PNG summary reports model dimensions, independent of view and size. A view with no visible area is an error. Larger images cost more time and memory. ## 3MF, STL and STEP 3MF uses millimeters, Z up, the model centered in X and Y with its base at Z = 0, and export-quality curves: finer than the interactive preview, while explicit polygon side counts stay. It keeps colors and parts. STL is binary with the same geometry and placement, triangles only: colors and part names are dropped and all bodies share one mesh. Prefer 3MF when the slicer accepts it. STEP keeps exact curved surfaces where available; unsupported surfaces become flat facets, and very large faceted conversions are refused. Each body carries its part name, or the script file name, and its color. STEP needs the optional OpenCascade package in the matching version: ```sh npx --yes -p pegmatite -p replicad-opencascadejs@1.1.0 pegmatite model.pegm -o model.step ``` Or install `pegmatite` and `replicad-opencascadejs@1.1.0` in one project. Missing or incompatible OpenCascade is a setup error before compilation. `--check` with a `.step` output needs no OpenCascade and writes nothing. ## Fonts `font` can name a local `.ttf`, `.otf`, `.woff`, `.woff2` or `.ttc` file, relative to the script's directory, or the working directory for stdin. Files are limited to 10 MB; a collection uses its first font. A missing file is an error. Google Fonts names and HTTPS URLs need network access. See [text](https://pegmatite.dev/guide/text.md) for the syntax. ## Diagnostics and exit codes Errors and warnings go to stderr with source path, line and column (1-based) when known. Ordinary warnings allow export; `--strict` fails them. A shell needing a slow general computation is left out with a warning; `--slow` builds it, which can take minutes. Success summaries also go to stderr, leaving stdout for check reports, help or binary 3MF. `--quiet` never hides diagnostics. There is no JSON report. | Exit code | Meaning | | --------- | ---------------------------------------------------------- | | `0` | All inputs succeeded, with warnings allowed unless strict. | | `1` | Script or model errors, or warnings under `--strict`. | | `2` | Invalid options, input/output, or setup failure. | With mixed failures, `2` wins.