import { createMockFacade } from './facade/mockFacade' import { createAssembly } from './facade/assembly' import { inspectFcstdArchive, serializeFcstdMetadataArchive } from './facade/fcstd' import type { AssemblySnapshot } from './facade/assembly' import type { DocumentSnapshot } from './facade/types' type AssemblyReport = { schemaVersion: 1 status: 'pass' | 'failed' browserId: 'chrome' crossOriginIsolated: boolean geometry?: { components: number; validShapes: number; releasedShapes: number } solver?: { status: string; iterations: number; coincident: string; distance: string; shaftX: number; diagnostics: number; workerStatus: string; angleStatus: string; angleYaw: number } tools?: { bomRows: number; collisionCount: number; explodedMoved: boolean; variantX: number; motionFrames: number; motionEndX: number } scale?: { components: number; bomRows: number; durationMs: number; budgetMs: number } fcstd?: { bytes: number; objects: number; links: number; reopenedObjects: number; roundTrip: boolean; released: boolean } persistence?: { mode: string; byteLength: number; hash: string; roundTrip: boolean; released: boolean } opfs?: { markerSuite: string; markerComponents: 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 solveInWorker = async (snapshot: AssemblySnapshot) => { const worker = new Worker(new URL('./assemblySolverWorker.ts', import.meta.url), { type: 'module', name: 'assembly-solver' }) try { return await new Promise((resolve, reject) => { const timer = setTimeout(() => reject(new Error('Assembly solver Worker timed out.')), 10_000) worker.addEventListener('message', (event: MessageEvent<{ id: number; snapshot?: AssemblySnapshot; error?: string }>) => { clearTimeout(timer); event.data.snapshot ? resolve(event.data.snapshot) : reject(new Error(event.data.error ?? 'Assembly solver Worker failed.')) }, { once: true }) worker.addEventListener('error', (event) => { clearTimeout(timer); reject(new Error(event.message)) }, { once: true }) worker.postMessage({ id: 1, snapshot }) }) } finally { worker.terminate() } } const fcstdFixture = (): DocumentSnapshot => ({ id: 'assembly-fcstd', label: 'Assembly FCStd', version: 1, dirty: false, readOnly: false, units: 'mm', tree: [{ id: 'Assembly', label: 'Assembly', type: 'folder', state: 'valid' }, { id: 'Base', label: 'Base', type: 'feature', state: 'valid' }, { id: 'Shaft', label: 'Shaft', type: 'feature', state: 'valid' }, { id: 'ShaftLink', label: 'Shaft Link', type: 'feature', state: 'valid' }], objects: [ { id: 'Assembly', typeId: 'App::Part', properties: [] }, { id: 'Base', typeId: 'Part::Box', properties: [{ name: 'Length', label: 'Length', group: 'Box', scope: 'data', type: 'App::PropertyLength', value: 10 }] }, { id: 'Shaft', typeId: 'Part::Box', properties: [{ name: 'Length', label: 'Length', group: 'Box', scope: 'data', type: 'App::PropertyLength', value: 8 }] }, { id: 'ShaftLink', typeId: 'App::Link', properties: [{ name: 'LinkedObject', label: 'Linked Object', group: 'Link', scope: 'data', type: 'App::PropertyLink', value: 'Shaft' }] }, ], dependencies: [{ sourceId: 'ShaftLink', targetId: 'Shaft', relation: 'link', propertyName: 'LinkedObject' }], recompute: { generation: 0, status: 'idle', objectStates: { Assembly: 'up-to-date', Base: 'up-to-date', Shaft: 'up-to-date', ShaftLink: 'up-to-date' }, dirtyObjects: [], order: ['Assembly', 'Base', 'Shaft', 'ShaftLink'], errors: [] }, }) const run = async (): Promise => { const facade = createMockFacade() const report: AssemblyReport = { schemaVersion: 1, status: 'failed', browserId: 'chrome', crossOriginIsolated: globalThis.crossOriginIsolated === true } try { if (!report.crossOriginIsolated || !navigator.storage?.getDirectory) throw new Error('Chrome Assembly evidence requires isolated OPFS.') await facade.project.list() const context = { documentId: 'assembly', documentVersion: 1 } const baseShape = await facade.geometry.createBox({ ...context, width: 10, length: 10, height: 2 }) const shaftShape = await facade.geometry.createBox({ ...context, width: 2, length: 2, height: 8 }) const assembly = createAssembly('motor', 'Motor assembly') assembly.addComponent({ id: 'base', sourceObjectId: baseShape.id, label: 'Base', grounded: true, bounds: { min: { x: -1, y: -1, z: -1 }, max: { x: 1, y: 1, z: 1 } } }) assembly.addComponent({ id: 'shaft', sourceObjectId: shaftShape.id, label: 'Shaft', grounded: false, placement: { x: 1 }, bounds: { min: { x: -1, y: -1, z: -1 }, max: { x: 1, y: 1, z: 1 } } }) assembly.addConnector({ id: 'base-origin', componentId: 'base', origin: { x: 0, y: 0, z: 0 }, axis: { x: 1, y: 0, z: 0 } }) assembly.addConnector({ id: 'shaft-origin', componentId: 'shaft', origin: { x: 0, y: 0, z: 0 }, axis: { x: 1, y: 0, z: 0 } }) assembly.addJoint({ id: 'coincident', kind: 'coincident', first: 'base-origin', second: 'shaft-origin' }) assembly.addJoint({ id: 'distance', kind: 'distance', first: 'base-origin', second: 'shaft-origin', value: 5 }) const initialCollisionCount = assembly.collisions().length const workerSolved = await solveInWorker(assembly.snapshot()) const solved = assembly.solve() const exploded = assembly.exploded(2) const variant = assembly.variant('inspection', { shaft: { x: 7 } }) const motion = assembly.motion('shaft', { x: 9, yaw: Math.PI / 2 }, 4) const angled = createAssembly('angle', 'Angle fixture') angled.addComponent({ id: 'fixed', sourceObjectId: baseShape.id, label: 'Fixed', grounded: true }) angled.addComponent({ id: 'arm', sourceObjectId: shaftShape.id, label: 'Arm', grounded: false }) angled.addConnector({ id: 'fixed-axis', componentId: 'fixed', origin: { x: 0, y: 0, z: 0 }, axis: { x: 1, y: 0, z: 0 } }) angled.addConnector({ id: 'arm-axis', componentId: 'arm', origin: { x: 0, y: 0, z: 0 }, axis: { x: 1, y: 0, z: 0 } }) angled.addJoint({ id: 'angle', kind: 'angle', first: 'fixed-axis', second: 'arm-axis', value: Math.PI / 2 }) const angleSolved = angled.solve() const scaleStarted = performance.now() const large = createAssembly('large', 'Large assembly') for (let index = 0; index < 1000; index += 1) large.addComponent({ id: `component-${index}`, sourceObjectId: `part-${index % 10}`, label: `Part ${index % 10}`, grounded: index === 0, placement: { x: index * 2 } }) const largeBom = large.bom() const scaleDurationMs = performance.now() - scaleStarted const fcstdBytes = serializeFcstdMetadataArchive(fcstdFixture(), { guiViews: [{ name: 'Assembly', type: 'orthographic', visibility: 'true' }] }) const inspected = inspectFcstdArchive(fcstdBytes) const reopenedFcstd = serializeFcstdMetadataArchive(inspected.proxyDocument, { guiViews: [{ name: 'Assembly', type: 'orthographic', visibility: 'true' }] }) const reopenedInspection = inspectFcstdArchive(reopenedFcstd) const fcstdStored = await facade.project.resource.put(fcstdBytes, 'application/vnd.freecad.fcstd') const fcstdLoaded = await facade.project.resource.get(fcstdStored.hash) const fcstdRoundTrip = fcstdLoaded !== null && await sha256(fcstdLoaded) === await sha256(fcstdBytes) await facade.project.resource.release(fcstdStored.hash) const fcstdReleased = await facade.project.resource.get(fcstdStored.hash) === null const payload = new TextEncoder().encode(JSON.stringify(solved)) const stored = await facade.project.resource.put(payload, 'application/vnd.bitbybit.assembly+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 = 'assembly-chrome-marker.json' const marker = await directory.getFileHandle(markerName, { create: true }) const writable = await marker.createWritable() await writable.write(JSON.stringify({ suite: 'ASM-TOOLS', components: solved.components.length })) await writable.close() const markerPayload = JSON.parse(await (await marker.getFile()).text()) as { suite: string; components: number } await directory.removeEntry(markerName) let markerRemoved = false try { await directory.getFileHandle(markerName) } catch (error) { markerRemoved = error instanceof DOMException && error.name === 'NotFoundError' } report.geometry = { components: solved.components.length, validShapes: [baseShape, shaftShape].filter((shape) => shape.id.length > 0).length, releasedShapes: 0 } report.solver = { status: solved.solver.status, iterations: solved.solver.iterations, coincident: solved.joints.find((joint) => joint.id === 'coincident')?.status || 'missing', distance: solved.joints.find((joint) => joint.id === 'distance')?.status || 'missing', shaftX: solved.components.find((component) => component.id === 'shaft')?.placement.x || 0, diagnostics: solved.diagnostics.length, workerStatus: workerSolved.solver.status, angleStatus: angleSolved.solver.status, angleYaw: angleSolved.components.find((component) => component.id === 'arm')?.placement.yaw ?? Number.NaN } report.tools = { bomRows: assembly.bom().length, collisionCount: initialCollisionCount, explodedMoved: exploded.some((component) => component.placement.x !== solved.components.find((entry) => entry.id === component.id)?.placement.x), variantX: variant.placements.find((component) => component.id === 'shaft')?.placement.x || 0, motionFrames: motion.length, motionEndX: motion.at(-1)?.placement.x ?? Number.NaN } report.scale = { components: large.snapshot().components.length, bomRows: largeBom.length, durationMs: scaleDurationMs, budgetMs: 1000 } const linkProperties = inspected.objects.flatMap((object) => object.properties.filter((property) => property.typeId === 'App::PropertyLink' && property.value === 'Shaft')) report.fcstd = { bytes: fcstdBytes.byteLength, objects: inspected.objects.length, links: (inspected.proxyDocument?.dependencies ?? []).filter((dependency) => dependency.relation === 'link').length + linkProperties.length, reopenedObjects: reopenedInspection.objects.length, roundTrip: fcstdRoundTrip, released: fcstdReleased } report.persistence = { mode: facade.project.capabilities().mode, byteLength: stored.byteLength, hash: stored.hash, roundTrip, released } report.opfs = { markerSuite: markerPayload.suite, markerComponents: markerPayload.components, markerRemoved } report.status = report.crossOriginIsolated && report.geometry.validShapes === 2 && report.solver.status === 'solved' && report.solver.iterations === 2 && report.solver.coincident === 'solved' && report.solver.distance === 'solved' && report.solver.shaftX === 5 && report.solver.diagnostics === 0 && report.solver.workerStatus === 'solved' && report.solver.angleStatus === 'solved' && Math.abs(report.solver.angleYaw - Math.PI / 2) < 1e-12 && report.tools.bomRows === 2 && report.tools.collisionCount === 1 && report.tools.explodedMoved && report.tools.variantX === 7 && report.tools.motionFrames === 5 && report.tools.motionEndX === 9 && report.scale.components === 1000 && report.scale.bomRows === 10 && report.scale.durationMs <= report.scale.budgetMs && report.fcstd.bytes > 0 && report.fcstd.objects === 4 && report.fcstd.links === 1 && report.fcstd.reopenedObjects === 4 && report.fcstd.roundTrip && report.fcstd.released && report.persistence.mode === 'sqlite-opfs' && report.persistence.byteLength > 0 && report.persistence.roundTrip && report.persistence.released && report.opfs.markerSuite === 'ASM-TOOLS' && report.opfs.markerComponents === 2 && 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 & { __bitbybitAssemblyReport?: AssemblyReport }).__bitbybitAssemblyReport = 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 {}