Files
Web_FreeCAD_Bitbybit/src/chromeProductionDocumentHarness.ts

141 lines
11 KiB
TypeScript

import { createMockFacade } from './facade/mockFacade'
import { createDraftDocument } from './facade/draft'
import { createPlot } from './facade/plot'
import { createProductionDocument } from './facade/productionDocument'
import { createSpreadsheet } from './facade/spreadsheet'
import { createTechDrawPage } from './facade/techDraw'
import type { TopoRefValue } from './facade/types'
type ProductionReport = {
schemaVersion: 1
status: 'pass' | 'failed'
browserId: 'chrome'
crossOriginIsolated: boolean
closure?: { complete: boolean; artifacts: number; missingKinds: number; missingDependencies: number; cycles: number; documentVersion: number; reopenedComplete: boolean; artifactVersions: number[] }
workflow?: {
created: { spreadsheetCells: number; draftObjects: number; techdrawViews: number; plotSeries: number }
edited: { spreadsheetValue: number; draftStartX: number; techdrawDimension: number; plotValue: number }
exports: { spreadsheetCsvBytes: number; draftJsonBytes: number; techdrawSvgBytes: number; techdrawPdfBytes: number; techdrawPdfValid: boolean; plotSvgBytes: number; plotCsvBytes: number }
}
persistence?: { mode: string; byteLength: number; hash: string; roundTrip: boolean; released: boolean }
opfs?: { markerSuite: string; artifacts: 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 ref = (id: string, version: number): TopoRefValue => ({ schemaVersion: 1, objectId: 'draft-line', kind: 'vertex', persistentId: id, topologyVersion: version, generation: version, status: 'stable', signature: id })
const run = async (): Promise<ProductionReport> => {
const facade = createMockFacade()
const report: ProductionReport = { schemaVersion: 1, status: 'failed', browserId: 'chrome', crossOriginIsolated: globalThis.crossOriginIsolated === true }
try {
if (!report.crossOriginIsolated || !navigator.storage?.getDirectory) throw new Error('Chrome production document evidence requires isolated OPFS.')
await facade.project.list()
const document = createProductionDocument('doc', 'Production workflow')
const sheet = createSpreadsheet('sheet', 'Parameters')
sheet.setCell('A1', 2, { alias: 'Input' })
sheet.setCell('A2', '=Input * 2', { alias: 'Output' })
const initialSheet = sheet.evaluate()
document.add({ id: 'sheet', kind: 'Spreadsheet', dependencies: [], payload: { snapshot: sheet.snapshot(), evaluation: initialSheet } })
const draft = createDraftDocument('draft', 'Profile')
draft.createLine('profile', { x: 0, y: 0 }, { x: 4, y: 0 })
document.add({ id: 'draft', kind: 'Draft', dependencies: [], payload: { snapshot: draft.snapshot() } })
const page = createTechDrawPage('page', 'Profile drawing')
page.addSource({ id: 'draft-line', revision: 1, points: [{ id: 'start', ref: ref('Vertex1', 1), point: { x: 0, y: 0, z: 0 } }, { id: 'end', ref: ref('Vertex2', 1), point: { x: 4, y: 0, z: 0 } }], edges: [{ id: 'edge', start: 'start', end: 'end' }] })
page.addView({ id: 'front', sourceId: 'draft-line', direction: { x: 0, y: 0, z: 1 }, position: { x: 80, y: 80 }, scale: 8 })
page.addDimension({ id: 'length', sourceId: 'draft-line', first: ref('Vertex1', 1), second: ref('Vertex2', 1), label: 'Profile length' })
page.addAnnotation({ id: 'note', text: 'Production profile', position: { x: 20, y: 20 } })
page.addGeometricTolerance({ id: 'straightness', sourceId: 'draft-line', reference: ref('Vertex1', 1), characteristic: 'flatness', value: 0.05 })
document.add({ id: 'page', kind: 'TechDraw', dependencies: ['draft'], payload: { snapshot: page.snapshot() } })
const plot = createPlot('plot', 'Parameter history')
plot.setAxes({ x: { label: 'Revision', scale: 'linear', minimum: 2, maximum: 3 }, y: { label: 'Output', scale: 'linear', minimum: 0, maximum: 8 } })
plot.setSeries({ id: 'output', label: 'Output', points: [{ x: 2, y: 4 }, { x: 3, y: 4 }], style: { color: '#007f86', lineWidth: 2 } })
plot.bindSeries('output', { sourceId: 'sheet', xRange: 'A1:A2', yRange: 'A1:A2' })
document.add({ id: 'plot', kind: 'Plot', dependencies: ['sheet'], payload: { snapshot: plot.snapshot() } })
sheet.setCell('A1', 3, { alias: 'Input' })
const editedSheet = sheet.evaluate()
const spreadsheetValue = editedSheet.cells.A2.value?.value ?? Number.NaN
const movedLine = draft.move('profile', { x: 1, y: 2 })
page.updateSource({ id: 'draft-line', revision: 2, points: [{ id: 'start', ref: ref('Vertex1', 2), point: { x: 1, y: 2, z: 0 } }, { id: 'end', ref: ref('Vertex2', 2), point: { x: spreadsheetValue, y: 2, z: 0 } }], edges: [{ id: 'edge', start: 'start', end: 'end' }] })
plot.updateBoundSeries('output', [{ x: 2, y: 4 }, { x: 3, y: spreadsheetValue }])
const spreadsheetCsv = sheet.exportCsv()
const draftJson = JSON.stringify(draft.snapshot())
const techdrawSvg = page.exportSvg()
const techdrawPdf = page.exportPdf()
const plotSvg = plot.exportSvg()
const plotCsv = plot.exportCsv()
const techdrawPdfText = new TextDecoder().decode(techdrawPdf)
document.update('sheet', { payload: { snapshot: sheet.snapshot(), evaluation: editedSheet, exportBytes: new TextEncoder().encode(spreadsheetCsv).byteLength } })
document.update('draft', { payload: { snapshot: draft.snapshot(), exportBytes: new TextEncoder().encode(draftJson).byteLength } })
document.update('page', { payload: { snapshot: page.snapshot(), svgBytes: new TextEncoder().encode(techdrawSvg).byteLength, pdfBytes: techdrawPdf.byteLength } })
document.update('plot', { payload: { snapshot: plot.snapshot(), svgBytes: new TextEncoder().encode(plotSvg).byteLength, csvBytes: new TextEncoder().encode(plotCsv).byteLength } })
const closure = document.closure()
const serialized = document.save()
const reopened = createProductionDocument('reopened', 'Reopened production workflow')
const reopenedSnapshot = reopened.load(serialized)
const reopenedClosure = reopened.closure()
const payload = new TextEncoder().encode(serialized)
const stored = await facade.project.resource.put(payload, 'application/vnd.bitbybit.production-document+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 = 'production-document-chrome-marker.json'
const marker = await directory.getFileHandle(markerName, { create: true })
const writable = await marker.createWritable()
await writable.write(JSON.stringify({ suite: 'DOC-CLOSURE', artifacts: closure.artifacts }))
await writable.close()
const markerPayload = JSON.parse(await (await marker.getFile()).text()) as { suite: string; artifacts: number }
await directory.removeEntry(markerName)
let markerRemoved = false
try { await directory.getFileHandle(markerName) } catch (error) { markerRemoved = error instanceof DOMException && error.name === 'NotFoundError' }
const pageSnapshot = page.snapshot()
const plotSnapshot = plot.snapshot()
report.closure = { complete: closure.complete, artifacts: closure.artifacts, missingKinds: closure.missingKinds.length, missingDependencies: closure.missingDependencies.length, cycles: closure.cycles.length, documentVersion: document.snapshot().version, reopenedComplete: reopenedClosure.complete && reopenedSnapshot.artifacts.length === 4, artifactVersions: document.snapshot().artifacts.map((artifact) => artifact.version).sort() }
report.workflow = {
created: { spreadsheetCells: 2, draftObjects: 1, techdrawViews: 1, plotSeries: 1 },
edited: { spreadsheetValue, draftStartX: movedLine.kind === 'line' ? movedLine.start.x : Number.NaN, techdrawDimension: pageSnapshot.dimensions[0]?.value ?? Number.NaN, plotValue: plotSnapshot.series[0]?.points.at(-1)?.y ?? Number.NaN },
exports: { spreadsheetCsvBytes: new TextEncoder().encode(spreadsheetCsv).byteLength, draftJsonBytes: new TextEncoder().encode(draftJson).byteLength, techdrawSvgBytes: new TextEncoder().encode(techdrawSvg).byteLength, techdrawPdfBytes: techdrawPdf.byteLength, techdrawPdfValid: techdrawPdfText.startsWith('%PDF-1.4') && techdrawPdfText.endsWith('%%EOF\n'), plotSvgBytes: new TextEncoder().encode(plotSvg).byteLength, plotCsvBytes: new TextEncoder().encode(plotCsv).byteLength },
}
report.persistence = { mode: facade.project.capabilities().mode, byteLength: stored.byteLength, hash: stored.hash, roundTrip, released }
report.opfs = { markerSuite: markerPayload.suite, artifacts: markerPayload.artifacts, markerRemoved }
report.status = report.closure.complete && report.closure.artifacts === 4 && report.closure.missingKinds === 0 && report.closure.missingDependencies === 0 && report.closure.cycles === 0 && report.closure.documentVersion === 8 && report.closure.reopenedComplete && report.closure.artifactVersions.every((version) => version === 2)
&& report.workflow.created.spreadsheetCells === 2 && report.workflow.created.draftObjects === 1 && report.workflow.created.techdrawViews === 1 && report.workflow.created.plotSeries === 1
&& report.workflow.edited.spreadsheetValue === 6 && report.workflow.edited.draftStartX === 1 && report.workflow.edited.techdrawDimension === 5 && report.workflow.edited.plotValue === 6
&& Object.entries(report.workflow.exports).every(([name, value]) => name === 'techdrawPdfValid' ? value === true : typeof value === 'number' && value > 0)
&& report.persistence.mode === 'sqlite-opfs' && report.persistence.roundTrip && report.persistence.released && report.opfs.markerSuite === 'DOC-CLOSURE' && report.opfs.artifacts === 4 && 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 & { __bitbybitProductionReport?: ProductionReport }).__bitbybitProductionReport = 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 {}