feat: add experimental polar pattern feature
This commit is contained in:
46
src/App.tsx
46
src/App.tsx
File diff suppressed because one or more lines are too long
@@ -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.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.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 },
|
||||
@@ -117,6 +117,12 @@ const featureProperties = (item: ModelTreeItem): ObjectPropertySnapshot[] => {
|
||||
{ name: 'Length', label: 'Length', group: 'Pattern', scope: 'data', type: 'App::PropertyLength', value: 20, unit: 'mm', recompute: true },
|
||||
{ name: 'Direction', label: 'Direction', group: 'Pattern', scope: 'data', type: 'App::PropertyEnumeration', value: 'Horizontal', options: ['Horizontal', 'Vertical', 'Normal'], recompute: true },
|
||||
]
|
||||
if (item.id.startsWith('polar-pattern')) return [
|
||||
{ name: 'Base', label: 'Base', group: 'Pattern', scope: 'data', type: 'App::PropertyLink', value: null, recompute: true },
|
||||
{ name: 'Occurrences', label: 'Occurrences', group: 'Pattern', scope: 'data', type: 'App::PropertyInteger', value: 3, recompute: true },
|
||||
{ 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.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 },
|
||||
@@ -175,10 +181,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', 'primitive', 'union', 'cut', 'intersection', 'check-shape', 'solve-sketch'])
|
||||
const partDesignCommands = new Set(['create-body', 'create-sketch', 'pad', 'pocket', 'revolution', 'fillet', 'chamfer', 'linear-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', '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 partCommands = new Set(['primitive', 'union', 'cut', 'intersection', 'check-shape'])
|
||||
const shapeSelectionCommands = new Set(['union', 'cut', 'intersection', 'check-shape', 'fillet', 'chamfer', 'linear-pattern'])
|
||||
const shapeSelectionCommands = new Set(['union', 'cut', 'intersection', 'check-shape', 'fillet', 'chamfer', 'linear-pattern', 'polar-pattern'])
|
||||
const featureSelectionCommands = new Set(['pad', 'pocket', 'revolution'])
|
||||
const shapeTypeIds = new Set([
|
||||
'Part::Box',
|
||||
@@ -196,6 +202,7 @@ const shapeTypeIds = new Set([
|
||||
'PartDesign::Fillet',
|
||||
'PartDesign::Chamfer',
|
||||
'PartDesign::LinearPattern',
|
||||
'PartDesign::PolarPattern',
|
||||
])
|
||||
const featureCommands: Record<string, { label: string; detail: string }> = {
|
||||
'create-body': { label: 'Body', detail: 'Part Design body' },
|
||||
@@ -206,6 +213,7 @@ const featureCommands: Record<string, { label: string; detail: string }> = {
|
||||
fillet: { label: 'Fillet', detail: 'Radius 3 mm' },
|
||||
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' },
|
||||
primitive: { label: 'Box', detail: '10 × 10 × 10 mm' },
|
||||
union: { label: 'Union', detail: 'Boolean fuse' },
|
||||
cut: { label: 'Cut', detail: 'Boolean difference' },
|
||||
@@ -362,6 +370,7 @@ 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.')
|
||||
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'].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'].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
|
||||
@@ -378,6 +378,24 @@ export const createFacadeGeometryRecomputeExecutor = (
|
||||
} finally {
|
||||
await Promise.allSettled(copies.map((copy) => geometry.release(copy)))
|
||||
}
|
||||
} else if (object.typeId === 'PartDesign::PolarPattern') {
|
||||
const baseObject = linkedObject(object, 'Base', document)
|
||||
const baseShape = baseObject ? shapes.get(baseObject.id) : undefined
|
||||
if (!baseShape) return geometryFailure(object.id, 'BASE_SHAPE_MISSING', 'Polar pattern base has no valid recomputed Shape.')
|
||||
const occurrences = numberProperty('Occurrences', 3)
|
||||
if (!Number.isSafeInteger(occurrences) || occurrences < 2 || occurrences > 100) return geometryFailure(object.id, 'PATTERN_OCCURRENCES_INVALID', 'Polar pattern occurrences must be an integer between 2 and 100.')
|
||||
const angle = numberProperty('Angle', 360)
|
||||
if (!(angle > 0) || angle > 360) return geometryFailure(object.id, 'PATTERN_ANGLE_INVALID', 'Polar pattern angle must be greater than zero and no more than 360 degrees.')
|
||||
const axisName = propertyValue(object, 'Axis')
|
||||
const axis: [number, number, number] = axisName === 'Horizontal' ? [1, 0, 0] : axisName === 'Vertical' ? [0, 1, 0] : [0, 0, 1]
|
||||
const step = angle === 360 ? angle / occurrences : angle / (occurrences - 1)
|
||||
const copies: ShapeHandle[] = []
|
||||
try {
|
||||
for (let index = 1; index < occurrences; index += 1) copies.push(await geometry.applyPlacement({ ...documentContext, shape: baseShape, placement: { translation: [0, 0, 0], rotationAxis: axis, rotationAngle: step * index } }))
|
||||
result = await geometry.union({ ...documentContext, shapes: [baseShape, ...copies] })
|
||||
} finally {
|
||||
await Promise.allSettled(copies.map((copy) => geometry.release(copy)))
|
||||
}
|
||||
} else {
|
||||
const baseObject = linkedObject(object, 'Base', document)
|
||||
const baseShape = baseObject ? shapes.get(baseObject.id) : undefined
|
||||
|
||||
Reference in New Issue
Block a user