feat: establish reproducible FreeCAD web compatibility baseline
This commit is contained in:
6
public/_headers
Normal file
6
public/_headers
Normal file
@@ -0,0 +1,6 @@
|
||||
/*
|
||||
Cross-Origin-Opener-Policy: same-origin
|
||||
Cross-Origin-Embedder-Policy: require-corp
|
||||
Cross-Origin-Resource-Policy: same-origin
|
||||
Content-Security-Policy: default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; worker-src 'self' blob:; connect-src 'self'; img-src 'self' data: blob:; font-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'
|
||||
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
|
||||
1
public/chrome-addon-harness.html
Normal file
1
public/chrome-addon-harness.html
Normal file
@@ -0,0 +1 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Chrome Addon Harness</title></head><body><pre id="result">running</pre><script type="module" src="/src/chromeAddonHarness.ts"></script></body></html>
|
||||
5
public/chrome-assembly-harness.html
Normal file
5
public/chrome-assembly-harness.html
Normal file
@@ -0,0 +1,5 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>Chrome Assembly harness</title>
|
||||
<pre id="result">running</pre>
|
||||
<script type="module" src="/src/chromeAssemblyHarness.ts"></script>
|
||||
5
public/chrome-bim-harness.html
Normal file
5
public/chrome-bim-harness.html
Normal file
@@ -0,0 +1,5 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>Chrome BIM harness</title>
|
||||
<pre id="result">running</pre>
|
||||
<script type="module" src="/src/chromeBimHarness.ts"></script>
|
||||
5
public/chrome-cam-harness.html
Normal file
5
public/chrome-cam-harness.html
Normal file
@@ -0,0 +1,5 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>Chrome CAM harness</title>
|
||||
<pre id="result">running</pre>
|
||||
<script type="module" src="/src/chromeCamHarness.ts"></script>
|
||||
5
public/chrome-cam-native-simulation-harness.html
Normal file
5
public/chrome-cam-native-simulation-harness.html
Normal file
@@ -0,0 +1,5 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>Chrome native CAM simulation harness</title>
|
||||
<pre id="result">running</pre>
|
||||
<script type="module" src="/src/chromeCamNativeSimulationHarness.ts"></script>
|
||||
5
public/chrome-data-harness.html
Normal file
5
public/chrome-data-harness.html
Normal file
@@ -0,0 +1,5 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>Chrome Data modules harness</title>
|
||||
<pre id="result">running</pre>
|
||||
<script type="module" src="/src/chromeDataHarness.ts"></script>
|
||||
5
public/chrome-draft-harness.html
Normal file
5
public/chrome-draft-harness.html
Normal file
@@ -0,0 +1,5 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>Chrome Draft harness</title>
|
||||
<pre id="result">running</pre>
|
||||
<script type="module" src="/src/chromeDraftHarness.ts"></script>
|
||||
1
public/chrome-engineering-harness.html
Normal file
1
public/chrome-engineering-harness.html
Normal file
@@ -0,0 +1 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Chrome Engineering Harness</title></head><body><pre id="result">running</pre><script type="module" src="/src/chromeEngineeringHarness.ts"></script></body></html>
|
||||
158
public/chrome-fault-injection-harness.html
Normal file
158
public/chrome-fault-injection-harness.html
Normal file
@@ -0,0 +1,158 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Chrome fault injection harness</title></head>
|
||||
<body>
|
||||
<pre id="status">loading</pre>
|
||||
<script type="module">
|
||||
import { createSketch } from '/src/facade/sketcher.ts'
|
||||
import { PlanegcsWorkerProvider } from '/src/facade/planegcsWorkerClient.ts'
|
||||
import { NativeOcctHistoryWorkerProvider } from '/src/facade/nativeHistoryWorkerClient.ts'
|
||||
import { SketchSolverCoordinator } from '/src/facade/sketchSolverProtocol.ts'
|
||||
|
||||
const browserId = new URL(location.href).searchParams.get('browser') || 'chrome'
|
||||
const report = {
|
||||
schemaVersion: 1,
|
||||
browserId,
|
||||
executionContext: 'dedicated-worker',
|
||||
crossOriginIsolated,
|
||||
sharedArrayBuffer: typeof SharedArrayBuffer === 'function',
|
||||
status: 'failed',
|
||||
}
|
||||
|
||||
const wrappers = []
|
||||
let activeWrapper = null
|
||||
const workerFactory = () => {
|
||||
const real = new Worker(new URL('/src/facade/planegcsWorkerEntry.ts', location.href), { type: 'module', name: 'qa-06-planegcs' })
|
||||
const listeners = { message: new Set(), error: new Set() }
|
||||
let terminated = false
|
||||
const wrapper = {
|
||||
postMessage: (message) => real.postMessage(message),
|
||||
terminate: () => { terminated = true; real.terminate() },
|
||||
get terminated() { return terminated },
|
||||
addEventListener: (type, listener) => { listeners[type].add(listener) },
|
||||
removeEventListener: (type, listener) => { listeners[type].delete(listener) },
|
||||
crash: () => {
|
||||
terminated = true
|
||||
real.terminate()
|
||||
const event = { message: 'QA-06 injected Worker crash', error: new Error('QA-06 injected Worker crash') }
|
||||
for (const listener of listeners.error) listener(event)
|
||||
},
|
||||
}
|
||||
real.addEventListener('message', (event) => { for (const listener of listeners.message) listener(event) })
|
||||
real.addEventListener('error', (event) => { for (const listener of listeners.error) listener(event) })
|
||||
wrappers.push(wrapper)
|
||||
activeWrapper = wrapper
|
||||
return wrapper
|
||||
}
|
||||
|
||||
const nativeWrappers = []
|
||||
let activeNativeWrapper = null
|
||||
const nativeWorkerFactory = () => {
|
||||
const real = new Worker(new URL('/src/facade/nativeHistoryWorkerEntry.ts', location.href), { type: 'module', name: 'qa-06-native-history' })
|
||||
const listeners = { message: new Set(), error: new Set() }
|
||||
let terminated = false
|
||||
const wrapper = {
|
||||
postMessage: (message) => real.postMessage(message),
|
||||
terminate: () => { terminated = true; real.terminate() },
|
||||
get terminated() { return terminated },
|
||||
addEventListener: (type, listener) => { listeners[type].add(listener) },
|
||||
removeEventListener: (type, listener) => { listeners[type].delete(listener) },
|
||||
crash: () => {
|
||||
terminated = true
|
||||
real.terminate()
|
||||
const event = { message: 'QA-06 injected native history Worker crash', error: new Error('QA-06 injected native history Worker crash') }
|
||||
for (const listener of listeners.error) listener(event)
|
||||
},
|
||||
}
|
||||
real.addEventListener('message', (event) => { for (const listener of listeners.message) listener(event) })
|
||||
real.addEventListener('error', (event) => { for (const listener of listeners.error) listener(event) })
|
||||
nativeWrappers.push(wrapper)
|
||||
activeNativeWrapper = wrapper
|
||||
return wrapper
|
||||
}
|
||||
|
||||
const snapshot = createSketch('qa-06-sketch', [{ id: 'line', type: 'line', start: { x: 0, y: 0 }, end: { x: 3, y: 4 } }], [
|
||||
{ id: 'horizontal', type: 'horizontal', geometryId: 'line' },
|
||||
{ id: 'length', type: 'distance', first: { geometryId: 'line', point: 'start' }, second: { geometryId: 'line', point: 'end' }, value: 5 },
|
||||
])
|
||||
const request = (requestId) => ({ requestId, documentId: 'qa-06-doc', documentVersion: 1, snapshot })
|
||||
const settle = (promise) => promise.then(() => 'resolved', (error) => ({ name: error?.name || 'Error', message: String(error?.message || error) }))
|
||||
|
||||
let provider
|
||||
let nativeProvider
|
||||
let markerName
|
||||
try {
|
||||
if (!crossOriginIsolated || typeof SharedArrayBuffer !== 'function') throw new Error('Chrome fault harness requires cross-origin isolation and SharedArrayBuffer.')
|
||||
if (!navigator.storage?.getDirectory) throw new Error('Chrome OPFS is unavailable.')
|
||||
|
||||
const root = await navigator.storage.getDirectory()
|
||||
markerName = `qa-06-fault-${crypto.randomUUID()}.marker`
|
||||
const marker = await root.getFileHandle(markerName, { create: true })
|
||||
await (await marker.createWritable()).close()
|
||||
const markerBeforeCleanup = await root.getFileHandle(markerName)
|
||||
const markerBytes = (await (await markerBeforeCleanup.getFile()).arrayBuffer()).byteLength
|
||||
|
||||
provider = new PlanegcsWorkerProvider({ workerFactory, initializationTimeoutMs: 30_000 })
|
||||
const initial = await provider.initialize()
|
||||
const completed = await new SketchSolverCoordinator(() => 1).solve(provider, request('qa-06-completed'))
|
||||
|
||||
const cancellationCoordinator = new SketchSolverCoordinator(() => 1)
|
||||
const cancellation = cancellationCoordinator.solve(provider, request('qa-06-cancelled'))
|
||||
cancellationCoordinator.cancel()
|
||||
const cancelled = await cancellation
|
||||
|
||||
const wrapperBeforeCrash = activeWrapper
|
||||
wrapperBeforeCrash?.crash()
|
||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||
const unavailableAfterCrash = provider.capabilities().availability === 'unavailable'
|
||||
const recovered = await new SketchSolverCoordinator(() => 1).solve(provider, request('qa-06-recovered'))
|
||||
const stale = await new SketchSolverCoordinator(() => 2).solve(provider, request('qa-06-stale'))
|
||||
const recoveredLine = recovered.response?.result.snapshot.geometry[0]
|
||||
|
||||
nativeProvider = new NativeOcctHistoryWorkerProvider({ workerFactory: nativeWorkerFactory, moduleUrl: '/native/occt-history/bitbybit-occt-history.js', initializationTimeoutMs: 30_000 })
|
||||
const nativeInitial = await nativeProvider.initialize()
|
||||
const nativeWrapperBeforeCrash = activeNativeWrapper
|
||||
nativeWrapperBeforeCrash?.crash()
|
||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||
const nativeUnavailableAfterCrash = nativeProvider.capabilities().availability === 'unavailable'
|
||||
const nativeRecovered = await nativeProvider.initialize()
|
||||
|
||||
await root.removeEntry(markerName)
|
||||
let markerRemoved = false
|
||||
try { await root.getFileHandle(markerName); } catch (error) { markerRemoved = error?.name === 'NotFoundError' }
|
||||
markerName = undefined
|
||||
|
||||
const lifecycle = {
|
||||
initialAvailable: initial.availability === 'available',
|
||||
completed: completed.status,
|
||||
cancelled: cancelled.status,
|
||||
crashInjected: true,
|
||||
unavailableAfterCrash,
|
||||
workerFactoryCount: wrappers.length,
|
||||
recovered: recovered.status,
|
||||
stale: stale.status,
|
||||
recoveredEnd: recoveredLine?.type === 'line' ? recoveredLine.end : null,
|
||||
nativeInitialAvailable: nativeInitial.availability === 'available',
|
||||
nativeCrashInjected: true,
|
||||
nativeUnavailableAfterCrash,
|
||||
nativeWorkerFactoryCount: nativeWrappers.length,
|
||||
nativeRecoveredAvailable: nativeRecovered.availability === 'available',
|
||||
}
|
||||
const opfs = { markerBytes, markerRemoved }
|
||||
const passed = lifecycle.initialAvailable && lifecycle.completed === 'completed' && lifecycle.cancelled === 'cancelled' && lifecycle.crashInjected && lifecycle.unavailableAfterCrash && lifecycle.workerFactoryCount >= 3 && lifecycle.recovered === 'completed' && lifecycle.stale === 'stale' && Math.abs(lifecycle.recoveredEnd?.x - 5) <= 1e-7 && Math.abs(lifecycle.recoveredEnd?.y) <= 1e-7 && lifecycle.nativeInitialAvailable && lifecycle.nativeCrashInjected && lifecycle.nativeUnavailableAfterCrash && lifecycle.nativeWorkerFactoryCount >= 2 && lifecycle.nativeRecoveredAvailable && opfs.markerBytes === 0 && opfs.markerRemoved
|
||||
Object.assign(report, { status: passed ? 'pass' : 'failed', lifecycle, opfs, activeWorkersBeforeDispose: wrappers.length, activeNativeWorkersBeforeDispose: nativeWrappers.length })
|
||||
} catch (error) {
|
||||
report.error = error instanceof Error ? `${error.name}: ${error.message}` : String(error)
|
||||
if (markerName) {
|
||||
try { await (await navigator.storage.getDirectory()).removeEntry(markerName) } catch {}
|
||||
}
|
||||
} finally {
|
||||
provider?.dispose()
|
||||
nativeProvider?.dispose()
|
||||
Object.assign(report, { activeWorkersAfterDispose: wrappers.filter((wrapper) => wrapper.terminated).length, activeNativeWorkersAfterDispose: nativeWrappers.filter((wrapper) => wrapper.terminated).length })
|
||||
}
|
||||
document.querySelector('#status').textContent = JSON.stringify(report, null, 2)
|
||||
await fetch('/__chrome-fault-injection-report', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(report) })
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
1
public/chrome-fcstd-roundtrip-harness.html
Normal file
1
public/chrome-fcstd-roundtrip-harness.html
Normal file
@@ -0,0 +1 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Chrome FCStd Round Trip Harness</title></head><body><pre id="result">running</pre><script type="module" src="/src/chromeFcstdRoundTripHarness.ts"></script></body></html>
|
||||
5
public/chrome-fem-harness.html
Normal file
5
public/chrome-fem-harness.html
Normal file
@@ -0,0 +1,5 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>Chrome FEM harness</title>
|
||||
<pre id="result">running</pre>
|
||||
<script type="module" src="/src/chromeFemHarness.ts"></script>
|
||||
5
public/chrome-inspection-harness.html
Normal file
5
public/chrome-inspection-harness.html
Normal file
@@ -0,0 +1,5 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>Chrome Inspection harness</title>
|
||||
<pre id="result">running</pre>
|
||||
<script type="module" src="/src/chromeInspectionHarness.ts"></script>
|
||||
5
public/chrome-mesh-harness.html
Normal file
5
public/chrome-mesh-harness.html
Normal file
@@ -0,0 +1,5 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>Chrome Mesh harness</title>
|
||||
<pre id="result">running</pre>
|
||||
<script type="module" src="/src/chromeMeshHarness.ts"></script>
|
||||
52
public/chrome-native-chamfer-history-harness.html
Normal file
52
public/chrome-native-chamfer-history-harness.html
Normal file
@@ -0,0 +1,52 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>Chrome native Chamfer history harness</title>
|
||||
<pre id="result">running</pre>
|
||||
<script type="module">
|
||||
import { BitbybitGeometryRuntime } from '/src/facade/geometryRuntime.ts'
|
||||
import { NativeOcctHistoryWorkerProvider } from '/src/facade/nativeHistoryWorkerClient.ts'
|
||||
const output = document.querySelector('#result')
|
||||
const report = { schemaVersion: 1, browserId: 'chrome', executionContext: 'geometry-and-history-workers', crossOriginIsolated: globalThis.crossOriginIsolated === true, status: 'running' }
|
||||
let runtime, provider, base, result
|
||||
try {
|
||||
runtime = new BitbybitGeometryRuntime()
|
||||
const capabilities = await runtime.initialize()
|
||||
provider = new NativeOcctHistoryWorkerProvider({ moduleUrl: '/native/occt-history/bitbybit-occt-history.js' })
|
||||
const nativeCapabilities = await provider.initialize()
|
||||
runtime.configureNativeHistory(provider)
|
||||
const context = { documentId: 'chrome-native-chamfer', documentVersion: 1 }
|
||||
base = await runtime.createBox({ ...context, width: 6, length: 6, height: 6, center: [0, 0, 0], originOnCenter: true })
|
||||
result = await runtime.chamfer({ ...context, base, distance: 0.4, indexes: [0] })
|
||||
const [quality, mass] = await Promise.all([runtime.qualityReport(result), runtime.massProperties(result)])
|
||||
const markerDirectory = await navigator.storage.getDirectory()
|
||||
const markerName = 'native-chamfer-history-chrome-marker.json'
|
||||
const marker = await markerDirectory.getFileHandle(markerName, { create: true })
|
||||
const writable = await marker.createWritable()
|
||||
await writable.write(JSON.stringify({ operation: 'chamfer', provider: nativeCapabilities.providerId }))
|
||||
await writable.close()
|
||||
const markerText = await (await marker.getFile()).text()
|
||||
const records = await runtime.topologyHistory({ ...context, operationId: 'chrome-native-chamfer-history', operation: 'chamfer', distance: 0.4, inputs: [{ objectId: 'Base', shape: base }], result })
|
||||
const relations = { modified: records.filter((record) => record.relation === 'modified').length, generated: records.filter((record) => record.relation === 'generated').length, deleted: records.filter((record) => record.relation === 'deleted').length }
|
||||
report.capabilities = capabilities
|
||||
report.nativeCapabilities = nativeCapabilities
|
||||
report.history = { recordCount: records.length, relations, sourceObjects: [...new Set(records.map((record) => record.sourceObjectId))] }
|
||||
report.result = { structuralValid: quality.structuralValid, solids: quality.solids, volume: mass.volume }
|
||||
report.opfs = { markerCreated: true, markerSuite: JSON.parse(markerText).operation, markerRemoved: false }
|
||||
await markerDirectory.removeEntry(markerName)
|
||||
report.opfs.markerRemoved = true
|
||||
await runtime.release(result); result = undefined
|
||||
await runtime.release(base); base = undefined
|
||||
report.afterRelease = { shapeCount: runtime.capabilities().shapeCount, kernelReferenceCount: runtime.capabilities().kernelReferenceCount }
|
||||
report.status = report.crossOriginIsolated && capabilities.status === 'ready' && nativeCapabilities.availability === 'available' && nativeCapabilities.operations.includes('chamfer') && records.length > 0 && relations.modified > 0 && relations.generated > 0 && relations.deleted > 0 && quality.structuralValid && quality.solids === 1 && mass.volume > 0 && mass.volume < 216 && report.opfs.markerSuite === 'chamfer' && report.opfs.markerRemoved && report.afterRelease.shapeCount === 0 && report.afterRelease.kernelReferenceCount === 0 ? 'pass' : 'failed'
|
||||
} catch (error) {
|
||||
report.status = 'failed'
|
||||
report.error = error instanceof Error ? `${error.name}: ${error.message}` : String(error)
|
||||
} finally {
|
||||
if (result) await runtime?.release(result).catch(() => {})
|
||||
if (base) await runtime?.release(base).catch(() => {})
|
||||
provider?.dispose(); runtime?.dispose()
|
||||
}
|
||||
output.textContent = JSON.stringify(report, null, 2)
|
||||
await fetch('/__native-chamfer-history-report', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(report) })
|
||||
if (report.status !== 'pass') throw new Error(report.error || 'Chrome native Chamfer history harness failed.')
|
||||
</script>
|
||||
42
public/chrome-native-draft-history-harness.html
Normal file
42
public/chrome-native-draft-history-harness.html
Normal file
@@ -0,0 +1,42 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>Chrome native Draft history harness</title>
|
||||
<pre id="result">running</pre>
|
||||
<script type="module">
|
||||
import { BitbybitGeometryRuntime } from '/src/facade/geometryRuntime.ts'
|
||||
import { NativeOcctHistoryWorkerProvider } from '/src/facade/nativeHistoryWorkerClient.ts'
|
||||
const output = document.querySelector('#result')
|
||||
const report = { schemaVersion: 1, browserId: 'chrome', executionContext: 'geometry-and-history-workers', crossOriginIsolated: globalThis.crossOriginIsolated === true, status: 'running' }
|
||||
let runtime, provider, base, result, stage = 'init'
|
||||
try {
|
||||
stage = 'initialize'
|
||||
runtime = new BitbybitGeometryRuntime()
|
||||
const capabilities = await runtime.initialize()
|
||||
provider = new NativeOcctHistoryWorkerProvider({ moduleUrl: '/native/occt-history/bitbybit-occt-history.js' })
|
||||
const nativeCapabilities = await provider.initialize()
|
||||
runtime.configureNativeHistory(provider)
|
||||
const context = { documentId: 'chrome-native-draft', documentVersion: 1 }
|
||||
stage = 'create-box'; base = await runtime.createBox({ ...context, width: 4, length: 4, height: 4 })
|
||||
stage = 'bitbybit-draft'; result = await runtime.draft({ ...context, base, angle: 5, direction: [0, 0, 1], neutralPlaneOrigin: [0, 0, 0], neutralPlaneDirection: [0, 0, 1], indexes: [1] })
|
||||
stage = 'quality'; const [quality, mass] = await Promise.all([runtime.qualityReport(result), runtime.massProperties(result)])
|
||||
const markerDirectory = await navigator.storage.getDirectory()
|
||||
const markerName = 'native-draft-history-chrome-marker.json'
|
||||
const marker = await markerDirectory.getFileHandle(markerName, { create: true })
|
||||
const writable = await marker.createWritable(); await writable.write(JSON.stringify({ operation: 'draft', provider: nativeCapabilities.providerId })); await writable.close()
|
||||
const markerText = await (await marker.getFile()).text()
|
||||
stage = 'native-history'; const records = await runtime.topologyHistory({ ...context, operationId: 'chrome-native-draft-history', operation: 'draft', angle: 5, direction: [0, 0, 1], axisOrigin: [0, 0, 0], neutralPlaneDirection: [0, 0, 1], faceIndexes: [1], inputs: [{ objectId: 'Base', shape: base }], result })
|
||||
const relations = { modified: records.filter((record) => record.relation === 'modified').length, generated: records.filter((record) => record.relation === 'generated').length, deleted: records.filter((record) => record.relation === 'deleted').length }
|
||||
report.capabilities = capabilities; report.nativeCapabilities = nativeCapabilities
|
||||
report.history = { recordCount: records.length, relations, sourceObjects: [...new Set(records.map((record) => record.sourceObjectId))] }
|
||||
report.result = { structuralValid: quality.structuralValid, solids: quality.solids, volume: mass.volume }
|
||||
report.opfs = { markerCreated: true, markerSuite: JSON.parse(markerText).operation, markerRemoved: false }
|
||||
await markerDirectory.removeEntry(markerName); report.opfs.markerRemoved = true
|
||||
await runtime.release(result); result = undefined; await runtime.release(base); base = undefined
|
||||
report.afterRelease = { shapeCount: runtime.capabilities().shapeCount, kernelReferenceCount: runtime.capabilities().kernelReferenceCount }
|
||||
report.status = report.crossOriginIsolated && capabilities.status === 'ready' && nativeCapabilities.availability === 'available' && nativeCapabilities.operations.includes('draft') && records.length > 0 && relations.modified > 0 && report.result.structuralValid && report.result.solids === 1 && report.result.volume > 0 && report.opfs.markerSuite === 'draft' && report.opfs.markerRemoved && report.afterRelease.shapeCount === 0 && report.afterRelease.kernelReferenceCount === 0 ? 'pass' : 'failed'
|
||||
} catch (error) { report.status = 'failed'; report.error = `${stage}: ${error instanceof Error ? `${error.name}: ${error.message}` : String(error)}` }
|
||||
finally { if (result) await runtime?.release(result).catch(() => {}); if (base) await runtime?.release(base).catch(() => {}); provider?.dispose(); runtime?.dispose() }
|
||||
output.textContent = JSON.stringify(report, null, 2)
|
||||
await fetch('/__native-draft-history-report', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(report) })
|
||||
if (report.status !== 'pass') throw new Error(report.error || 'Chrome native Draft history harness failed.')
|
||||
</script>
|
||||
59
public/chrome-native-fillet-history-harness.html
Normal file
59
public/chrome-native-fillet-history-harness.html
Normal file
@@ -0,0 +1,59 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>Chrome native Fillet history harness</title>
|
||||
<pre id="result">running</pre>
|
||||
<script type="module">
|
||||
import { BitbybitGeometryRuntime } from '/src/facade/geometryRuntime.ts'
|
||||
import { NativeOcctHistoryWorkerProvider } from '/src/facade/nativeHistoryWorkerClient.ts'
|
||||
|
||||
const output = document.querySelector('#result')
|
||||
const report = { schemaVersion: 1, browserId: 'chrome', executionContext: 'geometry-and-history-workers', crossOriginIsolated: globalThis.crossOriginIsolated === true, status: 'running' }
|
||||
let runtime
|
||||
let provider
|
||||
let base
|
||||
let result
|
||||
try {
|
||||
runtime = new BitbybitGeometryRuntime()
|
||||
const capabilities = await runtime.initialize()
|
||||
provider = new NativeOcctHistoryWorkerProvider({ moduleUrl: '/native/occt-history/bitbybit-occt-history.js' })
|
||||
const nativeCapabilities = await provider.initialize()
|
||||
runtime.configureNativeHistory(provider)
|
||||
const context = { documentId: 'chrome-native-fillet', documentVersion: 1 }
|
||||
base = await runtime.createBox({ ...context, width: 6, length: 6, height: 6, center: [0, 0, 0], originOnCenter: true })
|
||||
result = await runtime.fillet({ ...context, base, radius: 0.4, indexes: [0] })
|
||||
const [quality, mass] = await Promise.all([runtime.qualityReport(result), runtime.massProperties(result)])
|
||||
const markerDirectory = await navigator.storage.getDirectory()
|
||||
const markerName = 'native-fillet-history-chrome-marker.json'
|
||||
const marker = await markerDirectory.getFileHandle(markerName, { create: true })
|
||||
const writable = await marker.createWritable()
|
||||
await writable.write(JSON.stringify({ operation: 'fillet', provider: nativeCapabilities.providerId }))
|
||||
await writable.close()
|
||||
const markerText = await (await marker.getFile()).text()
|
||||
const records = await runtime.topologyHistory({ ...context, operationId: 'chrome-native-fillet-history', operation: 'fillet', radius: 0.4, inputs: [{ objectId: 'Base', shape: base }], result })
|
||||
const relations = { modified: records.filter((record) => record.relation === 'modified').length, generated: records.filter((record) => record.relation === 'generated').length, deleted: records.filter((record) => record.relation === 'deleted').length }
|
||||
report.capabilities = capabilities
|
||||
report.nativeCapabilities = nativeCapabilities
|
||||
report.history = { recordCount: records.length, relations, sourceObjects: [...new Set(records.map((record) => record.sourceObjectId))] }
|
||||
report.result = { structuralValid: quality.structuralValid, solids: quality.solids, volume: mass.volume }
|
||||
report.opfs = { markerCreated: true, markerSuite: JSON.parse(markerText).operation, markerRemoved: false }
|
||||
await markerDirectory.removeEntry(markerName)
|
||||
report.opfs.markerRemoved = true
|
||||
await runtime.release(result)
|
||||
result = undefined
|
||||
await runtime.release(base)
|
||||
base = undefined
|
||||
report.afterRelease = { shapeCount: runtime.capabilities().shapeCount, kernelReferenceCount: runtime.capabilities().kernelReferenceCount }
|
||||
report.status = report.crossOriginIsolated && capabilities.status === 'ready' && nativeCapabilities.availability === 'available' && nativeCapabilities.operations.includes('fillet') && records.length > 0 && relations.modified > 0 && relations.generated > 0 && relations.deleted > 0 && quality.structuralValid && quality.solids === 1 && mass.volume > 0 && mass.volume < 216 && report.opfs.markerSuite === 'fillet' && report.opfs.markerRemoved && report.afterRelease.shapeCount === 0 && report.afterRelease.kernelReferenceCount === 0 ? 'pass' : 'failed'
|
||||
} catch (error) {
|
||||
report.status = 'failed'
|
||||
report.error = error instanceof Error ? `${error.name}: ${error.message}` : String(error)
|
||||
} finally {
|
||||
if (result) await runtime?.release(result).catch(() => {})
|
||||
if (base) await runtime?.release(base).catch(() => {})
|
||||
provider?.dispose()
|
||||
runtime?.dispose()
|
||||
}
|
||||
output.textContent = JSON.stringify(report, null, 2)
|
||||
await fetch('/__native-fillet-history-report', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(report) })
|
||||
if (report.status !== 'pass') throw new Error(report.error || 'Chrome native Fillet history harness failed.')
|
||||
</script>
|
||||
74
public/chrome-native-groove-history-harness.html
Normal file
74
public/chrome-native-groove-history-harness.html
Normal file
@@ -0,0 +1,74 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>Chrome native Groove history harness</title>
|
||||
<pre id="result">running</pre>
|
||||
<script type="module">
|
||||
import { BitbybitGeometryRuntime } from '/src/facade/geometryRuntime.ts'
|
||||
import { NativeOcctHistoryWorkerProvider } from '/src/facade/nativeHistoryWorkerClient.ts'
|
||||
|
||||
const output = document.querySelector('#result')
|
||||
const report = { schemaVersion: 1, browserId: 'chrome', crossOriginIsolated: globalThis.crossOriginIsolated === true, status: 'running' }
|
||||
let runtime
|
||||
let provider
|
||||
try {
|
||||
const [baseStep, profileStep] = await Promise.all([
|
||||
fetch('/native-groove-base.step').then((response) => response.ok ? response.text() : Promise.reject(new Error(`Groove base fixture failed: ${response.status}`))),
|
||||
fetch('/native-groove-profile.step').then((response) => response.ok ? response.text() : Promise.reject(new Error(`Groove profile fixture failed: ${response.status}`))),
|
||||
])
|
||||
runtime = new BitbybitGeometryRuntime()
|
||||
const capabilities = await runtime.initialize()
|
||||
provider = new NativeOcctHistoryWorkerProvider({ moduleUrl: '/native/occt-history/bitbybit-occt-history.js' })
|
||||
const nativeCapabilities = await provider.initialize()
|
||||
runtime.configureNativeHistory(provider)
|
||||
const context = { documentId: 'chrome-native-groove', documentVersion: 1 }
|
||||
const base = await runtime.createBox({ ...context, width: 10, length: 8, height: 10, center: [0, 0, 0], originOnCenter: false })
|
||||
const profile = await runtime.importShape({ ...context, format: 'step', text: profileStep })
|
||||
const planarProfile = { outer: [[0.5, 2, 0], [1.5, 2, 0], [1.5, 5, 0], [0.5, 5, 0]] }
|
||||
const result = await runtime.groove({ ...context, base, profile: planarProfile, axisOrigin: [0, 0, 0], axisDirection: [0, 1, 0], angle: 360 })
|
||||
const [quality, mass] = await Promise.all([runtime.qualityReport(result), runtime.massProperties(result)])
|
||||
const baseQuality = await runtime.qualityReport(base)
|
||||
const markerDirectory = await navigator.storage.getDirectory()
|
||||
const markerName = 'native-groove-history-chrome-marker.json'
|
||||
const marker = await markerDirectory.getFileHandle(markerName, { create: true })
|
||||
const writable = await marker.createWritable()
|
||||
await writable.write(JSON.stringify({ operation: 'groove', provider: nativeCapabilities.providerId }))
|
||||
await writable.close()
|
||||
const markerPayload = JSON.parse(await (await marker.getFile()).text())
|
||||
const records = await runtime.topologyHistory({ ...context, operationId: 'chrome-native-groove-history', operation: 'groove', inputs: [{ objectId: 'Base', shape: base }, { objectId: 'Profile', shape: profile }], axisOrigin: [0, 0, 0], direction: [0, 1, 0], angle: 360, result })
|
||||
const stageCaptures = records.stageCaptures || []
|
||||
const twoAngleResult = await runtime.groove({ ...context, base, profile: planarProfile, axisOrigin: [0, 0, 0], axisDirection: [0, 1, 0], angle: 180, profileRotationAngle: -60 })
|
||||
const [twoAngleQuality, twoAngleMass] = await Promise.all([runtime.qualityReport(twoAngleResult), runtime.massProperties(twoAngleResult)])
|
||||
const twoAngleUnexplainedErrors = (twoAngleQuality.structuralIssues || []).filter((issue) => issue.severity === 'error' && issue.description !== 'CoEdgeDef has no Curve2D representation').length
|
||||
const twoAngleRecords = await runtime.topologyHistory({ ...context, operationId: 'chrome-native-groove-two-angles-history', operation: 'groove', inputs: [{ objectId: 'Base', shape: base }, { objectId: 'Profile', shape: profile }], stages: [{ stageId: 'Groove:stage:0', operation: 'groove', inputObjectIds: ['Base', 'Profile'], resultObjectId: 'GrooveTwoAngles', ordinal: 0 }], featureSides: [{ direction: [0, 1, 0], angle: 120 }, { direction: [0, -1, 0], angle: 60 }], axisOrigin: [0, 0, 0], direction: [0, 1, 0], angle: 120, result: twoAngleResult })
|
||||
const twoAngleStages = twoAngleRecords.stageCaptures || []
|
||||
const modified = records.filter((record) => record.relation === 'modified').length
|
||||
const generated = records.filter((record) => record.relation === 'generated').length
|
||||
const deleted = records.filter((record) => record.relation === 'deleted').length
|
||||
report.capabilities = capabilities
|
||||
report.nativeCapabilities = nativeCapabilities
|
||||
report.history = { recordCount: records.length, sourceObjects: [...new Set(stageCaptures.flatMap((stage) => stage.records.map((record) => record.sourceObjectId)))].sort(), relations: { modified, generated, deleted }, stages: stageCaptures.map((stage) => ({ stageId: stage.stageId, operation: stage.operation, inputObjectIds: stage.inputObjectIds, resultObjectId: stage.resultObjectId, ordinal: stage.ordinal, topologyEntries: stage.topology.entries.length, recordCount: stage.records.length })) }
|
||||
report.twoAngleHistory = { recordCount: twoAngleRecords.length, sourceObjects: [...new Set(twoAngleStages.flatMap((stage) => stage.records.map((record) => record.sourceObjectId)))].sort(), structuralValid: twoAngleQuality.nativeKernelValid === true, graphStructuralValid: twoAngleQuality.structuralValid, graphStructuralErrors: twoAngleQuality.structuralErrors, graphStructuralWarnings: twoAngleQuality.structuralWarnings, graphStructuralIssues: twoAngleQuality.structuralIssues, unexplainedStructuralErrors: twoAngleUnexplainedErrors, solids: twoAngleQuality.solids, volume: twoAngleMass.volume, stages: twoAngleStages.map((stage) => ({ stageId: stage.stageId, operation: stage.operation, inputObjectIds: stage.inputObjectIds, resultObjectId: stage.resultObjectId, ordinal: stage.ordinal, topologyEntries: stage.topology.entries.length, recordCount: stage.records.length })) }
|
||||
report.base = { boundingBox: baseQuality.boundingBox, volume: (await runtime.massProperties(base)).volume }
|
||||
report.result = { structuralValid: quality.structuralValid, structuralErrors: quality.structuralErrors, structuralWarnings: quality.structuralWarnings, solids: quality.solids, faces: quality.faces, edges: quality.edges, vertices: quality.vertices, volume: mass.volume }
|
||||
await runtime.release(twoAngleResult)
|
||||
await runtime.release(result)
|
||||
await runtime.release(profile)
|
||||
await runtime.release(base)
|
||||
report.afterRelease = { shapeCount: runtime.capabilities().shapeCount, kernelReferenceCount: runtime.capabilities().kernelReferenceCount }
|
||||
await markerDirectory.removeEntry(markerName)
|
||||
let markerRemoved = false
|
||||
try { await markerDirectory.getFileHandle(markerName) } catch (error) { markerRemoved = error?.name === 'NotFoundError' }
|
||||
report.opfs = { markerOperation: markerPayload.operation, markerRemoved }
|
||||
const expectedVolume = 800 - 6 * Math.PI
|
||||
report.status = report.crossOriginIsolated && capabilities.status === 'ready' && nativeCapabilities.availability === 'available' && nativeCapabilities.operations.includes('groove') && nativeCapabilities.operations.includes('rotate') && records.length > 0 && modified > 0 && generated + deleted > 0 && stageCaptures.length === 2 && stageCaptures[0].operation === 'revolution' && stageCaptures[0].inputObjectIds.join(',') === 'Profile' && stageCaptures[0].records.length > 0 && stageCaptures[0].topology.entries.length > 0 && stageCaptures[1].operation === 'cut' && stageCaptures[1].inputObjectIds.join(',') === `Base,${stageCaptures[0].resultObjectId}` && stageCaptures[1].records.length === records.length && stageCaptures[1].topology.entries.length > 0 && report.history.sourceObjects.includes('Base') && report.history.sourceObjects.includes('Profile') && report.history.sourceObjects.includes(stageCaptures[0].resultObjectId) && quality.structuralValid && quality.solids === 1 && Math.abs(mass.volume - expectedVolume) <= 1e-5 && twoAngleRecords.length > 0 && twoAngleQuality.nativeKernelValid === true && twoAngleUnexplainedErrors === 0 && twoAngleQuality.solids === 1 && Math.abs(twoAngleMass.volume - (800 - 3 * Math.PI)) <= 1e-5 && twoAngleStages.length === 3 && twoAngleStages[0].operation === 'rotate' && twoAngleStages[0].inputObjectIds.join(',') === 'Profile' && twoAngleStages[0].topology.entries.length > 0 && twoAngleStages[1].operation === 'revolution' && twoAngleStages[1].inputObjectIds.join(',') === `${twoAngleStages[0].resultObjectId}` && twoAngleStages[1].topology.entries.length > 0 && twoAngleStages[2].operation === 'cut' && twoAngleStages[2].inputObjectIds.join(',') === `Base,${twoAngleStages[1].resultObjectId}` && twoAngleStages[2].resultObjectId === 'GrooveTwoAngles' && twoAngleStages[2].topology.entries.length > 0 && report.twoAngleHistory.sourceObjects.includes('Base') && report.twoAngleHistory.sourceObjects.includes('Profile') && report.twoAngleHistory.sourceObjects.includes(twoAngleStages[0].resultObjectId) && report.twoAngleHistory.sourceObjects.includes(twoAngleStages[1].resultObjectId) && report.afterRelease.shapeCount === 0 && report.afterRelease.kernelReferenceCount === 0 && report.opfs.markerOperation === 'groove' && report.opfs.markerRemoved ? 'pass' : 'failed'
|
||||
} catch (error) {
|
||||
report.status = 'failed'
|
||||
report.error = error instanceof Error ? error.message : String(error)
|
||||
} finally {
|
||||
provider?.dispose()
|
||||
runtime?.dispose()
|
||||
}
|
||||
output.textContent = JSON.stringify(report, null, 2)
|
||||
await fetch('/__native-groove-history-report', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(report) })
|
||||
if (report.status !== 'pass') throw new Error(report.error || 'Chrome native Groove history harness failed.')
|
||||
</script>
|
||||
48
public/chrome-native-history-harness.html
Normal file
48
public/chrome-native-history-harness.html
Normal file
@@ -0,0 +1,48 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>Chrome native OCCT history harness</title>
|
||||
<pre id="result">running</pre>
|
||||
<script type="module">
|
||||
import { BitbybitGeometryRuntime } from '/src/facade/geometryRuntime.ts'
|
||||
import { NativeOcctHistoryWorkerProvider } from '/src/facade/nativeHistoryWorkerClient.ts'
|
||||
|
||||
const output = document.querySelector('#result')
|
||||
const report = { schemaVersion: 1, browserId: 'chrome', crossOriginIsolated: globalThis.crossOriginIsolated === true, status: 'running' }
|
||||
let runtime
|
||||
let provider
|
||||
try {
|
||||
runtime = new BitbybitGeometryRuntime()
|
||||
const capabilities = await runtime.initialize()
|
||||
provider = new NativeOcctHistoryWorkerProvider({ moduleUrl: '/native/occt-history/bitbybit-occt-history.js' })
|
||||
const nativeCapabilities = await provider.initialize()
|
||||
runtime.configureNativeHistory(provider)
|
||||
const context = { documentId: 'chrome-native-history', documentVersion: 1 }
|
||||
const base = await runtime.createBox({ ...context, width: 10, length: 10, height: 10 })
|
||||
const tool = await runtime.createBox({ ...context, width: 6, length: 6, height: 6, center: [5, 5, 5], originOnCenter: true })
|
||||
const result = await runtime.cut({ ...context, base, tools: [tool] })
|
||||
const markerDirectory = await navigator.storage.getDirectory()
|
||||
const marker = await markerDirectory.getFileHandle('native-history-chrome-marker.json', { create: true })
|
||||
const writable = await marker.createWritable()
|
||||
await writable.write(JSON.stringify({ operation: 'cut', provider: nativeCapabilities.providerId }))
|
||||
await writable.close()
|
||||
const records = await runtime.topologyHistory({ ...context, operationId: 'chrome-native-history-cut', operation: 'cut', inputs: [{ objectId: 'Base', shape: base }, { objectId: 'Tool', shape: tool }], result })
|
||||
report.capabilities = capabilities
|
||||
report.nativeCapabilities = nativeCapabilities
|
||||
report.history = { recordCount: records.length, relations: Object.fromEntries(records.reduce((counts, record) => counts.set(record.relation, (counts.get(record.relation) || 0) + 1), new Map())) }
|
||||
await runtime.release(result)
|
||||
await runtime.release(tool)
|
||||
await runtime.release(base)
|
||||
report.afterRelease = { shapeCount: runtime.capabilities().shapeCount, kernelReferenceCount: runtime.capabilities().kernelReferenceCount }
|
||||
await markerDirectory.removeEntry('native-history-chrome-marker.json').catch(() => {})
|
||||
report.status = report.crossOriginIsolated && capabilities.status === 'ready' && nativeCapabilities.availability === 'available' && nativeCapabilities.operations.includes('cut') && records.length > 0 && report.afterRelease.shapeCount === 0 && report.afterRelease.kernelReferenceCount === 0 ? 'pass' : 'failed'
|
||||
} catch (error) {
|
||||
report.status = 'failed'
|
||||
report.error = error instanceof Error ? error.message : String(error)
|
||||
} finally {
|
||||
provider?.dispose()
|
||||
runtime?.dispose()
|
||||
}
|
||||
output.textContent = JSON.stringify(report, null, 2)
|
||||
await fetch('/__native-history-report', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(report) })
|
||||
if (report.status !== 'pass') throw new Error(report.error || 'Chrome native history harness failed.')
|
||||
</script>
|
||||
28
public/chrome-native-hole-history-harness.html
Normal file
28
public/chrome-native-hole-history-harness.html
Normal file
@@ -0,0 +1,28 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>Chrome native Hole history harness</title>
|
||||
<pre id="result">running</pre>
|
||||
<script type="module">
|
||||
import { BitbybitGeometryRuntime } from '/src/facade/geometryRuntime.ts'
|
||||
import { NativeOcctHistoryWorkerProvider } from '/src/facade/nativeHistoryWorkerClient.ts'
|
||||
const output = document.querySelector('#result')
|
||||
const report = { schemaVersion: 1, browserId: 'chrome', executionContext: 'geometry-and-history-workers', crossOriginIsolated: globalThis.crossOriginIsolated === true, status: 'running' }
|
||||
let runtime, provider, base, tool, result
|
||||
try {
|
||||
runtime = new BitbybitGeometryRuntime(); const capabilities = await runtime.initialize()
|
||||
provider = new NativeOcctHistoryWorkerProvider({ moduleUrl: '/native/occt-history/bitbybit-occt-history.js' }); const nativeCapabilities = await provider.initialize(); runtime.configureNativeHistory(provider)
|
||||
const context = { documentId: 'chrome-native-hole', documentVersion: 1 }
|
||||
base = await runtime.createBox({ ...context, width: 10, length: 10, height: 10, center: [0, 0, 0], originOnCenter: true })
|
||||
tool = await runtime.createCylinder({ ...context, radius: 1, height: 10, center: [0, 0, -5], direction: [0, 0, 1] })
|
||||
result = await runtime.cut({ ...context, base, tools: [tool] })
|
||||
const [quality, mass] = await Promise.all([runtime.qualityReport(result), runtime.massProperties(result)])
|
||||
const markerDirectory = await navigator.storage.getDirectory(); const markerName = 'native-hole-history-chrome-marker.json'; const marker = await markerDirectory.getFileHandle(markerName, { create: true }); const writable = await marker.createWritable(); await writable.write(JSON.stringify({ operation: 'hole', provider: nativeCapabilities.providerId })); await writable.close(); const markerText = await (await marker.getFile()).text()
|
||||
const records = await runtime.topologyHistory({ ...context, operationId: 'chrome-native-hole-history', operation: 'hole', radius: 1, depth: 10, position: [0, 0, -5], direction: [0, 0, 1], inputs: [{ objectId: 'Base', shape: base }], result })
|
||||
const stageCaptures = records.stageCaptures || []
|
||||
const relations = { modified: records.filter((record) => record.relation === 'modified').length, generated: records.filter((record) => record.relation === 'generated').length, deleted: records.filter((record) => record.relation === 'deleted').length }
|
||||
report.capabilities = capabilities; report.nativeCapabilities = nativeCapabilities; report.history = { recordCount: records.length, relations, sourceObjects: [...new Set(records.map((record) => record.sourceObjectId))], stages: stageCaptures.map((stage) => ({ stageId: stage.stageId, operation: stage.operation, inputObjectIds: stage.inputObjectIds, resultObjectId: stage.resultObjectId, ordinal: stage.ordinal, topologyEntries: stage.topology.entries.length, recordCount: stage.records.length })), sourceStageIds: [...new Set(records.map((record) => record.sourceStageId).filter(Boolean))], resultStageIds: [...new Set(records.map((record) => record.resultStageId).filter(Boolean))] }; report.result = { structuralValid: quality.structuralValid, solids: quality.solids, volume: mass.volume }; report.opfs = { markerCreated: true, markerSuite: JSON.parse(markerText).operation, markerRemoved: false }; await markerDirectory.removeEntry(markerName); report.opfs.markerRemoved = true
|
||||
await runtime.release(result); result = undefined; await runtime.release(tool); tool = undefined; await runtime.release(base); base = undefined; report.afterRelease = { shapeCount: runtime.capabilities().shapeCount, kernelReferenceCount: runtime.capabilities().kernelReferenceCount }
|
||||
report.status = report.crossOriginIsolated && capabilities.status === 'ready' && nativeCapabilities.availability === 'available' && nativeCapabilities.operations.includes('hole') && records.length > 0 && relations.modified > 0 && relations.deleted > 0 && stageCaptures.length === 2 && stageCaptures[0].operation === 'hole' && stageCaptures[0].inputObjectIds.length === 0 && stageCaptures[0].topology.entries.length > 0 && stageCaptures[1].operation === 'cut' && stageCaptures[1].inputObjectIds.join(',') === `Base,${stageCaptures[0].resultObjectId}` && stageCaptures[1].topology.entries.length > 0 && records.every((record) => record.resultStageId === stageCaptures[1].stageId) && quality.structuralValid && quality.solids === 1 && mass.volume > 0 && mass.volume < 1000 && report.opfs.markerSuite === 'hole' && report.opfs.markerRemoved && report.afterRelease.shapeCount === 0 && report.afterRelease.kernelReferenceCount === 0 ? 'pass' : 'failed'
|
||||
} catch (error) { report.status = 'failed'; report.error = error instanceof Error ? `${error.name}: ${error.message}` : String(error) } finally { if (result) await runtime?.release(result).catch(() => {}); if (tool) await runtime?.release(tool).catch(() => {}); if (base) await runtime?.release(base).catch(() => {}); provider?.dispose(); runtime?.dispose() }
|
||||
output.textContent = JSON.stringify(report, null, 2); await fetch('/__native-hole-history-report', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(report) }); if (report.status !== 'pass') throw new Error(report.error || 'Chrome native Hole history harness failed.')
|
||||
</script>
|
||||
5
public/chrome-native-linear-pattern-history-harness.html
Normal file
5
public/chrome-native-linear-pattern-history-harness.html
Normal file
@@ -0,0 +1,5 @@
|
||||
<!doctype html><meta charset="utf-8"><title>Chrome native LinearPattern history</title><pre id="result">running</pre><script type="module">
|
||||
import { BitbybitGeometryRuntime } from '/src/facade/geometryRuntime.ts'; import { NativeOcctHistoryWorkerProvider } from '/src/facade/nativeHistoryWorkerClient.ts'; import { NativeOcctHistoryCoordinator } from '/src/facade/nativeHistoryProtocol.ts'
|
||||
const output=document.querySelector('#result'); const report={schemaVersion:1,browserId:'chrome',crossOriginIsolated:globalThis.crossOriginIsolated===true,status:'running'}; let runtime,provider,base,copy,result
|
||||
try { runtime=new BitbybitGeometryRuntime(); const capabilities=await runtime.initialize(); provider=new NativeOcctHistoryWorkerProvider({moduleUrl:'/native/occt-history/bitbybit-occt-history.js'}); const nativeCapabilities=await provider.initialize(); runtime.configureNativeHistory(provider); const context={documentId:'chrome-native-linear-pattern',documentVersion:1}; base=await runtime.createBox({...context,width:2,length:2,height:2}); copy=await runtime.applyPlacement({...context,shape:base,placement:{translation:[1,0,0],rotationAxis:[0,0,1],rotationAngle:0}}); result=await runtime.union({...context,shapes:[base,copy]}); const [quality,mass,baseStep]=await Promise.all([runtime.qualityReport(result),runtime.massProperties(result),runtime.exportStep(base,'Base.step')]); const execution=await new NativeOcctHistoryCoordinator(()=>1).capture(provider,{...context,operationId:'linear-pattern-history',operation:'linear-pattern',objectStep:baseStep.text,direction:[1,0,0]}); if(execution.status!=='completed'||!execution.response)throw new Error(`Native pattern history ${execution.status}`); const records=execution.response.history.records; const relations={modified:records.filter(r=>r.relation==='modified').length,generated:records.filter(r=>r.relation==='generated').length,deleted:records.filter(r=>r.relation==='deleted').length}; const directory=await navigator.storage.getDirectory(); const name='native-linear-pattern-history-marker.json'; const marker=await directory.getFileHandle(name,{create:true}); const writable=await marker.createWritable(); await writable.write(JSON.stringify({operation:'linear-pattern'})); await writable.close(); const markerSuite=JSON.parse(await (await marker.getFile()).text()).operation; await directory.removeEntry(name); report.capabilities=capabilities; report.nativeCapabilities=nativeCapabilities; report.history={recordCount:records.length,relations,sourceObjects:[...new Set(records.map(r=>r.source==='object'?'Base':'Base'))]}; report.result={bitbybitStructuralValid:quality.structuralValid,bitbybitSolids:quality.solids,bitbybitVolume:mass.volume,nativeStructuralValid:execution.response.history.summary.isValid,nativeSolids:execution.response.history.summary.solids,nativeVolume:execution.response.history.summary.volume}; report.opfs={markerSuite,markerRemoved:true}; await runtime.release(result); result=undefined; await runtime.release(copy); copy=undefined; await runtime.release(base); base=undefined; report.afterRelease={shapeCount:runtime.capabilities().shapeCount,kernelReferenceCount:runtime.capabilities().kernelReferenceCount}; report.status=report.crossOriginIsolated&&nativeCapabilities.operations.includes('linear-pattern')&&records.length>0&&quality.solids===1&&Math.abs(mass.volume-12)<1e-7&&report.result.nativeStructuralValid&&report.result.nativeSolids===1&&Math.abs(report.result.nativeVolume-12)<1e-7&&markerSuite==='linear-pattern'&&report.afterRelease.shapeCount===0&&report.afterRelease.kernelReferenceCount===0?'pass':'failed' } catch(error){report.status='failed';report.error=error instanceof Error?error.stack||error.message:String(error)} finally {if(result)await runtime?.release(result).catch(()=>{});if(copy)await runtime?.release(copy).catch(()=>{});if(base)await runtime?.release(base).catch(()=>{});provider?.dispose();runtime?.dispose()} output.textContent=JSON.stringify(report,null,2); await fetch('/__native-linear-pattern-history-report',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(report)}); if(report.status!=='pass')throw new Error(report.error||'failed')
|
||||
</script>
|
||||
36
public/chrome-native-loft-history-harness.html
Normal file
36
public/chrome-native-loft-history-harness.html
Normal file
@@ -0,0 +1,36 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>Chrome native Loft history</title>
|
||||
<pre id="result">running</pre>
|
||||
<script type="module">
|
||||
import { BitbybitGeometryRuntime } from '/src/facade/geometryRuntime.ts'
|
||||
import { NativeOcctHistoryWorkerProvider } from '/src/facade/nativeHistoryWorkerClient.ts'
|
||||
|
||||
const output = document.querySelector('#result')
|
||||
const report = { schemaVersion: 1, browserId: 'chrome', crossOriginIsolated: globalThis.crossOriginIsolated === true, status: 'running' }
|
||||
let runtime, provider, first, second, result
|
||||
try {
|
||||
const [firstStep, secondStep] = await Promise.all([
|
||||
fetch('/native-loft-first.step').then((response) => response.ok ? response.text() : Promise.reject(new Error(`First Loft fixture failed: ${response.status}`))),
|
||||
fetch('/native-loft-second.step').then((response) => response.ok ? response.text() : Promise.reject(new Error(`Second Loft fixture failed: ${response.status}`))),
|
||||
])
|
||||
runtime = new BitbybitGeometryRuntime(); const capabilities = await runtime.initialize()
|
||||
provider = new NativeOcctHistoryWorkerProvider({ moduleUrl: '/native/occt-history/bitbybit-occt-history.js' }); const nativeCapabilities = await provider.initialize(); runtime.configureNativeHistory(provider)
|
||||
const context = { documentId: 'chrome-native-loft', documentVersion: 1 }
|
||||
first = await runtime.importShape({ ...context, format: 'step', text: firstStep })
|
||||
second = await runtime.importShape({ ...context, format: 'step', text: secondStep })
|
||||
result = await runtime.loft({ ...context, sections: [
|
||||
{ outer: [[0, 0, 0], [2, 0, 0], [2, 2, 0], [0, 2, 0]] },
|
||||
{ outer: [[0, 0, 5], [2, 0, 5], [2, 2, 5], [0, 2, 5]] },
|
||||
], mode: 'standalone', ruled: false, closed: false })
|
||||
const records = await runtime.topologyHistory({ ...context, operationId: 'chrome-native-loft-history', operation: 'loft', inputs: [{ objectId: 'FirstSection', shape: first }, { objectId: 'SecondSection', shape: second }], ruled: false, result })
|
||||
const [quality, mass] = await Promise.all([runtime.qualityReport(result), runtime.massProperties(result)])
|
||||
const relations = { modified: records.filter((entry) => entry.relation === 'modified').length, generated: records.filter((entry) => entry.relation === 'generated').length, deleted: records.filter((entry) => entry.relation === 'deleted').length }
|
||||
const directory = await navigator.storage.getDirectory(); const markerName = 'native-loft-history-marker.json'; const marker = await directory.getFileHandle(markerName, { create: true }); const writable = await marker.createWritable(); await writable.write(JSON.stringify({ operation: 'loft', sections: 2, ruled: false })); await writable.close(); const markerPayload = JSON.parse(await (await marker.getFile()).text()); await directory.removeEntry(markerName)
|
||||
report.capabilities = capabilities; report.nativeCapabilities = nativeCapabilities; report.history = { recordCount: records.length, relations, sourceObjects: [...new Set(records.map((entry) => entry.sourceObjectId))].sort() }; report.result = { bitbybitStructuralValid: quality.structuralValid, bitbybitSolids: quality.solids, bitbybitVolume: mass.volume }; report.opfs = { markerPayload, markerRemoved: true }
|
||||
await runtime.release(result); result = undefined; await runtime.release(second); second = undefined; await runtime.release(first); first = undefined; report.afterRelease = { shapeCount: runtime.capabilities().shapeCount, kernelReferenceCount: runtime.capabilities().kernelReferenceCount }
|
||||
report.status = report.crossOriginIsolated && capabilities.status === 'ready' && nativeCapabilities.operations.includes('loft') && records.length > 0 && relations.generated > 0 && report.history.sourceObjects.includes('FirstSection') && report.history.sourceObjects.includes('SecondSection') && quality.solids === 1 && Math.abs(mass.volume - 20) < 1e-7 && markerPayload.sections === 2 && report.afterRelease.shapeCount === 0 && report.afterRelease.kernelReferenceCount === 0 ? 'pass' : 'failed'
|
||||
} catch (error) { report.status = 'failed'; report.error = error instanceof Error ? error.stack || error.message : String(error) }
|
||||
finally { if (result) await runtime?.release(result).catch(() => {}); if (second) await runtime?.release(second).catch(() => {}); if (first) await runtime?.release(first).catch(() => {}); provider?.dispose(); runtime?.dispose() }
|
||||
output.textContent = JSON.stringify(report, null, 2); await fetch('/__native-loft-history-report', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(report) }); if (report.status !== 'pass') throw new Error(report.error || 'failed')
|
||||
</script>
|
||||
31
public/chrome-native-mirrored-history-harness.html
Normal file
31
public/chrome-native-mirrored-history-harness.html
Normal file
@@ -0,0 +1,31 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>Chrome native Mirrored history</title>
|
||||
<pre id="result">running</pre>
|
||||
<script type="module">
|
||||
import { BitbybitGeometryRuntime } from '/src/facade/geometryRuntime.ts'
|
||||
import { NativeOcctHistoryWorkerProvider } from '/src/facade/nativeHistoryWorkerClient.ts'
|
||||
import { NativeOcctHistoryCoordinator } from '/src/facade/nativeHistoryProtocol.ts'
|
||||
const output = document.querySelector('#result')
|
||||
const report = { schemaVersion: 1, browserId: 'chrome', crossOriginIsolated: globalThis.crossOriginIsolated === true, status: 'running' }
|
||||
let runtime, provider, base, copy, result
|
||||
try {
|
||||
runtime = new BitbybitGeometryRuntime(); const capabilities = await runtime.initialize()
|
||||
provider = new NativeOcctHistoryWorkerProvider({ moduleUrl: '/native/occt-history/bitbybit-occt-history.js' }); const nativeCapabilities = await provider.initialize(); runtime.configureNativeHistory(provider)
|
||||
const context = { documentId: 'chrome-native-mirrored', documentVersion: 1 }
|
||||
base = await runtime.createBox({ ...context, width: 2, length: 1, height: 1, center: [0.5, 0, 0], originOnCenter: true })
|
||||
copy = await runtime.mirror({ ...context, shape: base, origin: [0, 0, 0], normal: [1, 0, 0] })
|
||||
result = await runtime.union({ ...context, shapes: [base, copy] })
|
||||
const [quality, mass, baseStep] = await Promise.all([runtime.qualityReport(result), runtime.massProperties(result), runtime.exportStep(base, 'Base.step')])
|
||||
const execution = await new NativeOcctHistoryCoordinator(() => 1).capture(provider, { ...context, operationId: 'mirrored-history', operation: 'mirrored', objectStep: baseStep.text, axisOrigin: [0, 0, 0], direction: [1, 0, 0] })
|
||||
if (execution.status !== 'completed' || !execution.response) throw new Error(`Native Mirrored history ${execution.status}.`)
|
||||
const records = execution.response.history.records
|
||||
const relations = { modified: records.filter((entry) => entry.relation === 'modified').length, generated: records.filter((entry) => entry.relation === 'generated').length, deleted: records.filter((entry) => entry.relation === 'deleted').length }
|
||||
const directory = await navigator.storage.getDirectory(); const markerName = 'native-mirrored-history-marker.json'; const marker = await directory.getFileHandle(markerName, { create: true }); const writable = await marker.createWritable(); await writable.write(JSON.stringify({ operation: 'mirrored' })); await writable.close(); const markerSuite = JSON.parse(await (await marker.getFile()).text()).operation; await directory.removeEntry(markerName)
|
||||
report.capabilities = capabilities; report.nativeCapabilities = nativeCapabilities; report.history = { recordCount: records.length, relations, sourceObjects: ['Base'] }; report.result = { bitbybitStructuralValid: quality.structuralValid, bitbybitSolids: quality.solids, bitbybitVolume: mass.volume, nativeStructuralValid: execution.response.history.summary.isValid, nativeSolids: execution.response.history.summary.solids, nativeVolume: execution.response.history.summary.volume }; report.opfs = { markerSuite, markerRemoved: true }
|
||||
await runtime.release(result); result = undefined; await runtime.release(copy); copy = undefined; await runtime.release(base); base = undefined; report.afterRelease = { shapeCount: runtime.capabilities().shapeCount, kernelReferenceCount: runtime.capabilities().kernelReferenceCount }
|
||||
report.status = report.crossOriginIsolated && nativeCapabilities.operations.includes('mirrored') && records.length > 0 && quality.solids === 1 && Math.abs(mass.volume - 3) < 1e-7 && report.result.nativeStructuralValid && report.result.nativeSolids === 1 && Math.abs(report.result.nativeVolume - 3) < 1e-7 && markerSuite === 'mirrored' && report.afterRelease.shapeCount === 0 && report.afterRelease.kernelReferenceCount === 0 ? 'pass' : 'failed'
|
||||
} catch (error) { report.status = 'failed'; report.error = error instanceof Error ? error.stack || error.message : String(error) }
|
||||
finally { if (result) await runtime?.release(result).catch(() => {}); if (copy) await runtime?.release(copy).catch(() => {}); if (base) await runtime?.release(base).catch(() => {}); provider?.dispose(); runtime?.dispose() }
|
||||
output.textContent = JSON.stringify(report, null, 2); await fetch('/__native-mirrored-history-report', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(report) }); if (report.status !== 'pass') throw new Error(report.error || 'failed')
|
||||
</script>
|
||||
36
public/chrome-native-multi-transform-history-harness.html
Normal file
36
public/chrome-native-multi-transform-history-harness.html
Normal file
@@ -0,0 +1,36 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>Chrome native MultiTransform history</title>
|
||||
<pre id="result">running</pre>
|
||||
<script type="module">
|
||||
import { BitbybitGeometryRuntime } from '/src/facade/geometryRuntime.ts'
|
||||
import { NativeOcctHistoryWorkerProvider } from '/src/facade/nativeHistoryWorkerClient.ts'
|
||||
import { NativeOcctHistoryCoordinator } from '/src/facade/nativeHistoryProtocol.ts'
|
||||
const output = document.querySelector('#result')
|
||||
const report = { schemaVersion: 1, browserId: 'chrome', crossOriginIsolated: globalThis.crossOriginIsolated === true, status: 'running' }
|
||||
let runtime, provider, base, copy, mirrorBase, mirrorCopy, result
|
||||
try {
|
||||
runtime = new BitbybitGeometryRuntime(); const capabilities = await runtime.initialize()
|
||||
provider = new NativeOcctHistoryWorkerProvider({ moduleUrl: '/native/occt-history/bitbybit-occt-history.js' }); const nativeCapabilities = await provider.initialize(); runtime.configureNativeHistory(provider)
|
||||
const context = { documentId: 'chrome-native-multi-transform', documentVersion: 1 }
|
||||
base = await runtime.createBox({ ...context, width: 2, length: 1, height: 1, center: [0.5, 0, 0], originOnCenter: true })
|
||||
copy = await runtime.applyPlacement({ ...context, shape: base, placement: { translation: [1, 0, 0], rotationAxis: [0, 0, 1], rotationAngle: 0 } })
|
||||
mirrorBase = await runtime.mirror({ ...context, shape: base, origin: [0, 0, 0], normal: [1, 0, 0] })
|
||||
mirrorCopy = await runtime.mirror({ ...context, shape: copy, origin: [0, 0, 0], normal: [1, 0, 0] })
|
||||
result = await runtime.union({ ...context, shapes: [base, copy, mirrorBase, mirrorCopy] })
|
||||
const [quality, mass, baseStep] = await Promise.all([runtime.qualityReport(result), runtime.massProperties(result), runtime.exportStep(base, 'Base.step')])
|
||||
const transforms = [{ type: 'linear', direction: [1, 0, 0] }, { type: 'mirrored', axisOrigin: [0, 0, 0], direction: [1, 0, 0] }]
|
||||
const stagedRecords = await runtime.topologyHistory({ ...context, operationId: 'multi-transform-staged-history', operation: 'multi-transform', transforms, inputs: [{ objectId: 'Base', shape: base }], stages: [{ stageId: 'MultiTransform:stage:0', operation: 'multi-transform', inputObjectIds: ['Base'], resultObjectId: 'MultiTransform', ordinal: 0 }], result })
|
||||
const stageCaptures = stagedRecords.stageCaptures || []
|
||||
const execution = await new NativeOcctHistoryCoordinator(() => 1).capture(provider, { ...context, operationId: 'multi-transform-history', operation: 'multi-transform', objectStep: baseStep.text, transforms })
|
||||
if (execution.status !== 'completed' || !execution.response) throw new Error(`Native MultiTransform history ${execution.status}.`)
|
||||
const records = execution.response.history.records
|
||||
const relations = { modified: records.filter((entry) => entry.relation === 'modified').length, generated: records.filter((entry) => entry.relation === 'generated').length, deleted: records.filter((entry) => entry.relation === 'deleted').length }
|
||||
const directory = await navigator.storage.getDirectory(); const markerName = 'native-multi-transform-history-marker.json'; const marker = await directory.getFileHandle(markerName, { create: true }); const writable = await marker.createWritable(); await writable.write(JSON.stringify({ operation: 'multi-transform', transformKinds: transforms.map((step) => step.type) })); await writable.close(); const markerPayload = JSON.parse(await (await marker.getFile()).text()); await directory.removeEntry(markerName)
|
||||
report.capabilities = capabilities; report.nativeCapabilities = nativeCapabilities; report.history = { recordCount: records.length, relations, sourceObjects: ['Base'], transformKinds: transforms.map((step) => step.type), stagedRecordCount: stagedRecords.length, stages: stageCaptures.map((stage) => ({ stageId: stage.stageId, operation: stage.operation, inputObjectIds: stage.inputObjectIds, resultObjectId: stage.resultObjectId, ordinal: stage.ordinal, topologyEntries: stage.topology.entries.length, recordCount: stage.records.length })), finalRecordLineage: { records: stagedRecords.length, sourceObjectIds: [...new Set(stagedRecords.map((entry) => entry.sourceObjectId))], sourceStageIds: [...new Set(stagedRecords.map((entry) => entry.sourceStageId))], resultStageIds: [...new Set(stagedRecords.map((entry) => entry.resultStageId))] } }; report.result = { bitbybitStructuralValid: quality.structuralValid, bitbybitSolids: quality.solids, bitbybitVolume: mass.volume, nativeStructuralValid: execution.response.history.summary.isValid, nativeSolids: execution.response.history.summary.solids, nativeVolume: execution.response.history.summary.volume }; report.opfs = { markerPayload, markerRemoved: true }
|
||||
await runtime.release(result); result = undefined; await runtime.release(mirrorCopy); mirrorCopy = undefined; await runtime.release(mirrorBase); mirrorBase = undefined; await runtime.release(copy); copy = undefined; await runtime.release(base); base = undefined; report.afterRelease = { shapeCount: runtime.capabilities().shapeCount, kernelReferenceCount: runtime.capabilities().kernelReferenceCount }
|
||||
report.status = report.crossOriginIsolated && nativeCapabilities.operations.includes('multi-transform') && records.length > 0 && relations.modified > 0 && relations.generated > 0 && stageCaptures.length === 2 && stageCaptures[0].operation === 'linear-pattern' && stageCaptures[1].operation === 'mirrored' && stageCaptures[0].inputObjectIds[0] === 'Base' && stageCaptures[1].inputObjectIds[0] === stageCaptures[0].resultObjectId && stageCaptures[1].resultObjectId === 'MultiTransform' && stageCaptures.every((stage) => stage.topology.entries.length > 0 && stage.records.length > 0) && stagedRecords.every((entry) => entry.sourceStageId === stageCaptures[0].stageId && entry.resultStageId === stageCaptures[1].stageId) && quality.solids === 1 && Math.abs(mass.volume - 5) < 1e-7 && report.result.nativeStructuralValid && report.result.nativeSolids === 1 && Math.abs(report.result.nativeVolume - 5) < 1e-7 && markerPayload.transformKinds.join(',') === 'linear,mirrored' && report.afterRelease.shapeCount === 0 && report.afterRelease.kernelReferenceCount === 0 ? 'pass' : 'failed'
|
||||
} catch (error) { report.status = 'failed'; report.error = error instanceof Error ? error.stack || error.message : String(error) }
|
||||
finally { if (result) await runtime?.release(result).catch(() => {}); if (mirrorCopy) await runtime?.release(mirrorCopy).catch(() => {}); if (mirrorBase) await runtime?.release(mirrorBase).catch(() => {}); if (copy) await runtime?.release(copy).catch(() => {}); if (base) await runtime?.release(base).catch(() => {}); provider?.dispose(); runtime?.dispose() }
|
||||
output.textContent = JSON.stringify(report, null, 2); await fetch('/__native-multi-transform-history-report', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(report) }); if (report.status !== 'pass') throw new Error(report.error || 'failed')
|
||||
</script>
|
||||
38
public/chrome-native-pad-history-harness.html
Normal file
38
public/chrome-native-pad-history-harness.html
Normal file
@@ -0,0 +1,38 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>Chrome native Pad history harness</title>
|
||||
<pre id="status">loading</pre>
|
||||
<script type="module">
|
||||
import { NativeOcctHistoryCoordinator } from '/src/facade/nativeHistoryProtocol.ts'
|
||||
import { NativeOcctHistoryWorkerProvider } from '/src/facade/nativeHistoryWorkerClient.ts'
|
||||
|
||||
const report = { schemaVersion: 1, browserId: 'chrome', executionContext: 'dedicated-worker', crossOriginIsolated, status: 'failed' }
|
||||
let provider
|
||||
try {
|
||||
const profileStep = await fetch('/native-pad-profile.step').then((response) => response.ok ? response.text() : Promise.reject(new Error(`Pad profile fixture failed: ${response.status}`)))
|
||||
provider = new NativeOcctHistoryWorkerProvider()
|
||||
const capabilities = await provider.initialize()
|
||||
const execution = await new NativeOcctHistoryCoordinator(() => 1).capture(provider, {
|
||||
documentId: 'chrome-native-pad', documentVersion: 1, operationId: 'pad-history', operation: 'pad', objectStep: profileStep, direction: [0, 0, 5],
|
||||
})
|
||||
const history = execution.response?.history
|
||||
const records = history?.records || []
|
||||
const crossKind = records.filter((record) => record.resultKind && record.resultKind !== record.kind)
|
||||
const summary = history?.summary
|
||||
const validRelations = records.every((record) => ['modified', 'generated', 'deleted'].includes(record.relation) && Number.isSafeInteger(record.sourceIndex) && record.sourceIndex >= 0)
|
||||
const profileKinds = new Set(records.map((record) => record.kind))
|
||||
const shapeSummary = summary && typeof summary === 'object' ? summary : null
|
||||
Object.assign(report, {
|
||||
status: capabilities.availability === 'available' && capabilities.operations.includes('pad') && execution.status === 'completed' && records.length > 0 && validRelations && crossKind.length > 0 && profileKinds.has('vertex') && profileKinds.has('edge') && profileKinds.has('face') && shapeSummary?.isValid === true && shapeSummary?.solids === 1 && Math.abs(shapeSummary.volume - 30) <= 1e-7 ? 'pass' : 'failed',
|
||||
nativeCapabilities: capabilities,
|
||||
execution: { status: execution.status, recordCount: records.length, crossKindRecords: crossKind.length, relations: { modified: records.filter((record) => record.relation === 'modified').length, generated: records.filter((record) => record.relation === 'generated').length, deleted: records.filter((record) => record.relation === 'deleted').length }, profileKinds: [...profileKinds], summary: shapeSummary },
|
||||
})
|
||||
} catch (error) {
|
||||
report.error = error instanceof Error ? `${error.name}: ${error.message}` : String(error)
|
||||
} finally {
|
||||
provider?.dispose()
|
||||
report.workerDisposed = true
|
||||
}
|
||||
document.querySelector('#status').textContent = JSON.stringify(report, null, 2)
|
||||
await fetch('/__native-pad-history-report', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(report) })
|
||||
</script>
|
||||
33
public/chrome-native-pipe-history-harness.html
Normal file
33
public/chrome-native-pipe-history-harness.html
Normal file
@@ -0,0 +1,33 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>Chrome native Pipe history</title>
|
||||
<pre id="result">running</pre>
|
||||
<script type="module">
|
||||
import { BitbybitGeometryRuntime } from '/src/facade/geometryRuntime.ts'
|
||||
import { NativeOcctHistoryWorkerProvider } from '/src/facade/nativeHistoryWorkerClient.ts'
|
||||
|
||||
const output = document.querySelector('#result')
|
||||
const report = { schemaVersion: 1, browserId: 'chrome', crossOriginIsolated: globalThis.crossOriginIsolated === true, status: 'running' }
|
||||
let runtime, provider, profile, spine, result
|
||||
try {
|
||||
const [profileStep, spineStep] = await Promise.all([
|
||||
fetch('/native-pipe-profile.step').then((response) => response.ok ? response.text() : Promise.reject(new Error(`Pipe profile fixture failed: ${response.status}`))),
|
||||
fetch('/native-pipe-spine.step').then((response) => response.ok ? response.text() : Promise.reject(new Error(`Pipe spine fixture failed: ${response.status}`))),
|
||||
])
|
||||
runtime = new BitbybitGeometryRuntime(); const capabilities = await runtime.initialize()
|
||||
provider = new NativeOcctHistoryWorkerProvider({ moduleUrl: '/native/occt-history/bitbybit-occt-history.js' }); const nativeCapabilities = await provider.initialize(); runtime.configureNativeHistory(provider)
|
||||
const context = { documentId: 'chrome-native-pipe', documentVersion: 1 }
|
||||
profile = await runtime.importShape({ ...context, format: 'step', text: profileStep })
|
||||
spine = await runtime.importShape({ ...context, format: 'step', text: spineStep })
|
||||
result = await runtime.pipe({ ...context, profile: { outer: [[0, 0, 0], [2, 0, 0], [2, 2, 0], [0, 2, 0]] }, path: [[0, 0, 0], [0, 0, 5]], mode: 'standalone' })
|
||||
const records = await runtime.topologyHistory({ ...context, operationId: 'chrome-native-pipe-history', operation: 'pipe', inputs: [{ objectId: 'Profile', shape: profile }, { objectId: 'Spine', shape: spine }], result })
|
||||
const [quality, mass] = await Promise.all([runtime.qualityReport(result), runtime.massProperties(result)])
|
||||
const relations = { modified: records.filter((entry) => entry.relation === 'modified').length, generated: records.filter((entry) => entry.relation === 'generated').length, deleted: records.filter((entry) => entry.relation === 'deleted').length }
|
||||
const directory = await navigator.storage.getDirectory(); const markerName = 'native-pipe-history-marker.json'; const marker = await directory.getFileHandle(markerName, { create: true }); const writable = await marker.createWritable(); await writable.write(JSON.stringify({ operation: 'pipe', profiles: 1, spineEdges: 1 })); await writable.close(); const markerPayload = JSON.parse(await (await marker.getFile()).text()); await directory.removeEntry(markerName)
|
||||
report.capabilities = capabilities; report.nativeCapabilities = nativeCapabilities; report.history = { recordCount: records.length, relations, sourceObjects: [...new Set(records.map((entry) => entry.sourceObjectId))].sort() }; report.result = { bitbybitStructuralValid: quality.structuralValid, bitbybitSolids: quality.solids, bitbybitVolume: mass.volume }; report.opfs = { markerPayload, markerRemoved: true }
|
||||
await runtime.release(result); result = undefined; await runtime.release(spine); spine = undefined; await runtime.release(profile); profile = undefined; report.afterRelease = { shapeCount: runtime.capabilities().shapeCount, kernelReferenceCount: runtime.capabilities().kernelReferenceCount }
|
||||
report.status = report.crossOriginIsolated && capabilities.status === 'ready' && nativeCapabilities.operations.includes('pipe') && records.length > 0 && relations.modified > 0 && relations.generated > 0 && report.history.sourceObjects.includes('Profile') && report.history.sourceObjects.includes('Spine') && quality.solids === 1 && Math.abs(mass.volume - 20) < 1e-7 && markerPayload.spineEdges === 1 && report.afterRelease.shapeCount === 0 && report.afterRelease.kernelReferenceCount === 0 ? 'pass' : 'failed'
|
||||
} catch (error) { report.status = 'failed'; report.error = error instanceof Error ? error.stack || error.message : String(error) }
|
||||
finally { if (result) await runtime?.release(result).catch(() => {}); if (spine) await runtime?.release(spine).catch(() => {}); if (profile) await runtime?.release(profile).catch(() => {}); provider?.dispose(); runtime?.dispose() }
|
||||
output.textContent = JSON.stringify(report, null, 2); await fetch('/__native-pipe-history-report', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(report) }); if (report.status !== 'pass') throw new Error(report.error || 'failed')
|
||||
</script>
|
||||
70
public/chrome-native-pocket-history-harness.html
Normal file
70
public/chrome-native-pocket-history-harness.html
Normal file
@@ -0,0 +1,70 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>Chrome native Pocket history harness</title>
|
||||
<pre id="result">running</pre>
|
||||
<script type="module">
|
||||
import { BitbybitGeometryRuntime } from '/src/facade/geometryRuntime.ts'
|
||||
import { NativeOcctHistoryWorkerProvider } from '/src/facade/nativeHistoryWorkerClient.ts'
|
||||
|
||||
const output = document.querySelector('#result')
|
||||
const report = { schemaVersion: 1, browserId: 'chrome', crossOriginIsolated: globalThis.crossOriginIsolated === true, status: 'running' }
|
||||
let runtime
|
||||
let provider
|
||||
try {
|
||||
const [baseStep, profileStep] = await Promise.all([
|
||||
fetch('/native-pocket-base.step').then((response) => response.ok ? response.text() : Promise.reject(new Error(`Pocket base fixture failed: ${response.status}`))),
|
||||
fetch('/native-pocket-profile.step').then((response) => response.ok ? response.text() : Promise.reject(new Error(`Pocket profile fixture failed: ${response.status}`))),
|
||||
])
|
||||
runtime = new BitbybitGeometryRuntime()
|
||||
const capabilities = await runtime.initialize()
|
||||
provider = new NativeOcctHistoryWorkerProvider({ moduleUrl: '/native/occt-history/bitbybit-occt-history.js' })
|
||||
const nativeCapabilities = await provider.initialize()
|
||||
runtime.configureNativeHistory(provider)
|
||||
const context = { documentId: 'chrome-native-pocket', documentVersion: 1 }
|
||||
const base = await runtime.importShape({ ...context, format: 'step', text: baseStep })
|
||||
const profile = await runtime.importShape({ ...context, format: 'step', text: profileStep })
|
||||
const result = await runtime.pocket({ ...context, base, profile: { outer: [[0, 0, 0], [2, 0, 0], [2, 3, 0], [0, 3, 0]] }, length: 5, direction: [0, 0, 1], reversed: false, symmetricToPlane: false })
|
||||
const markerDirectory = await navigator.storage.getDirectory()
|
||||
const marker = await markerDirectory.getFileHandle('native-pocket-history-chrome-marker.json', { create: true })
|
||||
const writable = await marker.createWritable()
|
||||
await writable.write(JSON.stringify({ operation: 'pocket', provider: nativeCapabilities.providerId }))
|
||||
await writable.close()
|
||||
const records = await runtime.topologyHistory({ ...context, operationId: 'chrome-native-pocket-history', operation: 'pocket', inputs: [{ objectId: 'Base', shape: base }, { objectId: 'Profile', shape: profile }], direction: [0, 0, 5], result })
|
||||
const twoSidedBase = await runtime.createBox({ ...context, width: 10, length: 8, height: 10, center: [0, 0, 0], originOnCenter: true })
|
||||
const firstSideTool = await runtime.pad({ ...context, profile: { outer: [[0, 0, 0], [2, 0, 0], [2, 3, 0], [0, 3, 0]] }, length: 4, direction: [0, 0, 1] })
|
||||
const secondSideTool = await runtime.pad({ ...context, profile: { outer: [[0, 0, 0], [2, 0, 0], [2, 3, 0], [0, 3, 0]] }, length: 4, direction: [0, 0, 1], reversed: true })
|
||||
const fusedTool = await runtime.union({ ...context, shapes: [firstSideTool, secondSideTool] })
|
||||
const twoSidedResult = await runtime.cut({ ...context, base: twoSidedBase, tools: [fusedTool] })
|
||||
const twoSidedRecords = await runtime.topologyHistory({ ...context, operationId: 'chrome-native-pocket-two-sided-history', operation: 'pocket', inputs: [{ objectId: 'BaseTwoSided', shape: twoSidedBase }, { objectId: 'Profile', shape: profile }], stages: [{ stageId: 'Pocket:stage:0', operation: 'pocket', inputObjectIds: ['BaseTwoSided', 'Profile'], resultObjectId: 'PocketTwoSided', ordinal: 0 }], featureSides: [{ direction: [0, 0, 4] }, { direction: [0, 0, -4] }], direction: [0, 0, 4], result: twoSidedResult })
|
||||
const twoSidedStages = twoSidedRecords.stageCaptures || []
|
||||
const twoSidedQuality = await runtime.qualityReport(twoSidedResult)
|
||||
const stageCaptures = records.stageCaptures || []
|
||||
const modified = records.filter((record) => record.relation === 'modified').length
|
||||
const generated = records.filter((record) => record.relation === 'generated').length
|
||||
const deleted = records.filter((record) => record.relation === 'deleted').length
|
||||
report.capabilities = capabilities
|
||||
report.nativeCapabilities = nativeCapabilities
|
||||
report.history = { recordCount: records.length, sourceObjects: [...new Set(stageCaptures.flatMap((stage) => stage.records.map((record) => record.sourceObjectId)))].sort(), relations: { modified, generated, deleted }, stages: stageCaptures.map((stage) => ({ stageId: stage.stageId, operation: stage.operation, inputObjectIds: stage.inputObjectIds, resultObjectId: stage.resultObjectId, ordinal: stage.ordinal, topologyEntries: stage.topology.entries.length, recordCount: stage.records.length })) }
|
||||
report.twoSidedHistory = { recordCount: twoSidedRecords.length, structuralValid: twoSidedQuality.structuralValid, solids: twoSidedQuality.solids, sourceObjects: [...new Set(twoSidedStages.flatMap((stage) => stage.records.map((record) => record.sourceObjectId)))].sort(), stages: twoSidedStages.map((stage) => ({ stageId: stage.stageId, operation: stage.operation, inputObjectIds: stage.inputObjectIds, resultObjectId: stage.resultObjectId, ordinal: stage.ordinal, topologyEntries: stage.topology.entries.length, recordCount: stage.records.length })) }
|
||||
await runtime.release(twoSidedResult)
|
||||
await runtime.release(fusedTool)
|
||||
await runtime.release(secondSideTool)
|
||||
await runtime.release(firstSideTool)
|
||||
await runtime.release(twoSidedBase)
|
||||
await runtime.release(result)
|
||||
await runtime.release(profile)
|
||||
await runtime.release(base)
|
||||
report.afterRelease = { shapeCount: runtime.capabilities().shapeCount, kernelReferenceCount: runtime.capabilities().kernelReferenceCount }
|
||||
await markerDirectory.removeEntry('native-pocket-history-chrome-marker.json').catch(() => {})
|
||||
report.status = report.crossOriginIsolated && capabilities.status === 'ready' && nativeCapabilities.availability === 'available' && nativeCapabilities.operations.includes('pocket') && nativeCapabilities.operations.includes('pad') && nativeCapabilities.operations.includes('fuse') && nativeCapabilities.operations.includes('cut') && records.length > 0 && modified > 0 && generated + deleted > 0 && stageCaptures.length === 2 && stageCaptures[0].operation === 'pad' && stageCaptures[0].inputObjectIds.join(',') === 'Profile' && stageCaptures[0].records.length > 0 && stageCaptures[0].topology.entries.length > 0 && stageCaptures[1].operation === 'cut' && stageCaptures[1].inputObjectIds.join(',') === `Base,${stageCaptures[0].resultObjectId}` && stageCaptures[1].records.length === records.length && stageCaptures[1].topology.entries.length > 0 && report.history.sourceObjects.includes('Base') && report.history.sourceObjects.includes('Profile') && report.history.sourceObjects.includes(stageCaptures[0].resultObjectId) && twoSidedRecords.length > 0 && twoSidedQuality.structuralValid && twoSidedQuality.solids === 1 && twoSidedStages.length === 4 && twoSidedStages[0].operation === 'pad' && twoSidedStages[0].inputObjectIds.join(',') === 'Profile' && twoSidedStages[0].topology.entries.length > 0 && twoSidedStages[1].operation === 'pad' && twoSidedStages[1].inputObjectIds.join(',') === 'Profile' && twoSidedStages[1].topology.entries.length > 0 && twoSidedStages[2].operation === 'fuse' && twoSidedStages[2].inputObjectIds.join(',') === `${twoSidedStages[0].resultObjectId},${twoSidedStages[1].resultObjectId}` && twoSidedStages[2].topology.entries.length > 0 && twoSidedStages[3].operation === 'cut' && twoSidedStages[3].inputObjectIds.join(',') === `BaseTwoSided,${twoSidedStages[2].resultObjectId}` && twoSidedStages[3].resultObjectId === 'PocketTwoSided' && twoSidedStages[3].topology.entries.length > 0 && report.twoSidedHistory.sourceObjects.includes('BaseTwoSided') && report.twoSidedHistory.sourceObjects.includes('Profile') && report.afterRelease.shapeCount === 0 && report.afterRelease.kernelReferenceCount === 0 ? 'pass' : 'failed'
|
||||
} catch (error) {
|
||||
report.status = 'failed'
|
||||
report.error = error instanceof Error ? error.message : String(error)
|
||||
} finally {
|
||||
provider?.dispose()
|
||||
runtime?.dispose()
|
||||
}
|
||||
output.textContent = JSON.stringify(report, null, 2)
|
||||
await fetch('/__native-pocket-history-report', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(report) })
|
||||
if (report.status !== 'pass') throw new Error(report.error || 'Chrome native Pocket history harness failed.')
|
||||
</script>
|
||||
31
public/chrome-native-polar-pattern-history-harness.html
Normal file
31
public/chrome-native-polar-pattern-history-harness.html
Normal file
@@ -0,0 +1,31 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>Chrome native PolarPattern history</title>
|
||||
<pre id="result">running</pre>
|
||||
<script type="module">
|
||||
import { BitbybitGeometryRuntime } from '/src/facade/geometryRuntime.ts'
|
||||
import { NativeOcctHistoryWorkerProvider } from '/src/facade/nativeHistoryWorkerClient.ts'
|
||||
import { NativeOcctHistoryCoordinator } from '/src/facade/nativeHistoryProtocol.ts'
|
||||
const output = document.querySelector('#result')
|
||||
const report = { schemaVersion: 1, browserId: 'chrome', crossOriginIsolated: globalThis.crossOriginIsolated === true, status: 'running' }
|
||||
let runtime, provider, base, copy, result
|
||||
try {
|
||||
runtime = new BitbybitGeometryRuntime(); const capabilities = await runtime.initialize()
|
||||
provider = new NativeOcctHistoryWorkerProvider({ moduleUrl: '/native/occt-history/bitbybit-occt-history.js' }); const nativeCapabilities = await provider.initialize(); runtime.configureNativeHistory(provider)
|
||||
const context = { documentId: 'chrome-native-polar-pattern', documentVersion: 1 }
|
||||
base = await runtime.createBox({ ...context, width: 2, length: 1, height: 1, center: [0, 0, 0], originOnCenter: true })
|
||||
copy = await runtime.applyPlacement({ ...context, shape: base, placement: { translation: [0, 0, 0], rotationAxis: [0, 0, 1], rotationAngle: 90 } })
|
||||
result = await runtime.union({ ...context, shapes: [base, copy] })
|
||||
const [quality, mass, baseStep] = await Promise.all([runtime.qualityReport(result), runtime.massProperties(result), runtime.exportStep(base, 'Base.step')])
|
||||
const execution = await new NativeOcctHistoryCoordinator(() => 1).capture(provider, { ...context, operationId: 'polar-pattern-history', operation: 'polar-pattern', objectStep: baseStep.text, axisOrigin: [0, 0, 0], direction: [0, 0, 1], angle: 90 })
|
||||
if (execution.status !== 'completed' || !execution.response) throw new Error(`Native PolarPattern history ${execution.status}.`)
|
||||
const records = execution.response.history.records
|
||||
const relations = { modified: records.filter((entry) => entry.relation === 'modified').length, generated: records.filter((entry) => entry.relation === 'generated').length, deleted: records.filter((entry) => entry.relation === 'deleted').length }
|
||||
const directory = await navigator.storage.getDirectory(); const markerName = 'native-polar-pattern-history-marker.json'; const marker = await directory.getFileHandle(markerName, { create: true }); const writable = await marker.createWritable(); await writable.write(JSON.stringify({ operation: 'polar-pattern' })); await writable.close(); const markerSuite = JSON.parse(await (await marker.getFile()).text()).operation; await directory.removeEntry(markerName)
|
||||
report.capabilities = capabilities; report.nativeCapabilities = nativeCapabilities; report.history = { recordCount: records.length, relations, sourceObjects: ['Base'] }; report.result = { bitbybitStructuralValid: quality.structuralValid, bitbybitSolids: quality.solids, bitbybitVolume: mass.volume, nativeStructuralValid: execution.response.history.summary.isValid, nativeSolids: execution.response.history.summary.solids, nativeVolume: execution.response.history.summary.volume }; report.opfs = { markerSuite, markerRemoved: true }
|
||||
await runtime.release(result); result = undefined; await runtime.release(copy); copy = undefined; await runtime.release(base); base = undefined; report.afterRelease = { shapeCount: runtime.capabilities().shapeCount, kernelReferenceCount: runtime.capabilities().kernelReferenceCount }
|
||||
report.status = report.crossOriginIsolated && nativeCapabilities.operations.includes('polar-pattern') && records.length > 0 && quality.solids === 1 && Math.abs(mass.volume - 3) < 1e-7 && report.result.nativeStructuralValid && report.result.nativeSolids === 1 && Math.abs(report.result.nativeVolume - 3) < 1e-7 && markerSuite === 'polar-pattern' && report.afterRelease.shapeCount === 0 && report.afterRelease.kernelReferenceCount === 0 ? 'pass' : 'failed'
|
||||
} catch (error) { report.status = 'failed'; report.error = error instanceof Error ? error.stack || error.message : String(error) }
|
||||
finally { if (result) await runtime?.release(result).catch(() => {}); if (copy) await runtime?.release(copy).catch(() => {}); if (base) await runtime?.release(base).catch(() => {}); provider?.dispose(); runtime?.dispose() }
|
||||
output.textContent = JSON.stringify(report, null, 2); await fetch('/__native-polar-pattern-history-report', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(report) }); if (report.status !== 'pass') throw new Error(report.error || 'failed')
|
||||
</script>
|
||||
61
public/chrome-native-revolution-history-harness.html
Normal file
61
public/chrome-native-revolution-history-harness.html
Normal file
@@ -0,0 +1,61 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>Chrome native Revolution history harness</title>
|
||||
<pre id="result">running</pre>
|
||||
<script type="module">
|
||||
import { BitbybitGeometryRuntime } from '/src/facade/geometryRuntime.ts'
|
||||
import { NativeOcctHistoryWorkerProvider } from '/src/facade/nativeHistoryWorkerClient.ts'
|
||||
|
||||
const output = document.querySelector('#result')
|
||||
const report = { schemaVersion: 1, browserId: 'chrome', crossOriginIsolated: globalThis.crossOriginIsolated === true, status: 'running' }
|
||||
let runtime
|
||||
let provider
|
||||
try {
|
||||
const profileStep = await fetch('/native-revolution-profile.step').then((response) => response.ok ? response.text() : Promise.reject(new Error(`Revolution profile fixture failed: ${response.status}`)))
|
||||
runtime = new BitbybitGeometryRuntime()
|
||||
const capabilities = await runtime.initialize()
|
||||
provider = new NativeOcctHistoryWorkerProvider({ moduleUrl: '/native/occt-history/bitbybit-occt-history.js' })
|
||||
const nativeCapabilities = await provider.initialize()
|
||||
runtime.configureNativeHistory(provider)
|
||||
const context = { documentId: 'chrome-native-revolution', documentVersion: 1 }
|
||||
const profile = await runtime.importShape({ ...context, format: 'step', text: profileStep })
|
||||
const planarProfile = { outer: [[0, 0, 0], [2, 0, 0], [2, 3, 0], [0, 3, 0]] }
|
||||
const result = await runtime.revolution({ ...context, profile: planarProfile, axisOrigin: [-1, 0, 0], axisDirection: [0, 1, 0], angle: 360 })
|
||||
const [quality, mass] = await Promise.all([runtime.qualityReport(result), runtime.massProperties(result)])
|
||||
const markerDirectory = await navigator.storage.getDirectory()
|
||||
const marker = await markerDirectory.getFileHandle('native-revolution-history-chrome-marker.json', { create: true })
|
||||
const writable = await marker.createWritable()
|
||||
await writable.write(JSON.stringify({ operation: 'revolution', provider: nativeCapabilities.providerId }))
|
||||
await writable.close()
|
||||
const records = await runtime.topologyHistory({ ...context, operationId: 'chrome-native-revolution-history', operation: 'revolution', inputs: [{ objectId: 'Profile', shape: profile }], axisOrigin: [-1, 0, 0], direction: [0, 1, 0], angle: 360, result })
|
||||
const twoSidedResult = await runtime.revolution({ ...context, profile: planarProfile, axisOrigin: [-1, 0, 0], axisDirection: [0, 1, 0], angle: 180, profileRotationAngle: -60 })
|
||||
const twoSidedRecords = await runtime.topologyHistory({ ...context, operationId: 'chrome-native-revolution-two-sided-history', operation: 'revolution', inputs: [{ objectId: 'Profile', shape: profile }], stages: [{ stageId: 'Revolution:stage:0', operation: 'revolution', inputObjectIds: ['Profile'], resultObjectId: 'RevolutionTwoSided', ordinal: 0 }], featureSides: [{ direction: [0, 1, 0], angle: 120 }, { direction: [0, -1, 0], angle: 60 }], axisOrigin: [-1, 0, 0], direction: [0, 1, 0], angle: 120, result: twoSidedResult })
|
||||
const twoSidedStages = twoSidedRecords.stageCaptures || []
|
||||
const [twoSidedQuality, twoSidedMass] = await Promise.all([runtime.qualityReport(twoSidedResult), runtime.massProperties(twoSidedResult)])
|
||||
const twoSidedUnexplainedErrors = (twoSidedQuality.structuralIssues || []).filter((issue) => issue.severity === 'error' && issue.description !== 'CoEdgeDef has no Curve2D representation').length
|
||||
const modified = records.filter((record) => record.relation === 'modified').length
|
||||
const generated = records.filter((record) => record.relation === 'generated').length
|
||||
const deleted = records.filter((record) => record.relation === 'deleted').length
|
||||
const crossKind = records.filter((record) => record.resultKind && record.resultKind !== record.sourceKind).length
|
||||
report.capabilities = capabilities
|
||||
report.nativeCapabilities = nativeCapabilities
|
||||
report.history = { recordCount: records.length, crossKindRecords: crossKind, relations: { modified, generated, deleted } }
|
||||
report.twoSidedHistory = { recordCount: twoSidedRecords.length, structuralValid: twoSidedQuality.nativeKernelValid === true, graphStructuralValid: twoSidedQuality.structuralValid, graphStructuralErrors: twoSidedQuality.structuralErrors, graphStructuralWarnings: twoSidedQuality.structuralWarnings, graphStructuralIssues: twoSidedQuality.structuralIssues, unexplainedStructuralErrors: twoSidedUnexplainedErrors, solids: twoSidedQuality.solids, volume: twoSidedMass.volume, stages: twoSidedStages.map((stage) => ({ stageId: stage.stageId, operation: stage.operation, inputObjectIds: stage.inputObjectIds, resultObjectId: stage.resultObjectId, ordinal: stage.ordinal, topologyEntries: stage.topology.entries.length, recordCount: stage.records.length })) }
|
||||
report.result = { structuralValid: quality.structuralValid, solids: quality.solids, volume: mass.volume }
|
||||
await runtime.release(twoSidedResult)
|
||||
await runtime.release(result)
|
||||
await runtime.release(profile)
|
||||
report.afterRelease = { shapeCount: runtime.capabilities().shapeCount, kernelReferenceCount: runtime.capabilities().kernelReferenceCount }
|
||||
await markerDirectory.removeEntry('native-revolution-history-chrome-marker.json').catch(() => {})
|
||||
report.status = report.crossOriginIsolated && capabilities.status === 'ready' && nativeCapabilities.availability === 'available' && nativeCapabilities.operations.includes('revolution') && nativeCapabilities.operations.includes('rotate') && records.length > 0 && modified > 0 && generated > 0 && crossKind > 0 && quality.structuralValid && quality.solids === 1 && Math.abs(mass.volume - 24 * Math.PI) <= 1e-5 && twoSidedRecords.length > 0 && twoSidedQuality.nativeKernelValid === true && twoSidedUnexplainedErrors === 0 && twoSidedQuality.solids === 1 && twoSidedStages.length === 2 && twoSidedStages[0].operation === 'rotate' && twoSidedStages[0].inputObjectIds.join(',') === 'Profile' && twoSidedStages[0].topology.entries.length > 0 && twoSidedStages[1].operation === 'revolution' && twoSidedStages[1].inputObjectIds.join(',') === `${twoSidedStages[0].resultObjectId}` && twoSidedStages[1].resultObjectId === 'RevolutionTwoSided' && twoSidedStages[1].topology.entries.length > 0 && Math.abs(twoSidedMass.volume - 12 * Math.PI) <= 1e-5 && report.afterRelease.shapeCount === 0 && report.afterRelease.kernelReferenceCount === 0 ? 'pass' : 'failed'
|
||||
} catch (error) {
|
||||
report.status = 'failed'
|
||||
report.error = error instanceof Error ? error.message : String(error)
|
||||
} finally {
|
||||
provider?.dispose()
|
||||
runtime?.dispose()
|
||||
}
|
||||
output.textContent = JSON.stringify(report, null, 2)
|
||||
await fetch('/__native-revolution-history-report', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(report) })
|
||||
if (report.status !== 'pass') throw new Error(report.error || 'Chrome native Revolution history harness failed.')
|
||||
</script>
|
||||
22
public/chrome-native-thickness-history-harness.html
Normal file
22
public/chrome-native-thickness-history-harness.html
Normal file
@@ -0,0 +1,22 @@
|
||||
<!doctype html><meta charset="utf-8"><title>Chrome native Thickness history harness</title><pre id="result">running</pre>
|
||||
<script type="module">
|
||||
import { BitbybitGeometryRuntime } from '/src/facade/geometryRuntime.ts'
|
||||
import { NativeOcctHistoryWorkerProvider } from '/src/facade/nativeHistoryWorkerClient.ts'
|
||||
import { NativeOcctHistoryCoordinator } from '/src/facade/nativeHistoryProtocol.ts'
|
||||
const output = document.querySelector('#result'); const report = { schemaVersion: 1, browserId: 'chrome', executionContext: 'geometry-and-history-workers', crossOriginIsolated: globalThis.crossOriginIsolated === true, status: 'running' }
|
||||
let runtime, provider, base, result, stage = 'init'
|
||||
try {
|
||||
stage = 'initialize'; runtime = new BitbybitGeometryRuntime(); const capabilities = await runtime.initialize(); provider = new NativeOcctHistoryWorkerProvider({ moduleUrl: '/native/occt-history/bitbybit-occt-history.js' }); const nativeCapabilities = await provider.initialize(); runtime.configureNativeHistory(provider)
|
||||
const context = { documentId: 'chrome-native-thickness', documentVersion: 1 }
|
||||
stage = 'create-box'; base = await runtime.createBox({ ...context, width: 6, length: 6, height: 6 })
|
||||
stage = 'bitbybit-thickness'; result = await runtime.thickness({ ...context, base, offset: -0.4, removeFaceIndexes: [1], joinType: 'Arc' })
|
||||
stage = 'quality'; const [quality, mass] = await Promise.all([runtime.qualityReport(result), runtime.massProperties(result)])
|
||||
const directory = await navigator.storage.getDirectory(); const markerName = 'native-thickness-history-chrome-marker.json'; const marker = await directory.getFileHandle(markerName, { create: true }); const writable = await marker.createWritable(); await writable.write(JSON.stringify({ operation: 'thickness', provider: nativeCapabilities.providerId })); await writable.close(); const markerText = await (await marker.getFile()).text()
|
||||
stage = 'native-history'; const baseStep = await runtime.exportStep(base, 'Base.step'); const execution = await new NativeOcctHistoryCoordinator(() => 1).capture(provider, { ...context, operationId: 'chrome-native-thickness-history', operation: 'thickness', objectStep: baseStep.text, offset: -0.4, joinType: 'Arc', faceIndex: 1 }); if (execution.status !== 'completed' || !execution.response) throw new Error(`Native Thickness history ${execution.status}.`); const records = execution.response.history.records
|
||||
const relations = { modified: records.filter((record) => record.relation === 'modified').length, generated: records.filter((record) => record.relation === 'generated').length, deleted: records.filter((record) => record.relation === 'deleted').length }
|
||||
report.capabilities = capabilities; report.nativeCapabilities = nativeCapabilities; report.history = { recordCount: records.length, relations, sourceObjects: [...new Set(records.map((record) => record.source === 'object' ? 'Base' : 'Tool'))] }; report.result = { bitbybitStructuralValid: quality.structuralValid, bitbybitSolids: quality.solids, bitbybitVolume: mass.volume, nativeStructuralValid: execution.response.history.summary.isValid, nativeSolids: execution.response.history.summary.solids, nativeVolume: execution.response.history.summary.volume }; report.opfs = { markerCreated: true, markerSuite: JSON.parse(markerText).operation, markerRemoved: false }
|
||||
await directory.removeEntry(markerName); report.opfs.markerRemoved = true; await runtime.release(result); result = undefined; await runtime.release(base); base = undefined; report.afterRelease = { shapeCount: runtime.capabilities().shapeCount, kernelReferenceCount: runtime.capabilities().kernelReferenceCount }
|
||||
report.status = report.crossOriginIsolated && capabilities.status === 'ready' && nativeCapabilities.operations.includes('thickness') && records.length > 0 && relations.modified > 0 && quality.solids === 1 && mass.volume > 0 && report.result.nativeStructuralValid && report.result.nativeSolids === 1 && report.result.nativeVolume > 0 && report.opfs.markerRemoved && report.afterRelease.shapeCount === 0 && report.afterRelease.kernelReferenceCount === 0 ? 'pass' : 'failed'
|
||||
} catch (error) { report.status = 'failed'; report.error = `${stage}: ${error instanceof Error ? `${error.name}: ${error.message}` : String(error)}` } finally { if (result) await runtime?.release(result).catch(() => {}); if (base) await runtime?.release(base).catch(() => {}); provider?.dispose(); runtime?.dispose() }
|
||||
output.textContent = JSON.stringify(report, null, 2); await fetch('/__native-thickness-history-report', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(report) }); if (report.status !== 'pass') throw new Error(report.error || 'Chrome native Thickness history harness failed.')
|
||||
</script>
|
||||
1
public/chrome-offline-harness.html
Normal file
1
public/chrome-offline-harness.html
Normal file
@@ -0,0 +1 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Chrome Offline Harness</title></head><body><pre id="result">running</pre><script type="module" src="/src/chromeOfflineHarness.ts"></script></body></html>
|
||||
101
public/chrome-opfs-migration-harness.html
Normal file
101
public/chrome-opfs-migration-harness.html
Normal file
@@ -0,0 +1,101 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>Chrome OPFS migration harness</title>
|
||||
<pre id="result">running</pre>
|
||||
<script type="module">
|
||||
import { createSqliteProjectPersistence } from '/src/facade/projectStore.ts'
|
||||
|
||||
const output = document.querySelector('#result')
|
||||
const report = { schemaVersion: 1, browserId: 'chrome', crossOriginIsolated: globalThis.crossOriginIsolated === true, status: 'running' }
|
||||
const documentId = `chrome-rel02-${crypto.randomUUID()}`
|
||||
const resourceBytes = new TextEncoder().encode('legacy-opfs-resource')
|
||||
const resourceHash = [...new Uint8Array(await crypto.subtle.digest('SHA-256', resourceBytes))].map((value) => value.toString(16).padStart(2, '0')).join('')
|
||||
const databasePath = `/bitbybit-rel02-${crypto.randomUUID()}.sqlite3`
|
||||
const removeDatabaseFiles = async () => {
|
||||
const root = await navigator.storage.getDirectory()
|
||||
const prefix = databasePath.slice(1)
|
||||
const removed = []
|
||||
for await (const [name] of root.entries()) {
|
||||
if (name !== prefix && !name.startsWith(prefix)) continue
|
||||
try { await root.removeEntry(name); removed.push(name) } catch {}
|
||||
}
|
||||
return removed
|
||||
}
|
||||
const seedLegacyDatabase = () => new Promise((resolve, reject) => {
|
||||
const worker = new Worker('/src/facade/projectMigrationSeedWorker.ts', { type: 'module', name: 'qa-rel02-legacy-seed' })
|
||||
const timeout = setTimeout(() => { worker.terminate(); reject(new Error('Legacy OPFS seed Worker timed out.')) }, 60_000)
|
||||
worker.onmessage = (event) => { clearTimeout(timeout); worker.terminate(); if (event.data?.ok) resolve(event.data); else reject(new Error(event.data?.error || 'Legacy OPFS seed failed.')) }
|
||||
worker.onerror = (event) => { clearTimeout(timeout); worker.terminate(); reject(new Error(event.message || 'Legacy OPFS seed Worker failed.')) }
|
||||
worker.postMessage({ operation: 'seed', databasePath, documentId, resourceHash, resourceBytes: resourceBytes.buffer }, [resourceBytes.buffer])
|
||||
})
|
||||
const inspectDatabase = () => new Promise((resolve, reject) => {
|
||||
const worker = new Worker('/src/facade/projectMigrationSeedWorker.ts', { type: 'module', name: 'qa-rel02-legacy-inspect' })
|
||||
const timeout = setTimeout(() => { worker.terminate(); reject(new Error('Legacy OPFS inspect Worker timed out.')) }, 60_000)
|
||||
worker.onmessage = (event) => { clearTimeout(timeout); worker.terminate(); if (event.data?.ok) resolve(event.data); else reject(new Error(event.data?.error || 'Legacy OPFS inspect failed.')) }
|
||||
worker.onerror = (event) => { clearTimeout(timeout); worker.terminate(); reject(new Error(event.message || 'Legacy OPFS inspect Worker failed.')) }
|
||||
worker.postMessage({ operation: 'inspect', databasePath })
|
||||
})
|
||||
|
||||
let persistence
|
||||
let reopened
|
||||
let failedPersistence
|
||||
try {
|
||||
if (!report.crossOriginIsolated || !navigator.storage?.getDirectory) throw new Error('Chrome REL-02 harness requires isolated OPFS.')
|
||||
await removeDatabaseFiles()
|
||||
await seedLegacyDatabase()
|
||||
failedPersistence = createSqliteProjectPersistence({ databasePath, migrationInterruptAfterVersion: 3 })
|
||||
let rollbackError = ''
|
||||
try { await failedPersistence.initialize() } catch (error) { rollbackError = error instanceof Error ? error.message : String(error) }
|
||||
await failedPersistence.dispose()
|
||||
failedPersistence = undefined
|
||||
const afterRollback = await inspectDatabase()
|
||||
persistence = createSqliteProjectPersistence({ databasePath })
|
||||
const migrated = await persistence.initialize()
|
||||
const loaded = await persistence.load(documentId)
|
||||
const restoredResource = await persistence.resource.get(resourceHash)
|
||||
const recovery = await persistence.recovery(documentId)
|
||||
const migratedDocument = loaded ? { ...loaded, version: loaded.version + 1, dirty: false, label: 'Migrated OPFS project' } : null
|
||||
if (!migratedDocument) throw new Error('Legacy document did not load after migration.')
|
||||
await persistence.save(migratedDocument)
|
||||
await persistence.dispose()
|
||||
persistence = undefined
|
||||
|
||||
reopened = createSqliteProjectPersistence({ databasePath })
|
||||
const reopenedCapabilities = await reopened.initialize()
|
||||
const reopenedDocument = await reopened.load(documentId)
|
||||
const checkpoint = await reopened.loadCheckpoint(documentId, migratedDocument.version)
|
||||
const reopenedRecovery = await reopened.recovery(documentId)
|
||||
await reopened.resource.release(resourceHash)
|
||||
const releasedResource = await reopened.resource.get(resourceHash)
|
||||
report.migration = {
|
||||
sourceSchemaVersion: 1,
|
||||
migratedSchemaVersion: migrated.schemaVersion,
|
||||
migratedDocumentVersion: loaded?.version ?? null,
|
||||
migratedLabel: loaded?.label ?? null,
|
||||
migratedObjectCount: loaded?.objects.length ?? 0,
|
||||
recomputeDefaulted: loaded?.recompute?.status === 'idle',
|
||||
reopenedSchemaVersion: reopenedCapabilities.schemaVersion,
|
||||
reopenedVersion: reopenedDocument?.version ?? null,
|
||||
reopenedLabel: reopenedDocument?.label ?? null,
|
||||
checkpointVersion: checkpoint?.version ?? null,
|
||||
recoveryIntegrity: recovery.integrity,
|
||||
reopenedRecoveryIntegrity: reopenedRecovery.integrity,
|
||||
resourceRoundTrip: restoredResource ? new TextDecoder().decode(restoredResource) === 'legacy-opfs-resource' : false,
|
||||
resourceReleased: releasedResource === null,
|
||||
}
|
||||
report.rollback = { rejected: rollbackError.includes('interruption after schema version 3'), interruptedAfterMarker: rollbackError.includes('interruption after schema version 3'), error: rollbackError, appliedVersionsAfterFailure: afterRollback.appliedVersions || [] }
|
||||
report.cleanup = { databasePath, removedFiles: [] }
|
||||
report.status = report.crossOriginIsolated && migrated.mode === 'sqlite-opfs' && migrated.schemaVersion === 6 && report.rollback.rejected && report.rollback.interruptedAfterMarker && report.rollback.appliedVersionsAfterFailure.length === 1 && report.rollback.appliedVersionsAfterFailure[0] === 1 && report.migration.migratedDocumentVersion === 3 && report.migration.migratedObjectCount === 1 && report.migration.recomputeDefaulted && report.migration.reopenedSchemaVersion === 6 && report.migration.reopenedVersion === 4 && report.migration.checkpointVersion === 4 && report.migration.recoveryIntegrity === 'ok' && report.migration.reopenedRecoveryIntegrity === 'ok' && report.migration.resourceRoundTrip && report.migration.resourceReleased ? 'pass' : 'failed'
|
||||
} catch (error) {
|
||||
report.status = 'failed'
|
||||
report.error = error instanceof Error ? `${error.name}: ${error.message}` : String(error)
|
||||
} finally {
|
||||
await persistence?.dispose().catch(() => {})
|
||||
await reopened?.dispose().catch(() => {})
|
||||
await failedPersistence?.dispose().catch(() => {})
|
||||
try { report.cleanup = { databasePath, removedFiles: await removeDatabaseFiles() } } catch (error) { report.cleanupError = error instanceof Error ? error.message : String(error) }
|
||||
}
|
||||
output.textContent = JSON.stringify(report, null, 2)
|
||||
await fetch('/__opfs-migration-report', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(report) })
|
||||
if (report.status !== 'pass') throw new Error(report.error || 'Chrome OPFS migration harness failed.')
|
||||
</script>
|
||||
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>
|
||||
5
public/chrome-partdesign-loft-harness.html
Normal file
5
public/chrome-partdesign-loft-harness.html
Normal file
@@ -0,0 +1,5 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>Chrome PartDesign loft harness</title>
|
||||
<pre id="result">running</pre>
|
||||
<script type="module" src="/src/chromePartdesignLoftHarness.ts"></script>
|
||||
5
public/chrome-partdesign-transform-harness.html
Normal file
5
public/chrome-partdesign-transform-harness.html
Normal file
@@ -0,0 +1,5 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>Chrome PartDesign transform harness</title>
|
||||
<pre id="result">running</pre>
|
||||
<script type="module" src="/src/chromePartdesignTransformHarness.ts"></script>
|
||||
1
public/chrome-performance-harness.html
Normal file
1
public/chrome-performance-harness.html
Normal file
@@ -0,0 +1 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Chrome Performance Harness</title></head><body><pre id="result">running</pre><script type="module" src="/src/chromePerformanceHarness.ts"></script></body></html>
|
||||
40
public/chrome-persistence-harness.html
Normal file
40
public/chrome-persistence-harness.html
Normal file
@@ -0,0 +1,40 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>Chrome OPFS persistence harness</title>
|
||||
<pre id="result">running</pre>
|
||||
<script type="module">
|
||||
import { createSqliteProjectPersistence } from '/src/facade/projectStore.ts'
|
||||
|
||||
const output = document.querySelector('#result')
|
||||
const report = { schemaVersion: 1, browserId: 'chrome', crossOriginIsolated: globalThis.crossOriginIsolated === true, status: 'running' }
|
||||
const documentSnapshot = {
|
||||
id: 'chrome-opfs-persistence', label: 'Chrome OPFS persistence', version: 7, dirty: true, readOnly: false, units: 'mm',
|
||||
tree: [{ id: 'root', label: 'Root', type: 'feature', state: 'valid' }],
|
||||
objects: [{ id: 'root', typeId: 'PartDesign::Feature', properties: [{ name: 'Label', label: 'Label', type: 'App::PropertyString', value: 'Root', recompute: false }] }],
|
||||
dependencies: [], recompute: { generation: 2, status: 'completed', objectStates: { root: 'up-to-date' }, dirtyObjects: [], order: ['root'], errors: [] },
|
||||
}
|
||||
let persistence
|
||||
try {
|
||||
persistence = createSqliteProjectPersistence()
|
||||
const capabilities = await persistence.initialize()
|
||||
const saved = await persistence.save(documentSnapshot)
|
||||
const loaded = await persistence.load(documentSnapshot.id)
|
||||
const checkpoint = await persistence.loadCheckpoint(documentSnapshot.id, documentSnapshot.version)
|
||||
const bytes = new TextEncoder().encode('chrome-opfs-resource')
|
||||
const resource = await persistence.resource.put(bytes, 'text/plain')
|
||||
const restoredBytes = await persistence.resource.get(resource.hash)
|
||||
await persistence.resource.release(resource.hash)
|
||||
const recovery = await persistence.recovery(documentSnapshot.id)
|
||||
report.capabilities = capabilities
|
||||
report.roundTrip = { savedMode: saved.mode, loadedVersion: loaded?.version ?? null, loadedLabel: loaded?.label ?? null, checkpointVersion: checkpoint?.version ?? null, resourceRoundTrip: restoredBytes ? new TextDecoder().decode(restoredBytes) === 'chrome-opfs-resource' : false, recoveryIntegrity: recovery.integrity, checkpointCount: recovery.checkpoints.length }
|
||||
report.status = report.crossOriginIsolated && capabilities.mode === 'sqlite-opfs' && capabilities.sqliteWasm && capabilities.opfs && saved.mode === 'sqlite-opfs' && loaded?.id === documentSnapshot.id && loaded.version === documentSnapshot.version && checkpoint?.version === documentSnapshot.version && report.roundTrip.resourceRoundTrip && recovery.integrity === 'ok' ? 'pass' : 'failed'
|
||||
} catch (error) {
|
||||
report.status = 'failed'
|
||||
report.error = error instanceof Error ? `${error.name}: ${error.message}` : String(error)
|
||||
} finally {
|
||||
await persistence?.dispose().catch(() => {})
|
||||
}
|
||||
output.textContent = JSON.stringify(report, null, 2)
|
||||
await fetch('/__persistence-report', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(report) })
|
||||
if (report.status !== 'pass') throw new Error(report.error || 'Chrome OPFS persistence harness failed.')
|
||||
</script>
|
||||
5
public/chrome-plot-harness.html
Normal file
5
public/chrome-plot-harness.html
Normal file
@@ -0,0 +1,5 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>Chrome Plot harness</title>
|
||||
<pre id="result">running</pre>
|
||||
<script type="module" src="/src/chromePlotHarness.ts"></script>
|
||||
1
public/chrome-production-document-harness.html
Normal file
1
public/chrome-production-document-harness.html
Normal file
@@ -0,0 +1 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Chrome Production Document Harness</title></head><body><pre id="result">running</pre><script type="module" src="/src/chromeProductionDocumentHarness.ts"></script></body></html>
|
||||
112
public/chrome-profile-validation-harness.html
Normal file
112
public/chrome-profile-validation-harness.html
Normal file
@@ -0,0 +1,112 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>Chrome Sketcher profile validation harness</title>
|
||||
<pre id="result">running</pre>
|
||||
<script type="module">
|
||||
import { classifyPlanarProfile, validatePlanarProfile } from '/src/facade/geometryRuntime.ts'
|
||||
import { BitbybitGeometryRuntime } from '/src/facade/geometryRuntime.ts'
|
||||
import { sketchProfile } from '/src/facade/recomputeEngine.ts'
|
||||
import { createSketch } from '/src/facade/sketcher.ts'
|
||||
|
||||
const output = document.querySelector('#result')
|
||||
const report = { schemaVersion: 1, browserId: 'chrome', crossOriginIsolated: globalThis.crossOriginIsolated === true, status: 'running' }
|
||||
const outer = [[0, 0, 0], [6, 0, 0], [6, 6, 0], [0, 6, 0]]
|
||||
const hole = [[2, 2, 0], [2, 4, 0], [4, 4, 0], [4, 2, 0]]
|
||||
const bowTie = { outer: [[0, 0, 0], [4, 4, 0], [0, 4, 0], [4, 0, 0]] }
|
||||
const outsideHole = { outer, holes: [[[8, 8, 0], [8, 9, 0], [9, 9, 0], [9, 8, 0]]] }
|
||||
const nonPlanar = { outer: [[0, 0, 0], [2, 0, 0], [2, 2, 1], [0, 2, 0]] }
|
||||
const degenerate = { outer: [[0, 0, 0], [1, 0, 0], [2, 0, 0]] }
|
||||
const classification = (profile) => classifyPlanarProfile(profile)
|
||||
const profileSketch = (id, loops) => createSketch(id, loops.flatMap((loop, loopIndex) => loop.map((start, pointIndex) => {
|
||||
const end = loop[(pointIndex + 1) % loop.length]
|
||||
return { id: `${loopIndex}-${pointIndex}`, type: 'line', start: { x: start[0], y: start[1] }, end: { x: end[0], y: end[1] } }
|
||||
})))
|
||||
const validationError = (profile) => {
|
||||
try {
|
||||
validatePlanarProfile(profile)
|
||||
return null
|
||||
} catch (error) {
|
||||
return error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
}
|
||||
try {
|
||||
const markerDirectory = await navigator.storage.getDirectory()
|
||||
const markerName = 'profile-validation-chrome-marker.json'
|
||||
const marker = await markerDirectory.getFileHandle(markerName, { create: true })
|
||||
const writable = await marker.createWritable()
|
||||
await writable.write(JSON.stringify({ suite: 'SK-11', cases: 12 }))
|
||||
await writable.close()
|
||||
const markerPayload = JSON.parse(await (await marker.getFile()).text())
|
||||
await markerDirectory.removeEntry(markerName)
|
||||
let markerRemoved = false
|
||||
try { await markerDirectory.getFileHandle(markerName) } catch (error) { markerRemoved = error?.name === 'NotFoundError' }
|
||||
|
||||
report.fixtures = {
|
||||
closed: classification({ outer }),
|
||||
open: classification({ outer, closed: false }),
|
||||
multiRing: classification({ outer, holes: [hole] }),
|
||||
selfIntersecting: classification(bowTie),
|
||||
invalidNesting: classification(outsideHole),
|
||||
nonPlanar: classification(nonPlanar),
|
||||
degenerate: classification(degenerate),
|
||||
}
|
||||
report.validationErrors = {
|
||||
closed: validationError({ outer }),
|
||||
multiRing: validationError({ outer, holes: [hole] }),
|
||||
open: validationError({ outer, closed: false }),
|
||||
selfIntersecting: validationError(bowTie),
|
||||
invalidNesting: validationError(outsideHole),
|
||||
nonPlanar: validationError(nonPlanar),
|
||||
degenerate: validationError(degenerate),
|
||||
}
|
||||
const partDesignClosed = sketchProfile(profileSketch('partdesign-closed', [outer.map(([x, y]) => [x, y])]))
|
||||
const partDesignMultiRing = sketchProfile(profileSketch('partdesign-multi-ring', [hole.map(([x, y]) => [x, y]), outer.map(([x, y]) => [x, y])]))
|
||||
const partDesignSelfIntersecting = sketchProfile(profileSketch('partdesign-self-intersecting', [[[0, 0], [4, 4], [0, 4], [4, 0]]]))
|
||||
const partDesignInvalidNesting = sketchProfile(profileSketch('partdesign-invalid-nesting', [outer.map(([x, y]) => [x, y]), [[8, 8], [9, 8], [9, 9], [8, 9]]]))
|
||||
const partDesignOpen = sketchProfile(createSketch('partdesign-open', [
|
||||
{ id: 'open-0', type: 'line', start: { x: 0, y: 0 }, end: { x: 6, y: 0 } },
|
||||
{ id: 'open-1', type: 'line', start: { x: 6, y: 0 }, end: { x: 6, y: 6 } },
|
||||
{ id: 'open-2', type: 'line', start: { x: 6, y: 6 }, end: { x: 0, y: 6 } },
|
||||
]))
|
||||
report.partDesignProfiles = {
|
||||
closed: { code: partDesignClosed.code ?? null, regionCount: 1 + (partDesignClosed.profile?.additionalRegions?.length ?? 0) },
|
||||
multiRing: { code: partDesignMultiRing.code ?? null, outerPoints: partDesignMultiRing.profile?.outer.length ?? 0, holeCount: partDesignMultiRing.profile?.holes?.length ?? 0, regionCount: 1 + (partDesignMultiRing.profile?.additionalRegions?.length ?? 0) },
|
||||
open: { code: partDesignOpen.code ?? null },
|
||||
selfIntersecting: { code: partDesignSelfIntersecting.code ?? null, regionCount: 1 + (partDesignSelfIntersecting.profile?.additionalRegions?.length ?? 0), regionPoints: [partDesignSelfIntersecting.profile?.outer.length ?? 0, ...(partDesignSelfIntersecting.profile?.additionalRegions ?? []).map((region) => region.outer.length)] },
|
||||
invalidNesting: { code: partDesignInvalidNesting.code ?? null, regionCount: 1 + (partDesignInvalidNesting.profile?.additionalRegions?.length ?? 0), regionPoints: [partDesignInvalidNesting.profile?.outer.length ?? 0, ...(partDesignInvalidNesting.profile?.additionalRegions ?? []).map((region) => region.outer.length)] },
|
||||
}
|
||||
const runtime = new BitbybitGeometryRuntime()
|
||||
const geometryProfiles = { closed: partDesignClosed.profile, multiRing: partDesignMultiRing.profile, selfIntersecting: partDesignSelfIntersecting.profile, invalidNesting: partDesignInvalidNesting.profile }
|
||||
const expectedGeometry = { closed: [360, 1, 'solid'], multiRing: [320, 1, 'solid'], selfIntersecting: [80, 2, 'compound'], invalidNesting: [370, 2, 'compound'] }
|
||||
const geometryShapes = []
|
||||
report.partDesignGeometry = {}
|
||||
try {
|
||||
const capabilities = await runtime.initialize()
|
||||
if (capabilities.status !== 'ready') throw new Error(capabilities.reason || `Bitbybit geometry status is ${capabilities.status}`)
|
||||
for (const [name, profile] of Object.entries(geometryProfiles)) {
|
||||
if (!profile) throw new Error(`PartDesign ${name} profile was not produced.`)
|
||||
const shape = await runtime.pad({ documentId: 'sk-11-profile-oracle', documentVersion: 1, profile, length: 10, direction: [0, 0, 1] })
|
||||
geometryShapes.push(shape)
|
||||
const mass = await runtime.massProperties(shape)
|
||||
const quality = await runtime.qualityReport(shape)
|
||||
report.partDesignGeometry[name] = { volume: mass.volume, solids: quality.solids, shapeType: quality.shapeType, structuralValid: quality.structuralValid, structuralErrors: quality.structuralErrors, structuralWarnings: quality.structuralWarnings }
|
||||
}
|
||||
} finally {
|
||||
await Promise.all(geometryShapes.map((shape) => runtime.release(shape)))
|
||||
runtime.dispose()
|
||||
}
|
||||
report.opfs = { markerSuite: markerPayload.suite, markerCases: markerPayload.cases, markerRemoved }
|
||||
const is = (fixture, status, ringCount, intersections = 0) => fixture?.status === status && fixture.ringCount === ringCount && fixture.selfIntersections === intersections
|
||||
const validErrors = report.validationErrors.closed === null && report.validationErrors.multiRing === null
|
||||
const expectedErrors = /open/.test(report.validationErrors.open ?? '') && /self-intersecting/.test(report.validationErrors.selfIntersecting ?? '') && /contained directly/.test(report.validationErrors.invalidNesting ?? '') && /coplanar/.test(report.validationErrors.nonPlanar ?? '') && /collinear/.test(report.validationErrors.degenerate ?? '')
|
||||
const partDesignValid = report.partDesignProfiles.closed.code === null && report.partDesignProfiles.closed.regionCount === 1 && report.partDesignProfiles.multiRing.code === null && report.partDesignProfiles.multiRing.outerPoints === 4 && report.partDesignProfiles.multiRing.holeCount === 1 && report.partDesignProfiles.multiRing.regionCount === 1 && report.partDesignProfiles.open.code === 'PROFILE_OPEN' && report.partDesignProfiles.selfIntersecting.code === null && report.partDesignProfiles.selfIntersecting.regionCount === 2 && report.partDesignProfiles.selfIntersecting.regionPoints.every((count) => count === 3) && report.partDesignProfiles.invalidNesting.code === null && report.partDesignProfiles.invalidNesting.regionCount === 2 && report.partDesignProfiles.invalidNesting.regionPoints.every((count) => count === 4)
|
||||
const geometryValid = Object.entries(expectedGeometry).every(([name, [volume, solids, shapeType]]) => Math.abs(report.partDesignGeometry[name]?.volume - volume) <= 1e-7 && report.partDesignGeometry[name]?.solids === solids && report.partDesignGeometry[name]?.shapeType === shapeType)
|
||||
report.status = report.crossOriginIsolated && is(report.fixtures.closed, 'closed', 1) && is(report.fixtures.open, 'open', 1) && is(report.fixtures.multiRing, 'multi-ring', 2) && is(report.fixtures.selfIntersecting, 'self-intersecting', 1, 1) && is(report.fixtures.invalidNesting, 'invalid-nesting', 2) && is(report.fixtures.nonPlanar, 'non-planar', 1) && is(report.fixtures.degenerate, 'degenerate', 1) && validErrors && expectedErrors && partDesignValid && geometryValid && report.opfs.markerSuite === 'SK-11' && report.opfs.markerCases === 12 && report.opfs.markerRemoved ? 'pass' : 'failed'
|
||||
} catch (error) {
|
||||
report.status = 'failed'
|
||||
report.error = error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
output.textContent = JSON.stringify(report, null, 2)
|
||||
await fetch('/__chrome-profile-validation-report', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(report) })
|
||||
if (report.status !== 'pass') throw new Error(report.error || 'Chrome profile validation harness failed.')
|
||||
</script>
|
||||
1
public/chrome-qa08-harness.html
Normal file
1
public/chrome-qa08-harness.html
Normal file
@@ -0,0 +1 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Chrome QA-08 Harness</title></head><body><pre id="result">running</pre><script type="module" src="/src/chromeQa08Harness.ts"></script></body></html>
|
||||
5
public/chrome-robot-harness.html
Normal file
5
public/chrome-robot-harness.html
Normal file
@@ -0,0 +1,5 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>Chrome Robot harness</title>
|
||||
<pre id="result">running</pre>
|
||||
<script type="module" src="/src/chromeRobotHarness.ts"></script>
|
||||
1
public/chrome-script-harness.html
Normal file
1
public/chrome-script-harness.html
Normal file
@@ -0,0 +1 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Chrome Script Harness</title></head><body><pre id="result">running</pre><script type="module" src="/src/chromeScriptHarness.ts"></script></body></html>
|
||||
1
public/chrome-secondary-formats-harness.html
Normal file
1
public/chrome-secondary-formats-harness.html
Normal file
@@ -0,0 +1 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Chrome Secondary Formats Harness</title></head><body><pre id="result">running</pre><script type="module" src="/src/chromeSecondaryFormatsHarness.ts"></script></body></html>
|
||||
1
public/chrome-security-harness.html
Normal file
1
public/chrome-security-harness.html
Normal file
@@ -0,0 +1 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Chrome Security Harness</title></head><body><pre id="result">running</pre><script type="module" src="/src/chromeSecurityHarness.ts"></script></body></html>
|
||||
89
public/chrome-sketcher-bspline-harness.html
Normal file
89
public/chrome-sketcher-bspline-harness.html
Normal file
@@ -0,0 +1,89 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>Chrome Sketcher B-spline editor harness</title>
|
||||
<pre id="result">running</pre>
|
||||
<script type="module">
|
||||
import { createSketch, editSketchBspline, sketchGeometrySignature } from '/src/facade/sketcher.ts'
|
||||
import { inspectFcstdArchive, serializeFcstdMetadataArchive } from '/src/facade/fcstd.ts'
|
||||
|
||||
const output = document.querySelector('#result')
|
||||
const report = { schemaVersion: 1, browserId: 'chrome', crossOriginIsolated: globalThis.crossOriginIsolated === true, status: 'running' }
|
||||
const source = createSketch('chrome-bspline', [{ id: 'spline', type: 'bspline', degree: 2, controlPoints: [{ x: 0, y: 0 }, { x: 1, y: 2 }, { x: 3, y: 0 }], weights: [1, 1, 1], knots: [0, 0, 0, 1, 1, 1] }])
|
||||
const errorFor = (patch) => {
|
||||
try {
|
||||
editSketchBspline(source, 'spline', patch)
|
||||
return null
|
||||
} catch (error) {
|
||||
return error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
}
|
||||
try {
|
||||
const edited = editSketchBspline(source, 'spline', {
|
||||
degree: 1,
|
||||
controlPoints: [{ x: 0, y: 0 }, { x: 2, y: 3 }, { x: 4, y: 0 }],
|
||||
weights: [1, 0.5, 1],
|
||||
knots: [0, 0, 0.5, 1, 1],
|
||||
periodic: true,
|
||||
})
|
||||
const editedGeometry = edited.geometry[0]
|
||||
const sourceGeometry = source.geometry[0]
|
||||
const constrained = createSketch('Sketch', [{
|
||||
id: 'weighted-spline', type: 'bspline', degree: 3,
|
||||
controlPoints: [{ x: 0, y: 0 }, { x: 1, y: 2 }, { x: 3, y: 2 }, { x: 4, y: 0 }],
|
||||
weights: [1, 1.5, 1.25, 1], knots: [0, 0, 0, 0, 1, 1, 1, 1], periodic: false,
|
||||
}], [
|
||||
{ id: 'middle-weight', type: 'weight', geometryId: 'weighted-spline', controlPointIndex: 1, value: 1.5, driving: true },
|
||||
{ id: 'third-alignment', type: 'internalAlignment', geometryId: 'weighted-spline', internalGeometryIndex: 2, alignmentType: 'bspline-control-point', driving: true },
|
||||
{ id: 'third-weight', type: 'weight', geometryId: 'weighted-spline', controlPointIndex: 2, value: 1.25, driving: true },
|
||||
])
|
||||
const document = {
|
||||
id: 'chrome-bspline-fcstd', label: 'B-spline round-trip', version: 1, dirty: true, readOnly: false, units: 'mm',
|
||||
tree: [{ id: 'Sketch', label: 'Sketch', type: 'sketch', state: 'valid' }],
|
||||
objects: [{ id: 'Sketch', typeId: 'Sketcher::SketchObject', properties: [], sketch: constrained }],
|
||||
dependencies: [], recompute: { generation: 0, status: 'idle', objectStates: { Sketch: 'up-to-date' }, dirtyObjects: [], order: [], errors: [] },
|
||||
}
|
||||
const archive = serializeFcstdMetadataArchive(document)
|
||||
const restored = inspectFcstdArchive(archive).objects[0]?.sketch
|
||||
const markerDirectory = await navigator.storage.getDirectory()
|
||||
const markerName = 'sketcher-bspline-chrome-marker.json'
|
||||
const marker = await markerDirectory.getFileHandle(markerName, { create: true })
|
||||
const writable = await marker.createWritable()
|
||||
await writable.write(JSON.stringify({ suite: 'SK-10', fields: 8 }))
|
||||
await writable.close()
|
||||
const markerPayload = JSON.parse(await (await marker.getFile()).text())
|
||||
await markerDirectory.removeEntry(markerName)
|
||||
let markerRemoved = false
|
||||
try { await markerDirectory.getFileHandle(markerName) } catch (error) { markerRemoved = error?.name === 'NotFoundError' }
|
||||
report.edit = {
|
||||
stableId: editedGeometry?.id === 'spline',
|
||||
degree: editedGeometry?.type === 'bspline' ? editedGeometry.degree : null,
|
||||
poleCount: editedGeometry?.type === 'bspline' ? editedGeometry.controlPoints.length : 0,
|
||||
middlePole: editedGeometry?.type === 'bspline' ? editedGeometry.controlPoints[1] : null,
|
||||
weights: editedGeometry?.type === 'bspline' ? editedGeometry.weights : null,
|
||||
knots: editedGeometry?.type === 'bspline' ? editedGeometry.knots : null,
|
||||
periodic: editedGeometry?.type === 'bspline' ? editedGeometry.periodic === true : false,
|
||||
sourceUnchanged: sketchGeometrySignature(sourceGeometry) === JSON.stringify({ id: 'spline', type: 'bspline', degree: 2, controlPoints: [{ x: 0, y: 0 }, { x: 1, y: 2 }, { x: 3, y: 0 }], weights: [1, 1, 1], knots: [0, 0, 0, 1, 1, 1] }),
|
||||
}
|
||||
report.validationErrors = {
|
||||
invalidWeight: errorFor({ weights: [1, 0, 1] }),
|
||||
invalidKnots: errorFor({ knots: [0, 1] }),
|
||||
invalidDegree: errorFor({ degree: 3 }),
|
||||
}
|
||||
report.roundTrip = {
|
||||
archiveBytes: archive.byteLength,
|
||||
geometryEqual: JSON.stringify(restored?.geometry) === JSON.stringify(constrained.geometry),
|
||||
constraintsEqual: JSON.stringify(restored?.constraints) === JSON.stringify(constrained.constraints),
|
||||
constraintTypes: restored?.constraints.map((constraint) => constraint.type) ?? [],
|
||||
weights: restored?.geometry[0]?.type === 'bspline' ? restored.geometry[0].weights : null,
|
||||
knots: restored?.geometry[0]?.type === 'bspline' ? restored.geometry[0].knots : null,
|
||||
}
|
||||
report.opfs = { markerSuite: markerPayload.suite, markerFields: markerPayload.fields, markerRemoved }
|
||||
report.status = report.crossOriginIsolated && report.edit.stableId && report.edit.degree === 1 && report.edit.poleCount === 3 && report.edit.middlePole?.x === 2 && report.edit.middlePole?.y === 3 && JSON.stringify(report.edit.weights) === JSON.stringify([1, 0.5, 1]) && JSON.stringify(report.edit.knots) === JSON.stringify([0, 0, 0.5, 1, 1]) && report.edit.periodic && report.edit.sourceUnchanged && /weights/.test(report.validationErrors.invalidWeight ?? '') && /knots/.test(report.validationErrors.invalidKnots ?? '') && /degree \+ 1/.test(report.validationErrors.invalidDegree ?? '') && report.roundTrip.archiveBytes > 0 && report.roundTrip.geometryEqual && report.roundTrip.constraintsEqual && JSON.stringify(report.roundTrip.constraintTypes) === JSON.stringify(['weight', 'internalAlignment', 'weight']) && JSON.stringify(report.roundTrip.weights) === JSON.stringify([1, 1.5, 1.25, 1]) && JSON.stringify(report.roundTrip.knots) === JSON.stringify([0, 0, 0, 0, 1, 1, 1, 1]) && report.opfs.markerSuite === 'SK-10' && report.opfs.markerFields === 8 && report.opfs.markerRemoved ? 'pass' : 'failed'
|
||||
} catch (error) {
|
||||
report.status = 'failed'
|
||||
report.error = error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
output.textContent = JSON.stringify(report, null, 2)
|
||||
await fetch('/__chrome-sketcher-bspline-report', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(report) })
|
||||
if (report.status !== 'pass') throw new Error(report.error || 'Chrome Sketcher B-spline harness failed.')
|
||||
</script>
|
||||
59
public/chrome-sketcher-diagnostics-harness.html
Normal file
59
public/chrome-sketcher-diagnostics-harness.html
Normal file
@@ -0,0 +1,59 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>Chrome Sketcher dimensional diagnostics harness</title>
|
||||
<pre id="result">running</pre>
|
||||
<script type="module">
|
||||
import { createSketch, solveSketch } from '/src/facade/sketcher.ts'
|
||||
|
||||
const output = document.querySelector('#result')
|
||||
const report = { schemaVersion: 1, browserId: 'chrome', crossOriginIsolated: globalThis.crossOriginIsolated === true, status: 'running' }
|
||||
try {
|
||||
const referenceInput = createSketch('chrome-reference', [{ id: 'line', type: 'line', start: { x: 0, y: 0 }, end: { x: 2, y: 1 } }], [
|
||||
{ id: 'horizontal', type: 'horizontal', geometryId: 'line' },
|
||||
{ id: 'reference-length', type: 'distance', first: { geometryId: 'line', point: 'start' }, second: { geometryId: 'line', point: 'end' }, value: 99, driving: false },
|
||||
])
|
||||
const duplicateInput = createSketch('chrome-duplicate', [{ id: 'line', type: 'line', start: { x: 0, y: 0 }, end: { x: 1, y: 0 } }], [
|
||||
{ id: 'length-1', type: 'distance', first: { geometryId: 'line', point: 'start' }, second: { geometryId: 'line', point: 'end' }, value: 5 },
|
||||
{ id: 'length-2', type: 'distance', first: { geometryId: 'line', point: 'start' }, second: { geometryId: 'line', point: 'end' }, value: 5 },
|
||||
])
|
||||
const conflictInput = createSketch('chrome-conflict', [{ id: 'line', type: 'line', start: { x: 0, y: 0 }, end: { x: 1, y: 0 } }], [
|
||||
{ id: 'length-1', type: 'distance', first: { geometryId: 'line', point: 'start' }, second: { geometryId: 'line', point: 'end' }, value: 5 },
|
||||
{ id: 'length-2', type: 'distance', first: { geometryId: 'line', point: 'start' }, second: { geometryId: 'line', point: 'end' }, value: 8 },
|
||||
])
|
||||
const coincidentInput = createSketch('chrome-coincident', [
|
||||
{ id: 'first', type: 'line', start: { x: 0, y: 0 }, end: { x: 2, y: 0 } },
|
||||
{ id: 'second', type: 'line', start: { x: 5, y: 3 }, end: { x: 6, y: 4 } },
|
||||
], [{ id: 'coincident', type: 'coincident', first: { geometryId: 'first', point: 'end' }, second: { geometryId: 'second', point: 'start' } }])
|
||||
const blockInput = createSketch('chrome-block', [{ id: 'line', type: 'line', start: { x: 1, y: 2 }, end: { x: 4, y: 6 } }], [{ id: 'block', type: 'block', geometryId: 'line' }])
|
||||
const reference = solveSketch(referenceInput)
|
||||
const duplicate = solveSketch(duplicateInput)
|
||||
const conflict = solveSketch(conflictInput)
|
||||
const coincident = solveSketch(coincidentInput)
|
||||
const block = solveSketch(blockInput)
|
||||
const markerDirectory = await navigator.storage.getDirectory()
|
||||
const markerName = 'sketcher-diagnostics-chrome-marker.json'
|
||||
const marker = await markerDirectory.getFileHandle(markerName, { create: true })
|
||||
const writable = await marker.createWritable()
|
||||
await writable.write(JSON.stringify({ suite: 'SK-06/SK-07', cases: 5 }))
|
||||
await writable.close()
|
||||
const markerPayload = JSON.parse(await (await marker.getFile()).text())
|
||||
await markerDirectory.removeEntry(markerName)
|
||||
let markerRemoved = false
|
||||
try { await markerDirectory.getFileHandle(markerName) } catch (error) { markerRemoved = error?.name === 'NotFoundError' }
|
||||
report.fixtures = {
|
||||
reference: { status: reference.status, degreesOfFreedom: reference.degreesOfFreedom, diagnostic: reference.diagnostics.some((diagnostic) => diagnostic.code === 'REFERENCE_DIMENSION' && diagnostic.constraintId === 'reference-length'), unchangedEnd: reference.snapshot.geometry[0].type === 'line' && reference.snapshot.geometry[0].end.x === 2 },
|
||||
redundant: { status: duplicate.status, diagnostic: duplicate.diagnostics.some((diagnostic) => diagnostic.code === 'REDUNDANT_CONSTRAINT' && diagnostic.constraintId === 'length-2') },
|
||||
conflicting: { status: conflict.status, diagnostic: conflict.diagnostics.some((diagnostic) => diagnostic.code === 'CONSTRAINT_CONFLICT' && diagnostic.constraintId === 'length-2'), notConverged: conflict.diagnostics.some((diagnostic) => diagnostic.code === 'SOLVER_NOT_CONVERGED') },
|
||||
coincident: { status: coincident.status, residual: coincident.residual, sharedPoint: coincident.snapshot.geometry[0].type === 'line' && coincident.snapshot.geometry[1].type === 'line' && coincident.snapshot.geometry[0].end.x === coincident.snapshot.geometry[1].start.x && coincident.snapshot.geometry[0].end.y === coincident.snapshot.geometry[1].start.y },
|
||||
block: { status: block.status, degreesOfFreedom: block.degreesOfFreedom, unchanged: block.snapshot.geometry[0].type === 'line' && block.snapshot.geometry[0].start.x === 1 && block.snapshot.geometry[0].start.y === 2 && block.snapshot.geometry[0].end.x === 4 && block.snapshot.geometry[0].end.y === 6 },
|
||||
}
|
||||
report.opfs = { markerSuite: markerPayload.suite, markerRemoved }
|
||||
report.status = report.crossOriginIsolated && report.fixtures.reference.status === 'under-constrained' && report.fixtures.reference.degreesOfFreedom === 3 && report.fixtures.reference.diagnostic && report.fixtures.reference.unchangedEnd && report.fixtures.redundant.status === 'under-constrained' && report.fixtures.redundant.diagnostic && report.fixtures.conflicting.status === 'conflicting' && report.fixtures.conflicting.diagnostic && report.fixtures.conflicting.notConverged && report.fixtures.coincident.status === 'under-constrained' && report.fixtures.coincident.residual === 0 && report.fixtures.coincident.sharedPoint && report.fixtures.block.status === 'solved' && report.fixtures.block.degreesOfFreedom === 0 && report.fixtures.block.unchanged && report.opfs.markerSuite === 'SK-06/SK-07' && report.opfs.markerRemoved ? 'pass' : 'failed'
|
||||
} catch (error) {
|
||||
report.status = 'failed'
|
||||
report.error = error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
output.textContent = JSON.stringify(report, null, 2)
|
||||
await fetch('/__chrome-sketcher-diagnostics-report', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(report) })
|
||||
if (report.status !== 'pass') throw new Error(report.error || 'Chrome Sketcher diagnostics harness failed.')
|
||||
</script>
|
||||
92
public/chrome-sketcher-editor-harness.html
Normal file
92
public/chrome-sketcher-editor-harness.html
Normal file
@@ -0,0 +1,92 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>Chrome Sketcher interactive editor harness</title>
|
||||
<div id="viewport" tabindex="0" style="position:relative;width:200px;height:100px">
|
||||
<button id="drag-handle" data-geometry-id="line" data-point="end" aria-label="Line end point" style="position:absolute;left:50px;top:10px;width:16px;height:16px"></button>
|
||||
</div>
|
||||
<pre id="result">running</pre>
|
||||
<script type="module">
|
||||
import { applySketchAutoConstraints, createSketch, dragSketchPoint, extendSketchLine, replaySketchEditorEvents, setSketchConstruction, SketchEditorInteractionSession, splitSketchLine, suggestSketchAutoConstraints, trimSketchLine } from '/src/facade/sketcher.ts'
|
||||
|
||||
const output = document.querySelector('#result')
|
||||
const report = { schemaVersion: 1, browserId: 'chrome', crossOriginIsolated: globalThis.crossOriginIsolated === true, status: 'running' }
|
||||
const base = createSketch('chrome-editor', [
|
||||
{ id: 'line-a', type: 'line', start: { x: 0, y: 0 }, end: { x: 5, y: 0.0001 } },
|
||||
{ id: 'line-b', type: 'line', start: { x: 5.0001, y: 0 }, end: { x: 5, y: 4 } },
|
||||
])
|
||||
const line = createSketch('chrome-editor-line', [{ id: 'line', type: 'line', start: { x: 0, y: 0 }, end: { x: 10, y: 0 } }])
|
||||
const dragLine = createSketch('chrome-editor-drag-line', [{ id: 'line', type: 'line', start: { x: 0, y: 0 }, end: { x: 5, y: 1 } }], [{ id: 'horizontal', type: 'horizontal', geometryId: 'line' }])
|
||||
const trimLine = createSketch('chrome-editor-trim-line', [
|
||||
{ id: 'line', type: 'line', start: { x: 0, y: 0 }, end: { x: 10, y: 0 } },
|
||||
{ id: 'cutter', type: 'line', start: { x: 7, y: -2 }, end: { x: 7, y: 2 } },
|
||||
])
|
||||
const endpoint = (snapshot, id = 'line') => {
|
||||
const geometry = snapshot.geometry.find((candidate) => candidate.id === id)
|
||||
return geometry?.type === 'line' ? geometry : null
|
||||
}
|
||||
try {
|
||||
const dragged = dragSketchPoint(dragLine, { geometryId: 'line', point: 'end' }, { x: 8, y: 2 })
|
||||
const split = splitSketchLine(line, 'line', { x: 4, y: 0 }, 'line-2')
|
||||
const extended = extendSketchLine(line, 'line', 'end', { x: 15, y: 0 })
|
||||
const trimmed = trimSketchLine(trimLine, 'line', 'end', { x: 7, y: 0 })
|
||||
const construction = setSketchConstruction(line, 'line', true)
|
||||
const suggestions = suggestSketchAutoConstraints(base, 'line-a', 0.001)
|
||||
const auto = applySketchAutoConstraints(base, suggestions)
|
||||
const replay = replaySketchEditorEvents(line, [
|
||||
{ type: 'drag', point: { geometryId: 'line', point: 'end' }, target: { x: 8, y: 0 } },
|
||||
{ type: 'undo' },
|
||||
{ type: 'redo' },
|
||||
{ type: 'trim', geometryId: 'line', endpoint: 'end', target: { x: 6, y: 0 } },
|
||||
])
|
||||
const interaction = new SketchEditorInteractionSession(dragLine)
|
||||
interaction.setTool('drag')
|
||||
const viewport = document.querySelector('#viewport')
|
||||
const dragHandle = document.querySelector('#drag-handle')
|
||||
const nativeEvents = { pointer: 0, keyboard: 0 }
|
||||
const pointerPosition = (event) => ({ x: event.clientX / 10, y: event.clientY / 10 })
|
||||
viewport.addEventListener('pointerdown', (event) => {
|
||||
nativeEvents.pointer += 1
|
||||
const target = event.target.closest?.('[data-geometry-id]')
|
||||
interaction.pointerDown({ position: pointerPosition(event), target: target ? { geometryId: target.dataset.geometryId, point: target.dataset.point } : undefined })
|
||||
})
|
||||
viewport.addEventListener('pointerup', (event) => { nativeEvents.pointer += 1; interaction.pointerUp({ position: pointerPosition(event) }) })
|
||||
viewport.addEventListener('keydown', (event) => { nativeEvents.keyboard += 1; if (interaction.keyDown(event)) event.preventDefault() })
|
||||
dragHandle.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, clientX: 50, clientY: 10, pointerId: 1 }))
|
||||
viewport.dispatchEvent(new PointerEvent('pointerup', { bubbles: true, clientX: 80, clientY: 20, pointerId: 1 }))
|
||||
viewport.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, key: 'z', ctrlKey: true }))
|
||||
viewport.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, key: 'y', ctrlKey: true }))
|
||||
interaction.setTool('drag')
|
||||
dragHandle.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, clientX: 80, clientY: 20, pointerId: 2 }))
|
||||
viewport.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, key: 'Escape' }))
|
||||
const markerDirectory = await navigator.storage.getDirectory()
|
||||
const markerName = 'sketcher-editor-chrome-marker.json'
|
||||
const marker = await markerDirectory.getFileHandle(markerName, { create: true })
|
||||
const writable = await marker.createWritable()
|
||||
await writable.write(JSON.stringify({ suite: 'SK-09', operations: 9 }))
|
||||
await writable.close()
|
||||
const markerPayload = JSON.parse(await (await marker.getFile()).text())
|
||||
await markerDirectory.removeEntry(markerName)
|
||||
let markerRemoved = false
|
||||
try { await markerDirectory.getFileHandle(markerName) } catch (error) { markerRemoved = error?.name === 'NotFoundError' }
|
||||
report.operations = {
|
||||
drag: { start: endpoint(dragged.snapshot)?.start ?? null, end: endpoint(dragged.snapshot)?.end ?? null, sourceUnchanged: endpoint(dragLine)?.end?.x === 5 && endpoint(dragLine)?.end?.y === 1 },
|
||||
split: { ids: split.geometry.map((geometry) => geometry.id), endpoints: split.geometry.filter((geometry) => geometry.type === 'line').map((geometry) => [geometry.start, geometry.end]), constraintTypes: split.constraints.map((constraint) => constraint.type) },
|
||||
extend: { end: endpoint(extended)?.end ?? null, sourceUnchanged: endpoint(line)?.end?.x === 10 },
|
||||
trim: { end: endpoint(trimmed)?.end ?? null, constraintTypes: trimmed.constraints.map((constraint) => constraint.type), sourceUnchanged: endpoint(trimLine)?.end?.x === 10 },
|
||||
construction: { enabled: construction.geometry[0]?.construction === true, sourceUnchanged: line.geometry[0]?.construction !== true },
|
||||
autoConstraint: { suggestionReasons: suggestions.map((suggestion) => suggestion.reason), appliedIds: auto.constraints.map((constraint) => constraint.id), sourceUnchanged: base.constraints.length === 0 },
|
||||
replay: { applied: replay.applied, undone: replay.undone, redone: replay.redone, finalEnd: endpoint(replay.snapshot)?.end ?? null },
|
||||
}
|
||||
const interactionLine = endpoint(interaction.getSnapshot())
|
||||
const interactionState = interaction.getState()
|
||||
report.interactions = { nativeEvents, start: interactionLine?.start ?? null, end: interactionLine?.end ?? null, tool: interactionState.tool, pointerActive: interactionState.pointerActive, applied: interactionState.applied, undone: interactionState.undone, redone: interactionState.redone }
|
||||
report.opfs = { markerSuite: markerPayload.suite, markerOperations: markerPayload.operations, markerRemoved }
|
||||
report.status = report.crossOriginIsolated && report.operations.drag.start?.y === 2 && report.operations.drag.end?.x === 8 && report.operations.drag.end?.y === 2 && report.operations.drag.sourceUnchanged && JSON.stringify(report.operations.split.ids) === JSON.stringify(['line', 'line-2']) && JSON.stringify(report.operations.split.constraintTypes) === JSON.stringify(['coincident']) && report.operations.extend.end?.x === 15 && report.operations.extend.sourceUnchanged && report.operations.trim.end?.x === 7 && JSON.stringify(report.operations.trim.constraintTypes) === JSON.stringify(['pointOnObject']) && report.operations.trim.sourceUnchanged && report.operations.construction.enabled && report.operations.construction.sourceUnchanged && JSON.stringify(report.operations.autoConstraint.suggestionReasons) === JSON.stringify(['coincident', 'horizontal']) && report.operations.autoConstraint.appliedIds.length === 2 && report.operations.autoConstraint.sourceUnchanged && report.operations.replay.applied === 2 && report.operations.replay.undone === 1 && report.operations.replay.redone === 1 && report.operations.replay.finalEnd?.x === 6 && report.interactions.nativeEvents.pointer === 3 && report.interactions.nativeEvents.keyboard === 3 && report.interactions.start?.y === 2 && report.interactions.end?.x === 8 && report.interactions.end?.y === 2 && report.interactions.tool === 'select' && report.interactions.pointerActive === false && report.interactions.applied === 1 && report.interactions.undone === 1 && report.interactions.redone === 1 && report.opfs.markerSuite === 'SK-09' && report.opfs.markerOperations === 9 && report.opfs.markerRemoved ? 'pass' : 'failed'
|
||||
} catch (error) {
|
||||
report.status = 'failed'
|
||||
report.error = error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
output.textContent = JSON.stringify(report, null, 2)
|
||||
await fetch('/__chrome-sketcher-editor-report', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(report) })
|
||||
if (report.status !== 'pass') throw new Error(report.error || 'Chrome Sketcher editor harness failed.')
|
||||
</script>
|
||||
32
public/chrome-sketcher-stress-harness.html
Normal file
32
public/chrome-sketcher-stress-harness.html
Normal file
@@ -0,0 +1,32 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>Chrome Sketcher golden stress harness</title>
|
||||
<pre id="result">running</pre>
|
||||
<script type="module">
|
||||
import { runSketchStress } from '/src/facade/sketchStress.ts'
|
||||
|
||||
const output = document.querySelector('#result')
|
||||
const report = { schemaVersion: 1, browserId: 'chrome', crossOriginIsolated: globalThis.crossOriginIsolated === true, status: 'running' }
|
||||
try {
|
||||
const stress = runSketchStress({ models: 500, seed: 0x20260803 })
|
||||
const markerDirectory = await navigator.storage.getDirectory()
|
||||
const markerName = 'sketcher-stress-chrome-marker.json'
|
||||
const marker = await markerDirectory.getFileHandle(markerName, { create: true })
|
||||
const writable = await marker.createWritable()
|
||||
await writable.write(JSON.stringify({ suite: 'SK-12', models: stress.models, solves: stress.solves }))
|
||||
await writable.close()
|
||||
const markerPayload = JSON.parse(await (await marker.getFile()).text())
|
||||
await markerDirectory.removeEntry(markerName)
|
||||
let markerRemoved = false
|
||||
try { await markerDirectory.getFileHandle(markerName) } catch (error) { markerRemoved = error?.name === 'NotFoundError' }
|
||||
report.stress = stress
|
||||
report.opfs = { markerSuite: markerPayload.suite, markerModels: markerPayload.models, markerSolves: markerPayload.solves, markerRemoved }
|
||||
report.status = report.crossOriginIsolated && stress.models === 500 && stress.solves === 1000 && stress.deterministicMismatches === 0 && stress.unexpectedResults === 0 && stress.maxIterations <= 64 && stress.maxResidual <= 1e-12 && stress.digest === 'edac8aed' && stress.durationMs <= 2_000 && report.opfs.markerSuite === 'SK-12' && report.opfs.markerModels === 500 && report.opfs.markerSolves === 1000 && report.opfs.markerRemoved ? 'pass' : 'failed'
|
||||
} catch (error) {
|
||||
report.status = 'failed'
|
||||
report.error = error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
output.textContent = JSON.stringify(report, null, 2)
|
||||
await fetch('/__chrome-sketcher-stress-report', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(report) })
|
||||
if (report.status !== 'pass') throw new Error(report.error || 'Chrome Sketcher stress harness failed.')
|
||||
</script>
|
||||
5
public/chrome-spreadsheet-harness.html
Normal file
5
public/chrome-spreadsheet-harness.html
Normal file
@@ -0,0 +1,5 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>Chrome Spreadsheet harness</title>
|
||||
<pre id="result">running</pre>
|
||||
<script type="module" src="/src/chromeSpreadsheetHarness.ts"></script>
|
||||
1
public/chrome-surface-harness.html
Normal file
1
public/chrome-surface-harness.html
Normal file
@@ -0,0 +1 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Chrome Surface Harness</title></head><body><pre id="result">running</pre><script type="module" src="/src/chromeSurfaceHarness.ts"></script></body></html>
|
||||
5
public/chrome-techdraw-harness.html
Normal file
5
public/chrome-techdraw-harness.html
Normal file
@@ -0,0 +1,5 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>Chrome TechDraw harness</title>
|
||||
<pre id="result">running</pre>
|
||||
<script type="module" src="/src/chromeTechDrawHarness.ts"></script>
|
||||
59
public/chrome-topology-harness.html
Normal file
59
public/chrome-topology-harness.html
Normal file
@@ -0,0 +1,59 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>Chrome Topology Harness</title>
|
||||
<pre id="result">running</pre>
|
||||
<script type="module">
|
||||
import { BitbybitGeometryRuntime } from '/src/facade/geometryRuntime.ts'
|
||||
|
||||
const output = document.querySelector('#result')
|
||||
const report = { schemaVersion: 1, browserId: 'chrome', crossOriginIsolated: globalThis.crossOriginIsolated === true, status: 'running' }
|
||||
const persistTopologyInOpfs = async (topology) => {
|
||||
if (!navigator.storage?.getDirectory) return false
|
||||
const root = await navigator.storage.getDirectory()
|
||||
const directory = await root.getDirectoryHandle('bitbybit-topology-harness', { create: true })
|
||||
const name = `topology-${crypto.randomUUID()}.json`
|
||||
const payload = JSON.stringify({ adjacency: topology.adjacency, ids: topology.entries.map((entry) => entry.ref.persistentId) })
|
||||
try {
|
||||
const handle = await directory.getFileHandle(name, { create: true })
|
||||
const writable = await handle.createWritable()
|
||||
await writable.write(payload)
|
||||
await writable.close()
|
||||
const restored = JSON.parse(await (await handle.getFile()).text())
|
||||
return JSON.stringify(restored) === JSON.stringify(JSON.parse(payload))
|
||||
} finally {
|
||||
await directory.removeEntry(name).catch(() => {})
|
||||
}
|
||||
}
|
||||
try {
|
||||
const runtime = new BitbybitGeometryRuntime()
|
||||
report.capabilities = await runtime.initialize()
|
||||
const shape = await runtime.createBox({ documentId: 'chrome-topology', documentVersion: 1, width: 10, length: 20, height: 30, originOnCenter: false })
|
||||
try {
|
||||
const topology = await runtime.topology(shape, 0.05)
|
||||
const analyticFaces = topology.entries.filter((entry) => entry.ref.kind === 'face' && entry.signature.analytic?.type === 'Plane').length
|
||||
const analyticEdges = topology.entries.filter((entry) => entry.ref.kind === 'edge' && entry.signature.analytic?.type === 'Line').length
|
||||
const adjacency = topology.adjacency
|
||||
const opfsTopologyRoundTrip = await persistTopologyInOpfs(topology)
|
||||
report.topology = {
|
||||
faces: topology.faces.length,
|
||||
edges: topology.edges.length,
|
||||
vertices: topology.vertices.length,
|
||||
analyticFaces,
|
||||
analyticEdges,
|
||||
adjacencyKeys: adjacency ? Object.values(adjacency).map((map) => Object.keys(map).length) : [],
|
||||
transientIndexFree: topology.entries.every((entry) => !/index|faceIndex|edgeIndex|vertexIndex/i.test(entry.ref.persistentId)),
|
||||
opfsTopologyRoundTrip,
|
||||
}
|
||||
report.status = report.crossOriginIsolated && report.capabilities.status === 'ready' && topology.faces.length === 6 && topology.edges.length === 12 && topology.vertices.length === 8 && analyticFaces === 6 && analyticEdges === 12 && adjacency && Object.values(adjacency).every((map) => Object.keys(map).length > 0) && report.topology.transientIndexFree && opfsTopologyRoundTrip ? 'pass' : 'failed'
|
||||
} finally {
|
||||
await runtime.release(shape)
|
||||
runtime.dispose()
|
||||
}
|
||||
} catch (error) {
|
||||
report.status = 'failed'
|
||||
report.error = error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
output.textContent = JSON.stringify(report)
|
||||
await fetch('/__topology-report', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(report) })
|
||||
if (report.status !== 'pass') throw new Error(report.error || 'Chrome topology harness failed.')
|
||||
</script>
|
||||
13
public/manifest.webmanifest
Normal file
13
public/manifest.webmanifest
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "BitBybit CAD Studio",
|
||||
"short_name": "BitBybit CAD",
|
||||
"description": "Local-first FreeCAD-compatible Web CAD workspace",
|
||||
"start_url": "/start",
|
||||
"scope": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#111519",
|
||||
"theme_color": "#111519",
|
||||
"lang": "en",
|
||||
"categories": ["productivity", "utilities"],
|
||||
"icons": []
|
||||
}
|
||||
22
public/native/planegcs/freecad-planegcs.js
Normal file
22
public/native/planegcs/freecad-planegcs.js
Normal file
File diff suppressed because one or more lines are too long
BIN
public/native/planegcs/freecad-planegcs.wasm
Executable file
BIN
public/native/planegcs/freecad-planegcs.wasm
Executable file
Binary file not shown.
105
public/occt-browser-harness.html
Normal file
105
public/occt-browser-harness.html
Normal file
@@ -0,0 +1,105 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>OCCT Browser Harness</title>
|
||||
<pre id="result">running</pre>
|
||||
<script type="module">
|
||||
const output = document.querySelector('#result')
|
||||
let lastWorkerEvent = null
|
||||
const MiB = 1024 * 1024
|
||||
const memoryBudgets = { wasmPeakBytes: 256 * MiB, wasmRetainedBytes: 16 * MiB, browserRetainedBytes: 64 * MiB, gpuLiveResources: 0 }
|
||||
const measureBrowserMemory = async () => {
|
||||
if (typeof performance.measureUserAgentSpecificMemory === 'function') {
|
||||
try {
|
||||
const measurement = await performance.measureUserAgentSpecificMemory()
|
||||
return { api: 'measureUserAgentSpecificMemory', bytes: measurement.bytes }
|
||||
} catch (error) {
|
||||
// Chrome may expose the API while disabling it in a particular headless mode.
|
||||
}
|
||||
}
|
||||
if (performance.memory && Number.isFinite(performance.memory.usedJSHeapSize)) return { api: 'performance.memory.usedJSHeapSize', bytes: performance.memory.usedJSHeapSize }
|
||||
return { api: 'unavailable', bytes: null }
|
||||
}
|
||||
const waitFor = (worker, type, timeoutMs = 180000) => new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => reject(new Error(`${type} timeout; last=${JSON.stringify(lastWorkerEvent)}`)), timeoutMs)
|
||||
const listener = (event) => {
|
||||
lastWorkerEvent = event.data
|
||||
if (event.data.type === 'error') { clearTimeout(timer); worker.removeEventListener('message', listener); reject(new Error(event.data.error)); return }
|
||||
if (event.data.type !== type) return
|
||||
clearTimeout(timer); worker.removeEventListener('message', listener); resolve(event.data)
|
||||
}
|
||||
const errorListener = (event) => { clearTimeout(timer); worker.removeEventListener('message', listener); worker.removeEventListener('error', errorListener); reject(new Error(event.message || `${type} worker error`)) }
|
||||
worker.addEventListener('message', listener)
|
||||
worker.addEventListener('error', errorListener)
|
||||
})
|
||||
|
||||
const run = async () => {
|
||||
const browserId = new URL(location.href).searchParams.get('browser') || 'unknown'
|
||||
const result = { schemaVersion: 1, browserId, crossOriginIsolated: globalThis.crossOriginIsolated === true, browser: navigator.userAgent, opfs: 'pending', initialize: 'pending', history: 'pending', memory: 'pending', abort: 'pending', crashRecovery: 'pending' }
|
||||
const verifyOpfs = async () => {
|
||||
const opfsRoot = await navigator.storage.getDirectory()
|
||||
const opfsDirectory = await opfsRoot.getDirectoryHandle('bitbybit-browser-gate', { create: true })
|
||||
const opfsFile = await opfsDirectory.getFileHandle(`${browserId}.probe`, { create: true })
|
||||
const opfsWritable = await opfsFile.createWritable()
|
||||
await opfsWritable.write(`OPFS-${browserId}`)
|
||||
await opfsWritable.close()
|
||||
const opfsText = await (await opfsFile.getFile()).text()
|
||||
if (opfsText !== `OPFS-${browserId}`) throw new Error('OPFS read-after-write verification failed.')
|
||||
await opfsDirectory.removeEntry(`${browserId}.probe`)
|
||||
return { status: 'pass', persistence: 'opfs', bytes: opfsText.length }
|
||||
}
|
||||
try {
|
||||
result.opfs = await Promise.race([verifyOpfs(), new Promise((_, reject) => setTimeout(() => reject(new Error('OPFS API did not respond within the browser capability window.')), 5000))])
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
let worker = new Worker('/native/occt-history/browser-harness-worker.js', { type: 'module' })
|
||||
worker.postMessage({ type: 'initialize' })
|
||||
const ready = await waitFor(worker, 'ready')
|
||||
result.initialize = ready.occtVersion
|
||||
worker.postMessage({ type: 'history', requestId: 'history-1', operation: 'cut' })
|
||||
const history = await waitFor(worker, 'history')
|
||||
result.history = { provider: history.provider, recordCount: history.recordCount }
|
||||
const stressIterations = 1000
|
||||
const browserBefore = await measureBrowserMemory()
|
||||
worker.postMessage({ type: 'stress', requestId: 'stress-1', iterations: stressIterations })
|
||||
const stress = await waitFor(worker, 'stress')
|
||||
const browserAfter = await measureBrowserMemory()
|
||||
const browserRetainedBytes = browserBefore.bytes === null || browserAfter.bytes === null ? null : browserAfter.bytes - browserBefore.bytes
|
||||
result.memory = {
|
||||
iterations: stress.iterations,
|
||||
budgets: memoryBudgets,
|
||||
wasm: stress.wasm,
|
||||
browser: { before: browserBefore, after: browserAfter, retainedBytes: browserRetainedBytes },
|
||||
gpu: stress.gpu,
|
||||
ownership: stress.ownership
|
||||
}
|
||||
result.memory.withinBudget =
|
||||
stress.wasm.peakHeapBytes <= memoryBudgets.wasmPeakBytes &&
|
||||
stress.wasm.retainedBytes <= memoryBudgets.wasmRetainedBytes &&
|
||||
browserRetainedBytes !== null && browserRetainedBytes <= memoryBudgets.browserRetainedBytes &&
|
||||
stress.gpu.live <= memoryBudgets.gpuLiveResources &&
|
||||
stress.ownership.live === 0 && stress.ownership.created === stress.ownership.released
|
||||
worker.postMessage({ type: 'cancel', requestId: 'cancel-1' })
|
||||
const cancelled = await waitFor(worker, 'cancelled')
|
||||
result.abort = cancelled.requestId === 'cancel-1' ? 'recovered' : 'failed'
|
||||
worker.terminate()
|
||||
worker = new Worker('/native/occt-history/browser-harness-worker.js', { type: 'module' })
|
||||
worker.postMessage({ type: 'initialize' })
|
||||
const restarted = await waitFor(worker, 'ready')
|
||||
result.crashRecovery = restarted.occtVersion === ready.occtVersion ? 'recovered' : 'failed'
|
||||
worker.terminate()
|
||||
if (!result.crossOriginIsolated || result.opfs?.status !== 'pass' || result.opfs.persistence !== 'opfs' || result.abort !== 'recovered' || result.crashRecovery !== 'recovered' || result.memory.iterations !== stressIterations || result.memory.withinBudget !== true) throw new Error('Browser isolation, OPFS, Worker recovery or memory budget contract failed.')
|
||||
return result
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await run()
|
||||
output.textContent = `BROWSER_OCCT_RESULT=${JSON.stringify(result)}`
|
||||
await fetch('/__occt-report', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(result) })
|
||||
} catch (error) {
|
||||
const result = { status: 'failed', error: error instanceof Error ? error.message : String(error) }
|
||||
output.textContent = `BROWSER_OCCT_RESULT=${JSON.stringify(result)}`
|
||||
await fetch('/__occt-report', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(result) })
|
||||
throw error
|
||||
}
|
||||
</script>
|
||||
455
public/planegcs-browser-harness.html
Normal file
455
public/planegcs-browser-harness.html
Normal file
@@ -0,0 +1,455 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>planegcs browser harness</title></head>
|
||||
<body>
|
||||
<pre id="status">loading</pre>
|
||||
<script type="module">
|
||||
import { createSketch } from '/src/facade/sketcher.ts'
|
||||
import { PlanegcsWorkerProvider } from '/src/facade/planegcsWorkerClient.ts'
|
||||
import { SketchSolverCoordinator } from '/src/facade/sketchSolverProtocol.ts'
|
||||
|
||||
const browserId = new URL(location.href).searchParams.get('browser') || 'chrome'
|
||||
const report = { schemaVersion: 1, browserId, executionContext: 'dedicated-worker', crossOriginIsolated, sharedArrayBuffer: typeof SharedArrayBuffer === 'function', status: 'failed' }
|
||||
const worker = new Worker('/planegcs-browser-worker.js', { type: 'module', name: 'freecad-planegcs-verification' })
|
||||
const provider = new PlanegcsWorkerProvider()
|
||||
try {
|
||||
const requestId = crypto.randomUUID()
|
||||
const response = await new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => reject(new Error('planegcs Worker solve timed out.')), 60_000)
|
||||
worker.addEventListener('message', (event) => {
|
||||
if (event.data?.requestId !== requestId) return
|
||||
clearTimeout(timeout)
|
||||
if (event.data.ok) resolve(event.data)
|
||||
else reject(new Error(event.data.error || 'planegcs Worker solve failed.'))
|
||||
})
|
||||
worker.addEventListener('error', (event) => {
|
||||
clearTimeout(timeout)
|
||||
reject(new Error(event.message || 'planegcs Worker crashed.'))
|
||||
}, { once: true })
|
||||
worker.postMessage({ requestId, operation: 'solve-horizontal-distance', input: { startX: 0, startY: 0, endX: 3, endY: 4, targetLength: 5 } })
|
||||
})
|
||||
const values = response.values
|
||||
const [solveStatus, startX, startY, endX, endY, residual] = values
|
||||
const nativePassed = solveStatus <= 1 && Math.abs(startY - endY) <= 1e-7 && Math.abs(Math.hypot(endX - startX, endY - startY) - 5) <= 1e-7 && Math.abs(residual) <= 1e-7
|
||||
|
||||
const verticalRequestId = crypto.randomUUID()
|
||||
const verticalResponse = await new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => reject(new Error('planegcs vertical Worker solve timed out.')), 60_000)
|
||||
const onMessage = (event) => {
|
||||
if (event.data?.requestId !== verticalRequestId) return
|
||||
clearTimeout(timeout)
|
||||
worker.removeEventListener('message', onMessage)
|
||||
if (event.data.ok) resolve(event.data)
|
||||
else reject(new Error(event.data.error || 'planegcs vertical Worker solve failed.'))
|
||||
}
|
||||
worker.addEventListener('message', onMessage)
|
||||
worker.postMessage({ requestId: verticalRequestId, operation: 'solve-vertical-distance', input: { startX: 0, startY: 0, endX: 4, endY: 3, targetLength: 5 } })
|
||||
})
|
||||
const verticalValues = verticalResponse.values
|
||||
const [verticalSolveStatus, verticalStartX, verticalStartY, verticalEndX, verticalEndY, verticalResidual] = verticalValues
|
||||
const verticalPassed = verticalSolveStatus <= 1 && Math.abs(verticalStartX - verticalEndX) <= 1e-7 && Math.abs(Math.hypot(verticalEndX - verticalStartX, verticalEndY - verticalStartY) - 5) <= 1e-7 && Math.abs(verticalResidual) <= 1e-7
|
||||
|
||||
const distanceXRequestId = crypto.randomUUID()
|
||||
const distanceXResponse = await new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => reject(new Error('planegcs DistanceX Worker solve timed out.')), 60_000)
|
||||
const onMessage = (event) => {
|
||||
if (event.data?.requestId !== distanceXRequestId) return
|
||||
clearTimeout(timeout)
|
||||
worker.removeEventListener('message', onMessage)
|
||||
if (event.data.ok) resolve(event.data)
|
||||
else reject(new Error(event.data.error || 'planegcs DistanceX Worker solve failed.'))
|
||||
}
|
||||
worker.addEventListener('message', onMessage)
|
||||
worker.postMessage({ requestId: distanceXRequestId, operation: 'solve-distance-x', input: { startX: 0, startY: 0, endX: 3, endY: 4, targetDistance: 6 } })
|
||||
})
|
||||
const distanceXValues = distanceXResponse.values
|
||||
const [distanceXSolveStatus, distanceXStartX, distanceXStartY, distanceXEndX, distanceXEndY, distanceXResidual] = distanceXValues
|
||||
const distanceXPassed = distanceXSolveStatus <= 1 && Math.abs(distanceXEndX - distanceXStartX - 6) <= 1e-7 && Math.abs(distanceXStartY - distanceXEndY) <= 1e-7 && Math.abs(distanceXResidual) <= 1e-7
|
||||
const distanceYRequestId = crypto.randomUUID()
|
||||
const distanceYResponse = await new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => reject(new Error('planegcs DistanceY Worker solve timed out.')), 60_000)
|
||||
const onMessage = (event) => {
|
||||
if (event.data?.requestId !== distanceYRequestId) return
|
||||
clearTimeout(timeout)
|
||||
worker.removeEventListener('message', onMessage)
|
||||
if (event.data.ok) resolve(event.data)
|
||||
else reject(new Error(event.data.error || 'planegcs DistanceY Worker solve failed.'))
|
||||
}
|
||||
worker.addEventListener('message', onMessage)
|
||||
worker.postMessage({ requestId: distanceYRequestId, operation: 'solve-distance-y', input: { startX: 0, startY: 0, endX: 3, endY: 4, targetDistance: 6 } })
|
||||
})
|
||||
const distanceYValues = distanceYResponse.values
|
||||
const [distanceYSolveStatus, distanceYStartX, distanceYStartY, distanceYEndX, distanceYEndY, distanceYResidual] = distanceYValues
|
||||
const distanceYPassed = distanceYSolveStatus <= 1 && Math.abs(distanceYEndY - distanceYStartY - 6) <= 1e-7 && Math.abs(distanceYStartX - distanceYEndX) <= 1e-7 && Math.abs(distanceYResidual) <= 1e-7
|
||||
|
||||
const angleRequestId = crypto.randomUUID()
|
||||
const angleResponse = await new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => reject(new Error('planegcs Angle Worker solve timed out.')), 60_000)
|
||||
const onMessage = (event) => {
|
||||
if (event.data?.requestId !== angleRequestId) return
|
||||
clearTimeout(timeout)
|
||||
worker.removeEventListener('message', onMessage)
|
||||
if (event.data.ok) resolve(event.data)
|
||||
else reject(new Error(event.data.error || 'planegcs Angle Worker solve failed.'))
|
||||
}
|
||||
worker.addEventListener('message', onMessage)
|
||||
worker.postMessage({ requestId: angleRequestId, operation: 'solve-angle', input: { startX: 0, startY: 0, endX: 3, endY: 4, targetLength: 5, targetAngle: Math.PI / 4 } })
|
||||
})
|
||||
const angleValues = angleResponse.values
|
||||
const [angleSolveStatus, angleStartX, angleStartY, angleEndX, angleEndY, angleLengthResidual, angleResidual] = angleValues
|
||||
const anglePassed = angleSolveStatus <= 1 && Math.abs(Math.hypot(angleEndX - angleStartX, angleEndY - angleStartY) - 5) <= 1e-7 && Math.abs(Math.atan2(angleEndY - angleStartY, angleEndX - angleStartX) - Math.PI / 4) <= 1e-7 && Math.abs(angleLengthResidual) <= 1e-7 && Math.abs(angleResidual) <= 1e-7
|
||||
|
||||
const circleRadiusRequestId = crypto.randomUUID()
|
||||
const circleRadiusResponse = await new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => reject(new Error('planegcs Circle Radius Worker solve timed out.')), 60_000)
|
||||
const onMessage = (event) => {
|
||||
if (event.data?.requestId !== circleRadiusRequestId) return
|
||||
clearTimeout(timeout)
|
||||
worker.removeEventListener('message', onMessage)
|
||||
if (event.data.ok) resolve(event.data)
|
||||
else reject(new Error(event.data.error || 'planegcs Circle Radius Worker solve failed.'))
|
||||
}
|
||||
worker.addEventListener('message', onMessage)
|
||||
worker.postMessage({ requestId: circleRadiusRequestId, operation: 'solve-circle-radius', input: { centerX: 2, centerY: 3, radius: 1, targetRadius: 4 } })
|
||||
})
|
||||
const circleRadiusValues = circleRadiusResponse.values
|
||||
const [circleRadiusSolveStatus, circleCenterX, circleCenterY, circleRadius, circleRadiusResidual] = circleRadiusValues
|
||||
const circleRadiusPassed = circleRadiusSolveStatus <= 1 && Math.abs(circleCenterX - 2) <= 1e-7 && Math.abs(circleCenterY - 3) <= 1e-7 && Math.abs(circleRadius - 4) <= 1e-7 && Math.abs(circleRadiusResidual) <= 1e-7
|
||||
|
||||
const circleDiameterRequestId = crypto.randomUUID()
|
||||
const circleDiameterResponse = await new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => reject(new Error('planegcs Circle Diameter Worker solve timed out.')), 60_000)
|
||||
const onMessage = (event) => {
|
||||
if (event.data?.requestId !== circleDiameterRequestId) return
|
||||
clearTimeout(timeout)
|
||||
worker.removeEventListener('message', onMessage)
|
||||
if (event.data.ok) resolve(event.data)
|
||||
else reject(new Error(event.data.error || 'planegcs Circle Diameter Worker solve failed.'))
|
||||
}
|
||||
worker.addEventListener('message', onMessage)
|
||||
worker.postMessage({ requestId: circleDiameterRequestId, operation: 'solve-circle-diameter', input: { centerX: 2, centerY: 3, radius: 1, targetDiameter: 8 } })
|
||||
})
|
||||
const circleDiameterValues = circleDiameterResponse.values
|
||||
const [circleDiameterSolveStatus, circleDiameterCenterX, circleDiameterCenterY, circleDiameterRadius, circleDiameterResidual] = circleDiameterValues
|
||||
const circleDiameterPassed = circleDiameterSolveStatus <= 1 && Math.abs(circleDiameterCenterX - 2) <= 1e-7 && Math.abs(circleDiameterCenterY - 3) <= 1e-7 && Math.abs(circleDiameterRadius * 2 - 8) <= 1e-7 && Math.abs(circleDiameterResidual) <= 1e-7
|
||||
|
||||
const relationInput = { firstStartX: 0, firstStartY: 0, firstEndX: 4, firstEndY: 0, secondStartX: 0, secondStartY: 2, secondEndX: 3, secondEndY: 6, secondLength: 5 }
|
||||
const relationRequest = (operation, requestId, input = relationInput) => new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => reject(new Error(`planegcs ${operation} Worker solve timed out.`)), 60_000)
|
||||
const onMessage = (event) => {
|
||||
if (event.data?.requestId !== requestId) return
|
||||
clearTimeout(timeout)
|
||||
worker.removeEventListener('message', onMessage)
|
||||
if (event.data.ok) resolve(event.data)
|
||||
else reject(new Error(event.data.error || `planegcs ${operation} Worker solve failed.`))
|
||||
}
|
||||
worker.addEventListener('message', onMessage)
|
||||
worker.postMessage({ requestId, operation, input })
|
||||
})
|
||||
const equalLinesResponse = await relationRequest('solve-equal-lines', crypto.randomUUID())
|
||||
const equalLinesValues = equalLinesResponse.values
|
||||
const [equalLinesSolveStatus, equalLinesFirstStartX, equalLinesFirstStartY, equalLinesFirstEndX, equalLinesFirstEndY, equalLinesSecondStartX, equalLinesSecondStartY, equalLinesSecondEndX, equalLinesSecondEndY, equalLinesResidual] = equalLinesValues
|
||||
const equalLinesPassed = equalLinesSolveStatus <= 1 && Math.abs(Math.hypot(equalLinesFirstEndX - equalLinesFirstStartX, equalLinesFirstEndY - equalLinesFirstStartY) - Math.hypot(equalLinesSecondEndX - equalLinesSecondStartX, equalLinesSecondEndY - equalLinesSecondStartY)) <= 1e-7 && Math.abs(equalLinesResidual) <= 1e-7
|
||||
const equalCirclesResponse = await relationRequest('solve-equal-circles', crypto.randomUUID(), { firstCenterX: 2, firstCenterY: 3, firstRadius: 4, secondCenterX: 8, secondCenterY: 9, secondRadius: 1 })
|
||||
const equalCirclesValues = equalCirclesResponse.values
|
||||
const [equalCirclesSolveStatus, equalCirclesFirstCenterX, equalCirclesFirstCenterY, equalCirclesFirstRadius, equalCirclesSecondCenterX, equalCirclesSecondCenterY, equalCirclesSecondRadius, equalCirclesResidual] = equalCirclesValues
|
||||
const equalCirclesPassed = equalCirclesSolveStatus <= 1 && Math.abs(equalCirclesFirstCenterX - 2) <= 1e-7 && Math.abs(equalCirclesFirstCenterY - 3) <= 1e-7 && Math.abs(equalCirclesFirstRadius - 4) <= 1e-7 && Math.abs(equalCirclesSecondCenterX - 8) <= 1e-7 && Math.abs(equalCirclesSecondCenterY - 9) <= 1e-7 && Math.abs(equalCirclesSecondRadius - 4) <= 1e-7 && Math.abs(equalCirclesResidual) <= 1e-7
|
||||
const tangentCirclesResponse = await relationRequest('solve-tangent-circles', crypto.randomUUID(), { firstCenterX: 2, firstCenterY: 3, firstRadius: 4, secondCenterX: 10, secondCenterY: 3, secondRadius: 1 })
|
||||
const tangentCirclesValues = tangentCirclesResponse.values
|
||||
const [tangentCirclesSolveStatus, tangentCirclesFirstCenterX, tangentCirclesFirstCenterY, tangentCirclesFirstRadius, tangentCirclesSecondCenterX, tangentCirclesSecondCenterY, tangentCirclesSecondRadius, tangentCirclesResidual] = tangentCirclesValues
|
||||
const tangentCirclesPassed = tangentCirclesSolveStatus <= 1 && Math.abs(tangentCirclesFirstCenterX - 2) <= 1e-7 && Math.abs(tangentCirclesFirstCenterY - 3) <= 1e-7 && Math.abs(tangentCirclesFirstRadius - 4) <= 1e-7 && Math.abs(tangentCirclesSecondCenterX - 10) <= 1e-7 && Math.abs(tangentCirclesSecondCenterY - 3) <= 1e-7 && Math.abs(tangentCirclesSecondRadius - 4) <= 1e-7 && Math.abs(tangentCirclesResidual) <= 1e-7
|
||||
const pointSymmetryResponse = await relationRequest('solve-point-symmetry', crypto.randomUUID(), { firstX: 1, firstY: 2, secondX: 0, secondY: 0, centerX: 4, centerY: 6 })
|
||||
const pointSymmetryValues = pointSymmetryResponse.values
|
||||
const [pointSymmetrySolveStatus, pointSymmetryFirstX, pointSymmetryFirstY, pointSymmetrySecondX, pointSymmetrySecondY, pointSymmetryCenterX, pointSymmetryCenterY, pointSymmetryResidual] = pointSymmetryValues
|
||||
const pointSymmetryPassed = pointSymmetrySolveStatus <= 1 && Math.abs(pointSymmetryFirstX - 1) <= 1e-7 && Math.abs(pointSymmetryFirstY - 2) <= 1e-7 && Math.abs(pointSymmetrySecondX - 7) <= 1e-7 && Math.abs(pointSymmetrySecondY - 10) <= 1e-7 && Math.abs(pointSymmetryCenterX - 4) <= 1e-7 && Math.abs(pointSymmetryCenterY - 6) <= 1e-7 && Math.abs(pointSymmetryResidual) <= 1e-7
|
||||
const pointOnLineResponse = await relationRequest('solve-point-on-line', crypto.randomUUID(), { pointX: 2, pointY: 3, startX: 0, startY: 0, endX: 4, endY: 0, vertical: false })
|
||||
const pointOnLineValues = pointOnLineResponse.values
|
||||
const [pointOnLineSolveStatus, pointOnLineX, pointOnLineY, pointOnLineStartX, pointOnLineStartY, pointOnLineEndX, pointOnLineEndY, pointOnLineResidual] = pointOnLineValues
|
||||
const pointOnLinePassed = pointOnLineSolveStatus <= 1 && Math.abs(pointOnLineX - 2) <= 1e-7 && Math.abs(pointOnLineY) <= 1e-7 && Math.abs(pointOnLineStartX) <= 1e-7 && Math.abs(pointOnLineStartY) <= 1e-7 && Math.abs(pointOnLineEndX - 4) <= 1e-7 && Math.abs(pointOnLineEndY) <= 1e-7 && Math.abs(pointOnLineResidual) <= 1e-7
|
||||
const pointOnCircleResponse = await relationRequest('solve-point-on-circle', crypto.randomUUID(), { pointX: 4, pointY: 6, centerX: 2, centerY: 3, radius: 4 })
|
||||
const pointOnCircleValues = pointOnCircleResponse.values
|
||||
const [pointOnCircleSolveStatus, pointOnCircleX, pointOnCircleY, pointOnCircleCenterX, pointOnCircleCenterY, pointOnCircleRadius, pointOnCircleResidual] = pointOnCircleValues
|
||||
const pointOnCirclePassed = pointOnCircleSolveStatus <= 1 && Math.abs(pointOnCircleX - 4) <= 1e-7 && Math.abs(pointOnCircleY - 6.464101615137754) <= 1e-7 && Math.abs(pointOnCircleCenterX - 2) <= 1e-7 && Math.abs(pointOnCircleCenterY - 3) <= 1e-7 && Math.abs(pointOnCircleRadius - 4) <= 1e-7 && Math.abs(pointOnCircleResidual) <= 1e-7
|
||||
const pointOnArcResponse = await relationRequest('solve-point-on-arc', crypto.randomUUID(), { pointX: 2, pointY: 6, centerX: 2, centerY: 3, radius: 4, startAngle: 0, endAngle: Math.PI })
|
||||
const pointOnArcValues = pointOnArcResponse.values
|
||||
const [pointOnArcSolveStatus, pointOnArcX, pointOnArcY, pointOnArcCenterX, pointOnArcCenterY, pointOnArcRadius, pointOnArcStartAngle, pointOnArcEndAngle, pointOnArcResidual] = pointOnArcValues
|
||||
const pointOnArcPassed = pointOnArcSolveStatus <= 1 && Math.abs(pointOnArcX - 2) <= 1e-7 && Math.abs(pointOnArcY - 7) <= 1e-7 && Math.abs(pointOnArcCenterX - 2) <= 1e-7 && Math.abs(pointOnArcCenterY - 3) <= 1e-7 && Math.abs(pointOnArcRadius - 4) <= 1e-7 && Math.abs(pointOnArcStartAngle) <= 1e-7 && Math.abs(pointOnArcEndAngle - Math.PI) <= 1e-7 && Math.abs(pointOnArcResidual) <= 1e-7
|
||||
const pointOnEllipseResponse = await relationRequest('solve-point-on-ellipse', crypto.randomUUID(), { pointX: 2, pointY: 7, centerX: 2, centerY: 3, focusX: 6, focusY: 3, minorRadius: 3 })
|
||||
const pointOnEllipseValues = pointOnEllipseResponse.values
|
||||
const [pointOnEllipseSolveStatus, pointOnEllipseX, pointOnEllipseY, pointOnEllipseCenterX, pointOnEllipseCenterY, pointOnEllipseFocusX, pointOnEllipseFocusY, pointOnEllipseMinorRadius, pointOnEllipseResidual] = pointOnEllipseValues
|
||||
const pointOnEllipsePassed = pointOnEllipseSolveStatus <= 1 && Math.abs(pointOnEllipseX - 2) <= 1e-7 && Math.abs(pointOnEllipseY - 6) <= 1e-7 && Math.abs(pointOnEllipseCenterX - 2) <= 1e-7 && Math.abs(pointOnEllipseCenterY - 3) <= 1e-7 && Math.abs(pointOnEllipseFocusX - 6) <= 1e-7 && Math.abs(pointOnEllipseFocusY - 3) <= 1e-7 && Math.abs(pointOnEllipseMinorRadius - 3) <= 1e-7 && Math.abs(pointOnEllipseResidual) <= 1e-7
|
||||
const pointOnBsplineResponse = await relationRequest('solve-point-on-cubic-bspline', crypto.randomUUID(), { pointX: 2, pointY: 1, pole0X: 0, pole0Y: 0, pole1X: 1, pole1Y: 2, pole2X: 3, pole2Y: 2, pole3X: 4, pole3Y: 0, pointParameter: 0.4 })
|
||||
const pointOnBsplineValues = pointOnBsplineResponse.values
|
||||
const [pointOnBsplineSolveStatus, pointOnBsplineX, pointOnBsplineY, pointOnBsplineParameter, pointOnBsplineResidualX, pointOnBsplineResidualY] = pointOnBsplineValues
|
||||
const pointOnBsplinePassed = pointOnBsplineSolveStatus <= 1 && Math.abs(pointOnBsplineX - 2) <= 1e-7 && Math.abs(pointOnBsplineY - 1.5) <= 1e-7 && Math.abs(pointOnBsplineParameter - 0.5) <= 1e-7 && Math.abs(pointOnBsplineResidualX) <= 1e-7 && Math.abs(pointOnBsplineResidualY) <= 1e-7
|
||||
const bsplineWeightResponse = await relationRequest('solve-cubic-bspline-weight', crypto.randomUUID(), { pole0X: 0, pole0Y: 0, pole1X: 1, pole1Y: 2, pole2X: 3, pole2Y: 2, pole3X: 4, pole3Y: 0, weight0: 1, weight1: 0.75, weight2: 1.25, weight3: 1, controlPointIndex: 1, targetWeight: 1.5 })
|
||||
const [bsplineWeightSolveStatus, bsplineWeight0, bsplineWeight1, bsplineWeight2, bsplineWeight3, bsplineWeightHelperX, bsplineWeightHelperY, bsplineWeightHelperRadius, bsplineWeightAlignmentResidual, bsplineWeightResidual] = bsplineWeightResponse.values
|
||||
const bsplineWeightPassed = bsplineWeightSolveStatus <= 1 && Math.abs(bsplineWeight0 - 1) <= 1e-7 && Math.abs(bsplineWeight1 - 1.5) <= 1e-7 && Math.abs(bsplineWeight2 - 1.25) <= 1e-7 && Math.abs(bsplineWeight3 - 1) <= 1e-7 && Math.abs(bsplineWeightHelperX - 1) <= 1e-7 && Math.abs(bsplineWeightHelperY - 2) <= 1e-7 && Math.abs(bsplineWeightHelperRadius - 1.5) <= 1e-7 && Math.abs(bsplineWeightAlignmentResidual) <= 1e-7 && Math.abs(bsplineWeightResidual) <= 1e-7
|
||||
const parallelResponse = await relationRequest('solve-parallel-lines', crypto.randomUUID())
|
||||
const parallelValues = parallelResponse.values
|
||||
const [parallelSolveStatus, parallelFirstStartX, parallelFirstStartY, parallelFirstEndX, parallelFirstEndY, parallelSecondStartX, parallelSecondStartY, parallelSecondEndX, parallelSecondEndY, parallelResidual] = parallelValues
|
||||
const parallelPassed = parallelSolveStatus <= 1 && Math.abs(parallelFirstEndY - parallelFirstStartY) <= 1e-7 && Math.abs(parallelSecondEndY - parallelSecondStartY) <= 1e-7 && Math.abs(parallelSecondEndX - 5) <= 1e-7 && Math.abs(parallelResidual) <= 1e-7
|
||||
const perpendicularResponse = await relationRequest('solve-perpendicular-lines', crypto.randomUUID())
|
||||
const perpendicularValues = perpendicularResponse.values
|
||||
const [perpendicularSolveStatus, perpendicularFirstStartX, perpendicularFirstStartY, perpendicularFirstEndX, perpendicularFirstEndY, perpendicularSecondStartX, perpendicularSecondStartY, perpendicularSecondEndX, perpendicularSecondEndY, perpendicularResidual] = perpendicularValues
|
||||
const perpendicularPassed = perpendicularSolveStatus <= 1 && Math.abs(perpendicularFirstEndY - perpendicularFirstStartY) <= 1e-7 && Math.abs(perpendicularSecondEndX - perpendicularSecondStartX) <= 1e-7 && Math.abs(perpendicularSecondEndY - 7) <= 1e-7 && Math.abs(perpendicularResidual) <= 1e-7
|
||||
const coincidentResponse = await relationRequest('solve-coincident-lines', crypto.randomUUID())
|
||||
const coincidentValues = coincidentResponse.values
|
||||
const [coincidentSolveStatus, coincidentFirstStartX, coincidentFirstStartY, coincidentFirstEndX, coincidentFirstEndY, coincidentSecondStartX, coincidentSecondStartY, coincidentSecondEndX, coincidentSecondEndY, coincidentResidual] = coincidentValues
|
||||
const coincidentPassed = coincidentSolveStatus <= 1 && Math.abs(coincidentFirstEndX - coincidentSecondStartX) <= 1e-7 && Math.abs(coincidentFirstEndY - coincidentSecondStartY) <= 1e-7 && Math.abs(coincidentResidual) <= 1e-7
|
||||
const coincidentEndpointSolves = []
|
||||
for (const firstPoint of ['start', 'end']) {
|
||||
for (const secondPoint of ['start', 'end']) {
|
||||
const firstEnd = firstPoint === 'end'
|
||||
const secondEnd = secondPoint === 'end'
|
||||
const endpointResponse = await relationRequest('solve-coincident-line-points', crypto.randomUUID(), { ...relationInput, firstEnd, secondEnd })
|
||||
const [endpointSolveStatus, endpointFirstStartX, endpointFirstStartY, endpointFirstEndX, endpointFirstEndY, endpointSecondStartX, endpointSecondStartY, endpointSecondEndX, endpointSecondEndY, endpointResidual] = endpointResponse.values
|
||||
const first = firstEnd ? { x: endpointFirstEndX, y: endpointFirstEndY } : { x: endpointFirstStartX, y: endpointFirstStartY }
|
||||
const second = secondEnd ? { x: endpointSecondEndX, y: endpointSecondEndY } : { x: endpointSecondStartX, y: endpointSecondStartY }
|
||||
coincidentEndpointSolves.push({ firstPoint, secondPoint, solveStatus: endpointSolveStatus, first, second, secondLength: Math.hypot(endpointSecondEndX - endpointSecondStartX, endpointSecondEndY - endpointSecondStartY), residual: endpointResidual })
|
||||
}
|
||||
}
|
||||
const coincidentEndpointSolvesPassed = coincidentEndpointSolves.length === 4 && coincidentEndpointSolves.every((entry) => entry.solveStatus <= 1 && Math.abs(entry.first.x - entry.second.x) <= 1e-7 && Math.abs(entry.first.y - entry.second.y) <= 1e-7 && Math.abs(entry.secondLength - 5) <= 1e-7 && Math.abs(entry.residual) <= 1e-7)
|
||||
const snellsLawResponse = await relationRequest('solve-snells-law-lines', crypto.randomUUID(), { firstStartX: 0, firstStartY: 0, firstEndX: -4, firstEndY: 4, secondStartX: 0, secondStartY: 0, secondEndX: 4, secondEndY: 3, boundaryStartX: -5, boundaryStartY: 0, boundaryEndX: 5, boundaryEndY: 0, refractiveRatio: 1.5, firstEnd: false, secondEnd: false })
|
||||
const [snellsLawSolveStatus, snellsFirstStartX, snellsFirstStartY, snellsFirstEndX, snellsFirstEndY, snellsSecondStartX, snellsSecondStartY, snellsSecondEndX, snellsSecondEndY, snellsBoundaryStartX, snellsBoundaryStartY, snellsBoundaryEndX, snellsBoundaryEndY, snellsLawResidual] = snellsLawResponse.values
|
||||
const snellsLawPassed = snellsLawSolveStatus <= 1 && Math.abs(snellsFirstStartX - snellsSecondStartX) <= 1e-7 && Math.abs(snellsFirstStartY - snellsSecondStartY) <= 1e-7 && Math.abs(Math.hypot(snellsSecondEndX - snellsSecondStartX, snellsSecondEndY - snellsSecondStartY) - 5) <= 1e-7 && Math.abs(snellsBoundaryStartY) <= 1e-7 && Math.abs(snellsBoundaryEndY) <= 1e-7 && Math.abs(snellsLawResidual) <= 1e-7
|
||||
const ellipseInternalAlignmentSolves = []
|
||||
for (const alignmentType of [1, 2, 3, 4]) {
|
||||
const response = await relationRequest('solve-ellipse-internal-alignment', crypto.randomUUID(), { centerX: 2, centerY: 3, majorRadius: 5, minorRadius: 3, rotation: 0.25, alignmentType })
|
||||
const [solveStatus, centerX, centerY, focusX, focusY, minorRadius, helperStartX, helperStartY, helperEndX, helperEndY, residual] = response.values
|
||||
const focalDistance = 4
|
||||
const major = { x: Math.cos(0.25), y: Math.sin(0.25) }
|
||||
const minor = { x: -major.y, y: major.x }
|
||||
const expectedStart = alignmentType === 1 ? { x: 2 + 5 * major.x, y: 3 + 5 * major.y } : alignmentType === 2 ? { x: 2 + 3 * minor.x, y: 3 + 3 * minor.y } : { x: 2 + (alignmentType === 3 ? 1 : -1) * focalDistance * major.x, y: 3 + (alignmentType === 3 ? 1 : -1) * focalDistance * major.y }
|
||||
const expectedEnd = alignmentType === 1 ? { x: 2 - 5 * major.x, y: 3 - 5 * major.y } : alignmentType === 2 ? { x: 2 - 3 * minor.x, y: 3 - 3 * minor.y } : expectedStart
|
||||
const helperError = Math.max(Math.abs(helperStartX - expectedStart.x), Math.abs(helperStartY - expectedStart.y), Math.abs(helperEndX - expectedEnd.x), Math.abs(helperEndY - expectedEnd.y))
|
||||
ellipseInternalAlignmentSolves.push({ alignmentType, solveStatus, center: { x: centerX, y: centerY }, focus: { x: focusX, y: focusY }, minorRadius, helperStart: { x: helperStartX, y: helperStartY }, helperEnd: { x: helperEndX, y: helperEndY }, helperError, residual })
|
||||
}
|
||||
const ellipseInternalAlignmentSolvesPassed = ellipseInternalAlignmentSolves.length === 4 && ellipseInternalAlignmentSolves.every((entry, index) => entry.alignmentType === index + 1 && entry.solveStatus <= 1 && Math.abs(entry.center.x - 2) <= 1e-7 && Math.abs(entry.center.y - 3) <= 1e-7 && Math.abs(entry.minorRadius - 3) <= 1e-7 && entry.helperError <= 1e-7 && Math.abs(entry.residual) <= 1e-7)
|
||||
|
||||
const capabilities = await provider.initialize()
|
||||
const snapshot = createSketch('chrome-provider', [{ id: 'line', type: 'line', start: { x: 0, y: 0 }, end: { x: 3, y: 4 } }], [
|
||||
{ id: 'horizontal', type: 'horizontal', geometryId: 'line' },
|
||||
{ id: 'length', type: 'distance', first: { geometryId: 'line', point: 'start' }, second: { geometryId: 'line', point: 'end' }, value: 5 },
|
||||
])
|
||||
const request = (requestId, requestSnapshot = snapshot) => ({ requestId, documentId: 'chrome-doc', documentVersion: 1, snapshot: requestSnapshot })
|
||||
const completed = await new SketchSolverCoordinator(() => 1).solve(provider, request('provider-completed'))
|
||||
const verticalSnapshot = createSketch('chrome-provider-vertical', [{ id: 'line', type: 'line', start: { x: 0, y: 0 }, end: { x: 4, y: 3 } }], [
|
||||
{ id: 'vertical', type: 'vertical', geometryId: 'line' },
|
||||
{ id: 'length', type: 'distance', first: { geometryId: 'line', point: 'start' }, second: { geometryId: 'line', point: 'end' }, value: 5 },
|
||||
])
|
||||
const verticalCompleted = await new SketchSolverCoordinator(() => 1).solve(provider, request('provider-vertical-completed', verticalSnapshot))
|
||||
const parallelSnapshot = createSketch('chrome-provider-parallel', [
|
||||
{ id: 'first', type: 'line', start: { x: 0, y: 0 }, end: { x: 4, y: 0 } },
|
||||
{ id: 'second', type: 'line', start: { x: 0, y: 2 }, end: { x: 3, y: 6 } },
|
||||
], [{ id: 'parallel', type: 'parallel', firstGeometryId: 'first', secondGeometryId: 'second' }])
|
||||
const parallelCompleted = await new SketchSolverCoordinator(() => 1).solve(provider, request('provider-parallel-completed', parallelSnapshot))
|
||||
const perpendicularSnapshot = createSketch('chrome-provider-perpendicular', [
|
||||
{ id: 'first', type: 'line', start: { x: 0, y: 0 }, end: { x: 4, y: 0 } },
|
||||
{ id: 'second', type: 'line', start: { x: 0, y: 2 }, end: { x: 3, y: 6 } },
|
||||
], [{ id: 'perpendicular', type: 'perpendicular', firstGeometryId: 'first', secondGeometryId: 'second' }])
|
||||
const perpendicularCompleted = await new SketchSolverCoordinator(() => 1).solve(provider, request('provider-perpendicular-completed', perpendicularSnapshot))
|
||||
const coincidentSnapshot = createSketch('chrome-provider-coincident', [
|
||||
{ id: 'first', type: 'line', start: { x: 0, y: 0 }, end: { x: 4, y: 0 } },
|
||||
{ id: 'second', type: 'line', start: { x: 0, y: 2 }, end: { x: 3, y: 6 } },
|
||||
], [{ id: 'coincident', type: 'coincident', first: { geometryId: 'first', point: 'end' }, second: { geometryId: 'second', point: 'start' } }])
|
||||
const coincidentCompleted = await new SketchSolverCoordinator(() => 1).solve(provider, request('provider-coincident-completed', coincidentSnapshot))
|
||||
const coincidentEndpointProviders = []
|
||||
for (const firstPoint of ['start', 'end']) {
|
||||
for (const secondPoint of ['start', 'end']) {
|
||||
const endpointSnapshot = createSketch(`chrome-provider-coincident-${firstPoint}-${secondPoint}`, [
|
||||
{ id: 'first', type: 'line', start: { x: 0, y: 0 }, end: { x: 4, y: 0 } },
|
||||
{ id: 'second', type: 'line', start: { x: 0, y: 2 }, end: { x: 3, y: 6 } },
|
||||
], [{ id: 'coincident', type: 'coincident', first: { geometryId: 'first', point: firstPoint }, second: { geometryId: 'second', point: secondPoint } }])
|
||||
const completed = await new SketchSolverCoordinator(() => 1).solve(provider, request(`provider-coincident-${firstPoint}-${secondPoint}-completed`, endpointSnapshot))
|
||||
const firstLine = completed.response?.result.snapshot.geometry[0]
|
||||
const secondLine = completed.response?.result.snapshot.geometry[1]
|
||||
coincidentEndpointProviders.push({
|
||||
firstPoint,
|
||||
secondPoint,
|
||||
completed: completed.status,
|
||||
first: firstLine?.type === 'line' ? firstLine[firstPoint] : null,
|
||||
second: secondLine?.type === 'line' ? secondLine[secondPoint] : null,
|
||||
secondLength: secondLine?.type === 'line' ? Math.hypot(secondLine.end.x - secondLine.start.x, secondLine.end.y - secondLine.start.y) : null,
|
||||
})
|
||||
}
|
||||
}
|
||||
const coincidentEndpointProvidersPassed = coincidentEndpointProviders.length === 4 && coincidentEndpointProviders.every((entry) => entry.completed === 'completed' && entry.first && entry.second && Math.abs(entry.first.x - entry.second.x) <= 1e-7 && Math.abs(entry.first.y - entry.second.y) <= 1e-7 && Math.abs(entry.secondLength - 5) <= 1e-7)
|
||||
const snellsLawSnapshot = createSketch('chrome-provider-snells-law', [
|
||||
{ id: 'incident', type: 'line', start: { x: 0, y: 0 }, end: { x: -4, y: 4 } },
|
||||
{ id: 'refracted', type: 'line', start: { x: 0, y: 0 }, end: { x: 4, y: 3 } },
|
||||
{ id: 'boundary', type: 'line', start: { x: -5, y: 0 }, end: { x: 5, y: 0 } },
|
||||
], [
|
||||
{ id: 'junction', type: 'coincident', first: { geometryId: 'incident', point: 'start' }, second: { geometryId: 'refracted', point: 'start' } },
|
||||
{ id: 'boundary-point', type: 'pointOnObject', point: { geometryId: 'incident', point: 'start' }, geometryId: 'boundary' },
|
||||
{ id: 'snells-law', type: 'snellsLaw', first: { geometryId: 'incident', point: 'start' }, second: { geometryId: 'refracted', point: 'start' }, boundaryGeometryId: 'boundary', value: 1.5 },
|
||||
])
|
||||
const snellsLawCompleted = await new SketchSolverCoordinator(() => 1).solve(provider, request('provider-snells-law-completed', snellsLawSnapshot))
|
||||
const snellsLawProviderFirst = snellsLawCompleted.response?.result.snapshot.geometry[0]
|
||||
const snellsLawProviderSecond = snellsLawCompleted.response?.result.snapshot.geometry[1]
|
||||
const snellsLawProviderBoundary = snellsLawCompleted.response?.result.snapshot.geometry[2]
|
||||
const snellsLawProviderPassed = snellsLawCompleted.status === 'completed' && snellsLawCompleted.response?.result.degreesOfFreedom === 8 && snellsLawProviderFirst?.type === 'line' && snellsLawProviderSecond?.type === 'line' && snellsLawProviderBoundary?.type === 'line' && Math.abs(snellsLawProviderFirst.start.x - snellsLawProviderSecond.start.x) <= 1e-7 && Math.abs(snellsLawProviderFirst.start.y - snellsLawProviderSecond.start.y) <= 1e-7 && Math.abs(Math.hypot(snellsLawProviderSecond.end.x - snellsLawProviderSecond.start.x, snellsLawProviderSecond.end.y - snellsLawProviderSecond.start.y) - 5) <= 1e-7 && Math.abs(snellsLawProviderBoundary.start.y) <= 1e-7 && Math.abs(snellsLawProviderBoundary.end.y) <= 1e-7 && Math.abs(snellsLawCompleted.response?.result.residual ?? Infinity) <= 1e-7
|
||||
const ellipseInternalAlignmentSnapshot = createSketch('chrome-provider-ellipse-internal-alignment', [
|
||||
{ id: 'ellipse', type: 'ellipse', center: { x: 2, y: 3 }, majorRadius: 5, minorRadius: 3, rotation: 0.25 },
|
||||
], [
|
||||
{ id: 'major', type: 'internalAlignment', geometryId: 'ellipse', internalGeometryIndex: 0, alignmentType: 'ellipse-major' },
|
||||
{ id: 'minor', type: 'internalAlignment', geometryId: 'ellipse', internalGeometryIndex: 0, alignmentType: 'ellipse-minor' },
|
||||
{ id: 'focus-1', type: 'internalAlignment', geometryId: 'ellipse', internalGeometryIndex: 0, alignmentType: 'ellipse-focus' },
|
||||
{ id: 'focus-2', type: 'internalAlignment', geometryId: 'ellipse', internalGeometryIndex: 1, alignmentType: 'ellipse-focus' },
|
||||
])
|
||||
const ellipseInternalAlignmentCompleted = await new SketchSolverCoordinator(() => 1).solve(provider, request('provider-ellipse-internal-alignment-completed', ellipseInternalAlignmentSnapshot))
|
||||
const ellipseInternalAlignmentProviderGeometry = ellipseInternalAlignmentCompleted.response?.result.snapshot.geometry[0]
|
||||
const ellipseInternalAlignmentProviderPassed = ellipseInternalAlignmentCompleted.status === 'completed' && ellipseInternalAlignmentCompleted.response?.result.degreesOfFreedom === 5 && Math.abs(ellipseInternalAlignmentCompleted.response?.result.residual ?? Infinity) <= 1e-7 && ellipseInternalAlignmentProviderGeometry?.type === 'ellipse' && ellipseInternalAlignmentProviderGeometry.center.x === 2 && ellipseInternalAlignmentProviderGeometry.center.y === 3 && ellipseInternalAlignmentProviderGeometry.majorRadius === 5 && ellipseInternalAlignmentProviderGeometry.minorRadius === 3 && ellipseInternalAlignmentProviderGeometry.rotation === 0.25
|
||||
const bsplineWeightSnapshot = createSketch('chrome-provider-B-spline-weight', [
|
||||
{ id: 'bspline', type: 'bspline', degree: 3, controlPoints: [{ x: 0, y: 0 }, { x: 1, y: 2 }, { x: 3, y: 2 }, { x: 4, y: 0 }], weights: [1, 0.75, 1.25, 1], knots: [0, 0, 0, 0, 1, 1, 1, 1], periodic: false },
|
||||
], [{ id: 'weight', type: 'weight', geometryId: 'bspline', controlPointIndex: 1, value: 1.5 }])
|
||||
const bsplineWeightCompleted = await new SketchSolverCoordinator(() => 1).solve(provider, request('provider-B-spline-weight-completed', bsplineWeightSnapshot))
|
||||
const bsplineWeightProviderGeometry = bsplineWeightCompleted.response?.result.snapshot.geometry[0]
|
||||
const bsplineWeightProviderPassed = bsplineWeightCompleted.status === 'completed' && bsplineWeightCompleted.response?.result.degreesOfFreedom === 11 && Math.abs(bsplineWeightCompleted.response?.result.residual ?? Infinity) <= 1e-7 && bsplineWeightProviderGeometry?.type === 'bspline' && bsplineWeightProviderGeometry.weights.length === 4 && Math.abs(bsplineWeightProviderGeometry.weights[0] - 1) <= 1e-7 && Math.abs(bsplineWeightProviderGeometry.weights[1] - 1.5) <= 1e-7 && Math.abs(bsplineWeightProviderGeometry.weights[2] - 1.25) <= 1e-7 && Math.abs(bsplineWeightProviderGeometry.weights[3] - 1) <= 1e-7
|
||||
const cancellationCoordinator = new SketchSolverCoordinator(() => 1)
|
||||
const cancellation = cancellationCoordinator.solve(provider, request('provider-cancelled'))
|
||||
cancellationCoordinator.cancel()
|
||||
const cancelled = await cancellation
|
||||
const recovered = await new SketchSolverCoordinator(() => 1).solve(provider, request('provider-recovered'))
|
||||
const stale = await new SketchSolverCoordinator(() => 2).solve(provider, request('provider-stale'))
|
||||
const recoveredLine = recovered.response?.result.snapshot.geometry[0]
|
||||
const verticalProviderLine = verticalCompleted.response?.result.snapshot.geometry[0]
|
||||
const parallelProviderLine = parallelCompleted.response?.result.snapshot.geometry[1]
|
||||
const perpendicularProviderLine = perpendicularCompleted.response?.result.snapshot.geometry[1]
|
||||
const coincidentProviderLine = coincidentCompleted.response?.result.snapshot.geometry[1]
|
||||
const angleSnapshot = createSketch('chrome-provider-angle', [{ id: 'line', type: 'line', start: { x: 0, y: 0 }, end: { x: 3, y: 4 } }], [
|
||||
{ id: 'angle', type: 'angle', geometryId: 'line', value: Math.PI / 4 },
|
||||
{ id: 'length', type: 'distance', first: { geometryId: 'line', point: 'start' }, second: { geometryId: 'line', point: 'end' }, value: 5 },
|
||||
])
|
||||
const angleCompleted = await new SketchSolverCoordinator(() => 1).solve(provider, request('provider-angle-completed', angleSnapshot))
|
||||
const angleProviderLine = angleCompleted.response?.result.snapshot.geometry[0]
|
||||
const circleRadiusSnapshot = createSketch('chrome-provider-circle-radius', [{ id: 'circle', type: 'circle', center: { x: 2, y: 3 }, radius: 1 }], [
|
||||
{ id: 'radius', type: 'radius', geometryId: 'circle', value: 4 },
|
||||
])
|
||||
const circleRadiusCompleted = await new SketchSolverCoordinator(() => 1).solve(provider, request('provider-circle-radius-completed', circleRadiusSnapshot))
|
||||
const circleProvider = circleRadiusCompleted.response?.result.snapshot.geometry[0]
|
||||
const circleDiameterSnapshot = createSketch('chrome-provider-circle-diameter', [{ id: 'circle', type: 'circle', center: { x: 2, y: 3 }, radius: 1 }], [
|
||||
{ id: 'diameter', type: 'diameter', geometryId: 'circle', value: 8 },
|
||||
])
|
||||
const circleDiameterCompleted = await new SketchSolverCoordinator(() => 1).solve(provider, request('provider-circle-diameter-completed', circleDiameterSnapshot))
|
||||
const circleDiameterProvider = circleDiameterCompleted.response?.result.snapshot.geometry[0]
|
||||
const equalLinesSnapshot = createSketch('chrome-provider-equal-lines', [
|
||||
{ id: 'first', type: 'line', start: { x: 0, y: 0 }, end: { x: 4, y: 0 } },
|
||||
{ id: 'second', type: 'line', start: { x: 0, y: 2 }, end: { x: 3, y: 6 } },
|
||||
], [{ id: 'equal', type: 'equal', firstGeometryId: 'first', secondGeometryId: 'second' }])
|
||||
const equalLinesCompleted = await new SketchSolverCoordinator(() => 1).solve(provider, request('provider-equal-lines-completed', equalLinesSnapshot))
|
||||
const equalLinesProviderSecond = equalLinesCompleted.response?.result.snapshot.geometry[1]
|
||||
const equalCirclesSnapshot = createSketch('chrome-provider-equal-circles', [
|
||||
{ id: 'first', type: 'circle', center: { x: 2, y: 3 }, radius: 4 },
|
||||
{ id: 'second', type: 'circle', center: { x: 8, y: 9 }, radius: 1 },
|
||||
], [{ id: 'equal', type: 'equal', firstGeometryId: 'first', secondGeometryId: 'second' }])
|
||||
const equalCirclesCompleted = await new SketchSolverCoordinator(() => 1).solve(provider, request('provider-equal-circles-completed', equalCirclesSnapshot))
|
||||
const equalCirclesProviderSecond = equalCirclesCompleted.response?.result.snapshot.geometry[1]
|
||||
const tangentCirclesSnapshot = createSketch('chrome-provider-tangent-circles', [
|
||||
{ id: 'first', type: 'circle', center: { x: 2, y: 3 }, radius: 4 },
|
||||
{ id: 'second', type: 'circle', center: { x: 10, y: 3 }, radius: 1 },
|
||||
], [{ id: 'tangent', type: 'tangent', firstGeometryId: 'first', secondGeometryId: 'second' }])
|
||||
const tangentCirclesCompleted = await new SketchSolverCoordinator(() => 1).solve(provider, request('provider-tangent-circles-completed', tangentCirclesSnapshot))
|
||||
const tangentCirclesProviderSecond = tangentCirclesCompleted.response?.result.snapshot.geometry[1]
|
||||
const pointSymmetrySnapshot = createSketch('chrome-provider-point-symmetry', [
|
||||
{ id: 'first', type: 'point', position: { x: 1, y: 2 } },
|
||||
{ id: 'second', type: 'point', position: { x: 0, y: 0 } },
|
||||
{ id: 'center', type: 'point', position: { x: 4, y: 6 } },
|
||||
], [{ id: 'symmetric', type: 'symmetric', first: { geometryId: 'first', point: 'position' }, second: { geometryId: 'second', point: 'position' }, center: { geometryId: 'center', point: 'position' } }])
|
||||
const pointSymmetryCompleted = await new SketchSolverCoordinator(() => 1).solve(provider, request('provider-point-symmetry-completed', pointSymmetrySnapshot))
|
||||
const pointSymmetryProviderSecond = pointSymmetryCompleted.response?.result.snapshot.geometry[1]
|
||||
const pointOnLineSnapshot = createSketch('chrome-provider-point-on-object', [
|
||||
{ id: 'point', type: 'point', position: { x: 2, y: 3 } },
|
||||
{ id: 'line', type: 'line', start: { x: 0, y: 0 }, end: { x: 4, y: 0 } },
|
||||
], [{ id: 'point-on-object', type: 'pointOnObject', point: { geometryId: 'point', point: 'position' }, geometryId: 'line' }])
|
||||
const pointOnLineCompleted = await new SketchSolverCoordinator(() => 1).solve(provider, request('provider-point-on-object-completed', pointOnLineSnapshot))
|
||||
const pointOnLineProviderPoint = pointOnLineCompleted.response?.result.snapshot.geometry[0]
|
||||
const pointOnCircleSnapshot = createSketch('chrome-provider-point-on-circle', [
|
||||
{ id: 'point', type: 'point', position: { x: 4, y: 6 } },
|
||||
{ id: 'circle', type: 'circle', center: { x: 2, y: 3 }, radius: 4 },
|
||||
], [{ id: 'point-on-circle', type: 'pointOnObject', point: { geometryId: 'point', point: 'position' }, geometryId: 'circle' }])
|
||||
const pointOnCircleCompleted = await new SketchSolverCoordinator(() => 1).solve(provider, request('provider-point-on-circle-completed', pointOnCircleSnapshot))
|
||||
const pointOnCircleProviderPoint = pointOnCircleCompleted.response?.result.snapshot.geometry[0]
|
||||
const pointOnCircleProviderPassed = pointOnCircleCompleted.status === 'completed' && pointOnCircleProviderPoint?.type === 'point' && Math.abs(pointOnCircleProviderPoint.position.x - 4) <= 1e-7 && Math.abs(pointOnCircleProviderPoint.position.y - 6.464101615137754) <= 1e-7
|
||||
const pointOnArcSnapshot = createSketch('chrome-provider-point-on-arc', [
|
||||
{ id: 'point', type: 'point', position: { x: 2, y: 6 } },
|
||||
{ id: 'arc', type: 'arc', center: { x: 2, y: 3 }, radius: 4, startAngle: 0, endAngle: Math.PI },
|
||||
], [{ id: 'point-on-arc', type: 'pointOnObject', point: { geometryId: 'point', point: 'position' }, geometryId: 'arc' }])
|
||||
const pointOnArcCompleted = await new SketchSolverCoordinator(() => 1).solve(provider, request('provider-point-on-arc-completed', pointOnArcSnapshot))
|
||||
const pointOnArcProviderPoint = pointOnArcCompleted.response?.result.snapshot.geometry[0]
|
||||
const pointOnArcProviderTarget = pointOnArcCompleted.response?.result.snapshot.geometry[1]
|
||||
const pointOnArcProviderPassed = pointOnArcCompleted.status === 'completed' && pointOnArcProviderPoint?.type === 'point' && Math.abs(pointOnArcProviderPoint.position.x - 2) <= 1e-7 && Math.abs(pointOnArcProviderPoint.position.y - 7) <= 1e-7 && pointOnArcProviderTarget?.type === 'arc' && Math.abs(pointOnArcProviderTarget.radius - 4) <= 1e-7 && Math.abs(pointOnArcProviderTarget.startAngle) <= 1e-7 && Math.abs(pointOnArcProviderTarget.endAngle - Math.PI) <= 1e-7
|
||||
const pointOnEllipseSnapshot = createSketch('chrome-provider-point-on-ellipse', [
|
||||
{ id: 'point', type: 'point', position: { x: 2, y: 7 } },
|
||||
{ id: 'ellipse', type: 'ellipse', center: { x: 2, y: 3 }, majorRadius: 5, minorRadius: 3, rotation: 0 },
|
||||
], [{ id: 'point-on-ellipse', type: 'pointOnObject', point: { geometryId: 'point', point: 'position' }, geometryId: 'ellipse' }])
|
||||
const pointOnEllipseCompleted = await new SketchSolverCoordinator(() => 1).solve(provider, request('provider-point-on-ellipse-completed', pointOnEllipseSnapshot))
|
||||
const pointOnEllipseProviderPoint = pointOnEllipseCompleted.response?.result.snapshot.geometry[0]
|
||||
const pointOnEllipseProviderTarget = pointOnEllipseCompleted.response?.result.snapshot.geometry[1]
|
||||
const pointOnEllipseProviderPassed = pointOnEllipseCompleted.status === 'completed' && pointOnEllipseProviderPoint?.type === 'point' && Math.abs(pointOnEllipseProviderPoint.position.x - 2) <= 1e-7 && Math.abs(pointOnEllipseProviderPoint.position.y - 6) <= 1e-7 && pointOnEllipseProviderTarget?.type === 'ellipse' && Math.abs(pointOnEllipseProviderTarget.majorRadius - 5) <= 1e-7 && Math.abs(pointOnEllipseProviderTarget.minorRadius - 3) <= 1e-7 && Math.abs(pointOnEllipseProviderTarget.rotation) <= 1e-7
|
||||
const pointOnBsplineSnapshot = createSketch('chrome-provider-point-on-B-spline', [
|
||||
{ id: 'point', type: 'point', position: { x: 2, y: 1 } },
|
||||
{ id: 'bspline', type: 'bspline', degree: 3, controlPoints: [{ x: 0, y: 0 }, { x: 1, y: 2 }, { x: 3, y: 2 }, { x: 4, y: 0 }], weights: [1, 1, 1, 1], knots: [0, 0, 0, 0, 1, 1, 1, 1], periodic: false },
|
||||
], [{ id: 'point-on-B-spline', type: 'pointOnObject', point: { geometryId: 'point', point: 'position' }, geometryId: 'bspline' }])
|
||||
const pointOnBsplineCompleted = await new SketchSolverCoordinator(() => 1).solve(provider, request('provider-point-on-B-spline-completed', pointOnBsplineSnapshot))
|
||||
const pointOnBsplineProviderPoint = pointOnBsplineCompleted.response?.result.snapshot.geometry[0]
|
||||
const pointOnBsplineProviderTarget = pointOnBsplineCompleted.response?.result.snapshot.geometry[1]
|
||||
const pointOnBsplineProviderPassed = pointOnBsplineCompleted.status === 'completed' && pointOnBsplineProviderPoint?.type === 'point' && Math.abs(pointOnBsplineProviderPoint.position.x - 2) <= 1e-7 && Math.abs(pointOnBsplineProviderPoint.position.y - 1.5) <= 1e-7 && pointOnBsplineProviderTarget?.type === 'bspline' && pointOnBsplineProviderTarget.controlPoints.length === 4
|
||||
const blockSnapshot = createSketch('chrome-provider-block', [{ id: 'line', type: 'line', start: { x: 1, y: 2 }, end: { x: 4, y: 6 } }], [{ id: 'block', type: 'block', geometryId: 'line' }])
|
||||
const blockCompleted = await new SketchSolverCoordinator(() => 1).solve(provider, request('provider-block-completed', blockSnapshot))
|
||||
const blockProviderLine = blockCompleted.response?.result.snapshot.geometry[0]
|
||||
const blockProviderPassed = blockCompleted.status === 'completed' && blockCompleted.response?.result.degreesOfFreedom === 0 && blockProviderLine?.type === 'line' && blockProviderLine.start.x === 1 && blockProviderLine.start.y === 2 && blockProviderLine.end.x === 4 && blockProviderLine.end.y === 6
|
||||
const providerPassed = capabilities.availability === 'available' && capabilities.engine === 'planegcs-wasm' && capabilities.supportedGeometry.includes('point') && capabilities.supportedConstraints.includes('block') && capabilities.supportedConstraints.includes('vertical') && capabilities.supportedConstraints.includes('distanceX') && capabilities.supportedConstraints.includes('distanceY') && capabilities.supportedConstraints.includes('angle') && capabilities.supportedConstraints.includes('radius') && capabilities.supportedConstraints.includes('diameter') && capabilities.supportedConstraints.includes('equal') && capabilities.supportedConstraints.includes('tangent') && capabilities.supportedConstraints.includes('symmetric') && capabilities.supportedConstraints.includes('pointOnObject') && capabilities.supportedConstraints.includes('parallel') && capabilities.supportedConstraints.includes('perpendicular') && capabilities.supportedConstraints.includes('coincident') && completed.status === 'completed' && verticalCompleted.status === 'completed' && angleCompleted.status === 'completed' && circleRadiusCompleted.status === 'completed' && circleDiameterCompleted.status === 'completed' && equalLinesCompleted.status === 'completed' && equalCirclesCompleted.status === 'completed' && tangentCirclesCompleted.status === 'completed' && pointSymmetryCompleted.status === 'completed' && pointOnLineCompleted.status === 'completed' && parallelCompleted.status === 'completed' && perpendicularCompleted.status === 'completed' && coincidentCompleted.status === 'completed' && coincidentEndpointProvidersPassed && cancelled.status === 'cancelled' && recovered.status === 'completed' && stale.status === 'stale' && recoveredLine?.type === 'line' && Math.abs(recoveredLine.end.x - 5) <= 1e-7 && Math.abs(recoveredLine.end.y) <= 1e-7 && verticalProviderLine?.type === 'line' && Math.abs(verticalProviderLine.end.x) <= 1e-7 && Math.abs(verticalProviderLine.end.y - 5) <= 1e-7 && angleProviderLine?.type === 'line' && Math.abs(Math.hypot(angleProviderLine.end.x - angleProviderLine.start.x, angleProviderLine.end.y - angleProviderLine.start.y) - 5) <= 1e-7 && Math.abs(Math.atan2(angleProviderLine.end.y - angleProviderLine.start.y, angleProviderLine.end.x - angleProviderLine.start.x) - Math.PI / 4) <= 1e-7 && circleProvider?.type === 'circle' && Math.abs(circleProvider.center.x - 2) <= 1e-7 && Math.abs(circleProvider.center.y - 3) <= 1e-7 && Math.abs(circleProvider.radius - 4) <= 1e-7 && circleDiameterProvider?.type === 'circle' && Math.abs(circleDiameterProvider.center.x - 2) <= 1e-7 && Math.abs(circleDiameterProvider.center.y - 3) <= 1e-7 && Math.abs(circleDiameterProvider.radius * 2 - 8) <= 1e-7 && equalLinesProviderSecond?.type === 'line' && Math.abs(Math.hypot(equalLinesProviderSecond.end.x - equalLinesProviderSecond.start.x, equalLinesProviderSecond.end.y - equalLinesProviderSecond.start.y) - 4) <= 1e-7 && equalCirclesProviderSecond?.type === 'circle' && Math.abs(equalCirclesProviderSecond.radius - 4) <= 1e-7 && tangentCirclesProviderSecond?.type === 'circle' && Math.abs(tangentCirclesProviderSecond.radius - 4) <= 1e-7 && pointSymmetryProviderSecond?.type === 'point' && Math.abs(pointSymmetryProviderSecond.position.x - 7) <= 1e-7 && Math.abs(pointSymmetryProviderSecond.position.y - 10) <= 1e-7 && pointOnLineProviderPoint?.type === 'point' && Math.abs(pointOnLineProviderPoint.position.x - 2) <= 1e-7 && Math.abs(pointOnLineProviderPoint.position.y) <= 1e-7 && parallelProviderLine?.type === 'line' && Math.abs(parallelProviderLine.end.y - parallelProviderLine.start.y) <= 1e-7 && Math.abs(parallelProviderLine.end.x - 5) <= 1e-7 && perpendicularProviderLine?.type === 'line' && Math.abs(perpendicularProviderLine.end.x - perpendicularProviderLine.start.x) <= 1e-7 && Math.abs(perpendicularProviderLine.end.y - 7) <= 1e-7 && coincidentProviderLine?.type === 'line' && Math.abs(coincidentProviderLine.start.x - 4) <= 1e-7 && Math.abs(coincidentProviderLine.start.y) <= 1e-7
|
||||
const snellsLawCapabilityPassed = capabilities.supportedConstraints.includes('snellsLaw')
|
||||
const internalAlignmentCapabilityPassed = capabilities.supportedConstraints.includes('internalAlignment')
|
||||
const weightCapabilityPassed = capabilities.supportedConstraints.includes('weight')
|
||||
const arcProviderPassed = capabilities.supportedGeometry.includes('arc') && pointOnArcProviderPassed
|
||||
const ellipseProviderPassed = capabilities.supportedGeometry.includes('ellipse') && pointOnEllipseProviderPassed
|
||||
const bsplineProviderPassed = capabilities.supportedGeometry.includes('bspline') && pointOnBsplineProviderPassed
|
||||
Object.assign(report, {
|
||||
status: nativePassed && verticalPassed && distanceXPassed && distanceYPassed && anglePassed && circleRadiusPassed && circleDiameterPassed && equalLinesPassed && equalCirclesPassed && tangentCirclesPassed && pointSymmetryPassed && pointOnLinePassed && pointOnCirclePassed && pointOnCircleProviderPassed && pointOnArcPassed && arcProviderPassed && pointOnEllipsePassed && ellipseProviderPassed && pointOnBsplinePassed && bsplineProviderPassed && bsplineWeightPassed && bsplineWeightProviderPassed && weightCapabilityPassed && blockProviderPassed && parallelPassed && perpendicularPassed && coincidentPassed && coincidentEndpointSolvesPassed && snellsLawPassed && snellsLawProviderPassed && snellsLawCapabilityPassed && ellipseInternalAlignmentSolvesPassed && ellipseInternalAlignmentProviderPassed && internalAlignmentCapabilityPassed && providerPassed ? 'pass' : 'failed',
|
||||
solve: { solveStatus, startX, startY, endX, endY, residual },
|
||||
verticalSolve: { solveStatus: verticalSolveStatus, startX: verticalStartX, startY: verticalStartY, endX: verticalEndX, endY: verticalEndY, residual: verticalResidual },
|
||||
distanceXSolve: { solveStatus: distanceXSolveStatus, startX: distanceXStartX, startY: distanceXStartY, endX: distanceXEndX, endY: distanceXEndY, residual: distanceXResidual },
|
||||
distanceYSolve: { solveStatus: distanceYSolveStatus, startX: distanceYStartX, startY: distanceYStartY, endX: distanceYEndX, endY: distanceYEndY, residual: distanceYResidual },
|
||||
angleSolve: { solveStatus: angleSolveStatus, startX: angleStartX, startY: angleStartY, endX: angleEndX, endY: angleEndY, lengthResidual: angleLengthResidual, residual: angleResidual, targetAngle: Math.PI / 4 },
|
||||
circleRadiusSolve: { solveStatus: circleRadiusSolveStatus, centerX: circleCenterX, centerY: circleCenterY, radius: circleRadius, residual: circleRadiusResidual },
|
||||
circleDiameterSolve: { solveStatus: circleDiameterSolveStatus, centerX: circleDiameterCenterX, centerY: circleDiameterCenterY, radius: circleDiameterRadius, residual: circleDiameterResidual },
|
||||
equalLinesSolve: { solveStatus: equalLinesSolveStatus, firstStartX: equalLinesFirstStartX, firstStartY: equalLinesFirstStartY, firstEndX: equalLinesFirstEndX, firstEndY: equalLinesFirstEndY, secondStartX: equalLinesSecondStartX, secondStartY: equalLinesSecondStartY, secondEndX: equalLinesSecondEndX, secondEndY: equalLinesSecondEndY, residual: equalLinesResidual },
|
||||
equalCirclesSolve: { solveStatus: equalCirclesSolveStatus, firstCenterX: equalCirclesFirstCenterX, firstCenterY: equalCirclesFirstCenterY, firstRadius: equalCirclesFirstRadius, secondCenterX: equalCirclesSecondCenterX, secondCenterY: equalCirclesSecondCenterY, secondRadius: equalCirclesSecondRadius, residual: equalCirclesResidual },
|
||||
tangentCirclesSolve: { solveStatus: tangentCirclesSolveStatus, firstCenterX: tangentCirclesFirstCenterX, firstCenterY: tangentCirclesFirstCenterY, firstRadius: tangentCirclesFirstRadius, secondCenterX: tangentCirclesSecondCenterX, secondCenterY: tangentCirclesSecondCenterY, secondRadius: tangentCirclesSecondRadius, residual: tangentCirclesResidual },
|
||||
pointSymmetrySolve: { solveStatus: pointSymmetrySolveStatus, firstX: pointSymmetryFirstX, firstY: pointSymmetryFirstY, secondX: pointSymmetrySecondX, secondY: pointSymmetrySecondY, centerX: pointSymmetryCenterX, centerY: pointSymmetryCenterY, residual: pointSymmetryResidual },
|
||||
pointOnLineSolve: { solveStatus: pointOnLineSolveStatus, pointX: pointOnLineX, pointY: pointOnLineY, startX: pointOnLineStartX, startY: pointOnLineStartY, endX: pointOnLineEndX, endY: pointOnLineEndY, residual: pointOnLineResidual },
|
||||
pointOnCircleSolve: { solveStatus: pointOnCircleSolveStatus, pointX: pointOnCircleX, pointY: pointOnCircleY, centerX: pointOnCircleCenterX, centerY: pointOnCircleCenterY, radius: pointOnCircleRadius, residual: pointOnCircleResidual },
|
||||
pointOnArcSolve: { solveStatus: pointOnArcSolveStatus, pointX: pointOnArcX, pointY: pointOnArcY, centerX: pointOnArcCenterX, centerY: pointOnArcCenterY, radius: pointOnArcRadius, startAngle: pointOnArcStartAngle, endAngle: pointOnArcEndAngle, residual: pointOnArcResidual },
|
||||
pointOnEllipseSolve: { solveStatus: pointOnEllipseSolveStatus, pointX: pointOnEllipseX, pointY: pointOnEllipseY, centerX: pointOnEllipseCenterX, centerY: pointOnEllipseCenterY, focusX: pointOnEllipseFocusX, focusY: pointOnEllipseFocusY, minorRadius: pointOnEllipseMinorRadius, residual: pointOnEllipseResidual },
|
||||
pointOnBsplineSolve: { solveStatus: pointOnBsplineSolveStatus, pointX: pointOnBsplineX, pointY: pointOnBsplineY, pointParameter: pointOnBsplineParameter, residualX: pointOnBsplineResidualX, residualY: pointOnBsplineResidualY },
|
||||
bsplineWeightSolve: { solveStatus: bsplineWeightSolveStatus, weights: [bsplineWeight0, bsplineWeight1, bsplineWeight2, bsplineWeight3], helper: { center: { x: bsplineWeightHelperX, y: bsplineWeightHelperY }, radius: bsplineWeightHelperRadius }, alignmentResidual: bsplineWeightAlignmentResidual, residual: bsplineWeightResidual },
|
||||
parallelSolve: { solveStatus: parallelSolveStatus, firstStartX: parallelFirstStartX, firstStartY: parallelFirstStartY, firstEndX: parallelFirstEndX, firstEndY: parallelFirstEndY, secondStartX: parallelSecondStartX, secondStartY: parallelSecondStartY, secondEndX: parallelSecondEndX, secondEndY: parallelSecondEndY, residual: parallelResidual },
|
||||
perpendicularSolve: { solveStatus: perpendicularSolveStatus, firstStartX: perpendicularFirstStartX, firstStartY: perpendicularFirstStartY, firstEndX: perpendicularFirstEndX, firstEndY: perpendicularFirstEndY, secondStartX: perpendicularSecondStartX, secondStartY: perpendicularSecondStartY, secondEndX: perpendicularSecondEndX, secondEndY: perpendicularSecondEndY, residual: perpendicularResidual },
|
||||
coincidentSolve: { solveStatus: coincidentSolveStatus, firstEndX: coincidentFirstEndX, firstEndY: coincidentFirstEndY, secondStartX: coincidentSecondStartX, secondStartY: coincidentSecondStartY, residual: coincidentResidual },
|
||||
coincidentEndpointSolves,
|
||||
coincidentEndpointProviders,
|
||||
snellsLawSolve: { solveStatus: snellsLawSolveStatus, firstStart: { x: snellsFirstStartX, y: snellsFirstStartY }, firstEnd: { x: snellsFirstEndX, y: snellsFirstEndY }, secondStart: { x: snellsSecondStartX, y: snellsSecondStartY }, secondEnd: { x: snellsSecondEndX, y: snellsSecondEndY }, boundaryStart: { x: snellsBoundaryStartX, y: snellsBoundaryStartY }, boundaryEnd: { x: snellsBoundaryEndX, y: snellsBoundaryEndY }, ratio: 1.5, residual: snellsLawResidual },
|
||||
snellsLawProvider: { completed: snellsLawCompleted.status, degreesOfFreedom: snellsLawCompleted.response?.result.degreesOfFreedom, residual: snellsLawCompleted.response?.result.residual, first: snellsLawProviderFirst?.type === 'line' ? { start: snellsLawProviderFirst.start, end: snellsLawProviderFirst.end } : null, second: snellsLawProviderSecond?.type === 'line' ? { start: snellsLawProviderSecond.start, end: snellsLawProviderSecond.end } : null, boundary: snellsLawProviderBoundary?.type === 'line' ? { start: snellsLawProviderBoundary.start, end: snellsLawProviderBoundary.end } : null },
|
||||
ellipseInternalAlignmentSolves,
|
||||
ellipseInternalAlignmentProvider: { completed: ellipseInternalAlignmentCompleted.status, degreesOfFreedom: ellipseInternalAlignmentCompleted.response?.result.degreesOfFreedom, residual: ellipseInternalAlignmentCompleted.response?.result.residual, ellipse: ellipseInternalAlignmentProviderGeometry?.type === 'ellipse' ? { center: ellipseInternalAlignmentProviderGeometry.center, majorRadius: ellipseInternalAlignmentProviderGeometry.majorRadius, minorRadius: ellipseInternalAlignmentProviderGeometry.minorRadius, rotation: ellipseInternalAlignmentProviderGeometry.rotation } : null },
|
||||
bsplineWeightProvider: { completed: bsplineWeightCompleted.status, degreesOfFreedom: bsplineWeightCompleted.response?.result.degreesOfFreedom, residual: bsplineWeightCompleted.response?.result.residual, bspline: bsplineWeightProviderGeometry?.type === 'bspline' ? { degree: bsplineWeightProviderGeometry.degree, controlPoints: bsplineWeightProviderGeometry.controlPoints, weights: bsplineWeightProviderGeometry.weights, knots: bsplineWeightProviderGeometry.knots, periodic: bsplineWeightProviderGeometry.periodic } : null },
|
||||
pointOnCircleProvider: { completed: pointOnCircleCompleted.status, point: pointOnCircleProviderPoint?.type === 'point' ? pointOnCircleProviderPoint.position : null },
|
||||
pointOnArcProvider: { completed: pointOnArcCompleted.status, point: pointOnArcProviderPoint?.type === 'point' ? pointOnArcProviderPoint.position : null, arc: pointOnArcProviderTarget?.type === 'arc' ? { center: pointOnArcProviderTarget.center, radius: pointOnArcProviderTarget.radius, startAngle: pointOnArcProviderTarget.startAngle, endAngle: pointOnArcProviderTarget.endAngle } : null },
|
||||
pointOnEllipseProvider: { completed: pointOnEllipseCompleted.status, point: pointOnEllipseProviderPoint?.type === 'point' ? pointOnEllipseProviderPoint.position : null, ellipse: pointOnEllipseProviderTarget?.type === 'ellipse' ? { center: pointOnEllipseProviderTarget.center, majorRadius: pointOnEllipseProviderTarget.majorRadius, minorRadius: pointOnEllipseProviderTarget.minorRadius, rotation: pointOnEllipseProviderTarget.rotation } : null },
|
||||
pointOnBsplineProvider: { completed: pointOnBsplineCompleted.status, point: pointOnBsplineProviderPoint?.type === 'point' ? pointOnBsplineProviderPoint.position : null, bspline: pointOnBsplineProviderTarget?.type === 'bspline' ? { degree: pointOnBsplineProviderTarget.degree, controlPoints: pointOnBsplineProviderTarget.controlPoints, weights: pointOnBsplineProviderTarget.weights, knots: pointOnBsplineProviderTarget.knots, periodic: pointOnBsplineProviderTarget.periodic } : null },
|
||||
blockProvider: { completed: blockCompleted.status, degreesOfFreedom: blockCompleted.response?.result.degreesOfFreedom, line: blockProviderLine?.type === 'line' ? { start: blockProviderLine.start, end: blockProviderLine.end } : null },
|
||||
providerLifecycle: { availability: capabilities.availability, engine: capabilities.engine, supportedGeometry: capabilities.supportedGeometry, supportedConstraints: capabilities.supportedConstraints, completed: completed.status, verticalCompleted: verticalCompleted.status, verticalEnd: verticalProviderLine?.type === 'line' ? verticalProviderLine.end : null, angleCompleted: angleCompleted.status, angleEnd: angleProviderLine?.type === 'line' ? angleProviderLine.end : null, circleRadiusCompleted: circleRadiusCompleted.status, circleRadius: circleProvider?.type === 'circle' ? { center: circleProvider.center, radius: circleProvider.radius } : null, circleDiameterCompleted: circleDiameterCompleted.status, circleDiameter: circleDiameterProvider?.type === 'circle' ? { center: circleDiameterProvider.center, radius: circleDiameterProvider.radius } : null, equalLinesCompleted: equalLinesCompleted.status, equalLinesSecond: equalLinesProviderSecond?.type === 'line' ? { start: equalLinesProviderSecond.start, end: equalLinesProviderSecond.end } : null, equalCirclesCompleted: equalCirclesCompleted.status, equalCirclesSecond: equalCirclesProviderSecond?.type === 'circle' ? { center: equalCirclesProviderSecond.center, radius: equalCirclesProviderSecond.radius } : null, tangentCirclesCompleted: tangentCirclesCompleted.status, tangentCirclesSecond: tangentCirclesProviderSecond?.type === 'circle' ? { center: tangentCirclesProviderSecond.center, radius: tangentCirclesProviderSecond.radius } : null, pointSymmetryCompleted: pointSymmetryCompleted.status, pointSymmetrySecond: pointSymmetryProviderSecond?.type === 'point' ? pointSymmetryProviderSecond.position : null, pointOnLineCompleted: pointOnLineCompleted.status, pointOnLinePoint: pointOnLineProviderPoint?.type === 'point' ? pointOnLineProviderPoint.position : null, parallelCompleted: parallelCompleted.status, parallelEnd: parallelProviderLine?.type === 'line' ? parallelProviderLine.end : null, perpendicularCompleted: perpendicularCompleted.status, perpendicularEnd: perpendicularProviderLine?.type === 'line' ? perpendicularProviderLine.end : null, coincidentCompleted: coincidentCompleted.status, coincidentStart: coincidentProviderLine?.type === 'line' ? coincidentProviderLine.start : null, cancelled: cancelled.status, recovered: recovered.status, stale: stale.status, recoveredEnd: recoveredLine?.type === 'line' ? recoveredLine.end : null },
|
||||
})
|
||||
} catch (error) {
|
||||
report.error = error instanceof Error ? `${error.name}: ${error.message}` : String(error)
|
||||
} finally {
|
||||
worker.terminate()
|
||||
provider.dispose()
|
||||
}
|
||||
document.querySelector('#status').textContent = JSON.stringify(report, null, 2)
|
||||
await fetch('/__planegcs-report', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(report) })
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
69
public/planegcs-browser-worker.js
Normal file
69
public/planegcs-browser-worker.js
Normal file
@@ -0,0 +1,69 @@
|
||||
import createPlanegcs from '/native/planegcs/freecad-planegcs.js'
|
||||
|
||||
let modulePromise
|
||||
|
||||
const getModule = () => {
|
||||
modulePromise ??= createPlanegcs()
|
||||
return modulePromise
|
||||
}
|
||||
|
||||
self.addEventListener('message', async (event) => {
|
||||
const { requestId, operation, input } = event.data ?? {}
|
||||
if (!requestId || !['solve-horizontal-distance', 'solve-vertical-distance', 'solve-distance-x', 'solve-distance-y', 'solve-angle', 'solve-circle-radius', 'solve-circle-diameter', 'solve-equal-lines', 'solve-equal-circles', 'solve-tangent-circles', 'solve-point-symmetry', 'solve-point-on-line', 'solve-point-on-circle', 'solve-point-on-arc', 'solve-point-on-ellipse', 'solve-point-on-cubic-bspline', 'solve-cubic-bspline-weight', 'solve-parallel-lines', 'solve-perpendicular-lines', 'solve-coincident-lines', 'solve-coincident-line-points', 'solve-snells-law-lines', 'solve-ellipse-internal-alignment'].includes(operation)) {
|
||||
self.postMessage({ requestId, ok: false, error: 'Unsupported planegcs Worker request.' })
|
||||
return
|
||||
}
|
||||
try {
|
||||
const module = await getModule()
|
||||
const result = operation === 'solve-vertical-distance'
|
||||
? module.solveVerticalDistance(input.startX, input.startY, input.endX, input.endY, input.targetLength)
|
||||
: operation === 'solve-distance-x'
|
||||
? module.solveDistanceX(input.startX, input.startY, input.endX, input.endY, input.targetDistance)
|
||||
: operation === 'solve-distance-y'
|
||||
? module.solveDistanceY(input.startX, input.startY, input.endX, input.endY, input.targetDistance)
|
||||
: operation === 'solve-angle'
|
||||
? module.solveAngle(input.startX, input.startY, input.endX, input.endY, input.targetLength, input.targetAngle)
|
||||
: operation === 'solve-circle-radius'
|
||||
? module.solveCircleRadius(input.centerX, input.centerY, input.radius, input.targetRadius)
|
||||
: operation === 'solve-circle-diameter'
|
||||
? module.solveCircleDiameter(input.centerX, input.centerY, input.radius, input.targetDiameter)
|
||||
: operation === 'solve-equal-lines'
|
||||
? module.solveEqualLines(input.firstStartX, input.firstStartY, input.firstEndX, input.firstEndY, input.secondStartX, input.secondStartY, input.secondEndX, input.secondEndY)
|
||||
: operation === 'solve-equal-circles'
|
||||
? module.solveEqualCircles(input.firstCenterX, input.firstCenterY, input.firstRadius, input.secondCenterX, input.secondCenterY, input.secondRadius)
|
||||
: operation === 'solve-tangent-circles'
|
||||
? module.solveTangentCircles(input.firstCenterX, input.firstCenterY, input.firstRadius, input.secondCenterX, input.secondCenterY, input.secondRadius)
|
||||
: operation === 'solve-point-symmetry'
|
||||
? module.solvePointSymmetry(input.firstX, input.firstY, input.secondX, input.secondY, input.centerX, input.centerY)
|
||||
: operation === 'solve-point-on-line'
|
||||
? module.solvePointOnLine(input.pointX, input.pointY, input.startX, input.startY, input.endX, input.endY, input.vertical)
|
||||
: operation === 'solve-point-on-circle'
|
||||
? module.solvePointOnCircle(input.pointX, input.pointY, input.centerX, input.centerY, input.radius)
|
||||
: operation === 'solve-point-on-arc'
|
||||
? module.solvePointOnArc(input.pointX, input.pointY, input.centerX, input.centerY, input.radius, input.startAngle, input.endAngle)
|
||||
: operation === 'solve-point-on-ellipse'
|
||||
? module.solvePointOnEllipse(input.pointX, input.pointY, input.centerX, input.centerY, input.focusX, input.focusY, input.minorRadius)
|
||||
: operation === 'solve-point-on-cubic-bspline'
|
||||
? module.solvePointOnCubicBspline(input.pointX, input.pointY, input.pole0X, input.pole0Y, input.pole1X, input.pole1Y, input.pole2X, input.pole2Y, input.pole3X, input.pole3Y, input.pointParameter)
|
||||
: operation === 'solve-cubic-bspline-weight'
|
||||
? module.solveCubicBsplineWeight(input.pole0X, input.pole0Y, input.pole1X, input.pole1Y, input.pole2X, input.pole2Y, input.pole3X, input.pole3Y, input.weight0, input.weight1, input.weight2, input.weight3, input.controlPointIndex, input.targetWeight)
|
||||
: operation === 'solve-parallel-lines'
|
||||
? module.solveParallelLines(input.firstStartX, input.firstStartY, input.firstEndX, input.firstEndY, input.secondStartX, input.secondStartY, input.secondEndX, input.secondEndY, input.secondLength)
|
||||
: operation === 'solve-coincident-lines'
|
||||
? module.solveCoincidentLines(input.firstStartX, input.firstStartY, input.firstEndX, input.firstEndY, input.secondStartX, input.secondStartY, input.secondEndX, input.secondEndY, input.secondLength)
|
||||
: operation === 'solve-coincident-line-points'
|
||||
? module.solveCoincidentLinePoints(input.firstStartX, input.firstStartY, input.firstEndX, input.firstEndY, input.secondStartX, input.secondStartY, input.secondEndX, input.secondEndY, input.secondLength, input.firstEnd, input.secondEnd)
|
||||
: operation === 'solve-snells-law-lines'
|
||||
? module.solveSnellsLawLines(input.firstStartX, input.firstStartY, input.firstEndX, input.firstEndY, input.secondStartX, input.secondStartY, input.secondEndX, input.secondEndY, input.boundaryStartX, input.boundaryStartY, input.boundaryEndX, input.boundaryEndY, input.refractiveRatio, input.firstEnd, input.secondEnd)
|
||||
: operation === 'solve-ellipse-internal-alignment'
|
||||
? module.solveEllipseInternalAlignment(input.centerX, input.centerY, input.majorRadius, input.minorRadius, input.rotation, input.alignmentType)
|
||||
: operation === 'solve-perpendicular-lines'
|
||||
? module.solvePerpendicularLines(input.firstStartX, input.firstStartY, input.firstEndX, input.firstEndY, input.secondStartX, input.secondStartY, input.secondEndX, input.secondEndY, input.secondLength)
|
||||
: module.solveHorizontalDistance(input.startX, input.startY, input.endX, input.endY, input.targetLength)
|
||||
const values = Array.from({ length: result.size() }, (_, index) => result.get(index))
|
||||
result.delete()
|
||||
self.postMessage({ requestId, ok: true, values })
|
||||
} catch (error) {
|
||||
self.postMessage({ requestId, ok: false, error: error instanceof Error ? `${error.name}: ${error.message}` : String(error) })
|
||||
}
|
||||
})
|
||||
18
public/sw.js
Normal file
18
public/sw.js
Normal file
@@ -0,0 +1,18 @@
|
||||
const CACHE_NAME = 'bitbybit-cad-shell-v1'
|
||||
const SHELL = ['/','/index.html','/manifest.webmanifest']
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
event.waitUntil(caches.open(CACHE_NAME).then((cache) => cache.addAll(SHELL)).then(() => self.skipWaiting()))
|
||||
})
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(caches.keys().then((keys) => Promise.all(keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key)))).then(() => self.clients.claim()))
|
||||
})
|
||||
|
||||
self.addEventListener('fetch', (event) => {
|
||||
if (event.request.method !== 'GET' || new URL(event.request.url).origin !== self.location.origin) return
|
||||
event.respondWith(fetch(event.request).then((response) => {
|
||||
if (response.ok) void caches.open(CACHE_NAME).then((cache) => cache.put(event.request, response.clone()))
|
||||
return response
|
||||
}).catch(() => caches.match(event.request).then((cached) => cached || caches.match('/index.html'))))
|
||||
})
|
||||
BIN
public/vendor/camotics/camotics-sweep.wasm
vendored
Executable file
BIN
public/vendor/camotics/camotics-sweep.wasm
vendored
Executable file
Binary file not shown.
19
public/vendor/opencamlib/ocl.js
vendored
Executable file
19
public/vendor/opencamlib/ocl.js
vendored
Executable file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user