0
0
Fork 0
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:
liabru 2024-03-11 21:33:04 +00:00
commit 2c91e7400f
20 changed files with 4850 additions and 5680 deletions

View file

@ -13,7 +13,7 @@ jobs:
strategy: strategy:
matrix: matrix:
node-version: [12.x, 14.x, 16.x] node-version: [16.x]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2

View file

@ -46,6 +46,7 @@
.matter-js-compare-build.matter-demo canvas { .matter-js-compare-build.matter-demo canvas {
opacity: 0.5; opacity: 0.5;
background: transparent !important; background: transparent !important;
z-index: 25 !important;
} }
</style> </style>
</head> </head>

97
demo/src/Multi.js Normal file
View 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 };

View file

@ -35,7 +35,6 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
border: 0; border: 0;
z-index: 1;
pointer-events: none; pointer-events: none;
} }
@ -43,9 +42,15 @@
display: none; display: none;
} }
.matter-js.dev.comparing.matter-demo canvas {
background: transparent !important;
z-index: 20;
}
.matter-js-compare-build.matter-demo canvas { .matter-js-compare-build.matter-demo canvas {
opacity: 0.5; opacity: 0.5;
background: transparent !important; background: transparent !important;
z-index: 15 !important;
} }
@media only screen and (min-width: 1300px) { @media only screen and (min-width: 1300px) {
@ -54,6 +59,11 @@
position: relative; position: relative;
z-index: 20; z-index: 20;
} }
.matter-js-compare-build.matter-demo canvas {
position: relative;
z-index: 15;
}
} }
</style> </style>
</head> </head>

View file

