A Word Cut as a Stencil
Cut a word out of a sheet and some of the letters fall apart: the middle of an O drops out, and so do both middles of a B. A stencil needs a bridge across each of those holes. This page finds them with three parts of Field that know nothing about each other. Typography supplies the letter outlines, Manifold cuts them out of a plate and splits the result into pieces, and a Datalog program decides which letters lost a piece. The bridges then go into exactly those letters.

The example has three boxes, each a child of the one before: the first cuts the word, the second judges it, and the third adds the bridges and counts again.
Cutting the word
layoutText turns a string into placed glyphs, and toFLines gives each glyph's outline as an
FLine, holes wound the opposite way to outer rings. Section.ofFLines reads that winding, so the
counters stay open when Manifold fills the letters. The plate is the word's bounds grown by a
rounded margin, and decompose splits whatever is left after the subtraction into separate pieces:
stencilWord = layoutText("BRIDGE", typeface("SansSerif", 120.0))
val (lo, hi) = Section.ofFLines(stencilWord.toFLines(), sampleDistance = 0.5).bounds
stencilBlank =
Section.square((hi.x - lo.x).toDouble(), (hi.y - lo.y).toDouble())
.translated(lo.x.toDouble(), lo.y.toDouble())
.offset(14.0, JoinType.ROUND)
val letters = Section.ofFLines(stencilWord.toFLines(), sampleDistance = 0.5)
val pieces = (stencilBlank - letters).decompose()

The two counts come from different places. The font's count, holeCount, adds up the rings each
glyph winds as a hole. Manifold's count comes from the Boolean. The plate itself is one piece and
every counter is one more, so the two agree when the outlines and the subtraction are both right.
The box publishes the pieces and the glyphs as plain rows, an id followed by numbers, so the next box doesn't need Manifold or the font to read them:
stencilGlyphs =
stencilWord.glyphs.mapIndexed { g, p ->
[g, p.text, p.origin.x.toDouble(), p.origin.x.toDouble() + p.glyph.advance]
}
stencilPieces = pieces.mapIndexed { i, s ->
val (a, b) = s.bounds
[i, (a.x + b.x) / 2.0, (a.y + b.y) / 2.0, s.area]
}
Which letters fall apart
The second box hands both lists to Datalog as piece and glyph facts. The plate is the piece
with the largest area, and every other piece is an island. An island belongs to the glyph whose
advance contains its centre:
"""
#datalog
biggest(M) :- M = max A : { piece(_, _, _, A) }.
plate(P) :- piece(P, _, _, A), biggest(A).
island(P, X) :- piece(P, X, _, _), !plate(P).
falls(G, P) :- island(P, X), glyph(G, _, X0, X1), X > X0, X < X1.
lost(G, C, N) :- glyph(G, C, _, _), N = count : { falls(G, P) }, N > 0.
needs(G) :- falls(G, _).
safe(C) :- glyph(G, C, _, _), !needs(G).
"""

B loses two pieces, R and D one each. I, G and E have no counters, so cutting them leaves the
plate whole. The box publishes the indices of the three letters that need bridges as
needsBridge.
Testing the centre against the glyph's advance is enough for a word set on one line. It would need a vertical test as well for text on several lines.
Bridging only those letters
The third box cuts a vertical bar, 5 units wide, down the middle of each listed letter. It takes the bars out of the letters before the letters come out of the plate, so the bars stay behind as plate:
val bars = Section.union(
needsBridge.map { g ->
val p = stencilWord.glyphs[g]
val mid = p.origin.x + p.glyph.advance / 2.0
Section.square(5.0, (hi.y - lo.y).toDouble() + 2.0).translated(mid - 2.5, lo.y.toDouble() - 1.0)
},
)
val before = (stencilBlank - letters).decompose()
val after = (stencilBlank - (letters - bars)).decompose()
The figure at the top of the page is this box's output: the plate before bridging, with its islands in red, and after, in one piece. Each bar crosses every counter in its letter, so one bar is enough even for the two counters of B.
Where each piece is documented
- Typography as geometry for
layoutText,toFLinesandholeCount. - Manifold: 2D sections and offsets for
Section,offsetand the Booleans. - Datalog in a box for facts, rules, aggregates and negation.
- Agent transcripts to replay the three boxes into a document.