FLinesAndJavaShapes.kt
src/jvmMain/kotlin/afield/geometry/path/java2d/FLinesAndJavaShapes.kt · 2556 lines
package and imports — 84 lines, from line 1
package afield.geometry.path.java2d
import afield.app.protocol.Rect
import afield.geometry.path.FLine
import afield.geometry.path.PathFlattener
import afield.geometry.path.FLineLocation
import afield.geometry.path.FLineSeg
import afield.geometry.path.drawnSegmentEndingAt
import afield.geometry.path.locate
import afield.geometry.path.nearestSegmentParameter
import afield.ai.learning.*
import afield.ai.retrieval.*
import afield.animation.interpolation.*
import afield.animation.keyframes.*
import afield.animation.motion.*
import afield.app.commands.*
import afield.app.diagnostics.*
import afield.audio.analysis.*
import afield.audio.synth.*
import afield.data.collections.*
import afield.data.formats.*
import afield.data.graph.*
import afield.data.sorting.*
import afield.document.context.*
import afield.document.graph.*
import afield.document.properties.*
import afield.document.workspace.*
import afield.geometry.mesh.*
import afield.geometry.path.*
import afield.geometry.spatial.*
import afield.graphics.color.*
import afield.graphics.interaction.*
import afield.graphics.render.*
import afield.graphics.shader.*
import afield.graphics.text.*
import afield.graphics.vulkan.buffers.*
import afield.io.osc.*
import afield.io.process.*
import afield.lang.lambda.*
import afield.lang.python.*
import afield.lang.remotejs.*
import afield.lang.stacksl.*
import afield.math.autodiff.*
import afield.math.control.*
import afield.math.estimation.*
import afield.math.functions.*
import afield.math.linalg.*
import afield.math.optimization.*
import afield.math.statistics.*
import afield.math.units.*
import afield.media.image.*
import afield.media.video.*
import afield.runtime.execution.*
import afield.runtime.languages.*
import afield.runtime.lifecycle.*
import afield.simulation.agents.*
import afield.simulation.automata.*
import afield.simulation.particles.*
import afield.study.build.*
import afield.study.evidence.*
import afield.study.search.*
import afield.study.source.*
import afield.study.ui.*
import afield.ui.controls.*
import afield.ui.output.*
import afield.ui.visualization.*
import org.apache.commons.math4.legacy.optim.MaxEval
import org.apache.commons.math4.legacy.optim.nonlinear.scalar.GoalType
import org.apache.commons.math4.legacy.optim.univariate.BrentOptimizer
import org.apache.commons.math4.legacy.optim.univariate.SearchInterval
import org.apache.commons.math4.legacy.optim.univariate.UnivariateObjectiveFunction
import org.apache.commons.math4.legacy.optim.univariate.UnivariatePointValuePair
import afield.math.linalg.Vector4d
import afield.math.linalg.minus
import afield.math.linalg.plus
import afield.math.linalg.times
import java.awt.BasicStroke
import java.awt.Shape
import java.awt.geom.*
import java.util.*
import java.util.function.Consumer
import java.util.function.Function
import java.util.function.Predicate
import kotlin.math.*
Converts FLines to and from java.awt.geom shapes, and walks them by distance with
FLinesAndJavaShapes.Cursor.
The round trip through java.awt.geom (geometry only, not attributes) gives 2D FLines
Java2D's flattening, stroking, insetting and outsetting. Cursor moves along a
line by arc length or by node parameter, reports the position, tangent and curvature there, and
splits the line. CubicSegment2 and CubicSegment3 hold the per-segment cubic maths: roots,
inflections, bounding boxes and intersections.
class FLinesAndJavaShapes
Utility classes for manipulating and traversing FLines.
In addition to the tools that exploit the ability to round trip the geometry (but not the attributes) of 2d FLines to java.awt.geom classes (and thus gain quite a bit of 2d computational geometry for no cost), the star here is the Cursor class which solves some key problems in traversing FLines by equal units of distance (rather than node by node). *
class FLinesAndJavaShapes {
fun fromFourPoints
returns the c1, c2 control points for a cubic segement to go through p0@0, p1@u, p2@v and p3@ t=1.0
body — 46 lines, from line 108
fun fromFourPoints(
p0: Vec3, u: Float, p1: Vec3, v: Float, p2: Vec3, p3: Vec3
): Pair<Vec3, Vec3>? {
var a = 0.0f
var b = 0.0f
var c = 0.0f
var d = 0.0f
var det = 0.0f
val q1: Vec3 = Vec3()
val q2: Vec3 = Vec3()
if ((u <= 0.0f) || (u >= 1.0f) || (v <= 0.0f) || (v >= 1.0f) || (u >= v)) return null
a = (3 * (1 - u) * (1 - u) * u)
b = (3 * (1 - u) * u * u)
c = (3 * (1 - v) * (1 - v) * v)
d = (3 * (1 - v) * v * v)
det = a * d - b * c
if (det == 0f) return null
val r: Pair<Vec3, Vec3> = Pair(Vec3(), Vec3())
q1.x = p1.x - ((1 - u) * (1 - u) * (1 - u) * p0.x + u * u * u * p3.x)
q1.y = p1.y - ((1 - u) * (1 - u) * (1 - u) * p0.y + u * u * u * p3.y)
q1.z = p1.z - ((1 - u) * (1 - u) * (1 - u) * p0.z + u * u * u * p3.z)
q2.x = p2.x - ((1 - v) * (1 - v) * (1 - v) * p0.x + v * v * v * p3.x)
q2.y = p2.y - ((1 - v) * (1 - v) * (1 - v) * p0.y + v * v * v * p3.y)
q2.z = p2.z - ((1 - v) * (1 - v) * (1 - v) * p0.z + v * v * v * p3.z)
r.first.x = d * q1.x - b * q2.x
r.first.y = d * q1.y - b * q2.y
r.first.z = d * q1.z - b * q2.z
r.first.x /= det
r.first.y /= det
r.first.z /= det
r.second.x = (-c) * q1.x + a * q2.x
r.second.y = (-c) * q1.y + a * q2.y
r.second.z = (-c) * q1.z + a * q2.z
r.second.x /= det
r.second.y /= det
r.second.z /= det
return r
}
fun segment(f: FLine): List<FLine>
segments this along .moveTo's
fun segment(f: FLine): List<FLine> {
val r = mutableListOf<FLine>()
var at: FLine? = null
f.nodes.forEach {
when (it) {
is FLine.MoveTo -> {
val line = FLine()
at = line
r.add(line)
line.nodes.add(it.copy())
}
is FLine.LineTo -> {
at!!.nodes.add(it.copy())
}
is FLine.CubicTo -> {
at!!.nodes.add(it.copy())
}
is FLine.Close -> {
at!!.nodes.add(it.copy())
}
}
}
return r
}
fun positions(f: FLine, flatness: Float): List<Vec3>
A list of low curvature positions out of a FLine
fun positions(f: FLine, flatness: Float): List<Vec3> {
val c = Cursor(f, flatness)
val m: List<PathFlattener.Mapping> = c.pathFlattener.mappings
return m.mapNotNull { it.start }
}
class Cursor(on: FLine, private val tol: Float)
A class for navigating FLines either by distance "D" or by node "T".
protected val on: FLine = on
protected var p: PathFlattener? = null
lazy inited
protected var index: Int = 0
protected var alpha: Float = 0.0f
fun hasNext(): Boolean
fun hasNext(): Boolean {
return index < on.nodes.size - 1
}
fun segmentIsLinear(): Boolean
returns whether the current segment is a .lineTo
fun segmentIsLinear(): Boolean {
return nodeAt(index + 1) is FLine.LineTo
}
fun segmentIsCubic(): Boolean
returns whether the current segment is a .cubicTo
fun segmentIsCubic(): Boolean {
return nodeAt(index + 1) is FLine.CubicTo
}
fun segmentIsMove(): Boolean
returns whether the current segment is a .moveTo
fun segmentIsMove(): Boolean {
return nodeAt(index + 1) is FLine.MoveTo
}
fun segmentIsClose(): Boolean
returns whether the current segment is a .close
fun segmentIsClose(): Boolean {
return nodeAt(index + 1) is FLine.Close
}
The node at i, clamped, or null if this line has no nodes at all.
clamp returns nodes.size - 1, which is -1 for an empty line, so every
nodes[clamp(...)] read threw IndexOutOfBoundsException on an FLine with no nodes — and
position() reaches four of them. FLine().sampleByDistance(5f) blew up.
private fun nodeAt(i: Int): FLine.Node? =
if (on.nodes.isEmpty()) null else on.nodes[clamp(i)]
Resolve the target point for a close segment: the start of the subpath being closed.
private fun closeTarget(): Vec3 = subpathStartAt(clamp(index))
Walk back from from to the most recent FLine.MoveTo and return its point.
private fun subpathStartAt(from: Int): Vec3 {
for (i in from downTo 0) {
val node = on.nodes[i]
if (node is FLine.MoveTo) return Vec3(node.to)
}
return Vec3(on.nodes.firstOrNull()?.toOrNull ?: Vec3())
}
Where the instruction at i leaves the pen.
A FLine.Close stores no point of its own — reading Node.to on one throws
IndexOutOfBoundsException, because to is storage[0] and a Close has empty storage.
Its end point is the start of the subpath it closes. Every "start of the current segment"
read in this class goes through here, so a cursor can sit on, or immediately after, a
Close without blowing up.
private fun endOf(i: Int): Vec3 {
val at = clamp(i)
val node = on.nodes[at]
return if (node is FLine.Close) subpathStartAt(at) else node.to
}
fun next(): Array<Vec3>
fun next(): Array<Vec3> {
try {
return if (segmentIsCubic()) arrayOf<Vec3>(
Vec3(endOf(index)),
Vec3((on.nodes[clamp(index + 1)] as FLine.CubicTo).c1),
Vec3(
(on.nodes[clamp(index + 1)] as FLine.CubicTo).c2
),
Vec3((on.nodes[clamp(index + 1)] as FLine.CubicTo).to)
)
else if (segmentIsLinear()) arrayOf<Vec3>(
Vec3(endOf(index)),
Vec3(on.nodes[clamp(index + 1)].to)
)
else if (segmentIsClose()) arrayOf<Vec3>(
Vec3(endOf(index)),
closeTarget()
)
else if (segmentIsMove()) arrayOf<Vec3>(Vec3(endOf(index)))
else throw java.lang.IllegalStateException(" segment not implemented ")
} finally {
index++
}
}
fun reset()
resets the cursor to the start of its line
fun reset() {
index = 0
alpha = 0.0f
}
The drawn segment under the cursor and the parameter u on it.
t = i + u is fraction u (by curve parameter) of the segment ending at node i + 1. At a
node (u == 0) that is the outgoing segment, except at the very end of the line, which is
the last segment at u == 1. A cursor inside a MoveTo jump snaps to the nearer drawn
segment: the end of the one before it while u < 0.5, the start of the one after it from
there on — the same rule position has always used. null only when nothing is drawn.
private fun here(): Pair<FLineSeg, Double>? {
val n = on.nodes.size
if (n < 2) return null
var end = index + 1
var u = alpha.toDouble()
if (end > n - 1) { end = n - 1; u = 1.0 }
if (end < 1) { end = 1; u = 0.0 }
on.drawnSegmentEndingAt(end)?.let { return it to u }
var after: FLineSeg? = null
for (i in end + 1 until n) { after = on.drawnSegmentEndingAt(i); if (after != null) break }
var before: FLineSeg? = null
for (i in end - 1 downTo 1) { before = on.drawnSegmentEndingAt(i); if (before != null) break }
return if (after != null && (before == null || u >= 0.5)) after to 0.0 else before?.let { it to 1.0 }
}
the frame here, without paying for the arc-length lookup that location does.
private fun frameHere(): FLineLocation? = here()?.let { (s, u) -> s.locate(u, Float.NaN) }
fun position(): Vec3
The position of the cursor. On a line with nothing drawn (no nodes, or a single moveTo)
it is that lone point, or the origin.
fun position(): Vec3 {
here()?.let { (s, u) -> return s.at(u) }
return Vec3(on.nodes.firstOrNull()?.toOrNull ?: Vec3())
}
fun location(): FLineLocation? = here()?.let { (s, u) -> s.locate(u, d) }
Everything about the point under the cursor at once — position, unit tangent, the 3D
Frenet frame and curvature, the explicitly-2D normal and signed curvature, and where it is
as both a distance and a t. See FLineLocation for what each field means on straight
runs and at degenerate ends. null only when the line draws nothing.
fun tangent(): Vec3? = frameHere()?.tangent
the unit direction of travel; see FLineLocation.tangent. null when nothing is drawn.
fun normal(): Vec3? = frameHere()?.normal
the unit principal normal, towards the centre of curvature; null on straight runs. See FLineLocation.normal.
fun binormal(): Vec3? = frameHere()?.binormal
tangent × normal; null on straight runs. See FLineLocation.binormal.
fun curvature(): Float = frameHere()?.curvature ?: 0f
unsigned 3D curvature, 1 / radius; 0 on straight runs.
fun normal2D(): Vec2? = frameHere()?.normal2D?.takeIf { it.x != 0f || it.y != 0f }
The unit normal in the xy plane, tangent turned a quarter turn: (tangent.y, -tangent.x)
normalised. On Field's canvas (y down) that is to the left of the direction of travel.
null when nothing is drawn or the line runs straight along z here. See FLineLocation.normal2D.
fun signedCurvature2D(): Float = frameHere()?.signedCurvature2D ?: 0f
curvature of the xy projection, positive when the line bends towards normal2D.
fun radiusOfCurvature(): Vec3?
The vector from the cursor to the centre of its osculating circle: normal scaled by the
radius 1 / curvature. null on straight runs. Works on every kind of segment.
fun radiusOfCurvature(): Vec3? {
val f = frameHere() ?: return null
val n = f.normal ?: return null
if (f.curvature <= 0f) return null
return Vec3(n).mul(1f / f.curvature)
}
fun setNearest(point: Vec3): Cursor
Move the cursor to the point on the line nearest point (3D distance). Exact on straight
segments; on cubics a 32-sample scan refined by Newton's method. Paper.js
getNearestLocation; location() afterwards gives the full answer.
fun setNearest(point: Vec3): Cursor {
val best = on.nearestSegmentParameter(point) ?: return this
return setT(best)
}
the unit direction the line leaves this point along, or null if none (a MoveTo or the end).
private fun directionOut(): Vec3? {
val s = on.drawnSegmentEndingAt(index + 1) ?: return null
if (index + 1 > on.nodes.size - 1) return null
return s.locate(alpha.toDouble(), Float.NaN).tangent.takeIf { it.lengthSquared() > 0f }
}
the unit direction the line arrives at this point along, or null if none.
private fun directionIn(): Vec3? {
if (alpha != 0.0f) return directionOut()
val s = on.drawnSegmentEndingAt(index) ?: return null
return s.locate(1.0, Float.NaN).tangent.takeIf { it.lengthSquared() > 0f }
}
fun extremalInnerPoints(): List<Float>
returns a list of 't' positions where dx, dy, or dz is zero. These points only exist inside cubic segments.
fun extremalInnerPoints(): List<Float> {
val q: MutableList<Float> = ArrayList()
for (i in 1 until on.nodes.size) {
if (on.nodes.get(i) is FLine.CubicTo) {
val fi = i
val c: CubicSegment3 = CubicSegment3(on, i)
val z1 = c.findDXZeroRoots()
val z2 = c.findDYZeroRoots()
val z3 = c.findDZZeroRoots()
z1.forEach(Consumer { x: Float -> q.add(fi + x - 1) })
z2.forEach(Consumer { x: Float -> q.add(fi + x - 1) })
z3.forEach(Consumer { x: Float -> q.add(fi + x - 1) })
}
}
Collections.sort(q)
if (q.size == 0) return q
val o = q[0]
var i = 1
while (i < q.size) {
if (abs(q[i] - o) < 1e-3) {
q.removeAt(i)
i--
}
i++
}
return q
}
fun tangentBreaks(): List<Float>
returns a list of node 't's where the incoming and outgoing unit directions differ (corners), plus the ends of every drawn run
fun tangentBreaks(): List<Float> {
return extremalOuterPoints(Predicate<Pair<Vec3, Vec3>> { p: Pair<Vec3, Vec3> ->
p.first.distance(
p.second
) > 1e-3
})
}
fun extremalOuterPoints(): List<Float>
returns a list of 't's where tangent's change dx, dy or dz
fun extremalOuterPoints(): List<Float> {
return this.extremalOuterPoints { p: Pair<Vec3, Vec3> ->
Math.signum(p.first.x) != Math.signum(
p.second.x
) || Math.signum(p.first.y) != Math.signum(p.second.y) || Math.signum(
p.first.z
) != Math.signum(p.second.z)
}
}
fun inflectionPointsXY(): List<Float>
returns a list of 't's that are points of inflection (this only happens inside cubic segments)
fun inflectionPointsXY(): List<Float> {
val q = ArrayList<Float>()
for (i in 1 until on.nodes.size) {
val fi = i
val nn: FLine.Node = on.nodes.get(i)
if (nn is FLine.CubicTo) {
val c: CubicSegment3 = CubicSegment3(on, i)
val d = c.inflectionPointsXY()
d.forEach(Consumer { x: Float? -> q.add(x!! + fi - 1) })
}
}
return q
}
protected fun extremalOuterPoints(p: Predicate<Pair<Vec3, Vec3>>): List<Float>
protected fun extremalOuterPoints(p: Predicate<Pair<Vec3, Vec3>>): List<Float> {
val was = t
val q: MutableList<Float> = ArrayList()
try {
for (i in 0 until lengthT()) {
setT(i.toFloat())
val b: Vec3? = directionIn()
val f: Vec3? = directionOut()
if (b == null && f != null) q.add(i + 0.0f)
else if (b != null && f == null) q.add(i + 0.0f)
else if (b == null && f == null) {
} else {
if (p.test(b!! to f!!)) q.add(i + 0.0f)
}
}
} finally {
setT(was)
}
return q
}
fun split(): Pair<FLine?, FLine?>
returns two new FLines: one on each side of the current cursor. For example myLine.cursor().setD(20.3).split().first is a line that is the first 20.3 units of myLine.
body — 66 lines, from line 518
fun split(): Pair<FLine?, FLine?> {
if (alpha == 0.0f && index == 0) return Pair(null, on.copy())
if (alpha == 1.0f && index == on.nodes.size - 2) return Pair(on.copy(), null)
if (alpha == 0.0f && index == on.nodes.size - 1) return Pair(on.copy(), null)
val left: FLine = FLine()
val right: FLine = FLine()
for (i in 0 until index + 1) {
left.copyTo(on.nodes.get(i))
}
// The right-hand tail. A Close in the cut subpath would now close back to the cut point
// (the tail's own moveTo), not to where the subpath started, so it becomes the straight
// run it stood for. Before this, splitting a closed square part-way along its first edge
// gave a right-hand piece whose last edge went back to the cut.
val cutStart = subpathStartAt(clamp(index))
fun tail(from: Int) {
var inCut = true
for (i in from until on.nodes.size) {
val n = on.nodes.get(i)
if (n is FLine.MoveTo) inCut = false
if (inCut && n is FLine.Close) right.lineTo(cutStart) else right.copyTo(n)
}
}
if (alpha == 0.0f) {
// Exactly on a node: cut there, rather than appending a zero-length piece of the
// following segment to the left half (the old behaviour for lines and cubics alike).
if (!segmentIsMove()) right.moveTo(Vec3(endOf(index)))
tail(index + 1)
} else if (segmentIsMove()) {
tail(index + 1)
} else if (segmentIsLinear()) {
left.lineTo(position())
right.moveTo(position())
tail(index + 1)
} else if (segmentIsClose()) {
// A Close is a straight segment back to the subpath start, so cutting inside one
// works like the linear case — the right-hand piece is an open run from the cut
// point to that start.
left.lineTo(position())
right.moveTo(position())
right.lineTo(closeTarget())
tail(index + 2)
} else if (segmentIsCubic()) {
val a: Vec3 = Vec3(endOf(index))
val c1: Vec3 = Vec3((on.nodes.get(clamp(index + 1)) as FLine.CubicTo).c1)
val c2: Vec3 = Vec3((on.nodes.get(clamp(index + 1)) as FLine.CubicTo).c2)
val b: Vec3 = Vec3((on.nodes.get(clamp(index + 1)) as FLine.CubicTo).to)
val c12: Vec3 = Vec3()
val m: Vec3 = Vec3()
val c21: Vec3 = Vec3()
val t: Vec3 = Vec3()
splitCubicFrame(a, c1, c2, b, alpha, c12, m, c21, t)
left.cubicTo(c1, c12, m)
right.moveTo(m)
right.cubicTo(c21, c2, b)
tail(index + 2)
}
return Pair(left, right)
}
fun splitD(d: List<Float>): List<FLine>
returns a list of new FLines that are this line split at an (ordered) list of points. For example myLine.splitD([3.2,4,8]) returns a list of four FLines all pieces of myLine,
the first line runs from 0 to 3.2, the second from 3.2 to 4, the third from 4 to 8 and the fourth from 8 to the end of myLine.
fun splitD(d: List<Float>): List<FLine> {
val was = t
try {
var right: FLine? = on
var soFar = 0.0f
var cursor = this
val f: MutableList<FLine> = ArrayList<FLine>()
for (i in d.indices) {
val at = d[i] - soFar
cursor.setD(at)
val s: Pair<FLine?, FLine?> = cursor.split()
val left = s.first ?: return f
f.add(left)
// `soFar` is how far along the ORIGINAL line the remaining piece now starts, so
// it accumulates to d[i], not to the relative offset `at`. It used to be
// `soFar = at`, which is right for the first two cuts by coincidence (at == d[1]
// - d[0] measured from a piece starting at d[0]) and then drifts: splitD on a
// 500-unit line asking for [100, 225, 350] cut at 100, 225 and 450.
soFar = d[i]
right = s.second
val nextRight = right ?: return f
cursor = Cursor(nextRight, tol)
}
f.add(right!!)
return f
} finally {
setT(was)
}
}
fun splitT(t: List<Float>): List<FLine>
splits on a list of T positions
fun splitT(t: List<Float>): List<FLine> {
val was = this.t
try {
val d: MutableList<Float> = ArrayList(t.size)
for (tt in t) {
setT(tt)
d.add(this.d)
}
return splitD(d)
} finally {
setT(was)
}
}
fun segment(): FLine.Node?
modifies this line, to include a new node here. Note this will increment lengthT and possibly t, but this cursor will still be in the same actual .position() This returns the added
body — 41 lines, from line 643
fun segment(): FLine.Node? {
if (alpha == 0.0f) return null
if (segmentIsMove()) {
return null
} else if (segmentIsLinear()) {
val s = FLine.LineTo().set(Vec3(position()))
on.nodes.add(index + 1, s)
on.rev++
alpha = 0.0f
index++
return on.nodes.get(index + 1)
} else if (segmentIsCubic()) {
val a: Vec3 = Vec3(endOf(index))
val c1: Vec3 = Vec3((on.nodes.get(clamp(index + 1)) as FLine.CubicTo).c1)
val c2: Vec3 = Vec3((on.nodes.get(clamp(index + 1)) as FLine.CubicTo).c2)
val b: Vec3 = Vec3((on.nodes.get(clamp(index + 1)) as FLine.CubicTo).to)
val c12: Vec3 = Vec3()
val m: Vec3 = Vec3()
val c21: Vec3 = Vec3()
val t: Vec3 = Vec3()
splitCubicFrame(a, c1, c2, b, alpha, c12, m, c21, t)
(on.nodes.get(clamp(index + 1)) as FLine.CubicTo).c1.set(c1)
(on.nodes.get(clamp(index + 1)) as FLine.CubicTo).c2.set(c12)
(on.nodes.get(clamp(index + 1)) as FLine.CubicTo).to.set(m)
val inserted = FLine.CubicTo().set(Vec3(b), Vec3(c21), Vec3(c2))
on.nodes.add(index + 2, inserted)
on.rev++
p = null
alpha = 0.0f
index++
return on.nodes.get(index + 1)
}
return null
}
fun lengthT(): Int
the overal length in 'node' aka 't' units. A line with 4 drawing instructions has a lengthT of 4.
fun lengthT(): Int {
return on.nodes.size - 1
}
fun lengthD(): Float
length in actual units.
fun lengthD(): Float {
return pathFlattener.length()
}
val d: Float
The cursor's current position as a distance along the line, in the same units as lengthD.
Written above the property rather than above its get(): a KDoc on an accessor attaches to
nothing (the extractor cannot parse get() = …) and slid this description onto setD.
val d: Float
get() = pathFlattener.dotToLength(index + alpha)
fun setD(d: Float): Cursor
set the current position as a distance along this line.
fun setD(d: Float): Cursor {
val q: Float = pathFlattener.lengthToDot(d)
index = q.toInt()
alpha = q - index
return this
}
val t: Float
The cursor's current position in node.t format: 0 is the start of the line, 1 is the first
node, and 1.5 is roughly — but certainly not exactly — half way between the first and second.
The inexactness matters. evalLinear(x, y, alpha) parameterizes a linear segment evenly, but
evalCubic(x, c1, c2, y, alpha) does not, so alpha = 0.5 is not the half-way point by
distance. If you care about even distribution use the much slower but accurate d / setD.
val t: Float
get() = index + alpha
fun setT(q: Float): Cursor
set the current position as a distance along this line. Returns position in node.t format
fun setT(q: Float): Cursor {
index = q.toInt()
alpha = (q - index)
return this
}
val pathFlattener: PathFlattener
val pathFlattener: PathFlattener
get() {
if (p == null) return PathFlattener(this.on, tol).also { p = it }
return p as PathFlattener
}
private fun clamp(index: Int): Int {
if (index >= on.nodes.size) return on.nodes.size - 1
if (index < 0) return 0
return index
}
}
class CubicSegment3
var a: Vec3
var b: Vec3
var c: Vec3
var d: Vec3
constructor(a: Vec3, b: Vec3, c: Vec3, d: Vec3)
constructor(a: Vec3, b: Vec3, c: Vec3, d: Vec3) {
this.a = Vec3(a)
this.b = Vec3(b)
this.c = Vec3(c)
this.d = Vec3(d)
}
constructor(a: Vec3, b: Vec3, c: Vec3, d: Vec3, dx: Float)
constructor(a: Vec3, b: Vec3, c: Vec3, d: Vec3, dx: Float) {
this.a = Vec3(a)
this.b = Vec3(b)
this.c = Vec3(c)
this.d = Vec3(d)
this.a.x += dx
this.b.x += dx
this.c.x += dx
this.d.x += dx
}
constructor(a: Vec3, b: Vec3, c: Vec3, d: Vec3, dx: Float, dy: Float)
constructor(a: Vec3, b: Vec3, c: Vec3, d: Vec3, dx: Float, dy: Float) {
this.a = Vec3(a)
this.b = Vec3(b)
this.c = Vec3(c)
this.d = Vec3(d)
this.a.x += dx
this.b.x += dx
this.c.x += dx
this.d.x += dx
this.a.y += dy
this.b.y += dy
this.c.y += dy
this.d.y += dy
}
constructor(a: Vec3, d: Vec3)
constructor(a: Vec3, d: Vec3) {
this.a = Vec3(a)
this.b = Vec3(a.x + (d.x - a.x) / 3, a.y + (d.y - a.y) / 3, a.z + (d.z - a.z) / 3)
this.c = Vec3(a.x + 2 * (d.x - a.x) / 3, a.y + 2 * (d.y - a.y) / 3, a.z + 2 * (d.z - a.z) / 3)
this.d = Vec3(d)
}
constructor(f: FLine, index: Int)
The cubic segment that ends at f.nodes[index](#L208), so index must be at least 1 — index
0 has no segment, only a start point. endPointOf, not .to, for the start: the node
before a cubic can be a Close, which stores no point of its own.
constructor(f: FLine, index: Int) {
require(index >= 1) { "CubicSegment3(FLine, $index): a segment needs the node before it, so index must be >= 1" }
this.a = Vec3(f.endPointOf(index - 1))
this.b = Vec3((f.nodes.get(index) as FLine.CubicTo).c1)
this.c = Vec3((f.nodes.get(index) as FLine.CubicTo).c2)
this.d = Vec3((f.nodes.get(index) as FLine.CubicTo).to)
}
fun intersect(x1: Vec3, x2: Vec3): List<Vec3>
fun intersect(x1: Vec3, x2: Vec3): List<Vec3> {
val c2 = rotateToX(x1, x2)
return c2.findYZeroRoots().filter { x ->
Math.abs(
evaluateCubicFrame(c2.a.z, c2.b.z, c2.c.z, c2.d.z, x).x
) < 1e-5f
}.map { x ->
evaluateCubicFrame(
a, b, c, d, x, Vec3()
)
}.toList()
// filter out things passed the end of x1->x2?
}
fun rotateToX(x1: Vec3, x2: Vec3): CubicSegment3
rotates so that x1->x2 is along the x axis
fun rotateToX(x1: Vec3, x2: Vec3): CubicSegment3 {
var x1: Vec3 = x1
var x2: Vec3 = x2
val dir: Vec3 = x2 - x1
val q = Quat().rotateTo(vec(1.0, 0.0, 0.0f), dir)
x1 = q.transform(Vec3(x1))
x2 = q.transform(Vec3(x2))
val a2: Vec3 = q.transform(a) - x1
val b2: Vec3 = q.transform(b) - x1
val c2: Vec3 = q.transform(c) - x1
val d2: Vec3 = q.transform(d) - x1
return CubicSegment3(a2, b2, c2, d2)
}
fun boundingBox(): Cuboid
fun boundingBox(): Cuboid {
val y = findDYZeroRoots()
val x = findDXZeroRoots()
val z = findDZZeroRoots()
val min: Vec3 = Vec3(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY)
val max: Vec3 = Vec3(Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY)
run {
val s: Vec3 = evaluateCubicFrame(a, b, c, d, 0.0f, Vec3())
min.min(s)
max.max(s)
}
run {
val s: Vec3 = evaluateCubicFrame(a, b, c, d, 1.0f, Vec3())
min.min(s)
max.max(s)
}
for (yy in y) {
val s: Vec3 = evaluateCubicFrame(a, b, c, d, yy, Vec3())
min.min(s)
max.max(s)
}
for (yy in x) {
val s: Vec3 = evaluateCubicFrame(a, b, c, d, yy, Vec3())
min.min(s)
max.max(s)
}
for (yy in z) {
val s: Vec3 = evaluateCubicFrame(a, b, c, d, yy, Vec3())
min.min(s)
max.max(s)
}
return Cuboid(min.x, min.y, min.z, max.x - min.x, max.y - min.y, max.z - min.z)
}
fun split(alpha: Float): Pair<CubicSegment3, CubicSegment3>
fun split(alpha: Float): Pair<CubicSegment3, CubicSegment3> {
val c12: Vec3 = Vec3()
val c21: Vec3 = Vec3()
val m: Vec3 = Vec3()
val s1 = CubicSegment3(a, b, c, d)
val s2 = CubicSegment3(a, b, c, d)
splitCubicFrame(s1.a, s1.b, s2.c, s2.d, alpha, c12, m, c21, Vec3())
s1.c = c12
s1.d = m
s2.a = Vec3(m)
s2.b = c21
return Pair(s1, s2)
}
override fun toString(): String
override fun toString(): String {
return "c[$a,$b,$c,$d]"
}
fun intersection(with: CubicSegment3, sz: Float): List<Pair<CubicSegment3, CubicSegment3>>
body — 74 lines, from line 906
fun intersection(with: CubicSegment3, sz: Float): List<Pair<CubicSegment3, CubicSegment3>> {
val a: MutableList<CubicSegment3> = ArrayList()
a.add(this)
val b: MutableList<CubicSegment3> = ArrayList()
b.add(with)
val out: MutableList<Pair<CubicSegment3, CubicSegment3>> = ArrayList()
var gen = 0
while (a.size > 0 && b.size > 0 && gen < 20) {
val cache: MutableMap<CubicSegment3, Cuboid> = LinkedHashMap<CubicSegment3, Cuboid>()
val na: MutableSet<CubicSegment3> = LinkedHashSet()
val nb: MutableSet<CubicSegment3> = LinkedHashSet()
o@ for (aa in a) {
val baa: Cuboid = aa.boundingBox()
val ib = b.iterator()
while (ib.hasNext()) {
val nib = ib.next()
val bnn: Cuboid = cache.computeIfAbsent(
nib,
Function<CubicSegment3, Cuboid> { x: CubicSegment3? -> nib.boundingBox() })
if (baa.intersects(bnn)) {
if (Math.max(baa.w, baa.h) < sz && Math.max(bnn.w, bnn.h) < sz) {
ib.remove()
out.add(Pair(aa, nib))
} else {
na.add(aa)
nb.add(nib)
}
//continue o;
}
}
}
a.clear()
b.clear()
for (c in na) {
val r: Cuboid = cache.computeIfAbsent(
c,
Function<CubicSegment3, Cuboid> { x: CubicSegment3? -> c.boundingBox() })
if (Math.max(r.w, r.h) < sz) a.add(c)
else {
val lr = c.split(0.5f)
a.add(lr.first)
a.add(lr.second)
}
}
for (c in nb) {
val r: Cuboid = cache.computeIfAbsent(
c,
Function<CubicSegment3, Cuboid> { x: CubicSegment3? -> c.boundingBox() })
if (Math.max(r.w, r.h) < sz) b.add(c)
else {
val lr = c.split(0.5f)
b.add(lr.first)
b.add(lr.second)
}
}
gen++
}
return out
}
fun findYZeroRoots(): List<Float>
body — 52 lines, from line 982
fun findYZeroRoots(): List<Float> {
val subdiv = 20
val dd: MutableList<Float> = ArrayList()
for (i in 0 until subdiv) {
val alphaStart = (i - 0.75f) / subdiv
val alphaEnd = (i + 0.75f) / subdiv
val yStart: Float = evaluateCubicFrame(a.y, b.y, c.y, d.y, alphaStart).x
val yEnd: Float = evaluateCubicFrame(a.y, b.y, c.y, d.y, alphaEnd).x
if (sign(yStart) != sign(yEnd)) {
val t = findYZeroRoot_bracketed(alphaStart, alphaEnd)
if (t != null) dd.add(t)
}
}
for (i in dd.indices) {
var j = i + 1
while (j < dd.size) {
if (abs(dd[i] - dd[j]) < 1e-6) {
dd.removeAt(j)
j--
}
j++
}
}
// filter dd for repeated roots
return dd
}
private fun findYZeroRoot_bracketed(yStart: Float, yEnd: Float): Float? {
var t = (yStart + yEnd) / 2
for (i in 0..9) {
val D: Vec2 = evaluateCubicFrame(a.y, b.y, c.y, d.y, t)
val dt: Float = D.x / D.y
t = t - dt
if (abs(dt) < 1e-5) {
return t
}
}
if (t >= yStart && t <= yEnd && evaluateCubicFrame(
a.y, b.y, c.y, d.y, t
).x < 1e-6
) {
return t
}
return null
}
fun findXZeroRoots(): List<Float>
body — 51 lines, from line 1035
fun findXZeroRoots(): List<Float> {
val subdiv = 20
val dd: MutableList<Float> = ArrayList()
for (i in 0 until subdiv) {
val alphaStart = (i - 0.75f) / subdiv
val alphaEnd = (i + 0.75f) / subdiv
val yStart: Float = evaluateCubicFrame(a.x, b.x, c.x, d.x, alphaStart).x
val yEnd: Float = evaluateCubicFrame(a.x, b.x, c.x, d.x, alphaEnd).x
if (sign(yStart) != sign(yEnd)) {
val t = findXZeroRoot_bracketed(alphaStart, alphaEnd)
if (t != null) dd.add(t)
}
}
for (i in dd.indices) {
var j = i + 1
while (j < dd.size) {
if (abs(dd[i] - dd[j]) < 1e-6) {
dd.removeAt(j)
j--
}
j++
}
}
return dd
}
private fun findXZeroRoot_bracketed(yStart: Float, yEnd: Float): Float? {
var t = (yStart + yEnd) / 2
for (i in 0..9) {
val D: Vec2 = evaluateCubicFrame(a.x, b.x, c.x, d.x, t)
val dt: Float = D.x / D.y
t = t - dt
if (abs(dt) < 1e-5) {
return t
}
}
if (t >= yStart && t <= yEnd && evaluateCubicFrame(
a.x, b.x, c.x, d.x, t
).x < 1e-6
) {
return t
}
return null
}
fun findDYZeroRoots(): List<Float>
body — 52 lines, from line 1087
fun findDYZeroRoots(): List<Float> {
val subdiv = 20
val dd: MutableList<Float> = ArrayList()
for (i in 0 until subdiv) {
val alphaStart = (i - 0.75f) / subdiv
val alphaEnd = (i + 0.75f) / subdiv
val yStart: Float = evaluateCubicFrame(a.y, b.y, c.y, d.y, alphaStart).y
val yEnd: Float = evaluateCubicFrame(a.y, b.y, c.y, d.y, alphaEnd).y
if (sign(yStart) != sign(yEnd)) {
val t = findDYZeroRoot_bracketed(alphaStart, alphaEnd)
if (t != null) dd.add(t)
}
}
for (i in dd.indices) {
var j = i + 1
while (j < dd.size) {
if (abs(dd[i] - dd[j]) < 1e-6) {
dd.removeAt(j)
j--
}
j++
}
}
// filter dd for repeated roots
return dd
}
private fun findDYZeroRoot_bracketed(yStart: Float, yEnd: Float): Float? {
var t = (yStart + yEnd) / 2
for (i in 0..9) {
val D: Vec2 = evaluateDCubicFrame(a.y, b.y, c.y, d.y, t)
val dt: Float = D.x / D.y
t = t - dt
if (abs(dt) < 1e-5) {
return t
}
}
if (t >= yStart && t <= yEnd && evaluateDCubicFrame(
a.y, b.y, c.y, d.y, t
).x < 1e-6
) {
return t
}
return null
}
fun findDXZeroRoots(): List<Float>
body — 52 lines, from line 1140
fun findDXZeroRoots(): List<Float> {
val subdiv = 20
val dd: MutableList<Float> = ArrayList()
for (i in 0 until subdiv) {
val alphaStart = (i - 0.75f) / subdiv
val alphaEnd = (i + 0.75f) / subdiv
val yStart: Float = evaluateCubicFrame(a.x, b.x, c.x, d.x, alphaStart).y
val yEnd: Float = evaluateCubicFrame(a.x, b.x, c.x, d.x, alphaEnd).y
if (sign(yStart) != sign(yEnd)) {
val t = findDXZeroRoot_bracketed(alphaStart, alphaEnd)
if (t != null) dd.add(t)
}
}
for (i in dd.indices) {
var j = i + 1
while (j < dd.size) {
if (abs(dd[i] - dd[j]) < 1e-6) {
dd.removeAt(j)
j--
}
j++
}
}
// filter dd for repeated roots
return dd
}
private fun findDXZeroRoot_bracketed(yStart: Float, yEnd: Float): Float? {
var t = (yStart + yEnd) / 2
for (i in 0..9) {
val D: Vec2 = evaluateDCubicFrame(a.x, b.x, c.x, d.x, t)
val dt: Float = D.x / D.y
t = t - dt
if (abs(dt) < 1e-5) {
return t
}
}
if (t >= yStart && t <= yEnd && evaluateDCubicFrame(
a.x, b.x, c.x, d.x, t
).x < 1e-6
) {
return t
}
return null
}
fun findDZZeroRoots(): List<Float>
body — 52 lines, from line 1194
fun findDZZeroRoots(): List<Float> {
val subdiv = 20
val dd: MutableList<Float> = ArrayList()
for (i in 0 until subdiv) {
val alphaStart = (i - 0.75f) / subdiv
val alphaEnd = (i + 0.75f) / subdiv
val yStart: Float = evaluateCubicFrame(a.z, b.z, c.z, d.z, alphaStart).y
val yEnd: Float = evaluateCubicFrame(a.z, b.z, c.z, d.z, alphaEnd).y
if (sign(yStart) != sign(yEnd)) {
val t = findDZZeroRoot_bracketed(alphaStart, alphaEnd)
if (t != null) dd.add(t)
}
}
for (i in dd.indices) {
var j = i + 1
while (j < dd.size) {
if (abs(dd[i] - dd[j]) < 1e-6) {
dd.removeAt(j)
j--
}
j++
}
}
// filter dd for repeated roots
return dd
}
private fun findDZZeroRoot_bracketed(yStart: Float, yEnd: Float): Float? {
var t = (yStart + yEnd) / 2
for (i in 0..9) {
val D: Vec2 = evaluateDCubicFrame(a.z, b.z, c.z, d.z, t)
val dt: Float = D.x / D.y
t = t - dt
if (abs(dt) < 1e-5) {
return t
}
}
if (t >= yStart && t <= yEnd && evaluateDCubicFrame(
a.z, b.z, c.z, d.z, t
).x < 1e-6
) {
return t
}
return null
}
fun osculating(alpha: Float): Vec3
fun osculating(alpha: Float): Vec3 {
return D(alpha).cross(DD(alpha), Vec3())
}
fun D(alpha: Float): Vec3
fun D(alpha: Float): Vec3 {
val a: Vec3 = this.b - this.a
val b: Vec3 = this.c + this.b * -2.0 + this.a
val c: Vec3 = this.d + this.c * -3.0 + this.b * 3.0 + this.a * -1.0
val D: Vec3 = a * 3.0 + b * 6.0 * alpha + c * 3.0 * alpha * alpha
return D
}
fun DD(alpha: Float): Vec3
fun DD(alpha: Float): Vec3 {
val a: Vec3 = this.b - this.a
val b: Vec3 = this.c + this.b * -2.0 + this.a
val c: Vec3 = this.d + this.c * -3.0 + this.b * 3.0 + this.a * -1.0
val DD: Vec3 = b * 6.0 + c * 6.0 * alpha
return DD
}
fun R(alpha: Float): Float
fun R(alpha: Float): Float {
val D: Vec3 = D(alpha)
val DD: Vec3 = DD(alpha)
return (D.x * D.x + D.y * D.y + D.z * D.z).pow(3 / 2f) / D.cross(DD, Vec3())
.length()
}
fun inflectionPointsXY(): List<Float>
fun inflectionPointsXY(): List<Float> {
val ax: Float = -a.x + 3 * b.x - 3 * c.x + d.x
val bx: Float = 3 * a.x - 6 * b.x + 3 * c.x
val cx: Float = -3 * a.x + 3 * b.x
val dx: Float = a.x
val ay: Float = -a.y + 3 * b.y - 3 * c.y + d.y
val by: Float = 3 * a.y - 6 * b.y + 3 * c.y
val cy: Float = -3 * a.y + 3 * b.y
val dy: Float = a.y
val az: Float = -a.z + 3 * b.z - 3 * c.z + d.z
val bz: Float = 3 * a.z - 6 * b.z + 3 * c.z
val cz: Float = -3 * a.z + 3 * b.z
val dz: Float = a.z
// Inflections are the roots of P'(t) x P''(t) = 0 inside the segment. With A x B ~ 0
// that quadratic degenerates to a linear equation.
val axb = ay * bx - ax * by
val inside = { t: Float -> t > 1e-4f && t < 1f - 1e-4f }
if (abs(axb) < 1e-9f * max(1f, abs(ay * cx - ax * cy) + abs(by * cx - bx * cy))) {
val lin = ay * cx - ax * cy
if (abs(lin) < 1e-12f) return emptyList()
val t = -(by * cx - bx * cy) / (3 * lin)
return listOf(t).filter(inside)
}
val cuspxy = -(ay * cx - ax * cy) / (2 * axb)
val disxy = cuspxy * cuspxy - (by * cx - bx * cy) / (3 * axb)
return when {
disxy < 0 -> emptyList()
disxy == 0.0f -> listOf(cuspxy).filter(inside)
else -> listOf(cuspxy - sqrt(disxy), cuspxy + sqrt(disxy)).filter(inside)
}
}
fun inflectionPointsXZ(): List<Float>
fun inflectionPointsXZ(): List<Float> {
val ax: Float = -a.x + 3 * b.x - 3 * c.x + d.x
val bx: Float = 3 * a.x - 6 * b.x + 3 * c.x
val cx: Float = -3 * a.x + 3 * b.x
val dx: Float = a.x
// Float ay = -a.y+3*b.y-3*c.y+d.y;
// Float by = 3*a.y-6*b.y+3*c.y;
// Float cy = -3*a.y+3*b.y;
// Float dy = a.y;
val ay: Float = -a.z + 3 * b.z - 3 * c.z + d.z
val by: Float = 3 * a.z - 6 * b.z + 3 * c.z
val cy: Float = -3 * a.z + 3 * b.z
val dy: Float = a.z
// Inflections are the roots of P'(t) x P''(t) = 0 inside the segment. With A x B ~ 0
// that quadratic degenerates to a linear equation.
val axb = ay * bx - ax * by
val inside = { t: Float -> t > 1e-4f && t < 1f - 1e-4f }
if (abs(axb) < 1e-9f * max(1f, abs(ay * cx - ax * cy) + abs(by * cx - bx * cy))) {
val lin = ay * cx - ax * cy
if (abs(lin) < 1e-12f) return emptyList()
val t = -(by * cx - bx * cy) / (3 * lin)
return listOf(t).filter(inside)
}
val cuspxy = -(ay * cx - ax * cy) / (2 * axb)
val disxy = cuspxy * cuspxy - (by * cx - bx * cy) / (3 * axb)
return when {
disxy < 0 -> emptyList()
disxy == 0.0f -> listOf(cuspxy).filter(inside)
else -> listOf(cuspxy - sqrt(disxy), cuspxy + sqrt(disxy)).filter(inside)
}
}
fun inflectionPointsYZ(): List<Float>
fun inflectionPointsYZ(): List<Float> {
// Float ax = -a.x+3*b.x-3*c.x+d.x;
// Float bx = 3*a.x-6*b.x+3*c.x;
// Float cx = -3*a.x+3*b.x;
// Float dx = a.x;
val ay: Float = -a.y + 3 * b.y - 3 * c.y + d.y
val by: Float = 3 * a.y - 6 * b.y + 3 * c.y
val cy: Float = -3 * a.y + 3 * b.y
val dy: Float = a.y
val ax: Float = -a.z + 3 * b.z - 3 * c.z + d.z
val bx: Float = 3 * a.z - 6 * b.z + 3 * c.z
val cx: Float = -3 * a.z + 3 * b.z
val dx: Float = a.z
// Inflections are the roots of P'(t) x P''(t) = 0 inside the segment. With A x B ~ 0
// that quadratic degenerates to a linear equation.
val axb = ay * bx - ax * by
val inside = { t: Float -> t > 1e-4f && t < 1f - 1e-4f }
if (abs(axb) < 1e-9f * max(1f, abs(ay * cx - ax * cy) + abs(by * cx - bx * cy))) {
val lin = ay * cx - ax * cy
if (abs(lin) < 1e-12f) return emptyList()
val t = -(by * cx - bx * cy) / (3 * lin)
return listOf(t).filter(inside)
}
val cuspxy = -(ay * cx - ax * cy) / (2 * axb)
val disxy = cuspxy * cuspxy - (by * cx - bx * cy) / (3 * axb)
return when {
disxy < 0 -> emptyList()
disxy == 0.0f -> listOf(cuspxy).filter(inside)
else -> listOf(cuspxy - sqrt(disxy), cuspxy + sqrt(disxy)).filter(inside)
}
}
fun closestToPoint(v: Vec3): Float
fun closestToPoint(v: Vec3): Float {
// right now we split this segment into two pieces to reduce the changes of getting stuck in a local minimum. We ought to split at inflection points instead
val o: BrentOptimizer = BrentOptimizer(1e-4, 1e-4)
val p1: UnivariatePointValuePair = o.optimize(
UnivariateObjectiveFunction { x ->
evaluateCubicFrame(
a, b, c, d, x.toFloat(), Vec3()
).distance(v).toDouble()
}, SearchInterval(0.0, 0.5, 0.25), GoalType.MINIMIZE, MaxEval(200)
)
val p2: UnivariatePointValuePair = o.optimize(
UnivariateObjectiveFunction { x ->
evaluateCubicFrame(
a, b, c, d, x.toFloat(), Vec3()
).distance(v).toDouble()
}, SearchInterval(0.5, 1.0, 0.75), GoalType.MINIMIZE, MaxEval(200)
)
return if (p1.getValue() < p2.getValue()) p1.getPoint().toFloat()
else p2.getPoint().toFloat()
}
fun evaluate(t: Float): Vec3
fun evaluate(t: Float): Vec3 {
return evaluateCubicFrame(a, b, c, d, t, Vec3())
}
}
inner class CubicSegment2
helper class for a 2d cubic segment, for the things that we can only reasonably do in 2d (like intersections).
var a: Vec2
var b: Vec2
var c: Vec2
var d: Vec2
constructor(a: Vec2, b: Vec2, c: Vec2, d: Vec2)
constructor(a: Vec2, b: Vec2, c: Vec2, d: Vec2) {
this.a = Vec2(a)
this.b = Vec2(b)
this.c = Vec2(c)
this.d = Vec2(d)
}
constructor(a: Vec3, b: Vec3, c: Vec3, d: Vec3)
constructor(a: Vec3, b: Vec3, c: Vec3, d: Vec3) {
this.a = a.v2
this.b = b.v2
this.c = c.v2
this.d = d.v2
}
fun intersect(x1: Vec2, x2: Vec2): List<Vec2>
fun intersect(x1: Vec2, x2: Vec2): List<Vec2> {
val c2 = rotateToX(x1, x2)
return c2.findYZeroRoots().map { x ->
evaluateCubicFrame(
a, b, c, d, x, Vec2()
)
}.toList()
}
fun rotateToX(x1: Vec2, x2: Vec2): CubicSegment2
rotates so that x1->x2 is along the x axis
fun rotateToX(x1: Vec2, x2: Vec2): CubicSegment2 {
var x1: Vec2 = x1
var x2: Vec2 = x2
val dir: Vec2 = x2 - x1
val q = Quat().rotateTo(vec(1.0, 0.0, 0.0f), dir.v3)
x1 = (x1 * q) as Vec2
x2 = (x2 * q) as Vec2
val a2 = (a * q) - x1
val b2: Vec2 = (b * q) - x1
val c2: Vec2 = (c * q) - x1
val d2: Vec2 = (d * q) - x1
return CubicSegment2(a2, b2, c2, d2)
}
fun boundingBox(): Rect
fun boundingBox(): Rect {
val y = findDYZeroRoots()
val x = findDXZeroRoots()
val min: Vec2 = Vec2(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY)
val max: Vec2 = Vec2(Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY)
run {
val s: Vec2 = evaluateCubicFrame(a, b, c, d, 0.0f, Vec2())
min.min(s)
max.max(s)
}
run {
val s: Vec2 = evaluateCubicFrame(a, b, c, d, 1.0f, Vec2())
min.min(s)
max.max(s)
}
for (yy in y) {
val s: Vec2 = evaluateCubicFrame(a, b, c, d, yy, Vec2())
min.min(s)
max.max(s)
}
for (yy in x) {
val s: Vec2 = evaluateCubicFrame(a, b, c, d, yy, Vec2())
min.min(s)
max.max(s)
}
return Rect(min.x.toDouble(), min.y.toDouble(), (max.x - min.x).toDouble(), (max.y - min.y).toDouble())
}
fun split(alpha: Float): Pair<CubicSegment2, CubicSegment2>
fun split(alpha: Float): Pair<CubicSegment2, CubicSegment2> {
val c12: Vec2 = Vec2()
val c21: Vec2 = Vec2()
val m: Vec2 = Vec2()
val s1: CubicSegment2 = CubicSegment2(a, b, c, d)
val s2: CubicSegment2 = CubicSegment2(a, b, c, d)
splitCubicFrame(s1.a, s1.b, s2.c, s2.d, alpha, c12, m, c21, Vec2())
s1.c = c12
s1.d = m
s2.a = Vec2(m)
s2.b = c21
return Pair(s1, s2)
}
fun intersection(with: CubicSegment2, sz: Float): List<Pair<CubicSegment2, CubicSegment2>>
body — 69 lines, from line 1518
fun intersection(with: CubicSegment2, sz: Float): List<Pair<CubicSegment2, CubicSegment2>> {
val a: MutableList<CubicSegment2> = ArrayList()
a.add(this)
val b: MutableList<CubicSegment2> = ArrayList()
b.add(with)
val out: MutableList<Pair<CubicSegment2, CubicSegment2>> = ArrayList()
var gen = 0
while (a.size > 0 && b.size > 0 && gen < 20) {
val cache: MutableMap<CubicSegment2, Rect> = LinkedHashMap<CubicSegment2, Rect>()
val na: MutableSet<CubicSegment2> = LinkedHashSet()
val nb: MutableSet<CubicSegment2> = LinkedHashSet()
o@ for (aa in a) {
val baa: Rect = aa.boundingBox()
val ib = b.iterator()
while (ib.hasNext()) {
val nib = ib.next()
val bnn: Rect = cache.computeIfAbsent(
nib,
Function<CubicSegment2, Rect> { x: CubicSegment2? -> nib.boundingBox() })
if (baa.intersects(bnn)) {
if (Math.max(baa.w, baa.h) < sz && Math.max(bnn.w, bnn.h) < sz) {
ib.remove()
out.add(Pair(aa, nib))
} else {
na.add(aa)
nb.add(nib)
}
//continue o;
}
}
}
a.clear()
b.clear()
for (c in na) {
val r: Rect =
cache.computeIfAbsent(c, Function<CubicSegment2, Rect> { x: CubicSegment2? -> c.boundingBox() })
if (Math.max(r.w, r.h) < sz) a.add(c)
else {
val lr = c.split(0.5f)
a.add(lr.first)
a.add(lr.second)
}
}
for (c in nb) {
val r: Rect =
cache.computeIfAbsent(c, Function<CubicSegment2, Rect> { x: CubicSegment2? -> c.boundingBox() })
if (Math.max(r.w, r.h) < sz) b.add(c)
else {
val lr = c.split(0.5f)
b.add(lr.first)
b.add(lr.second)
}
}
gen++
}
return out
}
fun findYZeroRoots(): List<Float>
body — 52 lines, from line 1589
fun findYZeroRoots(): List<Float> {
val subdiv = 20
val dd: MutableList<Float> = ArrayList()
for (i in 0 until subdiv) {
val alphaStart = (i - 0.75f) / subdiv
val alphaEnd = (i + 0.75f) / subdiv
val yStart: Float = evaluateCubicFrame(a.y, b.y, c.y, d.y, alphaStart).x
val yEnd: Float = evaluateCubicFrame(a.y, b.y, c.y, d.y, alphaEnd).x
if (sign(yStart) != sign(yEnd)) {
val t = findYZeroRoot_bracketed(alphaStart, alphaEnd)
if (t != null) dd.add(t)
}
}
for (i in dd.indices) {
var j = i + 1
while (j < dd.size) {
if (abs(dd[i] - dd[j]) < 1e-6) {
dd.removeAt(j)
j--
}
j++
}
}
// filter dd for repeated roots
return dd
}
private fun findYZeroRoot_bracketed(yStart: Float, yEnd: Float): Float? {
var t = (yStart + yEnd) / 2
for (i in 0..9) {
val D: Vec2 = evaluateCubicFrame(a.y, b.y, c.y, d.y, t)
val dt: Float = D.x / D.y
t = t - dt
if (abs(dt) < 1e-5) {
return t
}
}
if (t >= yStart && t <= yEnd && evaluateCubicFrame(
a.y, b.y, c.y, d.y, t
).x < 1e-6
) {
return t
}
return null
}
fun findXZeroRoots(): List<Float>
body — 52 lines, from line 1642
fun findXZeroRoots(): List<Float> {
val subdiv = 20
val dd: MutableList<Float> = ArrayList()
for (i in 0 until subdiv) {
val alphaStart = (i - 0.75f) / subdiv
val alphaEnd = (i + 0.75f) / subdiv
val yStart: Float = evaluateCubicFrame(a.x, b.x, c.x, d.x, alphaStart).x
val yEnd: Float = evaluateCubicFrame(a.x, b.x, c.x, d.x, alphaEnd).x
if (sign(yStart) != sign(yEnd)) {
val t = findXZeroRoot_bracketed(alphaStart, alphaEnd)
if (t != null) dd.add(t)
}
}
for (i in dd.indices) {
var j = i + 1
while (j < dd.size) {
if (abs(dd[i] - dd[j]) < 1e-6) {
dd.removeAt(j)
j--
}
j++
}
}
// filter dd for repeated roots
return dd
}
private fun findXZeroRoot_bracketed(yStart: Float, yEnd: Float): Float? {
var t = (yStart + yEnd) / 2
for (i in 0..9) {
val D: Vec2 = evaluateCubicFrame(a.x, b.x, c.x, d.x, t)
val dt: Float = D.x / D.y
t = t - dt
if (abs(dt) < 1e-5) {
return t
}
}
if (t >= yStart && t <= yEnd && evaluateCubicFrame(
a.x, b.x, c.x, d.x, t
).x < 1e-6
) {
return t
}
return null
}
fun findDYZeroRoots(): List<Float>
body — 52 lines, from line 1695
fun findDYZeroRoots(): List<Float> {
val subdiv = 20
val dd: MutableList<Float> = ArrayList()
for (i in 0 until subdiv) {
val alphaStart = (i - 0.75f) / subdiv
val alphaEnd = (i + 0.75f) / subdiv
val yStart: Float = evaluateCubicFrame(a.y, b.y, c.y, d.y, alphaStart).y
val yEnd: Float = evaluateCubicFrame(a.y, b.y, c.y, d.y, alphaEnd).y
if (sign(yStart) != sign(yEnd)) {
val t = findDYZeroRoot_bracketed(alphaStart, alphaEnd)
if (t != null) dd.add(t)
}
}
for (i in dd.indices) {
var j = i + 1
while (j < dd.size) {
if (abs(dd[i] - dd[j]) < 1e-6) {
dd.removeAt(j)
j--
}
j++
}
}
// filter dd for repeated roots
return dd
}
private fun findDYZeroRoot_bracketed(yStart: Float, yEnd: Float): Float? {
var t = (yStart + yEnd) / 2
for (i in 0..9) {
val D: Vec2 = evaluateDCubicFrame(a.y, b.y, c.y, d.y, t)
val dt: Float = D.x / D.y
t = t - dt
if (abs(dt) < 1e-5) {
return t
}
}
if (t >= yStart && t <= yEnd && evaluateDCubicFrame(
a.y, b.y, c.y, d.y, t
).x < 1e-6
) {
return t
}
return null
}
fun findDXZeroRoots(): List<Float>
body — 52 lines, from line 1748
fun findDXZeroRoots(): List<Float> {
val subdiv = 20
val dd: MutableList<Float> = ArrayList()
for (i in 0 until subdiv) {
val alphaStart = (i - 0.75f) / subdiv
val alphaEnd = (i + 0.75f) / subdiv
val yStart: Float = evaluateCubicFrame(a.x, b.x, c.x, d.x, alphaStart).y
val yEnd: Float = evaluateCubicFrame(a.x, b.x, c.x, d.x, alphaEnd).y
if (sign(yStart) != sign(yEnd)) {
val t = findDXZeroRoot_bracketed(alphaStart, alphaEnd)
if (t != null) dd.add(t)
}
}
for (i in dd.indices) {
var j = i + 1
while (j < dd.size) {
if (abs(dd[i] - dd[j]) < 1e-6) {
dd.removeAt(j)
j--
}
j++
}
}
// filter dd for repeated roots
return dd
}
private fun findDXZeroRoot_bracketed(yStart: Float, yEnd: Float): Float? {
var t = (yStart + yEnd) / 2
for (i in 0..9) {
val D: Vec2 = evaluateDCubicFrame(a.x, b.x, c.x, d.x, t)
val dt: Float = D.x / D.y
t = t - dt
if (abs(dt) < 1e-5) {
return t
}
}
if (t >= yStart && t <= yEnd && evaluateDCubicFrame(
a.x, b.x, c.x, d.x, t
).x < 1e-6
) {
return t
}
return null
}
fun closestToPoint(v: Vec2): Float
fun closestToPoint(v: Vec2): Float {
// right now we split this segment into two pieces to reduce the changes of getting stuck in a local minimum.
val o: BrentOptimizer = BrentOptimizer(1e-4, 1e-4)
val p1: UnivariatePointValuePair = o.optimize(
UnivariateObjectiveFunction { x ->
evaluateCubicFrame(
a, b, c, d, x.toFloat(), Vec2()
).distance(v).toDouble()
}, SearchInterval(0.0, 0.5, 0.25), GoalType.MINIMIZE
)
val p2: UnivariatePointValuePair = o.optimize(
UnivariateObjectiveFunction { x ->
evaluateCubicFrame(
a, b, c, d, x.toFloat(), Vec2()
).distance(v).toDouble()
}, SearchInterval(0.5, 1.0, 0.75), GoalType.MINIMIZE
)
return if (p1.getValue() < p2.getValue()) p1.getPoint().toFloat()
else p2.getPoint().toFloat()
}
}
companion object
companion object {
private const val angle = Math.PI / 4.0
private val a = 1.0 - cos(angle)
private val b = tan(angle)
private val c = sqrt(1.0 + b * b) - 1 + a
private val cv = 4.0 / 3.0 * a * b / c
private val acv = (1.0 - cv) / 2.0
For each array: 4 values for each point {v0, v1, v2, v3}: point = (x + v0 * w + v1 * arcWidth, y + v2 * h + v3 * arcHeight);
private val ctrlpts = arrayOf(
doubleArrayOf(0.0, 0.0, 0.0, 0.5),
doubleArrayOf(0.0, 0.0, 1.0, -0.5),
doubleArrayOf(0.0, 0.0, 1.0, -acv, 0.0, acv, 1.0, 0.0, 0.0, 0.5, 1.0, 0.0),
doubleArrayOf(1.0, -0.5, 1.0, 0.0),
doubleArrayOf(
1.0, -acv, 1.0, 0.0, 1.0, 0.0, 1.0, -acv, 1.0, 0.0, 1.0, -0.5
),
doubleArrayOf(1.0, 0.0, 0.0, 0.5),
doubleArrayOf(1.0, 0.0, 0.0, acv, 1.0, -acv, 0.0, 0.0, 1.0, -0.5, 0.0, 0.0),
doubleArrayOf(0.0, 0.5, 0.0, 0.0),
doubleArrayOf(
0.0, acv, 0.0, 0.0, 0.0, 0.0, 0.0, acv, 0.0, 0.0, 0.0, 0.5
),
doubleArrayOf(),
)
fun flineToJavaShape(f: FLine): Shape
fun flineToJavaShape(f: FLine): Shape {
f.thicken?.let {
return it.createStrokedShape(_flineToJavaShape(f))
}
f.thickness?.let {
return BasicStroke(it.toFloat()).createStrokedShape(_flineToJavaShape(f))
}
return _flineToJavaShape(f)
}
fun flineToJavaShape_notThickened(f: FLine): Shape
fun flineToJavaShape_notThickened(f: FLine): Shape {
return _flineToJavaShape(f)
}
private fun _flineToJavaShape(f: FLine): GeneralPath {
val p = GeneralPath()
// AWT insists on an explicit initial moveTo and throws IllegalPathStateException
// without one; the renderer does not, and treats a leading LineTo/CubicTo as a moveTo
// (someSkiko.kt:332-337, :339-352). Following the renderer, so that a line that draws
// is a line that converts.
var hasCurrentPoint = false
for (n in f.nodes) {
if (n is FLine.MoveTo) {
p.moveTo(n.to.x, n.to.y); hasCurrentPoint = true
} else if (n is FLine.LineTo) {
if (hasCurrentPoint) p.lineTo(n.to.x, n.to.y)
else { p.moveTo(n.to.x, n.to.y); hasCurrentPoint = true }
} else if (n is FLine.CubicTo) {
if (hasCurrentPoint) p.curveTo(n.c1.x, n.c1.y, n.c2.x, n.c2.y, n.to.x, n.to.y)
else { p.moveTo(n.to.x, n.to.y); hasCurrentPoint = true }
}
// a Close has no branch here at all until now, so it was silently dropped and the
// contour arrived at AWT open. Everything downstream of this — the four boolean
// ops (FLineGeometry.kt:1006-1042), thickening, hit-testing against an Area — then
// worked on a shape that was not the shape it was handed. Guarded on there being a
// current point, because GeneralPath.closePath() throws IllegalPathStateException
// without one and `FLine().close()` is a shape someone can hand us.
else if (n is FLine.Close && hasCurrentPoint) p.closePath()
}
return p
}
fun flineToJavaShape(f: Collection<FLine>): Shape
fun flineToJavaShape(f: Collection<FLine>): Shape {
val p = GeneralPath()
for (ff in f) {
p.append(flineToJavaShape(ff), false)
}
return p
}
fun evaluateCubicFrame
fun evaluateCubicFrame(
a: Vec2, c1: Vec2, c2: Vec2, b: Vec2, alpha: Float, out: Vec2
): Vec2 {
val oma = 1 - alpha
val oma2 = oma * oma
val oma3 = oma2 * oma
val alpha2 = alpha * alpha
val alpha3 = alpha2 * alpha
out.x = a.x * oma3 + 3 * c1.x * alpha * oma2 + 3 * c2.x * alpha2 * oma + b.x * alpha3
out.y = a.y * oma3 + 3 * c1.y * alpha * oma2 + 3 * c2.y * alpha2 * oma + b.y * alpha3
return out
}
fun evaluateCubicFrame(a: Float, c1: Float, c2: Float, b: Float, alpha: Float): Vec2
returns both the evaluation (x) and it's derivative (y)
fun evaluateCubicFrame(a: Float, c1: Float, c2: Float, b: Float, alpha: Float): Vec2 {
val oma = 1 - alpha
val oma2 = oma * oma
var oma3 = oma2 * oma
val alpha2 = alpha * alpha
var alpha3 = alpha2 * alpha
val out: Vec2 = Vec2()
out.x = a * oma3 + 3 * c1 * alpha * oma2 + 3 * c2 * alpha2 * oma + b * alpha3
oma3 = -3 * (1 - alpha) * (1 - alpha)
val alphaoma2 = 3 * alpha * alpha - 4 * alpha + 1
val alpha2oma = alpha * (2 - 3 * alpha)
alpha3 = 3 * alpha * alpha
out.y = a * oma3 + 3 * c1 * alphaoma2 + 3 * c2 * alpha2oma + b * alpha3
return out
}
fun evaluateDCubicFrame(a: Float, c1: Float, c2: Float, b: Float, alpha: Float): Vec2
returns both the derivative (x) and it's second derivative (y)
fun evaluateDCubicFrame(a: Float, c1: Float, c2: Float, b: Float, alpha: Float): Vec2 {
val out: Vec2 = Vec2()
var oma3 = -3 * (1 - alpha) * (1 - alpha)
var alphaoma2 = 3 * alpha * alpha - 4 * alpha + 1
var alpha2oma = alpha * (2 - 3 * alpha)
var alpha3 = 3 * alpha * alpha
out.x = a * oma3 + 3 * c1 * alphaoma2 + 3 * c2 * alpha2oma + b * alpha3
oma3 = -2 + 2 * alpha
alphaoma2 = 6 * alpha - 4
alpha2oma = 2 - 6 * alpha
alpha3 = 6 * alpha
out.y = a * oma3 + 3 * c1 * alphaoma2 + 3 * c2 * alpha2oma + b * alpha3
return out
}
fun evaluateCubicFrame
fun evaluateCubicFrame(
a: Vec3, c1: Vec3, c2: Vec3, b: Vec3, alpha: Float, out: Vec3
): Vec3 {
val oma = 1 - alpha
val oma2 = oma * oma
val oma3 = oma2 * oma
val alpha2 = alpha * alpha
val alpha3 = alpha2 * alpha
out.x = a.x * oma3 + 3 * c1.x * alpha * oma2 + 3 * c2.x * alpha2 * oma + b.x * alpha3
out.y = a.y * oma3 + 3 * c1.y * alpha * oma2 + 3 * c2.y * alpha2 * oma + b.y * alpha3
out.z = a.z * oma3 + 3 * c1.z * alpha * oma2 + 3 * c2.z * alpha2 * oma + b.z * alpha3
return out
}
fun evaluateCubicFrame
fun evaluateCubicFrame(
a: Vector4d, c1: Vector4d, c2: Vector4d, b: Vector4d, alpha: Float, out: Vector4d
): Vector4d {
val oma = 1 - alpha
val oma2 = oma * oma
val oma3 = oma2 * oma
val alpha2 = alpha * alpha
val alpha3 = alpha2 * alpha
out.x = (a.x * oma3) + (3 * c1.x * alpha * oma2) + (3 * c2.x * alpha2 * oma) + (b.x * alpha3)
out.y = (a.y * oma3) + (3 * c1.y * alpha * oma2) + (3 * c2.y * alpha2 * oma) + (b.y * alpha3)
out.z = (a.z * oma3) + (3 * c1.z * alpha * oma2) + (3 * c2.z * alpha2 * oma) + (b.z * alpha3)
out.w = (a.w * oma3) + (3 * c1.w * alpha * oma2) + (3 * c2.w * alpha2 * oma) + (b.w * alpha3)
return out
}
fun evaluateDCubicFrame
fun evaluateDCubicFrame(
a: Vec3, c1: Vec3, c2: Vec3, b: Vec3, alpha: Float, out: Vec3
): Vec3 {
val oma3 = -3 * (1 - alpha) * (1 - alpha)
val alphaoma2 = 3 * alpha * alpha - 4 * alpha + 1
val alpha2oma = alpha * (2 - 3 * alpha)
val alpha3 = 3 * alpha * alpha
out.x = a.x * oma3 + 3 * c1.x * alphaoma2 + 3 * c2.x * alpha2oma + b.x * alpha3
out.y = a.y * oma3 + 3 * c1.y * alphaoma2 + 3 * c2.y * alpha2oma + b.y * alpha3
out.z = a.z * oma3 + 3 * c1.z * alphaoma2 + 3 * c2.z * alpha2oma + b.z * alpha3
return out
}
fun evaluateDDCubicFrame
fun evaluateDDCubicFrame(
a: Vec3, c1: Vec3, c2: Vec3, b: Vec3, alpha: Float, out: Vec3
): Vec3 {
val oma3 = 6 * (1 - alpha)
val alphaoma2 = 6 * alpha - 4
val alpha2oma = (2 - 6 * alpha)
val alpha3 = 6 * alpha
out.x = a.x * oma3 + 3 * c1.x * alphaoma2 + 3 * c2.x * alpha2oma + b.x * alpha3
out.y = a.y * oma3 + 3 * c1.y * alphaoma2 + 3 * c2.y * alpha2oma + b.y * alpha3
out.z = a.z * oma3 + 3 * c1.z * alphaoma2 + 3 * c2.z * alpha2oma + b.z * alpha3
return out
}
fun ptSegDistSq3
fun ptSegDistSq3(
x1: Float, y1: Float, z1: Float, x2: Float, y2: Float, z2: Float, px: Float, py: Float, pz: Float
): Float {
var x2 = x2
var y2 = y2
var z2 = z2
var px = px
var py = py
var pz = pz
x2 -= x1
y2 -= y1
z2 -= z1
px -= x1
py -= y1
pz -= z1
var dotprod = px * x2 + py * y2 + pz * z2
val projlenSq: Float
if (dotprod <= 0.0f) {
projlenSq = 0.0f
} else {
px = x2 - px
py = y2 - py
pz = z2 - pz
dotprod = px * x2 + py * y2 + pz * z2
projlenSq = if (dotprod <= 0.0f) {
0.0f
} else {
dotprod * dotprod / (x2 * x2 + y2 * y2 + z2 * z2)
}
}
var lenSq = px * px + py * py + pz * pz - projlenSq
if (lenSq < 0) {
lenSq = 0.0f
}
return lenSq
}
fun splitCubicFrame
mutates c1 and c2
fun splitCubicFrame(
a: Vec2,
c1: Vec2,
c2: Vec2,
b: Vec2,
alpha: Float,
c12: Vec2,
m: Vec2,
c21: Vec2,
tmp: Vec2
) {
c1.lerp(c2, alpha, tmp)
evaluateCubicFrame(a, c1, c2, b, alpha, m)
a.lerp(c1, alpha, c1)
c1.lerp(tmp, alpha, c12)
c2.lerp(b, alpha, c2)
tmp.lerp(c2, alpha, c21)
}
fun splitCubicFrame
mutates c1 and c2
fun splitCubicFrame(
a: Vec3,
c1: Vec3,
c2: Vec3,
b: Vec3,
alpha: Float,
c12: Vec3,
m: Vec3,
c21: Vec3,
tmp: Vec3
) {
c1.lerp(c2, alpha, tmp)
evaluateCubicFrame(a, c1, c2, b, alpha, m)
a.lerp(c1, alpha, c1)
c1.lerp(tmp, alpha, c12)
c2.lerp(b, alpha, c2)
tmp.lerp(c2, alpha, c21)
}
fun javaShapeToFLine(f: Shape): FLine
fun javaShapeToFLine(f: Shape): FLine {
return javaShapeToFLine(f, AffineTransform.getTranslateInstance(0.0, 0.0))
}
fun javaShapeToFLineFlat(f: Shape, tol: Float, levels: Int): FLine
body — 64 lines, from line 2129
fun javaShapeToFLineFlat(f: Shape, tol: Float, levels: Int): FLine {
var pi = f.getPathIterator(AffineTransform.getTranslateInstance(0.0, 0.0))
pi = FlatteningPathIterator(pi, tol.toDouble(), levels)
val cc = FloatArray(6)
var lastAt: Vec2? = null
var lastMoveTo: Vec2? = null
val `in`: FLine = FLine()
while (!pi.isDone()) {
val ty = pi.currentSegment(cc)
if (ty == PathIterator.SEG_CLOSE) {
if (lastMoveTo != null && lastAt!!.distance(lastMoveTo) > 1e-6) `in`.lineTo(
lastMoveTo.x, lastMoveTo.y
)
lastAt = null
} else if (ty == PathIterator.SEG_CUBICTO) {
if (lastAt == null || (Math.abs(lastAt.x - cc[4]) + Math.abs(lastAt.y - cc[5]) > 1e-15)) `in`.cubicTo(
cc[0], cc[1], cc[2], cc[3], cc[4], cc[5]
)
if (lastAt == null) lastAt = Vec2(cc[4], cc[5])
else {
lastAt.x = cc[4]
lastAt.y = cc[5]
}
} else if (ty == PathIterator.SEG_LINETO) {
if (lastAt == null || (Math.abs(lastAt.x - cc[0]) + Math.abs(lastAt.y - cc[1]) > 1e-15)) `in`.lineTo(
cc[0], cc[1]
)
if (lastAt == null) lastAt = Vec2(cc[0], cc[1])
else {
lastAt.x = cc[0]
lastAt.y = cc[1]
}
} else if (ty == PathIterator.SEG_MOVETO) {
if (lastAt == null || (Math.abs(lastAt.x - cc[0]) + Math.abs(lastAt.y - cc[1]) > 1e-15)) `in`.moveTo(
cc[0], cc[1]
)
lastMoveTo = Vec2(cc[0], cc[1])
if (lastAt == null) lastAt = Vec2(cc[0], cc[1])
else {
lastAt.x = cc[0]
lastAt.y = cc[1]
}
} else if (ty == PathIterator.SEG_QUADTO) {
val start = lastAt ?: Vec2(cc[2], cc[3])
if (lastAt == null || (Math.abs(lastAt.x - cc[2]) + Math.abs(lastAt.y - cc[3]) > 1e-15)) `in`.cubicTo(
(cc[0] - start.x) * (2 / 3f) + start.x,
(cc[1] - start.y) * (2 / 3f) + start.y,
(cc[0] - cc[2]) * (2 / 3f) + cc[2],
(cc[1] - cc[3]) * (2 / 3f) + cc[3],
cc[2],
cc[3]
)
lastAt = Vec2(cc[2], cc[3])
}
pi.next()
}
return `in`
}
fun javaShapeToFLine(f: Shape, at: AffineTransform?): FLine
body — 68 lines, from line 2194
fun javaShapeToFLine(f: Shape, at: AffineTransform?): FLine {
val pi = f.getPathIterator(at)
val cc = FloatArray(6)
var lastAt: Vec2? = null
var lastMoveTo: Vec2? = null
val `in`: FLine = FLine()
while (!pi.isDone) {
val ty = pi.currentSegment(cc)
if (ty == PathIterator.SEG_CLOSE) {
if (lastMoveTo != null && lastAt!!.distance(lastMoveTo) > 1e-6) `in`.lineTo(
lastMoveTo.x, lastMoveTo.y
)
lastAt = null
} else if (ty == PathIterator.SEG_CUBICTO) {
if (lastAt == null || (Math.abs(lastAt.x - cc[4]) + Math.abs(lastAt.y - cc[5]) > 1e-15)) `in`.cubicTo(
cc[0],
cc[1],
cc[2],
cc[3],
cc[4],
cc[5]
)
if (lastAt == null) lastAt = Vec2(cc[4], cc[5])
else {
lastAt.x = cc[4]
lastAt.y = cc[5]
}
} else if (ty == PathIterator.SEG_LINETO) {
if (lastAt == null || (Math.abs(lastAt.x - cc[0]) + Math.abs(lastAt.y - cc[1]) > 1e-15)) `in`.lineTo(
cc[0], cc[1]
)
if (lastAt == null) lastAt = Vec2(cc[0], cc[1])
else {
lastAt.x = cc[0]
lastAt.y = cc[1]
}
} else if (ty == PathIterator.SEG_MOVETO) {
if (lastAt == null || (Math.abs(lastAt.x - cc[0]) + Math.abs(lastAt.y - cc[1]) > 1e-15)) `in`.moveTo(
cc[0], cc[1]
)
lastMoveTo = Vec2(cc[0], cc[1])
if (lastAt == null) lastAt = Vec2(cc[0], cc[1])
else {
lastAt.x = cc[0]
lastAt.y = cc[1]
}
} else if (ty == PathIterator.SEG_QUADTO) {
val start = lastAt ?: Vec2(cc[2], cc[3])
if (lastAt == null || (Math.abs(lastAt.x - cc[2]) + Math.abs(lastAt.y - cc[3]) > 1e-15)) `in`.cubicTo(
(cc[0] - start.x) * (2 / 3f) + start.x,
(cc[1] - start.y) * (2 / 3f) + start.y,
(cc[0] - cc[2]) * (2 / 3f) + cc[2],
(cc[1] - cc[3]) * (2 / 3f) + cc[3],
cc[2],
cc[3]
)
lastAt = Vec2(cc[2], cc[3])
}
pi.next()
}
return `in`
}
fun javaShapeToFLine(f: Shape, copyDepthFrom: FLine, at: AffineTransform?): FLine
body — 55 lines, from line 2263
fun javaShapeToFLine(f: Shape, copyDepthFrom: FLine, at: AffineTransform?): FLine {
val s1: FLine = javaShapeToFLine(f, at)
copyDepthFrom(s1, Function<Vec2, Float> { n: Vec2 ->
var d = Float.POSITIVE_INFINITY
var z = 0.0
for (n2 in copyDepthFrom.nodes) {
// `toOrNull`: the depth source is a caller-supplied FLine, so it can be closed,
// and a Close stores no point to take a depth from — its end point is the
// subpath's `moveTo`, already in this loop.
n2.toOrNull?.let { to ->
val dd = (to.x - n.x) * (to.x - n.x) + (to.y - n.y) * (to.y - n.y)
if (dd < d) {
d = dd
z = to.z.toDouble()
}
}
if (n2 is FLine.CubicTo) {
run {
val dd =
(n2.c1.x - n.x) * (n2.c1.x - n.x) + (n2.c1.y - n.y) * (n2.c1.y - n.y)
if (dd < d) {
d = dd
z = n2.c1.z.toDouble()
}
}
run {
val dd =
(n2.c2.x - n.x) * (n2.c2.x - n.x) + (n2.c2.y - n.y) * (n2.c2.y - n.y)
if (dd < d) {
d = dd
z = (n2).c2.z.toDouble()
}
}
}
}
z.toFloat()
})
return s1
}
private fun copyDepthFrom(s1: FLine, close: Function<Vec2, Float>) {
for (n in s1.nodes) {
// `toOrNull`: nothing to give a z to on a Close, which stores no point.
val to = n.toOrNull ?: continue
var z = close.apply(to.v2)
to.z = z
if (n is FLine.CubicTo) {
z = close.apply(n.c1.v2)
n.c1.z = z
z = close.apply(n.c2.v2)
n.c2.z = z
}
}
// s1.modify()
}
fun insetShape(f: FLine, amount: Float): FLine
fun insetShape(f: FLine, amount: Float): FLine {
val s = flineToJavaShape_notThickened(f)
val ss = BasicStroke(amount, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND).createStrokedShape(s)
val sa = Area(s)
val ssa = Area(ss)
sa.subtract(ssa)
return javaShapeToFLine(sa)
}
fun outsetShape(f: FLine, amount: Float): Shape
fun outsetShape(f: FLine, amount: Float): Shape {
val s = flineToJavaShape_notThickened(f)
val ss = BasicStroke(amount, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND).createStrokedShape(s)
val sa = Area(s)
val ssa = Area(ss)
sa.add(ssa)
return sa
}
fun samplePoints(f: FLine, distance: Float): List<Vec3>
fun samplePoints(f: FLine, distance: Float): List<Vec3> {
val cc: Cursor = f.cursor()
val r: ArrayList<Vec3> = ArrayList<Vec3>()
var D = 0.0
while (D < cc.lengthD()) {
r.add(cc.position())
cc.setD(distance.let { D += it; D }.toFloat())
}
return r
}
fun drawRoundedRectInto(into: FLine, x: Float, y: Float, w: Float, h: Float, r: Float): FLine
body — 50 lines, from line 2350
fun drawRoundedRectInto(into: FLine, x: Float, y: Float, w: Float, h: Float, r: Float): FLine {
into.moveTo(
x + ctrlpts[0][0] * w + ctrlpts[0][1] * r, y + ctrlpts[0][2] * h + ctrlpts[0][3] * r
)
into.lineTo(
x + ctrlpts[1][0] * w + ctrlpts[1][1] * r, y + ctrlpts[1][2] * h + ctrlpts[1][3] * r
)
into.cubicTo(
x + ctrlpts[2][0] * w + ctrlpts[2][1] * r,
y + ctrlpts[2][2] * h + ctrlpts[2][3] * r,
x + ctrlpts[2][4] * w + ctrlpts[2][5] * r,
y + ctrlpts[2][6] * h + (ctrlpts[2][7] * r),
x + ctrlpts[2][8] * w + ctrlpts[2][9] * r,
y + ctrlpts[2][10] * h + ctrlpts[2][11] * r
)
into.lineTo(
x + ctrlpts[3][0] * w + ctrlpts[3][1] * r, y + ctrlpts[3][2] * h + ctrlpts[3][3] * r
)
into.cubicTo(
x + ctrlpts[4][0] * w + ctrlpts[4][1] * r,
y + ctrlpts[4][2] * h + ctrlpts[4][3] * r,
x + ctrlpts[4][4] * w + ctrlpts[4][5] * r,
y + ctrlpts[4][6] * h + (ctrlpts[4][7] * r),
x + ctrlpts[4][8] * w + ctrlpts[4][9] * r,
y + ctrlpts[4][10] * h + ctrlpts[4][11] * r
)
into.lineTo(
x + ctrlpts[5][0] * w + ctrlpts[5][1] * r, y + ctrlpts[5][2] * h + ctrlpts[5][3] * r
)
into.cubicTo(
x + ctrlpts[6][0] * w + ctrlpts[6][1] * r,
y + ctrlpts[6][2] * h + ctrlpts[6][3] * r,
x + ctrlpts[6][4] * w + ctrlpts[6][5] * r,
y + ctrlpts[6][6] * h + (ctrlpts[6][7] * r),
x + ctrlpts[6][8] * w + ctrlpts[6][9] * r,
y + ctrlpts[6][10] * h + ctrlpts[6][11] * r
)
into.lineTo(
x + ctrlpts[7][0] * w + ctrlpts[7][1] * r, y + ctrlpts[7][2] * h + ctrlpts[7][3] * r
)
into.cubicTo(
x + ctrlpts[8][0] * w + ctrlpts[8][1] * r,
y + ctrlpts[8][2] * h + ctrlpts[8][3] * r,
x + ctrlpts[8][4] * w + ctrlpts[8][5] * r,
y + ctrlpts[8][6] * h + (ctrlpts[8][7] * r),
x + ctrlpts[8][8] * w + ctrlpts[8][9] * r,
y + ctrlpts[8][10] * h + ctrlpts[8][11] * r
)
return into
}
fun intersectTwoLineSegments(a1: Vec3, a2: Vec3, b1: Vec3, b2: Vec3): Vec3?
fun intersectTwoLineSegments(a1: Vec3, a2: Vec3, b1: Vec3, b2: Vec3): Vec3? {
val da: Vec3 = Vec3(a2).sub(a1)
val db: Vec3 = Vec3(b2).sub(b1)
val dc: Vec3 = Vec3(b1).sub(a1)
if (dc.dot(da.cross(db, Vec3())) != 0.0f) // lines are not coplanar
return null
val s: Float =
dc.cross(db, Vec3()).dot(
da.cross(db, Vec3())
) / da.cross(db, Vec3()).lengthSquared()
// s places the hit on segment a; it is only a hit if it is also on segment b. This
// used to check s alone, so two segments whose *lines* crossed reported a point even
// when b stopped well short of a (a 0..100 horizontal and a vertical from y=10 to 50
// "met" at (50, 0)).
if (s >= 0.0 && s <= 1.0f) {
val hit = a1 + da * s
val dbl = db.lengthSquared()
val r = if (dbl == 0f) 0f else Vec3(hit).sub(b1).dot(db) / dbl
if (r < -1e-6f || r > 1f + 1e-6f) return null
return hit
}
return null
}
fun intersect(a: FLine, b: FLine): List<Vec3>
Every point where a crosses b.
Both loops start at 1, not 0. A segment needs the node before it to say where it starts,
so index 0 has no segment of its own — the loops used to start at 0 and read
nodes.get(-1), an outright IndexOutOfBoundsException on any line whose first node is
a LineTo or a CubicTo. (An FLine normally opens with a MoveTo, which is neither, and that
is the only reason this went unnoticed: FLine().lineTo(...) is perfectly legal and it
threw.) And the start point is read with FLine.endPointOf rather than Node.to,
because a Close stores no point and the instruction after one starts from where the
close() left the pen.
body — 47 lines, from line 2441
fun intersect(a: FLine, b: FLine): List<Vec3> {
val ret: MutableList<Vec3> = ArrayList<Vec3>()
for (i in 1 until a.nodes.size) {
val ni: FLine.Node = a.nodes.get(i)
if (ni is FLine.CubicTo) {
val ci = CubicSegment3(a.endPointOf(i - 1), ni.c1, (ni).c2, ni.to)
for (j in 1 until b.nodes.size) {
val nj: FLine.Node = b.nodes.get(j)
if (nj is FLine.CubicTo) {
val cj = CubicSegment3(b.endPointOf(j - 1), (nj).c1, (nj).c2, nj.to)
val intersections = ci.intersection(cj, 1f)
intersections.forEach(Consumer<Pair<CubicSegment3, CubicSegment3>> { x: Pair<CubicSegment3, CubicSegment3?> ->
ret.add(
Vec3(x.first.a).add(x.first.d).mul(0.5f)
)
})
} else if (nj is FLine.LineTo) {
val intersections = ci.intersection(CubicSegment3(b.endPointOf(j - 1), nj.to), 1f)
intersections.forEach(Consumer<Pair<CubicSegment3, CubicSegment3>> { x: Pair<CubicSegment3, CubicSegment3?> ->
ret.add(
Vec3(x.first.a).add(x.first.d).mul(0.5f)
)
})
}
}
} else if (ni is FLine.LineTo) {
for (j in 1 until b.nodes.size) {
val nj: FLine.Node = b.nodes.get(j)
if (nj is FLine.CubicTo) {
val cj = CubicSegment3(b.endPointOf(j - 1), (nj).c1, (nj).c2, nj.to)
val intersections = cj.intersection(CubicSegment3(a.endPointOf(i - 1), ni.to), 1f)
intersections.forEach(Consumer<Pair<CubicSegment3, CubicSegment3>> { x: Pair<CubicSegment3, CubicSegment3?> ->
ret.add(
Vec3(x.first.a).add(x.first.d).mul(0.5f)
)
})
} else if (nj is FLine.LineTo) {
val aa: Vec3? =
intersectTwoLineSegments(b.endPointOf(j - 1), nj.to, a.endPointOf(i - 1), ni.to)
if (aa != null) ret.add(aa)
}
}
}
}
return ret
}
fun intersectX(a: FLine, x: Float): List<Vec3>
Where a crosses the vertical line x. Same i - 1 rules as intersect.
fun intersectX(a: FLine, x: Float): List<Vec3> {
val ret: MutableList<Vec3> = ArrayList<Vec3>()
for (i in 1 until a.nodes.size) {
val ni: FLine.Node = a.nodes.get(i)
if (ni is FLine.CubicTo) {
val ci = CubicSegment3(a.endPointOf(i - 1), (ni).c1, (ni).c2, ni.to, -x)
val roots = ci.findXZeroRoots()
roots.forEach(Consumer<Float> { xx: Float ->
val vv: Vec3 = ci.evaluate(xx)
vv.x += x
ret.add(vv)
})
} else if (ni is FLine.LineTo) {
val aa: Vec3 = a.endPointOf(i - 1)
val bb: Vec3 = ni.to
if (Math.min(aa.x, bb.x) <= x && Math.max(aa.x, bb.x) >= x) {
val ii: Vec3? = intersectTwoLineSegments(
aa,
bb,
Vec3(x, Math.max(aa.y, bb.y) + 1, 0.0f),
Vec3(x, Math.min(aa.y, bb.y) - 1, 0.0f)
)
if (ii != null) ret.add(ii)
}
}
}
return ret
}
fun intersectY(a: FLine, x: Float): List<Vec3>
Where a crosses the horizontal line x. Same i - 1 rules as intersect.
fun intersectY(a: FLine, x: Float): List<Vec3> {
val ret: MutableList<Vec3> = ArrayList<Vec3>()
for (i in 1 until a.nodes.size) {
val ni: FLine.Node = a.nodes.get(i)
if (ni is FLine.CubicTo) {
val ci =
CubicSegment3(a.endPointOf(i - 1), (ni).c1, (ni).c2, ni.to, 0.0f, -x)
val roots = ci.findYZeroRoots()
roots.forEach(Consumer<Float> { xx: Float ->
val vv: Vec3 = ci.evaluate(xx)
// the segment was shifted by -x in y, so the shift comes back off y
// (this was `vv.x += x`, copied from intersectX: every hit landed at y≈0)
vv.y += x
ret.add(vv)
})
} else if (ni is FLine.LineTo) {
val aa: Vec3 = a.endPointOf(i - 1)
val bb: Vec3 = ni.to
// a horizontal at y = x is crossed when the segment spans x in **y** (this
// compared the segment's x range, so steep segments were missed)
if (Math.min(aa.y, bb.y) <= x && Math.max(aa.y, bb.y) >= x) {
val ii: Vec3? = intersectTwoLineSegments(
aa, bb, Vec3(Math.max(aa.x, bb.x) + 1, x, 0.0f), Vec3(
Math.min(aa.x, bb.x) - 1, x, 0.0f
)
)
if (ii != null) ret.add(ii)
}
}
}
return ret
}
}
}