"""Make one solid two-thickness panel containing 16 cut-out C washers. Run using FreeCAD's bundled Python. Cutting guide is documentation only; the resulting STL deliberately contains no artificial connecting bars. """ from pathlib import Path import json, math import FreeCAD as App import Part, Mesh, MeshPart ROOT = Path(r'C:\projects\wheel') OUT = ROOT / 'models' / 'washer_panel_4x4' OUT.mkdir(parents=True, exist_ok=True) V = App.Vector doc = App.newDocument('WasherPanel4x4') sheet = doc.addObject('Spreadsheet::Sheet', 'Parameters') sheet.set('A1', 'Parameter'); sheet.set('B1', 'Value'); sheet.set('C1', 'Notes') for row, name, value, desc in [(2, 'Thin', '0.5 mm', 'Thickness rows 1-2'), (3, 'Thick', '1 mm', 'Thickness rows 3-4')]: sheet.set('A%d'%row,name); sheet.set('B%d'%row,value); sheet.set('C%d'%row,desc); sheet.setAlias('B%d'%row,name) blanks=[] for name, y, p in [('ThinHalf',0,'Parameters.Thin'),('ThickHalf',24,'Parameters.Thick')]: b=doc.addObject('Part::Box',name); b.Length=48; b.Width=24; b.Placement.Base=V(0,y,0); b.setExpression('Height',p); blanks.append(b) blank=doc.addObject('Part::MultiFuse','PanelBlank'); blank.Shapes=blanks cutters=[] for row in range(4): for col in range(4): x,y=12*col+6,12*row+6 circle=doc.addObject('Part::Cylinder',f'Hole{row}{col}'); circle.Radius=2.2; circle.Height=3; circle.Placement.Base=V(x,y,-1); cutters.append(circle) # Original 90-degree sector after rotating its 22.5-degree centre to 45 degrees. # NE quadrant within this cell. The cutter is bounded by the cell's # top and right edges so the cut exposes the notch after separation. opening=doc.addObject('Part::Box',f'Opening{row}{col}') opening.Length=6; opening.Width=6; opening.Height=3; opening.Placement.Base=V(x,y,-1); cutters.append(opening) allcuts=doc.addObject('Part::MultiFuse','HoleAndNotchCutters'); allcuts.Shapes=cutters panel=doc.addObject('Part::Cut','WasherPanel'); panel.Base=blank; panel.Tool=allcuts panel.Label='One-piece 4 x 4 washer panel - cut to separate' doc.recompute() s=panel.Shape assert not s.isNull() and s.isValid() and len(s.Solids)==1, (s.isValid(),len(s.Solids)) baseline=s.Volume for row in range(4): for col in range(4): t=.5 if row<2 else 1 cell=s.common(Part.makeBox(12,12,2,V(12*col,12*row,0))) assert cell.isValid() and len(cell.Solids)==1 and abs(cell.BoundBox.ZLength-t)<1e-6 # At the centre and within the quadrant, the bore and open sector are clear. for xx,yy in [(0,0),(1,0),(.3,.3),(3,3),(5.9,5.9)]: assert not cell.isInside(V(12*col+6+xx,12*row+6+yy,t/2),1e-7,False),(row,col,xx,yy) # Outside the original round bore and notch, the square retained material. assert cell.isInside(V(12*col+2,12*row+2,t/2),1e-7,False) for o in doc.Objects: if hasattr(o,'ViewObject') and o.ViewObject: o.ViewObject.Visibility=False if panel.ViewObject: panel.ViewObject.Visibility=True doc.recompute() fc=OUT/'一体式4x4双厚垫片.FCStd'; step=OUT/'一体式4x4双厚垫片.step'; stl=OUT/'一体式4x4双厚垫片.stl' doc.saveAs(str(fc)); Part.export([panel],str(step)) MeshPart.meshFromShape(Shape=s,LinearDeflection=.025,AngularDeflection=.15,Relative=False).write(str(stl)) sheet.set('B2','0.6 mm'); doc.recompute(); test=panel.Shape.Volume assert panel.Shape.isValid() and test>baseline sheet.set('B2','0.5 mm');doc.recompute();assert abs(panel.Shape.Volume-baseline)<1e-6 App.closeDocument(doc.Name) dd=App.openDocument(str(fc));dd.recompute();f=dd.getObject('WasherPanel').Shape; ss=Part.read(str(step)); mesh=Mesh.Mesh(str(stl)); components=mesh.getSeparateComponents() assert f.isValid() and ss.isValid() and len(f.Solids)==len(ss.Solids)==1 and len(components)==1 and components[0].isSolid() assert f.cut(ss).Volume<1e-5 and ss.cut(f).Volume<1e-5 data={'sheet_size_mm':[48,48], 'cell_mm':[12,12], 'diameter_mm':4.4,'opening_sector_degrees':90,'counts':[8,8], 'thicknesses_mm':[.5,1], 'number_of_solids':len(ss.Solids), 'stl_components':len(components), 'stl_is_solid':components[0].isSolid(), 'part_is_valid':ss.isValid(),'fcstd_reopen':True, 'step_minus_fcstd_volume':ss.cut(f).Volume,'fcstd_minus_step_volume':f.cut(ss).Volume,'original_opening_direction':'northeast, rotate after separation to match each pillar', 'cut_boundaries_mm':[12,24,36], 'cut_line_is_physical_feature':False,'thickness_parameter_recompute_test':True,'nominal_kerf_mm':0,'nominal_volume_mm3':baseline} (OUT/'几何验证.json').write_text(json.dumps(data,ensure_ascii=False,indent=2),encoding='utf-8') print(json.dumps(data,ensure_ascii=False,indent=2))