feat: add whole-shape mirrored feature
This commit is contained in:
@@ -14,7 +14,7 @@ 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, validatePadInput, validatePlacementInput, validatePlanarProfile, validateRevolutionInput, validateSphereInput } from '../src/facade/geometryRuntime'
|
||||
import { assertShapeHandleIntegrity, normalizeBitbybitMesh, validateBooleanUnionInput, validateBoxInput, validateChamferInput, validateConeInput, validateCylinderInput, validateFilletInput, validateMirrorInput, validatePadInput, validatePlacementInput, validatePlanarProfile, validateRevolutionInput, validateSphereInput } from '../src/facade/geometryRuntime'
|
||||
import type { DocumentObjectSnapshot, DocumentSnapshot, ObjectTopologySnapshot, ShapeHandle, SubshapeTopology } from '../src/facade/types'
|
||||
|
||||
const recomputeDocumentFixture = (edges: DocumentSnapshot['dependencies'] = []): DocumentSnapshot => ({
|
||||
@@ -61,6 +61,7 @@ test('geometry boundary validates dimensions before invoking OCCT', () => {
|
||||
assert.throws(() => validateSphereInput({ radius: -1, documentId: 'doc', documentVersion: 1 }), /radius/)
|
||||
assert.throws(() => validateConeInput({ radius1: 0, radius2: 0, height: 2, documentId: 'doc', documentVersion: 1 }), /radius/)
|
||||
assert.throws(() => validatePlacementInput({ shape: { id: 'shape', kernel: 'bitbybit-occt', kind: 'solid', documentId: 'doc', documentVersion: 1 }, placement: { translation: [0, 0, 0], rotationAxis: [0, 0, 0], rotationAngle: 0 }, documentId: 'doc', documentVersion: 2 }), /rotationAxis/)
|
||||
assert.throws(() => validateMirrorInput({ shape: { id: 'shape', kernel: 'bitbybit-occt', kind: 'solid', documentId: 'doc', documentVersion: 1 }, origin: [0, 0, 0], normal: [0, 0, 0], documentId: 'doc', documentVersion: 1 }), /normal/)
|
||||
})
|
||||
|
||||
test('geometry handles reject forged document ownership', () => {
|
||||
@@ -704,6 +705,46 @@ test('Part primitive and boolean nodes execute through the same Shape cache', as
|
||||
assert.ok(calls.includes('union:box-shape,box-shape'))
|
||||
})
|
||||
|
||||
test('Mirrored recompute mirrors a whole Shape across a document plane and releases fused copies', async () => {
|
||||
const calls: string[] = []
|
||||
const shape = (id: string): ShapeHandle => ({ id, kernel: 'bitbybit-occt', kind: 'solid', documentId: 'doc-mirrored', documentVersion: 4 })
|
||||
let mirrorSequence = 0
|
||||
const runtime: RecomputeGeometryRuntime = {
|
||||
capabilities: () => ({ status: 'ready' }),
|
||||
createBox: async () => shape('box-shape'),
|
||||
createCylinder: async () => shape('cylinder'),
|
||||
createSphere: async () => shape('sphere'),
|
||||
createCone: async () => shape('cone'),
|
||||
applyPlacement: async (input) => input.shape,
|
||||
mirror: async (input) => { const result = shape(`mirror-${++mirrorSequence}`); calls.push(`mirror:${input.shape.id}:${input.origin.join(',')}:${input.normal.join(',')}`); return result },
|
||||
union: async (input) => { calls.push(`union:${input.shapes.map((entry) => entry.id).join(',')}`); return shape('mirrored-union') },
|
||||
cut: async () => shape('cut'), intersection: async () => shape('intersection'), pad: async () => shape('pad'), pocket: async () => shape('pocket'), revolution: async () => shape('revolution'), fillet: async () => shape('fillet'), chamfer: async () => shape('chamfer'),
|
||||
release: async (released) => { calls.push(`release:${released.id}`) },
|
||||
}
|
||||
const base: DocumentObjectSnapshot = { id: 'box', typeId: 'Part::Box', properties: [] }
|
||||
const mirrored: DocumentObjectSnapshot = { id: 'mirrored', typeId: 'PartDesign::Mirrored', properties: [
|
||||
{ name: 'Base', label: 'Base', group: 'Mirrored', scope: 'data', type: 'App::PropertyLink', value: 'box' },
|
||||
{ name: 'Plane', label: 'Plane', group: 'Mirrored', scope: 'data', type: 'App::PropertyEnumeration', value: 'XZ plane', options: ['XY plane', 'XZ plane', 'YZ plane'] },
|
||||
{ name: 'Fuse', label: 'Fuse', group: 'Mirrored', scope: 'data', type: 'App::PropertyBool', value: true },
|
||||
] }
|
||||
const document: DocumentSnapshot = { ...recomputeDocumentFixture(), id: 'doc-mirrored', version: 4, tree: [{ id: 'box', label: 'Box', type: 'feature' }, { id: 'mirrored', label: 'Mirrored', type: 'feature' }], objects: [base, mirrored], dependencies: [{ sourceId: 'mirrored', targetId: 'box', relation: 'link' }] }
|
||||
const shapes = new Map<string, ShapeHandle>()
|
||||
const executor = createFacadeGeometryRecomputeExecutor(runtime, shapes)
|
||||
await executor(base, document, { documentId: document.id, documentVersion: 4, generation: 1, signal: new AbortController().signal })
|
||||
const fused = await executor(mirrored, document, { documentId: document.id, documentVersion: 4, generation: 1, signal: new AbortController().signal })
|
||||
assert.equal(fused.status, 'success')
|
||||
assert.equal(shapes.get('mirrored')?.id, 'mirrored-union')
|
||||
assert.ok(calls.includes('mirror:box-shape:0,0,0:0,1,0'))
|
||||
assert.ok(calls.includes('union:box-shape,mirror-1'))
|
||||
assert.ok(calls.includes('release:mirror-1'))
|
||||
|
||||
const unfused = { ...mirrored, id: 'mirrored-unfused', properties: mirrored.properties.map((property) => property.name === 'Fuse' ? { ...property, value: false } : property) }
|
||||
const unfusedResult = await executor(unfused, { ...document, objects: [base, unfused] }, { documentId: document.id, documentVersion: 4, generation: 2, signal: new AbortController().signal })
|
||||
assert.equal(unfusedResult.status, 'success')
|
||||
assert.equal(shapes.get('mirrored-unfused')?.id, 'mirror-2')
|
||||
assert.equal(calls.includes('release:mirror-2'), false)
|
||||
})
|
||||
|
||||
test('object Placement transforms recomputed shapes before committing the cache', async () => {
|
||||
const shape = (id: string): ShapeHandle => ({ id, kernel: 'bitbybit-occt', kind: 'solid', documentId: 'doc-placement', documentVersion: 3 })
|
||||
const calls: string[] = []
|
||||
@@ -1112,10 +1153,10 @@ test('disabled commands return a reason instead of mutating the document', () =>
|
||||
test('manifest commands without a Bitbybit executor are explicitly unsupported', () => {
|
||||
const facade = createMockFacade()
|
||||
const before = facade.getState().document.version
|
||||
const state = facade.gui.command.getState('mirrored')
|
||||
const state = facade.gui.command.getState('multi-transform')
|
||||
assert.equal(state.status, 'disabled')
|
||||
assert.match(state.reason || '', /business executor/)
|
||||
facade.gui.command.execute({ commandId: 'mirrored' })
|
||||
facade.gui.command.execute({ commandId: 'multi-transform' })
|
||||
assert.equal(facade.getState().diagnostics.at(-1)?.code, 'COMMAND_UNIMPLEMENTED')
|
||||
assert.equal(facade.getState().document.version, before)
|
||||
})
|
||||
@@ -1486,6 +1527,17 @@ test('Part Design feature tasks commit a document object and remain undoable', (
|
||||
assert.equal(hole?.properties.find((property) => property.name === 'Depth')?.value, 12)
|
||||
assert.equal(hole?.properties.find((property) => property.name === 'Type')?.value, 'Through all')
|
||||
assert.equal(facade.app.document.getObject('body')?.properties.find((property) => property.name === 'Tip')?.value, 'hole')
|
||||
|
||||
assert.equal(facade.gui.command.getState('mirrored').status, 'enabled')
|
||||
facade.gui.command.execute({ commandId: 'mirrored' })
|
||||
facade.task.update({ plane: 'XZ plane', fuse: false })
|
||||
facade.task.apply()
|
||||
const mirrored = facade.app.document.getObject('mirrored')
|
||||
assert.equal(mirrored?.typeId, 'PartDesign::Mirrored')
|
||||
assert.equal(mirrored?.properties.find((property) => property.name === 'Base')?.value, 'hole')
|
||||
assert.equal(mirrored?.properties.find((property) => property.name === 'Plane')?.value, 'XZ plane')
|
||||
assert.equal(mirrored?.properties.find((property) => property.name === 'Fuse')?.value, false)
|
||||
assert.equal(facade.app.document.getObject('body')?.properties.find((property) => property.name === 'Tip')?.value, 'mirrored')
|
||||
})
|
||||
|
||||
test('Part workbench commands create primitive and boolean document objects', () => {
|
||||
|
||||
Reference in New Issue
Block a user