hatch.kt
src/jvmMain/kotlin/afield/ai/learning/pen/hatch.kt · 117 lines
Hatching around a drawing with a sensing pen: every stroke of the drawing gets passes of the pen's own trembling line laid beside it, which keep clear of the other strokes and of the hatching already done.
A pen cannot remember which strokes it has not hatched yet, so Hatcher chooses: it hands the
pen one stroke at a time as a finished primer, with everything else on the page as
surroundings, and moves on when the pen has drawn enough or wandered off. Tests: PenHatchTest.
package afield.ai.learning.pen
import afield.geometry.path.FLine
import afield.math.linalg.Vec3
class Hatcher
Hatches lines stroke by stroke with model, grow points at a time, so a box can watch it.
val hatcher = Hatcher(listOf(HersheyFont.load("futural").text("FIELD", size = 90.0)))
hatcher.finish()
lines(hatcher.hatching)
@param lines the drawing to hatch around; every subpath is a stroke.
@param model a sensing pen; "echoes" (the default) was trained for this.
@param passes roughly how many passes to lay beside each stroke: a stroke gets passes times its
own length in points.
@param wander how far, in drawing units, the pen may get from the stroke it is hatching before it
is moved on to the next.
@param gap when not null, steer the pen with a FollowGuide to keep this many drawing units from
the ink beside it; null leaves the pen to find its own way, which only a pen trained for
hatching can do.
class Hatcher(
lines: List<FLine>,
val model: StrokeModel = PenModel.bundled("echoes"),
val passes: Int = 6,
val scale: Double = 8.0,
// the learned spread: a guide needs proposals that differ to have something to choose from
var controls: PenControls = PenControls(temperature = 1.0, noise = 1.0),
val seed: Long = 0,
val step: Double = 0.35,
val wander: Double = 40.0,
val gap: Double? = 4.0,
) {
the strokes, longest first: a long stroke's hatching gives the short ones something to meet
private val strokes: List<FLine> = lines.flatMap { it.pieces() }
.filter { PenCorpus.strokes(it, scale, step).sumOf { s -> s.size } >= 6 }
.sortedByDescending { PenCorpus.strokes(it, scale, step).sumOf { s -> s.size } }
val hatching: MutableList<FLine> = ArrayList()
The hatching so far, one FLine per stroke started.
private var index = -1
private var writer: PenLine? = null
private var budget = 0
private var points: List<Vec3> = emptyList()
val done: Boolean get() = index >= strokes.size
True once every stroke has been hatched.
val current: FLine? get() = strokes.getOrNull(index)
The stroke being hatched now, or null when done.
fun grow(steps: Int = 8): Boolean
Draw up to steps more points; returns false once everything is hatched.
fun grow(steps: Int = 8): Boolean {
var left = steps
while (left > 0 && !done) {
val w = writer ?: next() ?: return false
val n = minOf(left, budget)
w.grow(n)
budget -= n
left -= n
if (budget <= 0 || nearest(w.tip) > wander) writer = null
}
return !done
}
fun finish(): List<FLine>
Hatch everything; returns hatching.
fun finish(): List<FLine> {
while (grow(256)) {}
return hatching
}
private fun next(): PenLine? {
index++
val stroke = strokes.getOrNull(index) ?: return null
val others = strokes.filterIndexed { i, _ -> i != index } + hatching
points = PenCorpus.strokes(stroke, scale, step).flatten()
budget = passes * points.size
// pen down at the stroke's end: guided, it carries on round the end and back beside the
// stroke; unguided, a finished stroke (a lift) is what precedes passes in the echoes corpus
val w = PenLine(stroke, model, scale, controls, seed + index, step, surroundings = others, liftAtEnd = gap == null)
// hatching around one stroke is one unbroken stroke: lifting would let the pen jump anywhere
if (gap != null) w.guide = FollowGuide(gap, candidates = 32, liftCost = 1000.0).also { it.see(strokes + hatching) }
hatching += w.drawn
writer = w
return w
}
private fun nearest(at: Vec3): Double = points.minOf { it.distance(at).toDouble() }
}
fun hatch
Hatch around lines with a sensing pen and return the hatching, one FLine per stroke: Hatcher
run to the end.
fun hatch(
lines: List<FLine>,
model: StrokeModel = PenModel.bundled("echoes"),
passes: Int = 6,
seed: Long = 0,
controls: PenControls = PenControls(temperature = 1.0, noise = 1.0),
gap: Double? = 4.0,
): List<FLine> = Hatcher(lines, model, passes, controls = controls, seed = seed, gap = gap).finish()