建立方向盘项目初始版本

This commit is contained in:
2026-10-29 22:04:55 +08:00
commit 9962cbfbce
834 changed files with 1519203 additions and 0 deletions

View File

@@ -0,0 +1,165 @@
"""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