source/modules/SAT.js
/**
* Determines if two bodies are colliding using the Separating Axis Theorem
* @private
* @param {Circle|Polygon|Point} a The source body to test
* @param {Circle|Polygon|Point} b The target body to test against
* @param {Result} [result = null] A Result object on which to store information about the collision
* @param {Boolean} [aabb = true] Set to false to skip the AABB test (useful if you use your own collision heuristic)
* @returns {Boolean}
*/
function SAT (a, b, result = null, aabb = true) {
const a_polygon = a._polygon
const b_polygon = b._polygon
let collision = false
if (result) {
result.a = a
result.b = b
result.a_in_b = true
result.b_in_a = true
result.overlap = null
result.overlap_x = 0
result.overlap_y = 0
}
if (a_polygon) {
if (
a._dirty_coords ||
a.x !== a._x ||
a.y !== a._y ||
a.angle !== a._angle ||
a.scale_x !== a._scale_x ||
a.scale_y !== a._scale_y
) {
a._calculateCoords()
}
}
if (b_polygon) {
if (
b._dirty_coords ||
b.x !== b._x ||
b.y !== b._y ||
b.angle !== b._angle ||
b.scale_x !== b._scale_x ||
b.scale_y !== b._scale_y
) {
b._calculateCoords()
}
}
if (!aabb || aabbAABB(a, b)) {
if (a_polygon && a._dirty_normals) {
a._calculateNormals()
}
if (b_polygon && b._dirty_normals) {
b._calculateNormals()
}
collision = (
a_polygon && b_polygon ? polygonPolygon(a, b, result)
: a_polygon ? polygonCircle(a, b, result, false)
: b_polygon ? polygonCircle(b, a, result, true)
: circleCircle(a, b, result)
)
}
if (result) {
result.collision = collision
}
return collision
};
/**
* Determines if two bodies' axis aligned bounding boxes are colliding
* @param {Circle|Polygon|Point} a The source body to test
* @param {Circle|Polygon|Point} b The target body to test against
*/
function aabbAABB (a, b) {
const a_polygon = a._polygon
const a_x = a_polygon ? 0 : a.x
const a_y = a_polygon ? 0 : a.y
const a_radius = a_polygon ? 0 : a.radius * a.scale
const a_min_x = a_polygon ? a._min_x : a_x - a_radius
const a_min_y = a_polygon ? a._min_y : a_y - a_radius
const a_max_x = a_polygon ? a._max_x : a_x + a_radius
const a_max_y = a_polygon ? a._max_y : a_y + a_radius
const b_polygon = b._polygon
const b_x = b_polygon ? 0 : b.x
const b_y = b_polygon ? 0 : b.y
const b_radius = b_polygon ? 0 : b.radius * b.scale
const b_min_x = b_polygon ? b._min_x : b_x - b_radius
const b_min_y = b_polygon ? b._min_y : b_y - b_radius
const b_max_x = b_polygon ? b._max_x : b_x + b_radius
const b_max_y = b_polygon ? b._max_y : b_y + b_radius
return a_min_x < b_max_x && a_min_y < b_max_y && a_max_x > b_min_x && a_max_y > b_min_y
}
/**
* Determines if two polygons are colliding
* @param {Polygon} a The source polygon to test
* @param {Polygon} b The target polygon to test against
* @param {Result} [result = null] A Result object on which to store information about the collision
* @returns {Boolean}
*/
function polygonPolygon (a, b, result = null) {
const a_count = a._coords.length
const b_count = b._coords.length
// Handle points specially
if (a_count === 2 && b_count === 2) {
const a_coords = a._coords
const b_coords = b._coords
if (result) {
result.overlap = 0
}
return a_coords[0] === b_coords[0] && a_coords[1] === b_coords[1]
}
const a_coords = a._coords
const b_coords = b._coords
const a_normals = a._normals
const b_normals = b._normals
if (a_count > 2) {
for (let ix = 0, iy = 1; ix < a_count; ix += 2, iy += 2) {
if (separatingAxis(a_coords, b_coords, a_normals[ix], a_normals[iy], result)) {
return false
}
}
}
if (b_count > 2) {
for (let ix = 0, iy = 1; ix < b_count; ix += 2, iy += 2) {
if (separatingAxis(a_coords, b_coords, b_normals[ix], b_normals[iy], result)) {
return false
}
}
}
return true
}
/**
* Determines if a polygon and a circle are colliding
* @param {Polygon} a The source polygon to test
* @param {Circle} b The target circle to test against
* @param {Result} [result = null] A Result object on which to store information about the collision
* @param {Boolean} [reverse = false] Set to true to reverse a and b in the result parameter when testing circle->polygon instead of polygon->circle
* @returns {Boolean}
*/
function polygonCircle (a, b, result = null, reverse = false) {
const a_coords = a._coords
const a_edges = a._edges
const a_normals = a._normals
const b_x = b.x
const b_y = b.y
const b_radius = b.radius * b.scale
const b_radius2 = b_radius * 2
const radius_squared = b_radius * b_radius
const count = a_coords.length
let a_in_b = true
let b_in_a = true
let overlap = null
let overlap_x = 0
let overlap_y = 0
// Handle points specially
if (count === 2) {
const coord_x = b_x - a_coords[0]
const coord_y = b_y - a_coords[1]
const length_squared = coord_x * coord_x + coord_y * coord_y
if (length_squared > radius_squared) {
return false
}
if (result) {
const length = Math.sqrt(length_squared)
overlap = b_radius - length
overlap_x = coord_x / length
overlap_y = coord_y / length
b_in_a = false
}
} else {
for (let ix = 0, iy = 1; ix < count; ix += 2, iy += 2) {
const coord_x = b_x - a_coords[ix]
const coord_y = b_y - a_coords[iy]
const edge_x = a_edges[ix]
const edge_y = a_edges[iy]
const dot = coord_x * edge_x + coord_y * edge_y
const region = dot < 0 ? -1 : dot > edge_x * edge_x + edge_y * edge_y ? 1 : 0
let tmp_overlapping = false
let tmp_overlap = 0
let tmp_overlap_x = 0
let tmp_overlap_y = 0
if (result && a_in_b && coord_x * coord_x + coord_y * coord_y > radius_squared) {
a_in_b = false
}
if (region) {
const left = region === -1
const other_x = left ? (ix === 0 ? count - 2 : ix - 2) : (ix === count - 2 ? 0 : ix + 2)
const other_y = other_x + 1
const coord2_x = b_x - a_coords[other_x]
const coord2_y = b_y - a_coords[other_y]
const edge2_x = a_edges[other_x]
const edge2_y = a_edges[other_y]
const dot2 = coord2_x * edge2_x + coord2_y * edge2_y
const region2 = dot2 < 0 ? -1 : dot2 > edge2_x * edge2_x + edge2_y * edge2_y ? 1 : 0
if (region2 === -region) {
const target_x = left ? coord_x : coord2_x
const target_y = left ? coord_y : coord2_y
const length_squared = target_x * target_x + target_y * target_y
if (length_squared > radius_squared) {
return false
}
if (result) {
const length = Math.sqrt(length_squared)
tmp_overlapping = true
tmp_overlap = b_radius - length
tmp_overlap_x = target_x / length
tmp_overlap_y = target_y / length
b_in_a = false
}
}
} else {
const normal_x = a_normals[ix]
const normal_y = a_normals[iy]
const length = coord_x * normal_x + coord_y * normal_y
const absolute_length = length < 0 ? -length : length
if (length > 0 && absolute_length > b_radius) {
return false
}
if (result) {
tmp_overlapping = true
tmp_overlap = b_radius - length
tmp_overlap_x = normal_x
tmp_overlap_y = normal_y
if (b_in_a && (length >= 0) || (tmp_overlap < b_radius2)) {
b_in_a = false
}
}
}
if (tmp_overlapping && (overlap === null || overlap > tmp_overlap)) {
overlap = tmp_overlap
overlap_x = tmp_overlap_x
overlap_y = tmp_overlap_y
}
}
}
if (result) {
result.a_in_b = reverse ? b_in_a : a_in_b
result.b_in_a = reverse ? a_in_b : b_in_a
result.overlap = overlap
result.overlap_x = reverse ? -overlap_x : overlap_x
result.overlap_y = reverse ? -overlap_y : overlap_y
}
return true
}
/**
* Determines if two circles are colliding
* @param {Circle} a The source circle to test
* @param {Circle} b The target circle to test against
* @param {Result} [result = null] A Result object on which to store information about the collision
* @returns {Boolean}
*/
function circleCircle (a, b, result = null) {
const a_radius = a.radius * a.scale
const b_radius = b.radius * b.scale
const difference_x = b.x - a.x
const difference_y = b.y - a.y
const radius_sum = a_radius + b_radius
const length_squared = difference_x * difference_x + difference_y * difference_y
if (length_squared > radius_sum * radius_sum) {
return false
}
if (result) {
const length = Math.sqrt(length_squared)
result.a_in_b = a_radius <= b_radius && length <= b_radius - a_radius
result.b_in_a = b_radius <= a_radius && length <= a_radius - b_radius
result.overlap = radius_sum - length
result.overlap_x = difference_x / length
result.overlap_y = difference_y / length
}
return true
}
/**
* Determines if two polygons are separated by an axis
* @param {Array<Number[]>} a_coords The coordinates of the polygon to test
* @param {Array<Number[]>} b_coords The coordinates of the polygon to test against
* @param {Number} x The X direction of the axis
* @param {Number} y The Y direction of the axis
* @param {Result} [result = null] A Result object on which to store information about the collision
* @returns {Boolean}
*/
function separatingAxis (a_coords, b_coords, x, y, result = null) {
const a_count = a_coords.length
const b_count = b_coords.length
if (!a_count || !b_count) {
return true
}
let a_start = null
let a_end = null
let b_start = null
let b_end = null
for (let ix = 0, iy = 1; ix < a_count; ix += 2, iy += 2) {
const dot = a_coords[ix] * x + a_coords[iy] * y
if (a_start === null || a_start > dot) {
a_start = dot
}
if (a_end === null || a_end < dot) {
a_end = dot
}
}
for (let ix = 0, iy = 1; ix < b_count; ix += 2, iy += 2) {
const dot = b_coords[ix] * x + b_coords[iy] * y
if (b_start === null || b_start > dot) {
b_start = dot
}
if (b_end === null || b_end < dot) {
b_end = dot
}
}
if (a_start > b_end || a_end < b_start) {
return true
}
if (result) {
let overlap = 0
if (a_start < b_start) {
result.a_in_b = false
if (a_end < b_end) {
overlap = a_end - b_start
result.b_in_a = false
} else {
const option1 = a_end - b_start
const option2 = b_end - a_start
overlap = option1 < option2 ? option1 : -option2
}
} else {
result.b_in_a = false
if (a_end > b_end) {
overlap = a_start - b_end
result.a_in_b = false
} else {
const option1 = a_end - b_start
const option2 = b_end - a_start
overlap = option1 < option2 ? option1 : -option2
}
}
const current_overlap = result.overlap
const absolute_overlap = overlap < 0 ? -overlap : overlap
if (current_overlap === null || current_overlap > absolute_overlap) {
const sign = overlap < 0 ? -1 : 1
result.overlap = absolute_overlap
result.overlap_x = x * sign
result.overlap_y = y * sign
}
}
return false
}
module.exports = SAT
module.exports.default = module.exports