112 lines
7.5 KiB
Python
112 lines
7.5 KiB
Python
"""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_v2'
|
|
O.mkdir(parents=True, exist_ok=True)
|
|
try:
|
|
d=A.newDocument('KFL001_Plate_V2')
|
|
d.Label='KFL001 8mm bearing mounting plate V2'
|
|
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.5 mm','Both outer perimeter edges only'),
|
|
('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()
|
|
outer=[]
|
|
for i,e in enumerate(shape.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=(chamfer.BaseFeature,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_V2');final.Label='KFL001 bearing mounting plate V2';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_V2.FCStd'));Import.export([final],str(O/'KFL001_plate_V2.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_V2.FCStd'));dd.recompute();ff=dd.getObject('Plate_V2')
|
|
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_V2.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.5,'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)
|