Ask the Path

Part 1 of 5 in Drawing Without Bookkeeping — a form on one side, its annotated twin on the other, and nothing copied between them.

Series: Drawing Without Bookkeeping

  1. Ask the Path (this post) — query() and queryAll()
  2. Thinking and Drawing in Parallelsubscribe()
  3. A Linkage That Dimensions Itself — a four-bar linkage
  4. The Panel Prints Its Own Drill Schedule — a Eurorack front panel
  5. The Fretboard Is a Formula — a fretboard from one scale length

Prerequisites: This post assumes path block basics — the @{ } sigil, draw(), drawTo() — and the as segment(...) / as endpoint(...) clauses from Name Your Corners. Labels are optional here: most of what follows needs none.

Every sample in this series has the same shape. On the left, a form: the thing being drawn. On the right, its twin: the same form again with schematic information on top — dots, centres, boxes, tints. The rule is that the twin never repeats a coordinate. Whatever it knows about the form, it asked for.

Here is the smallest version of that rule. A tab with two arcs, and a dot on every corner. The left panel types six coordinates a second time. The right panel asks:

rightDots.apply {
  for (corner in rightForm.queryAll('endpoint')) {
    circle(corner.x, corner.y, 3);
  }
}

//-- The contrast row. Left: a dot on every corner, each coordinate typed a //-- second time by hand. Right: the same picture from one query — the //-- annotation layer asks the form where its corners are. define ViewBox(0, 0, 480, 230); // ─── Core tokens ─────────────────────────────────────────────── let bg_color = Color(CSSVar('--bg', #d0d7f0)); let fg_auto = Color('#0d1638'); let fg_muted = Color('#0d1638').alpha(0.6); let fg_hair = Color('#0d1638').alpha(0.22); let accent = oklch(0.55 0.16 260); let font = 'sans-serif'; let bg = PathLayer('bg') #{ fill: bg_color; stroke: none; }; bg.apply { rect(0, 0, 480, 230); } // ─── The form: one tab, drawn identically in both panels ─────── let tab = @{ h 120 a 20 20 0 0 1 20 20 v 50 a 20 20 0 0 1 -20 20 h -120 z }; // ─── Left panel: coordinates written twice ───────────────────── let leftPanel = GroupLayer('left') #{ translate-x: 50; translate-y: 75; }; let leftForm = PathLayer('left-form') #{ stroke: fg_auto; stroke-width: 1.5; fill: none; }; let leftDots = PathLayer('left-dots') #{ fill: accent; stroke: none; }; let leftEyebrow = TextLayer('left-eyebrow') #{ font-family: font; font-size: 8; font-weight: 700; letter-spacing: 3; fill: fg_muted; text-anchor: start; }; let leftNote = TextLayer('left-note') #{ font-family: font; font-size: 8; letter-spacing: 0.5; fill: fg_auto; text-anchor: start; }; leftPanel.append(leftForm, leftDots, leftEyebrow, leftNote); leftForm.apply { M 0 0 tab.draw() } // Every corner the tab has, typed again — and wrong the moment the tab changes. leftDots.apply { circle(120, 0, 3); circle(140, 20, 3); circle(140, 70, 3); circle(120, 90, 3); circle(0, 90, 3); circle(0, 0, 3); } leftEyebrow.apply { text(0, -18)`WRITTEN TWICE`; } leftNote.apply { text(0, 112)`six circles, six coordinates copied by hand`; } // ─── Right panel: asked once ─────────────────────────────────── let rightPanel = GroupLayer('right') #{ translate-x: 290; translate-y: 75; }; let rightForm = PathLayer('right-form') #{ stroke: fg_auto; stroke-width: 1.5; fill: none; }; let rightDots = PathLayer('right-dots') #{ fill: accent; stroke: none; }; let rightEyebrow = TextLayer('right-eyebrow') #{ font-family: font; font-size: 8; font-weight: 700; letter-spacing: 3; fill: fg_muted; text-anchor: start; }; let rightNote = TextLayer('right-note') #{ font-family: font; font-size: 8; letter-spacing: 0.5; fill: fg_auto; text-anchor: start; }; rightPanel.append(rightForm, rightDots, rightEyebrow, rightNote); rightForm.apply { M 0 0 tab.draw() } // The form answers: one Endpoint per drawing command, in drawing order. rightDots.apply { for (corner in rightForm.queryAll('endpoint')) { circle(corner.x, corner.y, 3); } } rightEyebrow.apply { text(0, -18)`ASKED ONCE`; } rightNote.apply { text(0, 112)`one queryAll('endpoint'), zero coordinates`; } // ─── Divider ─────────────────────────────────────────────────── let divider = PathLayer('divider') #{ stroke: fg_hair; stroke-width: 0.5; fill: none; }; divider.apply { M 240 45 L 240 200 } Left: six circles, six coordinates copied by hand. Right: one queryAll('endpoint') — change the tab and the dots follow.

