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

@@ -39,7 +39,7 @@ const initialTree: ModelTreeItem[] = [
{ id: 'reference', label: 'Reference geometry', type: 'folder', children: ['DatumPlane', 'DatumAxis'] },
]
const typeIdForItem = (item: ModelTreeItem) => item.type === 'body' ? 'PartDesign::Body' : item.type === 'sketch' ? 'Sketcher::SketchObject' : item.id.startsWith('pad') ? 'PartDesign::Pad' : item.id.startsWith('pocket') ? 'PartDesign::Pocket' : item.id.startsWith('revolution') ? 'PartDesign::Revolution' : item.id.startsWith('fillet') ? 'PartDesign::Fillet' : item.id.startsWith('chamfer') ? 'PartDesign::Chamfer' : item.type === 'feature' ? 'PartDesign::Feature' : 'App::DocumentObjectGroup'
const typeIdForItem = (item: ModelTreeItem) => item.type === 'body' ? 'PartDesign::Body' : item.type === 'sketch' ? 'Sketcher::SketchObject' : item.id.startsWith('box') ? 'Part::Box' : item.id.startsWith('cylinder') ? 'Part::Cylinder' : item.id.startsWith('sphere') ? 'Part::Sphere' : item.id.startsWith('cone') ? 'Part::Cone' : item.id.startsWith('union') ? 'Part::Fuse' : item.id.startsWith('cut') ? 'Part::Cut' : item.id.startsWith('intersection') ? 'Part::Common' : item.id.startsWith('pad') ? 'PartDesign::Pad' : item.id.startsWith('pocket') ? 'PartDesign::Pocket' : item.id.startsWith('revolution') ? 'PartDesign::Revolution' : item.id.startsWith('fillet') ? 'PartDesign::Fillet' : item.id.startsWith('chamfer') ? 'PartDesign::Chamfer' : item.type === 'feature' ? 'PartDesign::Feature' : 'App::DocumentObjectGroup'
const commonProperties = (item: ModelTreeItem): ObjectPropertySnapshot[] => [
{ name: 'Label', label: 'Label', group: 'Identity', scope: 'data', type: 'App::PropertyString', value: item.label },
@@ -58,6 +58,29 @@ const viewProperties = (): ObjectPropertySnapshot[] => [
]
const featureProperties = (item: ModelTreeItem): ObjectPropertySnapshot[] => {
if (item.id.startsWith('box')) return [
{ name: 'Length', label: 'Length', group: 'Box', scope: 'data', type: 'App::PropertyLength', value: 10, unit: 'mm', recompute: true },
{ name: 'Width', label: 'Width', group: 'Box', scope: 'data', type: 'App::PropertyLength', value: 10, unit: 'mm', recompute: true },
{ name: 'Height', label: 'Height', group: 'Box', scope: 'data', type: 'App::PropertyLength', value: 10, unit: 'mm', recompute: true },
]
if (item.id.startsWith('cylinder')) return [
{ name: 'Radius', label: 'Radius', group: 'Cylinder', scope: 'data', type: 'App::PropertyLength', value: 5, unit: 'mm', recompute: true },
{ name: 'Height', label: 'Height', group: 'Cylinder', scope: 'data', type: 'App::PropertyLength', value: 10, unit: 'mm', recompute: true },
{ name: 'Angle', label: 'Angle', group: 'Cylinder', scope: 'data', type: 'App::PropertyAngle', value: 360, unit: 'deg', recompute: true },
]
if (item.id.startsWith('sphere')) return [
{ name: 'Radius', label: 'Radius', group: 'Sphere', scope: 'data', type: 'App::PropertyLength', value: 5, unit: 'mm', recompute: true },
]
if (item.id.startsWith('cone')) return [
{ name: 'Radius1', label: 'Bottom radius', group: 'Cone', scope: 'data', type: 'App::PropertyLength', value: 5, unit: 'mm', recompute: true },
{ name: 'Radius2', label: 'Top radius', group: 'Cone', scope: 'data', type: 'App::PropertyLength', value: 0, unit: 'mm', recompute: true },
{ name: 'Height', label: 'Height', group: 'Cone', scope: 'data', type: 'App::PropertyLength', value: 10, unit: 'mm', recompute: true },
{ name: 'Angle', label: 'Angle', group: 'Cone', scope: 'data', type: 'App::PropertyAngle', value: 360, unit: 'deg', recompute: true },
]
if (item.id.startsWith('union') || item.id.startsWith('cut') || item.id.startsWith('intersection')) return [
{ name: 'Base', label: 'Base', group: 'Boolean', scope: 'data', type: 'App::PropertyLink', value: null, recompute: true },
{ name: 'Tool', label: 'Tool', group: 'Boolean', scope: 'data', type: 'App::PropertyLink', value: null, recompute: true },
]
if (item.id.startsWith('pad')) return [
{ name: 'Length', label: 'Length', group: 'Parameters', scope: 'data', type: 'App::PropertyLength', value: 42, unit: 'mm', recompute: true, expression: '42 mm' },
{ name: 'Profile', label: 'Profile', group: 'Parameters', scope: 'data', type: 'App::PropertyLink', value: 'sketch', recompute: true },
@@ -140,10 +163,11 @@ const createDocument = (label = 'Pump Housing'): DocumentSnapshot => {
return document
}
const selectionRequired = new Set(['pad', 'pocket', 'revolution', 'fillet', 'chamfer', 'hole', 'linear-pattern', 'polar-pattern', 'measure-distance', 'measure-angle', 'measure-area', 'solve-sketch'])
const selectionRequired = new Set(['pad', 'pocket', 'revolution', 'fillet', 'chamfer', 'union', 'cut', 'intersection', 'hole', 'linear-pattern', 'polar-pattern', 'measure-distance', 'measure-angle', 'measure-area', 'solve-sketch'])
const systemCommands = new Set(['new-document', 'save', 'select-object'])
const implementedCommandIds = new Set(['new-document', 'save', 'select-object', 'create-body', 'create-sketch', 'new-sketch', 'pad', 'pocket', 'revolution', 'fillet', 'chamfer', 'solve-sketch'])
const implementedCommandIds = new Set(['new-document', 'save', 'select-object', 'create-body', 'create-sketch', 'new-sketch', 'pad', 'pocket', 'revolution', 'fillet', 'chamfer', 'primitive', 'union', 'cut', 'intersection', 'solve-sketch'])
const partDesignCommands = new Set(['create-body', 'create-sketch', 'pad', 'pocket', 'revolution', 'fillet', 'chamfer'])
const partCommands = new Set(['primitive', 'union', 'cut', 'intersection'])
const featureCommands: Record<string, { label: string; detail: string }> = {
'create-body': { label: 'Body', detail: 'Part Design body' },
'create-sketch': { label: 'Sketch', detail: 'Fully constrained' },
@@ -152,6 +176,10 @@ const featureCommands: Record<string, { label: string; detail: string }> = {
revolution: { label: 'Revolution', detail: 'Angle 360 deg' },
fillet: { label: 'Fillet', detail: 'Radius 3 mm' },
chamfer: { label: 'Chamfer', detail: 'Length 2 mm' },
primitive: { label: 'Box', detail: '10 × 10 × 10 mm' },
union: { label: 'Union', detail: 'Boolean fuse' },
cut: { label: 'Cut', detail: 'Boolean difference' },
intersection: { label: 'Intersection', detail: 'Boolean common' },
}
const commandState = (commandId: string, activeWorkbench: WorkbenchId, selectedObjectId: string): CommandState => {
@@ -159,6 +187,7 @@ const commandState = (commandId: string, activeWorkbench: WorkbenchId, selectedO
if (!known) return { id: commandId, status: 'disabled', reason: 'Command is not registered in the active manifest.' }
if (!implementedCommandIds.has(commandId)) return { id: commandId, status: 'disabled', reason: 'Command is visible in the FreeCAD-compatible manifest but its BitBybit business executor is not implemented yet.' }
if (partDesignCommands.has(commandId) && activeWorkbench !== 'Part Design') return { id: commandId, status: 'disabled', reason: `Switch to Part Design to use ${commandId}.` }
if (partCommands.has(commandId) && activeWorkbench !== 'Part') return { id: commandId, status: 'disabled', reason: `Switch to Part to use ${commandId}.` }
if ((commandId === 'new-sketch' || commandId === 'solve-sketch') && activeWorkbench !== 'Sketcher') return { id: commandId, status: 'disabled', reason: `Switch to Sketcher to use ${commandId}.` }
if (selectionRequired.has(commandId) && !selectedObjectId) return { id: commandId, status: 'disabled', reason: 'Select a compatible object or sub-shape first.' }
return { id: commandId, status: 'enabled' }
@@ -265,13 +294,18 @@ export function createMockFacade(): BitBybitWebCadFacade {
const type = commandId === 'create-sketch' ? 'sketch' : commandId === 'create-body' ? 'body' : 'feature'
const item: ModelTreeItem = { id: objectId, label: definition.label, type, state: type === 'body' ? 'active' : 'valid', detail: definition.detail }
const tree: ModelTreeItem[] = document.tree.map((entry) => ({ ...entry, children: entry.children ? [...entry.children] : undefined }))
if (type === 'body') tree.push({ ...item, children: [] })
const isPartObject = partCommands.has(commandId)
if (type === 'body' || isPartObject) tree.push({ ...item, children: type === 'body' ? [] : undefined })
else {
const body = tree.find((entry) => entry.type === 'body')
if (body) body.children = [...(body.children || []), objectId]
tree.push(item)
}
const objects = [...document.objects.map((object) => ({ ...object, properties: object.properties.map((property) => ({ ...property })), sketch: object.sketch ? cloneSketch(object.sketch) : undefined })), createObjectSnapshot(item)]
const objectSnapshot = createObjectSnapshot(item)
if (isPartObject && commandId !== 'primitive') {
objectSnapshot.properties = objectSnapshot.properties.map((property) => property.name === 'Base' && state.selectedObjectId ? { ...property, value: state.selectedObjectId } : property)
}
const objects = [...document.objects.map((object) => ({ ...object, properties: object.properties.map((property) => ({ ...property })), sketch: object.sketch ? cloneSketch(object.sketch) : undefined })), objectSnapshot]
const nextDocument: DocumentSnapshot = { ...document, version: document.version + 1, dirty: true, tree, objects }
nextDocument.dependencies = collectDependencyEdges(nextDocument)
nextDocument.recompute = createRecomputeSnapshot(objects.map((object) => object.id), document.recompute?.generation ?? 0)