Merged in geometry work from #51.

This commit is contained in:
Nick Winter 2014-07-16 15:57:53 -07:00
parent 855461a67e
commit 42af807e5c
12 changed files with 556 additions and 38 deletions

View file

@ -96,7 +96,9 @@ Aether.addGlobal('_', _);
var serializedClasses = { var serializedClasses = {
"Thang": self.require('lib/world/thang'), "Thang": self.require('lib/world/thang'),
"Vector": self.require('lib/world/vector'), "Vector": self.require('lib/world/vector'),
"Rectangle": self.require('lib/world/rectangle') "Rectangle": self.require('lib/world/rectangle'),
"Ellipse": self.require('lib/world/ellipse'),
"LineSegment": self.require('lib/world/line_segment')
}; };
self.currentUserCodeMapCopy = ""; self.currentUserCodeMapCopy = "";
self.currentDebugWorldFrame = 0; self.currentDebugWorldFrame = 0;

View file

@ -181,12 +181,11 @@ module.exports = class Mark extends CocoClass
buildDebug: -> buildDebug: ->
@mark = new createjs.Shape() @mark = new createjs.Shape()
PX = 3 PX = 3
[w, h] = [Math.max(PX, @sprite.thang.width * Camera.PPM), Math.max(PX, @sprite.thang.height * Camera.PPM) * @camera.y2x] [w, h] = [Math.max(PX, @sprite.thang.width * Camera.PPM), Math.max(PX, @sprite.thang.height * Camera.PPM) * @camera.y2x] # TODO: doesn't work with rotation
@mark.alpha = 0.5 @mark.alpha = 0.5
@mark.graphics.beginFill '#abcdef' @mark.graphics.beginFill '#abcdef'
if @sprite.thang.shape in ['ellipsoid', 'disc'] if @sprite.thang.shape in ['ellipsoid', 'disc']
[w, h] = [Math.max(PX, w, h), Math.max(PX, w, h)] @mark.graphics.drawEllipse -w / 2, -h / 2, w, h
@mark.graphics.drawCircle 0, 0, w / 2
else else
@mark.graphics.drawRect -w / 2, -h / 2, w, h @mark.graphics.drawRect -w / 2, -h / 2, w, h
@mark.graphics.endFill() @mark.graphics.endFill()
@ -259,7 +258,7 @@ module.exports = class Mark extends CocoClass
updateRotation: -> updateRotation: ->
if @name is 'debug' or (@name is 'shadow' and @sprite.thang?.shape in ['rectangle', 'box']) if @name is 'debug' or (@name is 'shadow' and @sprite.thang?.shape in ['rectangle', 'box'])
@mark.rotation = @sprite.thang.rotation * 180 / Math.PI @mark.rotation = -@sprite.thang.rotation * 180 / Math.PI
updateScale: -> updateScale: ->
if @name is 'bounds' and ((@sprite.thang.width isnt @lastWidth or @sprite.thang.height isnt @lastHeight) or (@sprite.thang.drawsBoundsIndex isnt @drawsBoundsIndex)) if @name is 'bounds' and ((@sprite.thang.width isnt @lastWidth or @sprite.thang.height isnt @lastHeight) or (@sprite.thang.drawsBoundsIndex isnt @drawsBoundsIndex))

View file