@ -12,6 +12,7 @@
var Matter = require('matter-js'); var Matter = require('matter-js');
var Examples = require('../../examples/index'); var Examples = require('../../examples/index');
var compare = require('./Compare').compare; var compare = require('./Compare').compare;
var multi = require('./Multi').multi;
var demo = require('./Demo').demo; var demo = require('./Demo').demo;
// browser globals // browser globals
@ -31,9 +32,13 @@ var examples = Matter.Common.keys(Examples).map(function(id){
// start the requested tool // start the requested tool
var isCompare = window.location.search.indexOf('compare') >= 0; var isCompare = window.location.search.indexOf('compare') >= 0;
var isMulti = window.location.search.indexOf('multi') >= 0;
var isDev = __MATTER_IS_DEV__; var isDev = __MATTER_IS_DEV__;
if (isCompare) { if (isCompare) {
compare(examples, isDev); compare(examples, isDev);
} else if (isMulti) {
multi(examples, isDev);
} else { } else {
demo(examples, isDev); demo(examples, isDev);
} }

View file

@ -13,8 +13,11 @@ Example.remove = function() {
Events = Matter.Events; Events = Matter.Events;
// create engine // create engine
var engine = Engine.create(), var engine = Engine.create({
world = engine.world; enableSleeping: true
});
var world = engine.world;
// create renderer // create renderer
var render = Render.create({ var render = Render.create({
@ -24,6 +27,7 @@ Example.remove = function() {
width: 800, width: 800,
height: 600, height: 600,
showAngleIndicator: true, showAngleIndicator: true,
showSleeping: true
} }
}); });
@ -33,9 +37,6 @@ Example.remove = function() {
var runner = Runner.create(); var runner = Runner.create();
Runner.run(runner, engine); Runner.run(runner, engine);
var stack = null,
lastTimestamp = 0;
var createStack = function() { var createStack = function() {
return Composites.stack(20, 20, 10, 5, 0, 0, function(x, y) { return Composites.stack(20, 20, 10, 5, 0, 0, function(x, y) {
var sides = Math.round(Common.random(1, 8)); 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) { Events.on(engine, 'afterUpdate', function(event) {
// limit rate // limit rate
if (stack && event.timestamp - lastTimestamp < 800) { if (event.timestamp - lastTimestamp < 800) {
return; return;
} }
lastTimestamp = event.timestamp; 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 // remove last stack
if (stack) { if (stack) {
Composite.remove(world, stack); Composite.remove(world, stack);
@ -82,10 +96,9 @@ Example.remove = function() {
Composite.add(world, stack); Composite.add(world, stack);
}); });
// add another stack that will not be removed
Composite.add(world, createStack());
Composite.add(world, [ Composite.add(world, [
bottomStack,
// walls // walls
Bodies.rectangle(400, 0, 800, 50, { isStatic: true }), Bodies.rectangle(400, 0, 800, 50, { isStatic: true }),
Bodies.rectangle(400, 600, 800, 50, { isStatic: true }), Bodies.rectangle(400, 600, 800, 50, { isStatic: true }),

9638
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -23,8 +23,8 @@
"conventional-changelog-cli": "^2.1.1", "conventional-changelog-cli": "^2.1.1",
"eslint": "^6.8.0", "eslint": "^6.8.0",
"html-webpack-plugin": "^4.5.1", "html-webpack-plugin": "^4.5.1",
"jest": "^25.1.0", "jest": "^29.7.0",
"jest-worker": "^24.9.0", "jest-worker": "^29.7.0",
"json-stringify-pretty-compact": "^2.0.0", "json-stringify-pretty-compact": "^2.0.0",
"matter-tools": "^0.14.0", "matter-tools": "^0.14.0",
"matter-wrap": "^0.2.0", "matter-wrap": "^0.2.0",
@ -50,7 +50,7 @@
"lint": "eslint 'src/**/*.js' 'demo/src/**/*.js' 'examples/*.js' 'webpack.*.js'", "lint": "eslint 'src/**/*.js' 'demo/src/**/*.js' 'examples/*.js' 'webpack.*.js'",
"doc": "yuidoc --config yuidoc.json --project-version $npm_package_version", "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'", "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": "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-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-browser": "node --expose-gc node_modules/.bin/jest --force-exit --no-cache --runInBand ./test/Browser.spec.js",

View file

@ -192,8 +192,15 @@ var Body = require('./Body');
*/ */
Composite.removeComposite = function(compositeA, compositeB, deep) { Composite.removeComposite = function(compositeA, compositeB, deep) {
var position = Common.indexOf(compositeA.composites, compositeB); var position = Common.indexOf(compositeA.composites, compositeB);
if (position !== -1) { if (position !== -1) {
var bodies = Composite.allBodies(compositeB);
Composite.removeCompositeAt(compositeA, position); Composite.removeCompositeAt(compositeA, position);
for (var i = 0; i < bodies.length; i++) {
bodies[i].sleepCounter = 0;
}
} }
if (deep) { if (deep) {
@ -244,8 +251,10 @@ var Body = require('./Body');
*/ */
Composite.removeBody = function(composite, body, deep) { Composite.removeBody = function(composite, body, deep) {
var position = Common.indexOf(composite.bodies, body); var position = Common.indexOf(composite.bodies, body);
if (position !== -1) { if (position !== -1) {
Composite.removeBodyAt(composite, position); Composite.removeBodyAt(composite, position);
body.sleepCounter = 0;
} }
if (deep) { if (deep) {
@ -296,6 +305,7 @@ var Body = require('./Body');
*/ */
Composite.removeConstraint = function(composite, constraint, deep) { Composite.removeConstraint = function(composite, constraint, deep) {
var position = Common.indexOf(composite.constraints, constraint); var position = Common.indexOf(composite.constraints, constraint);
if (position !== -1) { if (position !== -1) {
Composite.removeConstraintAt(composite, position); Composite.removeConstraintAt(composite, position);
} }

View file

@ -47,7 +47,8 @@ var Pair = require('./Pair');
normal: { x: 0, y: 0 }, normal: { x: 0, y: 0 },
tangent: { x: 0, y: 0 }, tangent: { x: 0, y: 0 },
penetration: { 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, var normal = collision.normal,
tangent = collision.tangent,
penetration = collision.penetration,
supports = collision.supports, supports = collision.supports,
depth = minOverlap.overlap,
minAxis = minOverlap.axis, minAxis = minOverlap.axis,
minAxisX = minAxis.x, normalX = minAxis.x,
minAxisY = minAxis.y; normalY = minAxis.y,
deltaX = bodyB.position.x - bodyA.position.x,
deltaY = bodyB.position.y - bodyA.position.y;
// ensure normal is facing away from bodyA // ensure normal is facing away from bodyA
if (minAxisX * (bodyB.position.x - bodyA.position.x) + minAxisY * (bodyB.position.y - bodyA.position.y) < 0) { if (normalX * deltaX + normalY * deltaY >= 0) {
normal.x = minAxisX; normalX = -normalX;
normal.y = minAxisY; normalY = -normalY;
} else {
normal.x = -minAxisX;
normal.y = -minAxisY;
} }
collision.tangent.x = -normal.y; normal.x = normalX;
collision.tangent.y = normal.x; normal.y = normalY;
collision.depth = minOverlap.overlap; tangent.x = -normalY;
tangent.y = normalX;
collision.penetration.x = normal.x * collision.depth; penetration.x = normalX * depth;
collision.penetration.y = normal.y * collision.depth; penetration.y = normalY * depth;
collision.depth = depth;
// find support points, there is always either exactly one or two // find support points, there is always either exactly one or two
var supportsB = Collision._findSupports(bodyA, bodyB, normal, 1), var supportsB = Collision._findSupports(bodyA, bodyB, normal, 1),
@ -152,8 +158,8 @@ var Pair = require('./Pair');
supports[supportCount++] = supportsB[0]; supports[supportCount++] = supportsB[0];
} }
// update supports array size // update support count
supports.length = supportCount; collision.supportCount = supportCount;
return collision; return collision;
}; };
@ -232,32 +238,6 @@ var Pair = require('./Pair');
result.overlap = overlapMin; 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. * Finds supporting vertices given two bodies along a given direction using hill-climbing.
* @method _findSupports * @method _findSupports
@ -275,15 +255,15 @@ var Pair = require('./Pair');
bodyAPositionY = bodyA.position.y, bodyAPositionY = bodyA.position.y,
normalX = normal.x * direction, normalX = normal.x * direction,
normalY = normal.y * direction, normalY = normal.y * direction,
nearestDistance = Number.MAX_VALUE, vertexA = vertices[0],
vertexA, vertexB = vertexA,
vertexB, nearestDistance = normalX * (bodyAPositionX - vertexB.x) + normalY * (bodyAPositionY - vertexB.y),
vertexC, vertexC,
distance, distance,
j; j;
// find deepest vertex relative to the axis // find deepest vertex relative to the axis
for (j = 0; j < verticesLength; j += 1) { for (j = 1; j < verticesLength; j += 1) {
vertexB = vertices[j]; vertexB = vertices[j];
distance = normalX * (bodyAPositionX - vertexB.x) + normalY * (bodyAPositionY - vertexB.y); 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. * 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. * These are the deepest vertices (along the collision normal) of each body that are contained by the other body's vertices.
* *
* @property supports * @property supports
@ -405,4 +389,15 @@ var Pair = require('./Pair');
* @default [] * @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
*/
})(); })();

View file

@ -13,7 +13,7 @@ module.exports = Contact;
/** /**
* Creates a new contact. * Creates a new contact.
* @method create * @method create
* @param {vertex} vertex * @param {vertex} [vertex]
* @return {contact} A new contact * @return {contact} A new contact
*/ */
Contact.create = function(vertex) { Contact.create = function(vertex) {

View file

@ -22,6 +22,7 @@ var Collision = require('./Collision');
Detector.create = function(options) { Detector.create = function(options) {
var defaults = { var defaults = {
bodies: [], bodies: [],
collisions: [],
pairs: null pairs: null
}; };
@ -45,6 +46,7 @@ var Collision = require('./Collision');
*/ */
Detector.clear = function(detector) { Detector.clear = function(detector) {
detector.bodies = []; detector.bodies = [];
detector.collisions = [];
}; };
/** /**
@ -57,12 +59,13 @@ var Collision = require('./Collision');
* @return {collision[]} collisions * @return {collision[]} collisions
*/ */
Detector.collisions = function(detector) { Detector.collisions = function(detector) {
var collisions = [], var pairs = detector.pairs,
pairs = detector.pairs,
bodies = detector.bodies, bodies = detector.bodies,
bodiesLength = bodies.length, bodiesLength = bodies.length,
canCollide = Detector.canCollide, canCollide = Detector.canCollide,
collides = Collision.collides, collides = Collision.collides,
collisions = detector.collisions,
collisionIndex = 0,
i, i,
j; j;
@ -104,7 +107,7 @@ var Collision = require('./Collision');
var collision = collides(bodyA, bodyB, pairs); var collision = collides(bodyA, bodyB, pairs);
if (collision) { if (collision) {
collisions.push(collision); collisions[collisionIndex++] = collision;
} }
} else { } else {
var partsAStart = partsALength > 1 ? 1 : 0, var partsAStart = partsALength > 1 ? 1 : 0,
@ -126,7 +129,7 @@ var Collision = require('./Collision');
var collision = collides(partA, partB, pairs); var collision = collides(partA, partB, pairs);
if (collision) { 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; return collisions;
}; };
@ -180,6 +187,13 @@ var Collision = require('./Collision');
* @default [] * @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. * Optional. A `Matter.Pairs` object from which previous collision objects may be reused. Intended for internal `Matter.Engine` usage.
* @property pairs * @property pairs

View file

@ -28,11 +28,10 @@ var Contact = require('./Contact');
bodyA: bodyA, bodyA: bodyA,
bodyB: bodyB, bodyB: bodyB,
collision: collision, collision: collision,
contacts: [], contacts: [Contact.create(), Contact.create()],
activeContacts: [], contactCount: 0,
separation: 0, separation: 0,
isActive: true, isActive: true,
confirmedActive: true,
isSensor: bodyA.isSensor || bodyB.isSensor, isSensor: bodyA.isSensor || bodyB.isSensor,
timeCreated: timestamp, timeCreated: timestamp,
timeUpdated: timestamp, timeUpdated: timestamp,
@ -56,12 +55,11 @@ var Contact = require('./Contact');
* @param {number} timestamp * @param {number} timestamp
*/ */
Pair.update = function(pair, collision, timestamp) { Pair.update = function(pair, collision, timestamp) {
var contacts = pair.contacts, var supports = collision.supports,
supports = collision.supports, supportCount = collision.supportCount,
activeContacts = pair.activeContacts, contacts = pair.contacts,
parentA = collision.parentA, parentA = collision.parentA,
parentB = collision.parentB, parentB = collision.parentB;
parentAVerticesLength = parentA.vertices.length;
pair.isActive = true; pair.isActive = true;
pair.timeUpdated = timestamp; pair.timeUpdated = timestamp;
@ -73,20 +71,24 @@ var Contact = require('./Contact');
pair.restitution = parentA.restitution > parentB.restitution ? parentA.restitution : parentB.restitution; pair.restitution = parentA.restitution > parentB.restitution ? parentA.restitution : parentB.restitution;
pair.slop = parentA.slop > parentB.slop ? parentA.slop : parentB.slop; pair.slop = parentA.slop > parentB.slop ? parentA.slop : parentB.slop;
pair.contactCount = supportCount;
collision.pair = pair; collision.pair = pair;
activeContacts.length = 0;
for (var i = 0; i < supports.length; i++) { var supportA = supports[0],
var support = supports[i], contactA = contacts[0],
contactId = support.body === parentA ? support.index : parentAVerticesLength + support.index, supportB = supports[1],
contact = contacts[contactId]; contactB = contacts[1];
if (contact) { // match contacts to supports
activeContacts.push(contact); if (contactB.vertex === supportA || contactA.vertex === supportB) {
} else { contacts[1] = contactA;
activeContacts.push(contacts[contactId] = Contact.create(support)); 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; pair.timeUpdated = timestamp;
} else { } else {
pair.isActive = false; pair.isActive = false;
pair.activeContacts.length = 0; pair.contactCount = 0;
} }
}; };
@ -114,11 +116,8 @@ var Contact = require('./Contact');
* @return {string} Unique pairId * @return {string} Unique pairId
*/ */
Pair.id = function(bodyA, bodyB) { Pair.id = function(bodyA, bodyB) {
if (bodyA.id < bodyB.id) { return bodyA.id < bodyB.id ? bodyA.id.toString(36) + ':' + bodyB.id.toString(36)
return 'A' + bodyA.id + 'B' + bodyB.id; : bodyB.id.toString(36) + ':' + bodyA.id.toString(36);
} else {
return 'A' + bodyB.id + 'B' + bodyA.id;
}
}; };
})(); })();

View file

@ -37,27 +37,24 @@ var Common = require('../core/Common');
* @param {number} timestamp * @param {number} timestamp
*/ */
Pairs.update = function(pairs, collisions, timestamp) { Pairs.update = function(pairs, collisions, timestamp) {
var pairsList = pairs.list, var pairUpdate = Pair.update,
pairsListLength = pairsList.length, pairCreate = Pair.create,
pairSetActive = Pair.setActive,
pairsTable = pairs.table, pairsTable = pairs.table,
collisionsLength = collisions.length, pairsList = pairs.list,
pairsListLength = pairsList.length,
pairsListIndex = pairsListLength,
collisionStart = pairs.collisionStart, collisionStart = pairs.collisionStart,
collisionEnd = pairs.collisionEnd, collisionEnd = pairs.collisionEnd,
collisionActive = pairs.collisionActive, collisionActive = pairs.collisionActive,
collisionsLength = collisions.length,
collisionStartIndex = 0,
collisionEndIndex = 0,
collisionActiveIndex = 0,
collision, collision,
pairIndex,
pair, pair,
i; 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++) { for (i = 0; i < collisionsLength; i++) {
collision = collisions[i]; collision = collisions[i];
pair = collision.pair; pair = collision.pair;
@ -66,50 +63,63 @@ var Common = require('../core/Common');
// pair already exists (but may or may not be active) // pair already exists (but may or may not be active)
if (pair.isActive) { if (pair.isActive) {
// pair exists and is active // pair exists and is active
collisionActive.push(pair); collisionActive[collisionActiveIndex++] = pair;
} else {
// pair exists but was inactive, so a collision has just started again
collisionStart.push(pair);
} }
// update the pair // update the pair
Pair.update(pair, collision, timestamp); pairUpdate(pair, collision, timestamp);
pair.confirmedActive = true;
} else { } else {
// pair did not exist, create a new pair // pair did not exist, create a new pair
pair = Pair.create(collision, timestamp); pair = pairCreate(collision, timestamp);
pairsTable[pair.id] = pair; pairsTable[pair.id] = pair;
// push the new pair // add the new pair
collisionStart.push(pair); collisionStart[collisionStartIndex++] = pair;
pairsList.push(pair); pairsList[pairsListIndex++] = pair;
} }
} }
// find pairs that are no longer active // find pairs that are no longer active
var removePairIndex = []; pairsListIndex = 0;
pairsListLength = pairsList.length; pairsListLength = pairsList.length;
for (i = 0; i < pairsListLength; i++) { for (i = 0; i < pairsListLength; i++) {
pair = pairsList[i]; pair = pairsList[i];
if (!pair.confirmedActive) { // pair is active if updated this timestep
Pair.setActive(pair, false, timestamp); if (pair.timeUpdated >= timestamp) {
collisionEnd.push(pair); // keep active pairs
pairsList[pairsListIndex++] = pair;
} else {
pairSetActive(pair, false, timestamp);
if (!pair.collision.bodyA.isSleeping && !pair.collision.bodyB.isSleeping) { // keep inactive pairs if both bodies may be sleeping
removePairIndex.push(i); 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;
// remove inactive pairs
for (i = 0; i < removePairIndex.length; i++) {
pairIndex = removePairIndex[i] - i;
pair = pairsList[pairIndex];
pairsList.splice(pairIndex, 1);
delete pairsTable[pair.id]; 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;
}
}; };
/** /**

View file

@ -29,7 +29,7 @@ var Bounds = require('../geometry/Bounds');
Resolver.preSolvePosition = function(pairs) { Resolver.preSolvePosition = function(pairs) {
var i, var i,
pair, pair,
activeCount, contactCount,
pairsLength = pairs.length; pairsLength = pairs.length;
// find total contacts on each body // find total contacts on each body
@ -39,9 +39,9 @@ var Bounds = require('../geometry/Bounds');
if (!pair.isActive) if (!pair.isActive)
continue; continue;
activeCount = pair.activeContacts.length; contactCount = pair.contactCount;
pair.collision.parentA.totalContacts += activeCount; pair.collision.parentA.totalContacts += contactCount;
pair.collision.parentB.totalContacts += activeCount; pair.collision.parentB.totalContacts += contactCount;
} }
}; };
@ -79,8 +79,8 @@ var Bounds = require('../geometry/Bounds');
// get current separation between body edges involved in collision // get current separation between body edges involved in collision
pair.separation = pair.separation =
normal.x * (bodyB.positionImpulse.x + collision.penetration.x - bodyA.positionImpulse.x) collision.depth + normal.x * (bodyB.positionImpulse.x - bodyA.positionImpulse.x)
+ normal.y * (bodyB.positionImpulse.y + collision.penetration.y - bodyA.positionImpulse.y); + normal.y * (bodyB.positionImpulse.y - bodyA.positionImpulse.y);
} }
for (i = 0; i < pairsLength; i++) { for (i = 0; i < pairsLength; i++) {
@ -176,8 +176,8 @@ var Bounds = require('../geometry/Bounds');
if (!pair.isActive || pair.isSensor) if (!pair.isActive || pair.isSensor)
continue; continue;
var contacts = pair.activeContacts, var contacts = pair.contacts,
contactsLength = contacts.length, contactCount = pair.contactCount,
collision = pair.collision, collision = pair.collision,
bodyA = collision.parentA, bodyA = collision.parentA,
bodyB = collision.parentB, bodyB = collision.parentB,
@ -185,7 +185,7 @@ var Bounds = require('../geometry/Bounds');
tangent = collision.tangent; tangent = collision.tangent;
// resolve each contact // resolve each contact
for (j = 0; j < contactsLength; j++) { for (j = 0; j < contactCount; j++) {
var contact = contacts[j], var contact = contacts[j],
contactVertex = contact.vertex, contactVertex = contact.vertex,
normalImpulse = contact.normalImpulse, normalImpulse = contact.normalImpulse,
@ -248,28 +248,26 @@ var Bounds = require('../geometry/Bounds');
var collision = pair.collision, var collision = pair.collision,
bodyA = collision.parentA, bodyA = collision.parentA,
bodyB = collision.parentB, bodyB = collision.parentB,
bodyAVelocity = bodyA.velocity,
bodyBVelocity = bodyB.velocity,
normalX = collision.normal.x, normalX = collision.normal.x,
normalY = collision.normal.y, normalY = collision.normal.y,
tangentX = collision.tangent.x, tangentX = collision.tangent.x,
tangentY = collision.tangent.y, tangentY = collision.tangent.y,
contacts = pair.activeContacts, inverseMassTotal = pair.inverseMass,
contactsLength = contacts.length, friction = pair.friction * pair.frictionStatic * frictionNormalMultiplier,
contactShare = 1 / contactsLength, contacts = pair.contacts,
inverseMassTotal = bodyA.inverseMass + bodyB.inverseMass, contactCount = pair.contactCount,
friction = pair.friction * pair.frictionStatic * frictionNormalMultiplier; contactShare = 1 / contactCount;
// update body velocities // get body velocities
bodyAVelocity.x = bodyA.position.x - bodyA.positionPrev.x; var bodyAVelocityX = bodyA.position.x - bodyA.positionPrev.x,
bodyAVelocity.y = bodyA.position.y - bodyA.positionPrev.y; bodyAVelocityY = bodyA.position.y - bodyA.positionPrev.y,
bodyBVelocity.x = bodyB.position.x - bodyB.positionPrev.x; bodyAAngularVelocity = bodyA.angle - bodyA.anglePrev,
bodyBVelocity.y = bodyB.position.y - bodyB.positionPrev.y; bodyBVelocityX = bodyB.position.x - bodyB.positionPrev.x,
bodyA.angularVelocity = bodyA.angle - bodyA.anglePrev; bodyBVelocityY = bodyB.position.y - bodyB.positionPrev.y,
bodyB.angularVelocity = bodyB.angle - bodyB.anglePrev; bodyBAngularVelocity = bodyB.angle - bodyB.anglePrev;
// resolve each contact // resolve each contact
for (j = 0; j < contactsLength; j++) { for (j = 0; j < contactCount; j++) {
var contact = contacts[j], var contact = contacts[j],
contactVertex = contact.vertex; contactVertex = contact.vertex;
@ -278,10 +276,10 @@ var Bounds = require('../geometry/Bounds');
offsetBX = contactVertex.x - bodyB.position.x, offsetBX = contactVertex.x - bodyB.position.x,
offsetBY = contactVertex.y - bodyB.position.y; offsetBY = contactVertex.y - bodyB.position.y;
var velocityPointAX = bodyAVelocity.x - offsetAY * bodyA.angularVelocity, var velocityPointAX = bodyAVelocityX - offsetAY * bodyAAngularVelocity,
velocityPointAY = bodyAVelocity.y + offsetAX * bodyA.angularVelocity, velocityPointAY = bodyAVelocityY + offsetAX * bodyAAngularVelocity,
velocityPointBX = bodyBVelocity.x - offsetBY * bodyB.angularVelocity, velocityPointBX = bodyBVelocityX - offsetBY * bodyBAngularVelocity,
velocityPointBY = bodyBVelocity.y + offsetBX * bodyB.angularVelocity; velocityPointBY = bodyBVelocityY + offsetBX * bodyBAngularVelocity;
var relativeVelocityX = velocityPointAX - velocityPointBX, var relativeVelocityX = velocityPointAX - velocityPointBX,
relativeVelocityY = velocityPointAY - velocityPointBY; relativeVelocityY = velocityPointAY - velocityPointBY;

View file

@ -63,6 +63,7 @@ var Body = require('../body/Body');
engine.world = options.world || Composite.create({ label: 'World' }); engine.world = options.world || Composite.create({ label: 'World' });
engine.pairs = options.pairs || Pairs.create(); engine.pairs = options.pairs || Pairs.create();
engine.detector = options.detector || Detector.create(); engine.detector = options.detector || Detector.create();
engine.detector.pairs = engine.pairs;
// for temporary back compatibility only // for temporary back compatibility only
engine.grid = { buckets: [] }; engine.grid = { buckets: [] };
@ -148,7 +149,6 @@ var Body = require('../body/Body');
Constraint.postSolveAll(allBodies); Constraint.postSolveAll(allBodies);
// find all collisions // find all collisions
detector.pairs = engine.pairs;
var collisions = Detector.collisions(detector); var collisions = Detector.collisions(detector);
// update collision pairs // update collision pairs

View file

@ -1222,8 +1222,8 @@ var Mouse = require('../core/Mouse');
continue; continue;
collision = pair.collision; collision = pair.collision;
for (j = 0; j < pair.activeContacts.length; j++) { for (j = 0; j < pair.contactCount; j++) {
var contact = pair.activeContacts[j], var contact = pair.contacts[j],
vertex = contact.vertex; vertex = contact.vertex;
c.rect(vertex.x - 1.5, vertex.y - 1.5, 3.5, 3.5); 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; collision = pair.collision;
if (pair.activeContacts.length > 0) { if (pair.contactCount > 0) {
var normalPosX = pair.activeContacts[0].vertex.x, var normalPosX = pair.contacts[0].vertex.x,
normalPosY = pair.activeContacts[0].vertex.y; normalPosY = pair.contacts[0].vertex.y;
if (pair.activeContacts.length === 2) { if (pair.contactCount === 2) {
normalPosX = (pair.activeContacts[0].vertex.x + pair.activeContacts[1].vertex.x) / 2; normalPosX = (pair.contacts[0].vertex.x + pair.contacts[1].vertex.x) / 2;
normalPosY = (pair.activeContacts[0].vertex.y + pair.activeContacts[1].vertex.y) / 2; normalPosY = (pair.contacts[0].vertex.y + pair.contacts[1].vertex.y) / 2;
} }
if (collision.bodyB === collision.supports[0].body || collision.bodyA.isStatic === true) { if (collision.bodyB === collision.supports[0].body || collision.bodyA.isStatic === true) {

View file

@ -3,8 +3,9 @@
"use strict"; "use strict";
const mock = require('mock-require'); const mock = require('mock-require');
const { requireUncached, serialize } = require('./TestTools'); const { requireUncached, serialize, smoothExp } = require('./TestTools');
const consoleOriginal = global.console; const consoleOriginal = global.console;
const DateOriginal = global.Date;
const runExample = options => { const runExample = options => {
const { const {
@ -13,8 +14,8 @@ const runExample = options => {
frameCallbacks frameCallbacks
} = prepareEnvironment(options); } = prepareEnvironment(options);
let totalMemory = 0; let memoryDeltaAverage = 0;
let totalDuration = 0; let timeDeltaAverage = 0;
let overlapTotal = 0; let overlapTotal = 0;
let overlapCount = 0; let overlapCount = 0;
let i; let i;
@ -24,6 +25,12 @@ const runExample = options => {
let runner; let runner;
let engine; let engine;
let render; 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) { for (i = 0; i < options.repeats; i += 1) {
if (global.gc) { if (global.gc) {
@ -41,29 +48,51 @@ const runExample = options => {
const time = j * runner.delta; const time = j * runner.delta;
const callbackCount = frameCallbacks.length; const callbackCount = frameCallbacks.length;
global.timeNow = time;
for (let p = 0; p < callbackCount; p += 1) { for (let p = 0; p < callbackCount; p += 1) {
totalMemory += process.memoryUsage().heapUsed; const frameCallback = frameCallbacks.shift();
const callback = frameCallbacks.shift(); const memoryBefore = process.memoryUsage().heapUsed;
const startTime = process.hrtime(); const timeBefore = process.hrtime();
callback(time); frameCallback(time);
const duration = process.hrtime(startTime); const timeDuration = process.hrtime(timeBefore);
totalMemory += process.memoryUsage().heapUsed; const timeDelta = timeDuration[0] * 1e9 + timeDuration[1];
totalDuration += duration[0] * 1e9 + duration[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 pairsList = engine.pairs.list;
const pairsListLength = engine.pairs.list.length; const pairsListLength = engine.pairs.list.length;
for (let p = 0; p < pairsListLength; p += 1) { for (let p = 0; p < pairsListLength; p += 1) {
const pair = pairsList[p]; const pair = pairsList[p];
const separation = pair.separation - pair.slop;
if (pair.isActive && !pair.isSensor) { if (pair.isActive && !pair.isSensor){
overlapTotal += separation > 0 ? separation : 0; const overlap = pairOverlap(pair);
if (overlap >= 0) {
overlapTotalUpdate += overlap;
overlapCountUpdate += 1;
}
}
}
if (overlapCountUpdate > 0) {
overlapTotal += overlapTotalUpdate / overlapCountUpdate;
overlapCount += 1; overlapCount += 1;
} }
if (!extrinsicCapture && engine.timing.timestamp >= 1000) {
extrinsicCapture = captureExtrinsics(engine, Matter);
extrinsicCapture.updates = j;
} }
} }
} }
@ -72,13 +101,13 @@ const runExample = options => {
return { return {
name: options.name, name: options.name,
duration: totalDuration, duration: timeDeltaAverage,
memory: memoryDeltaAverage,
overlap: overlapTotal / (overlapCount || 1), overlap: overlapTotal / (overlapCount || 1),
memory: totalMemory, extrinsic: extrinsicCapture,
logs: logs,
extrinsic: captureExtrinsics(engine, Matter),
intrinsic: captureIntrinsics(engine, Matter), intrinsic: captureIntrinsics(engine, Matter),
state: captureState(engine, runner, render) state: captureState(engine, runner, render),
logs
}; };
} catch (err) { } catch (err) {
@ -144,6 +173,7 @@ const prepareEnvironment = options => {
const frameCallbacks = []; const frameCallbacks = [];
global.document = global.window = { global.document = global.window = {
performance: {},
addEventListener: () => {}, addEventListener: () => {},
requestAnimationFrame: callback => { requestAnimationFrame: callback => {
frameCallbacks.push(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); const Matter = prepareMatter(options);
mock('matter-js', Matter); mock('matter-js', Matter);
global.Matter = Matter; global.Matter = Matter;
@ -187,6 +232,7 @@ const prepareEnvironment = options => {
const resetEnvironment = () => { const resetEnvironment = () => {
global.console = consoleOriginal; global.console = consoleOriginal;
global.Date = DateOriginal;
global.window = undefined; global.window = undefined;
global.document = undefined; global.document = undefined;
global.Matter = undefined; global.Matter = undefined;
@ -195,46 +241,22 @@ const resetEnvironment = () => {
const captureExtrinsics = ({ world }, Matter) => ({ const captureExtrinsics = ({ world }, Matter) => ({
bodies: Matter.Composite.allBodies(world).reduce((bodies, body) => { bodies: Matter.Composite.allBodies(world).reduce((bodies, body) => {
bodies[body.id] = [ bodies[body.id] = {
body.position.x, position: { x: body.position.x, y: body.position.y },
body.position.y, vertices: body.vertices.map(vertex => ({ x: vertex.x, y: vertex.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; 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: Matter.Composite.allBodies(world).reduce((bodies, body) => {
bodies[body.id] = body; bodies[body.id] = body;
return bodies; return bodies;
@ -289,6 +311,9 @@ const extrinsicProperties = [
'velocity', 'velocity',
'position', 'position',
'positionPrev', 'positionPrev',
'motion',
'sleepCounter',
'positionImpulse'
]; ];
const excludeStateProperties = [ const excludeStateProperties = [

View file

@ -17,13 +17,14 @@ const {
const Example = requireUncached('../examples/index'); const Example = requireUncached('../examples/index');
const MatterBuild = requireUncached('../build/matter'); const MatterBuild = requireUncached('../build/matter');
const { versionSatisfies } = requireUncached('../src/core/Plugin'); 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 testComparison = getArg('compare', null) === 'true';
const saveComparison = getArg('save', null) === 'true'; const saveComparison = getArg('save', null) === 'true';
const specificExamples = getArg('examples', null, (val) => val.split(',')); const specificExamples = getArg('examples', null, (val) => val.split(','));
const repeats = getArg('repeats', 1, parseFloat); const repeats = getArg('repeats', 1, parseFloat);
const updates = getArg('updates', 150, parseFloat); const updates = getArg('updates', 150, parseFloat);
const benchmark = getArg('benchmark', null) === 'true';
const excludeExamples = ['svg', 'terrain']; const excludeExamples = ['svg', 'terrain'];
const excludeJitter = ['stack', 'circleStack', 'restitution', 'staticFriction', 'friction', 'newtonsCradle', 'catapult']; 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 captureExamples = async useDev => {
const multiThreadWorker = new Worker(require.resolve('./ExampleWorker'), { const worker = 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'), {
enableWorkerThreads: true, 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, name,
useDev, useDev,
updates: updates, updates: updates,
repeats: repeats, repeats: benchmark ? Math.max(repeats, 3) : repeats,
stableSort: false, stableSort: false,
jitter: excludeJitter.includes(name) ? 0 : 1e-10 jitter: excludeJitter.includes(name) ? 0 : 1e-10
}))); })));
await singleThreadWorker.end(); await worker.end();
const capture = {}; const capture = {};
for (const completeRun of completeRuns) { for (const completeRun of completeRuns) {
const behaviourRun = behaviourRuns.find(({ name }) => name === completeRun.name); capture[completeRun.name] = completeRun;
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; return capture;
@ -119,7 +78,7 @@ afterAll(async () => {
'Examples ran against previous release and current build\n\n' 'Examples ran against previous release and current build\n\n'
+ logReport(build, `release`) + '\n' + logReport(build, `release`) + '\n'
+ logReport(dev, `current`) + '\n' + logReport(dev, `current`) + '\n'
+ comparisonReport(dev, build, devSize, buildSize, MatterBuild.version, saveComparison) + comparisonReport(dev, build, devSize, buildSize, MatterBuild.version, saveComparison, benchmark)
); );
}); });

View file

@ -8,25 +8,26 @@ const comparePath = './test/__compare__';
const compareCommand = 'open http://localhost:8000/?compare'; const compareCommand = 'open http://localhost:8000/?compare';
const diffSaveCommand = 'npm run test-save'; const diffSaveCommand = 'npm run test-save';
const diffCommand = 'code -n -d test/__compare__/examples-build.json test/__compare__/examples-dev.json'; 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 colors = { Red: 31, Green: 32, Yellow: 33, White: 37, BrightWhite: 90, BrightCyan: 36 };
const comparisonReport = (capturesDev, capturesBuild, devSize, buildSize, buildVersion, save) => { const comparisonReport = (capturesDev, capturesBuild, devSize, buildSize, buildVersion, save, benchmark) => {
const performanceDev = capturePerformanceTotals(capturesDev); const {
const performanceBuild = capturePerformanceTotals(capturesBuild); 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 filesizeChange = (devSize / buildSize) - 1;
const behaviourSimilaritys = extrinsicSimilarity(capturesDev, capturesBuild, 'behaviourExtrinsic'); const firstCapture = Object.entries(capturesDev)[0][1];
const behaviourSimilarityAverage = extrinsicSimilarityAverage(behaviourSimilaritys); const updates = firstCapture.extrinsic.updates;
const behaviourSimilarityEntries = Object.entries(behaviourSimilaritys);
behaviourSimilarityEntries.sort((a, b) => a[1] - b[1]);
const similaritys = extrinsicSimilarity(capturesDev, capturesBuild, 'similarityExtrinsic'); const similaritys = extrinsicSimilarity(capturesDev, capturesBuild);
const similarityAverage = extrinsicSimilarityAverage(similaritys); 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 devIntrinsicsChanged = {};
const buildIntrinsicsChanged = {}; const buildIntrinsicsChanged = {};
@ -50,32 +51,31 @@ const comparisonReport = (capturesDev, capturesBuild, devSize, buildSize, buildV
.sort((a, b) => a.name.localeCompare(b.name)); .sort((a, b) => a.name.localeCompare(b.name));
const report = (breakEvery, format) => [ const report = (breakEvery, format) => [
[`Output comparison of ${behaviourSimilarityEntries.length}`, [`Output sample comparison estimates of ${similarityEntries.length} examples`,
`examples against previous release ${format('matter-js@' + buildVersion, colors.Yellow)}` `against previous release ${format('matter-js@' + buildVersion, colors.Yellow)}:`
].join(' '), ].join(' '),
`\n\n${format('Behaviour ', colors.White)}`, `\n\n${format(`Similarity`, colors.White)} `,
`${format(formatPercent(behaviourSimilarityAverage), behaviourSimilarityAverage === 1 ? colors.Green : colors.Yellow)}%`, `${format(formatPercent(similarityAveragePerUpdate, false, true), formatColor(similarityAveragePerUpdate === 1))}% `,
` ${format('Similarity', colors.White)}`,
`${format(formatPercent(similarityAverage), similarityAverage === 1 ? colors.Green : colors.Yellow)}%`,
` ${format('Overlap', colors.White)}`, ` ${format('Overlap', colors.White)}`,
` ${format((overlapChange >= 0 ? '+' : '-') + formatPercent(overlapChange, true), overlapChange <= 0 ? colors.Green : colors.Yellow)}%`, ` ${format(formatPercent(overlapChange), formatColor(overlapChange <= 0))}%`,
`\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('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)}`, `${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) => { captureSummary.reduce((output, p, i) => {
output += `${p.name} `; output += `${p.name} `;
output += `${similarityRatings(behaviourSimilaritys[p.name])} `; output += `${similarityRatings(similaritys[p.name])} `;
output += `${changeRatings(capturesDev[p.name].changedIntrinsics)} `; output += `${changeRatings(capturesDev[p.name].changedIntrinsics)} `;
if (i > 0 && i < captureSummary.length && breakEvery > 0 && i % breakEvery === 0) { if (i > 0 && i < captureSummary.length && breakEvery > 0 && i % breakEvery === 0) {
output += '\n'; output += '\n';
@ -83,9 +83,9 @@ const comparisonReport = (capturesDev, capturesBuild, devSize, buildSize, buildV
return output; return output;
}, '\n\n'), }, '\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)}` : '' intrinsicChangeCount > 0 ? `\n${format('▶', colors.White)} ${format((save ? diffCommand : diffSaveCommand), colors.BrightCyan)}` : ''
].join(' '); ].join(' ');
@ -98,17 +98,29 @@ const comparisonReport = (capturesDev, capturesBuild, devSize, buildSize, buildV
return report(5, color); 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 similarityRatings = similarity => similarity < equalityThreshold ? color('●', colors.Yellow) : '·';
const changeRatings = isChanged => isChanged ? color('◆', colors.White) : '·'; const changeRatings = isChanged => isChanged ? color('◆', colors.White) : '·';
const color = (text, number) => number ? `\x1b[${number}m${text}\x1b[0m` : text; 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 noiseThreshold = (val, threshold) => {
const sign = val < 0 ? -1 : 1; const sign = val < 0 ? -1 : 1;
@ -116,6 +128,38 @@ const noiseThreshold = (val, threshold) => {
return sign * Math.max(0, magnitude - threshold) / (1 - 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) => { const equals = (a, b) => {
try { try {
expect(a).toEqual(b); expect(a).toEqual(b);
@ -125,41 +169,77 @@ const equals = (a, b) => {
return true; return true;
}; };
const capturePerformanceTotals = (captures) => { const captureBenchmark = (capturesDev, capturesBuild) => {
const totals = { const overlapChanges = [];
duration: 0,
overlap: 0, let durationDev = 0;
memory: 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)) { const durationChange = 1 - noiseThreshold(durationDev / durationBuild, 0.02);
totals.duration += captures[name].duration; const memoryChange = noiseThreshold(memoryDev / memoryBuild, 0.02) - 1;
totals.overlap += captures[name].overlap; const overlapChange = noiseThreshold(median(overlapChanges, 0.45, 0.55), 0.001) - 1;
totals.memory += captures[name].memory;
};
return totals; return {
durationChange,
memoryChange,
overlapChange
};
}; };
const extrinsicSimilarity = (currentCaptures, referenceCaptures, key) => { const extrinsicSimilarity = (currentCaptures, referenceCaptures, key='extrinsic') => {
const result = {}; const result = {};
const zeroVector = { x: 0, y: 0 };
Object.entries(currentCaptures).forEach(([name, current]) => { for (const name in currentCaptures) {
const reference = referenceCaptures[name]; const currentExtrinsic = currentCaptures[name][key];
const worldVector = []; const referenceExtrinsic = referenceCaptures[name][key];
const worldVectorRef = [];
const currentExtrinsic = current[key];
const referenceExtrinsic = reference[key];
Object.keys(currentExtrinsic).forEach(objectType => { let totalCount = 0;
Object.keys(currentExtrinsic[objectType]).forEach(objectId => { let totalSimilarity = 0;
worldVector.push(...currentExtrinsic[objectType][objectId]);
worldVectorRef.push(...referenceExtrinsic[objectType][objectId]);
});
});
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; return result;
}; };
@ -291,6 +371,6 @@ const toMatchIntrinsics = {
}; };
module.exports = { module.exports = {
requireUncached, comparisonReport, logReport, getArg, requireUncached, comparisonReport, logReport, getArg, smoothExp,
serialize, toMatchExtrinsics, toMatchIntrinsics serialize, toMatchExtrinsics, toMatchIntrinsics
}; };