From d097fda55b4221e6c2664b50594105756e680096 Mon Sep 17 00:00:00 2001 From: Ryan Oldenburg Date: Sun, 18 Jul 2021 13:20:22 -0500 Subject: [PATCH] context isPointInPath isPointInStroke --- pixie.nimble | 2 +- src/pixie/contexts.nim | 52 ++++++++++++++++++++++++++++ src/pixie/paths.nim | 76 ++++++++++++++++++++++++++++++++++++++--- tests/test_contexts.nim | 29 ++++++++++++++++ tests/test_paths.nim | 38 +++++++++++++++++++++ 5 files changed, 191 insertions(+), 6 deletions(-) diff --git a/pixie.nimble b/pixie.nimble index 4dfaca5..57bb0e0 100644 --- a/pixie.nimble +++ b/pixie.nimble @@ -1,4 +1,4 @@ -version = "2.1.0" +version = "2.1.1" author = "Andre von Houck and Ryan Oldenburg" description = "Full-featured 2d graphics library for Nim." license = "MIT" diff --git a/src/pixie/contexts.nim b/src/pixie/contexts.nim index 3e73989..c0a050b 100644 --- a/src/pixie/contexts.nim +++ b/src/pixie/contexts.nim @@ -647,3 +647,55 @@ proc arcTo*(ctx: Context, x1, y1, x2, y2, radius: float32) = proc arcTo*(ctx: Context, a, b: Vec2, r: float32) = ## Adds a circular arc using the given control points and radius. ctx.path.arcTo(a, b, r) + +proc isPointInPath*( + ctx: Context, path: Path, pos: Vec2, windingRule = wrNonZero +): bool = + ## Returns whether or not the specified point is contained in the current path. + path.fillOverlaps(pos, ctx.mat, windingRule) + +proc isPointInPath*( + ctx: Context, path: Path, x, y: float32, windingRule = wrNonZero +): bool {.inline.} = + ## Returns whether or not the specified point is contained in the current path. + ctx.isPointInPath(path, vec2(x, y), windingRule) + +proc isPointInPath*( + ctx: Context, pos: Vec2, windingRule = wrNonZero +): bool {.inline.} = + ## Returns whether or not the specified point is contained in the current path. + ctx.isPointInPath(ctx.path, pos, windingRule) + +proc isPointInPath*( + ctx: Context, x, y: float32, windingRule = wrNonZero +): bool {.inline.} = + ## Returns whether or not the specified point is contained in the current path. + ctx.isPointInPath(ctx.path, vec2(x, y), windingRule) + +proc isPointInStroke*(ctx: Context, path: Path, pos: Vec2): bool = + ## Returns whether or not the specified point is inside the area contained + ## by the stroking of a path. + path.strokeOverlaps( + pos, + ctx.mat, + ctx.lineWidth, + ctx.lineCap, + ctx.lineJoin, + ctx.miterLimit, + ctx.lineDash + ) + +proc isPointInStroke*(ctx: Context, path: Path, x, y: float32): bool {.inline.} = + ## Returns whether or not the specified point is inside the area contained + ## by the stroking of a path. + ctx.isPointInStroke(path, vec2(x, y)) + +proc isPointInStroke*(ctx: Context, pos: Vec2): bool {.inline.} = + ## Returns whether or not the specified point is inside the area contained + ## by the stroking of a path. + ctx.isPointInStroke(ctx.path, pos) + +proc isPointInStroke*(ctx: Context, x, y: float32): bool {.inline.} = + ## Returns whether or not the specified point is inside the area contained + ## by the stroking of a path. + ctx.isPointInStroke(ctx.path, vec2(x, y)) diff --git a/src/pixie/paths.nim b/src/pixie/paths.nim index ff28af9..4cd8c95 100644 --- a/src/pixie/paths.nim +++ b/src/pixie/paths.nim @@ -1029,7 +1029,7 @@ proc requiresAntiAliasing(segments: seq[(Segment, int16)]): bool = template hasFractional(v: float32): bool = v - trunc(v) != 0 - for i in 0 ..< segments.len: # For arc + for i in 0 ..< segments.len: # For gc:arc let segment = segments[i][0] if segment.at.x != segment.to.x or segment.at.x.hasFractional() or # at.x and to.x are the same @@ -1045,7 +1045,7 @@ proc computePixelBounds(segments: seq[(Segment, int16)]): Rect = xMax = float32.low yMin = float32.high yMax = float32.low - for i in 0 ..< segments.len: # For arc + for i in 0 ..< segments.len: # For gc:arc let segment = segments[i][0] xMin = min(xMin, min(segment.at.x, segment.to.x)) xMax = max(xMax, max(segment.at.x, segment.to.x)) @@ -1153,7 +1153,7 @@ iterator walk( var prevAt: float32 count: int32 - for i in 0 ..< numHits: + for i in 0 ..< numHits: # For gc:arc let (at, winding) = hits[i] if windingRule == wrNonZero and (count != 0) == (count + winding != 0) and @@ -1201,13 +1201,13 @@ proc computeCoverages( scanline.a.y = yLine scanline.b.y = yLine numHits = 0 - for i in 0 ..< partitioning.partitions[partitionIndex].len: # For arc + for i in 0 ..< partitioning.partitions[partitionIndex].len: # For gc:arc let segment = partitioning.partitions[partitionIndex][i][0] winding = partitioning.partitions[partitionIndex][i][1] if segment.at.y <= scanline.a.y and segment.to.y >= scanline.a.y: var at: Vec2 - if segment.to != at and scanline.intersects(segment, at): + if scanline.intersects(segment, at) and segment.to != at: if numHits == hits.len: hits.setLen(hits.len * 2) hits[numHits] = (min(at.x, size.x), winding) @@ -1889,5 +1889,71 @@ proc strokePath*( fill.draw(mask) image.draw(fill, blendMode = paint.blendMode) +proc overlaps( + shapes: seq[seq[Vec2]], + test: Vec2, + windingRule: WindingRule +): bool = + var hits: seq[(float32, int16)] + + let + scanline = line(vec2(0, test.y), vec2(1000, test.y)) + segments = shapes.shapesToSegments() + for i in 0 ..< segments.len: # For gc:arc + let + segment = segments[i][0] + winding = segments[i][1] + if segment.at.y <= scanline.a.y and segment.to.y >= scanline.a.y: + var at: Vec2 + if scanline.intersects(segment, at) and segment.to != at: + hits.add((at.x, winding)) + + if hits.len > 32: + quickSort(hits, 0, hits.high) + else: + insertionSort(hits, hits.high) + + var count: int + for i in 0 ..< hits.len: # For gc:arc + let (at, winding) = hits[i] + if at > test.x: + result = shouldFill(windingRule, count) + break + count += winding + +proc fillOverlaps*( + path: Path, + test: Vec2, + transform: Vec2 | Mat3 = vec2(), ## Applied to the path, not the test point. + windingRule = wrNonZero +): bool = + ## Returns whether or not the specified point is contained in the current path. + var shapes = parseSomePath(path, true, transform.pixelScale()) + shapes.transform(transform) + shapes.overlaps(test, windingRule) + +proc strokeOverlaps*( + path: Path, + test: Vec2, + transform: Vec2 | Mat3 = vec2(), ## Applied to the path, not the test point. + strokeWidth = 1.0, + lineCap = lcButt, + lineJoin = ljMiter, + miterLimit = defaultMiterLimit, + dashes: seq[float32] = @[], +): bool = + ## Returns whether or not the specified point is inside the area contained + ## by the stroking of a path. + var strokeShapes = strokeShapes( + parseSomePath(path, false, transform.pixelScale()), + strokeWidth, + lineCap, + lineJoin, + miterLimit, + dashes + ) + strokeShapes.transform(transform) + strokeShapes.overlaps(test, wrNonZero) + when defined(release): {.pop.} diff --git a/tests/test_contexts.nim b/tests/test_contexts.nim index c982305..f66e597 100644 --- a/tests/test_contexts.nim +++ b/tests/test_contexts.nim @@ -602,3 +602,32 @@ block: rhino = readImage("tests/images/rhino.png") ctx.drawImage(rhino, rect(33, 71, 104, 124), rect(21, 20, 87, 104)); image.writeFile("tests/images/context/draw_image_rhino2.png") + +block: + let + image = newImage(100, 100) + ctx = newContext(image) + ctx.rect(10, 10, 100, 100) + doAssert ctx.isPointInPath(30, 70) + +block: + let + image = newImage(300, 150) + ctx = newContext(image) + ctx.arc(150, 75, 50, 0, 2 * PI) + doAssert ctx.isPointInPath(150, 50) + +block: + let + image = newImage(100, 100) + ctx = newContext(image) + ctx.rect(10, 10, 100, 100) + doAssert ctx.isPointInStroke(50, 10) + +block: + let + image = newImage(300, 150) + ctx = newContext(image) + ctx.ellipse(150, 75, 40, 60) + ctx.lineWidth = 25 + doAssert ctx.isPointInStroke(110, 75) diff --git a/tests/test_paths.nim b/tests/test_paths.nim index 2bc40b3..915639e 100644 --- a/tests/test_paths.nim +++ b/tests/test_paths.nim @@ -530,3 +530,41 @@ block: ctx.stroke(); surface.writeFile("tests/images/paths/arcTo3.png") + +block: + var path: Path + path.rect(0, 0, 10, 10) + + doAssert path.fillOverlaps(vec2(5, 5)) + doAssert path.fillOverlaps(vec2(0, 0)) + doAssert path.fillOverlaps(vec2(9, 0)) + doAssert path.fillOverlaps(vec2(0, 9)) + doAssert not path.fillOverlaps(vec2(10, 10)) + +block: + var path: Path + path.ellipse(20, 20, 20, 10) + + doAssert not path.fillOverlaps(vec2(0, 0)) + doAssert path.fillOverlaps(vec2(20, 20)) + doAssert path.fillOverlaps(vec2(10, 20)) + doAssert path.fillOverlaps(vec2(30, 20)) + +block: + var path: Path + path.rect(10, 10, 10, 10) + + doAssert path.strokeOverlaps(vec2(10, 10)) + doAssert path.strokeOverlaps(vec2(20.1, 20.1)) + doAssert not path.strokeOverlaps(vec2(5, 5)) + +block: + var path: Path + path.ellipse(20, 20, 20, 10) + + doAssert not path.strokeOverlaps(vec2(0, 0)) + doAssert not path.strokeOverlaps(vec2(20, 20)) + doAssert path.strokeOverlaps(vec2(0, 20)) + doAssert path.strokeOverlaps(vec2(40, 20)) + doAssert path.strokeOverlaps(vec2(19.8, 30.2)) + doAssert not path.strokeOverlaps(vec2(19.4, 30.6))