Test if a point is inside a Shape

This post is about testing if 2D points are located inside a 2D shape or not. It is actually very simple to test :

if(myShape.contains(myPoint) { ... }
// or
if(myPoint in myShape) { ... }

But what if the shape has a hole? Like in the letter “O”? In that case we need to make sure that the hole has the right winding when creating the shape.

Hole with correct winding

TestShapeContainsVector2-2022-02-13-11.35.49

imports
import aBeLibs.geometry.circleish
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.isolated
import org.openrndr.extensions.Screenshots
import org.openrndr.math.Vector2
import org.openrndr.shape.LineSegment
import org.openrndr.shape.Shape
import org.openrndr.shape.contains

/**
 * Shows that Shape.contains(Vector2) does take into account holes
 * if they have the correct winding (use .reversed!)
 */
fun main() = application {
    program {
        val shp = Shape(
            listOf(
                circleish(drawer.bounds.center, 200.0).contour,
                circleish(drawer.bounds.center, 100.0).contour.reversed
            )
        )
        val allPoints = LineSegment(Vector2.ZERO, drawer.bounds.dimensions)
            .sub(0.1, 0.9).contour.equidistantPositions(20)
        val insidePoints = allPoints.filter { it in shp }

        extend {
            drawer.isolated {
                clear(ColorRGBa.WHITE)
                stroke = null
                fill = ColorRGBa.GRAY
                shape(shp)
                fill = ColorRGBa.GREEN
                circles(insidePoints, 10.0)
                fill = ColorRGBa.PINK
                circles(allPoints, 6.0)
            }
        }
    }
}

Hole with incorrect winding

TestShapeContainsVector2-2022-02-13-11.36.14

The only difference between both images is that in the second one I forgot to use .reversed when constructing the second contour (the hole).

Note: In this program I used a function I called circleish() but you can replace it by Circle() to have regular circles instead.

I’ll explain two lines of this program. The first one is

val allPoints = LineSegment(Vector2.ZERO, drawer.bounds.dimensions)
     .sub(0.1, 0.9).contour.equidistantPositions(20)

It creates a straight line segment that goes from top-left to bottom right of the canvas. Another way to write the same would be LineSegment(0.0, 0.0, width.toDouble(), height.toDouble()) but I prefer to use Vector2s because there’s so many things you can do with them in OPENRNDR (pass them to methods, query information about them, do arithmetic operations, etc).

From that Segment I obtain a shorter sub-segment that goes from 10% to 90% of the original length because I want to leave some margin and avoid the line starting and ending on the edges of the window.

Next I convert it to a ShapeContour because ShapeContour provides a bunch of interesting methods, for example equidistantPositions() which returns a list of Vector2.

Now I can use this list of positions in the .circles() method to draw many circles.

The second line I wanted to describe is

val insidePoints = allPoints.filter { it in shp }

which creates a subset of allPoints, specifically, the ones that are inside shp. Very short, right? :slight_smile:

Why write code inside program {} but before extends {} ?

Code outside the extends {} block runs just once instead of once per animation frame.

Inside program I already have access to drawer.bounds, width, height, drawer, etc. which I may need to lay out things relative to the canvas. I could as well do all calculations inside extend { ... }, but then the program would be recalculating everything on every animation frame.

With simple programs like this it does not make much of a difference but I often create designs that take up to a minute to generate and I wouldn’t want to recreate such designs again and again keeping my CPU usage at 100%. I think it’s probably a good idea to set up the data that won’t change per frame using this approach.