feat: advance FreeCAD parity and native OCCT history
This commit is contained in:
@@ -8,13 +8,16 @@ import { DependencyGraph } from '../src/facade/dependencyGraph'
|
||||
import { evaluateQuantityExpression, quantityFromNumber } from '../src/facade/units'
|
||||
import { createEdgeSubshapeRefs, createSubshapeRefs, createVertexSubshapeRefs, matchSubshapes, signatureForEdge, signatureForFace, signatureForVertex } from '../src/facade/topologyNaming'
|
||||
import { createPersistedTopoRef, migrateDocumentTopologyReferences, migrateTopoRefs, parseTopoRef, resolveDocumentTopologyReference, resolveTopoRef, serializeTopoRef } from '../src/facade/topologyReferences'
|
||||
import { captureSignatureTopologyHistory } from '../src/facade/topologyHistory'
|
||||
import { captureNativeTopologyHistory, captureSignatureTopologyHistory } from '../src/facade/topologyHistory'
|
||||
import { createNativeOcctStepHistoryBridge, mapNativeOcctHistoryRecords } from '../src/facade/nativeHistoryProvider'
|
||||
import { DirectNativeOcctHistoryProvider, NativeOcctHistoryCoordinator, NATIVE_OCCT_HISTORY_PROTOCOL_VERSION, type NativeOcctHistoryProvider } from '../src/facade/nativeHistoryProtocol'
|
||||
import { NativeOcctHistoryWorkerProvider } from '../src/facade/nativeHistoryWorkerClient'
|
||||
import { assessResourceQuota, planResourceSweep } from '../src/facade/resourcePolicy'
|
||||
import { cloneSketch, createSketch, solveSketch } from '../src/facade/sketcher'
|
||||
import { BasicSketchSolverProvider, SKETCH_SOLVER_PROTOCOL_VERSION, SketchSolverCoordinator, SketchSolverUnavailableError, UnavailablePlanegcsProvider, runSketchSolverReplay, type SketchSolverProvider, type SketchSolverRequest } from '../src/facade/sketchSolverProtocol'
|
||||
import { createFacadeGeometryRecomputeExecutor, executeFacadeRecomputeNode, RecomputeCoordinator, type RecomputeGeometryRuntime } from '../src/facade/recomputeEngine'
|
||||
import { inspectFcstdArchive } from '../src/facade/fcstd'
|
||||
import { assertShapeHandleIntegrity, normalizeBitbybitMesh, validateBooleanUnionInput, validateBoxInput, validateChamferInput, validateConeInput, validateCylinderInput, validateFilletInput, validateMirrorInput, validatePadInput, validatePlacementInput, validatePlanarProfile, validateRevolutionInput, validateSphereInput } from '../src/facade/geometryRuntime'
|
||||
import { assertShapeHandleIntegrity, BitbybitGeometryRuntime, normalizeBitbybitMesh, validateBooleanUnionInput, validateBoxInput, validateChamferInput, validateConeInput, validateCylinderInput, validateFilletInput, validateMirrorInput, validatePadInput, validatePlacementInput, validatePlanarProfile, validateRevolutionInput, validateSphereInput } from '../src/facade/geometryRuntime'
|
||||
import type { DocumentObjectSnapshot, DocumentSnapshot, MultiTransformValue, ObjectTopologySnapshot, ShapeHandle, SubshapeTopology } from '../src/facade/types'
|
||||
|
||||
const recomputeDocumentFixture = (edges: DocumentSnapshot['dependencies'] = []): DocumentSnapshot => ({
|
||||
@@ -226,6 +229,194 @@ test('signature topology history conservatively classifies boolean fallback rela
|
||||
assert.deepEqual(new Set(ambiguous.relations[0].candidates?.map((candidate) => candidate.sourceObjectId)), new Set(['base', 'tool']))
|
||||
})
|
||||
|
||||
test('native topology history maps OCCT relation indexes without transient references', () => {
|
||||
const face = { vertexCoord: [0, 0, 0, 2, 0, 0, 2, 2, 0, 0, 2, 0], normalCoord: [], triIndexes: [0, 1, 2, 0, 2, 3] }
|
||||
const secondFace = { vertexCoord: [0, 0, 1, 2, 0, 1, 2, 2, 1, 0, 2, 1], normalCoord: [], triIndexes: [0, 1, 2, 0, 2, 3] }
|
||||
const input = createSubshapeRefs('native-input', 1, [face, secondFace])
|
||||
const output = createSubshapeRefs('native-output', 2, [face])
|
||||
const result = captureNativeTopologyHistory(
|
||||
'boolean-native',
|
||||
[{ objectId: 'base', entries: input.refs.map((ref, index) => ({ ref, signature: input.signatures[index] })) }],
|
||||
output.refs.map((ref, index) => ({ ref, signature: output.signatures[index] })),
|
||||
[
|
||||
{ sourceObjectId: 'base', sourceKind: 'face', sourceIndex: 0, relation: 'modified', resultIndexes: [0] },
|
||||
{ sourceObjectId: 'base', sourceKind: 'face', sourceIndex: 1, relation: 'deleted' },
|
||||
],
|
||||
)
|
||||
assert.equal(result.provider, 'occt-native')
|
||||
assert.equal(result.counts.modified, 1)
|
||||
assert.equal(result.counts.deleted, 1)
|
||||
assert.equal(result.relations[0].sourcePersistentId, input.refs[0].persistentId)
|
||||
assert.equal(result.relations[0].resultPersistentId, output.refs[0].persistentId)
|
||||
assert.equal(result.relations[1].sourcePersistentId, input.refs[1].persistentId)
|
||||
assert.throws(() => captureNativeTopologyHistory('boolean-native', [{ objectId: 'base', entries: [] }], [], [{ sourceObjectId: 'base', sourceKind: 'face', sourceIndex: 0, relation: 'deleted' }]), /out of range/)
|
||||
})
|
||||
|
||||
test('native OCCT provider protocol maps worker records to facade source IDs', () => {
|
||||
const records = mapNativeOcctHistoryRecords({
|
||||
provider: 'occt-native',
|
||||
occtVersion: '8.0.0',
|
||||
hasModified: true,
|
||||
hasGenerated: false,
|
||||
hasDeleted: true,
|
||||
records: [
|
||||
{ relation: 'modified', source: 'object', kind: 'face', sourceIndex: 0, resultIndex: 1 },
|
||||
{ relation: 'deleted', source: 'tool', kind: 'face', sourceIndex: 2, resultIndex: -1 },
|
||||
],
|
||||
}, { object: 'base', tool: 'tool' })
|
||||
assert.deepEqual(records, [
|
||||
{ sourceObjectId: 'base', sourceKind: 'face', sourceIndex: 0, relation: 'modified', resultKind: 'face', resultIndexes: [1] },
|
||||
{ sourceObjectId: 'tool', sourceKind: 'face', sourceIndex: 2, relation: 'deleted' },
|
||||
])
|
||||
})
|
||||
|
||||
test('native OCCT STEP bridge serializes handles before invoking the provider', async () => {
|
||||
const calls: string[] = []
|
||||
const bridge = createNativeOcctStepHistoryBridge(
|
||||
{ exportStep: async (shape) => { calls.push(shape.id); return { format: 'step', fileName: `${shape.id}.step`, mediaType: 'application/step', text: `ISO-10303-21; ${shape.id}` } } },
|
||||
{
|
||||
occtVersion: () => '8.0.0',
|
||||
booleanHistoryFromStep: (objectStep, toolStep, operation) => {
|
||||
assert.match(objectStep, /base-shape/)
|
||||
assert.match(toolStep, /tool-shape/)
|
||||
assert.equal(operation, 'cut')
|
||||
return { provider: 'occt-native', occtVersion: '8.0.0', hasModified: true, hasGenerated: false, hasDeleted: false, records: [{ relation: 'modified', source: 'object', kind: 'face', sourceIndex: 0, resultIndex: 0 }] }
|
||||
},
|
||||
},
|
||||
)
|
||||
const records = await bridge({ documentId: 'doc', documentVersion: 1, operationId: 'cut-1', operation: 'cut', inputs: [
|
||||
{ objectId: 'base', shape: { id: 'base-shape', kernel: 'bitbybit-occt', kind: 'solid', documentId: 'doc', documentVersion: 1 } },
|
||||
{ objectId: 'tool', shape: { id: 'tool-shape', kernel: 'bitbybit-occt', kind: 'solid', documentId: 'doc', documentVersion: 1 } },
|
||||
], result: { id: 'result-shape', kernel: 'bitbybit-occt', kind: 'solid', documentId: 'doc', documentVersion: 1 } })
|
||||
assert.deepEqual(calls, ['base-shape', 'tool-shape'])
|
||||
assert.deepEqual(records, [{ sourceObjectId: 'base', sourceKind: 'face', sourceIndex: 0, relation: 'modified', resultKind: 'face', resultIndexes: [0] }])
|
||||
})
|
||||
|
||||
test('native OCCT history protocol validates STEP context and isolates stale generations', async () => {
|
||||
const provider = new DirectNativeOcctHistoryProvider({
|
||||
occtVersion: () => '8.0.0',
|
||||
booleanHistoryFromStep: (objectStep, toolStep, operation) => ({
|
||||
provider: 'occt-native', occtVersion: '8.0.0', hasModified: true, hasGenerated: false, hasDeleted: false,
|
||||
resultStep: objectStep, records: [{ relation: operation === 'cut' ? 'modified' : 'generated', source: 'object', kind: 'face', sourceIndex: 0, resultIndex: 0 }],
|
||||
}),
|
||||
})
|
||||
assert.deepEqual(provider.capabilities(), { providerId: 'occt-native.history-step', providerVersion: '8.0.0-embind', occtVersion: '8.0.0', availability: 'available', operations: ['fuse', 'cut', 'common'], transport: 'step-text' })
|
||||
const coordinator = new NativeOcctHistoryCoordinator(() => 2)
|
||||
const execution = await coordinator.capture(provider, { documentId: 'doc', documentVersion: 2, operationId: 'cut-1', operation: 'cut', objectStep: 'ISO-10303-21; object', toolStep: 'ISO-10303-21; tool' })
|
||||
assert.equal(execution.status, 'completed')
|
||||
assert.equal(execution.response?.protocolVersion, NATIVE_OCCT_HISTORY_PROTOCOL_VERSION)
|
||||
assert.equal(execution.response?.history.records[0].relation, 'modified')
|
||||
await assert.rejects(() => provider.capture({ protocolVersion: NATIVE_OCCT_HISTORY_PROTOCOL_VERSION, requestId: 'bad', documentId: 'doc', documentVersion: 2, operationId: 'bad', operation: 'cut', objectStep: 'not-step', toolStep: 'ISO-10303-21; tool' }, new AbortController().signal), /STEP text/)
|
||||
const stale = new NativeOcctHistoryCoordinator(() => 3)
|
||||
const staleExecution = await stale.capture(provider, { documentId: 'doc', documentVersion: 2, operationId: 'cut-2', operation: 'cut', objectStep: 'ISO-10303-21; object', toolStep: 'ISO-10303-21; tool' })
|
||||
assert.equal(staleExecution.status, 'stale')
|
||||
})
|
||||
|
||||
test('native OCCT history coordinator reports timeout and cancellation', async () => {
|
||||
const hanging: NativeOcctHistoryProvider = { capabilities: () => ({ providerId: 'test', providerVersion: '1', occtVersion: '8.0.0', availability: 'available', operations: ['cut'], transport: 'step-text' }), capture: async (_request, signal) => await new Promise<never>((_, reject) => { signal.addEventListener('abort', () => reject(new DOMException('cancelled', 'AbortError')), { once: true }) }) }
|
||||
const timedOut = await new NativeOcctHistoryCoordinator(() => 1, 5).capture(hanging, { documentId: 'doc', documentVersion: 1, operationId: 'cut-timeout', operation: 'cut', objectStep: 'ISO-10303-21; object', toolStep: 'ISO-10303-21; tool' })
|
||||
assert.equal(timedOut.status, 'timed-out')
|
||||
const coordinator = new NativeOcctHistoryCoordinator(() => 1, 120_000)
|
||||
const pending = coordinator.capture(hanging, { documentId: 'doc', documentVersion: 1, operationId: 'cut-cancel', operation: 'cut', objectStep: 'ISO-10303-21; object', toolStep: 'ISO-10303-21; tool' })
|
||||
coordinator.cancel()
|
||||
assert.equal((await pending).status, 'cancelled')
|
||||
})
|
||||
|
||||
test('native OCCT Worker provider preserves versioned request and response context', async () => {
|
||||
const messageListeners = new Set<(event: MessageEvent) => void>()
|
||||
const errorListeners = new Set<(event: ErrorEvent) => void>()
|
||||
const fakeWorker = {
|
||||
postMessage(message: { type: string; request?: { requestId: string; protocolVersion: number; documentId: string; documentVersion: number; operationId: string; objectStep: string; toolStep: string; operation: 'cut' } }) {
|
||||
if (message.type === 'initialize') {
|
||||
queueMicrotask(() => messageListeners.forEach((listener) => listener({ data: { type: 'ready', capabilities: { providerId: 'occt-native.history-step', providerVersion: '8.0.0-embind', occtVersion: '8.0.0', availability: 'available', operations: ['fuse', 'cut', 'common'], transport: 'step-text' } } } as MessageEvent)))
|
||||
} else if (message.type === 'capture' && message.request) {
|
||||
const request = message.request
|
||||
const response = {
|
||||
protocolVersion: request.protocolVersion,
|
||||
requestId: request.requestId,
|
||||
documentId: request.documentId,
|
||||
documentVersion: request.documentVersion,
|
||||
operationId: request.operationId,
|
||||
provider: { providerId: 'occt-native.history-step', providerVersion: '8.0.0-embind', occtVersion: '8.0.0', availability: 'available', operations: ['fuse', 'cut', 'common'], transport: 'step-text' },
|
||||
history: { provider: 'occt-native', occtVersion: '8.0.0', hasModified: true, hasGenerated: false, hasDeleted: false, records: [] },
|
||||
}
|
||||
queueMicrotask(() => messageListeners.forEach((listener) => listener({ data: { type: 'response', response } } as MessageEvent)))
|
||||
}
|
||||
},
|
||||
terminate() {},
|
||||
addEventListener(type: 'message' | 'error', listener: (event: MessageEvent | ErrorEvent) => void) { (type === 'message' ? messageListeners : errorListeners).add(listener as never) },
|
||||
removeEventListener(type: 'message' | 'error', listener: (event: MessageEvent | ErrorEvent) => void) { (type === 'message' ? messageListeners : errorListeners).delete(listener as never) },
|
||||
}
|
||||
const provider = new NativeOcctHistoryWorkerProvider({ workerFactory: () => fakeWorker })
|
||||
const request = { protocolVersion: NATIVE_OCCT_HISTORY_PROTOCOL_VERSION, requestId: 'worker-1', documentId: 'doc', documentVersion: 4, operationId: 'cut-worker', operation: 'cut' as const, objectStep: 'ISO-10303-21; object', toolStep: 'ISO-10303-21; tool' }
|
||||
const response = await provider.capture(request, new AbortController().signal)
|
||||
assert.equal(response.requestId, request.requestId)
|
||||
assert.equal(response.documentVersion, request.documentVersion)
|
||||
assert.equal(provider.capabilities().availability, 'available')
|
||||
provider.dispose()
|
||||
})
|
||||
|
||||
test('native OCCT Worker provider propagates AbortSignal cancellation to the worker', async () => {
|
||||
const messageListeners = new Set<(event: MessageEvent) => void>()
|
||||
const cancelRequests: string[] = []
|
||||
const fakeWorker = {
|
||||
postMessage(message: { type: string; requestId?: string }) {
|
||||
if (message.type === 'initialize') {
|
||||
queueMicrotask(() => messageListeners.forEach((listener) => listener({ data: { type: 'ready', capabilities: { providerId: 'occt-native.history-step', providerVersion: '8.0.0-embind', occtVersion: '8.0.0', availability: 'available', operations: ['cut'], transport: 'step-text' } } } as MessageEvent)))
|
||||
} else if (message.type === 'cancel' && message.requestId) cancelRequests.push(message.requestId)
|
||||
},
|
||||
terminate() {},
|
||||
addEventListener(type: 'message' | 'error', listener: (event: MessageEvent | ErrorEvent) => void) { if (type === 'message') messageListeners.add(listener as never) },
|
||||
removeEventListener(type: 'message' | 'error', listener: (event: MessageEvent | ErrorEvent) => void) { if (type === 'message') messageListeners.delete(listener as never) },
|
||||
}
|
||||
const provider = new NativeOcctHistoryWorkerProvider({ workerFactory: () => fakeWorker })
|
||||
const request = { protocolVersion: NATIVE_OCCT_HISTORY_PROTOCOL_VERSION, requestId: 'worker-cancel-1', documentId: 'doc', documentVersion: 4, operationId: 'cut-worker-cancel', operation: 'cut' as const, objectStep: 'ISO-10303-21; object', toolStep: 'ISO-10303-21; tool' }
|
||||
const controller = new AbortController()
|
||||
const pending = provider.capture(request, controller.signal)
|
||||
await new Promise<void>((resolve) => queueMicrotask(resolve))
|
||||
controller.abort()
|
||||
await assert.rejects(pending, (error: unknown) => error instanceof DOMException && error.name === 'AbortError')
|
||||
assert.deepEqual(cancelRequests, [request.requestId])
|
||||
provider.dispose()
|
||||
})
|
||||
|
||||
test('Bitbybit geometry runtime routes configured native history through STEP and the facade mapper', async () => {
|
||||
const runtime = new BitbybitGeometryRuntime()
|
||||
runtime.exportStep = async (shape, fileName = `${shape.id}.step`) => ({ format: 'step', fileName, mediaType: 'application/step', text: `ISO-10303-21; ${shape.id}` })
|
||||
let capturedRequestId = ''
|
||||
const provider: NativeOcctHistoryProvider = {
|
||||
capabilities: () => ({ providerId: 'occt-native.history-step', providerVersion: '8.0.0-embind', occtVersion: '8.0.0', availability: 'available', operations: ['cut'], transport: 'step-text' }),
|
||||
capture: async (request) => {
|
||||
capturedRequestId = request.requestId
|
||||
assert.match(request.objectStep, /base/)
|
||||
assert.match(request.toolStep, /tool/)
|
||||
return {
|
||||
protocolVersion: NATIVE_OCCT_HISTORY_PROTOCOL_VERSION,
|
||||
requestId: request.requestId,
|
||||
documentId: request.documentId,
|
||||
documentVersion: request.documentVersion,
|
||||
operationId: request.operationId,
|
||||
provider: provider.capabilities(),
|
||||
history: { provider: 'occt-native', occtVersion: '8.0.0', hasModified: true, hasGenerated: false, hasDeleted: true, records: [
|
||||
{ relation: 'modified', source: 'object', kind: 'face', sourceIndex: 0, resultIndex: 0 },
|
||||
{ relation: 'deleted', source: 'tool', kind: 'edge', sourceIndex: 1 },
|
||||
] },
|
||||
}
|
||||
},
|
||||
}
|
||||
runtime.configureNativeHistory(provider, 1000)
|
||||
const handle = (id: string): ShapeHandle => ({ id, kernel: 'bitbybit-occt', kind: 'solid', documentId: 'doc', documentVersion: 3 })
|
||||
const records = await runtime.topologyHistory({ documentId: 'doc', documentVersion: 3, operationId: 'cut-runtime', operation: 'cut', inputs: [{ objectId: 'base', shape: handle('base') }, { objectId: 'tool', shape: handle('tool') }], result: handle('result') })
|
||||
assert.match(capturedRequestId, /^native-history-/)
|
||||
assert.deepEqual(records, [
|
||||
{ sourceObjectId: 'base', sourceKind: 'face', sourceIndex: 0, relation: 'modified', resultKind: 'face', resultIndexes: [0] },
|
||||
{ sourceObjectId: 'tool', sourceKind: 'edge', sourceIndex: 1, relation: 'deleted' },
|
||||
])
|
||||
runtime.configureNativeHistory(null)
|
||||
assert.equal(runtime.nativeHistoryCapabilities().availability, 'unavailable')
|
||||
runtime.dispose()
|
||||
})
|
||||
|
||||
test('edge and vertex topology signatures are orientation/index independent', () => {
|
||||
const edge = signatureForEdge([0, 0, 0], [2, 0, 0])
|
||||
assert.equal(edge.hash, signatureForEdge([2, 0, 0], [0, 0, 0]).hash)
|
||||
@@ -1489,6 +1680,7 @@ test('FCStd inspection reports recognized, proxy, and Python-backed objects with
|
||||
assert.equal(inspection.label, 'Imported assembly')
|
||||
assert.equal(inspection.schemaVersion, '4')
|
||||
assert.equal(inspection.objects.find((object) => object.name === 'Body')?.label, 'Main body')
|
||||
assert.deepEqual(inspection.objects.find((object) => object.name === 'Body')?.properties, [{ name: 'Label', typeId: 'App::PropertyString', element: 'String', value: 'Main body' }])
|
||||
assert.equal(inspection.compatibility.recognizedObjects, 1)
|
||||
assert.equal(inspection.compatibility.proxyObjects, 1)
|
||||
assert.equal(inspection.compatibility.blockedObjects, 1)
|
||||
|
||||
Reference in New Issue
Block a user