/* Module "polygon.wren" */
import "graphics" for Canvas, Color
import "math" for Point, Vector
/* Selectable is an abstract class representing the interface needed for an identifiable
object to be selectable by mouse click or otherwise */
class Selectable {
tag { "" }
tag=(t) {}
selected { false }
selected=(s) {}
contains(x, y) { false }
}
/* Polygon represents a polygon in 2 dimensional space. */
class Polygon is Selectable {
/* Private static helper methods for 'overlaps' instance method. */
static getAxes_(poly) {
var axes = List.filled(poly.sides, null)
var vertices = poly.vertices
for (i in 0...poly.sides) {
var vertex1 = vertices[i]
var vertex2 = vertices[(i+1 == poly.sides) ? 0 : i+1]
var vector1 = Vector.new(vertex1[0], vertex1[1])
var vector2 = Vector.new(vertex2[0], vertex2[1])
var edge = vector1 - vector2
axes[i] = edge.perp
}
return axes
}
static projectOntoAxis_(poly, axis) {
var vertices = poly.vertices
var vertex0 = vertices[0]
var vector0 = Vector.new(vertex0[0], vertex0[1])
var min = axis.dot(vector0)
var max = min
for (i in 1...poly.sides) {
var vertex = vertices[i]
var vector = Vector.new(vertex[0], vertex[1])
var p = axis.dot(vector)
if (p < min) {
min = p
} else if (p > max) {
max = p
}
}
return [min, max]
}
static projectionsOverlap_(proj1, proj2) {
if (proj1[1] < proj2[0]) return false
if (proj2[1] < proj1[0]) return false
return true
}
// Constructs a new Polygon object from its vertices and tag.
construct new(vertices, tag) {
if (vertices.count < 3) Fiber.abort("Number of vertices cannot be less than 3.")
if (vertices[0].type != List || vertices[0].count != 2 || vertices[0][0].type != Num) {
Fiber.abort("Each vertex must be a pair of numbers.")
}
_v = vertices.toList
_tag = tag
_selected = false
}
// Constructs a new Polygon object from its vertices and tag without checking or copying them.
construct quick(vertices, tag) {
_v = vertices
_tag = tag
_selected = false
}
// Constructs a new regular Polygon object with n sides, centered at (cx, cy), radius r from the
// center to each vertex and start angle sa in degrees measured clockwise from the horizonal line
// drawn to the polygon's rightmost point.
static regular(n, cx, cy, r, sa, tag) {
var vertices = List.filled(n, null)
var inc = 360 / n
var angle = sa
for (i in 0...n) {
var a = angle * Num.pi / 180
var x = a.cos * r + cx
var y = a.sin * r + cy
vertices[i] = [x, y]
angle = angle + inc
}
return Polygon.new(vertices, tag)
}
// Convenience versions of the above constructors for objects with an empty tag.
static new(vertices) { new(vertices, "") }
static quick(vertices) { quick(vertices, "") }
static regular(n, cx, cy, r, sa) { regular(n, cx, cy, r, sa, "") }
// Properties
sides { _v.count } // returns the number of sides of the curent instance
vertices { _v.toList } // returns a shallow copy of the current instance's vertices
tag { _tag } // gets the tag of the current instance
tag=(t) { _tag = t } // sets the tag of the current instance
selected { _selected } // gets whether the current instance is selected or not.
selected=(s) { _selected = s } // sets whether the current instance is selected or not.
// Returns what kind of polygon this instance is.
kind {
return (sides < 11) ?
["triangle", "quadrilateral", "pentagon", "hexagon",
"octagon", "nonagon", "decagon"][sides-3] : "%(sides)-sided polygon"
}
// Returns whether the current instance contains a point (x, y) using the 'even-odd' rule.
// Vertices or points on the boundary are considered to be contained by the polygon.
contains(x, y) {
var n = sides
var j = n - 1
var contained = false
for (i in 0...n) {
if (x == _v[i][0] && y == _v[i][1]) return true // vertex
if ((_v[i][1] > y) != (_v[j][1] > y)) {
var slope = (x-_v[i][0])*(_v[j][1]-_v[i][1]) - (_v[j][0]-_v[i][0])*(y-_v[i][1])
if (slope == 0) return true // point on boundary
if ((slope < 0) != (_v[j][1] < _v[i][1])) contained = !contained
}
j = i
}
return contained
}
// Returns whether a point (x, y) is a vertex of the current instance.
hasVertex(x, y) {
for (i in 0...sides) {
if (x == _v[i][0] && y == _v[i][1]) return true
}
return false
}
// Returns whether a point (x, y) is a vertex or on the boundary of the current instance.
hasEdgePoint(x, y) {
var n = sides
var j = n - 1
for (i in 0...n) {
if (x == _v[i][0] && y == _v[i][1]) return true // vertex
if ((_v[i][1] > y) != (_v[j][1] > y)) {
var slope = (x-_v[i][0])*(_v[j][1]-_v[i][1]) - (_v[j][0]-_v[i][0])*(y-_v[i][1])
if (slope == 0) return true // point on boundary
}
j = i
}
return false
}
// Returns whether a point (x, y) is an interior point of the current instance.
// Vertices or points on the boundary are not considered to be interior ponts.
hasInteriorPoint(x, y) {
var n = sides
var j = n - 1
var contained = false
for (i in 0...n) {
if (x == _v[i][0] && y == _v[i][1]) return false // vertex
if ((_v[i][1] > y) != (_v[j][1] > y)) {
var slope = (x-_v[i][0])*(_v[j][1]-_v[i][1]) - (_v[j][0]-_v[i][0])*(y-_v[i][1])
if (slope == 0) return false // point on boundary
if ((slope < 0) != (_v[j][1] < _v[i][1])) contained = !contained
}
j = i
}
return contained
}
// Returns whether this instance overlaps another Polygon object i.e. they have
// at least one point in common.
overlaps(other) {
if (!(other is Polygon)) Fiber.abort("Argument must be a Polygon.")
var axes1 = Polygon.getAxes_(this)
var axes2 = Polygon.getAxes_(other)
for (axes in [axes1, axes2]) {
for (axis in axes) {
var proj1 = Polygon.projectOntoAxis_(this, axis)
var proj2 = Polygon.projectOntoAxis_(other, axis)
if (!Polygon.projectionsOverlap_(proj1, proj2)) return false
}
}
return true
}
// Draws the current instance in a given color and pixel size.
draw(col, size) {
if (col.type != Color) Fiber.abort("First argument must be a color.")
if (size.type != Num || !size.isInteger || size < 1) {
Fiber.abort("Size must be a positive integer.")
}
var j = sides - 1
for (i in 0...j) {
Canvas.line(_v[i][0], _v[i][1], _v[i+1][0], _v[i+1][1], col, size)
}
Canvas.line(_v[j][0], _v[j][1], _v[0][0], _v[0][1], col, size)
}
// Draws the current instance in a given color and pixel size of 1.
draw(col) { draw(col, 1) }
// Draws the current instance filled with a given color.
drawfill(col) {
if (col.type != Color) Fiber.abort("First argument must be a color.")
// get coordinates of bounding rectangle
var vx = _v.map { |v| v[0] }
var vy = _v.map { |v| v[1] }
var xmin = vx.reduce { |acc, x| (x < acc) ? x : acc }
var xmax = vx.reduce { |acc, x| (x > acc) ? x : acc }
var ymin = vy.reduce { |acc, y| (y < acc) ? y : acc }
var ymax = vy.reduce { |acc, y| (y > acc) ? y : acc }
for (x in xmin..xmax) {
for (y in ymin..ymax) {
if (contains(x, y)) Canvas.pset(x, y, col)
}
}
}
// Draws the current instance using a given fill color, border color and border thickness.
drawfill(fillColor, borderColor, borderSize) {
drawfill(fillColor)
draw(borderColor, borderSize)
}
// Draws the current instance using a given fill color, border color and border thickness of 1.
drawfill(fillColor, borderColor) {
drawfill(fillColor)
draw(borderColor)
}
// Returns the string representation of the current instance.
toString { _vertices.toString }
}
/* Rectangle represents a rectangle in 2 dimensional space. */
class Rectangle is Polygon {
// Constructs a new Rectangle object with top left corner (x, y), width w and height h.
construct new (x, y, w, h, tag) {
if (!((x is Num) && (y is Num) && (w is Num) && (h is Num))) {
Fiber.abort("First four arguments must be numbers.")
}
super([[x, y], [x + w, y], [x + w, y + h] , [x, y + h]], tag)
_x = x
_y = y
_w = w
_h = h
}
// Constructs a new Rectangle object with center (cx, cy), width w and height h.
static fromCenter(cx, cy, w, h, tag) { new(cx - w/2, cy - h/2, w, h, tag) }
// Convenience versions of the above constructors for objects with an empty tag.
static new(x, y, w, h) { new(x, y, w, h, "") }
static fromCenter(cx, cy, w, h) { fromCenter(cx, cy, w, h, "") }
// Properties
x { _x }
y { _y }
cx { _x + _w/2 }
cy { _y + _h/2 }
width { _w }
height { _h }
topLeft { Point.new(_x, _y) }
center { Point.new(cx, cy) }
perimeter { 2 * (_w + _h) }
area { _w * _h }
}
/* Square represents a square in 2 dimensional space. */
class Square is Rectangle {
// Constructs a new Square object with top left corner (x, y) and side s.
construct new (x, y, s, tag) {
super(x, y, s, s, tag)
_s = s
}
// Constructs a new Square object with center (cx, cy) and side s.
static fromCenter(cx, cy, s, tag) { new(cx - s/2, cy - s/2, s, tag) }
// Convenience versions of the above constructors for objects with an empty tag.
static new(x, y, s) { new(x, y, s, "") }
static fromCenter(cx, cy, s) { fromCenter(cx, cy, s, "") }
// Properties.
side { _s }
}