From 753352b53f28813ad88b639c6bc7582a688d54a7 Mon Sep 17 00:00:00 2001 From: treeform Date: Sun, 28 Nov 2021 11:11:38 -0800 Subject: [PATCH] f --- experiments/sweeps3.nim | 106 +++++++++ src/pixie/paths.nim | 491 +++++++++++++++++++++++++++++++++++----- tests/benchmark_svg.nim | 4 +- 3 files changed, 549 insertions(+), 52 deletions(-) create mode 100644 experiments/sweeps3.nim diff --git a/experiments/sweeps3.nim b/experiments/sweeps3.nim new file mode 100644 index 0000000..769067a --- /dev/null +++ b/experiments/sweeps3.nim @@ -0,0 +1,106 @@ + +import algorithm, bumpy, chroma, pixie/images, print, + sequtils, vmath, benchy, fidget2/perf + +import pixie, pixie/paths {.all.} + + +template test(name: string, p: Path, a: static int = 1, wr = wrNonZero) = + echo name + var image = newImage(200, 200) + timeIt " sweeps", a: + for i in 0 ..< a: + image.fill(color(0, 0, 0, 0)) + image.fillPath(p, color(1, 0, 0, 1), windingRule = wr) + image.writeFile("experiments/trapezoids/output_sweep.png") + + # var image2 = newImage(200, 200) + # timeIt " scanline", a: + # for i in 0 ..< a: + # image2.fill(color(0, 0, 0, 0)) + # image2.fillPath(p, color(1, 0, 0, 1), windingRule = wr) + # image2.writeFile("experiments/trapezoids/output_scanline.png") + + # let (score, diff) = diff(image, image2) + # if score > 0.05: + # echo "does not appear ot match" + # diff.writeFile("experiments/trapezoids/output_diff.png") + + +var rect = Path() +rect.moveTo(50.5, 50.5) +rect.lineTo(50.5, 150.5) +rect.lineTo(150.5, 150.5) +rect.lineTo(150.5, 50.5) +rect.closePath() + +var rhombus = Path() +rhombus.moveTo(100, 50) +rhombus.lineTo(150, 100) +rhombus.lineTo(100, 150) +rhombus.lineTo(50, 100) +rhombus.closePath() + +var heart = parsePath(""" + M 20 60 + A 40 40 90 0 1 100 60 + A 40 40 90 0 1 180 60 + Q 180 120 100 180 + Q 20 120 20 60 + z +""") + +var cricle = Path() +cricle.arc(100, 100, 50, 0, PI * 2, true) +cricle.closePath() + + +# Half arc (test cut lines) +var halfAarc = parsePath(""" + M 25 25 C 85 25 85 125 25 125 z +""") + +# Hour glass (test cross lines) +var hourGlass = parsePath(""" + M 20 20 L 180 20 L 20 180 L 180 180 z +""") + +# Hole +var hole = parsePath(""" + M 40 40 L 40 160 L 160 160 L 160 40 z + M 120 80 L 120 120 L 80 120 L 80 80 z +""") + +var holeEvenOdd = parsePath(""" + M 40 40 L 40 160 L 160 160 L 160 40 z + M 80 80 L 80 120 L 120 120 L 120 80 z +""") + +## g +var letterG = parsePath(""" + M 406 538 Q 394 546 359.5 558.5 T 279 571 Q 232 571 190.5 556 T 118 509.5 T 69 431 T 51 319 Q 51 262 68 214.5 T 117.5 132.5 T 197 78.5 T 303 59 Q 368 59 416.5 68.5 T 498 86 V 550 Q 498 670 436 724 T 248 778 Q 199 778 155.5 770 T 80 751 L 97 670 Q 125 681 165.5 689.5 T 250 698 Q 333 698 369.5 665 T 406 560 V 538 Z M 405 152 Q 391 148 367.5 144.5 T 304 141 Q 229 141 188.5 190 T 148 320 Q 148 365 159.5 397 T 190.5 450 T 235.5 481 T 288 491 Q 325 491 356 480.5 T 405 456 V 152 Z +""") +letterG.transform(scale(vec2(0.2, 0.2))) + +when defined(bench): + test("rect", rect, 100) + test("rhombus", rhombus, 100) + test("heart", heart, 100) + test("cricle", cricle, 100) + test("halfAarc", halfAarc, 100) + test("hourGlass", hourGlass, 100) + test("hole", hole, 100) + test("holeEvenOdd", holeEvenOdd, 100, wr=wrNonZero) + test("holeEvenOdd", holeEvenOdd, 100, wr=wrEvenOdd) + test("letterG", letterG, 100) +else: + # test("rect", rect) + # test("rhombus", rhombus) + # test("heart", heart) + # test("cricle", cricle) + # test("halfAarc", halfAarc) + # test("hourGlass", hourGlass) + #test("hole", hole, wr=wrEvenOdd) + test("holeEvenOdd", holeEvenOdd, wr=wrNonZero) + test("holeEvenOdd", holeEvenOdd, wr=wrEvenOdd) + # test("letterG", letterG) diff --git a/src/pixie/paths.nim b/src/pixie/paths.nim index 5d22dfe..31d3f5b 100644 --- a/src/pixie/paths.nim +++ b/src/pixie/paths.nim @@ -1537,64 +1537,73 @@ proc fillHits( mask.clearUnsafe(0, y, startX, y) mask.clearUnsafe(filledTo, y, mask.width, y) -proc fillShapes( - image: Image, - shapes: seq[seq[Vec2]], - color: SomeColor, - windingRule: WindingRule, - blendMode: BlendMode -) = - # Figure out the total bounds of all the shapes, - # rasterize only within the total bounds - let - rgbx = color.asRgbx() - segments = shapes.shapesToSegments() - aa = segments.requiresAntiAliasing() - bounds = computeBounds(segments).snapToPixels() - startX = max(0, bounds.x.int) - startY = max(0, bounds.y.int) - pathHeight = min(image.height, (bounds.y + bounds.h).int) - partitioning = partitionSegments(segments, startY, pathHeight - startY) +when not defined(sweeps): + proc fillShapes( + image: Image, + shapes: seq[seq[Vec2]], + color: SomeColor, + windingRule: WindingRule, + blendMode: BlendMode + ) = + # Figure out the total bounds of all the shapes, + # rasterize only within the total bounds + let + rgbx = color.asRgbx() + segments = shapes.shapesToSegments() + aa = segments.requiresAntiAliasing() + bounds = computeBounds(segments).snapToPixels() + startX = max(0, bounds.x.int) + startY = max(0, bounds.y.int) + pathHeight = min(image.height, (bounds.y + bounds.h).int) + partitioning = partitionSegments(segments, startY, pathHeight - startY) - var - coverages = newSeq[uint8](bounds.w.int) - hits = newSeq[(float32, int16)](partitioning.maxEntryCount) - numHits: int + var + coverages = newSeq[uint8](bounds.w.int) + hits = newSeq[(float32, int16)](partitioning.maxEntryCount) + numHits: int - for y in startY ..< pathHeight: - computeCoverage( - coverages, - hits, - numHits, - image.width.float32, - y, - startX, - aa, - partitioning, - windingRule - ) - if aa: - image.fillCoverage( - rgbx, - startX, - y, + for y in startY ..< pathHeight: + computeCoverage( coverages, - blendMode - ) - else: - image.fillHits( - rgbx, - startX, - y, hits, numHits, - windingRule, - blendMode + image.width.float32, + y, + startX, + aa, + partitioning, + windingRule ) + if aa: + image.fillCoverage( + rgbx, + startX, + y, + coverages, + blendMode + ) + else: + image.fillHits( + rgbx, + startX, + y, + hits, + numHits, + windingRule, + blendMode + ) - if blendMode == bmMask: - image.clearUnsafe(0, 0, 0, startY) - image.clearUnsafe(0, pathHeight, 0, image.height) + if blendMode == bmMask: + image.clearUnsafe(0, 0, 0, startY) + image.clearUnsafe(0, pathHeight, 0, image.height) +else: + proc fillShapes( + image: Image, + shapes: seq[seq[Vec2]], + color: SomeColor, + windingRule: WindingRule, + blendMode: BlendMode + ) proc fillShapes( mask: Mask, @@ -2030,5 +2039,385 @@ proc strokeOverlaps*( strokeShapes.transform(transform) strokeShapes.overlaps(test, wrNonZero) +when defined(sweeps): + import algorithm + + proc pixelCover(a0, b0: Vec2): float32 = + ## Returns the amount of area a given segment sweeps to the right + ## in a [0,0 to 1,1] box. + var + a = a0 + b = b0 + aI: Vec2 + bI: Vec2 + area: float32 = 0.0 + + # # Sort A on top. + # if a.y > b.y: + # let tmp = a + # a = b + # b = tmp + + # if (b.y < 0 or a.y > 1) or # Above or bellow, no effect. + # (a.x >= 1 and b.x >= 1) or # To the right, no effect. + # (a.y == b.y): # Horizontal line, no effect. + # return 0 + + if (a.x < 0 and b.x < 0) or # Both to the left. + (a.x == b.x): # Vertical line + # Area of the rectangle: + return (1 - clamp(a.x, 0, 1)) * (min(b.y, 1) - max(a.y, 0)) + + else: + # y = mm*x + bb + let + mm: float32 = (b.y - a.y) / (b.x - a.x) + bb: float32 = a.y - mm * a.x + + if a.x >= 0 and a.x <= 1 and a.y >= 0 and a.y <= 1: + # A is in pixel bounds. + aI = a + else: + aI = vec2((0 - bb) / mm, 0) + if aI.x < 0: + let y = mm * 0 + bb + # Area of the extra rectangle. + area += (min(bb, 1) - max(a.y, 0)).clamp(0, 1) + aI = vec2(0, y.clamp(0, 1)) + elif aI.x > 1: + let y = mm * 1 + bb + aI = vec2(1, y.clamp(0, 1)) + + if b.x >= 0 and b.x <= 1 and b.y >= 0 and b.y <= 1: + # B is in pixel bounds. + bI = b + else: + bI = vec2((1 - bb) / mm, 1) + if bI.x < 0: + let y = mm * 0 + bb + # Area of the extra rectangle. + area += (min(b.y, 1) - max(bb, 0)).clamp(0, 1) + bI = vec2(0, y.clamp(0, 1)) + elif bI.x > 1: + let y = mm * 1 + bb + bI = vec2(1, y.clamp(0, 1)) + + area += ((1 - aI.x) + (1 - bI.x)) / 2 * (bI.y - aI.y) + return area + + proc intersectsInner*(a, b: Segment, at: var Vec2): bool {.inline.} = + ## Checks if the a segment intersects b segment. + ## If it returns true, at will have point of intersection + let + s1 = a.to - a.at + s2 = b.to - b.at + denominator = (-s2.x * s1.y + s1.x * s2.y) + s = (-s1.y * (a.at.x - b.at.x) + s1.x * (a.at.y - b.at.y)) / denominator + t = (s2.x * (a.at.y - b.at.y) - s2.y * (a.at.x - b.at.x)) / denominator + + if s > 0 and s < 1 and t > 0 and t < 1: + #print s, t + at = a.at + (t * s1) + return true + + type + + Trapezoid = object + nw, ne, se, sw: Vec2 + + SweepLine = object + #m, x, b: float32 + atx, tox: float32 + winding: int16 + + proc toLine(s: (Segment, int16)): SweepLine = + var line = SweepLine() + line.atx = s[0].at.x + line.tox = s[0].to.x + # y = mx + b + # line.m = (s.at.y - s.to.y) / (s.at.x - s.to.x) + # line.b = s.at.y - line.m * s.at.x + line.winding = s[1] + return line + + proc computeBounds(polygons: seq[seq[Vec2]]): Rect = + ## Compute the bounds of the segments. + var + xMin = float32.high + xMax = float32.low + yMin = float32.high + yMax = float32.low + for segments in polygons: + for v in segments: + xMin = min(xMin, v.x) + xMax = max(xMax, v.x) + yMin = min(yMin, v.y) + yMax = max(yMax, v.y) + + if xMin.isNaN() or xMax.isNaN() or yMin.isNaN() or yMax.isNaN(): + discard + else: + result.x = xMin + result.y = yMin + result.w = xMax - xMin + result.h = yMax - yMin + + proc binaryInsert(arr: var seq[float32], v: float32) = + if arr.len == 0: + arr.add(v) + return + var + L = 0 + R = arr.len - 1 + while L < R: + let m = (L + R) div 2 + if arr[m] ~= v: + return + elif arr[m] < v: + L = m + 1 + else: # arr[m] > v: + R = m - 1 + if arr[L] ~= v: + return + elif arr[L] > v: + #print "insert", v, arr, L, R + arr.insert(v, L) + else: + #print "insert", v, arr, L, R + arr.insert(v, L + 1) + + proc fillShapes( + image: Image, + shapes: seq[seq[Vec2]], + color: SomeColor, + windingRule: WindingRule, + blendMode: BlendMode + ) = + const q = 1/256.0 + let rgbx = color.rgbx + var segments = shapes.shapesToSegments() + let + bounds = computeBounds(segments).snapToPixels() + startX = max(0, bounds.x.int) + + # Create sorted segments. + segments.sort(proc(a, b: (Segment, int16)): int = cmp(a[0].at.y, b[0].at.y)) + + # Compute cut lines + var cutLines: seq[float32] + for s in segments: + cutLines.binaryInsert(s[0].at.y) + cutLines.binaryInsert(s[0].to.y) + + if cutLines.len == 0 or cutLines.len == 1: + return + + var + sweeps = newSeq[seq[SweepLine]](cutLines.len - 1) # dont add bottom cutLine + lastSeg = 0 + i = 0 + while i < sweeps.len: + + #for i, sweep in sweeps.mpairs: + #print "sweep", i, cutLines[i] + + if lastSeg < segments.len: + + while segments[lastSeg][0].at.y == cutLines[i]: + let s = segments[lastSeg] + + if s[0].at.y != s[0].to.y: + + #print s + if s[0].to.y != cutLines[i + 1]: + #print "needs cut?", s + + #quit("need to cut lines") + var at: Vec2 + var seg = s[0] + for j in i ..< sweeps.len: + let y = cutLines[j + 1] + if intersects(line(vec2(0, y), vec2(1, y)), seg, at): + #print "cutting", j, seg + #print "add cut", j, segment(seg.at, at) + sweeps[j].add(toLine((segment(seg.at, at), s[1]))) + seg = segment(at, seg.to) + else: + if seg.at.y != seg.to.y: + #print "add rest", j, segment(seg.at, seg.to) + sweeps[j].add(toLine(s)) + # else: + # print "micro?" + break + else: + #print "add", s + sweeps[i].add(toLine(s)) + + inc lastSeg + + if lastSeg >= segments.len: + break + inc i + + i = 0 + #echo sweeps.len, " vs ", cutLines.len + + while i < sweeps.len: + for t in 0 ..< 10: + # keep cutting sweep + var needsCut = false + var cutterLine: float32 = 0 + block doubleFor: + for a in sweeps[i]: + let aSeg = segment(vec2(a.atx, cutLines[i]), vec2(a.tox, cutLines[i+1])) + for b in sweeps[i]: + let bSeg = segment(vec2(b.atx, cutLines[i]), vec2(b.tox, cutLines[i+1])) + var at: Vec2 + if intersectsInner(aSeg, bSeg, at): + needsCut = true + cutterLine = at.y + break doubleFor + if false and needsCut: + #echo "doing a cut" + var + thisSweep = sweeps[i] + sweeps[i].setLen(0) + sweeps.insert(newSeq[SweepLine](), i + 1) + for a in thisSweep: + let seg = segment(vec2(a.atx, cutLines[i]), vec2(a.tox, cutLines[i+1])) + var at: Vec2 + if intersects(line(vec2(0, cutterLine), vec2(1, cutterLine)), seg, at): + sweeps[i+0].add(toLine((segment(seg.at, at), a.winding))) + sweeps[i+1].add(toLine((segment(at, seg.to), a.winding))) + cutLines.binaryInsert(cutterLine) + else: + break + inc i + + + i = 0 + while i < sweeps.len: + # Sort the sweep by X + sweeps[i].sort proc(a, b: SweepLine): int = + result = cmp(a.atx, b.atx) + if result == 0: + result = cmp(a.tox, b.tox) + + # Do winding order + var + pen = 0 + prevFill = false + j = 0 + # print "sweep", i, "--------------" + while j < sweeps[i].len: + let a = sweeps[i][j] + # print a.winding + if a.winding == 1: + inc pen + if a.winding == -1: + dec pen + # print j, pen, prevFill, shouldFill(windingRule, pen) + let thisFill = shouldFill(windingRule, pen) + if prevFill == thisFill: + # remove this line + # print "remove", j + sweeps[i].delete(j) + continue + prevFill = thisFill + inc j + + # print sweeps[i] + + inc i + + #print sweeps + # for s in 0 ..< sweeps.len: + # let + # y1 = cutLines[s] + # echo "M -100 ", y1 + # echo "L 300 ", y1 + # for line in sweeps[s]: + # let + # nw = vec2(line.atx, cutLines[s]) + # sw = vec2(line.tox, cutLines[s + 1]) + # echo "M ", nw.x, " ", nw.y + # echo "L ", sw.x, " ", sw.y + + proc computeCoverage( + coverages: var seq[uint8], + y: int, + startX: int, + cutLines: seq[float32], + currCutLine: int, + sweep: seq[SweepLine] + ) = + + # if sweep.len mod 2 != 0: + # return + + let + sweepHeight = cutLines[currCutLine + 1] - cutLines[currCutLine] + yFracTop = ((y.float32 - cutLines[currCutLine]) / sweepHeight).clamp(0, 1) + yFracBottom = ((y.float32 + 1 - cutLines[currCutLine]) / sweepHeight).clamp(0, 1) + var i = 0 + while i < sweep.len: + let + nwX = mix(sweep[i+0].atx, sweep[i+0].tox, yFracTop) + neX = mix(sweep[i+1].atx, sweep[i+1].tox, yFracTop) + + swX = mix(sweep[i+0].atx, sweep[i+0].tox, yFracBottom) + seX = mix(sweep[i+1].atx, sweep[i+1].tox, yFracBottom) + + minWi = min(nwX, swX).int + maxWi = max(nwX, swX).ceil.int + + minEi = min(neX, seX).int + maxEi = max(neX, seX).ceil.int + + let + nw = vec2(sweep[i+0].atx, cutLines[currCutLine]) + sw = vec2(sweep[i+0].tox, cutLines[currCutLine + 1]) + for x in minWi ..< maxWi: + var area = pixelCover(nw - vec2(x.float32, y.float32), sw - vec2(x.float32, y.float32)) + coverages[x - startX] += (area * 255).uint8 + + let x = maxWi + var midArea = pixelCover(nw - vec2(x.float32, y.float32), sw - vec2(x.float32, y.float32)) + var midArea8 = (midArea * 255).uint8 + for x in maxWi ..< minEi: + coverages[x - startX] += midArea8 + + let + ne = vec2(sweep[i+1].atx, cutLines[currCutLine]) + se = vec2(sweep[i+1].tox, cutLines[currCutLine + 1]) + for x in minEi ..< maxEi: + var area = midArea - pixelCover(ne - vec2(x.float32, y.float32), se - vec2(x.float32, y.float32)) + coverages[x - startX] += (area * 255).uint8 + + i += 2 + + var + currCutLine = 0 + coverages = newSeq[uint8](bounds.w.int) + #echo "coverages", coverages.len + for scanLine in cutLines[0].int ..< cutLines[^1].ceil.int: + zeroMem(coverages[0].addr, coverages.len) + + coverages.computeCoverage(scanLine, startX, cutLines, currCutLine, sweeps[currCutLine]) + while cutLines[currCutLine + 1] < scanLine.float + 1.0: + inc currCutLine + if currCutLine == sweeps.len: + break + coverages.computeCoverage(scanLine, startX, cutLines, currCutLine, sweeps[currCutLine]) + + image.fillCoverage( + rgbx, + startX = startX, + y = scanLine, + coverages, + blendMode + ) + + when defined(release): {.pop.} diff --git a/tests/benchmark_svg.nim b/tests/benchmark_svg.nim index c158d6d..1c36a83 100644 --- a/tests/benchmark_svg.nim +++ b/tests/benchmark_svg.nim @@ -1,6 +1,8 @@ -import benchy, pixie/fileformats/svg +import benchy, pixie/fileformats/svg, pixie/fileformats/png let data = readFile("tests/fileformats/svg/Ghostscript_Tiger.svg") +writeFile("tiger.png", decodeSvg(data).encodePng()) + timeIt "svg decode": discard decodeSvg(data)