import test from 'node:test' import assert from 'node:assert/strict' import { createDataModules } from '../src/facade/dataModules' const bytes = (value: string) => new TextEncoder().encode(value) const close = (actual: number, expected: number, tolerance = 1e-9) => assert.ok(Math.abs(actual - expected) <= tolerance, `${actual} != ${expected}`) test('data adapters expose specialist module coverage and proxy records', () => { const data = createDataModules() assert.equal(data.descriptors().length, 7) assert.equal(data.descriptors().find((entry) => entry.module === 'Points')?.status, 'supported') assert.equal(data.descriptors().find((entry) => entry.module === 'ReverseEngineering')?.status, 'supported') const record = data.importRecord({ id: 'openscad', module: 'OpenSCAD', format: 'SCAD', bytes: bytes('cube(1);'), metadata: { units: 'mm' } }) assert.equal(record.proxy, true) assert.equal(record.format, 'scad') assert.match(data.exportManifest(), /"module": "OpenSCAD"/) }) test('Points imports, exports, translates, crops, downsamples, structures and merges point clouds', () => { const data = createDataModules() const cloud = data.importPointCloud({ id: 'grid', format: 'csv', bytes: bytes('x,y,z\n0,0,0\n1,0,1\n2,0,2\n0,1,3\n1,1,4\n2,1,5\n'), }) assert.equal(cloud.points.length, 6) assert.deepEqual(cloud.bounds, { min: { x: 0, y: 0, z: 0 }, max: { x: 2, y: 1, z: 5 } }) assert.deepEqual(cloud.centroid, { x: 1, y: 0.5, z: 2.5 }) const structured = data.structurePointCloud({ id: 'structured', sourceId: 'grid' }) assert.deepEqual(structured.structured, { width: 3, height: 2, grid: [0, 1, 2, 3, 4, 5] }) const translated = data.translatePointCloud({ id: 'centered', sourceId: 'grid', offset: { x: -1, y: -0.5, z: -2.5 } }) assert.deepEqual(translated.centroid, { x: 0, y: 0, z: 0 }) const cropped = data.cropPointCloud({ id: 'cropped', sourceId: 'grid', bounds: { min: { x: 1, y: 0, z: 0 }, max: { x: 2, y: 1, z: 5 } } }) assert.equal(cropped.points.length, 4) const polygonCropped = data.polygonCropPointCloud({ id: 'polygon-cropped', sourceId: 'grid', polygon: [{ x: 0, y: 0 }, { x: 1.1, y: 0 }, { x: 1.1, y: 1.1 }, { x: 0, y: 1.1 }] }) assert.equal(polygonCropped.points.length, 4) const reduced = data.voxelDownsample({ id: 'reduced', sourceId: 'grid', size: 10 }) assert.equal(reduced.points.length, 1) assert.deepEqual(reduced.points[0], cloud.centroid) const merged = data.mergePointClouds({ id: 'merged', sourceIds: ['grid', 'centered'] }) assert.equal(merged.points.length, 12) const xyz = new TextDecoder().decode(data.exportPointCloud('grid', 'xyz')) assert.equal(xyz, '0 0 0\n1 0 1\n2 0 2\n0 1 3\n1 1 4\n2 1 5\n') const roundTrip = createDataModules().importPointCloud({ id: 'roundtrip', format: 'xyz', bytes: bytes(xyz) }) assert.deepEqual(roundTrip.points, cloud.points) }) test('Points parses PTS, ASCII PCD and ASCII PLY with explicit format errors', () => { const data = createDataModules() assert.equal(data.importPointCloud({ id: 'pts', format: 'pts', bytes: bytes('2\n1 2 3\n4 5 6\n') }).points.length, 2) assert.equal(data.importPointCloud({ id: 'pcd', format: 'pcd', bytes: bytes('VERSION 0.7\nFIELDS x y z\nWIDTH 2\nHEIGHT 1\nPOINTS 2\nDATA ascii\n1 2 3\n4 5 6\n') }).points.length, 2) assert.equal(data.importPointCloud({ id: 'ply', format: 'ply', bytes: bytes('ply\nformat ascii 1.0\nelement vertex 2\nproperty float x\nproperty float y\nproperty float z\nend_header\n1 2 3\n4 5 6\n') }).points.length, 2) assert.throws(() => data.importPointCloud({ id: 'short', format: 'pts', bytes: bytes('3\n1 2 3\n4 5 6\n') }), /declared 3 points/) assert.throws(() => data.importPointCloud({ id: 'binary', format: 'ply', bytes: bytes('ply\nformat binary_little_endian 1.0\nend_header\n') }), /Only ASCII PLY/) assert.throws(() => data.importPointCloud({ id: 'empty', format: 'xyz', bytes: bytes('# no points\n') }), /at least one point/) }) test('ReverseEngineering fits FreeCAD-style plane, sphere and cylinder primitives', () => { const data = createDataModules() const planeRows: string[] = [] for (const x of [-2, 0, 2]) for (const y of [-1, 1]) planeRows.push(`${x},${y},${2 * x + 3 * y + 4}`) data.importPointCloud({ id: 'plane-points', format: 'csv', bytes: bytes(`x,y,z\n${planeRows.join('\n')}\n`) }) const plane = data.fitPlane({ id: 'plane-fit', sourceId: 'plane-points', referenceNormal: { x: -2, y: -3, z: 1 } }) close(plane.rms, 0) close(plane.normal.x, -2 / Math.sqrt(14)) close(plane.normal.y, -3 / Math.sqrt(14)) close(plane.normal.z, 1 / Math.sqrt(14)) const spherePoints = [ [3, 2, 3], [-1, 2, 3], [1, 4, 3], [1, 0, 3], [1, 2, 5], [1, 2, 1], [1 + Math.sqrt(2), 2 + Math.sqrt(2), 3], [1 - Math.sqrt(2), 2 - Math.sqrt(2), 3], ].map((entry) => entry.join(' ')).join('\n') data.importPointCloud({ id: 'sphere-points', format: 'xyz', bytes: bytes(`${spherePoints}\n`) }) const sphere = data.fitSphere({ id: 'sphere-fit', sourceId: 'sphere-points' }) close(sphere.center.x, 1); close(sphere.center.y, 2); close(sphere.center.z, 3); close(sphere.radius, 2); close(sphere.rms, 0) const cylinderRows: string[] = [] for (const z of [-4, 0, 4]) for (let index = 0; index < 8; index += 1) { const angle = index * Math.PI / 4; cylinderRows.push(`${2 + 3 * Math.cos(angle)} ${-1 + 3 * Math.sin(angle)} ${z}`) } data.importPointCloud({ id: 'cylinder-points', format: 'xyz', bytes: bytes(`${cylinderRows.join('\n')}\n`) }) const cylinder = data.fitCylinder({ id: 'cylinder-fit', sourceId: 'cylinder-points' }) close(cylinder.radius, 3); close(cylinder.height, 8); close(cylinder.rms, 0, 1e-8); close(Math.abs(cylinder.axis.z), 1) const polynomialRows: string[] = [] for (const x of [-1, 0, 1]) for (const y of [-1, 0, 1]) polynomialRows.push(`${x} ${y} ${x * x + 2 * x * y + 3 * y * y + 4 * x + 5 * y + 6}`) data.importPointCloud({ id: 'polynomial-points', format: 'xyz', bytes: bytes(`${polynomialRows.join('\n')}\n`) }) const polynomial = data.fitPolynomialSurface({ id: 'polynomial-fit', sourceId: 'polynomial-points' }) assert.deepEqual(polynomial.coefficients.map((value) => Number(value.toFixed(8))), [1, 2, 3, 4, 5, 6]) close(polynomial.rms, 0) data.importPointCloud({ id: 'segmentation-points', format: 'xyz', bytes: bytes('0 0 0\n0 0.1 0\n10 0 0\n10 0.1 0\n') }) const segments = data.segmentPointCloud({ id: 'segments', sourceId: 'segmentation-points', radius: 0.3 }) assert.deepEqual(segments.clusters.map((cluster) => cluster.pointIndices.length), [2, 2]) assert.equal(data.snapshot().reverseEngineering.length, 4) assert.equal(data.snapshot().segments.length, 1) }) test('data adapters reject unknown formats, degenerate fits and oversized records', () => { const data = createDataModules() assert.throws(() => data.importRecord({ id: 'bad', module: 'JtReader', format: 'step', bytes: new Uint8Array([1]) }), /does not accept/) assert.throws(() => data.importRecord({ id: 'large', module: 'Import', format: 'step', bytes: { length: 8 * 1024 * 1024 + 1 } }), /outside the supported limit/) data.importPointCloud({ id: 'line', format: 'xyz', bytes: bytes('0 0 0\n1 0 0\n2 0 0\n3 0 0\n') }) assert.throws(() => data.fitPlane({ id: 'bad-plane', sourceId: 'line' }), /degenerate|non-zero/) assert.throws(() => data.cropPointCloud({ id: 'empty-crop', sourceId: 'line', bounds: { min: { x: 9, y: 9, z: 9 }, max: { x: 10, y: 10, z: 10 } } }), /removed every point/) assert.throws(() => data.polygonCropPointCloud({ id: 'bad-polygon', sourceId: 'line', polygon: [{ x: 0, y: 0 }, { x: 1, y: 1 }] }), /non-degenerate/) assert.throws(() => data.segmentPointCloud({ id: 'bad-segment', sourceId: 'line', radius: 0 }), /positive/) })