@ -0,0 +1,174 @@
Vector = require './vector'
LineSegment = require './line_segment'
Rectangle = require './rectangle'
class Ellipse
@className: "Ellipse"
# TODO: add class methods for add, multiply, subtract, divide, rotate
isEllipse: true
apiProperties: ['x', 'y', 'width', 'height', 'rotation', 'distanceToPoint', 'distanceSquaredToPoint', 'distanceToRectangle', 'distanceSquaredToRectangle', 'distanceToEllipse', 'distanceSquaredToEllipse', 'distanceToShape', 'distanceSquaredToShape', 'containsPoint', 'intersectsLineSegment', 'intersectsRectangle', 'intersectsEllipse', 'getPos', 'containsPoint', 'copy']
constructor: (@x=0, @y=0, @width=0, @height=0, @rotation=0) ->
copy: ->
new Ellipse(@x, @y, @width, @height, @rotation)
getPos: ->
new Vector(@x, @y)
rectangle: ->
new Rectangle(@x, @y, @width, @height, @rotation)
axisAlignedBoundingBox: (rounded=true) ->
@rectangle().axisAlignedBoundingBox()
distanceToPoint: (p) ->
@rectangle().distanceToPoint p # TODO: actually implement ellipse ellipse-point distance
distanceSquaredToPoint: (p) ->
# Doesn't handle rotation; just supposed to be faster than distanceToPoint.
@rectangle().distanceSquaredToPoint p # TODO: actually implement ellipse-point distance
distanceToRectangle: (other) ->
Math.sqrt @distanceSquaredToRectangle other
distanceSquaredToRectangle: (other) ->
@rectangle().distanceSquaredToRectangle other # TODO: actually implement ellipse-rectangle distance
distanceToEllipse: (ellipse) ->
Math.sqrt @distanceSquaredToEllipse ellipse
distanceSquaredToEllipse: (ellipse) ->
@rectangle().distanceSquaredToEllipse ellipse # TODO: actually implement ellipse-ellipse distance
distanceToShape: (shape) ->
Math.sqrt @distanceSquaredToShape shape
distanceSquaredToShape: (shape) ->
if shape.isEllipse then @distanceSquaredToEllipse shape else @distanceSquaredToRectangle shape
containsPoint: (p, withRotation=true) ->
[a, b] = [@width / 2, @height / 2]
[h, k] = [@x, @y]
[x, y] = [p.x, p.y]
x2 = Math.pow(x, 2)
a2 = Math.pow(a, 2)
a4 = Math.pow(a, 4)
b2 = Math.pow(b, 2)
b4 = Math.pow(b, 4)
h2 = Math.pow(h, 2)
k2 = Math.pow(k, 2)
if withRotation and @rotation
sint = Math.sin(@rotation)
sin2t = Math.sin(2 * @rotation)
cost = Math.cos(@rotation)
cos2t = Math.cos(2 * @rotation)
numeratorLeft = (-a2 * h * sin2t) + (a2 * k * cos2t) + (a2 * k) + (a2 * x * sin2t)
numeratorMiddle = Math.SQRT2 * Math.sqrt((a4 * b2 * cos2t) + (a4 * b2) - (a2 * b4 * cos2t) + (a2 * b4) - (2 * a2 * b2 * h2) + (4 * a2 * b2 * h * x) - (2 * a2 * b2 * x2))
numeratorRight = (b2 * h * sin2t) - (b2 * k * cos2t) + (b2 * k) - (b2 * x * sin2t)
denominator = (a2 * cos2t) + a2 - (b2 * cos2t) + b2
solution1 = (numeratorLeft - numeratorMiddle + numeratorRight) / denominator
solution2 = (numeratorLeft + numeratorMiddle + numeratorRight) / denominator
if (not isNaN solution1) and (not isNaN solution2)
[bigSolution, littleSolution] = if solution1 > solution2 then [solution1, solution2] else [solution2, solution1]
if y > littleSolution and y < bigSolution
return true
else
return false
else
return false
else
numeratorLeft = a2 * k
numeratorRight = Math.sqrt((a4 * b2) - (a2 * b2 * h2) + (2 * a2 * b2 * h * x) - (a2 * b2 * x2))
denominator = a2
solution1 = (numeratorLeft + numeratorRight) / denominator
solution2 = (numeratorLeft - numeratorRight) / denominator
if (not isNaN solution1) and (not isNaN solution2)
[bigSolution, littleSolution] = if solution1 > solution2 then [solution1, solution2] else [solution2, solution1]
if y > littleSolution and y < bigSolution
return true
else
return false
else
return false
false
intersectsLineSegment: (p1, p2) ->
[px1, py1, px2, py2] = [p1.x, p1.y, p2.x, p2.y]
m = (py1 - py2) / (px1 - px2)
m2 = Math.pow(m, 2)
c = py1 - (m * px1)
c2 = Math.pow(c, 2)
[a, b] = [@width / 2, @height / 2]
[h, k] = [@x, @y]
a2 = Math.pow(a, 2)
a4 = Math.pow(a, 2)
b2 = Math.pow(b, 2)
b4 = Math.pow(b, 4)
h2 = Math.pow(h, 2)
k2 = Math.pow(k, 2)
sint = Math.sin(@rotation)
sin2t = Math.sin(2 * @rotation)
cost = Math.cos(@rotation)
cos2t = Math.cos(2 * @rotation)
if (not isNaN m) and m != Infinity and m != -Infinity
numeratorLeft = (-a2 * c * m * cos2t) - (a2 * c * m) + (a2 * c * sin2t) - (a2 * h * m * sin2t) - (a2 * h * cos2t) + (a2 * h) + (a2 * k * m * cos2t) + (a2 * k * m) - (a2 * k * sin2t)
numeratorMiddle = Math.SQRT2 * Math.sqrt((a4 * b2 * m2 * cos2t) + (a4 * b2 * m2) - (2 * a4 * b2 * m * sin2t) - (a4 * b2 * cos2t) + (a4 * b2) - (a2 * b4 * m2 * cos2t) + (a2 * b4 * m2) + (2 * a2 * b4 * m * sin2t) + (a2 * b4 * cos2t) + (a2 * b4) - (2 * a2 * b2 * c2) - (4 * a2 * b2 * c * h * m) + (4 * a2 * b2 * c * k) - (2 * a2 * b2 * h2 * m2) + (4 * a2 * b2 * h * k * m) - (2 * a2 * b2 * k2))
numeratorRight = (b2 * c * m * cos2t) - (b2 * c * m) - (b2 * c * sin2t) + (b2 * h * m * sin2t) + (b2 * h * cos2t) + (b2 * h) - (b2 * k * m * cos2t) + (b2 * k * m) + (b2 * k * sin2t)
denominator = (a2 * m2 * cos2t) + (a2 * m2) - (2 * a2 * m * sin2t) - (a2 * cos2t) + a2 - (b2 * m2 * cos2t) + (b2 * m2) + (2 * b2 * m * sin2t) + (b2 * cos2t) + b2
solution1 = (-numeratorLeft - numeratorMiddle + numeratorRight) / denominator
solution2 = (-numeratorLeft + numeratorMiddle + numeratorRight) / denominator
if (not isNaN solution1) and (not isNaN solution2)
[littleX, bigX] = if px1 < px2 then [px1, px2] else [px2, px1]
if (littleX <= solution1 and bigX >= solution1) or (littleX <= solution2 and bigX >= solution2)
return true
if (not isNaN solution1) or (not isNaN solution2)
solution = if not isNaN solution1 then solution1 else solution2
[littleX, bigX] = if px1 < px2 then [px1, px2] else [px2, px1]
if littleX <= solution and bigX >= solution
return true
else
return false
else
x = px1
x2 = Math.pow(x, 2)
numeratorLeft = (-a2 * h * sin2t) + (a2 * k * cos2t) + (a2 * k) + (a2 * x * sin2t)
numeratorMiddle = Math.SQRT2 * Math.sqrt((a4 * b2 * cos2t) + (a4 * b2) - (a2 * b4 * cos2t) + (a2 * b4) - (2 * a2 * b2 * h2) + (4 * a2 * b2 * h * x) - (2 * a2 * b2 * x2))
numeratorRight = (b2 * h * sin2t) - (b2 * k * cos2t) + (b2 * k) - (b2 * x * sin2t)
denominator = (a2 * cos2t) + a2 - (b2 * cos2t) + b2
solution1 = (numeratorLeft - numeratorMiddle + numeratorRight) / denominator
solution2 = (numeratorLeft + numeratorMiddle + numeratorRight) / denominator
if (not isNaN solution1) or (not isNaN solution2)
solution = if not isNaN solution1 then solution1 else solution2
[littleY, bigY] = if py1 < py2 then [py1, py2] else [py2, py1]
if littleY <= solution and bigY >= solution
return true
else
return false
false
intersectsRectangle: (rectangle) ->
rectangle.intersectsEllipse @
intersectsEllipse: (ellipse) ->
@rectangle().intersectsEllipse @ # TODO: actually implement ellipse-ellipse intersection
#return true if @containsPoint ellipse.getPos()
intersectsShape: (shape) ->
if shape.isEllipse then @intersectsEllipse shape else @intersectsRectangle shape
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 Ellipse o.x, o.y, o.w, o.h, o.r
serializeForAether: -> @serialize()
@deserializeFromAether: (o) -> @deserialize o
module.exports = Ellipse

