147 lines
6.8 KiB
Python
147 lines
6.8 KiB
Python
"""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')
|