import { createMockFacade } from './facade/mockFacade' import { createCamJob, type CamDressupKind, type CamOperationKind } from './facade/cam' import { menuDefinitions, workbenchDefinitions } from './freecadManifest' type CamReport = { schemaVersion: 1 status: 'pass' | 'failed' browserId: 'chrome' crossOriginIsolated: boolean geometry?: { sourceShape: string; valid: boolean; volume: number } job?: { stockMode: string tools: number controllers: number operations: number operationKinds: string[] operationStatuses: string[] pathPoints: number dressups: number setup: { safeHeightOffset: number; clearanceHeightOffset: number; coolantMode: string } sanity: string sanityIssues: number simulation: string collisions: number collisionFixture: number simulationDiagnostics: number postprocessors: string[] post: string postHashes: Record unsafePostRejected: boolean gcodeBytes: number gcodeHash: string deterministic: boolean } extended?: { commandSurface: number; operationKinds: number; dressupKinds: number; toolAssetRoundTrip: boolean; jobAssetRoundTrip: boolean; metadataRoundTrip: boolean; metadataComments: number; metadataProperties: number; metadataCompounds: number; undoRedo: boolean; startPointChanged: boolean; materialRemoval: boolean; removalTimelinePoints: number; fixtureCollision: boolean; multiAxis: boolean } persistence?: { mode: string; byteLength: number; hash: string; roundTrip: boolean; released: boolean } opfs?: { markerSuite: string; operations: 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 run = async (): Promise => { const facade = createMockFacade() const report: CamReport = { schemaVersion: 1, status: 'failed', browserId: 'chrome', crossOriginIsolated: globalThis.crossOriginIsolated === true } try { if (!report.crossOriginIsolated || !navigator.storage?.getDirectory) throw new Error('Chrome CAM evidence requires isolated OPFS.') await facade.project.list() const shape = await facade.geometry.createBox({ documentId: 'cam', documentVersion: 1, width: 5, length: 2, height: 3 }) const quality = await facade.geometry.qualityReport(shape) const mass = await facade.geometry.massProperties(shape) const cam = createCamJob('job', 'Box machining job', { min: [0, 0, 0], max: [5, 2, 3], mode: 'from-base-bound-box' }) cam.updateSetupSheet({ safeHeightOffset: 1, clearanceHeightOffset: 2, coolantMode: 'flood' }) cam.addTool({ id: 'T1', name: '1mm end mill', shape: 'endmill', diameter: 1, length: 20, cuttingEdgeHeight: 8, shankDiameter: 1 }) cam.addToolController({ id: 'TC1', label: 'End Mill Controller', toolId: 'T1', toolNumber: 1, spindleSpeed: 16000, spindleDirection: 'forward', horizontalFeed: 240, verticalFeed: 80, horizontalRapid: 1800, verticalRapid: 900 }) cam.addTool({ id: 'T2', name: '0.5mm drill', shape: 'drill', diameter: 0.5, length: 15, cuttingEdgeHeight: 6, shankDiameter: 0.5 }) cam.addToolController({ id: 'TC2', label: 'Drill Controller', toolId: 'T2', toolNumber: 2, spindleSpeed: 12000, spindleDirection: 'forward', horizontalFeed: 120, verticalFeed: 60, horizontalRapid: 1800, verticalRapid: 900 }) cam.addOperation({ id: 'profile', label: 'Outside Profile', kind: 'profile', toolId: 'T1', controllerId: 'TC1', depth: 1, feed: 240, verticalFeed: 80, stepDown: 1, stepOver: 50, coolantMode: 'flood' }) cam.generatePath('profile') cam.applyDressup('profile', { kind: 'lead-in-out', parameters: { length: 0.5 } }) cam.generatePath('profile') cam.addOperation({ id: 'pocket', label: 'Top Pocket', kind: 'pocket', toolId: 'T1', controllerId: 'TC1', depth: 1, feed: 220, verticalFeed: 80, stepDown: 1, stepOver: 50, direction: 'climb', coolantMode: 'flood' }) cam.generatePath('pocket') cam.addOperation({ id: 'drilling', label: 'Mounting Holes', kind: 'drilling', toolId: 'T2', controllerId: 'TC2', depth: 1.5, feed: 60, verticalFeed: 60, stepDown: 1.5, locations: [[1, 1, 3], [4, 1, 3]], coolantMode: 'mist' }) cam.generatePath('drilling') cam.addComment('Chrome CAM metadata evidence') cam.setPropertyBagValue('Material', 'Al6061') cam.setPropertyBagValue('BatchSize', 1) cam.setPropertyBagValue('DryRun', false) cam.createCompound(['profile', 'pocket'], 'roughing', 'Roughing Operations') const sanity = cam.sanityCheck() const simulation = cam.simulate() const postHashes: Record = {} for (const postprocessor of cam.postprocessors()) postHashes[postprocessor] = await sha256(new TextEncoder().encode(cam.exportGcode(postprocessor))) const gcodeText = cam.exportGcode('linuxcnc') const secondGcode = cam.exportGcode('linuxcnc') const gcode = new TextEncoder().encode(gcodeText) const collision = createCamJob('collision', 'Collision') collision.addTool({ id: 'T1', name: 'Tool', diameter: 2, length: 10 }) collision.addOperation({ id: 'draft', kind: 'contour', toolId: 'T1', depth: 1, feed: 10 }) const collisionFixture = collision.simulate().collisions.length let unsafePostRejected = false try { cam.exportGcode('shell' as never) } catch { unsafePostRejected = true } const assetText = cam.exportToolBit('T1') const assetCam = createCamJob('asset') const importedTool = assetCam.importToolBit(assetText, 'T99') const toolAssetRoundTrip = importedTool.id === 'T99' && importedTool.name === '1mm end mill' && assetText === cam.exportToolBit('T1') const extendedCam = createCamJob('extended', 'Extended command surface', { min: [0, 0, 0], max: [10, 10, 5] }) extendedCam.addTool({ id: 'T1', name: '2mm end mill', diameter: 2, length: 25 }) const operationKinds: CamOperationKind[] = ['profile', 'pocket', 'contour', 'mill-face', 'helix', 'adaptive', 'slot', 'drilling', 'tapping', 'engrave', 'deburr', 'v-carve', 'pocket-3d', 'surface', 'waterline', 'thread-milling', 'probe', 'area', 'area-workplane', 'custom', 'shape', 'path-shape-tool-controller'] for (const [index, kind] of operationKinds.entries()) { extendedCam.addOperation({ id: `extended-${index}`, kind, toolId: 'T1', depth: 1, feed: 100 }) extendedCam.generatePath(`extended-${index}`) } const beforeStart = extendedCam.snapshot().operations[0].path[0] const afterStart = extendedCam.setStartPoint('extended-0', 1).path[0] const startPointChanged = beforeStart.some((value, index) => value !== afterStart[index]) const dressupKinds: CamDressupKind[] = ['array', 'axis-map', 'boundary', 'dogbone', 'drag-knife', 'lead-in-out', 'ramp-entry', 'holding-tags', 'z-correct'] for (const [index, kind] of dressupKinds.entries()) { const operationId = `extended-${index + 1}` extendedCam.applyDressup(operationId, { kind, parameters: kind === 'array' ? { count: 2, offsetX: 0, offsetY: 0 } : kind === 'dogbone' ? { radius: 0.5 } : kind === 'drag-knife' ? { offset: 0.5 } : {} }) extendedCam.generatePath(operationId) } const fixtureCam = createCamJob('fixture', 'Fixture collision', { min: [0, 0, 0], max: [10, 10, 5] }) fixtureCam.addTool({ id: 'T1', name: 'Fixture probe tool', diameter: 2, length: 20, cuttingEdgeHeight: 1, shankDiameter: 2, holderDiameter: 4, holderLength: 20 }) fixtureCam.addOperation({ id: 'profile', kind: 'profile', toolId: 'T1', depth: 1, feed: 100 }) fixtureCam.generatePath('profile') fixtureCam.updateCollisionFixtures([{ id: 'clamp', min: [-1, -1, 5], max: [1, 1, 7] }]) const fixtureSimulation = fixtureCam.simulate() const jobAsset = cam.exportJob() const reopenedCam = createCamJob('job', 'Box machining job') reopenedCam.importJob(jobAsset) const jobAssetRoundTrip = reopenedCam.exportJob() === jobAsset const metadataRoundTrip = reopenedCam.snapshot().metadata.comments.length === 1 && reopenedCam.snapshot().metadata.propertyBag.Material === 'Al6061' && reopenedCam.snapshot().metadata.propertyBag.BatchSize === 1 && reopenedCam.snapshot().metadata.propertyBag.DryRun === false && reopenedCam.snapshot().metadata.compounds[0]?.id === 'roughing' const metadataBeforeRemoval = reopenedCam.snapshot().metadata reopenedCam.removeOperation('pocket') const metadataDependencyCleanup = reopenedCam.snapshot().metadata.compounds.length === 0 reopenedCam.undo() const metadataUndoRestored = reopenedCam.snapshot().metadata.compounds.length === metadataBeforeRemoval.compounds.length reopenedCam.toggleOperation('profile', false) const undoRedo = metadataDependencyCleanup && metadataUndoRestored && reopenedCam.canUndo() && reopenedCam.undo().operations.find((operation) => operation.id === 'profile')?.active === true && reopenedCam.canRedo() && reopenedCam.redo().operations.find((operation) => operation.id === 'profile')?.active === false const multiAxisResult = cam.kinematics('profile', { configuration: '5-axis', rotary: { axis: 'C', startDegrees: -30, endDegrees: 30 }, tilt: { axis: 'B', startDegrees: 0, endDegrees: 45 } }) const multiAxisGcode = cam.exportMultiAxisGcode('profile', 'linuxcnc', { configuration: '5-axis', rotary: { axis: 'C', startDegrees: -30, endDegrees: 30 }, tilt: { axis: 'B', startDegrees: 0, endDegrees: 45 } }) const multiAxis = multiAxisResult.status === 'pass' && multiAxisResult.poses.length === cam.snapshot().operations.find((operation) => operation.id === 'profile')?.path.length && multiAxisResult.poses.at(-1)?.c === 30 && multiAxisResult.poses.at(-1)?.b === 45 && multiAxisGcode.includes('B45.000 C30.000') && multiAxisGcode.includes('M428') && multiAxisGcode.includes('G93') && multiAxisGcode.includes('M429') report.extended = { commandSurface: workbenchDefinitions.CAM.groups.flatMap((group) => group.commands).length, operationKinds: new Set(extendedCam.snapshot().operations.map((operation) => operation.kind)).size, dressupKinds: new Set(extendedCam.snapshot().operations.flatMap((operation) => operation.dressups.map((dressup) => dressup.kind))).size, toolAssetRoundTrip, jobAssetRoundTrip, metadataRoundTrip: metadataRoundTrip && metadataDependencyCleanup && metadataUndoRestored, metadataComments: cam.snapshot().metadata.comments.length, metadataProperties: Object.keys(cam.snapshot().metadata.propertyBag).length, metadataCompounds: cam.snapshot().metadata.compounds.length, undoRedo, startPointChanged, materialRemoval: simulation.materialRemoval.enabled && simulation.materialRemoval.removedVolume > 0 && simulation.materialRemoval.remainingVolume < simulation.materialRemoval.stockVolume, removalTimelinePoints: simulation.materialRemoval.timeline.length, fixtureCollision: fixtureSimulation.status === 'collision' && fixtureSimulation.diagnostics.some((diagnostic) => diagnostic.type === 'fixture'), multiAxis } if (menuDefinitions.CAM.length !== report.extended.commandSurface) throw new Error('CAM menu and toolbar command surfaces diverged.') const snapshot = cam.snapshot() const payload = new TextEncoder().encode(JSON.stringify(snapshot)) const stored = await facade.project.resource.put(payload, 'application/vnd.bitbybit.cam-job+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 = 'cam-chrome-marker.json' const marker = await directory.getFileHandle(markerName, { create: true }) const writable = await marker.createWritable() await writable.write(JSON.stringify({ suite: 'CAM-ALL', operations: snapshot.operations.length })) await writable.close() const markerPayload = JSON.parse(await (await marker.getFile()).text()) as { suite: string; operations: number } await directory.removeEntry(markerName) let markerRemoved = false try { await directory.getFileHandle(markerName) } catch (error) { markerRemoved = error instanceof DOMException && error.name === 'NotFoundError' } report.geometry = { sourceShape: shape.id, valid: quality.structuralValid, volume: mass.volume } report.job = { stockMode: snapshot.stock.mode || '', tools: snapshot.tools.length, controllers: snapshot.toolControllers.length, operations: snapshot.operations.length, operationKinds: snapshot.operations.map((operation) => operation.kind), operationStatuses: snapshot.operations.map((operation) => operation.status), pathPoints: snapshot.operations.reduce((total, operation) => total + operation.path.length, 0), dressups: snapshot.operations.reduce((total, operation) => total + operation.dressups.length, 0), setup: { safeHeightOffset: snapshot.setupSheet.safeHeightOffset, clearanceHeightOffset: snapshot.setupSheet.clearanceHeightOffset, coolantMode: snapshot.setupSheet.coolantMode }, sanity: sanity.status, sanityIssues: sanity.issues.length, simulation: simulation.status, collisions: simulation.collisions.length, collisionFixture, simulationDiagnostics: simulation.diagnostics.length, postprocessors: cam.postprocessors(), post: 'linuxcnc', postHashes, unsafePostRejected, gcodeBytes: gcode.byteLength, gcodeHash: await sha256(gcode), deterministic: gcodeText === secondGcode, } report.persistence = { mode: facade.project.capabilities().mode, byteLength: stored.byteLength, hash: stored.hash, roundTrip, released } report.opfs = { markerSuite: markerPayload.suite, operations: markerPayload.operations, markerRemoved } const expectedKinds = JSON.stringify(['profile', 'pocket', 'drilling']) report.status = report.geometry.valid && report.job.stockMode === 'from-base-bound-box' && report.job.tools === 2 && report.job.controllers === 2 && report.job.operations === 3 && JSON.stringify(report.job.operationKinds) === expectedKinds && report.job.operationStatuses.every((status) => status === 'generated') && report.job.pathPoints >= 20 && report.job.dressups === 1 && report.job.setup.safeHeightOffset === 1 && report.job.setup.clearanceHeightOffset === 2 && report.job.setup.coolantMode === 'flood' && report.job.sanity === 'pass' && report.job.sanityIssues === 0 && report.job.simulation === 'pass' && report.job.collisions === 0 && report.job.collisionFixture === 1 && report.job.simulationDiagnostics === 0 && report.job.postprocessors.length === 7 && Object.keys(report.job.postHashes).length === 7 && report.job.post === 'linuxcnc' && report.job.unsafePostRejected && report.job.gcodeBytes > 0 && report.job.deterministic && report.extended.commandSurface === 60 && report.extended.operationKinds === 22 && report.extended.dressupKinds === 9 && report.extended.toolAssetRoundTrip && report.extended.jobAssetRoundTrip && report.extended.metadataRoundTrip && report.extended.metadataComments === 1 && report.extended.metadataProperties === 3 && report.extended.metadataCompounds === 1 && report.extended.undoRedo && report.extended.startPointChanged && report.extended.materialRemoval && report.extended.removalTimelinePoints === report.job.pathPoints && report.extended.fixtureCollision && report.extended.multiAxis && report.persistence.mode === 'sqlite-opfs' && report.persistence.roundTrip && report.persistence.released && report.opfs.markerSuite === 'CAM-ALL' && report.opfs.operations === 3 && 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' } return report } run().then((report) => { ;(window as Window & { __bitbybitCamReport?: CamReport }).__bitbybitCamReport = 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 = String(error) }) export {}