View file

@ -0,0 +1,80 @@
class LineSegment
@className: "LineSegment"
constructor: (@a, @b) ->
@slope = (@a.y - @b.y) / (@a.x - @b.x)
@y0 = @a.y - (@slope * @a.x)
@left = if @a.x < @b.x then @a else @b
@right = if @a.x > @b.x then @a else @b
@bottom = if @a.y < @b.y then @a else @b
@top = if @a.y > @b.y then @a else @b
y: (x) ->
(@slope * x) + @y0
x: (y) ->
(y - @y0) / @slope
intersectsLineSegment: (lineSegment) ->
if lineSegment.slope is @slope
if lineSegment.y0 is @y0
if lineSegment.left.x is @left.x or lineSegment.left.x is @right.x or lineSegment.right.x is @right.x or lineSegment.right.x is @left.x
# segments are of the same line with shared start and/or end points
return true
else
[left, right] = if lineSegment.left.x < @left.x then [lineSegment, @] else [@, lineSegment]
if left.right.x > right.left.x
# segments are of the same line and one is contained within the other
return true
else if Math.abs(@slope) isnt Infinity and Math.abs(lineSegment.slope) isnt Infinity
x = (lineSegment.y0 - @y0) / (@slope - lineSegment.slope)
if x >= @left.x and x <= @right.x and x >= lineSegment.left.x and x <= lineSegment.right.x
return true
else if Math.abs(@slope) isnt Infinity or Math.abs(lineSegment.slope) isnt Infinity
[vertical, nonvertical] = if Math.abs(@slope) isnt Infinity then [lineSegment, @] else [@, lineSegment]
x = vertical.a.x
bottom = vertical.bottom.y
top = vertical.top.y
y = nonvertical.y(x)
left = nonvertical.left.x
right = nonvertical.right.x
if y >= bottom and y <= top and x >= left and x <= right
return true
false
pointOnLine: (point, segment=true) ->
if point.y is @y(point.x)
if segment
[littleY, bigY] = if @a.y < @b.y then [@a.y, @b.y] else [@b.y, @a.y]
if littleY <= point.y and bigY >= point.y
return true
else
return true
false
distanceSquaredToPoint: (point) ->
# http://stackoverflow.com/a/1501725/540620
return @a.distanceSquared point if @a.equals @b
res = Math.min point.distanceSquared(@a), point.distanceSquared(@b)
lineMagnitudeSquared = @a.distanceSquared @b
t = ((point.x - @a.x) * (@b.x - @a.x) + (point.y - @a.y) * (@b.y - @a.y)) / lineMagnitudeSquared
return @a.distanceSquared point if t < 0
return @b.distanceSquared point if t > 1
point.distanceSquared x: @a.x + t * (@b.x - @a.x), y: @a.y + t * (@b.y - @a.y)
distanceToPoint: (point) ->
Math.sqrt @distanceSquaredToPoint point
toString: ->
"lineSegment(a=#{@a}, b=#{@b}, slope=#{@slope}, y0=#{@y0}, left=#{@left}, right=#{@right}, bottom=#{@bottom}, top=#{@top})"
serialize: ->
{CN: @constructor.className, a: @a, b: @b}
@deserialize: (o, world, classMap) ->
new LineSegment o.a, o.b
serializeForAether: -> @serialize()
@deserializeFromAether: (o) -> @deserialize o
module.exports = LineSegment

View file

