diff --git a/test/ExampleWorker.js b/test/ExampleWorker.js index 9ba242a..2794563 100644 --- a/test/ExampleWorker.js +++ b/test/ExampleWorker.js @@ -6,155 +6,6 @@ const mock = require('mock-require'); const { requireUncached } = require('./TestTools'); const consoleOriginal = global.console; -const intrinsicProps = [ - // Common - 'id', 'label', - - // Constraint - 'angularStiffness', 'bodyA', 'bodyB', 'damping', 'length', 'stiffness', - - // Body - 'area', 'axes', 'collisionFilter', 'category', 'mask', - 'group', 'density', 'friction', 'frictionAir', 'frictionStatic', 'inertia', 'inverseInertia', 'inverseMass', 'isSensor', - 'isSleeping', 'isStatic', 'mass', 'parent', 'parts', 'restitution', 'sleepThreshold', 'slop', - 'timeScale', 'vertices', - - // Composite - 'bodies', 'constraints', 'composites' -]; - -const prepareMatter = (options) => { - const Matter = requireUncached(options.useDev ? '../build/matter.dev' : '../build/matter'); - - if (Matter.Common._nextId !== 0) { - throw 'Matter instance has already been used.'; - } - - const noop = () => ({ collisionFilter: {}, mouse: {} }); - - Matter.Render.create = () => ({ options: {}, bounds: { min: { x: 0, y: 0 }, max: { x: 800, y: 600 }}}); - Matter.Render.run = Matter.Render.lookAt = noop; - Matter.Runner.create = Matter.Runner.run = noop; - Matter.MouseConstraint.create = Matter.Mouse.create = noop; - Matter.Common.info = Matter.Common.warn = Matter.Common.log; - - return Matter; -}; - -const prepareEnvironment = Matter => { - mock('matter-js', Matter); - global.Matter = Matter; - - const logs = []; - global.document = global.window = { addEventListener: () => {} }; - global.console = { - log: (...args) => { - logs.push(args.join(' ')); - } - }; - - return logs; -}; - -const resetEnvironment = () => { - global.console = consoleOriginal; - global.window = undefined; - global.document = undefined; - global.Matter = undefined; - mock.stopAll(); -}; - -const limitPrecision = (val, precision=3) => parseFloat(val.toPrecision(precision)); - -const sortById = (objs) => { - objs.sort((objA, objB) => objA.id - objB.id); - return objs; -}; - -const engineCapture = (engine, Matter) => ({ - timestamp: limitPrecision(engine.timing.timestamp), - extrinsic: worldCaptureExtrinsic(engine.world, Matter), - intrinsic: worldCaptureIntrinsic(engine.world, Matter) -}); - -const worldCaptureExtrinsic = (world, Matter) => ({ - bodies: sortById(Matter.Composite.allBodies(world)).reduce((bodies, body) => { - bodies[body.id] = [ - body.position.x, - body.position.y, - body.positionPrev.x, - body.positionPrev.y, - body.angle, - body.anglePrev, - ...body.vertices.reduce((flat, vertex) => (flat.push(vertex.x, vertex.y), flat), []) - ]; - - return bodies; - }, {}), - constraints: sortById(Matter.Composite.allConstraints(world)).reduce((constraints, constraint) => { - const positionA = Matter.Constraint.pointAWorld(constraint); - const positionB = Matter.Constraint.pointBWorld(constraint); - - constraints[constraint.id] = [ - positionA.x, - positionA.y, - positionB.x, - positionB.y - ]; - - return constraints; - }, {}) -}); - -const worldCaptureIntrinsic = (world, Matter) => worldCaptureIntrinsicBase({ - bodies: sortById(Matter.Composite.allBodies(world)).reduce((bodies, body) => { - bodies[body.id] = body; - return bodies; - }, {}), - constraints: sortById(Matter.Composite.allConstraints(world)).reduce((constraints, constraint) => { - constraints[constraint.id] = constraint; - return constraints; - }, {}), - composites: sortById(Matter.Composite.allComposites(world)).reduce((composites, composite) => { - composites[composite.id] = { - bodies: sortById(Matter.Composite.allBodies(composite)).map(body => body.id), - constraints: sortById(Matter.Composite.allConstraints(composite)).map(constraint => constraint.id), - composites: sortById(Matter.Composite.allComposites(composite)).map(composite => composite.id) - }; - return composites; - }, {}) -}); - -const worldCaptureIntrinsicBase = (obj, depth=0) => { - if (obj === Infinity) { - return 'Infinity'; - } else if (typeof obj === 'number') { - return limitPrecision(obj); - } else if (Array.isArray(obj)) { - return obj.map(item => worldCaptureIntrinsicBase(item, depth + 1)); - } else if (typeof obj !== 'object') { - return obj; - } - - const result = Object.entries(obj) - .filter(([key]) => depth <= 1 || intrinsicProps.includes(key)) - .reduce((cleaned, [key, val]) => { - if (val && val.id && String(val.id) !== key) { - val = val.id; - } - - if (Array.isArray(val) && !['composites', 'constraints', 'bodies'].includes(key)) { - val = `[${val.length}]`; - } - - cleaned[key] = worldCaptureIntrinsicBase(val, depth + 1); - return cleaned; - }, {}); - - return Object.keys(result).sort() - .reduce((sorted, key) => (sorted[key] = result[key], sorted), {}); -}; - const runExample = options => { const Matter = prepareMatter(options); const logs = prepareEnvironment(Matter); @@ -168,22 +19,9 @@ const runExample = options => { let overlapTotal = 0; let overlapCount = 0; - const bodies = Matter.Composite.allBodies(engine.world); - - if (options.jitter) { - for (let i = 0; i < bodies.length; i += 1) { - const body = bodies[i]; - - Matter.Body.applyForce(body, body.position, { - x: Math.cos(i * i) * options.jitter * body.mass, - y: Math.sin(i * i) * options.jitter * body.mass - }); - } - } - global.gc(); - for (let i = 0; i < options.totalUpdates; i += 1) { + for (let i = 0; i < options.updates; i += 1) { const startTime = process.hrtime(); totalMemory += process.memoryUsage().heapUsed; @@ -214,9 +52,186 @@ const runExample = options => { duration: totalDuration, overlap: overlapTotal / (overlapCount || 1), memory: totalMemory, - logs, - ...engineCapture(engine, Matter) + logs: logs, + extrinsic: captureExtrinsics(engine, Matter), + intrinsic: captureIntrinsics(engine, Matter), }; }; +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.'; + } + + const noop = () => ({ collisionFilter: {}, mouse: {} }); + + Matter.Render.create = () => ({ options: {}, bounds: { min: { x: 0, y: 0 }, max: { x: 800, y: 600 }}}); + Matter.Render.run = Matter.Render.lookAt = noop; + Matter.Runner.create = Matter.Runner.run = noop; + Matter.MouseConstraint.create = Matter.Mouse.create = noop; + Matter.Common.info = Matter.Common.warn = Matter.Common.log; + + if (options.stableSort) { + 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 = Matter => { + mock('matter-js', Matter); + global.Matter = Matter; + + const logs = []; + global.document = global.window = { addEventListener: () => {} }; + global.console = { + log: (...args) => { + logs.push(args.join(' ')); + } + }; + + return logs; +}; + +const resetEnvironment = () => { + global.console = consoleOriginal; + 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] = [ + body.position.x, + body.position.y, + body.positionPrev.x, + body.positionPrev.y, + body.angle, + body.anglePrev, + ...body.vertices.reduce((flat, vertex) => (flat.push(vertex.x, vertex.y), flat), []) + ]; + + return bodies; + }, {}), + constraints: Matter.Composite.allConstraints(world).reduce((constraints, constraint) => { + const positionA = Matter.Constraint.pointAWorld(constraint); + const positionB = Matter.Constraint.pointBWorld(constraint); + + constraints[constraint.id] = [ + positionA.x, + positionA.y, + positionB.x, + positionB.y + ]; + + return constraints; + }, {}) +}); + +const captureIntrinsics = ({ world }, Matter) => formatIntrinsics({ + 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; + }, {}) +}); + +const formatIntrinsics = (obj, depth=0) => { + if (obj === Infinity) { + return 'Infinity'; + } else if (typeof obj === 'number') { + return limitPrecision(obj); + } else if (Array.isArray(obj)) { + return obj.map(item => formatIntrinsics(item, depth + 1)); + } else if (typeof obj !== 'object') { + return obj; + } + + const result = Object.entries(obj) + .filter(([key]) => depth <= 1 || intrinsicProperties.includes(key)) + .reduce((cleaned, [key, val]) => { + if (val && val.id && String(val.id) !== key) { + val = val.id; + } + + if (Array.isArray(val) && !['composites', 'constraints', 'bodies'].includes(key)) { + val = `[${val.length}]`; + } + + cleaned[key] = formatIntrinsics(val, depth + 1); + return cleaned; + }, {}); + + return Object.keys(result).sort() + .reduce((sorted, key) => (sorted[key] = result[key], sorted), {}); +}; + +const intrinsicProperties = [ + // Common + 'id', 'label', + + // Constraint + 'angularStiffness', 'bodyA', 'bodyB', 'damping', 'length', 'stiffness', + + // Body + 'area', 'axes', 'collisionFilter', 'category', 'mask', + 'group', 'density', 'friction', 'frictionAir', 'frictionStatic', 'inertia', 'inverseInertia', 'inverseMass', 'isSensor', + 'isSleeping', 'isStatic', 'mass', 'parent', 'parts', 'restitution', 'sleepThreshold', 'slop', + 'timeScale', 'vertices', + + // Composite + 'bodies', 'constraints', 'composites' +]; + +const 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); + +const limitPrecision = (val, precision=3) => parseFloat(val.toPrecision(precision)); + module.exports = { runExample }; \ No newline at end of file