78 lines
3.5 KiB
TypeScript
78 lines
3.5 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { classifyByRoles, normalizePositions } from '../src/view/layout';
|
|
|
|
/** Maximum distance of any point from the origin, over a flat xyz triple array. */
|
|
function maxRadius(points: Float32Array): number {
|
|
let radius = 0;
|
|
for (let i = 0; i < points.length; i += 3) radius = Math.max(radius, Math.hypot(points[i], points[i + 1], points[i + 2]));
|
|
return radius;
|
|
}
|
|
|
|
/** Mean of one component (0 = x, 1 = y, 2 = z) over a flat xyz triple array. */
|
|
function mean(points: Float32Array, component: number): number {
|
|
let total = 0;
|
|
for (let i = component; i < points.length; i += 3) total += points[i];
|
|
return total / (points.length / 3);
|
|
}
|
|
|
|
test('normalizePositions centers the cloud, scales the max radius to 1.3 and flattens z', () => {
|
|
const source = new Float32Array([
|
|
1000, 2000, -500,
|
|
1400, 2000, -500,
|
|
1200, 2600, -500,
|
|
1200, 1400, -500,
|
|
]);
|
|
const normalized = normalizePositions(source);
|
|
assert.equal(normalized.length, source.length);
|
|
assert.ok(Math.abs(mean(normalized, 0)) < 1e-6, `x mean ${mean(normalized, 0)}`);
|
|
assert.ok(Math.abs(mean(normalized, 1)) < 1e-6, `y mean ${mean(normalized, 1)}`);
|
|
assert.ok(Math.abs(maxRadius(normalized) - 1.3) < 1e-5, `max radius ${maxRadius(normalized)}`);
|
|
for (let i = 2; i < normalized.length; i += 3) assert.equal(normalized[i], 0);
|
|
});
|
|
|
|
test('normalizePositions scales by the 3D radius and only then flattens z', () => {
|
|
// Centroid (1.5, 0, 2), both points at 3D radius 2.5, so the scale is 1.3 / 2.5 = 0.52 and the
|
|
// flattened points land at x = +-1.5 * 0.52 = +-0.78 -- inside 1.3, because depth is discarded.
|
|
const normalized = normalizePositions(new Float32Array([0, 0, 0, 3, 0, 4]));
|
|
assert.ok(Math.abs(normalized[0] + 0.78) < 1e-6, `x0 ${normalized[0]}`);
|
|
assert.ok(Math.abs(normalized[3] - 0.78) < 1e-6, `x1 ${normalized[3]}`);
|
|
assert.equal(normalized[2], 0);
|
|
assert.equal(normalized[5], 0);
|
|
});
|
|
|
|
test('normalizePositions preserves relative geometry up to the uniform scale', () => {
|
|
// The centroid is already the origin here, so the scale is exactly 1.3 / hypot(2, 4).
|
|
const source = new Float32Array([0, 0, 0, 2, 0, 0, 0, 4, 0, -2, -4, 0]);
|
|
const scale = 1.3 / Math.hypot(2, 4);
|
|
const normalized = normalizePositions(source);
|
|
for (let i = 0; i < source.length; i += 3) {
|
|
assert.ok(Math.abs(normalized[i] - source[i] * scale) < 1e-6, `x at ${i}: ${normalized[i]}`);
|
|
assert.ok(Math.abs(normalized[i + 1] - source[i + 1] * scale) < 1e-6, `y at ${i}: ${normalized[i + 1]}`);
|
|
}
|
|
assert.ok(Math.abs(maxRadius(normalized) - 1.3) < 1e-6);
|
|
});
|
|
|
|
test('classifyByRoles labels sensory 0, output 2 and everything else 1', () => {
|
|
const roles = {
|
|
sensory: [0, 1],
|
|
motor: [4],
|
|
descending: [5, 6],
|
|
kenyon: [2, 3],
|
|
};
|
|
const classes = classifyByRoles(roles, 8);
|
|
assert.ok(classes instanceof Uint8Array);
|
|
assert.deepEqual(Array.from(classes), [0, 0, 1, 1, 2, 2, 2, 1]);
|
|
});
|
|
|
|
test('classifyByRoles applies the output roles last, so an overlap is drawn as output', () => {
|
|
const classes = classifyByRoles({ sensory: [0, 1], motor: [1] }, 3);
|
|
assert.deepEqual(Array.from(classes), [0, 2, 1]);
|
|
});
|
|
|
|
test('classifyByRoles honours custom role names and tolerates missing roles', () => {
|
|
const roles = { photoreceptor: [0], command_0: [2], sensory: [1], motor: [1] };
|
|
const classes = classifyByRoles(roles, 4, { sensory: ['photoreceptor'], output: ['command_0', 'absent'] });
|
|
assert.deepEqual(Array.from(classes), [0, 1, 2, 1]);
|
|
assert.deepEqual(Array.from(classifyByRoles({}, 3)), [1, 1, 1]);
|
|
});
|