feat: connect Part primitives and booleans

This commit is contained in:
2026-08-02 19:22:59 -04:00
parent fe89d89540
commit 9e15a6b10d
6 changed files with 145 additions and 13 deletions

View File

@@ -351,6 +351,13 @@ test('OCCT feature executor builds a Sketch to Pad to Pocket chain and retains t
let padShouldFail = false
const runtime: RecomputeGeometryRuntime = {
capabilities: () => ({ status: 'ready' }),
createBox: async () => shape('box-shape'),
createCylinder: async () => shape('cylinder-shape'),
createSphere: async () => shape('sphere-shape'),
createCone: async () => shape('cone-shape'),
union: async () => shape('union-shape'),
cut: async () => shape('cut-shape'),
intersection: async () => shape('intersection-shape'),
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') },
revolution: async (input) => { calls.push(`revolution:${input.angle}:${input.axisDirection?.[1]}`); return shape('revolution-shape') },
@@ -409,6 +416,53 @@ test('OCCT feature executor builds a Sketch to Pad to Pocket chain and retains t
assert.ok(calls.includes('revolution:270:-1'))
})
test('Part primitive and boolean nodes execute through the same Shape cache', async () => {
const shape = (id: string): ShapeHandle => ({ id, kernel: 'bitbybit-occt', kind: 'solid', documentId: 'doc-part', documentVersion: 2 })
const calls: string[] = []
const runtime: RecomputeGeometryRuntime = {
capabilities: () => ({ status: 'ready' }),
createBox: async (input) => { calls.push(`box:${input.width}:${input.length}:${input.height}`); return shape('box-shape') },
createCylinder: async () => shape('cylinder-shape'),
createSphere: async () => shape('sphere-shape'),
createCone: async () => shape('cone-shape'),
union: async (input) => { calls.push(`union:${input.shapes.map((entry) => entry.id).join(',')}`); return shape('fuse-shape') },
cut: async () => shape('cut-shape'),
intersection: async () => shape('common-shape'),
pad: async () => shape('pad-shape'),
pocket: async () => shape('pocket-shape'),
revolution: async () => shape('revolution-shape'),
fillet: async () => shape('fillet-shape'),
chamfer: async () => shape('chamfer-shape'),
release: async () => undefined,
}
const boxProperties = (width: number) => [
{ name: 'Width', label: 'Width', group: 'Box', scope: 'data' as const, type: 'App::PropertyLength' as const, value: width },
{ name: 'Length', label: 'Length', group: 'Box', scope: 'data' as const, type: 'App::PropertyLength' as const, value: 4 },
{ name: 'Height', label: 'Height', group: 'Box', scope: 'data' as const, type: 'App::PropertyLength' as const, value: 5 },
]
const document: DocumentSnapshot = {
id: 'doc-part', label: 'Part fixture', version: 2, dirty: true, readOnly: false, units: 'mm',
tree: [{ id: 'boxA', label: 'Box', type: 'feature', state: 'dirty' }, { id: 'boxB', label: 'Box001', type: 'feature', state: 'dirty' }, { id: 'fuse', label: 'Union', type: 'feature', state: 'dirty' }],
objects: [
{ id: 'boxA', typeId: 'Part::Box', properties: boxProperties(2) },
{ id: 'boxB', typeId: 'Part::Box', properties: boxProperties(3) },
{ id: 'fuse', typeId: 'Part::Fuse', properties: [
{ name: 'Base', label: 'Base', group: 'Boolean', scope: 'data', type: 'App::PropertyLink' as const, value: 'boxA' },
{ name: 'Tool', label: 'Tool', group: 'Boolean', scope: 'data', type: 'App::PropertyLink' as const, value: 'boxB' },
] },
],
dependencies: [{ sourceId: 'fuse', targetId: 'boxA', relation: 'link' }, { sourceId: 'fuse', targetId: 'boxB', relation: 'link' }],
recompute: { generation: 0, status: 'idle', objectStates: { boxA: 'touched', boxB: 'touched', fuse: 'touched' }, dirtyObjects: ['boxA', 'boxB'], order: [], errors: [] },
}
const shapes = new Map<string, ShapeHandle>()
const result = await new RecomputeCoordinator(createFacadeGeometryRecomputeExecutor(runtime, shapes), () => 2).run(document)
assert.equal(result.status, 'completed')
assert.equal(shapes.get('boxA')?.id, 'box-shape')
assert.equal(shapes.get('fuse')?.id, 'fuse-shape')
assert.ok(calls.includes('box:2:4:5'))
assert.ok(calls.includes('union:box-shape,box-shape'))
})
test('facade async recompute commits only an accepted generation', async () => {
const facade = createMockFacade()
facade.app.document.setProperty({ objectId: 'pad', propertyName: 'Length', value: 51 })
@@ -728,3 +782,23 @@ test('Part Design feature tasks commit a document object and remain undoable', (
facade.app.document.setProperty({ objectId: 'revolution', propertyName: 'Angle', value: 180 })
assert.equal(facade.app.document.getObject('revolution')?.properties.find((property) => property.name === 'Angle')?.value, 180)
})
test('Part workbench commands create primitive and boolean document objects', () => {
const facade = createMockFacade()
facade.gui.workbench.setActive('Part')
assert.equal(facade.gui.command.getState('primitive').status, 'enabled')
facade.gui.command.execute({ commandId: 'primitive' })
facade.task.apply()
assert.equal(facade.app.document.getObject('box')?.typeId, 'Part::Box')
facade.gui.command.execute({ commandId: 'primitive' })
facade.task.apply()
assert.equal(facade.app.document.getObject('box001')?.typeId, 'Part::Box')
assert.equal(facade.gui.command.getState('union').status, 'enabled')
facade.gui.command.execute({ commandId: 'union' })
facade.task.apply()
assert.equal(facade.app.document.getObject('union')?.typeId, 'Part::Fuse')
facade.app.document.setProperty({ objectId: 'union', propertyName: 'Base', value: 'box' })
facade.app.document.setProperty({ objectId: 'union', propertyName: 'Tool', value: 'box001' })
assert.deepEqual(facade.app.document.getDependencies().filter((edge) => edge.sourceId === 'union').map((edge) => edge.targetId).sort(), ['box', 'box001'])
})