@ -1,14 +1,16 @@
Vector = require './vector' Vector = require './vector'
LineSegment = require './line_segment'
class Rectangle class Rectangle
@className: 'Rectangle' @className: 'Rectangle'
# Class methods for nondestructively operating # Class methods for nondestructively operating - TODO: add rotate
for name in ['add', 'subtract', 'multiply', 'divide'] for name in ['add', 'subtract', 'multiply', 'divide']
do (name) -> do (name) ->
Rectangle[name] = (a, b) -> Rectangle[name] = (a, b) ->
a.copy()[name](b) a.copy()[name](b)
apiProperties: ['x', 'y', 'width', 'height', 'rotation', 'getPos', 'vertices', 'touchesRect', 'touchesPoint', 'distanceToPoint', 'containsPoint', 'copy'] 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']
constructor: (@x=0, @y=0, @width=0, @height=0, @rotation=0) -> constructor: (@x=0, @y=0, @width=0, @height=0, @rotation=0) ->
@ -28,6 +30,14 @@ class Rectangle
new Vector @x + (w2 * cos + h2 * sin), @y + (w2 * sin - h2 * cos) new Vector @x + (w2 * cos + h2 * sin), @y + (w2 * sin - h2 * cos)
] ]
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]
touchesRect: (other) -> touchesRect: (other) ->
# Whether this rect shares part of any edge with other rect, for non-rotated, non-overlapping rectangles. # 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. # I think it says kitty-corner rects touch, but I don't think I want that.
@ -62,25 +72,90 @@ class Rectangle
box box
distanceToPoint: (p) -> distanceToPoint: (p) ->
# Get p in rect's coordinate space, then operate in one quadrant # Get p in rect's coordinate space, then operate in one quadrant.
p = Vector.subtract(p, @getPos()).rotate(-@rotation) p = Vector.subtract(p, @getPos()).rotate(-@rotation)
dx = Math.max(Math.abs(p.x) - @width / 2, 0) dx = Math.max(Math.abs(p.x) - @width / 2, 0)
dy = Math.max(Math.abs(p.y) - @height / 2, 0) dy = Math.max(Math.abs(p.y) - @height / 2, 0)
Math.sqrt dx * dx + dy * dy Math.sqrt dx * dx + dy * dy
distanceSquaredToPoint: (p) -> distanceSquaredToPoint: (p) ->
# Doesn't handle rotation; just supposed to be faster than distanceToPoint # Doesn't handle rotation; just supposed to be faster than distanceToPoint.
p = Vector.subtract(p, @getPos()) p = Vector.subtract(p, @getPos())
dx = Math.max(Math.abs(p.x) - @width / 2, 0) dx = Math.max(Math.abs(p.x) - @width / 2, 0)
dy = Math.max(Math.abs(p.y) - @height / 2, 0) dy = Math.max(Math.abs(p.y) - @height / 2, 0)
dx * dx + dy * dy dx * dx + dy * dy
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
containsPoint: (p, withRotation=true) -> containsPoint: (p, withRotation=true) ->
if withRotation and @rotation if withRotation and @rotation
not @distanceToPoint(p) not @distanceToPoint(p)
else else
@x - @width / 2 < p.x < @x + @width / 2 and @y - @height / 2 < p.y < @y + @height / 2 @x - @width / 2 < p.x < @x + @width / 2 and @y - @height / 2 < p.y < @y + @height / 2
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
subtract: (point) -> subtract: (point) ->
@x -= point.x @x -= point.x
@y -= point.y @y -= point.y
@ -102,10 +177,10 @@ class Rectangle
@ @
isEmpty: () -> isEmpty: () ->
@width == 0 and @height == 0 @width is 0 and @height is 0
invalid: () -> invalid: () ->
return (@x == Infinity) || isNaN(@x) || @y == Infinity || isNaN(@y) || @width == Infinity || isNaN(@width) || @height == Infinity || isNaN(@height) || @rotation == Infinity || isNaN(@rotation) 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)
toString: -> toString: ->
return "{x: #{@x.toFixed(0)}, y: #{@y.toFixed(0)}, w: #{@width.toFixed(0)}, h: #{@height.toFixed(0)}, rot: #{@rotation.toFixed(3)}}" return "{x: #{@x.toFixed(0)}, y: #{@y.toFixed(0)}, w: #{@width.toFixed(0)}, h: #{@height.toFixed(0)}, rot: #{@rotation.toFixed(3)}}"

View file

@ -1,5 +1,7 @@
Vector = require './vector' Vector = require './vector'
Rectangle = require './rectangle' Rectangle = require './rectangle'
Ellipse = require './ellipse'
LineSegment = require './line_segment'
WorldFrame = require './world_frame' WorldFrame = require './world_frame'
Thang = require './thang' Thang = require './thang'
ThangState = require './thang_state' ThangState = require './thang_state'
@ -21,7 +23,7 @@ module.exports = class World
apiProperties: ['age', 'dt'] apiProperties: ['age', 'dt']
constructor: (@userCodeMap, classMap) -> constructor: (@userCodeMap, classMap) ->
# classMap is needed for deserializing Worlds, Thangs, and other classes # classMap is needed for deserializing Worlds, Thangs, and other classes
@classMap = classMap ? {Vector: Vector, Rectangle: Rectangle, Thang: Thang} @classMap = classMap ? {Vector: Vector, Rectangle: Rectangle, Thang: Thang, Ellipse: Ellipse, LineSegment: LineSegment}
Thang.resetThangIDs() Thang.resetThangIDs()
@userCodeMap ?= {} @userCodeMap ?= {}

View file

@ -1,5 +1,7 @@
Vector = require './vector' Vector = require './vector'
Rectangle = require './rectangle' Rectangle = require './rectangle'
Ellipse = require './ellipse'
LineSegment = require './line_segment'
Grid = require './Grid' Grid = require './Grid'
module.exports.typedArraySupport = typedArraySupport = Float32Array? # Not in IE until IE 10; we'll fall back to normal arrays module.exports.typedArraySupport = typedArraySupport = Float32Array? # Not in IE until IE 10; we'll fall back to normal arrays
@ -36,7 +38,7 @@ module.exports.clone = clone = (obj, skipThangs=false) ->
flags += 'y' if obj.sticky? flags += 'y' if obj.sticky?
return new RegExp(obj.source, flags) return new RegExp(obj.source, flags)
if (obj instanceof Vector) or (obj instanceof Rectangle) if (obj instanceof Vector) or (obj instanceof Rectangle) or (obj instanceof Ellipse) or (obj instanceof LineSegment)
return obj.copy() return obj.copy()
if skipThangs and obj.isThang if skipThangs and obj.isThang

