PegmatiteScript { // A definition stands among the statements of the script, never inside a body, and // a macro is known from its definition on. Script = RootStatement* RootStatement = Definition | Part | Statement // A part names the material built after it, up to the next part, for the export and // the viewer. It stands among the statements of the script, and its name is a string // written out, so the parts of a script are known without building it. Part = keyword<"part"> string StatementList = Statement* // DanglingSetter comes last: it only matches setters that QualifiedSolid could not // bind to a solid, and the parser turns that match into a diagnostic. Braces read as a // solid first, so a group of shapes followed by a step only a shape takes, such as // `| rotate 45` or `| offset 1`, is left to be a sketch. Statement = List | OnEach | On | PersistentSetting | Set | Led | Pattern | Conditional | QualifiedSolid ~ItemModifier | Sketch | DanglingSetter // A pattern repeats what follows it, and the word it begins with names the kind: // `repeat` runs its body a number of times, `linear` spaces it out along a line, // `circular` rings it about a circle, `grid` lays it out in cells, `mirror` adds a // reflected copy. A label of the same name hugs its ":", so `grid:` is still a // label. // // A repeat's setters bind to the pattern the way a QualifiedSolid's bind to its // solid, so they reach the first round only - the round that consumes the one-shot // layer. A linear, circular, grid or mirror pattern is a volume of its own and places // itself with them. Pattern = PropertySetter* (RepeatPattern | LinearPattern | CircularPattern | GridPattern | MirrorPattern) RepeatPattern = keyword<"repeat"> NumberArg PatternIndex? PatternBody // The distance from the centre of one round to the centre of the next, then how many // there are. A repeat puts each round against the one before it, so rounds of // different sizes crowd or spread; a linear pattern keeps their centres evenly spaced. // An inset stands for the distance: the row then spans what it is placed against, // its end centres that far in from the two ends. LinearPattern = keyword<"linear"> (Inset | NumberArg) NumberArg PatternIndex? PatternBody // The circle the rounds sit on and how many there are, then what the ring does: the // sweep they share out and where it begins. Those two name themselves, the way a // revolve's do, so a reader need not count arguments to tell which is which. CircularPattern = keyword<"circular"> NumberArg NumberArg TurnOption* PatternIndex? PatternBody // Columns, then rows, then the gap between neighbouring cells. The gap can stand bare // because no statement begins with a number, so it cannot be read as the body. // An inset stands for the gap, along x and then y, the y one the same when left off. GridPattern = keyword<"grid"> NumberArg NumberArg (GridInset | NumberArg)? PatternIndex? PatternBody // The axis the mirror plane is normal to, then the gap between the two halves. The // gap can stand bare for the reason a grid's can: no statement begins with a number. MirrorPattern = keyword<"mirror"> axisName (Inset | NumberArg)? PatternIndex? PatternBody // How far in from the edges of what a pattern is placed against its outermost centres // stand. The pattern then spans that reference, and is spaced to fit it. Inset = keyword<"inset"> NumberArg GridInset = keyword<"inset"> NumberArg NumberArg? PatternIndex = keyword<"with"> keyword<"index"> varref // A body is one statement. Braces after a pattern are a compound like any other, so // each round builds one volume and `{ }` means the same thing wherever it is written. // An `on` names what the next solid is built against, so a body takes the statement // it leads into along with it rather than being only the `on`. PatternBody = On* Statement // A label and one-shot setters may stand before the `on`s of the statement they // qualify, so a label can start its line: `plate: on #front cube 10` is `on #front // plate: cube 10`. Either way they belong to what the `on`s lead into. A face list // builds one volume on each face, so a label cannot stand before one - setters can, // and reach every item. The parser checks the rest: a label names a solid, and a // statement takes one. Led = Lead On* OnEach -- each | Lead On+ LedTarget -- single Lead = PropertySetter* label PropertySetter* -- labelled | PropertySetter+ -- plain LedTarget = Pattern | Conditional | QualifiedSolid ~ItemModifier | Sketch // A conditional builds the branch its test picks, as though only that branch were // written, and with no branch picked it is as though nothing were. So it is no scope // and no volume of its own: what the branch leaves behind is what it leaves behind, // and setters written before it bind to the branch, the way a repeat's reach its first // round. `else` takes one statement like any branch, so `else if` chains, and an // `else` belongs to the nearest `if` that can still take one. Conditional = PropertySetter* If If = keyword<"if"> Test Branch Else? Else = keyword<"else"> Branch Branch = On* Statement // A bare test compares two arguments, which are bare the way any argument is, and a // test of more than that is written in parentheses. That keeps a test from running on // into the branch: `if $n > 2 %pi` stays a test of 2 before a call to %pi. Test = "(" Logic ")" -- grouped | Operand comparison Operand -- bare // The words shared by everything that rings something about an axis: how far the // turn goes, where it begins, and whether the slot it leaves follows its material // rather than the circle it was rung about. Each names itself, so they may be written // in either order, and one written twice keeps its last value the way a repeated // property setter does. TurnOption = keyword<"angle"> NumberArg -- angle | TurnPlacement // Where the turn begins and what it claims, apart from how far it goes. A helix // shares these two but counts its sweep in turns rather than degrees, so it takes // this pair and leaves `angle` behind. TurnPlacement = keyword<"start"> NumberArg -- start | flag<"shrink"> -- shrink // The axis a sweep turns about, shared by everything that sweeps a profile. AboutOption = keyword<"about"> axisName // A list branches: every item starts from the reference in force at the "[", and // that reference is back in force at the "]". It builds the tree a flat file // describes. Persistent settings and labels written inside reach no further than // the closing bracket; variables and boolean operations keep the surrounding scope. List = "[" StatementList "]" // A solid together with everything that qualifies it. One-shot property setters // bind to the solid they precede, so they cannot stand on their own or before a // list, and before an `on` only as part of a Led statement. A boolean operator says what the volume does to what was // built before it - union is the default and needs no sign. Whitespace between the // sign and solid is allowed; arithmetic arguments require parentheses. A label names // the volume this statement builds, so a later `on` can come back to it. A `slot` // before the solid keeps only the room it takes: it is placed, measured and named // like the solid, its faces are there to build on, and it adds no material. QualifiedSolid = PropertySetter* label PropertySetter* booleanOp? slotPrefix? SolidExpr -- labelled | PropertySetter* booleanOp? slotPrefix? SolidExpr -- plain slotPrefix = keyword<"slot"> DanglingSetter = PropertySetter+ // A flat shape with no sweep after it is a sketch: it is drawn where the base of a // sweep would lie, and builds nothing, takes no slot and leaves the reference where it // was. It is tried after a solid, so braces and macro calls read as solids first and // are told apart once every definition is known. Sketch = PropertySetter* label? PropertySetter* booleanOp? slotPrefix? Profile booleanOp = "-" | "&" | "^" // Geometry modifiers form a left-to-right pipeline. They remain part of one solid, // so placement, anchoring and subtraction see the final modified geometry. SolidExpr = Solid GeometryModifier* // Two families share the pipeline. Chamfer, fillet and shell treat the edges and // faces of a shape; rotate, scale and flip change the frame it occupies. Both are // shape space: the bounding box the pipeline ends with is the one placement uses, // and an edge selector reads the frame left by the step before it. GeometryModifier = Chamfer | Fillet | Shell | Cut | Rotate | Scale | Flip // Left without `at`, chamfer and fillet treat every edge. The clause is optional // because no statement begins with `at`; one beginning with `on` would take the `on` // of the line after it. A chamfer's second distance is safe the same way: a statement // that begins with "-" subtracts a solid, which no number begins with. Chamfer = "|" keyword<"chamfer"> NumberArg NumberArg? EdgeTarget? Fillet = "|" keyword<"fillet"> NumberArg EdgeTarget? EdgeTarget = keyword<"at"> EdgeSelector // Left without a clause, a shell opens the face that points up where it stands in the // pipeline, the top of the solid's bounding box. `open` names the faces instead, one // or a list of them the way `on` takes them, and `closed` opens none. Both begin with // a word no statement begins with, so the line after keeps its own. Shell = "|" keyword<"shell"> NumberArg ShellOpening? ShellOpening = keyword<"open"> FaceList -- faces | keyword<"open"> planeRef -- face | flag<"closed"> -- closed // Two levels on the solid's own z, and what lies between them is what stays. A level // is a height above the base or a percentage of the whole, the way an alignment names // a point on a box; the first must be the lower one, so the pair reads bottom to top. Cut = "|" keyword<"cut"> CutLevel CutLevel CutLevel = percent -- percent | NumberArg -- height // Degrees, one axis per step: composing two turns states their order, where a // triple of angles would hide an Euler convention. Rotate = "|" keyword<"rotate"> NumberArg keyword<"about"> RotationAxis RotationAxis = axisName -- axis | planeRef -- face // The three-factor form scales each axis, the single-factor form all of them Scale = "|" keyword<"scale"> NumberArg NumberArg NumberArg -- axes | "|" keyword<"scale"> NumberArg -- uniform Flip = "|" keyword<"flip"> axisName // An edge selector is a predicate over the edges of a shape. Every kind of test // combines with every other: "not" and "tangent" bind tightest, then "and", then // "or", and parentheses regroup them. "tangent" grows its operand along the edges // that carry it on smoothly. Each operand is decided over all the shape's edges // before they combine, so `nearest` ranks every edge, not only those beside it. EdgeSelector = EdgeSelector keyword<"or"> EdgeConjunction -- or | EdgeConjunction EdgeConjunction = EdgeConjunction keyword<"and"> EdgeNegation -- and | EdgeNegation EdgeNegation = keyword<"not"> EdgeNegation -- not | keyword<"tangent"> EdgeNegation -- tangent | EdgePredicate EdgePredicate = keyword<"parallel"> axisName -- parallel // The one predicate that points rather than describes: the edge whose // nearest point lies closest to a place in the solid's own frame. | keyword<"nearest"> NumberArg NumberArg NumberArg -- nearest | planeRef -- face | axisName comparison Expr -- compare | "(" EdgeSelector ")" -- group // The two-character operators must be tried first, "<" would swallow "<=". comparison = "<=" | ">=" | "==" | "!=" | "<" | ">" axisName = ("x" | "y" | "z") ~identifierPart Solid = Block | Cube | Cylinder | Cone | Sphere | Torus | Tube | Pyramid | StarPrism | Prism | Tetrahedron | Octahedron | Dodecahedron | Icosahedron | Mesh | SweptSolid | Compound | MacroCall Compound = "{" StatementList "}" // A macro is a named compound solid or a named 2D shape. A shape is tried before a statement: every shape begins with its own // keyword, and none of those can begin a statement, so the two never compete. Without // braces the body is one statement, the way a pattern's is, and still builds a compound. // A macro call is either, so only a shape modifier after it says it is a shape here; // a bare one is decided once every definition is known. Braces are a compound when // they hold statements and a group of shapes when they hold shapes; braces holding // nothing but macro calls read as a compound and are decided the same way. Definition = keyword<"def"> macroName varref* DefinitionBody // A shape followed by a sweep is a solid, so the shape bodies stop short of one. DefinitionBody = Compound -- compound | ShapeGroup ItemModifier* ~Sweep -- group | ShapeLiteral ItemModifier* ~Sweep -- shape | MacroShape ShapeModifier ItemModifier* ~Sweep -- shapeCall | On* Statement -- single MacroCall = macroName NumberArg* // A macro's name carries its "%" wherever it is written, the way a variable carries // its "$". Built-in commands and macros then never share a name, so a built-in added // later cannot change what an existing script means, and a bare word that is no // command is an error at that word rather than a call to a macro nobody defined. macroName = "%" identifier identifier = letter identifierPart* identifierPart = letter | digit | "_" keyword = name ~identifierPart // An option that is a word alone, with no argument after it to tell it apart from a // label of the same name on the next line: `round:` there is a label, not the option. flag = keyword ~":" // A command's word written whole, lexical so that no space can come between the word // and the end of it. commandName = commandWord ~identifierPart // The words of built-in commands and their options. No name is reserved: a label // hugs its ":", a macro carries its "%" and a variable its "$". These words are only // kept from naming a function, below. commandWord = "dodecahedron" | "tetrahedron" | "icosahedron" | "octahedron" | "starprism" | "cylinder" | "pyramid" | "extrude" | "revolve" | "sphere" | "torus" | "block" | "slot" | "prism" | "unset" | "helix" | "sweep" | "along" | "mesh" | "tube" | "cone" | "cube" | "cut" | "outside" | "inside" | "inset" | "wall" | "chamfer" | "fillet" | "shell" | "rotate" | "scale" | "flip" | "parallel" | "nearest" | "tangent" | "root" | "and" | "not" | "on" | "at" | "honeycomb" | "squares" | "triangles" | "stripes" | "voronoi" | "polygon" | "ellipse" | "circle" | "star" | "rect" | "angle" | "about" | "start" | "shrink" | "offset" | "fill" | "open" | "closed" | "font-size" | "font" | "color" | "path" | "text" | "def" | "part" | "set" | "or" | "x" | "y" | "z" // A "!" makes a setter persistent: it keeps applying to following and nested // solids instead of binding to the next one. PersistentSetting = persist PropertySetter persist = "!" ~space PropertySetter = Color | X | Y | Z | FontSize | Font | Anchor Color = keyword<"color"> ColorArg ColorArg (hex color, html named color or unset) = unset -- unset | colorValue -- value colorValue = colorhex | namedColor namedColor = colorname ~identifierPart // A label names the volume its statement builds. It is written where a name belongs, // before the thing named, and it is visible from there to the end of the level it is // in. The ":" must hug it, which is what keeps it apart from an alignment pair, and // apart from a command of the same name: `cube: cube 1` is a cube labelled cube. // Only `root` and the constants are taken: every context already has a label named // `root`, and a constant stands bare where a label is read, as in `@top pi #top`. label = labelName ":" labelName = ~reservedWord identifier reservedWord (a reserved word) = ("root" | "pi") ~identifierPart // The reference the next solid is built against, and in whose frame it is placed: // a face, a labelled volume, or a face of one. `root` is the label every context // has, naming the plane it grows from. // // Before the first solid of a context there is no solid to name a face of, and there // the same word names the plane the context itself is built from. On = keyword<"on"> volumeRef planeRef -- volumeFace | keyword<"on"> volumeRef ~FaceList -- volume | keyword<"on"> planeRef -- face // A list of faces builds what follows on each of them, the way a list with an `on` // for every face would: `on box [#f2 #f3] cube 1` is `[ on box #f2 cube 1 on box #f3 // cube 1 ]`. With no label the faces are of the volume the statement follows. A face // is all a statement cannot begin with, so `on box [cube 1]` is still `on box` and a list. OnEach = keyword<"on"> volumeRef? FaceList On* Statement FaceList = "[" planeRef+ "]" volumeRef = keyword<"root"> -- root | labelName -- named // A reference to a named face of a solid. The names are open ended - a solid names // its own - so an unknown one is reported when the solid is built. // // A bare name follows the solid: it stays on the same face once the solid is turned. // The "bounds." prefix asks for the axis-aligned bounding box instead, which is the // face of the silhouette rather than of the shape. The two coincide until a solid is // transformed. planeRef = "#" keyword<"bounds"> "." featureName -- bounds | "#" featureName -- solid featureName = letter identifierPart* Block = keyword<"block"> NumberArg NumberArg NumberArg Cube = keyword<"cube"> NumberArg Cylinder = keyword<"cylinder"> NumberArg NumberArg Cone = keyword<"cone"> NumberArg NumberArg // A regular pyramid, its base of so many sides measured on the circumcircle. The // count comes first because it says which solid the rest of the numbers measure. Pyramid = keyword<"pyramid"> NumberArg measure? NumberArg NumberArg // a regular prism, counted and measured the way a pyramid is Prism = keyword<"prism"> NumberArg measure? NumberArg NumberArg // What a size is measured along: through the corners, the length of an edge, or the // distance between two parallel flats. Written before the size it changes, so the // number that follows still says how big and only the word says along what. Left // off, a prism or a pyramid is measured through its corners and a platonic solid // along its edge, the way a cube is. measure = keyword<"diameter"> | keyword<"edge"> | keyword<"across"> // A star swept upright: the spikes reach the outer circle, the notches between them // the inner one. Both sizes are diameters. StarPrism = keyword<"starprism"> NumberArg NumberArg NumberArg NumberArg Sphere = keyword<"sphere"> NumberArg Torus = keyword<"torus"> NumberArg NumberArg // a tube: the wall between two upright circles Tube = keyword<"tube"> NumberArg NumberArg NumberArg Tetrahedron = keyword<"tetrahedron"> measure? NumberArg Octahedron = keyword<"octahedron"> measure? NumberArg Dodecahedron = keyword<"dodecahedron"> measure? NumberArg Icosahedron = keyword<"icosahedron"> measure? NumberArg // A solid written out corner by corner: the points it has, then the flat sides that // join them, each side the points it runs through by their place in the list, counted // from 0 the way a pattern's index is. The sides are numbered #f1, #f2, ... in the // order they are written. A bracket that opens a list statement can follow, and // cannot be mistaken for one more side, because no statement begins with a number. Mesh = keyword<"mesh"> keyword<"vertices"> MeshVertex+ keyword<"faces"> MeshFace+ MeshVertex = "[" NumberArg NumberArg NumberArg "]" MeshFace = "[" NumberArg NumberArg NumberArg NumberArg* "]" // A swept solid is written the way it is made: the profile first, then what is done // to it. Everything before the sweep treats the flat shape - its outline modifiers, // and the turns, flips and scales a shape takes in a group - and everything after it // treats the solid, so the sweep's own word is where the one pipeline turns from 2D // to 3D, and `path ... | flip x | revolve` flips the profile where `path ... | // revolve | flip x` flips the solid. SweptSolid = Profile Sweep // A profile written `% $t shape` is a family: the shape drawn anew for every value of // `$t` the sweep gives it, from 0 where the sweep starts to 1 where it ends - a shape // macro written in place, its one parameter last. Everything up to the sweep's word // is the shape, so its modifiers are the family's. Profile = "%" varref ShapeTerm ItemModifier* -- family | ShapeTerm ItemModifier* -- shape Sweep = "|" (Extrude | Revolve | Helix | SweepAlong) Extrude = keyword<"extrude"> NumberArg ExtrudeOption* // The wall gives the outline a thickness instead of filling the area it encloses. It // belongs to the extrude rather than to the profile because it keeps its thickness // while the outline shrinks towards an apex or turns into a top, and because it is // the only way an open path - one that never returns to where it began, and so // encloses nothing - names a solid at all. // // The apex is the point the sides run towards, written in the frame the shape is drawn // in, as the corner of a mesh is. Left without a z it sits at the height, and the // extrude closes to a point there; further up, the height cuts the sides short of it, // and below the base they spread apart as they rise. // // `to` names the shape the top is instead, in the same frame as the base, so the side // runs from one outline to the other: a square base can rise into a round neck. It is // an option like the apex, which is the same thing with a point for the top. // // A shape written as an argument of a sweep takes no modifiers of its own: what // follows it treats the solid. One that needs them is written in a group, `to { circle // 20 | round 2 }`, the braces saying where the shape ends. ExtrudeOption = keyword<"wall"> NumberArg wallSide? -- wall | keyword<"apex"> "[" NumberArg NumberArg NumberArg? "]" -- apex | keyword<"to"> ShapeTerm -- to | keyword<"twist"> Law -- twist | keyword<"size"> Law -- size // `twist` turns the outline about its own origin as it rises, counter-clockwise seen // from above, and `size` grows or shrinks it about that origin. A number is where the // top ends up, reached evenly from the base: no turn and the drawn size. A list is the // value at evenly spaced levels, the base and the top included, with a smooth curve // through them, so an outline can swell and narrow again or turn faster as it climbs. Law = "[" NumberArg NumberArg+ "]" -- stations | NumberArg -- value // Which side of the outline the wall falls on. Centred by default: it is the one rule // an open path can follow, so both kinds of path read the same way without it. wallSide = flag<"inside"> | flag<"outside"> // The optional diameter is the circle the shape's origin travels on, the way a // circular pattern rings its rounds. Left off, the profile sweeps where its own // coordinates put it. The options that follow say what the turn does. Revolve = keyword<"revolve"> NumberArg? RevolveOption* RevolveOption = TurnOption | AboutOption // A helix is a revolve that rises: the same profile, in the same plane through the // axis, carried round while it climbs. `pitch` is the distance between levels, and // how far the sweep goes is counted in `turns` - more than one is the usual case - // or given as the `height` the path climbs, which is the same thing divided by the // pitch. The first diameter is the circle the profile rides on, and the optional // second one the circle it ends on, so the path climbs a cone rather than a // cylinder. Both come first, before the named options. Helix = keyword<"helix"> NumberArg NumberArg? HelixOption* HelixOption = TurnPlacement | AboutOption | keyword<"pitch"> NumberArg -- pitch | keyword<"turns"> NumberArg -- turns | keyword<"height"> NumberArg -- height // A sweep carries the profile along the outline of another shape, the trajectory. The // trajectory lies in the build plane, where an extrude's shape lies, and is read the // way a wall reads it: an open path stays open. The profile stands across it, its x // to the outside of a closed outline or the right of an open one, its y off the // plane. SweepAlong = keyword<"sweep"> keyword<"along"> ShapeTerm SweepOption* // `round` is also a function, but `round(` is a call, and a shape's `| round` carries // its bar, so the bare word after the trajectory can only be the sweep's option. SweepOption = AboutOption | flag<"round"> -- round // The flat shapes every sweep accepts. The drawn ones carry their outline // in a string, the measured ones state their size and are centred on their own origin. // A macro call stands for the shape its definition names, so a shape written once can // be swept many times over. A face reference stands for the outline of that face, in // the face's own frame, so the shape covers exactly the face it is built on. // // A shape takes modifiers after it the way a solid does, so a profile is rounded or // offset before it is swept: `rect 40 30 | round 3 | extrude 10 | fillet 1`. An // outline modifier binds to the innermost shape before it, so one written after the // area of a tiling treats that area rather than the cells laid over it. Shape2D = ShapeTerm ShapeModifier* ShapeTerm = ShapeLiteral | MacroShape | FaceShape | ShapeGroup // A group combines shapes the way a compound combines solids, and places them the // same way: each against the one before it, centred unless an `x`, `y` or anchor says // otherwise, with lists to branch and the boolean signs to cut, keep or cancel. The // first shape keeps the coordinates it is drawn in, so a group of one shape is that // shape. A flat shape has no z, so neither `z` nor a colour stands here. ShapeGroup = "{" ShapeItem+ "}" ShapeItem = ShapeList | PersistentShapeSetting | GroupedShape ShapeList = "[" ShapeItem* "]" PersistentShapeSetting = persist ShapeSetter ShapeSetter = X | Y | Anchor GroupedShape = ShapeSetter* booleanOp? slotPrefix? Shape2D ItemModifier* // A profile and a shape in a group can be turned, mirrored and resized too. These // belong to the whole shape before them, the item on its line, and come after any // outline modifier the shape took for itself; an outline modifier written after them // treats the outline they left. ItemModifier = ShapeRotate | ShapeFlip | ShapeScale | ShapeModifier // Degrees counter-clockwise about the shape's own origin: a flat shape has one axis. ShapeRotate = "|" keyword<"rotate"> NumberArg ShapeFlip = "|" keyword<"flip"> planeAxis planeAxis = ("x" | "y") ~identifierPart // One factor for both axes, or one for each. ShapeScale = "|" keyword<"scale"> NumberArg NumberArg? ShapeModifier = Offset | RoundCorners | Fill // A signed distance: the outline grows by it, or shrinks by one below zero. Corners // stay as sharp as the outline's own, the way a wall's do; `round` keeps every point // of the new outline the same distance from the old one instead. Offset = "|" keyword<"offset"> NumberArg flag<"round">? // Every corner rounded to the radius, the outer ones and those turning in alike. RoundCorners = "|" keyword<"round"> NumberArg // The outline alone, with the holes inside it filled. Fill = "|" keyword<"fill"> ShapeLiteral = SvgPath | TextShape | Circle | Rect | Ellipse | Star | Polygon | Honeycomb | Squares | Triangles | Stripes | Voronoi MacroShape = macroName NumberArg* // A label may share a command's name, but a shape that begins a statement is read off // a face only when its first word is no command, so `color #12` stays a bad colour. FaceShape = ~commandName volumeRef planeRef -- volumeFace | planeRef -- face Circle = keyword<"circle"> NumberArg Rect = keyword<"rect"> NumberArg NumberArg Ellipse = keyword<"ellipse"> NumberArg NumberArg // The outline a starprism raises: spikes, then the circles the spikes and the notches // between them reach. Centred on its own origin, first spike along +X. Star = keyword<"star"> NumberArg NumberArg NumberArg // A polygon is its corners. Written out, they are in the shape's own coordinates and // stay where they are put, the way a path's do. Counted, they are a regular polygon // measured as a prism's base is, so `extrude h polygon n d` is `prism n d h`, and // centred on its own origin, as the measured shapes are. It has three corners at least. Polygon = keyword<"polygon"> keyword<"points"> PolygonPoint PolygonPoint PolygonPoint+ -- points | keyword<"polygon"> NumberArg measure? NumberArg -- regular PolygonPoint = "[" NumberArg NumberArg "]" // The tilings lay cells over the area of another shape: regular polygons of a // diameter, hexagons, squares or triangles, or stripes of a width, with a strut of // material between neighbours. The angle turns the cells inside the area, which itself stays where it // is, and the rim is the band along the outline the cells leave standing. The area // comes last, after `in`, since a macro call takes every number that follows it. Honeycomb = keyword<"honeycomb"> NumberArg NumberArg TilingOption* TilingArea Squares = keyword<"squares"> NumberArg NumberArg TilingOption* TilingArea Triangles = keyword<"triangles"> NumberArg NumberArg TilingOption* TilingArea Stripes = keyword<"stripes"> NumberArg NumberArg TilingOption* TilingArea TilingOption = keyword<"angle"> NumberArg -- angle | TilingRim TilingRim = keyword<"rim"> NumberArg TilingArea = keyword<"in"> Shape2D // A voronoi pattern divides its area into the cells nearest each of its sites, a // strut apart. The sites are a count of random ones, drawn from a seed, or written // out one by one; relaxing moves each towards the middle of its cell, evening them out. Voronoi = keyword<"voronoi"> NumberArg VoronoiSites TilingArea VoronoiSites = keyword<"cells"> NumberArg VoronoiRandomOption* -- random | keyword<"points"> VoronoiPoint+ VoronoiPointOption* -- points VoronoiRandomOption = keyword<"seed"> NumberArg -- seed | VoronoiRelax | TilingRim VoronoiPointOption = VoronoiRelax | TilingRim VoronoiRelax = keyword<"relax"> NumberArg VoronoiPoint = "[" NumberArg NumberArg "]" TextShape = keyword<"text"> string -- literal | keyword<"text"> varref -- variable Set = keyword<"set"> varref ValueArg // A variable holds a number or a string. Strings supply text and path profiles. ValueArg = string -- text | NumberArg -- number // Each axis carries two independent settings: an alignment, which is a point on the // reference paired with a point on the volume, and an offset, a signed distance // along the axis. A command writes whichever of the two it names, so an alignment // from one setter and an offset from another combine. // // A point is a percentage of the box, and the side names are aliases for the three // that come up: left is 0%, center 50%, right 100%, and likewise on the other axes. // A single point is the same one on both boxes, so `x right` means `x right:right`. X = keyword<"x"> Alignment Y = keyword<"y"> Alignment Z = keyword<"z"> Alignment Alignment = alignTo NumberArg -- both | alignTo -- align | SettingArg -- offset alignTo = alignPoint ":" alignPoint -- pair | alignPoint -- same alignPoint = percent | side percent = number "%" xSide = ("left" | "center" | "right") ~identifierPart ySide = ("front" | "center" | "back") ~identifierPart zSide = ("bottom" | "center" | "top") ~identifierPart FontSize = keyword<"font-size"> SettingArg // Literals only, so the fonts a script uses are known, and loaded, before it runs. Font = keyword<"font"> FontArg FontArg (font name or URL, optional weight, or unset) = unset -- unset | string fontWeight? -- value fontWeight = digit+ ~identifierPart // A bare number or variable needs no parentheses, a compound expression does. // // `//` divides rounding down and `%` is what that leaves over, so the two agree. A // `%` inside an expression can never begin a macro name, because a macro is no value. // `**` binds tightest and to the right, and a sign in front of it applies to the // power, not the base: `-2 ** 2` is -4, as it is written in mathematics. // // A choice is the loosest of all: a comparison, then the value for when it holds and // the value for when it does not. It compares with the operators an edge selector // does, and either value can be a choice of its own, so a chain reads left to right. Expr = CondExpr CondExpr = Logic "?" Expr ":" Expr -- choice | AddExpr // A test is comparisons joined by `and` and `or`, and turned by `not`, binding the // way an edge selector's do. It is only ever read before a `?` or after an `if`, so // an edge selector's `z > 5 and z < 9` keeps its `and`: `5 and` compares nothing. Logic = Logic keyword<"or"> LogicAnd -- or | LogicAnd LogicAnd = LogicAnd keyword<"and"> LogicNot -- and | LogicNot LogicNot = keyword<"not"> LogicNot -- not | Condition | "(" Logic ")" -- group // A text compares only by `==` and `!=`, and never equals a number. Condition = Operand comparison Operand Operand = string -- text | value AddExpr = AddExpr ("+" | "-") MulExpr -- operation | MulExpr MulExpr = MulExpr mulOp UnaryExpr -- operation | UnaryExpr // `//` starts a line comment, so a division is never followed by another slash mulOp = "*" ~"*" | "/" | "%" UnaryExpr = "-" UnaryExpr -- negate | PowExpr PowExpr = PrimaryExpr "**" UnaryExpr -- operation | PrimaryExpr PrimaryExpr = "(" Expr ")" -- paren | ShapeMeasure | Call | Measure | varref | constant | number // A function's name hugs its "(", the way a label hugs its ":". That is what keeps // `@top block (2 * $a)` an anchor with no gap before a block rather than a call. Its // arguments are parted by commas: an expression is already loose about spaces, so // `max(a -b)` could not tell one argument from two. The name is any word but a // command's, and one that names no function is reported at that word. Color names // stay open to it, since a color is never written where a number is: `tan` is both. // // The words that begin a pattern or a conditional are no function names either. A // macro call takes every argument after it, so `%m 3 repeat($n) cube 5` would // otherwise call `repeat` as the macro's last argument. Call = functionName #"(" ListOf ")" // A flat shape measured in its own frame: `width(circle 20)` is how far it reaches // along x, and `width(circle 20, 5)` how far at y 5, from its leftmost point there to // its rightmost; `height` measures along y the same way. The shape is written as a // profile is, modifiers and all, and ends at the comma, since its own arguments are // parted by spaces. It is tried before a call, which would take `width` for a function. ShapeMeasure = shapeMeasureName #"(" ShapeTerm ItemModifier* ("," Expr)? ")" shapeMeasureName = ("width" | "height") ~identifierPart functionName = ~(commandWord ~identifierPart) ~(statementWord ~identifierPart) identifier // The words that begin a pattern or a conditional rather than a solid. statementWord = "repeat" | "linear" | "circular" | "grid" | "mirror" | "if" | "else" // A size read back from the slot of a labelled volume, measured in the frame that // volume was placed in, so a block of a label's three measures is its slot. The "." // hugs both words, the way a function's "(" hugs its name. No statement begins with // a word followed by a ".", and a label is set off by a ":", so a measure stands bare // as an argument the way a call does. Measure = labelName #("." measureName) measureName = ("width" | "depth" | "height") ~identifierPart // A constant is written bare, the way a number is, and names no label. constant = keyword<"pi"> // A call carries its own parentheses, so like a number or a variable it can stand as // an argument without another pair around it. // // A sign negates any of them as it does a number, and hugs it the same way. A volume // signed for a difference begins with a command, a macro, a brace or a face, never // with a value, so `x -$inset` is an offset and `x 2 -cube 1` still a difference. NumberArg = negSign PrimaryExpr -- negate | "(" Expr ")" -- expr | ShapeMeasure -- shapeMeasure | Call -- call | Measure -- measure | varref -- variable | constant -- constant | number -- literal SettingArg = unset -- unset | NumberArg -- value unset = keyword<"unset"> // A number literal carries its own sign, so a sign is never doubled. negSign = "-" ~(space | "-" | digit) varref = "$" varname varname = letter (letter | digit | "_")* SvgPath = keyword<"path"> "\"" SvgPathContent "\"" -- literal | keyword<"path"> varref -- variable SvgPathContent = (SvgPathCommand|digit|"."|","|"-")* SvgPathCommand = "M"|"m"|"L"|"l"|"H"|"h"|"V"|"v"|"Z"|"z"|"C"|"c"|"S"|"s"|"Q"|"q"|"T"|"t"|"A"|"a" // An anchor names the direction the volume travels from the one before it, and may // carry the gap along that direction. A signed volume travels the other way, into // the material, so which end of it meets the reference follows from the sign. Anchor = anchorRef SettingArg? anchorRef = "@" anchorName anchorName = ("center" | "bottom" | "right" | "front" | "left" | "back" | "top") ~identifierPart colorhex = "#" hexDigit hexDigit hexDigit hexDigit hexDigit hexDigit ~identifierPart colorname = "lightgoldenrodyellow" | "mediumspringgreen" | "mediumaquamarine" | "mediumslateblue" | "mediumturquoise" | "mediumvioletred" | "blanchedalmond" | "cornflowerblue" | "darkolivegreen" | "lightslategray" | "lightslategrey" | "lightsteelblue" | "mediumseagreen" | "darkgoldenrod" | "darkslateblue" | "darkslategray" | "darkslategrey" | "darkturquoise" | "lavenderblush" | "lightseagreen" | "palegoldenrod" | "paleturquoise" | "palevioletred" | "rebeccapurple" | "antiquewhite" | "darkseagreen" | "lemonchiffon" | "lightskyblue" | "mediumorchid" | "mediumpurple" | "midnightblue" | "darkmagenta" | "deepskyblue" | "floralwhite" | "forestgreen" | "greenyellow" | "lightsalmon" | "lightyellow" | "navajowhite" | "saddlebrown" | "springgreen" | "yellowgreen" | "aquamarine" | "blueviolet" | "chartreuse" | "darkorange" | "darkorchid" | "darksalmon" | "darkviolet" | "dodgerblue" | "ghostwhite" | "lightcoral" | "lightgreen" | "mediumblue" | "papayawhip" | "powderblue" | "sandybrown" | "whitesmoke" | "aliceblue" | "burlywood" | "cadetblue" | "chocolate" | "darkgreen" | "darkkhaki" | "firebrick" | "gainsboro" | "goldenrod" | "indianred" | "lawngreen" | "lightblue" | "lightcyan" | "lightgray" | "lightgrey" | "lightpink" | "limegreen" | "mintcream" | "mistyrose" | "olivedrab" | "orangered" | "palegreen" | "peachpuff" | "rosybrown" | "royalblue" | "slateblue" | "slategray" | "slategrey" | "steelblue" | "turquoise" | "cornsilk" | "darkblue" | "darkcyan" | "darkgray" | "darkgrey" | "deeppink" | "honeydew" | "lavender" | "moccasin" | "seagreen" | "seashell" | "crimson" | "darkred" | "dimgray" | "dimgrey" | "fuchsia" | "hotpink" | "magenta" | "oldlace" | "skyblue" | "thistle" | "bisque" | "indigo" | "maroon" | "orange" | "orchid" | "purple" | "salmon" | "sienna" | "silver" | "tomato" | "violet" | "yellow" | "azure" | "beige" | "black" | "brown" | "coral" | "green" | "ivory" | "khaki" | "linen" | "olive" | "wheat" | "white" | "aqua" | "blue" | "cyan" | "gold" | "gray" | "grey" | "lime" | "navy" | "peru" | "pink" | "plum" | "snow" | "teal" | "red" | "tan" // A word glued to a number is an error rather than a second argument, so `2pi` is not // `2 pi`, and an exponent such as `1e3` stays free to mean one number some day. number = "-"? digit+ fraction? numberEnd numberEnd (a space between a number and a word) = ~identifierPart fraction = "." digit+ // a backslash starts an escape; which escapes exist is checked after parsing string = "\"" stringChar* "\"" stringChar = "\\" any -- escape | ~("\"" | "\\") any -- char comment = "/*" (~"*/" any)* "*/" -- block | "//" (~"\n" any)* -- line space += comment }