"""PCD50 3/4-open pillar print trial. Run through FreeCAD GUI macro.""" from pathlib import Path import math, time, traceback import FreeCAD as A, FreeCADGui as G, Part, Mesh, Import from pivy import coin R=Path(__file__).resolve().parent.parent O=R/'models'/'pcd50_open_pillar_v1'; O.mkdir(parents=True,exist_ok=True) V=A.Vector def cy(r,h,x=0,y=0,z=0): return Part.makeCylinder(r,h,V(x,y,z)) def item(d,n,label,s,c,trans=0): o=d.addObject('PartDesign::Feature',n);o.Label=label;o.Shape=s o.ViewObject.ShapeColor=c;o.ViewObject.LineColor=(.1,.1,.1);o.ViewObject.Transparency=trans;return o def check(n,s): if s.isNull() or not s.isValid() or len(s.Solids)!=1 or s.Volume<=0:raise RuntimeError('Invalid solid: '+n) def wedge(x,y,z,h): # A 90 degree sector points 22.5 degrees from the outward X direction toward # the nearby corner; mirrored independently for all four quadrants. sx=1 if x>0 else -1;sy=1 if y>0 else -1 points=[V(x,y,z)] for i in range(19): a=math.radians(-22.5+90.0*i/18.0) points.append(V(x+sx*20*math.cos(a),y+sy*20*math.sin(a),z)) points.append(points[0]) return Part.Face(Part.Wire(Part.makePolygon(points).Edges)).extrude(V(0,0,h)) try: src=A.openDocument(str(R/'models'/'pcd50_pillar_concept'/'PCD50_Pillar_Assembly.FCStd')) names=['Motor','Holder','Magnet','PCB','Chip']+['PCBHead%d'%i for i in range(1,4)]+['Corner%d'%i for i in range(4)]+['Rail%d'%i for i in range(2)]+['Mount76_%d'%i for i in range(4)]+['Washer76_%d'%i for i in range(4)]+['Head50_%d'%i for i in range(4)]+['Shaft50_%d'%i for i in range(4)] sh={n:src.getObject(n).Shape.copy() for n in names}; old=src.getObject('PillarBracket').Shape.copy() A.closeDocument(src.Name) pts=[(25*math.cos(math.radians(45+90*i)),25*math.sin(math.radians(45+90*i))) for i in range(4)] # Retain the already-verified closed-hole 49mm panel and PCB mounting pads. bracket=old.common(Part.makeBox(120,120,3.2,V(-60,-60,10.9))) for x,y in pts: col=cy(6,10.4,x,y,.5).cut(cy(2.2,10.42,x,y,.49)).cut(wedge(x,y,.49,10.42)) check('column',col);bracket=bracket.fuse(col) bracket=bracket.removeSplitter();check('bracket',bracket) def shim(x,y,thick,z): return cy(6,thick,x,y,z).cut(cy(2.2,thick+.02,x,y,z-.01)).cut(wedge(x,y,z-.01,thick+.02)).removeSplitter() shims=[shim(x,y,.5,0) for x,y in pts] for i,s in enumerate(shims):check('shim'+str(i),s) # Printable placement: panel flat on bed, open columns upwards. parts=A.newDocument('PCD50OpenPillarParts') orient=bracket.copy();orient.translate(V(0,0,-14.1));orient.rotate(V(),V(1,0,0),180) neutral05=shim(10,10,.5,0);neutral05.translate(V(-10,-10,0)) neutral10=shim(10,10,1,0);neutral10.translate(V(-10,-10,0)) outparts=[item(parts,'EncoderBracket','PCD50 open pillar bracket',orient,(.1,.42,.85)),item(parts,'Shim05','0.5mm C shim; rotate to opening',neutral05,(.7,.7,.7)),item(parts,'Shim10','1mm C shim; rotate to opening',neutral10,(.7,.7,.7))] parts.recompute();meshlog=[] for o in outparts: check(o.Name,o.Shape);Mesh.export([o],str(O/(o.Name+'.stl')));Import.export([o],str(O/(o.Name+'.step'))) m=Mesh.Mesh(str(O/(o.Name+'.stl')));meshlog.append('%s solid=%s triangles=%d volume=%.5f zmin=%.5f'%(o.Name,m.isSolid(),m.CountFacets,m.Volume,m.BoundBox.ZMin)) if not m.isSolid() or m.Volume<=0 or abs(m.BoundBox.ZMin)>.001:raise RuntimeError('bad mesh '+o.Name) (O/'mesh_check.txt').write_text('\n'.join(meshlog)+'\n',encoding='utf8') parts.saveAs(str(O/'PrintParts.FCStd'));A.closeDocument(parts.Name) # Real assembled orientation, from existing motor and hardware geometry. d=A.newDocument('PCD50OpenPillarAssembly'); colors={'Motor':(.32,.34,.37),'Holder':(.98,.55,.1),'Magnet':(.8,.8,.8),'PCB':(.04,.65,.19),'Chip':(.1,.1,.1)} objs={} for n in names: col=colors.get(n,(.84,.47,.13) if n.startswith('Corner') else (.55,.56,.60) if n.startswith('Rail') else (.8,.8,.8)) objs[n]=item(d,n,n,sh[n],col,70 if n=='Motor' else 20 if n.startswith('Corner') else 0) b=item(d,'OpenPillarBracket','four open pillars - print trial',bracket,(.12,.43,.88)) shimobjs=[item(d,'AdjustShim%d'%i,'0.5mm C shim',s,(.7,.7,.75)) for i,s in enumerate(shims)] d.recompute();hits=[] # Test all printed parts and all motor screws against every other object; # intended mating contact is only a shared zero-volume face. allparts=[o for o in d.Objects if hasattr(o,'Shape')];focus=[b]+shimobjs for a in focus: for c in allparts: if a.Name==c.Name:continue if not a.Shape.BoundBox.intersect(c.Shape.BoundBox):continue vol=a.Shape.common(c.Shape).Volume if vol>.001:hits.append('%s / %s %.6fmm3'%(a.Name,c.Name,vol)) # Motor-side nominal separation of every pillar from every corner. distances=[] for i,(x,y) in enumerate(pts): part=bracket.common(cy(6.01,10.39,x,y,.5)) dmin=min(part.distToShape(sh['Corner%d'%j])[0] for j in range(4)) distances.append(dmin) # Clearance for an axial 7.5mm diameter tool above each head. access=[] for i,(x,y) in enumerate(pts): tool=cy(3.75,20,x,y,17.11);h=[] for n in ['Corner%d'%j for j in range(4)]+['Rail%d'%j for j in range(2)]: v=tool.common(sh[n]).Volume if v>.001:h.append(n+':%.4f'%v) access.append('tool%d: %s'%(i,', '.join(h) if h else 'no checked obstruction')) # Verify top panel material lies under screw head and bores stay open. heads=[] for x,y in pts: count=0;step=.2 for ix in range(-18,19): for iy in range(-18,19): dx,dy=ix*step,iy*step if dx*dx+dy*dy<=3.5**2 and bracket.isInside(V(x+dx,y+dy,14.09),1e-7,True):count+=1 heads.append(count*step*step) report=['50 PCD; 76 corners flush against motor, 2.5mm outboard in 9.5x5.5 slot (width assumed).', '3/4 open pillars outer diameter 12mm, M4 bore 4.4mm; nominal radial wall away from opening 3.8mm.', 'Each 90 degree opening faces 22.5 degrees from outward X toward its respective nearby 76mm corner.', 'PCB and magnet positions retain nominal 1mm gap with 0.5mm C shims.', 'M4x20 bolt without a head washer: nominal motor engagement 5.9mm; actual thread depth unknown.', 'minimum pillar/corner distances mm: '+', '.join('%.4f'%x for x in distances), 'Estimated material under 7mm head by sampling mm2: '+', '.join('%.2f'%x for x in heads), 'Positive volume intersections >0.001mm3 for support and shims:']+(hits or ['none in checked pairs'])+['Tool clearance, 7.5mm cylinder:']+access (O/'geometry_check.txt').write_text('\n'.join(report)+'\n',encoding='utf8') if hits: raise RuntimeError('Interference in print trial: '+', '.join(hits)) view=G.activeDocument().activeView();G.activateWorkbench('PartWorkbench') def render(kind,path): if kind=='rear':view.viewTop() else: view.viewAxonometric();G.updateGui();time.sleep(.5) view.getCameraNode().orientation.setValue(coin.SbRotation(.880476,.115917,-.279848,-.364705)) G.updateGui();time.sleep(.7);view.fitAll();G.updateGui();time.sleep(.7);view.saveImage(str(O/path),1600,1200,'White') render('rear','rear.png');render('axon','assembly.png') state={o.Name:o.ViewObject.Visibility for o in d.Objects if hasattr(o,'Shape')} for o in d.Objects: if hasattr(o,'Shape'):o.ViewObject.Visibility=False b.ViewObject.Visibility=True;render('axon','bracket.png') # Cutting one half through two bores makes the head-bearing face and slots visible. b.ViewObject.Visibility=False sec=bracket.cut(Part.makeBox(120,60,25,V(-60,-60-25/math.sqrt(2),0))).removeSplitter() so=item(d,'SectionPreview','section preview only',sec,(.12,.43,.88));d.recompute();render('axon','section.png');d.removeObject(so.Name) for o in d.Objects: if hasattr(o,'Shape'):o.ViewObject.Visibility=state.get(o.Name,True) render('axon','assembly.png');d.recompute();d.saveAs(str(O/'Assembly.FCStd')) Import.export([o for o in d.Objects if hasattr(o,'Shape')],str(O/'Assembly.step')) (O/'DONE.txt').write_text('ok\n',encoding='utf8') except Exception: (O/'ERROR.txt').write_text(traceback.format_exc(),encoding='utf8');raise