"""Create first-fit printing parts for M9C12 + MT6826S in FreeCAD GUI. Dimension assumptions are documented in the associated user instructions. Run using rebuild_encoder_print_parts.FCMacro. Does not change motor source. """ import math from pathlib import Path import FreeCAD as App import FreeCADGui as Gui import Part, Mesh, Import from pivy import coin root = Path(__file__).resolve().parent.parent out = root / 'models' / 'encoder_print_v1' out.mkdir(parents=True, exist_ok=True) V = App.Vector doc = App.newDocument('EncoderPrintV1') def cyl(r, h, x=0, y=0, z=0): return Part.makeCylinder(r, h, V(x,y,z)) def obj(name, label, shape, color): o=doc.addObject('PartDesign::Feature', name); o.Label=label; o.Shape=shape o.ViewObject.ShapeColor=color; o.ViewObject.LineColor=(0.1,0.1,0.1) return o def check(name, shape): if shape.isNull() or not shape.isValid() or len(shape.Solids)!=1 or shape.Volume<0.01: raise RuntimeError('Invalid or non-single solid '+name) # rear motor face z=0; positive z is away from motor. # Base and tower remain clear of three PCB screw heads when rotating. holder = cyl(13.5,3).fuse(cyl(10,4.5,z=3)).removeSplitter() holder = holder.cut(cyl(1.65,7.6,z=-0.05)) # Screw head pocket from z=1.9 to 4.5; nominal M3 head 6 mm assumed. holder = holder.cut(cyl(3.6,2.6,z=1.9)) # Magnet pocket: 10.3 mm diameter nominal, 3 mm deep. holder = holder.cut(cyl(5.15,3.05,z=4.5)) # An edge notch allows gentle magnet removal. Never pry on magnet face. notch=Part.makeBox(6,2.5,2.4,V(4.5,-1.25,5.1)) holder=holder.cut(notch).removeSplitter() check('holder',holder) # Gap at no shim 0.5 mm. Magnet face z=7.5, chip face z=8.0; # chip is assumed 0.8 mm high, so board near face z=8.8 and far face 10.4. # Add equal 0.5 mm under feet for initial nominal 1 mm gap. board_near=8.8 board_far=10.4 ring_z=board_far ring_h=3.2 bracket=cyl(20.5,ring_h,z=ring_z).cut(cyl(16.8,ring_h+0.02,z=ring_z-0.01)) pcb_centres=[(0,13.49),(-11.71,-6.76),(11.71,-6.76)] # Three local bosses/bridges; PCB rests only locally instead of an unbroken ring. for x,y in pcb_centres: boss=cyl(3.2,ring_h,x,y,ring_z) a=math.degrees(math.atan2(y,x)) bridge=Part.makeBox(6.0,4.0,ring_h,V(13,-2,ring_z)) bridge.rotate(V(0,0,0),V(0,0,1),a) bracket=bracket.fuse(boss).fuse(bridge) for i in range(4): angle=45+i*90 a=math.radians(angle); ux,uy=math.cos(a),math.sin(a) # Post nearest face at r=16.8; PCB nominal radius 16.5. # M4 head has 0.7 mm radial clearance if its diameter is 7 mm. post=cyl(2.0,ring_z-2,18.8*ux,18.8*uy,2) foot=cyl(4.5,2,25*ux,25*uy,0) bridge=Part.makeBox(10,4,2,V(18,-2,0)); bridge.rotate(V(0,0,0),V(0,0,1),angle) bracket=bracket.fuse(post).fuse(foot).fuse(bridge) bracket=bracket.cut(cyl(2.2,2.05,25*ux,25*uy,-0.02)) # Ø1.8 pilot bores for M2 self-tapping screws, through 3.2 mm support thickness. for x,y in pcb_centres: bracket=bracket.cut(cyl(0.9,ring_h+0.04,x,y,ring_z-0.02)) bracket=bracket.removeSplitter() check('bracket',bracket) shim05=cyl(4.5,.5).cut(cyl(2.2,.52,z=-.01)) shim10=cyl(4.5,1).cut(cyl(2.2,1.02,z=-.01)) check('shim05',shim05); check('shim10',shim10) # Export all printable parts at z=0 in an independent print-parts document. parts=App.newDocument('EncoderPartsPrint') def pobj(name,label,shape,color): o=parts.addObject('PartDesign::Feature',name);o.Label=label;o.Shape=shape o.ViewObject.ShapeColor=color;return o ph=pobj('MagnetHolder','Magnet holder - print motor face down',holder,(0.96,.55,.12)) print_bracket=bracket.copy();print_bracket.translate(V(0,0,-(ring_z+ring_h))) print_bracket.rotate(V(0,0,0),V(1,0,0),180) pb=pobj('EncoderBracket','Bracket - print PCB ring face down',print_bracket,(.2,.48,.8)) s05=pobj('Shim05','M4 shim 0.5mm - print 4',shim05,(.7,.7,.7)) s10=pobj('Shim10','M4 shim 1.0mm - print 4 optional',shim10,(.7,.7,.7)) parts.recompute() for o in [ph,pb,s05,s10]: check(o.Name,o.Shape) Mesh.export([o], str(out/(o.Name+'.stl'))) Import.export([o], str(out/(o.Name+'.step'))) mesh_results=[] for name in ['MagnetHolder','EncoderBracket','Shim05','Shim10']: mesh=Mesh.Mesh(str(out/(name+'.stl'))) solid=mesh.isSolid(); bounds=mesh.BoundBox mesh_results.append('%s solid=%s facets=%d volume=%.5f ZMin=%.5f ZMax=%.5f' % (name,solid,mesh.CountFacets,mesh.Volume,bounds.ZMin,bounds.ZMax)) if not solid or mesh.Volume <= 0 or abs(bounds.ZMin)>0.001: raise RuntimeError('STL verification failed: '+mesh_results[-1]) out.joinpath('mesh_check.txt').write_text('\n'.join(mesh_results)+'\n',encoding='utf-8') parts.saveAs(str(out/'EncoderParts_Print.FCStd')) # Assembly with nominal 0.5 mm shim under all four feet (1 mm sensor gap). App.setActiveDocument(doc.Name) motor=Part.read(str(root/'models'/'MAD_M9C12.step')) motor.rotate(V(0,0,0),V(1,0,0),180) mo=obj('Motor','Original approximate M9C12',motor,(.34,.36,.38));mo.ViewObject.Transparency=70 ho=obj('Holder','Print magnet holder',holder,(.96,.55,.12)) mag=cyl(5,3,z=4.5);mg=obj('Magnet','10 x 3 mm diametric magnet',mag,(.78,.78,.8)) br=bracket.copy();br.translate(V(0,0,.5));bo=obj('Bracket','Print bracket with 0.5mm shims',br,(.2,.48,.8)) pcb=cyl(16.5,1.6,z=board_near+.5) for x,y in pcb_centres: pcb=pcb.cut(cyl(1.5,1.62,x,y,board_near+.49)) pc=obj('PCB','PCB approximate',pcb,(.06,.6,.22)) chip=Part.makeBox(4,5,.8,V(-2,-2.5,board_near+.5-.8)) ch=obj('Chip','Chip approximate',chip,(.1,.1,.1)) for i in range(4): a=math.radians(45+i*90); x,y=25*math.cos(a),25*math.sin(a) so=obj('Shim%d'%i,'0.5mm shim %d'%(i+1),cyl(4.5,.5,x,y,0).cut(cyl(2.2,.52,x,y,-.01)),(.8,.8,.8)) # Inspect interferences in the simplified assembly. Include nominal M2 # screw heads 4.0 mm dia, 1.5 mm high on the motor-facing PCB side. m2heads=[] for x,y in pcb_centres: head=cyl(2,1.5,x,y,board_near+.5-1.5) m2heads.append(head) obj('PCBHead%d'%len(m2heads),'M2 head envelope 4x1.5 assumed',head,(.8,.8,.8)) checks=[('magnet_holder',mag,holder),('holder_bracket',holder,br),('magnet_bracket',mag,br),('chip_magnet',chip,mag),('board_bracket',pcb,br)] checks += [('holder_M2head_%d'%i,holder,h) for i,h in enumerate(m2heads,1)] results=[] for name,a,b in checks: vol=a.common(b).Volume results.append('%s %.6f mm3'%(name,vol)) if vol>0.001: raise RuntimeError('Interference '+results[-1]) doc.recompute() for o in [mo,ho,mg,bo,pc,ch]: if o.Shape.isNull() or not o.Shape.isValid(): raise RuntimeError('Invalid assembly '+o.Name) doc.saveAs(str(out/'EncoderAssembly_Fit.FCStd')) view=Gui.activeDocument().activeView() view.getCameraNode().orientation.setValue(coin.SbRotation(0.880476, 0.115917, -0.279848, -0.364705)) view.fitAll();Gui.updateGui();view.saveImage(str(out/'assembly.png'),1600,1200,'White') out.joinpath('geometry_check.txt').write_text('\n'.join(results)+'\nNominal chip-face-to-magnet-face gap: 1 mm\n',encoding='utf-8') out.joinpath('BUILD_OK.txt').write_text('success\n',encoding='utf-8') print('PRINT_PARTS_OK')