Stained Glass: Seams as Leading, Pieces as Panes

Part 4 of 4 in The Cutting Room — projects that put cut() and segment labels to work together.

Series: The Cutting Room

  1. Papercraft — cut lines, fold lines, and glue tabs from one plate
  2. Jigsaw — wavy knives, piece identity, and a scattered puzzle
  3. Garment patterns — named edges, seam allowances, and notches
  4. Stained glass (this post) — tinted panes, leading, and a rose window

Prerequisites: PathBlock.cut() and segment labels. The boolean operations post helps for Example 4, and part 1's seam-stroking idiom is assumed throughout.

What it does

In every project so far, the seams were annotation — fold here, align there. Stained glass inverts that: the seams are the picture. Lead came is nothing but the network of joints between panes, which means a stained-glass window is segmentAll('cut'), stroked wide, over pieces that have been filled with color. The finale also brings in the last label superpower this series has not used yet: boolean results keep the labels of both operands, so a frame made by difference() still answers for the outer shape's names and the opening's.

Why you'd use it

Because "decorate every joint" is a one-loop job when joints are queryable, and an evening of coordinate surgery when they are not. The whole aesthetic rests on the idiom from part 1 — this time as the main event rather than the annotation:

for (seam in placed.segmentAll('cut')) {
  seam.draw();    // stroked wide, this IS the leading
}

Example 1 — Seams as the artwork

A glass disc, four straight knives through the center, eight panes. The leading between panes is the seam group stroked at width 5 in the background color — no leading was drawn as such; it is the cut, made visible.

// viewBox="0 0 480 250" //-- Seams as the artwork: a glass disc cut by four radial knives becomes //-- eight panes, and stroking every healed seam wide draws the lead came //-- between them — the decoration IS the seam group. define ViewBox(0, 0, 480, 250); let bg = PathLayer('bg') ${ fill: #0f172a; stroke: none; }; layer('bg').apply { rect(0, 0, 480, 250); } let scene = GroupLayer('scene') ${}; let panes = PathLayer('panes') ${ fill: #38bdf8; stroke: none; }; let leading = PathLayer('leading') ${ stroke: #0f172a; stroke-width: 5; fill: none; stroke-linecap: round; }; let labels = TextLayer('labels') ${ font-family: monospace; font-size: 9; fill: #94a3b8; text-anchor: middle; }; scene.append(panes, leading, labels); let disc = @{ circle(0, 0, 88); }; // Four straight knives through the center, 45 degrees apart. let knifeReach = 104; // Four straight knives through the center, 45 degrees apart — built in // a loop and handed to cut() as an array. let knives = []; for (k in 0..3) { let knifeAngle = calc(k * PI() / 4); let dirX = cos(knifeAngle); let dirY = sin(knifeAngle); knives.push(@{ m calc(0 - knifeReach * dirX) calc(0 - knifeReach * dirY) l calc(knifeReach * 2 * dirX) calc(knifeReach * 2 * dirY) }); } let wedges = disc.cut(knives); let originX = 240; let originY = 122; for (wedge in wedges) { panes.apply { M originX originY wedge.draw() } let placed = wedge.project(originX, originY); leading.apply { for (seam in placed.segmentAll('cut')) { seam.draw(); } } } labels.apply { text(240, 232)`eight panes, one loop - the leading is segmentAll('cut'), stroked wide`; } Eight panes; the came between them is segmentAll('cut') stroked wide.

Compare part 1's Example 1, where the same query produced a thin amber highlight. Same seams, same loop — the meaning of a seam is entirely in how you draw it.

Example 2 — Your own geometry joins the came

The seam group takes volunteers. cut itself is reserved — the language stamps it on healed seams and won't let you author it, so your geometry can never fuse silently into the seams — but cut.<name> is the explicit opt-in: a label that says, out loud, "this edge belongs with the seams." Name the disc's rim as segment('cut.rim') and the one came loop picks it up with zero extra code, because the umbrella query segmentAll('cut') answers the whole namespace. Left disc: rim labeled 'rim', and the glass meets the stone bare. Right disc: rim labeled 'cut.rim', full came — and still addressable on its own as segmentAll('cut.rim') when the rim needs its own pass, or arc by arc as segmentAll('cut.rim:atomic') via query pseudo-selectors. (See label names for the rules.)

Note the symmetry with part 1, which approached the same namespace from the other side: there you labeled the knife and the cut. prefix appeared automatically on the seams it healed — sub-labels to distinguish seams from each other. Here you write cut. yourself on your own geometry — an opt-in to join them. Two directions, one namespace, one umbrella query.

