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

@@ -1,6 +1,6 @@
import { DependencyGraph, type RecomputeState } from './dependencyGraph'
import { cloneSketch, solveSketch } from './sketcher'
import type { ChamferInput, DocumentObjectSnapshot, DocumentSnapshot, FilletInput, PadInput, PlanarProfile, PocketInput, RevolutionInput, ShapeHandle } from './types'
import type { BooleanCutInput, BooleanIntersectionInput, BooleanUnionInput, ChamferInput, CreateBoxInput, CreateConeInput, CreateCylinderInput, CreateSphereInput, DocumentObjectSnapshot, DocumentSnapshot, FilletInput, PadInput, PlanarProfile, PocketInput, RevolutionInput, ShapeHandle } from './types'
export type RecomputeExecutionStatus = 'completed' | 'failed' | 'cancelled' | 'stale'
@@ -31,6 +31,13 @@ export type RecomputeNodeExecutor = (
export type RecomputeGeometryRuntime = {
capabilities(): { status: string }
createBox(input: CreateBoxInput): Promise<ShapeHandle>
createCylinder(input: CreateCylinderInput): Promise<ShapeHandle>
createSphere(input: CreateSphereInput): Promise<ShapeHandle>
createCone(input: CreateConeInput): Promise<ShapeHandle>
union(input: BooleanUnionInput): Promise<ShapeHandle>
cut(input: BooleanCutInput): Promise<ShapeHandle>
intersection(input: BooleanIntersectionInput): Promise<ShapeHandle>
pad(input: PadInput): Promise<ShapeHandle>
pocket(input: PocketInput): Promise<ShapeHandle>
revolution(input: RevolutionInput): Promise<ShapeHandle>
@@ -273,7 +280,7 @@ export const createFacadeGeometryRecomputeExecutor = (
): RecomputeNodeExecutor => async (object, document, context) => {
const base = await executeFacadeRecomputeNode(object, document, context)
if (base.status === 'failed' || object.sketch || geometry.capabilities().status !== 'ready') return base
if (!['PartDesign::Pad', 'PartDesign::Pocket', 'PartDesign::Revolution', 'PartDesign::Fillet', 'PartDesign::Chamfer'].includes(object.typeId)) return base
if (!['Part::Box', 'Part::Cylinder', 'Part::Sphere', 'Part::Cone', 'Part::Fuse', 'Part::Cut', 'Part::Common', 'PartDesign::Pad', 'PartDesign::Pocket', 'PartDesign::Revolution', 'PartDesign::Fillet', 'PartDesign::Chamfer'].includes(object.typeId)) return base
const requiresProfile = object.typeId === 'PartDesign::Pad' || object.typeId === 'PartDesign::Pocket' || object.typeId === 'PartDesign::Revolution'
const profileObject = requiresProfile ? linkedObject(object, 'Profile', document) : undefined
@@ -288,7 +295,24 @@ export const createFacadeGeometryRecomputeExecutor = (
const documentContext = { documentId: context.documentId, documentVersion: context.documentVersion }
try {
let result: ShapeHandle
if (object.typeId === 'PartDesign::Pad') {
if (object.typeId === 'Part::Box') {
result = await geometry.createBox({ ...documentContext, width: numberProperty('Width', 10), length: numberProperty('Length', 10), height: numberProperty('Height', 10) })
} else if (object.typeId === 'Part::Cylinder') {
result = await geometry.createCylinder({ ...documentContext, radius: numberProperty('Radius', 5), height: numberProperty('Height', 10), angle: numberProperty('Angle', 360) })
} else if (object.typeId === 'Part::Sphere') {
result = await geometry.createSphere({ ...documentContext, radius: numberProperty('Radius', 5) })
} else if (object.typeId === 'Part::Cone') {
result = await geometry.createCone({ ...documentContext, radius1: numberProperty('Radius1', 5), radius2: numberProperty('Radius2', 0), height: numberProperty('Height', 10), angle: numberProperty('Angle', 360) })
} else if (object.typeId === 'Part::Fuse' || object.typeId === 'Part::Cut' || object.typeId === 'Part::Common') {
const baseObject = linkedObject(object, 'Base', document)
const toolObject = linkedObject(object, 'Tool', document)
const baseShape = baseObject ? shapes.get(baseObject.id) : undefined
const toolShape = toolObject ? shapes.get(toolObject.id) : undefined
if (!baseShape || !toolShape) return geometryFailure(object.id, 'BOOLEAN_SHAPE_MISSING', 'Boolean operation requires recomputed Base and Tool shapes.')
if (object.typeId === 'Part::Fuse') result = await geometry.union({ ...documentContext, shapes: [baseShape, toolShape] })
else if (object.typeId === 'Part::Cut') result = await geometry.cut({ ...documentContext, base: baseShape, tools: [toolShape] })
else result = await geometry.intersection({ ...documentContext, shapes: [baseShape, toolShape] })
} else if (object.typeId === 'PartDesign::Pad') {
result = await geometry.pad({ ...documentContext, profile: profile.profile as PlanarProfile, length: numberProperty('Length', 1), direction: [0, 0, 1], reversed: propertyValue(object, 'Reversed') === true, symmetricToPlane: propertyValue(object, 'Midplane') === true })
} else if (object.typeId === 'PartDesign::Pocket') {
const pocketType = propertyValue(object, 'Type')