Skip to content
This repository has been archived by the owner on Jul 8, 2022. It is now read-only.

Commit

Permalink
Some fixes and implement NON_ZERO winding on the rasterizer
Browse files Browse the repository at this point in the history
  • Loading branch information
soywiz committed Apr 18, 2020
1 parent 2768207 commit f88f3ef
Show file tree
Hide file tree
Showing 4 changed files with 171 additions and 75 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ internal inline fun approximateCurve(
//println("curveSteps: $rcurveSteps, emittedCount=$emittedCount")
}

internal inline fun VectorPath.emitPoints2(crossinline flush: (close: Boolean) -> Unit, crossinline emit: (x: Double, y: Double, move: Boolean) -> Unit) {
internal inline fun VectorPath.emitPoints2(crossinline flush: (close: Boolean) -> Unit = {}, crossinline emit: (x: Double, y: Double, move: Boolean) -> Unit) {
var lx = 0.0
var ly = 0.0
flush(false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ class Bitmap32Context2d(val bmp: Bitmap32, val antialiasing: Boolean) : com.soyw
val gradientFiller = GradientFiller()
val bitmapFiller = BitmapFiller()
val scanlineWriter = ScanlineWriter()
private val tempPath = VectorPath()
private val tempPath = VectorPath(winding = Winding.NON_ZERO)
//private val tempPath = VectorPath(winding = Winding.EVEN_ODD)
private val tempFillStrokeTemp = FillStrokeTemp()

override fun render(state: Context2d.State, fill: Boolean) {
Expand All @@ -57,7 +58,7 @@ class Bitmap32Context2d(val bmp: Bitmap32, val antialiasing: Boolean) : com.soyw
//rasterizer.scale = 1
//state.path.getFilledStroke(state.lineWidth, state.startLineCap, state.endLineCap, state.lineJoin, rasterizer.scale)
tempPath.clear()
state.path.getFilledStroke(state.lineWidth, state.startLineCap, state.endLineCap, state.lineJoin, temp = tempFillStrokeTemp, outFill = tempPath)
state.path.strokeToFill(state.lineWidth, state.startLineCap, state.endLineCap, state.lineJoin, temp = tempFillStrokeTemp, outFill = tempPath)
}

fun flush() {
Expand All @@ -66,7 +67,7 @@ class Bitmap32Context2d(val bmp: Bitmap32, val antialiasing: Boolean) : com.soyw
rasterizer.quality = if (antialiasing) 4 else 1
scanlineWriter.filler = filler
scanlineWriter.reset()
rasterizer.rasterizeFill(bounds) { x0, x1, y ->
rasterizer.rasterizeFill(bounds, winding = fillPath.winding) { x0, x1, y ->
scanlineWriter.select(x0, x1, y)
}
scanlineWriter.flush()
Expand Down
118 changes: 65 additions & 53 deletions korim/src/commonMain/kotlin/com/soywiz/korim/vector/StrokeToFill.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,70 +5,82 @@ import com.soywiz.korma.geom.*
import com.soywiz.korma.geom.vector.*

class FillStrokeTemp {
val points = PointArrayList()
fun reset() {
points.clear()
private var weight: Double = 1.0
private lateinit var outFill: VectorPath
private var startCap: LineCap = LineCap.BUTT
private var endCap: LineCap = LineCap.BUTT
private var joins: LineJoin = LineJoin.BEVEL
internal val strokePoints = PointArrayList()
internal val rfillPoints = PointArrayList()

internal fun computeStroke() {
val weightD2 = weight / 2
rfillPoints.clear()
val nstrokePoints = strokePoints.size
for (n in 0 until nstrokePoints) {
val first = n == 0
val last = n == nstrokePoints - 1
val middle = !first && !last
val lastX = if (first) 0.0 else strokePoints.getX(n - 1)
val lastY = if (first) 0.0 else strokePoints.getY(n - 1)
val x = strokePoints.getX(n)
val y = strokePoints.getY(n)
val nextX = if (last) 0.0 else strokePoints.getX(n + 1)
val nextY = if (last) 0.0 else strokePoints.getY(n + 1)
val angle = Angle.between(lastX, lastY, x, y)
val a1 = angle - 45.degrees
val a2 = angle + 45.degrees
when {
first -> {
}
else -> {
if (n == 1) {
outFill.moveTo(lastX + a1.cosine * weightD2, lastY + a1.sine * weightD2)
rfillPoints.add(lastX + a2.cosine * weightD2, lastY + a2.sine * weightD2)
}
outFill.lineTo(x + a1.cosine * weightD2, y + a1.sine * weightD2)
if (!last) {
}
rfillPoints.add(x + a2.cosine * weightD2, y + a2.sine * weightD2)
}
}
}
// Draw the rest of the points
for (n in 0 until rfillPoints.size) {
val m = rfillPoints.size - n - 1
val x = rfillPoints.getX(m)
val y = rfillPoints.getY(m)
outFill.lineTo(x, y)
}
outFill.close()
strokePoints.clear()
}

fun set(outFill: VectorPath, weight: Double, startCap: LineCap, endCap: LineCap, joins: LineJoin) {
this.outFill = outFill
this.weight = weight
this.startCap = startCap
this.endCap = endCap
this.joins = joins
}
}

// @TODO: Implement LineCap + LineJoin
fun VectorPath.getFilledStroke(
fun VectorPath.strokeToFill(
weight: Double,
startCap: LineCap,
endCap: LineCap,
joins: LineJoin,
scale: Int = 1,
temp: FillStrokeTemp = FillStrokeTemp(),
outFill: VectorPath = VectorPath()
outFill: VectorPath = VectorPath(winding = Winding.NON_ZERO)
): VectorPath {
val stroke = this@getFilledStroke
var count = 0
val weightD2 = weight * scale / 2
val points = temp.points
temp.reset()

fun flush() {
if (points.size <= 0) return
for (n in 0 until points.size) {
val m = points.size - n - 1
val x = points.getX(m)
val y = points.getY(m)
outFill.lineTo(x, y)
}
outFill.close()
points.clear()
val stroke = this@strokeToFill
temp.set(outFill, weight, startCap, endCap, joins)
stroke.emitPoints2 { x, y, move ->
if (move) temp.computeStroke()
temp.strokePoints.add(x * scale, y * scale)
}

stroke.emitPoints2({ close ->
if (close && points.isNotEmpty()) {
//println("CLOSE")
//points.add(points.getX(0), points.getY(0))
} else {
//println("FINISH")
}
}, { x, y, move ->
if (move) {
flush()
count = 0
}
val x = x * scale
val y = y * scale
if (count > 0) {
val angle = Angle.between(lastX, lastY, x, y)
val a1 = angle - 45.degrees
val a2 = angle + 45.degrees
if (count == 1) {
outFill.moveTo(lastX + a1.cosine * weightD2, lastY + a1.sine * weightD2)
}
outFill.lineTo(x + a1.cosine * weightD2, y + a1.sine * weightD2)
points.add(lastX + a2.cosine * weightD2, lastY + a2.sine * weightD2)
points.add(x + a2.cosine * weightD2, y + a2.sine * weightD2)
}
lastX = x
lastY = y
count++
})
flush()

temp.computeStroke()
return outFill
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.soywiz.korim.vector.rasterizer
import com.soywiz.kds.*
import com.soywiz.kds.iterators.fastForEach
import com.soywiz.korma.geom.*
import com.soywiz.korma.geom.vector.*
import kotlin.math.absoluteValue
import kotlin.math.max
import kotlin.math.min
Expand Down Expand Up @@ -163,7 +164,7 @@ class Rasterizer {
}
}

fun rasterizeFill(bounds: Rectangle, quality: Int = this.quality, stats: Stats? = null, callback: RasterizerCallback) {
fun rasterizeFill(bounds: Rectangle, quality: Int = this.quality, stats: Stats? = null, winding: Winding = Winding.NON_ZERO, callback: RasterizerCallback) {
stats?.reset()
val xmin = bounds.left.s
val xmax = bounds.right.s
Expand Down Expand Up @@ -202,38 +203,120 @@ class Rasterizer {
var yCount = 0
yList.fastForEach { y ->
yCount++
tempX.clear()
tempXW.clear()
edgesChecked += forEachActiveEdgeAtY(y) {
if (!it.isCoplanarX) {
tempX.add(it.intersectX(y))
tempXW.add(it.intersectX(y), it.wind)
}
}
genericSort(tempX, 0, tempX.size - 1, IntArrayListSort)
if (tempX.size >= 2) {
for (i in 0 until tempX.size - 1 step 2) {
val a = tempX[i]
val b = tempX[i + 1]
func(a, b, y)
edgesEmitted++
genericSort(tempXW, 0, tempXW.size - 1, IntArrayListSort)
val tempX = tempXW.x
val tempW = tempXW.w
if (tempXW.size >= 2) {
when (winding) {
Winding.EVEN_ODD -> {
for (i in 0 until tempX.size - 1 step 2) {
val a = tempX[i]
val b = tempX[i + 1]
func(a, b, y)
edgesEmitted++
}
}
Winding.NON_ZERO -> {
//println("NON-ZERO")

var count = 0
var startX = 0
var endX = 0
var pending = false

for (i in 0 until tempX.size - 1) {
val a = tempX[i]
count += tempW[i]
val b = tempX[i + 1]
if (count != 0) {
if (pending && a != endX) {
func(startX, endX, y)
edgesEmitted++
startX = a
endX = b
} else {
if (!pending) {
startX = a
}
endX = b
}
//func(a, b, y)
pending = true
}
}

if (pending) {
func(startX, endX, y)
edgesEmitted++
}

/*
var count = 0
var i = 0
while (i < tempX.size) {
val startX = tempX[i]
count += tempW[i]
if (count != 0) {
while (i < tempX.size) {
count += tempW[i]
i++
if (count == 0) break
}
val endX = tempX[i - 1]
func(startX, endX, y)
edgesEmitted++
} else {
i++
}
}
*/
}
}
}
}
stats?.chunk(edgesChecked, edgesEmitted, yCount)
}
}
private val yList = IntArrayList(1024)
private val tempX = IntArrayList(1024)
private val tempXW = XWithWind()

private class XWithWind {
val x = IntArrayList(1024)
val w = IntArrayList(1024)
val size get() = x.size

fun add(x: Int, wind: Int) {
this.x.add(x)
this.w.add(wind)
}

fun clear() {
x.clear()
w.clear()
}
}

// @TODO: Change once KDS is updated
object IntArrayListSort : SortOps<IntArrayList>() {
override fun compare(subject: IntArrayList, l: Int, r: Int): Int = subject[l].compareTo(subject[r])
override fun swap(subject: IntArrayList, indexL: Int, indexR: Int) {
val l = subject[indexL]
val r = subject[indexR]
subject[indexR] = l
subject[indexL] = r
private object IntArrayListSort : SortOps<XWithWind>() {
override fun compare(subject: XWithWind, l: Int, r: Int): Int = subject.x[l].compareTo(subject.x[r])
override fun swap(subject: XWithWind, indexL: Int, indexR: Int) {
subject.x.swap(indexL, indexR)
subject.w.swap(indexL, indexR)
}
}

var strokeWidth: Double = 1.0
}

private fun IntArrayList.swap(x: Int, y: Int) {
val l = this[x]
val r = this[y]
this[x] = r
this[y] = l
}

0 comments on commit f88f3ef

Please sign in to comment.