View file

@ -6,6 +6,8 @@ serializedClasses =
Thang: require 'lib/world/thang' Thang: require 'lib/world/thang'
Vector: require 'lib/world/vector' Vector: require 'lib/world/vector'
Rectangle: require 'lib/world/rectangle' Rectangle: require 'lib/world/rectangle'
Ellipse: require 'lib/world/ellipse'
LineSegment: require 'lib/world/line_segment'
module.exports = class DebugView extends View module.exports = class DebugView extends View
className: 'spell-debug-view' className: 'spell-debug-view'

View file

@ -14,7 +14,7 @@ describe 'Camera (Surface point of view)', ->
sup = cam.worldToSurface wop sup = cam.worldToSurface wop
expect(sup.x).toBeCloseTo wop.x * Camera.PPM expect(sup.x).toBeCloseTo wop.x * Camera.PPM
expect(sup.y).toBeCloseTo cam.surfaceHeight - (wop.y + wop.z * cam.z2y) * cam.y2x * Camera.PPM expect(sup.y).toBeCloseTo -(wop.y + wop.z * cam.z2y) * cam.y2x * Camera.PPM
cap = cam.worldToCanvas wop cap = cam.worldToCanvas wop
expect(cap.x).toBeCloseTo (sup.x - cam.surfaceViewport.x) * cam.zoom expect(cap.x).toBeCloseTo (sup.x - cam.surfaceViewport.x) * cam.zoom
@ -83,47 +83,48 @@ describe 'Camera (Surface point of view)', ->
testAngles = [0, Math.PI / 4, null, Math.PI / 2] testAngles = [0, Math.PI / 4, null, Math.PI / 2]
testFOVs = [Math.PI / 6, Math.PI / 3, Math.PI / 2, Math.PI] testFOVs = [Math.PI / 6, Math.PI / 3, Math.PI / 2, Math.PI]
xit 'handles lots of different cases correctly', -> it 'handles lots of different cases correctly', ->
for wop in testWops for wop in testWops
for size in testCanvasSizes for size in testCanvasSizes
for zoom in testZooms for zoom in testZooms
for target in testZoomTargets for target in testZoomTargets
for angle in testAngles for angle in testAngles
for fov in testFOVs for fov in testFOVs
cam = new Camera size.width, size.height, size.width * Camera.MPP, size.height * Camera.MPP, testLayer, zoom, null, angle, fov cam = new Camera {attr: (attr) -> if 'attr' is 'width' then size.width else size.height}, angle, fov
checkCameraPos cam, wop checkCameraPos cam, wop
cam.zoomTo target, zoom, 0 cam.zoomTo target, zoom, 0
checkConversionsFromWorldPos wop, cam checkConversionsFromWorldPos wop, cam
checkCameraPos cam, wop checkCameraPos cam, wop
it 'works at 90 degrees', -> it 'works at 90 degrees', ->
cam = new Camera {attr: (x) -> 100}, 100 * Camera.MPP, 100 * Camera.MPP cam = new Camera {attr: (attr) -> 100}, Math.PI / 2
expect(cam.x2y).toBeCloseTo 1 expect(cam.x2y).toBeCloseTo 1
expect(cam.x2z).toBeGreaterThan 9001 expect(cam.x2z).toBeGreaterThan 9001
expect(cam.z2y).toBeCloseTo 0 expect(cam.z2y).toBeCloseTo 0
it 'works at 0 degrees', -> it 'works at 0 degrees', ->
cam = new Camera {attr: (x) -> 100}, 100 * Camera.MPP, 100 * Camera.MPP cam = new Camera {attr: (attr) -> 100}, 0
expect(cam.x2z).toBeGreaterThan 9001 expect(cam.x2y).toBeGreaterThan 9001
expect(cam.x2y).toBeCloseTo 1 expect(cam.x2z).toBeCloseTo 1
expect(cam.z2y).toBeCloseTo 0 expect(cam.z2y).toBeGreaterThan 9001
it 'works at 45 degrees', -> it 'works at 45 degrees', ->
cam = new Camera {attr: (x) -> 100}, 100 * Camera.MPP, 100 * Camera.MPP cam = new Camera {attr: (attr) -> 100}, Math.PI / 4
expect(cam.x2y).toBeCloseTo 1 expect(cam.x2y).toBeCloseTo Math.sqrt(2)
expect(cam.x2z).toBeGreaterThan 9001 expect(cam.x2z).toBeCloseTo Math.sqrt(2)
expect(cam.z2y).toBeCloseTo 0 expect(cam.z2y).toBeCloseTo 1
xit 'works at default angle of asin(0.75) ~= 48.9 degrees', -> it 'works at default angle of asin(0.75) ~= 48.9 degrees', ->
cam = new Camera {attr: (x) -> 100}, 100 * Camera.MPP, 100 * Camera.MPP cam = new Camera {attr: (attr) -> 100}, null
angle = 1 / Math.cos angle angle = Math.asin(3 / 4)
expect(cam.angle).toBeCloseTo angle expect(cam.angle).toBeCloseTo angle
expect(cam.x2y).toBeCloseTo 1 expect(cam.x2y).toBeCloseTo 4 / 3
expect(cam.x2z).toBeGreaterThan 9001 expect(cam.x2z).toBeCloseTo 1 / Math.cos angle
expect(cam.z2y).toBeCloseTo 0 expect(cam.z2y).toBeCloseTo (4 / 3) * Math.cos angle
xit 'works at 2x zoom, 90 degrees', -> xit 'works at 2x zoom, 90 degrees', ->
cam = new Camera {attr: (x) -> 100}, 100 * Camera.MPP, 100 * Camera.MPP cam = new Camera {attr: (attr) -> 100}, Math.PI / 2
cam.zoomTo null, 2, 0
checkCameraPos cam checkCameraPos cam
wop = x: 5, y: 2.5, z: 7 wop = x: 5, y: 2.5, z: 7
cap = cam.worldToCanvas wop cap = cam.worldToCanvas wop
@ -143,7 +144,8 @@ describe 'Camera (Surface point of view)', ->
expectPositionsEqual cap, {x: 0, y: 50} expectPositionsEqual cap, {x: 0, y: 50}
xit 'works at 2x zoom, 30 degrees', -> xit 'works at 2x zoom, 30 degrees', ->
cam = new Camera {attr: (x) -> 100}, 100 * Camera.MPP, 2 * 100 * Camera.MPP cam = new Camera {attr: (attr) -> 100}, Math.PI / 6
cam.zoomTo null, 2, 0
expect(cam.x2y).toBeCloseTo 1 expect(cam.x2y).toBeCloseTo 1
expect(cam.x2z).toBeGreaterThan 9001 expect(cam.x2z).toBeGreaterThan 9001
checkCameraPos cam checkCameraPos cam
@ -164,15 +166,18 @@ describe 'Camera (Surface point of view)', ->
expectPositionsEqual cap, {x: 50, y: -100} expectPositionsEqual cap, {x: 50, y: -100}
it 'works at 2x zoom, 60 degree hFOV', -> it 'works at 2x zoom, 60 degree hFOV', ->
cam = new Camera {attr: (x) -> 100}, 100 * Camera.MPP, 100 * Camera.MPP cam = new Camera {attr: (attr) -> 100}, null, Math.PI / 3
cam.zoomTo null, 2, 0
checkCameraPos cam checkCameraPos cam
xit 'works at 2x zoom, 60 degree hFOV, 40 degree hFOV', -> it 'works at 2x zoom, 60 degree hFOV, 40 degree vFOV', ->
cam = new Camera {attr: (x) -> x is 'height' ? 63.041494 : 100}, 100 * Camera.MPP, 63.041494 * Camera.MPP cam = new Camera {attr: (attr) -> if attr is 'height' then 63.041494 else 100}, null, Math.PI / 3
cam.zoomTo null, 2, 0
checkCameraPos cam checkCameraPos cam
xit 'works on a surface wider than it is tall, 30 degrees, default viewing upper left corner', -> xit 'works at 2x zoom on a surface wider than it is tall, 30 degrees, default viewing upper left corner', ->
cam = new Camera {attr: (x) -> 100}, 200 * Camera.MPP, 2 * 50 * Camera.MPP cam = new Camera {attr: (attr) -> 100}, Math.PI / 6 # 200 * Camera.MPP, 2 * 50 * Camera.MPP
cam.zoomTo null, 2, 0
checkCameraPos cam checkCameraPos cam
expect(cam.zoom).toBeCloseTo 2 expect(cam.zoom).toBeCloseTo 2
wop = x: 5, y: 4, z: 6 * cam.y2z # like x: 5, y: 10 out of world width: 20, height: 10 wop = x: 5, y: 4, z: 6 * cam.y2z # like x: 5, y: 10 out of world width: 20, height: 10

