Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:

steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5

- name: Update Wurst
run: grill install wurstscript
Expand All @@ -21,4 +21,4 @@ jobs:
run: grill install

- name: Test
run: grill test
run: grill test --quiet
200 changes: 200 additions & 0 deletions wurst/math/Polygon.wurst
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
package Polygon
import NoWurst
import ArrayList
import Vectors
import Rect
import ErrorHandling
import Maths
import Lightning
import ClosureTimers
import Colors

public enum PolygonPointRelation
OUTSIDE
BOUNDARY
INSIDE

/**
An immutable closed 2D path after seal(). Polygon owns its copied vertex
storage and cached bounds rect. Call destroy when finished: it releases both,
and invalidates every rect previously borrowed from bounds().
*/
public class Polygon
private ArrayList<vec2> vertices
private rect boundaryRect = null
private bool sealed = false

/** Builder form. expectedVertexCount is a capacity hint. Call seal() before querying. */
construct(int expectedVertexCount)
vertices = new ArrayList<vec2>(expectedVertexCount)

/** Convenience form. Copies the supplied vertices and seals immediately. */
construct(vararg vec2 initialVertices)
vertices = new ArrayList<vec2>()
for vertex in initialVertices
vertices.add(vertex)
seal()

/** Appends a copied vertex before seal(). Adding after seal() is a programmer error. */
function addVertex(vec2 vertex) returns thistype
if sealed
error("Polygon: cannot add vertices after seal")
return this
vertices.add(vertex)
return this

/**
Finalizes immutable query state and creates Polygon's owned bounds rect.
Repeated calls preserve the same rect handle. Polygon removes that rect
when it is destroyed; callers must not remove its borrowed bounds().
*/
function seal() returns thistype
if sealed
return this
if vertices.size() == 0
boundaryRect = Rect(0, 0, 0, 0)
else
let firstVertex = vertices.get(0)
var minX = firstVertex.x
var minY = firstVertex.y
var maxX = minX
var maxY = minY
for i = 1 to vertices.size() - 1
let vertex = vertices.get(i)
minX = min(minX, vertex.x)
minY = min(minY, vertex.y)
maxX = max(maxX, vertex.x)
maxY = max(maxY, vertex.y)
boundaryRect = Rect(minX, minY, maxX, maxY)
sealed = true
return this

private function requireSealed() returns bool
if not sealed
error("Polygon: must be sealed before querying")
return false
return true

function isSealed() returns bool
return sealed

function vertexCount() returns int
if not requireSealed()
return 0
return vertices.size()

function vertexAt(int index) returns vec2
if not requireSealed()
return ZERO2
if index < 0 or index >= vertices.size()
error("Polygon: vertex index out of bounds")
return ZERO2
return vertices.get(index)

/**
Returns Polygon's borrowed cached rect. Polygon retains ownership: callers
must not remove or destroy it, and must not use it after Polygon is
destroyed.
*/
function bounds() returns rect
if not requireSealed()
return null
return boundaryRect

function boundsCenter() returns vec2
if not requireSealed()
return ZERO2
return boundaryRect.getCenter()

private function isOnSegment(vec2 point, vec2 start, vec2 finish, real cross) returns bool
if cross != 0
return false
return (point.x - start.x) * (point.x - finish.x) <= 0 and
(point.y - start.y) * (point.y - finish.y) <= 0

function classify(vec2 point) returns PolygonPointRelation
if not requireSealed()
return PolygonPointRelation.OUTSIDE
if vertices.size() == 0
return PolygonPointRelation.OUTSIDE
if point.x < boundaryRect.getMinX() or point.x > boundaryRect.getMaxX() or
point.y < boundaryRect.getMinY() or point.y > boundaryRect.getMaxY()
return PolygonPointRelation.OUTSIDE
var inside = false
var previous = vertices.get(vertices.size() - 1)
for i = 0 to vertices.size() - 1
let current = vertices.get(i)
let deltaY = current.y - previous.y
let cross = (point.x - previous.x) * deltaY -
(point.y - previous.y) * (current.x - previous.x)
if isOnSegment(point, previous, current, cross)
return PolygonPointRelation.BOUNDARY
if (previous.y > point.y) != (current.y > point.y)
if (deltaY > 0 and cross < 0) or (deltaY < 0 and cross > 0)
inside = not inside
previous = current
return inside ? PolygonPointRelation.INSIDE : PolygonPointRelation.OUTSIDE

function contains(vec2 point) returns bool
return classify(point) != PolygonPointRelation.OUTSIDE

function containsStrict(vec2 point) returns bool
return classify(point) == PolygonPointRelation.INSIDE

/**
Draws every edge in standard green. Ownership transfers to the caller:
destroy every returned lightning handle, then destroy the returned list.
*/
function debugRender() returns ArrayList<lightning>
return this.debugRender(PLAYER_COLOR_GREEN.toColor())

/**
Draws every edge in the requested color. Ownership transfers to the caller:
destroy every returned lightning handle, then destroy the returned list.
*/
function debugRender(color col) returns ArrayList<lightning>
let result = new ArrayList<lightning>(max(1, vertices.size()))
if not requireSealed()
return result
if vertices.size() < 2
return result
var previous = vertices.get(vertices.size() - 1)
for i = 0 to vertices.size() - 1
let current = vertices.get(i)
let border = addLightning(LIGHTNING_MAGIC_LEASH, false, previous, current)
border.setColor(col)
result.add(border)
previous = current
return result

/**
Draws in standard green. Polygon retains ownership of all created lightning
handles and its internal list, and destroys both after duration. duration
must be positive.
*/
function debugRenderTimed(real duration)
this.debugRenderTimed(duration, PLAYER_COLOR_GREEN.toColor())

/**
Draws in the requested color. Polygon retains ownership of all created
lightning handles and its internal list, and destroys both after duration.
duration must be positive.
*/
function debugRenderTimed(real duration, color col)
if duration <= 0
error("Polygon: timed debug duration must be positive")
return
if not requireSealed()
return
let borders = this.debugRender(col)
doAfter(duration) ->
for i = 0 to borders.size() - 1
borders.get(i).destr()
destroy borders

ondestroy
if boundaryRect != null
boundaryRect.remove()
boundaryRect = null
destroy vertices
vertices = null
Loading