queryAll returns one Endpoint per drawing command, in drawing order. Each one knows its x and y, the command that ends there, the command that leaves, and the turn the path makes at that point. That is the whole idea: you ask a path for things by kind, and what comes back already knows its own geometry. The reference is the Path Queries docs; this post is about what it unlocks.

Before you lean on it

The sharp edges first, so nothing below feels like a trick.

  • Queries answer finished geometry. A corner rounded by with fillet has already been rounded when you ask; the arc the fillet inserted is a real arc and command(a) will find it, and endpoint(name) on that corner answers the trimmed tangent point, not the joint you typed. The older point('name') keeps its documented preference for the sharp corner; query does not.
  • endpoint skips pure moves. A move is where drawing starts, not where anything ends. A z that has length counts.
  • Coordinates come from the receiver. A PathBlock answers relative to its own origin; a layer, or the ProjectedPath that drawTo() returns (the block once it is placed on the page), answers in page coordinates. Every twin here asks the form where things are, so none of them re-types a coordinate the form already knows.
  • query insists, queryAll doesn't. query returns one match and errors if there is none, listing what the path actually has, so a typo is caught where you wrote it. queryAll returns an array, empty if nothing matched, so it loops safely over things that might not exist.
  • Command letters are case-insensitive. Path blocks report lowercase relative commands whatever you typed, so command(a) and command(A) mean the same arcs.

Ask by kind

Five nouns cover everything a path is made of. Each takes, in parentheses, the natural way to name some of its kind:

  • command(a) — by letter, or by shape word: line, arc, curve
  • call(circle) — by the function that emitted it
  • segment(rib) — by the label you gave the run
  • endpoint(base) — by the label you gave the joint
  • subpath(1..2) — by position

Leave the parentheses off and you get all of them.

The first noun is the one that makes labels optional. An arc command carries its radii and flags; the Command it returns adds the centre those imply.

//-- Every arc, no labels. Left: a tab with two drawn arcs, a corner rounded //-- by `with fillet`, and a circle. Right: the same form asked for //-- `command(a)` — each arc's centre marked and spoked, the fillet's arc and //-- the circle's two halves included. define ViewBox(0, 0, 480, 230); // ─── Core tokens ─────────────────────────────────────────────── let bg_color = Color(CSSVar('--bg', #d0d7f0)); let fg_auto = Color('#0d1638'); let fg_muted = Color('#0d1638').alpha(0.6); let fg_hair = Color('#0d1638').alpha(0.22); let accent = oklch(0.55 0.16 27); let font = 'sans-serif'; let bg = PathLayer('bg') #{ fill: bg_color; stroke: none; }; bg.apply { rect(0, 0, 480, 230); } // ─── The form ────────────────────────────────────────────────── let tab = @{ h 100 a 18 18 0 0 1 18 18 v 50 a 18 18 0 0 1 -18 18 h -100 z with fillet(16) }; fn drawForm(target) { target.apply { M 0 0 tab.draw() circle(168, 43, 20); } } // ─── Left panel: the form ────────────────────────────────────── let leftPanel = GroupLayer('left') #{ translate-x: 40; translate-y: 75; }; let leftForm = PathLayer('left-form') #{ stroke: fg_auto; stroke-width: 1.5; fill: none; }; let leftEyebrow = TextLayer('left-eyebrow') #{ font-family: font; font-size: 8; font-weight: 700; letter-spacing: 3; fill: fg_muted; text-anchor: start; }; let leftNote = TextLayer('left-note') #{ font-family: font; font-size: 8; letter-spacing: 0.5; fill: fg_auto; text-anchor: start; }; leftPanel.append(leftForm, leftEyebrow, leftNote); drawForm(leftForm); leftEyebrow.apply { text(0, -18)`THE FORM`; } leftNote.apply { text(0, 112)`two arcs drawn, one rounded in, a circle`; } // ─── Right panel: every arc, asked ───────────────────────────── let rightPanel = GroupLayer('right') #{ translate-x: 260; translate-y: 75; }; let rightForm = PathLayer('right-form') #{ stroke: fg_auto; stroke-width: 1.5; fill: none; }; let spokes = PathLayer('spokes') #{ stroke: accent; stroke-width: 0.75; fill: none; }; let centres = PathLayer('centres') #{ fill: accent; stroke: none; }; let rightEyebrow = TextLayer('right-eyebrow') #{ font-family: font; font-size: 8; font-weight: 700; letter-spacing: 3; fill: fg_muted; text-anchor: start; }; let rightNote = TextLayer('right-note') #{ font-family: font; font-size: 8; letter-spacing: 0.5; fill: fg_auto; text-anchor: start; }; rightPanel.append(rightForm, spokes, centres, rightEyebrow, rightNote); drawForm(rightForm); // An arc knows its own centre; the query hands back every arc there is — // including the one the fillet inserted after we typed the corner. Each // spoke runs from the arc's midpoint to its centre, so the two halves of // the circle stay countable even though they share one centre. let arcs = rightForm.queryAll('command(a)'); spokes.apply { for (arc in arcs) { let mid = arc.block.get(0.5); let hub = arc.center; M mid.x mid.y L hub.x hub.y } } centres.apply { for (arc in arcs) { let mid = arc.block.get(0.5); let hub = arc.center; circle(mid.x, mid.y, 1.6); circle(hub.x, hub.y, 2.2); } } rightEyebrow.apply { text(0, -18)`COMMAND(A), ASKED`; } rightNote.apply { text(0, 112)`${arcs.length} arcs found — the fillet's counted too`; } // ─── Divider ─────────────────────────────────────────────────── let divider = PathLayer('divider') #{ stroke: fg_hair; stroke-width: 0.5; fill: none; }; divider.apply { M 240 45 L 240 200 } command(a) finds five arcs — the two drawn, the one the fillet inserted, and the circle's two halves — and each one knows its own centre.

