0
0
Fork 0
mirror of https://github.com/liabru/matter-js.git synced 2024-11-23 09:26:51 -05:00
liabru-matter-js/test/TestTools.js
2019-09-30 23:28:17 +01:00

257 lines
No EOL
8.4 KiB
JavaScript

/* eslint-env es6 */
"use strict";
const fs = require('fs');
const compactStringify = require('json-stringify-pretty-compact');
const { Composite, Constraint } = require('../src/module/main');
const comparePath = './test/__compare__';
const compareCommand = 'open http://localhost:8000/?compare'
const diffCommand = 'code -n -d test/__compare__/examples-dev.json test/__compare__/examples-build.json';
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 stubBrowserFeatures = M => {
const noop = () => ({ collisionFilter: {}, mouse: {} });
M.Render.create = () => ({ options: {}, bounds: { min: { x: 0, y: 0 }, max: { x: 800, y: 600 }}});
M.Render.run = M.Render.lookAt = noop;
M.Runner.create = M.Runner.run = noop;
M.MouseConstraint.create = M.Mouse.create = noop;
M.Common.log = M.Common.info = M.Common.warn = noop;
return M;
};
const colors = { White: 37, BrightWhite: 90, BrightCyan: 36 };
const color = (text, number) => `\x1b[${number}m${text}\x1b[0m`;
const limit = (val, precision=3) => parseFloat(val.toPrecision(precision));
const engineSnapshot = (engine) => ({
timestamp: limit(engine.timing.timestamp),
world: worldSnapshotExtrinsic(engine.world),
worldIntrinsic: worldSnapshotIntrinsic(engine.world)
});
const worldSnapshotExtrinsic = world => ({
bodies: 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: Composite.allConstraints(world).reduce((constraints, constraint) => {
const positionA = Constraint.pointAWorld(constraint);
const positionB = Constraint.pointBWorld(constraint);
constraints[constraint.id] = [
positionA.x,
positionA.y,
positionB.x,
positionB.y
];
return constraints;
}, {})
});
const worldSnapshotIntrinsic = world => worldSnapshotIntrinsicBase({
bodies: Composite.allBodies(world).reduce((bodies, body) => {
bodies[body.id] = body;
return bodies;
}, {}),
constraints: Composite.allConstraints(world).reduce((constraints, constraint) => {
constraints[constraint.id] = constraint;
return constraints;
}, {}),
composites: Composite.allComposites(world).reduce((composites, composite) => {
composites[composite.id] = {
bodies: Composite.allBodies(composite).map(body => body.id),
constraints: Composite.allConstraints(composite).map(constraint => constraint.id),
composites: Composite.allComposites(composite).map(composite => composite.id)
};
return composites;
}, {})
});
const worldSnapshotIntrinsicBase = (obj, depth=0) => {
if (obj === Infinity) {
return 'Infinity';
} else if (typeof obj === 'number') {
return limit(obj);
} else if (Array.isArray(obj)) {
return obj.map(item => worldSnapshotIntrinsicBase(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] = worldSnapshotIntrinsicBase(val, depth + 1);
return cleaned;
}, {});
return Object.keys(result).sort()
.reduce((sorted, key) => (sorted[key] = result[key], sorted), {});
};
const similarity = (a, b) => {
const distance = Math.sqrt(a.reduce((sum, _val, i) => sum + Math.pow(a[i] - b[i], 2), 0));
return 1 / (1 + (distance / a.length));
};
const snapshotSimilarityExtrinsic = (currentSnapshots, referenceSnapshots) => {
const result = {};
Object.entries(currentSnapshots).forEach(([name, current]) => {
const reference = referenceSnapshots[name];
const worldVector = [];
const worldVectorRef = [];
Object.keys(current.world).forEach(objectType => {
Object.keys(current.world[objectType]).forEach(objectId => {
worldVector.push(...current.world[objectType][objectId]);
worldVectorRef.push(...reference.world[objectType][objectId]);
});
});
result[name] = similarity(worldVector, worldVectorRef);
});
return result;
};
const writeSnapshots = (name, obj) => {
try {
fs.mkdirSync(comparePath, { recursive: true });
} catch (err) {
if (err.code !== 'EEXIST') throw err;
}
fs.writeFileSync(`${comparePath}/${name}.json`, compactStringify(obj, { maxLength: 100 }), 'utf8');
};
const toMatchExtrinsics = {
toMatchExtrinsics(received, value, ticks) {
const changed = [];
const borderline = [];
const equal = [];
const similaritys = snapshotSimilarityExtrinsic(received, value);
const entries = Object.entries(similaritys);
entries.sort(([_nameA, similarityA], [_nameB, similarityB]) => similarityA - similarityB);
entries.forEach(([name, similarity], i) => {
const percentSimilar = similarity * 100;
if (percentSimilar < 99.99) {
const col = i < 5 ? colors.White : colors.BrightWhite;
changed.push(color(`${name}`, col) + ` ${percentSimilar.toFixed(2)}%`);
} else if (percentSimilar !== 100) {
borderline.push(`~ ${name}`);
} else {
equal.push(`${name}`);
}
});
const pass = equal.length === similaritys.length && changed.length === 0 && borderline.length === 0;
return {
message: () => `Expected positions and velocities to match between builds.
${color('▶', colors.White)} Debug using ${color(compareCommand + '=' + ticks + '#' + entries[0][0], colors.BrightCyan)}
(${changed.length}) Changed
${changed.join(' ')}
(${borderline.length}) Borderline (> 99.99%)
${borderline.join(' ').slice(0, 80)}...
(${equal.length}) Equal
${equal.join(' ').slice(0, 80)}...`,
pass
};
}
};
const toMatchIntrinsics = {
toMatchIntrinsics(currentSnapshots, referenceSnapshots) {
const changed = [];
const equal = [];
const currentChanged = {};
const referenceChanged = {};
const entries = Object.entries(currentSnapshots);
entries.forEach(([name, current]) => {
const reference = referenceSnapshots[name];
const endWorld = current.worldIntrinsic;
const endWorldRef = reference.worldIntrinsic;
if (this.equals(endWorld, endWorldRef)) {
equal.push(`${name}`);
} else {
changed.push(color(`${name}`, changed.length < 5 ? colors.White : colors.BrightWhite));
if (changed.length < 2) {
currentChanged[name] = endWorld;
referenceChanged[name] = endWorldRef;
}
}
});
const pass = equal.length === entries.length && changed.length === 0;
writeSnapshots('examples-dev', currentChanged);
writeSnapshots('examples-build', referenceChanged);
return {
message: () => `Expected intrinsic properties to match between builds.
(${changed.length}) Changed
${changed.join(' ')}
(${equal.length}) Equal
${equal.join(' ').slice(0, 80)}...
${color('▶', colors.White)} Inspect using ${color(diffCommand, colors.BrightCyan)}`,
pass
};
}
};
module.exports = {
stubBrowserFeatures, engineSnapshot, toMatchExtrinsics, toMatchIntrinsics
};