0
0
Fork 0
mirror of https://github.com/liabru/matter-js.git synced 2025-01-14 16:28:42 -05:00

Merge pull request #1 from liabru/master

Merged upstream
This commit is contained in:
Joe Gaffey 2021-12-28 12:19:07 +00:00 committed by GitHub
commit 79ab1622c1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 3243 additions and 2222 deletions

30
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,30 @@
name: CI
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [12.x, 14.x, 16.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm run benchmark
- run: npm run test
- run: npm run build
- run: npm run build-demo

5
.gitignore vendored
View file

@ -1,10 +1,13 @@
.idea
*.map
node_modules
npm-debug.log
docs
matter-doc-theme
build/matter-dev.js
build/matter-dev.min.js
build/matter.dev.js
build/matter.dev.min.js
demo/js/lib/matter-dev.js
demo/js/Examples.min.js
examples/build
@ -13,4 +16,4 @@ test/browser/refs
test/node/diffs
test/node/refs
__snapshots__
__compare__
__compare__

View file

@ -1,11 +0,0 @@
language: node_js
sudo: false
node_js:
- "node"
install:
- npm ci
script:
- npm run lint
- npm run test
- npm run build
- npm run build-demo

View file

@ -1,3 +1,44 @@
## 0.18.0 (2021-12-15)
* added test capture sort to improve comparison ([ea3c11b](https://github.com/liabru/matter-js/commit/ea3c11b))
* added triangles to mixed bodies example ([b116f64](https://github.com/liabru/matter-js/commit/b116f64))
* added behaviour metric to tests and refactor tests ([8125966](https://github.com/liabru/matter-js/commit/8125966))
* added benchmark test command ([7f34c45](https://github.com/liabru/matter-js/commit/7f34c45))
* added broadphase to Matter.Detector ([a6b5e7d](https://github.com/liabru/matter-js/commit/a6b5e7d))
* added cache checks to Matter.Composite ([32fd285](https://github.com/liabru/matter-js/commit/32fd285))
* added example for Composite.remove ([bc07f56](https://github.com/liabru/matter-js/commit/bc07f56))
* added example stress 3 ([d0ee246](https://github.com/liabru/matter-js/commit/d0ee246))
* added filesize to test comparison report ([b3a8aa3](https://github.com/liabru/matter-js/commit/b3a8aa3))
* added Matter.Collision ([9037f36](https://github.com/liabru/matter-js/commit/9037f36))
* added memory comparison to tests ([bedf84c](https://github.com/liabru/matter-js/commit/bedf84c))
* added note about webpack performance to readme ([80cf76b](https://github.com/liabru/matter-js/commit/80cf76b))
* added stable sorting to test worker and refactor ([81dd2fb](https://github.com/liabru/matter-js/commit/81dd2fb))
* added support for build metadata in Plugin.versionParse ([8bfaff0](https://github.com/liabru/matter-js/commit/8bfaff0))
* changed raycasting example events to enable use in tests ([10afaea](https://github.com/liabru/matter-js/commit/10afaea))
* changed build config to 'source-map' devtool ([e909b04](https://github.com/liabru/matter-js/commit/e909b04))
* changed tests to use a production build rather than source ([55feb89](https://github.com/liabru/matter-js/commit/55feb89))
* deprecated Matter.Grid ([e366d0e](https://github.com/liabru/matter-js/commit/e366d0e))
* fixed sync issues on demo compare tool ([826ed46](https://github.com/liabru/matter-js/commit/826ed46))
* improved performance measurement accuracy in tests ([cd289ec](https://github.com/liabru/matter-js/commit/cd289ec))
* improved test comparison report ([de04c00](https://github.com/liabru/matter-js/commit/de04c00))
* optimised Matter.Detector ([c7cec16](https://github.com/liabru/matter-js/commit/c7cec16)), ([fd1a70e](https://github.com/liabru/matter-js/commit/fd1a70e)), ([caeb07e](https://github.com/liabru/matter-js/commit/caeb07e)), ([efede6e](https://github.com/liabru/matter-js/commit/efede6e))
* optimised Matter.Composite ([52e7977](https://github.com/liabru/matter-js/commit/52e7977))
* optimised Matter.Pair ([d8a6380](https://github.com/liabru/matter-js/commit/d8a6380)), ([48673db](https://github.com/liabru/matter-js/commit/48673db)), ([1073dde](https://github.com/liabru/matter-js/commit/1073dde))
* optimised Matter.Pairs ([a30707f](https://github.com/liabru/matter-js/commit/a30707f)), ([a882a74](https://github.com/liabru/matter-js/commit/a882a74))
* optimised Matter.Resolver ([fceb0ca](https://github.com/liabru/matter-js/commit/fceb0ca)), ([49fbfba](https://github.com/liabru/matter-js/commit/49fbfba)), ([0b07a31](https://github.com/liabru/matter-js/commit/0b07a31)), ([f847f4c](https://github.com/liabru/matter-js/commit/f847f4c)), ([3cf65e8](https://github.com/liabru/matter-js/commit/3cf65e8)), ([30b899c](https://github.com/liabru/matter-js/commit/30b899c)), ([e4b35d3](https://github.com/liabru/matter-js/commit/e4b35d3))
* optimised Matter.SAT ([0d90a17](https://github.com/liabru/matter-js/commit/0d90a17)), ([2096961](https://github.com/liabru/matter-js/commit/2096961)), ([0af144c](https://github.com/liabru/matter-js/commit/0af144c))
* optimised Matter.Vertices ([c198878](https://github.com/liabru/matter-js/commit/c198878)), ([6883d0d](https://github.com/liabru/matter-js/commit/6883d0d)), ([792ae2e](https://github.com/liabru/matter-js/commit/792ae2e))
* refactor test worker and prevent test cache ([ca2fe75](https://github.com/liabru/matter-js/commit/ca2fe75)), ([bcc3168](https://github.com/liabru/matter-js/commit/bcc3168))
* replaced Matter.SAT with Matter.Collision ([b9e7d9d](https://github.com/liabru/matter-js/commit/b9e7d9d))
* show debug stats in dev demo ([2f14ec5](https://github.com/liabru/matter-js/commit/2f14ec5))
* updated dev dependencies ([c5028d5](https://github.com/liabru/matter-js/commit/c5028d5))
* updated examples ([c80ed5c](https://github.com/liabru/matter-js/commit/c80ed5c))
* updated test scripts ([afa467a](https://github.com/liabru/matter-js/commit/afa467a))
* use force exit in tests ([8adf810](https://github.com/liabru/matter-js/commit/8adf810))
* use Matter.Runner in test worker ([2581595](https://github.com/liabru/matter-js/commit/2581595))
## <small>0.17.1 (2021-04-14)</small>
* deprecate Engine.run alias replaced by Runner.run ([5817046](https://github.com/liabru/matter-js/commit/5817046))

View file

@ -58,7 +58,6 @@
<li><a href="https://brm.io/matter-js/demo/#airFriction">Air Friction</a></li>
<li><a href="https://brm.io/matter-js/demo/#staticFriction">Static Friction</a></li>
<li><a href="https://brm.io/matter-js/demo/#sleeping">Sleeping</a></li>
<li><a href="https://brm.io/matter-js/demo/#broadphase">Grid Broadphase</a></li>
<li><a href="https://brm.io/matter-js/demo/#beachBalls">Beach Balls</a></li>
<li><a href="https://brm.io/matter-js/demo/#stress">Stress 1</a></li>
<li><a href="https://brm.io/matter-js/demo/#stress2">Stress 2</a></li>
@ -125,6 +124,10 @@ Alternatively you can download a [stable release](https://github.com/liabru/matt
<script src="matter.js" type="text/javascript"></script>
### Webpack
Some [webpack](https://webpack.js.org/) configs including the default may impact your project's performance during development, for a solution see [issue](https://github.com/liabru/matter-js/issues/1001).
### Usage
Visit the [Getting started](https://github.com/liabru/matter-js/wiki/Getting-started) wiki page for a minimal usage example which should work in both browsers and Node.js.

View file

@ -1,3 +1,61 @@
## ▲.●matter.js`0.18.0`
Release notes for `0.18.0`. See the release [readme](https://github.com/liabru/matter-js/blob/0.18.0/README.md) for further information.
### Highlights
- **Up to ~40% performance improvement (on average measured over all examples, in Node on a Mac Air M1)**
- Replaces `Matter.Grid` with a faster and more efficient broadphase in `Matter.Detector`
- Reduced memory usage and garbage collection
- Resolves issues in `Matter.SAT` related to collision reuse
- Removes performance issues from `Matter.Grid`
- Improved collision accuracy
- Improved API and docs for collision modules
- Improved [documentation](https://brm.io/matter-js/docs/) pages
- Added note about avoiding Webpack [performance problems](https://github.com/liabru/matter-js/blob/master/README.md#install)
### Changes
See the release [compare page](https://github.com/liabru/matter-js/compare/0.17.1...0.18.0) and the [changelog](https://github.com/liabru/matter-js/blob/0.18.0/CHANGELOG.md) for a detailed list of changes.
### Migration ⌲
- Behaviour and similarity is in practice the same (a fractional difference from improved accuracy)
- API is the same other than:
- [Matter.Detector](https://brm.io/matter-js/docs/classes/Detector.html) replaces [Matter.Grid](https://brm.io/matter-js/docs/classes/Grid.html) (which is now deprecated but will remain for a short term)
- [render.options.showBroadphase](https://brm.io/matter-js/docs/classes/Render.html#property_options.showBroadphase) is deprecated (no longer implemented)
- [Matter.SAT](https://brm.io/matter-js/docs/classes/SAT.html) is renamed [Matter.Collision](https://brm.io/matter-js/docs/classes/Collision.html)
- [Matter.SAT.collides](https://brm.io/matter-js/docs/classes/SAT.html#method_collides) is now [Matter.Collision.collides](https://brm.io/matter-js/docs/classes/Collision.html#method_collides) with a slightly different set of arguments
### Comparison
Differences in behaviour, quality and performance against the previous release `0.17.1`. For more information see [comparison method](https://github.com/liabru/matter-js/pull/794).
```ocaml
Output comparison of 43 examples against previous release matter-js@0.17.1
Behaviour 99.99% Similarity 99.98% Overlap -0.00%
Performance +40.62% Memory -6.18% Filesize -0.16% 77.73 KB
airFriction · · avalanche ● · ballPool · · bridge · · car · · catapult · ·
chains · · circleStack · · cloth · · collisionFiltering · · compositeManipulation ● ·
compound · · compoundStack · · concave · · constraints ● · doublePendulum · ·
events · · friction · · gravity · · gyro · · manipulation · ◆
mixed · · newtonsCradle · · pyramid · · ragdoll · · raycasting · ·
remove · · restitution · · rounded · · sensors · · sleeping · ◆
slingshot · · softBody · · sprites · · stack · · staticFriction · ·
stats · · stress · · stress2 · · stress3 · · timescale · ·
views · · wreckingBall · ·
where · no change ● extrinsics changed ◆ intrinsics changed
```
### Contributors
Many thanks to the [contributors](https://github.com/liabru/matter-js/compare/0.17.1...0.18.0) of this release, [past contributors](https://github.com/liabru/matter-js/graphs/contributors) as well those involved in the [community](https://github.com/liabru/matter-js/issues) for your input and support.
---
## ▲.●matter.js`0.17.0`
Release notes for `0.17.0`. See the release [readme](https://github.com/liabru/matter-js/blob/0.17.0/README.md) for further information.

File diff suppressed because it is too large Load diff

4
build/matter.min.js vendored

File diff suppressed because one or more lines are too long

View file

@ -50,11 +50,11 @@
</style>
</head>
<body>
<script src="./js/matter-demo.main.5a504f.min.js"></script>
<script src="./js/matter-demo.matter-tools.5e580e.min.js"></script>
<script src="./js/matter-demo.matter-wrap.b3a896.min.js"></script>
<script src="./js/matter-demo.pathseg.067c95.min.js"></script>
<script src="./js/matter-demo.poly-decomp.59954b.min.js"></script>
<script src="./js/matter-demo.e7da73.min.js"></script>
<script src="./js/matter-demo.main.5754e1.min.js"></script>
<script src="./js/matter-demo.matter-tools.97f38a.min.js"></script>
<script src="./js/matter-demo.matter-wrap.dbda1f.min.js"></script>
<script src="./js/matter-demo.pathseg.cf21c2.min.js"></script>
<script src="./js/matter-demo.poly-decomp.c3d015.min.js"></script>
<script src="./js/matter-demo.a280d3.min.js"></script>
</body>
</html>

6
demo/js/matter-demo.a280d3.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,5 +1,5 @@
/*!
* matter-demo bundle 0.17.1 by @liabru
* matter-demo bundle 0.18.0 by @liabru
* http://brm.io/matter-js/
* License MIT
*/!function(e){function t(t){for(var n,l,a=t[0],f=t[1],i=t[2],c=0,s=[];c<a.length;c++)l=a[c],Object.prototype.hasOwnProperty.call(o,l)&&o[l]&&s.push(o[l][0]),o[l]=0;for(n in f)Object.prototype.hasOwnProperty.call(f,n)&&(e[n]=f[n]);for(p&&p(t);s.length;)s.shift()();return u.push.apply(u,i||[]),r()}function r(){for(var e,t=0;t<u.length;t++){for(var r=u[t],n=!0,a=1;a<r.length;a++){var f=r[a];0!==o[f]&&(n=!1)}n&&(u.splice(t--,1),e=l(l.s=r[0]))}return e}var n={},o={1:0},u=[];function l(t){if(n[t])return n[t].exports;var r=n[t]={i:t,l:!1,exports:{}};return e[t].call(r.exports,r,r.exports,l),r.l=!0,r.exports}l.m=e,l.c=n,l.d=function(e,t,r){l.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},l.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},l.t=function(e,t){if(1&t&&(e=l(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(l.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var n in e)l.d(r,n,function(t){return e[t]}.bind(null,n));return r},l.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return l.d(t,"a",t),t},l.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},l.p="./js";var a=this.webpackJsonpMatterDemo=this.webpackJsonpMatterDemo||[],f=a.push.bind(a);a.push=t,a=a.slice();for(var i=0;i<a.length;i++)t(a[i]);var p=f;r()}([]);

View file

@ -1,5 +1,5 @@
/*!
* matter-demo bundle 0.17.1 by @liabru
* matter-demo bundle 0.18.0 by @liabru
* http://brm.io/matter-js/
* License MIT
*/

View file

@ -1,5 +1,5 @@
/*!
* matter-demo bundle 0.17.1 by @liabru
* matter-demo bundle 0.18.0 by @liabru
* http://brm.io/matter-js/
* License MIT
*/

View file

@ -1,5 +1,5 @@
/*!
* matter-demo bundle 0.17.1 by @liabru
* matter-demo bundle 0.18.0 by @liabru
* http://brm.io/matter-js/
* License MIT
*/

View file

@ -1,5 +1,5 @@
/*!
* matter-demo bundle 0.17.1 by @liabru
* matter-demo bundle 0.18.0 by @liabru
* http://brm.io/matter-js/
* License MIT
*/

View file

@ -36,9 +36,6 @@ Example.gyro = function() {
var stack = Composites.stack(20, 20, 10, 5, 0, 0, function(x, y) {
var sides = Math.round(Common.random(1, 8));
// triangles can be a little unstable, so avoid until fixed
sides = (sides === 3) ? 4 : sides;
// round the edges of some bodies
var chamfer = null;
if (sides > 2 && Common.random() > 0.7) {

View file

@ -3,7 +3,6 @@ module.exports = {
avalanche: require('./avalanche.js'),
ballPool: require('./ballPool.js'),
bridge: require('./bridge.js'),
broadphase: require('./broadphase.js'),
car: require('./car.js'),
catapult: require('./catapult.js'),
chains: require('./chains.js'),
@ -28,6 +27,7 @@ module.exports = {
raycasting: require('./raycasting.js'),
restitution: require('./restitution.js'),
rounded: require('./rounded.js'),
remove: require('./remove.js'),
sensors: require('./sensors.js'),
sleeping: require('./sleeping.js'),
slingshot: require('./slingshot.js'),

View file

@ -62,7 +62,7 @@ Example.manipulation = function() {
var counter = 0,
scaleFactor = 1.01;
Events.on(engine, 'beforeUpdate', function(event) {
Events.on(runner, 'afterTick', function(event) {
counter += 1;
if (counter === 40)

View file

@ -36,9 +36,6 @@ Example.mixed = function() {
var stack = Composites.stack(20, 20, 10, 5, 0, 0, function(x, y) {
var sides = Math.round(Common.random(1, 8));
// triangles can be a little unstable, so avoid until fixed
sides = (sides === 3) ? 4 : sides;
// round the edges of some bodies
var chamfer = null;
if (sides > 2 && Common.random() > 0.7) {

View file

@ -68,14 +68,21 @@ Example.raycasting = function() {
Bodies.rectangle(0, 300, 50, 600, { isStatic: true })
]);
var collisions = [],
startPoint = { x: 400, y: 100 };
Events.on(engine, 'afterUpdate', function() {
var mouse = mouseConstraint.mouse,
bodies = Composite.allBodies(engine.world),
endPoint = mouse.position || { x: 100, y: 600 };
collisions = Query.ray(bodies, startPoint, endPoint);
});
Events.on(render, 'afterRender', function() {
var mouse = mouseConstraint.mouse,
context = render.context,
bodies = Composite.allBodies(engine.world),
startPoint = { x: 400, y: 100 },
endPoint = mouse.position;
var collisions = Query.ray(bodies, startPoint, endPoint);
endPoint = mouse.position || { x: 100, y: 600 };
Render.startViewTransform(render);

View file

@ -1,6 +1,6 @@
var Example = Example || {};
Example.broadphase = function() {
Example.remove = function() {
var Engine = Matter.Engine,
Render = Matter.Render,
Runner = Matter.Runner,
@ -9,7 +9,8 @@ Example.broadphase = function() {
MouseConstraint = Matter.MouseConstraint,
Mouse = Matter.Mouse,
Composite = Matter.Composite,
Bodies = Matter.Bodies;
Bodies = Matter.Bodies,
Events = Matter.Events;
// create engine
var engine = Engine.create(),
@ -23,7 +24,6 @@ Example.broadphase = function() {
width: 800,
height: 600,
showAngleIndicator: true,
showBroadphase: true
}
});
@ -33,7 +33,59 @@ Example.broadphase = function() {
var runner = Runner.create();
Runner.run(runner, engine);
// add bodies
var stack = null,
updateCount = 0;
var createStack = function() {
return Composites.stack(20, 20, 10, 5, 0, 0, function(x, y) {
var sides = Math.round(Common.random(1, 8));
// round the edges of some bodies
var chamfer = null;
if (sides > 2 && Common.random() > 0.7) {
chamfer = {
radius: 10
};
}
switch (Math.round(Common.random(0, 1))) {
case 0:
if (Common.random() < 0.8) {
return Bodies.rectangle(x, y, Common.random(25, 50), Common.random(25, 50), { chamfer: chamfer });
} else {
return Bodies.rectangle(x, y, Common.random(80, 120), Common.random(25, 30), { chamfer: chamfer });
}
case 1:
return Bodies.polygon(x, y, sides, Common.random(25, 50), { chamfer: chamfer });
}
});
};
// add and remove stacks every few updates
Events.on(engine, 'afterUpdate', function() {
// limit rate
if (stack && updateCount <= 50) {
updateCount += 1;
return;
}
updateCount = 0;
// remove last stack
if (stack) {
Composite.remove(world, stack);
}
// create a new stack
stack = createStack();
// add the new stack
Composite.add(world, stack);
});
// add another stack that will not be removed
Composite.add(world, createStack());
Composite.add(world, [
// walls
Bodies.rectangle(400, 0, 800, 50, { isStatic: true }),
@ -42,23 +94,6 @@ Example.broadphase = function() {
Bodies.rectangle(0, 300, 50, 600, { isStatic: true })
]);
var stack = Composites.stack(20, 20, 12, 5, 0, 0, function(x, y) {
switch (Math.round(Common.random(0, 1))) {
case 0:
if (Common.random() < 0.8) {
return Bodies.rectangle(x, y, Common.random(20, 50), Common.random(20, 50));
} else {
return Bodies.rectangle(x, y, Common.random(80, 120), Common.random(20, 30));
}
case 1:
return Bodies.polygon(x, y, Math.round(Common.random(1, 8)), Common.random(20, 50));
}
});
Composite.add(world, stack);
// add mouse control
var mouse = Mouse.create(render.canvas),
mouseConstraint = MouseConstraint.create(engine, {
@ -95,9 +130,9 @@ Example.broadphase = function() {
};
};
Example.broadphase.title = 'Broadphase';
Example.broadphase.for = '>=0.14.2';
Example.remove.title = 'Composite Remove';
Example.remove.for = '>=0.14.2';
if (typeof module !== 'undefined') {
module.exports = Example.broadphase;
module.exports = Example.remove;
}

View file

@ -61,12 +61,15 @@ Example.sleeping = function() {
Composite.add(world, stack);
/*
// sleep events
for (var i = 0; i < stack.bodies.length; i++) {
Events.on(stack.bodies[i], 'sleepStart sleepEnd', function(event) {
var body = this;
console.log('body id', body.id, 'sleeping:', body.isSleeping);
});
}
*/
// add mouse control
var mouse = Mouse.create(render.canvas),

View file

@ -12,8 +12,12 @@ Example.stress3 = function() {
Bodies = Matter.Bodies;
// create engine
var engine = Engine.create(),
world = engine.world;
var engine = Engine.create({
positionIterations: 10,
velocityIterations: 10
});
var world = engine.world;
// create renderer
var render = Render.create({

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "matter-js",
"version": "0.17.1",
"version": "0.18.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "matter-js",
"version": "0.17.1",
"version": "0.18.0",
"license": "MIT",
"devDependencies": {
"conventional-changelog-cli": "^2.1.1",

View file

@ -1,6 +1,6 @@
{
"name": "matter-js",
"version": "0.17.1",
"version": "0.18.0",
"license": "MIT",
"homepage": "http://brm.io/matter-js/",
"author": "Liam Brummitt <liam@brm.io> (http://brm.io/)",
@ -44,17 +44,19 @@
"serve": "webpack-dev-server --no-cache --mode development --config webpack.demo.config.js",
"watch": "nodemon --watch webpack.demo.config.js --exec \"npm run serve\"",
"build": "webpack --mode=production --no-hot --no-watch & webpack --mode=production --no-hot --no-watch --env.MINIMIZE",
"build-alpha": "webpack --mode=production --env.ALPHA & webpack --mode=production --env.MINIMIZE --env.ALPHA",
"build-alpha": "webpack --mode=production --no-hot --no-watch --env.KIND=alpha & webpack --mode=production --no-hot --no-watch --env.MINIMIZE --env.KIND=alpha",
"build-dev": "webpack --mode=production --no-hot --no-watch --env.KIND=dev & webpack --mode=production --no-hot --no-watch --env.MINIMIZE --env.KIND=dev",
"build-demo": "rm -rf ./demo/js && webpack --no-hot --no-watch --config webpack.demo.config.js --mode=production && webpack --no-hot --no-watch --config webpack.demo.config.js --mode=production --env.MINIMIZE",
"lint": "eslint 'src/**/*.js' 'demo/src/**/*.js' 'examples/*.js' 'webpack.*.js'",
"doc": "yuidoc --config yuidoc.json --project-version $npm_package_version",
"doc-watch": "nodemon --delay 3 --watch 'matter-doc-theme' --watch src -e 'js,html,css,handlebars' --exec 'npm run doc'",
"benchmark": "EXAMPLES=stress3 npm run test-node",
"test": "npm run test-node",
"test-all": "jest --no-cache",
"test-node": "npm run build-dev && node --expose-gc node_modules/.bin/jest --force-exit --no-cache --runInBand ./test/Examples.spec.js",
"test-browser": "node --expose-gc node_modules/.bin/jest --force-exit --no-cache --runInBand ./test/Browser.spec.js",
"test-all": "npm run test-node && npm run test-browser",
"test-save": "SAVE=true npm run test-node",
"test-watch": "npm run test-node -- --watch",
"test-node": "jest --no-cache ./test/Examples.spec.js",
"test-browser": "jest --no-cache ./test/Browser.spec.js",
"changelog": "conventional-changelog -i CHANGELOG.md -s -r",
"release": "npm version --no-git-tag-version",
"preversion": "git checkout master && npm run lint && SAVE=true npm run test-all",

View file

@ -39,7 +39,12 @@ var Body = require('./Body');
constraints: [],
composites: [],
label: 'Composite',
plugin: {}
plugin: {},
cache: {
allBodies: null,
allConstraints: null,
allComposites: null
}
}, options);
};
@ -47,6 +52,7 @@ var Body = require('./Body');
* Sets the composite's `isModified` flag.
* If `updateParents` is true, all parents will be set (default: false).
* If `updateChildren` is true, all children will be set (default: false).
* @private
* @method setModified
* @param {composite} composite
* @param {boolean} isModified
@ -56,12 +62,18 @@ var Body = require('./Body');
Composite.setModified = function(composite, isModified, updateParents, updateChildren) {
composite.isModified = isModified;
if (isModified && composite.cache) {
composite.cache.allBodies = null;
composite.cache.allConstraints = null;
composite.cache.allComposites = null;
}
if (updateParents && composite.parent) {
Composite.setModified(composite.parent, isModified, updateParents, updateChildren);
}
if (updateChildren) {
for(var i = 0; i < composite.composites.length; i++) {
for (var i = 0; i < composite.composites.length; i++) {
var childComposite = composite.composites[i];
Composite.setModified(childComposite, isModified, updateParents, updateChildren);
}
@ -182,7 +194,6 @@ var Body = require('./Body');
var position = Common.indexOf(compositeA.composites, compositeB);
if (position !== -1) {
Composite.removeCompositeAt(compositeA, position);
Composite.setModified(compositeA, true, true, false);
}
if (deep) {
@ -235,7 +246,6 @@ var Body = require('./Body');
var position = Common.indexOf(composite.bodies, body);
if (position !== -1) {
Composite.removeBodyAt(composite, position);
Composite.setModified(composite, true, true, false);
}
if (deep) {
@ -336,6 +346,7 @@ var Body = require('./Body');
composite.constraints.length = 0;
composite.composites.length = 0;
Composite.setModified(composite, true, true, false);
return composite;
@ -348,11 +359,19 @@ var Body = require('./Body');
* @return {body[]} All the bodies
*/
Composite.allBodies = function(composite) {
if (composite.cache && composite.cache.allBodies) {
return composite.cache.allBodies;
}
var bodies = [].concat(composite.bodies);
for (var i = 0; i < composite.composites.length; i++)
bodies = bodies.concat(Composite.allBodies(composite.composites[i]));
if (composite.cache) {
composite.cache.allBodies = bodies;
}
return bodies;
};
@ -363,11 +382,19 @@ var Body = require('./Body');
* @return {constraint[]} All the constraints
*/
Composite.allConstraints = function(composite) {
if (composite.cache && composite.cache.allConstraints) {
return composite.cache.allConstraints;
}
var constraints = [].concat(composite.constraints);
for (var i = 0; i < composite.composites.length; i++)
constraints = constraints.concat(Composite.allConstraints(composite.composites[i]));
if (composite.cache) {
composite.cache.allConstraints = constraints;
}
return constraints;
};
@ -378,11 +405,19 @@ var Body = require('./Body');
* @return {composite[]} All the composites
*/
Composite.allComposites = function(composite) {
if (composite.cache && composite.cache.allComposites) {
return composite.cache.allComposites;
}
var composites = [].concat(composite.composites);
for (var i = 0; i < composite.composites.length; i++)
composites = composites.concat(Composite.allComposites(composite.composites[i]));
if (composite.cache) {
composite.cache.allComposites = composites;
}
return composites;
};
@ -449,8 +484,6 @@ var Body = require('./Body');
objects[i].id = Common.nextId();
}
Composite.setModified(composite, true, true, false);
return composite;
};
@ -469,8 +502,6 @@ var Body = require('./Body');
Body.translate(bodies[i], translation);
}
Composite.setModified(composite, true, true, false);
return composite;
};
@ -500,8 +531,6 @@ var Body = require('./Body');
Body.rotate(body, rotation);
}
Composite.setModified(composite, true, true, false);
return composite;
};
@ -530,8 +559,6 @@ var Body = require('./Body');
Body.scale(body, scaleX, scaleY);
}
Composite.setModified(composite, true, true, false);
return composite;
};
@ -631,8 +658,7 @@ var Body = require('./Body');
/**
* A flag that specifies whether the composite has been modified during the current step.
* Most `Matter.Composite` methods will automatically set this flag to `true` to inform the engine of changes to be handled.
* If you need to change it manually, you should use the `Composite.setModified` method.
* This is automatically managed when bodies, constraints or composites are added or removed.
*
* @property isModified
* @type boolean
@ -684,4 +710,13 @@ var Body = require('./Body');
* @type {}
*/
/**
* An object used for storing cached results for performance reasons.
* This is used internally only and is automatically managed.
*
* @private
* @property cache
* @type {}
*/
})();

408
src/collision/Collision.js Normal file
View file

@ -0,0 +1,408 @@
/**
* The `Matter.Collision` module contains methods for detecting collisions between a given pair of bodies.
*
* For efficient detection between a list of bodies, see `Matter.Detector` and `Matter.Query`.
*
* See `Matter.Engine` for collision events.
*
* @class Collision
*/
var Collision = {};
module.exports = Collision;
var Vertices = require('../geometry/Vertices');
var Pair = require('./Pair');
(function() {
var _supports = [];
var _overlapAB = {
overlap: 0,
axis: null
};
var _overlapBA = {
overlap: 0,
axis: null
};
/**
* Creates a new collision record.
* @method create
* @param {body} bodyA The first body part represented by the collision record
* @param {body} bodyB The second body part represented by the collision record
* @return {collision} A new collision record
*/
Collision.create = function(bodyA, bodyB) {
return {
pair: null,
collided: false,
bodyA: bodyA,
bodyB: bodyB,
parentA: bodyA.parent,
parentB: bodyB.parent,
depth: 0,
normal: { x: 0, y: 0 },
tangent: { x: 0, y: 0 },
penetration: { x: 0, y: 0 },
supports: []
};
};
/**
* Detect collision between two bodies.
* @method collides
* @param {body} bodyA
* @param {body} bodyB
* @param {pairs} [pairs] Optionally reuse collision records from existing pairs.
* @return {collision|null} A collision record if detected, otherwise null
*/
Collision.collides = function(bodyA, bodyB, pairs) {
Collision._overlapAxes(_overlapAB, bodyA.vertices, bodyB.vertices, bodyA.axes);
if (_overlapAB.overlap <= 0) {
return null;
}
Collision._overlapAxes(_overlapBA, bodyB.vertices, bodyA.vertices, bodyB.axes);
if (_overlapBA.overlap <= 0) {
return null;
}
// reuse collision records for gc efficiency
var pair = pairs && pairs.table[Pair.id(bodyA, bodyB)],
collision;
if (!pair) {
collision = Collision.create(bodyA, bodyB);
collision.collided = true;
collision.bodyA = bodyA.id < bodyB.id ? bodyA : bodyB;
collision.bodyB = bodyA.id < bodyB.id ? bodyB : bodyA;
collision.parentA = collision.bodyA.parent;
collision.parentB = collision.bodyB.parent;
} else {
collision = pair.collision;
}
bodyA = collision.bodyA;
bodyB = collision.bodyB;
var minOverlap;
if (_overlapAB.overlap < _overlapBA.overlap) {
minOverlap = _overlapAB;
} else {
minOverlap = _overlapBA;
}
var normal = collision.normal,
supports = collision.supports,
minAxis = minOverlap.axis,
minAxisX = minAxis.x,
minAxisY = minAxis.y;
// ensure normal is facing away from bodyA
if (minAxisX * (bodyB.position.x - bodyA.position.x) + minAxisY * (bodyB.position.y - bodyA.position.y) < 0) {
normal.x = minAxisX;
normal.y = minAxisY;
} else {
normal.x = -minAxisX;
normal.y = -minAxisY;
}
collision.tangent.x = -normal.y;
collision.tangent.y = normal.x;
collision.depth = minOverlap.overlap;
collision.penetration.x = normal.x * collision.depth;
collision.penetration.y = normal.y * collision.depth;
// find support points, there is always either exactly one or two
var supportsB = Collision._findSupports(bodyA, bodyB, normal, 1),
supportCount = 0;
// find the supports from bodyB that are inside bodyA
if (Vertices.contains(bodyA.vertices, supportsB[0])) {
supports[supportCount++] = supportsB[0];
}
if (Vertices.contains(bodyA.vertices, supportsB[1])) {
supports[supportCount++] = supportsB[1];
}
// find the supports from bodyA that are inside bodyB
if (supportCount < 2) {
var supportsA = Collision._findSupports(bodyB, bodyA, normal, -1);
if (Vertices.contains(bodyB.vertices, supportsA[0])) {
supports[supportCount++] = supportsA[0];
}
if (supportCount < 2 && Vertices.contains(bodyB.vertices, supportsA[1])) {
supports[supportCount++] = supportsA[1];
}
}
// account for the edge case of overlapping but no vertex containment
if (supportCount === 0) {
supports[supportCount++] = supportsB[0];
}
// update supports array size
supports.length = supportCount;
return collision;
};
/**
* Find the overlap between two sets of vertices.
* @method _overlapAxes
* @private
* @param {object} result
* @param {vertices} verticesA
* @param {vertices} verticesB
* @param {axes} axes
*/
Collision._overlapAxes = function(result, verticesA, verticesB, axes) {
var verticesALength = verticesA.length,
verticesBLength = verticesB.length,
verticesAX = verticesA[0].x,
verticesAY = verticesA[0].y,
verticesBX = verticesB[0].x,
verticesBY = verticesB[0].y,
axesLength = axes.length,
overlapMin = Number.MAX_VALUE,
overlapAxisNumber = 0,
overlap,
overlapAB,
overlapBA,
dot,
i,
j;
for (i = 0; i < axesLength; i++) {
var axis = axes[i],
axisX = axis.x,
axisY = axis.y,
minA = verticesAX * axisX + verticesAY * axisY,
minB = verticesBX * axisX + verticesBY * axisY,
maxA = minA,
maxB = minB;
for (j = 1; j < verticesALength; j += 1) {
dot = verticesA[j].x * axisX + verticesA[j].y * axisY;
if (dot > maxA) {
maxA = dot;
} else if (dot < minA) {
minA = dot;
}
}
for (j = 1; j < verticesBLength; j += 1) {
dot = verticesB[j].x * axisX + verticesB[j].y * axisY;
if (dot > maxB) {
maxB = dot;
} else if (dot < minB) {
minB = dot;
}
}
overlapAB = maxA - minB;
overlapBA = maxB - minA;
overlap = overlapAB < overlapBA ? overlapAB : overlapBA;
if (overlap < overlapMin) {
overlapMin = overlap;
overlapAxisNumber = i;
if (overlap <= 0) {
// can not be intersecting
break;
}
}
}
result.axis = axes[overlapAxisNumber];
result.overlap = overlapMin;
};
/**
* Projects vertices on an axis and returns an interval.
* @method _projectToAxis
* @private
* @param {} projection
* @param {} vertices
* @param {} axis
*/
Collision._projectToAxis = function(projection, vertices, axis) {
var min = vertices[0].x * axis.x + vertices[0].y * axis.y,
max = min;
for (var i = 1; i < vertices.length; i += 1) {
var dot = vertices[i].x * axis.x + vertices[i].y * axis.y;
if (dot > max) {
max = dot;
} else if (dot < min) {
min = dot;
}
}
projection.min = min;
projection.max = max;
};
/**
* Finds supporting vertices given two bodies along a given direction using hill-climbing.
* @method _findSupports
* @private
* @param {body} bodyA
* @param {body} bodyB
* @param {vector} normal
* @param {number} direction
* @return [vector]
*/
Collision._findSupports = function(bodyA, bodyB, normal, direction) {
var vertices = bodyB.vertices,
verticesLength = vertices.length,
bodyAPositionX = bodyA.position.x,
bodyAPositionY = bodyA.position.y,
normalX = normal.x * direction,
normalY = normal.y * direction,
nearestDistance = Number.MAX_VALUE,
vertexA,
vertexB,
vertexC,
distance,
j;
// find deepest vertex relative to the axis
for (j = 0; j < verticesLength; j += 1) {
vertexB = vertices[j];
distance = normalX * (bodyAPositionX - vertexB.x) + normalY * (bodyAPositionY - vertexB.y);
// convex hill-climbing
if (distance < nearestDistance) {
nearestDistance = distance;
vertexA = vertexB;
}
}
// measure next vertex
vertexC = vertices[(verticesLength + vertexA.index - 1) % verticesLength];
nearestDistance = normalX * (bodyAPositionX - vertexC.x) + normalY * (bodyAPositionY - vertexC.y);
// compare with previous vertex
vertexB = vertices[(vertexA.index + 1) % verticesLength];
if (normalX * (bodyAPositionX - vertexB.x) + normalY * (bodyAPositionY - vertexB.y) < nearestDistance) {
_supports[0] = vertexA;
_supports[1] = vertexB;
return _supports;
}
_supports[0] = vertexA;
_supports[1] = vertexC;
return _supports;
};
/*
*
* Properties Documentation
*
*/
/**
* A reference to the pair using this collision record, if there is one.
*
* @property pair
* @type {pair|null}
* @default null
*/
/**
* A flag that indicates if the bodies were colliding when the collision was last updated.
*
* @property collided
* @type boolean
* @default false
*/
/**
* The first body part represented by the collision (see also `collision.parentA`).
*
* @property bodyA
* @type body
*/
/**
* The second body part represented by the collision (see also `collision.parentB`).
*
* @property bodyB
* @type body
*/
/**
* The first body represented by the collision (i.e. `collision.bodyA.parent`).
*
* @property parentA
* @type body
*/
/**
* The second body represented by the collision (i.e. `collision.bodyB.parent`).
*
* @property parentB
* @type body
*/
/**
* A `Number` that represents the minimum separating distance between the bodies along the collision normal.
*
* @readOnly
* @property depth
* @type number
* @default 0
*/
/**
* A normalised `Vector` that represents the direction between the bodies that provides the minimum separating distance.
*
* @property normal
* @type vector
* @default { x: 0, y: 0 }
*/
/**
* A normalised `Vector` that is the tangent direction to the collision normal.
*
* @property tangent
* @type vector
* @default { x: 0, y: 0 }
*/
/**
* A `Vector` that represents the direction and depth of the collision.
*
* @property penetration
* @type vector
* @default { x: 0, y: 0 }
*/
/**
* An array of body vertices that represent the support points in the collision.
* These are the deepest vertices (along the collision normal) of each body that are contained by the other body's vertices.
*
* @property supports
* @type vector[]
* @default []
*/
})();

View file

@ -18,21 +18,10 @@ module.exports = Contact;
*/
Contact.create = function(vertex) {
return {
id: Contact.id(vertex),
vertex: vertex,
normalImpulse: 0,
tangentImpulse: 0
};
};
/**
* Generates a contact id.
* @method id
* @param {vertex} vertex
* @return {string} Unique contactID
*/
Contact.id = function(vertex) {
return vertex.body.id + '_' + vertex.index;
};
})();

View file

@ -1,66 +1,131 @@
/**
* The `Matter.Detector` module contains methods for detecting collisions given a set of pairs.
* The `Matter.Detector` module contains methods for efficiently detecting collisions between a list of bodies using a broadphase algorithm.
*
* @class Detector
*/
// TODO: speculative contacts
var Detector = {};
module.exports = Detector;
var SAT = require('./SAT');
var Pair = require('./Pair');
var Bounds = require('../geometry/Bounds');
var Common = require('../core/Common');
var Collision = require('./Collision');
(function() {
/**
* Finds all collisions given a list of pairs.
* @method collisions
* @param {pair[]} broadphasePairs
* @param {engine} engine
* @return {array} collisions
* Creates a new collision detector.
* @method create
* @param {} options
* @return {detector} A new collision detector
*/
Detector.collisions = function(broadphasePairs, engine) {
Detector.create = function(options) {
var defaults = {
bodies: [],
pairs: null
};
return Common.extend(defaults, options);
};
/**
* Sets the list of bodies in the detector.
* @method setBodies
* @param {detector} detector
* @param {body[]} bodies
*/
Detector.setBodies = function(detector, bodies) {
detector.bodies = bodies.slice(0);
};
/**
* Clears the detector including its list of bodies.
* @method clear
* @param {detector} detector
*/
Detector.clear = function(detector) {
detector.bodies = [];
};
/**
* Efficiently finds all collisions among all the bodies in `detector.bodies` using a broadphase algorithm.
*
* _Note:_ The specific ordering of collisions returned is not guaranteed between releases and may change for performance reasons.
* If a specific ordering is required then apply a sort to the resulting array.
* @method collisions
* @param {detector} detector
* @return {collision[]} collisions
*/
Detector.collisions = function(detector) {
var collisions = [],
pairsTable = engine.pairs.table;
pairs = detector.pairs,
bodies = detector.bodies,
bodiesLength = bodies.length,
canCollide = Detector.canCollide,
collides = Collision.collides,
i,
j;
for (var i = 0; i < broadphasePairs.length; i++) {
var bodyA = broadphasePairs[i][0],
bodyB = broadphasePairs[i][1];
bodies.sort(Detector._compareBoundsX);
if ((bodyA.isStatic || bodyA.isSleeping) && (bodyB.isStatic || bodyB.isSleeping))
continue;
if (!Detector.canCollide(bodyA.collisionFilter, bodyB.collisionFilter))
continue;
for (i = 0; i < bodiesLength; i++) {
var bodyA = bodies[i],
boundsA = bodyA.bounds,
boundXMax = bodyA.bounds.max.x,
boundYMax = bodyA.bounds.max.y,
boundYMin = bodyA.bounds.min.y,
bodyAStatic = bodyA.isStatic || bodyA.isSleeping,
partsALength = bodyA.parts.length,
partsASingle = partsALength === 1;
// mid phase
if (Bounds.overlaps(bodyA.bounds, bodyB.bounds)) {
for (var j = bodyA.parts.length > 1 ? 1 : 0; j < bodyA.parts.length; j++) {
var partA = bodyA.parts[j];
for (j = i + 1; j < bodiesLength; j++) {
var bodyB = bodies[j],
boundsB = bodyB.bounds;
for (var k = bodyB.parts.length > 1 ? 1 : 0; k < bodyB.parts.length; k++) {
var partB = bodyB.parts[k];
if (boundsB.min.x > boundXMax) {
break;
}
if ((partA === bodyA && partB === bodyB) || Bounds.overlaps(partA.bounds, partB.bounds)) {
// find a previous collision we could reuse
var pairId = Pair.id(partA, partB),
pair = pairsTable[pairId],
previousCollision;
if (boundYMax < boundsB.min.y || boundYMin > boundsB.max.y) {
continue;
}
if (pair && pair.isActive) {
previousCollision = pair.collision;
} else {
previousCollision = null;
if (bodyAStatic && (bodyB.isStatic || bodyB.isSleeping)) {
continue;
}
if (!canCollide(bodyA.collisionFilter, bodyB.collisionFilter)) {
continue;
}
var partsBLength = bodyB.parts.length;
if (partsASingle && partsBLength === 1) {
var collision = collides(bodyA, bodyB, pairs);
if (collision) {
collisions.push(collision);
}
} else {
var partsAStart = partsALength > 1 ? 1 : 0,
partsBStart = partsBLength > 1 ? 1 : 0;
for (var k = partsAStart; k < partsALength; k++) {
var partA = bodyA.parts[k],
boundsA = partA.bounds;
for (var z = partsBStart; z < partsBLength; z++) {
var partB = bodyB.parts[z],
boundsB = partB.bounds;
if (boundsA.min.x > boundsB.max.x || boundsA.max.x < boundsB.min.x
|| boundsA.max.y < boundsB.min.y || boundsA.min.y > boundsB.max.y) {
continue;
}
// narrow phase
var collision = SAT.collides(partA, partB, previousCollision);
var collision = collides(partA, partB, pairs);
if (collision.collided) {
if (collision) {
collisions.push(collision);
}
}
@ -87,4 +152,39 @@ var Bounds = require('../geometry/Bounds');
return (filterA.mask & filterB.category) !== 0 && (filterB.mask & filterA.category) !== 0;
};
/**
* The comparison function used in the broadphase algorithm.
* Returns the signed delta of the bodies bounds on the x-axis.
* @private
* @method _sortCompare
* @param {body} bodyA
* @param {body} bodyB
* @return {number} The signed delta used for sorting
*/
Detector._compareBoundsX = function(bodyA, bodyB) {
return bodyA.bounds.min.x - bodyB.bounds.min.x;
};
/*
*
* Properties Documentation
*
*/
/**
* The array of `Matter.Body` between which the detector finds collisions.
*
* _Note:_ The order of bodies in this array _is not fixed_ and will be continually managed by the detector.
* @property bodies
* @type body[]
* @default []
*/
/**
* Optional. A `Matter.Pairs` object from which previous collision objects may be reused. Intended for internal `Matter.Engine` usage.
* @property pairs
* @type {pairs|null}
* @default null
*/
})();

View file

@ -1,7 +1,13 @@
/**
* This module has now been replaced by `Matter.Detector`.
*
* All usage should be migrated to `Matter.Detector` or another alternative.
* For back-compatibility purposes this module will remain for a short term and then later removed in a future release.
*
* The `Matter.Grid` module contains methods for creating and manipulating collision broadphase grid structures.
*
* @class Grid
* @deprecated
*/
var Grid = {};
@ -10,11 +16,13 @@ module.exports = Grid;
var Pair = require('./Pair');
var Common = require('../core/Common');
var deprecated = Common.deprecated;
(function() {
/**
* Creates a new grid.
* @deprecated replaced by Matter.Detector
* @method create
* @param {} options
* @return {grid} A new grid
@ -49,6 +57,7 @@ var Common = require('../core/Common');
/**
* Updates the grid.
* @deprecated replaced by Matter.Detector
* @method update
* @param {grid} grid
* @param {body[]} bodies
@ -127,8 +136,11 @@ var Common = require('../core/Common');
grid.pairsList = Grid._createActivePairsList(grid);
};
deprecated(Grid, 'update', 'Grid.update ➤ replaced by Matter.Detector');
/**
* Clears the grid.
* @deprecated replaced by Matter.Detector
* @method clear
* @param {grid} grid
*/
@ -138,9 +150,12 @@ var Common = require('../core/Common');
grid.pairsList = [];
};
deprecated(Grid, 'clear', 'Grid.clear ➤ replaced by Matter.Detector');
/**
* Finds the union of two regions.
* @method _regionUnion
* @deprecated replaced by Matter.Detector
* @private
* @param {} regionA
* @param {} regionB
@ -158,6 +173,7 @@ var Common = require('../core/Common');
/**
* Gets the region a given body falls in for a given grid.
* @method _getRegion
* @deprecated replaced by Matter.Detector
* @private
* @param {} grid
* @param {} body
@ -176,6 +192,7 @@ var Common = require('../core/Common');
/**
* Creates a region.
* @method _createRegion
* @deprecated replaced by Matter.Detector
* @private
* @param {} startCol
* @param {} endCol
@ -196,6 +213,7 @@ var Common = require('../core/Common');
/**
* Gets the bucket id at the given position.
* @method _getBucketId
* @deprecated replaced by Matter.Detector
* @private
* @param {} column
* @param {} row
@ -208,6 +226,7 @@ var Common = require('../core/Common');
/**
* Creates a bucket.
* @method _createBucket
* @deprecated replaced by Matter.Detector
* @private
* @param {} buckets
* @param {} bucketId
@ -221,14 +240,20 @@ var Common = require('../core/Common');
/**
* Adds a body to a bucket.
* @method _bucketAddBody
* @deprecated replaced by Matter.Detector
* @private
* @param {} grid
* @param {} bucket
* @param {} body
*/
Grid._bucketAddBody = function(grid, bucket, body) {
var gridPairs = grid.pairs,
pairId = Pair.id,
bucketLength = bucket.length,
i;
// add new pairs
for (var i = 0; i < bucket.length; i++) {
for (i = 0; i < bucketLength; i++) {
var bodyB = bucket[i];
if (body.id === bodyB.id || (body.isStatic && bodyB.isStatic))
@ -236,13 +261,13 @@ var Common = require('../core/Common');
// keep track of the number of buckets the pair exists in
// important for Grid.update to work
var pairId = Pair.id(body, bodyB),
pair = grid.pairs[pairId];
var id = pairId(body, bodyB),
pair = gridPairs[id];
if (pair) {
pair[2] += 1;
} else {
grid.pairs[pairId] = [body, bodyB, 1];
gridPairs[id] = [body, bodyB, 1];
}
}
@ -253,22 +278,27 @@ var Common = require('../core/Common');
/**
* Removes a body from a bucket.
* @method _bucketRemoveBody
* @deprecated replaced by Matter.Detector
* @private
* @param {} grid
* @param {} bucket
* @param {} body
*/
Grid._bucketRemoveBody = function(grid, bucket, body) {
var gridPairs = grid.pairs,
pairId = Pair.id,
i;
// remove from bucket
bucket.splice(Common.indexOf(bucket, body), 1);
var bucketLength = bucket.length;
// update pair counts
for (var i = 0; i < bucket.length; i++) {
for (i = 0; i < bucketLength; i++) {
// keep track of the number of buckets the pair exists in
// important for _createActivePairsList to work
var bodyB = bucket[i],
pairId = Pair.id(body, bodyB),
pair = grid.pairs[pairId];
var pair = gridPairs[pairId(body, bucket[i])];
if (pair)
pair[2] -= 1;
@ -278,28 +308,29 @@ var Common = require('../core/Common');
/**
* Generates a list of the active pairs in the grid.
* @method _createActivePairsList
* @deprecated replaced by Matter.Detector
* @private
* @param {} grid
* @return [] pairs
*/
Grid._createActivePairsList = function(grid) {
var pairKeys,
pair,
pairs = [];
// grid.pairs is used as a hashmap
pairKeys = Common.keys(grid.pairs);
var pair,
gridPairs = grid.pairs,
pairKeys = Common.keys(gridPairs),
pairKeysLength = pairKeys.length,
pairs = [],
k;
// iterate over grid.pairs
for (var k = 0; k < pairKeys.length; k++) {
pair = grid.pairs[pairKeys[k]];
for (k = 0; k < pairKeysLength; k++) {
pair = gridPairs[pairKeys[k]];
// if pair exists in at least one bucket
// it is a pair that needs further collision testing so push it
if (pair[2] > 0) {
pairs.push(pair);
} else {
delete grid.pairs[pairKeys[k]];
delete gridPairs[pairKeys[k]];
}
}

View file

@ -21,15 +21,14 @@ var Contact = require('./Contact');
*/
Pair.create = function(collision, timestamp) {
var bodyA = collision.bodyA,
bodyB = collision.bodyB,
parentA = collision.parentA,
parentB = collision.parentB;
bodyB = collision.bodyB;
var pair = {
id: Pair.id(bodyA, bodyB),
bodyA: bodyA,
bodyB: bodyB,
contacts: {},
collision: collision,
contacts: [],
activeContacts: [],
separation: 0,
isActive: true,
@ -37,11 +36,11 @@ var Contact = require('./Contact');
isSensor: bodyA.isSensor || bodyB.isSensor,
timeCreated: timestamp,
timeUpdated: timestamp,
inverseMass: parentA.inverseMass + parentB.inverseMass,
friction: Math.min(parentA.friction, parentB.friction),
frictionStatic: Math.max(parentA.frictionStatic, parentB.frictionStatic),
restitution: Math.max(parentA.restitution, parentB.restitution),
slop: Math.max(parentA.slop, parentB.slop)
inverseMass: 0,
friction: 0,
frictionStatic: 0,
restitution: 0,
slop: 0
};
Pair.update(pair, collision, timestamp);
@ -61,34 +60,32 @@ var Contact = require('./Contact');
supports = collision.supports,
activeContacts = pair.activeContacts,
parentA = collision.parentA,
parentB = collision.parentB;
parentB = collision.parentB,
parentAVerticesLength = parentA.vertices.length;
pair.isActive = true;
pair.timeUpdated = timestamp;
pair.collision = collision;
pair.separation = collision.depth;
pair.inverseMass = parentA.inverseMass + parentB.inverseMass;
pair.friction = Math.min(parentA.friction, parentB.friction);
pair.frictionStatic = Math.max(parentA.frictionStatic, parentB.frictionStatic);
pair.restitution = Math.max(parentA.restitution, parentB.restitution);
pair.slop = Math.max(parentA.slop, parentB.slop);
pair.friction = parentA.friction < parentB.friction ? parentA.friction : parentB.friction;
pair.frictionStatic = parentA.frictionStatic > parentB.frictionStatic ? parentA.frictionStatic : parentB.frictionStatic;
pair.restitution = parentA.restitution > parentB.restitution ? parentA.restitution : parentB.restitution;
pair.slop = parentA.slop > parentB.slop ? parentA.slop : parentB.slop;
collision.pair = pair;
activeContacts.length = 0;
if (collision.collided) {
for (var i = 0; i < supports.length; i++) {
var support = supports[i],
contactId = Contact.id(support),
contact = contacts[contactId];
for (var i = 0; i < supports.length; i++) {
var support = supports[i],
contactId = support.body === parentA ? support.index : parentAVerticesLength + support.index,
contact = contacts[contactId];
if (contact) {
activeContacts.push(contact);
} else {
activeContacts.push(contacts[contactId] = Contact.create(support));
}
if (contact) {
activeContacts.push(contact);
} else {
activeContacts.push(contacts[contactId] = Contact.create(support));
}
pair.separation = collision.depth;
Pair.setActive(pair, true, timestamp);
} else {
if (pair.isActive === true)
Pair.setActive(pair, false, timestamp);
}
};

View file

@ -12,8 +12,6 @@ var Pair = require('./Pair');
var Common = require('../core/Common');
(function() {
Pairs._pairMaxIdleLife = 1000;
/**
* Creates a new pairs structure.
@ -40,12 +38,14 @@ var Common = require('../core/Common');
*/
Pairs.update = function(pairs, collisions, timestamp) {
var pairsList = pairs.list,
pairsListLength = pairsList.length,
pairsTable = pairs.table,
collisionsLength = collisions.length,
collisionStart = pairs.collisionStart,
collisionEnd = pairs.collisionEnd,
collisionActive = pairs.collisionActive,
collision,
pairId,
pairIndex,
pair,
i;
@ -54,90 +54,61 @@ var Common = require('../core/Common');
collisionEnd.length = 0;
collisionActive.length = 0;
for (i = 0; i < pairsList.length; i++) {
for (i = 0; i < pairsListLength; i++) {
pairsList[i].confirmedActive = false;
}
for (i = 0; i < collisions.length; i++) {
for (i = 0; i < collisionsLength; i++) {
collision = collisions[i];
pair = collision.pair;
if (collision.collided) {
pairId = Pair.id(collision.bodyA, collision.bodyB);
pair = pairsTable[pairId];
if (pair) {
// pair already exists (but may or may not be active)
if (pair.isActive) {
// pair exists and is active
collisionActive.push(pair);
} else {
// pair exists but was inactive, so a collision has just started again
collisionStart.push(pair);
}
// update the pair
Pair.update(pair, collision, timestamp);
pair.confirmedActive = true;
if (pair) {
// pair already exists (but may or may not be active)
if (pair.isActive) {
// pair exists and is active
collisionActive.push(pair);
} else {
// pair did not exist, create a new pair
pair = Pair.create(collision, timestamp);
pairsTable[pairId] = pair;
// push the new pair
// pair exists but was inactive, so a collision has just started again
collisionStart.push(pair);
pairsList.push(pair);
}
// update the pair
Pair.update(pair, collision, timestamp);
pair.confirmedActive = true;
} else {
// pair did not exist, create a new pair
pair = Pair.create(collision, timestamp);
pairsTable[pair.id] = pair;
// push the new pair
collisionStart.push(pair);
pairsList.push(pair);
}
}
// find pairs that are no longer active
var removePairIndex = [];
pairsListLength = pairsList.length;
for (i = 0; i < pairsListLength; i++) {
pair = pairsList[i];
if (!pair.confirmedActive) {
Pair.setActive(pair, false, timestamp);
collisionEnd.push(pair);
if (!pair.collision.bodyA.isSleeping && !pair.collision.bodyB.isSleeping) {
removePairIndex.push(i);
}
}
}
// deactivate previously active pairs that are now inactive
for (i = 0; i < pairsList.length; i++) {
pair = pairsList[i];
if (pair.isActive && !pair.confirmedActive) {
Pair.setActive(pair, false, timestamp);
collisionEnd.push(pair);
}
}
};
/**
* Finds and removes pairs that have been inactive for a set amount of time.
* @method removeOld
* @param {object} pairs
* @param {number} timestamp
*/
Pairs.removeOld = function(pairs, timestamp) {
var pairsList = pairs.list,
pairsTable = pairs.table,
indexesToRemove = [],
pair,
collision,
pairIndex,
i;
for (i = 0; i < pairsList.length; i++) {
pair = pairsList[i];
collision = pair.collision;
// never remove sleeping pairs
if (collision.bodyA.isSleeping || collision.bodyB.isSleeping) {
pair.timeUpdated = timestamp;
continue;
}
// if pair is inactive for too long, mark it to be removed
if (timestamp - pair.timeUpdated > Pairs._pairMaxIdleLife) {
indexesToRemove.push(i);
}
}
// remove marked pairs
for (i = 0; i < indexesToRemove.length; i++) {
pairIndex = indexesToRemove[i] - i;
// remove inactive pairs
for (i = 0; i < removePairIndex.length; i++) {
pairIndex = removePairIndex[i] - i;
pair = pairsList[pairIndex];
delete pairsTable[pair.id];
pairsList.splice(pairIndex, 1);
delete pairsTable[pair.id];
}
};

View file

@ -11,7 +11,7 @@ var Query = {};
module.exports = Query;
var Vector = require('../geometry/Vector');
var SAT = require('./SAT');
var Collision = require('./Collision');
var Bounds = require('../geometry/Bounds');
var Bodies = require('../factory/Bodies');
var Vertices = require('../geometry/Vertices');
@ -23,22 +23,28 @@ var Vertices = require('../geometry/Vertices');
* @method collides
* @param {body} body
* @param {body[]} bodies
* @return {object[]} Collisions
* @return {collision[]} Collisions
*/
Query.collides = function(body, bodies) {
var collisions = [];
var collisions = [],
bodiesLength = bodies.length,
bounds = body.bounds,
collides = Collision.collides,
overlaps = Bounds.overlaps;
for (var i = 0; i < bodies.length; i++) {
var bodyA = bodies[i];
for (var i = 0; i < bodiesLength; i++) {
var bodyA = bodies[i],
partsALength = bodyA.parts.length,
partsAStart = partsALength === 1 ? 0 : 1;
if (Bounds.overlaps(bodyA.bounds, body.bounds)) {
for (var j = bodyA.parts.length === 1 ? 0 : 1; j < bodyA.parts.length; j++) {
if (overlaps(bodyA.bounds, bounds)) {
for (var j = partsAStart; j < partsALength; j++) {
var part = bodyA.parts[j];
if (Bounds.overlaps(part.bounds, body.bounds)) {
var collision = SAT.collides(part, body);
if (overlaps(part.bounds, bounds)) {
var collision = collides(part, body);
if (collision.collided) {
if (collision) {
collisions.push(collision);
break;
}
@ -57,7 +63,7 @@ var Vertices = require('../geometry/Vertices');
* @param {vector} startPoint
* @param {vector} endPoint
* @param {number} [rayWidth]
* @return {object[]} Collisions
* @return {collision[]} Collisions
*/
Query.ray = function(bodies, startPoint, endPoint, rayWidth) {
rayWidth = rayWidth || 1e-100;

View file

@ -9,8 +9,6 @@ var Resolver = {};
module.exports = Resolver;
var Vertices = require('../geometry/Vertices');
var Vector = require('../geometry/Vector');
var Common = require('../core/Common');
var Bounds = require('../geometry/Bounds');
(function() {
@ -29,10 +27,11 @@ var Bounds = require('../geometry/Bounds');
Resolver.preSolvePosition = function(pairs) {
var i,
pair,
activeCount;
activeCount,
pairsLength = pairs.length;
// find total contacts on each body
for (i = 0; i < pairs.length; i++) {
for (i = 0; i < pairsLength; i++) {
pair = pairs[i];
if (!pair.isActive)
@ -57,17 +56,13 @@ var Bounds = require('../geometry/Bounds');
bodyA,
bodyB,
normal,
bodyBtoA,
contactShare,
positionImpulse,
contactCount = {},
tempA = Vector._temp[0],
tempB = Vector._temp[1],
tempC = Vector._temp[2],
tempD = Vector._temp[3];
positionDampen = Resolver._positionDampen,
pairsLength = pairs.length;
// find impulses required to resolve penetration
for (i = 0; i < pairs.length; i++) {
for (i = 0; i < pairsLength; i++) {
pair = pairs[i];
if (!pair.isActive || pair.isSensor)
@ -79,14 +74,12 @@ var Bounds = require('../geometry/Bounds');
normal = collision.normal;
// get current separation between body edges involved in collision
bodyBtoA = Vector.sub(Vector.add(bodyB.positionImpulse, bodyB.position, tempA),
Vector.add(bodyA.positionImpulse,
Vector.sub(bodyB.position, collision.penetration, tempB), tempC), tempD);
pair.separation = Vector.dot(normal, bodyBtoA);
pair.separation =
normal.x * (bodyB.positionImpulse.x + collision.penetration.x - bodyA.positionImpulse.x)
+ normal.y * (bodyB.positionImpulse.y + collision.penetration.y - bodyA.positionImpulse.y);
}
for (i = 0; i < pairs.length; i++) {
for (i = 0; i < pairsLength; i++) {
pair = pairs[i];
if (!pair.isActive || pair.isSensor)
@ -102,13 +95,13 @@ var Bounds = require('../geometry/Bounds');
positionImpulse *= 2;
if (!(bodyA.isStatic || bodyA.isSleeping)) {
contactShare = Resolver._positionDampen / bodyA.totalContacts;
contactShare = positionDampen / bodyA.totalContacts;
bodyA.positionImpulse.x += normal.x * positionImpulse * contactShare;
bodyA.positionImpulse.y += normal.y * positionImpulse * contactShare;
}
if (!(bodyB.isStatic || bodyB.isSleeping)) {
contactShare = Resolver._positionDampen / bodyB.totalContacts;
contactShare = positionDampen / bodyB.totalContacts;
bodyB.positionImpulse.x -= normal.x * positionImpulse * contactShare;
bodyB.positionImpulse.y -= normal.y * positionImpulse * contactShare;
}
@ -121,34 +114,43 @@ var Bounds = require('../geometry/Bounds');
* @param {body[]} bodies
*/
Resolver.postSolvePosition = function(bodies) {
for (var i = 0; i < bodies.length; i++) {
var body = bodies[i];
var positionWarming = Resolver._positionWarming,
bodiesLength = bodies.length,
verticesTranslate = Vertices.translate,
boundsUpdate = Bounds.update;
for (var i = 0; i < bodiesLength; i++) {
var body = bodies[i],
positionImpulse = body.positionImpulse,
positionImpulseX = positionImpulse.x,
positionImpulseY = positionImpulse.y,
velocity = body.velocity;
// reset contact count
body.totalContacts = 0;
if (body.positionImpulse.x !== 0 || body.positionImpulse.y !== 0) {
if (positionImpulseX !== 0 || positionImpulseY !== 0) {
// update body geometry
for (var j = 0; j < body.parts.length; j++) {
var part = body.parts[j];
Vertices.translate(part.vertices, body.positionImpulse);
Bounds.update(part.bounds, part.vertices, body.velocity);
part.position.x += body.positionImpulse.x;
part.position.y += body.positionImpulse.y;
verticesTranslate(part.vertices, positionImpulse);
boundsUpdate(part.bounds, part.vertices, velocity);
part.position.x += positionImpulseX;
part.position.y += positionImpulseY;
}
// move the body without changing velocity
body.positionPrev.x += body.positionImpulse.x;
body.positionPrev.y += body.positionImpulse.y;
body.positionPrev.x += positionImpulseX;
body.positionPrev.y += positionImpulseY;
if (Vector.dot(body.positionImpulse, body.velocity) < 0) {
if (positionImpulseX * velocity.x + positionImpulseY * velocity.y < 0) {
// reset cached impulse if the body has velocity along it
body.positionImpulse.x = 0;
body.positionImpulse.y = 0;
positionImpulse.x = 0;
positionImpulse.y = 0;
} else {
// warm the next iteration
body.positionImpulse.x *= Resolver._positionWarming;
body.positionImpulse.y *= Resolver._positionWarming;
positionImpulse.x *= positionWarming;
positionImpulse.y *= positionWarming;
}
}
}
@ -160,61 +162,53 @@ var Bounds = require('../geometry/Bounds');
* @param {pair[]} pairs
*/
Resolver.preSolveVelocity = function(pairs) {
var i,
j,
pair,
contacts,
collision,
bodyA,
bodyB,
normal,
tangent,
contact,
contactVertex,
normalImpulse,
tangentImpulse,
offset,
impulse = Vector._temp[0],
tempA = Vector._temp[1];
var pairsLength = pairs.length,
i,
j;
for (i = 0; i < pairs.length; i++) {
pair = pairs[i];
for (i = 0; i < pairsLength; i++) {
var pair = pairs[i];
if (!pair.isActive || pair.isSensor)
continue;
contacts = pair.activeContacts;
collision = pair.collision;
bodyA = collision.parentA;
bodyB = collision.parentB;
normal = collision.normal;
tangent = collision.tangent;
var contacts = pair.activeContacts,
contactsLength = contacts.length,
collision = pair.collision,
bodyA = collision.parentA,
bodyB = collision.parentB,
normal = collision.normal,
tangent = collision.tangent;
// resolve each contact
for (j = 0; j < contacts.length; j++) {
contact = contacts[j];
contactVertex = contact.vertex;
normalImpulse = contact.normalImpulse;
tangentImpulse = contact.tangentImpulse;
for (j = 0; j < contactsLength; j++) {
var contact = contacts[j],
contactVertex = contact.vertex,
normalImpulse = contact.normalImpulse,
tangentImpulse = contact.tangentImpulse;
if (normalImpulse !== 0 || tangentImpulse !== 0) {
// total impulse from contact
impulse.x = (normal.x * normalImpulse) + (tangent.x * tangentImpulse);
impulse.y = (normal.y * normalImpulse) + (tangent.y * tangentImpulse);
var impulseX = normal.x * normalImpulse + tangent.x * tangentImpulse,
impulseY = normal.y * normalImpulse + tangent.y * tangentImpulse;
// apply impulse from contact
if (!(bodyA.isStatic || bodyA.isSleeping)) {
offset = Vector.sub(contactVertex, bodyA.position, tempA);
bodyA.positionPrev.x += impulse.x * bodyA.inverseMass;
bodyA.positionPrev.y += impulse.y * bodyA.inverseMass;
bodyA.anglePrev += Vector.cross(offset, impulse) * bodyA.inverseInertia;
bodyA.positionPrev.x += impulseX * bodyA.inverseMass;
bodyA.positionPrev.y += impulseY * bodyA.inverseMass;
bodyA.anglePrev += bodyA.inverseInertia * (
(contactVertex.x - bodyA.position.x) * impulseY
- (contactVertex.y - bodyA.position.y) * impulseX
);
}
if (!(bodyB.isStatic || bodyB.isSleeping)) {
offset = Vector.sub(contactVertex, bodyB.position, tempA);
bodyB.positionPrev.x -= impulse.x * bodyB.inverseMass;
bodyB.positionPrev.y -= impulse.y * bodyB.inverseMass;
bodyB.anglePrev -= Vector.cross(offset, impulse) * bodyB.inverseInertia;
bodyB.positionPrev.x -= impulseX * bodyB.inverseMass;
bodyB.positionPrev.y -= impulseY * bodyB.inverseMass;
bodyB.anglePrev -= bodyB.inverseInertia * (
(contactVertex.x - bodyB.position.x) * impulseY
- (contactVertex.y - bodyB.position.y) * impulseX
);
}
}
}
@ -229,14 +223,17 @@ var Bounds = require('../geometry/Bounds');
*/
Resolver.solveVelocity = function(pairs, timeScale) {
var timeScaleSquared = timeScale * timeScale,
impulse = Vector._temp[0],
tempA = Vector._temp[1],
tempB = Vector._temp[2],
tempC = Vector._temp[3],
tempD = Vector._temp[4],
tempE = Vector._temp[5];
for (var i = 0; i < pairs.length; i++) {
restingThresh = Resolver._restingThresh * timeScaleSquared,
frictionNormalMultiplier = Resolver._frictionNormalMultiplier,
restingThreshTangent = Resolver._restingThreshTangent * timeScaleSquared,
NumberMaxValue = Number.MAX_VALUE,
pairsLength = pairs.length,
tangentImpulse,
maxFriction,
i,
j;
for (i = 0; i < pairsLength; i++) {
var pair = pairs[i];
if (!pair.isActive || pair.isSensor)
@ -245,97 +242,119 @@ var Bounds = require('../geometry/Bounds');
var collision = pair.collision,
bodyA = collision.parentA,
bodyB = collision.parentB,
normal = collision.normal,
tangent = collision.tangent,
bodyAVelocity = bodyA.velocity,
bodyBVelocity = bodyB.velocity,
normalX = collision.normal.x,
normalY = collision.normal.y,
tangentX = collision.tangent.x,
tangentY = collision.tangent.y,
contacts = pair.activeContacts,
contactShare = 1 / contacts.length;
contactsLength = contacts.length,
contactShare = 1 / contactsLength,
inverseMassTotal = bodyA.inverseMass + bodyB.inverseMass,
friction = pair.friction * pair.frictionStatic * frictionNormalMultiplier * timeScaleSquared;
// update body velocities
bodyA.velocity.x = bodyA.position.x - bodyA.positionPrev.x;
bodyA.velocity.y = bodyA.position.y - bodyA.positionPrev.y;
bodyB.velocity.x = bodyB.position.x - bodyB.positionPrev.x;
bodyB.velocity.y = bodyB.position.y - bodyB.positionPrev.y;
bodyAVelocity.x = bodyA.position.x - bodyA.positionPrev.x;
bodyAVelocity.y = bodyA.position.y - bodyA.positionPrev.y;
bodyBVelocity.x = bodyB.position.x - bodyB.positionPrev.x;
bodyBVelocity.y = bodyB.position.y - bodyB.positionPrev.y;
bodyA.angularVelocity = bodyA.angle - bodyA.anglePrev;
bodyB.angularVelocity = bodyB.angle - bodyB.anglePrev;
// resolve each contact
for (var j = 0; j < contacts.length; j++) {
for (j = 0; j < contactsLength; j++) {
var contact = contacts[j],
contactVertex = contact.vertex,
offsetA = Vector.sub(contactVertex, bodyA.position, tempA),
offsetB = Vector.sub(contactVertex, bodyB.position, tempB),
velocityPointA = Vector.add(bodyA.velocity, Vector.mult(Vector.perp(offsetA), bodyA.angularVelocity), tempC),
velocityPointB = Vector.add(bodyB.velocity, Vector.mult(Vector.perp(offsetB), bodyB.angularVelocity), tempD),
relativeVelocity = Vector.sub(velocityPointA, velocityPointB, tempE),
normalVelocity = Vector.dot(normal, relativeVelocity);
contactVertex = contact.vertex;
var tangentVelocity = Vector.dot(tangent, relativeVelocity),
tangentSpeed = Math.abs(tangentVelocity),
tangentVelocityDirection = Common.sign(tangentVelocity);
var offsetAX = contactVertex.x - bodyA.position.x,
offsetAY = contactVertex.y - bodyA.position.y,
offsetBX = contactVertex.x - bodyB.position.x,
offsetBY = contactVertex.y - bodyB.position.y;
var velocityPointAX = bodyAVelocity.x - offsetAY * bodyA.angularVelocity,
velocityPointAY = bodyAVelocity.y + offsetAX * bodyA.angularVelocity,
velocityPointBX = bodyBVelocity.x - offsetBY * bodyB.angularVelocity,
velocityPointBY = bodyBVelocity.y + offsetBX * bodyB.angularVelocity;
// raw impulses
var normalImpulse = (1 + pair.restitution) * normalVelocity,
normalForce = Common.clamp(pair.separation + normalVelocity, 0, 1) * Resolver._frictionNormalMultiplier;
var relativeVelocityX = velocityPointAX - velocityPointBX,
relativeVelocityY = velocityPointAY - velocityPointBY;
var normalVelocity = normalX * relativeVelocityX + normalY * relativeVelocityY,
tangentVelocity = tangentX * relativeVelocityX + tangentY * relativeVelocityY;
// coulomb friction
var tangentImpulse = tangentVelocity,
maxFriction = Infinity;
var normalOverlap = pair.separation + normalVelocity;
var normalForce = Math.min(normalOverlap, 1);
normalForce = normalOverlap < 0 ? 0 : normalForce;
var frictionLimit = normalForce * friction;
if (tangentSpeed > pair.friction * pair.frictionStatic * normalForce * timeScaleSquared) {
maxFriction = tangentSpeed;
tangentImpulse = Common.clamp(
pair.friction * tangentVelocityDirection * timeScaleSquared,
-maxFriction, maxFriction
);
if (tangentVelocity > frictionLimit || -tangentVelocity > frictionLimit) {
maxFriction = tangentVelocity > 0 ? tangentVelocity : -tangentVelocity;
tangentImpulse = pair.friction * (tangentVelocity > 0 ? 1 : -1) * timeScaleSquared;
if (tangentImpulse < -maxFriction) {
tangentImpulse = -maxFriction;
} else if (tangentImpulse > maxFriction) {
tangentImpulse = maxFriction;
}
} else {
tangentImpulse = tangentVelocity;
maxFriction = NumberMaxValue;
}
// modify impulses accounting for mass, inertia and offset
var oAcN = Vector.cross(offsetA, normal),
oBcN = Vector.cross(offsetB, normal),
share = contactShare / (bodyA.inverseMass + bodyB.inverseMass + bodyA.inverseInertia * oAcN * oAcN + bodyB.inverseInertia * oBcN * oBcN);
// account for mass, inertia and contact offset
var oAcN = offsetAX * normalY - offsetAY * normalX,
oBcN = offsetBX * normalY - offsetBY * normalX,
share = contactShare / (inverseMassTotal + bodyA.inverseInertia * oAcN * oAcN + bodyB.inverseInertia * oBcN * oBcN);
normalImpulse *= share;
// raw impulses
var normalImpulse = (1 + pair.restitution) * normalVelocity * share;
tangentImpulse *= share;
// handle high velocity and resting collisions separately
if (normalVelocity < 0 && normalVelocity * normalVelocity > Resolver._restingThresh * timeScaleSquared) {
if (normalVelocity * normalVelocity > restingThresh && normalVelocity < 0) {
// high normal velocity so clear cached contact normal impulse
contact.normalImpulse = 0;
} else {
// solve resting collision constraints using Erin Catto's method (GDC08)
// impulse constraint tends to 0
var contactNormalImpulse = contact.normalImpulse;
contact.normalImpulse = Math.min(contact.normalImpulse + normalImpulse, 0);
contact.normalImpulse += normalImpulse;
contact.normalImpulse = Math.min(contact.normalImpulse, 0);
normalImpulse = contact.normalImpulse - contactNormalImpulse;
}
// handle high velocity and resting collisions separately
if (tangentVelocity * tangentVelocity > Resolver._restingThreshTangent * timeScaleSquared) {
if (tangentVelocity * tangentVelocity > restingThreshTangent) {
// high tangent velocity so clear cached contact tangent impulse
contact.tangentImpulse = 0;
} else {
// solve resting collision constraints using Erin Catto's method (GDC08)
// tangent impulse tends to -tangentSpeed or +tangentSpeed
var contactTangentImpulse = contact.tangentImpulse;
contact.tangentImpulse = Common.clamp(contact.tangentImpulse + tangentImpulse, -maxFriction, maxFriction);
contact.tangentImpulse += tangentImpulse;
if (contact.tangentImpulse < -maxFriction) contact.tangentImpulse = -maxFriction;
if (contact.tangentImpulse > maxFriction) contact.tangentImpulse = maxFriction;
tangentImpulse = contact.tangentImpulse - contactTangentImpulse;
}
// total impulse from contact
impulse.x = (normal.x * normalImpulse) + (tangent.x * tangentImpulse);
impulse.y = (normal.y * normalImpulse) + (tangent.y * tangentImpulse);
var impulseX = normalX * normalImpulse + tangentX * tangentImpulse,
impulseY = normalY * normalImpulse + tangentY * tangentImpulse;
// apply impulse from contact
if (!(bodyA.isStatic || bodyA.isSleeping)) {
bodyA.positionPrev.x += impulse.x * bodyA.inverseMass;
bodyA.positionPrev.y += impulse.y * bodyA.inverseMass;
bodyA.anglePrev += Vector.cross(offsetA, impulse) * bodyA.inverseInertia;
bodyA.positionPrev.x += impulseX * bodyA.inverseMass;
bodyA.positionPrev.y += impulseY * bodyA.inverseMass;
bodyA.anglePrev += (offsetAX * impulseY - offsetAY * impulseX) * bodyA.inverseInertia;
}
if (!(bodyB.isStatic || bodyB.isSleeping)) {
bodyB.positionPrev.x -= impulse.x * bodyB.inverseMass;
bodyB.positionPrev.y -= impulse.y * bodyB.inverseMass;
bodyB.anglePrev -= Vector.cross(offsetB, impulse) * bodyB.inverseInertia;
bodyB.positionPrev.x -= impulseX * bodyB.inverseMass;
bodyB.positionPrev.y -= impulseY * bodyB.inverseMass;
bodyB.anglePrev -= (offsetBX * impulseY - offsetBY * impulseX) * bodyB.inverseInertia;
}
}
}

View file

@ -1,270 +1,37 @@
/**
* This module has now been replaced by `Matter.Collision`.
*
* All usage should be migrated to `Matter.Collision`.
* For back-compatibility purposes this module will remain for a short term and then later removed in a future release.
*
* The `Matter.SAT` module contains methods for detecting collisions using the Separating Axis Theorem.
*
* @class SAT
* @deprecated
*/
// TODO: true circles and curves
var SAT = {};
module.exports = SAT;
var Vertices = require('../geometry/Vertices');
var Vector = require('../geometry/Vector');
var Collision = require('./Collision');
var Common = require('../core/Common');
var deprecated = Common.deprecated;
(function() {
/**
* Detect collision between two bodies using the Separating Axis Theorem.
* @deprecated replaced by Collision.collides
* @method collides
* @param {body} bodyA
* @param {body} bodyB
* @param {collision} previousCollision
* @return {collision} collision
*/
SAT.collides = function(bodyA, bodyB, previousCollision) {
var overlapAB,
overlapBA,
minOverlap,
collision,
canReusePrevCol = false;
if (previousCollision) {
// estimate total motion
var parentA = bodyA.parent,
parentB = bodyB.parent,
motion = parentA.speed * parentA.speed + parentA.angularSpeed * parentA.angularSpeed
+ parentB.speed * parentB.speed + parentB.angularSpeed * parentB.angularSpeed;
// we may be able to (partially) reuse collision result
// but only safe if collision was resting
canReusePrevCol = previousCollision && previousCollision.collided && motion < 0.2;
// reuse collision object
collision = previousCollision;
} else {
collision = { collided: false, bodyA: bodyA, bodyB: bodyB };
}
if (previousCollision && canReusePrevCol) {
// if we can reuse the collision result
// we only need to test the previously found axis
var axisBodyA = collision.axisBody,
axisBodyB = axisBodyA === bodyA ? bodyB : bodyA,
axes = [axisBodyA.axes[previousCollision.axisNumber]];
minOverlap = SAT._overlapAxes(axisBodyA.vertices, axisBodyB.vertices, axes);
collision.reused = true;
if (minOverlap.overlap <= 0) {
collision.collided = false;
return collision;
}
} else {
// if we can't reuse a result, perform a full SAT test
overlapAB = SAT._overlapAxes(bodyA.vertices, bodyB.vertices, bodyA.axes);
if (overlapAB.overlap <= 0) {
collision.collided = false;
return collision;
}
overlapBA = SAT._overlapAxes(bodyB.vertices, bodyA.vertices, bodyB.axes);
if (overlapBA.overlap <= 0) {
collision.collided = false;
return collision;
}
if (overlapAB.overlap < overlapBA.overlap) {
minOverlap = overlapAB;
collision.axisBody = bodyA;
} else {
minOverlap = overlapBA;
collision.axisBody = bodyB;
}
// important for reuse later
collision.axisNumber = minOverlap.axisNumber;
}
collision.bodyA = bodyA.id < bodyB.id ? bodyA : bodyB;
collision.bodyB = bodyA.id < bodyB.id ? bodyB : bodyA;
collision.collided = true;
collision.depth = minOverlap.overlap;
collision.parentA = collision.bodyA.parent;
collision.parentB = collision.bodyB.parent;
bodyA = collision.bodyA;
bodyB = collision.bodyB;
// ensure normal is facing away from bodyA
if (Vector.dot(minOverlap.axis, Vector.sub(bodyB.position, bodyA.position)) < 0) {
collision.normal = {
x: minOverlap.axis.x,
y: minOverlap.axis.y
};
} else {
collision.normal = {
x: -minOverlap.axis.x,
y: -minOverlap.axis.y
};
}
collision.tangent = Vector.perp(collision.normal);
collision.penetration = collision.penetration || {};
collision.penetration.x = collision.normal.x * collision.depth;
collision.penetration.y = collision.normal.y * collision.depth;
// find support points, there is always either exactly one or two
var verticesB = SAT._findSupports(bodyA, bodyB, collision.normal),
supports = [];
// find the supports from bodyB that are inside bodyA
if (Vertices.contains(bodyA.vertices, verticesB[0]))
supports.push(verticesB[0]);
if (Vertices.contains(bodyA.vertices, verticesB[1]))
supports.push(verticesB[1]);
// find the supports from bodyA that are inside bodyB
if (supports.length < 2) {
var verticesA = SAT._findSupports(bodyB, bodyA, Vector.neg(collision.normal));
if (Vertices.contains(bodyB.vertices, verticesA[0]))
supports.push(verticesA[0]);
if (supports.length < 2 && Vertices.contains(bodyB.vertices, verticesA[1]))
supports.push(verticesA[1]);
}
// account for the edge case of overlapping but no vertex containment
if (supports.length < 1)
supports = [verticesB[0]];
collision.supports = supports;
return collision;
SAT.collides = function(bodyA, bodyB) {
return Collision.collides(bodyA, bodyB);
};
/**
* Find the overlap between two sets of vertices.
* @method _overlapAxes
* @private
* @param {} verticesA
* @param {} verticesB
* @param {} axes
* @return result
*/
SAT._overlapAxes = function(verticesA, verticesB, axes) {
var projectionA = Vector._temp[0],
projectionB = Vector._temp[1],
result = { overlap: Number.MAX_VALUE },
overlap,
axis;
for (var i = 0; i < axes.length; i++) {
axis = axes[i];
SAT._projectToAxis(projectionA, verticesA, axis);
SAT._projectToAxis(projectionB, verticesB, axis);
overlap = Math.min(projectionA.max - projectionB.min, projectionB.max - projectionA.min);
if (overlap <= 0) {
result.overlap = overlap;
return result;
}
if (overlap < result.overlap) {
result.overlap = overlap;
result.axis = axis;
result.axisNumber = i;
}
}
return result;
};
/**
* Projects vertices on an axis and returns an interval.
* @method _projectToAxis
* @private
* @param {} projection
* @param {} vertices
* @param {} axis
*/
SAT._projectToAxis = function(projection, vertices, axis) {
var min = Vector.dot(vertices[0], axis),
max = min;
for (var i = 1; i < vertices.length; i += 1) {
var dot = Vector.dot(vertices[i], axis);
if (dot > max) {
max = dot;
} else if (dot < min) {
min = dot;
}
}
projection.min = min;
projection.max = max;
};
/**
* Finds supporting vertices given two bodies along a given direction using hill-climbing.
* @method _findSupports
* @private
* @param {} bodyA
* @param {} bodyB
* @param {} normal
* @return [vector]
*/
SAT._findSupports = function(bodyA, bodyB, normal) {
var nearestDistance = Number.MAX_VALUE,
vertexToBody = Vector._temp[0],
vertices = bodyB.vertices,
bodyAPosition = bodyA.position,
distance,
vertex,
vertexA,
vertexB;
// find closest vertex on bodyB
for (var i = 0; i < vertices.length; i++) {
vertex = vertices[i];
vertexToBody.x = vertex.x - bodyAPosition.x;
vertexToBody.y = vertex.y - bodyAPosition.y;
distance = -Vector.dot(normal, vertexToBody);
if (distance < nearestDistance) {
nearestDistance = distance;
vertexA = vertex;
}
}
// find next closest vertex using the two connected to it
var prevIndex = vertexA.index - 1 >= 0 ? vertexA.index - 1 : vertices.length - 1;
vertex = vertices[prevIndex];
vertexToBody.x = vertex.x - bodyAPosition.x;
vertexToBody.y = vertex.y - bodyAPosition.y;
nearestDistance = -Vector.dot(normal, vertexToBody);
vertexB = vertex;
var nextIndex = (vertexA.index + 1) % vertices.length;
vertex = vertices[nextIndex];
vertexToBody.x = vertex.x - bodyAPosition.x;
vertexToBody.y = vertex.y - bodyAPosition.y;
distance = -Vector.dot(normal, vertexToBody);
if (distance < nearestDistance) {
vertexB = vertex;
}
return [vertexA, vertexB];
};
deprecated(SAT, 'collides', 'SAT.collides ➤ replaced by Collision.collides');
})();

View file

@ -308,8 +308,10 @@ var Common = require('../core/Common');
*/
Constraint.pointAWorld = function(constraint) {
return {
x: (constraint.bodyA ? constraint.bodyA.position.x : 0) + constraint.pointA.x,
y: (constraint.bodyA ? constraint.bodyA.position.y : 0) + constraint.pointA.y
x: (constraint.bodyA ? constraint.bodyA.position.x : 0)
+ (constraint.pointA ? constraint.pointA.x : 0),
y: (constraint.bodyA ? constraint.bodyA.position.y : 0)
+ (constraint.pointA ? constraint.pointA.y : 0)
};
};
@ -321,8 +323,10 @@ var Common = require('../core/Common');
*/
Constraint.pointBWorld = function(constraint) {
return {
x: (constraint.bodyB ? constraint.bodyB.position.x : 0) + constraint.pointB.x,
y: (constraint.bodyB ? constraint.bodyB.position.y : 0) + constraint.pointB.y
x: (constraint.bodyB ? constraint.bodyB.position.x : 0)
+ (constraint.pointB ? constraint.pointB.x : 0),
y: (constraint.bodyB ? constraint.bodyB.position.y : 0)
+ (constraint.pointB ? constraint.pointB.y : 0)
};
};

View file

@ -16,7 +16,6 @@ var Sleeping = require('./Sleeping');
var Resolver = require('../collision/Resolver');
var Detector = require('../collision/Detector');
var Pairs = require('../collision/Pairs');
var Grid = require('../collision/Grid');
var Events = require('./Events');
var Composite = require('../body/Composite');
var Constraint = require('../constraint/Constraint');
@ -43,7 +42,6 @@ var Body = require('../body/Body');
enableSleeping: false,
events: [],
plugin: {},
grid: null,
gravity: {
x: 0,
y: 1,
@ -60,10 +58,11 @@ var Body = require('../body/Body');
var engine = Common.extend(defaults, options);
engine.world = options.world || Composite.create({ label: 'World' });
engine.grid = Grid.create(options.grid || options.broadphase);
engine.pairs = Pairs.create();
engine.pairs = options.pairs || Pairs.create();
engine.detector = options.detector || Detector.create();
// temporary back compatibility
// for temporary back compatibility only
engine.grid = { buckets: [] };
engine.world.gravity = engine.gravity;
engine.broadphase = engine.grid;
engine.metrics = {};
@ -93,9 +92,10 @@ var Body = require('../body/Body');
correction = correction || 1;
var world = engine.world,
detector = engine.detector,
pairs = engine.pairs,
timing = engine.timing,
grid = engine.grid,
gridPairs = [],
timestamp = timing.timestamp,
i;
// increment timestamp
@ -109,15 +109,25 @@ var Body = require('../body/Body');
Events.trigger(engine, 'beforeUpdate', event);
// get lists of all bodies and constraints, no matter what composites they are in
// get all bodies and all constraints in the world
var allBodies = Composite.allBodies(world),
allConstraints = Composite.allConstraints(world);
// if sleeping enabled, call the sleeping controller
// update the detector bodies if they have changed
if (world.isModified) {
Detector.setBodies(detector, allBodies);
}
// reset all composite modified flags
if (world.isModified) {
Composite.setModified(world, false, false, true);
}
// update sleeping if enabled
if (engine.enableSleeping)
Sleeping.update(allBodies, timing.timeScale);
// applies gravity to all bodies
// apply gravity to all bodies
Engine._bodiesApplyGravity(allBodies, engine.gravity);
// update all body position and rotation by integration
@ -130,29 +140,12 @@ var Body = require('../body/Body');
}
Constraint.postSolveAll(allBodies);
// broadphase pass: find potential collision pairs
// if world is dirty, we must flush the whole grid
if (world.isModified)
Grid.clear(grid);
// update the grid buckets based on current bodies
Grid.update(grid, allBodies, engine, world.isModified);
gridPairs = grid.pairsList;
// clear all composite modified flags
if (world.isModified) {
Composite.setModified(world, false, false, true);
}
// narrowphase pass: find actual collisions, then create or update collision pairs
var collisions = Detector.collisions(gridPairs, engine);
// find all collisions
detector.pairs = engine.pairs;
var collisions = Detector.collisions(detector);
// update collision pairs
var pairs = engine.pairs,
timestamp = timing.timestamp;
Pairs.update(pairs, collisions, timestamp);
Pairs.removeOld(pairs, timestamp);
// wake up bodies involved in collisions
if (engine.enableSleeping)
@ -225,17 +218,13 @@ var Body = require('../body/Body');
};
/**
* Clears the engine including the world, pairs and broadphase.
* Clears the engine pairs and detector.
* @method clear
* @param {engine} engine
*/
Engine.clear = function(engine) {
var world = engine.world,
bodies = Composite.allBodies(world);
Pairs.clear(engine.pairs);
Grid.clear(engine.grid);
Grid.update(engine.grid, bodies, engine, true);
Detector.clear(engine.detector);
};
/**
@ -315,53 +304,53 @@ var Body = require('../body/Body');
* Fired just before an update
*
* @event beforeUpdate
* @param {} event An event object
* @param {object} event An event object
* @param {number} event.timestamp The engine.timing.timestamp of the event
* @param {} event.source The source object of the event
* @param {} event.name The name of the event
* @param {engine} event.source The source object of the event
* @param {string} event.name The name of the event
*/
/**
* Fired after engine update and all collision events
*
* @event afterUpdate
* @param {} event An event object
* @param {object} event An event object
* @param {number} event.timestamp The engine.timing.timestamp of the event
* @param {} event.source The source object of the event
* @param {} event.name The name of the event
* @param {engine} event.source The source object of the event
* @param {string} event.name The name of the event
*/
/**
* Fired after engine update, provides a list of all pairs that have started to collide in the current tick (if any)
*
* @event collisionStart
* @param {} event An event object
* @param {} event.pairs List of affected pairs
* @param {object} event An event object
* @param {pair[]} event.pairs List of affected pairs
* @param {number} event.timestamp The engine.timing.timestamp of the event
* @param {} event.source The source object of the event
* @param {} event.name The name of the event
* @param {engine} event.source The source object of the event
* @param {string} event.name The name of the event
*/
/**
* Fired after engine update, provides a list of all pairs that are colliding in the current tick (if any)
*
* @event collisionActive
* @param {} event An event object
* @param {} event.pairs List of affected pairs
* @param {object} event An event object
* @param {pair[]} event.pairs List of affected pairs
* @param {number} event.timestamp The engine.timing.timestamp of the event
* @param {} event.source The source object of the event
* @param {} event.name The name of the event
* @param {engine} event.source The source object of the event
* @param {string} event.name The name of the event
*/
/**
* Fired after engine update, provides a list of all pairs that have ended collision in the current tick (if any)
*
* @event collisionEnd
* @param {} event An event object
* @param {} event.pairs List of affected pairs
* @param {object} event An event object
* @param {pair[]} event.pairs List of affected pairs
* @param {number} event.timestamp The engine.timing.timestamp of the event
* @param {} event.source The source object of the event
* @param {} event.name The name of the event
* @param {engine} event.source The source object of the event
* @param {string} event.name The name of the event
*/
/*
@ -453,9 +442,18 @@ var Body = require('../body/Body');
* @default 0
*/
/**
* A `Matter.Detector` instance.
*
* @property detector
* @type detector
* @default a Matter.Detector instance
*/
/**
* A `Matter.Grid` instance.
*
* @deprecated replaced by `engine.detector`
* @property grid
* @type grid
* @default a Matter.Grid instance
@ -464,7 +462,7 @@ var Body = require('../body/Body');
/**
* Replaced by and now alias for `engine.grid`.
*
* @deprecated use `engine.grid`
* @deprecated replaced by `engine.detector`
* @property broadphase
* @type grid
* @default a Matter.Grid instance

View file

@ -240,7 +240,7 @@ var Common = require('./Common');
*/
Plugin.dependencyParse = function(dependency) {
if (Common.isString(dependency)) {
var pattern = /^[\w-]+(@(\*|[\^~]?\d+\.\d+\.\d+(-[0-9A-Za-z-]+)?))?$/;
var pattern = /^[\w-]+(@(\*|[\^~]?\d+\.\d+\.\d+(-[0-9A-Za-z-+]+)?))?$/;
if (!pattern.test(dependency)) {
Common.warn('Plugin.dependencyParse:', dependency, 'is not a valid dependency string.');
@ -275,7 +275,7 @@ var Common = require('./Common');
* @return {object} The version range parsed into its components.
*/
Plugin.versionParse = function(range) {
var pattern = /^(\*)|(\^|~|>=|>)?\s*((\d+)\.(\d+)\.(\d+))(-[0-9A-Za-z-]+)?$/;
var pattern = /^(\*)|(\^|~|>=|>)?\s*((\d+)\.(\d+)\.(\d+))(-[0-9A-Za-z-+]+)?$/;
if (!pattern.test(range)) {
Common.warn('Plugin.versionParse:', range, 'is not a valid version or range.');

View file

@ -169,17 +169,16 @@ var Common = require('../core/Common');
* @param {number} scalar
*/
Vertices.translate = function(vertices, vector, scalar) {
var i;
if (scalar) {
for (i = 0; i < vertices.length; i++) {
vertices[i].x += vector.x * scalar;
vertices[i].y += vector.y * scalar;
}
} else {
for (i = 0; i < vertices.length; i++) {
vertices[i].x += vector.x;
vertices[i].y += vector.y;
}
scalar = typeof scalar !== 'undefined' ? scalar : 1;
var verticesLength = vertices.length,
translateX = vector.x * scalar,
translateY = vector.y * scalar,
i;
for (i = 0; i < verticesLength; i++) {
vertices[i].x += translateX;
vertices[i].y += translateY;
}
return vertices;
@ -197,15 +196,21 @@ var Common = require('../core/Common');
return;
var cos = Math.cos(angle),
sin = Math.sin(angle);
sin = Math.sin(angle),
pointX = point.x,
pointY = point.y,
verticesLength = vertices.length,
vertex,
dx,
dy,
i;
for (var i = 0; i < vertices.length; i++) {
var vertice = vertices[i],
dx = vertice.x - point.x,
dy = vertice.y - point.y;
vertice.x = point.x + (dx * cos - dy * sin);
vertice.y = point.y + (dx * sin + dy * cos);
for (i = 0; i < verticesLength; i++) {
vertex = vertices[i];
dx = vertex.x - pointX;
dy = vertex.y - pointY;
vertex.x = pointX + (dx * cos - dy * sin);
vertex.y = pointY + (dx * sin + dy * cos);
}
return vertices;
@ -219,12 +224,21 @@ var Common = require('../core/Common');
* @return {boolean} True if the vertices contains point, otherwise false
*/
Vertices.contains = function(vertices, point) {
for (var i = 0; i < vertices.length; i++) {
var vertice = vertices[i],
nextVertice = vertices[(i + 1) % vertices.length];
if ((point.x - vertice.x) * (nextVertice.y - vertice.y) + (point.y - vertice.y) * (vertice.x - nextVertice.x) > 0) {
var pointX = point.x,
pointY = point.y,
verticesLength = vertices.length,
vertex = vertices[verticesLength - 1],
nextVertex;
for (var i = 0; i < verticesLength; i++) {
nextVertex = vertices[i];
if ((pointX - vertex.x) * (nextVertex.y - vertex.y)
+ (pointY - vertex.y) * (vertex.x - nextVertex.x) > 0) {
return false;
}
vertex = nextVertex;
}
return true;

View file

@ -4,6 +4,7 @@ Matter.Axes = require('../geometry/Axes');
Matter.Bodies = require('../factory/Bodies');
Matter.Body = require('../body/Body');
Matter.Bounds = require('../geometry/Bounds');
Matter.Collision = require('../collision/Collision');
Matter.Common = require('../core/Common');
Matter.Composite = require('../body/Composite');
Matter.Composites = require('../factory/Composites');

View file

@ -44,7 +44,6 @@ var Mouse = require('../core/Mouse');
*/
Render.create = function(options) {
var defaults = {
controller: Render,
engine: null,
element: null,
canvas: null,
@ -76,7 +75,6 @@ var Mouse = require('../core/Mouse');
showDebug: false,
showStats: false,
showPerformance: false,
showBroadphase: false,
showBounds: false,
showVelocity: false,
showCollisions: false,
@ -116,14 +114,16 @@ var Mouse = require('../core/Mouse');
}
};
// for temporary back compatibility only
render.controller = Render;
render.options.showBroadphase = false;
if (render.options.pixelRatio !== 1) {
Render.setPixelRatio(render, render.options.pixelRatio);
}
if (Common.isElement(render.element)) {
render.element.appendChild(render.canvas);
} else if (!render.canvas.parentNode) {
Common.log('Render.create: options.element was undefined, render.canvas was created but not appended', 'warn');
}
return render;
@ -438,9 +438,6 @@ var Mouse = require('../core/Mouse');
Render.constraints(constraints, context);
if (options.showBroadphase)
Render.grid(render, engine.grid, context);
if (options.hasBounds) {
// revert view transforms
Render.endViewTransform(render);
@ -1287,45 +1284,6 @@ var Mouse = require('../core/Mouse');
c.stroke();
};
/**
* Description
* @private
* @method grid
* @param {render} render
* @param {grid} grid
* @param {RenderingContext} context
*/
Render.grid = function(render, grid, context) {
var c = context,
options = render.options;
if (options.wireframes) {
c.strokeStyle = 'rgba(255,180,0,0.1)';
} else {
c.strokeStyle = 'rgba(255,180,0,0.5)';
}
c.beginPath();
var bucketKeys = Common.keys(grid.buckets);
for (var i = 0; i < bucketKeys.length; i++) {
var bucketId = bucketKeys[i];
if (grid.buckets[bucketId].length < 2)
continue;
var region = bucketId.split(/C|R/);
c.rect(0.5 + parseInt(region[1], 10) * grid.bucketWidth,
0.5 + parseInt(region[2], 10) * grid.bucketHeight,
grid.bucketWidth,
grid.bucketHeight);
}
c.lineWidth = 1;
c.stroke();
};
/**
* Description
* @private
@ -1577,6 +1535,7 @@ var Mouse = require('../core/Mouse');
/**
* A back-reference to the `Matter.Render` module.
*
* @deprecated
* @property controller
* @type render
*/
@ -1777,6 +1736,7 @@ var Mouse = require('../core/Mouse');
/**
* A flag to enable or disable the collision broadphase debug overlay.
*
* @deprecated no longer implemented
* @property options.showBroadphase
* @type boolean
* @default false

View file

@ -2,76 +2,56 @@
/* eslint no-global-assign: 0 */
"use strict";
const stubBrowserFeatures = M => {
const noop = () => ({ collisionFilter: {}, mouse: {} });
M.Render.create = () => ({ options: {}, bounds: { min: { x: 0, y: 0 }, max: { x: 800, y: 600 }}});
M.Render.run = M.Render.lookAt = noop;
M.Runner.create = M.Runner.run = noop;
M.MouseConstraint.create = M.Mouse.create = noop;
M.Common.info = M.Common.warn = M.Common.log;
return M;
};
const reset = M => {
M.Common._nextId = M.Common._seed = 0;
M.Body._nextCollidingGroupId = 1;
M.Body._nextNonCollidingGroupId = -1;
M.Body._nextCategory = 0x0001;
};
const mock = require('mock-require');
const { engineCapture } = require('./TestTools');
const MatterDev = stubBrowserFeatures(require('../src/module/main'));
const MatterBuild = stubBrowserFeatures(require('../build/matter'));
const Example = require('../examples/index');
const { requireUncached, serialize } = require('./TestTools');
const consoleOriginal = global.console;
const runExample = options => {
const Matter = options.useDev ? MatterDev : MatterBuild;
const consoleOriginal = global.console;
const logs = [];
const {
Matter,
logs,
frameCallbacks
} = prepareEnvironment(options);
mock('matter-js', Matter);
global.Matter = Matter;
const Examples = requireUncached('../examples/index');
const example = Examples[options.name]();
global.document = global.window = { addEventListener: () => {} };
global.console = {
log: (...args) => {
logs.push(args.join(' '));
}
};
reset(Matter);
const example = Example[options.name]();
const engine = example.engine;
const runner = example.runner;
const render = example.render;
let totalMemory = 0;
let totalDuration = 0;
let overlapTotal = 0;
let overlapCount = 0;
let i;
const bodies = Matter.Composite.allBodies(engine.world);
if (options.jitter) {
for (let i = 0; i < bodies.length; i += 1) {
const body = bodies[i];
Matter.Body.applyForce(body, body.position, {
x: Math.cos(i * i) * options.jitter * body.mass,
y: Math.sin(i * i) * options.jitter * body.mass
});
}
if (global.gc) {
global.gc();
}
for (let i = 0; i < options.totalUpdates; i += 1) {
const startTime = process.hrtime();
try {
for (i = 0; i < options.updates; i += 1) {
const time = i * runner.delta;
const callbackCount = frameCallbacks.length;
Matter.Engine.update(engine, 1000 / 60);
for (let p = 0; p < callbackCount; p += 1) {
totalMemory += process.memoryUsage().heapUsed;
const callback = frameCallbacks.shift();
const startTime = process.hrtime();
const duration = process.hrtime(startTime);
totalDuration += duration[0] * 1e9 + duration[1];
callback(time);
for (let p = 0; p < engine.pairs.list.length; p += 1) {
const pair = engine.pairs.list[p];
const duration = process.hrtime(startTime);
totalMemory += process.memoryUsage().heapUsed;
totalDuration += duration[0] * 1e9 + duration[1];
}
const pairsList = engine.pairs.list;
const pairsListLength = engine.pairs.list.length;
for (let p = 0; p < pairsListLength; p += 1) {
const pair = pairsList[p];
const separation = pair.separation - pair.slop;
if (pair.isActive && !pair.isSensor) {
@ -79,21 +59,253 @@ const runExample = options => {
overlapCount += 1;
}
}
}
resetEnvironment();
return {
name: options.name,
duration: totalDuration,
overlap: overlapTotal / (overlapCount || 1),
memory: totalMemory,
logs: logs,
extrinsic: captureExtrinsics(engine, Matter),
intrinsic: captureIntrinsics(engine, Matter),
state: captureState(engine, runner, render)
};
} catch (err) {
err.message = `On example '${options.name}' update ${i}:\n\n ${err.message}`;
throw err;
}
};
const prepareMatter = (options) => {
const Matter = requireUncached(options.useDev ? '../build/matter.dev' : '../build/matter');
if (Matter.Common._nextId !== 0) {
throw 'Matter instance has already been used.';
}
Matter.Common.info = Matter.Common.warn = Matter.Common.log;
if (options.stableSort) {
if (Matter.Collision) {
const MatterCollisionCollides = Matter.Collision.collides;
Matter.Collision.collides = function(bodyA, bodyB, pairs) {
const _bodyA = bodyA.id < bodyB.id ? bodyA : bodyB;
const _bodyB = bodyA.id < bodyB.id ? bodyB : bodyA;
return MatterCollisionCollides(_bodyA, _bodyB, pairs);
};
} else {
const MatterSATCollides = Matter.SAT.collides;
Matter.SAT.collides = function(bodyA, bodyB, previousCollision, pairActive) {
const _bodyA = bodyA.id < bodyB.id ? bodyA : bodyB;
const _bodyB = bodyA.id < bodyB.id ? bodyB : bodyA;
return MatterSATCollides(_bodyA, _bodyB, previousCollision, pairActive);
};
}
Matter.after('Detector.collisions', function() { this.sort(collisionCompareId); });
Matter.after('Composite.allBodies', function() { sortById(this); });
Matter.after('Composite.allConstraints', function() { sortById(this); });
Matter.after('Composite.allComposites', function() { sortById(this); });
Matter.before('Pairs.update', function(pairs) {
pairs.list.sort((pairA, pairB) => collisionCompareId(pairA.collision, pairB.collision));
});
Matter.after('Pairs.update', function(pairs) {
pairs.list.sort((pairA, pairB) => collisionCompareId(pairA.collision, pairB.collision));
});
}
if (options.jitter) {
Matter.after('Body.create', function() {
Matter.Body.applyForce(this, this.position, {
x: Math.cos(this.id * this.id) * options.jitter * this.mass,
y: Math.sin(this.id * this.id) * options.jitter * this.mass
});
});
}
return Matter;
};
const prepareEnvironment = options => {
const logs = [];
const frameCallbacks = [];
global.document = global.window = {
addEventListener: () => {},
requestAnimationFrame: callback => {
frameCallbacks.push(callback);
return frameCallbacks.length;
},
createElement: () => ({
parentNode: {},
width: 800,
height: 600,
style: {},
addEventListener: () => {},
getAttribute: name => ({
'data-pixel-ratio': '1'
}[name]),
getContext: () => new Proxy({}, {
get() { return () => {}; }
})
})
};
global.document.body = global.document.createElement();
global.Image = function Image() { };
global.console = {
log: (...args) => {
logs.push(args.join(' '));
}
};
const Matter = prepareMatter(options);
mock('matter-js', Matter);
global.Matter = Matter;
return {
Matter,
logs,
frameCallbacks
};
};
const resetEnvironment = () => {
global.console = consoleOriginal;
global.window = undefined;
global.document = undefined;
global.Matter = undefined;
mock.stopAll();
return {
name: options.name,
duration: totalDuration,
overlap: overlapTotal / (overlapCount || 1),
logs,
...engineCapture(engine)
};
};
module.exports = { runExample };
const captureExtrinsics = ({ world }, Matter) => ({
bodies: Matter.Composite.allBodies(world).reduce((bodies, body) => {
bodies[body.id] = [
body.position.x,
body.position.y,
body.positionPrev.x,
body.positionPrev.y,
body.angle,
body.anglePrev,
...body.vertices.reduce((flat, vertex) => (flat.push(vertex.x, vertex.y), flat), [])
];
return bodies;
}, {}),
constraints: Matter.Composite.allConstraints(world).reduce((constraints, constraint) => {
let positionA;
let positionB;
try {
positionA = Matter.Constraint.pointAWorld(constraint);
} catch (err) {
positionA = { x: 0, y: 0 };
}
try {
positionB = Matter.Constraint.pointBWorld(constraint);
} catch (err) {
positionB = { x: 0, y: 0 };
}
constraints[constraint.id] = [
positionA.x,
positionA.y,
positionB.x,
positionB.y
];
return constraints;
}, {})
});
const captureIntrinsics = ({ world }, Matter) => serialize({
bodies: Matter.Composite.allBodies(world).reduce((bodies, body) => {
bodies[body.id] = body;
return bodies;
}, {}),
constraints: Matter.Composite.allConstraints(world).reduce((constraints, constraint) => {
constraints[constraint.id] = constraint;
return constraints;
}, {}),
composites: Matter.Composite.allComposites(world).reduce((composites, composite) => {
composites[composite.id] = {
bodies: Matter.Composite.allBodies(composite).map(body => body.id),
constraints: Matter.Composite.allConstraints(composite).map(constraint => constraint.id),
composites: Matter.Composite.allComposites(composite).map(composite => composite.id)
};
return composites;
}, {})
}, (key) => !Number.isInteger(parseInt(key)) && !intrinsicProperties.includes(key));
const captureState = (engine, runner, render, excludeKeys=excludeStateProperties) => (
serialize({ engine, runner, render }, (key) => excludeKeys.includes(key))
);
const intrinsicProperties = [
// Composite
'bodies', 'constraints', 'composites',
// Common
'id', 'label',
// Constraint
'angularStiffness', 'bodyA', 'bodyB', 'damping', 'length', 'stiffness',
// Body
'area', 'collisionFilter', 'category', 'mask', 'group', 'density', 'friction',
'frictionAir', 'frictionStatic', 'inertia', 'inverseInertia', 'inverseMass',
'isSensor', 'isSleeping', 'isStatic', 'mass', 'parent', 'parts', 'restitution',
'sleepThreshold', 'slop', 'timeScale',
// Composite
'bodies', 'constraints', 'composites'
];
const extrinsicProperties = [
'axes',
'vertices',
'bounds',
'angle',
'anglePrev',
'angularVelocity',
'angularSpeed',
'speed',
'velocity',
'position',
'positionPrev',
];
const excludeStateProperties = [
'cache',
'grid',
'context',
'broadphase',
'metrics',
'controller',
'detector',
'pairs',
'lastElapsed',
'deltaHistory',
'elapsedHistory',
'engineDeltaHistory',
'engineElapsedHistory',
'timestampElapsedHistory',
].concat(extrinsicProperties);
const collisionId = (collision) =>
Math.min(collision.bodyA.id, collision.bodyB.id) + Math.max(collision.bodyA.id, collision.bodyB.id) * 10000;
const collisionCompareId = (collisionA, collisionB) => collisionId(collisionA) - collisionId(collisionB);
const sortById = (objs) => objs.sort((objA, objB) => objA.id - objB.id);
module.exports = { runExample };

View file

@ -1,26 +1,31 @@
/* eslint-env es6 */
"use strict";
jest.setTimeout(30 * 1000);
jest.setTimeout(2 * 60 * 1000);
const {
const fs = require('fs');
const {
requireUncached,
comparisonReport,
logReport,
toMatchExtrinsics,
toMatchIntrinsics
} = require('./TestTools');
const Example = require('../examples/index');
const MatterBuild = require('../build/matter');
const { versionSatisfies } = require('../src/core/Plugin');
const Example = requireUncached('../examples/index');
const MatterBuild = requireUncached('../build/matter');
const { versionSatisfies } = requireUncached('../src/core/Plugin');
const Worker = require('jest-worker').default;
const specificExamples = process.env.EXAMPLES ? process.env.EXAMPLES.split(' ') : null;
const testComparison = process.env.COMPARE === 'true';
const saveComparison = process.env.SAVE === 'true';
const excludeExamples = ['svg', 'terrain'];
const excludeJitter = ['stack', 'circleStack', 'restitution', 'staticFriction', 'friction', 'newtonsCradle', 'catapult'];
const examples = Object.keys(Example).filter(key => {
const examples = (specificExamples || Object.keys(Example)).filter(key => {
const excluded = excludeExamples.includes(key);
const buildVersion = MatterBuild.version;
const exampleFor = Example[key].for;
@ -28,36 +33,86 @@ const examples = Object.keys(Example).filter(key => {
return !excluded && supported;
});
const runExamples = async useDev => {
const worker = new Worker(require.resolve('./ExampleWorker'), {
const captureExamples = async useDev => {
const multiThreadWorker = new Worker(require.resolve('./ExampleWorker'), {
enableWorkerThreads: true
});
const result = await Promise.all(examples.map(name => worker.runExample({
const overlapRuns = await Promise.all(examples.map(name => multiThreadWorker.runExample({
name,
useDev,
totalUpdates: 120,
updates: 1,
stableSort: true,
jitter: excludeJitter.includes(name) ? 0 : 1e-10
})));
await worker.end();
const behaviourRuns = await Promise.all(examples.map(name => multiThreadWorker.runExample({
name,
useDev,
updates: 2,
stableSort: true,
jitter: excludeJitter.includes(name) ? 0 : 1e-10
})));
return result.reduce((out, capture) => (out[capture.name] = capture, out), {});
const similarityRuns = await Promise.all(examples.map(name => multiThreadWorker.runExample({
name,
useDev,
updates: 2,
stableSort: false,
jitter: excludeJitter.includes(name) ? 0 : 1e-10
})));
await multiThreadWorker.end();
const singleThreadWorker = new Worker(require.resolve('./ExampleWorker'), {
enableWorkerThreads: true,
numWorkers: 1
});
const completeRuns = await Promise.all(examples.map(name => singleThreadWorker.runExample({
name,
useDev,
updates: 150,
stableSort: false,
jitter: excludeJitter.includes(name) ? 0 : 1e-10
})));
await singleThreadWorker.end();
const capture = {};
for (const completeRun of completeRuns) {
const behaviourRun = behaviourRuns.find(({ name }) => name === completeRun.name);
const similarityRun = similarityRuns.find(({ name }) => name === completeRun.name);
const overlapRun = overlapRuns.find(({ name }) => name === completeRun.name);
capture[overlapRun.name] = {
...completeRun,
behaviourExtrinsic: behaviourRun.extrinsic,
similarityExtrinsic: similarityRun.extrinsic,
overlap: overlapRun.overlap
};
}
return capture;
};
const capturesDev = runExamples(true);
const capturesBuild = runExamples(false);
const capturesDev = captureExamples(true);
const capturesBuild = captureExamples(false);
afterAll(async () => {
// Report experimental capture comparison.
const dev = await capturesDev;
const build = await capturesBuild;
const buildSize = fs.statSync('./build/matter.min.js').size;
const devSize = fs.statSync('./build/matter.dev.min.js').size;
console.log(
'Examples ran against previous release and current version\n\n'
'Examples ran against previous release and current build\n\n'
+ logReport(build, `release`) + '\n'
+ logReport(dev, `current`) + '\n'
+ comparisonReport(dev, build, MatterBuild.version, saveComparison)
+ comparisonReport(dev, build, devSize, buildSize, MatterBuild.version, saveComparison)
);
});

View file

@ -3,118 +3,99 @@
const fs = require('fs');
const compactStringify = require('json-stringify-pretty-compact');
const { Composite, Constraint } = require('../src/module/main');
const comparePath = './test/__compare__';
const compareCommand = 'open http://localhost:8000/?compare';
const diffSaveCommand = 'npm run test-save';
const diffCommand = 'code -n -d test/__compare__/examples-build.json test/__compare__/examples-dev.json';
const equalityThreshold = 0.99999;
const intrinsicProps = [
// Common
'id', 'label',
// Constraint
'angularStiffness', 'bodyA', 'bodyB', 'damping', 'length', 'stiffness',
// Body
'area', 'axes', 'collisionFilter', 'category', 'mask',
'group', 'density', 'friction', 'frictionAir', 'frictionStatic', 'inertia', 'inverseInertia', 'inverseMass', 'isSensor',
'isSleeping', 'isStatic', 'mass', 'parent', 'parts', 'restitution', 'sleepThreshold', 'slop',
'timeScale', 'vertices',
// Composite
'bodies', 'constraints', 'composites'
];
const colors = { Red: 31, Green: 32, Yellow: 33, White: 37, BrightWhite: 90, BrightCyan: 36 };
const color = (text, number) => number ? `\x1b[${number}m${text}\x1b[0m` : text;
const limit = (val, precision=3) => parseFloat(val.toPrecision(precision));
const toPercent = val => (100 * val).toPrecision(3);
const engineCapture = (engine) => ({
timestamp: limit(engine.timing.timestamp),
extrinsic: worldCaptureExtrinsic(engine.world),
intrinsic: worldCaptureIntrinsic(engine.world)
});
const comparisonReport = (capturesDev, capturesBuild, devSize, buildSize, buildVersion, save) => {
const performanceDev = capturePerformanceTotals(capturesDev);
const performanceBuild = capturePerformanceTotals(capturesBuild);
const worldCaptureExtrinsic = world => ({
bodies: Composite.allBodies(world).reduce((bodies, body) => {
bodies[body.id] = [
body.position.x,
body.position.y,
body.positionPrev.x,
body.positionPrev.y,
body.angle,
body.anglePrev,
...body.vertices.reduce((flat, vertex) => (flat.push(vertex.x, vertex.y), flat), [])
];
const perfChange = noiseThreshold(1 - (performanceDev.duration / performanceBuild.duration), 0.01);
const memoryChange = noiseThreshold((performanceDev.memory / performanceBuild.memory) - 1, 0.01);
const overlapChange = (performanceDev.overlap / (performanceBuild.overlap || 1)) - 1;
const filesizeChange = (devSize / buildSize) - 1;
return bodies;
}, {}),
constraints: Composite.allConstraints(world).reduce((constraints, constraint) => {
const positionA = Constraint.pointAWorld(constraint);
const positionB = Constraint.pointBWorld(constraint);
const behaviourSimilaritys = extrinsicSimilarity(capturesDev, capturesBuild, 'behaviourExtrinsic');
const behaviourSimilarityAverage = extrinsicSimilarityAverage(behaviourSimilaritys);
const behaviourSimilarityEntries = Object.entries(behaviourSimilaritys);
behaviourSimilarityEntries.sort((a, b) => a[1] - b[1]);
constraints[constraint.id] = [
positionA.x,
positionA.y,
positionB.x,
positionB.y
];
const similaritys = extrinsicSimilarity(capturesDev, capturesBuild, 'similarityExtrinsic');
const similarityAverage = extrinsicSimilarityAverage(similaritys);
return constraints;
}, {})
});
const devIntrinsicsChanged = {};
const buildIntrinsicsChanged = {};
let intrinsicChangeCount = 0;
const worldCaptureIntrinsic = world => worldCaptureIntrinsicBase({
bodies: Composite.allBodies(world).reduce((bodies, body) => {
bodies[body.id] = body;
return bodies;
}, {}),
constraints: Composite.allConstraints(world).reduce((constraints, constraint) => {
constraints[constraint.id] = constraint;
return constraints;
}, {}),
composites: Composite.allComposites(world).reduce((composites, composite) => {
composites[composite.id] = {
bodies: Composite.allBodies(composite).map(body => body.id),
constraints: Composite.allConstraints(composite).map(constraint => constraint.id),
composites: Composite.allComposites(composite).map(composite => composite.id)
};
return composites;
}, {})
});
const captureSummary = Object.entries(capturesDev)
.map(([name]) => {
const changedIntrinsics = !equals(capturesDev[name].intrinsic, capturesBuild[name].intrinsic);
const worldCaptureIntrinsicBase = (obj, depth=0) => {
if (obj === Infinity) {
return 'Infinity';
} else if (typeof obj === 'number') {
return limit(obj);
} else if (Array.isArray(obj)) {
return obj.map(item => worldCaptureIntrinsicBase(item, depth + 1));
} else if (typeof obj !== 'object') {
return obj;
}
const result = Object.entries(obj)
.filter(([key]) => depth <= 1 || intrinsicProps.includes(key))
.reduce((cleaned, [key, val]) => {
if (val && val.id && String(val.id) !== key) {
val = val.id;
if (changedIntrinsics) {
capturesDev[name].changedIntrinsics = true;
if (intrinsicChangeCount < 1) {
devIntrinsicsChanged[name] = capturesDev[name].state;
buildIntrinsicsChanged[name] = capturesBuild[name].state;
intrinsicChangeCount += 1;
}
}
if (Array.isArray(val) && !['composites', 'constraints', 'bodies'].includes(key)) {
val = `[${val.length}]`;
return { name };
})
.sort((a, b) => a.name.localeCompare(b.name));
const report = (breakEvery, format) => [
[`Output comparison of ${behaviourSimilarityEntries.length}`,
`examples against previous release ${format('matter-js@' + buildVersion, colors.Yellow)}`
].join(' '),
`\n\n${format('Behaviour ', colors.White)}`,
`${format(formatPercent(behaviourSimilarityAverage), behaviourSimilarityAverage === 1 ? colors.Green : colors.Yellow)}%`,
` ${format('Similarity', colors.White)}`,
`${format(formatPercent(similarityAverage), similarityAverage === 1 ? colors.Green : colors.Yellow)}%`,
` ${format('Overlap', colors.White)}`,
` ${format((overlapChange >= 0 ? '+' : '-') + formatPercent(overlapChange, true), overlapChange <= 0 ? colors.Green : colors.Yellow)}%`,
`\n${format('Performance', colors.White)}`,
`${format((perfChange >= 0 ? '+' : '-') + formatPercent(perfChange, true), perfChange >= 0 ? colors.Green : colors.Yellow)}%`,
` ${format('Memory', colors.White)}`,
` ${format((memoryChange >= 0 ? '+' : '-') + formatPercent(memoryChange, true), memoryChange <= 0 ? colors.Green : colors.Yellow)}%`,
` ${format('Filesize', colors.White)}`,
`${format((filesizeChange >= 0 ? '+' : '-') + formatPercent(filesizeChange, true), filesizeChange <= 0 ? colors.Green : colors.Yellow)}%`,
`${format(`${(devSize / 1024).toPrecision(4)} KB`, colors.White)}`,
captureSummary.reduce((output, p, i) => {
output += `${p.name} `;
output += `${similarityRatings(behaviourSimilaritys[p.name])} `;
output += `${changeRatings(capturesDev[p.name].changedIntrinsics)} `;
if (i > 0 && i < captureSummary.length && breakEvery > 0 && i % breakEvery === 0) {
output += '\n';
}
return output;
}, '\n\n'),
cleaned[key] = worldCaptureIntrinsicBase(val, depth + 1);
return cleaned;
}, {});
`\n\nwhere · no change ● extrinsics changed ◆ intrinsics changed\n`,
return Object.keys(result).sort()
.reduce((sorted, key) => (sorted[key] = result[key], sorted), {});
behaviourSimilarityAverage < 1 ? `\n${format('▶', colors.White)} ${format(compareCommand + '=' + 150 + '#' + behaviourSimilarityEntries[0][0], colors.BrightCyan)}` : '',
intrinsicChangeCount > 0 ? `\n${format('▶', colors.White)} ${format((save ? diffCommand : diffSaveCommand), colors.BrightCyan)}` : ''
].join(' ');
if (save) {
writeResult('examples-dev', devIntrinsicsChanged);
writeResult('examples-build', buildIntrinsicsChanged);
writeResult('examples-report', report(5, s => s));
}
return report(5, color);
};
const similarity = (a, b) => {
@ -124,18 +105,56 @@ const similarity = (a, b) => {
return 1 / (1 + (distance / a.length));
};
const captureSimilarityExtrinsic = (currentCaptures, referenceCaptures) => {
const similarityRatings = similarity => similarity < equalityThreshold ? color('●', colors.Yellow) : '·';
const changeRatings = isChanged => isChanged ? color('◆', colors.White) : '·';
const color = (text, number) => number ? `\x1b[${number}m${text}\x1b[0m` : text;
const formatPercent = (val, abs) => (100 * (abs ? Math.abs(val) : val)).toFixed(2);
const noiseThreshold = (val, threshold) => {
const sign = val < 0 ? -1 : 1;
const magnitude = Math.abs(val);
return sign * Math.max(0, magnitude - threshold) / (1 - threshold);
};
const equals = (a, b) => {
try {
expect(a).toEqual(b);
} catch (e) {
return false;
}
return true;
};
const capturePerformanceTotals = (captures) => {
const totals = {
duration: 0,
overlap: 0,
memory: 0
};
for (const [ name ] of Object.entries(captures)) {
totals.duration += captures[name].duration;
totals.overlap += captures[name].overlap;
totals.memory += captures[name].memory;
};
return totals;
};
const extrinsicSimilarity = (currentCaptures, referenceCaptures, key) => {
const result = {};
Object.entries(currentCaptures).forEach(([name, current]) => {
const reference = referenceCaptures[name];
const worldVector = [];
const worldVectorRef = [];
const currentExtrinsic = current[key];
const referenceExtrinsic = reference[key];
Object.keys(current.extrinsic).forEach(objectType => {
Object.keys(current.extrinsic[objectType]).forEach(objectId => {
worldVector.push(...current.extrinsic[objectType][objectId]);
worldVectorRef.push(...reference.extrinsic[objectType][objectId]);
Object.keys(currentExtrinsic).forEach(objectType => {
Object.keys(currentExtrinsic[objectType]).forEach(objectId => {
worldVector.push(...currentExtrinsic[objectType][objectId]);
worldVectorRef.push(...referenceExtrinsic[objectType][objectId]);
});
});
@ -145,6 +164,56 @@ const captureSimilarityExtrinsic = (currentCaptures, referenceCaptures) => {
return result;
};
const extrinsicSimilarityAverage = (similaritys) => {
const entries = Object.entries(similaritys);
let average = 0;
entries.forEach(([_, similarity]) => average += similarity);
return average /= entries.length;
};
const serialize = (obj, exclude=()=>false, precision=4, path='$', visited=[], paths=[]) => {
if (typeof obj === 'number') {
return parseFloat(obj.toPrecision(precision));
} else if (typeof obj === 'string' || typeof obj === 'boolean') {
return obj;
} else if (obj === null) {
return 'null';
} else if (typeof obj === 'undefined') {
return 'undefined';
} else if (obj === Infinity) {
return 'Infinity';
} else if (obj === -Infinity) {
return '-Infinity';
} else if (typeof obj === 'function') {
return 'function';
} else if (Array.isArray(obj)) {
return obj.map(
(item, index) => serialize(item, exclude, precision, path + '.' + index, visited, paths)
);
}
const visitedIndex = visited.indexOf(obj);
if (visitedIndex !== -1) {
return paths[visitedIndex];
}
visited.push(obj);
paths.push(path);
const result = {};
for (const key of Object.keys(obj).sort()) {
if (!exclude(key, obj[key], path + '.' + key)) {
result[key] = serialize(obj[key], exclude, precision, path + '.' + key, visited, paths);
}
}
return result;
};
const writeResult = (name, obj) => {
try {
fs.mkdirSync(comparePath, { recursive: true });
@ -159,9 +228,35 @@ const writeResult = (name, obj) => {
}
};
const logReport = (captures, version) => {
let report = '';
for (const capture of Object.values(captures)) {
if (!capture.logs.length) {
continue;
}
report += ` ${capture.name}\n`;
for (const log of capture.logs) {
report += ` ${log}\n`;
}
}
return `Output logs from ${color(version, colors.Yellow)} build on last run\n\n`
+ (report ? report : ' None\n');
};
const requireUncached = path => {
delete require.cache[require.resolve(path)];
const module = require(path);
delete require.cache[require.resolve(path)];
return module;
};
const toMatchExtrinsics = {
toMatchExtrinsics(received, value) {
const similaritys = captureSimilarityExtrinsic(received, value);
const similaritys = extrinsicSimilarity(received, value, 'extrinsic');
const pass = Object.values(similaritys).every(similarity => similarity >= equalityThreshold);
return {
@ -190,119 +285,7 @@ const toMatchIntrinsics = {
}
};
const similarityRatings = similarity => similarity < equalityThreshold ? color('●', colors.Yellow) : '·';
const changeRatings = isChanged => isChanged ? color('◆', colors.White) : '·';
const equals = (a, b) => {
try {
expect(a).toEqual(b);
} catch (e) {
return false;
}
return true;
};
const logReport = (captures, version) => {
let report = '';
for (const capture of Object.values(captures)) {
if (!capture.logs.length) {
continue;
}
report += ` ${capture.name}\n`;
for (const log of capture.logs) {
report += ` ${log}\n`;
}
}
return `Output logs from ${color(version, colors.Yellow)} version on last run\n\n`
+ (report ? report : ' None\n');
};
const comparisonReport = (capturesDev, capturesBuild, buildVersion, save) => {
const similaritys = captureSimilarityExtrinsic(capturesDev, capturesBuild);
const similarityEntries = Object.entries(similaritys);
const devIntrinsicsChanged = {};
const buildIntrinsicsChanged = {};
let intrinsicChangeCount = 0;
let totalTimeBuild = 0;
let totalTimeDev = 0;
let totalOverlapBuild = 0;
let totalOverlapDev = 0;
const capturePerformance = Object.entries(capturesDev).map(([name]) => {
totalTimeBuild += capturesBuild[name].duration;
totalTimeDev += capturesDev[name].duration;
totalOverlapBuild += capturesBuild[name].overlap;
totalOverlapDev += capturesDev[name].overlap;
const changedIntrinsics = !equals(capturesDev[name].intrinsic, capturesBuild[name].intrinsic);
if (changedIntrinsics) {
capturesDev[name].changedIntrinsics = true;
if (intrinsicChangeCount < 2) {
devIntrinsicsChanged[name] = capturesDev[name].intrinsic;
buildIntrinsicsChanged[name] = capturesBuild[name].intrinsic;
intrinsicChangeCount += 1;
}
}
return { name };
});
capturePerformance.sort((a, b) => a.name.localeCompare(b.name));
similarityEntries.sort((a, b) => a[1] - b[1]);
let perfChange = 1 - (totalTimeDev / totalTimeBuild);
const perfChangeThreshold = 0.075;
const perfChangeLarge = Math.abs(perfChange) > perfChangeThreshold;
perfChange = perfChangeLarge ? perfChange : 0;
let similarityAvg = 0;
similarityEntries.forEach(([_, similarity]) => {
similarityAvg += similarity;
});
similarityAvg /= similarityEntries.length;
const overlapChange = (totalOverlapDev / (totalOverlapBuild || 1)) - 1;
const report = (breakEvery, format) => [
[`Output comparison of ${similarityEntries.length}`,
`examples against previous release ${format('matter-js@' + buildVersion, colors.Yellow)}`
].join(' '),
`\n\n${format('Similarity', colors.White)}`,
`${format(toPercent(similarityAvg), similarityAvg === 1 ? colors.Green : colors.Yellow)}%`,
`${format('Performance', colors.White)}`,
`${format((perfChange >= 0 ? '+' : '') + toPercent(perfChange), perfChange >= 0 ? colors.Green : colors.Red)}%`,
`${format('Overlap', colors.White)}`,
`${format((overlapChange >= 0 ? '+' : '') + toPercent(overlapChange), overlapChange > 0 ? colors.Red : colors.Green)}%`,
capturePerformance.reduce((output, p, i) => {
output += `${p.name} `;
output += `${similarityRatings(similaritys[p.name])} `;
output += `${changeRatings(capturesDev[p.name].changedIntrinsics)} `;
if (i > 0 && i < capturePerformance.length && breakEvery > 0 && i % breakEvery === 0) {
output += '\n';
}
return output;
}, '\n\n'),
`\nwhere · no change ● extrinsics changed ◆ intrinsics changed\n`,
similarityAvg < 1 ? `\n${format('▶', colors.White)} ${format(compareCommand + '=' + 120 + '#' + similarityEntries[0][0], colors.BrightCyan)}` : '',
intrinsicChangeCount > 0 ? `\n${format('▶', colors.White)} ${format((save ? diffCommand : diffSaveCommand), colors.BrightCyan)}` : ''
].join(' ');
if (save) {
writeResult('examples-dev', devIntrinsicsChanged);
writeResult('examples-build', buildIntrinsicsChanged);
writeResult('examples-report', report(5, s => s));
}
return report(5, color);
};
module.exports = {
engineCapture, comparisonReport, logReport,
toMatchExtrinsics, toMatchIntrinsics
requireUncached, comparisonReport, logReport,
serialize, toMatchExtrinsics, toMatchIntrinsics
};

View file

@ -9,18 +9,17 @@ const execSync = require('child_process').execSync;
module.exports = (env = {}) => {
const minimize = env.MINIMIZE || false;
const alpha = env.ALPHA || false;
const kind = env.KIND || null;
const sizeThreshold = minimize ? 100 * 1024 : 512 * 1024;
const commitHash = execSync('git rev-parse --short HEAD').toString().trim();
const version = !alpha ? pkg.version : `${pkg.version}-alpha+${commitHash}`;
const version = !kind ? pkg.version : `${pkg.version}-${kind}+${commitHash}`;
const license = fs.readFileSync('LICENSE', 'utf8');
const resolve = relativePath => path.resolve(__dirname, relativePath);
const alphaInfo = 'Experimental pre-release build.\n ';
const banner =
`${pkg.name} ${version} by @liabru
${alpha ? alphaInfo : ''}${pkg.homepage}
${kind ? 'Experimental pre-release build.\n ' : ''}${pkg.homepage}
License ${pkg.license}${!minimize ? '\n\n' + license : ''}`;
return {
@ -32,7 +31,7 @@ License ${pkg.license}${!minimize ? '\n\n' + license : ''}`;
umdNamedDefine: true,
globalObject: 'this',
path: resolve('./build'),
filename: `[name]${alpha ? '.alpha' : ''}${minimize ? '.min' : ''}.js`
filename: `[name]${kind ? '.' + kind : ''}${minimize ? '.min' : ''}.js`
},
optimization: { minimize },
performance: {

View file

@ -29,7 +29,7 @@ License ${pkg.license}`;
return {
entry: { [name]: './demo/src/index.js' },
node: false,
devtool: 'source-map',
devtool: devServer ? 'source-map' : 'none',
output: {
library: 'MatterDemo',
libraryTarget: 'umd',