// viewBox="0 0 480 260" //-- The seam group takes volunteers: 'cut.<name>' is the explicit opt-in //-- that joins your own geometry to the came. Left: rim labeled 'rim' — //-- a bare edge the seam loop ignores. Right: rim labeled 'cut.rim' — //-- same loop, full came, and the rim stays addressable by name. define ViewBox(0, 0, 480, 260); let bg = PathLayer('bg') ${ fill: #0f172a; stroke: none; }; layer('bg').apply { rect(0, 0, 480, 260); } let scene = GroupLayer('scene') ${}; let stone = PathLayer('stone') ${ fill: #64748b; stroke: none; }; let panes = PathLayer('panes') ${ fill: #a78bfa; stroke: none; }; let leading = PathLayer('leading') ${ stroke: #0f172a; stroke-width: 5; fill: none; stroke-linecap: round; }; let labels = TextLayer('labels') ${ font-family: monospace; font-size: 9; fill: #94a3b8; text-anchor: middle; }; scene.append(stone, panes, leading, labels); // A stone surround behind both discs so the dark came reads against it. stone.apply { circle(128, 112, 82); circle(352, 112, 82); } // The same disc twice — only the rim's label differs. An as-clause on // a stdlib call names everything the call draws, so one label covers // the whole circle. let discA = @{ circle(0, 0, 70) as segment('rim'); }; let discB = @{ circle(0, 0, 70) as segment('cut.rim'); }; let knives = @{ m -85 0 l 170 0 m -85 -85 l 0 170 }; fn cameWindow(disc, centerX, centerY) { let pieces = disc.cut(knives); for (piece in pieces) { panes.apply { M centerX centerY piece.draw() } let placed = piece.project(centerX, centerY); // One decoration loop, both windows: stroke the 'cut' group wide. leading.apply { for (seam in placed.segmentAll('cut')) { seam.draw(); } } } } cameWindow(discA, 128, 112); cameWindow(discB, 352, 112); labels.apply { text(128, 226)`rim labeled 'rim' - bare edge`; text(352, 226)`rim labeled 'cut.rim' - it joins the came`; } Same decoration loop on both windows; only the rim's label name differs.

Note the labeling shortcut, too: an as clause on a stdlib call like circle() names everything the call draws, so one clause labels the whole rim — no need to author the arcs by hand.

Example 3 — Tinted panes

The picture emerges. Pieces are full PathBlocks, so coloring them is layer routing: four glass layers with jewel-tone fills, pieces dealt round-robin, came stroked over the top. (The same routing pattern the cutting post used for its shattered glyph.)

// viewBox="0 0 480 250" //-- The picture emerges: the same eight panes, each routed to one of //-- four glass layers — jewel tints round-robin — with the came stroked //-- on top. Pieces are full PathBlocks, so styling them is layer routing. define ViewBox(0, 0, 480, 250); let bg = PathLayer('bg') ${ fill: #0f172a; stroke: none; }; layer('bg').apply { rect(0, 0, 480, 250); } let scene = GroupLayer('scene') ${}; let glass0 = PathLayer('glass0') ${ fill: #38bdf8; stroke: none; }; let glass1 = PathLayer('glass1') ${ fill: #a78bfa; stroke: none; }; let glass2 = PathLayer('glass2') ${ fill: #2dd4bf; stroke: none; }; let glass3 = PathLayer('glass3') ${ fill: #fbbf24; stroke: none; }; let leading = PathLayer('leading') ${ stroke: #0f172a; stroke-width: 5; fill: none; stroke-linecap: round; }; let labels = TextLayer('labels') ${ font-family: monospace; font-size: 9; fill: #94a3b8; text-anchor: middle; }; scene.append(glass0, glass1, glass2, glass3, leading, labels); let disc = @{ circle(0, 0, 88); }; let knifeReach = 104; // Four straight knives through the center, 45 degrees apart — built in // a loop and handed to cut() as an array. let knives = []; for (k in 0..3) { let knifeAngle = calc(k * PI() / 4); let dirX = cos(knifeAngle); let dirY = sin(knifeAngle); knives.push(@{ m calc(0 - knifeReach * dirX) calc(0 - knifeReach * dirY) l calc(knifeReach * 2 * dirX) calc(knifeReach * 2 * dirY) }); } let wedges = disc.cut(knives); let originX = 240; let originY = 122; for ([wedge, i] in wedges) { // Deal the jewel tones round-robin by computed layer name. layer(`glass${i % 4}`).apply { M originX originY wedge.draw() } let placed = wedge.project(originX, originY); leading.apply { for (seam in placed.segmentAll('cut')) { seam.draw(); } } } labels.apply { text(240, 232)`four glass layers, pieces routed round-robin - came on top`; } Four glass layers, pieces routed by index, leading on top.

