mirror of
https://github.com/liabru/matter-js.git
synced 2024-11-27 09:50:52 -05:00
343 lines
9.5 KiB
JavaScript
343 lines
9.5 KiB
JavaScript
/* eslint-env es6 */
|
|
/* eslint no-global-assign: 0 */
|
|
"use strict";
|
|
|
|
const mock = require('mock-require');
|
|
const { requireUncached, serialize, smoothExp } = require('./TestTools');
|
|
const consoleOriginal = global.console;
|
|
const DateOriginal = global.Date;
|
|
|
|
const runExample = options => {
|
|
const {
|
|
Matter,
|
|
logs,
|
|
frameCallbacks
|
|
} = prepareEnvironment(options);
|
|
|
|
let memoryDeltaAverage = 0;
|
|
let timeDeltaAverage = 0;
|
|
let overlapTotal = 0;
|
|
let overlapCount = 0;
|
|
let i;
|
|
let j;
|
|
|
|
try {
|
|
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) {
|
|
global.gc();
|
|
}
|
|
|
|
const Examples = requireUncached('../examples/index');
|
|
const example = Examples[options.name]();
|
|
|
|
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) {
|
|
const frameCallback = frameCallbacks.shift();
|
|
const memoryBefore = process.memoryUsage().heapUsed;
|
|
const timeBefore = process.hrtime();
|
|
|
|
frameCallback(time);
|
|
|
|
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];
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
resetEnvironment();
|
|
|
|
return {
|
|
name: options.name,
|
|
duration: timeDeltaAverage,
|
|
memory: memoryDeltaAverage,
|
|
overlap: overlapTotal / (overlapCount || 1),
|
|
extrinsic: extrinsicCapture,
|
|
intrinsic: captureIntrinsics(engine, Matter),
|
|
state: captureState(engine, runner, render),
|
|
logs
|
|
};
|
|
|
|
} catch (err) {
|
|
err.message = `On example '${options.name}' update ${j}:\n\n ${err.message}`;
|
|
throw err;
|
|
}
|
|
};
|
|
|
|
const prepareMatter = (options) => {
|
|
const Matter = requireUncached(options.useDev ? '../build/matter.dev' : '../build/matter');
|
|
|
|
if (Matter.Common._nextId !== 0) {
|
|
throw 'Matter instance has already been used.';
|
|
}
|
|
|
|
Matter.Common.info = Matter.Common.warn = Matter.Common.log;
|
|
|
|
if (options.stableSort) {
|
|
if (Matter.Collision) {
|
|
const MatterCollisionCollides = Matter.Collision.collides;
|
|
Matter.Collision.collides = function(bodyA, bodyB, pairs) {
|
|
const _bodyA = bodyA.id < bodyB.id ? bodyA : bodyB;
|
|
const _bodyB = bodyA.id < bodyB.id ? bodyB : bodyA;
|
|
return MatterCollisionCollides(_bodyA, _bodyB, pairs);
|
|
};
|
|
} else {
|
|
const MatterSATCollides = Matter.SAT.collides;
|
|
Matter.SAT.collides = function(bodyA, bodyB, previousCollision, pairActive) {
|
|
const _bodyA = bodyA.id < bodyB.id ? bodyA : bodyB;
|
|
const _bodyB = bodyA.id < bodyB.id ? bodyB : bodyA;
|
|
return MatterSATCollides(_bodyA, _bodyB, previousCollision, pairActive);
|
|
};
|
|
}
|
|
|
|
Matter.after('Detector.collisions', function() { this.sort(collisionCompareId); });
|
|
Matter.after('Composite.allBodies', function() { sortById(this); });
|
|
Matter.after('Composite.allConstraints', function() { sortById(this); });
|
|
Matter.after('Composite.allComposites', function() { sortById(this); });
|
|
|
|
Matter.before('Pairs.update', function(pairs) {
|
|
pairs.list.sort((pairA, pairB) => collisionCompareId(pairA.collision, pairB.collision));
|
|
});
|
|
|
|
Matter.after('Pairs.update', function(pairs) {
|
|
pairs.list.sort((pairA, pairB) => collisionCompareId(pairA.collision, pairB.collision));
|
|
});
|
|
}
|
|
|
|
if (options.jitter) {
|
|
Matter.after('Body.create', function() {
|
|
Matter.Body.applyForce(this, this.position, {
|
|
x: Math.cos(this.id * this.id) * options.jitter * this.mass,
|
|
y: Math.sin(this.id * this.id) * options.jitter * this.mass
|
|
});
|
|
});
|
|
}
|
|
|
|
return Matter;
|
|
};
|
|
|
|
const prepareEnvironment = options => {
|
|
const logs = [];
|
|
const frameCallbacks = [];
|
|
|
|
global.document = global.window = {
|
|
performance: {},
|
|
addEventListener: () => {},
|
|
requestAnimationFrame: callback => {
|
|
frameCallbacks.push(callback);
|
|
return frameCallbacks.length;
|
|
},
|
|
createElement: () => ({
|
|
parentNode: {},
|
|
width: 800,
|
|
height: 600,
|
|
style: {},
|
|
addEventListener: () => {},
|
|
getAttribute: name => ({
|
|
'data-pixel-ratio': '1'
|
|
}[name]),
|
|
getContext: () => new Proxy({}, {
|
|
get() { return () => {}; }
|
|
})
|
|
})
|
|
};
|
|
|
|
global.document.body = global.document.createElement();
|
|
|
|
global.Image = function Image() { };
|
|
|
|
global.console = {
|
|
log: (...args) => {
|
|
logs.push(args.join(' '));
|
|
}
|
|
};
|
|
|
|
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;
|
|
|
|
return {
|
|
Matter,
|
|
logs,
|
|
frameCallbacks
|
|
};
|
|
};
|
|
|
|
const resetEnvironment = () => {
|
|
global.console = consoleOriginal;
|
|
global.Date = DateOriginal;
|
|
global.window = undefined;
|
|
global.document = undefined;
|
|
global.Matter = undefined;
|
|
mock.stopAll();
|
|
};
|
|
|
|
const captureExtrinsics = ({ world }, Matter) => ({
|
|
bodies: Matter.Composite.allBodies(world).reduce((bodies, body) => {
|
|
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;
|
|
}, {})
|
|
});
|
|
|
|
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;
|
|
}, {}),
|
|
constraints: Matter.Composite.allConstraints(world).reduce((constraints, constraint) => {
|
|
constraints[constraint.id] = constraint;
|
|
return constraints;
|
|
}, {}),
|
|
composites: Matter.Composite.allComposites(world).reduce((composites, composite) => {
|
|
composites[composite.id] = {
|
|
bodies: Matter.Composite.allBodies(composite).map(body => body.id),
|
|
constraints: Matter.Composite.allConstraints(composite).map(constraint => constraint.id),
|
|
composites: Matter.Composite.allComposites(composite).map(composite => composite.id)
|
|
};
|
|
return composites;
|
|
}, {})
|
|
}, (key) => !Number.isInteger(parseInt(key)) && !intrinsicProperties.includes(key));
|
|
|
|
const captureState = (engine, runner, render, excludeKeys=excludeStateProperties) => (
|
|
serialize({ engine, runner, render }, (key) => excludeKeys.includes(key))
|
|
);
|
|
|
|
const intrinsicProperties = [
|
|
// Composite
|
|
'bodies', 'constraints', 'composites',
|
|
|
|
// Common
|
|
'id', 'label',
|
|
|
|
// Constraint
|
|
'angularStiffness', 'bodyA', 'bodyB', 'damping', 'length', 'stiffness',
|
|
|
|
// Body
|
|
'area', 'collisionFilter', 'category', 'mask', 'group', 'density', 'friction',
|
|
'frictionAir', 'frictionStatic', 'inertia', 'inverseInertia', 'inverseMass',
|
|
'isSensor', 'isSleeping', 'isStatic', 'mass', 'parent', 'parts', 'restitution',
|
|
'sleepThreshold', 'slop', 'timeScale',
|
|
|
|
// Composite
|
|
'bodies', 'constraints', 'composites'
|
|
];
|
|
|
|
const extrinsicProperties = [
|
|
'axes',
|
|
'vertices',
|
|
'bounds',
|
|
'angle',
|
|
'anglePrev',
|
|
'angularVelocity',
|
|
'angularSpeed',
|
|
'speed',
|
|
'velocity',
|
|
'position',
|
|
'positionPrev',
|
|
'motion',
|
|
'sleepCounter',
|
|
'positionImpulse'
|
|
];
|
|
|
|
const excludeStateProperties = [
|
|
'cache',
|
|
'grid',
|
|
'context',
|
|
'broadphase',
|
|
'metrics',
|
|
'controller',
|
|
'detector',
|
|
'pairs',
|
|
'lastElapsed',
|
|
'deltaHistory',
|
|
'elapsedHistory',
|
|
'engineDeltaHistory',
|
|
'engineElapsedHistory',
|
|
'timestampElapsedHistory',
|
|
].concat(extrinsicProperties);
|
|
|
|
const collisionId = (collision) =>
|
|
Math.min(collision.bodyA.id, collision.bodyB.id) + Math.max(collision.bodyA.id, collision.bodyB.id) * 10000;
|
|
|
|
const collisionCompareId = (collisionA, collisionB) => collisionId(collisionA) - collisionId(collisionB);
|
|
|
|
const sortById = (objs) => objs.sort((objA, objB) => objA.id - objB.id);
|
|
|
|
module.exports = { runExample };
|