"""Editable KFL001 bearing mounting plate for prototype. Run with rebuild_kfl001_plate_v1.FCMacro in a hidden FreeCAD instance. Both assembly plates have the same geometry; flip the second when fitting. The centre cut provides 0.5 mm radial clearance for a 38 mm boss. """ from pathlib import Path import os, time, traceback, json import FreeCAD as A, FreeCADGui as G, Part, Import R=Path(r'C:\projects\wheel') O=R/'models'/'kfl001_plate_v3_alt' O.mkdir(parents=True, exist_ok=True) try: d=A.newDocument('KFL001_Plate_V3') d.Label='KFL001 8mm bearing mounting plate V3' s=d.addObject('Spreadsheet::Sheet','Parameters') s.Label='Parameters - dimensions in mm' rows=[('PlateWidth','76 mm','Overall width'),('PlateHeight','75 mm','Overall height'), ('Thickness','8 mm','Plate thickness'),('AxisFromBottom','51 mm','Axle height above lower edge'), ('CenterClearance','39 mm','39 mm clearance for user confirmed 38 mm boss'), ('BearingPitch','48 mm','Bolt centre spacing for KFL001'), ('BearingHole','7 mm','Bearing mount clearance diameter'), ('SlotPitch','47 mm','Centre spacing of lower mounting slots'), ('SlotBottom','9 mm','Mounting slot centre above plate lower edge'), ('SlotLength','9.5 mm','Horizontal slot overall length'), ('SlotWidth','5.5 mm','Horizontal slot width'), ('EdgeChamfer','1 mm','Both outer perimeter edges only'), ('CornerRadius','3 mm','Rounded upper transitions and lower plan corners'), ('ArchRadius','60 mm','Top arc radius')] for row,(name,val,note) in enumerate(rows,2): s.set('A%d'%row,name);s.set('B%d'%row,val);s.set('C%d'%row,note);s.setAlias('B%d'%row,name) def expr(o,p,v):o.setExpression(p,v) plate=d.addObject('Part::Box','PlateBlank');plate.Label='76 x 75 x 8 plate blank' expr(plate,'Length','Parameters.PlateWidth');expr(plate,'Width','Parameters.PlateHeight');expr(plate,'Height','Parameters.Thickness') arc=d.addObject('Part::Cylinder','ArchLimit');arc.Label='Upper R60 arc' expr(arc,'Radius','Parameters.ArchRadius');expr(arc,'Height','Parameters.Thickness') expr(arc,'Placement.Base.x','Parameters.PlateWidth / 2');expr(arc,'Placement.Base.y','Parameters.PlateHeight - Parameters.ArchRadius') d.recompute() shape=d.addObject('Part::Common','ArchedOutline');shape.Base=plate;shape.Tool=arc;d.recompute() assert shape.Shape.isValid() and len(shape.Shape.Solids)==1 body=d.addObject('PartDesign::Body','ChamferBody');body.Label='Perimeter chamfer' body.BaseFeature=shape;d.recompute() vertical=[] for i,e in enumerate(shape.Shape.Edges,1): b=e.BoundBox if abs((b.ZMax-b.ZMin)-8)<1e-7 and (b.XLength<1e-6 and b.YLength<1e-6): vertical.append('Edge%d'%i) assert len(vertical)==4,vertical fillet=body.newObject('PartDesign::Fillet','OutlineCornerFillets') fillet.Base=(fillet.BaseFeature,vertical);expr(fillet,'Radius','Parameters.CornerRadius') d.recompute() assert not fillet.Shape.isNull() and fillet.Shape.isValid(),fillet.State outer=[] for i,e in enumerate(fillet.Shape.Edges,1): b=e.BoundBox if abs(b.ZMax-b.ZMin)<1e-7 and (abs(b.ZMin)<1e-7 or abs(b.ZMin-8)<1e-7):outer.append('Edge%d'%i) chamfer=body.newObject('PartDesign::Chamfer','PerimeterChamfer') chamfer.Base=(fillet,outer);expr(chamfer,'Size','Parameters.EdgeChamfer') d.recompute() assert not chamfer.Shape.isNull() and chamfer.Shape.isValid(),chamfer.State shifted=d.addObject('App::Link','ShiftedChamferBlank');shifted.setLink(chamfer) shifted.LinkTransform=True expr(shifted,'Placement.Base.x','-Parameters.PlateWidth / 2') d.recompute() def cyl(name,x,y,r): o=d.addObject('Part::Cylinder',name);expr(o,'Radius',r);expr(o,'Height','Parameters.Thickness + 0.2 mm');expr(o,'Placement.Base.x',x);expr(o,'Placement.Base.y',y);o.Placement.Base.z=-.1;return o tools=[] tools.append(cyl('CentreHole','0 mm','Parameters.AxisFromBottom','Parameters.CenterClearance / 2')) for side in (-1,1): sign='-' if side<0 else '' tools.append(cyl('BearingHole_'+('L' if side<0 else 'R'),sign+'Parameters.BearingPitch / 2','Parameters.AxisFromBottom','Parameters.BearingHole / 2')) cx=sign+'Parameters.SlotPitch / 2';tag='L' if side<0 else 'R' tools.append(cyl('Slot'+tag+'End1','(%s) - (Parameters.SlotLength - Parameters.SlotWidth) / 2'%cx,'Parameters.SlotBottom','Parameters.SlotWidth / 2')) tools.append(cyl('Slot'+tag+'End2','(%s) + (Parameters.SlotLength - Parameters.SlotWidth) / 2'%cx,'Parameters.SlotBottom','Parameters.SlotWidth / 2')) b=d.addObject('Part::Box','Slot'+tag+'Middle') expr(b,'Length','Parameters.SlotLength - Parameters.SlotWidth');expr(b,'Width','Parameters.SlotWidth');expr(b,'Height','Parameters.Thickness + 0.2 mm') expr(b,'Placement.Base.x','(%s) - (Parameters.SlotLength - Parameters.SlotWidth) / 2'%cx) expr(b,'Placement.Base.y','Parameters.SlotBottom - Parameters.SlotWidth / 2');b.Placement.Base.z=-.1 tools.append(b) d.recompute() cutters=d.addObject('Part::MultiFuse','Cutters');cutters.Shapes=tools;d.recompute() final=d.addObject('Part::Cut','Plate_V3');final.Label='KFL001 bearing mounting plate V3';final.Base=shifted;final.Tool=cutters d.recompute() assert not final.Shape.isNull() and final.Shape.isValid() and len(final.Shape.Solids)==1 for o in d.Objects: if o.ViewObject:o.ViewObject.Visibility=False final.ViewObject.Visibility=True;final.ViewObject.ShapeColor=(.71,.74,.77);final.ViewObject.LineColor=(.12,.12,.12) final.ViewObject.Deviation=0.05 final.ViewObject.AngularDeflection=10 G.activateWorkbench('PartWorkbench');view=G.activeDocument().activeView();view.viewAxonometric();G.updateGui();view.fitAll();time.sleep(.3);G.updateGui();view.saveImage(str(O/'plate_isometric.png'),1400,1100,'White') view.viewTop();G.updateGui();view.fitAll();time.sleep(.3);G.updateGui();view.saveImage(str(O/'plate_top.png'),1400,1100,'White') d.saveAs(str(O/'KFL001_plate_V3.FCStd'));Import.export([final],str(O/'KFL001_plate_V3.step')) baseline=final.Shape.Volume s.set('B6','38 mm');d.recompute() assert final.Shape.isValid() and abs(final.Shape.Volume-baseline)>0.1 changed=final.Shape.Volume s.set('B6','39 mm');d.recompute() assert final.Shape.isValid() and abs(final.Shape.Volume-baseline)<1e-5 A.closeDocument(d.Name) dd=A.openDocument(str(O/'KFL001_plate_V3.FCStd'));dd.recompute();ff=dd.getObject('Plate_V3') assert ff.Shape.isValid() and len(ff.Shape.Solids)==1 and abs(ff.Shape.Volume-baseline)<1e-5 imported=Part.read(str(O/'KFL001_plate_V3.step')) assert imported.isValid() and len(imported.Solids)==1 # At the middle Z plane compare the STEP vs native shape by mutual differences. stepdiff1=ff.Shape.cut(imported).Volume;stepdiff2=imported.cut(ff.Shape).Volume assert stepdiff1<1e-4 and stepdiff2<1e-4 angles=[(3.5,24,51),(3.5,-24,51),(19.5,0,51)] cylinders=[] for f in imported.Faces: surf=f.Surface if hasattr(surf,'Radius') and hasattr(surf,'Center'): cylinders.append({'radius':round(float(surf.Radius),5),'x':round(float(surf.Center.x),5),'y':round(float(surf.Center.y),5),'z_min':round(f.BoundBox.ZMin,5),'z_max':round(f.BoundBox.ZMax,5)}) for r,x,y in angles: assert any(abs(c['radius']-r)<1e-4 and abs(c['x']-x)<1e-4 and abs(c['y']-y)<1e-4 and c['z_min']<=.001 and c['z_max']>=7.999 for c in cylinders), (r,x,y,cylinders) info={'fcstd_valid':True,'step_valid':True,'solid_count':len(imported.Solids),'volume':baseline,'diameter_parameter_test_38':changed,'diameter_test_restored':True,'step_old_minus_new':stepdiff1,'step_new_minus_old':stepdiff2,'dimensions_mm':{'width':76,'height':75,'thickness':8,'axis_from_bottom':51,'center_diameter':39,'bearing_pitch':48,'bearing_hole':7,'slot_pitch':47,'slot_bottom':9,'slot_length':9.5,'slot_width':5.5,'perimeter_chamfer':1.0,'outline_corner_radius':3.0,'upper_arc_radius':60,'upper_arc_centre_from_bottom':15},'cylinder_faces':cylinders,'nominal_m5_slot_range_mm':4.5,'quantity_identical_plates':2,'boss_diameter_user_confirmed_mm':38} (O/'validation.json').write_text(json.dumps(info,ensure_ascii=False,indent=2),encoding='utf-8') A.closeDocument(dd.Name) (O/'DONE.txt').write_text('ok',encoding='utf-8') os._exit(0) except Exception: (O/'ERROR.txt').write_text(traceback.format_exc(),encoding='utf-8');os._exit(1)