feat: add experimental linear pattern feature

This commit is contained in:
2026-08-02 23:13:09 -04:00
parent aeafc6c607
commit 8995b90917
8 changed files with 97 additions and 21 deletions

View File

@@ -377,7 +377,8 @@ test('OCCT feature executor builds a Sketch to Pad to Pocket chain and retains t
createCylinder: async () => shape('cylinder-shape'),
createSphere: async () => shape('sphere-shape'),
createCone: async () => shape('cone-shape'),
union: async () => shape('union-shape'),
applyPlacement: async (input) => { calls.push(`placement:${input.placement.translation.join(',')}`); return shape(`pattern-copy-${calls.filter((call) => call.startsWith('placement:')).length}`) },
union: async (input) => { calls.push(`union:${input.shapes.map((entry) => entry.id).join(',')}`); return 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') },
@@ -437,6 +438,26 @@ test('OCCT feature executor builds a Sketch to Pad to Pocket chain and retains t
assert.equal(shapes.get('revolution')?.id, 'revolution-shape')
assert.ok(calls.includes('revolution:270:-1'))
const linearPattern = { id: 'linear-pattern', typeId: 'PartDesign::LinearPattern', properties: [
{ name: 'Base', label: 'Base', group: 'Pattern', scope: 'data' as const, type: 'App::PropertyLink' as const, value: 'pad' },
{ name: 'Occurrences', label: 'Occurrences', group: 'Pattern', scope: 'data' as const, type: 'App::PropertyInteger' as const, value: 3 },
{ name: 'Length', label: 'Length', group: 'Pattern', scope: 'data' as const, type: 'App::PropertyLength' as const, value: 20, unit: 'mm' },
{ name: 'Direction', label: 'Direction', group: 'Pattern', scope: 'data' as const, type: 'App::PropertyEnumeration' as const, value: 'Vertical', options: ['Horizontal', 'Vertical', 'Normal'] },
] }
const patternResult = await executor(linearPattern, document, context)
assert.equal(patternResult.status, 'success')
assert.equal(shapes.get('linear-pattern')?.id, 'union-shape')
assert.ok(calls.includes('placement:0,10,0'))
assert.ok(calls.includes('placement:0,20,0'))
assert.ok(calls.includes('union:pad-shape,pattern-copy-1,pattern-copy-2'))
assert.ok(calls.includes('release:pattern-copy-1'))
assert.ok(calls.includes('release:pattern-copy-2'))
const invalidPattern = { ...linearPattern, properties: linearPattern.properties.map((property) => property.name === 'Occurrences' ? { ...property, value: 1 } : property) }
const invalidPatternResult = await executor(invalidPattern, document, context)
assert.equal(invalidPatternResult.status, 'failed')
assert.equal(invalidPatternResult.errors?.[0].code, 'PATTERN_OCCURRENCES_INVALID')
const suppressedPad = { ...pad, properties: [...pad.properties, { name: 'Suppressed', label: 'Suppressed', group: 'Feature state', scope: 'data' as const, type: 'App::PropertyBool' as const, value: true }] }
const suppressedResult = await executor(suppressedPad, document, context)
assert.equal(suppressedResult.status, 'suppressed')
@@ -453,6 +474,7 @@ test('Part primitive and boolean nodes execute through the same Shape cache', as
createCylinder: async () => shape('cylinder-shape'),
createSphere: async () => shape('sphere-shape'),
createCone: async () => shape('cone-shape'),
applyPlacement: async (input) => { calls.push(`placement:${input.placement.translation.join(',')}`); return shape('placed-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'),
@@ -620,10 +642,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('linear-pattern')
const state = facade.gui.command.getState('polar-pattern')
assert.equal(state.status, 'disabled')
assert.match(state.reason || '', /business executor/)
facade.gui.command.execute({ commandId: 'linear-pattern' })
facade.gui.command.execute({ commandId: 'polar-pattern' })
assert.equal(facade.getState().diagnostics.at(-1)?.code, 'COMMAND_UNIMPLEMENTED')
assert.equal(facade.getState().document.version, before)
})
@@ -878,6 +900,25 @@ test('Part Design feature tasks commit a document object and remain undoable', (
assert.equal(revolution?.properties.find((property) => property.name === 'Angle')?.value, 360)
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)
facade.gui.command.execute({ commandId: 'linear-pattern' })
assert.equal(facade.task.getActive()?.commandId, 'linear-pattern')
facade.task.update({ occurrences: 4, length: 30, direction: 'Vertical' })
facade.task.apply()
const pattern = facade.app.document.getObject('linear-pattern')
assert.equal(pattern?.typeId, 'PartDesign::LinearPattern')
assert.equal(pattern?.properties.find((property) => property.name === 'Base')?.value, 'revolution')
assert.equal(pattern?.properties.find((property) => property.name === 'Occurrences')?.value, 4)
assert.equal(pattern?.properties.find((property) => property.name === 'Length')?.value, 30)
assert.equal(pattern?.properties.find((property) => property.name === 'Direction')?.value, 'Vertical')
assert.equal(facade.app.document.getObject('body')?.properties.find((property) => property.name === 'Tip')?.value, 'linear-pattern')
assert.throws(() => facade.app.document.setProperty({ objectId: 'linear-pattern', propertyName: 'Occurrences', value: 1 }), /between 2 and 100/)
facade.gui.command.execute({ commandId: 'linear-pattern' })
facade.task.update({ occurrences: 1, length: 0 })
assert.throws(() => facade.task.apply(), /between 2 and 100/)
assert.equal(facade.app.document.getObject('linear-pattern001'), null)
facade.task.cancel()
})
test('Part workbench commands create primitive and boolean document objects', () => {