View file

@ -0,0 +1,99 @@
describe 'Ellipse', ->
Ellipse = require 'lib/world/ellipse'
Rectangle = require 'lib/world/rectangle'
Vector = require 'lib/world/vector'
#it 'contains its own center', ->
# ellipse = new Ellipse 0, 0, 10, 10
# expect(ellipse.containsPoint(new Vector 0, 0)).toBe true
#
#it 'contains a point when rotated', ->
# ellipse = new Ellipse 0, -20, 40, 40, 3 * Math.PI / 4
# p = new Vector 0, 2
# expect(ellipse.containsPoint(p, true)).toBe true
#
#it 'contains more points properly', ->
# # ellipse with y major axis, off-origin center, and 45 degree rotation
# ellipse = new Ellipse 1, 2, 4, 6, Math.PI / 4
# expect(ellipse.contains new Vector(1, 2)).toBe true
# expect(ellipse.contains new Vector(-1, 3)).toBe true
# expect(ellipse.contains new Vector(0, 4)).toBe true
# expect(ellipse.contains new Vector(1, 4)).toBe true
# expect(ellipse.contains new Vector(3, 0)).toBe true
# expect(ellipse.contains new Vector(1, 0)).toBe true
# expect(ellipse.contains new Vector(0, 1)).toBe true
# expect(ellipse.contains new Vector(-1, 2)).toBe true
# expect(ellipse.contains new Vector(2, 2)).toBe true
# expect(ellipse.contains new Vector(0, 0)).toBe false
# expect(ellipse.contains new Vector(0, 5)).toBe false
# expect(ellipse.contains new Vector(3, 4)).toBe false
# expect(ellipse.contains new Vector(4, 0)).toBe false
# expect(ellipse.contains new Vector(2, -1)).toBe false
# expect(ellipse.contains new Vector(0, -3)).toBe false
# expect(ellipse.contains new Vector(-2, -2)).toBe false
# expect(ellipse.contains new Vector(-2, 0)).toBe false
# expect(ellipse.contains new Vector(-2, 4)).toBe false
#
#it 'correctly calculates distance to a faraway point', ->
# ellipse = new Ellipse 100, 50, 20, 40
# p = new Vector 200, 300
# d = 10 * Math.sqrt(610)
# expect(ellipse.distanceToPoint(p)).toBeCloseTo d
# ellipse.rotation = Math.PI / 2
# d = 80 * Math.sqrt(10)
# expect(ellipse.distanceToPoint(p)).toBeCloseTo d
#
#it 'does not modify itself or target Vector when calculating distance', ->
# ellipse = new Ellipse -100, -200, 1, 100
# ellipse2 = ellipse.copy()
# p = new Vector -100.25, -101
# p2 = p.copy()
# ellipse.distanceToPoint(p)
# expect(p.x).toEqual p2.x
# expect(p.y).toEqual p2.y
# expect(ellipse.x).toEqual ellipse2.x
# expect(ellipse.y).toEqual ellipse2.y
# expect(ellipse.width).toEqual ellipse2.width
# expect(ellipse.height).toEqual ellipse2.height
# expect(ellipse.rotation).toEqual ellipse2.rotation
#
#it 'correctly calculates distance to contained point', ->
# ellipse = new Ellipse -100, -200, 1, 100
# ellipse2 = ellipse.copy()
# p = new Vector -100.25, -160
# p2 = p.copy()
# expect(ellipse.distanceToPoint(p)).toBe 0
# ellipse.rotation = 0.00000001 * Math.PI
# expect(ellipse.distanceToPoint(p)).toBe 0
#
#it 'AABB works when not rotated', ->
# ellipse = new Ellipse 10, 20, 30, 40
# rect = new Rectangle 10, 20, 30, 40
# aabb1 = ellipse.axisAlignedBoundingBox()
# aabb2 = ellipse.axisAlignedBoundingBox()
# for prop in ['x', 'y', 'width', 'height']
# expect(aabb1[prop]).toBe aabb2[prop]
#
#it 'AABB works when rotated', ->
# ellipse = new Ellipse 10, 20, 30, 40, Math.PI / 3
# rect = new Rectangle 10, 20, 30, 40, Math.PI / 3
# aabb1 = ellipse.axisAlignedBoundingBox()
# aabb2 = ellipse.axisAlignedBoundingBox()
# for prop in ['x', 'y', 'width', 'height']
# expect(aabb1[prop]).toBe aabb2[prop]
#
#it 'calculates ellipse intersections properly', ->
# # ellipse with y major axis, off-origin center, and 45 degree rotation
# ellipse = new Ellipse 1, 2, 4, 6, Math.PI / 4
# expect(ellipse.intersectsShape new Rectangle(0, 0, 2, 2, 0)).toBe true
# expect(ellipse.intersectsShape new Rectangle(0, -1, 2, 3, 0)).toBe true
# expect(ellipse.intersectsShape new Rectangle(-1, -0.5, 2 * Math.SQRT2, 2 * Math.SQRT2, Math.PI / 4)).toBe true
# expect(ellipse.intersectsShape new Rectangle(-1, -0.5, 2 * Math.SQRT2, 2 * Math.SQRT2, 0)).toBe true
# expect(ellipse.intersectsShape new Rectangle(-1, -1, 2 * Math.SQRT2, 2 * Math.SQRT2, 0)).toBe true
# expect(ellipse.intersectsShape new Rectangle(-1, -1, 2 * Math.SQRT2, 2 * Math.SQRT2, Math.PI / 4)).toBe false
# expect(ellipse.intersectsShape new Rectangle(-2, -2, 2, 2, 0)).toBe false
# expect(ellipse.intersectsShape new Rectangle(-Math.SQRT2 / 2, -Math.SQRT2 / 2, Math.SQRT2, Math.SQRT2, 0)).toBe false
# expect(ellipse.intersectsShape new Rectangle(-Math.SQRT2 / 2, -Math.SQRT2 / 2, Math.SQRT2, Math.SQRT2, Math.PI / 4)).toBe false
# expect(ellipse.intersectsShape new Rectangle(-2, 0, 2, 2, 0)).toBe false
# expect(ellipse.intersectsShape new Rectangle(0, -2, 2, 2, 0)).toBe false
# expect(ellipse.intersectsShape new Rectangle(1, 2, 1, 1, 0)).toBe true