Read the count on the right. Two arcs were typed. The with fillet(16) on the closing edge added a third when the path was finished, and circle() emits two half-circles, so the query reports five. Nothing was labelled; nothing was counted by hand. Each spoke runs from the arc's midpoint to arc.center, two things the same result knows — a result is a struct, a small object whose members you read with a dot.

Labels through the same grammar

Labels did not go anywhere. They are the argument to segment and endpoint, mirroring the way you wrote them: as segment('tooth') is asked back with segment(tooth), as endpoint('root') with endpoint(root). A space between two parts means inside: the right part is searched only within the left, exactly as a CSS descendant selector reads.

//-- Labels through the same grammar. A comb labels every tooth `as segment` //-- and every root `as endpoint`. Right: `segment(tooth)` runs tinted, the //-- tips found by the combinator `segment(tooth) endpoint`, and the roots by //-- `endpoint(root)` — three questions, no indexes. define ViewBox(0, 0, 480, 230); // ─── Core tokens ─────────────────────────────────────────────── let bg_color = Color(CSSVar('--bg', #d0d7f0)); let fg_auto = Color('#0d1638'); let fg_muted = Color('#0d1638').alpha(0.6); let fg_hair = Color('#0d1638').alpha(0.22); let points = oklch(0.55 0.16 260); let runs = oklch(0.55 0.16 80); let font = 'sans-serif'; let bg = PathLayer('bg') #{ fill: bg_color; stroke: none; }; bg.apply { rect(0, 0, 480, 230); } // ─── The form: a comb, labelled where it is drawn ────────────── fn drawComb(target) { target.apply { M 0 70 for (i in 0..4) { v -34 as segment('tooth'); v 34 as endpoint('root'); h 32 } } } // ─── Left panel ──────────────────────────────────────────────── let leftPanel = GroupLayer('left') #{ translate-x: 40; translate-y: 75; }; let leftForm = PathLayer('left-form') #{ stroke: fg_auto; stroke-width: 1.5; fill: none; stroke-linejoin: round; }; let leftEyebrow = TextLayer('left-eyebrow') #{ font-family: font; font-size: 8; font-weight: 700; letter-spacing: 3; fill: fg_muted; text-anchor: start; }; let leftNote = TextLayer('left-note') #{ font-family: font; font-size: 8; letter-spacing: 0.5; fill: fg_auto; text-anchor: start; }; leftPanel.append(leftForm, leftEyebrow, leftNote); drawComb(leftForm); leftEyebrow.apply { text(0, -18)`THE FORM, LABELLED`; } leftNote.apply { text(0, 100)`as segment('tooth'), as endpoint('root')`; text(0, 111)`five teeth, one name each`; } // ─── Right panel ─────────────────────────────────────────────── let rightPanel = GroupLayer('right') #{ translate-x: 270; translate-y: 75; }; let rightForm = PathLayer('right-form') #{ stroke: fg_auto; stroke-width: 1.5; fill: none; stroke-linejoin: round; }; let teeth = PathLayer('teeth') #{ stroke: runs; stroke-width: 4; fill: none; stroke-linecap: round; }; let tips = PathLayer('tips') #{ fill: points; stroke: none; }; let roots = PathLayer('roots') #{ fill: bg_color; stroke: points; stroke-width: 1.5; }; let rightEyebrow = TextLayer('right-eyebrow') #{ font-family: font; font-size: 8; font-weight: 700; letter-spacing: 3; fill: fg_muted; text-anchor: start; }; let rightNote = TextLayer('right-note') #{ font-family: font; font-size: 8; letter-spacing: 0.5; fill: fg_auto; text-anchor: start; }; rightPanel.append(teeth, rightForm, tips, roots, rightEyebrow, rightNote); drawComb(rightForm); // segment(tooth): every labelled run, as a block you can draw in place. teeth.apply { for (tooth in rightForm.queryAll('segment(tooth)')) { tooth.block.draw(); } } // segment(tooth) endpoint: the joint at the end of each run — the tip. tips.apply { for (tip in rightForm.queryAll('segment(tooth) endpoint')) { circle(tip.x, tip.y, 3); } } // endpoint(root): the labelled joints, by their own name. roots.apply { for (root in rightForm.queryAll('endpoint(root)')) { circle(root.x, root.y, 3); } } rightEyebrow.apply { text(0, -18)`THREE QUESTIONS`; } rightNote.apply { text(0, 100)`segment(tooth): the runs, tinted`; text(0, 111)`segment(tooth) endpoint: the tips, solid`; text(0, 122)`endpoint(root): the roots, ringed`; } // ─── Divider ─────────────────────────────────────────────────── let divider = PathLayer('divider') #{ stroke: fg_hair; stroke-width: 0.5; fill: none; }; divider.apply { M 240 45 L 240 200 } segment(tooth) tints each labelled run; segment(tooth) endpoint finds the tip at the end of each run; endpoint(root) finds the joints by their own name.

