166 lines
8.2 KiB
Python
166 lines
8.2 KiB
Python
"""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
|