View file

@ -0,0 +1,51 @@
describe 'LineSegment', ->
LineSegment = require 'lib/world/line_segment'
Vector = require 'lib/world/vector'
v00 = new Vector(0, 0)
v11 = new Vector(1, 1)
v22 = new Vector(2, 2)
v34 = new Vector(3, 4)
v04 = new Vector(0, 4)
v30 = new Vector(3, 0)
vneg = new Vector(-1, -1)
it 'intersects itself', ->
lineSegment = new LineSegment v00, v34
expect(lineSegment.intersectsLineSegment lineSegment).toBe true
it 'intersects other segments properly', ->
l1 = new LineSegment v00, v34
l2 = new LineSegment v04, v30
l3 = new LineSegment v00, v11
expect(l1.intersectsLineSegment l2).toBe true
expect(l2.intersectsLineSegment l1).toBe true
expect(l1.intersectsLineSegment l3).toBe true
expect(l3.intersectsLineSegment l1).toBe true
expect(l2.intersectsLineSegment l3).toBe false
expect(l3.intersectsLineSegment l2).toBe false
it 'can tell when a point is on a line or segment', ->
lineSegment = new LineSegment v00, v11
expect(lineSegment.pointOnLine v22, false).toBe true
#expect(lineSegment.pointOnLine v22, true).toBe false
#expect(lineSegment.pointOnLine v00, false).toBe true
#expect(lineSegment.pointOnLine v00, true).toBe true
#expect(lineSegment.pointOnLine v11, true).toBe true
#expect(lineSegment.pointOnLine v11, false).toBe true
#expect(lineSegment.pointOnLine v34, false).toBe false
#expect(lineSegment.pointOnLine v34, true).toBe false
it 'correctly calculates distance to points', ->
lineSegment = new LineSegment v00, v11
expect(lineSegment.distanceToPoint v00).toBe 0
expect(lineSegment.distanceToPoint v11).toBe 0
expect(lineSegment.distanceToPoint v22).toBeCloseTo Math.SQRT2
expect(lineSegment.distanceToPoint v34).toBeCloseTo Math.sqrt(2 * 2 + 3 * 3)
expect(lineSegment.distanceToPoint v04).toBeCloseTo Math.sqrt(1 * 1 + 3 * 3)
expect(lineSegment.distanceToPoint v30).toBeCloseTo Math.sqrt(2 * 2 + 1 * 1)
expect(lineSegment.distanceToPoint vneg).toBeCloseTo Math.SQRT2
nullSegment = new LineSegment v11, v11
expect(lineSegment.distanceToPoint v11).toBe 0
expect(lineSegment.distanceToPoint v22).toBeCloseTo Math.SQRT2