The middle query is the one to notice. segment(tooth) endpoint is "the joint at the end of each tooth," and it works because a Segment is a run of commands and an Endpoint belongs to the command it ends. No index, no offset, no counting teeth.

Everything one statement drew

Shape functions emit several commands at once: a circle() is a move and two arcs, a roundRect() is lines and quadratics. The call noun groups commands by the statement that produced them, which is usually the unit you were thinking in.

//-- Everything one statement drew. Left: a control knob from four stdlib //-- calls. Right: `queryAll('call')` — one bounding box and one name per //-- statement, the unit a shape function emits. define ViewBox(0, 0, 480, 230); // ─── Core tokens ─────────────────────────────────────────────── let bg_color = Color(CSSVar('--bg', #d0d7f0)); let fg_auto = Color('#0d1638'); let fg_muted = Color('#0d1638').alpha(0.6); let fg_hair = Color('#0d1638').alpha(0.22); let accent = oklch(0.55 0.16 200); let font = 'sans-serif'; let bg = PathLayer('bg') #{ fill: bg_color; stroke: none; }; bg.apply { rect(0, 0, 480, 230); } // ─── The form: four calls ────────────────────────────────────── fn drawKnob(target) { target.apply { circle(70, 50, 44); roundRect(67, 12, 6, 40, 3); circle(70, 50, 7); circle(100, 22, 3); } } // ─── Left panel ──────────────────────────────────────────────── let leftPanel = GroupLayer('left') #{ translate-x: 40; translate-y: 70; }; let leftForm = PathLayer('left-form') #{ stroke: fg_auto; stroke-width: 1.5; fill: none; }; let leftEyebrow = TextLayer('left-eyebrow') #{ font-family: font; font-size: 8; font-weight: 700; letter-spacing: 3; fill: fg_muted; text-anchor: start; }; let leftNote = TextLayer('left-note') #{ font-family: font; font-size: 8; letter-spacing: 0.5; fill: fg_auto; text-anchor: start; }; leftPanel.append(leftForm, leftEyebrow, leftNote); drawKnob(leftForm); leftEyebrow.apply { text(0, -18)`FOUR STATEMENTS`; } leftNote.apply { text(0, 116)`bezel, pointer, cap, detent — four statements`; } // ─── Right panel ─────────────────────────────────────────────── let rightPanel = GroupLayer('right') #{ translate-x: 270; translate-y: 70; }; let rightForm = PathLayer('right-form') #{ stroke: fg_auto; stroke-width: 1.5; fill: none; }; let boxes = PathLayer('boxes') #{ stroke: accent; stroke-width: 0.75; stroke-dasharray: 3 4; fill: none; }; let leaders = PathLayer('leaders') #{ stroke: accent; stroke-width: 0.5; fill: none; }; let names = TextLayer('names') #{ font-family: font; font-size: 7; letter-spacing: 0.8; fill: accent; text-anchor: start; }; let rightEyebrow = TextLayer('right-eyebrow') #{ font-family: font; font-size: 8; font-weight: 700; letter-spacing: 3; fill: fg_muted; text-anchor: start; }; let rightNote = TextLayer('right-note') #{ font-family: font; font-size: 8; letter-spacing: 0.5; fill: fg_auto; text-anchor: start; }; rightPanel.append(rightForm, boxes, leaders, names, rightEyebrow, rightNote); drawKnob(rightForm); // call: everything one statement emitted, with the function that emitted it. let calls = rightForm.queryAll('call'); boxes.apply { for (drawn in calls) { let bounds = drawn.block.boundingBox(); rect(bounds.x - 3, bounds.y - 3, bounds.width + 6, bounds.height + 6); } } // Names in a column beside the form. Rows follow each box's height (ties go // to the wider box), so no leader crosses another and each crosses only the // outline that contains it. let byHeight = calls.sort() {|a, b| let boxA = a.block.boundingBox(); let boxB = b.block.boundingBox(); let dy = boxA.y + boxA.height / 2 - (boxB.y + boxB.height / 2); if (abs(dy) > 0.5) { return dy; } return boxB.x + boxB.width - (boxA.x + boxA.width); }; leaders.apply { for ([drawn, row] in byHeight) { let bounds = drawn.block.boundingBox(); let labelY = 12 + row * 18; M calc(bounds.x + bounds.width + 3) calc(bounds.y + bounds.height / 2) L 132 labelY } } names.apply { for ([drawn, row] in byHeight) { let labelY = 12 + row * 18; text(136, calc(labelY + 2.5))`${drawn.index} ${drawn.name}`; } } rightEyebrow.apply { text(0, -18)`CALL, ASKED`; } rightNote.apply { text(0, 116)`one box per statement, named`; } // ─── Divider ─────────────────────────────────────────────────── let divider = PathLayer('divider') #{ stroke: fg_hair; stroke-width: 0.5; fill: none; }; divider.apply { M 240 45 L 240 200 } queryAll('call') returns one Call per statement, each with the commands it emitted, a block to measure, and the name of the function that drew it.