Routing by index is the bluntest instrument — part 2 routed by label (rim or not), and part 3 by name. Pick the classifier that matches the design; the mechanism is the same apply block either way.

Example 4 — The frame keeps both names

Boolean results carry labels from both operands. This window frame is outer.difference(opening) — the outer square's bottom edge brought 'sill', the opening's two side edges brought 'light', and the frame answers for both names. Then a diagonal cut splits the frame, and each half still answers for whatever it kept: the lower half holds most of the sill, the upper half a shorter stretch of it, and each keeps one of the opening's sides.

// viewBox="0 0 480 260" //-- Labels from both operands coexist: a frame made by difference keeps //-- the outer square's 'sill' AND the inner square's 'light' — and after //-- a diagonal cut, both halves still answer for both names. define ViewBox(0, 0, 480, 260); let bg = PathLayer('bg') ${ fill: #0f172a; stroke: none; }; layer('bg').apply { rect(0, 0, 480, 260); } let scene = GroupLayer('scene') ${}; let frame = PathLayer('frame') ${ fill: #e7dfd0; stroke: #0f172a; stroke-width: 1.5; }; let sillRun = PathLayer('sill-run') ${ stroke: #f59e0b; stroke-width: 3.5; fill: none; stroke-linecap: round; }; let lightRun = PathLayer('light-run') ${ stroke: #2dd4bf; stroke-width: 3.5; fill: none; stroke-linecap: round; }; let legendSill = TextLayer('legend-sill') ${ font-family: monospace; font-size: 9; fill: #f59e0b; }; let legendLight = TextLayer('legend-light') ${ font-family: monospace; font-size: 9; fill: #2dd4bf; }; scene.append(frame, sillRun, lightRun, legendSill, legendLight); let outer = @{ h 170 v 150 h -170 as segment('sill'); z }; let opening = @{ h 110 v 90 as segment('light'); h -110 v -90 as segment('light'); z }; let ring = outer.project(155, 40).difference(opening.project(185, 70)); let knife = @{ m 150 25 l 160 180 }; let halves = ring.cut(knife); for (half in halves) { // Open the cut along the diagonal: a ±10 nudge perpendicular to it. // draw() keeps each half's own placement, so the nudge is all the // move there is. let bounds = half.boundingBox(); let side = calc(bounds.x + bounds.width / 2 < 240 ? -10 : 10); frame.apply { M calc(side) calc(0 - side) half.draw() } let placed = half.project(side, calc(0 - side)); sillRun.apply { for (run in placed.segmentAll('sill')) { run.draw(); } } lightRun.apply { for (run in placed.segmentAll('light')) { run.draw(); } } } legendSill.apply { text(352, 110)`'sill' = bottom edge`; } legendLight.apply { text(352, 128)`'light' = the sides`; } difference() merges label sets; cut() distributes them to the halves.

This is the compositional guarantee the whole series stands on: labels survive chains of operations — draft, subtract, cut — not just single steps.

Example 5 — The rose window

