Skip to content

guide.kt

src/jvmMain/kotlin/afield/ai/learning/pen/guide.kt · 103 lines

Guided sampling for a learned pen: the pen proposes several next points and a PenGuide picks one. The pen keeps its hand (its tremor, its rhythm, its habits) while the guide decides where it goes. FollowGuide keeps a pen a set distance from the ink beside it, which is hatching. Tests: PenHatchTest.

package afield.ai.learning.pen

import afield.geometry.path.FLine
import afield.math.linalg.Vec3
import kotlin.math.floor
import kotlin.math.hypot
import kotlin.math.max

interface PenGuide

Chooses among a pen's proposals for its next point; set one on PenLine.guide.

interface PenGuide {

val candidates: Int

How many proposals the pen makes each point.

fun score(from: Vec3, to: Vec3, lift: Boolean, penUp: Boolean): Double

How bad a proposal is, lower is better: a step from from to to in drawing units, after which the pen lifts if lift. penUp says whether this step is a move rather than a mark.

fun drawn(at: Vec3, lift: Boolean) {}

The pen drew the chosen point; a guide that tracks ink records it here.

class FollowGuide

Keeps a pen gap drawing units from the ink beside it, without crossing any: hatching.

The ink is whatever was given to see and everything the pen has drawn, except its own last recent points. A proposal scores by how far its distance to that ink is from gap, with a heavy cost for coming closer than half the gap, for a step longer than longest (a jump that happens to land at the right distance from some far-off ink is not following anything), and for lifting the pen. With nothing in reach (more than reach away) only step length counts.

class FollowGuide(
    val gap: Double = 4.0,
    override val candidates: Int = 12,
    val recent: Int = 10,
    val reach: Double = 30.0,
    val liftCost: Double = 2.0,
    val longest: Double = 6.0,
) : PenGuide {
    private val xs = ArrayList<Double>()
    private val ys = ArrayList<Double>()
    private val grid = HashMap<Long, MutableList<Int>>()
    private val pending = ArrayDeque<Int>()

fun see(lines: List<FLine>, spacing: Double = 2.0)

Ink the pen should keep its distance from, visible at once: the drawing being hatched.

    fun see(lines: List<FLine>, spacing: Double = 2.0) {
        lines.forEach { l -> l.pieces().forEach { piece -> resampleForPen(piece, spacing).forEach { index(it.x.toDouble(), it.y.toDouble()) } } }
    }

override fun drawn(at: Vec3, lift: Boolean)

    override fun drawn(at: Vec3, lift: Boolean) {
        xs += at.x.toDouble(); ys += at.y.toDouble()
        pending.addLast(xs.size - 1)
        while (pending.size > recent) enter(pending.removeFirst())
    }

override fun score(from: Vec3, to: Vec3, lift: Boolean, penUp: Boolean): Double

    override fun score(from: Vec3, to: Vec3, lift: Boolean, penUp: Boolean): Double {
        val d = nearest(to.x.toDouble(), to.y.toDouble())
        var s = if (d > reach) 0.0 else ((d - gap) / gap).let { it * it }
        if (d < gap * 0.5) s += 50.0
        // a mark that passes over ink between its ends also crosses it
        if (!penUp) {
            val mx = (from.x + to.x) / 2.0
            val my = (from.y + to.y) / 2.0
            if (nearest(mx, my) < gap * 0.5) s += 50.0
        }
        val length = hypot((to.x - from.x).toDouble(), (to.y - from.y).toDouble())
        if (length > longest) s += 50.0 + length
        if (lift) s += liftCost
        return s
    }

    private fun index(x: Double, y: Double) {
        xs += x; ys += y
        enter(xs.size - 1)
    }

    private fun enter(i: Int) {
        grid.getOrPut(key(cell(xs[i]), cell(ys[i]))) { ArrayList() } += i
    }

    private fun nearest(x: Double, y: Double): Double {
        val gx = cell(x)
        val gy = cell(y)
        var best = Double.POSITIVE_INFINITY
        for (cx in gx - 1..gx + 1) for (cy in gy - 1..gy + 1) {
            for (i in grid[key(cx, cy)] ?: continue) best = minOf(best, hypot(xs[i] - x, ys[i] - y))
        }
        return best
    }

    private val size = max(reach, 1.0)
    private fun cell(v: Double) = floor(v / size).toInt()
    private fun key(cx: Int, cy: Int) = (cx.toLong() shl 32) xor (cy.toLong() and 0xffffffffL)
}