Each Call carries name, its commands, and a block you can measure or redraw. The twin draws a box around every statement's bounding box and leads a label out to the side. call(circle) narrows to the three circles; call(circle) command(a) is their arcs. One rule to know: a call is the statement you wrote. A circle() inside a function you called is reachable as call(myFn), not as call(circle).

Filters, and ranges the language already knows

Square brackets test one scalar property of each match, with the comparison operators you expect. Position pseudo-selectors pick from whatever list the rest of the query built, and :nth takes the same spellings for loops and .slice() use: a single index, a..b, a..<b, and negative numbers that count from the end.

//-- Filters and the language's own ranges. Three squares of three sizes. //-- Right: `command(line, close)[length>40]` tinted, `subpath(1..2)` filled faintly, //-- and `endpoint:nth(-3..-1)` — the last three corners drawn — dotted. define ViewBox(0, 0, 480, 230); // ─── Core tokens ─────────────────────────────────────────────── let bg_color = Color(CSSVar('--bg', #d0d7f0)); let fg_auto = Color('#0d1638'); let fg_muted = Color('#0d1638').alpha(0.6); let fg_hair = Color('#0d1638').alpha(0.22); let fg_faint = Color('#0d1638').alpha(0.1); let accent = oklch(0.55 0.16 27); let accent2 = oklch(0.55 0.16 260); let font = 'sans-serif'; let bg = PathLayer('bg') #{ fill: bg_color; stroke: none; }; bg.apply { rect(0, 0, 480, 230); } // ─── The form: three squares, three sizes ────────────────────── fn drawSquares(target) { target.apply { M 0 50 h 28 v 28 h -28 z M 44 32 h 46 v 46 h -46 z M 106 18 h 60 v 60 h -60 z } } // ─── Left panel ──────────────────────────────────────────────── let leftPanel = GroupLayer('left') #{ translate-x: 40; translate-y: 70; }; let leftForm = PathLayer('left-form') #{ stroke: fg_auto; stroke-width: 1.5; fill: none; }; let leftEyebrow = TextLayer('left-eyebrow') #{ font-family: font; font-size: 8; font-weight: 700; letter-spacing: 3; fill: fg_muted; text-anchor: start; }; let leftNote = TextLayer('left-note') #{ font-family: font; font-size: 8; letter-spacing: 0.5; fill: fg_auto; text-anchor: start; }; leftPanel.append(leftForm, leftEyebrow, leftNote); drawSquares(leftForm); leftEyebrow.apply { text(0, -18)`THREE SUBPATHS`; } leftNote.apply { text(0, 100)`edges of 28, 46 and 60`; } // ─── Right panel ─────────────────────────────────────────────── let rightPanel = GroupLayer('right') #{ translate-x: 270; translate-y: 70; }; let fills = PathLayer('fills') #{ fill: fg_faint; stroke: none; }; let rightForm = PathLayer('right-form') #{ stroke: fg_auto; stroke-width: 1.5; fill: none; }; let longEdges = PathLayer('long-edges') #{ stroke: accent; stroke-width: 3.5; fill: none; stroke-linecap: round; }; let lastThree = PathLayer('last-three') #{ fill: accent2; stroke: none; }; let rightEyebrow = TextLayer('right-eyebrow') #{ font-family: font; font-size: 8; font-weight: 700; letter-spacing: 3; fill: fg_muted; text-anchor: start; }; let rightNote = TextLayer('right-note') #{ font-family: font; font-size: 8; letter-spacing: 0.5; fill: fg_auto; text-anchor: start; }; rightPanel.append(fills, rightForm, longEdges, lastThree, rightEyebrow, rightNote); drawSquares(rightForm); // subpath(1..2): the second and third pen-down runs, as blocks — filled. fills.apply { for (run in rightForm.queryAll('subpath(1..2)')) { run.block.draw(); } } // command(line, close)[length>40]: shape words combine, and the filter tests a // scalar property of each command — the closing edge counts when it has length. longEdges.apply { for (edge in rightForm.queryAll('command(line, close)[length>40]')) { edge.block.draw(); } } // endpoint:nth(-3..-1): negative indexes count from the end, as in .slice(). lastThree.apply { for (corner in rightForm.queryAll('endpoint:nth(-3..-1)')) { circle(corner.x, corner.y, 3); } } rightEyebrow.apply { text(0, -18)`FILTERED, RANGED`; } rightNote.apply { text(0, 100)`[length>40] · subpath(1..2) · nth(-3..-1)`; } // ─── Divider ─────────────────────────────────────────────────── let divider = PathLayer('divider') #{ stroke: fg_hair; stroke-width: 0.5; fill: none; }; divider.apply { M 240 45 L 240 200 } SVG preview

