141 lines
11 KiB
TypeScript
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 {}
|