"""Inspect outboard 2020 brackets without changing the existing 76 mm parts. Run as a GUI macro via rebuild_shared76_outward_check.FCMacro. All fastener, slot, phase and motor data remain the documented first-fit assumptions. """ from pathlib import Path import math, time, traceback import FreeCAD as A import FreeCADGui as G import Part, Import from pivy import coin R = Path(__file__).resolve().parent.parent O = R / 'models' / 'shared76_outward_check' O.mkdir(parents=True, exist_ok=True) V = A.Vector def cylinder(r, h, x, y, z): return Part.makeCylinder(r, h, V(x,y,z)) def valid(shape): return not shape.isNull() and shape.isValid() def item(doc, name, label, shape, color, transparent=0): obj = doc.addObject('PartDesign::Feature', name) obj.Label = label obj.Shape = shape obj.ViewObject.ShapeColor = color obj.ViewObject.LineColor = (0.1,0.1,0.1) obj.ViewObject.Transparency = transparent return obj try: source = A.openDocument(str(R/'models'/'encoder_shared76_print_v1'/'Shared76_Assembly.FCStd')) objects = [o for o in source.Objects if hasattr(o, 'Shape') and not o.Shape.isNull()] original = {o.Name:o.Shape.copy() for o in objects} colors = {o.Name:tuple(o.ViewObject.ShapeColor) for o in objects} transparencies = {o.Name:o.ViewObject.Transparency for o in objects} labels = {o.Name:o.Label for o in objects} A.closeDocument(source.Name) names = list(original) corners = [n for n in names if n.startswith('Corner')] rails = [n for n in names if n.startswith('Rail')] frame_name = 'EncoderBracket' if len(corners)!=4 or len(rails)!=2 or frame_name not in original: raise RuntimeError('Expected existing components were not found: ' + str(names)) # M4 shaft 3.9 assumed; 9.5x5.5 oval gives 4mm straight travel and # 0.8mm radial clearance; 2.75mm shift leaves ~0.05mm at the slot end. shifted = {} for name in corners+rails: shape = original[name].copy() x = (shape.BoundBox.XMin+shape.BoundBox.XMax)/2 direction = -1 if x < 0 else 1 shape.translate(V(direction*2.75,0,0)) shifted[name] = shape p50 = 25/math.sqrt(2) heads = [] tools = [] centers=[] for side in (-1,1): for ysign in (-1,1): x, y = side*p50, ysign*p50 centers.append((x,y)) heads.append(cylinder(3.5,3,x,y,0)) # Access envelope: 7.5mm diameter including screw head to lift it, # measured from the top of the head through the rear access side. tools.append(cylinder(3.75,22,x,y,3.01)) reports = ['ASSEMBLY STUDY, all dimensions mm.', '50 mm screws ARE NOT in the current product; heads are only assumed envelopes Ø7 x 3 at z=0..3.', 'Corner/rail move ±2.75 in X, encoder, motor and 76 mm fixing studs unchanged.', 'Oval slot 9.5x5.5, M4 male diameter 3.9 assumed; outer limit clearance 0.05.', '50 and 76 mm hole-circle angular phase 45 degrees is assumed.', 'Screw head zero separation may be intended contact; positive volume indicates interference.', '50 mm head versus same-quadrant corner:'] for i,(x,y) in enumerate(centers): matching = min(corners, key=lambda n: (((original[n].BoundBox.XMin+original[n].BoundBox.XMax)/2)-x)**2+(((original[n].BoundBox.YMin+original[n].BoundBox.YMax)/2)-y)**2) before=original[matching]; after=shifted[matching]; head=heads[i] reports.append('%s (%+.2f,%+.2f) before common=%.4f after common=%.4f mm3; after separation=%.4f mm' % (matching,x,y,before.common(head).Volume,after.common(head).Volume,after.distToShape(head)[0])) fixed = [n for n in names if n not in corners+rails] collisions=[] for c in corners+rails: a=shifted[c] for n in fixed: b=original[n] if not a.BoundBox.intersect(b.BoundBox): continue volume=a.common(b).Volume if volume>.001:collisions.append('%s / %s: %.5f mm3'%(c,n,volume)) for i,a_name in enumerate(corners): for b_name in corners[i+1:]+rails: a,b=shifted[a_name],shifted[b_name] if a.BoundBox.intersect(b.BoundBox): volume=a.common(b).Volume if volume>.001:collisions.append('%s / %s: %.5f mm3'%(a_name,b_name,volume)) reports += ['Shifted corners and rails versus existing components (>0.001mm3):']+(collisions or ['none in checked pairs']) for i, tool in enumerate(tools): found=[] for name in [frame_name]+corners+rails+['PCB','Chip','Holder','Magnet']: sh=shifted.get(name,original[name]) if tool.BoundBox.intersect(sh.BoundBox): vol=tool.common(sh).Volume if vol>.001:found.append('%s %.3fmm3'%(name,vol)) reports.append('50 hole %d access envelope Ø7.5 from z=3.01..25.01: %s'%(i, ', '.join(found) if found else 'no checked obstruction')) # Separate optional candidate, no mutation of existing printed STL: # Ø8 clearance holes allow the assumed Ø7 screw head to pass from behind. frame=original[frame_name].copy() for x,y in centers: frame=frame.cut(cylinder(4,3.24,x,y,11.28)) frame=frame.removeSplitter() if not valid(frame) or len(frame.Solids)!=1:raise RuntimeError('access hole candidate not a valid single solid') candidate_hits=[] for i,tool in enumerate(tools): for name in [frame_name]+corners+rails+['PCB','Chip','Holder','Magnet']: sh=frame if name==frame_name else shifted.get(name,original[name]) if tool.BoundBox.intersect(sh.BoundBox): vol=tool.common(sh).Volume if vol>.001:candidate_hits.append('hole%d/%s: %.3fmm3'%(i,name,vol)) reports += ['OPTIONAL Ø8 access holes in encoder bracket, only a concept, NOT a released print file:', 'Assumed 50 mm head Ø7; fastener head diameter, tool, screw, and wiring need physical verification.', 'Ø7.5 access envelope candidate:'] + (candidate_hits or ['no checked obstruction']) (O/'geometry_check.txt').write_text('\n'.join(reports)+'\n',encoding='utf8') def make_doc(name, use_candidate): doc=A.newDocument(name) colors2={'Motor':(.35,.35,.37),'EncoderBracket':(.16,.43,.85),'PCB':(.04,.65,.22), 'Holder':(.98,.55,.10),'Magnet':(.8,.8,.8)} items=[] for n in names: shape=frame if n==frame_name and use_candidate else shifted.get(n,original[n]) c=colors2.get(n, colors[n]) items.append(item(doc,n,labels[n],shape,c,75 if n=='Motor' else transparencies[n])) # pink marks hypothetical 50-hole heads; omitted from STEP export by design. for i,head in enumerate(heads): items.append(item(doc,'Assumed50Head%d'%i,'50mm M4 head envelope ONLY - not installed',head,(.85,.15,.62))) doc.recompute() if any(not valid(o.Shape) for o in items):raise RuntimeError('Invalid object in '+name) G.activateWorkbench('PartWorkbench') view=G.activeDocument().activeView() def render(kind,path): if kind=='rear':view.viewTop() else: view.viewAxonometric();G.updateGui();time.sleep(.4) view.getCameraNode().orientation.setValue(coin.SbRotation(.880476,.115917,-.279848,-.364705)) G.updateGui();time.sleep(.7);view.fitAll();G.updateGui();time.sleep(.7);G.updateGui() view.saveImage(str(O/path),1600,1200,'White') render('rear',name+'_rear.png') render('axon',name+'_axon.png') for ob in items: if ob.Name.startswith('Assumed50Head'):ob.ViewObject.Visibility=False render('axon',name+'_assembly.png') for ob in items: if ob.Name.startswith('Assumed50Head'):ob.ViewObject.Visibility=True doc.recompute();doc.saveAs(str(O/(name+'.FCStd'))) Import.export([o for o in items if not o.Name.startswith('Assumed50Head')],str(O/(name+'.step'))) A.closeDocument(doc.Name) make_doc('Outward',False) make_doc('ServiceHolesConcept',True) (O/'DONE.txt').write_text('success\n',encoding='utf8') except Exception: (O/'ERROR.txt').write_text(traceback.format_exc(),encoding='utf8') raise