mirror of
https://github.com/liabru/matter-js.git
synced 2024-11-23 09:26:51 -05:00
Merge branch 'master' into runner-2
* master: (22 commits) improve test comparison report update ci update ci preserve pair.contacts order optimised Resolver.solvePosition bump package lock improve test comparison report fixed compare tool layer order in demo testbed fixed compare tool layer order in demo testbed added multi example testing tool to demo added body removal to Example.remove changed Composte.removeComposite and Composte.removeBody to reset body.sleepCounter optimised Collision.collides fix collision events for sleeping pairs, closes #1077 added local pairs functions in Pairs.update removed pair.confirmedActive changed Pair.id format to use shorter ids optimised Resolver.solveVelocity optimised contacts and supports memory and gc use optimised pairs and collisions memory and gc use ...
This commit is contained in:
commit
2c91e7400f
20 changed files with 4850 additions and 5680 deletions
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
@ -13,7 +13,7 @@ jobs:
|
|||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [12.x, 14.x, 16.x]
|
||||
node-version: [16.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
|
|
@ -46,6 +46,7 @@
|
|||
.matter-js-compare-build.matter-demo canvas {
|
||||
opacity: 0.5;
|
||||
background: transparent !important;
|
||||
z-index: 25 !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
|
97
demo/src/Multi.js
Normal file
97
demo/src/Multi.js
Normal file
|
@ -0,0 +1,97 @@
|
|||
/**
|
||||
* A Matter.js multi example testbed based on MatterTools.
|
||||
*
|
||||
* Tool to interactively test multiple examples at once.
|
||||
*
|
||||
* USAGE: [host]?multi#[example1,example2,example3...]
|
||||
* e.g. http://localhost:8000/?multi#mixed
|
||||
*
|
||||
* @module Multi
|
||||
*/
|
||||
|
||||
var MatterTools = require('matter-tools');
|
||||
|
||||
var multi = function(examples, isDev) {
|
||||
var demo = MatterTools.Demo.create({
|
||||
toolbar: {
|
||||
title: 'matter-js ・ ' + (isDev ? 'dev' : '') + ' ・ multi',
|
||||
url: 'https://github.com/liabru/matter-js',
|
||||
reset: false,
|
||||
source: false,
|
||||
inspector: false,
|
||||
tools: false,
|
||||
fullscreen: false,
|
||||
exampleSelect: false
|
||||
},
|
||||
tools: {
|
||||
inspector: false,
|
||||
gui: false
|
||||
},
|
||||
inline: false,
|
||||
preventZoom: false,
|
||||
resetOnOrientation: false,
|
||||
routing: false,
|
||||
startExample: false
|
||||
});
|
||||
|
||||
var urlHash = window.location.hash,
|
||||
allExampleIds = examples.map(function(example) { return example.id; }),
|
||||
exampleIds = urlHash ? urlHash.slice(1).split(',') : allExampleIds.slice(0, 4),
|
||||
exampleCount = Math.ceil(Math.sqrt(exampleIds.length));
|
||||
|
||||
var container = document.createElement('div');
|
||||
container.style = 'display: grid; grid-template-columns: repeat(' + exampleCount + ', 1fr); grid-template-rows: repeat(' + exampleCount + ', 1fr); max-width: calc(100vmin * 1.25 - 40px); max-height: 100vmin;';
|
||||
|
||||
demo.dom.root.appendChild(container);
|
||||
document.body.appendChild(demo.dom.root);
|
||||
|
||||
document.title = 'Matter.js Multi' + (isDev ? ' ・ Dev' : '');
|
||||
console.info('Demo.Multi: matter-js@' + Matter.version);
|
||||
|
||||
// always show debug info
|
||||
Matter.before('Render.create', function(renderOptions) {
|
||||
renderOptions.options.showDebug = true;
|
||||
});
|
||||
|
||||
Matter.after('Runner.create', function() {
|
||||
this.isFixed = true;
|
||||
});
|
||||
|
||||
var runExamples = function(exampleIds) {
|
||||
for (var i = 0; i < exampleIds.length; i += 1) {
|
||||
var exampleId = exampleIds[i],
|
||||
example = examples.find(function(example) { return example.id === exampleId; });
|
||||
|
||||
if (!example) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var canvas = example.init().render.canvas;
|
||||
container.appendChild(canvas);
|
||||
}
|
||||
};
|
||||
|
||||
runExamples(exampleIds);
|
||||
|
||||
// arrow key navigation of examples
|
||||
document.addEventListener('keyup', function(event) {
|
||||
var isBackKey = event.key === 'ArrowLeft' || event.key === 'ArrowUp',
|
||||
isForwardKey = event.key === 'ArrowRight' || event.key === 'ArrowDown';
|
||||
|
||||
if (isBackKey || isForwardKey) {
|
||||
var direction = isBackKey ? -1 : 1;
|
||||
|
||||
var currentExampleIndex = allExampleIds.findIndex(function(exampleId) {
|
||||
return exampleId === exampleIds[0];
|
||||
});
|
||||
|
||||
var nextExampleId = (allExampleIds.length + currentExampleIndex + direction * exampleIds.length) % allExampleIds.length,
|
||||
nextExamples = allExampleIds.slice(nextExampleId, (nextExampleId + exampleIds.length) % allExampleIds.length);
|
||||
|
||||
window.location.hash = nextExamples.join(',');
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = { multi: multi };
|
|
@ -35,7 +35,6 @@
|
|||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
@ -43,9 +42,15 @@
|
|||
display: none;
|
||||
}
|
||||
|
||||
.matter-js.dev.comparing.matter-demo canvas {
|
||||
background: transparent !important;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.matter-js-compare-build.matter-demo canvas {
|
||||
opacity: 0.5;
|
||||
background: transparent !important;
|
||||
z-index: 15 !important;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 1300px) {
|
||||
|
@ -54,6 +59,11 @@
|
|||
position: relative;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.matter-js-compare-build.matter-demo canvas {
|
||||
position: relative;
|
||||
z-index: 15;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
var Matter = require('matter-js');
|
||||
var Examples = require('../../examples/index');
|
||||
var compare = require('./Compare').compare;
|
||||
var multi = require('./Multi').multi;
|
||||
var demo = require('./Demo').demo;
|
||||
|
||||
// browser globals
|
||||
|
@ -31,9 +32,13 @@ var examples = Matter.Common.keys(Examples).map(function(id){
|
|||
|
||||
// start the requested tool
|
||||
var isCompare = window.location.search.indexOf('compare') >= 0;
|
||||
var isMulti = window.location.search.indexOf('multi') >= 0;
|
||||
var isDev = __MATTER_IS_DEV__;
|
||||
|
||||
if (isCompare) {
|
||||
compare(examples, isDev);
|
||||
} else if (isMulti) {
|
||||
multi(examples, isDev);
|
||||
} else {
|
||||
demo(examples, isDev);
|
||||
}
|
||||
|
|
|
@ -13,8 +13,11 @@ Example.remove = function() {
|
|||
Events = Matter.Events;
|
||||
|
||||
// create engine
|
||||
var engine = Engine.create(),
|
||||
world = engine.world;
|
||||
var engine = Engine.create({
|
||||
enableSleeping: true
|
||||
});
|
||||
|
||||
var world = engine.world;
|
||||
|
||||
// create renderer
|
||||
var render = Render.create({
|
||||
|
@ -24,6 +27,7 @@ Example.remove = function() {
|
|||
width: 800,
|
||||
height: 600,
|
||||
showAngleIndicator: true,
|
||||
showSleeping: true
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -33,9 +37,6 @@ Example.remove = function() {
|
|||
var runner = Runner.create();
|
||||
Runner.run(runner, engine);
|
||||
|
||||
var stack = null,
|
||||
lastTimestamp = 0;
|
||||
|
||||
var createStack = function() {
|
||||
return Composites.stack(20, 20, 10, 5, 0, 0, function(x, y) {
|
||||
var sides = Math.round(Common.random(1, 8));
|
||||
|
@ -61,15 +62,28 @@ Example.remove = function() {
|
|||
});
|
||||
};
|
||||
|
||||
// add and remove stacks every few updates
|
||||
var stack = null,
|
||||
bottomStack = createStack(),
|
||||
lastTimestamp = 0;
|
||||
|
||||
// add and remove bodies and composites every few updates
|
||||
Events.on(engine, 'afterUpdate', function(event) {
|
||||
// limit rate
|
||||
if (stack && event.timestamp - lastTimestamp < 800) {
|
||||
if (event.timestamp - lastTimestamp < 800) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastTimestamp = event.timestamp;
|
||||
|
||||
// remove an old body
|
||||
Composite.remove(bottomStack, bottomStack.bodies[0]);
|
||||
|
||||
// add a new body
|
||||
Composite.add(
|
||||
bottomStack,
|
||||
Bodies.rectangle(Common.random(100, 500), 50, Common.random(25, 50), Common.random(25, 50))
|
||||
);
|
||||
|
||||
// remove last stack
|
||||
if (stack) {
|
||||
Composite.remove(world, stack);
|
||||
|
@ -82,10 +96,9 @@ Example.remove = function() {
|
|||
Composite.add(world, stack);
|
||||
});
|
||||
|
||||
// add another stack that will not be removed
|
||||
Composite.add(world, createStack());
|
||||
|
||||
Composite.add(world, [
|
||||
bottomStack,
|
||||
|
||||
// walls
|
||||
Bodies.rectangle(400, 0, 800, 50, { isStatic: true }),
|
||||
Bodies.rectangle(400, 600, 800, 50, { isStatic: true }),
|
||||
|
|
9638
package-lock.json
generated
9638
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -23,8 +23,8 @@
|
|||
"conventional-changelog-cli": "^2.1.1",
|
||||
"eslint": "^6.8.0",
|
||||
"html-webpack-plugin": "^4.5.1",
|
||||
"jest": "^25.1.0",
|
||||
"jest-worker": "^24.9.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-worker": "^29.7.0",
|
||||
"json-stringify-pretty-compact": "^2.0.0",
|
||||
"matter-tools": "^0.14.0",
|
||||
"matter-wrap": "^0.2.0",
|
||||
|
@ -50,7 +50,7 @@
|
|||
"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": "npm run test-node -- --examples=stress3,stress4 --updates=300 --repeats=3",
|
||||
"benchmark": "npm run test-node -- --examples=stress3,stress4 --benchmark=true --updates=300 --repeats=3",
|
||||
"test": "npm run test-node",
|
||||
"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",
|
||||
|
|
|
@ -192,8 +192,15 @@ var Body = require('./Body');
|
|||
*/
|
||||
Composite.removeComposite = function(compositeA, compositeB, deep) {
|
||||
var position = Common.indexOf(compositeA.composites, compositeB);
|
||||
|
||||
if (position !== -1) {
|
||||
var bodies = Composite.allBodies(compositeB);
|
||||
|
||||
Composite.removeCompositeAt(compositeA, position);
|
||||
|
||||
for (var i = 0; i < bodies.length; i++) {
|
||||
bodies[i].sleepCounter = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (deep) {
|
||||
|
@ -244,8 +251,10 @@ var Body = require('./Body');
|
|||
*/
|
||||
Composite.removeBody = function(composite, body, deep) {
|
||||
var position = Common.indexOf(composite.bodies, body);
|
||||
|
||||
if (position !== -1) {
|
||||
Composite.removeBodyAt(composite, position);
|
||||
body.sleepCounter = 0;
|
||||
}
|
||||
|
||||
if (deep) {
|
||||
|
@ -296,6 +305,7 @@ var Body = require('./Body');
|
|||
*/
|
||||
Composite.removeConstraint = function(composite, constraint, deep) {
|
||||
var position = Common.indexOf(composite.constraints, constraint);
|
||||
|
||||
if (position !== -1) {
|
||||
Composite.removeConstraintAt(composite, position);
|
||||
}
|
||||
|
|
|
@ -47,7 +47,8 @@ var Pair = require('./Pair');
|
|||
normal: { x: 0, y: 0 },
|
||||
tangent: { x: 0, y: 0 },
|
||||
penetration: { x: 0, y: 0 },
|
||||
supports: []
|
||||
supports: [null, null],
|
||||
supportCount: 0
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -99,27 +100,32 @@ var Pair = require('./Pair');
|
|||
}
|
||||
|
||||
var normal = collision.normal,
|
||||
tangent = collision.tangent,
|
||||
penetration = collision.penetration,
|
||||
supports = collision.supports,
|
||||
depth = minOverlap.overlap,
|
||||
minAxis = minOverlap.axis,
|
||||
minAxisX = minAxis.x,
|
||||
minAxisY = minAxis.y;
|
||||
normalX = minAxis.x,
|
||||
normalY = minAxis.y,
|
||||
deltaX = bodyB.position.x - bodyA.position.x,
|
||||
deltaY = bodyB.position.y - bodyA.position.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;
|
||||
if (normalX * deltaX + normalY * deltaY >= 0) {
|
||||
normalX = -normalX;
|
||||
normalY = -normalY;
|
||||
}
|
||||
|
||||
collision.tangent.x = -normal.y;
|
||||
collision.tangent.y = normal.x;
|
||||
normal.x = normalX;
|
||||
normal.y = normalY;
|
||||
|
||||
collision.depth = minOverlap.overlap;
|
||||
tangent.x = -normalY;
|
||||
tangent.y = normalX;
|
||||
|
||||
collision.penetration.x = normal.x * collision.depth;
|
||||
collision.penetration.y = normal.y * collision.depth;
|
||||
penetration.x = normalX * depth;
|
||||
penetration.y = normalY * depth;
|
||||
|
||||
collision.depth = depth;
|
||||
|
||||
// find support points, there is always either exactly one or two
|
||||
var supportsB = Collision._findSupports(bodyA, bodyB, normal, 1),
|
||||
|
@ -152,8 +158,8 @@ var Pair = require('./Pair');
|
|||
supports[supportCount++] = supportsB[0];
|
||||
}
|
||||
|
||||
// update supports array size
|
||||
supports.length = supportCount;
|
||||
// update support count
|
||||
collision.supportCount = supportCount;
|
||||
|
||||
return collision;
|
||||
};
|
||||
|
@ -232,32 +238,6 @@ var Pair = require('./Pair');
|
|||
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
|
||||
|
@ -275,15 +255,15 @@ var Pair = require('./Pair');
|
|||
bodyAPositionY = bodyA.position.y,
|
||||
normalX = normal.x * direction,
|
||||
normalY = normal.y * direction,
|
||||
nearestDistance = Number.MAX_VALUE,
|
||||
vertexA,
|
||||
vertexB,
|
||||
vertexA = vertices[0],
|
||||
vertexB = vertexA,
|
||||
nearestDistance = normalX * (bodyAPositionX - vertexB.x) + normalY * (bodyAPositionY - vertexB.y),
|
||||
vertexC,
|
||||
distance,
|
||||
j;
|
||||
|
||||
// find deepest vertex relative to the axis
|
||||
for (j = 0; j < verticesLength; j += 1) {
|
||||
for (j = 1; j < verticesLength; j += 1) {
|
||||
vertexB = vertices[j];
|
||||
distance = normalX * (bodyAPositionX - vertexB.x) + normalY * (bodyAPositionY - vertexB.y);
|
||||
|
||||
|
@ -398,6 +378,10 @@ var Pair = require('./Pair');
|
|||
|
||||
/**
|
||||
* An array of body vertices that represent the support points in the collision.
|
||||
*
|
||||
* _Note:_ Only the first `collision.supportCount` items of `collision.supports` are active.
|
||||
* Therefore use `collision.supportCount` instead of `collision.supports.length` when iterating the active supports.
|
||||
*
|
||||
* These are the deepest vertices (along the collision normal) of each body that are contained by the other body's vertices.
|
||||
*
|
||||
* @property supports
|
||||
|
@ -405,4 +389,15 @@ var Pair = require('./Pair');
|
|||
* @default []
|
||||
*/
|
||||
|
||||
/**
|
||||
* The number of active supports for this collision found in `collision.supports`.
|
||||
*
|
||||
* _Note:_ Only the first `collision.supportCount` items of `collision.supports` are active.
|
||||
* Therefore use `collision.supportCount` instead of `collision.supports.length` when iterating the active supports.
|
||||
*
|
||||
* @property supportCount
|
||||
* @type number
|
||||
* @default 0
|
||||
*/
|
||||
|
||||
})();
|
||||
|
|
|
@ -13,7 +13,7 @@ module.exports = Contact;
|
|||
/**
|
||||
* Creates a new contact.
|
||||
* @method create
|
||||
* @param {vertex} vertex
|
||||
* @param {vertex} [vertex]
|
||||
* @return {contact} A new contact
|
||||
*/
|
||||
Contact.create = function(vertex) {
|
||||
|
|
|
@ -22,6 +22,7 @@ var Collision = require('./Collision');
|
|||
Detector.create = function(options) {
|
||||
var defaults = {
|
||||
bodies: [],
|
||||
collisions: [],
|
||||
pairs: null
|
||||
};
|
||||
|
||||
|
@ -45,6 +46,7 @@ var Collision = require('./Collision');
|
|||
*/
|
||||
Detector.clear = function(detector) {
|
||||
detector.bodies = [];
|
||||
detector.collisions = [];
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -57,12 +59,13 @@ var Collision = require('./Collision');
|
|||
* @return {collision[]} collisions
|
||||
*/
|
||||
Detector.collisions = function(detector) {
|
||||
var collisions = [],
|
||||
pairs = detector.pairs,
|
||||
var pairs = detector.pairs,
|
||||
bodies = detector.bodies,
|
||||
bodiesLength = bodies.length,
|
||||
canCollide = Detector.canCollide,
|
||||
collides = Collision.collides,
|
||||
collisions = detector.collisions,
|
||||
collisionIndex = 0,
|
||||
i,
|
||||
j;
|
||||
|
||||
|
@ -104,7 +107,7 @@ var Collision = require('./Collision');
|
|||
var collision = collides(bodyA, bodyB, pairs);
|
||||
|
||||
if (collision) {
|
||||
collisions.push(collision);
|
||||
collisions[collisionIndex++] = collision;
|
||||
}
|
||||
} else {
|
||||
var partsAStart = partsALength > 1 ? 1 : 0,
|
||||
|
@ -126,7 +129,7 @@ var Collision = require('./Collision');
|
|||
var collision = collides(partA, partB, pairs);
|
||||
|
||||
if (collision) {
|
||||
collisions.push(collision);
|
||||
collisions[collisionIndex++] = collision;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -134,6 +137,10 @@ var Collision = require('./Collision');
|
|||
}
|
||||
}
|
||||
|
||||
if (collisions.length !== collisionIndex) {
|
||||
collisions.length = collisionIndex;
|
||||
}
|
||||
|
||||
return collisions;
|
||||
};
|
||||
|
||||
|
@ -180,6 +187,13 @@ var Collision = require('./Collision');
|
|||
* @default []
|
||||
*/
|
||||
|
||||
/**
|
||||
* The array of `Matter.Collision` found in the last call to `Detector.collisions` on this detector.
|
||||
* @property collisions
|
||||
* @type collision[]
|
||||
* @default []
|
||||
*/
|
||||
|
||||
/**
|
||||
* Optional. A `Matter.Pairs` object from which previous collision objects may be reused. Intended for internal `Matter.Engine` usage.
|
||||
* @property pairs
|
||||
|
|
|
@ -28,11 +28,10 @@ var Contact = require('./Contact');
|
|||
bodyA: bodyA,
|
||||
bodyB: bodyB,
|
||||
collision: collision,
|
||||
contacts: [],
|
||||
activeContacts: [],
|
||||
contacts: [Contact.create(), Contact.create()],
|
||||
contactCount: 0,
|
||||
separation: 0,
|
||||
isActive: true,
|
||||
confirmedActive: true,
|
||||
isSensor: bodyA.isSensor || bodyB.isSensor,
|
||||
timeCreated: timestamp,
|
||||
timeUpdated: timestamp,
|
||||
|
@ -56,12 +55,11 @@ var Contact = require('./Contact');
|
|||
* @param {number} timestamp
|
||||
*/
|
||||
Pair.update = function(pair, collision, timestamp) {
|
||||
var contacts = pair.contacts,
|
||||
supports = collision.supports,
|
||||
activeContacts = pair.activeContacts,
|
||||
var supports = collision.supports,
|
||||
supportCount = collision.supportCount,
|
||||
contacts = pair.contacts,
|
||||
parentA = collision.parentA,
|
||||
parentB = collision.parentB,
|
||||
parentAVerticesLength = parentA.vertices.length;
|
||||
parentB = collision.parentB;
|
||||
|
||||
pair.isActive = true;
|
||||
pair.timeUpdated = timestamp;
|
||||
|
@ -73,20 +71,24 @@ var Contact = require('./Contact');
|
|||
pair.restitution = parentA.restitution > parentB.restitution ? parentA.restitution : parentB.restitution;
|
||||
pair.slop = parentA.slop > parentB.slop ? parentA.slop : parentB.slop;
|
||||
|
||||
pair.contactCount = supportCount;
|
||||
collision.pair = pair;
|
||||
activeContacts.length = 0;
|
||||
|
||||
for (var i = 0; i < supports.length; i++) {
|
||||
var support = supports[i],
|
||||
contactId = support.body === parentA ? support.index : parentAVerticesLength + support.index,
|
||||
contact = contacts[contactId];
|
||||
var supportA = supports[0],
|
||||
contactA = contacts[0],
|
||||
supportB = supports[1],
|
||||
contactB = contacts[1];
|
||||
|
||||
if (contact) {
|
||||
activeContacts.push(contact);
|
||||
} else {
|
||||
activeContacts.push(contacts[contactId] = Contact.create(support));
|
||||
}
|
||||
// match contacts to supports
|
||||
if (contactB.vertex === supportA || contactA.vertex === supportB) {
|
||||
contacts[1] = contactA;
|
||||
contacts[0] = contactA = contactB;
|
||||
contactB = contacts[1];
|
||||
}
|
||||
|
||||
// update contacts
|
||||
contactA.vertex = supportA;
|
||||
contactB.vertex = supportB;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -102,7 +104,7 @@ var Contact = require('./Contact');
|
|||
pair.timeUpdated = timestamp;
|
||||
} else {
|
||||
pair.isActive = false;
|
||||
pair.activeContacts.length = 0;
|
||||
pair.contactCount = 0;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -114,11 +116,8 @@ var Contact = require('./Contact');
|
|||
* @return {string} Unique pairId
|
||||
*/
|
||||
Pair.id = function(bodyA, bodyB) {
|
||||
if (bodyA.id < bodyB.id) {
|
||||
return 'A' + bodyA.id + 'B' + bodyB.id;
|
||||
} else {
|
||||
return 'A' + bodyB.id + 'B' + bodyA.id;
|
||||
}
|
||||
return bodyA.id < bodyB.id ? bodyA.id.toString(36) + ':' + bodyB.id.toString(36)
|
||||
: bodyB.id.toString(36) + ':' + bodyA.id.toString(36);
|
||||
};
|
||||
|
||||
})();
|
||||
|
|
|
@ -37,27 +37,24 @@ var Common = require('../core/Common');
|
|||
* @param {number} timestamp
|
||||
*/
|
||||
Pairs.update = function(pairs, collisions, timestamp) {
|
||||
var pairsList = pairs.list,
|
||||
pairsListLength = pairsList.length,
|
||||
var pairUpdate = Pair.update,
|
||||
pairCreate = Pair.create,
|
||||
pairSetActive = Pair.setActive,
|
||||
pairsTable = pairs.table,
|
||||
collisionsLength = collisions.length,
|
||||
pairsList = pairs.list,
|
||||
pairsListLength = pairsList.length,
|
||||
pairsListIndex = pairsListLength,
|
||||
collisionStart = pairs.collisionStart,
|
||||
collisionEnd = pairs.collisionEnd,
|
||||
collisionActive = pairs.collisionActive,
|
||||
collisionsLength = collisions.length,
|
||||
collisionStartIndex = 0,
|
||||
collisionEndIndex = 0,
|
||||
collisionActiveIndex = 0,
|
||||
collision,
|
||||
pairIndex,
|
||||
pair,
|
||||
i;
|
||||
|
||||
// clear collision state arrays, but maintain old reference
|
||||
collisionStart.length = 0;
|
||||
collisionEnd.length = 0;
|
||||
collisionActive.length = 0;
|
||||
|
||||
for (i = 0; i < pairsListLength; i++) {
|
||||
pairsList[i].confirmedActive = false;
|
||||
}
|
||||
|
||||
for (i = 0; i < collisionsLength; i++) {
|
||||
collision = collisions[i];
|
||||
pair = collision.pair;
|
||||
|
@ -66,50 +63,63 @@ var Common = require('../core/Common');
|
|||
// 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);
|
||||
collisionActive[collisionActiveIndex++] = pair;
|
||||
}
|
||||
|
||||
// update the pair
|
||||
Pair.update(pair, collision, timestamp);
|
||||
pair.confirmedActive = true;
|
||||
pairUpdate(pair, collision, timestamp);
|
||||
} else {
|
||||
// pair did not exist, create a new pair
|
||||
pair = Pair.create(collision, timestamp);
|
||||
pair = pairCreate(collision, timestamp);
|
||||
pairsTable[pair.id] = pair;
|
||||
|
||||
// push the new pair
|
||||
collisionStart.push(pair);
|
||||
pairsList.push(pair);
|
||||
// add the new pair
|
||||
collisionStart[collisionStartIndex++] = pair;
|
||||
pairsList[pairsListIndex++] = pair;
|
||||
}
|
||||
}
|
||||
|
||||
// find pairs that are no longer active
|
||||
var removePairIndex = [];
|
||||
pairsListIndex = 0;
|
||||
pairsListLength = pairsList.length;
|
||||
|
||||
for (i = 0; i < pairsListLength; i++) {
|
||||
pair = pairsList[i];
|
||||
|
||||
if (!pair.confirmedActive) {
|
||||
Pair.setActive(pair, false, timestamp);
|
||||
collisionEnd.push(pair);
|
||||
// pair is active if updated this timestep
|
||||
if (pair.timeUpdated >= timestamp) {
|
||||
// keep active pairs
|
||||
pairsList[pairsListIndex++] = pair;
|
||||
} else {
|
||||
pairSetActive(pair, false, timestamp);
|
||||
|
||||
if (!pair.collision.bodyA.isSleeping && !pair.collision.bodyB.isSleeping) {
|
||||
removePairIndex.push(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// remove inactive pairs
|
||||
for (i = 0; i < removePairIndex.length; i++) {
|
||||
pairIndex = removePairIndex[i] - i;
|
||||
pair = pairsList[pairIndex];
|
||||
pairsList.splice(pairIndex, 1);
|
||||
// keep inactive pairs if both bodies may be sleeping
|
||||
if (pair.collision.bodyA.sleepCounter > 0 && pair.collision.bodyB.sleepCounter > 0) {
|
||||
pairsList[pairsListIndex++] = pair;
|
||||
} else {
|
||||
// remove inactive pairs if either body awake
|
||||
collisionEnd[collisionEndIndex++] = pair;
|
||||
delete pairsTable[pair.id];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// update array lengths if changed
|
||||
if (pairsList.length !== pairsListIndex) {
|
||||
pairsList.length = pairsListIndex;
|
||||
}
|
||||
|
||||
if (collisionStart.length !== collisionStartIndex) {
|
||||
collisionStart.length = collisionStartIndex;
|
||||
}
|
||||
|
||||
if (collisionEnd.length !== collisionEndIndex) {
|
||||
collisionEnd.length = collisionEndIndex;
|
||||
}
|
||||
|
||||
if (collisionActive.length !== collisionActiveIndex) {
|
||||
collisionActive.length = collisionActiveIndex;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -29,7 +29,7 @@ var Bounds = require('../geometry/Bounds');
|
|||
Resolver.preSolvePosition = function(pairs) {
|
||||
var i,
|
||||
pair,
|
||||
activeCount,
|
||||
contactCount,
|
||||
pairsLength = pairs.length;
|
||||
|
||||
// find total contacts on each body
|
||||
|
@ -39,9 +39,9 @@ var Bounds = require('../geometry/Bounds');
|
|||
if (!pair.isActive)
|
||||
continue;
|
||||
|
||||
activeCount = pair.activeContacts.length;
|
||||
pair.collision.parentA.totalContacts += activeCount;
|
||||
pair.collision.parentB.totalContacts += activeCount;
|
||||
contactCount = pair.contactCount;
|
||||
pair.collision.parentA.totalContacts += contactCount;
|
||||
pair.collision.parentB.totalContacts += contactCount;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -79,8 +79,8 @@ var Bounds = require('../geometry/Bounds');
|
|||
|
||||
// get current separation between body edges involved in collision
|
||||
pair.separation =
|
||||
normal.x * (bodyB.positionImpulse.x + collision.penetration.x - bodyA.positionImpulse.x)
|
||||
+ normal.y * (bodyB.positionImpulse.y + collision.penetration.y - bodyA.positionImpulse.y);
|
||||
collision.depth + normal.x * (bodyB.positionImpulse.x - bodyA.positionImpulse.x)
|
||||
+ normal.y * (bodyB.positionImpulse.y - bodyA.positionImpulse.y);
|
||||
}
|
||||
|
||||
for (i = 0; i < pairsLength; i++) {
|
||||
|
@ -176,8 +176,8 @@ var Bounds = require('../geometry/Bounds');
|
|||
if (!pair.isActive || pair.isSensor)
|
||||
continue;
|
||||
|
||||
var contacts = pair.activeContacts,
|
||||
contactsLength = contacts.length,
|
||||
var contacts = pair.contacts,
|
||||
contactCount = pair.contactCount,
|
||||
collision = pair.collision,
|
||||
bodyA = collision.parentA,
|
||||
bodyB = collision.parentB,
|
||||
|
@ -185,7 +185,7 @@ var Bounds = require('../geometry/Bounds');
|
|||
tangent = collision.tangent;
|
||||
|
||||
// resolve each contact
|
||||
for (j = 0; j < contactsLength; j++) {
|
||||
for (j = 0; j < contactCount; j++) {
|
||||
var contact = contacts[j],
|
||||
contactVertex = contact.vertex,
|
||||
normalImpulse = contact.normalImpulse,
|
||||
|
@ -248,28 +248,26 @@ var Bounds = require('../geometry/Bounds');
|
|||
var collision = pair.collision,
|
||||
bodyA = collision.parentA,
|
||||
bodyB = collision.parentB,
|
||||
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,
|
||||
contactsLength = contacts.length,
|
||||
contactShare = 1 / contactsLength,
|
||||
inverseMassTotal = bodyA.inverseMass + bodyB.inverseMass,
|
||||
friction = pair.friction * pair.frictionStatic * frictionNormalMultiplier;
|
||||
inverseMassTotal = pair.inverseMass,
|
||||
friction = pair.friction * pair.frictionStatic * frictionNormalMultiplier,
|
||||
contacts = pair.contacts,
|
||||
contactCount = pair.contactCount,
|
||||
contactShare = 1 / contactCount;
|
||||
|
||||
// update body velocities
|
||||
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;
|
||||
// get body velocities
|
||||
var bodyAVelocityX = bodyA.position.x - bodyA.positionPrev.x,
|
||||
bodyAVelocityY = bodyA.position.y - bodyA.positionPrev.y,
|
||||
bodyAAngularVelocity = bodyA.angle - bodyA.anglePrev,
|
||||
bodyBVelocityX = bodyB.position.x - bodyB.positionPrev.x,
|
||||
bodyBVelocityY = bodyB.position.y - bodyB.positionPrev.y,
|
||||
bodyBAngularVelocity = bodyB.angle - bodyB.anglePrev;
|
||||
|
||||
// resolve each contact
|
||||
for (j = 0; j < contactsLength; j++) {
|
||||
for (j = 0; j < contactCount; j++) {
|
||||
var contact = contacts[j],
|
||||
contactVertex = contact.vertex;
|
||||
|
||||
|
@ -278,10 +276,10 @@ var Bounds = require('../geometry/Bounds');
|
|||
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;
|
||||
var velocityPointAX = bodyAVelocityX - offsetAY * bodyAAngularVelocity,
|
||||
velocityPointAY = bodyAVelocityY + offsetAX * bodyAAngularVelocity,
|
||||
velocityPointBX = bodyBVelocityX - offsetBY * bodyBAngularVelocity,
|
||||
velocityPointBY = bodyBVelocityY + offsetBX * bodyBAngularVelocity;
|
||||
|
||||
var relativeVelocityX = velocityPointAX - velocityPointBX,
|
||||
relativeVelocityY = velocityPointAY - velocityPointBY;
|
||||
|
|
|
@ -63,6 +63,7 @@ var Body = require('../body/Body');
|
|||
engine.world = options.world || Composite.create({ label: 'World' });
|
||||
engine.pairs = options.pairs || Pairs.create();
|
||||
engine.detector = options.detector || Detector.create();
|
||||
engine.detector.pairs = engine.pairs;
|
||||
|
||||
// for temporary back compatibility only
|
||||
engine.grid = { buckets: [] };
|
||||
|
@ -148,7 +149,6 @@ var Body = require('../body/Body');
|
|||
Constraint.postSolveAll(allBodies);
|
||||
|
||||
// find all collisions
|
||||
detector.pairs = engine.pairs;
|
||||
var collisions = Detector.collisions(detector);
|
||||
|
||||
// update collision pairs
|
||||
|
|
|
@ -1222,8 +1222,8 @@ var Mouse = require('../core/Mouse');
|
|||
continue;
|
||||
|
||||
collision = pair.collision;
|
||||
for (j = 0; j < pair.activeContacts.length; j++) {
|
||||
var contact = pair.activeContacts[j],
|
||||
for (j = 0; j < pair.contactCount; j++) {
|
||||
var contact = pair.contacts[j],
|
||||
vertex = contact.vertex;
|
||||
c.rect(vertex.x - 1.5, vertex.y - 1.5, 3.5, 3.5);
|
||||
}
|
||||
|
@ -1247,13 +1247,13 @@ var Mouse = require('../core/Mouse');
|
|||
|
||||
collision = pair.collision;
|
||||
|
||||
if (pair.activeContacts.length > 0) {
|
||||
var normalPosX = pair.activeContacts[0].vertex.x,
|
||||
normalPosY = pair.activeContacts[0].vertex.y;
|
||||
if (pair.contactCount > 0) {
|
||||
var normalPosX = pair.contacts[0].vertex.x,
|
||||
normalPosY = pair.contacts[0].vertex.y;
|
||||
|
||||
if (pair.activeContacts.length === 2) {
|
||||
normalPosX = (pair.activeContacts[0].vertex.x + pair.activeContacts[1].vertex.x) / 2;
|
||||
normalPosY = (pair.activeContacts[0].vertex.y + pair.activeContacts[1].vertex.y) / 2;
|
||||
if (pair.contactCount === 2) {
|
||||
normalPosX = (pair.contacts[0].vertex.x + pair.contacts[1].vertex.x) / 2;
|
||||
normalPosY = (pair.contacts[0].vertex.y + pair.contacts[1].vertex.y) / 2;
|
||||
}
|
||||
|
||||
if (collision.bodyB === collision.supports[0].body || collision.bodyA.isStatic === true) {
|
||||
|
|
|
@ -3,8 +3,9 @@
|
|||
"use strict";
|
||||
|
||||
const mock = require('mock-require');
|
||||
const { requireUncached, serialize } = require('./TestTools');
|
||||
const { requireUncached, serialize, smoothExp } = require('./TestTools');
|
||||
const consoleOriginal = global.console;
|
||||
const DateOriginal = global.Date;
|
||||
|
||||
const runExample = options => {
|
||||
const {
|
||||
|
@ -13,8 +14,8 @@ const runExample = options => {
|
|||
frameCallbacks
|
||||
} = prepareEnvironment(options);
|
||||
|
||||
let totalMemory = 0;
|
||||
let totalDuration = 0;
|
||||
let memoryDeltaAverage = 0;
|
||||
let timeDeltaAverage = 0;
|
||||
let overlapTotal = 0;
|
||||
let overlapCount = 0;
|
||||
let i;
|
||||
|
@ -24,6 +25,12 @@ const runExample = options => {
|
|||
let runner;
|
||||
let engine;
|
||||
let render;
|
||||
let extrinsicCapture;
|
||||
|
||||
const pairOverlap = (pair) => {
|
||||
const collision = Matter.Collision.collides(pair.bodyA, pair.bodyB);
|
||||
return collision ? Math.max(collision.depth - pair.slop, 0) : -1;
|
||||
};
|
||||
|
||||
for (i = 0; i < options.repeats; i += 1) {
|
||||
if (global.gc) {
|
||||
|
@ -41,29 +48,51 @@ const runExample = options => {
|
|||
const time = j * runner.delta;
|
||||
const callbackCount = frameCallbacks.length;
|
||||
|
||||
global.timeNow = time;
|
||||
|
||||
for (let p = 0; p < callbackCount; p += 1) {
|
||||
totalMemory += process.memoryUsage().heapUsed;
|
||||
const callback = frameCallbacks.shift();
|
||||
const startTime = process.hrtime();
|
||||
const frameCallback = frameCallbacks.shift();
|
||||
const memoryBefore = process.memoryUsage().heapUsed;
|
||||
const timeBefore = process.hrtime();
|
||||
|
||||
callback(time);
|
||||
frameCallback(time);
|
||||
|
||||
const duration = process.hrtime(startTime);
|
||||
totalMemory += process.memoryUsage().heapUsed;
|
||||
totalDuration += duration[0] * 1e9 + duration[1];
|
||||
const timeDuration = process.hrtime(timeBefore);
|
||||
const timeDelta = timeDuration[0] * 1e9 + timeDuration[1];
|
||||
const memoryAfter = process.memoryUsage().heapUsed;
|
||||
const memoryDelta = Math.max(memoryAfter - memoryBefore, 0);
|
||||
|
||||
memoryDeltaAverage = smoothExp(memoryDeltaAverage, memoryDelta);
|
||||
timeDeltaAverage = smoothExp(timeDeltaAverage, timeDelta);
|
||||
}
|
||||
|
||||
let overlapTotalUpdate = 0;
|
||||
let overlapCountUpdate = 0;
|
||||
|
||||
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) {
|
||||
overlapTotal += separation > 0 ? separation : 0;
|
||||
if (pair.isActive && !pair.isSensor){
|
||||
const overlap = pairOverlap(pair);
|
||||
|
||||
if (overlap >= 0) {
|
||||
overlapTotalUpdate += overlap;
|
||||
overlapCountUpdate += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (overlapCountUpdate > 0) {
|
||||
overlapTotal += overlapTotalUpdate / overlapCountUpdate;
|
||||
overlapCount += 1;
|
||||
}
|
||||
|
||||
if (!extrinsicCapture && engine.timing.timestamp >= 1000) {
|
||||
extrinsicCapture = captureExtrinsics(engine, Matter);
|
||||
extrinsicCapture.updates = j;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -72,13 +101,13 @@ const runExample = options => {
|
|||
|
||||
return {
|
||||
name: options.name,
|
||||
duration: totalDuration,
|
||||
duration: timeDeltaAverage,
|
||||
memory: memoryDeltaAverage,
|
||||
overlap: overlapTotal / (overlapCount || 1),
|
||||
memory: totalMemory,
|
||||
logs: logs,
|
||||
extrinsic: captureExtrinsics(engine, Matter),
|
||||
extrinsic: extrinsicCapture,
|
||||
intrinsic: captureIntrinsics(engine, Matter),
|
||||
state: captureState(engine, runner, render)
|
||||
state: captureState(engine, runner, render),
|
||||
logs
|
||||
};
|
||||
|
||||
} catch (err) {
|
||||
|
@ -144,6 +173,7 @@ const prepareEnvironment = options => {
|
|||
const frameCallbacks = [];
|
||||
|
||||
global.document = global.window = {
|
||||
performance: {},
|
||||
addEventListener: () => {},
|
||||
requestAnimationFrame: callback => {
|
||||
frameCallbacks.push(callback);
|
||||
|
@ -174,6 +204,21 @@ const prepareEnvironment = options => {
|
|||
}
|
||||
};
|
||||
|
||||
global.Math.random = () => {
|
||||
throw new Error("Math.random was called during tests, output can not be compared.");
|
||||
};
|
||||
|
||||
global.timeNow = 0;
|
||||
|
||||
global.window.performance.now = () => global.timeNow;
|
||||
|
||||
global.Date = function() {
|
||||
this.toString = () => global.timeNow.toString();
|
||||
this.valueOf = () => global.timeNow;
|
||||
};
|
||||
|
||||
global.Date.now = () => global.timeNow;
|
||||
|
||||
const Matter = prepareMatter(options);
|
||||
mock('matter-js', Matter);
|
||||
global.Matter = Matter;
|
||||
|
@ -187,6 +232,7 @@ const prepareEnvironment = options => {
|
|||
|
||||
const resetEnvironment = () => {
|
||||
global.console = consoleOriginal;
|
||||
global.Date = DateOriginal;
|
||||
global.window = undefined;
|
||||
global.document = undefined;
|
||||
global.Matter = undefined;
|
||||
|
@ -195,46 +241,22 @@ const resetEnvironment = () => {
|
|||
|
||||
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), [])
|
||||
];
|
||||
bodies[body.id] = {
|
||||
position: { x: body.position.x, y: body.position.y },
|
||||
vertices: body.vertices.map(vertex => ({ x: vertex.x, y: vertex.y }))
|
||||
};
|
||||
|
||||
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({
|
||||
const captureIntrinsics = ({ world, timing }, Matter) => serialize({
|
||||
engine: {
|
||||
timing: {
|
||||
timeScale: timing.timeScale,
|
||||
timestamp: timing.timestamp
|
||||
}
|
||||
},
|
||||
bodies: Matter.Composite.allBodies(world).reduce((bodies, body) => {
|
||||
bodies[body.id] = body;
|
||||
return bodies;
|
||||
|
@ -289,6 +311,9 @@ const extrinsicProperties = [
|
|||
'velocity',
|
||||
'position',
|
||||
'positionPrev',
|
||||
'motion',
|
||||
'sleepCounter',
|
||||
'positionImpulse'
|
||||
];
|
||||
|
||||
const excludeStateProperties = [
|
||||
|
|
|
@ -17,13 +17,14 @@ const {
|
|||
const Example = requireUncached('../examples/index');
|
||||
const MatterBuild = requireUncached('../build/matter');
|
||||
const { versionSatisfies } = requireUncached('../src/core/Plugin');
|
||||
const Worker = require('jest-worker').default;
|
||||
const Worker = require('jest-worker').Worker;
|
||||
|
||||
const testComparison = getArg('compare', null) === 'true';
|
||||
const saveComparison = getArg('save', null) === 'true';
|
||||
const specificExamples = getArg('examples', null, (val) => val.split(','));
|
||||
const repeats = getArg('repeats', 1, parseFloat);
|
||||
const updates = getArg('updates', 150, parseFloat);
|
||||
const benchmark = getArg('benchmark', null) === 'true';
|
||||
|
||||
const excludeExamples = ['svg', 'terrain'];
|
||||
const excludeJitter = ['stack', 'circleStack', 'restitution', 'staticFriction', 'friction', 'newtonsCradle', 'catapult'];
|
||||
|
@ -37,68 +38,26 @@ const examples = (specificExamples || Object.keys(Example)).filter(key => {
|
|||
});
|
||||
|
||||
const captureExamples = async useDev => {
|
||||
const multiThreadWorker = new Worker(require.resolve('./ExampleWorker'), {
|
||||
enableWorkerThreads: true
|
||||
});
|
||||
|
||||
const overlapRuns = await Promise.all(examples.map(name => multiThreadWorker.runExample({
|
||||
name,
|
||||
useDev,
|
||||
updates: 2,
|
||||
repeats: 1,
|
||||
stableSort: true,
|
||||
jitter: excludeJitter.includes(name) ? 0 : 1e-10
|
||||
})));
|
||||
|
||||
const behaviourRuns = await Promise.all(examples.map(name => multiThreadWorker.runExample({
|
||||
name,
|
||||
useDev,
|
||||
updates: 2,
|
||||
repeats: 1,
|
||||
stableSort: true,
|
||||
jitter: excludeJitter.includes(name) ? 0 : 1e-10
|
||||
})));
|
||||
|
||||
const similarityRuns = await Promise.all(examples.map(name => multiThreadWorker.runExample({
|
||||
name,
|
||||
useDev,
|
||||
updates: 2,
|
||||
repeats: 1,
|
||||
stableSort: false,
|
||||
jitter: excludeJitter.includes(name) ? 0 : 1e-10
|
||||
})));
|
||||
|
||||
await multiThreadWorker.end();
|
||||
|
||||
const singleThreadWorker = new Worker(require.resolve('./ExampleWorker'), {
|
||||
const worker = new Worker(require.resolve('./ExampleWorker'), {
|
||||
enableWorkerThreads: true,
|
||||
numWorkers: 1
|
||||
numWorkers: benchmark ? 1 : undefined
|
||||
});
|
||||
|
||||
const completeRuns = await Promise.all(examples.map(name => singleThreadWorker.runExample({
|
||||
const completeRuns = await Promise.all(examples.map(name => worker.runExample({
|
||||
name,
|
||||
useDev,
|
||||
updates: updates,
|
||||
repeats: repeats,
|
||||
repeats: benchmark ? Math.max(repeats, 3) : repeats,
|
||||
stableSort: false,
|
||||
jitter: excludeJitter.includes(name) ? 0 : 1e-10
|
||||
})));
|
||||
|
||||
await singleThreadWorker.end();
|
||||
await worker.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
|
||||
};
|
||||
capture[completeRun.name] = completeRun;
|
||||
}
|
||||
|
||||
return capture;
|
||||
|
@ -119,7 +78,7 @@ afterAll(async () => {
|
|||
'Examples ran against previous release and current build\n\n'
|
||||
+ logReport(build, `release`) + '\n'
|
||||
+ logReport(dev, `current`) + '\n'
|
||||
+ comparisonReport(dev, build, devSize, buildSize, MatterBuild.version, saveComparison)
|
||||
+ comparisonReport(dev, build, devSize, buildSize, MatterBuild.version, saveComparison, benchmark)
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -8,25 +8,26 @@ 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 equalityThreshold = 1;
|
||||
const colors = { Red: 31, Green: 32, Yellow: 33, White: 37, BrightWhite: 90, BrightCyan: 36 };
|
||||
|
||||
const comparisonReport = (capturesDev, capturesBuild, devSize, buildSize, buildVersion, save) => {
|
||||
const performanceDev = capturePerformanceTotals(capturesDev);
|
||||
const performanceBuild = capturePerformanceTotals(capturesBuild);
|
||||
const comparisonReport = (capturesDev, capturesBuild, devSize, buildSize, buildVersion, save, benchmark) => {
|
||||
const {
|
||||
durationChange,
|
||||
memoryChange,
|
||||
overlapChange
|
||||
} = captureBenchmark(capturesDev, capturesBuild);
|
||||
|
||||
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;
|
||||
|
||||
const behaviourSimilaritys = extrinsicSimilarity(capturesDev, capturesBuild, 'behaviourExtrinsic');
|
||||
const behaviourSimilarityAverage = extrinsicSimilarityAverage(behaviourSimilaritys);
|
||||
const behaviourSimilarityEntries = Object.entries(behaviourSimilaritys);
|
||||
behaviourSimilarityEntries.sort((a, b) => a[1] - b[1]);
|
||||
const firstCapture = Object.entries(capturesDev)[0][1];
|
||||
const updates = firstCapture.extrinsic.updates;
|
||||
|
||||
const similaritys = extrinsicSimilarity(capturesDev, capturesBuild, 'similarityExtrinsic');
|
||||
const similaritys = extrinsicSimilarity(capturesDev, capturesBuild);
|
||||
const similarityAverage = extrinsicSimilarityAverage(similaritys);
|
||||
const similarityAveragePerUpdate = Math.pow(1, -1 / updates) * Math.pow(similarityAverage, 1 / updates);
|
||||
const similarityEntries = Object.entries(similaritys);
|
||||
similarityEntries.sort((a, b) => a[1] - b[1]);
|
||||
|
||||
const devIntrinsicsChanged = {};
|
||||
const buildIntrinsicsChanged = {};
|
||||
|
@ -50,32 +51,31 @@ const comparisonReport = (capturesDev, capturesBuild, devSize, buildSize, buildV
|
|||
.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)}`
|
||||
[`Output sample comparison estimates of ${similarityEntries.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)}%`,
|
||||
`\n\n${format(`Similarity`, colors.White)} `,
|
||||
`${format(formatPercent(similarityAveragePerUpdate, false, true), formatColor(similarityAveragePerUpdate === 1))}% `,
|
||||
|
||||
` ${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(formatPercent(overlapChange), formatColor(overlapChange <= 0))}%`,
|
||||
|
||||
` ${format('Filesize', colors.White)}`,
|
||||
`${format((filesizeChange >= 0 ? '+' : '-') + formatPercent(filesizeChange, true), filesizeChange <= 0 ? colors.Green : colors.Yellow)}%`,
|
||||
`${format(formatPercent(filesizeChange), formatColor(filesizeChange <= 0))}%`,
|
||||
`${format(`${(devSize / 1024).toPrecision(4)} KB`, colors.White)}`,
|
||||
|
||||
...(benchmark ? [
|
||||
`\n${format('Performance', colors.White)}`,
|
||||
` ${format(formatPercent(durationChange), formatColor(durationChange >= 0))}%`,
|
||||
|
||||
` ${format('Memory', colors.White)} `,
|
||||
` ${format(formatPercent(memoryChange), formatColor(memoryChange <= 0))}%`,
|
||||
] : []),
|
||||
|
||||
captureSummary.reduce((output, p, i) => {
|
||||
output += `${p.name} `;
|
||||
output += `${similarityRatings(behaviourSimilaritys[p.name])} `;
|
||||
output += `${similarityRatings(similaritys[p.name])} `;
|
||||
output += `${changeRatings(capturesDev[p.name].changedIntrinsics)} `;
|
||||
if (i > 0 && i < captureSummary.length && breakEvery > 0 && i % breakEvery === 0) {
|
||||
output += '\n';
|
||||
|
@ -83,9 +83,9 @@ const comparisonReport = (capturesDev, capturesBuild, devSize, buildSize, buildV
|
|||
return output;
|
||||
}, '\n\n'),
|
||||
|
||||
`\n\nwhere · no change ● extrinsics changed ◆ intrinsics changed\n`,
|
||||
`\n\nwhere for the sample · no change detected ● extrinsics changed ◆ intrinsics changed\n`,
|
||||
|
||||
behaviourSimilarityAverage < 1 ? `\n${format('▶', colors.White)} ${format(compareCommand + '=' + 150 + '#' + behaviourSimilarityEntries[0][0], colors.BrightCyan)}` : '',
|
||||
similarityAverage < 1 ? `\n${format('▶', colors.White)} ${format(compareCommand + '=' + 150 + '#' + similarityEntries[0][0], colors.BrightCyan)}` : '',
|
||||
intrinsicChangeCount > 0 ? `\n${format('▶', colors.White)} ${format((save ? diffCommand : diffSaveCommand), colors.BrightCyan)}` : ''
|
||||
].join(' ');
|
||||
|
||||
|
@ -98,17 +98,29 @@ const comparisonReport = (capturesDev, capturesBuild, devSize, buildSize, buildV
|
|||
return report(5, color);
|
||||
};
|
||||
|
||||
const similarity = (a, b) => {
|
||||
const distance = Math.sqrt(a.reduce(
|
||||
(sum, _val, i) => sum + Math.pow((a[i] || 0) - (b[i] || 0), 2), 0)
|
||||
);
|
||||
return 1 / (1 + (distance / a.length));
|
||||
};
|
||||
|
||||
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 formatColor = isGreen => isGreen ? colors.Green : colors.Yellow;
|
||||
|
||||
const formatPercent = (val, showSign=true, showFractional=false, padStart=6) => {
|
||||
let fractionalSign = '';
|
||||
|
||||
if (showFractional && val > 0.9999 && val < 1) {
|
||||
val = 0.9999;
|
||||
fractionalSign = '>';
|
||||
} else if (showFractional && val > 0 && val < 0.0001) {
|
||||
val = 0.0001;
|
||||
fractionalSign = '<';
|
||||
}
|
||||
|
||||
const percentFixed = Math.abs(100 * val).toFixed(2);
|
||||
const sign = parseFloat((100 * val).toFixed(2)) >= 0 ? '+' : '-';
|
||||
return ((showFractional ? fractionalSign : '') + (showSign ? sign : '') + percentFixed).padStart(padStart, ' ');
|
||||
};
|
||||
|
||||
const noiseThreshold = (val, threshold) => {
|
||||
const sign = val < 0 ? -1 : 1;
|
||||
|
@ -116,6 +128,38 @@ const noiseThreshold = (val, threshold) => {
|
|||
return sign * Math.max(0, magnitude - threshold) / (1 - threshold);
|
||||
};
|
||||
|
||||
const median = (values, lower, upper) => {
|
||||
const valuesSorted = values.slice(0).sort();
|
||||
|
||||
return mean(valuesSorted.slice(
|
||||
Math.floor(valuesSorted.length * lower),
|
||||
Math.floor(valuesSorted.length * upper)
|
||||
));
|
||||
};
|
||||
|
||||
const mean = (values) => {
|
||||
const valuesLength = values.length;
|
||||
let result = 0;
|
||||
|
||||
for (let i = 0; i < valuesLength; i += 1) {
|
||||
result += values[i];
|
||||
}
|
||||
|
||||
return (result / valuesLength) || 0;
|
||||
};
|
||||
|
||||
const smoothExp = (last, current) => {
|
||||
const delta = current - last;
|
||||
const sign = delta < 0 ? -1 : 1;
|
||||
const magnitude = Math.abs(delta);
|
||||
|
||||
if (magnitude < 1) {
|
||||
return last + 0.01 * delta;
|
||||
}
|
||||
|
||||
return last + Math.sqrt(magnitude) * sign;
|
||||
};
|
||||
|
||||
const equals = (a, b) => {
|
||||
try {
|
||||
expect(a).toEqual(b);
|
||||
|
@ -125,41 +169,77 @@ const equals = (a, b) => {
|
|||
return true;
|
||||
};
|
||||
|
||||
const capturePerformanceTotals = (captures) => {
|
||||
const totals = {
|
||||
duration: 0,
|
||||
overlap: 0,
|
||||
memory: 0
|
||||
const captureBenchmark = (capturesDev, capturesBuild) => {
|
||||
const overlapChanges = [];
|
||||
|
||||
let durationDev = 0;
|
||||
let durationBuild = 0;
|
||||
let memoryDev = 0;
|
||||
let memoryBuild = 0;
|
||||
|
||||
for (const name in capturesDev) {
|
||||
durationDev += capturesDev[name].duration;
|
||||
durationBuild += capturesBuild[name].duration;
|
||||
|
||||
memoryDev += capturesDev[name].memory;
|
||||
memoryBuild += capturesBuild[name].memory;
|
||||
|
||||
if (capturesBuild[name].overlap > 0.1 && capturesDev[name].overlap > 0.1){
|
||||
overlapChanges.push(capturesDev[name].overlap / capturesBuild[name].overlap);
|
||||
}
|
||||
};
|
||||
|
||||
for (const [ name ] of Object.entries(captures)) {
|
||||
totals.duration += captures[name].duration;
|
||||
totals.overlap += captures[name].overlap;
|
||||
totals.memory += captures[name].memory;
|
||||
};
|
||||
const durationChange = 1 - noiseThreshold(durationDev / durationBuild, 0.02);
|
||||
const memoryChange = noiseThreshold(memoryDev / memoryBuild, 0.02) - 1;
|
||||
const overlapChange = noiseThreshold(median(overlapChanges, 0.45, 0.55), 0.001) - 1;
|
||||
|
||||
return totals;
|
||||
return {
|
||||
durationChange,
|
||||
memoryChange,
|
||||
overlapChange
|
||||
};
|
||||
};
|
||||
|
||||
const extrinsicSimilarity = (currentCaptures, referenceCaptures, key) => {
|
||||
const extrinsicSimilarity = (currentCaptures, referenceCaptures, key='extrinsic') => {
|
||||
const result = {};
|
||||
const zeroVector = { x: 0, y: 0 };
|
||||
|
||||
Object.entries(currentCaptures).forEach(([name, current]) => {
|
||||
const reference = referenceCaptures[name];
|
||||
const worldVector = [];
|
||||
const worldVectorRef = [];
|
||||
const currentExtrinsic = current[key];
|
||||
const referenceExtrinsic = reference[key];
|
||||
for (const name in currentCaptures) {
|
||||
const currentExtrinsic = currentCaptures[name][key];
|
||||
const referenceExtrinsic = referenceCaptures[name][key];
|
||||
|
||||
Object.keys(currentExtrinsic).forEach(objectType => {
|
||||
Object.keys(currentExtrinsic[objectType]).forEach(objectId => {
|
||||
worldVector.push(...currentExtrinsic[objectType][objectId]);
|
||||
worldVectorRef.push(...referenceExtrinsic[objectType][objectId]);
|
||||
});
|
||||
});
|
||||
let totalCount = 0;
|
||||
let totalSimilarity = 0;
|
||||
|
||||
result[name] = similarity(worldVector, worldVectorRef);
|
||||
});
|
||||
for (const objectType in currentExtrinsic) {
|
||||
for (const objectId in currentExtrinsic[objectType]) {
|
||||
const currentObject = currentExtrinsic[objectType][objectId];
|
||||
const referenceObject = referenceExtrinsic[objectType][objectId];
|
||||
|
||||
for (let i = 0; i < currentObject.vertices.length; i += 1) {
|
||||
const currentPosition = currentObject.position;
|
||||
const currentVertex = currentObject.vertices[i];
|
||||
const referenceVertex = referenceObject.vertices[i] ? referenceObject.vertices[i] : zeroVector;
|
||||
|
||||
const radius = Math.sqrt(
|
||||
Math.pow(currentVertex.x - currentPosition.x, 2)
|
||||
+ Math.pow(currentVertex.y - currentPosition.y, 2)
|
||||
);
|
||||
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(currentVertex.x - referenceVertex.x, 2)
|
||||
+ Math.pow(currentVertex.y - referenceVertex.y, 2)
|
||||
);
|
||||
|
||||
totalSimilarity += Math.min(1, distance / (2 * radius)) / currentObject.vertices.length;
|
||||
}
|
||||
|
||||
totalCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
result[name] = 1 - (totalSimilarity / totalCount);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
@ -291,6 +371,6 @@ const toMatchIntrinsics = {
|
|||
};
|
||||
|
||||
module.exports = {
|
||||
requireUncached, comparisonReport, logReport, getArg,
|
||||
requireUncached, comparisonReport, logReport, getArg, smoothExp,
|
||||
serialize, toMatchExtrinsics, toMatchIntrinsics
|
||||
};
|
Loading…
Reference in a new issue