diff --git a/app/assets/javascripts/workers/worker_world.js b/app/assets/javascripts/workers/worker_world.js index 04d63ea5d..dd1142eff 100644 --- a/app/assets/javascripts/workers/worker_world.js +++ b/app/assets/javascripts/workers/worker_world.js @@ -96,7 +96,9 @@ Aether.addGlobal('_', _); var serializedClasses = { "Thang": self.require('lib/world/thang'), "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.currentDebugWorldFrame = 0; diff --git a/app/lib/surface/Mark.coffee b/app/lib/surface/Mark.coffee index 07f671e78..bce9d84be 100644 --- a/app/lib/surface/Mark.coffee +++ b/app/lib/surface/Mark.coffee @@ -181,12 +181,11 @@ module.exports = class Mark extends CocoClass buildDebug: -> @mark = new createjs.Shape() 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.graphics.beginFill '#abcdef' if @sprite.thang.shape in ['ellipsoid', 'disc'] - [w, h] = [Math.max(PX, w, h), Math.max(PX, w, h)] - @mark.graphics.drawCircle 0, 0, w / 2 + @mark.graphics.drawEllipse -w / 2, -h / 2, w, h else @mark.graphics.drawRect -w / 2, -h / 2, w, h @mark.graphics.endFill() @@ -259,7 +258,7 @@ module.exports = class Mark extends CocoClass updateRotation: -> 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: -> if @name is 'bounds' and ((@sprite.thang.width isnt @lastWidth or @sprite.thang.height isnt @lastHeight) or (@sprite.thang.drawsBoundsIndex isnt @drawsBoundsIndex)) diff --git a/app/lib/world/ellipse.coffee b/app/lib/world/ellipse.coffee new file mode 100644 index 000000000..6aaf88057 --- /dev/null +++ b/app/lib/world/ellipse.coffee @@ -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 diff --git a/app/lib/world/line_segment.coffee b/app/lib/world/line_segment.coffee new file mode 100644 index 000000000..cf6de236a --- /dev/null +++ b/app/lib/world/line_segment.coffee @@ -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 diff --git a/app/lib/world/rectangle.coffee b/app/lib/world/rectangle.coffee index e3fec8d76..dadfaea25 100644 --- a/app/lib/world/rectangle.coffee +++ b/app/lib/world/rectangle.coffee @@ -1,14 +1,16 @@ Vector = require './vector' +LineSegment = require './line_segment' class Rectangle @className: 'Rectangle' - # Class methods for nondestructively operating + # Class methods for nondestructively operating - TODO: add rotate for name in ['add', 'subtract', 'multiply', 'divide'] do (name) -> Rectangle[name] = (a, 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) -> @@ -28,6 +30,14 @@ class Rectangle 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) -> # 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. @@ -62,25 +72,90 @@ class Rectangle box 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) 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) -> - # 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()) dx = Math.max(Math.abs(p.x) - @width / 2, 0) dy = Math.max(Math.abs(p.y) - @height / 2, 0) 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) -> 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 + 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) -> @x -= point.x @y -= point.y @@ -102,10 +177,10 @@ class Rectangle @ isEmpty: () -> - @width == 0 and @height == 0 + @width is 0 and @height is 0 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: -> return "{x: #{@x.toFixed(0)}, y: #{@y.toFixed(0)}, w: #{@width.toFixed(0)}, h: #{@height.toFixed(0)}, rot: #{@rotation.toFixed(3)}}" diff --git a/app/lib/world/world.coffee b/app/lib/world/world.coffee index eb496133b..f94a6d26a 100644 --- a/app/lib/world/world.coffee +++ b/app/lib/world/world.coffee @@ -1,5 +1,7 @@ Vector = require './vector' Rectangle = require './rectangle' +Ellipse = require './ellipse' +LineSegment = require './line_segment' WorldFrame = require './world_frame' Thang = require './thang' ThangState = require './thang_state' @@ -21,7 +23,7 @@ module.exports = class World apiProperties: ['age', 'dt'] constructor: (@userCodeMap, classMap) -> # 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() @userCodeMap ?= {} diff --git a/app/lib/world/world_utils.coffee b/app/lib/world/world_utils.coffee index d6d5c1171..1398c409b 100644 --- a/app/lib/world/world_utils.coffee +++ b/app/lib/world/world_utils.coffee @@ -1,5 +1,7 @@ Vector = require './vector' Rectangle = require './rectangle' +Ellipse = require './ellipse' +LineSegment = require './line_segment' Grid = require './Grid' 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? 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() if skipThangs and obj.isThang diff --git a/app/views/play/level/tome/spell_debug_view.coffee b/app/views/play/level/tome/spell_debug_view.coffee index e69fadc98..54fceb2f5 100644 --- a/app/views/play/level/tome/spell_debug_view.coffee +++ b/app/views/play/level/tome/spell_debug_view.coffee @@ -6,6 +6,8 @@ serializedClasses = Thang: require 'lib/world/thang' Vector: require 'lib/world/vector' Rectangle: require 'lib/world/rectangle' + Ellipse: require 'lib/world/ellipse' + LineSegment: require 'lib/world/line_segment' module.exports = class DebugView extends View className: 'spell-debug-view' diff --git a/test/app/lib/surface/camera.spec.coffee b/test/app/lib/surface/camera.spec.coffee index f875a1135..9383ce89a 100644 --- a/test/app/lib/surface/camera.spec.coffee +++ b/test/app/lib/surface/camera.spec.coffee @@ -14,7 +14,7 @@ describe 'Camera (Surface point of view)', -> sup = cam.worldToSurface wop 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 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] 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 size in testCanvasSizes for zoom in testZooms for target in testZoomTargets for angle in testAngles 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 cam.zoomTo target, zoom, 0 checkConversionsFromWorldPos wop, cam checkCameraPos cam, wop 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.x2z).toBeGreaterThan 9001 expect(cam.z2y).toBeCloseTo 0 it 'works at 0 degrees', -> - cam = new Camera {attr: (x) -> 100}, 100 * Camera.MPP, 100 * Camera.MPP - expect(cam.x2z).toBeGreaterThan 9001 - expect(cam.x2y).toBeCloseTo 1 - expect(cam.z2y).toBeCloseTo 0 + cam = new Camera {attr: (attr) -> 100}, 0 + expect(cam.x2y).toBeGreaterThan 9001 + expect(cam.x2z).toBeCloseTo 1 + expect(cam.z2y).toBeGreaterThan 9001 it 'works at 45 degrees', -> - cam = new Camera {attr: (x) -> 100}, 100 * Camera.MPP, 100 * Camera.MPP - expect(cam.x2y).toBeCloseTo 1 - expect(cam.x2z).toBeGreaterThan 9001 - expect(cam.z2y).toBeCloseTo 0 + cam = new Camera {attr: (attr) -> 100}, Math.PI / 4 + expect(cam.x2y).toBeCloseTo Math.sqrt(2) + expect(cam.x2z).toBeCloseTo Math.sqrt(2) + expect(cam.z2y).toBeCloseTo 1 - xit 'works at default angle of asin(0.75) ~= 48.9 degrees', -> - cam = new Camera {attr: (x) -> 100}, 100 * Camera.MPP, 100 * Camera.MPP - angle = 1 / Math.cos angle + it 'works at default angle of asin(0.75) ~= 48.9 degrees', -> + cam = new Camera {attr: (attr) -> 100}, null + angle = Math.asin(3 / 4) expect(cam.angle).toBeCloseTo angle - expect(cam.x2y).toBeCloseTo 1 - expect(cam.x2z).toBeGreaterThan 9001 - expect(cam.z2y).toBeCloseTo 0 + expect(cam.x2y).toBeCloseTo 4 / 3 + expect(cam.x2z).toBeCloseTo 1 / Math.cos angle + expect(cam.z2y).toBeCloseTo (4 / 3) * Math.cos angle 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 wop = x: 5, y: 2.5, z: 7 cap = cam.worldToCanvas wop @@ -143,7 +144,8 @@ describe 'Camera (Surface point of view)', -> expectPositionsEqual cap, {x: 0, y: 50} 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.x2z).toBeGreaterThan 9001 checkCameraPos cam @@ -164,15 +166,18 @@ describe 'Camera (Surface point of view)', -> expectPositionsEqual cap, {x: 50, y: -100} 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 - xit 'works at 2x zoom, 60 degree hFOV, 40 degree hFOV', -> - cam = new Camera {attr: (x) -> x is 'height' ? 63.041494 : 100}, 100 * Camera.MPP, 63.041494 * Camera.MPP + it 'works at 2x zoom, 60 degree hFOV, 40 degree vFOV', -> + 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 - xit 'works 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 + xit 'works at 2x zoom on a surface wider than it is tall, 30 degrees, default viewing upper left corner', -> + cam = new Camera {attr: (attr) -> 100}, Math.PI / 6 # 200 * Camera.MPP, 2 * 50 * Camera.MPP + cam.zoomTo null, 2, 0 checkCameraPos cam 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 diff --git a/test/app/lib/world/ellipse.spec.coffee b/test/app/lib/world/ellipse.spec.coffee new file mode 100644 index 000000000..bb71fdad5 --- /dev/null +++ b/test/app/lib/world/ellipse.spec.coffee @@ -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 diff --git a/test/app/lib/world/line_segment.spec.coffee b/test/app/lib/world/line_segment.spec.coffee new file mode 100644 index 000000000..d3b6ca939 --- /dev/null +++ b/test/app/lib/world/line_segment.spec.coffee @@ -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 diff --git a/test/app/lib/world/rectangle.spec.coffee b/test/app/lib/world/rectangle.spec.coffee index 76b13e0d3..937e388c5 100644 --- a/test/app/lib/world/rectangle.spec.coffee +++ b/test/app/lib/world/rectangle.spec.coffee @@ -1,6 +1,7 @@ describe 'Rectangle', -> Rectangle = require 'lib/world/rectangle' Vector = require 'lib/world/vector' + Ellipse = require 'lib/world/ellipse' it 'contains its own center', -> rect = new Rectangle 0, 0, 10, 10 @@ -43,6 +44,13 @@ describe 'Rectangle', -> rect.rotation = 0.00000001 * Math.PI 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', -> rect = new Rectangle 50, 50, 100, 100 v = rect.vertices() @@ -79,3 +87,22 @@ describe 'Rectangle', -> aabb = rect.axisAlignedBoundingBox() for prop in ['x', 'y', 'width', 'height'] 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