feat: wire Part Design task parameters

This commit is contained in:
2026-08-02 19:44:54 -04:00
parent ed3d1cddc5
commit 288260b1e2
3 changed files with 22 additions and 8 deletions

View File

@@ -351,7 +351,16 @@ function TaskPanel({ workbench, facade, showNotice }: { workbench: Workbench; fa
const primitiveUnits: Record<string, string> = { width: 'mm', length: 'mm', height: 'mm', radius: 'mm', radius1: 'mm', radius2: 'mm', angle: 'deg' } const primitiveUnits: Record<string, string> = { width: 'mm', length: 'mm', height: 'mm', radius: 'mm', radius1: 'mm', radius2: 'mm', angle: 'deg' }
const primitiveFallbacks: Record<string, number> = { width: 10, length: 10, height: 10, radius: 5, radius1: 5, radius2: 0, angle: 360 } const primitiveFallbacks: Record<string, number> = { width: 10, length: 10, height: 10, radius: 5, radius1: 5, radius2: 0, angle: 360 }
const primitiveLabel = (field: string) => field === 'radius1' ? 'Bottom radius' : field === 'radius2' ? 'Top radius' : field[0].toUpperCase() + field.slice(1) const primitiveLabel = (field: string) => field === 'radius1' ? 'Bottom radius' : field === 'radius2' ? 'Top radius' : field[0].toUpperCase() + field.slice(1)
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>{primitiveFields.map((field) => <label className="field-label" key={field}>{primitiveLabel(field)} <span className="field-unit">{primitiveUnits[field]}</span><input className="field-input" type="number" min={field === 'radius2' ? 0 : 0.001} step={primitiveUnits[field] === 'deg' ? 1 : 0.1} value={typeof activeTask.draft[field] === 'number' ? Number(activeTask.draft[field]) : primitiveFallbacks[field]} onChange={(event) => facade.task.update({ [field]: Number(event.target.value) })} /></label>)}</>}{activeTask?.commandId !== 'primitive' && <><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 featureFields: Record<string, { key: string; label: string; unit: string; fallback: number }> = {
pad: { key: 'length', label: 'Length', unit: 'mm', fallback: 42 },
pocket: { key: 'length', label: 'Length', unit: 'mm', fallback: 18 },
revolution: { key: 'angle', label: 'Angle', unit: 'deg', fallback: 360 },
fillet: { key: 'radius', label: 'Radius', unit: 'mm', fallback: 3 },
chamfer: { key: 'distance', label: 'Distance', unit: 'mm', fallback: 2 },
}
const currentFeatureField = activeTask?.commandId ? featureFields[activeTask.commandId] : undefined
const activeCommand = activeTask?.commandId
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>{activeCommand === '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>{primitiveFields.map((field) => <label className="field-label" key={field}>{primitiveLabel(field)} <span className="field-unit">{primitiveUnits[field]}</span><input className="field-input" type="number" min={field === 'radius2' ? 0 : 0.001} step={primitiveUnits[field] === 'deg' ? 1 : 0.1} value={typeof activeTask?.draft[field] === 'number' ? Number(activeTask?.draft[field]) : primitiveFallbacks[field]} onChange={(event) => facade.task.update({ [field]: Number(event.target.value) })} /></label>)}</>}{currentFeatureField && <label className="field-label">{currentFeatureField.label} <span className="field-unit">{currentFeatureField.unit}</span><input className="field-input" type="number" min={0.001} step={currentFeatureField.unit === 'deg' ? 1 : 0.1} value={typeof activeTask?.draft[currentFeatureField.key] === 'number' ? Number(activeTask?.draft[currentFeatureField.key]) : currentFeatureField.fallback} onChange={(event) => facade.task.update({ [currentFeatureField.key]: Number(event.target.value) })} /></label>}{activeCommand === 'pocket' && <label className="field-label">Type<select className="field-input" value={typeof activeTask?.draft.type === 'string' ? activeTask.draft.type : 'Through all'} onChange={(event) => facade.task.update({ type: event.target.value })}><option>Dimension</option><option>Through all</option><option>Up to face</option></select></label>}{['pad', 'pocket', 'revolution'].includes(activeCommand || '') && <label className="check-row"><input type="checkbox" checked={activeTask?.draft.reversed === true} onChange={(event) => facade.task.update({ reversed: event.target.checked })} /><span>Reversed</span></label>}{activeCommand === 'pad' && <label className="check-row"><input type="checkbox" checked={activeTask?.draft.midplane === true} onChange={(event) => facade.task.update({ midplane: event.target.checked })} /><span>Symmetric to plane</span></label>}{!activeCommand || activeCommand === 'create-sketch' ? <><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></> : null}<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

@@ -303,12 +303,14 @@ export function createMockFacade(): BitBybitWebCadFacade {
tree.push(item) tree.push(item)
} }
const objectSnapshot = createObjectSnapshot(item) const objectSnapshot = createObjectSnapshot(item)
if (commandId === 'primitive') {
objectSnapshot.properties = objectSnapshot.properties.map((property) => { objectSnapshot.properties = objectSnapshot.properties.map((property) => {
if (!property.recompute || property.type === 'App::PropertyLink') return property
const candidate = draft[property.name.toLowerCase()] const candidate = draft[property.name.toLowerCase()]
return typeof candidate === 'number' && Number.isFinite(candidate) ? { ...property, value: candidate } : property if (property.type === 'App::PropertyBool' && typeof candidate === 'boolean') return { ...property, value: candidate }
if ((property.type === 'App::PropertyLength' || property.type === 'App::PropertyAngle' || property.type === 'App::PropertyPercent' || property.type === 'App::PropertyFloat') && typeof candidate === 'number' && Number.isFinite(candidate)) return { ...property, value: candidate }
if (property.type === 'App::PropertyEnumeration' && typeof candidate === 'string' && property.options?.includes(candidate)) return { ...property, value: candidate }
return property
}) })
}
if (isPartObject && commandId !== 'primitive') { if (isPartObject && commandId !== 'primitive') {
objectSnapshot.properties = objectSnapshot.properties.map((property) => property.name === 'Base' && state.selectedObjectId ? { ...property, value: state.selectedObjectId } : property) objectSnapshot.properties = objectSnapshot.properties.map((property) => property.name === 'Base' && state.selectedObjectId ? { ...property, value: state.selectedObjectId } : property)
} }

View File

@@ -758,7 +758,7 @@ test('Part Design feature tasks commit a document object and remain undoable', (
const before = facade.app.document.getActive() const before = facade.app.document.getActive()
facade.gui.command.execute({ commandId: 'pad' }) facade.gui.command.execute({ commandId: 'pad' })
assert.equal(facade.task.getActive()?.commandId, 'pad') assert.equal(facade.task.getActive()?.commandId, 'pad')
facade.task.update({ length: 42, reversed: false }) facade.task.update({ length: 55, reversed: true, midplane: true })
facade.task.apply() facade.task.apply()
const committed = facade.app.document.getActive() const committed = facade.app.document.getActive()
@@ -767,6 +767,9 @@ test('Part Design feature tasks commit a document object and remain undoable', (
assert.equal(committed.version, before.version + 1) assert.equal(committed.version, before.version + 1)
assert.equal(facade.selection.getObjectId(), 'pad001') assert.equal(facade.selection.getObjectId(), 'pad001')
assert.equal(committed.dirty, true) assert.equal(committed.dirty, true)
assert.equal(committed.objects.find((object) => object.id === 'pad001')?.properties.find((property) => property.name === 'Length')?.value, 55)
assert.equal(committed.objects.find((object) => object.id === 'pad001')?.properties.find((property) => property.name === 'Reversed')?.value, true)
assert.equal(committed.objects.find((object) => object.id === 'pad001')?.properties.find((property) => property.name === 'Midplane')?.value, true)
facade.history.undo() facade.history.undo()
assert.equal(facade.app.document.getActive().tree.some((item) => item.id === 'pad001'), false) assert.equal(facade.app.document.getActive().tree.some((item) => item.id === 'pad001'), false)