2014-01-03 13:32:13 -05:00
|
|
|
Vector = require './vector'
|
2014-07-16 18:57:53 -04:00
|
|
|
LineSegment = require './line_segment'
|
2014-01-03 13:32:13 -05:00
|
|
|
|
|
|
|
class Rectangle
|
2014-06-30 22:16:26 -04:00
|
|
|
@className: 'Rectangle'
|
2014-07-16 18:57:53 -04:00
|
|
|
# Class methods for nondestructively operating - TODO: add rotate
|
2014-01-03 13:32:13 -05:00
|
|
|
for name in ['add', 'subtract', 'multiply', 'divide']
|
|
|
|
do (name) ->
|
|
|
|
Rectangle[name] = (a, b) ->
|
|
|
|
a.copy()[name](b)
|
|
|
|
|
2014-07-16 18:57:53 -04:00
|
|
|
isRectangle: true
|
|
|
|
apiProperties: ['x', 'y', 'width', 'height', 'rotation', 'getPos', 'vertices', 'touchesRect', 'touchesPoint', 'distanceToPoint', 'distanceSquaredToPoint', 'distanceToRectangle', 'distanceSquaredToRectangle', 'distanceToEllipse', 'distanceSquaredToEllipse', 'distanceToShape', 'distanceSquaredToShape', 'containsPoint', 'copy', 'intersectsLineSegment', 'intersectsEllipse', 'intersectsRectangle', 'intersectsShape']
|
2014-01-28 18:24:08 -05:00
|
|
|
|
2014-01-03 13:32:13 -05:00
|
|
|
constructor: (@x=0, @y=0, @width=0, @height=0, @rotation=0) ->
|
|
|
|
|
|
|
|
copy: ->
|
|
|
|
new Rectangle(@x, @y, @width, @height, @rotation)
|
|
|
|
|
|
|
|
getPos: ->
|
|
|
|
new Vector(@x, @y)
|
|
|
|
|
|
|
|
vertices: ->
|
|
|
|
# Counter-clockwise, starting from bottom left (when unrotated)
|
|
|
|
[w2, h2, cos, sin] = [@width / 2, @height / 2, Math.cos(@rotation), Math.sin(-@rotation)]
|
|
|
|
[
|
|
|
|
new Vector @x - (w2 * cos - h2 * sin), @y - (w2 * sin + h2 * cos)
|
|
|
|
new Vector @x - (w2 * cos + h2 * sin), @y - (w2 * sin - h2 * cos)
|
|
|
|
new Vector @x + (w2 * cos - h2 * sin), @y + (w2 * sin + h2 * cos)
|
|
|
|
new Vector @x + (w2 * cos + h2 * sin), @y + (w2 * sin - h2 * cos)
|
|
|
|
]
|
|
|
|
|
2014-07-16 18:57:53 -04:00
|
|
|
lineSegments: ->
|
|
|
|
vertices = @vertices()
|
|
|
|
lineSegment0 = new LineSegment vertices[0], vertices[1]
|
|
|
|
lineSegment1 = new LineSegment vertices[1], vertices[2]
|
|
|
|
lineSegment2 = new LineSegment vertices[2], vertices[3]
|
|
|
|
lineSegment3 = new LineSegment vertices[3], vertices[0]
|
|
|
|
[lineSegment0, lineSegment1, lineSegment2, lineSegment3]
|
|
|
|
|
2014-01-03 13:32:13 -05:00
|
|
|
touchesRect: (other) ->
|
|
|
|
# Whether this rect shares part of any edge with other rect, for non-rotated, non-overlapping rectangles.
|
|
|
|
# I think it says kitty-corner rects touch, but I don't think I want that.
|
|
|
|
# Float instability might get me, too.
|
|
|
|
[bl1, tl1, tr1, br1] = @vertices()
|
|
|
|
[bl2, tl2, tr2, br2] = other.vertices()
|
|
|
|
return false if tl1.x > tr2.x or tl2.x > tr1.x
|
|
|
|
return false if bl1.y > tl2.y or bl2.y > tl1.y
|
|
|
|
return true if tl1.x is tr2.x or tl2.x is tr1.x
|
|
|
|
return true if tl1.y is bl2.y or tl2.y is bl1.y
|
|
|
|
false
|
|
|
|
|
|
|
|
touchesPoint: (p) ->
|
|
|
|
# Whether this rect has point p exactly on one of its edges, assuming no rotation.
|
|
|
|
[bl, tl, tr, br] = @vertices()
|
|
|
|
return false unless p.y >= bl.y and p.y <= tl.y
|
|
|
|
return false unless p.x >= bl.x and p.x <= br.x
|
|
|
|
return true if p.x is bl.x or p.x is br.x
|
|
|
|
return true if p.y is bl.y or p.y is tl.y
|
|
|
|
false
|
|
|
|
|
|
|
|
axisAlignedBoundingBox: (rounded=true) ->
|
|
|
|
box = @copy()
|
|
|
|
return box unless @rotation
|
|
|
|
box.rotation = 0
|
|
|
|
[left, top] = [9001, 9001]
|
|
|
|
for vertex in @vertices()
|
|
|
|
[left, top] = [Math.min(left, vertex.x), Math.min(top, vertex.y)]
|
|
|
|
if rounded
|
|
|
|
[left, top] = [Math.round(left), Math.round(top)]
|
|
|
|
[box.width, box.height] = [2 * (@x - left), 2 * (@y - top)]
|
|
|
|
box
|
|
|
|
|
|
|
|
distanceToPoint: (p) ->
|
2014-07-16 18:57:53 -04:00
|
|
|
# Get p in rect's coordinate space, then operate in one quadrant.
|
2014-01-03 13:32:13 -05:00
|
|
|
p = Vector.subtract(p, @getPos()).rotate(-@rotation)
|
|
|
|
dx = Math.max(Math.abs(p.x) - @width / 2, 0)
|
|
|
|
dy = Math.max(Math.abs(p.y) - @height / 2, 0)
|
|
|
|
Math.sqrt dx * dx + dy * dy
|
|
|
|
|
|
|
|
distanceSquaredToPoint: (p) ->
|
2014-07-16 18:57:53 -04:00
|
|
|
# Doesn't handle rotation; just supposed to be faster than distanceToPoint.
|
2014-01-20 18:27:03 -05:00
|
|
|
p = Vector.subtract(p, @getPos())
|
2014-01-03 13:32:13 -05:00
|
|
|
dx = Math.max(Math.abs(p.x) - @width / 2, 0)
|
|
|
|
dy = Math.max(Math.abs(p.y) - @height / 2, 0)
|
|
|
|
dx * dx + dy * dy
|
|
|
|
|
2014-07-16 18:57:53 -04:00
|
|
|
distanceToRectangle: (other) ->
|
|
|
|
Math.sqrt @distanceSquaredToRectangle other
|
|
|
|
|
|
|
|
distanceSquaredToRectangle: (other) ->
|
|
|
|
return 0 if @intersectsRectangle other
|
|
|
|
[firstVertices, secondVertices] = [@vertices(), other.vertices()]
|
|
|
|
[firstEdges, secondEdges] = [@lineSegments(), other.lineSegments()]
|
|
|
|
ans = Infinity
|
|
|
|
for i in [0 ... 4]
|
|
|
|
for j in [0 ... firstEdges.length]
|
|
|
|
ans = Math.min ans, firstEdges[j].distanceSquaredToPoint(secondVertices[i])
|
|
|
|
for j in [0 ... secondEdges.length]
|
|
|
|
ans = Math.min ans, secondEdges[j].distanceSquaredToPoint(firstVertices[i])
|
|
|
|
ans
|
|
|
|
|
|
|
|
distanceToEllipse: (ellipse) ->
|
|
|
|
Math.sqrt @distanceSquaredToEllipse ellipse
|
|
|
|
|
|
|
|
distanceSquaredToEllipse: (ellipse) ->
|
|
|
|
@distanceSquaredToRectangle ellipse.rectangle() # TODO: actually implement rectangle-ellipse distance
|
|
|
|
|
|
|
|
distanceToShape: (shape) ->
|
|
|
|
Math.sqrt @distanceSquaredToShape shape
|
|
|
|
|
|
|
|
distanceSquaredToShape: (shape) ->
|
|
|
|
if shape.isEllipse then @distanceSquaredToEllipse shape else @distanceSquaredToRectangle shape
|
|
|
|
|
2014-01-03 13:32:13 -05:00
|
|
|
containsPoint: (p, withRotation=true) ->
|
|
|
|
if withRotation and @rotation
|
|
|
|
not @distanceToPoint(p)
|
|
|
|
else
|
|
|
|
@x - @width / 2 < p.x < @x + @width / 2 and @y - @height / 2 < p.y < @y + @height / 2
|
|
|
|
|
2014-07-16 18:57:53 -04:00
|
|
|
intersectsLineSegment: (p1, p2) ->
|
|
|
|
[px1, py1, px2, py2] = [p1.x, p1.y, p2.x, p2.y]
|
|
|
|
m1 = (py1 - py2) / (px1 - px2)
|
|
|
|
b1 = py1 - (m1 * px1)
|
|
|
|
vertices = @vertices()
|
|
|
|
lineSegments = [[vertices[0], vertices[1]], [vertices[1], vertices[2]], [vertices[2], vertices[3]], [vertices[3], vertices[0]]]
|
|
|
|
for lineSegment in lineSegments
|
|
|
|
[px1, py1, px2, py2] = [p1.x, p1.y, p2.x, p2.y]
|
|
|
|
m2 = (py1 - py2) / (px1 - px2)
|
|
|
|
b2 = py1 - (m * px1)
|
|
|
|
if m1 isnt m2
|
|
|
|
m = m1 - m2
|
|
|
|
b = b2 - b1
|
|
|
|
x = b / m
|
|
|
|
[littleX, bigX] = if px1 < px2 then [px1, px2] else [px2, px1]
|
|
|
|
if x >= littleX and x <= bigX
|
|
|
|
y = (m1 * x) + b1
|
|
|
|
[littleY, bigY] = if py1 < py2 then [py1, py2] else [py2, py1]
|
|
|
|
if littleY <= solution and bigY >= solution
|
|
|
|
return true
|
|
|
|
false
|
|
|
|
|
|
|
|
intersectsRectangle: (rectangle) ->
|
|
|
|
return true if @containsPoint rectangle.getPos()
|
|
|
|
for thisLineSegment in @lineSegments()
|
|
|
|
for thatLineSegment in rectangle.lineSegments()
|
|
|
|
if thisLineSegment.intersectsLineSegment(thatLineSegment)
|
|
|
|
return true
|
|
|
|
false
|
|
|
|
|
|
|
|
intersectsEllipse: (ellipse) ->
|
|
|
|
return true if @containsPoint ellipse.getPos()
|
|
|
|
return true for lineSegment in @lineSegments() when ellipse.intersectsLineSegment lineSegment.a, lineSegment.b
|
|
|
|
false
|
|
|
|
|
|
|
|
intersectsShape: (shape) ->
|
|
|
|
if shape.isEllipse then @intersectsEllipse shape else @intersectsRectangle shape
|
|
|
|
|
2014-01-03 13:32:13 -05:00
|
|
|
subtract: (point) ->
|
|
|
|
@x -= point.x
|
|
|
|
@y -= point.y
|
|
|
|
@pos.subtract point
|
|
|
|
@
|
|
|
|
|
|
|
|
add: (point) ->
|
|
|
|
@x += point.x
|
|
|
|
@y += point.y
|
|
|
|
@pos.add point
|
|
|
|
@
|
|
|
|
|
|
|
|
divide: (n) ->
|
|
|
|
[@width, @height] = [@width / n, @height / n]
|
|
|
|
@
|
|
|
|
|
|
|
|
multiply: (n) ->
|
|
|
|
[@width, @height] = [@width * n, @height * n]
|
|
|
|
@
|
|
|
|
|
|
|
|
isEmpty: () ->
|
2014-07-16 18:57:53 -04:00
|
|
|
@width is 0 and @height is 0
|
2014-01-03 13:32:13 -05:00
|
|
|
|
|
|
|
invalid: () ->
|
2014-07-16 18:57:53 -04:00
|
|
|
return (@x is Infinity) || isNaN(@x) || @y is Infinity || isNaN(@y) || @width is Infinity || isNaN(@width) || @height is Infinity || isNaN(@height) || @rotation is Infinity || isNaN(@rotation)
|
2014-01-03 13:32:13 -05:00
|
|
|
|
|
|
|
toString: ->
|
|
|
|
return "{x: #{@x.toFixed(0)}, y: #{@y.toFixed(0)}, w: #{@width.toFixed(0)}, h: #{@height.toFixed(0)}, rot: #{@rotation.toFixed(3)}}"
|
|
|
|
|
|
|
|
serialize: ->
|
|
|
|
{CN: @constructor.className, x: @x, y: @y, w: @width, h: @height, r: @rotation}
|
|
|
|
|
|
|
|
@deserialize: (o, world, classMap) ->
|
|
|
|
new Rectangle o.x, o.y, o.w, o.h, o.r
|
|
|
|
|
2014-01-24 16:03:04 -05:00
|
|
|
serializeForAether: -> @serialize()
|
|
|
|
@deserializeFromAether: (o) -> @deserialize o
|
|
|
|
|
2014-01-03 13:32:13 -05:00
|
|
|
module.exports = Rectangle
|