The finale composes everything. The knives are built in a loop — eight spokes pushed onto an array, plus a closed ring knife (a cookie cutter) — and the whole set goes to one cut([...]) call, which stamps out the golden center medallion and slices the surrounding ring into eight panes. The spokes stop short of the ring, leaving the medallion whole. Then:

  • the medallion is found by classification — it is the piece that kept none of the rim label (part 2's trick, part 3's workflow);
  • the panes are tinted by layer routing (Example 3's deal, spelled layer(`glass${i % 3}`) — amber sits out of the rotation because it belongs to the medallion);
  • the came strokes seams and rim in one loop — the rim opted into the seam group with cut.rim (Example 2's trick), so the umbrella query covers both;
  • the solder dots sit at every seam's startPoint and endPoint, where real joints get soldered (on the medallion's closed boundary the two coincide — one dot, drawn twice, harmlessly).

// viewBox="0 0 480 320" //-- The rose window: a cookie-cutter ring knife stamps out the center //-- medallion while eight spokes (stopping short of it) slice the ring //-- into panes. Rim labels sort medallion from ring; the came and solder //-- dots are all seam queries. define ViewBox(0, 0, 480, 320); let bg = PathLayer('bg') ${ fill: #0f172a; stroke: none; }; layer('bg').apply { rect(0, 0, 480, 320); } let scene = GroupLayer('scene') ${}; let stone = PathLayer('stone') ${ fill: #1e293b; stroke: #334155; stroke-width: 2; }; let glass0 = PathLayer('glass0') ${ fill: #38bdf8; stroke: none; }; let glass1 = PathLayer('glass1') ${ fill: #a78bfa; stroke: none; }; let glass2 = PathLayer('glass2') ${ fill: #2dd4bf; stroke: none; }; let medallion = PathLayer('medallion') ${ fill: #fbbf24; stroke: none; }; let leading = PathLayer('leading') ${ stroke: #0f172a; stroke-width: 4.5; fill: none; stroke-linecap: round; }; let solder = PathLayer('solder') ${ fill: #cbd5e1; stroke: none; }; let title = TextLayer('title') ${ font-family: monospace; font-size: 10; fill: #94a3b8; text-anchor: middle; letter-spacing: 3; }; scene.append(stone, glass0, glass1, glass2, medallion, leading, solder, title); // The glass disc — the as-clause on circle() labels the whole rim, // and the cut.rim opt-in joins it to the seam group (Example 2's // trick), so ONE came loop strokes seams and rim together. let disc = @{ circle(0, 0, 92) as segment('cut.rim'); }; // Eight spokes from r=36 out past the rim — one knife per spoke, built // in a loop — plus a ring knife that stamps out the medallion. The // whole set goes to cut() as an array; the spokes stop short of the // ring so the medallion stays whole. let knives = []; for (k in 0..7) { let spokeAngle = calc(k * PI() / 4); let dirX = cos(spokeAngle); let dirY = sin(spokeAngle); knives.push(@{ m calc(36 * dirX) calc(36 * dirY) l calc(76 * dirX) calc(76 * dirY) }); } knives.push(@{ circle(0, 0, 36); }); let panes = disc.cut(knives); let originX = 240; let originY = 158; stone.apply { circle(originX, originY, 112); } for ([piece, i] in panes) { let placed = piece.project(originX, originY); // The medallion is the piece that kept none of the rim. if (placed.segmentAll('cut.rim').length > 0) { // Deal the three glass tones round-robin by computed layer name — // the fourth jewel tone, amber, is reserved for the medallion. layer(`glass${i % 3}`).apply { M originX originY piece.draw() } } else { medallion.apply { M originX originY piece.draw() } } // Came on every seam AND the opted-in rim — one umbrella query. leading.apply { for (seam in placed.segmentAll('cut')) { seam.draw(); } } // Solder dots where seams start and end. solder.apply { for (seam in placed.segmentAll('cut')) { circle(seam.startPoint.x, seam.startPoint.y, 2.2); circle(seam.endPoint.x, seam.endPoint.y, 2.2); } } } title.apply { text(240, 34)`ROSE WINDOW`; } One cut: cookie-cutter medallion plus eight spoke panes — came, tints, and solder all queried.

Every technique in it was introduced as a bare mechanism somewhere in these four posts. That is the shape of the whole series: the seams know where they are, the pieces know what they kept, and everything else is a loop.

What this project taught the language

This series doubles as a working friction log (part 1 explains the convention). The leading loop above originally read seam.drawTo(seam.startPoint.x, seam.startPoint.y) — projected values have since grown an in-place draw(), and every came stroke in this post got one line simpler. Part 1's closing section tells the story; the garment post's tells its darker sibling — the same expression once silently misplaced whole cut pieces — along with the epilogue where that hazard was fixed at the root.

(The came loops in this post keep their per-piece form deliberately — each pane declaring its own boundary is the teaching. When you want each physical seam once instead — solder budgets, fold lines — pieces.seams() now exists; part 1's closing section shows it.)

The rose window's knives became a loop. The first version of Example 5 hand-chained eight spokes in one cutter block — sixteen lines of relative-move arithmetic between stroke endpoints, the same pattern that caused two authoring bugs elsewhere in the series. cut() now accepts an array of cutters, so the spokes are pushed onto a list in a for loop and handed over in one call — knife geometry you can parameterize. Change 0..7 to 0..11 and the window grows four panes.

// before: one block, every m computed from the previous stroke's end
m calc(36 * cos45 - 112) calc(36 * cos45)
l calc(76 * cos45) calc(76 * cos45)
// ...six more chained pairs

// after: one knife per spoke, built in a loop
for (k in 0..7) {
  let spokeAngle = calc(k * PI() / 4);
  knives.push(@{
    m calc(36 * cos(spokeAngle)) calc(36 * sin(spokeAngle))
    l calc(76 * cos(spokeAngle)) calc(76 * sin(spokeAngle))
  });
}
let panes = disc.cut(knives);

Example 2's trick became a contract. The rim-joins-the-came demo originally leaned on an accident: cut was an ordinary label, so naming your own geometry 'cut' happened to merge it with the seams — silently, and with no way back out. Working the friction log turned the accident into a named rule: bare 'cut' is now reserved (authoring it is a compile error), and cut.<name> is the explicit opt-in the sample now uses. Same render, byte for byte — but the rim reads as a decision instead of a coincidence, and segmentAll('cut.rim') can still address it alone.

Where to go next