73 lines
10 KiB
TypeScript
73 lines
10 KiB
TypeScript
import { createMockFacade } from './facade/mockFacade'
|
|
import { createDataModules } from './facade/dataModules'
|
|
|
|
type DataReport = {
|
|
schemaVersion: 1
|
|
status: 'pass' | 'failed'
|
|
browserId: 'chrome'
|
|
crossOriginIsolated: boolean
|
|
adapters?: { total: number; supported: number; proxy: number }
|
|
records?: { total: number; proxy: number; manifestBytes: number }
|
|
points?: { sourceCount: number; parsedFormats: string[]; mergedCount: number; croppedCount: number; polygonCroppedCount: number; reducedCount: number; structured: { width: number; height: number; cells: number }; centered: { x: number; y: number; z: number }; exportBytes: Record<string, number> }
|
|
reverseEngineering?: { planeRms: number; sphereRadius: number; cylinderRadius: number; cylinderHeight: number; polynomialRms: number; segmentCount: number; segmentSizes: number[] }
|
|
persistence?: { mode: string; byteLength: number; hash: string; roundTrip: boolean; released: boolean }
|
|
opfs?: { markerSuite: string; records: number; markerRemoved: boolean }
|
|
afterRelease?: { shapeCount: number; kernelReferenceCount: number }
|
|
error?: string
|
|
}
|
|
|
|
const sha256 = async (bytes: Uint8Array) => [...new Uint8Array(await crypto.subtle.digest('SHA-256', Uint8Array.from(bytes).buffer))].map((value) => value.toString(16).padStart(2, '0')).join('')
|
|
const encode = (value: string) => new TextEncoder().encode(value)
|
|
const close = (actual: number, expected: number, tolerance = 1e-7) => Math.abs(actual - expected) <= tolerance
|
|
|
|
const run = async (): Promise<DataReport> => {
|
|
const facade = createMockFacade(); const report: DataReport = { schemaVersion: 1, status: 'failed', browserId: 'chrome', crossOriginIsolated: globalThis.crossOriginIsolated === true }
|
|
try {
|
|
if (!report.crossOriginIsolated || !navigator.storage?.getDirectory) throw new Error('Chrome data adapter evidence requires isolated OPFS.')
|
|
await facade.project.list()
|
|
const data = createDataModules()
|
|
const genericInputs = [['reverse', 'ReverseEngineering', 'point-cloud'], ['openscad', 'OpenSCAD', 'scad'], ['idf', 'Idf', 'idf'], ['jt', 'JtReader', 'jt'], ['material', 'Material', 'json'], ['import', 'Import', 'step']] as const
|
|
for (const [id, module, format] of genericInputs) data.importRecord({ id, module, format, bytes: encode(`${module}:${format}`), metadata: { source: 'chrome' } })
|
|
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}`)
|
|
const grid = data.importPointCloud({ id: 'points', format: 'csv', bytes: encode(`x,y,z\n${['0,0,0', '1,0,1', '2,0,2', '0,1,3', '1,1,4', '2,1,5'].join('\n')}\n`) })
|
|
const parsedFormats = ['pts', 'pcd', 'ply'].map((format, index) => {
|
|
const payload = format === 'pts' ? '2\n1 2 3\n4 5 6\n' : format === 'pcd' ? 'VERSION 0.7\nFIELDS x y z\nWIDTH 2\nHEIGHT 1\nPOINTS 2\nDATA ascii\n1 2 3\n4 5 6\n' : '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'
|
|
data.importPointCloud({ id: `parsed-${index}`, format: format as 'pts' | 'pcd' | 'ply', bytes: encode(payload), record: false }); return format
|
|
})
|
|
const centered = data.translatePointCloud({ id: 'centered', sourceId: 'points', offset: { x: -1, y: -0.5, z: -2.5 } })
|
|
const merged = data.mergePointClouds({ id: 'merged', sourceIds: ['points', 'centered'] })
|
|
const cropped = data.cropPointCloud({ id: 'cropped', sourceId: 'points', bounds: { min: { x: 1, y: 0, z: 0 }, max: { x: 2, y: 1, z: 5 } } })
|
|
const polygonCropped = data.polygonCropPointCloud({ id: 'polygon-cropped', sourceId: 'points', polygon: [{ x: 0, y: 0 }, { x: 1.1, y: 0 }, { x: 1.1, y: 1.1 }, { x: 0, y: 1.1 }] })
|
|
const reduced = data.voxelDownsample({ id: 'reduced', sourceId: 'points', size: 10 })
|
|
const structured = data.structurePointCloud({ id: 'structured', sourceId: 'points' })
|
|
const plane = data.importPointCloud({ id: 'plane', format: 'csv', bytes: encode(`x,y,z\n${planeRows.join('\n')}\n`), record: false })
|
|
const planeFit = data.fitPlane({ id: 'plane-fit', sourceId: plane.id, referenceNormal: { x: -2, y: -3, z: 1 } })
|
|
const sphere = data.importPointCloud({ id: 'sphere', format: 'xyz', bytes: encode('3 2 3\n-1 2 3\n1 4 3\n1 0 3\n1 2 5\n1 2 1\n2.414213562373095 3.414213562373095 3\n-0.414213562373095 0.585786437626905 3\n'), record: false })
|
|
const sphereFit = data.fitSphere({ id: 'sphere-fit', sourceId: sphere.id })
|
|
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}`) }
|
|
const cylinder = data.importPointCloud({ id: 'cylinder', format: 'xyz', bytes: encode(`${cylinderRows.join('\n')}\n`), record: false })
|
|
const cylinderFit = data.fitCylinder({ id: 'cylinder-fit', sourceId: cylinder.id })
|
|
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}`)
|
|
const polynomial = data.importPointCloud({ id: 'polynomial', format: 'xyz', bytes: encode(`${polynomialRows.join('\n')}\n`), record: false })
|
|
const polynomialFit = data.fitPolynomialSurface({ id: 'polynomial-fit', sourceId: polynomial.id })
|
|
const segmentation = data.importPointCloud({ id: 'segmentation', format: 'xyz', bytes: encode('0 0 0\n0 0.1 0\n10 0 0\n10 0.1 0\n'), record: false })
|
|
const segmented = data.segmentPointCloud({ id: 'segmented', sourceId: segmentation.id, radius: 0.3 })
|
|
const snapshot = data.snapshot(); const manifest = encode(data.exportManifest()); const payload = encode(JSON.stringify(snapshot)); const stored = await facade.project.resource.put(payload, 'application/json'); const loaded = await facade.project.resource.get(stored.hash); const roundTrip = loaded !== null && await sha256(loaded) === await sha256(payload); await facade.project.resource.release(stored.hash); const released = await facade.project.resource.get(stored.hash) === null
|
|
const directory = await navigator.storage.getDirectory(); const markerName = 'data-chrome-marker.json'; const marker = await directory.getFileHandle(markerName, { create: true }); const writable = await marker.createWritable(); await writable.write(JSON.stringify({ suite: 'DATA-ALL', records: snapshot.records.length })); await writable.close(); const markerPayload = JSON.parse(await (await marker.getFile()).text()) as { suite: string; records: number }; await directory.removeEntry(markerName); let markerRemoved = false; try { await directory.getFileHandle(markerName) } catch (error) { markerRemoved = error instanceof DOMException && error.name === 'NotFoundError' }
|
|
const exportBytes = Object.fromEntries((['asc', 'pts', 'xyz', 'csv', 'pcd', 'ply'] as const).map((format) => [format, data.exportPointCloud('points', format).byteLength]))
|
|
report.adapters = { total: snapshot.adapters.length, supported: snapshot.adapters.filter((entry) => entry.status === 'supported').length, proxy: snapshot.adapters.filter((entry) => entry.status === 'proxy').length }
|
|
report.records = { total: snapshot.records.length, proxy: snapshot.records.filter((entry) => entry.proxy).length, manifestBytes: manifest.byteLength }
|
|
report.points = { sourceCount: grid.points.length, parsedFormats, mergedCount: merged.points.length, croppedCount: cropped.points.length, polygonCroppedCount: polygonCropped.points.length, reducedCount: reduced.points.length, structured: { width: structured.structured!.width, height: structured.structured!.height, cells: structured.structured!.grid.length }, centered: centered.centroid, exportBytes }
|
|
report.reverseEngineering = { planeRms: planeFit.rms, sphereRadius: sphereFit.radius, cylinderRadius: cylinderFit.radius, cylinderHeight: cylinderFit.height, polynomialRms: polynomialFit.rms, segmentCount: segmented.clusters.length, segmentSizes: segmented.clusters.map((cluster) => cluster.pointIndices.length) }
|
|
report.persistence = { mode: facade.project.capabilities().mode, byteLength: stored.byteLength, hash: stored.hash, roundTrip, released }
|
|
report.opfs = { markerSuite: markerPayload.suite, records: markerPayload.records, markerRemoved }
|
|
report.status = report.adapters.total === 7 && report.adapters.supported === 4 && report.adapters.proxy === 3 && report.records.total === 7 && report.records.proxy === 3 && report.points.sourceCount === 6 && report.points.parsedFormats.join(',') === 'pts,pcd,ply' && report.points.mergedCount === 12 && report.points.croppedCount === 4 && report.points.polygonCroppedCount === 4 && report.points.reducedCount === 1 && report.points.structured.width === 3 && report.points.structured.height === 2 && close(report.points.centered.x, 0) && close(report.points.centered.y, 0) && close(report.points.centered.z, 0) && Object.values(report.points.exportBytes).every((value) => value > 0) && close(report.reverseEngineering.planeRms, 0) && close(report.reverseEngineering.sphereRadius, 2) && close(report.reverseEngineering.cylinderRadius, 3) && close(report.reverseEngineering.cylinderHeight, 8) && close(report.reverseEngineering.polynomialRms, 0) && report.reverseEngineering.segmentCount === 2 && JSON.stringify(report.reverseEngineering.segmentSizes) === JSON.stringify([2, 2]) && report.persistence.mode === 'sqlite-opfs' && report.persistence.roundTrip && report.persistence.released && report.opfs.markerSuite === 'DATA-ALL' && report.opfs.records === 7 && report.opfs.markerRemoved ? 'pass' : 'failed'
|
|
} catch (error) { report.error = error instanceof Error ? error.stack || error.message : String(error) } finally {
|
|
facade.geometry.dispose(); await new Promise((resolve) => setTimeout(resolve, 0)); const capabilities = facade.geometry.capabilities(); report.afterRelease = { shapeCount: capabilities.shapeCount, kernelReferenceCount: capabilities.kernelReferenceCount }; if (capabilities.shapeCount !== 0 || capabilities.kernelReferenceCount !== 0) { report.status = 'failed'; report.error = `${report.error ? `${report.error} ` : ''}Shape ownership gate failed.` }
|
|
}
|
|
return report
|
|
}
|
|
|
|
run().then((report) => { ;(window as Window & { __bitbybitDataReport?: DataReport }).__bitbybitDataReport = report; document.documentElement.dataset.status = report.status; document.querySelector('#result')!.textContent = JSON.stringify(report, null, 2) }).catch((error) => { document.documentElement.dataset.status = 'failed'; document.querySelector('#result')!.textContent = error instanceof Error ? error.stack || error.message : String(error) })
|
|
export {}
|