feat: add Part primitive task selection

This commit is contained in:
2026-08-02 19:25:37 -04:00
parent 9e15a6b10d
commit 42e59ac068
3 changed files with 12 additions and 8 deletions

View File

@@ -345,7 +345,9 @@ function TreeItem({ item, level, expanded, onToggle, itemsById, selectedObject,
function TaskPanel({ workbench, facade, showNotice }: { workbench: Workbench; facade: BitBybitWebCadFacade; showNotice: (message: string) => void }) { function TaskPanel({ workbench, facade, showNotice }: { workbench: Workbench; facade: BitBybitWebCadFacade; showNotice: (message: string) => void }) {
const definition = workbenchDefinitions[workbench] const definition = workbenchDefinitions[workbench]
const isSketch = workbench === 'Sketcher' const isSketch = workbench === 'Sketcher'
return <div className="task-panel"><div className="task-actions-top"><button className="button button-primary" onClick={() => { facade.task.apply(); showNotice('Task accepted') }}>OK</button><button className="button button-outline" onClick={() => { facade.task.update({ preview: true }); showNotice('Preview applied') }}>Apply</button><button className="button button-quiet" onClick={() => { facade.task.cancel(); showNotice('Task cancelled') }}>Cancel</button></div><div className="task-header"><div className="task-icon"><Pencil size={16} /></div><div><span className="eyebrow">Active command</span><h2>{isSketch ? 'Edit Sketch' : definition.taskTitle}</h2></div><Badge tone="cyan">Preview</Badge></div><div className="task-body"><div className="task-step"><span className="step-index">1</span><div><strong>{isSketch ? 'Geometry and constraints' : definition.objectType}</strong><span>{definition.taskSummary}</span></div></div><label className="field-label">Primary value <span className="field-unit">mm</span><input className="field-input" value={isSketch ? 'Fully constrained' : '42.00'} readOnly /></label><label className="field-label">Operation<select className="field-input"><option>{workbench === 'Part Design' ? 'Dimension' : 'Contextual preview'}</option><option>Through all</option><option>Up to face</option></select></label><label className="check-row"><input type="checkbox" defaultChecked /><span>Preview result in viewport</span></label><div className="task-note"><AlertTriangle size={14} /><span>Changes remain local until the document is recomputed.</span></div></div></div> const activeTask = facade.task.getActive()
const primitiveType = activeTask?.commandId === 'primitive' && typeof activeTask.draft.primitiveType === 'string' ? activeTask.draft.primitiveType : 'Box'
return <div className="task-panel"><div className="task-actions-top"><button className="button button-primary" onClick={() => { facade.task.apply(); showNotice('Task accepted') }}>OK</button><button className="button button-outline" onClick={() => { facade.task.update({ preview: true }); showNotice('Preview applied') }}>Apply</button><button className="button button-quiet" onClick={() => { facade.task.cancel(); showNotice('Task cancelled') }}>Cancel</button></div><div className="task-header"><div className="task-icon"><Pencil size={16} /></div><div><span className="eyebrow">Active command</span><h2>{isSketch ? 'Edit Sketch' : definition.taskTitle}</h2></div><Badge tone="cyan">Preview</Badge></div><div className="task-body"><div className="task-step"><span className="step-index">1</span><div><strong>{isSketch ? 'Geometry and constraints' : definition.objectType}</strong><span>{definition.taskSummary}</span></div></div>{activeTask?.commandId === 'primitive' && <label className="field-label">Primitive type<select className="field-input" value={primitiveType} onChange={(event) => facade.task.update({ primitiveType: event.target.value })}><option>Box</option><option>Cylinder</option><option>Sphere</option><option>Cone</option></select></label>}<label className="field-label">Primary value <span className="field-unit">mm</span><input className="field-input" value={isSketch ? 'Fully constrained' : '42.00'} readOnly /></label><label className="field-label">Operation<select className="field-input"><option>{workbench === 'Part Design' ? 'Dimension' : 'Contextual preview'}</option><option>Through all</option><option>Up to face</option></select></label><label className="check-row"><input type="checkbox" defaultChecked /><span>Preview result in viewport</span></label><div className="task-note"><AlertTriangle size={14} /><span>Changes remain local until the document is recomputed.</span></div></div></div>
} }
function PropertyPanel({ facade, objectId, scope, showNotice }: { facade: BitBybitWebCadFacade; objectId: string; scope: 'data' | 'view'; showNotice: (message: string) => void }) { function PropertyPanel({ facade, objectId, scope, showNotice }: { facade: BitBybitWebCadFacade; objectId: string; scope: 'data' | 'view'; showNotice: (message: string) => void }) {

View File

@@ -287,8 +287,9 @@ export function createMockFacade(): BitBybitWebCadFacade {
const existing = state.document.tree.filter((item) => item.label.toLowerCase().startsWith(base)).length const existing = state.document.tree.filter((item) => item.label.toLowerCase().startsWith(base)).length
return existing === 0 ? base : `${base}${String(existing).padStart(3, '0')}` return existing === 0 ? base : `${base}${String(existing).padStart(3, '0')}`
} }
const appendFeature = (document: DocumentSnapshot, commandId: string): { document: DocumentSnapshot; objectId: string } => { const appendFeature = (document: DocumentSnapshot, commandId: string, draft: Record<string, unknown> = {}): { document: DocumentSnapshot; objectId: string } => {
const definition = featureCommands[commandId] const primitiveType = commandId === 'primitive' && ['Box', 'Cylinder', 'Sphere', 'Cone'].includes(String(draft.primitiveType)) ? String(draft.primitiveType) : 'Box'
const definition = commandId === 'primitive' ? { label: primitiveType, detail: primitiveType === 'Box' ? '10 × 10 × 10 mm' : `${primitiveType} primitive` } : featureCommands[commandId]
if (!definition) return { document, objectId: '' } if (!definition) return { document, objectId: '' }
const objectId = nextFeatureId(definition.label) const objectId = nextFeatureId(definition.label)
const type = commandId === 'create-sketch' ? 'sketch' : commandId === 'create-body' ? 'body' : 'feature' const type = commandId === 'create-sketch' ? 'sketch' : commandId === 'create-body' ? 'body' : 'feature'
@@ -472,14 +473,14 @@ export function createMockFacade(): BitBybitWebCadFacade {
const applyTask = () => { const applyTask = () => {
const task = state.task const task = state.task
if (!task || task.status !== 'preview') return if (!task || task.status !== 'preview') return
const result = appendFeature(state.document, task.commandId) const result = appendFeature(state.document, task.commandId, task.draft)
if (!result.objectId) { if (!result.objectId) {
state = { ...state, task: { ...task, status: 'completed' } } state = { ...state, task: { ...task, status: 'completed' } }
emitState() emitState()
return return
} }
commit({ ...state, document: result.document, selectedObjectId: result.objectId, task: { ...task, status: 'completed' } }) commit({ ...state, document: result.document, selectedObjectId: result.objectId, task: { ...task, status: 'completed' } })
notify(`${featureCommands[task.commandId].label} created`) notify(`${result.document.tree.find((item) => item.id === result.objectId)?.label || featureCommands[task.commandId]?.label || task.commandId} created`)
} }
const execute = ({ commandId, payload }: ExecuteCommandInput) => { const execute = ({ commandId, payload }: ExecuteCommandInput) => {
const requestId = `req-${++requestSequence}` const requestId = `req-${++requestSequence}`

View File

@@ -791,14 +791,15 @@ test('Part workbench commands create primitive and boolean document objects', ()
facade.task.apply() facade.task.apply()
assert.equal(facade.app.document.getObject('box')?.typeId, 'Part::Box') assert.equal(facade.app.document.getObject('box')?.typeId, 'Part::Box')
facade.gui.command.execute({ commandId: 'primitive' }) facade.gui.command.execute({ commandId: 'primitive' })
facade.task.update({ primitiveType: 'Cylinder' })
facade.task.apply() facade.task.apply()
assert.equal(facade.app.document.getObject('box001')?.typeId, 'Part::Box') assert.equal(facade.app.document.getObject('cylinder')?.typeId, 'Part::Cylinder')
assert.equal(facade.gui.command.getState('union').status, 'enabled') assert.equal(facade.gui.command.getState('union').status, 'enabled')
facade.gui.command.execute({ commandId: 'union' }) facade.gui.command.execute({ commandId: 'union' })
facade.task.apply() facade.task.apply()
assert.equal(facade.app.document.getObject('union')?.typeId, 'Part::Fuse') 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: 'Base', value: 'box' })
facade.app.document.setProperty({ objectId: 'union', propertyName: 'Tool', value: 'box001' }) facade.app.document.setProperty({ objectId: 'union', propertyName: 'Tool', value: 'cylinder' })
assert.deepEqual(facade.app.document.getDependencies().filter((edge) => edge.sourceId === 'union').map((edge) => edge.targetId).sort(), ['box', 'box001']) assert.deepEqual(facade.app.document.getDependencies().filter((edge) => edge.sourceId === 'union').map((edge) => edge.targetId).sort(), ['box', 'cylinder'])
}) })