from pathlib import Path import FreeCAD as A, FreeCADGui as G, Part, Import import math, json, os, time, traceback V=A.Vector root=Path(r'C:\projects\wheel') out=root/'models'/'assembly_latest_standoff' out.mkdir(parents=True,exist_ok=True) try: d=A.openDocument(str(root/'models'/'symmetric_corner_18_concept'/'Assembly_对称角码装配_概念.FCStd')) d.recompute() for n in ['EncoderPlateConcept']+['EncoderHex%d'%i for i in range(4)]+['EncoderSpacer%d'%i for i in range(4)]: o=d.getObject(n) if o: o.ViewObject.Visibility=False pdoc=A.openDocument(str(root/'models'/'encoder_standoff_plate_print'/'EncoderPlate.FCStd')) plate=pdoc.getObject('EncoderPlate').Shape.copy() plate.translate(V(0,10,11)) po=d.addObject('PartDesign::Feature','FinalEncoderPlate') po.Shape=plate;po.Label='Encoder printable plate, 10mm standoffs + 1mm shim';po.ViewObject.ShapeColor=(.08,.30,.85) A.closeDocument(pdoc.Name) # Move sensor PCB and chip by the same 0.1 mm as the real plate. for n in ['PCB','Chip']+[o.Name for o in d.Objects if o.Name.startswith('PCBHead')]: o=d.getObject(n) if o: sh=o.Shape.copy();sh.translate(V(0,0,.1));o.Placement=A.Placement();o.Shape=sh def obj(name,shape,label,color): o=d.addObject('PartDesign::Feature',name);o.Shape=shape.removeSplitter();o.Label=label;o.ViewObject.ShapeColor=color;o.ViewObject.LineColor=(.12,.12,.12);return o fast=[] for i in range(4): a=math.radians(45+90*i);x=25*math.cos(a);y=10+25*math.sin(a) rr=7/(2*math.cos(math.pi/6)) p=[V(x+rr*math.cos(math.radians(30+60*k)),y+rr*math.sin(math.radians(30+60*k)),0) for k in range(6)];p.append(p[0]) body=Part.Face(Part.makePolygon(p)).extrude(V(0,0,10)) body=body.cut(Part.makeCylinder(1.6,4,V(x,y,6))) # Placeholder stud length 5 mm, nominal diameter: actual hardware TBD. body=body.fuse(Part.makeCylinder(2,5,V(x,y,-5))) st=obj('BrassStandoff%d'%i,body,'M4 brass stand-off body 10 mm AF7, male 5 mm EXAMPLE',(.83,.65,.23));fast.append(st) sh=Part.makeCylinder(4.5,1,V(x,y,10)).cut(Part.makeCylinder(2.2,1,V(x,y,10))) wash=obj('StandoffShim%d'%i,sh,'M4 adjustment washer 1 mm assumed',(.75,.75,.76));fast.append(wash) screw=Part.makeCylinder(1.95,6.2,V(x,y,8)).fuse(Part.makeCylinder(3.5,2,V(x,y,14.2))) sc=obj('EncoderScrew%d'%i,screw,'M4 fixing screw placeholder; length not specified',(.65,.65,.68));fast.append(sc) d.recompute() objlist=[o for o in d.Objects if hasattr(o,'Shape') and not o.Shape.isNull() and o.ViewObject.Visibility] # Intersections for panel, standoff, washers and heads against unrelated visible objects. ignore={'Motor','PCB','Chip','Holder','Magnet'} # checked separately where appropriate; PCB/plate intentionally meet potential=[] for a in [po]+fast: for b in objlist: if a==b or b.Name in ignore or b in fast or b==po:continue if not a.Shape.BoundBox.intersect(b.Shape.BoundBox):continue v=a.Shape.common(b.Shape).Volume if v>0.001:potential.append([a.Name,b.Name,round(v,5)]) # PCB and chip dimensions and actual modeled gap, if geometry is available. chip=d.getObject('Chip'); magnet=d.getObject('Magnet') gap=chip.Shape.distToShape(magnet.Shape)[0] if chip and magnet else None selected_names=[o.Name for o in objlist] counts={k:sum(n.startswith(k) for n in selected_names) for k in ('NewMotorCorner','NewMotorBase','NewSwitchBase','NewBeamBase','PurchasedAngle','SwitchAngle','BrassStandoff','StandoffShim')} assert counts['NewMotorCorner']==4 and counts['NewMotorBase']==2 and counts['NewSwitchBase']==2 and counts['NewBeamBase']==4 assert counts['PurchasedAngle']==4 and counts['SwitchAngle']==2 and counts['BrassStandoff']==4 and counts['StandoffShim']==4 corners=[d.getObject('NewMotorCorner%d%d'%(i,j)).Shape for i in range(2) for j in range(2)] post_corner_clearance=[min(d.getObject('BrassStandoff%d'%i).Shape.distToShape(c)[0] for c in corners) for i in range(4)] sensor_contact={} for name in ('PCB','Chip','Magnet'): o=d.getObject(name) if o: sensor_contact[name]={'distance_mm':po.Shape.distToShape(o.Shape)[0],'volume_mm3':po.Shape.common(o.Shape).Volume} d.Label='最新装配 - M4 铜柱编码器' d.recompute();d.saveAs(str(out/'Assembly.FCStd')) Import.export(objlist,str(out/'Assembly.step')) A.setActiveDocument(d.Name);view=G.activeDocument().activeView();G.activateWorkbench('PartWorkbench') view.viewAxonometric();view.fitAll();G.updateGui();time.sleep(.5);view.saveImage(str(out/'装配全景.png'),1800,1300,'White') # Back-side encoder detail excluding parts obstructing the view; restore visibility. vis={o.Name:o.ViewObject.Visibility for o in d.Objects if hasattr(o,'Shape')} show={'Motor','Holder','Magnet','PCB','Chip','FinalEncoderPlate','Rail0','Rail1'} show.update('BrassStandoff%d'%i for i in range(4));show.update('StandoffShim%d'%i for i in range(4));show.update('EncoderScrew%d'%i for i in range(4)) show.update('NewMotorCorner%d%d'%(i,j) for i in range(2) for j in range(2));show.update('RailWasher%d%d'%(i,j) for i in range(2) for j in range(2)) for o in d.Objects: if hasattr(o,'Shape'):o.ViewObject.Visibility=o.Name in show view.viewTop();view.fitAll();G.updateGui();time.sleep(.5);view.saveImage(str(out/'编码器背面.png'),1600,1200,'White') for o in d.Objects: if hasattr(o,'Shape'):o.ViewObject.Visibility=vis.get(o.Name,False) d.recompute();d.saveAs(str(out/'Assembly.FCStd')) A.closeDocument(d.Name) d=A.openDocument(str(out/'Assembly.FCStd'));d.recompute() assert d.getObject('FinalEncoderPlate').Shape.isValid() A.closeDocument(d.Name) step=Part.read(str(out/'Assembly.step'));assert step.isValid() report={'counts':counts,'new_component_intersections_mm3':potential,'stand_off_to_motor_corner_clearance_mm':post_corner_clearance,'panel_to_sensor_relations':sensor_contact,'magnet_to_chip_shape_distance_mm':gap,'panel_bottom_from_motor_mm':11,'stand_off_body_mm':10,'shim_mm':1,'stud_length_mm':'5 schematic only','visible_names':selected_names,'limitations':'No real slot, fastener threads, countersink, switch wiring or real sensor dimensions; not physical fit or strength verification.'} (out/'验证.json').write_text(json.dumps(report,ensure_ascii=False,indent=2),encoding='utf8') (out/'完成.txt').write_text('ok',encoding='utf8') except Exception: (out/'错误.txt').write_text(traceback.format_exc(),encoding='utf8') os._exit(0)