import { Vector3, Matrix4, Matrix3 } from 'three'
import { kdTree } from './kdtree.js'
import { Subject, BehaviorSubject } from 'rxjs'
import { sample } from 'rxjs/operators'
import { profiler, createProfiler } from './profile'
import { transformable } from './shapes'
export * from './shapes'
export { profiler, createProfiler }
/**
* @property {Vector3} amount
* @property {BodySupport} support
* @property {BodySupport} other
* @typedef {Object} OverlapInfo
*/
/**
* A Support that is paired with a body.
*
* @typedef {Support} BodySupport
*/
/**
* A transformable collection of shapes.
*
* @typedef {Object} Body
* @property {BodySupport[]} supports - An array of support functions, each
* with a body field pointing to the
* returned object
* @property {Matrix4} transform - The current transformation
* @property {Function} update - Updates the objects bounding box in the scene
* @property {Vector3} position - The current position. Set the position using
* transform and then calling update()
* @property {Vector3} velocity - The current velocity
* @property {Subject} isKinematic - Whether the object should be affected by physics
*/
const privateMaps = {
beginOverlap: new WeakMap(),
stayOverlap: new WeakMap(),
endOverlap: new WeakMap(),
overlap: new WeakMap(),
changed: new WeakMap(),
bodyOf: new WeakMap()
}
/**
* Returns an RxJS observable that emits any time the given body changes.
*
* @param {Body} body
*/
export function changed (body) {
return privateMaps.changed.get(body).asObservable()
}
/**
* Returns an RxJS observable that emits an OverlapInfo object any time the
* given body collides with another body.
*
* @param {Body} body
*/
export function beginOverlap (body) {
return privateMaps.beginOverlap.get(body).asObservable()
}
/**
* Returns an RxJS observable that emits an OverlapInfo object when the given
* body stops colliding with another body.
*
* @param {Body} body
*/
export function endOverlap (body) {
return privateMaps.endOverlap.get(body).asObservable()
}
/**
* Returns an RxJS observable that emits an OverlapInfo object while the given
* body is overlapping another body.
*
* @param {Body} body
*/
export function stayOverlap (body) {
return privateMaps.stayOverlap.get(body).asObservable()
}
/**
* Returns an RxJS observable that emits an OverlapInfo any time two objects in
* the scene are overlapping.
*
* @param {Scene} scene
*/
export function overlap (scene) {
return privateMaps.overlap.get(scene).asObservable()
}
/**
* Returns an object that can be added to the scene, built with the given
* support functions. For each supplied support function, a new support function
* is created, with an additional `body` field, which points to the returned
* body.
*
* @param {Support[]} props.supports - A set of support functions.
* @param {Boolean} props.isKinematic - Whether the body should move based on
* the physics simulation or not.
* @returns {Body}
*/
export function body ({ supports, isKinematic }) {
const transform = new Matrix4()
const transformInverse = new Matrix3()
const position = new Vector3(0, 0, 0)
const velocity = new Vector3(0, 0, 0)
const ret = {
update () {
transformInverse.setFromMatrix4(transform)
transformInverse.transpose()
position.setFromMatrixPosition(transform)
privateMaps.changed.get(ret).next()
},
position,
transform,
velocity,
isKinematic
}
privateMaps.beginOverlap.set(ret, new Subject())
privateMaps.stayOverlap.set(ret, new Subject())
privateMaps.endOverlap.set(ret, new Subject())
privateMaps.changed.set(ret, new BehaviorSubject())
ret.supports = supports.map(x => {
const f = transformable(
x,
(d) => {
d.applyMatrix4(transform)
},
(d) => {
d.applyMatrix3(transformInverse)
})
privateMaps.bodyOf.set(f, ret)
return f
})
return Object.freeze(ret)
}
/**
* Returns the body that the given support is attached too, or null if it is not
* attached to any body.
*
* @param {BodySupport} support
* @returns {Body}
*/
export function bodyOf (support) {
return privateMaps.bodyOf.get(support) || null
}
/**
* @property {Function} add - Add a body to the scene
* @property {Function} remove - Remove a body from the scene
* @property {Function} update - Update the scene, trigger events
* @property {Function} getOverlap - Given a support, returns an observable that
* emits OverlapInfo objects for each object
* the support is overlapping.
* @typedef CollisionScene
*/
/**
* Returns a scene object. The scene efficiently handles collisions for multiple
* bodies, and supports the overlap events for bodies.
*
* To check for overlaps, call the update() method of the returned scene object.
* This will then emit the beginOverlap, endOverlap, and stayOverlap subjects
* for each body that overlaps another body. It also has a `overlapped` subject,
* which emits an OverlapInfo object when two bodies overlap. This object is
* emitted only once per pair.
*
* @param {Number} props.tolerance - Tolerance for GJK and EPA algorithms.
* Reduce this number to increase precision
* (default 0.001)
* @returns {CollisionScene}
*/
export function collisionScene ({ tolerance }) {
tolerance = tolerance || 0.001
const bodies = []
const subscriptions = new WeakMap()
const onOverlap = (support, other, amount) => {
privateMaps.overlap.get(ret).next({ support, other, amount })
}
const onBeginOverlap = (support, other, amount) => {
privateMaps.beginOverlap.get(bodyOf(support)).next({
support,
other,
amount: amount.clone()
})
privateMaps.beginOverlap.get(bodyOf(other)).next({
support: other,
other: support,
amount: amount.clone().negate()
})
}
const onEndOverlap = (support, other) => {
privateMaps.endOverlap.get(bodyOf(support)).next({
support,
other
})
privateMaps.endOverlap.get(bodyOf(other)).next({
support: other,
other: support
})
}
const onStayOverlap = (support, other, amount) => {
if (privateMaps.stayOverlap.get(bodyOf(support)).observers.length > 0) {
privateMaps.stayOverlap.get(bodyOf(support)).next({
support,
other,
amount: amount.clone()
})
}
if (privateMaps.stayOverlap.get(bodyOf(other)).observers.length > 0) {
privateMaps.stayOverlap.get(bodyOf(other)).next({
support: other,
other: support,
amount: amount.clone().negate()
})
}
}
const tree = kdTree({
onBeginOverlap,
onEndOverlap,
onStayOverlap,
onOverlap
}, tolerance)
const updated = new Subject()
const ret = Object.freeze({
add (body) {
bodies.push(body)
body.supports.forEach(support => {
/* Subscribe to update events. */
subscriptions.set(
support,
changed(body).pipe(
sample(updated)
).subscribe(tree.add(support))
)
})
},
remove (body) {
bodies.splice(bodies.indexOf(body), 1)
/* Unsubscribe from update events. */
body.supports.forEach(support => {
tree.remove(support)
const subscription = subscriptions.get(support)
if (subscription) {
subscription.unsubscribe()
}
})
},
update () {
updated.next()
},
getOverlap (support) {
return tree.getOverlap(support)
}
})
privateMaps.overlap.set(ret, new Subject())
return ret
}
/**
* @property {Function} add - Add a Body to the scene
* @property {Function} remove - Remove a Body from the scene
* @property {Function} update - Update the scene given dt, the delta time in
* seconds
* @property {Function} getOverlap - Given a support, returns an observable that
* emits OverlapInfo objects for each object
* the support is overlapping.
* @typedef Scene
*/
/**
* Creates a scene with very basic physics.
*
* @param {Number[]} props.gravity - Acceleration due to gravity
* (default [0, -9.8, 0])
* @param {Number} props.tolerance - Tolerance for GJK and EPA algorithms.
* Reduce this number to increase precision
* (default 0.001)
* @returns {Scene}
*/
export function scene ({ gravity, tolerance }) {
const cScene = collisionScene({ tolerance })
const dynamicBodies = []
gravity = new Vector3(...(gravity || [0, -9.8, 0]))
const normal = new Vector3()
const onOverlap = ({ support, other, amount }) => {
const a = support
const b = other
if (amount.lengthSq() > 0) {
normal.copy(amount).normalize()
/* Handle collisions. */
if (!bodyOf(a).isKinematic && !bodyOf(b).isKinematic) {
bodyOf(a).position.addScaledVector(amount, -0.5)
bodyOf(a).transform.setPosition(bodyOf(a).position)
bodyOf(b).position.addScaledVector(amount, 0.5)
bodyOf(b).transform.setPosition(bodyOf(b).position)
bodyOf(a).update()
bodyOf(b).update()
} else if (!bodyOf(b).isKinematic) {
bodyOf(b).position.add(amount)
bodyOf(b).transform.setPosition(bodyOf(b).position)
bodyOf(b).update()
} else if (!bodyOf(a).isKinematic) {
bodyOf(a).position.addScaledVector(amount, -1.0)
bodyOf(a).transform.setPosition(bodyOf(a).position)
bodyOf(a).update()
}
/* Adjust velocities. */
if (!bodyOf(a).isKinematic) {
bodyOf(a).velocity.addScaledVector(
normal,
-bodyOf(a).velocity.dot(normal))
/* Apply friction. TODO change constant. */
bodyOf(a).velocity.addScaledVector(bodyOf(a).velocity, -0.1)
}
if (!bodyOf(b).isKinematic) {
bodyOf(b).velocity.addScaledVector(
normal,
-bodyOf(b).velocity.dot(normal))
/* Apply friction. TODO change constant. */
bodyOf(b).velocity.addScaledVector(bodyOf(b).velocity, -0.1)
}
}
}
const ret = Object.freeze({
add (body) {
if (!body.isKinematic) {
dynamicBodies.push(body)
}
cScene.add(body)
},
remove (body) {
if (!body.isKinematic) {
dynamicBodies.splice(dynamicBodies.indexOf(body), 1)
}
cScene.remove(body)
},
update (dt) {
dynamicBodies.forEach(body => {
body.position.addScaledVector(body.velocity, dt || 0.0)
body.velocity.addScaledVector(gravity, dt || 0.0)
body.transform.setPosition(body.position)
body.update()
})
cScene.update()
},
getOverlap (support) {
return cScene.getOverlap(support)
}
})
privateMaps.overlap.set(ret, privateMaps.overlap.get(cScene))
overlap(ret).subscribe(onOverlap)
return ret
}