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:
matrix:
node-version: [12.x, 14.x, 16.x]
node-version: [16.x]
steps:
- uses: actions/checkout@v2

View file

@ -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
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%;
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>

View file

@ -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);
}

View file

@ -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 }),

9648
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",
"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",

View file

@ -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);
}

View file

@ -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;
}
normal.x = normalX;
normal.y = normalY;
collision.tangent.x = -normal.y;
collision.tangent.y = normal.x;
tangent.x = -normalY;
tangent.y = normalX;
collision.depth = minOverlap.overlap;
penetration.x = normalX * depth;
penetration.y = normalY * depth;
collision.penetration.x = normal.x * collision.depth;
collision.penetration.y = normal.y * collision.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
*/
})();

View file

@ -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) {

View file

@ -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

View file

@ -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];
if (contact) {
activeContacts.push(contact);
} else {
activeContacts.push(contacts[contactId] = Contact.create(support));
}
var supportA = supports[0],
contactA = contacts[0],
supportB = supports[1],
contactB = contacts[1];
// 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);
};
})();

View file

@ -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,49 +63,62 @@ 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);
// 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];
}
}
}
// 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];
// 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) {
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;

View file

@ -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

View file

@ -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) {

View file

@ -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) {
@ -36,35 +43,57 @@ const runExample = options => {
runner = example.runner;
engine = example.engine;
render = example.render;
for (j = 0; j < options.updates; j += 1) {
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;
overlapCount += 1;
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 = [

View file

@ -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)
);
});

View file

@ -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
};