"""Build mixed-thickness 4x4 sprue; run with FreeCAD bundled Python.""" from pathlib import Path import json, math, zipfile, shutil, hashlib import FreeCAD as App import Part, Mesh, MeshPart ROOT = Path(__file__).resolve().parent.parent SRC = ROOT/'models'/'pcd50_open_pillar_v1' OUT = ROOT/'models'/'washer_grid_4x4' OUT.mkdir(parents=True, exist_ok=True) V=App.Vector def load(name, thickness): s=Part.read(str(SRC/name)) s.translate(V(-10,-10,0)) if s.isNull() or not s.isValid() or len(s.Solids)!=1 or abs(s.BoundBox.ZLength-thickness)>1e-6: raise RuntimeError('Invalid source '+name) # The original 90-degree opening is centered 22.5 degrees above +X. # Rotate to the NE quadrant so bar attachments on the SW side miss it. s.rotate(V(),V(0,0,1),22.5) return s source05=load('Shim05.step',.5) source10=load('Shim10.step',1.0) # Bars are 2x2 across their width/height. Offset attachment centerlines by # 1.5mm away from the open quadrant. At that line, the nominal free length # between the circular outer edges is 2.0mm; rod ends overlap by 0.8mm each. offset=1.5 edge=math.sqrt(6.0**2-offset**2) pitch=2*edge+2.0 rod_start=edge-.8 rod_length=pitch-2*rod_start pads=[] for row in range(4): for col in range(4): p=(source05 if row<2 else source10).copy() p.translate(V(col*pitch,row*pitch,0)) pads.append(p) rods=[] endpoints=[] for row in range(4): for col in range(3): rods.append(Part.makeBox(rod_length,2,2,V(col*pitch+rod_start,row*pitch-offset-1,0))) endpoints.append((row*4+col,row*4+col+1,'H')) for row in range(3): for col in range(4): rods.append(Part.makeBox(2,rod_length,2,V(col*pitch-offset-1,row*pitch+rod_start,0))) endpoints.append((row*4+col,(row+1)*4+col,'V')) if len(rods)!=24 or abs(rod_length-3.6)>1e-7: raise RuntimeError('Incorrect rods') contacts=[] for k,(a,b,d) in enumerate(endpoints): va=rods[k].common(pads[a]).Volume; vb=rods[k].common(pads[b]).Volume if min(va,vb)<0.2: raise RuntimeError('Weak or absent rod overlap %d: %.4f %.4f'%(k,va,vb)) contacts.append([round(va,5),round(vb,5)]) # Do not allow any bars into the 4.4mm bore. Bars are on the SW sides, so # they also avoid the NE open quadrant; test that region on each pad. for i,p in enumerate(pads): x=(i%4)*pitch; y=(i//4)*pitch bore=Part.makeCylinder(2.2,2,V(x,y,0)) opening=Part.makeBox(6.1,6.1,2,V(x,y,0)) for rod in rods: if rod.common(bore).Volume>1e-6: raise RuntimeError('Rod blocks bore %d'%i) if rod.common(opening).Volume>1e-6: raise RuntimeError('Rod blocks open quadrant %d'%i) shape=pads[0].multiFuse(pads[1:]+rods).removeSplitter() if shape.isNull() or not shape.isValid() or len(shape.Solids)!=1: raise RuntimeError('Not one valid solid') if any(p.cut(shape).Volume>1e-5 for p in pads): raise RuntimeError('Washer removed') doc=App.newDocument('Washers4x4') obj=doc.addObject('Part::Feature','Grid');obj.Label='4x4 washers - 2mm links';obj.Shape=shape doc.recompute() doc.saveAs(str(OUT/'4x4_混合垫片.FCStd')) Part.export([obj],str(OUT/'4x4_混合垫片.step')) mesh=MeshPart.meshFromShape(Shape=shape,LinearDeflection=.03,AngularDeflection=.2,Relative=False) mesh.write(str(OUT/'4x4_混合垫片.stl')) App.closeDocument(doc.Name) check=Mesh.Mesh(str(OUT/'4x4_混合垫片.stl')) parts=check.getSeparateComponents() if len(parts)!=1 or not parts[0].isSolid(): raise RuntimeError('STL is not one closed component') shutil.copyfile(SRC/'EncoderBracket.stl',OUT/'编码器支架_单件.stl') if hashlib.sha256((SRC/'EncoderBracket.stl').read_bytes()).digest()!=hashlib.sha256((OUT/'编码器支架_单件.stl').read_bytes()).digest(): raise RuntimeError('Bracket copy changed') b=shape.BoundBox data={'count':16,'washers_05':8,'washers_10':8,'rod_count':24, 'rod_width_mm':2.0,'rod_height_mm':2.0,'rod_total_length_mm':round(rod_length,6), 'rod_centerline_free_span_mm':2.0,'rod_nominal_end_overlap_mm':.8, 'rod_contact_volumes_mm3':contacts,'hole_opening_check':'passed', 'was_original_washer_geometry_retained':True,'closed_components':len(parts), 'bounds_mm':[b.XMin,b.XMax,b.YMin,b.YMax,b.ZMin,b.ZMax]} (OUT/'几何检查.json').write_text(json.dumps(data,ensure_ascii=False,indent=2)+'\n',encoding='utf8') print(json.dumps(data,ensure_ascii=False,indent=2))