feat: establish reproducible FreeCAD web compatibility baseline
This commit is contained in:
128
public/chrome-partdesign-lifecycle-harness.html
Normal file
128
public/chrome-partdesign-lifecycle-harness.html
Normal file
@@ -0,0 +1,128 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>Chrome PartDesign lifecycle harness</title>
|
||||
<pre id="result">running</pre>
|
||||
<script type="module">
|
||||
import { createMockFacade } from '/src/facade/mockFacade.ts'
|
||||
|
||||
const output = document.querySelector('#result')
|
||||
const report = {
|
||||
schemaVersion: 1,
|
||||
browserId: 'chrome',
|
||||
crossOriginIsolated: globalThis.crossOriginIsolated === true,
|
||||
status: 'running',
|
||||
}
|
||||
const facade = createMockFacade()
|
||||
const property = (objectId, name) => facade.app.document.getObject(objectId)?.properties.find((entry) => entry.name === name)?.value ?? null
|
||||
const shapeEvidence = async (objectId) => {
|
||||
const shape = facade.geometry.getObjectShape(objectId)
|
||||
if (!shape) throw new Error(`${objectId} did not expose a ShapeHandle.`)
|
||||
const [mass, quality] = await Promise.all([facade.geometry.massProperties(shape), facade.geometry.qualityReport(shape)])
|
||||
if (!(mass.volume > 0) || quality.solids !== 1) throw new Error(`${objectId} did not expose one positive-volume solid.`)
|
||||
return { shapeId: shape.id, volume: mass.volume, solids: quality.solids, structuralValid: quality.structuralValid }
|
||||
}
|
||||
const rectangle = [
|
||||
{ id: 'lifecycle-edge-1', type: 'line', start: { x: 0, y: 0 }, end: { x: 4, y: 0 } },
|
||||
{ id: 'lifecycle-edge-2', type: 'line', start: { x: 4, y: 0 }, end: { x: 4, y: 3 } },
|
||||
{ id: 'lifecycle-edge-3', type: 'line', start: { x: 4, y: 3 }, end: { x: 0, y: 3 } },
|
||||
{ id: 'lifecycle-edge-4', type: 'line', start: { x: 0, y: 3 }, end: { x: 0, y: 0 } },
|
||||
]
|
||||
|
||||
try {
|
||||
if (!report.crossOriginIsolated || !navigator.storage?.getDirectory) throw new Error('Chrome PartDesign lifecycle requires isolated OPFS.')
|
||||
const initialized = await facade.geometry.initialize()
|
||||
if (initialized.status !== 'ready') throw new Error(initialized.reason || 'Bitbybit geometry runtime is unavailable.')
|
||||
|
||||
for (const edge of rectangle) facade.app.sketcher.addGeometry('sketch', edge)
|
||||
if (facade.gui.command.getState('pad').status !== 'enabled') throw new Error('Closed Sketch did not enable Pad.')
|
||||
|
||||
const initialOrder = facade.app.document.getActive().tree.find((item) => item.id === 'body')?.children ?? []
|
||||
let reorderRejected = false
|
||||
try { facade.app.document.reorderBodyFeature({ bodyId: 'body', objectId: 'pocket', beforeObjectId: 'pad' }) } catch (error) { reorderRejected = /dependency/.test(String(error)) }
|
||||
let dependencyRemovalRejected = false
|
||||
try { facade.app.document.removeObject({ objectId: 'pad' }) } catch (error) { dependencyRemovalRejected = /dependent/.test(String(error)) }
|
||||
const removed = facade.app.document.removeObject({ objectId: 'fillet' })
|
||||
const deleted = { removed, tip: property('body', 'Tip'), filletExists: facade.app.document.getObject('fillet') !== null }
|
||||
facade.history.undo()
|
||||
const deletionUndone = { tip: property('body', 'Tip'), filletExists: facade.app.document.getObject('fillet') !== null }
|
||||
facade.history.redo()
|
||||
const deletionRedone = { tip: property('body', 'Tip'), filletExists: facade.app.document.getObject('fillet') !== null }
|
||||
facade.history.undo()
|
||||
const restoredOrder = facade.app.document.getActive().tree.find((item) => item.id === 'body')?.children ?? []
|
||||
const structural = { initialOrder, restoredOrder, reorderRejected, dependencyRemovalRejected, deleted, deletionUndone, deletionRedone, restoredTip: property('body', 'Tip') }
|
||||
if (!reorderRejected || !dependencyRemovalRejected || deleted.tip !== 'pocket' || deleted.filletExists || !deletionUndone.filletExists || deletionUndone.tip !== 'fillet' || deletionRedone.filletExists || deletionRedone.tip !== 'pocket' || structural.restoredTip !== 'fillet') throw new Error('PartDesign structural lifecycle is inconsistent.')
|
||||
|
||||
facade.app.document.setProperty({ objectId: 'pocket', propertyName: 'Suppressed', value: true })
|
||||
facade.app.document.setProperty({ objectId: 'fillet', propertyName: 'Suppressed', value: true })
|
||||
const suppression = await facade.app.document.recomputeAsync()
|
||||
if (suppression.status !== 'completed') throw new Error(`Suppression recompute failed: ${suppression.status}.`)
|
||||
|
||||
const initialShape = await shapeEvidence('pad')
|
||||
const initial = {
|
||||
length: property('pad', 'Length'),
|
||||
tip: property('body', 'Tip'),
|
||||
support: property('sketch', 'Support'),
|
||||
pocketSuppressed: property('pocket', 'Suppressed'),
|
||||
filletSuppressed: property('fillet', 'Suppressed'),
|
||||
...initialShape,
|
||||
}
|
||||
facade.app.document.setProperty({ objectId: 'pad', propertyName: 'Length', value: 31 })
|
||||
const editedRecompute = await facade.app.document.recomputeAsync()
|
||||
const editedShape = await shapeEvidence('pad')
|
||||
const edited = {
|
||||
length: property('pad', 'Length'),
|
||||
tip: property('body', 'Tip'),
|
||||
support: property('sketch', 'Support'),
|
||||
recompute: editedRecompute.status,
|
||||
shapeCount: facade.geometry.capabilities().shapeCount,
|
||||
kernelReferenceCount: facade.geometry.capabilities().kernelReferenceCount,
|
||||
...editedShape,
|
||||
}
|
||||
|
||||
facade.history.undo()
|
||||
const undoRecompute = await facade.app.document.recomputeAsync({ dirtyObjectIds: ['pad'] })
|
||||
const undoneShape = await shapeEvidence('pad')
|
||||
const undone = {
|
||||
length: property('pad', 'Length'),
|
||||
tip: property('body', 'Tip'),
|
||||
support: property('sketch', 'Support'),
|
||||
recompute: undoRecompute.status,
|
||||
shapeCount: facade.geometry.capabilities().shapeCount,
|
||||
kernelReferenceCount: facade.geometry.capabilities().kernelReferenceCount,
|
||||
...undoneShape,
|
||||
}
|
||||
|
||||
const saved = await facade.project.save()
|
||||
const persisted = await facade.project.load('doc-pump-housing')
|
||||
await facade.app.document.load('doc-pump-housing')
|
||||
const reopenedRecompute = await facade.app.document.recomputeAsync({ dirtyObjectIds: ['pad'] })
|
||||
const reopenedShape = await shapeEvidence('pad')
|
||||
const reopened = {
|
||||
length: property('pad', 'Length'),
|
||||
tip: property('body', 'Tip'),
|
||||
support: property('sketch', 'Support'),
|
||||
pocketSuppressed: property('pocket', 'Suppressed'),
|
||||
filletSuppressed: property('fillet', 'Suppressed'),
|
||||
persistedLength: persisted?.objects.find((object) => object.id === 'pad')?.properties.find((entry) => entry.name === 'Length')?.value ?? null,
|
||||
savedVersion: saved.documentVersion,
|
||||
activeVersion: facade.app.document.getActive().version,
|
||||
recompute: reopenedRecompute.status,
|
||||
persistenceMode: facade.project.capabilities().mode,
|
||||
...reopenedShape,
|
||||
}
|
||||
|
||||
facade.geometry.dispose()
|
||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||
const released = {
|
||||
shapeCount: facade.geometry.capabilities().shapeCount,
|
||||
kernelReferenceCount: facade.geometry.capabilities().kernelReferenceCount,
|
||||
}
|
||||
Object.assign(report, { status: 'pass', structural, initial, edited, undone, reopened, released })
|
||||
} catch (error) {
|
||||
facade.geometry.dispose()
|
||||
Object.assign(report, { status: 'failed', error: error instanceof Error ? error.stack || error.message : String(error) })
|
||||
}
|
||||
|
||||
output.textContent = JSON.stringify(report, null, 2)
|
||||
await fetch('/__partdesign-lifecycle-report', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(report) })
|
||||
</script>
|
||||
Reference in New Issue
Block a user