:nth counts inside whatever came before it. subpath(1) command:nth(0) is the first command of that subpath; command:nth(0) is the first command of the whole path. The docs walk through a three-square example if the distinction is not yet sitting right.

Same word, two jobs

There is a subPath() method on path blocks that predates all of this, and it does something different: it slices a path between two arc-length fractions. The subpath noun selects whole pen-down runs, the SVG notion: a run starts at every move, and again after a z if drawing continues without one. One word, two unrelated jobs, and both are useful.

//-- Same word, two jobs. One path with two pen-down runs. Middle: the //-- `subpath` noun selects those runs by the SVG rule, banded by index. //-- Right: the `.subPath(0.2, 0.7)` method slices the same path by //-- arc-length fraction, run boundaries ignored. define ViewBox(0, 0, 480, 230); // ─── Core tokens ─────────────────────────────────────────────── let bg_color = Color(CSSVar('--bg', #d0d7f0)); let fg_auto = Color('#0d1638'); let fg_muted = Color('#0d1638').alpha(0.6); let fg_hair = Color('#0d1638').alpha(0.22); let runA_color = oklch(0.55 0.16 80); let runB_color = oklch(0.55 0.16 320); let sliced = oklch(0.55 0.16 27); let font = 'sans-serif'; let bg = PathLayer('bg') #{ fill: bg_color; stroke: none; }; bg.apply { rect(0, 0, 480, 230); } // ─── The form: a closed square, then an open curve ───────────── let shape = @{ h 40 v 54 h -40 z m 56 54 c 14 -80 34 50 62 -50 }; fn eyebrowStyle() { return #{ font-family: font; font-size: 8; font-weight: 700; letter-spacing: 3; fill: fg_muted; text-anchor: start; }; } // ─── Left: the form ──────────────────────────────────────────── let leftPanel = GroupLayer('left') #{ translate-x: 22; translate-y: 80; }; let leftForm = PathLayer('left-form') #{ stroke: fg_auto; stroke-width: 1.5; fill: none; }; let leftEyebrow = TextLayer('left-eyebrow') << eyebrowStyle(); let leftNote = TextLayer('left-note') #{ font-family: font; font-size: 8; letter-spacing: 0.5; fill: fg_auto; text-anchor: start; }; leftPanel.append(leftForm, leftEyebrow, leftNote); leftForm.apply { M 0 0 shape.draw() } leftEyebrow.apply { text(0, -18)`ONE PATH`; } leftNote.apply { text(0, 88)`a closed run, then an open curve`; } // ─── Middle: subpath, the noun ───────────────────────────────── let midPanel = GroupLayer('mid') #{ translate-x: 176; translate-y: 80; }; let midForm = PathLayer('mid-form') #{ stroke: fg_hair; stroke-width: 1.5; fill: none; }; let runA = PathLayer('run-a') #{ stroke: runA_color; stroke-width: 3; fill: none; stroke-linejoin: round; }; let runB = PathLayer('run-b') #{ stroke: runB_color; stroke-width: 3; fill: none; }; let midEyebrow = TextLayer('mid-eyebrow') << eyebrowStyle(); let midNote = TextLayer('mid-note') #{ font-family: font; font-size: 8; letter-spacing: 0.5; fill: fg_auto; text-anchor: start; }; midPanel.append(midForm, runA, runB, midEyebrow, midNote); midForm.apply { M 0 0 shape.draw() } // subpath(0) and subpath(1): pen-down runs, delimited by the move. runA.apply { midForm.query('subpath(0)').block.draw(); } runB.apply { midForm.query('subpath(1)').block.draw(); } midEyebrow.apply { text(0, -18)`THE NOUN`; } midNote.apply { text(0, 88)`subpath(0), subpath(1)`; text(0, 100)`two runs, by the SVG rule`; } // ─── Right: subPath(t0, t1), the method ──────────────────────── let rightPanel = GroupLayer('right') #{ translate-x: 330; translate-y: 80; }; let rightForm = PathLayer('right-form') #{ stroke: fg_hair; stroke-width: 1.5; fill: none; }; let slice = PathLayer('slice') #{ stroke: sliced; stroke-width: 3; fill: none; stroke-linecap: round; }; let rightEyebrow = TextLayer('right-eyebrow') << eyebrowStyle(); let rightNote = TextLayer('right-note') #{ font-family: font; font-size: 8; letter-spacing: 0.5; fill: fg_auto; text-anchor: start; }; rightPanel.append(rightForm, slice, rightEyebrow, rightNote); rightForm.apply { M 0 0 shape.draw() } // .subPath(0.2, 0.7): twenty to seventy percent of the arc length, whatever // runs that crosses. The slice comes back re-based, so it is placed where it started. let placed = shape.project(0, 0); let piece = placed.subPath(0.2, 0.7); let pieceStart = placed.get(0.2); slice.apply { piece.drawTo(pieceStart.x, pieceStart.y); } rightEyebrow.apply { text(0, -18)`THE METHOD`; } rightNote.apply { text(0, 88)`subPath(0.2, 0.7)`; text(0, 100)`a slice by arc length`; } // ─── Dividers ────────────────────────────────────────────────── let dividerA = PathLayer('divider-a') #{ stroke: fg_hair; stroke-width: 0.5; fill: none; }; let dividerB = PathLayer('divider-b') #{ stroke: fg_hair; stroke-width: 0.5; fill: none; }; dividerA.apply { M 163 45 L 163 200 } dividerB.apply { M 317 45 L 317 200 } Middle: the subpath noun, one colour per run. Right: subPath(0.2, 0.7), a slice by arc length that crosses from the square into the curve — the move between them survives.

