feat: add experimental basic hole feature
This commit is contained in:
@@ -364,7 +364,7 @@ function TaskPanel({ workbench, facade, showNotice }: { workbench: Workbench; fa
|
||||
const activeCommand = activeTask?.commandId
|
||||
const activeDocument = facade.app.document.getActive()
|
||||
const booleanCommand = activeCommand === 'union' || activeCommand === 'cut' || activeCommand === 'intersection'
|
||||
const shapeObjects = activeDocument.objects.filter((object) => object.typeId.startsWith('Part::') || ['PartDesign::Feature', 'PartDesign::Pad', 'PartDesign::Pocket', 'PartDesign::Revolution', 'PartDesign::Fillet', 'PartDesign::Chamfer', 'PartDesign::LinearPattern', 'PartDesign::PolarPattern'].includes(object.typeId))
|
||||
const shapeObjects = activeDocument.objects.filter((object) => object.typeId.startsWith('Part::') || ['PartDesign::Feature', 'PartDesign::Pad', 'PartDesign::Pocket', 'PartDesign::Revolution', 'PartDesign::Fillet', 'PartDesign::Chamfer', 'PartDesign::LinearPattern', 'PartDesign::PolarPattern', 'PartDesign::Hole'].includes(object.typeId))
|
||||
const draftLink = (name: string) => typeof activeTask?.draft[name] === 'string' ? String(activeTask.draft[name]) : ''
|
||||
const acceptTask = () => {
|
||||
if (!activeTask || activeTask.status !== 'preview') { showNotice('No active task'); return }
|
||||
@@ -403,6 +403,11 @@ function TaskPanel({ workbench, facade, showNotice }: { workbench: Workbench; fa
|
||||
<label className="field-label">Angle <span className="field-unit">deg</span><input className="field-input" type="number" min={0.001} max={360} step={1} value={typeof activeTask?.draft.angle === 'number' ? Number(activeTask.draft.angle) : 360} onChange={(event) => facade.task.update({ angle: Number(event.target.value) })} /></label>
|
||||
<label className="field-label">Axis<select className="field-input" value={typeof activeTask?.draft.axis === 'string' ? activeTask.draft.axis : 'Normal'} onChange={(event) => facade.task.update({ axis: event.target.value })}><option>Normal</option><option>Horizontal</option><option>Vertical</option></select></label>
|
||||
</>}
|
||||
{activeCommand === 'hole' && <>
|
||||
<label className="field-label">Diameter <span className="field-unit">mm</span><input className="field-input" type="number" min={0.001} step={0.1} value={typeof activeTask?.draft.diameter === 'number' ? Number(activeTask.draft.diameter) : 5} onChange={(event) => facade.task.update({ diameter: Number(event.target.value) })} /></label>
|
||||
<label className="field-label">Depth <span className="field-unit">mm</span><input className="field-input" type="number" min={0.001} step={0.1} value={typeof activeTask?.draft.depth === 'number' ? Number(activeTask.draft.depth) : 10} onChange={(event) => facade.task.update({ depth: Number(event.target.value) })} /></label>
|
||||
<label className="field-label">Type<select className="field-input" value={typeof activeTask?.draft.type === 'string' ? activeTask.draft.type : 'Dimension'} onChange={(event) => facade.task.update({ type: event.target.value })}><option>Dimension</option><option>Through all</option></select></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>}
|
||||
|
||||
@@ -42,7 +42,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('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.id.startsWith('linear-pattern') ? 'PartDesign::LinearPattern' : item.id.startsWith('polar-pattern') ? 'PartDesign::PolarPattern' : 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.id.startsWith('linear-pattern') ? 'PartDesign::LinearPattern' : item.id.startsWith('polar-pattern') ? 'PartDesign::PolarPattern' : item.id.startsWith('hole') ? 'PartDesign::Hole' : 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 },
|
||||
@@ -123,6 +123,12 @@ const featureProperties = (item: ModelTreeItem): ObjectPropertySnapshot[] => {
|
||||
{ name: 'Angle', label: 'Angle', group: 'Pattern', scope: 'data', type: 'App::PropertyAngle', value: 360, unit: 'deg', recompute: true },
|
||||
{ name: 'Axis', label: 'Axis', group: 'Pattern', scope: 'data', type: 'App::PropertyEnumeration', value: 'Normal', options: ['Normal', 'Horizontal', 'Vertical'], recompute: true },
|
||||
]
|
||||
if (item.id.startsWith('hole')) return [
|
||||
{ name: 'Base', label: 'Base', group: 'Hole', scope: 'data', type: 'App::PropertyLink', value: null, recompute: true },
|
||||
{ name: 'Diameter', label: 'Diameter', group: 'Hole', scope: 'data', type: 'App::PropertyLength', value: 5, unit: 'mm', recompute: true },
|
||||
{ name: 'Depth', label: 'Depth', group: 'Hole', scope: 'data', type: 'App::PropertyLength', value: 10, unit: 'mm', recompute: true },
|
||||
{ name: 'Type', label: 'Type', group: 'Hole', scope: 'data', type: 'App::PropertyEnumeration', value: 'Dimension', options: ['Dimension', 'Through all'], recompute: true },
|
||||
]
|
||||
if (item.type === 'sketch') return [
|
||||
{ name: 'Support', label: 'Support', group: 'Attachment', scope: 'data', type: 'App::PropertyLink', value: 'XY_Plane', recompute: true },
|
||||
{ name: 'ConstraintStatus', label: 'Solver state', group: 'Constraints', scope: 'data', type: 'App::PropertyString', value: 'Fully constrained', readOnly: true },
|
||||
@@ -181,10 +187,10 @@ const createDocument = (label = 'Pump Housing'): DocumentSnapshot => {
|
||||
|
||||
const selectionRequired = new Set(['pad', 'pocket', 'revolution', 'fillet', 'chamfer', 'union', 'cut', 'intersection', 'check-shape', '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', 'linear-pattern', 'polar-pattern', 'primitive', 'union', 'cut', 'intersection', 'check-shape', 'solve-sketch'])
|
||||
const partDesignCommands = new Set(['create-body', 'create-sketch', 'pad', 'pocket', 'revolution', 'fillet', 'chamfer', 'linear-pattern', 'polar-pattern'])
|
||||
const implementedCommandIds = new Set(['new-document', 'save', 'select-object', 'create-body', 'create-sketch', 'new-sketch', 'pad', 'pocket', 'revolution', 'fillet', 'chamfer', 'linear-pattern', 'polar-pattern', 'hole', 'primitive', 'union', 'cut', 'intersection', 'check-shape', 'solve-sketch'])
|
||||
const partDesignCommands = new Set(['create-body', 'create-sketch', 'pad', 'pocket', 'revolution', 'fillet', 'chamfer', 'linear-pattern', 'polar-pattern', 'hole'])
|
||||
const partCommands = new Set(['primitive', 'union', 'cut', 'intersection', 'check-shape'])
|
||||
const shapeSelectionCommands = new Set(['union', 'cut', 'intersection', 'check-shape', 'fillet', 'chamfer', 'linear-pattern', 'polar-pattern'])
|
||||
const shapeSelectionCommands = new Set(['union', 'cut', 'intersection', 'check-shape', 'fillet', 'chamfer', 'linear-pattern', 'polar-pattern', 'hole'])
|
||||
const featureSelectionCommands = new Set(['pad', 'pocket', 'revolution'])
|
||||
const shapeTypeIds = new Set([
|
||||
'Part::Box',
|
||||
@@ -203,6 +209,7 @@ const shapeTypeIds = new Set([
|
||||
'PartDesign::Chamfer',
|
||||
'PartDesign::LinearPattern',
|
||||
'PartDesign::PolarPattern',
|
||||
'PartDesign::Hole',
|
||||
])
|
||||
const featureCommands: Record<string, { label: string; detail: string }> = {
|
||||
'create-body': { label: 'Body', detail: 'Part Design body' },
|
||||
@@ -214,6 +221,7 @@ const featureCommands: Record<string, { label: string; detail: string }> = {
|
||||
chamfer: { label: 'Chamfer', detail: 'Length 2 mm' },
|
||||
'linear-pattern': { label: 'Linear Pattern', detail: '2 occurrences over 20 mm' },
|
||||
'polar-pattern': { label: 'Polar Pattern', detail: '3 occurrences over 360 deg' },
|
||||
hole: { label: 'Hole', detail: 'Simple 5 mm diameter hole' },
|
||||
primitive: { label: 'Box', detail: '10 × 10 × 10 mm' },
|
||||
union: { label: 'Union', detail: 'Boolean fuse' },
|
||||
cut: { label: 'Cut', detail: 'Boolean difference' },
|
||||
@@ -371,6 +379,10 @@ export function createMockFacade(): BitBybitWebCadFacade {
|
||||
for (const property of objectSnapshot.properties.filter((candidate) => candidate.recompute && !candidate.readOnly)) validatePropertyValue(document, property, property.value)
|
||||
if (commandId === 'linear-pattern' && Number(objectSnapshot.properties.find((property) => property.name === 'Length')?.value) <= 0) throw new RangeError('Linear pattern length must be greater than zero.')
|
||||
if (commandId === 'polar-pattern' && Number(objectSnapshot.properties.find((property) => property.name === 'Angle')?.value) <= 0) throw new RangeError('Polar pattern angle must be greater than zero.')
|
||||
if (commandId === 'hole') {
|
||||
if (Number(objectSnapshot.properties.find((property) => property.name === 'Diameter')?.value) <= 0) throw new RangeError('Hole diameter must be greater than zero.')
|
||||
if (Number(objectSnapshot.properties.find((property) => property.name === 'Depth')?.value) <= 0) throw new RangeError('Hole depth must be greater than zero.')
|
||||
}
|
||||
const objects = [...document.objects.map((object) => ({ ...object, properties: object.properties.map((property) => ({ ...property })), sketch: object.sketch ? cloneSketch(object.sketch) : undefined })), objectSnapshot]
|
||||
const tipObject = type === 'feature' && partDesignCommands.has(commandId) && commandId !== 'create-sketch' ? objects.find((object) => object.id === 'body') : undefined
|
||||
if (tipObject) {
|
||||
|
||||
@@ -302,7 +302,7 @@ export const createFacadeGeometryRecomputeExecutor = (
|
||||
return base
|
||||
}
|
||||
if (base.status === 'failed' || object.sketch || geometry.capabilities().status !== 'ready') 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', 'PartDesign::LinearPattern', 'PartDesign::PolarPattern'].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', 'PartDesign::LinearPattern', 'PartDesign::PolarPattern', 'PartDesign::Hole'].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
|
||||
@@ -396,6 +396,20 @@ export const createFacadeGeometryRecomputeExecutor = (
|
||||
} finally {
|
||||
await Promise.allSettled(copies.map((copy) => geometry.release(copy)))
|
||||
}
|
||||
} else if (object.typeId === 'PartDesign::Hole') {
|
||||
const baseObject = linkedObject(object, 'Base', document)
|
||||
const baseShape = baseObject ? shapes.get(baseObject.id) : undefined
|
||||
if (!baseShape) return geometryFailure(object.id, 'BASE_SHAPE_MISSING', 'Hole base has no valid recomputed Shape.')
|
||||
const diameter = numberProperty('Diameter', 5)
|
||||
if (!(diameter > 0)) return geometryFailure(object.id, 'HOLE_DIAMETER_INVALID', 'Hole diameter must be greater than zero.')
|
||||
const depth = numberProperty('Depth', 10)
|
||||
if (!(depth > 0)) return geometryFailure(object.id, 'HOLE_DEPTH_INVALID', 'Hole depth must be greater than zero.')
|
||||
const radius = diameter / 2
|
||||
const outer = Array.from({ length: 32 }, (_, index): [number, number, number] => {
|
||||
const angle = 2 * Math.PI * index / 32
|
||||
return [radius * Math.cos(angle), radius * Math.sin(angle), 0]
|
||||
})
|
||||
result = await geometry.pocket({ ...documentContext, base: baseShape, profile: { outer }, length: depth, direction: [0, 0, 1], throughAll: propertyValue(object, 'Type') === 'Through all' })
|
||||
} else {
|
||||
const baseObject = linkedObject(object, 'Base', document)
|
||||
const baseShape = baseObject ? shapes.get(baseObject.id) : undefined
|
||||
|
||||
Reference in New Issue
Block a user