View file

@ -1,6 +1,7 @@
describe 'Rectangle', -> describe 'Rectangle', ->
Rectangle = require 'lib/world/rectangle' Rectangle = require 'lib/world/rectangle'
Vector = require 'lib/world/vector' Vector = require 'lib/world/vector'
Ellipse = require 'lib/world/ellipse'
it 'contains its own center', -> it 'contains its own center', ->
rect = new Rectangle 0, 0, 10, 10 rect = new Rectangle 0, 0, 10, 10
@ -43,6 +44,13 @@ describe 'Rectangle', ->
rect.rotation = 0.00000001 * Math.PI rect.rotation = 0.00000001 * Math.PI
expect(rect.distanceToPoint(p)).toBe 0 expect(rect.distanceToPoint(p)).toBe 0
it 'correctly calculates distance to other rectangles', ->
expect(new Rectangle(0, 0, 4, 4, Math.PI / 4).distanceToRectangle(new Rectangle(4, -4, 2, 2, 0))).toBeCloseTo 2.2426
expect(new Rectangle(0, 0, 3, 3, 0).distanceToRectangle(new Rectangle(0, 0, 2, 2, 0))).toBe 0
expect(new Rectangle(0, 0, 3, 3, 0).distanceToRectangle(new Rectangle(0, 0, 2.5, 2.5, Math.PI / 4))).toBe 0
expect(new Rectangle(0, 0, 4, 4, 0).distanceToRectangle(new Rectangle(4, 2, 2, 2, 0))).toBe 1
expect(new Rectangle(0, 0, 4, 4, 0).distanceToRectangle(new Rectangle(4, 2, 2, 2, Math.PI / 4))).toBeCloseTo 2 - Math.SQRT2
it 'has predictable vertices', -> it 'has predictable vertices', ->
rect = new Rectangle 50, 50, 100, 100 rect = new Rectangle 50, 50, 100, 100
v = rect.vertices() v = rect.vertices()
@ -79,3 +87,22 @@ describe 'Rectangle', ->
aabb = rect.axisAlignedBoundingBox() aabb = rect.axisAlignedBoundingBox()
for prop in ['x', 'y', 'width', 'height'] for prop in ['x', 'y', 'width', 'height']
expect(rect[prop]).toBe aabb[prop] expect(rect[prop]).toBe aabb[prop]
it 'calculates rectangle intersections properly', ->
rect = new Rectangle 1, 1, 2, 2, 0
expect(rect.intersectsShape new Rectangle(3, 1, 2, 2, 0)).toBe true
expect(rect.intersectsShape new Rectangle(3, 3, 2, 2, 0)).toBe true
expect(rect.intersectsShape new Rectangle(1, 1, 2, 2, 0)).toBe true
expect(rect.intersectsShape new Rectangle(1, 1, Math.SQRT1_2, Math.SQRT1_2, Math.PI / 4)).toBe true
expect(rect.intersectsShape new Rectangle(4, 1, 2, 2, 0)).toBe false
expect(rect.intersectsShape new Rectangle(3, 4, 2, 2, 0)).toBe false
expect(rect.intersectsShape new Rectangle(1, 4, 2 * Math.SQRT1_2, 2 * Math.SQRT1_2, Math.PI / 4)).toBe false
expect(rect.intersectsShape new Rectangle(3, 1, 2, 2, Math.PI / 4)).toBe true
expect(rect.intersectsShape new Rectangle(1, 2, 2 * Math.SQRT2, 2 * Math.SQRT2, Math.PI / 4)).toBe true
it 'calculates ellipse intersections properly', ->
rect = new Rectangle 1, 1, 2, 2, 0
expect(rect.intersectsShape new Ellipse(1, 1, Math.SQRT1_2, Math.SQRT1_2, Math.PI / 4)).toBe true
expect(rect.intersectsShape new Ellipse(4, 1, 2, 2, 0)).toBe false
expect(rect.intersectsShape new Ellipse(3, 4, 2, 2, 0)).toBe false
expect(rect.intersectsShape new Ellipse(1, 4, 2 * Math.SQRT1_2, 2 * Math.SQRT1_2, Math.PI / 4)).toBe false