If you want a run, ask with the noun. If you want twenty to seventy percent of the ink, call the method.

What comes back

Every result is a struct: read members with ., or destructure with let { x, y } = corner;. Command has the command letter, args, start and end, index, length, a one-command block, its labels, and kind-specific members — cp1 and cp2 for cubics, cp for quadratics, rx, ry, rotation, largeArc, sweep and center for arcs. Endpoint has point, label, command, next, turn and isJoint, plus the corner operations the old vertex handle had. Call, Segment and Subpath each carry their commands and a block. The full tables are in What comes back.

Two consequences fall out. PathBlock.commands now returns the same Command struct, so a program that read cmd.end before keeps working and now also sees labels. And segment(), point() and vertex(), the label shortcuts from Name Your Corners, are unchanged — they are the shortest spelling when all you want is a labelled block or point, and the docs list what each is sugar for.

What this project taught the language

One more thing this series is: a working friction log. Every sample was built against the real language, and where one exposed a bug or a missing piece, the fix went back into Pathogen before the post shipped. Each post grows this closing section to tell that story, ordered by the example that hit it.

The one-line draw idiom started recording what it draws. Every panel in this post draws its form with M 0 0 tab.draw() on one line, the spelling the formatter itself produces. The layer's emitted path was always right, but its structured record kept only the M, and the block's commands were tracked from the pen position before the move. So the first twin had no dots: queryAll('endpoint') found nothing past the move, and ctx.position after the statement sat at the move rather than the block's end. The evaluator now snapshots the context before a command's arguments evaluate and replays the whole emitted fragment in order when an argument drew something. Path Queries and every layer query since depend on it.

