P3/P4: execute OCCT features and export shapes
This commit is contained in:
@@ -8,7 +8,7 @@ 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 { createSketch, solveSketch } from '../src/facade/sketcher'
|
||||
import { RecomputeCoordinator } from '../src/facade/recomputeEngine'
|
||||
import { createFacadeGeometryRecomputeExecutor, RecomputeCoordinator, type RecomputeGeometryRuntime } from '../src/facade/recomputeEngine'
|
||||
import { inspectFcstdArchive } from '../src/facade/fcstd'
|
||||
import { assertShapeHandleIntegrity, normalizeBitbybitMesh, validateBooleanUnionInput, validateBoxInput, validateChamferInput, validateConeInput, validateCylinderInput, validateFilletInput, validatePadInput, validatePlacementInput, validatePlanarProfile, validateRevolutionInput, validateSphereInput } from '../src/facade/geometryRuntime'
|
||||
import type { DocumentSnapshot, ShapeHandle } from '../src/facade/types'
|
||||
@@ -345,6 +345,59 @@ test('recompute failure marks dependent objects as upstream-failed', async () =>
|
||||
assert.ok(result.errors.some((error) => error.code === 'UPSTREAM_FAILED'))
|
||||
})
|
||||
|
||||
test('OCCT feature executor builds a Sketch to Pad to Pocket chain and retains the last valid Shape', async () => {
|
||||
const shape = (id: string): ShapeHandle => ({ id, kernel: 'bitbybit-occt', kind: 'solid', documentId: 'doc-feature', documentVersion: 2 })
|
||||
const calls: string[] = []
|
||||
let padShouldFail = false
|
||||
const runtime: RecomputeGeometryRuntime = {
|
||||
capabilities: () => ({ status: 'ready' }),
|
||||
pad: async (input) => { calls.push(`pad:${input.profile.outer.length}`); if (padShouldFail) throw new Error('OCCT pad failed'); return shape('pad-shape') },
|
||||
pocket: async (input) => { calls.push(`pocket:${input.base.id}:${String(input.throughAll)}`); return shape('pocket-shape') },
|
||||
fillet: async () => shape('fillet-shape'),
|
||||
chamfer: async () => shape('chamfer-shape'),
|
||||
release: async (released) => { calls.push(`release:${released.id}`) },
|
||||
}
|
||||
const sketch = { id: 'sketch', typeId: 'Sketcher::SketchObject', properties: [], sketch: createSketch('sketch', [
|
||||
{ id: 'a', type: 'line' as const, start: { x: -1, y: -1 }, end: { x: 1, y: -1 } },
|
||||
{ id: 'b', type: 'line' as const, start: { x: 1, y: -1 }, end: { x: 1, y: 1 } },
|
||||
{ id: 'c', type: 'line' as const, start: { x: 1, y: 1 }, end: { x: -1, y: 1 } },
|
||||
{ id: 'd', type: 'line' as const, start: { x: -1, y: 1 }, end: { x: -1, y: -1 } },
|
||||
]) }
|
||||
const pad = { id: 'pad', typeId: 'PartDesign::Pad', properties: [
|
||||
{ name: 'Length', label: 'Length', group: 'Parameters', scope: 'data' as const, type: 'App::PropertyLength' as const, value: 10, unit: 'mm' },
|
||||
{ name: 'Profile', label: 'Profile', group: 'Parameters', scope: 'data' as const, type: 'App::PropertyLink' as const, value: 'sketch' },
|
||||
] }
|
||||
const pocket = { id: 'pocket', typeId: 'PartDesign::Pocket', properties: [
|
||||
{ name: 'Type', label: 'Type', group: 'Parameters', scope: 'data' as const, type: 'App::PropertyEnumeration' as const, value: 'Through all', options: ['Dimension', 'Through all', 'Up to face'] },
|
||||
{ name: 'Length', label: 'Length', group: 'Parameters', scope: 'data' as const, type: 'App::PropertyLength' as const, value: 5, unit: 'mm' },
|
||||
{ name: 'Profile', label: 'Profile', group: 'Parameters', scope: 'data' as const, type: 'App::PropertyLink' as const, value: 'sketch' },
|
||||
{ name: 'Base', label: 'Base', group: 'Parameters', scope: 'data' as const, type: 'App::PropertyLink' as const, value: 'pad' },
|
||||
] }
|
||||
const document: DocumentSnapshot = { ...recomputeDocumentFixture(), version: 2, objects: [sketch, pad, pocket], dependencies: [
|
||||
{ sourceId: 'pad', targetId: 'sketch', relation: 'link' },
|
||||
{ sourceId: 'pocket', targetId: 'pad', relation: 'link' },
|
||||
{ sourceId: 'pocket', targetId: 'sketch', relation: 'link' },
|
||||
], recompute: { generation: 0, status: 'idle', objectStates: { sketch: 'touched', pad: 'touched', pocket: 'touched' }, dirtyObjects: ['sketch'], order: [], errors: [] } }
|
||||
const shapes = new Map<string, ShapeHandle>()
|
||||
const executor = createFacadeGeometryRecomputeExecutor(runtime, shapes)
|
||||
const context = { documentId: document.id, documentVersion: document.version, generation: 1, signal: new AbortController().signal }
|
||||
const result = await new RecomputeCoordinator(executor, () => 2).run(document)
|
||||
assert.equal(result.status, 'completed')
|
||||
assert.deepEqual(calls.slice(0, 2), ['pad:3', 'pocket:pad-shape:true'])
|
||||
assert.equal(shapes.get('pad')?.id, 'pad-shape')
|
||||
assert.equal(shapes.get('pocket')?.id, 'pocket-shape')
|
||||
|
||||
padShouldFail = true
|
||||
const failed = await executor(pad, document, context)
|
||||
assert.equal(failed.status, 'failed')
|
||||
assert.equal(failed.errors?.[0].code, 'GEOMETRY_EXECUTION_FAILED')
|
||||
assert.equal(shapes.get('pad')?.id, 'pad-shape')
|
||||
const upToFace = { ...pocket, properties: pocket.properties.map((property) => property.name === 'Type' ? { ...property, value: 'Up to face' } : property) }
|
||||
const unsupported = await executor(upToFace, document, context)
|
||||
assert.equal(unsupported.status, 'failed')
|
||||
assert.equal(unsupported.errors?.[0].code, 'UP_TO_FACE_UNSUPPORTED')
|
||||
})
|
||||
|
||||
test('facade async recompute commits only an accepted generation', async () => {
|
||||
const facade = createMockFacade()
|
||||
facade.app.document.setProperty({ objectId: 'pad', propertyName: 'Length', value: 51 })
|
||||
|
||||
Reference in New Issue
Block a user