A circle finally measures its circumference. The second sample's note originally read the circle's length back to prove the arc struct was real, and the number was 4r. Arc length had always been taken from the chord alone, so a half circle counted as its diameter and a large arc as its minor complement. Arcs now go through the same endpoint-to-centre solver that gives Command.center: circular arcs exactly, elliptical ones by integrating the true speed. partition() and get(t) on a path that mixes lines with half circles weight the arc correctly for the first time. Published sample output was byte-identical before and after.

Blocks taken from a layer draw in place whatever case you typed. The faint subpath(1..2) fills in the fifth sample first landed nowhere near their squares. A run copied from a layer keeps the letters you authored, and the relative serializer compared them case-sensitively, so a run beginning with your M was emitted with an absolute letter and relative numbers. It compares lowercase now, which also fixes an uppercase L in a segment() drawn in place.

subPath() keeps the move between runs. The sixth sample's slice crosses from the square into the curve, and the first render drew a straight line across the gap. Moves had been filtered out to measure arc length and never put back, so the second run's curve was spliced onto the end of the first run's z. A gap between fragments is now a move. And when a slice cuts a closed run short, its z now closes the run's real edge rather than snapping back to the slice's own start, which is what the right-hand panel shows.

// before: the slice ran the curve from where the z ended
l 0 34 h -40 z c 1.78 -10.19 3.66 -16.97 5.65 -21.26

// after: the close is a real edge, the move survives, the curve starts where it should
l 0 34 h -40 l 0 -54 m 56 54 c 1.78 -10.19 3.66 -16.97 5.65 -21.26

Two members had to dodge keywords. fn and layer are reserved words, so the obvious call.fn and subscription.layer cannot be parsed after a dot. The members are Call.name and, in the next post, Subscription.source. The docs say so where each struct is listed.

The design system and the style sanitizer disagreed about fonts. The example design system asks for a quoted font stack; the style-value allow-list rejects the quotes. Every sample here binds a bare sans-serif, as the published samples before it quietly did. One of the two documents is wrong, and that is logged rather than papered over.

Where to go next

Part 2 lets a layer answer these questions by itself: subscribe a selector to a layer and the twin's annotations are drawn for you once the program finishes, once per match, in drawing order, wherever in the program the drawing happened. The finished pieces start in part 3, where three projects put both to work: a linkage, a front panel, a fretboard.

The reference is Path Queries. Every sample above is live in one step: the code panel is read-only, but the "Open in playground workspace" button drops it into an editor where you can add a command, move a corner, change a radius, and watch the twin follow. Or start from a blank one in the playground.