建立方向盘项目初始版本

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

172
scripts/build_70.py Normal file
View File

@@ -0,0 +1,172 @@
"""Build the native FreeCAD feature model for 70转接板.step.
Run from the FreeCAD GUI Python console, not FreeCADCmd. Set the project
directory first, then execute this script, for example:
import os
os.chdir(r'C:\projects\wheel')
exec(open(r'scripts\build_70.py', encoding='utf-8').read())
GUI execution is required to persist the visible object and camera state. The
generated document uses only built-in Part and Spreadsheet objects and
expressions; no script is needed to open or edit the generated model.
"""
import math
import os
import FreeCAD as App
import Part
import Import
if not App.GuiUp:
raise RuntimeError('Run this script from the FreeCAD GUI Python console; '
'FreeCADCmd cannot persist the required visibility state.')
import FreeCADGui as Gui
root = os.getcwd()
source = os.path.join(root, 'models', '\u0037\u0030\u8f6c\u63a5\u677f.step')
target = os.path.join(root, 'models', '\u0037\u0030\u8f6c\u63a5\u677f_\u53c2\u6570\u5316.FCStd')
doc = App.newDocument('Adapter70')
sheet = doc.addObject('Spreadsheet::Sheet', 'Parameters')
sheet.Label = 'Parameters (mm, degrees)'
rows = [
('Thickness', '8 mm', 'Plate thickness'),
('CenterDiameter', '32.5 mm', 'Center through hole diameter'),
('HoleDiameter', '4.234 mm', 'Through hole diameter (all 24 holes)'),
('PCD42', '42 mm', 'Countersunk holes pitch circle diameter'),
('Angle42', '45 deg', 'Countersunk holes first angle'),
('PCD48', '48 mm', 'Six holes pitch circle diameter'),
('Angle48', '0 deg', 'PCD48 first angle'),
('PCD508', '50.8 mm', 'Six holes pitch circle diameter'),
('Angle508', '30 deg', 'PCD50.8 first angle'),
('PCD70', '70 mm', 'Outer six holes pitch circle diameter'),
('Angle70', '30 deg', 'Outer six holes first angle'),
('OutlineScale', '1', 'Overall outline scale relative to PCD70'),
('CountersinkDiameter', '6.8 mm', 'Countersink at bottom face'),
]
sheet.set('A1', 'Parameter'); sheet.set('B1', 'Value'); sheet.set('C1', 'Meaning')
for i, (alias, val, desc) in enumerate(rows, 2):
sheet.set('A%d' % i, alias); sheet.set('B%d' % i, val)
sheet.set('C%d' % i, desc); sheet.setAlias('B%d' % i, alias)
sheet.set('A16', 'CountersinkDepth'); sheet.set('B16', '= (B14 - B4) / 2')
sheet.set('C16', '90 degree included angle, depth derived from diameters')
sheet.setAlias('B16', 'CountersinkDepth')
sheet.set('A17', 'OuterChamfer'); sheet.set('B17', '1 mm')
sheet.set('C17', 'Equal-distance chamfer on both outer edges')
sheet.setAlias('B17', 'OuterChamfer')
doc.recompute()
def setexpr(obj, prop, formula):
obj.setExpression(prop, formula)
def cylinder(name, label, radius, height, pcd=None, angle=None, index=0):
obj = doc.addObject('Part::Cylinder', name)
obj.Label = label
setexpr(obj, 'Radius', radius)
setexpr(obj, 'Height', height)
if pcd:
phase = 'Parameters.%s + %d deg' % (angle, 60 * index)
setexpr(obj, 'Placement.Base.x', 'Parameters.%s / 2 * cos(%s)' % (pcd, phase))
setexpr(obj, 'Placement.Base.y', 'Parameters.%s / 2 * sin(%s)' % (pcd, phase))
return obj
original = Part.read(source)
outer_wire = max(original.slice(App.Vector(0, 0, 1), 4), key=lambda wire: wire.Length)
original_face = Part.Face(outer_wire)
original_face.translate(App.Vector(0, 0, -4))
profile = doc.addObject('Part::Feature', 'OriginalOutline')
profile.Label = 'Original outline at mid-thickness (reference geometry)'
profile.Shape = original_face
# A built-in link uniformly scales the original plan view in XY, without
# scaling the extrusion height; the local curve dimensions are not editable.
scaled = doc.addObject('App::Link', 'ScaledOutline')
scaled.Label = 'Original outline - scaled and rotated'
scaled.setLink(profile)
scaled.LinkTransform = True
setexpr(scaled, 'ScaleVector.x', 'Parameters.PCD70 / 70 mm * Parameters.OutlineScale')
setexpr(scaled, 'ScaleVector.y', 'Parameters.PCD70 / 70 mm * Parameters.OutlineScale')
setexpr(scaled, 'Placement.Rotation.Angle', 'Parameters.Angle70 - 30 deg')
outline = doc.addObject('Part::Extrusion', 'Outline')
outline.Label = 'Outline - original profile extruded'
outline.Base = scaled
outline.DirMode = 'Custom'
outline.Dir = App.Vector(0, 0, 1)
outline.LengthFwd = 8
outline.Solid = True
setexpr(outline, 'LengthFwd', 'Parameters.Thickness')
doc.recompute()
assert not outline.Shape.isNull() and outline.Shape.isValid() and len(outline.Shape.Solids) == 1
# Import the parametric outline into a Body as a linked base feature. Its
# PartDesign chamfer has a single native size that accepts an expression.
edge_ids = []
for n, edge in enumerate(outline.Shape.Edges, 1):
z0, z1 = edge.BoundBox.ZMin, edge.BoundBox.ZMax
if abs(z0-z1) < 1e-7 and (abs(z0) < 1e-7 or abs(z0-8) < 1e-7):
edge_ids.append(n)
body = doc.addObject('PartDesign::Body', 'OutlineBody')
body.Label = 'Outline with chamfer'
body.BaseFeature = outline
doc.recompute()
chamfer = body.newObject('PartDesign::Chamfer', 'OuterChamfer')
chamfer.Label = 'Outer chamfer (1 mm)'
chamfer.Base = (chamfer.BaseFeature, ['Edge%d' % i for i in edge_ids])
setexpr(chamfer, 'Size', 'Parameters.OuterChamfer')
doc.recompute()
assert not chamfer.Shape.isNull() and chamfer.Shape.isValid(), 'outer chamfer failed'
center = cylinder('CenterBore', 'Center through hole', 'Parameters.CenterDiameter / 2', 'Parameters.Thickness')
current = doc.addObject('Part::Cut', 'CenterCut')
current.Label = 'Center hole'
current.Base, current.Tool = body, center
doc.recompute()
for name, pcd, angle in [('42', 'PCD42', 'Angle42'), ('48', 'PCD48', 'Angle48'),
('508', 'PCD508', 'Angle508'), ('70', 'PCD70', 'Angle70')]:
holes = [cylinder('Hole%s_%d' % (name, i), 'PCD %s hole %d' % (name, i+1),
'Parameters.HoleDiameter / 2', 'Parameters.Thickness', pcd, angle, i)
for i in range(6)]
group = doc.addObject('Part::MultiFuse', 'Bores%s' % name)
group.Label = 'PCD %s - six bores' % name
group.Shapes = holes
cut = doc.addObject('Part::Cut', 'Cut%s' % name)
cut.Label = 'PCD %s holes' % name
cut.Base, cut.Tool = current, group
current = cut
doc.recompute()
cones = []
for i in range(6):
cone = doc.addObject('Part::Cone', 'Countersink%d' % i)
cone.Label = 'Bottom countersink %d' % (i+1)
setexpr(cone, 'Radius1', 'Parameters.CountersinkDiameter / 2')
setexpr(cone, 'Radius2', 'Parameters.HoleDiameter / 2')
setexpr(cone, 'Height', 'Parameters.CountersinkDepth')
phase = 'Parameters.Angle42 + %d deg' % (i*60)
setexpr(cone, 'Placement.Base.x', 'Parameters.PCD42 / 2 * cos(%s)' % phase)
setexpr(cone, 'Placement.Base.y', 'Parameters.PCD42 / 2 * sin(%s)' % phase)
cones.append(cone)
sink_group = doc.addObject('Part::MultiFuse', 'Countersinks')
sink_group.Label = 'PCD 42 - six bottom countersinks'
sink_group.Shapes = cones
final = doc.addObject('Part::Cut', 'FinishedPlate')
final.Label = '70 adapter - finished plate'
final.Base, final.Tool = current, sink_group
doc.recompute()
assert not final.Shape.isNull() and final.Shape.isValid() and len(final.Shape.Solids) == 1, 'invalid result'
prior = set(doc.Objects)
Import.insert(source, doc.Name)
reference = list(set(doc.Objects) - prior)
assert len(reference) == 1
reference[0].Label = 'Original STEP - reference (hidden)'
doc.recompute()
for obj in doc.Objects:
if obj.ViewObject:
obj.ViewObject.Visibility = False
final.ViewObject.Visibility = True
doc.recompute()
Gui.activeDocument().activeView().viewAxonometric()
Gui.activeDocument().activeView().fitAll()
doc.saveAs(target)
print('OUTPUT', target)
print('VALID', final.Shape.isValid(), 'VOLUME', final.Shape.Volume, 'FACES', len(final.Shape.Faces))

View File

@@ -0,0 +1,46 @@
from pathlib import Path
import os, json, math, traceback
import FreeCAD as App
import FreeCADGui as Gui
import Import, Part
root = Path(r'C:\projects\wheel')
out = root / 'models' / '70_m4_drill_quote'
out.mkdir(parents=True, exist_ok=True)
try:
os.chdir(str(root))
base = (root / 'scripts' / 'build_70.py').read_text(encoding='utf-8')
base = base.replace("target = os.path.join(root, 'models', '\\u0037\\u0030\\u8f6c\\u63a5\\u677f_\\u53c2\\u6570\\u5316.FCStd')", "target = str(out / '70_M4_drill.FCStd')")
base = base.replace("('HoleDiameter', '4.234 mm', 'Through hole diameter (all 24 holes)')", "('HoleDiameter', '3.3 mm', 'M4 tapping pilot diameter for all 24 holes')")
base = base.replace("print('OUTPUT', target)", "Import.export([final], str(out / '70_M4_drill.step'))\nGui.activeDocument().activeView().saveImage(str(out / '70_M4_drill.png'), 1600, 1200, 'White')\nprint('OUTPUT', target)")
if "target = str(out / '70_M4_drill.FCStd')" not in base or "'3.3 mm'" not in base:
raise RuntimeError('Failed to patch baseline; original file not modified')
exec(compile(base, str(root/'scripts'/'build_70.py'), 'exec'), {'out':out, '__name__':'__main__'})
for name in list(App.listDocuments()): App.closeDocument(name)
f=App.openDocument(str(out/'70_M4_drill.FCStd')); f.recompute()
plate=f.getObject('FinishedPlate'); shape=plate.Shape
assert shape.isValid() and len(shape.Solids)==1
assert abs(f.getObject('Hole42_0').Radius.Value-1.65)<1e-8
assert abs(f.getObject('Countersink0').Height.Value-1.75)<1e-8
step=App.newDocument('VerifyExport'); Import.insert(str(out/'70_M4_drill.step'), step.Name); step.recompute()
stepShapes=[o.Shape for o in step.Objects if hasattr(o,'Shape') and not o.Shape.isNull()]
assert len(stepShapes)==1 and stepShapes[0].isValid() and len(stepShapes[0].Solids)==1
cylinder_faces=[]
for face in stepShapes[0].Faces:
surf=face.Surface
if isinstance(surf,Part.Cylinder) and abs(surf.Radius-1.65)<1e-6 and abs(abs(surf.Axis.z)-1)<1e-7:
center=surf.Center
cylinder_faces.append({'x':round(center.x,6),'y':round(center.y,6),'z_min':round(face.BoundBox.ZMin,6),'z_max':round(face.BoundBox.ZMax,6),'r':surf.Radius})
assert len(cylinder_faces)==24, 'Expected 24 pilot cylinders, found '+str(len(cylinder_faces))
radii=sorted(round(math.hypot(c['x'],c['y']),4) for c in cylinder_faces)
assert radii == [21.0]*6+[24.0]*6+[25.4]*6+[35.0]*6, str(radii)
pcd42=[c for c in cylinder_faces if abs(math.hypot(c['x'],c['y'])-21)<0.001]
assert all(abs(c['z_min']-1.75)<1e-6 and abs(c['z_max']-8)<1e-6 for c in pcd42)
assert all(abs(c['z_min'])<1e-6 and abs(c['z_max']-8)<1e-6 for c in cylinder_faces if c not in pcd42)
result={'fcstd_valid':True,'fcstd_solids':1,'step_valid':True,'step_solids':1,'step_volume':stepShapes[0].Volume,'pilot_diameter_mm':3.3,'number_pilot_cylinders':len(cylinder_faces),'positions_and_extents':cylinder_faces,'countersink_diameter_mm':6.8,'countersink_angle_deg':90,'countersink_depth_mm':1.75,'pcd42_straight_segment_mm':6.25,'other_straight_segment_mm':8.0,'interpretation':'These are smooth pilot holes, not semantic or geometric threads. Automatic quotation recognition untested.'}
(out/'validation.json').write_text(json.dumps(result,ensure_ascii=False,indent=2),encoding='utf-8')
(out/'status.txt').write_text('OK',encoding='utf-8')
except:
(out/'status.txt').write_text(traceback.format_exc(),encoding='utf-8')
for name in list(App.listDocuments()): App.closeDocument(name)
Gui.getMainWindow().close()

View File

@@ -0,0 +1,38 @@
"""Rebuild a distinct 70 mm adapter with 24 plain M4 tap-drill holes.
Run through rebuild_70_m4_drill_plain.FCMacro in FreeCAD. The existing source
builder is used to retain its native editable PCD features and source contour.
"""
import os
from pathlib import Path
import FreeCAD as App
import FreeCADGui as Gui
import Part, Import
root = Path(r'C:\projects\wheel')
out = root / 'models' / '70_m4_drill_plain'
out.mkdir(parents=True, exist_ok=True)
src = root / 'scripts' / 'build_70.py'
code = src.read_text(encoding='utf-8')
old_target = "target = os.path.join(root, 'models', '\\u0037\\u0030\\u8f6c\\u63a5\\u677f_\\u53c2\\u6570\\u5316.FCStd')"
new_target = "target = os.path.join(root, 'models', '70_m4_drill_plain', '70_M4_drill_plain.FCStd')"
assert old_target in code
code = code.replace(old_target, new_target)
code = code.replace("('HoleDiameter', '4.234 mm', 'Through hole diameter (all 24 holes)')", "('HoleDiameter', '3.3 mm', 'M4x0.7 tap drill, all 24 holes')")
code = code.replace(" ('CountersinkDiameter', '6.8 mm', 'Countersink at bottom face'),\n", '')
begin = code.index("sheet.set('A16', 'CountersinkDepth')")
end = code.index("sheet.set('A17', 'OuterChamfer')", begin)
code = code[:begin] + code[end:]
begin = code.index('cones = []')
end = code.index('prior = set(doc.Objects)', begin)
code = code[:begin] + "final = current\nfinal.Label = '70 adapter - 24 plain M4 tap-drill holes'\ndoc.recompute()\nassert final.Shape.isValid() and len(final.Shape.Solids)==1\n\n" + code[end:]
os.chdir(str(root))
ns = {'__name__': '__main__', '__file__': str(src)}
exec(compile(code, str(src), 'exec'), ns)
doc, final = ns['doc'], ns['final']
assert abs(doc.getObject('Parameters').get('B4').Value-3.3) < 1e-8
Import.export([final], str(out / '70_M4_drill_plain.step'))
Gui.activeDocument().activeView().viewAxonometric()
Gui.activeDocument().activeView().fitAll()
Gui.activeDocument().activeView().saveImage(str(out / '70_M4_drill_plain.png'), 1600, 1200, 'White')
doc.save()

View File

@@ -0,0 +1,60 @@
"""Build a separate, editable 70 adapter with 24 M4x0.7 modeled threads.
Run via rebuild_70_m4_threaded.FCMacro; does not overwrite original files.
"""
from pathlib import Path
import os
ROOT = Path(__file__).resolve().parent.parent
os.chdir(str(ROOT))
OUT = ROOT / 'models' / '70_m4_threaded'
OUT.mkdir(parents=True, exist_ok=True)
base = (ROOT / 'scripts' / 'build_70.py').read_text(encoding='utf-8')
base = base.replace('import Part\nimport Import', 'import Part\nimport Sketcher\nimport Import')
base = base.replace("target = os.path.join(root, 'models', '\\u0037\\u0030\\u8f6c\\u63a5\\u677f_\\u53c2\\u6570\\u5316.FCStd')", "target = str(OUT / '70_M4_threaded.FCStd')")
base = base.replace("('HoleDiameter', '4.234 mm', 'Through hole diameter (all 24 holes)')", "('HoleDiameter', '3.322 mm', 'M4x0.7 modeled hole minor diameter for continuous 90 degree countersinks')")
start = base.index("for name, pcd, angle in [('42', 'PCD42', 'Angle42')")
end = base.index('\ncones = []', start)
replacement = '''# One native PartDesign Hole with a 24-circle parametric profile.
thread_body = doc.addObject('PartDesign::Body', 'ThreadBody')
thread_body.Label = 'M4 threaded holes - editable'
thread_body.BaseFeature = current
doc.recompute()
s = thread_body.newObject('Sketcher::SketchObject', 'ThreadProfiles')
s.Label = '24 thread centers, four PCD groups'
s.Placement.Base.z = 8
s.setExpression('Placement.Base.z', 'Parameters.Thickness')
for name,pcd,angle,angle0 in [('42','PCD42','Angle42',45),('48','PCD48','Angle48',0),('508','PCD508','Angle508',30),('70','PCD70','Angle70',30)]:
radius0={'42':21.0,'48':24.0,'508':25.4,'70':35.0}[name]
for i in range(6):
t=math.radians(angle0+i*60)
x,y=radius0*math.cos(t),radius0*math.sin(t)
if abs(x)<1e-8: x=0.0
if abs(y)<1e-8: y=0.0
k=s.addGeometry(Part.Circle(App.Vector(x,y,0),App.Vector(0,0,1),1.0),False)
q=s.addConstraint(Sketcher.Constraint('DistanceX',k,3,x))
s.setExpression('Constraints[%d]' % q, 'Parameters.%s / 2 * cos(Parameters.%s + %d deg)' % (pcd,angle,i*60))
q=s.addConstraint(Sketcher.Constraint('DistanceY',k,3,y))
s.setExpression('Constraints[%d]' % q, 'Parameters.%s / 2 * sin(Parameters.%s + %d deg)' % (pcd,angle,i*60))
s.addConstraint(Sketcher.Constraint('Radius',k,1.0))
doc.recompute()
h=thread_body.newObject('PartDesign::Hole','ThreadHoles')
h.Label='24 modeled M4x0.7 through threads'
h.Profile=s
h.ThreadType='ISOMetricProfile'
h.Threaded=True
doc.recompute()
h.ThreadSize='M4x0.7'
h.DepthType='ThroughAll'
h.ThreadDepthType='Hole Depth'
h.ModelThread=True
doc.recompute()
if h.Shape.isNull() or not h.Shape.isValid() or len(h.Shape.Solids)!=1: raise RuntimeError('Invalid thread feature '+str(h.State))
current=h
print('THREAD', 'valid',h.Shape.isValid(),'faces',len(h.Shape.Faces),'dia',h.Diameter,'thread',h.ThreadSize)
'''
base = base[:start] + replacement + base[end:]
base = base.replace("print('OUTPUT', target)", "Import.export([final], str(OUT / '70_M4_threaded.step'))\nGui.activeDocument().activeView().saveImage(str(OUT / '70_M4_preview.png'), 1600, 1200, 'White')\nprint('OUTPUT', target)")
exec(compile(base, str(ROOT/'scripts'/'build_70.py'), 'exec'), {'OUT':OUT, '__name__':'__main__'})

View File

@@ -0,0 +1,183 @@
from pathlib import Path
import FreeCAD as A, FreeCADGui as G, Part, Import, os, math, time, json, traceback
from pivy import coin
V=A.Vector
root=Path(r'C:\projects\wheel'); out=root/'models'/'all_new_corner_18_concept';out.mkdir(parents=True,exist_ok=True)
def box(x,y,z,dx,dy,dz):return Part.makeBox(dx,dy,dz,V(x,y,z))
def cyl(r,h,x,y,z,axis=V(0,0,1)):return Part.makeCylinder(r,h,V(x,y,z),axis)
def add(doc,n,shape,label,color,trans=0):
o=doc.addObject('PartDesign::Feature',n);o.Shape=shape.removeSplitter();o.Label=label;o.ViewObject.ShapeColor=color;o.ViewObject.LineColor=(.12,.12,.12);o.ViewObject.Transparency=trans;return o
def vol(a,b):return a.common(b).Volume if a.BoundBox.intersect(b.BoundBox) else 0.
def cc(a,b):return {'vol':vol(a,b),'dist':a.distToShape(b)[0]}
try:
d=A.openDocument(str(root/'models'/'fan_direct_motor_side_140'/'Assembly.FCStd'));d.recompute();d.Label='新电机角码与2mm垫片概念装配'
p=38/math.sqrt(2); xinner=p+10.5; xwall=xinner+1.8; xrail=xwall+2; xc=xrail+10
# Reference washer sits on bracket's extrusion facing X surface, never between bracket and motor.
oldlist=['Corner0','Corner1','Corner2','Corner3','Mount76_0','Mount76_1','Mount76_2','Mount76_3','Washer76_0','Washer76_1','Washer76_2','Washer76_3','OpenPillarBracket','Head50_0','Head50_1','Head50_2','Head50_3','Shaft50_0','Shaft50_1','Shaft50_2','Shaft50_3','AdjustShim0','AdjustShim1','AdjustShim2','AdjustShim3','DirectFan','FanBolt0','FanBolt1','Crossbeam0','Crossbeam1','BaseJoint00','BaseJoint01','BaseJoint10','BaseJoint11','PurchasedAngle00','PurchasedAngle01','PurchasedAngle10','PurchasedAngle11','BoardM5_00','BoardM5_01','BoardM5_10','BoardM5_11','FootHead00','FootHead01','FootHead10','FootHead11']
for n in oldlist:
o=d.getObject(n)
if o:o.ViewObject.Visibility=False
# previously purchased profile lengths and axial position, model vertical profiles remain solid envelopes
for i,sgn in enumerate((-1,1)):
rail=d.getObject('Rail%d'%i); rail.Placement=A.Placement();rail.Shape=box(sgn*xc-10,-55,0,20,140,20);rail.Label='Purchased 2020, 140mm'
base=d.getObject('NewRail%d'%i);base.Placement=A.Placement();base.Shape=box(sgn*xc-15,-85,-215,30,30,250);base.Label='Purchased 3030, 250mm'
for stem in ['MotorBaseCorner','SwitchCrossJoint']:
a=d.getObject(stem+str(i));old_center=(-1 if i==0 else 1)*50.6200577
dx=sgn*xc-old_center; s=a.Shape.copy();s.translate(V(dx,0,0));a.Placement=A.Placement();a.Shape=s
# Lighting crossbeam 130mm, fixed by supplied stock; no guessed changes to control profile
sw=d.getObject('SwitchCross2020');sw.Placement=A.Placement();sw.Shape=box(-65,-55,-142,130,20,20);sw.Label='Purchased 2020 switch 130mm'
motor=d.getObject('Motor')
# Cut shape: round end at opposite side of bend. Plan view is x-z extrusion directions; y is 16mm width.
corners=[];washers=[];studs=[];screws=[]
for i,sgn in enumerate((-1,1)):
for j,y in enumerate((10-p,10+p)):
# +X prototype motor leg. 20 length includes 2mm bend wall, 16 width. Round free end R8.
flat=box(xinner-10.2,y-8,0,12,16,1.8)
cap=cyl(8,1.8,xinner-10.2,y,0).common(box(xinner-18.2,y-8,0,8,16,1.8))
leg=flat.fuse(cap).cut(cyl(2.5,1.9,p,y,-.05))
wall=box(xinner,y-8,0,1.8,16,12)
cap2=cyl(8,1.8,xinner,y,12,V(1,0,0)).common(box(xinner,y-8,12,1.8,16,8))
wall=wall.fuse(cap2).cut(cyl(2.5,1.9,xinner-.05,y,12,V(1,0,0)))
corner=leg.fuse(wall).removeSplitter()
washer=cyl(4.5,2,xwall,y,12,V(1,0,0)).cut(cyl(2.2,2.1,xwall-.05,y,12,V(1,0,0)))
# Motor M4 shank and simple head, countersink depth is unknown
motorbolt=cyl(1.95,7,p,y,-5).fuse(cyl(3.5,2.2,p,y,2))
if sgn<0:
corner=corner.mirror(V(),V(1,0,0));washer=washer.mirror(V(),V(1,0,0));motorbolt=motorbolt.mirror(V(),V(1,0,0))
c=add(d,'NewMotorCorner%d%d'%(i,j),corner,'New stainless 20x20x16x1.8 Ø5, round ends assumed',(.9,.55,.13));corners.append(c)
w=add(d,'RailWasher%d%d'%(i,j),washer,'2mm total washer BETWEEN motor corner and 2020 rail',(.95,.2,.12));washers.append(w)
screws.append(add(d,'NewMotorM4%d%d'%(i,j),motorbolt,'M4 motor shank + head schematic',(.65,.65,.68)))
# Encoder bracket as plate-only visualization, 4 hex standoffs per user idea; no claim of finalized printable design
old=d.getObject('OpenPillarBracket').Shape
plate=old.common(box(-70,-70,10.9,140,150,3.2))
bp=add(d,'EncoderPlateConcept',plate,'Encoder panel only; metal standoffs visual concept, not print-ready',(.08,.3,.85))
for i in range(4):
ang=math.radians(45+90*i);x=25*math.cos(ang);y=10+25*math.sin(ang);Rhex=7/(2*math.cos(math.pi/6))
points=[V(x+Rhex*math.cos(math.radians(30+60*k)),y+Rhex*math.sin(math.radians(30+60*k)),0) for k in range(6)];points.append(points[0])
hexshape=Part.Face(Part.makePolygon(points)).extrude(V(0,0,10)).cut(cyl(2.2,10.02,x,y,-.01))
st=add(d,'EncoderHex%d'%i,hexshape,'M4 7mm AF 10mm stand-off ASSUMED',(.86,.72,.3));studs.append(st)
top=cyl(4.5,.9,x,y,10).cut(cyl(2.2,.92,x,y,9.99))
add(d,'EncoderSpacer%d'%i,top,'Encoder metal shim .9mm SCHEMATIC',(.72,.72,.75))
# Two purchased 70mm beams are centred between the 3030 rails.
# The original mounting corners remain at the same ends, translated with the new rails.
conflicts=[]; joint_checks=[]
for i,z in enumerate((-114.,-179.)):
cross=d.getObject('Crossbeam%d'%i); cross.Placement=A.Placement();cross.Shape=box(-35,-85,z,70,30,30);cross.Label='Purchased 3030 crossbeam 70mm';cross.ViewObject.Visibility=True
for j,sgn in enumerate((-1,1)):
rr=d.getObject('NewRail%d'%j).Shape
c=cross.Shape.common(rr)
if c.Volume>1e-5: conflicts.append((i,j,c.Volume))
name='BaseJoint%d%d'%(i,j); a=d.getObject(name)
ss=a.Shape.copy();ss.translate(V(sgn*(xc-50.6200577),0,0));a.Placement=A.Placement();a.Shape=ss;a.ViewObject.Visibility=True
joint_checks.append({'name':name,'cross':cc(a.Shape,cross.Shape),'base':cc(a.Shape,rr)})
for stem in ('PurchasedAngle','BoardM5_','FootHead'):
for j in (0,1):
o=d.getObject(stem+str(i)+str(j))
if o:
o.ViewObject.Visibility=True
if stem in ('PurchasedAngle','FootHead'):
ss=o.Shape.copy();ss.translate(V(0,2,0));o.Placement=A.Placement();o.Shape=ss
# An initial fan frame rotated for new spacing, check conflicts and hide if nonzero.
theta=math.degrees(math.acos((2*xc)/(105*math.sqrt(2))))-45;alpha=math.radians(theta)
fan=box(-60,-60,20,120,120,25).cut(cyl(56,25.04,0,0,19.98))
for x in (-52.5,52.5):
for y in (-52.5,52.5):fan=fan.cut(cyl(2.2,25.1,x,y,19.95))
fan=fan.removeSplitter();fan.rotate(V(),V(0,0,1),theta);fan.translate(V(0,10,0))
f=d.getObject('DirectFan');f.Placement=A.Placement();f.Shape=fan; f.Label='FAN illustrative; rotated to new spacing';f.ViewObject.Visibility=True
for sgn,i in [(-1,0),(1,1)]:
x=sgn*52.5*(math.cos(alpha)-math.sin(alpha));y=10+sgn*52.5*(math.cos(alpha)+math.sin(alpha))
o=d.getObject('FanBolt%d'%i);o.Placement=A.Placement();o.Shape=cyl(2,25,x,y,20).fuse(cyl(3.5,3,x,y,45));o.ViewObject.Visibility=True
d.recompute()
checks=[]
for i,c in enumerate(corners):
j=i//2;rr=d.getObject('Rail%d'%j).Shape
checks.append({'corner':c.Name,'motor_volume':vol(c.Shape,motor.Shape),'washer_corner':cc(c.Shape,washers[i].Shape),'washer_rail':cc(washers[i].Shape,rr),'hex_volume':sum(vol(c.Shape,s.Shape) for s in studs),'hex_min_distance':min(c.Shape.distToShape(s.Shape)[0] for s in studs)})
fanhits=[]
for o in d.Objects:
if o==f or not hasattr(o,'Shape') or o.Shape.isNull() or not o.ViewObject.Visibility or o.Name.startswith('FanBolt'):continue
v=vol(fan,o.Shape)
if v>1e-4:fanhits.append([o.Name,v])
# hide guessed fan when conflicts (never imply it has been fitted)
if fanhits:
f.ViewObject.Visibility=False
for i in (0,1):d.getObject('FanBolt%d'%i).ViewObject.Visibility=False
# Replace every visible old bracket, including the two types of long brackets.
# New L shape made with round distal ends. Its holes are both Ø5 mm. We model
# only sharp-bend nominal geometry; countersink and tool access are unknown.
def l_yz(x, yc, zface, upwards_z, foot_direction):
# width X=16; foot contacts y=yc, upright contacts z=zface.
t=1.8
zz=zface if upwards_z>0 else zface-t
side=box(x-8,yc,zz,16,12,t).fuse(cyl(8,t,x,yc+12,zz).common(box(x-8,yc+12,zz,16,8,t)))
side=side.cut(cyl(2.5,t+.1,x,yc+12,zz-.05))
if foot_direction>0:
foot=box(x-8,yc,zface,16,t,12).fuse(cyl(8,t,x,yc,zface+12,V(0,1,0)).common(box(x-8,yc,zface+12,16,t,8)))
foot=foot.cut(cyl(2.5,t+.1,x,yc-.05,zface+12,V(0,1,0)))
else:
foot=box(x-8,yc,zface-12,16,t,12).fuse(cyl(8,t,x,yc,zface-12,V(0,1,0)).common(box(x-8,yc,zface-20,16,t,8)))
foot=foot.cut(cyl(2.5,t+.1,x,yc-.05,zface-12,V(0,1,0)))
return side.fuse(foot).removeSplitter()
# Same purchased bracket at connections between each motor 2020 and 3030.
for i,sgn in enumerate((-1,1)):
old=d.getObject('MotorBaseCorner%d'%i); old.ViewObject.Visibility=False
add(d,'NewMotorBase%d'%i,l_yz(sgn*xc,-55,0,-1,-1),'20x20x16x1.8 NEW M4 concept',(.12,.42,.15))
# Switch cross-beam connections. Both legs assumed the same end geometry.
for i,sgn in enumerate((-1,1)):
old=d.getObject('SwitchCrossJoint%d'%i);old.ViewObject.Visibility=False
add(d,'NewSwitchBase%d'%i,l_yz(sgn*xc,-55,-122,1,1),'20x20x16x1.8 NEW M4 concept',(.12,.42,.15))
# New small brackets on the four 70 mm cross-beam to 3030 joints. Slot and
# screw centre alignment checked separately: crossbeam and rail centre Y
# differ by 2 mm, larger than the available nominal M4/Ø5 clearance.
def l_xz(side, zface, y):
t=1.8; inner=side*(xc-15)
if side>0:
x0=inner-t; direction=-1
else:
x0=inner; direction=1
if direction>0:
face=box(inner,y-8,zface,12,16,t).fuse(cyl(8,t,inner+12,y,zface).common(box(inner+12,y-8,zface,8,16,t)))
face=face.cut(cyl(2.5,t+.1,inner+12,y,zface-.05))
else:
face=box(inner-12,y-8,zface,12,16,t).fuse(cyl(8,t,inner-12,y,zface).common(box(inner-20,y-8,zface,8,16,t)))
face=face.cut(cyl(2.5,t+.1,inner-12,y,zface-.05))
# Plane on inside of rail; legs extend from zface toward the 3030 slot.
vert=box(x0,y-8,zface, t,16,12).fuse(cyl(8,t,x0,y,zface+12,V(1,0,0)).common(box(x0,y-8,zface+12,t,16,8)))
vert=vert.cut(cyl(2.5,t+.1,x0-.05,y,zface+12,V(1,0,0)))
return face.fuse(vert).removeSplitter()
for i in range(2):
for j,side in enumerate((-1,1)):
d.getObject('BaseJoint%d%d'%(i,j)).ViewObject.Visibility=False
zface=-84 if i==0 else -179
# For the rear bearing make the angle face outward, mirror about zface.
sh=l_xz(side,zface,-70)
if i==1: sh=sh.mirror(V(0,0,zface),V(0,0,1))
add(d,'NewBeamBase%d%d'%(i,j),sh,'20x20x16x1.8: nominal screw-axis mismatch',(.12,.42,.15))
# Four bearing and two switch long-slot brackets intentionally remain, per
# user clarification. Beam top and long-angle feet rise 2mm to align beam and
# longitudinal 3030 groove centre lines; plate, shaft, and motor stay fixed.
new_joint_checks=[]
for i in range(2):
for j in range(2):
n='NewBeamBase%d%d'%(i,j);a=d.getObject(n)
new_joint_checks.append({'name':n,'cross':cc(a.Shape,d.getObject('Crossbeam%d'%i).Shape),'base':cc(a.Shape,d.getObject('NewRail%d'%j).Shape)})
d.recompute();d.saveAs(str(out/'Assembly_新小角码装配_概念.FCStd'))
selected=[o for o in d.Objects if hasattr(o,'Shape') and not o.Shape.isNull() and o.ViewObject.Visibility]
Import.export(selected,str(out/'Assembly_新小角码装配_概念.step'))
view=G.activeDocument().activeView();G.activateWorkbench('PartWorkbench');view.viewAxonometric();G.updateGui();view.fitAll();time.sleep(.5);G.updateGui();view.saveImage(str(out/'新小角码_长角码保留_3D.png'),1800,1300,'White')
vis={o.Name:o.ViewObject.Visibility for o in d.Objects if hasattr(o,'Shape')}; show={'Motor','Holder','Magnet','PCB','Chip','EncoderPlateConcept','Rail0','Rail1','NewRail0','NewRail1','MotorBaseCorner0','MotorBaseCorner1'}|{o.Name for o in corners+washers+studs+screws}|{'DirectFan','FanBolt0','FanBolt1'}
for o in d.Objects:
if hasattr(o,'Shape'):o.ViewObject.Visibility=(o.Name in show and (not fanhits or not o.Name.startswith(('DirectFan','FanBolt'))))
view.viewTop();G.updateGui();view.fitAll();time.sleep(.5);G.updateGui();view.saveImage(str(out/'电机端_正视.png'),1800,1300,'White')
view.viewAxonometric();G.updateGui();view.fitAll();time.sleep(.5);G.updateGui();view.saveImage(str(out/'角码与垫片_3D.png'),1800,1300,'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('Rail0').Shape.isValid(); A.closeDocument(d.Name)
step=Part.read(str(out/'Assembly_新小角码装配_概念.step')); assert step.isValid()
info={'all_user_lengths_mm':{'3030_long':250,'3030_cross':70,'2020_motor':140,'2020_switch':130},'hole_centres_76':p,'corner_wall_outside_x':xwall,'rail_inside_x':xrail,'washer_thickness':2,'base_joint_checks':new_joint_checks,'rail_centre_x':xc,'motor_track_centres_mm':2*xc,'3030_inside_gap_mm':2*(xc-15),'70_cross_member_length_mm':70,'crossbar_intersections_mm3':conflicts,'angle_rotation_degrees':theta,'corner_checks':checks,'fan_hits':fanhits,'note':'All 12 small angles replaced with nominal 20x20x16x1.8; 6 long-slot angles retained; real T slots, screws and fan hubs unmodelled.'}
(out/'验证.json').write_text(json.dumps(info,ensure_ascii=False,indent=2),encoding='utf8');(out/'完成.txt').write_text('ok',encoding='utf8');os._exit(0)
except Exception:
(out/'错误.txt').write_text(traceback.format_exc(),encoding='utf8');os._exit(1)

View File

@@ -0,0 +1,109 @@
# Rebuild through rebuild_conversion_disk.FCMacro in FreeCAD's GUI to save the
# same visibility and view as the delivered file. Running this .py directly
# with FreeCADCmd also builds the geometry, but has no GUI view to save.
# Rebuilding overwrites models/conversion_disk_parametric.FCStd.
import FreeCAD as App
import Part, Sketcher, math, pathlib
ROOT = pathlib.Path(__file__).resolve().parent.parent
source = ROOT / 'models' / '转换盘1.step'
target = ROOT / 'models' / 'conversion_disk_parametric.FCStd'
doc = App.newDocument('ConversionDisk')
params = doc.addObject('Spreadsheet::Sheet', 'Parameters')
params.Label = 'Parameters (mm)'
rows = [
('Thickness','8 mm','Plate thickness'),
('OuterPCD','48 mm','Outer 6-hole pitch circle diameter'),
('OuterHole','4.234 mm','Outer 6-hole diameter'),
('CenterHole','15 mm','Center through hole diameter'),
('InnerPCD','20 mm','Inner 8-hole pitch circle diameter'),
('InnerHole','3 mm','Inner 8-hole diameter'),
('RecessDiameter','28 mm','Bottom recess diameter'),
('RecessDepth','2 mm','Bottom recess depth'),
('OutlineScale','1','Overall outline scale relative to outer PCD'),
('EdgeChamfer','1.5 mm','Outer edges chamfer'),
('OuterAngle','30 deg','Outer 6-hole start angle'),
('InnerAngle','0 deg','Inner 8-hole start angle'),
]
params.set('A1','Parameter'); params.set('B1','Value'); params.set('C1','Description')
for i,(key,val,desc) in enumerate(rows,2):
params.set('A%d'%i,key);params.set('B%d'%i,val);params.set('C%d'%i,desc);params.setAlias('B%d'%i,key)
doc.recompute()
reference=doc.addObject('PartDesign::Feature','OriginalReference')
reference.Label='Original STEP reference'
reference.Shape=Part.read(str(source))
body=doc.addObject('PartDesign::Body','Body')
body.Label='Conversion disk'
def sketch(name, z=None):
s=doc.addObject('Sketcher::SketchObject',name);body.addObject(s)
if z is not None:
s.Placement.Base.z=z
s.setExpression('Placement.Base.z','Parameters.Thickness')
return s
def circle(s,x,y,r,exprx=None,expry=None,exprr=None):
k=s.addGeometry(Part.Circle(App.Vector(x,y,0),App.Vector(0,0,1),r),False)
if abs(x)<1e-8 and abs(y)<1e-8:
s.addConstraint(Sketcher.Constraint('Coincident',k,3,-1,1))
else:
if abs(x)<1e-8: s.addConstraint(Sketcher.Constraint('PointOnObject',k,3,-2))
else:
ci=s.addConstraint(Sketcher.Constraint('DistanceX',k,3,x))
if exprx: s.setExpression('Constraints[%d]'%ci,exprx)
if abs(y)<1e-8: s.addConstraint(Sketcher.Constraint('PointOnObject',k,3,-1))
else:
ci=s.addConstraint(Sketcher.Constraint('DistanceY',k,3,y))
if expry: s.setExpression('Constraints[%d]'%ci,expry)
ci=s.addConstraint(Sketcher.Constraint('Radius',k,r))
if exprr:s.setExpression('Constraints[%d]'%ci,exprr)
def verify(obj):
doc.recompute()
print('CHECK',obj.Name, 'shape', obj.Shape.isNull(), 'valid', False if obj.Shape.isNull() else obj.Shape.isValid(), 'status',obj.State)
if obj.Shape.isNull() or not obj.Shape.isValid() or 'Invalid' in obj.State:raise RuntimeError('Invalid feature '+obj.Name)
# Preserve the true STEP mid-plane perimeter. A native link scales the whole
# profile in XY and rotates it with the outer hole group; the individual arcs
# are fixed source geometry rather than independently editable constraints.
wire=max(reference.Shape.slice(App.Vector(0,0,1),4),key=lambda w:w.Length)
face=Part.Face(wire);face.translate(App.Vector(0,0,-4))
profile=doc.addObject('Part::Feature','OriginalOutline')
profile.Label='Original outline at mid-thickness (reference geometry)';profile.Shape=face
scaled=doc.addObject('App::Link','ScaledOutline');scaled.Label='Original outline - scaled and rotated'
scaled.setLink(profile);scaled.LinkTransform=True
scaled.setExpression('ScaleVector.x','Parameters.OuterPCD / 48 mm * Parameters.OutlineScale')
scaled.setExpression('ScaleVector.y','Parameters.OuterPCD / 48 mm * Parameters.OutlineScale')
scaled.setExpression('Placement.Rotation.Angle','Parameters.OuterAngle - 30 deg')
outline=doc.addObject('Part::Extrusion','Outline');outline.Label='Original profile extruded'
outline.Base=scaled;outline.DirMode='Custom';outline.Dir=App.Vector(0,0,1);outline.LengthFwd=8;outline.Solid=True
outline.setExpression('LengthFwd','Parameters.Thickness');verify(outline)
body.BaseFeature=outline;doc.recompute()
edges=[]
for i,e in enumerate(outline.Shape.Edges,1):
b=e.BoundBox
if abs(b.ZMax-b.ZMin)<1e-7 and (abs(b.ZMin)<1e-7 or abs(b.ZMin-8)<1e-7):edges.append('Edge%d'%i)
print('CHAMFER edges',edges)
ch=body.newObject('PartDesign::Chamfer','PerimeterChamfer');ch.Base=(ch.BaseFeature,edges);ch.Size=1.5;ch.setExpression('Size','Parameters.EdgeChamfer');verify(ch)
recesssk=sketch('RecessProfile');circle(recesssk,0,0,14,exprr='Parameters.RecessDiameter / 2');doc.recompute()
recess=body.newObject('PartDesign::Pocket','BottomRecess');recess.Profile=recesssk;recess.Length=2;recess.Reversed=True;recess.setExpression('Length','Parameters.RecessDepth');verify(recess)
center=sketch('CenterProfile',8);center.Label='Center through hole'
circle(center,0,0,7.5,exprr='Parameters.CenterHole / 2');doc.recompute()
center_cut=body.newObject('PartDesign::Pocket','CenterHole');center_cut.Profile=center;center_cut.Length=8;center_cut.setExpression('Length','Parameters.Thickness');verify(center_cut)
for count,pcd,r,diam,angle in [(6,'OuterPCD',2.117,'OuterHole','OuterAngle'),(8,'InnerPCD',1.5,'InnerHole','InnerAngle')]:
holes=sketch(diam+'Profile',8);holes.Label=diam+' pattern'
holes.setExpression('Placement.Rotation.Angle','Parameters.'+angle)
for i in range(count):
a=math.radians(i*360/count);c=math.cos(a);s=math.sin(a);d=24 if count==6 else 10
circle(holes,d*c,d*s,r,'(%s) * Parameters.%s / 2'%(c,pcd),'(%s) * Parameters.%s / 2'%(s,pcd),'Parameters.%s / 2'%diam)
doc.recompute()
holes_cut=body.newObject('PartDesign::Pocket',diam+'Through');holes_cut.Profile=holes;holes_cut.Length=8;holes_cut.setExpression('Length','Parameters.Thickness');verify(holes_cut)
print('FINAL volume',holes_cut.Shape.Volume,'bounds',holes_cut.Shape.BoundBox)
doc.recompute()
if App.GuiUp:
import FreeCADGui as Gui
for obj in doc.Objects:
if obj.ViewObject:
obj.ViewObject.Visibility = False
body.ViewObject.Visibility = True
holes_cut.ViewObject.Visibility = True
Gui.activeDocument().activeView().viewAxonometric()
Gui.activeDocument().activeView().fitAll()
doc.saveAs(str(target))
print('SAVED',str(target))

View File

@@ -0,0 +1,82 @@
"""Create a separate copy of the conversion disk with six true M4 threads.
Run with FreeCAD GUI (headless/hidden window is fine). Original files untouched.
"""
from pathlib import Path
import traceback, math, time
import FreeCAD as App, FreeCADGui as Gui, Part, Import
root=Path(r'C:\projects\wheel')
out=root/'models'/'conversion_disk_m4'
out.mkdir(parents=True, exist_ok=True)
logs=[]
doc=None
try:
source=root/'scripts'/'build_conversion_disk.py'
src=source.read_text(encoding='utf-8')
src=src.replace("target = ROOT / 'models' / 'conversion_disk_parametric.FCStd'", "target = ROOT / 'models' / 'conversion_disk_m4' / '转换盘_M4.FCStd'")
src=src.replace("('OuterHole','4.234 mm','Outer 6-hole diameter'),", "('OuterHole','3.3 mm','Hole sketch circle for the 6x M4 thread centers; native Hole determines final geometry'),")
old=" holes_cut=body.newObject('PartDesign::Pocket',diam+'Through');holes_cut.Profile=holes;holes_cut.Length=8;holes_cut.setExpression('Length','Parameters.Thickness');verify(holes_cut)"
new=""" if count==6:
holes_cut=body.newObject('PartDesign::Hole','OuterM4Thread');holes_cut.Label='PCD48: 6 x M4 x 0.7, 6H through (modeled)';holes_cut.Profile=holes
holes_cut.Threaded=True;holes_cut.ThreadType='ISOMetricProfile';doc.recompute()
holes_cut.ThreadSize='M4x0.7';holes_cut.ThreadClass='6H';holes_cut.DepthType='ThroughAll';holes_cut.ModelThread=True;doc.recompute();verify(holes_cut)
else:
holes_cut=body.newObject('PartDesign::Pocket',diam+'Through');holes_cut.Profile=holes;holes_cut.Length=8;holes_cut.setExpression('Length','Parameters.Thickness');verify(holes_cut)"""
assert old in src and "conversion_disk_m4' / '转换盘_M4.FCStd'" in src
src=src.replace(old,new)
context={'__name__':'__main__','__file__':str(source)}
exec(compile(src,str(source),'exec'),context)
doc=context['doc'];body=doc.getObject('Body');thread=doc.getObject('OuterM4Thread');final=doc.getObject('InnerHoleThrough')
doc.recompute()
assert thread.Threaded and thread.ModelThread and thread.ThreadSize=='M4x0.7' and thread.ThreadClass=='6H'
assert thread.Shape.isValid() and final.Shape.isValid() and len(final.Shape.Solids)==1
logs.append('Native Hole: threaded=%s modeled=%s size=%s class=%s depth_type=%s valid=%s faces=%d' % (thread.Threaded,thread.ModelThread,thread.ThreadSize,thread.ThreadClass,thread.DepthType,thread.Shape.isValid(),len(thread.Shape.Faces)))
logs.append('Final: valid=%s solids=%d volume=%.8f faces=%d' % (final.Shape.isValid(),len(final.Shape.Solids),final.Shape.Volume,len(final.Shape.Faces)))
original=App.openDocument(str(root/'models'/'conversion_disk_parametric.FCStd'))
reference=original.getObject('InnerHoleThrough').Shape
masks=[]
for i in range(6):
a=math.radians(30+60*i);masks.append(Part.makeCylinder(3.2,8.2,App.Vector(24*math.cos(a),24*math.sin(a),-.1)))
mask=masks[0].multiFuse(masks[1:]); mask=mask.removeSplitter()
def outside_delta(a,b):
delta=a.cut(b)
if delta.isNull():return 0.0
try: remaining=delta.cut(mask)
except ValueError as exc:
if str(exc)=='Null shape':return 0.0
raise
return 0.0 if remaining.isNull() else remaining.Volume
delta1=outside_delta(reference,final.Shape)
delta2=outside_delta(final.Shape,reference)
logs.append('Outside six PCD48 masks, old minus new volume=%.9f; new minus old=%.9f mm3' % (delta1,delta2))
assert delta1<1e-5 and delta2<1e-5
App.closeDocument(original.Name)
App.setActiveDocument(doc.Name)
for obj in doc.Objects:
if obj.ViewObject:obj.ViewObject.Visibility=False
body.ViewObject.Visibility=True;final.ViewObject.Visibility=True
Gui.activeDocument().activeView().viewAxonometric();Gui.activeDocument().activeView().fitAll();Gui.updateGui()
Gui.activeDocument().activeView().saveImage(str(out/'转换盘_M4预览.png'),1500,1100,'White')
doc.recompute();doc.saveAs(str(out/'转换盘_M4.FCStd'))
final_volume=final.Shape.Volume
Import.export([final],str(out/'转换盘_M4.step'))
name=doc.Name;App.closeDocument(name)
reopened=App.openDocument(str(out/'转换盘_M4.FCStd'));reopened.recompute()
rt=reopened.getObject('OuterM4Thread');rf=reopened.getObject('InnerHoleThrough')
assert rt.ModelThread and rt.Threaded and rt.ThreadSize=='M4x0.7' and rt.ThreadClass=='6H' and rf.Shape.isValid()
logs.append('Reopened FCStd: modeled=%s size=%s class=%s final_valid=%s' % (rt.ModelThread,rt.ThreadSize,rt.ThreadClass,rf.Shape.isValid()))
App.closeDocument(reopened.Name)
stepdoc=App.newDocument('STEP_validation');Import.insert(str(out/'转换盘_M4.step'),stepdoc.Name);stepdoc.recompute()
shapes=[o.Shape for o in stepdoc.Objects if hasattr(o,'Shape') and not o.Shape.isNull()]
assert len(shapes)>0 and all(s.isValid() for s in shapes)
logs.append('STEP objects=%d total_volume=%.8f total_faces=%d valid=True' % (len(shapes),sum(s.Volume for s in shapes),sum(len(s.Faces) for s in shapes)))
assert abs(sum(s.Volume for s in shapes)-final_volume)<0.001
App.closeDocument(stepdoc.Name)
(out/'validation.txt').write_text('\n'.join(logs)+'\n',encoding='utf-8')
(out/'DONE.txt').write_text('ok',encoding='utf-8')
except Exception:
(out/'ERROR.txt').write_text(traceback.format_exc()+'\n'+'\n'.join(logs),encoding='utf-8')
finally:
for d in list(App.listDocuments().values()):
App.closeDocument(d.Name)
Gui.getMainWindow().close()

View File

@@ -0,0 +1,70 @@
"""Build a separate Ø3.3 drill-hole quoting test copy, not a threaded model."""
from pathlib import Path
import FreeCAD as App, FreeCADGui as Gui, Part, Import
import math, traceback
root=Path(r'C:\projects\wheel')
out=root/'models'/'conversion_disk_m4_drill'
out.mkdir(parents=True,exist_ok=True)
logs=[]
try:
source=root/'scripts'/'build_conversion_disk.py'
src=source.read_text(encoding='utf-8')
src=src.replace("target = ROOT / 'models' / 'conversion_disk_parametric.FCStd'", "target = ROOT / 'models' / 'conversion_disk_m4_drill' / '转换盘_M4底孔.FCStd'")
src=src.replace("('OuterHole','4.234 mm','Outer 6-hole diameter'),", "('OuterHole','3.3 mm','Outer 6-hole M4 tap-drill, no modeled thread'),")
assert "conversion_disk_m4_drill' / '转换盘_M4底孔.FCStd'" in src and "('OuterHole','3.3 mm'" in src
ctx={'__name__':'__main__','__file__':str(source)}
exec(compile(src,str(source),'exec'),ctx)
d=ctx['doc'];body=d.getObject('Body');f=d.getObject('InnerHoleThrough');d.recompute()
assert f.Shape.isValid() and len(f.Shape.Solids)==1
def inspect(shape):
found=[]
for face in shape.Faces:
surf=face.Surface
if isinstance(surf,Part.Cylinder) and abs(surf.Radius-1.65)<1e-5:
c=surf.Center
if abs(math.hypot(c.x,c.y)-24)<1e-4 and face.BoundBox.ZLength>7.99: found.append((c.x,c.y))
assert len(found)==6, 'found %d intended Ø3.3 through bores' % len(found)
for i in range(6):
a=math.radians(30+i*60);x=24*math.cos(a);y=24*math.sin(a)
assert any(math.hypot(x-u,y-v)<1e-4 for u,v in found), 'Missing bore %d' % i
return found
pos=inspect(f.Shape);logs.append('FCStd: six Ø3.3 full-depth PCD48 holes, phase 30°, coordinates='+str(pos))
original=App.openDocument(str(root/'models'/'conversion_disk_parametric.FCStd'))
orig=original.getObject('InnerHoleThrough').Shape
masks=[Part.makeCylinder(3,8.2,App.Vector(24*math.cos(math.radians(30+i*60)),24*math.sin(math.radians(30+i*60)),-.1)) for i in range(6)]
mask=masks[0].multiFuse(masks[1:])
def outside(a,b):
t=a.cut(b)
if t.isNull():return 0.0
try:r=t.cut(mask)
except ValueError as e:
if str(e)=='Null shape':return 0.0
raise
return 0.0 if r.isNull() else r.Volume
old_new=outside(orig,f.Shape);new_old=outside(f.Shape,orig)
assert old_new<1e-5 and new_old<1e-5
logs.append('Outside hole masks differences: old-new=%.9f new-old=%.9f mm³' % (old_new,new_old))
App.closeDocument(original.Name);App.setActiveDocument(d.Name)
for o in d.Objects:
if o.ViewObject:o.ViewObject.Visibility=False
body.ViewObject.Visibility=True;f.ViewObject.Visibility=True
Gui.activeDocument().activeView().viewAxonometric();Gui.activeDocument().activeView().fitAll();Gui.updateGui()
Gui.activeDocument().activeView().saveImage(str(out/'转换盘_M4底孔预览.png'),1500,1100,'White')
d.saveAs(str(out/'转换盘_M4底孔.FCStd'));volume=f.Shape.Volume
Import.export([f],str(out/'转换盘_M4底孔.step'))
App.closeDocument(d.Name)
d=App.openDocument(str(out/'转换盘_M4底孔.FCStd'));d.recompute();f=d.getObject('InnerHoleThrough')
assert f.Shape.isValid();inspect(f.Shape);logs.append('FCStd reopened and recomputed: valid')
App.closeDocument(d.Name)
d=App.newDocument('STEPCheck');Import.insert(str(out/'转换盘_M4底孔.step'),d.Name);d.recompute()
shapes=[o.Shape for o in d.Objects if hasattr(o,'Shape') and not o.Shape.isNull()]
assert len(shapes)==1 and shapes[0].isValid(); inspect(shapes[0]);assert abs(shapes[0].Volume-volume)<0.001
logs.append('STEP reopened: valid, six Ø3.3 through bores, volume=%.8f, error=%.9f' % (shapes[0].Volume,abs(shapes[0].Volume-volume)))
App.closeDocument(d.Name)
(out/'validation.txt').write_text('\n'.join(logs)+'\n',encoding='utf-8')
(out/'DONE.txt').write_text('ok',encoding='utf-8')
except Exception:
(out/'ERROR.txt').write_text(traceback.format_exc()+'\n'+'\n'.join(logs),encoding='utf-8')
finally:
for d in list(App.listDocuments().values()):App.closeDocument(d.Name)
Gui.getMainWindow().close()

View File

@@ -0,0 +1,65 @@
from pathlib import Path
import os, time, traceback, math
import FreeCAD as A, FreeCADGui as G, Part, Import
from pivy import coin
R=Path(r'C:\projects\wheel'); O=R/'models'/'switch_2020_direct_profiles';O.mkdir(parents=True,exist_ok=True)
try:
d=A.openDocument(str(R/'models'/'switch_2020_raise10_concept'/'Assembly.FCStd'))
for n in ('Rail0','Rail1','MotorBaseCorner0','MotorBaseCorner1'):
o=d.getObject(n);p=o.Placement;p.Base.y-=10;o.Placement=p
for n in ('MotorShim0','MotorShim1'):
if d.getObject(n):d.removeObject(n)
d.Label='Switch crossbeam - raised shaft, direct profile contact'
d.recompute()
checks=[]
def compare(a,b):
aa=d.getObject(a).Shape;bb=d.getObject(b).Shape
volume=aa.common(bb).Volume if aa.BoundBox.intersect(bb.BoundBox) else 0
distance=aa.distToShape(bb)[0]
checks.append('%s / %s: overlap=%.9f distance=%.6f'%(a,b,volume,distance))
return volume,distance
for i in range(2):
compare('Rail%d'%i,'NewRail%d'%i)
compare('Rail%d'%i,'MotorBaseCorner%d'%i)
compare('NewRail%d'%i,'MotorBaseCorner%d'%i)
for i in range(4):
q=d.getObject('Corner%d'%i); rail=d.getObject('Rail%d'%(i//2));bb=q.Shape.BoundBox
compare(q.Name,rail.Name); compare(q.Name,'Motor')
centers=[]
for face in q.Shape.Faces:
s=face.Surface
if hasattr(s,'Radius') and abs(s.Radius-2.75)<1e-5 and hasattr(s,'Axis') and abs(abs(s.Axis.x)-1)<1e-5:
centers.append((s.Center.y,s.Center.z))
checks.append('%s side-hole cylinders %s bbox=%s rail_y=%s..%s'%(q.Name,str(centers),str(bb),rail.Shape.BoundBox.YMin,rail.Shape.BoundBox.YMax))
if not centers:raise Exception('Missing profile side hole in '+q.Name)
for y,z in centers:
if not (rail.Shape.BoundBox.YMin < y < rail.Shape.BoundBox.YMax and abs(z-10)<1e-5):raise Exception('Profile side hole outside rail slot: '+q.Name)
checks.append('%s hole y=%.5f z=%.5f near_end=%.5f'%(q.Name,y,z,min(y-rail.Shape.BoundBox.YMin,rail.Shape.BoundBox.YMax-y)))
for name in ('Rail0','Rail1','MotorBaseCorner0','MotorBaseCorner1'):
x=d.getObject(name)
for o in d.Objects:
if o==x or not hasattr(o,'Shape') or o.Shape.isNull() or not o.ViewObject.Visibility:continue
if o.Name in ('Rail0','Rail1','MotorBaseCorner0','MotorBaseCorner1'):continue
if x.Shape.BoundBox.intersect(o.Shape.BoundBox):
vol=x.Shape.common(o.Shape).Volume
if vol>.001:checks.append('POSITIVE %s / %s volume=%.8f'%(name,o.Name,vol))
(O/'检查.txt').write_text('\n'.join(checks)+'\n',encoding='utf8')
d.saveAs(str(O/'Assembly.FCStd'))
Import.export([o for o in d.Objects if hasattr(o,'Shape') and not o.Shape.isNull() and o.ViewObject.Visibility],str(O/'Assembly.step'))
G.activateWorkbench('PartWorkbench');view=G.activeDocument().activeView()
def snap(name,mode):
if mode=='top':view.viewTop()
else:view.viewAxonometric();G.updateGui();view.getCameraNode().orientation.setValue(coin.SbRotation(.880476,.115917,-.279848,-.364705))
G.updateGui();time.sleep(.3);view.fitAll();G.updateGui();time.sleep(.4);view.saveImage(str(O/name),1800,1250,'White')
snap('assembly.png','axon')
vis={o.Name:o.ViewObject.Visibility for o in d.Objects if hasattr(o,'Shape')}
show=set(['Motor','Rail0','Rail1','NewRail0','NewRail1','MotorBaseCorner0','MotorBaseCorner1','Corner0','Corner1','Corner2','Corner3','OpenPillarBracket','PCB','Holder','Magnet'])
for o in d.Objects:
if hasattr(o,'Shape'):o.ViewObject.Visibility=o.Name in show
snap('motor_detail.png','axon');snap('motor_top.png','top')
for o in d.Objects:
if hasattr(o,'Shape'):o.ViewObject.Visibility=vis.get(o.Name,True)
d.recompute();d.saveAs(str(O/'Assembly.FCStd'));A.closeDocument(d.Name)
(O/'DONE.txt').write_text('ok',encoding='utf8');os._exit(0)
except Exception:
(O/'ERROR.txt').write_text(traceback.format_exc(),encoding='utf8');os._exit(1)

View File

@@ -0,0 +1,147 @@
"""Concept assembly only; run with FreeCAD GUI using rebuild_encoder_concept.FCMacro.
All placements except the explicitly supplied nominal dimensions are provisional.
Do not use this model as a fabrication drawing.
"""
import math
from pathlib import Path
import FreeCAD as App
import FreeCADGui as Gui
import Part
import Import
from pivy import coin
root = Path(__file__).resolve().parent.parent
out = root / 'models'
doc = App.newDocument('M9C12_EncoderConcept')
V = App.Vector
def cyl(r, h, x, y, z):
return Part.makeCylinder(r, h, V(x, y, z))
def item(name, label, shape, color, transparency=0):
obj = doc.addObject('PartDesign::Feature', name)
obj.Label = label
obj.Shape = shape
obj.ViewObject.ShapeColor = color
obj.ViewObject.LineColor = (0.13, 0.13, 0.13)
obj.ViewObject.Transparency = transparency
return obj
gray = (0.32, 0.35, 0.38)
blue = (0.20, 0.48, 0.80)
orange = (0.96, 0.55, 0.12)
green = (0.05, 0.55, 0.25)
silver = (0.78, 0.80, 0.82)
# The supplied existing model has rear at z=0 and front at z=30.
motor = item('Motor', '01 M9C12 original approximate model',
Part.read(str(out / 'MAD_M9C12.step')), gray)
motor.ViewObject.Transparency = 60
# User-supplied 27 mm rear interface is a flat mating face at z=0, not a
# claimed protruding shaft. Seat lies on the rear side (-Z).
# 2.2 mm screw head nests in an assumed 2.6 mm counterbore underneath the magnet.
seat = cyl(13.5, 7.5, 0, 0, -7.5)
seat = seat.cut(cyl(1.65, 7.52, 0, 0, -7.51))
seat = seat.cut(cyl(5.1, 3.0, 0, 0, -7.5)) # magnet pocket
seat = seat.cut(cyl(3.5, 2.6, 0, 0, -4.5)) # assumed M3 head pocket
holder = item('MagnetHolder', '02 Rotating 3D printed magnet holder - concept', seat, orange)
mag = item('Magnet', '03 Magnet 10 x 3 mm - magnetization to verify', cyl(5, 3.0, 0, 0, -7.5), silver)
# Board: 33 mm, 1.6 mm thickness assumed. Three holes as pictured.
boardshape = cyl(16.5, 1.6, 0, 0, -10.9)
board_holes = [(0, 13.49), (-11.71, -6.76), (11.71, -6.76)]
for x, y in board_holes:
boardshape = boardshape.cut(cyl(1.5, 1.62, x, y, -10.91))
board = item('EncoderPCB', '04 MT6826S PCB 33mm - thickness assumed', boardshape, green)
# 1 mm spacing from magnet face z=-7.5 to chip surface z=-8.5. Chip height and
# body size are visual approximations, not package dimensions.
chip = item('SensorChip', '05 Sensor face toward magnet - gap 1mm',
Part.makeBox(4, 5, 0.8, V(-2, -2.5, -9.3)), (0.10, 0.10, 0.11))
# Fixed stand: ring behind the PCB plus four slender uprights and 2 mm
# mounting pads attached to the assumed four 45-degree 50 mm PCD holes.
stand = cyl(20.75, 2.6, 0, 0, -13.5).cut(cyl(11, 2.62, 0, 0, -13.51))
for x, y in board_holes:
stand = stand.cut(cyl(1.65, 2.62, x, y, -13.51))
for i in range(4):
a = math.radians(45 + i * 90)
ux, uy = math.cos(a), math.sin(a)
px, py = 19 * ux, 19 * uy
x, y = 25 * ux, 25 * uy
post = cyl(1.6, 11.5, px, py, -13.5)
pad = cyl(4.8, 2, x, y, -2)
bridge = Part.makeBox(8, 3.2, 2, V(18, -1.6, -2))
bridge.rotate(V(0, 0, 0), V(0, 0, 1), 45 + i * 90)
stand = stand.fuse(post).fuse(pad).fuse(bridge)
stand = stand.cut(cyl(2.15, 2.02, x, y, -2.01))
stand = stand.removeSplitter()
support = item('EncoderBracket', '06 Fixed 3D printed bracket - PCD 50 concept', stand, blue)
# M3 head shown on rear at z=-4.5 and nominal shank through to front;
# actual length, head diameter, nut and hole fit are placeholders.
shaft = cyl(1.48, 45.9, 0, 0, -1.9)
head = cyl(3.0, 2.2, 0, 0, -4.1)
fastener = item('M3Bolt', '07 M3 fastener - head thickness 2.2mm', shaft.fuse(head), silver)
# A nut is a symbolic approximate hex in front of the original model.
points = [V(3.2*math.cos(math.radians(30+i*60)),
3.2*math.sin(math.radians(30+i*60)), 44) for i in range(6)]
points.append(points[0])
nut = Part.Face(Part.Wire(Part.makePolygon(points).Edges)).extrude(V(0,0,2.4))
nut = nut.cut(cyl(1.5, 2.42, 0, 0, 43.99))
nut_obj = item('M3Nut', '08 M3 nut - location indicative', nut, silver)
fasteners=[]
for i in range(4):
a = math.radians(45+i*90)
x,y=25*math.cos(a),25*math.sin(a)
# Nominal 8 mm length extends 2 mm through bracket and 6 into motor.
shank=cyl(1.95,8,x,y,-2)
screwhead=cyl(3.4,2.5,x,y,-4.5)
fasteners.append(item('M4Screw%d'%i, '09 M4x8 mounting screw %d - illustrative'%(i+1), shank.fuse(screwhead), silver))
doc.recompute()
for obj in [motor,holder,mag,board,chip,support,fastener,nut_obj]+fasteners:
if obj.Shape.isNull() or not obj.Shape.isValid():
raise RuntimeError('invalid: '+obj.Name)
view = Gui.activeDocument().activeView()
# Rear three-quarter view; adjust after visual inspection.
view.getCameraNode().orientation.setValue(coin.SbRotation(0.880476, 0.115917, -0.279848, -0.364705))
view.fitAll()
Gui.updateGui()
view.saveImage(str(out / 'M9C12_MT6826S_安装示意.png'), 1600, 1200, 'White')
# Diagram-only exploded view: move pieces outward along the axis, preserving
# their relative orientation. The saved assembly remains assembled.
for obj, dz in [(holder,-8), (mag,-15), (board,-25), (chip,-25), (support,-33)]:
obj.Placement = App.Placement(V(0, 0, dz), App.Rotation())
doc.recompute()
view.fitAll()
Gui.updateGui()
view.saveImage(str(out / 'M9C12_MT6826S_爆炸示意.png'), 1600, 1200, 'White')
for obj in [holder,mag,board,chip,support]:
obj.Placement = App.Placement()
doc.recompute()
view.fitAll()
Gui.updateGui()
# The principal conceptual parts should not physically overlap in this model;
# mere face contacts have zero common volume. Sensor fasteners are omitted.
parts = [motor,holder,mag,board,chip,support,fastener,nut_obj]+fasteners
warnings = []
for i, first in enumerate(parts):
for second in parts[i+1:]:
overlap = first.Shape.common(second.Shape).Volume
if overlap > 0.001:
warnings.append('%s / %s : %.4f mm3' % (first.Name, second.Name, overlap))
(out / 'M9C12_MT6826S_几何检查.txt').write_text(
'Concept only. Assumed dimensions are not production measurements.\n'
'Sensor face to magnet gap: 1.0 mm.\n'
+ ('Positive-volume intersections:\n' + '\n'.join(warnings) if warnings else
'No positive-volume intersections above 0.001 mm3 in simplified model.') + '\n',
encoding='utf-8')
doc.recompute()
doc.saveAs(str(out / 'M9C12_MT6826S_安装示意.FCStd'))
Import.export([motor,holder,mag,board,chip,support,fastener,nut_obj]+fasteners,
str(out / 'M9C12_MT6826S_安装示意.step'))
print('ENCODER_CONCEPT_OK')

View File

@@ -0,0 +1,146 @@
"""Create first-fit printing parts for M9C12 + MT6826S in FreeCAD GUI.
Dimension assumptions are documented in the associated user instructions.
Run using rebuild_encoder_print_parts.FCMacro. Does not change motor source.
"""
import math
from pathlib import Path
import FreeCAD as App
import FreeCADGui as Gui
import Part, Mesh, Import
from pivy import coin
root = Path(__file__).resolve().parent.parent
out = root / 'models' / 'encoder_print_v1'
out.mkdir(parents=True, exist_ok=True)
V = App.Vector
doc = App.newDocument('EncoderPrintV1')
def cyl(r, h, x=0, y=0, z=0):
return Part.makeCylinder(r, h, V(x,y,z))
def obj(name, label, shape, color):
o=doc.addObject('PartDesign::Feature', name); o.Label=label; o.Shape=shape
o.ViewObject.ShapeColor=color; o.ViewObject.LineColor=(0.1,0.1,0.1)
return o
def check(name, shape):
if shape.isNull() or not shape.isValid() or len(shape.Solids)!=1 or shape.Volume<0.01:
raise RuntimeError('Invalid or non-single solid '+name)
# rear motor face z=0; positive z is away from motor.
# Base and tower remain clear of three PCB screw heads when rotating.
holder = cyl(13.5,3).fuse(cyl(10,4.5,z=3)).removeSplitter()
holder = holder.cut(cyl(1.65,7.6,z=-0.05))
# Screw head pocket from z=1.9 to 4.5; nominal M3 head 6 mm assumed.
holder = holder.cut(cyl(3.6,2.6,z=1.9))
# Magnet pocket: 10.3 mm diameter nominal, 3 mm deep.
holder = holder.cut(cyl(5.15,3.05,z=4.5))
# An edge notch allows gentle magnet removal. Never pry on magnet face.
notch=Part.makeBox(6,2.5,2.4,V(4.5,-1.25,5.1))
holder=holder.cut(notch).removeSplitter()
check('holder',holder)
# Gap at no shim 0.5 mm. Magnet face z=7.5, chip face z=8.0;
# chip is assumed 0.8 mm high, so board near face z=8.8 and far face 10.4.
# Add equal 0.5 mm under feet for initial nominal 1 mm gap.
board_near=8.8
board_far=10.4
ring_z=board_far
ring_h=3.2
bracket=cyl(20.5,ring_h,z=ring_z).cut(cyl(16.8,ring_h+0.02,z=ring_z-0.01))
pcb_centres=[(0,13.49),(-11.71,-6.76),(11.71,-6.76)]
# Three local bosses/bridges; PCB rests only locally instead of an unbroken ring.
for x,y in pcb_centres:
boss=cyl(3.2,ring_h,x,y,ring_z)
a=math.degrees(math.atan2(y,x))
bridge=Part.makeBox(6.0,4.0,ring_h,V(13,-2,ring_z))
bridge.rotate(V(0,0,0),V(0,0,1),a)
bracket=bracket.fuse(boss).fuse(bridge)
for i in range(4):
angle=45+i*90
a=math.radians(angle); ux,uy=math.cos(a),math.sin(a)
# Post nearest face at r=16.8; PCB nominal radius 16.5.
# M4 head has 0.7 mm radial clearance if its diameter is 7 mm.
post=cyl(2.0,ring_z-2,18.8*ux,18.8*uy,2)
foot=cyl(4.5,2,25*ux,25*uy,0)
bridge=Part.makeBox(10,4,2,V(18,-2,0)); bridge.rotate(V(0,0,0),V(0,0,1),angle)
bracket=bracket.fuse(post).fuse(foot).fuse(bridge)
bracket=bracket.cut(cyl(2.2,2.05,25*ux,25*uy,-0.02))
# Ø1.8 pilot bores for M2 self-tapping screws, through 3.2 mm support thickness.
for x,y in pcb_centres: bracket=bracket.cut(cyl(0.9,ring_h+0.04,x,y,ring_z-0.02))
bracket=bracket.removeSplitter()
check('bracket',bracket)
shim05=cyl(4.5,.5).cut(cyl(2.2,.52,z=-.01))
shim10=cyl(4.5,1).cut(cyl(2.2,1.02,z=-.01))
check('shim05',shim05); check('shim10',shim10)
# Export all printable parts at z=0 in an independent print-parts document.
parts=App.newDocument('EncoderPartsPrint')
def pobj(name,label,shape,color):
o=parts.addObject('PartDesign::Feature',name);o.Label=label;o.Shape=shape
o.ViewObject.ShapeColor=color;return o
ph=pobj('MagnetHolder','Magnet holder - print motor face down',holder,(0.96,.55,.12))
print_bracket=bracket.copy();print_bracket.translate(V(0,0,-(ring_z+ring_h)))
print_bracket.rotate(V(0,0,0),V(1,0,0),180)
pb=pobj('EncoderBracket','Bracket - print PCB ring face down',print_bracket,(.2,.48,.8))
s05=pobj('Shim05','M4 shim 0.5mm - print 4',shim05,(.7,.7,.7))
s10=pobj('Shim10','M4 shim 1.0mm - print 4 optional',shim10,(.7,.7,.7))
parts.recompute()
for o in [ph,pb,s05,s10]:
check(o.Name,o.Shape)
Mesh.export([o], str(out/(o.Name+'.stl')))
Import.export([o], str(out/(o.Name+'.step')))
mesh_results=[]
for name in ['MagnetHolder','EncoderBracket','Shim05','Shim10']:
mesh=Mesh.Mesh(str(out/(name+'.stl')))
solid=mesh.isSolid(); bounds=mesh.BoundBox
mesh_results.append('%s solid=%s facets=%d volume=%.5f ZMin=%.5f ZMax=%.5f' %
(name,solid,mesh.CountFacets,mesh.Volume,bounds.ZMin,bounds.ZMax))
if not solid or mesh.Volume <= 0 or abs(bounds.ZMin)>0.001:
raise RuntimeError('STL verification failed: '+mesh_results[-1])
out.joinpath('mesh_check.txt').write_text('\n'.join(mesh_results)+'\n',encoding='utf-8')
parts.saveAs(str(out/'EncoderParts_Print.FCStd'))
# Assembly with nominal 0.5 mm shim under all four feet (1 mm sensor gap).
App.setActiveDocument(doc.Name)
motor=Part.read(str(root/'models'/'MAD_M9C12.step'))
motor.rotate(V(0,0,0),V(1,0,0),180)
mo=obj('Motor','Original approximate M9C12',motor,(.34,.36,.38));mo.ViewObject.Transparency=70
ho=obj('Holder','Print magnet holder',holder,(.96,.55,.12))
mag=cyl(5,3,z=4.5);mg=obj('Magnet','10 x 3 mm diametric magnet',mag,(.78,.78,.8))
br=bracket.copy();br.translate(V(0,0,.5));bo=obj('Bracket','Print bracket with 0.5mm shims',br,(.2,.48,.8))
pcb=cyl(16.5,1.6,z=board_near+.5)
for x,y in pcb_centres: pcb=pcb.cut(cyl(1.5,1.62,x,y,board_near+.49))
pc=obj('PCB','PCB approximate',pcb,(.06,.6,.22))
chip=Part.makeBox(4,5,.8,V(-2,-2.5,board_near+.5-.8))
ch=obj('Chip','Chip approximate',chip,(.1,.1,.1))
for i in range(4):
a=math.radians(45+i*90); x,y=25*math.cos(a),25*math.sin(a)
so=obj('Shim%d'%i,'0.5mm shim %d'%(i+1),cyl(4.5,.5,x,y,0).cut(cyl(2.2,.52,x,y,-.01)),(.8,.8,.8))
# Inspect interferences in the simplified assembly. Include nominal M2
# screw heads 4.0 mm dia, 1.5 mm high on the motor-facing PCB side.
m2heads=[]
for x,y in pcb_centres:
head=cyl(2,1.5,x,y,board_near+.5-1.5)
m2heads.append(head)
obj('PCBHead%d'%len(m2heads),'M2 head envelope 4x1.5 assumed',head,(.8,.8,.8))
checks=[('magnet_holder',mag,holder),('holder_bracket',holder,br),('magnet_bracket',mag,br),('chip_magnet',chip,mag),('board_bracket',pcb,br)]
checks += [('holder_M2head_%d'%i,holder,h) for i,h in enumerate(m2heads,1)]
results=[]
for name,a,b in checks:
vol=a.common(b).Volume
results.append('%s %.6f mm3'%(name,vol))
if vol>0.001: raise RuntimeError('Interference '+results[-1])
doc.recompute()
for o in [mo,ho,mg,bo,pc,ch]:
if o.Shape.isNull() or not o.Shape.isValid(): raise RuntimeError('Invalid assembly '+o.Name)
doc.saveAs(str(out/'EncoderAssembly_Fit.FCStd'))
view=Gui.activeDocument().activeView()
view.getCameraNode().orientation.setValue(coin.SbRotation(0.880476, 0.115917, -0.279848, -0.364705))
view.fitAll();Gui.updateGui();view.saveImage(str(out/'assembly.png'),1600,1200,'White')
out.joinpath('geometry_check.txt').write_text('\n'.join(results)+'\nNominal chip-face-to-magnet-face gap: 1 mm\n',encoding='utf-8')
out.joinpath('BUILD_OK.txt').write_text('success\n',encoding='utf-8')
print('PRINT_PARTS_OK')

View File

@@ -0,0 +1,52 @@
from pathlib import Path
import os,time,traceback,json
import FreeCAD as A, FreeCADGui as G, Part, Mesh, Import
V=A.Vector
R=Path(r'C:\projects\wheel'); O=R/'models'/'fan120_integer_v2'; O.mkdir(parents=True,exist_ok=True)
def cy(r,h,x,y,z): return Part.makeCylinder(r,h,V(x,y,z))
def slot(x,y,z,r,travel,h): return Part.makeBox(travel,2*r,h,V(x-travel/2,y-r,z)).fuse(cy(r,h,x-travel/2,y,z)).fuse(cy(r,h,x+travel/2,y,z))
def feature(d,name,shape,color):
o=d.addObject('PartDesign::Feature',name);o.Shape=shape;o.Label=name;o.ViewObject.ShapeColor=color;return o
try:
d=A.openDocument(str(R/'models'/'integer_profiles'/'Assembly.FCStd'));d.recompute()
source=[o for o in d.Objects if hasattr(o,'Shape') and not o.Shape.isNull() and o.ViewObject.Visibility]
centers=[(d.getObject('Rail%d'%i).Shape.BoundBox.XMin+d.getObject('Rail%d'%i).Shape.BoundBox.XMax)/2 for i in range(2)]
zrail=20.; zframe=42.
frame=Part.makeBox(120,120,4,V(-60,-60,zframe)).cut(Part.makeBox(96,96,4.04,V(-48,-48,zframe-.02)))
for x in centers:
for y in (-25,25):frame=frame.fuse(Part.makeBox(24,18,zframe+4-zrail,V(x-12,y-9,zrail)))
frame=frame.removeSplitter()
for x in (-52.5,52.5):
for y in (-52.5,52.5):frame=frame.cut(cy(2.25,4.04,x,y,zframe-.02))
for x in centers:
for y in (-25,25):
frame=frame.cut(slot(x,y,zrail-.02,2.75,10,zframe+4-zrail+.04))
frame=frame.cut(slot(x,y,zframe-1.5,4.9,10,5.52))
frame=frame.removeSplitter()
assert frame.isValid() and len(frame.Solids)==1
fan=Part.makeBox(120,120,25,V(-60,-60,zframe+4)).cut(cy(56,25.04,0,0,zframe+3.98))
for x in (-52.5,52.5):
for y in (-52.5,52.5):fan=fan.cut(cy(2.2,25.04,x,y,zframe+3.98))
fan=fan.removeSplitter()
result=[]
for o in source:
for name,sh in [('frame',frame),('fan',fan)]:
if sh.BoundBox.intersect(o.Shape.BoundBox):
v=sh.common(o.Shape).Volume
if v>1e-4:result.append([name,o.Name,v])
(O/'hits.json').write_text(json.dumps(result,indent=2),encoding='utf8')
if result:raise RuntimeError('Intersecting assemblies '+str(result))
for i in range(2):
rail=d.getObject('Rail%d'%i).Shape
assert frame.distToShape(rail)[0]<1e-6 and frame.common(rail).Volume<1e-5
mount=feature(d,'FanSupportV2',frame,(0.95,.55,.08)); ff=feature(d,'FanEnvelopeV2',fan,(.2,.2,.2));ff.ViewObject.Transparency=55
d.recompute();d.saveAs(str(O/'Assembly.FCStd'))
G.activeDocument().activeView().viewAxonometric();G.updateGui();G.activeDocument().activeView().fitAll();time.sleep(.5);G.updateGui();G.activeDocument().activeView().saveImage(str(O/'assembly.png'),1600,1200,'White')
p=frame.copy();p.translate(V(0,0,-(zframe+4)));p.rotate(V(),V(1,0,0),180)
pd=A.newDocument('Fan120Print');po=feature(pd,'FanMount',Part.makeCompound([p]),(.15,.5,.86));pd.recompute();Mesh.export([po],str(O/'Fan120Mount.stl'));Import.export([po],str(O/'Fan120Mount.step'));pd.saveAs(str(O/'Fan120Mount.FCStd'))
mesh=Mesh.Mesh(str(O/'Fan120Mount.stl'));assert mesh.isSolid() and abs(mesh.BoundBox.ZMin)<1e-3
data={'rail_centres_x':centers,'leg_y':[ -25,25],'rail_top_z':zrail,'ring_bottom_z':zframe,'fan_bottom_z':zframe+4,'leg_height':zframe+4-zrail,'frame_volume':frame.Volume,'mesh_solid':mesh.isSolid(),'stl_z_min':mesh.BoundBox.ZMin,'static_hits':result,'fan_envelope_mm':[120,120,25],'not_modeled':'slot hardware, fan hub, cable, user physical components'}
(O/'validation.json').write_text(json.dumps(data,indent=2),encoding='utf8');(O/'DONE.txt').write_text('ok',encoding='utf8')
A.closeDocument(pd.Name);A.closeDocument(d.Name);os._exit(0)
except Exception:
(O/'ERROR.txt').write_text(traceback.format_exc(),encoding='utf8');os._exit(1)

View File

@@ -0,0 +1,98 @@
"""Build nominal 120 mm fan support; FreeCAD GUI macro entrypoint."""
from pathlib import Path
import math, time, traceback
import FreeCAD as A, FreeCADGui as G, Part, Mesh, Import
from pivy import coin
R=Path(__file__).resolve().parent.parent
O=R/'models'/'fan120_2020_mount_v1'; O.mkdir(parents=True,exist_ok=True)
V=A.Vector
def add(d,n,label,s,c,t=0):
o=d.addObject('PartDesign::Feature',n); o.Label=label; o.Shape=s
o.ViewObject.ShapeColor=c;o.ViewObject.LineColor=(.12,.12,.12);o.ViewObject.Transparency=t
return o
def cyl(r,h,x,y,z):return Part.makeCylinder(r,h,V(x,y,z))
def slot(x,y,z,r,travel,h):
s=Part.makeBox(travel,2*r,h,V(x-travel/2,y-r,z))
return s.fuse(cyl(r,h,x-travel/2,y,z)).fuse(cyl(r,h,x+travel/2,y,z))
def chk(n,s):
if s.isNull() or not s.isValid() or len(s.Solids)!=1 or s.Volume<=0:raise RuntimeError(n+' invalid')
try:
src=A.openDocument(str(R/'models'/'pcd50_open_pillar_v1'/'Assembly.FCStd'))
names=[o.Name for o in src.Objects if hasattr(o,'Shape')]
sh={n:src.getObject(n).Shape.copy() for n in names}
bounds=[sh['Rail%d'%i].BoundBox for i in range(2)]
centers=sorted([(b.XMin+b.XMax)/2 for b in bounds]); A.closeDocument(src.Name)
if abs(centers[0]+centers[1])>.01:raise RuntimeError('rail not symmetric')
zrail=max(b.ZMax for b in bounds); zframe=zrail+10
# Rectangular continuous ring; four supports rest on rail upper faces.
frame=Part.makeBox(120,120,4,V(-60,-60,zframe)).cut(Part.makeBox(96,96,4.04,V(-48,-48,zframe-.02)))
for x in centers:
for y in (-30,30):frame=frame.fuse(Part.makeBox(24,18,14,V(x-12,y-9,zrail)))
frame=frame.removeSplitter()
# Four 4.5 mm through holes for 105 mm standard fan pattern.
for x in (-52.5,52.5):
for y in (-52.5,52.5):frame=frame.cut(cyl(2.25,4.04,x,y,zframe-.02))
# M5 rail screws: x slots provide +/-5 mm adjustment. Counterbores seat heads
# below fan mating face and give access before installing fan.
for x in centers:
for y in (-30,30):
frame=frame.cut(slot(x,y,zrail-.02,2.75,10,14.04))
frame=frame.cut(slot(x,y,zframe-1.5,4.9,10,5.52))
frame=frame.removeSplitter();chk('frame',frame)
# Print fan face down; support legs stand upward with no scaffolding.
printed=frame.copy();printed.translate(V(0,0,-(zframe+4)));printed.rotate(V(),V(1,0,0),180)
pd=A.newDocument('Fan120MountPrint'); po=add(pd,'FanMount','120mm fan support single piece print orientation',printed,(.15,.5,.86));pd.recompute()
Mesh.export([po],str(O/'Fan120Mount.stl'));Import.export([po],str(O/'Fan120Mount.step'))
mesh=Mesh.Mesh(str(O/'Fan120Mount.stl'))
if not mesh.isSolid() or abs(mesh.BoundBox.ZMin)>.001:raise RuntimeError('non-solid STL or off bed')
pd.saveAs(str(O/'Fan120Mount.FCStd'));A.closeDocument(pd.Name)
d=A.newDocument('Fan120Assembly');objects={}
colors={'Motor':(.35,.36,.38),'Holder':(.97,.53,.1),'Magnet':(.8,.8,.8),'PCB':(.03,.65,.19),'Chip':(.1,.1,.1),'OpenPillarBracket':(.12,.43,.88)}
for n in names:
c=colors.get(n,(.82,.49,.15) if n.startswith('Corner') else (.5,.52,.56) if n.startswith('Rail') else (.8,.8,.8))
objects[n]=add(d,n,n,sh[n],c,55 if n=='Motor' else 0)
support=add(d,'FanSupport','PRINT: 120 mm fan mount',frame,(.96,.57,.12))
fan=Part.makeBox(120,120,25,V(-60,-60,zframe+4)).cut(cyl(56,25.04,0,0,zframe+3.98))
for x in (-52.5,52.5):
for y in (-52.5,52.5):fan=fan.cut(cyl(2.2,25.04,x,y,zframe+3.98))
fan=fan.removeSplitter();fo=add(d,'FanEnvelope','120 x 120 x 25 mm FAN envelope, schematic',fan,(.2,.2,.2),45)
# Fan hub/body indicative only (fan-dependent; not included in collision test).
hub=cyl(18,8,0,0,zframe+4+8);add(d,'FanHub','Fan hub schematic only',hub,(.25,.25,.25),35)
d.recompute();hits=[]
for n,s in sh.items():
if frame.BoundBox.intersect(s.BoundBox):
v=frame.common(s).Volume
if v>.001:hits.append('support / %s = %.5f mm3'%(n,v))
if fan.BoundBox.intersect(s.BoundBox):
v=fan.common(s).Volume
if v>.001:hits.append('fan envelope / %s = %.5f mm3'%(n,v))
if hits:raise RuntimeError('Collisions: '+', '.join(hits))
text=['Frame solid=%d volume=%.3f mm3 mesh watertight=%s triangles=%d'%(len(frame.Solids),frame.Volume,mesh.isSolid(),mesh.CountFacets),
'Rail centers X='+str(centers)+' rail top Z=%.3f'%zrail,
'Fan envelope 120x120x25 mm, fan holes 105x105 mm, diameter 4.5 mm.',
'Fan lower face Z=%.1f mm; bracket top Z=%.1f mm; 10 mm gap under cross ring.'%(zframe+4,zframe+4),
'Fan-frame basic opening 96x96 mm with local supports; M5 extrusion slots at y +/-30 mm, transverse travel 10 mm.',
'Nominal clearance from encoder screw heads to fan bottom is %.2f mm.'%((zframe+4)-max(sh[n].BoundBox.ZMax for n in sh if n.startswith('Head50_'))),
'Positive-volume collisions of frame/fan envelope against previous modeled objects:']+(hits or ['none'])+[
'Fan is only a simplified envelope, no real fan structure, grille, leads or cable clearance included.',
'Motor housing is simplified; airflow and real installation are not validated.']
(O/'geometry_check.txt').write_text('\n'.join(text)+'\n',encoding='utf8')
view=G.activeDocument().activeView();G.activateWorkbench('PartWorkbench')
def render(mode,path):
if mode=='top':view.viewTop()
else:
view.viewAxonometric();G.updateGui();view.getCameraNode().orientation.setValue(coin.SbRotation(.880476,.115917,-.279848,-.364705))
G.updateGui();time.sleep(.7);view.fitAll();G.updateGui();time.sleep(.7);view.saveImage(str(O/path),1600,1200,'White')
render('axon','assembly.png');render('top','rear.png')
fo.ViewObject.Visibility=False;d.getObject('FanHub').ViewObject.Visibility=False;render('axon','without_fan.png')
for obj in d.Objects:
if hasattr(obj,'Shape'):obj.ViewObject.Visibility=(obj.Name=='FanSupport')
render('axon','support.png')
for obj in d.Objects:
if hasattr(obj,'Shape'):obj.ViewObject.Visibility=True
render('axon','assembly.png');d.recompute();d.saveAs(str(O/'Assembly.FCStd'))
Import.export([o for o in d.Objects if hasattr(o,'Shape')],str(O/'Assembly.step'))
(O/'DONE.txt').write_text('ok\n',encoding='utf8')
except Exception:
(O/'ERROR.txt').write_text(traceback.format_exc(),encoding='utf8');raise

View File

@@ -0,0 +1,103 @@
from pathlib import Path
import os, math, json, time, traceback
import FreeCAD as A, FreeCADGui as G, Part, Import
V = A.Vector
root = Path(r'C:\projects\wheel')
out = root / 'models' / 'fan_direct_145'
out.mkdir(parents=True, exist_ok=True)
try:
d = A.openDocument(str(root / 'models' / 'integer_profiles' / 'Assembly.FCStd'))
d.recompute()
rails = [d.getObject('Rail0'), d.getObject('Rail1')]
xs = [(o.Shape.BoundBox.XMin + o.Shape.BoundBox.XMax) / 2 for o in rails]
spacing = xs[1] - xs[0]
theta = math.degrees(math.acos(spacing / (105.0 * math.sqrt(2.0)))) - 45.0
corners_before = [d.getObject('MotorBaseCorner%d' % i).Shape.copy() for i in range(2)]
old_rails = [o.Shape.copy() for o in rails]
for i, o in enumerate(rails):
o.Placement = A.Placement()
o.Shape = Part.makeBox(20, 145, 20, V(xs[i] - 10, -55, 0))
o.Label = '2020 motor profile - 145 mm'
d.recompute()
src = Part.makeBox(120, 120, 25, V(-60, -60, 20)).cut(Part.makeCylinder(56, 25.04, V(0,0,19.98)))
for x in (-52.5, 52.5):
for y in (-52.5, 52.5):
src = src.cut(Part.makeCylinder(2.2, 25.04, V(x,y,19.98)))
fan = src.removeSplitter()
fan.rotate(V(0,0,0), V(0,0,1), theta)
fan.translate(V(0,28,0))
assert fan.isValid() and len(fan.Solids) == 1
original = [o for o in d.Objects if hasattr(o,'Shape') and not o.Shape.isNull() and o.ViewObject.Visibility]
fan_hits = []
for o in original:
if fan.BoundBox.intersect(o.Shape.BoundBox):
volume = fan.common(o.Shape).Volume
if volume > 1e-4: fan_hits.append([o.Name, volume])
assert not fan_hits, 'fan collisions: ' + str(fan_hits)
extra_hits = []
for i, rail in enumerate(rails):
extra = rail.Shape.cut(old_rails[i])
for o in original:
if o == rail or o in rails: continue
if extra.BoundBox.intersect(o.Shape.BoundBox):
volume = extra.common(o.Shape).Volume
if volume > 1e-4: extra_hits.append([rail.Name, o.Name, volume])
assert old_rails[i].cut(rail.Shape).Volume < 1e-5
assert not extra_hits, 'rail extension collisions: ' + str(extra_hits)
for i in range(2):
assert corners_before[i].cut(d.getObject('MotorBaseCorner%d' % i).Shape).Volume < 1e-7
assert d.getObject('MotorBaseCorner%d' % i).Shape.cut(corners_before[i]).Volume < 1e-7
o = d.addObject('PartDesign::Feature', 'DirectFan')
o.Label = '120 mm fan envelope - rotated, two diagonal fixings; user fan assumed'
o.Shape = fan
o.ViewObject.ShapeColor = (.22,.24,.27)
o.ViewObject.Transparency = 18
alpha = math.radians(theta)
fasteners=[]
holes=[]
for sign in (-1,1):
px = sign * 52.5 * (math.cos(alpha) - math.sin(alpha))
py = 28 + sign * 52.5 * (math.cos(alpha) + math.sin(alpha))
idx=0 if sign<0 else 1
assert abs(px-xs[idx]) < 1e-6
assert -55 < py < 90
shaft=Part.makeCylinder(2,25,V(px,py,20))
head=Part.makeCylinder(3.5,3,V(px,py,45))
screw=shaft.fuse(head).removeSplitter()
assert screw.common(fan).Volume < 1e-5
f=d.addObject('PartDesign::Feature','FanBolt'+str(idx))
f.Shape=screw; f.Label='Illustrative M4 fastener, nut not modeled'; f.ViewObject.ShapeColor=(.65,.66,.68)
fasteners.append(f)
holes.append({'x':px,'y':py,'rail_center_x':xs[idx], 'distance_to_end_mm': min(py+55,90-py)})
d.recompute()
for obj in [o]+fasteners:
assert not obj.Shape.isNull() and obj.Shape.isValid()
d.Label='120 mm fan on 145 mm profiles - 2 diagonal screws, no bracket'
d.saveAs(str(out/'Assembly.FCStd'))
visible=[a for a in d.Objects if hasattr(a,'Shape') and not a.Shape.isNull() and a.ViewObject.Visibility]
Import.export(visible, str(out/'Assembly.step'))
view=G.activeDocument().activeView(); view.viewAxonometric(); G.updateGui();view.fitAll();time.sleep(.5);G.updateGui()
view.saveImage(str(out/'assembly.png'),1800,1300,'White')
vis={a.Name:a.ViewObject.Visibility for a in d.Objects if hasattr(a,'Shape')}
names={'Rail0','Rail1','NewRail0','NewRail1','MotorBaseCorner0','MotorBaseCorner1','DirectFan','FanBolt0','FanBolt1','Motor','OpenPillarBracket','Holder','Magnet','PCB','Chip'}
for a in d.Objects:
if hasattr(a,'Shape'):a.ViewObject.Visibility=a.Name in names
view.viewTop();G.updateGui();view.fitAll();time.sleep(.5);G.updateGui();view.saveImage(str(out/'fan_top.png'),1800,1300,'White')
view.viewAxonometric();G.updateGui();view.fitAll();time.sleep(.5);G.updateGui();view.saveImage(str(out/'fan_detail.png'),1800,1300,'White')
for a in d.Objects:
if hasattr(a,'Shape'):a.ViewObject.Visibility=vis.get(a.Name,False)
d.recompute();d.saveAs(str(out/'Assembly.FCStd'))
expected=[a.Name for a in visible]
A.closeDocument(d.Name)
d2=A.openDocument(str(out/'Assembly.FCStd'));d2.recompute()
assert all(d2.getObject(x) and d2.getObject(x).Shape.isValid() for x in ['Rail0','Rail1','DirectFan','FanBolt0','FanBolt1'])
assert all(abs(d2.getObject('Rail%d'%i).Shape.BoundBox.YLength-145)<1e-7 for i in (0,1))
st=Part.read(str(out/'Assembly.step'));assert st.isValid() and len(st.Solids)>0
data={'motor_2020_length_mm':145,'2020_longitudinal_limits_mm':[-55,90], 'fan_envelope_mm':[120,120,25], 'fan_mount_pitch_mm':105,'fan_center_y_mm':28,'motor_center_y_mm':10,'fan_offset_from_motor_mm':18,'rotation_deg':theta,'fastener_centers':holes,'fan_collisions':fan_hits,'extension_collisions':extra_hits,'original_corners_unchanged':True,'fcstd_reopened':True,'step_valid':True,'assumptions':'Fan solid frame and holes schematic; no blade, hub, wires, real T-slot or nut. Shaft of screw only above extrusion surface.'}
(out/'validation.json').write_text(json.dumps(data,ensure_ascii=False,indent=2),encoding='utf8')
A.closeDocument(d2.Name)
(out/'DONE.txt').write_text('ok',encoding='utf8')
os._exit(0)
except Exception:
(out/'ERROR.txt').write_text(traceback.format_exc(),encoding='utf8')
os._exit(1)

View File

@@ -0,0 +1,74 @@
from pathlib import Path
import os, math, json, time, traceback
import FreeCAD as A, FreeCADGui as G, Part, Import
V=A.Vector
root=Path(r'C:\projects\wheel');out=root/'models'/'fan_direct_motor_side_125';out.mkdir(parents=True,exist_ok=True)
try:
d=A.openDocument(str(root/'models'/'integer_profiles'/'Assembly.FCStd'));d.recompute()
rails=[d.getObject('Rail0'),d.getObject('Rail1')]; corners=[d.getObject('MotorBaseCorner0'),d.getObject('MotorBaseCorner1')]
xs=[(o.Shape.BoundBox.XMin+o.Shape.BoundBox.XMax)/2 for o in rails];spacing=xs[1]-xs[0]
theta=math.degrees(math.acos(spacing/(105*math.sqrt(2))))-45
oldrails=[o.Shape.copy() for o in rails]
for i,rail in enumerate(rails):
rail.Placement=A.Placement();rail.Shape=Part.makeBox(20,125,20,V(xs[i]-10,-55,0));rail.Label='2020 motor support 125mm'
for i,obj in enumerate(corners):
orig=obj.Shape.copy();orig.rotate(V(xs[i],0,10),V(0,1,0),180);obj.Placement=A.Placement();obj.Shape=orig;obj.Label='Motor side 2020-3030 connecting corner'
d.recompute()
visible=[o for o in d.Objects if hasattr(o,'Shape') and not o.Shape.isNull() and o.ViewObject.Visibility]
pairs=[];hits=[];skip={'Rail0','Rail1','MotorBaseCorner0','MotorBaseCorner1'}
for i,c in enumerate(corners):
for o in visible:
if o==c:continue
if c.Shape.BoundBox.intersect(o.Shape.BoundBox):
vol=c.Shape.common(o.Shape).Volume
if vol>1e-4:hits.append([c.Name,o.Name,vol])
if o.Name in ('Motor','Rail%d'%i,'NewRail%d'%i,'Corner%d'%(i*2),'Corner%d'%(i*2+1)):
pairs.append([c.Name,o.Name,c.Shape.distToShape(o.Shape)[0]])
assert c.Shape.distToShape(rails[i].Shape)[0]<1e-5
assert c.Shape.distToShape(d.getObject('NewRail%d'%i).Shape)[0]<1e-5
for i,rail in enumerate(rails):
extra=rail.Shape.cut(oldrails[i])
for o in visible:
if o in rails or o in corners:continue
if extra.BoundBox.intersect(o.Shape.BoundBox):
vol=extra.common(o.Shape).Volume
if vol>1e-4:hits.append(['extra_'+rail.Name,o.Name,vol])
fan=Part.makeBox(120,120,25,V(-60,-60,20)).cut(Part.makeCylinder(56,25.04,V(0,0,19.98)))
for x in (-52.5,52.5):
for y in (-52.5,52.5):fan=fan.cut(Part.makeCylinder(2.2,25.04,V(x,y,19.98)))
fan=fan.removeSplitter();fan.rotate(V(0,0,0),V(0,0,1),theta);fan.translate(V(0,7.5,0))
for o in visible:
if fan.BoundBox.intersect(o.Shape.BoundBox):
vol=fan.common(o.Shape).Volume
if vol>1e-4:hits.append(['DirectFan',o.Name,vol])
if hits: raise Exception('COLLISION '+str(hits))
ff=d.addObject('PartDesign::Feature','DirectFan');ff.Shape=fan;ff.Label='120mm fan assumed geometry, rotated';ff.ViewObject.ShapeColor=(.22,.24,.27);ff.ViewObject.Transparency=18
holes=[];alpha=math.radians(theta);bolts=[]
for sign in (-1,1):
px=sign*52.5*(math.cos(alpha)-math.sin(alpha));py=7.5+sign*52.5*(math.cos(alpha)+math.sin(alpha));idx=0 if sign<0 else 1
assert abs(px-xs[idx])<1e-6 and -55<py<70
screw=Part.makeCylinder(2,25,V(px,py,20)).fuse(Part.makeCylinder(3.5,3,V(px,py,45))).removeSplitter()
assert screw.common(fan).Volume<1e-5
for o in visible:
if screw.BoundBox.intersect(o.Shape.BoundBox):
vol=screw.common(o.Shape).Volume
if vol>1e-4:hits.append(['bolt',idx,o.Name,vol])
b=d.addObject('PartDesign::Feature','FanBolt'+str(idx));b.Shape=screw;b.Label='M4 fastener illustrative, T-nut not modelled';b.ViewObject.ShapeColor=(.65,.66,.68);bolts.append(b)
holes.append({'x':px,'y':py,'distance_to_rail_end':min(py+55,70-py)})
if hits:raise Exception('SCREW COLLISION '+str(hits))
d.recompute();d.Label='Motor side corners, direct fan, 125mm profiles'
d.saveAs(str(out/'Assembly.FCStd'));obj=[o for o in d.Objects if hasattr(o,'Shape') and not o.Shape.isNull() and o.ViewObject.Visibility];Import.export(obj,str(out/'Assembly.step'))
view=G.activeDocument().activeView();view.viewAxonometric();G.updateGui();view.fitAll();time.sleep(.5);G.updateGui();view.saveImage(str(out/'assembly.png'),1800,1300,'White')
vis={o.Name:o.ViewObject.Visibility for o in d.Objects if hasattr(o,'Shape')};show={'Rail0','Rail1','NewRail0','NewRail1','MotorBaseCorner0','MotorBaseCorner1','DirectFan','FanBolt0','FanBolt1','Motor','OpenPillarBracket','Holder','Magnet','PCB','Chip','Corner0','Corner1','Corner2','Corner3'}
for o in d.Objects:
if hasattr(o,'Shape'):o.ViewObject.Visibility=o.Name in show
view.viewTop();G.updateGui();view.fitAll();time.sleep(.5);G.updateGui();view.saveImage(str(out/'fan_top.png'),1800,1300,'White')
view.viewAxonometric();G.updateGui();view.fitAll();time.sleep(.5);G.updateGui();view.saveImage(str(out/'fan_detail.png'),1800,1300,'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 all(d.getObject(n).Shape.isValid() for n in ['Rail0','Rail1','DirectFan','MotorBaseCorner0','MotorBaseCorner1']);assert all(abs(d.getObject('Rail%d'%i).Shape.BoundBox.YLength-125)<1e-6 for i in (0,1));step=Part.read(str(out/'Assembly.step'));assert step.isValid() and len(step.Solids)>0
data={'rail_length':125,'rail_limits':[-55,70],'fan_center_y':7.5,'motor_center_y':10,'fan_rotation_degrees':theta,'holes':holes,'corner_contact_distance':pairs,'fan_screw_extension_collisions':hits,'note':'Simplified fan, motor geometry; T nut and T slot, actual screw insertion trajectory, tooling, wires and fan hub unmodelled.'}
(out/'validation.json').write_text(json.dumps(data,ensure_ascii=False,indent=2),encoding='utf-8');A.closeDocument(d.Name);(out/'DONE.txt').write_text('ok');os._exit(0)
except Exception:
(out/'ERROR.txt').write_text(traceback.format_exc(),encoding='utf-8');os._exit(1)

View File

@@ -0,0 +1,75 @@
from pathlib import Path
import os, math, json, time, traceback
import FreeCAD as A, FreeCADGui as G, Part, Import
V=A.Vector
root=Path(r'C:\projects\wheel');out=root/'models'/'fan_direct_motor_side_130';out.mkdir(parents=True,exist_ok=True)
try:
d=A.openDocument(str(root/'models'/'integer_profiles'/'Assembly.FCStd'));d.recompute()
rails=[d.getObject('Rail0'),d.getObject('Rail1')]; corners=[d.getObject('MotorBaseCorner0'),d.getObject('MotorBaseCorner1')]
xs=[(o.Shape.BoundBox.XMin+o.Shape.BoundBox.XMax)/2 for o in rails];spacing=xs[1]-xs[0]
theta=math.degrees(math.acos(spacing/(105*math.sqrt(2))))-45
oldrails=[o.Shape.copy() for o in rails]
for i,rail in enumerate(rails):
rail.Placement=A.Placement();rail.Shape=Part.makeBox(20,130,20,V(xs[i]-10,-55,0));rail.Label='2020 motor support 130mm'
for i,obj in enumerate(corners):
orig=obj.Shape.copy();orig.rotate(V(xs[i],0,10),V(0,1,0),180);obj.Placement=A.Placement();obj.Shape=orig;obj.Label='Motor side 2020-3030 connecting corner'
d.recompute()
visible=[o for o in d.Objects if hasattr(o,'Shape') and not o.Shape.isNull() and o.ViewObject.Visibility]
pairs=[];hits=[];skip={'Rail0','Rail1','MotorBaseCorner0','MotorBaseCorner1'}
for i,c in enumerate(corners):
for o in visible:
if o==c:continue
if c.Shape.BoundBox.intersect(o.Shape.BoundBox):
vol=c.Shape.common(o.Shape).Volume
if vol>1e-4:hits.append([c.Name,o.Name,vol])
if o.Name in ('Motor','Rail%d'%i,'NewRail%d'%i,'Corner%d'%(i*2),'Corner%d'%(i*2+1)):
pairs.append([c.Name,o.Name,c.Shape.distToShape(o.Shape)[0]])
assert c.Shape.distToShape(rails[i].Shape)[0]<1e-5
assert c.Shape.distToShape(d.getObject('NewRail%d'%i).Shape)[0]<1e-5
for i,rail in enumerate(rails):
extra=rail.Shape.cut(oldrails[i])
for o in visible:
if o in rails or o in corners:continue
if extra.BoundBox.intersect(o.Shape.BoundBox):
vol=extra.common(o.Shape).Volume
if vol>1e-4:hits.append(['extra_'+rail.Name,o.Name,vol])
fan=Part.makeBox(120,120,25,V(-60,-60,20)).cut(Part.makeCylinder(56,25.04,V(0,0,19.98)))
for x in (-52.5,52.5):
for y in (-52.5,52.5):fan=fan.cut(Part.makeCylinder(2.2,25.04,V(x,y,19.98)))
fan=fan.removeSplitter();fan.rotate(V(0,0,0),V(0,0,1),theta);fan.translate(V(0,10,0))
for o in visible:
if fan.BoundBox.intersect(o.Shape.BoundBox):
vol=fan.common(o.Shape).Volume
if vol>1e-4:hits.append(['DirectFan',o.Name,vol])
if hits: raise Exception('COLLISION '+str(hits))
ff=d.addObject('PartDesign::Feature','DirectFan');ff.Shape=fan;ff.Label='120mm fan assumed geometry, rotated';ff.ViewObject.ShapeColor=(.22,.24,.27);ff.ViewObject.Transparency=18
holes=[];alpha=math.radians(theta);bolts=[]
for sign in (-1,1):
px=sign*52.5*(math.cos(alpha)-math.sin(alpha));py=10+sign*52.5*(math.cos(alpha)+math.sin(alpha));idx=0 if sign<0 else 1
assert abs(px-xs[idx])<1e-6 and -55<py<75
screw=Part.makeCylinder(2,25,V(px,py,20)).fuse(Part.makeCylinder(3.5,3,V(px,py,45))).removeSplitter()
assert screw.common(fan).Volume<1e-5
for o in visible:
if screw.BoundBox.intersect(o.Shape.BoundBox):
vol=screw.common(o.Shape).Volume
if vol>1e-4:hits.append(['bolt',idx,o.Name,vol])
b=d.addObject('PartDesign::Feature','FanBolt'+str(idx));b.Shape=screw;b.Label='M4 fastener illustrative, T-nut not modelled';b.ViewObject.ShapeColor=(.65,.66,.68);bolts.append(b)
holes.append({'x':px,'y':py,'distance_to_rail_end':min(py+55,75-py)})
if hits:raise Exception('SCREW COLLISION '+str(hits))
d.recompute();d.Label='Motor side corners, direct fan, 130mm profiles'
d.saveAs(str(out/'Assembly.FCStd'));obj=[o for o in d.Objects if hasattr(o,'Shape') and not o.Shape.isNull() and o.ViewObject.Visibility];Import.export(obj,str(out/'Assembly.step'))
view=G.activeDocument().activeView();view.viewAxonometric();G.updateGui();view.fitAll();time.sleep(.5);G.updateGui();view.saveImage(str(out/'assembly.png'),1800,1300,'White')
vis={o.Name:o.ViewObject.Visibility for o in d.Objects if hasattr(o,'Shape')};show={'Rail0','Rail1','NewRail0','NewRail1','MotorBaseCorner0','MotorBaseCorner1','DirectFan','FanBolt0','FanBolt1','Motor','OpenPillarBracket','Holder','Magnet','PCB','Chip','Corner0','Corner1','Corner2','Corner3'}
for o in d.Objects:
if hasattr(o,'Shape'):o.ViewObject.Visibility=o.Name in show
view.viewTop();G.updateGui();view.fitAll();time.sleep(.5);G.updateGui();view.saveImage(str(out/'fan_top.png'),1800,1300,'White')
view.viewAxonometric();G.updateGui();view.fitAll();time.sleep(.5);G.updateGui();view.saveImage(str(out/'fan_detail.png'),1800,1300,'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 all(d.getObject(n).Shape.isValid() for n in ['Rail0','Rail1','DirectFan','MotorBaseCorner0','MotorBaseCorner1']);assert all(abs(d.getObject('Rail%d'%i).Shape.BoundBox.YLength-130)<1e-6 for i in (0,1));step=Part.read(str(out/'Assembly.step'));assert step.isValid() and len(step.Solids)>0
data={'rail_length':130,'rail_limits':[-55,75],'fan_center_y':10,'motor_center_y':10,'fan_rotation_degrees':theta,'holes':holes,'corner_contact_distance':pairs,'fan_screw_extension_collisions':hits,'note':'Simplified fan, motor geometry; T nut and T slot, actual screw insertion trajectory, tooling, wires and fan hub unmodelled.'}
(out/'validation.json').write_text(json.dumps(data,ensure_ascii=False,indent=2),encoding='utf-8');A.closeDocument(d.Name);(out/'DONE.txt').write_text('ok');os._exit(0)
except Exception:
(out/'ERROR.txt').write_text(traceback.format_exc(),encoding='utf-8');os._exit(1)

View File

@@ -0,0 +1,76 @@
from pathlib import Path
import os, math, json, time, traceback
import FreeCAD as A, FreeCADGui as G, Part, Import
V=A.Vector
root=Path(r'C:\projects\wheel');out=root/'models'/'fan_direct_motor_side_140';out.mkdir(parents=True,exist_ok=True)
try:
d=A.openDocument(str(root/'models'/'integer_profiles'/'Assembly.FCStd'));d.recompute()
rails=[d.getObject('Rail0'),d.getObject('Rail1')]; corners=[d.getObject('MotorBaseCorner0'),d.getObject('MotorBaseCorner1')]
xs=[(o.Shape.BoundBox.XMin+o.Shape.BoundBox.XMax)/2 for o in rails];spacing=xs[1]-xs[0]
theta=math.degrees(math.acos(spacing/(105*math.sqrt(2))))-45
oldrails=[o.Shape.copy() for o in rails]
for i,rail in enumerate(rails):
rail.Placement=A.Placement();rail.Shape=Part.makeBox(20,140,20,V(xs[i]-10,-55,0));rail.Label='2020 motor support 140mm'
for i,obj in enumerate(corners):
orig=obj.Shape.copy();orig.rotate(V(xs[i],0,10),V(0,1,0),180);obj.Placement=A.Placement();obj.Shape=orig;obj.Label='Motor side 2020-3030 connecting corner'
d.recompute()
visible=[o for o in d.Objects if hasattr(o,'Shape') and not o.Shape.isNull() and o.ViewObject.Visibility]
pairs=[];hits=[];skip={'Rail0','Rail1','MotorBaseCorner0','MotorBaseCorner1'}
for i,c in enumerate(corners):
for o in visible:
if o==c:continue
if c.Shape.BoundBox.intersect(o.Shape.BoundBox):
vol=c.Shape.common(o.Shape).Volume
if vol>1e-4:hits.append([c.Name,o.Name,vol])
if o.Name in ('Motor','Rail%d'%i,'NewRail%d'%i,'Corner%d'%(i*2),'Corner%d'%(i*2+1)):
pairs.append([c.Name,o.Name,c.Shape.distToShape(o.Shape)[0]])
assert c.Shape.distToShape(rails[i].Shape)[0]<1e-5
assert c.Shape.distToShape(d.getObject('NewRail%d'%i).Shape)[0]<1e-5
for i,rail in enumerate(rails):
extra=rail.Shape.cut(oldrails[i])
for o in visible:
if o in rails or o in corners:continue
if extra.BoundBox.intersect(o.Shape.BoundBox):
vol=extra.common(o.Shape).Volume
if vol>1e-4:hits.append(['extra_'+rail.Name,o.Name,vol])
fan=Part.makeBox(120,120,25,V(-60,-60,20)).cut(Part.makeCylinder(56,25.04,V(0,0,19.98)))
for x in (-52.5,52.5):
for y in (-52.5,52.5):fan=fan.cut(Part.makeCylinder(2.2,25.04,V(x,y,19.98)))
fan=fan.removeSplitter();fan.rotate(V(0,0,0),V(0,0,1),theta);fan.translate(V(0,10,0))
for o in visible:
if fan.BoundBox.intersect(o.Shape.BoundBox):
vol=fan.common(o.Shape).Volume
if vol>1e-4:hits.append(['DirectFan',o.Name,vol])
if hits: raise Exception('COLLISION '+str(hits))
ff=d.addObject('PartDesign::Feature','DirectFan');ff.Shape=fan;ff.Label='120mm fan assumed geometry, rotated';ff.ViewObject.ShapeColor=(.22,.24,.27);ff.ViewObject.Transparency=18
holes=[];alpha=math.radians(theta);bolts=[]
for sign in (-1,1):
px=sign*52.5*(math.cos(alpha)-math.sin(alpha));py=10+sign*52.5*(math.cos(alpha)+math.sin(alpha));idx=0 if sign<0 else 1
assert abs(px-xs[idx])<1e-6 and -55<py<85
screw=Part.makeCylinder(2,25,V(px,py,20)).fuse(Part.makeCylinder(3.5,3,V(px,py,45))).removeSplitter()
assert screw.common(fan).Volume<1e-5
for o in visible:
if screw.BoundBox.intersect(o.Shape.BoundBox):
vol=screw.common(o.Shape).Volume
if vol>1e-4:hits.append(['bolt',idx,o.Name,vol])
b=d.addObject('PartDesign::Feature','FanBolt'+str(idx));b.Shape=screw;b.Label='M4 fastener illustrative, T-nut not modelled';b.ViewObject.ShapeColor=(.65,.66,.68);bolts.append(b)
holes.append({'x':px,'y':py,'distance_to_rail_end':min(py+55,85-py)})
if hits:raise Exception('SCREW COLLISION '+str(hits))
d.recompute();d.Label='Motor side corners, direct fan, 140mm profiles'
d.saveAs(str(out/'Assembly.FCStd'));obj=[o for o in d.Objects if hasattr(o,'Shape') and not o.Shape.isNull() and o.ViewObject.Visibility];Import.export(obj,str(out/'Assembly.step'))
view=G.activeDocument().activeView();view.viewAxonometric();G.updateGui();view.fitAll();time.sleep(.5);G.updateGui();view.saveImage(str(out/'assembly.png'),1800,1300,'White')
vis={o.Name:o.ViewObject.Visibility for o in d.Objects if hasattr(o,'Shape')};show={'Rail0','Rail1','NewRail0','NewRail1','MotorBaseCorner0','MotorBaseCorner1','DirectFan','FanBolt0','FanBolt1','Motor','OpenPillarBracket','Holder','Magnet','PCB','Chip','Corner0','Corner1','Corner2','Corner3'}
for o in d.Objects:
if hasattr(o,'Shape'):o.ViewObject.Visibility=o.Name in show
view.viewTop();G.updateGui();view.fitAll();time.sleep(.5);G.updateGui();view.saveImage(str(out/'fan_top.png'),1800,1300,'White')
view.viewAxonometric();G.updateGui();view.fitAll();time.sleep(.5);G.updateGui();view.saveImage(str(out/'fan_detail.png'),1800,1300,'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 all(d.getObject(n).Shape.isValid() for n in ['Rail0','Rail1','DirectFan','MotorBaseCorner0','MotorBaseCorner1']);assert all(abs(d.getObject('Rail%d'%i).Shape.BoundBox.YLength-140)<1e-6 for i in (0,1));step=Part.read(str(out/'Assembly.step'));assert step.isValid() and len(step.Solids)>0
data={'rail_length':140,'rail_limits':[-55,85],'fan_center_y':10,'motor_center_y':10,'fan_rotation_degrees':theta,'holes':holes,'corner_contact_distance':pairs,'fan_screw_extension_collisions':hits,'note':'Simplified fan, motor geometry; T nut and T slot, actual screw insertion trajectory, tooling, wires and fan hub unmodelled.'}
(out/'validation.json').write_text(json.dumps(data,ensure_ascii=False,indent=2),encoding='utf-8');A.closeDocument(d.Name);(out/'DONE.txt').write_text('ok');os._exit(0)
except Exception:
(out/'ERROR.txt').write_text(traceback.format_exc(),encoding='utf-8');os._exit(1)

View File

@@ -0,0 +1,55 @@
from pathlib import Path
import os, time, json, traceback
import FreeCAD as A, FreeCADGui as G, Part, Import
R=Path(r'C:\projects\wheel')
O=R/'models'/'integer_profiles';O.mkdir(parents=True,exist_ok=True)
try:
d=A.openDocument(str(R/'models'/'switch_2020_crossbar132'/'Assembly.FCStd'))
d.Label='Wheel assembly - integer profile lengths'
check={}; baselines={}
for i in range(2):
o=d.getObject('Crossbeam%d'%i);b=o.Shape.BoundBox
old=o.Shape.copy()
cutbox=Part.makeBox(71, b.YLength, b.ZLength,A.Vector(-35.5,b.YMin,b.ZMin))
new=old.common(cutbox)
assert new.isValid() and len(new.Solids)==1 and abs(new.BoundBox.XLength-71)<1e-6
assert new.cut(old).Volume <1e-5
o.Shape=new;o.Label='3030 crossbeam %d - 71 mm'%i
baselines[o.Name]={'removed_mm3':old.cut(new).Volume,'added_mm3':new.cut(old).Volume}
d.recompute()
for n,dimensions in [('NewRail0',(30,30,255)),('NewRail1',(30,30,255)),('Crossbeam0',(71,30,30)),('Crossbeam1',(71,30,30)),('Rail0',(20,110,20)),('Rail1',(20,110,20)),('SwitchCross2020',(132,20,20))]:
o=d.getObject(n);b=o.Shape.BoundBox
dims=(b.XLength,b.YLength,b.ZLength)
assert all(abs(a-b)<1e-6 for a,b in zip(dims,dimensions)),(n,dims)
check[n]={'dimensions_xyz_mm':dims}
for i in range(2):
for j in range(2):
cross=d.getObject('Crossbeam%d'%i).Shape
rail=d.getObject('NewRail%d'%j).Shape
angle=d.getObject('BaseJoint%d%d'%(i,j)).Shape
pd=d.getObject('PurchasedAngle%d%d'%(i,j)).Shape
check['beam%d_rail%d'%(i,j)]={'distance_mm':cross.distToShape(rail)[0],'intersection_mm3':cross.common(rail).Volume}
check['beam%d_angle%d'%(i,j)]={'distance_mm':cross.distToShape(angle)[0],'intersection_mm3':cross.common(angle).Volume}
check['rail%d_angle%d%d'%(j,i,j)]={'distance_mm':rail.distToShape(angle)[0],'intersection_mm3':rail.common(angle).Volume}
check['beam%d_longangle%d'%(i,j)]={'distance_mm':cross.distToShape(pd)[0],'intersection_mm3':cross.common(pd).Volume}
assert cross.distToShape(angle)[0]<1e-6 and cross.common(angle).Volume<1e-6
assert rail.distToShape(angle)[0]<1e-6 and rail.common(angle).Volume<1e-6
assert cross.distToShape(pd)[0]<1e-6 and cross.common(pd).Volume<1e-6
assert abs(cross.distToShape(rail)[0]-.1200577)<1e-5
assert cross.common(rail).Volume<1e-6
d.recompute()
d.saveAs(str(O/'Assembly.FCStd'))
visible=[o for o in d.Objects if hasattr(o,'Shape') and not o.Shape.isNull() and o.ViewObject.Visibility]
Import.export(visible,str(O/'Assembly.step'))
v=G.activeDocument().activeView();v.viewAxonometric();G.updateGui();v.fitAll();time.sleep(.5);G.updateGui();v.saveImage(str(O/'assembly.png'),1800,1250,'White')
source=O/'Assembly.FCStd';A.closeDocument(d.Name)
dd=A.openDocument(str(source));dd.recompute()
for i in range(2): assert abs(dd.getObject('Crossbeam%d'%i).Shape.BoundBox.XLength-71)<1e-6
s=Part.read(str(O/'Assembly.step'));assert s.isValid() and len(s.Solids)>0
check['baselines']=baselines;check['fcstd_reopened']=True;check['step_valid']=True;check['step_solid_count']=len(s.Solids)
(O/'validation.json').write_text(json.dumps(check,indent=2),encoding='utf8')
A.closeDocument(dd.Name)
(O/'DONE.txt').write_text('OK',encoding='utf8')
os._exit(0)
except Exception:
(O/'ERROR.txt').write_text(traceback.format_exc(),encoding='utf8');os._exit(1)

View File

@@ -0,0 +1,88 @@
"""Editable KFL001 bearing mounting plate -- provisional concept only.
Run with rebuild_kfl001_plate_draft.FCMacro in a hidden FreeCAD instance.
Both assembly plates have the same geometry; flip the second when fitting.
The centre cut is an unverified placeholder and MUST NOT be used for CNC.
"""
from pathlib import Path
import os, time, traceback, json
import FreeCAD as A, FreeCADGui as G, Part, Import
R=Path(r'C:\projects\wheel')
O=R/'models'/'kfl001_plate_draft'
O.mkdir(parents=True, exist_ok=True)
try:
d=A.newDocument('KFL001_Plate_Draft')
d.Label='KFL001 8mm plate DRAFT - centre cut unverified'
s=d.addObject('Spreadsheet::Sheet','Parameters')
s.Label='Parameters - centre diameter UNVERIFIED'
rows=[('PlateWidth','76 mm','Overall width'),('PlateHeight','80 mm','Overall height'),
('Thickness','8 mm','Plate thickness'),('AxisFromBottom','51 mm','Axle height above lower edge'),
('CenterClearance','39 mm','TEMPORARY ONLY - verify real KFL back geometry'),
('BearingPitch','48 mm','Bolt centre spacing for KFL001'),
('BearingHole','7 mm','Bearing mount clearance diameter'),
('SlotPitch','47 mm','Centre spacing of lower mounting slots'),
('SlotBottom','9 mm','Mounting slot centre above plate lower edge'),
('SlotLength','9.5 mm','Horizontal slot overall length'),
('SlotWidth','5.5 mm','Horizontal slot width')]
for row,(name,val,note) in enumerate(rows,2):
s.set('A%d'%row,name);s.set('B%d'%row,val);s.set('C%d'%row,note);s.setAlias('B%d'%row,name)
def expr(o,p,v):o.setExpression(p,v)
plate=d.addObject('Part::Box','PlateBlank');plate.Label='76 x 80 x 8 plate blank'
expr(plate,'Length','Parameters.PlateWidth');expr(plate,'Width','Parameters.PlateHeight');expr(plate,'Height','Parameters.Thickness')
expr(plate,'Placement.Base.x','-Parameters.PlateWidth / 2')
def cyl(name,x,y,r):
o=d.addObject('Part::Cylinder',name);expr(o,'Radius',r);expr(o,'Height','Parameters.Thickness + 0.2 mm');expr(o,'Placement.Base.x',x);expr(o,'Placement.Base.y',y);o.Placement.Base.z=-.1;return o
tools=[]
tools.append(cyl('TEMP_CentreHole','0 mm','Parameters.AxisFromBottom','Parameters.CenterClearance / 2'))
for side in (-1,1):
sign='-' if side<0 else ''
tools.append(cyl('BearingHole_'+('L' if side<0 else 'R'),sign+'Parameters.BearingPitch / 2','Parameters.AxisFromBottom','Parameters.BearingHole / 2'))
cx=sign+'Parameters.SlotPitch / 2';tag='L' if side<0 else 'R'
tools.append(cyl('Slot'+tag+'End1','(%s) - (Parameters.SlotLength - Parameters.SlotWidth) / 2'%cx,'Parameters.SlotBottom','Parameters.SlotWidth / 2'))
tools.append(cyl('Slot'+tag+'End2','(%s) + (Parameters.SlotLength - Parameters.SlotWidth) / 2'%cx,'Parameters.SlotBottom','Parameters.SlotWidth / 2'))
b=d.addObject('Part::Box','Slot'+tag+'Middle')
expr(b,'Length','Parameters.SlotLength - Parameters.SlotWidth');expr(b,'Width','Parameters.SlotWidth');expr(b,'Height','Parameters.Thickness + 0.2 mm')
expr(b,'Placement.Base.x','(%s) - (Parameters.SlotLength - Parameters.SlotWidth) / 2'%cx)
expr(b,'Placement.Base.y','Parameters.SlotBottom - Parameters.SlotWidth / 2');b.Placement.Base.z=-.1
tools.append(b)
d.recompute()
cutters=d.addObject('Part::MultiFuse','Cutters');cutters.Shapes=tools;d.recompute()
final=d.addObject('Part::Cut','Plate_DRAFT');final.Label='DRAFT bearing plate - centre 39mm NOT verified';final.Base=plate;final.Tool=cutters
d.recompute()
assert not final.Shape.isNull() and final.Shape.isValid() and len(final.Shape.Solids)==1
for o in d.Objects:
if o.ViewObject:o.ViewObject.Visibility=False
final.ViewObject.Visibility=True;final.ViewObject.ShapeColor=(.71,.74,.77);final.ViewObject.LineColor=(.12,.12,.12)
G.activateWorkbench('PartWorkbench');view=G.activeDocument().activeView();view.viewAxonometric();G.updateGui();view.fitAll();time.sleep(.3);G.updateGui();view.saveImage(str(O/'plate_isometric.png'),1400,1100,'White')
view.viewTop();G.updateGui();view.fitAll();time.sleep(.3);G.updateGui();view.saveImage(str(O/'plate_top.png'),1400,1100,'White')
d.saveAs(str(O/'KFL001_plate_DRAFT.FCStd'));Import.export([final],str(O/'KFL001_plate_DRAFT.step'))
baseline=final.Shape.Volume
s.set('B6','38 mm');d.recompute()
assert final.Shape.isValid() and abs(final.Shape.Volume-baseline)>0.1
changed=final.Shape.Volume
s.set('B6','39 mm');d.recompute()
assert final.Shape.isValid() and abs(final.Shape.Volume-baseline)<1e-5
A.closeDocument(d.Name)
dd=A.openDocument(str(O/'KFL001_plate_DRAFT.FCStd'));dd.recompute();ff=dd.getObject('Plate_DRAFT')
assert ff.Shape.isValid() and len(ff.Shape.Solids)==1 and abs(ff.Shape.Volume-baseline)<1e-5
imported=Part.read(str(O/'KFL001_plate_DRAFT.step'))
assert imported.isValid() and len(imported.Solids)==1
# At the middle Z plane compare the STEP vs native shape by mutual differences.
stepdiff1=ff.Shape.cut(imported).Volume;stepdiff2=imported.cut(ff.Shape).Volume
assert stepdiff1<1e-4 and stepdiff2<1e-4
angles=[(3.5,24,51),(3.5,-24,51),(19.5,0,51)]
cylinders=[]
for f in imported.Faces:
surf=f.Surface
if hasattr(surf,'Radius') and hasattr(surf,'Center'):
cylinders.append({'radius':round(float(surf.Radius),5),'x':round(float(surf.Center.x),5),'y':round(float(surf.Center.y),5),'z_min':round(f.BoundBox.ZMin,5),'z_max':round(f.BoundBox.ZMax,5)})
for r,x,y in angles:
assert any(abs(c['radius']-r)<1e-4 and abs(c['x']-x)<1e-4 and abs(c['y']-y)<1e-4 and c['z_min']<=.001 and c['z_max']>=7.999 for c in cylinders), (r,x,y,cylinders)
info={'fcstd_valid':True,'step_valid':True,'solid_count':len(imported.Solids),'volume':baseline,'diameter_parameter_test_38':changed,'diameter_test_restored':True,'step_old_minus_new':stepdiff1,'step_new_minus_old':stepdiff2,'dimensions_mm':{'width':76,'height':80,'thickness':8,'axis_from_bottom':51,'center_diameter_provisional':39,'bearing_pitch':48,'bearing_hole':7,'slot_pitch':47,'slot_bottom':9,'slot_length':9.5,'slot_width':5.5},'cylinder_faces':cylinders,'nominal_m5_slot_range_mm':4.5,'quantity_identical_plates':2,'center_UNVERIFIED':True}
(O/'validation.json').write_text(json.dumps(info,ensure_ascii=False,indent=2),encoding='utf-8')
A.closeDocument(dd.Name)
(O/'DONE.txt').write_text('ok',encoding='utf-8')
os._exit(0)
except Exception:
(O/'ERROR.txt').write_text(traceback.format_exc(),encoding='utf-8');os._exit(1)

View File

@@ -0,0 +1,88 @@
"""Editable KFL001 bearing mounting plate for prototype.
Run with rebuild_kfl001_plate_v1.FCMacro in a hidden FreeCAD instance.
Both assembly plates have the same geometry; flip the second when fitting.
The centre cut provides 0.5 mm radial clearance for a 38 mm boss.
"""
from pathlib import Path
import os, time, traceback, json
import FreeCAD as A, FreeCADGui as G, Part, Import
R=Path(r'C:\projects\wheel')
O=R/'models'/'kfl001_plate_v1'
O.mkdir(parents=True, exist_ok=True)
try:
d=A.newDocument('KFL001_Plate_V1')
d.Label='KFL001 8mm bearing mounting plate V1'
s=d.addObject('Spreadsheet::Sheet','Parameters')
s.Label='Parameters - dimensions in mm'
rows=[('PlateWidth','76 mm','Overall width'),('PlateHeight','80 mm','Overall height'),
('Thickness','8 mm','Plate thickness'),('AxisFromBottom','51 mm','Axle height above lower edge'),
('CenterClearance','39 mm','39 mm clearance for user confirmed 38 mm boss'),
('BearingPitch','48 mm','Bolt centre spacing for KFL001'),
('BearingHole','7 mm','Bearing mount clearance diameter'),
('SlotPitch','47 mm','Centre spacing of lower mounting slots'),
('SlotBottom','9 mm','Mounting slot centre above plate lower edge'),
('SlotLength','9.5 mm','Horizontal slot overall length'),
('SlotWidth','5.5 mm','Horizontal slot width')]
for row,(name,val,note) in enumerate(rows,2):
s.set('A%d'%row,name);s.set('B%d'%row,val);s.set('C%d'%row,note);s.setAlias('B%d'%row,name)
def expr(o,p,v):o.setExpression(p,v)
plate=d.addObject('Part::Box','PlateBlank');plate.Label='76 x 80 x 8 plate blank'
expr(plate,'Length','Parameters.PlateWidth');expr(plate,'Width','Parameters.PlateHeight');expr(plate,'Height','Parameters.Thickness')
expr(plate,'Placement.Base.x','-Parameters.PlateWidth / 2')
def cyl(name,x,y,r):
o=d.addObject('Part::Cylinder',name);expr(o,'Radius',r);expr(o,'Height','Parameters.Thickness + 0.2 mm');expr(o,'Placement.Base.x',x);expr(o,'Placement.Base.y',y);o.Placement.Base.z=-.1;return o
tools=[]
tools.append(cyl('CentreHole','0 mm','Parameters.AxisFromBottom','Parameters.CenterClearance / 2'))
for side in (-1,1):
sign='-' if side<0 else ''
tools.append(cyl('BearingHole_'+('L' if side<0 else 'R'),sign+'Parameters.BearingPitch / 2','Parameters.AxisFromBottom','Parameters.BearingHole / 2'))
cx=sign+'Parameters.SlotPitch / 2';tag='L' if side<0 else 'R'
tools.append(cyl('Slot'+tag+'End1','(%s) - (Parameters.SlotLength - Parameters.SlotWidth) / 2'%cx,'Parameters.SlotBottom','Parameters.SlotWidth / 2'))
tools.append(cyl('Slot'+tag+'End2','(%s) + (Parameters.SlotLength - Parameters.SlotWidth) / 2'%cx,'Parameters.SlotBottom','Parameters.SlotWidth / 2'))
b=d.addObject('Part::Box','Slot'+tag+'Middle')
expr(b,'Length','Parameters.SlotLength - Parameters.SlotWidth');expr(b,'Width','Parameters.SlotWidth');expr(b,'Height','Parameters.Thickness + 0.2 mm')
expr(b,'Placement.Base.x','(%s) - (Parameters.SlotLength - Parameters.SlotWidth) / 2'%cx)
expr(b,'Placement.Base.y','Parameters.SlotBottom - Parameters.SlotWidth / 2');b.Placement.Base.z=-.1
tools.append(b)
d.recompute()
cutters=d.addObject('Part::MultiFuse','Cutters');cutters.Shapes=tools;d.recompute()
final=d.addObject('Part::Cut','Plate_V1');final.Label='KFL001 bearing mounting plate V1';final.Base=plate;final.Tool=cutters
d.recompute()
assert not final.Shape.isNull() and final.Shape.isValid() and len(final.Shape.Solids)==1
for o in d.Objects:
if o.ViewObject:o.ViewObject.Visibility=False
final.ViewObject.Visibility=True;final.ViewObject.ShapeColor=(.71,.74,.77);final.ViewObject.LineColor=(.12,.12,.12)
G.activateWorkbench('PartWorkbench');view=G.activeDocument().activeView();view.viewAxonometric();G.updateGui();view.fitAll();time.sleep(.3);G.updateGui();view.saveImage(str(O/'plate_isometric.png'),1400,1100,'White')
view.viewTop();G.updateGui();view.fitAll();time.sleep(.3);G.updateGui();view.saveImage(str(O/'plate_top.png'),1400,1100,'White')
d.saveAs(str(O/'KFL001_plate_V1.FCStd'));Import.export([final],str(O/'KFL001_plate_V1.step'))
baseline=final.Shape.Volume
s.set('B6','38 mm');d.recompute()
assert final.Shape.isValid() and abs(final.Shape.Volume-baseline)>0.1
changed=final.Shape.Volume
s.set('B6','39 mm');d.recompute()
assert final.Shape.isValid() and abs(final.Shape.Volume-baseline)<1e-5
A.closeDocument(d.Name)
dd=A.openDocument(str(O/'KFL001_plate_V1.FCStd'));dd.recompute();ff=dd.getObject('Plate_V1')
assert ff.Shape.isValid() and len(ff.Shape.Solids)==1 and abs(ff.Shape.Volume-baseline)<1e-5
imported=Part.read(str(O/'KFL001_plate_V1.step'))
assert imported.isValid() and len(imported.Solids)==1
# At the middle Z plane compare the STEP vs native shape by mutual differences.
stepdiff1=ff.Shape.cut(imported).Volume;stepdiff2=imported.cut(ff.Shape).Volume
assert stepdiff1<1e-4 and stepdiff2<1e-4
angles=[(3.5,24,51),(3.5,-24,51),(19.5,0,51)]
cylinders=[]
for f in imported.Faces:
surf=f.Surface
if hasattr(surf,'Radius') and hasattr(surf,'Center'):
cylinders.append({'radius':round(float(surf.Radius),5),'x':round(float(surf.Center.x),5),'y':round(float(surf.Center.y),5),'z_min':round(f.BoundBox.ZMin,5),'z_max':round(f.BoundBox.ZMax,5)})
for r,x,y in angles:
assert any(abs(c['radius']-r)<1e-4 and abs(c['x']-x)<1e-4 and abs(c['y']-y)<1e-4 and c['z_min']<=.001 and c['z_max']>=7.999 for c in cylinders), (r,x,y,cylinders)
info={'fcstd_valid':True,'step_valid':True,'solid_count':len(imported.Solids),'volume':baseline,'diameter_parameter_test_38':changed,'diameter_test_restored':True,'step_old_minus_new':stepdiff1,'step_new_minus_old':stepdiff2,'dimensions_mm':{'width':76,'height':80,'thickness':8,'axis_from_bottom':51,'center_diameter':39,'bearing_pitch':48,'bearing_hole':7,'slot_pitch':47,'slot_bottom':9,'slot_length':9.5,'slot_width':5.5},'cylinder_faces':cylinders,'nominal_m5_slot_range_mm':4.5,'quantity_identical_plates':2,'boss_diameter_user_confirmed_mm':38}
(O/'validation.json').write_text(json.dumps(info,ensure_ascii=False,indent=2),encoding='utf-8')
A.closeDocument(dd.Name)
(O/'DONE.txt').write_text('ok',encoding='utf-8')
os._exit(0)
except Exception:
(O/'ERROR.txt').write_text(traceback.format_exc(),encoding='utf-8');os._exit(1)

View File

@@ -0,0 +1,111 @@
"""Editable KFL001 bearing mounting plate for prototype.
Run with rebuild_kfl001_plate_v1.FCMacro in a hidden FreeCAD instance.
Both assembly plates have the same geometry; flip the second when fitting.
The centre cut provides 0.5 mm radial clearance for a 38 mm boss.
"""
from pathlib import Path
import os, time, traceback, json
import FreeCAD as A, FreeCADGui as G, Part, Import
R=Path(r'C:\projects\wheel')
O=R/'models'/'kfl001_plate_v2'
O.mkdir(parents=True, exist_ok=True)
try:
d=A.newDocument('KFL001_Plate_V2')
d.Label='KFL001 8mm bearing mounting plate V2'
s=d.addObject('Spreadsheet::Sheet','Parameters')
s.Label='Parameters - dimensions in mm'
rows=[('PlateWidth','76 mm','Overall width'),('PlateHeight','75 mm','Overall height'),
('Thickness','8 mm','Plate thickness'),('AxisFromBottom','51 mm','Axle height above lower edge'),
('CenterClearance','39 mm','39 mm clearance for user confirmed 38 mm boss'),
('BearingPitch','48 mm','Bolt centre spacing for KFL001'),
('BearingHole','7 mm','Bearing mount clearance diameter'),
('SlotPitch','47 mm','Centre spacing of lower mounting slots'),
('SlotBottom','9 mm','Mounting slot centre above plate lower edge'),
('SlotLength','9.5 mm','Horizontal slot overall length'),
('SlotWidth','5.5 mm','Horizontal slot width'),
('EdgeChamfer','1.5 mm','Both outer perimeter edges only'),
('ArchRadius','60 mm','Top arc radius')]
for row,(name,val,note) in enumerate(rows,2):
s.set('A%d'%row,name);s.set('B%d'%row,val);s.set('C%d'%row,note);s.setAlias('B%d'%row,name)
def expr(o,p,v):o.setExpression(p,v)
plate=d.addObject('Part::Box','PlateBlank');plate.Label='76 x 75 x 8 plate blank'
expr(plate,'Length','Parameters.PlateWidth');expr(plate,'Width','Parameters.PlateHeight');expr(plate,'Height','Parameters.Thickness')
arc=d.addObject('Part::Cylinder','ArchLimit');arc.Label='Upper R60 arc'
expr(arc,'Radius','Parameters.ArchRadius');expr(arc,'Height','Parameters.Thickness')
expr(arc,'Placement.Base.x','Parameters.PlateWidth / 2');expr(arc,'Placement.Base.y','Parameters.PlateHeight - Parameters.ArchRadius')
d.recompute()
shape=d.addObject('Part::Common','ArchedOutline');shape.Base=plate;shape.Tool=arc;d.recompute()
assert shape.Shape.isValid() and len(shape.Shape.Solids)==1
body=d.addObject('PartDesign::Body','ChamferBody');body.Label='Perimeter chamfer'
body.BaseFeature=shape;d.recompute()
outer=[]
for i,e in enumerate(shape.Shape.Edges,1):
b=e.BoundBox
if abs(b.ZMax-b.ZMin)<1e-7 and (abs(b.ZMin)<1e-7 or abs(b.ZMin-8)<1e-7):outer.append('Edge%d'%i)
chamfer=body.newObject('PartDesign::Chamfer','PerimeterChamfer')
chamfer.Base=(chamfer.BaseFeature,outer);expr(chamfer,'Size','Parameters.EdgeChamfer')
d.recompute()
assert not chamfer.Shape.isNull() and chamfer.Shape.isValid(),chamfer.State
shifted=d.addObject('App::Link','ShiftedChamferBlank');shifted.setLink(chamfer)
shifted.LinkTransform=True
expr(shifted,'Placement.Base.x','-Parameters.PlateWidth / 2')
d.recompute()
def cyl(name,x,y,r):
o=d.addObject('Part::Cylinder',name);expr(o,'Radius',r);expr(o,'Height','Parameters.Thickness + 0.2 mm');expr(o,'Placement.Base.x',x);expr(o,'Placement.Base.y',y);o.Placement.Base.z=-.1;return o
tools=[]
tools.append(cyl('CentreHole','0 mm','Parameters.AxisFromBottom','Parameters.CenterClearance / 2'))
for side in (-1,1):
sign='-' if side<0 else ''
tools.append(cyl('BearingHole_'+('L' if side<0 else 'R'),sign+'Parameters.BearingPitch / 2','Parameters.AxisFromBottom','Parameters.BearingHole / 2'))
cx=sign+'Parameters.SlotPitch / 2';tag='L' if side<0 else 'R'
tools.append(cyl('Slot'+tag+'End1','(%s) - (Parameters.SlotLength - Parameters.SlotWidth) / 2'%cx,'Parameters.SlotBottom','Parameters.SlotWidth / 2'))
tools.append(cyl('Slot'+tag+'End2','(%s) + (Parameters.SlotLength - Parameters.SlotWidth) / 2'%cx,'Parameters.SlotBottom','Parameters.SlotWidth / 2'))
b=d.addObject('Part::Box','Slot'+tag+'Middle')
expr(b,'Length','Parameters.SlotLength - Parameters.SlotWidth');expr(b,'Width','Parameters.SlotWidth');expr(b,'Height','Parameters.Thickness + 0.2 mm')
expr(b,'Placement.Base.x','(%s) - (Parameters.SlotLength - Parameters.SlotWidth) / 2'%cx)
expr(b,'Placement.Base.y','Parameters.SlotBottom - Parameters.SlotWidth / 2');b.Placement.Base.z=-.1
tools.append(b)
d.recompute()
cutters=d.addObject('Part::MultiFuse','Cutters');cutters.Shapes=tools;d.recompute()
final=d.addObject('Part::Cut','Plate_V2');final.Label='KFL001 bearing mounting plate V2';final.Base=shifted;final.Tool=cutters
d.recompute()
assert not final.Shape.isNull() and final.Shape.isValid() and len(final.Shape.Solids)==1
for o in d.Objects:
if o.ViewObject:o.ViewObject.Visibility=False
final.ViewObject.Visibility=True;final.ViewObject.ShapeColor=(.71,.74,.77);final.ViewObject.LineColor=(.12,.12,.12)
final.ViewObject.Deviation=0.05
final.ViewObject.AngularDeflection=10
G.activateWorkbench('PartWorkbench');view=G.activeDocument().activeView();view.viewAxonometric();G.updateGui();view.fitAll();time.sleep(.3);G.updateGui();view.saveImage(str(O/'plate_isometric.png'),1400,1100,'White')
view.viewTop();G.updateGui();view.fitAll();time.sleep(.3);G.updateGui();view.saveImage(str(O/'plate_top.png'),1400,1100,'White')
d.saveAs(str(O/'KFL001_plate_V2.FCStd'));Import.export([final],str(O/'KFL001_plate_V2.step'))
baseline=final.Shape.Volume
s.set('B6','38 mm');d.recompute()
assert final.Shape.isValid() and abs(final.Shape.Volume-baseline)>0.1
changed=final.Shape.Volume
s.set('B6','39 mm');d.recompute()
assert final.Shape.isValid() and abs(final.Shape.Volume-baseline)<1e-5
A.closeDocument(d.Name)
dd=A.openDocument(str(O/'KFL001_plate_V2.FCStd'));dd.recompute();ff=dd.getObject('Plate_V2')
assert ff.Shape.isValid() and len(ff.Shape.Solids)==1 and abs(ff.Shape.Volume-baseline)<1e-5
imported=Part.read(str(O/'KFL001_plate_V2.step'))
assert imported.isValid() and len(imported.Solids)==1
# At the middle Z plane compare the STEP vs native shape by mutual differences.
stepdiff1=ff.Shape.cut(imported).Volume;stepdiff2=imported.cut(ff.Shape).Volume
assert stepdiff1<1e-4 and stepdiff2<1e-4
angles=[(3.5,24,51),(3.5,-24,51),(19.5,0,51)]
cylinders=[]
for f in imported.Faces:
surf=f.Surface
if hasattr(surf,'Radius') and hasattr(surf,'Center'):
cylinders.append({'radius':round(float(surf.Radius),5),'x':round(float(surf.Center.x),5),'y':round(float(surf.Center.y),5),'z_min':round(f.BoundBox.ZMin,5),'z_max':round(f.BoundBox.ZMax,5)})
for r,x,y in angles:
assert any(abs(c['radius']-r)<1e-4 and abs(c['x']-x)<1e-4 and abs(c['y']-y)<1e-4 and c['z_min']<=.001 and c['z_max']>=7.999 for c in cylinders), (r,x,y,cylinders)
info={'fcstd_valid':True,'step_valid':True,'solid_count':len(imported.Solids),'volume':baseline,'diameter_parameter_test_38':changed,'diameter_test_restored':True,'step_old_minus_new':stepdiff1,'step_new_minus_old':stepdiff2,'dimensions_mm':{'width':76,'height':75,'thickness':8,'axis_from_bottom':51,'center_diameter':39,'bearing_pitch':48,'bearing_hole':7,'slot_pitch':47,'slot_bottom':9,'slot_length':9.5,'slot_width':5.5,'perimeter_chamfer':1.5,'upper_arc_radius':60,'upper_arc_centre_from_bottom':15},'cylinder_faces':cylinders,'nominal_m5_slot_range_mm':4.5,'quantity_identical_plates':2,'boss_diameter_user_confirmed_mm':38}
(O/'validation.json').write_text(json.dumps(info,ensure_ascii=False,indent=2),encoding='utf-8')
A.closeDocument(dd.Name)
(O/'DONE.txt').write_text('ok',encoding='utf-8')
os._exit(0)
except Exception:
(O/'ERROR.txt').write_text(traceback.format_exc(),encoding='utf-8');os._exit(1)

View File

@@ -0,0 +1,121 @@
"""Editable KFL001 bearing mounting plate for prototype.
Run with rebuild_kfl001_plate_v1.FCMacro in a hidden FreeCAD instance.
Both assembly plates have the same geometry; flip the second when fitting.
The centre cut provides 0.5 mm radial clearance for a 38 mm boss.
"""
from pathlib import Path
import os, time, traceback, json
import FreeCAD as A, FreeCADGui as G, Part, Import
R=Path(r'C:\projects\wheel')
O=R/'models'/'kfl001_plate_v3_alt'
O.mkdir(parents=True, exist_ok=True)
try:
d=A.newDocument('KFL001_Plate_V3')
d.Label='KFL001 8mm bearing mounting plate V3'
s=d.addObject('Spreadsheet::Sheet','Parameters')
s.Label='Parameters - dimensions in mm'
rows=[('PlateWidth','76 mm','Overall width'),('PlateHeight','75 mm','Overall height'),
('Thickness','8 mm','Plate thickness'),('AxisFromBottom','51 mm','Axle height above lower edge'),
('CenterClearance','39 mm','39 mm clearance for user confirmed 38 mm boss'),
('BearingPitch','48 mm','Bolt centre spacing for KFL001'),
('BearingHole','7 mm','Bearing mount clearance diameter'),
('SlotPitch','47 mm','Centre spacing of lower mounting slots'),
('SlotBottom','9 mm','Mounting slot centre above plate lower edge'),
('SlotLength','9.5 mm','Horizontal slot overall length'),
('SlotWidth','5.5 mm','Horizontal slot width'),
('EdgeChamfer','1 mm','Both outer perimeter edges only'),
('CornerRadius','3 mm','Rounded upper transitions and lower plan corners'),
('ArchRadius','60 mm','Top arc radius')]
for row,(name,val,note) in enumerate(rows,2):
s.set('A%d'%row,name);s.set('B%d'%row,val);s.set('C%d'%row,note);s.setAlias('B%d'%row,name)
def expr(o,p,v):o.setExpression(p,v)
plate=d.addObject('Part::Box','PlateBlank');plate.Label='76 x 75 x 8 plate blank'
expr(plate,'Length','Parameters.PlateWidth');expr(plate,'Width','Parameters.PlateHeight');expr(plate,'Height','Parameters.Thickness')
arc=d.addObject('Part::Cylinder','ArchLimit');arc.Label='Upper R60 arc'
expr(arc,'Radius','Parameters.ArchRadius');expr(arc,'Height','Parameters.Thickness')
expr(arc,'Placement.Base.x','Parameters.PlateWidth / 2');expr(arc,'Placement.Base.y','Parameters.PlateHeight - Parameters.ArchRadius')
d.recompute()
shape=d.addObject('Part::Common','ArchedOutline');shape.Base=plate;shape.Tool=arc;d.recompute()
assert shape.Shape.isValid() and len(shape.Shape.Solids)==1
body=d.addObject('PartDesign::Body','ChamferBody');body.Label='Perimeter chamfer'
body.BaseFeature=shape;d.recompute()
vertical=[]
for i,e in enumerate(shape.Shape.Edges,1):
b=e.BoundBox
if abs((b.ZMax-b.ZMin)-8)<1e-7 and (b.XLength<1e-6 and b.YLength<1e-6): vertical.append('Edge%d'%i)
assert len(vertical)==4,vertical
fillet=body.newObject('PartDesign::Fillet','OutlineCornerFillets')
fillet.Base=(fillet.BaseFeature,vertical);expr(fillet,'Radius','Parameters.CornerRadius')
d.recompute()
assert not fillet.Shape.isNull() and fillet.Shape.isValid(),fillet.State
outer=[]
for i,e in enumerate(fillet.Shape.Edges,1):
b=e.BoundBox
if abs(b.ZMax-b.ZMin)<1e-7 and (abs(b.ZMin)<1e-7 or abs(b.ZMin-8)<1e-7):outer.append('Edge%d'%i)
chamfer=body.newObject('PartDesign::Chamfer','PerimeterChamfer')
chamfer.Base=(fillet,outer);expr(chamfer,'Size','Parameters.EdgeChamfer')
d.recompute()
assert not chamfer.Shape.isNull() and chamfer.Shape.isValid(),chamfer.State
shifted=d.addObject('App::Link','ShiftedChamferBlank');shifted.setLink(chamfer)
shifted.LinkTransform=True
expr(shifted,'Placement.Base.x','-Parameters.PlateWidth / 2')
d.recompute()
def cyl(name,x,y,r):
o=d.addObject('Part::Cylinder',name);expr(o,'Radius',r);expr(o,'Height','Parameters.Thickness + 0.2 mm');expr(o,'Placement.Base.x',x);expr(o,'Placement.Base.y',y);o.Placement.Base.z=-.1;return o
tools=[]
tools.append(cyl('CentreHole','0 mm','Parameters.AxisFromBottom','Parameters.CenterClearance / 2'))
for side in (-1,1):
sign='-' if side<0 else ''
tools.append(cyl('BearingHole_'+('L' if side<0 else 'R'),sign+'Parameters.BearingPitch / 2','Parameters.AxisFromBottom','Parameters.BearingHole / 2'))
cx=sign+'Parameters.SlotPitch / 2';tag='L' if side<0 else 'R'
tools.append(cyl('Slot'+tag+'End1','(%s) - (Parameters.SlotLength - Parameters.SlotWidth) / 2'%cx,'Parameters.SlotBottom','Parameters.SlotWidth / 2'))
tools.append(cyl('Slot'+tag+'End2','(%s) + (Parameters.SlotLength - Parameters.SlotWidth) / 2'%cx,'Parameters.SlotBottom','Parameters.SlotWidth / 2'))
b=d.addObject('Part::Box','Slot'+tag+'Middle')
expr(b,'Length','Parameters.SlotLength - Parameters.SlotWidth');expr(b,'Width','Parameters.SlotWidth');expr(b,'Height','Parameters.Thickness + 0.2 mm')
expr(b,'Placement.Base.x','(%s) - (Parameters.SlotLength - Parameters.SlotWidth) / 2'%cx)
expr(b,'Placement.Base.y','Parameters.SlotBottom - Parameters.SlotWidth / 2');b.Placement.Base.z=-.1
tools.append(b)
d.recompute()
cutters=d.addObject('Part::MultiFuse','Cutters');cutters.Shapes=tools;d.recompute()
final=d.addObject('Part::Cut','Plate_V3');final.Label='KFL001 bearing mounting plate V3';final.Base=shifted;final.Tool=cutters
d.recompute()
assert not final.Shape.isNull() and final.Shape.isValid() and len(final.Shape.Solids)==1
for o in d.Objects:
if o.ViewObject:o.ViewObject.Visibility=False
final.ViewObject.Visibility=True;final.ViewObject.ShapeColor=(.71,.74,.77);final.ViewObject.LineColor=(.12,.12,.12)
final.ViewObject.Deviation=0.05
final.ViewObject.AngularDeflection=10
G.activateWorkbench('PartWorkbench');view=G.activeDocument().activeView();view.viewAxonometric();G.updateGui();view.fitAll();time.sleep(.3);G.updateGui();view.saveImage(str(O/'plate_isometric.png'),1400,1100,'White')
view.viewTop();G.updateGui();view.fitAll();time.sleep(.3);G.updateGui();view.saveImage(str(O/'plate_top.png'),1400,1100,'White')
d.saveAs(str(O/'KFL001_plate_V3.FCStd'));Import.export([final],str(O/'KFL001_plate_V3.step'))
baseline=final.Shape.Volume
s.set('B6','38 mm');d.recompute()
assert final.Shape.isValid() and abs(final.Shape.Volume-baseline)>0.1
changed=final.Shape.Volume
s.set('B6','39 mm');d.recompute()
assert final.Shape.isValid() and abs(final.Shape.Volume-baseline)<1e-5
A.closeDocument(d.Name)
dd=A.openDocument(str(O/'KFL001_plate_V3.FCStd'));dd.recompute();ff=dd.getObject('Plate_V3')
assert ff.Shape.isValid() and len(ff.Shape.Solids)==1 and abs(ff.Shape.Volume-baseline)<1e-5
imported=Part.read(str(O/'KFL001_plate_V3.step'))
assert imported.isValid() and len(imported.Solids)==1
# At the middle Z plane compare the STEP vs native shape by mutual differences.
stepdiff1=ff.Shape.cut(imported).Volume;stepdiff2=imported.cut(ff.Shape).Volume
assert stepdiff1<1e-4 and stepdiff2<1e-4
angles=[(3.5,24,51),(3.5,-24,51),(19.5,0,51)]
cylinders=[]
for f in imported.Faces:
surf=f.Surface
if hasattr(surf,'Radius') and hasattr(surf,'Center'):
cylinders.append({'radius':round(float(surf.Radius),5),'x':round(float(surf.Center.x),5),'y':round(float(surf.Center.y),5),'z_min':round(f.BoundBox.ZMin,5),'z_max':round(f.BoundBox.ZMax,5)})
for r,x,y in angles:
assert any(abs(c['radius']-r)<1e-4 and abs(c['x']-x)<1e-4 and abs(c['y']-y)<1e-4 and c['z_min']<=.001 and c['z_max']>=7.999 for c in cylinders), (r,x,y,cylinders)
info={'fcstd_valid':True,'step_valid':True,'solid_count':len(imported.Solids),'volume':baseline,'diameter_parameter_test_38':changed,'diameter_test_restored':True,'step_old_minus_new':stepdiff1,'step_new_minus_old':stepdiff2,'dimensions_mm':{'width':76,'height':75,'thickness':8,'axis_from_bottom':51,'center_diameter':39,'bearing_pitch':48,'bearing_hole':7,'slot_pitch':47,'slot_bottom':9,'slot_length':9.5,'slot_width':5.5,'perimeter_chamfer':1.0,'outline_corner_radius':3.0,'upper_arc_radius':60,'upper_arc_centre_from_bottom':15},'cylinder_faces':cylinders,'nominal_m5_slot_range_mm':4.5,'quantity_identical_plates':2,'boss_diameter_user_confirmed_mm':38}
(O/'validation.json').write_text(json.dumps(info,ensure_ascii=False,indent=2),encoding='utf-8')
A.closeDocument(dd.Name)
(O/'DONE.txt').write_text('ok',encoding='utf-8')
os._exit(0)
except Exception:
(O/'ERROR.txt').write_text(traceback.format_exc(),encoding='utf-8');os._exit(1)

View File

@@ -0,0 +1,127 @@
from pathlib import Path
import math, time, traceback, os
import FreeCAD as A, FreeCADGui as G, Part, Import
from pivy import coin
V=A.Vector
R=Path(r'C:\projects\wheel'); variant=os.environ.get('KFL_HUB_VARIANT')=='narrow';O=R/'models'/('kfl_100_narrow_concept' if variant else 'kfl_100_concept');O.mkdir(parents=True,exist_ok=True)
def cy(r,h,x=0,y=0,z=0):return Part.makeCylinder(r,h,V(x,y,z))
def add(d,n,label,s,c,t=0):
o=d.addObject('PartDesign::Feature',n);o.Label=label;o.Shape=s;o.ViewObject.ShapeColor=c;o.ViewObject.LineColor=(.08,.08,.08);o.ViewObject.Transparency=t;return o
def booleanvol(a,b):
return a.common(b).Volume if a.BoundBox.intersect(b.BoundBox) else 0
try:
src=A.openDocument(str(R/'models'/'pcd50_open_pillar_v1'/'Assembly.FCStd'))
existing={o.Name:(o.Shape.copy(),tuple(o.ViewObject.ShapeColor),o.ViewObject.Transparency) for o in src.Objects if hasattr(o,'Shape') and not o.Shape.isNull()}
A.closeDocument(src.Name)
src=A.openDocument(str(R/'models'/'turn_signal_cam_v2'/'Assembly.FCStd'))
cam={n:src.getObject(n).Shape.copy() for n in ['Shaft','Key','Upper','Lower','Hardware0','Hardware1','Hardware2','Hardware3']}
A.closeDocument(src.Name)
for s in cam.values():s.translate(V(0,0,-102))
doc=A.newDocument('KFL100_Schematic'); objs={}
for n,(s,c,t) in existing.items():
objs[n]=add(doc,n,'Prior existing '+n,s,c,55 if n=='Motor' else t)
for i,x in enumerate([-50.6200577,50.6200577]):
objs['NewRail%d'%i]=add(doc,'NewRail%d'%i,'Proposed 2020 longitudinal rail schematic',Part.makeBox(20,20,190,V(x-10,-75,-170)),(.48,.51,.55))
# Two vertical plates parallel to XY. The 30.2 mm bore is placeholder,
# with dummy bearing protrusion 29 mm OD; NOT a production bore.
plates=[]
for i,z in enumerate([-74,-140]):
s=Part.makeBox(140,90,8,V(-70,-55,z)).cut(cy(15.1,8.02,0,0,z-.01))
for x in (-24,24):s=s.cut(cy(3.5,8.02,x,0,z-.01))
o=add(doc,'Plate%d'%i,'8mm plate: hole 30.2 assumed',s,(.66,.69,.74));plates.append(o)
# Simplified illustrative corner brackets, contacting plate and rail only.
for j,x in enumerate([-50.6200577,50.6200577]):
a=Part.makeBox(16,12,3,V(x-8,-55,z-3))
b=Part.makeBox(16,3,11,V(x-8,-55,z-11))
o=add(doc,'L_%d_%d'%(i,j),'90deg bracket placeholder, fasteners omitted',a.fuse(b).removeSplitter(),(.88,.49,.16))
# Approximate KFL outside dimensions: 63 x 38 mm, pitch 48, flange
# ears 5.5, insert 14.5; protrusion 29mm OD and position are placeholders.
bearings=[]
for i,(plate_z,inner_z,sign) in enumerate([(-74,-78.5,1),(-140,-143,-1)]):
# face of plate towards outside axle area, mirrored.
if sign==1: face=plate_z+8; flangez=face; insertz=face-8
else: face=plate_z; flangez=face-5.5; insertz=face
ring=cy(19,5.5,0,0,flangez).cut(cy(9.5,5.52,0,0,flangez-.01))
for x in (-24,24):
ear=cy(7.5,5.5,x,0,flangez).fuse(Part.makeBox(12,15,5.5,V(min(x,0),-7.5,flangez)))
ring=ring.fuse(ear)
for x in (-24,24):ring=ring.cut(cy(3.5,5.52,x,0,flangez-.01))
ring=ring.cut(cy(9.5,5.52,0,0,flangez-.01))
ring=ring.fuse(cy(14.5,8,0,0,insertz).cut(cy(9.5,8.02,0,0,insertz-.01))).removeSplitter()
outer=add(doc,'KFLHousing%d'%i,'KFL001 housing: 29 protrusion ASSUMED',ring,(.45,.49,.53));bearings.append(outer)
inner=cy(9.2,14.5,0,0,inner_z).cut(cy(6.1,14.52,0,0,inner_z-.01))
bearings.append(add(doc,'KFLInner%d'%i,'KFL inner 14.5mm rotating schematic',inner,(.77,.77,.79)))
# M6 screws only illustrative straight 5.8mm shank; heads omitted.
for j,x in enumerate((-24,24)):
z0=plate_z-1 if sign==1 else flangez-1
o=add(doc,'KFLBolt%d_%d'%(i,j),'M6 shank schematic, heads omitted',cy(2.9,15.5,x,0,z0),(.18,.19,.20));objs[o.Name]=o
# Real shaft/cam from existing project.
colors={'Shaft':(.65,.66,.68),'Key':(.98,.65,.1),'Upper':(.05,.40,.80),'Lower':(.16,.62,.88)}
for n,s in cam.items():objs['Cam'+n]=add(doc,'Cam'+n,'Existing cam V2 '+n,s,colors.get(n,(.2,.2,.21)))
# Actual small conversion disk STEP: mirrored to front face at z=-38.
conv=Part.read(str(R/'models'/'转换盘1.step'));conv.rotate(V(),V(1,0,0),180);conv.translate(V(0,0,-38))
objs['Conversion']=add(doc,'Conversion','Existing conversion disk STEP; face position approx',conv,(.82,.83,.85))
# Six-hole keyed flange is a placeholder, the hub overlaps 12mm of shaft.
flange=cy(27,4,0,0,-50).cut(cy(6.2,4.02,0,0,-50.01))
for j in range(6):
a=2*math.pi*j/6+math.pi/6;flange=flange.cut(cy(2.2,4.02,24*math.cos(a),24*math.sin(a),-50.01))
hubradius=9.2 if variant else 10
hub=cy(hubradius,14,0,0,-64).cut(cy(6.2,14.02,0,0,-64.01));flange=flange.fuse(hub).removeSplitter()
objs['MotorFlange']=add(doc,'MotorFlange','MOTOR hub placeholder: 12mm shaft engagement assumed',flange,(.9,.66,.20))
# Wheel hub is deliberately modelled with only 9mm shaft engagement.
wh=cy(hubradius,9,0,0,-152).cut(cy(6.2,9.02,0,0,-152.01))
whplate=cy(27,3,0,0,-155).cut(cy(6.2,3.02,0,0,-155.01))
for j in range(6):
a=2*math.pi*j/6;whplate=whplate.cut(cy(2.2,3.02,24*math.cos(a),24*math.sin(a),-155.01))
wh=wh.fuse(whplate).removeSplitter()
objs['WheelHub']=add(doc,'WheelHub','Wheel flange schematic: only 9mm engagement unverified',wh,(.9,.66,.20))
wheel70=Part.read(str(R/'models'/'70转接板.step'));wheel70.rotate(V(),V(1,0,0),180);wheel70.translate(V(0,0,-155))
objs['Wheel70']=add(doc,'Wheel70','Existing 70 adapter, nominal assumed position, bolt access unchecked',wheel70,(.82,.83,.85))
# Switch envelope is deliberately only transparent rectangular with generic
# centre bore; 50mm depth includes off-axis connector. Actual contours unknown.
sw=Part.makeBox(110,80,50,V(-55,-40,-128.5)).cut(cy(23.5,50.02,0,0,-128.51))
objs['SwitchEnvelope']=add(doc,'SwitchEnvelope','50mm switch bounding placeholder; 47mm bore ASSUMED',sw,(.45,.73,.43),80)
doc.recompute()
# Restrict checks to shapes actually modelled. Intentional shaft-to-hub clearance.
checks=[]
pairs=[('Plate%d'%i,'KFLHousing%d'%i,plates[i].Shape,bearings[2*i].Shape) for i in range(2)]
pairs += [('Plate%d'%i,'KFLInner%d'%i,plates[i].Shape,bearings[2*i+1].Shape) for i in range(2)]
pairs += [('SwitchEnvelope','Cam'+n,sw,cam[n]) for n in ('Upper','Lower','Hardware0','Hardware1','Hardware2','Hardware3')]
pairs += [('MotorFlange','Shaft',flange,cam['Shaft']),('WheelHub','Shaft',wh,cam['Shaft']),('WheelHub','70Adapter',wh,wheel70),('Plate0','Plate1',plates[0].Shape,plates[1].Shape)]
pairs += [('MotorFlange','RearBearing',flange,bearings[0].Shape),('WheelHub','FrontBearing',wh,bearings[2].Shape)]
for a,b,sa,sb in pairs:
checks.append('%s / %s = %.6f mm3'%(a,b,booleanvol(sa,sb)))
for n,s in [('RearConflict',flange.common(bearings[0].Shape)),('FrontConflict',wh.common(bearings[2].Shape))]:
if not s.isNull() and s.Volume>.001: add(doc,n,'RED: nominal collision '+n,s,(1.0,.03,.03))
lines=['CONCEPT ONLY; NOT ASSEMBLY FIT VALIDATION.',
'Schematic hub outside diameter %.1fmm, not measured; variant narrowed only to explore geometry.'%(hubradius*2),
'Z decreases from motor toward steering wheel; motor rear z0; wheel shaft z=-52..-152 length100.',
'KFL actual nominal 63x38 housing envelope, 48 bolt centers, 7 hole, 5.5 ears, inner 14.5; protrusion OD29 and position assumed.',
'8mm plates central hole OD30.2 assumed, not actual production spec.',
'50mm switch opaque-enclosing envelope z=-128.5..-78.5; opening OD47 ASSUMED only; real rotary features and contacts unknown.',
'Allocation by shaft: motor hub12 + bearing14.5 + switch50 + bearing14.5 + wheel hub9 = 100 mm. No axial gaps. 9mm hub engagement NOT VALIDATED.',
'Sleeve20 sits inside switch envelope and is not additional 20mm.',
'Motor mounting boss, shaft end keys, original M3 nut, lights/white contacts, bearing set screws, pin fastening/head/tool and adjuster are not modelled.',
'Because ideal 100mm budget has zero clearance and unknown wheel hub engagement, this is not a workable fit verification; real assembly remains unverified. Narrow hub has no verified keyway strength or real KFL clearance.',
'Checked positive-volume intersections (other pairs, loaded operation, exact bearing and switch shape not tested):']+checks
(O/'geometry_check.txt').write_text('\n'.join(lines)+'\n',encoding='utf8')
doc.saveAs(str(O/'Assembly.FCStd'));Import.export([o for o in doc.Objects if hasattr(o,'Shape')],str(O/'Assembly.step'))
view=G.activeDocument().activeView();G.activateWorkbench('PartWorkbench')
def save(name,mode):
if mode=='side':view.viewFront()
else:
view.viewAxonometric();G.updateGui();view.getCameraNode().orientation.setValue(coin.SbRotation(.880476,.115917,-.279848,-.364705))
G.updateGui();time.sleep(.7);view.fitAll();G.updateGui();time.sleep(.7);view.saveImage(str(O/name),1800,1250,'White')
save('assembly.png','axon');save('side.png','side')
# Simplified focus hides rear encoder/rails so bearing through plate is visible.
for n,o in objs.items():
if n in existing or n.startswith('NewRail') or n=='Conversion':o.ViewObject.Visibility=False
for o in doc.Objects:
if o.Name.startswith('L_'):o.ViewObject.Visibility=False
save('focus.png','axon')
for o in doc.Objects:
if hasattr(o,'Shape'):o.ViewObject.Visibility=True
doc.recompute();doc.saveAs(str(O/'Assembly.FCStd'))
(O/'DONE.txt').write_text('ok',encoding='utf8')
except Exception:
(O/'ERROR.txt').write_text(traceback.format_exc(),encoding='utf8');raise

View File

@@ -0,0 +1,145 @@
from pathlib import Path
import math, time, traceback, os
import FreeCAD as A, FreeCADGui as G, Part, Import
from pivy import coin
V=A.Vector
R=Path(r'C:\projects\wheel'); shaftlen=125; O=R/'models'/'kfl_125_switch60_concept'; O.mkdir(parents=True,exist_ok=True)
def cy(r,h,x=0,y=0,z=0):return Part.makeCylinder(r,h,V(x,y,z))
def add(d,n,label,s,c,t=0):
o=d.addObject('PartDesign::Feature',n);o.Label=label;o.Shape=s;o.ViewObject.ShapeColor=c;o.ViewObject.LineColor=(.08,.08,.08);o.ViewObject.Transparency=t;return o
def booleanvol(a,b):
return a.common(b).Volume if a.BoundBox.intersect(b.BoundBox) else 0
try:
src=A.openDocument(str(R/'models'/'pcd50_open_pillar_v1'/'Assembly.FCStd'))
existing={o.Name:(o.Shape.copy(),tuple(o.ViewObject.ShapeColor),o.ViewObject.Transparency) for o in src.Objects if hasattr(o,'Shape') and not o.Shape.isNull()}
A.closeDocument(src.Name)
src=A.openDocument(str(R/'models'/'turn_signal_cam_v2'/'Assembly.FCStd'))
cam={n:src.getObject(n).Shape.copy() for n in ['Shaft','Key','Upper','Lower','Hardware0','Hardware1','Hardware2','Hardware3']}
A.closeDocument(src.Name)
for n,s in cam.items():s.translate(V(0,0,-108 if n=='Shaft' else -118))
cam['Shaft']=cy(6,shaftlen,0,0,-56.5-shaftlen).cut(Part.makeBox(4.1,3,14,V(-2.05,4,-125)))
doc=A.newDocument('KFL125_Switch60_Schematic'); objs={}
for n,(s,c,t) in existing.items():
objs[n]=add(doc,n,'Prior existing '+n,s,c,55 if n=='Motor' else t)
for i,x in enumerate([-50.6200577,50.6200577]):
objs['NewRail%d'%i]=add(doc,'NewRail%d'%i,'Proposed 2020 longitudinal rail schematic',Part.makeBox(20,20,200,V(x-10,-75,-180)),(.48,.51,.55))
# Two vertical plates parallel to XY. The 30.2 mm bore is placeholder,
# with dummy bearing protrusion 29 mm OD; NOT a production bore.
plates=[]
for i,z in enumerate([-84,-162]):
s=Part.makeBox(140,90,8,V(-70,-55,z)).cut(cy(15.1,8.02,0,0,z-.01))
for x in (-24,24):s=s.cut(cy(3.5,8.02,x,0,z-.01))
o=add(doc,'Plate%d'%i,'8mm plate: hole 30.2 assumed',s,(.66,.69,.74));plates.append(o)
# Simplified illustrative corner brackets, contacting plate and rail only.
for j,x in enumerate([-50.6200577,50.6200577]):
a=Part.makeBox(16,12,3,V(x-8,-55,z-3))
b=Part.makeBox(16,3,11,V(x-8,-55,z-11))
o=add(doc,'L_%d_%d'%(i,j),'90deg bracket placeholder, fasteners omitted',a.fuse(b).removeSplitter(),(.88,.49,.16))
# Approximate KFL outside dimensions: 63 x 38 mm, pitch 48, flange
# ears 5.5, insert 14.5; protrusion 29mm OD and position are placeholders.
bearings=[]
for i,(plate_z,inner_z,sign) in enumerate([(-84,-88.5,1),(-162,-165,-1)]):
# face of plate towards outside axle area, mirrored.
if sign==1: face=plate_z+8; flangez=face; insertz=face-8
else: face=plate_z; flangez=face-5.5; insertz=face
ring=cy(19,5.5,0,0,flangez).cut(cy(9.5,5.52,0,0,flangez-.01))
for x in (-24,24):
ear=cy(7.5,5.5,x,0,flangez).fuse(Part.makeBox(12,15,5.5,V(min(x,0),-7.5,flangez)))
ring=ring.fuse(ear)
for x in (-24,24):ring=ring.cut(cy(3.5,5.52,x,0,flangez-.01))
ring=ring.cut(cy(9.5,5.52,0,0,flangez-.01))
ring=ring.fuse(cy(14.5,8,0,0,insertz).cut(cy(9.5,8.02,0,0,insertz-.01))).removeSplitter()
outer=add(doc,'KFLHousing%d'%i,'KFL001 housing: 29 protrusion ASSUMED',ring,(.45,.49,.53));bearings.append(outer)
inner=cy(9.2,14.5,0,0,inner_z).cut(cy(6.1,14.52,0,0,inner_z-.01))
bearings.append(add(doc,'KFLInner%d'%i,'KFL inner 14.5mm rotating schematic',inner,(.77,.77,.79)))
# M6 screws only illustrative straight 5.8mm shank; heads omitted.
for j,x in enumerate((-24,24)):
z0=plate_z-1 if sign==1 else flangez-1
o=add(doc,'KFLBolt%d_%d'%(i,j),'M6 shank schematic, heads omitted',cy(2.9,15.5,x,0,z0),(.18,.19,.20));objs[o.Name]=o
# Real shaft/cam from existing project.
colors={'Shaft':(.65,.66,.68),'Key':(.98,.65,.1),'Upper':(.05,.40,.80),'Lower':(.16,.62,.88)}
for n,s in cam.items():objs['Cam'+n]=add(doc,'Cam'+n,'Existing cam V2 '+n,s,colors.get(n,(.2,.2,.21)))
# Actual small conversion disk STEP: mirrored to front face at z=-38.
conv=Part.read(str(R/'models'/'转换盘1.step'));conv.rotate(V(),V(1,0,0),180);conv.translate(V(0,0,-38))
objs['Conversion']=add(doc,'Conversion','Existing conversion disk STEP; face position approx',conv,(.82,.83,.85))
# Six-hole keyed flange is a placeholder, the hub overlaps 12mm of shaft.
flange=cy(28.5,4,0,0,-50).cut(cy(6.2,4.02,0,0,-50.01))
for j in range(6):
a=2*math.pi*j/6+math.pi/6;flange=flange.cut(cy(2.75,4.02,24*math.cos(a),24*math.sin(a),-50.01))
hubradius=10
hub=cy(15,20,0,0,-70).cut(cy(6.2,20.02,0,0,-70.01));flange=flange.fuse(hub).removeSplitter()
objs['MotorFlange']=add(doc,'MotorFlange','MOTOR actual nominal OD57 flange 4mm, OD30 hub protrusion 20mm',flange,(.9,.66,.20))
# Front flange mirrors the motor-side unit: 57 disc, 4 thick, 30x20 hub.
# Front bearing housing occupies z=-155.5..-150; 0.5 mm nominal axial gap.
wh=cy(15,20,0,0,-188).cut(cy(6.2,20.02,0,0,-188.01))
whplate=cy(28.5,4,0,0,-192).cut(cy(6.2,4.02,0,0,-192.01))
for j in range(6):
a=2*math.pi*j/6; whplate=whplate.cut(cy(2.75,4.02,24*math.cos(a),24*math.sin(a),-192.01))
wh=wh.fuse(whplate).removeSplitter()
objs['WheelHub']=add(doc,'WheelHub','Wheel flange same 57x4 30x20 (key not modeled)',wh,(.9,.66,.20))
wheel70=Part.read(str(R/'models'/'70转接板.step'));wheel70.rotate(V(),V(1,0,0),180);wheel70.translate(V(0,0,-192))
objs['Wheel70']=add(doc,'Wheel70','Existing 70 adapter - contact and PCD48 phase shown',wheel70,(.82,.83,.85))
# Switch envelope is deliberately only transparent rectangular with generic
# centre bore; 50mm depth includes off-axis connector. Actual contours unknown.
sw=Part.makeBox(110,80,60,V(-55,-40,-149.5)).cut(cy(23.5,60.02,0,0,-149.51))
objs['SwitchEnvelope']=add(doc,'SwitchEnvelope','60mm switch and wiring clearance envelope; 47mm bore ASSUMED',sw,(.45,.73,.43),80)
doc.recompute()
# Restrict checks to shapes actually modelled. Intentional shaft-to-hub clearance.
checks=[]
pairs=[('Plate%d'%i,'KFLHousing%d'%i,plates[i].Shape,bearings[2*i].Shape) for i in range(2)]
pairs += [('Plate%d'%i,'KFLInner%d'%i,plates[i].Shape,bearings[2*i+1].Shape) for i in range(2)]
pairs += [('SwitchEnvelope','Cam'+n,sw,cam[n]) for n in ('Upper','Lower','Hardware0','Hardware1','Hardware2','Hardware3')]
pairs += [('MotorFlange','Shaft',flange,cam['Shaft']),('WheelHub','Shaft',wh,cam['Shaft']),('WheelHub','70Adapter',wh,wheel70),('Plate0','Plate1',plates[0].Shape,plates[1].Shape)]
pairs += [('MotorFlange','RearBearing',flange,bearings[0].Shape),('MotorFlange','RearInner',flange,bearings[1].Shape),('MotorFlange','RearPlate',flange,plates[0].Shape),('WheelHub','FrontBearing',wh,bearings[2].Shape),('WheelHub','FrontInner',wh,bearings[3].Shape),('WheelHub','FrontPlate',wh,plates[1].Shape),('MotorFlange','Conversion',flange,conv)]
pairs += [('SwitchEnvelope','Plate0',sw,plates[0].Shape),('SwitchEnvelope','Plate1',sw,plates[1].Shape)]
pairs += [('SwitchEnvelope','KFLHousing0',sw,bearings[0].Shape),('SwitchEnvelope','KFLHousing1',sw,bearings[2].Shape)]
pairs += [('Key','Shaft',cam['Key'],cam['Shaft']),('Key','CamUpper',cam['Key'],cam['Upper']),('Key','CamLower',cam['Key'],cam['Lower'])]
pairs += [('CamUpper','Shaft',cam['Upper'],cam['Shaft']),('CamLower','Shaft',cam['Lower'],cam['Shaft'])]
pairs += [('CamUpper','RearKFL',cam['Upper'],bearings[0].Shape),('CamUpper','FrontKFL',cam['Upper'],bearings[2].Shape)]
pairs += [('CamLower','RearKFL',cam['Lower'],bearings[0].Shape),('CamLower','FrontKFL',cam['Lower'],bearings[2].Shape)]
pairs += [('Shaft','Wheel70',cam['Shaft'],wheel70),('WheelHub','SwitchEnvelope',wh,sw),('MotorFlange','SwitchEnvelope',flange,sw)]
for a,b,sa,sb in pairs:
checks.append('%s / %s = %.6f mm3'%(a,b,booleanvol(sa,sb)))
# Probe through both mated plates at the nominal six 48mm bolt centers.
for j in range(6):
a=2*math.pi*j/6; probe=cy(1.0,12.0,24*math.cos(a),24*math.sin(a),-200)
checks.append('Wheel bolt path %d: shaft probe / flange %.8f / 70plate %.8f mm3'%(j,booleanvol(probe,wh),booleanvol(probe,wheel70)))
for j in range(6):
a=math.pi/6+2*math.pi*j/6; probe=cy(1.0,12.0,24*math.cos(a),24*math.sin(a),-50)
checks.append('Motor bolt path %d: probe / flange %.8f / converter %.8f mm3'%(j,booleanvol(probe,flange),booleanvol(probe,conv)))
for n,s in [('RearConflict',flange.common(bearings[0].Shape)),('FrontConflict',wh.common(bearings[2].Shape))]:
if not s.isNull() and s.Volume>.001: add(doc,n,'RED: nominal collision '+n,s,(1.0,.03,.03))
z_end=-56.5-shaftlen
front_overlap=max(0,min(-168,-56.5)-max(-188,z_end))
plate_overlap=max(0,min(-188,-56.5)-max(-192,z_end))
lines=['CONCEPT ONLY; NOT ASSEMBLY FIT VALIDATION.',
'Both flanges: disc OD57 thickness4, 6x PCD48 5.5 clearance; hub OD30 protrudes 20 from the base. Mirrored.',
'Shaft nominal 12 x %d; z=%.1f..-56.5; motor hub z=-70..-50; motor insertion 13.5mm assumed and axial gap to base 6.5mm.'%(shaftlen,z_end),
'Wheel hub z=-188..-168, front plate z=-192..-188; hub engagement %.1f mm; plate bore overlap %.1f mm (this is not extra key engagement).'%(front_overlap,plate_overlap),
'Rear bearing retained; front bearing shifted 12mm to wheel; switch 60mm with 1mm nominal gap on each side, cam shifted 6mm; rear/front hub clearance to simplified outer bearing 0.5mm nominal.',
'KFL approximate 48 bolt pitch and 14.5 inner width, housing/protrusion guessed; 8mm plate 30.2 hole guessed; 60mm switch/wire envelope placeholder, its 47mm hole is an assumption.',
'Wheel70 is real STEP, mirror/phase0, rear face in contact with front flange disc, PCD48 hole centers align nominally; bolt direction, washer and tool clearances not checked.',
'Real converter/70 STEP hole OD4.234, fastener M4 feasible only with washers and actual size checks; M5 cannot pass nominally; hole intersections/phase and actual hardware must be checked.',
'Shaft endpoint keys not modeled; front hub insertion alone does not establish permissible torque. Actual axial gap, M3 nut, alignments, float bearing arrangement, switch/fan/angles, bolt heads, tool accessibility, and fit/strength not verified.',
'Checked positive-volume intersections of listed pairs only:']+checks
(O/'geometry_check.txt').write_text('\n'.join(lines)+'\n',encoding='utf8')
doc.saveAs(str(O/'Assembly.FCStd'));Import.export([o for o in doc.Objects if hasattr(o,'Shape')],str(O/'Assembly.step'))
view=G.activeDocument().activeView();G.activateWorkbench('PartWorkbench')
def save(name,mode):
if mode=='side':view.viewFront()
else:
view.viewAxonometric();G.updateGui();view.getCameraNode().orientation.setValue(coin.SbRotation(.880476,.115917,-.279848,-.364705))
G.updateGui();time.sleep(.7);view.fitAll();G.updateGui();time.sleep(.7);view.saveImage(str(O/name),1800,1250,'White')
save('assembly.png','axon');save('side.png','side')
# Simplified focus hides rear encoder/rails so bearing through plate is visible.
for n,o in objs.items():
if n in existing or n.startswith('NewRail') or n=='Conversion':o.ViewObject.Visibility=False
for o in doc.Objects:
if o.Name.startswith('L_'):o.ViewObject.Visibility=False
save('focus.png','axon')
for o in doc.Objects:
if hasattr(o,'Shape'):o.ViewObject.Visibility=True
doc.recompute();doc.saveAs(str(O/'Assembly.FCStd'))
(O/'DONE.txt').write_text('ok',encoding='utf8')
except Exception:
(O/'ERROR.txt').write_text(traceback.format_exc(),encoding='utf8');raise

View File

@@ -0,0 +1,30 @@
from pathlib import Path
root = Path(r'C:\projects\wheel')
source = root / 'scripts' / 'build_kfl_125_switch60.py'
text = source.read_text(encoding='utf8')
replacements = [
("shaftlen=125; O=R/'models'/'kfl_125_switch60_concept'", "shaftlen=150; O=R/'models'/'kfl_150_switch60_concept'"),
("-108 if n=='Shaft' else -118", "-108 if n=='Shaft' else -130.5"),
("V(-2.05,4,-125)", "V(-2.05,4,-137.5)"),
("KFL125_Switch60_Schematic", "KFL150_Switch60_Schematic"),
("Part.makeBox(20,20,200,V(x-10,-75,-180))", "Part.makeBox(20,20,225,V(x-10,-75,-205))"),
("enumerate([-84,-162])", "enumerate([-84,-187])"),
("[(-84,-88.5,1),(-162,-165,-1)]", "[(-84,-88.5,1),(-187,-190,-1)]"),
("cy(15,20,0,0,-188).cut(cy(6.2,20.02,0,0,-188.01))", "cy(15,20,0,0,-213).cut(cy(6.2,20.02,0,0,-213.01))"),
("cy(28.5,4,0,0,-192).cut(cy(6.2,4.02,0,0,-192.01))", "cy(28.5,4,0,0,-217).cut(cy(6.2,4.02,0,0,-217.01))"),
("24*math.sin(a),-192.01", "24*math.sin(a),-217.01"),
("wheel70.translate(V(0,0,-192))", "wheel70.translate(V(0,0,-217))"),
("Part.makeBox(110,80,60,V(-55,-40,-149.5)).cut(cy(23.5,60.02,0,0,-149.51))", "Part.makeBox(110,80,60,V(-55,-40,-162)).cut(cy(23.5,60.02,0,0,-162.01))"),
("24*math.sin(a),-200)", "24*math.sin(a),-225)"),
("min(-168,-56.5)-max(-188,z_end)", "min(-193,-56.5)-max(-213,z_end)"),
("min(-188,-56.5)-max(-192,z_end)", "min(-213,-56.5)-max(-217,z_end)"),
("Wheel hub z=-188..-168, front plate z=-192..-188", "Wheel hub z=-213..-193, front plate z=-217..-213"),
("Rear bearing retained; front bearing shifted 12mm to wheel; switch 60mm with 1mm nominal gap on each side, cam shifted 6mm;", "Rear bearing retained; front bearing shifted 25mm forward from the 125mm layout; switch 60mm with 13.5mm nominal gap on each side, cam shifted 12.5mm;"),
]
for old, new in replacements:
count = text.count(old)
if count != 1:
raise RuntimeError('Expected one occurrence, got %d: %r' % (count, old))
text = text.replace(old, new)
exec(compile(text, str(source), 'exec'), {'__name__': '__main__', '__file__': str(source)})

View File

@@ -0,0 +1,99 @@
from pathlib import Path
import os, time, traceback
import FreeCAD as A, FreeCADGui as G, Part, Import
from pivy import coin
V=A.Vector
R=Path(r'C:\projects\wheel'); O=R/'models'/'kfl_3030_concept'; O.mkdir(parents=True,exist_ok=True)
def box(x,y,z,dx,dy,dz): return Part.makeBox(dx,dy,dz,V(x,y,z))
def cy(r,h,x,y,z,d=V(0,0,1)): return Part.makeCylinder(r,h,V(x,y,z),d)
def sx(x,y,z,r,h): return box(x-2,y-r,z,4,2*r,h).fuse(cy(r,h,x-2,y,z)).fuse(cy(r,h,x+2,y,z))
def sy(x,lo,hi,z,r,h): return box(x-r,lo,z,2*r,hi-lo,h).fuse(cy(r,h,x,lo,z)).fuse(cy(r,h,x,hi,z))
def sz(x,y,lo,hi,r,h): return box(x-r,y,lo,2*r,h,hi-lo).fuse(cy(r,h,x,y,lo,V(0,1,0))).fuse(cy(r,h,x,y,hi,V(0,1,0)))
def add(d,n,s,c):
o=d.addObject('PartDesign::Feature',n);o.Shape=s.removeSplitter();o.Label=n;o.ViewObject.ShapeColor=c;o.ViewObject.LineColor=(.15,.15,.15);return o
def cv(a,b): return a.common(b).Volume if a.BoundBox.intersect(b.BoundBox) else 0.
ok=False
try:
d=A.openDocument(str(R/'models'/'kfl_double_crossbar_concept'/'Assembly.FCStd'));d.Label='3030 base and purchased long-slotted angles (concept)'
for o in list(d.Objects):
if o.Name.startswith(('CompactPlate','LongAngle','CrossAngle','Crossbeam','BoardM5_','FootM5_','CrossTopM5_','CrossSideM5_')):d.removeObject(o.Name)
d.recompute()
for i,x in enumerate((-50.6200577,50.6200577)):
q=d.getObject('NewRail%d'%i);q.Shape=box(x-15,-85,-215,30,30,235);q.Label='3030 longitudinal base rail - solid envelope, groove omitted'
parts=[];plates=[];cross=[];angles=[];fast=[]
for i,z in enumerate((-84.,-187.)):
face=z if i==0 else z+8; sign=-1 if i==0 else 1
# Crossbeams butt between longitudinal members at the same height, centreline +/-35.6200577.
z0=face-30 if i==0 else face
cb=add(d,'Crossbeam%d'%i,box(-35.6200577,-87,z0,71.2401154,30,30),(.46,.49,.53));cross.append(cb)
p=box(-38,-51,z,76,80,8).cut(cy(19.5,8.04,0,0,z-.02))
for x in (-24.,24.):p=p.cut(cy(3.5,8.04,x,0,z-.02))
for x in (-23.5,23.5):p=p.cut(sx(x,-42,z-.02,2.75,8.04))
plates.append(add(d,'CompactPlate%d'%i,p,(.72,.74,.78)))
for j,side in enumerate((-1,1)):
x=side*23.5
# Overall 50 mm upright includes 3 mm foot; 33mm long slot 6 mm from inside bend.
vz=face-3 if i==0 else face
upright=box(x-9.5,-54,vz,19,47,3).cut(sy(x,-44.75,-18.25,vz-.02,3.25,3.04))
fz=face-31 if i==0 else face
# Short leg 31, 16 mm slot position assumed centered; T slot at 15 mm.
if i==0: lo,hi=face-20.25,face-10.75
else: lo,hi=face+10.75,face+20.25
foot=box(x-9.5,-57,fz,19,3,31).cut(sz(x,-57.02,lo,hi,3.25,3.04))
a=add(d,'PurchasedAngle%d%d'%(i,j),upright.fuse(foot),(.95,.5,.12));angles.append(a)
if i==1: stem=cy(2.45,18,x,-42,z-4);head=cy(4.25,3,x,-42,face+4);nut=cy(4.25,3,x,-42,z-3);wz=face+3
else: stem=cy(2.45,18,x,-42,z-5);head=cy(4.25,3,x,-42,face-7);nut=cy(4.25,3,x,-42,z+8);wz=face-4
washer=cy(5.5,1,x,-42,wz).cut(cy(2.75,1.02,x,-42,wz-.01))
fast.append(add(d,'BoardM5_%d%d'%(i,j),stem.fuse(head).fuse(nut).fuse(washer),(.14,.15,.17)))
cz=face+sign*15
# Shank intentionally omitted within solid extrusion; actual T slot is not modelled.
fast.append(add(d,'FootHead%d%d'%(i,j),cy(5.5,1,x,-54,cz,V(0,1,0)).cut(cy(2.75,1.02,x,-54.01,cz,V(0,1,0))).fuse(cy(4.25,3,x,-53,cz,V(0,1,0))),(.14,.15,.17)))
# Side connector concept: to rail inward vertical face and crossbeam top. 20 mm angle, one per end.
# Top leg occupies outside edge, held clear of bearing-plate long angles.
inner=side*35.6200577
if side<0: xa=inner; vx=inner; xb=inner+10
else: xa=inner-20; vx=inner-3; xb=inner-10
sidez=z0+30 if i==0 else z0-3
vz0=z0+30 if i==0 else z0-20
cz2=(vz0+10)
top=box(xa,-80,sidez,20,18,3).cut(cy(2.75,3.04,xb,-72,sidez-.02))
vert=box(vx,-80,vz0,3,18,20).cut(cy(2.75,3.04,vx-.02,-70,cz2,V(1,0,0)))
angles.append(add(d,'BaseJoint%d%d'%(i,j),top.fuse(vert),(.78,.38,.08)))
d.recompute()
# Full independent shape-pair screening for relevant structural parts. Suppressed solid T-slot
# screw shanks; inherited switch shape represents only an assumed block around its central opening.
new=[o for o in d.Objects if o.Name.startswith(('CompactPlate','PurchasedAngle','Crossbeam','BaseJoint','NewRail','BoardM5_','FootHead'))]
contextual=[o for o in d.Objects if hasattr(o,'Shape') and not o.Shape.isNull() and o not in new and o.ViewObject.Visibility]
lines=[];bad=[]
for i,a in enumerate(new):
for b in new[i+1:]+contextual:
try:
if not a.Shape.BoundBox.intersect(b.Shape.BoundBox):
lines.append('%s / %s = 0.0000 mm^3 (bounding boxes disjoint)'%(a.Name,b.Name));continue
v=cv(a.Shape,b.Shape)
if v>0.001:bad.append((a.Name,b.Name,v))
lines.append('%s / %s = %.4f mm^3'%(a.Name,b.Name,v))
except Exception as e: lines.append('ERROR '+a.Name+'/'+b.Name+': '+str(e))
(O/'all_checks.txt').write_text('\n'.join(lines)+'\n',encoding='utf8')
(O/'positive_intersections.txt').write_text('\n'.join('%s / %s = %.4f mm^3'%x for x in bad) or 'none',encoding='utf8')
(O/'visible_parts.txt').write_text('\n'.join('%s %s %s'%(o.Name,o.Label,str(o.Shape.BoundBox)) for o in contextual),encoding='utf8')
d.saveAs(str(O/'Assembly.FCStd'));Import.export([o for o in d.Objects if hasattr(o,'Shape')],str(O/'Assembly.step'))
view=G.activeDocument().activeView();G.activateWorkbench('PartWorkbench')
def save(name,mode):
if mode=='side':view.viewFront()
elif mode=='plate':view.viewBottom()
else:view.viewAxonometric();G.updateGui();view.getCameraNode().orientation.setValue(coin.SbRotation(.880476,.115917,-.279848,-.364705))
G.updateGui();time.sleep(.3);view.fitAll();G.updateGui();time.sleep(.4);view.saveImage(str(O/name),1800,1250,'White')
save('assembly.png','axon');save('side.png','side')
vis={o.Name:o.ViewObject.Visibility for o in d.Objects if hasattr(o,'Shape')}
show=set([o.Name for o in plates+angles+cross+fast]+['NewRail0','NewRail1','KFLHousing0','KFLHousing1','KFLInner0','KFLInner1','CamShaft'])
for o in d.Objects:
if hasattr(o,'Shape'):o.ViewObject.Visibility=o.Name in show
save('frame.png','axon');save('plate.png','plate')
for o in d.Objects:
if hasattr(o,'Shape'):o.ViewObject.Visibility=vis.get(o.Name,True)
d.recompute();d.saveAs(str(O/'Assembly.FCStd'));A.closeDocument(d.Name)
(O/'DONE.txt').write_text('ok',encoding='utf8');ok=True
except Exception:
(O/'ERROR.txt').write_text(traceback.format_exc(),encoding='utf8')
finally:os._exit(0 if ok else 1)

View File

@@ -0,0 +1,160 @@
from pathlib import Path
import math, time, traceback
import FreeCAD as A, FreeCADGui as G, Part, Import
from pivy import coin
V=A.Vector
R=Path(r'C:\projects\wheel'); O=R/'models'/'kfl_compact80_concept'; O.mkdir(parents=True,exist_ok=True)
def box(x,y,z,dx,dy,dz): return Part.makeBox(dx,dy,dz,V(x,y,z))
def c(r,h,x,y,z,d=V(0,0,1)): return Part.makeCylinder(r,h,V(x,y,z),d)
def slotx(x,y,z,r,h):return box(x-2,y-r,z,4,2*r,h).fuse(c(r,h,x-2,y,z)).fuse(c(r,h,x+2,y,z))
def sloty(x,y,z,r,h):return box(x-r,y-2,z,2*r,4,h).fuse(c(r,h,x,y-2,z)).fuse(c(r,h,x,y+2,z))
def add(d,n,s,col,trans=0):
o=d.addObject('PartDesign::Feature',n);o.Label=n;o.Shape=s.removeSplitter();o.ViewObject.ShapeColor=col;o.ViewObject.LineColor=(.1,.1,.1);o.ViewObject.Transparency=trans;return o
def cv(a,b): return a.common(b).Volume if a.BoundBox.intersect(b.BoundBox) else 0.
try:
d=A.openDocument(str(R/'models'/'kfl_plate_b38_concept'/'Assembly.FCStd'));d.Label='Compact 80x80 plates and one transverse beam'
for i in range(2):
for j in range(2):
for prefix in ('RealCorner','BoardFast','RailFast','Tnut'):
n='%s%d%d'%(prefix,i,j)
if d.getObject(n):d.removeObject(n)
if d.getObject('AdjustPlate%d'%i):d.removeObject('AdjustPlate%d'%i)
d.recompute()
plates=[]; angles=[]; bolts=[]
for i,z in enumerate((-84.,-187.)):
p=box(-38,-51,z,76,80,8).cut(c(19.5,8.04,0,0,z-.02))
for x in (-24.,24.):p=p.cut(c(3.5,8.04,x,0,z-.02))
for x in (-27.5,27.5):p=p.cut(slotx(x,-44,z-.02,2.75,8.04))
p=add(d,'CompactPlate%d'%i,p,(.7,.72,.76));plates.append(p)
face=z+8 if i==0 else z
sign=1 if i==0 else -1
lpz=face if sign==1 else face-3
lsz=face if sign==1 else face-20
zrail=face+10 if sign==1 else face-10
for j,side in enumerate((-1,1)):
# Rail side leg is against the inner x face; plate leg is on its axial face.
xmin=-40.6200577 if side==-1 else 20.6200577
plateleg=box(xmin,-74,lpz,20,18,3)
sx=side*27.5
plateleg=plateleg.cut(slotx(sx,-64,lpz-.02,2.75,3.04))
side_x=-40.6200577 if side==-1 else 37.6200577
sideleg=box(side_x,-74,lsz,3,18,20)
sideleg=sideleg.cut(c(2.75,3.04,side_x-.02,-65,zrail,V(1,0,0)))
angle=add(d,'SideAngle%d%d'%(i,j),plateleg.fuse(sideleg),(.9,.5,.15));angles.append(angle)
# Flat strip and spacer bridge normal 18mm angle to the compact plate.
linkz=face+3 if i==0 else face-6
link=box(sx-9,-70,linkz,18,40,3)
link=link.cut(c(2.75,3.04,sx,-64,linkz-.02))
link=link.cut(sloty(sx,-44,linkz-.02,2.75,3.04))
angles.append(add(d,'LinkPlate%d%d'%(i,j),link,(.92,.65,.23)))
spacerz=face if i==0 else face-3
spacer=c(5,3,sx,-44,spacerz).cut(c(2.75,3.04,sx,-44,spacerz-.02))
angles.append(add(d,'LinkSpacer%d%d'%(i,j),spacer,(.7,.72,.75)))
stem=c(2.45,22,sx,-44,z-10 if i==1 else z-4)
if i==0: head=c(4.25,2.75,sx,-44,face+7);nut=c(4.25,3,sx,-44,z-3);wz=face+6
else: head=c(4.25,2.75,sx,-44,z-9.75);nut=c(4.25,3,sx,-44,z+8);wz=z-7
washer=c(5,1,sx,-44,wz).cut(c(2.75,1.02,sx,-44,wz-.01))
fast=add(d,'PlateM5_%d%d'%(i,j),stem.fuse(head).fuse(nut).fuse(washer),(.12,.13,.15));bolts.append(fast)
linkshank=c(2.45,13,sx,-64,face-3 if i==0 else face-10)
if i==0: linkhead=c(4.25,2.75,sx,-64,face+7);linknut=c(4.25,3,sx,-64,face-3);linkwz=face+6
else: linkhead=c(4.25,2.75,sx,-64,face-9.75);linknut=c(4.25,3,sx,-64,face);linkwz=face-7
linkwasher=c(5,1,sx,-64,linkwz).cut(c(2.75,1.02,sx,-64,linkwz-.01))
bolts.append(add(d,'LinkM5_%d%d'%(i,j),linkshank.fuse(linkhead).fuse(linknut).fuse(linkwasher),(.12,.13,.15)))
# Side bolt enters the T-slot on the inner face of the longitudinal rail.
if side==-1:
shank=c(2.45,11,-46,-65,zrail,V(1,0,0));head=c(4.25,2.75,-37.6200577,-65,zrail,V(1,0,0));nut=box(-45,-69,zrail-4,4,8,8)
else:
shank=c(2.45,11,35,-65,zrail,V(1,0,0));head=c(4.25,2.75,37.6200577,-65,zrail,V(-1,0,0));nut=box(41,-69,zrail-4,4,8,8)
b=add(d,'RailM5_%d%d'%(i,j),shank.fuse(head),(.14,.15,.17));bolts.append(b)
add(d,'RailTnut_%d%d'%(i,j),nut,(.67,.67,.69))
# Single X-directed cross beam: below both lengthwise rails, halfway between plates.
cross=add(d,'SingleCrossbeam',box(-80,-95,-146,160,20,20),(.46,.49,.53))
crossangles=[]
for j,side in enumerate((-1,1)):
# Two ordinary right angles outside the long beams, attaching the crossmember to each.
x0=-80 if side==-1 else 60.6200577
horiz=box(x0,-75,-145,19.3799423,3,18)
vertx=-63.6200577 if side==-1 else 60.6200577
vert=box(vertx,-75,-145,3,20,18)
hx=-70 if side==-1 else 70
horiz=horiz.cut(c(2.75,3.04,hx,-75.02,-136,V(0,1,0)))
vert=vert.cut(c(2.75,3.04,vertx-.02,-65,-136,V(1,0,0)))
ca=add(d,'CrossAngle%d'%j,horiz.fuse(vert),(.85,.48,.15));crossangles.append(ca)
# Low-head M5 fastener indications; no T-slot channels modeled.
ba=c(2.4,10,hx,-82,-136,V(0,1,0)).fuse(c(4,2,hx,-72,-136,V(0,1,0)))
if side==-1: bb=c(2.4,10,-67,-65,-136,V(1,0,0)).fuse(c(4,2,-63.6200577,-65,-136,V(-1,0,0)))
else: bb=c(2.4,10,57,-65,-136,V(1,0,0)).fuse(c(4,2,63.6200577,-65,-136,V(1,0,0)))
bolts.append(add(d,'CrossFastTop%d'%j,ba,(.14,.15,.17)))
bolts.append(add(d,'CrossFastSide%d'%j,bb,(.14,.15,.17)))
d.recompute()
tests=[]
def check(a,b):
ao=d.getObject(a);bo=d.getObject(b)
if ao and bo:tests.append((a,b,cv(ao.Shape,bo.Shape)))
for i in range(2):
for n in ('KFLHousing%d'%i,'KFLInner%d'%i,'Motor','MotorFlange','WheelHub','Wheel70','SwitchEnvelope','NewRail0','NewRail1','SingleCrossbeam'):
check('CompactPlate%d'%i,n)
for j in range(2):
for n in ('CompactPlate%d'%i,'NewRail%d'%j,'Motor','MotorFlange','WheelHub','Wheel70','SwitchEnvelope','SingleCrossbeam','PlateM5_%d%d'%(i,j),'RailM5_%d%d'%(i,j)):
check('SideAngle%d%d'%(i,j),n)
check('PlateM5_%d%d'%(i,j),'RailM5_%d%d'%(i,j))
check('LinkM5_%d%d'%(i,j),'RailM5_%d%d'%(i,j))
check('LinkM5_%d%d'%(i,j),'LinkPlate%d%d'%(i,j))
check('PlateM5_%d%d'%(i,j),'LinkPlate%d%d'%(i,j))
check('PlateM5_%d%d'%(i,j),'CompactPlate%d'%i)
check('LinkPlate%d%d'%(i,j),'CompactPlate%d'%i)
check('LinkSpacer%d%d'%(i,j),'CompactPlate%d'%i)
for n in ('Motor','MotorFlange','WheelHub','Wheel70','SwitchEnvelope','NewRail%d'%j):check('LinkPlate%d%d'%(i,j),n)
for j in range(2):
for n in ('NewRail%d'%j,'SingleCrossbeam','SwitchEnvelope','Motor'):
check('CrossAngle%d'%j,n)
check('CrossFastTop%d'%j,'CrossFastSide%d'%j)
for n in ('NewRail0','NewRail1','SwitchEnvelope','Motor','CamShaft','CamUpper','CamLower'):
check('SingleCrossbeam',n)
motion=[]
for i in range(2):
for j in range(2):
a=d.getObject('SideAngle%d%d'%(i,j)).Shape
rail=d.getObject('NewRail%d'%j).Shape
railbolt=d.getObject('RailM5_%d%d'%(i,j)).Shape
for dx in (-2.,0.,2.):
for dy in (-2.,0.,2.):
pl=plates[i].Shape.copy();pl.translate(V(dx,dy,0))
link=d.getObject('LinkPlate%d%d'%(i,j)).Shape.copy();link.translate(V(dx,0,0))
lb=d.getObject('LinkM5_%d%d'%(i,j)).Shape.copy();lb.translate(V(dx,0,0))
pb=d.getObject('PlateM5_%d%d'%(i,j)).Shape.copy();pb.translate(V(dx,dy,0))
vals=[cv(pl,rail),cv(lb,a),cv(lb,railbolt),cv(pb,link),cv(pb,railbolt),cv(link,rail)]
motion.append((i,j,dx,dy,max(vals)))
d.recompute()
lines=['CONCEPT ONLY; NOT FOR CNC, PRINT, FINAL CUT OR SAFE-FIT APPROVAL.',
'Two 76x80x8 plates span x=-38..38,y=-51..29; 39 bore, 48 bolt spacing, 7 mounting holes.',
'Longitudinal profiles are solid schematic 2020, inner x=+/-40.6200577; plate-to-rail lateral nominal gap 2.6200577 mm.',
'Each plate connected to both longitudinal rails by side angles. Crossbeam is ONE 2020 below both rails between plates, reinforcing frame; it does NOT directly support both plates.',
'Crossbeam extent 160x20x20 schematic; stock angles approximated 20x20x18x3, M5 hex-head envelopes and T nuts illustrative; no T slot geometry.',
'Four additional 18x40x3 flat links and 3mm spacers bridge standard 18mm-wide side angles to the plates; the side angle T-slot bolt is centred on rail at y=-65.',
'Compact plate slot 9.5x5.5 along X crosses the flat link upper slot 9.5x5.5 along Y: nominal ~2mm adjustment each direction assuming nominal M5, subject to hardware and shaft constraints. Standard angle oval leg slot follows leg length X. Plate-to-rail margin 2.62mm.',
'Crossbeam fastening T nuts and head/tool accessibility, hardware tolerances, structural stiffness and angular adjuster remain unvalidated; switch geometry and axis are approximate.',
'KFL cylindrical protrusion OD38 is assumed b envelope, actual shape / bore OD39 must be verified; M6 washer OD12 partly over central hole. No tolerance or strength conclusion.',
'Selected positive volume intersection checks (mm3), any rail versus fastener may represent intentionally undrawn T-slot; inspect individually:']+[f'{a} / {b} = {v:.6f}' for a,b,v in tests]+['Sampled adjustments, max selected overlap mm3:']+[f'{i}{j} dx={dx:+.1f} dy={dy:+.1f}: {v:.6f}' for i,j,dx,dy,v in motion]
(O/'geometry_check.txt').write_text('\n'.join(lines)+'\n',encoding='utf8')
(O/'bad.txt').write_text('\n'.join([f'{a}/{b}: {v:.6f}' for a,b,v in tests if v>.001]+[f'motion {i}{j} dx={dx} dy={dy}: {v:.6f}' for i,j,dx,dy,v in motion if v>.001]) or 'none',encoding='utf8')
d.saveAs(str(O/'Assembly.FCStd'));Import.export([o for o in d.Objects if hasattr(o,'Shape')],str(O/'Assembly.step'))
view=G.activeDocument().activeView();G.activateWorkbench('PartWorkbench')
def save(name,mode):
if mode=='side':view.viewFront()
elif mode=='front':view.viewBottom()
else:view.viewAxonometric();G.updateGui();view.getCameraNode().orientation.setValue(coin.SbRotation(.880476,.115917,-.279848,-.364705))
G.updateGui();time.sleep(.5);view.fitAll();G.updateGui();time.sleep(.5);view.saveImage(str(O/name),1800,1250,'White')
save('assembly.png','axon');save('side.png','side')
visible={o.Name:o.ViewObject.Visibility for o in d.Objects if hasattr(o,'Shape')}
show=set(['CompactPlate0','CompactPlate1','SingleCrossbeam','NewRail0','NewRail1','KFLHousing0','KFLHousing1','KFLInner0','KFLInner1','CamShaft']+[o.Name for o in angles+crossangles+bolts])
for o in d.Objects:
if hasattr(o,'Shape'):o.ViewObject.Visibility=o.Name in show
save('frame.png','axon');save('plate.png','front')
for o in d.Objects:
if hasattr(o,'Shape'):o.ViewObject.Visibility=visible.get(o.Name,True)
d.recompute();d.saveAs(str(O/'Assembly.FCStd'))
if (O/'ERROR.txt').exists(): (O/'ERROR.txt').unlink()
(O/'DONE.txt').write_text('ok',encoding='utf8')
except Exception:
(O/'ERROR.txt').write_text(traceback.format_exc(),encoding='utf8');raise

View File

@@ -0,0 +1,106 @@
from pathlib import Path
import os, time, traceback
import FreeCAD as A, FreeCADGui as G, Part, Import
from pivy import coin
V=A.Vector
R=Path(r'C:\projects\wheel'); O=R/'models'/'kfl_double_crossbar_concept'; O.mkdir(parents=True,exist_ok=True)
def box(x,y,z,dx,dy,dz): return Part.makeBox(dx,dy,dz,V(x,y,z))
def cyl(r,h,x,y,z,d=V(0,0,1)): return Part.makeCylinder(r,h,V(x,y,z),d)
def slotx(x,y,z,r,h): return box(x-2,y-r,z,4,2*r,h).fuse(cyl(r,h,x-2,y,z)).fuse(cyl(r,h,x+2,y,z))
def sloty(x,y,z,r,h): return box(x-r,y-2,z,2*r,4,h).fuse(cyl(r,h,x,y-2,z)).fuse(cyl(r,h,x,y+2,z))
def add(d,n,s,col):
o=d.addObject('PartDesign::Feature',n);o.Shape=s.removeSplitter();o.Label=n;o.ViewObject.ShapeColor=col;o.ViewObject.LineColor=(.15,.15,.15);return o
def cv(a,b): return a.common(b).Volume if a.BoundBox.intersect(b.BoundBox) else 0.
ok=False
try:
d=A.openDocument(str(R/'models'/'kfl_plate_b38_concept'/'Assembly.FCStd'));d.Label='Two crossbeams, KFL001 reversed - concept'
for i in range(2):
for j in range(2):
for prefix in ('RealCorner','BoardFast','RailFast','Tnut'):
n='%s%d%d'%(prefix,i,j)
if d.getObject(n):d.removeObject(n)
if d.getObject('AdjustPlate%d'%i):d.removeObject('AdjustPlate%d'%i)
rail=d.getObject('NewRail%d'%i)
x=(-50.6200577,50.6200577)[i]
rail.Shape=box(x-10,-75,-215,20,20,235)
rail.Label='Longitudinal 2020, schematic solid'
d.recompute()
plates=[]; angles=[]; bolts=[]; cross=[]
for i,z in enumerate((-84.,-187.)):
p=box(-38,-51,z,76,80,8).cut(cyl(19.5,8.04,0,0,z-.02))
for x in (-24.,24.):p=p.cut(cyl(3.5,8.04,x,0,z-.02))
for x in (-27.5,27.5):p=p.cut(slotx(x,-42,z-.02,2.75,8.04))
plates.append(add(d,'CompactPlate%d'%i,p,(.72,.74,.78)))
face=z+8 if i==0 else z
z0=face if i==0 else face-20
cz=z0+10
cross.append(add(d,'Crossbeam%d'%i,box(-80,-95,z0,160,20,20),(.46,.49,.53)))
for j,side in enumerate((-1,1)):
x=side*27.5
verticalz=face if i==0 else face-3
upright=box(x-9,-75,verticalz,18,45,3).cut(sloty(x,-42,verticalz-.02,2.75,3.04))
foot=box(x-9,-75,z0,18,3,20).cut(cyl(2.75,3.04,x,-75.02,cz,V(0,1,0)))
angles.append(add(d,'LongAngle%d%d'%(i,j),upright.fuse(foot),(.95,.5,.12)))
if i==0:
stem=cyl(2.45,18,x,-42,z-4);head=cyl(4.25,3,x,-42,face+4);nut=cyl(4.25,3,x,-42,z-3);wz=face+3
else:
stem=cyl(2.45,18,x,-42,z-5);head=cyl(4.25,3,x,-42,face-7);nut=cyl(4.25,3,x,-42,z+8);wz=face-4
washer=cyl(5,1,x,-42,wz).cut(cyl(2.75,1.02,x,-42,wz-.01))
bolts.append(add(d,'BoardM5_%d%d'%(i,j),stem.fuse(head).fuse(nut).fuse(washer),(.14,.15,.17)))
stem=cyl(2.45,10,x,-82,cz,V(0,1,0));head=cyl(4.25,3,x,-72,cz,V(0,1,0))
bolts.append(add(d,'FootM5_%d%d'%(i,j),stem.fuse(head),(.14,.15,.17)))
x0=-80 if side==-1 else 60.6200577
vertx=-63.6200577 if side==-1 else 60.6200577
hx=-70 if side==-1 else 70
horiz=box(x0,-75,cz-9,19.3799423,3,18).cut(cyl(2.75,3.04,hx,-75.02,cz,V(0,1,0)))
vert=box(vertx,-75,cz-9,3,20,18).cut(cyl(2.75,3.04,vertx-.02,-65,cz,V(1,0,0)))
angles.append(add(d,'CrossAngle%d%d'%(i,j),horiz.fuse(vert),(.86,.48,.16)))
bolts.append(add(d,'CrossTopM5_%d%d'%(i,j),cyl(2.45,10,hx,-82,cz,V(0,1,0)).fuse(cyl(4.25,3,hx,-72,cz,V(0,1,0))),(.14,.15,.17)))
if side==-1: sh=cyl(2.45,10,-68,-65,cz,V(1,0,0)).fuse(cyl(4.25,3,-63.6200577,-65,cz,V(-1,0,0)))
else: sh=cyl(2.45,10,58,-65,cz,V(1,0,0)).fuse(cyl(4.25,3,63.6200577,-65,cz,V(1,0,0)))
bolts.append(add(d,'CrossSideM5_%d%d'%(i,j),sh,(.14,.15,.17)))
d.recompute()
tests=[]
def check(a,b):
ao=d.getObject(a);bo=d.getObject(b)
if ao and bo:tests.append((a,b,cv(ao.Shape,bo.Shape)))
for i in range(2):
for n in ('KFLHousing%d'%i,'KFLInner%d'%i,'Motor','MotorFlange','WheelHub','Wheel70','SwitchEnvelope','NewRail0','NewRail1','Crossbeam%d'%i):check('CompactPlate%d'%i,n)
for n in ('NewRail0','NewRail1','SwitchEnvelope','Motor','CamShaft','CamUpper','CamLower'):check('Crossbeam%d'%i,n)
for j in range(2):
long='LongAngle%d%d'%(i,j);crossa='CrossAngle%d%d'%(i,j)
for n in ('CompactPlate%d'%i,'KFLHousing%d'%i,'KFLInner%d'%i,'Motor','MotorFlange','WheelHub','Wheel70','SwitchEnvelope','Crossbeam%d'%i,'NewRail0','NewRail1','BoardM5_%d%d'%(i,j),'FootM5_%d%d'%(i,j),crossa):check(long,n)
for n in ('Crossbeam%d'%i,'NewRail%d'%j,'SwitchEnvelope','Motor','LongAngle%d%d'%(i,j),'CrossTopM5_%d%d'%(i,j),'CrossSideM5_%d%d'%(i,j)):check(crossa,n)
check('BoardM5_%d%d'%(i,j),'CompactPlate%d'%i)
check('FootM5_%d%d'%(i,j),'CrossTopM5_%d%d'%(i,j))
lines=['CONCEPT ONLY: schematic profile envelopes and simplified hardware; not CNC/manufacturing drawings.',
'Two plate sizes 76x80x8; 39 bore assumed, KFL bolt centres 48 and clearance holes 7. Shaft diameter 12 x 150; switch 60 mm envelope.',
'Crossbars x -80..80, y -95..-75, z -76..-56 / -207..-187, each beneath longitudinal profiles y -75..-55.',
'Four assumed bent metal angles: outer legs 45 x 20, width 18, thickness 3, actual bend radius unspecified; foot round hole diameter 5.5, vertical 9.5 x 5.5 slot Y; plate slot 9.5 x 5.5 X at y=-42.',
'Proposed nominal M5 adjustment about +/-2mm each axis; requires testing of all actual washer, shaft, bearing and interference conditions. Each plate washer OD10 at slot lower travel remains within plate edge by nominal 2mm.',
'T slots shown as SOLID 2020 ENVELOPES: fastener shanks intentionally enter solid beam; do not treat as interference free, actual T nut and bolt depth must be checked.',
'Four standard corner angles join crossbars to long profiles, two per crossbar. Screw access, T nuts, actual slot geometry, bending radii, switch casing and strength unverified.',
'Selected static common volume tests mm3; zero volume checks apply ONLY to modeled shapes and those specific pairs:']+[f'{a}/{b}={v:.5f}' for a,b,v in tests]
(O/'geometry_check.txt').write_text('\n'.join(lines)+'\n',encoding='utf8')
(O/'positive_intersections.txt').write_text('\n'.join(f'{a}/{b}: {v:.5f}' for a,b,v in tests if v>.001) or 'none',encoding='utf8')
d.saveAs(str(O/'Assembly.FCStd'));Import.export([o for o in d.Objects if hasattr(o,'Shape')],str(O/'Assembly.step'))
view=G.activeDocument().activeView();G.activateWorkbench('PartWorkbench')
def save(name,mode):
if mode=='side':view.viewFront()
elif mode=='front':view.viewBottom()
else:view.viewAxonometric();G.updateGui();view.getCameraNode().orientation.setValue(coin.SbRotation(.880476,.115917,-.279848,-.364705))
G.updateGui();time.sleep(.3);view.fitAll();G.updateGui();time.sleep(.4);view.saveImage(str(O/name),1800,1250,'White')
save('assembly.png','axon');save('side.png','side')
visible={o.Name:o.ViewObject.Visibility for o in d.Objects if hasattr(o,'Shape')}
show=set([o.Name for o in plates+angles+bolts+cross]+['NewRail0','NewRail1','KFLHousing0','KFLHousing1','KFLInner0','KFLInner1','CamShaft'])
for o in d.Objects:
if hasattr(o,'Shape'):o.ViewObject.Visibility=o.Name in show
save('frame.png','axon');save('plate.png','front')
for o in d.Objects:
if hasattr(o,'Shape'):o.ViewObject.Visibility=visible.get(o.Name,True)
d.recompute();d.saveAs(str(O/'Assembly.FCStd'));A.closeDocument(d.Name)
(O/'DONE.txt').write_text('ok',encoding='utf8');ok=True
except Exception:
(O/'ERROR.txt').write_text(traceback.format_exc(),encoding='utf8')
finally:
os._exit(0 if ok else 1)

View File

@@ -0,0 +1,127 @@
from pathlib import Path
import math, time, traceback, os
import FreeCAD as A, FreeCADGui as G, Part, Import
from pivy import coin
V=A.Vector
R=Path(r'C:\projects\wheel'); variant=os.environ.get('KFL_HUB_VARIANT')=='narrow';O=R/'models'/'kfl_100_flange57_concept';O.mkdir(parents=True,exist_ok=True)
def cy(r,h,x=0,y=0,z=0):return Part.makeCylinder(r,h,V(x,y,z))
def add(d,n,label,s,c,t=0):
o=d.addObject('PartDesign::Feature',n);o.Label=label;o.Shape=s;o.ViewObject.ShapeColor=c;o.ViewObject.LineColor=(.08,.08,.08);o.ViewObject.Transparency=t;return o
def booleanvol(a,b):
return a.common(b).Volume if a.BoundBox.intersect(b.BoundBox) else 0
try:
src=A.openDocument(str(R/'models'/'pcd50_open_pillar_v1'/'Assembly.FCStd'))
existing={o.Name:(o.Shape.copy(),tuple(o.ViewObject.ShapeColor),o.ViewObject.Transparency) for o in src.Objects if hasattr(o,'Shape') and not o.Shape.isNull()}
A.closeDocument(src.Name)
src=A.openDocument(str(R/'models'/'turn_signal_cam_v2'/'Assembly.FCStd'))
cam={n:src.getObject(n).Shape.copy() for n in ['Shaft','Key','Upper','Lower','Hardware0','Hardware1','Hardware2','Hardware3']}
A.closeDocument(src.Name)
for s in cam.values():s.translate(V(0,0,-102))
doc=A.newDocument('KFL100_Schematic'); objs={}
for n,(s,c,t) in existing.items():
objs[n]=add(doc,n,'Prior existing '+n,s,c,55 if n=='Motor' else t)
for i,x in enumerate([-50.6200577,50.6200577]):
objs['NewRail%d'%i]=add(doc,'NewRail%d'%i,'Proposed 2020 longitudinal rail schematic',Part.makeBox(20,20,190,V(x-10,-75,-170)),(.48,.51,.55))
# Two vertical plates parallel to XY. The 30.2 mm bore is placeholder,
# with dummy bearing protrusion 29 mm OD; NOT a production bore.
plates=[]
for i,z in enumerate([-74,-140]):
s=Part.makeBox(140,90,8,V(-70,-55,z)).cut(cy(15.1,8.02,0,0,z-.01))
for x in (-24,24):s=s.cut(cy(3.5,8.02,x,0,z-.01))
o=add(doc,'Plate%d'%i,'8mm plate: hole 30.2 assumed',s,(.66,.69,.74));plates.append(o)
# Simplified illustrative corner brackets, contacting plate and rail only.
for j,x in enumerate([-50.6200577,50.6200577]):
a=Part.makeBox(16,12,3,V(x-8,-55,z-3))
b=Part.makeBox(16,3,11,V(x-8,-55,z-11))
o=add(doc,'L_%d_%d'%(i,j),'90deg bracket placeholder, fasteners omitted',a.fuse(b).removeSplitter(),(.88,.49,.16))
# Approximate KFL outside dimensions: 63 x 38 mm, pitch 48, flange
# ears 5.5, insert 14.5; protrusion 29mm OD and position are placeholders.
bearings=[]
for i,(plate_z,inner_z,sign) in enumerate([(-74,-78.5,1),(-140,-143,-1)]):
# face of plate towards outside axle area, mirrored.
if sign==1: face=plate_z+8; flangez=face; insertz=face-8
else: face=plate_z; flangez=face-5.5; insertz=face
ring=cy(19,5.5,0,0,flangez).cut(cy(9.5,5.52,0,0,flangez-.01))
for x in (-24,24):
ear=cy(7.5,5.5,x,0,flangez).fuse(Part.makeBox(12,15,5.5,V(min(x,0),-7.5,flangez)))
ring=ring.fuse(ear)
for x in (-24,24):ring=ring.cut(cy(3.5,5.52,x,0,flangez-.01))
ring=ring.cut(cy(9.5,5.52,0,0,flangez-.01))
ring=ring.fuse(cy(14.5,8,0,0,insertz).cut(cy(9.5,8.02,0,0,insertz-.01))).removeSplitter()
outer=add(doc,'KFLHousing%d'%i,'KFL001 housing: 29 protrusion ASSUMED',ring,(.45,.49,.53));bearings.append(outer)
inner=cy(9.2,14.5,0,0,inner_z).cut(cy(6.1,14.52,0,0,inner_z-.01))
bearings.append(add(doc,'KFLInner%d'%i,'KFL inner 14.5mm rotating schematic',inner,(.77,.77,.79)))
# M6 screws only illustrative straight 5.8mm shank; heads omitted.
for j,x in enumerate((-24,24)):
z0=plate_z-1 if sign==1 else flangez-1
o=add(doc,'KFLBolt%d_%d'%(i,j),'M6 shank schematic, heads omitted',cy(2.9,15.5,x,0,z0),(.18,.19,.20));objs[o.Name]=o
# Real shaft/cam from existing project.
colors={'Shaft':(.65,.66,.68),'Key':(.98,.65,.1),'Upper':(.05,.40,.80),'Lower':(.16,.62,.88)}
for n,s in cam.items():objs['Cam'+n]=add(doc,'Cam'+n,'Existing cam V2 '+n,s,colors.get(n,(.2,.2,.21)))
# Actual small conversion disk STEP: mirrored to front face at z=-38.
conv=Part.read(str(R/'models'/'转换盘1.step'));conv.rotate(V(),V(1,0,0),180);conv.translate(V(0,0,-38))
objs['Conversion']=add(doc,'Conversion','Existing conversion disk STEP; face position approx',conv,(.82,.83,.85))
# Six-hole keyed flange is a placeholder, the hub overlaps 12mm of shaft.
flange=cy(28.5,4,0,0,-50).cut(cy(6.2,4.02,0,0,-50.01))
for j in range(6):
a=2*math.pi*j/6+math.pi/6;flange=flange.cut(cy(2.75,4.02,24*math.cos(a),24*math.sin(a),-50.01))
hubradius=10
hub=cy(15,20,0,0,-70).cut(cy(6.2,20.02,0,0,-70.01));flange=flange.fuse(hub).removeSplitter()
objs['MotorFlange']=add(doc,'MotorFlange','MOTOR actual nominal OD57 flange 4mm, OD30 hub protrusion 20mm',flange,(.9,.66,.20))
# Wheel hub is deliberately modelled with only 9mm shaft engagement.
wh=cy(hubradius,9,0,0,-152).cut(cy(6.2,9.02,0,0,-152.01))
whplate=cy(27,3,0,0,-155).cut(cy(6.2,3.02,0,0,-155.01))
for j in range(6):
a=2*math.pi*j/6;whplate=whplate.cut(cy(2.2,3.02,24*math.cos(a),24*math.sin(a),-155.01))
wh=wh.fuse(whplate).removeSplitter()
objs['WheelHub']=add(doc,'WheelHub','Wheel flange schematic: only 9mm engagement unverified',wh,(.9,.66,.20))
wheel70=Part.read(str(R/'models'/'70转接板.step'));wheel70.rotate(V(),V(1,0,0),180);wheel70.translate(V(0,0,-155))
objs['Wheel70']=add(doc,'Wheel70','Existing 70 adapter, nominal assumed position, bolt access unchecked',wheel70,(.82,.83,.85))
# Switch envelope is deliberately only transparent rectangular with generic
# centre bore; 50mm depth includes off-axis connector. Actual contours unknown.
sw=Part.makeBox(110,80,50,V(-55,-40,-128.5)).cut(cy(23.5,50.02,0,0,-128.51))
objs['SwitchEnvelope']=add(doc,'SwitchEnvelope','50mm switch bounding placeholder; 47mm bore ASSUMED',sw,(.45,.73,.43),80)
doc.recompute()
# Restrict checks to shapes actually modelled. Intentional shaft-to-hub clearance.
checks=[]
pairs=[('Plate%d'%i,'KFLHousing%d'%i,plates[i].Shape,bearings[2*i].Shape) for i in range(2)]
pairs += [('Plate%d'%i,'KFLInner%d'%i,plates[i].Shape,bearings[2*i+1].Shape) for i in range(2)]
pairs += [('SwitchEnvelope','Cam'+n,sw,cam[n]) for n in ('Upper','Lower','Hardware0','Hardware1','Hardware2','Hardware3')]
pairs += [('MotorFlange','Shaft',flange,cam['Shaft']),('WheelHub','Shaft',wh,cam['Shaft']),('WheelHub','70Adapter',wh,wheel70),('Plate0','Plate1',plates[0].Shape,plates[1].Shape)]
pairs += [('MotorFlange','RearBearing',flange,bearings[0].Shape),('MotorFlange','RearInner',flange,bearings[1].Shape),('MotorFlange','RearPlate',flange,plates[0].Shape),('WheelHub','FrontBearing',wh,bearings[2].Shape)]
for a,b,sa,sb in pairs:
checks.append('%s / %s = %.6f mm3'%(a,b,booleanvol(sa,sb)))
for n,s in [('RearConflict',flange.common(bearings[0].Shape)),('FrontConflict',wh.common(bearings[2].Shape))]:
if not s.isNull() and s.Volume>.001: add(doc,n,'RED: nominal collision '+n,s,(1.0,.03,.03))
lines=['CONCEPT ONLY; NOT ASSEMBLY FIT VALIDATION.',
'Motor side flange disc OD57 thickness4, six PCD48 holes nominal5.5; hub OD30 extending20 past disc face. Wheel side hub OD20 assumed.',
'Z decreases from motor toward steering wheel; motor rear z0; wheel shaft z=-52..-152 length100.',
'KFL actual nominal 63x38 housing envelope, 48 bolt centers, 7 hole, 5.5 ears, inner 14.5; protrusion OD29 and position assumed.',
'8mm plates central hole OD30.2 assumed, not actual production spec.',
'50mm switch opaque-enclosing envelope z=-128.5..-78.5; opening OD47 ASSUMED only; real rotary features and contacts unknown.',
'Shaft z=-52..-152; motor hub z=-70..-50: actual overlap 18 mm. Rear inner bearing z=-78.5..-64 overlaps motor hub from -70..-64 (6mm). Therefore prior 12mm serial budget is not valid; geometry includes nominal interference. Front hub engagement9 remains an unvalidated assumption.',
'Sleeve20 sits inside switch envelope and is not additional 20mm.',
'Motor mounting boss, shaft end keys, original M3 nut, lights/white contacts, bearing set screws, pin fastening/head/tool and adjuster are not modelled.',
'No workable fit is demonstrated. Real KFL and switch shapes, motor M3 nut and both shaft end details, fastener/tool paths, front hub and contact/clearances are unmeasured.',
'Checked positive-volume intersections (other pairs, loaded operation, exact bearing and switch shape not tested):']+checks
(O/'geometry_check.txt').write_text('\n'.join(lines)+'\n',encoding='utf8')
doc.saveAs(str(O/'Assembly.FCStd'));Import.export([o for o in doc.Objects if hasattr(o,'Shape')],str(O/'Assembly.step'))
view=G.activeDocument().activeView();G.activateWorkbench('PartWorkbench')
def save(name,mode):
if mode=='side':view.viewFront()
else:
view.viewAxonometric();G.updateGui();view.getCameraNode().orientation.setValue(coin.SbRotation(.880476,.115917,-.279848,-.364705))
G.updateGui();time.sleep(.7);view.fitAll();G.updateGui();time.sleep(.7);view.saveImage(str(O/name),1800,1250,'White')
save('assembly.png','axon');save('side.png','side')
# Simplified focus hides rear encoder/rails so bearing through plate is visible.
for n,o in objs.items():
if n in existing or n.startswith('NewRail') or n=='Conversion':o.ViewObject.Visibility=False
for o in doc.Objects:
if o.Name.startswith('L_'):o.ViewObject.Visibility=False
save('focus.png','axon')
for o in doc.Objects:
if hasattr(o,'Shape'):o.ViewObject.Visibility=True
doc.recompute();doc.saveAs(str(O/'Assembly.FCStd'))
(O/'DONE.txt').write_text('ok',encoding='utf8')
except Exception:
(O/'ERROR.txt').write_text(traceback.format_exc(),encoding='utf8');raise

View File

@@ -0,0 +1,128 @@
from pathlib import Path
import math, time, traceback, os
import FreeCAD as A, FreeCADGui as G, Part, Import
from pivy import coin
V=A.Vector
R=Path(r'C:\projects\wheel'); variant=os.environ.get('KFL_HUB_VARIANT')=='narrow';O=R/'models'/'kfl_100_flange57_shift_concept';O.mkdir(parents=True,exist_ok=True)
def cy(r,h,x=0,y=0,z=0):return Part.makeCylinder(r,h,V(x,y,z))
def add(d,n,label,s,c,t=0):
o=d.addObject('PartDesign::Feature',n);o.Label=label;o.Shape=s;o.ViewObject.ShapeColor=c;o.ViewObject.LineColor=(.08,.08,.08);o.ViewObject.Transparency=t;return o
def booleanvol(a,b):
return a.common(b).Volume if a.BoundBox.intersect(b.BoundBox) else 0
try:
src=A.openDocument(str(R/'models'/'pcd50_open_pillar_v1'/'Assembly.FCStd'))
existing={o.Name:(o.Shape.copy(),tuple(o.ViewObject.ShapeColor),o.ViewObject.Transparency) for o in src.Objects if hasattr(o,'Shape') and not o.Shape.isNull()}
A.closeDocument(src.Name)
src=A.openDocument(str(R/'models'/'turn_signal_cam_v2'/'Assembly.FCStd'))
cam={n:src.getObject(n).Shape.copy() for n in ['Shaft','Key','Upper','Lower','Hardware0','Hardware1','Hardware2','Hardware3']}
A.closeDocument(src.Name)
for n,s in cam.items():s.translate(V(0,0,-108 if n=='Shaft' else -112))
cam['Shaft']=cy(6,100,0,0,-158).cut(Part.makeBox(4.1,3,14,V(-2.05,4,-119)))
doc=A.newDocument('KFL100_Schematic'); objs={}
for n,(s,c,t) in existing.items():
objs[n]=add(doc,n,'Prior existing '+n,s,c,55 if n=='Motor' else t)
for i,x in enumerate([-50.6200577,50.6200577]):
objs['NewRail%d'%i]=add(doc,'NewRail%d'%i,'Proposed 2020 longitudinal rail schematic',Part.makeBox(20,20,190,V(x-10,-75,-170)),(.48,.51,.55))
# Two vertical plates parallel to XY. The 30.2 mm bore is placeholder,
# with dummy bearing protrusion 29 mm OD; NOT a production bore.
plates=[]
for i,z in enumerate([-84,-150]):
s=Part.makeBox(140,90,8,V(-70,-55,z)).cut(cy(15.1,8.02,0,0,z-.01))
for x in (-24,24):s=s.cut(cy(3.5,8.02,x,0,z-.01))
o=add(doc,'Plate%d'%i,'8mm plate: hole 30.2 assumed',s,(.66,.69,.74));plates.append(o)
# Simplified illustrative corner brackets, contacting plate and rail only.
for j,x in enumerate([-50.6200577,50.6200577]):
a=Part.makeBox(16,12,3,V(x-8,-55,z-3))
b=Part.makeBox(16,3,11,V(x-8,-55,z-11))
o=add(doc,'L_%d_%d'%(i,j),'90deg bracket placeholder, fasteners omitted',a.fuse(b).removeSplitter(),(.88,.49,.16))
# Approximate KFL outside dimensions: 63 x 38 mm, pitch 48, flange
# ears 5.5, insert 14.5; protrusion 29mm OD and position are placeholders.
bearings=[]
for i,(plate_z,inner_z,sign) in enumerate([(-84,-88.5,1),(-150,-153,-1)]):
# face of plate towards outside axle area, mirrored.
if sign==1: face=plate_z+8; flangez=face; insertz=face-8
else: face=plate_z; flangez=face-5.5; insertz=face
ring=cy(19,5.5,0,0,flangez).cut(cy(9.5,5.52,0,0,flangez-.01))
for x in (-24,24):
ear=cy(7.5,5.5,x,0,flangez).fuse(Part.makeBox(12,15,5.5,V(min(x,0),-7.5,flangez)))
ring=ring.fuse(ear)
for x in (-24,24):ring=ring.cut(cy(3.5,5.52,x,0,flangez-.01))
ring=ring.cut(cy(9.5,5.52,0,0,flangez-.01))
ring=ring.fuse(cy(14.5,8,0,0,insertz).cut(cy(9.5,8.02,0,0,insertz-.01))).removeSplitter()
outer=add(doc,'KFLHousing%d'%i,'KFL001 housing: 29 protrusion ASSUMED',ring,(.45,.49,.53));bearings.append(outer)
inner=cy(9.2,14.5,0,0,inner_z).cut(cy(6.1,14.52,0,0,inner_z-.01))
bearings.append(add(doc,'KFLInner%d'%i,'KFL inner 14.5mm rotating schematic',inner,(.77,.77,.79)))
# M6 screws only illustrative straight 5.8mm shank; heads omitted.
for j,x in enumerate((-24,24)):
z0=plate_z-1 if sign==1 else flangez-1
o=add(doc,'KFLBolt%d_%d'%(i,j),'M6 shank schematic, heads omitted',cy(2.9,15.5,x,0,z0),(.18,.19,.20));objs[o.Name]=o
# Real shaft/cam from existing project.
colors={'Shaft':(.65,.66,.68),'Key':(.98,.65,.1),'Upper':(.05,.40,.80),'Lower':(.16,.62,.88)}
for n,s in cam.items():objs['Cam'+n]=add(doc,'Cam'+n,'Existing cam V2 '+n,s,colors.get(n,(.2,.2,.21)))
# Actual small conversion disk STEP: mirrored to front face at z=-38.
conv=Part.read(str(R/'models'/'转换盘1.step'));conv.rotate(V(),V(1,0,0),180);conv.translate(V(0,0,-38))
objs['Conversion']=add(doc,'Conversion','Existing conversion disk STEP; face position approx',conv,(.82,.83,.85))
# Six-hole keyed flange is a placeholder, the hub overlaps 12mm of shaft.
flange=cy(28.5,4,0,0,-50).cut(cy(6.2,4.02,0,0,-50.01))
for j in range(6):
a=2*math.pi*j/6+math.pi/6;flange=flange.cut(cy(2.75,4.02,24*math.cos(a),24*math.sin(a),-50.01))
hubradius=10
hub=cy(15,20,0,0,-70).cut(cy(6.2,20.02,0,0,-70.01));flange=flange.fuse(hub).removeSplitter()
objs['MotorFlange']=add(doc,'MotorFlange','MOTOR actual nominal OD57 flange 4mm, OD30 hub protrusion 20mm',flange,(.9,.66,.20))
# Wheel hub is deliberately modelled with only 9mm shaft engagement.
wh=cy(hubradius,9,0,0,-162).cut(cy(6.2,9.02,0,0,-162.01))
whplate=cy(27,3,0,0,-165).cut(cy(6.2,3.02,0,0,-165.01))
for j in range(6):
a=2*math.pi*j/6;whplate=whplate.cut(cy(2.2,3.02,24*math.cos(a),24*math.sin(a),-165.01))
wh=wh.fuse(whplate).removeSplitter()
objs['WheelHub']=add(doc,'WheelHub','Wheel flange schematic: only 9mm engagement unverified',wh,(.9,.66,.20))
wheel70=Part.read(str(R/'models'/'70转接板.step'));wheel70.rotate(V(),V(1,0,0),180);wheel70.translate(V(0,0,-165))
objs['Wheel70']=add(doc,'Wheel70','Existing 70 adapter, nominal assumed position, bolt access unchecked',wheel70,(.82,.83,.85))
# Switch envelope is deliberately only transparent rectangular with generic
# centre bore; 50mm depth includes off-axis connector. Actual contours unknown.
sw=Part.makeBox(110,80,50,V(-55,-40,-138.5)).cut(cy(23.5,50.02,0,0,-138.51))
objs['SwitchEnvelope']=add(doc,'SwitchEnvelope','50mm switch bounding placeholder; 47mm bore ASSUMED',sw,(.45,.73,.43),80)
doc.recompute()
# Restrict checks to shapes actually modelled. Intentional shaft-to-hub clearance.
checks=[]
pairs=[('Plate%d'%i,'KFLHousing%d'%i,plates[i].Shape,bearings[2*i].Shape) for i in range(2)]
pairs += [('Plate%d'%i,'KFLInner%d'%i,plates[i].Shape,bearings[2*i+1].Shape) for i in range(2)]
pairs += [('SwitchEnvelope','Cam'+n,sw,cam[n]) for n in ('Upper','Lower','Hardware0','Hardware1','Hardware2','Hardware3')]
pairs += [('MotorFlange','Shaft',flange,cam['Shaft']),('WheelHub','Shaft',wh,cam['Shaft']),('WheelHub','70Adapter',wh,wheel70),('Plate0','Plate1',plates[0].Shape,plates[1].Shape)]
pairs += [('MotorFlange','RearBearing',flange,bearings[0].Shape),('MotorFlange','RearInner',flange,bearings[1].Shape),('MotorFlange','RearPlate',flange,plates[0].Shape),('WheelHub','FrontBearing',wh,bearings[2].Shape)]
for a,b,sa,sb in pairs:
checks.append('%s / %s = %.6f mm3'%(a,b,booleanvol(sa,sb)))
for n,s in [('RearConflict',flange.common(bearings[0].Shape)),('FrontConflict',wh.common(bearings[2].Shape))]:
if not s.isNull() and s.Volume>.001: add(doc,n,'RED: nominal collision '+n,s,(1.0,.03,.03))
lines=['CONCEPT ONLY; NOT ASSEMBLY FIT VALIDATION.',
'Motor side flange disc OD57 thickness4, six PCD48 holes nominal5.5; hub OD30 extending20 past disc face. Wheel side hub OD20 assumed.',
'Z decreases from motor toward steering wheel; motor rear z0; wheel shaft z=-58..-158 length100.',
'KFL actual nominal 63x38 housing envelope, 48 bolt centers, 7 hole, 5.5 ears, inner 14.5; protrusion OD29 and position assumed.',
'8mm plates central hole OD30.2 assumed, not actual production spec.',
'50mm switch opaque-enclosing envelope z=-138.5..-88.5; opening OD47 ASSUMED only; real rotary features and contacts unknown.',
'Comparison: shaft z=-58..-158, motor hub z=-70..-50: nominal overlap12mm. Rear bearing and switch and wheel hub shifted toward wheel 10mm. Wheel hub z=-162..-153: actual overlap5mm with shaft, is not adequate without verified engineering data.',
'Sleeve20 sits inside switch envelope and is not additional 20mm.',
'Motor mounting boss, shaft end keys, original M3 nut, lights/white contacts, bearing set screws, pin fastening/head/tool and adjuster are not modelled.',
'No workable fit is demonstrated. Real KFL and switch shapes, motor M3 nut and both shaft end details, fastener/tool paths, front hub and contact/clearances are unmeasured.',
'Checked positive-volume intersections (other pairs, loaded operation, exact bearing and switch shape not tested):']+checks
(O/'geometry_check.txt').write_text('\n'.join(lines)+'\n',encoding='utf8')
doc.saveAs(str(O/'Assembly.FCStd'));Import.export([o for o in doc.Objects if hasattr(o,'Shape')],str(O/'Assembly.step'))
view=G.activeDocument().activeView();G.activateWorkbench('PartWorkbench')
def save(name,mode):
if mode=='side':view.viewFront()
else:
view.viewAxonometric();G.updateGui();view.getCameraNode().orientation.setValue(coin.SbRotation(.880476,.115917,-.279848,-.364705))
G.updateGui();time.sleep(.7);view.fitAll();G.updateGui();time.sleep(.7);view.saveImage(str(O/name),1800,1250,'White')
save('assembly.png','axon');save('side.png','side')
# Simplified focus hides rear encoder/rails so bearing through plate is visible.
for n,o in objs.items():
if n in existing or n.startswith('NewRail') or n=='Conversion':o.ViewObject.Visibility=False
for o in doc.Objects:
if o.Name.startswith('L_'):o.ViewObject.Visibility=False
save('focus.png','axon')
for o in doc.Objects:
if hasattr(o,'Shape'):o.ViewObject.Visibility=True
doc.recompute();doc.saveAs(str(O/'Assembly.FCStd'))
(O/'DONE.txt').write_text('ok',encoding='utf8')
except Exception:
(O/'ERROR.txt').write_text(traceback.format_exc(),encoding='utf8');raise

View File

@@ -0,0 +1,155 @@
from pathlib import Path
import math, time, traceback
import FreeCAD as A, FreeCADGui as G, Part, Import
from pivy import coin
R=Path(r'C:\projects\wheel'); O=R/'models'/'kfl_plate_adjust_concept'; O.mkdir(parents=True,exist_ok=True)
V=A.Vector
def cylinder(r,h,p,d=V(0,0,1)): return Part.makeCylinder(r,h,p,d)
def obj(doc,name,label,shape,color,trans=0):
o=doc.addObject('PartDesign::Feature',name);o.Label=label;o.Shape=shape;o.ViewObject.ShapeColor=color;o.ViewObject.LineColor=(.12,.12,.12);o.ViewObject.Transparency=trans;return o
def slot_z(x,y1,y2,r,z,h):
return Part.makeBox(2*r,y2-y1,h,V(x-r,y1,z)).fuse(cylinder(r,h,V(x,y1,z))).fuse(cylinder(r,h,V(x,y2,z)))
def slot_y(x1,x2,z,r,y,h):
return Part.makeBox(x2-x1,h,2*r,V(x1,y,z-r)).fuse(cylinder(r,h,V(x1,y,z),V(0,1,0))).fuse(cylinder(r,h,V(x2,y,z),V(0,1,0)))
def slot_xz(x1,x2,y,r,z,h):
return Part.makeBox(x2-x1,2*r,h,V(x1,y-r,z)).fuse(cylinder(r,h,V(x1,y,z))).fuse(cylinder(r,h,V(x2,y,z)))
def hex_prism(radius,length,p,d): return Part.makePolygon([]) if False else Part.makeCylinder(radius,length,p,d,360)
def common(a,b):
if not a.BoundBox.intersect(b.BoundBox): return 0.0
return a.common(b).Volume
try:
doc=A.openDocument(str(R/'models'/'kfl_150_switch60_concept'/'Assembly.FCStd'))
# Prior rails are solid envelopes; retain their positions but extend forward 10mm.
for name in ('Plate0','Plate1','L_0_0','L_0_1','L_1_0','L_1_1'):
if doc.getObject(name): doc.removeObject(name)
doc.recompute()
# Give foreground rails their actual representation as outline envelopes only.
for i,x in enumerate((-50.6200577,50.6200577)):
rail=doc.getObject('NewRail%d'%i);rail.Shape=Part.makeBox(20,20,235,V(x-10,-75,-215))
rail.Label='2020 rail envelope; slots not modelled';rail.ViewObject.Transparency=25
# KMT KFL001 A0=16, A=11.5, A1=5.5: relative to the mounting
# plane, inner insert protrudes 10.5mm and housing protrudes 6mm.
# Circular protrusion OD29 is still a geometric placeholder.
rear=doc.getObject('KFLHousing0');rear.Shape=rear.Shape.cut(cylinder(20,2.02,V(0,0,-84.01))).removeSplitter();rear.Label='KFL housing 6mm nominal protrusion, OD29 assumed'
front=doc.getObject('KFLHousing1');front.Shape=front.Shape.cut(cylinder(20,2.02,V(0,0,-181.01))).removeSplitter();front.Label='KFL housing 6mm nominal protrusion, OD29 assumed'
inner=doc.getObject('KFLInner0');s=inner.Shape.copy();s.translate(V(0,0,2));inner.Shape=s;inner.Label='KFL insert 10.5mm nominal protrusion behind mounting plane'
inner=doc.getObject('KFLInner1');s=inner.Shape.copy();s.translate(V(0,0,-1));inner.Shape=s;inner.Label='KFL insert 10.5mm nominal protrusion behind mounting plane'
doc.recompute()
plates=[]; corners=[]; boardfast=[]; railfast=[]; knuts=[]
for i,z in enumerate((-84.,-187.)):
shape=Part.makeBox(140,86,8,V(-70,-51,z)).cut(cylinder(15.1,8.04,V(0,0,z-.02)))
for x in (-24.,24.):shape=shape.cut(cylinder(3.5,8.04,V(x,0,z-.02)))
for x in (-50.6200577,50.6200577): shape=shape.cut(slot_xz(x-2,x+2,-42.75,2.75,z-.02,8.04))
plates.append(obj(doc,'AdjustPlate%d'%i,'8mm plate, horizontal cross-slots, mounting aperture provisional',shape.removeSplitter(),(.72,.74,.78)))
for j,x in enumerate((-50.6200577,50.6200577)):
# Brackets on outer side of each plate, legs exactly meet each other.
sign=1 if i==0 else -1
face=z+8 if sign==1 else z
zplate=face if sign==1 else face-3
zfoot=face if sign==1 else face-20
leg=Part.makeBox(18,20,3,V(x-9,-55,zplate))
leg=leg.cut(slot_z(x,-45.75,-41.75,2.75,zplate-.02,3.04))
foot=Part.makeBox(18,3,20,V(x-9,-55,zfoot))
zb=face+10 if sign==1 else face-10
foot=foot.cut(cylinder(2.75,3.04,V(x,-55.02,zb),V(0,1,0)))
bracket=leg.fuse(foot).removeSplitter()
corners.append(obj(doc,'RealCorner%d%d'%(i,j),'2020 L angle 20x20x18: assumed 9.5x5.5 long slot and 5.5 round',bracket,(.88,.48,.14)))
# Plate M5 screw shank, head and nut: 5 mm diameter nominal.
# 1 mm thick washers and 19 mm shank envelope (not a selected screw length).
stem=cylinder(2.45,19,V(x,-42.75,z-4));
if sign==1:
head=cylinder(4.2,3,V(x,-42.75,face+4));nut=cylinder(4.3,3,V(x,-42.75,z-4));wash1=cylinder(5,1,V(x,-42.75,face+3));wash2=cylinder(5,1,V(x,-42.75,z-1))
else:
head=cylinder(4.2,3,V(x,-42.75,z-7));nut=cylinder(4.3,3,V(x,-42.75,z+9));wash1=cylinder(5,1,V(x,-42.75,z-4));wash2=cylinder(5,1,V(x,-42.75,z+8))
boardfast.append(obj(doc,'BoardFast%d%d'%(i,j),'M5 plate fastener and 10mm washer envelopes',stem.fuse(head).fuse(nut).fuse(wash1).fuse(wash2),(.13,.14,.16)))
# Foot screw goes into the rail; T-slot geometry omitted from the rail envelope.
# Ø5.5 round hole at profile, low-profile Ø9.5 x 2.75 head, no washer.
shank=cylinder(2.45,8,V(x,-60,zb),V(0,1,0));head=cylinder(4.75,2.75,V(x,-52,zb),V(0,1,0))
railfast.append(obj(doc,'RailFast%d%d'%(i,j),'M5 low-profile 9.5x2.75 head at round hole; no washer',shank.fuse(head),(.13,.14,.16)))
knuts.append(obj(doc,'Tnut%d%d'%(i,j),'2020 T nut location schematic inside rail',Part.makeBox(8,4,8,V(x-4,-61,zb-4)),(.7,.7,.72)))
# Accurate KFL heads and nuts are not dimensioned, add nominal cylindrical envelopes.
for i,z in enumerate((-84.,-187.)):
for j,x in enumerate((-24.,24.)):
if i==0: head=cylinder(5,3,V(x,0,-70.5));nut=cylinder(5,3,V(x,0,-87))
else: head=cylinder(5,3,V(x,0,-195.5));nut=cylinder(5,3,V(x,0,-179))
obj(doc,'KFLFastEnd%d%d'%(i,j),'M6 head / nut envelopes assumed',head.fuse(nut),(.15,.16,.18))
doc.recompute()
tests=[]
def check(label,a,b):
v=common(a.Shape,b.Shape);tests.append((label,v))
for i in range(2):
for n in ('KFLHousing%d'%i,'KFLInner%d'%i):check('Plate%d/%s'%(i,n),plates[i],doc.getObject(n))
for j in range(2):
k=2*i+j; check('Plate%d/corner%d%d'%(i,i,j),plates[i],corners[k]);check('boardfast%d%d/corner'%(i,j),boardfast[k],corners[k]);check('boardfast%d%d/plate'%(i,j),boardfast[k],plates[i]);check('railfast%d%d/corner'%(i,j),railfast[k],corners[k])
check('boardfast%d%d/railfast'%(i,j),boardfast[k],railfast[k]);check('boardfast%d%d/rail'%(i,j),boardfast[k],doc.getObject('NewRail%d'%j));check('railfast%d%d/plate'%(i,j),railfast[k],plates[i])
for n in ('Motor','SwitchEnvelope','MotorFlange','WheelHub','Wheel70'):
if doc.getObject(n):
check('corner%d%d/%s'%(i,j,n),corners[k],doc.getObject(n));check('boardfast%d%d/%s'%(i,j,n),boardfast[k],doc.getObject(n));check('railfast%d%d/%s'%(i,j,n),railfast[k],doc.getObject(n))
# Whole solid rail envelopes include a drawn solid area where a T-slot exists.
check('corner%d%d/ownRail'%(i,j),corners[k],doc.getObject('NewRail%d'%j))
for i in range(2):
for n in ('KFLHousing%d'%i,'KFLInner%d'%i):
for other in (('MotorFlange' if i==0 else 'WheelHub'),'CamShaft','SwitchEnvelope'):
if doc.getObject(n) and doc.getObject(other):check(n+'/'+other,doc.getObject(n),doc.getObject(other))
for x in (-24,24):
nut=doc.getObject('KFLFastEnd%d%d'%(i,0 if x<0 else 1))
for n in ('MotorFlange','WheelHub','SwitchEnvelope'):
if doc.getObject(n):check(nut.Name+'/'+n,nut,doc.getObject(n))
traveltests=[]
for i in range(4):
for dy in (-2.,0.,1.):
moved=boardfast[i].Shape.copy();moved.translate(V(0,dy,0))
vol=common(moved,railfast[i].Shape)
distance=moved.distToShape(railfast[i].Shape)[0]
traveltests.append((boardfast[i].Name,dy,vol,distance))
combined=[]
for i in range(4):
for dx in (-2.,0.,2.):
for dy in (-2.,0.,1.):
movedfast=boardfast[i].Shape.copy();movedfast.translate(V(0,dy,0))
movedplate=plates[i//2].Shape.copy();movedplate.translate(V(dx,dy,0))
overlaps=(common(movedfast,railfast[i].Shape),common(movedfast,corners[i].Shape),common(movedfast,movedplate),common(movedplate,corners[i].Shape),common(movedplate,doc.getObject('NewRail%d'%(i%2)).Shape))
combined.append((i,dx,dy,max(overlaps)))
doc.recompute()
lines=['CONCEPT ONLY: NOT FOR CNC OR PRINT.',
'Prior shaft 12x150, 60mm switch envelope, mirrored KFL and known motor assembly retained.',
'8 mm plates: 140x86 envelope; central hole OD30.2 assumed, KFL holes OD7 at +/-24 (48mm pitch).',
'KMT catalogue A0=16, A1=5.5, A=11.5: inner extends 10.5mm and housing 6mm from flange mounting plane. Housing projection OD29 assumed; not catalog-confirmed.',
'Minimum nominal clearance between centre and KFL bolt holes: 24-15.1-3.5=5.4 mm; not a strength calculation.',
'Whole plate height adjusts via 9.5x5.5 long slot in angle leg; initial bolt is 1mm off slot centre, nominal offsets -2 to +1mm. Plate to rail has 4mm nominal gap.',
'Whole plate left/right adjusts via 9.5x5.5 slots on plates, crossing each slotted angle leg, about +/-2mm subject to fit.',
'Axial positioning via sliding T-nuts in the 2020 profile. Brackets 20x20x18x3, 9.5x5.5 slot / 5.5 round as assumed stock.',
'M5 head/nut envelopes include 10mm board washers; round-hole rail fasteners use low-profile 9.5mm head, no washer; T nuts, shape and actual extrusion grooves need review.',
'Centre hole and bearing projection are assumptions: do NOT assume bearing moves in plate opening.',
'Slots geometry offers horizontal +/-2mm, vertical -2..+1mm, but assembled shaft, flange and small gaps constrain safe motion. Loosen and check continuously; do not force alignment.',
'KFL shape, fastener head/nut access, actual motor, electrical wiring, switch shape, stator etc not validated.',
'Bearing locating/floating and structural strength remain unresolved.',
'Positive-volume intersection checks in cubic mm:']+[f'{n}: {v:.6f}' for n,v in tests]
lines+=['M5 hardware clearance over candidate vertical travel (cubic mm intersection and mm minimum separation):']+[f'{n} dy={dy:+.1f} intersection={v:.6f} distance={d:.6f}' for n,dy,v,d in traveltests]
lines+=['Combined adjustment corners (plate dx/dy and M5s relative bracket; no mounted shaft or surrounding structure included):']+[f'corner={i} dx={dx:+.1f} dy={dy:+.1f} max_intersection={v:.6f}' for i,dx,dy,v in combined]
lines+=['Nominal simplified axial clearances (mm): rear housing/flange %.6f; rear inner/flange %.6f; front housing/flange %.6f; front inner/flange %.6f.'%(doc.getObject('KFLHousing0').Shape.distToShape(doc.getObject('MotorFlange').Shape)[0],doc.getObject('KFLInner0').Shape.distToShape(doc.getObject('MotorFlange').Shape)[0],doc.getObject('KFLHousing1').Shape.distToShape(doc.getObject('WheelHub').Shape)[0],doc.getObject('KFLInner1').Shape.distToShape(doc.getObject('WheelHub').Shape)[0])]
lines+=['KFL inner axial centre distance 104.5mm, switch envelope to inner end gaps rear15.5mm front14.5mm; computed from nominal adjusted model.']
(O/'geometry_check.txt').write_text('\n'.join(lines)+'\n',encoding='utf8')
# Red solids explicitly indicate any detected nonzero intersection.
bad=[(n,v) for n,v in tests if v>0.001]
(O/'checks_status.txt').write_text('INTERSECTIONS FOUND: '+str(bad)+'\n',encoding='utf8')
doc.saveAs(str(O/'Assembly.FCStd'));Import.export([o for o in doc.Objects if hasattr(o,'Shape')],str(O/'Assembly.step'))
view=G.activeDocument().activeView();G.activateWorkbench('PartWorkbench')
def save(path,mode):
if mode=='side': view.viewFront()
else: view.viewAxonometric();G.updateGui();view.getCameraNode().orientation.setValue(coin.SbRotation(.880476,.115917,-.279848,-.364705))
G.updateGui();time.sleep(.7);view.fitAll();G.updateGui();time.sleep(.7);view.saveImage(str(O/path),1800,1250,'White')
save('assembly.png','axon');save('side.png','side')
# Bearing/plates context only; hides shaft/end assemblies and green envelope, so holes visible.
visible={o.Name:o.ViewObject.Visibility for o in doc.Objects if hasattr(o,'Shape')}
show=set([o.Name for o in plates+corners+boardfast+railfast]+['KFLHousing0','KFLHousing1','KFLInner0','KFLInner1','KFLBolt0_0','KFLBolt0_1','KFLBolt1_0','KFLBolt1_1','KFLFastEnd00','KFLFastEnd01','KFLFastEnd10','KFLFastEnd11','NewRail0','NewRail1','CamShaft'])
for o in doc.Objects:
if hasattr(o,'Shape'):o.ViewObject.Visibility=o.Name in show
save('adjustment.png','axon')
for o in doc.Objects:
if hasattr(o,'Shape'):o.ViewObject.Visibility=visible.get(o.Name,True)
doc.recompute();doc.saveAs(str(O/'Assembly.FCStd'))
(O/'DONE.txt').write_text('ok',encoding='utf8')
except Exception:
(O/'ERROR.txt').write_text(traceback.format_exc(),encoding='utf8');raise

View File

@@ -0,0 +1,118 @@
from pathlib import Path
import math, time, traceback
import FreeCAD as A, FreeCADGui as G, Part, Import
from pivy import coin
R=Path(r'C:\projects\wheel'); O=R/'models'/'kfl_plate_b38_concept'; O.mkdir(parents=True,exist_ok=True)
V=A.Vector
def cyl(r,h,x,y,z):return Part.makeCylinder(r,h,V(x,y,z))
def add(doc,name,label,s,color,trans=0):
o=doc.addObject('PartDesign::Feature',name);o.Label=label;o.Shape=s;o.ViewObject.ShapeColor=color;o.ViewObject.LineColor=(.1,.1,.1);o.ViewObject.Transparency=trans;return o
def intersection(a,b):
return a.common(b).Volume if a.BoundBox.intersect(b.BoundBox) else 0.0
try:
doc=A.openDocument(str(R/'models'/'kfl_plate_adjust_concept'/'Assembly.FCStd'))
doc.Label='KFL b38 - 39 mm bore conceptual assembly'
for i,z in enumerate((-84.,-187.)):
p=doc.getObject('AdjustPlate%d'%i)
p.Shape=p.Shape.cut(cyl(19.5,8.04,0,0,z-.02)).removeSplitter()
p.Label='8 mm plate, central through-bore Ø39 assumption'
# Enlarge only the portion assumed to penetrate the plate; leave mounting ears intact.
# KMT catalog axial section: static housing protrudes 6 mm from ear seating plane.
# A circular Ø38 bounding section is a conservative stand-in for the unmeasured
# actual penetrating profile, not a manufacturer's individual diameter callout.
for i,z in enumerate((-82.,-187.)):
h=doc.getObject('KFLHousing%d'%i)
outer=cyl(19,6,0,0,z).cut(cyl(9.5,6.02,0,0,z-.01))
h.Shape=h.Shape.fuse(outer).removeSplitter()
h.Label='KFL seat Ø38 circular envelope assumed; 6mm inside plate'
# Replace previous simplified M6 heads/nuts with individually selectable shank,
# head, nut, and two washers. OD12 ID6.6 washers are nominal placeholders.
for i,z in enumerate((-84.,-187.)):
for j,x in enumerate((-24.,24.)):
name='KFLFastEnd%d%d'%(i,j)
if doc.getObject(name):doc.removeObject(name)
old='KFLBolt%d_%d'%(i,j)
if doc.getObject(old):doc.removeObject(old)
if i==0:
# flange ear z=-76..-70.5; plate z=-84..-76
wplatez=-85.; wearz=-70.5; nutz=-88.; headz=-69.5
else:
# ear z=-192.5..-187; plate z=-187..-179
wplatez=-179.; wearz=-193.5; nutz=-178.; headz=-196.5
# Each washer is hollow to avoid its shaft being a spurious positive overlap.
ring=lambda zz: cyl(6,1,x,0,zz).cut(cyl(3.3,1.02,x,0,zz-.01))
add(doc,'BearingWasherPlate%d%d'%(i,j),'M6 washer Ø12 on plate, schematic',ring(wplatez),(.42,.44,.47))
add(doc,'BearingWasherEar%d%d'%(i,j),'M6 washer Ø12 on bearing ear, schematic',ring(wearz),(.42,.44,.47))
add(doc,'BearingHead%d%d'%(i,j),'M6 head envelope Ø10',cyl(5,3,x,0,headz),(.13,.14,.16))
add(doc,'BearingNut%d%d'%(i,j),'M6 nut envelope Ø10',cyl(5,3,x,0,nutz),(.13,.14,.16))
add(doc,'KFLBolt%d_%d'%(i,j),'M6 bolt shank engagement schematic Ø5.8',cyl(2.9,18.5,x,0,nutz if i==0 else wearz),(.13,.14,.16))
doc.recompute()
tests=[]
def chk(label,sa,sb):tests.append((label,intersection(sa,sb)))
for i in range(2):
p=doc.getObject('AdjustPlate%d'%i).Shape
for n in ('KFLHousing%d'%i,'KFLInner%d'%i):chk('plate%d/%s'%(i,n),p,doc.getObject(n).Shape)
for j in range(2):
for k in ('BearingWasherPlate','BearingWasherEar','BearingHead','BearingNut'):
v=doc.getObject('%s%d%d'%(k,i,j)).Shape
for other in ('MotorFlange','WheelHub','SwitchEnvelope'):
o=doc.getObject(other)
if o:chk('%s%d%d/%s'%(k,i,j,other),v,o.Shape)
for n in ('KFLHousing%d'%i,'KFLInner%d'%i):
other=doc.getObject('MotorFlange' if i==0 else 'WheelHub')
chk(n+'/hub',doc.getObject(n).Shape,other.Shape)
# Contact support: intersect planar annulus with plate surface only; washer radius 6, hole radius 3.3.
# The single-bore-to-plate-hole line edges are 24 - 19.5 - 3.5 = 1 mm.
# Use a thin prism intersection for total plate contact footprint (normal separation < modelling epsilon).
contact=[]
for i,z in enumerate((-84.,-187.)):
p=doc.getObject('AdjustPlate%d'%i).Shape
platevolume=0
for j,x in enumerate((-24.,24.)):
v=(cyl(6,.1,x,0,z if i==0 else z+7.9).cut(cyl(3.3,.12,x,0,z-.01 if i==0 else z+7.89))).common(p).Volume/.1
contact.append((i,j,v))
# Two-bore nearest metal/nominal washer footprint purely geometric; not strength.
b=doc.getObject('KFLHousing0').Shape.BoundBox
lines=['CONCEPT ONLY; DO NOT USE FOR CNC OR SAFE-FIT APPROVAL.',
'Plate central bore nominal OD39, 8mm thick. 48mm KFL mounting pitch, nominal OD7 holes.',
'KFL protrusion given b=38 as conservative circular OD38 bounding assumption, not confirmed round nor actual profile; 0.5mm radial nominal bore clearance.',
'KFL ear thickness5.5, stationary central protrusion depth6, inner ring width14.5 and projects 10.5 past mating plane (KMT interpretation).',
'Centre-hole to each fastening-hole nominal edge gap = 24 - 19.5 - 3.5 = 1.0mm.',
'M6 plate-face washer assumed OD12, ID6.6, thickness1: towards centre hole it extends 1.5mm over nominal opening.',
'Plate-face washer nominal contact area mm^2 (face-intersection):']+[f'plate{i} washer{j}: {a:.6f}' for i,j,a in contact]+[
'These are static contact footprints, not load/fastener/tear-out calculations. Bearing ear footprint and flush contact require real KFL shape.',
'Axial clearance must not be inferred from standard b width. Encoder screw, wires, set-screw access and rotational clearances not fully modelled.',
'M6 heads/nuts/washer geometry is illustrative and no tool access is verified; M5 standard angles and crossing slots inherit prior notes.',
'Diameter 39 is proposed initial 1mm diametral allowance and is not a fit/tolerance guarantee. No FEA, validation or testing.',
'Checked positive-volume intersections (mm^3):']+[f'{n}: {v:.6f}' for n,v in tests]
(O/'geometry_check.txt').write_text('\n'.join(lines)+'\n',encoding='utf-8')
doc.recompute();doc.saveAs(str(O/'Assembly.FCStd'))
Import.export([o for o in doc.Objects if hasattr(o,'Shape')],str(O/'Assembly.step'))
view=G.activeDocument().activeView();G.activateWorkbench('PartWorkbench')
def save(name,mode):
if mode=='side':view.viewFront()
elif mode=='top':view.viewBottom()
else:view.viewAxonometric()
G.updateGui();time.sleep(.6);view.fitAll();G.updateGui();time.sleep(.6)
camera=view.getCameraNode();camera.nearDistance.setValue(.01);camera.farDistance.setValue(10000)
G.updateGui();view.saveImage(str(O/name),1800,1300,'White')
save('assembly.png','axon');save('side.png','side')
vis={o.Name:o.ViewObject.Visibility for o in doc.Objects if hasattr(o,'Shape')}
show={'AdjustPlate0','KFLHousing0','KFLInner0','KFLBolt0_0','KFLBolt0_1','BearingWasherPlate00','BearingWasherPlate01','BearingWasherEar00','BearingWasherEar01','BearingHead00','BearingHead01','BearingNut00','BearingNut01'}
for o in doc.Objects:
if hasattr(o,'Shape'):o.ViewObject.Visibility=o.Name in show
save('bearing_detail.png','axon')
# Plate + plate-face washers, as seen straight along axis; making relationship visible.
show={'AdjustPlate0','BearingWasherPlate00','BearingWasherPlate01'}
for o in doc.Objects:
if hasattr(o,'Shape'):o.ViewObject.Visibility=o.Name in show
save('plate_detail.png','top')
for o in doc.Objects:
if hasattr(o,'Shape'):o.ViewObject.Visibility=vis.get(o.Name,True)
doc.recompute();doc.saveAs(str(O/'Assembly.FCStd'))
if (O/'ERROR.txt').exists(): (O/'ERROR.txt').unlink()
(O/'DONE.txt').write_text('ok',encoding='utf8')
except Exception:
(O/'ERROR.txt').write_text(traceback.format_exc(),encoding='utf8')
raise

View File

@@ -0,0 +1,138 @@
from pathlib import Path
import math, time, traceback, os
import FreeCAD as A, FreeCADGui as G, Part, Import
from pivy import coin
V=A.Vector
R=Path(r'C:\projects\wheel'); shaftlen=int(os.environ.get('SAME_FLANGE_SHAFT_LEN','100')); O=R/'models'/('kfl_same_flange_%d'%shaftlen); O.mkdir(parents=True,exist_ok=True)
def cy(r,h,x=0,y=0,z=0):return Part.makeCylinder(r,h,V(x,y,z))
def add(d,n,label,s,c,t=0):
o=d.addObject('PartDesign::Feature',n);o.Label=label;o.Shape=s;o.ViewObject.ShapeColor=c;o.ViewObject.LineColor=(.08,.08,.08);o.ViewObject.Transparency=t;return o
def booleanvol(a,b):
return a.common(b).Volume if a.BoundBox.intersect(b.BoundBox) else 0
try:
src=A.openDocument(str(R/'models'/'pcd50_open_pillar_v1'/'Assembly.FCStd'))
existing={o.Name:(o.Shape.copy(),tuple(o.ViewObject.ShapeColor),o.ViewObject.Transparency) for o in src.Objects if hasattr(o,'Shape') and not o.Shape.isNull()}
A.closeDocument(src.Name)
src=A.openDocument(str(R/'models'/'turn_signal_cam_v2'/'Assembly.FCStd'))
cam={n:src.getObject(n).Shape.copy() for n in ['Shaft','Key','Upper','Lower','Hardware0','Hardware1','Hardware2','Hardware3']}
A.closeDocument(src.Name)
for n,s in cam.items():s.translate(V(0,0,-108 if n=='Shaft' else -112))
cam['Shaft']=cy(6,shaftlen,0,0,-58-shaftlen).cut(Part.makeBox(4.1,3,14,V(-2.05,4,-119)))
doc=A.newDocument('KFL100_Schematic'); objs={}
for n,(s,c,t) in existing.items():
objs[n]=add(doc,n,'Prior existing '+n,s,c,55 if n=='Motor' else t)
for i,x in enumerate([-50.6200577,50.6200577]):
objs['NewRail%d'%i]=add(doc,'NewRail%d'%i,'Proposed 2020 longitudinal rail schematic',Part.makeBox(20,20,190,V(x-10,-75,-170)),(.48,.51,.55))
# Two vertical plates parallel to XY. The 30.2 mm bore is placeholder,
# with dummy bearing protrusion 29 mm OD; NOT a production bore.
plates=[]
for i,z in enumerate([-84,-150]):
s=Part.makeBox(140,90,8,V(-70,-55,z)).cut(cy(15.1,8.02,0,0,z-.01))
for x in (-24,24):s=s.cut(cy(3.5,8.02,x,0,z-.01))
o=add(doc,'Plate%d'%i,'8mm plate: hole 30.2 assumed',s,(.66,.69,.74));plates.append(o)
# Simplified illustrative corner brackets, contacting plate and rail only.
for j,x in enumerate([-50.6200577,50.6200577]):
a=Part.makeBox(16,12,3,V(x-8,-55,z-3))
b=Part.makeBox(16,3,11,V(x-8,-55,z-11))
o=add(doc,'L_%d_%d'%(i,j),'90deg bracket placeholder, fasteners omitted',a.fuse(b).removeSplitter(),(.88,.49,.16))
# Approximate KFL outside dimensions: 63 x 38 mm, pitch 48, flange
# ears 5.5, insert 14.5; protrusion 29mm OD and position are placeholders.
bearings=[]
for i,(plate_z,inner_z,sign) in enumerate([(-84,-88.5,1),(-150,-153,-1)]):
# face of plate towards outside axle area, mirrored.
if sign==1: face=plate_z+8; flangez=face; insertz=face-8
else: face=plate_z; flangez=face-5.5; insertz=face
ring=cy(19,5.5,0,0,flangez).cut(cy(9.5,5.52,0,0,flangez-.01))
for x in (-24,24):
ear=cy(7.5,5.5,x,0,flangez).fuse(Part.makeBox(12,15,5.5,V(min(x,0),-7.5,flangez)))
ring=ring.fuse(ear)
for x in (-24,24):ring=ring.cut(cy(3.5,5.52,x,0,flangez-.01))
ring=ring.cut(cy(9.5,5.52,0,0,flangez-.01))
ring=ring.fuse(cy(14.5,8,0,0,insertz).cut(cy(9.5,8.02,0,0,insertz-.01))).removeSplitter()
outer=add(doc,'KFLHousing%d'%i,'KFL001 housing: 29 protrusion ASSUMED',ring,(.45,.49,.53));bearings.append(outer)
inner=cy(9.2,14.5,0,0,inner_z).cut(cy(6.1,14.52,0,0,inner_z-.01))
bearings.append(add(doc,'KFLInner%d'%i,'KFL inner 14.5mm rotating schematic',inner,(.77,.77,.79)))
# M6 screws only illustrative straight 5.8mm shank; heads omitted.
for j,x in enumerate((-24,24)):
z0=plate_z-1 if sign==1 else flangez-1
o=add(doc,'KFLBolt%d_%d'%(i,j),'M6 shank schematic, heads omitted',cy(2.9,15.5,x,0,z0),(.18,.19,.20));objs[o.Name]=o
# Real shaft/cam from existing project.
colors={'Shaft':(.65,.66,.68),'Key':(.98,.65,.1),'Upper':(.05,.40,.80),'Lower':(.16,.62,.88)}
for n,s in cam.items():objs['Cam'+n]=add(doc,'Cam'+n,'Existing cam V2 '+n,s,colors.get(n,(.2,.2,.21)))
# Actual small conversion disk STEP: mirrored to front face at z=-38.
conv=Part.read(str(R/'models'/'转换盘1.step'));conv.rotate(V(),V(1,0,0),180);conv.translate(V(0,0,-38))
objs['Conversion']=add(doc,'Conversion','Existing conversion disk STEP; face position approx',conv,(.82,.83,.85))
# Six-hole keyed flange is a placeholder, the hub overlaps 12mm of shaft.
flange=cy(28.5,4,0,0,-50).cut(cy(6.2,4.02,0,0,-50.01))
for j in range(6):
a=2*math.pi*j/6+math.pi/6;flange=flange.cut(cy(2.75,4.02,24*math.cos(a),24*math.sin(a),-50.01))
hubradius=10
hub=cy(15,20,0,0,-70).cut(cy(6.2,20.02,0,0,-70.01));flange=flange.fuse(hub).removeSplitter()
objs['MotorFlange']=add(doc,'MotorFlange','MOTOR actual nominal OD57 flange 4mm, OD30 hub protrusion 20mm',flange,(.9,.66,.20))
# Front flange mirrors the motor-side unit: 57 disc, 4 thick, 30x20 hub.
# Front bearing housing occupies z=-155.5..-150; 0.5 mm nominal axial gap.
wh=cy(15,20,0,0,-176).cut(cy(6.2,20.02,0,0,-176.01))
whplate=cy(28.5,4,0,0,-180).cut(cy(6.2,4.02,0,0,-180.01))
for j in range(6):
a=2*math.pi*j/6; whplate=whplate.cut(cy(2.75,4.02,24*math.cos(a),24*math.sin(a),-180.01))
wh=wh.fuse(whplate).removeSplitter()
objs['WheelHub']=add(doc,'WheelHub','Wheel flange same 57x4 30x20 (key not modeled)',wh,(.9,.66,.20))
wheel70=Part.read(str(R/'models'/'70转接板.step'));wheel70.rotate(V(),V(1,0,0),180);wheel70.translate(V(0,0,-180))
objs['Wheel70']=add(doc,'Wheel70','Existing 70 adapter - contact and PCD48 phase shown',wheel70,(.82,.83,.85))
# Switch envelope is deliberately only transparent rectangular with generic
# centre bore; 50mm depth includes off-axis connector. Actual contours unknown.
sw=Part.makeBox(110,80,50,V(-55,-40,-138.5)).cut(cy(23.5,50.02,0,0,-138.51))
objs['SwitchEnvelope']=add(doc,'SwitchEnvelope','50mm switch bounding placeholder; 47mm bore ASSUMED',sw,(.45,.73,.43),80)
doc.recompute()
# Restrict checks to shapes actually modelled. Intentional shaft-to-hub clearance.
checks=[]
pairs=[('Plate%d'%i,'KFLHousing%d'%i,plates[i].Shape,bearings[2*i].Shape) for i in range(2)]
pairs += [('Plate%d'%i,'KFLInner%d'%i,plates[i].Shape,bearings[2*i+1].Shape) for i in range(2)]
pairs += [('SwitchEnvelope','Cam'+n,sw,cam[n]) for n in ('Upper','Lower','Hardware0','Hardware1','Hardware2','Hardware3')]
pairs += [('MotorFlange','Shaft',flange,cam['Shaft']),('WheelHub','Shaft',wh,cam['Shaft']),('WheelHub','70Adapter',wh,wheel70),('Plate0','Plate1',plates[0].Shape,plates[1].Shape)]
pairs += [('MotorFlange','RearBearing',flange,bearings[0].Shape),('MotorFlange','RearInner',flange,bearings[1].Shape),('MotorFlange','RearPlate',flange,plates[0].Shape),('WheelHub','FrontBearing',wh,bearings[2].Shape),('WheelHub','FrontInner',wh,bearings[3].Shape),('WheelHub','FrontPlate',wh,plates[1].Shape),('MotorFlange','Conversion',flange,conv)]
for a,b,sa,sb in pairs:
checks.append('%s / %s = %.6f mm3'%(a,b,booleanvol(sa,sb)))
# Probe through both mated plates at the nominal six 48mm bolt centers.
for j in range(6):
a=2*math.pi*j/6; probe=cy(1.0,12.0,24*math.cos(a),24*math.sin(a),-188)
checks.append('Wheel bolt path %d: shaft probe / flange %.8f / 70plate %.8f mm3'%(j,booleanvol(probe,wh),booleanvol(probe,wheel70)))
for j in range(6):
a=math.pi/6+2*math.pi*j/6; probe=cy(1.0,12.0,24*math.cos(a),24*math.sin(a),-50)
checks.append('Motor bolt path %d: probe / flange %.8f / converter %.8f mm3'%(j,booleanvol(probe,flange),booleanvol(probe,conv)))
for n,s in [('RearConflict',flange.common(bearings[0].Shape)),('FrontConflict',wh.common(bearings[2].Shape))]:
if not s.isNull() and s.Volume>.001: add(doc,n,'RED: nominal collision '+n,s,(1.0,.03,.03))
z_end=-58-shaftlen
front_overlap=max(0,min(-156,-58)-max(-176,z_end))
plate_overlap=max(0,min(-176,-58)-max(-180,z_end))
lines=['CONCEPT ONLY; NOT ASSEMBLY FIT VALIDATION.',
'Both flanges: disc OD57 thickness4, 6x PCD48 5.5 clearance; hub OD30 protrudes 20 from the base. Mirrored.',
'Shaft nominal 12 x %d; z=%.1f..-58.0; motor hub z=-70..-50; motor insertion 12mm assumed.'%(shaftlen,z_end),
'Wheel hub z=-176..-156, front plate z=-180..-176; hub engagement %.1f mm; plate bore overlap %.1f mm (this is not extra key engagement).'%(front_overlap,plate_overlap),
'Bearing positions, 50mm switch envelope and cam as previous shifted design; rear/front hub clearance to simplified outer bearing 0.5mm nominal.',
'KFL approximate 48 bolt pitch and 14.5 inner width, housing/protrusion guessed; 8mm plate 30.2 hole guessed; no real module shape, its 47mm hole is an assumption.',
'Wheel70 is real STEP, mirror/phase0, rear face in contact with front flange disc, PCD48 hole centers align nominally; bolt direction, washer and tool clearances not checked.',
'Real converter/70 STEP hole OD4.234, fastener M4 feasible only with washers and actual size checks; M5 cannot pass nominally; hole intersections/phase and actual hardware must be checked.',
'Shaft endpoint keys not modeled; front hub insertion alone does not establish permissible torque. Actual axial gap, M3 nut, alignments, float bearing arrangement, switch/fan/angles, bolt heads, tool accessibility, and fit/strength not verified.',
'Checked positive-volume intersections of listed pairs only:']+checks
(O/'geometry_check.txt').write_text('\n'.join(lines)+'\n',encoding='utf8')
doc.saveAs(str(O/'Assembly.FCStd'));Import.export([o for o in doc.Objects if hasattr(o,'Shape')],str(O/'Assembly.step'))
view=G.activeDocument().activeView();G.activateWorkbench('PartWorkbench')
def save(name,mode):
if mode=='side':view.viewFront()
else:
view.viewAxonometric();G.updateGui();view.getCameraNode().orientation.setValue(coin.SbRotation(.880476,.115917,-.279848,-.364705))
G.updateGui();time.sleep(.7);view.fitAll();G.updateGui();time.sleep(.7);view.saveImage(str(O/name),1800,1250,'White')
save('assembly.png','axon');save('side.png','side')
# Simplified focus hides rear encoder/rails so bearing through plate is visible.
for n,o in objs.items():
if n in existing or n.startswith('NewRail') or n=='Conversion':o.ViewObject.Visibility=False
for o in doc.Objects:
if o.Name.startswith('L_'):o.ViewObject.Visibility=False
save('focus.png','axon')
for o in doc.Objects:
if hasattr(o,'Shape'):o.ViewObject.Visibility=True
doc.recompute();doc.saveAs(str(O/'Assembly.FCStd'))
(O/'DONE.txt').write_text('ok',encoding='utf8')
except Exception:
(O/'ERROR.txt').write_text(traceback.format_exc(),encoding='utf8');raise

View File

@@ -0,0 +1,138 @@
from pathlib import Path
import math, time, traceback, os
import FreeCAD as A, FreeCADGui as G, Part, Import
from pivy import coin
V=A.Vector
R=Path(r'C:\projects\wheel'); shaftlen=125; O=R/'models'/('kfl_same_flange_%d'%shaftlen); O.mkdir(parents=True,exist_ok=True)
def cy(r,h,x=0,y=0,z=0):return Part.makeCylinder(r,h,V(x,y,z))
def add(d,n,label,s,c,t=0):
o=d.addObject('PartDesign::Feature',n);o.Label=label;o.Shape=s;o.ViewObject.ShapeColor=c;o.ViewObject.LineColor=(.08,.08,.08);o.ViewObject.Transparency=t;return o
def booleanvol(a,b):
return a.common(b).Volume if a.BoundBox.intersect(b.BoundBox) else 0
try:
src=A.openDocument(str(R/'models'/'pcd50_open_pillar_v1'/'Assembly.FCStd'))
existing={o.Name:(o.Shape.copy(),tuple(o.ViewObject.ShapeColor),o.ViewObject.Transparency) for o in src.Objects if hasattr(o,'Shape') and not o.Shape.isNull()}
A.closeDocument(src.Name)
src=A.openDocument(str(R/'models'/'turn_signal_cam_v2'/'Assembly.FCStd'))
cam={n:src.getObject(n).Shape.copy() for n in ['Shaft','Key','Upper','Lower','Hardware0','Hardware1','Hardware2','Hardware3']}
A.closeDocument(src.Name)
for n,s in cam.items():s.translate(V(0,0,-108 if n=='Shaft' else -112))
cam['Shaft']=cy(6,shaftlen,0,0,-50.5-shaftlen).cut(Part.makeBox(4.1,3,14,V(-2.05,4,-119)))
doc=A.newDocument('KFL125_Schematic'); objs={}
for n,(s,c,t) in existing.items():
objs[n]=add(doc,n,'Prior existing '+n,s,c,55 if n=='Motor' else t)
for i,x in enumerate([-50.6200577,50.6200577]):
objs['NewRail%d'%i]=add(doc,'NewRail%d'%i,'Proposed 2020 longitudinal rail schematic',Part.makeBox(20,20,190,V(x-10,-75,-170)),(.48,.51,.55))
# Two vertical plates parallel to XY. The 30.2 mm bore is placeholder,
# with dummy bearing protrusion 29 mm OD; NOT a production bore.
plates=[]
for i,z in enumerate([-84,-150]):
s=Part.makeBox(140,90,8,V(-70,-55,z)).cut(cy(15.1,8.02,0,0,z-.01))
for x in (-24,24):s=s.cut(cy(3.5,8.02,x,0,z-.01))
o=add(doc,'Plate%d'%i,'8mm plate: hole 30.2 assumed',s,(.66,.69,.74));plates.append(o)
# Simplified illustrative corner brackets, contacting plate and rail only.
for j,x in enumerate([-50.6200577,50.6200577]):
a=Part.makeBox(16,12,3,V(x-8,-55,z-3))
b=Part.makeBox(16,3,11,V(x-8,-55,z-11))
o=add(doc,'L_%d_%d'%(i,j),'90deg bracket placeholder, fasteners omitted',a.fuse(b).removeSplitter(),(.88,.49,.16))
# Approximate KFL outside dimensions: 63 x 38 mm, pitch 48, flange
# ears 5.5, insert 14.5; protrusion 29mm OD and position are placeholders.
bearings=[]
for i,(plate_z,inner_z,sign) in enumerate([(-84,-88.5,1),(-150,-153,-1)]):
# face of plate towards outside axle area, mirrored.
if sign==1: face=plate_z+8; flangez=face; insertz=face-8
else: face=plate_z; flangez=face-5.5; insertz=face
ring=cy(19,5.5,0,0,flangez).cut(cy(9.5,5.52,0,0,flangez-.01))
for x in (-24,24):
ear=cy(7.5,5.5,x,0,flangez).fuse(Part.makeBox(12,15,5.5,V(min(x,0),-7.5,flangez)))
ring=ring.fuse(ear)
for x in (-24,24):ring=ring.cut(cy(3.5,5.52,x,0,flangez-.01))
ring=ring.cut(cy(9.5,5.52,0,0,flangez-.01))
ring=ring.fuse(cy(14.5,8,0,0,insertz).cut(cy(9.5,8.02,0,0,insertz-.01))).removeSplitter()
outer=add(doc,'KFLHousing%d'%i,'KFL001 housing: 29 protrusion ASSUMED',ring,(.45,.49,.53));bearings.append(outer)
inner=cy(9.2,14.5,0,0,inner_z).cut(cy(6.1,14.52,0,0,inner_z-.01))
bearings.append(add(doc,'KFLInner%d'%i,'KFL inner 14.5mm rotating schematic',inner,(.77,.77,.79)))
# M6 screws only illustrative straight 5.8mm shank; heads omitted.
for j,x in enumerate((-24,24)):
z0=plate_z-1 if sign==1 else flangez-1
o=add(doc,'KFLBolt%d_%d'%(i,j),'M6 shank schematic, heads omitted',cy(2.9,15.5,x,0,z0),(.18,.19,.20));objs[o.Name]=o
# Real shaft/cam from existing project.
colors={'Shaft':(.65,.66,.68),'Key':(.98,.65,.1),'Upper':(.05,.40,.80),'Lower':(.16,.62,.88)}
for n,s in cam.items():objs['Cam'+n]=add(doc,'Cam'+n,'Existing cam V2 '+n,s,colors.get(n,(.2,.2,.21)))
# Actual small conversion disk STEP: mirrored to front face at z=-38.
conv=Part.read(str(R/'models'/'转换盘1.step'));conv.rotate(V(),V(1,0,0),180);conv.translate(V(0,0,-38))
objs['Conversion']=add(doc,'Conversion','Existing conversion disk STEP; face position approx',conv,(.82,.83,.85))
# Six-hole keyed flange is a placeholder, the hub overlaps 12mm of shaft.
flange=cy(28.5,4,0,0,-50).cut(cy(6.2,4.02,0,0,-50.01))
for j in range(6):
a=2*math.pi*j/6+math.pi/6;flange=flange.cut(cy(2.75,4.02,24*math.cos(a),24*math.sin(a),-50.01))
hubradius=10
hub=cy(15,20,0,0,-70).cut(cy(6.2,20.02,0,0,-70.01));flange=flange.fuse(hub).removeSplitter()
objs['MotorFlange']=add(doc,'MotorFlange','MOTOR actual nominal OD57 flange 4mm, OD30 hub protrusion 20mm',flange,(.9,.66,.20))
# Front flange mirrors the motor-side unit: 57 disc, 4 thick, 30x20 hub.
# Front bearing housing occupies z=-155.5..-150; 0.5 mm nominal axial gap.
wh=cy(15,20,0,0,-176).cut(cy(6.2,20.02,0,0,-176.01))
whplate=cy(28.5,4,0,0,-180).cut(cy(6.2,4.02,0,0,-180.01))
for j in range(6):
a=2*math.pi*j/6; whplate=whplate.cut(cy(2.75,4.02,24*math.cos(a),24*math.sin(a),-180.01))
wh=wh.fuse(whplate).removeSplitter()
objs['WheelHub']=add(doc,'WheelHub','Wheel flange same 57x4 30x20 (key not modeled)',wh,(.9,.66,.20))
wheel70=Part.read(str(R/'models'/'70转接板.step'));wheel70.rotate(V(),V(1,0,0),180);wheel70.translate(V(0,0,-180))
objs['Wheel70']=add(doc,'Wheel70','Existing 70 adapter - contact and PCD48 phase shown',wheel70,(.82,.83,.85))
# Switch envelope is deliberately only transparent rectangular with generic
# centre bore; 50mm depth includes off-axis connector. Actual contours unknown.
sw=Part.makeBox(110,80,50,V(-55,-40,-138.5)).cut(cy(23.5,50.02,0,0,-138.51))
objs['SwitchEnvelope']=add(doc,'SwitchEnvelope','50mm switch bounding placeholder; 47mm bore ASSUMED',sw,(.45,.73,.43),80)
doc.recompute()
# Restrict checks to shapes actually modelled. Intentional shaft-to-hub clearance.
checks=[]
pairs=[('Plate%d'%i,'KFLHousing%d'%i,plates[i].Shape,bearings[2*i].Shape) for i in range(2)]
pairs += [('Plate%d'%i,'KFLInner%d'%i,plates[i].Shape,bearings[2*i+1].Shape) for i in range(2)]
pairs += [('SwitchEnvelope','Cam'+n,sw,cam[n]) for n in ('Upper','Lower','Hardware0','Hardware1','Hardware2','Hardware3')]
pairs += [('MotorFlange','Shaft',flange,cam['Shaft']),('WheelHub','Shaft',wh,cam['Shaft']),('WheelHub','70Adapter',wh,wheel70),('Plate0','Plate1',plates[0].Shape,plates[1].Shape)]
pairs += [('MotorFlange','RearBearing',flange,bearings[0].Shape),('MotorFlange','RearInner',flange,bearings[1].Shape),('MotorFlange','RearPlate',flange,plates[0].Shape),('WheelHub','FrontBearing',wh,bearings[2].Shape),('WheelHub','FrontInner',wh,bearings[3].Shape),('WheelHub','FrontPlate',wh,plates[1].Shape),('MotorFlange','Conversion',flange,conv)]
for a,b,sa,sb in pairs:
checks.append('%s / %s = %.6f mm3'%(a,b,booleanvol(sa,sb)))
# Probe through both mated plates at the nominal six 48mm bolt centers.
for j in range(6):
a=2*math.pi*j/6; probe=cy(1.0,12.0,24*math.cos(a),24*math.sin(a),-188)
checks.append('Wheel bolt path %d: shaft probe / flange %.8f / 70plate %.8f mm3'%(j,booleanvol(probe,wh),booleanvol(probe,wheel70)))
for j in range(6):
a=math.pi/6+2*math.pi*j/6; probe=cy(1.0,12.0,24*math.cos(a),24*math.sin(a),-50)
checks.append('Motor bolt path %d: probe / flange %.8f / converter %.8f mm3'%(j,booleanvol(probe,flange),booleanvol(probe,conv)))
for n,s in [('RearConflict',flange.common(bearings[0].Shape)),('FrontConflict',wh.common(bearings[2].Shape))]:
if not s.isNull() and s.Volume>.001: add(doc,n,'RED: nominal collision '+n,s,(1.0,.03,.03))
z_end=-50.5-shaftlen
front_overlap=max(0,min(-156,-50.5)-max(-176,z_end))
plate_overlap=max(0,min(-176,-50.5)-max(-180,z_end))
lines=['CONCEPT ONLY; NOT ASSEMBLY FIT VALIDATION.',
'Both flanges: disc OD57 thickness4, 6x PCD48 5.5 clearance; hub OD30 protrudes 20 from the base. Mirrored.',
'Shaft nominal 12 x %d; z=%.1f..-50.5; motor hub z=-70..-50; motor insertion 19.5mm assumed and axial gap to base 0.5mm.'%(shaftlen,z_end),
'Wheel hub z=-176..-156, front plate z=-180..-176; hub engagement %.1f mm; plate bore overlap %.1f mm (this is not extra key engagement).'%(front_overlap,plate_overlap),
'Bearing positions, 50mm switch envelope and cam as previous shifted design; rear/front hub clearance to simplified outer bearing 0.5mm nominal.',
'KFL approximate 48 bolt pitch and 14.5 inner width, housing/protrusion guessed; 8mm plate 30.2 hole guessed; no real module shape, its 47mm hole is an assumption.',
'Wheel70 is real STEP, mirror/phase0, rear face in contact with front flange disc, PCD48 hole centers align nominally; bolt direction, washer and tool clearances not checked.',
'Real converter/70 STEP hole OD4.234, fastener M4 feasible only with washers and actual size checks; M5 cannot pass nominally; hole intersections/phase and actual hardware must be checked.',
'Shaft endpoint keys not modeled; front hub insertion alone does not establish permissible torque. Actual axial gap, M3 nut, alignments, float bearing arrangement, switch/fan/angles, bolt heads, tool accessibility, and fit/strength not verified.',
'Checked positive-volume intersections of listed pairs only:']+checks
(O/'geometry_check.txt').write_text('\n'.join(lines)+'\n',encoding='utf8')
doc.saveAs(str(O/'Assembly.FCStd'));Import.export([o for o in doc.Objects if hasattr(o,'Shape')],str(O/'Assembly.step'))
view=G.activeDocument().activeView();G.activateWorkbench('PartWorkbench')
def save(name,mode):
if mode=='side':view.viewFront()
else:
view.viewAxonometric();G.updateGui();view.getCameraNode().orientation.setValue(coin.SbRotation(.880476,.115917,-.279848,-.364705))
G.updateGui();time.sleep(.7);view.fitAll();G.updateGui();time.sleep(.7);view.saveImage(str(O/name),1800,1250,'White')
save('assembly.png','axon');save('side.png','side')
# Simplified focus hides rear encoder/rails so bearing through plate is visible.
for n,o in objs.items():
if n in existing or n.startswith('NewRail') or n=='Conversion':o.ViewObject.Visibility=False
for o in doc.Objects:
if o.Name.startswith('L_'):o.ViewObject.Visibility=False
save('focus.png','axon')
for o in doc.Objects:
if hasattr(o,'Shape'):o.ViewObject.Visibility=True
doc.recompute();doc.saveAs(str(O/'Assembly.FCStd'))
(O/'DONE.txt').write_text('ok',encoding='utf8')
except Exception:
(O/'ERROR.txt').write_text(traceback.format_exc(),encoding='utf8');raise

134
scripts/build_mad_m9c12.py Normal file
View File

@@ -0,0 +1,134 @@
"""Build the approximate, editable MAD M9C12 model in FreeCAD.
Run via rebuild_mad_m9c12.FCMacro in the FreeCAD GUI. The model and STEP
file are overwritten. See models/MAD_M9C12_说明.md for measurement limits.
"""
import math
from pathlib import Path
import FreeCAD as App
import Part
import Import
ROOT = Path(__file__).resolve().parent.parent
MODEL = ROOT / 'models' / 'MAD_M9C12.FCStd'
STEP = ROOT / 'models' / 'MAD_M9C12.step'
if not App.GuiUp:
raise RuntimeError('Run through the GUI macro to save visibility and camera state')
import FreeCADGui as Gui
doc = App.newDocument('MAD_M9C12')
sheet = doc.addObject('Spreadsheet::Sheet', 'Parameters')
sheet.Label = 'Parameters - mm and degrees (verify assumptions)'
sheet.set('A1', 'Parameter'); sheet.set('B1', 'Value'); sheet.set('C1', 'Source / status')
rows = [
('BodyDiameter', '120 mm', 'User measurement: body outside diameter'),
('BodyThickness', '30 mm', 'User measurement: body thickness; excludes boss and shaft'),
('FrontPCD', '20 mm', 'User confirmed adapter inner alternating holes; verify with measurements'),
('FrontHoleDiameter', '3 mm', 'Assumed M3 nominal diameter; verify'),
('FrontHoleDepth', '5 mm', 'Placeholder blind depth; verify'),
('FrontAngle', '0 deg', 'Assumed first hole phase; verify'),
('RearInnerPCD', '50 mm', 'User ruler measurement; verify with calipers'),
('RearOuterPCD', '76 mm', 'User ruler measurement; verify with calipers'),
('RearHoleDiameter', '4 mm', 'M4 nominal diameter, no modeled thread'),
('RearHoleDepth', '8 mm', 'Placeholder; M4x8 does not establish thread depth'),
('RearInnerAngle', '45 deg', 'Assumed phase; verify'),
('RearOuterAngle', '45 deg', 'Assumed relative phase; verify'),
('BossDiameter', '26 mm', 'Visual approximation; front mounting holes enter its top face'),
('BossHeight', '8 mm', 'Visual approximation, verify adapter clearance'),
('ShaftDiameter', '8 mm', 'Visual approximation'),
('ShaftHeight', '6 mm', 'Visual approximation, measured above boss'),
('CenterHoleDiameter', '3 mm', 'Axial through hole; smooth nominal M3 diameter'),
]
for i, (name, value, meaning) in enumerate(rows, 2):
sheet.set('A%d' % i, name); sheet.set('B%d' % i, value)
sheet.set('C%d' % i, meaning); sheet.setAlias('B%d' % i, name)
doc.recompute()
def cylinder(name, label, radius, height, z, x=None, y=None):
obj = doc.addObject('Part::Cylinder', name)
obj.Label = label
obj.setExpression('Radius', radius)
obj.setExpression('Height', height)
obj.setExpression('Placement.Base.z', z)
if x is not None:
obj.setExpression('Placement.Base.x', x)
obj.setExpression('Placement.Base.y', y)
return obj
def pattern(name, pcd, angle, diameter, depth, z):
holes = []
for i in range(4):
phase = 'Parameters.%s + %d deg' % (angle, i * 90)
x = 'Parameters.%s / 2 * cos(%s)' % (pcd, phase)
y = 'Parameters.%s / 2 * sin(%s)' % (pcd, phase)
holes.append(cylinder(name + str(i + 1), name + ' hole ' + str(i + 1),
'Parameters.%s / 2' % diameter,
'Parameters.%s' % depth, z, x, y))
group = doc.addObject('Part::Compound', name + 'Tools')
group.Label = name + ' (four hole tools)'
group.Links = holes
return group
def checked(obj):
doc.recompute()
if obj.Shape.isNull() or not obj.Shape.isValid() or len(obj.Shape.Solids) != 1:
raise RuntimeError('Invalid geometry: ' + obj.Name)
body = cylinder('MotorBody', 'Motor housing - approximate',
'Parameters.BodyDiameter / 2', 'Parameters.BodyThickness', '0 mm')
boss = cylinder('Boss', 'Front boss - approximate',
'Parameters.BossDiameter / 2', 'Parameters.BossHeight', 'Parameters.BodyThickness')
shaft = cylinder('Shaft', 'Shaft - approximate',
'Parameters.ShaftDiameter / 2', 'Parameters.ShaftHeight',
'Parameters.BodyThickness + Parameters.BossHeight')
housing = doc.addObject('Part::MultiFuse', 'Housing')
housing.Label = 'Housing, boss and shaft'
housing.Shapes = [body, boss, shaft]
checked(housing)
front = pattern('Front', 'FrontPCD', 'FrontAngle', 'FrontHoleDiameter', 'FrontHoleDepth',
'Parameters.BodyThickness + Parameters.BossHeight - Parameters.FrontHoleDepth')
cut_front = doc.addObject('Part::Cut', 'FrontHoles')
cut_front.Label = 'Front 4 holes - phase and depth to verify'
cut_front.Base = housing; cut_front.Tool = front
checked(cut_front)
rear_inner = pattern('RearInner', 'RearInnerPCD', 'RearInnerAngle', 'RearHoleDiameter', 'RearHoleDepth', '0 mm')
cut_inner = doc.addObject('Part::Cut', 'RearInnerHoles')
cut_inner.Label = 'Rear inner 4 holes - measured approximately'
cut_inner.Base = cut_front; cut_inner.Tool = rear_inner
checked(cut_inner)
rear_outer = pattern('RearOuter', 'RearOuterPCD', 'RearOuterAngle', 'RearHoleDiameter', 'RearHoleDepth', '0 mm')
final = doc.addObject('Part::Cut', 'Motor')
final.Label = 'MAD M9C12 - approximate motor'
final.Base = cut_inner; final.Tool = rear_outer
checked(final)
center_bore = cylinder('CenterBore', 'Center bore tool',
'Parameters.CenterHoleDiameter / 2',
'Parameters.BodyThickness + Parameters.BossHeight + Parameters.ShaftHeight',
'0 mm')
center_cut = doc.addObject('Part::Cut', 'CenterThrough')
center_cut.Label = 'MAD M9C12 - center through hole'
center_cut.Base, center_cut.Tool = final, center_bore
checked(center_cut)
final = center_cut
for obj in doc.Objects:
if obj.ViewObject:
obj.ViewObject.Visibility = False
final.ViewObject.Visibility = True
final.ViewObject.ShapeColor = (0.47, 0.50, 0.53)
final.ViewObject.Deviation = 0.05
final.ViewObject.AngularDeflection = 10
from pivy import coin
view = Gui.activeDocument().activeView()
# Set the camera directly so a hidden GUI also saves the intended view.
view.getCameraNode().orientation.setValue(coin.SbRotation(0.364705, 0.279848, 0.115917, 0.880476))
view.fitAll()
doc.recompute()
Gui.updateGui()
view.saveImage(str(ROOT / 'models' / 'MAD_M9C12_预览.png'), 1400, 1000, 'White')
doc.saveAs(str(MODEL))
Import.export([final], str(STEP))
print('MODEL_OK', MODEL, 'STEP', STEP, 'volume', final.Shape.Volume,
'bounds', final.Shape.BoundBox)

View File

@@ -0,0 +1,71 @@
from pathlib import Path
import os, time, traceback
import FreeCAD as A, FreeCADGui as G, Part, Import
from pivy import coin
V=A.Vector
R=Path(r'C:\projects\wheel'); O=R/'models'/'kfl_3030_motor_corner_concept'; O.mkdir(parents=True,exist_ok=True)
def box(x,y,z,dx,dy,dz): return Part.makeBox(dx,dy,dz,V(x,y,z))
def cy(r,h,x,y,z,d=V(0,0,1)): return Part.makeCylinder(r,h,V(x,y,z),d)
def add(d,n,s,c):
o=d.addObject('PartDesign::Feature',n);o.Shape=s.removeSplitter();o.Label=n;o.ViewObject.ShapeColor=c;o.ViewObject.LineColor=(.15,.15,.15);return o
ok=False
try:
d=A.openDocument(str(R/'models'/'kfl_3030_concept'/'Assembly.FCStd'));d.Label='3030 base, motor rail corner joints - concept'
d.recompute()
corners=[]; extensions=[]
for i,x in enumerate((-50.6200577,50.6200577)):
rail=d.getObject('NewRail%d'%i);rail.Shape=box(x-15,-85,-215,30,30,255);rail.Label='3030 longitudinal base rail, extended 20 mm'
ext=box(x-15,-85,20,30,30,20);extensions.append(ext)
# Outer size 20 x 20, width 18, thickness 3. Short slot overall 9.5mm, 4mm from tip.
upright=box(x-9,-55,20,18,20,3)
slot=box(x-2.75,-45.75,19.98,5.5,4,3.04).fuse(cy(2.75,3.04,x,-45.75,19.98)).fuse(cy(2.75,3.04,x,-41.75,19.98))
upright=upright.cut(slot)
foot=box(x-9,-55,20,18,3,20).cut(cy(2.75,3.04,x,-55.02,30,V(0,1,0)))
corners.append(add(d,'MotorBaseCorner%d'%i,upright.fuse(foot),(.86,.12,.14)))
d.recompute()
# Only check new angles and the newly added portions of rails against existing visible geometry.
visible=[o for o in d.Objects if hasattr(o,'Shape') and not o.Shape.isNull() and o.ViewObject.Visibility and o not in corners]
lines=[]; bad=[]
for i,a in enumerate(corners):
for b in visible + corners[i+1:]:
v=a.Shape.common(b.Shape).Volume if a.Shape.BoundBox.intersect(b.Shape.BoundBox) else 0.
dist=a.Shape.distToShape(b.Shape)[0]
lines.append('%s / %s: overlap=%.6f mm3, distance=%.4f mm'%(a.Name,b.Name,v,dist))
if v>0.001:bad.append((a.Name,b.Name,v))
for i,ext in enumerate(extensions):
for b in visible:
if b.Name=='NewRail%d'%i: continue
v=ext.common(b.Shape).Volume if ext.BoundBox.intersect(b.Shape.BoundBox) else 0.
if v>0.001:bad.append(('rail extension %d'%i,b.Name,v))
if v>0.001:lines.append('rail extension %d / %s: overlap=%.6f mm3'%(i,b.Name,v))
(O/'interference_report.txt').write_text('\n'.join(lines)+'\n',encoding='utf8')
(O/'positive_intersections.txt').write_text('\n'.join('%s / %s = %.6f mm3'%x for x in bad) or 'none',encoding='utf8')
d.saveAs(str(O/'Assembly.FCStd'));Import.export([o for o in d.Objects if hasattr(o,'Shape')],str(O/'Assembly.step'))
view=G.activeDocument().activeView();G.activateWorkbench('PartWorkbench')
def save(name,mode):
if mode=='front':view.viewFront()
elif mode=='top':view.viewTop()
else:view.viewAxonometric();G.updateGui();view.getCameraNode().orientation.setValue(coin.SbRotation(.880476,.115917,-.279848,-.364705))
G.updateGui();time.sleep(.3);view.fitAll();G.updateGui();time.sleep(.4);view.saveImage(str(O/name),1800,1250,'White')
save('assembly.png','axon')
vis={o.Name:o.ViewObject.Visibility for o in d.Objects if hasattr(o,'Shape')}
# Hide the broad framing/shaft only to make the actual motor connections legible.
names={'Motor','OpenPillarBracket','Holder','Magnet','PCB','Chip','Rail0','Rail1','NewRail0','NewRail1','Corner0','Corner1','Corner2','Corner3','MotorBaseCorner0','MotorBaseCorner1'}
for o in d.Objects:
if hasattr(o,'Shape'):o.ViewObject.Visibility=o.Name in names
save('motor_corner_detail.png','axon');save('motor_corner_top.png','top')
for o in d.Objects:
if hasattr(o,'Shape'):o.ViewObject.Visibility=vis.get(o.Name,True)
d.recompute();d.saveAs(str(O/'Assembly.FCStd'));A.closeDocument(d.Name)
# Reopen FCStd and separately import STEP to confirm persistence/nonempty shape.
d=A.openDocument(str(O/'Assembly.FCStd'))
valid=[(n,not d.getObject(n).Shape.isNull(),d.getObject(n).Shape.isValid()) for n in ['MotorBaseCorner0','MotorBaseCorner1','NewRail0','NewRail1']]
A.closeDocument(d.Name)
stepdoc=A.newDocument('StepVerify');Import.insert(str(O/'Assembly.step'),stepdoc.Name);stepdoc.recompute()
stepcount=sum(1 for o in stepdoc.Objects if hasattr(o,'Shape') and not o.Shape.isNull())
A.closeDocument(stepdoc.Name)
(O/'validation.txt').write_text('FCStd reopen: '+str(valid)+'\nSTEP nonempty shapes: '+str(stepcount)+'\n',encoding='utf8')
(O/'DONE.txt').write_text('ok',encoding='utf8');ok=True
except Exception:
(O/'ERROR.txt').write_text(traceback.format_exc(),encoding='utf8')
finally:os._exit(0 if ok else 1)

View File

@@ -0,0 +1,110 @@
from pathlib import Path
import FreeCAD as A, FreeCADGui as G, Part, Import, os, math, time, json, traceback
from pivy import coin
V=A.Vector
root=Path(r'C:\projects\wheel'); out=root/'models'/'new_motor_corners_1mm_concept';out.mkdir(parents=True,exist_ok=True)
def box(x,y,z,dx,dy,dz):return Part.makeBox(dx,dy,dz,V(x,y,z))
def cyl(r,h,x,y,z,axis=V(0,0,1)):return Part.makeCylinder(r,h,V(x,y,z),axis)
def add(doc,n,shape,label,color,trans=0):
o=doc.addObject('PartDesign::Feature',n);o.Shape=shape.removeSplitter();o.Label=label;o.ViewObject.ShapeColor=color;o.ViewObject.LineColor=(.12,.12,.12);o.ViewObject.Transparency=trans;return o
def vol(a,b):return a.common(b).Volume if a.BoundBox.intersect(b.BoundBox) else 0.
def cc(a,b):return {'vol':vol(a,b),'dist':a.distToShape(b)[0]}
try:
d=A.openDocument(str(root/'models'/'fan_direct_motor_side_140'/'Assembly.FCStd'));d.recompute();d.Label='新电机角码与1mm垫片概念,横梁连接待解决'
p=38/math.sqrt(2); xinner=p+9.5; xwall=xinner+2; xrail=xwall+1; xc=xrail+10
# Reference washer sits on bracket's extrusion facing X surface, never between bracket and motor.
oldlist=['Corner0','Corner1','Corner2','Corner3','Mount76_0','Mount76_1','Mount76_2','Mount76_3','Washer76_0','Washer76_1','Washer76_2','Washer76_3','OpenPillarBracket','Head50_0','Head50_1','Head50_2','Head50_3','Shaft50_0','Shaft50_1','Shaft50_2','Shaft50_3','AdjustShim0','AdjustShim1','AdjustShim2','AdjustShim3','DirectFan','FanBolt0','FanBolt1','Crossbeam0','Crossbeam1','BaseJoint00','BaseJoint01','BaseJoint10','BaseJoint11','PurchasedAngle00','PurchasedAngle01','PurchasedAngle10','PurchasedAngle11','BoardM5_00','BoardM5_01','BoardM5_10','BoardM5_11','FootHead00','FootHead01','FootHead10','FootHead11']
for n in oldlist:
o=d.getObject(n)
if o:o.ViewObject.Visibility=False
# previously purchased profile lengths and axial position, model vertical profiles remain solid envelopes
for i,sgn in enumerate((-1,1)):
rail=d.getObject('Rail%d'%i); rail.Placement=A.Placement();rail.Shape=box(sgn*xc-10,-55,0,20,140,20);rail.Label='Purchased 2020, 140mm'
base=d.getObject('NewRail%d'%i);base.Placement=A.Placement();base.Shape=box(sgn*xc-15,-85,-215,30,30,250);base.Label='Purchased 3030, 250mm'
for stem in ['MotorBaseCorner','SwitchCrossJoint']:
a=d.getObject(stem+str(i));old_center=(-1 if i==0 else 1)*50.6200577
dx=sgn*xc-old_center; s=a.Shape.copy();s.translate(V(dx,0,0));a.Placement=A.Placement();a.Shape=s
# Lighting crossbeam 130mm, fixed by supplied stock; no guessed changes to control profile
sw=d.getObject('SwitchCross2020');sw.Placement=A.Placement();sw.Shape=box(-65,-55,-142,130,20,20);sw.Label='Purchased 2020 switch 130mm'
motor=d.getObject('Motor')
# Cut shape: round end at opposite side of bend. Plan view is x-z extrusion directions; y is 16mm width.
corners=[];washers=[];studs=[];screws=[]
for i,sgn in enumerate((-1,1)):
for j,y in enumerate((10-p,10+p)):
# +X prototype motor leg. 20 length includes 2mm bend wall, 16 width. Round free end R8.
flat=box(xinner-10,y-8,0,12,16,2)
cap=cyl(8,2,xinner-10,y,0).common(box(xinner-18,y-8,0,8,16,2))
leg=flat.fuse(cap).cut(cyl(2.5,2.1,p,y,-.05))
wall=box(xinner,y-8,0,2,16,12)
cap2=cyl(8,2,xinner,y,12,V(1,0,0)).common(box(xinner,y-8,12,2,16,8))
wall=wall.fuse(cap2).cut(cyl(2.5,2.1,xinner-.05,y,11.5,V(1,0,0)))
corner=leg.fuse(wall).removeSplitter()
washer=cyl(4.5,1,xwall,y,11.5,V(1,0,0)).cut(cyl(2.2,1.1,xwall-.05,y,11.5,V(1,0,0)))
# Motor M4 shank and simple head, countersink depth is unknown
motorbolt=cyl(1.95,7,p,y,-5).fuse(cyl(3.5,2.2,p,y,2))
if sgn<0:
corner=corner.mirror(V(),V(1,0,0));washer=washer.mirror(V(),V(1,0,0));motorbolt=motorbolt.mirror(V(),V(1,0,0))
c=add(d,'NewMotorCorner%d%d'%(i,j),corner,'New stainless 20x20x16x2 Ø5, round ends assumed',(.9,.55,.13));corners.append(c)
w=add(d,'RailWasher%d%d'%(i,j),washer,'1mm washer BETWEEN motor corner and 2020 rail',(.95,.2,.12));washers.append(w)
screws.append(add(d,'NewMotorM4%d%d'%(i,j),motorbolt,'M4 motor shank + head schematic',(.65,.65,.68)))
# Encoder bracket as plate-only visualization, 4 hex standoffs per user idea; no claim of finalized printable design
old=d.getObject('OpenPillarBracket').Shape
plate=old.common(box(-70,-70,10.9,140,150,3.2))
bp=add(d,'EncoderPlateConcept',plate,'Encoder panel only; metal standoffs visual concept, not print-ready',(.08,.3,.85))
for i in range(4):
ang=math.radians(45+90*i);x=25*math.cos(ang);y=10+25*math.sin(ang);Rhex=7/(2*math.cos(math.pi/6))
points=[V(x+Rhex*math.cos(math.radians(30+60*k)),y+Rhex*math.sin(math.radians(30+60*k)),0) for k in range(6)];points.append(points[0])
hexshape=Part.Face(Part.makePolygon(points)).extrude(V(0,0,10)).cut(cyl(2.2,10.02,x,y,-.01))
st=add(d,'EncoderHex%d'%i,hexshape,'M4 7mm AF 10mm stand-off ASSUMED',(.86,.72,.3));studs.append(st)
top=cyl(4.5,.9,x,y,10).cut(cyl(2.2,.92,x,y,9.99))
add(d,'EncoderSpacer%d'%i,top,'Encoder metal shim .9mm SCHEMATIC',(.72,.72,.75))
# Explicit real 70mm member collision shown as translucent red geometry; no fake fittings.
conflicts=[]
for i,z in enumerate((-114.,-179.)):
cross=d.getObject('Crossbeam%d'%i); cross.Placement=A.Placement();cross.Shape=box(-35,-87,z,70,30,30);cross.Label='Purchased 3030 70mm - not fitted';cross.ViewObject.Visibility=False
for j in (0,1):
rr=d.getObject('NewRail%d'%j).Shape
c=cross.Shape.common(rr)
if c.Volume>1e-5: conflicts.append((i,j,c.Volume));add(d,'CrossInterference%d%d'%(i,j),c,'UNRESOLVED 70mm crossbar intersects longitudinal rail',(.95,.05,.05),25)
# An initial fan frame rotated for new spacing, check conflicts and hide if nonzero.
theta=math.degrees(math.acos((2*xc)/(105*math.sqrt(2))))-45;alpha=math.radians(theta)
fan=box(-60,-60,20,120,120,25).cut(cyl(56,25.04,0,0,19.98))
for x in (-52.5,52.5):
for y in (-52.5,52.5):fan=fan.cut(cyl(2.2,25.1,x,y,19.95))
fan=fan.removeSplitter();fan.rotate(V(),V(0,0,1),theta);fan.translate(V(0,10,0))
f=d.getObject('DirectFan');f.Placement=A.Placement();f.Shape=fan; f.Label='FAN illustrative; rotated to new spacing';f.ViewObject.Visibility=True
for sgn,i in [(-1,0),(1,1)]:
x=sgn*52.5*(math.cos(alpha)-math.sin(alpha));y=10+sgn*52.5*(math.cos(alpha)+math.sin(alpha))
o=d.getObject('FanBolt%d'%i);o.Placement=A.Placement();o.Shape=cyl(2,25,x,y,20).fuse(cyl(3.5,3,x,y,45));o.ViewObject.Visibility=True
d.recompute()
checks=[]
for i,c in enumerate(corners):
j=i//2;rr=d.getObject('Rail%d'%j).Shape
checks.append({'corner':c.Name,'motor_volume':vol(c.Shape,motor.Shape),'washer_corner':cc(c.Shape,washers[i].Shape),'washer_rail':cc(washers[i].Shape,rr),'hex_volume':sum(vol(c.Shape,s.Shape) for s in studs),'hex_min_distance':min(c.Shape.distToShape(s.Shape)[0] for s in studs)})
fanhits=[]
for o in d.Objects:
if o==f or not hasattr(o,'Shape') or o.Shape.isNull() or not o.ViewObject.Visibility or o.Name.startswith('FanBolt'):continue
v=vol(fan,o.Shape)
if v>1e-4:fanhits.append([o.Name,v])
# hide guessed fan when conflicts (never imply it has been fitted)
if fanhits:
f.ViewObject.Visibility=False
for i in (0,1):d.getObject('FanBolt%d'%i).ViewObject.Visibility=False
d.recompute();d.saveAs(str(out/'Assembly_角码和垫片概念.FCStd'))
selected=[o for o in d.Objects if hasattr(o,'Shape') and not o.Shape.isNull() and o.ViewObject.Visibility]
Import.export(selected,str(out/'Assembly_角码和垫片概念.step'))
view=G.activeDocument().activeView();G.activateWorkbench('PartWorkbench');view.viewAxonometric();G.updateGui();view.fitAll();time.sleep(.5);G.updateGui();view.saveImage(str(out/'总装_横梁有干涉.png'),1800,1300,'White')
vis={o.Name:o.ViewObject.Visibility for o in d.Objects if hasattr(o,'Shape')}; show={'Motor','Holder','Magnet','PCB','Chip','EncoderPlateConcept','Rail0','Rail1','NewRail0','NewRail1','MotorBaseCorner0','MotorBaseCorner1'}|{o.Name for o in corners+washers+studs+screws}|{'DirectFan','FanBolt0','FanBolt1'}
for o in d.Objects:
if hasattr(o,'Shape'):o.ViewObject.Visibility=(o.Name in show and (not fanhits or not o.Name.startswith(('DirectFan','FanBolt'))))
view.viewTop();G.updateGui();view.fitAll();time.sleep(.5);G.updateGui();view.saveImage(str(out/'电机端_正视.png'),1800,1300,'White')
view.viewAxonometric();G.updateGui();view.fitAll();time.sleep(.5);G.updateGui();view.saveImage(str(out/'角码与垫片_3D.png'),1800,1300,'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('Rail0').Shape.isValid(); A.closeDocument(d.Name)
step=Part.read(str(out/'Assembly_角码和垫片概念.step')); assert step.isValid()
info={'all_user_lengths_mm':{'3030_long':250,'3030_cross':70,'2020_motor':140,'2020_switch':130},'hole_centres_76':p,'corner_wall_outside_x':xwall,'rail_inside_x':xrail,'washer_thickness':1,'rail_centre_x':xc,'motor_track_centres_mm':2*xc,'3030_inside_gap_mm':2*(xc-15),'70_cross_member_length_mm':70,'crossbar_intersections_mm3':conflicts,'angle_rotation_degrees':theta,'corner_checks':checks,'fan_hits':fanhits,'note':'Unfinished assembly: 70mm crossbeams hidden and actual intersection shown red. Base brackets at crossbeam hidden. New parts and holes schematic; no real T slots, fasteners or fan hubs.'}
(out/'验证.json').write_text(json.dumps(info,ensure_ascii=False,indent=2),encoding='utf8');(out/'完成.txt').write_text('ok',encoding='utf8');os._exit(0)
except Exception:
(out/'错误.txt').write_text(traceback.format_exc(),encoding='utf8');os._exit(1)

View File

@@ -0,0 +1,120 @@
from pathlib import Path
import FreeCAD as A, FreeCADGui as G, Part, Import, os, math, time, json, traceback
from pivy import coin
V=A.Vector
root=Path(r'C:\projects\wheel'); out=root/'models'/'new_motor_corners_2mm_concept';out.mkdir(parents=True,exist_ok=True)
def box(x,y,z,dx,dy,dz):return Part.makeBox(dx,dy,dz,V(x,y,z))
def cyl(r,h,x,y,z,axis=V(0,0,1)):return Part.makeCylinder(r,h,V(x,y,z),axis)
def add(doc,n,shape,label,color,trans=0):
o=doc.addObject('PartDesign::Feature',n);o.Shape=shape.removeSplitter();o.Label=label;o.ViewObject.ShapeColor=color;o.ViewObject.LineColor=(.12,.12,.12);o.ViewObject.Transparency=trans;return o
def vol(a,b):return a.common(b).Volume if a.BoundBox.intersect(b.BoundBox) else 0.
def cc(a,b):return {'vol':vol(a,b),'dist':a.distToShape(b)[0]}
try:
d=A.openDocument(str(root/'models'/'fan_direct_motor_side_140'/'Assembly.FCStd'));d.recompute();d.Label='新电机角码与2mm垫片概念装配'
p=38/math.sqrt(2); xinner=p+9.5; xwall=xinner+2; xrail=xwall+2; xc=xrail+10
# Reference washer sits on bracket's extrusion facing X surface, never between bracket and motor.
oldlist=['Corner0','Corner1','Corner2','Corner3','Mount76_0','Mount76_1','Mount76_2','Mount76_3','Washer76_0','Washer76_1','Washer76_2','Washer76_3','OpenPillarBracket','Head50_0','Head50_1','Head50_2','Head50_3','Shaft50_0','Shaft50_1','Shaft50_2','Shaft50_3','AdjustShim0','AdjustShim1','AdjustShim2','AdjustShim3','DirectFan','FanBolt0','FanBolt1','Crossbeam0','Crossbeam1','BaseJoint00','BaseJoint01','BaseJoint10','BaseJoint11','PurchasedAngle00','PurchasedAngle01','PurchasedAngle10','PurchasedAngle11','BoardM5_00','BoardM5_01','BoardM5_10','BoardM5_11','FootHead00','FootHead01','FootHead10','FootHead11']
for n in oldlist:
o=d.getObject(n)
if o:o.ViewObject.Visibility=False
# previously purchased profile lengths and axial position, model vertical profiles remain solid envelopes
for i,sgn in enumerate((-1,1)):
rail=d.getObject('Rail%d'%i); rail.Placement=A.Placement();rail.Shape=box(sgn*xc-10,-55,0,20,140,20);rail.Label='Purchased 2020, 140mm'
base=d.getObject('NewRail%d'%i);base.Placement=A.Placement();base.Shape=box(sgn*xc-15,-85,-215,30,30,250);base.Label='Purchased 3030, 250mm'
for stem in ['MotorBaseCorner','SwitchCrossJoint']:
a=d.getObject(stem+str(i));old_center=(-1 if i==0 else 1)*50.6200577
dx=sgn*xc-old_center; s=a.Shape.copy();s.translate(V(dx,0,0));a.Placement=A.Placement();a.Shape=s
# Lighting crossbeam 130mm, fixed by supplied stock; no guessed changes to control profile
sw=d.getObject('SwitchCross2020');sw.Placement=A.Placement();sw.Shape=box(-65,-55,-142,130,20,20);sw.Label='Purchased 2020 switch 130mm'
motor=d.getObject('Motor')
# Cut shape: round end at opposite side of bend. Plan view is x-z extrusion directions; y is 16mm width.
corners=[];washers=[];studs=[];screws=[]
for i,sgn in enumerate((-1,1)):
for j,y in enumerate((10-p,10+p)):
# +X prototype motor leg. 20 length includes 2mm bend wall, 16 width. Round free end R8.
flat=box(xinner-10,y-8,0,12,16,2)
cap=cyl(8,2,xinner-10,y,0).common(box(xinner-18,y-8,0,8,16,2))
leg=flat.fuse(cap).cut(cyl(2.5,2.1,p,y,-.05))
wall=box(xinner,y-8,0,2,16,12)
cap2=cyl(8,2,xinner,y,12,V(1,0,0)).common(box(xinner,y-8,12,2,16,8))
wall=wall.fuse(cap2).cut(cyl(2.5,2.1,xinner-.05,y,11.5,V(1,0,0)))
corner=leg.fuse(wall).removeSplitter()
washer=cyl(4.5,2,xwall,y,11.5,V(1,0,0)).cut(cyl(2.2,2.1,xwall-.05,y,11.5,V(1,0,0)))
# Motor M4 shank and simple head, countersink depth is unknown
motorbolt=cyl(1.95,7,p,y,-5).fuse(cyl(3.5,2.2,p,y,2))
if sgn<0:
corner=corner.mirror(V(),V(1,0,0));washer=washer.mirror(V(),V(1,0,0));motorbolt=motorbolt.mirror(V(),V(1,0,0))
c=add(d,'NewMotorCorner%d%d'%(i,j),corner,'New stainless 20x20x16x2 Ø5, round ends assumed',(.9,.55,.13));corners.append(c)
w=add(d,'RailWasher%d%d'%(i,j),washer,'2mm total washer BETWEEN motor corner and 2020 rail',(.95,.2,.12));washers.append(w)
screws.append(add(d,'NewMotorM4%d%d'%(i,j),motorbolt,'M4 motor shank + head schematic',(.65,.65,.68)))
# Encoder bracket as plate-only visualization, 4 hex standoffs per user idea; no claim of finalized printable design
old=d.getObject('OpenPillarBracket').Shape
plate=old.common(box(-70,-70,10.9,140,150,3.2))
bp=add(d,'EncoderPlateConcept',plate,'Encoder panel only; metal standoffs visual concept, not print-ready',(.08,.3,.85))
for i in range(4):
ang=math.radians(45+90*i);x=25*math.cos(ang);y=10+25*math.sin(ang);Rhex=7/(2*math.cos(math.pi/6))
points=[V(x+Rhex*math.cos(math.radians(30+60*k)),y+Rhex*math.sin(math.radians(30+60*k)),0) for k in range(6)];points.append(points[0])
hexshape=Part.Face(Part.makePolygon(points)).extrude(V(0,0,10)).cut(cyl(2.2,10.02,x,y,-.01))
st=add(d,'EncoderHex%d'%i,hexshape,'M4 7mm AF 10mm stand-off ASSUMED',(.86,.72,.3));studs.append(st)
top=cyl(4.5,.9,x,y,10).cut(cyl(2.2,.92,x,y,9.99))
add(d,'EncoderSpacer%d'%i,top,'Encoder metal shim .9mm SCHEMATIC',(.72,.72,.75))
# Two purchased 70mm beams are centred between the 3030 rails.
# The original mounting corners remain at the same ends, translated with the new rails.
conflicts=[]; joint_checks=[]
for i,z in enumerate((-114.,-179.)):
cross=d.getObject('Crossbeam%d'%i); cross.Placement=A.Placement();cross.Shape=box(-35,-87,z,70,30,30);cross.Label='Purchased 3030 crossbeam 70mm';cross.ViewObject.Visibility=True
for j,sgn in enumerate((-1,1)):
rr=d.getObject('NewRail%d'%j).Shape
c=cross.Shape.common(rr)
if c.Volume>1e-5: conflicts.append((i,j,c.Volume))
name='BaseJoint%d%d'%(i,j); a=d.getObject(name)
ss=a.Shape.copy();ss.translate(V(sgn*(xc-50.6200577),0,0));a.Placement=A.Placement();a.Shape=ss;a.ViewObject.Visibility=True
joint_checks.append({'name':name,'cross':cc(a.Shape,cross.Shape),'base':cc(a.Shape,rr)})
for stem in ('PurchasedAngle','BoardM5_','FootHead'):
for j in (0,1):
o=d.getObject(stem+str(i)+str(j))
if o:o.ViewObject.Visibility=True
# An initial fan frame rotated for new spacing, check conflicts and hide if nonzero.
theta=math.degrees(math.acos((2*xc)/(105*math.sqrt(2))))-45;alpha=math.radians(theta)
fan=box(-60,-60,20,120,120,25).cut(cyl(56,25.04,0,0,19.98))
for x in (-52.5,52.5):
for y in (-52.5,52.5):fan=fan.cut(cyl(2.2,25.1,x,y,19.95))
fan=fan.removeSplitter();fan.rotate(V(),V(0,0,1),theta);fan.translate(V(0,10,0))
f=d.getObject('DirectFan');f.Placement=A.Placement();f.Shape=fan; f.Label='FAN illustrative; rotated to new spacing';f.ViewObject.Visibility=True
for sgn,i in [(-1,0),(1,1)]:
x=sgn*52.5*(math.cos(alpha)-math.sin(alpha));y=10+sgn*52.5*(math.cos(alpha)+math.sin(alpha))
o=d.getObject('FanBolt%d'%i);o.Placement=A.Placement();o.Shape=cyl(2,25,x,y,20).fuse(cyl(3.5,3,x,y,45));o.ViewObject.Visibility=True
d.recompute()
checks=[]
for i,c in enumerate(corners):
j=i//2;rr=d.getObject('Rail%d'%j).Shape
checks.append({'corner':c.Name,'motor_volume':vol(c.Shape,motor.Shape),'washer_corner':cc(c.Shape,washers[i].Shape),'washer_rail':cc(washers[i].Shape,rr),'hex_volume':sum(vol(c.Shape,s.Shape) for s in studs),'hex_min_distance':min(c.Shape.distToShape(s.Shape)[0] for s in studs)})
fanhits=[]
for o in d.Objects:
if o==f or not hasattr(o,'Shape') or o.Shape.isNull() or not o.ViewObject.Visibility or o.Name.startswith('FanBolt'):continue
v=vol(fan,o.Shape)
if v>1e-4:fanhits.append([o.Name,v])
# hide guessed fan when conflicts (never imply it has been fitted)
if fanhits:
f.ViewObject.Visibility=False
for i in (0,1):d.getObject('FanBolt%d'%i).ViewObject.Visibility=False
d.recompute();d.saveAs(str(out/'Assembly_2mm垫片装配.FCStd'))
selected=[o for o in d.Objects if hasattr(o,'Shape') and not o.Shape.isNull() and o.ViewObject.Visibility]
Import.export(selected,str(out/'Assembly_2mm垫片装配.step'))
view=G.activeDocument().activeView();G.activateWorkbench('PartWorkbench');view.viewAxonometric();G.updateGui();view.fitAll();time.sleep(.5);G.updateGui();view.saveImage(str(out/'2mm方案_总装.png'),1800,1300,'White')
vis={o.Name:o.ViewObject.Visibility for o in d.Objects if hasattr(o,'Shape')}; show={'Motor','Holder','Magnet','PCB','Chip','EncoderPlateConcept','Rail0','Rail1','NewRail0','NewRail1','MotorBaseCorner0','MotorBaseCorner1'}|{o.Name for o in corners+washers+studs+screws}|{'DirectFan','FanBolt0','FanBolt1'}
for o in d.Objects:
if hasattr(o,'Shape'):o.ViewObject.Visibility=(o.Name in show and (not fanhits or not o.Name.startswith(('DirectFan','FanBolt'))))
view.viewTop();G.updateGui();view.fitAll();time.sleep(.5);G.updateGui();view.saveImage(str(out/'电机端_正视.png'),1800,1300,'White')
view.viewAxonometric();G.updateGui();view.fitAll();time.sleep(.5);G.updateGui();view.saveImage(str(out/'角码与垫片_3D.png'),1800,1300,'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_2mm垫片装配.FCStd'));A.closeDocument(d.Name)
d=A.openDocument(str(out/'Assembly_2mm垫片装配.FCStd'));d.recompute(); assert d.getObject('Rail0').Shape.isValid(); A.closeDocument(d.Name)
step=Part.read(str(out/'Assembly_2mm垫片装配.step')); assert step.isValid()
info={'all_user_lengths_mm':{'3030_long':250,'3030_cross':70,'2020_motor':140,'2020_switch':130},'hole_centres_76':p,'corner_wall_outside_x':xwall,'rail_inside_x':xrail,'washer_thickness':2,'base_joint_checks':joint_checks,'rail_centre_x':xc,'motor_track_centres_mm':2*xc,'3030_inside_gap_mm':2*(xc-15),'70_cross_member_length_mm':70,'crossbar_intersections_mm3':conflicts,'angle_rotation_degrees':theta,'corner_checks':checks,'fan_hits':fanhits,'note':'Concept assembly: 70mm crossbeams and original base joints restored; real T slots, screws and fan hubs unmodelled.'}
(out/'验证.json').write_text(json.dumps(info,ensure_ascii=False,indent=2),encoding='utf8');(out/'完成.txt').write_text('ok',encoding='utf8');os._exit(0)
except Exception:
(out/'错误.txt').write_text(traceback.format_exc(),encoding='utf8');os._exit(1)

View File

@@ -0,0 +1,116 @@
"""Concept test only: 50mm mounting with open slots, corners flat on motor."""
from pathlib import Path
import time, math, traceback
import FreeCAD as A, FreeCADGui as G, Part, Import
from pivy import coin
R=Path(__file__).resolve().parent.parent
O=R/'models'/'profile_mount_check'
V=A.Vector
def cyl(r,h,x,y,z): return Part.makeCylinder(r,h,V(x,y,z))
def add(doc,name,label,sh,color,trans=0):
o=doc.addObject('PartDesign::Feature',name); o.Label=label; o.Shape=sh
o.ViewObject.ShapeColor=color; o.ViewObject.LineColor=(.1,.1,.1); o.ViewObject.Transparency=trans
return o
try:
old=A.openDocument(str(O/'Direct.FCStd'))
keep=['Motor','Holder','Magnet','PCB','Chip']+['PCBHead%d'%i for i in range(1,4)]+['Corner%d'%i for i in range(4)]+['Rail%d'%i for i in range(2)]+['MountM4_%d'%i for i in range(1,5)]
shapes={n:old.getObject(n).Shape.copy() for n in keep}
orig=old.getObject('Bracket').Shape.copy()
shims=[old.getObject('Shim%d'%i).Shape.copy() for i in range(4)]
A.closeDocument(old.Name)
doc=A.newDocument('OpenSlot50')
colors={'Motor':(.32,.34,.37),'Holder':(.98,.55,.10),'Magnet':(.8,.8,.8),'PCB':(.04,.65,.19),'Chip':(.1,.1,.1)}
objects={}
for n in keep:
color=colors.get(n,(.84,.47,.13) if n.startswith('Corner') else (.55,.56,.60) if n.startswith('Rail') else (.8,.8,.8))
objects[n]=add(doc,n,n,shapes[n],color,70 if n=='Motor' else 20 if n.startswith('Corner') else 0)
pts=[]
for i in range(4):
a=math.radians(45+90*i);pts.append((25*math.cos(a),25*math.sin(a)))
# Existing foot top is z=2.5 with 0.5 shim. Increase clamping surface to 3.5;
# subtract motor-side angle brackets, keeping them directly on motor.
bracket=orig
for x,y in pts:
bracket=bracket.fuse(cyl(4.5,1,x,y,2.5))
# Add 0.2mm plan clearance to each metal foot and 0.2mm above it.
cutters=[]
for i in range(4):
bb=shapes['Corner%d'%i].BoundBox
cutters.append(Part.makeBox(bb.XLength+.4,bb.YLength+.4,3.22,V(bb.XMin-.2,bb.YMin-.2,-.01)))
for cutter in cutters: bracket=bracket.cut(cutter)
# Four slots open in the same -Y direction; existing Ø4.4 clearance continues.
for x,y in pts:
slot=Part.makeBox(4.4,6,3.7,V(x-2.2,y-6,-.05)).fuse(cyl(2.2,3.7,x,y,-.05))
bracket=bracket.cut(slot)
for j in range(4):
shims[j]=shims[j].cut(slot)
for j in range(4):
for cutter in cutters: shims[j]=shims[j].cut(cutter)
bracket=bracket.removeSplitter()
if bracket.isNull() or not bracket.isValid() or len(bracket.Solids)!=1:raise RuntimeError('support not a single solid')
blue=add(doc,'OpenBracket','PCD50 4 open slots - concept',bracket,(.12,.43,.88))
for i,sh in enumerate(shims):
sh=sh.removeSplitter()
if sh.isNull() or not sh.isValid():raise RuntimeError('shim invalid %d'%i)
add(doc,'SlottedShim%d'%i,'Open 0.5mm shim - concept',sh,(.65,.7,.75))
heads=[];shafts=[]
for i,(x,y) in enumerate(pts):
head=cyl(3.5,3,x,y,3.5);shaft=cyl(1.95,8.5,x,y,-5)
heads.append(add(doc,'EncoderHead%d'%i,'M4 head Ø7 x 3 - assumed',head,(.8,.8,.8)))
shafts.append(add(doc,'EncoderShaft%d'%i,'M4 shaft Ø3.9 - assumed',shaft,(.8,.8,.8)))
doc.recompute()
corners=[objects['Corner%d'%i] for i in range(4)]
rails=[objects['Rail%d'%i] for i in range(2)]
moving=[blue]+[doc.getObject('SlottedShim%d'%i) for i in range(4)]+heads+shafts
checks=[]
for o in moving:
for c in corners+rails:
if not o.Shape.BoundBox.intersect(c.Shape.BoundBox):continue
vol=o.Shape.common(c.Shape).Volume
if vol>.0001:checks.append('%s vs %s %.6f mm3'%(o.Name,c.Name,vol))
for o in shafts:
for c in [blue]+[doc.getObject('SlottedShim%d'%i) for i in range(4)]:
if not o.Shape.BoundBox.intersect(c.Shape.BoundBox): continue
vol=o.Shape.common(c.Shape).Volume
if vol>.0001: checks.append('%s vs %s %.6f mm3'%(o.Name,c.Name,vol))
# Effective supporting material under each head immediately below the top.
areas=[]
for x,y in pts:
# Planar sampling avoids degenerate thin-cylinder OCC intersections.
count=0; step=.1
for ix in range(-35,36):
for iy in range(-35,36):
dx=ix*step;dy=iy*step
if dx*dx+dy*dy<=3.5*3.5 and bracket.isInside(V(x+dx,y+dy,3.49),1e-7,True):count+=1
areas.append(count*step*step)
# Kinematic slide of support from y=+5 to its assembled position: only a
# quick volume test of support vs angles + shafts, not a tool/hand check.
slide=[]
for n in range(11):
dy=5.0-n*.5; moved=bracket.copy();moved.translate(V(0,dy,0));hits=[]
for c in corners+rails+shafts:
if moved.BoundBox.intersect(c.Shape.BoundBox):
v=moved.common(c.Shape).Volume
if v>.0001:hits.append('%s %.4f'%(c.Name,v))
slide.append('dy=%.1f: %s'%(dy,', '.join(hits) if hits else 'no sampled volume overlap'))
report=['CONCEPT ONLY; corners are flush to motor; no spacer under PCD76 corners.',
'Support feet 0.5..3.5mm; shim 0.5mm; screw head 7mm dia x 3mm assumed; 0.2mm corner clearance.',
'PCD50 screw slots open toward -Y; all four are parallel.',
'Only 0.05mm theoretical slot-end clearance in motor corner as original direct check.',
'Intersections of support/shims/screw envelope with corners and rails:']+(checks or ['none in checked pairs'])
report+=['Approx material area under 7mm screw head using 0.1mm sampled grid (mm2): '+', '.join('%.3f'%x for x in areas),'Sampled lateral slide +Y to 0:']+slide
(O/'OpenSlot50_check.txt').write_text('\n'.join(report)+'\n',encoding='utf8')
view=G.activeDocument().activeView()
def render(kind,name):
if kind=='rear':view.viewTop()
else:view.getCameraNode().orientation.setValue(coin.SbRotation(.880476,.115917,-.279848,-.364705))
G.updateGui();time.sleep(.5);view.fitAll();G.updateGui();time.sleep(.5);view.saveImage(str(O/name),1600,1200,'White')
render('rear','OpenSlot50_rear.png');render('axon','OpenSlot50_axon.png')
doc.recompute();doc.saveAs(str(O/'OpenSlot50.FCStd'))
Import.export([o for o in doc.Objects if hasattr(o,'Shape')],str(O/'OpenSlot50.step'))
(O/'OPEN_SLOT_DONE.txt').write_text('ok\n',encoding='utf8')
except Exception:
(O/'OPEN_SLOT_ERROR.txt').write_text(traceback.format_exc(),encoding='utf8')
raise

View File

@@ -0,0 +1,147 @@
"""PCD50 square-open bracket, with PCD76 corner slots offset 2 mm.
Concept assembly only. Rebuild via rebuild_pcd50_fit_concept.FCMacro.
"""
from pathlib import Path
import math, time, traceback
import FreeCAD as A, FreeCADGui as G, Part, Import
from pivy import coin
R=Path(__file__).resolve().parent.parent
O=R/'models'/'pcd50_fit_concept'; O.mkdir(parents=True,exist_ok=True)
V=A.Vector
def cy(r,h,x,y,z): return Part.makeCylinder(r,h,V(x,y,z))
def add(doc,n,label,s,c,t=0):
o=doc.addObject('PartDesign::Feature',n); o.Label=label; o.Shape=s
o.ViewObject.ShapeColor=c;o.ViewObject.LineColor=(.1,.1,.1);o.ViewObject.Transparency=t
return o
try:
src=A.openDocument(str(R/'models'/'profile_mount_check'/'Direct.FCStd'))
keep=['Motor','Holder','Magnet','PCB','Chip']+['PCBHead%d'%i for i in range(1,4)]+['Corner%d'%i for i in range(4)]+['Rail%d'%i for i in range(2)]
shapes={n:src.getObject(n).Shape.copy() for n in keep}
orig=src.getObject('Bracket').Shape.copy()
shims=[src.getObject('Shim%d'%i).Shape.copy() for i in range(4)]
A.closeDocument(src.Name)
# The old Direct model moved each bracket 2.75mm outwards. Inwards by
# 0.75mm makes the new shift 2.00mm, leaving nominal slot clearance.
for n in ['Corner%d'%i for i in range(4)]+['Rail%d'%i for i in range(2)]:
s=shapes[n]; cx=(s.BoundBox.XMin+s.BoundBox.XMax)/2
s.translate(V(-.75 if cx>0 else .75,0,0))
doc=A.newDocument('PCD50_Fit_2mm'); objs={}
colors={'Motor':(.32,.34,.37),'Holder':(.98,.55,.1),'Magnet':(.8,.8,.8),'PCB':(.04,.65,.19),'Chip':(.1,.1,.1)}
for n,s in shapes.items():
c=colors.get(n,(.84,.47,.13) if n.startswith('Corner') else (.55,.56,.60) if n.startswith('Rail') else (.8,.8,.8))
objs[n]=add(doc,n,n,s,c,70 if n=='Motor' else 20 if n.startswith('Corner') else 0)
pts=[(25*math.cos(math.radians(45+90*i)),25*math.sin(math.radians(45+90*i))) for i in range(4)]
# A 49mm frame leaves about 2.7mm beside the 8.2mm tool opening.
# Its outer corners are notched to permit PCD76 fastening tool access.
square=Part.makeBox(49,49,3.2,V(-24.5,-24.5,10.9)).cut(cy(16.8,3.22,0,0,10.89))
p76=38/math.sqrt(2)
for sx in (-1,1):
for sy in (-1,1):
square=square.cut(cy(5.2,3.24,sx*p76,sy*p76,10.88))
bracket=orig.fuse(square)
for x,y in pts:bracket=bracket.fuse(cy(4.5,1,x,y,2.5))
cutters=[]
for i in range(4):
bb=shapes['Corner%d'%i].BoundBox
cutters.append(Part.makeBox(bb.XLength+.4,bb.YLength+.4,3.22,V(bb.XMin-.2,bb.YMin-.2,-.01)))
for cutter in cutters:bracket=bracket.cut(cutter)
# Outward opening towards left/right per user sketch, with upper access.
for x,y in pts:
start=x if x>0 else x-10
slot=Part.makeBox(10,4.4,3.7,V(start,y-2.2,-.05)).fuse(cy(2.2,3.7,x,y,-.05))
upper=Part.makeBox(10,8.2,11,V(start,y-4.1,3.5)).fuse(cy(4.1,11,x,y,3.5))
bracket=bracket.cut(slot).cut(upper)
for j in range(4):shims[j]=shims[j].cut(slot)
for j in range(4):
for cutter in cutters:shims[j]=shims[j].cut(cutter)
bracket=bracket.removeSplitter()
if bracket.isNull() or not bracket.isValid() or len(bracket.Solids)!=1: raise RuntimeError('invalid bracket')
b=add(doc,'OpenBracket','PCD50 square frame, outward open - concept',bracket,(.12,.43,.88))
shimobjs=[]
for i,s in enumerate(shims):
s=s.removeSplitter()
if s.isNull() or not s.isValid() or len(s.Solids)!=1:raise RuntimeError('invalid shim %d'%i)
shimobjs.append(add(doc,'Shim50_%d'%i,'0.5mm open adjustment shim - concept',s,(.70,.70,.75)))
heads=[];shafts=[];mount76=[];washers76=[]
for i,(x,y) in enumerate(pts):
heads.append(add(doc,'Head50_%d'%i,'M4 50 head dia 7, height 3 assumed',cy(3.5,3,x,y,3.5),(.8,.8,.8)))
shafts.append(add(doc,'Shaft50_%d'%i,'M4x8 50 screw shank Ø4 concept',cy(2,8,x,y,-4.5),(.8,.8,.8)))
p=38/math.sqrt(2)
for sx in (-1,1):
for sy in (-1,1):
x,y=sx*p,sy*p
w=cy(5,.8,x,y,3).cut(cy(2.15,.82,x,y,2.99)).removeSplitter()
washers76.append(add(doc,'Washer76_%d'%len(washers76),'M4 washer Ø10 x 0.8 assumed',w,(.75,.75,.77)))
s=cy(2,8,x,y,-4.2).fuse(cy(3.5,2.5,x,y,3.8)).removeSplitter()
mount76.append(add(doc,'Mount76_%d'%len(mount76),'M4 76 shank Ø4 head Ø7 concept',s,(.8,.8,.8)))
doc.recompute(); corners=[objs['Corner%d'%i] for i in range(4)]; rails=[objs['Rail%d'%i] for i in range(2)]
pairs=[]
for a in [b]+shimobjs+heads+shafts:
for c in corners+rails:pairs.append((a,c))
for a in shafts:
for c in [b]+shimobjs:pairs.append((a,c))
for a in mount76:
for c in corners+rails+[b]+shimobjs+heads+shafts:pairs.append((a,c))
for a in washers76:
for c in corners+rails+[b]+shimobjs+heads+shafts+mount76:pairs.append((a,c))
for a in [b]+shimobjs+heads+shafts:
for n in ['Motor','Holder','Magnet','PCB','Chip']+['PCBHead%d'%i for i in range(1,4)]:pairs.append((a,objs[n]))
errors=[]
for a,c in pairs:
if not a.Shape.BoundBox.intersect(c.Shape.BoundBox):continue
vol=a.Shape.common(c.Shape).Volume
if vol>.001:errors.append('%s / %s %.6fmm3'%(a.Name,c.Name,vol))
# Record tool clearance against corner/support for four assumed 7.5mm
# axial screwdriver envelopes. Start above the modelled screw heads.
access=[]
for i,(x,y) in enumerate(pts):
tool=cy(3.75,20,x,y,6.51); hits=[]
for o in [b]+corners+rails:
if tool.BoundBox.intersect(o.Shape.BoundBox):
v=tool.common(o.Shape).Volume
if v>.001:hits.append('%s %.4f'%(o.Name,v))
access.append('50 head %d: %s'%(i,', '.join(hits) if hits else 'clear (model envelope only)'))
for sx in (-1,1):
for sy in (-1,1):
x,y=sx*p,sy*p; tool=cy(3.75,20,x,y,6.31); hits=[]
for o in [b]+corners+rails:
if tool.BoundBox.intersect(o.Shape.BoundBox):
v=tool.common(o.Shape).Volume
if v>.001:hits.append('%s %.4f'%(o.Name,v))
access.append('76 head %+.0f,%+.0f: %s'%(sx,sy,', '.join(hits) if hits else 'clear (model envelope only)'))
areas=[]
for x,y in pts:
count=0; step=.2
for ix in range(-18,19):
for iy in range(-18,19):
dx,dy=ix*step,iy*step
if dx*dx+dy*dy<=3.5**2 and bracket.isInside(V(x+dx,y+dy,3.49),1e-7,True):count+=1
areas.append(count*step*step)
report=['Concept only. PCD50 and PCD76 phase 45 deg assumed; 4x M4 Ø4.',
'PCD76 slot 9.5 x 5.5 assumed; angle brackets remain flush and shifted 2.00mm outwards.',
'Nominal M4 versus slot-end clearance: 0.75mm. 50 head Ø7 x 3 assumed.',
'76 washer Ø10x0.8 assumed; M4x8 gives nominal 4.2mm engagement, motor hole depth unknown.',
'0.5mm shim; 50 foot top 3.5mm; original PCB and magnet position unchanged.',
'Detected positive volumes >0.001mm3:']+(errors or ['none in checked pairs'])
report+=['Tool access, Ø7.5 envelope from z6.51:']+access+['Sampled 50 head contact areas, mm2 (not strength): '+', '.join('%.2f'%x for x in areas)]
(O/'geometry_check.txt').write_text('\n'.join(report)+'\n',encoding='utf-8')
G.activateWorkbench('PartWorkbench');view=G.activeDocument().activeView()
def render(top,path):
if top:view.viewTop()
else:
view.viewAxonometric();G.updateGui();time.sleep(.5)
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(True,'rear.png');render(False,'assembly.png')
states={o.Name:o.ViewObject.Visibility for o in doc.Objects if hasattr(o,'Shape')}
for o in doc.Objects:
if hasattr(o,'Shape'):o.ViewObject.Visibility=False
b.ViewObject.Visibility=True;render(True,'bracket.png')
for o in doc.Objects:
if hasattr(o,'Shape'):o.ViewObject.Visibility=states[o.Name]
render(False,'assembly.png');doc.recompute();doc.saveAs(str(O/'PCD50_Assembly.FCStd'))
Import.export([o for o in doc.Objects if hasattr(o,'Shape')],str(O/'PCD50_Assembly.step'))
(O/'DONE.txt').write_text('ok\n',encoding='utf-8')
except Exception:
(O/'ERROR.txt').write_text(traceback.format_exc(),encoding='utf-8');raise

View File

@@ -0,0 +1,135 @@
"""Place the bracket and both sets of shims on one print plate unchanged.
Run with FreeCAD's Python, for example:
& 'C:\\Users\\billy\\scoop\\apps\\freecad\\current\\bin\\python.exe' scripts/build_pcd50_full_print_plate.py
The plate is one bracket, eight 0.5 mm shims, and eight 1 mm shims.
Output is independent of the input files.
"""
from pathlib import Path
import json
import itertools
import math
import FreeCAD as App
import Mesh
ROOT = Path(__file__).resolve().parent.parent
SOURCE = ROOT / 'models' / 'pcd50_open_pillar_v1'
OUT = ROOT / 'models' / 'pcd50_full_print_plate'
OUT.mkdir(parents=True, exist_ok=True)
V = App.Vector
def load(name):
mesh = Mesh.Mesh(str(SOURCE / name))
if len(mesh.getSeparateComponents()) != 1 or not mesh.isSolid():
raise RuntimeError('Input is not one closed component: ' + name)
return mesh
bracket = load('EncoderBracket.stl')
shim05 = load('Shim05.stl')
shim10 = load('Shim10.stl')
# Two visually distinct 2x4 groups below the bracket. All nominal XY
# center distances are 20 mm, except for the 40 mm gap between groups.
# Every placement uses the original tessellation; no rotations or rescaling.
row05 = [(x, y) for y in (6.0,26.0,46.0,66.0) for x in (6.0,26.0)]
row10 = [(x, y) for y in (6.0,26.0,46.0,66.0) for x in (66.0,86.0)]
bracket_pos = (46.0, 109.5)
def moved(mesh, x, y):
result = mesh.copy()
result.translate(x, y, -mesh.BoundBox.ZMin)
return result
def bounds(m):
b = m.BoundBox
return [b.XMin, b.XMax, b.YMin, b.YMax, b.ZMin, b.ZMax]
def distance_xy(a, b):
a, b = a.BoundBox, b.BoundBox
dx = max(a.XMin - b.XMax, b.XMin - a.XMax, 0.0)
dy = max(a.YMin - b.YMax, b.YMin - a.YMax, 0.0)
return math.hypot(dx, dy)
def equivalent(a, b):
"""Compare vertices and facet topology after removing translation.
Float STL coordinates round slightly when translated/reloaded. A small
tolerance is therefore used rather than comparing quantized text.
"""
if a.CountFacets != b.CountFacets:
return False
pa, fa = a.Topology; pb, fb = b.Topology
if len(pa) != len(pb):
return False
ba, bb = a.BoundBox, b.BoundBox
def norm(p, box): return (p.x-box.XMin,p.y-box.YMin,p.z-box.ZMin)
qa=[norm(p,ba) for p in pa]; qb=[norm(p,bb) for p in pb]
tol=0.0001
def key(v): return tuple(math.floor(x/tol) for x in v)
bins={}
for i,v in enumerate(qa):bins.setdefault(key(v),[]).append(i)
matches=[]
for v in qb:
k=key(v); candidates=[]
for offset in itertools.product((-1,0,1),repeat=3):
for i in bins.get(tuple(k[j]+offset[j] for j in range(3)),[]):
dist=max(abs(v[j]-qa[i][j]) for j in range(3))
if dist<tol:candidates.append((dist,i))
if not candidates:return False
matches.append(min(candidates)[1])
if len(set(matches)) != len(qa):return False
af=sorted(tuple(sorted(tri)) for tri in fa)
bf=sorted(tuple(sorted(matches[i] for i in tri)) for tri in fb)
return af==bf
def plate(stem, specs):
document = App.newDocument(stem)
meshes = []
combined = Mesh.Mesh()
info = []
for name, original, x, y in specs:
m = moved(original, x, y)
if not m.isSolid() or len(m.getSeparateComponents()) != 1:
raise RuntimeError('Invalid input mesh after placement: ' + name)
if not equivalent(original, m):
raise RuntimeError('Geometry changed during placement: ' + name)
obj = document.addObject('Mesh::Feature', name)
obj.Label = name
obj.Mesh = m
combined.addMesh(m)
meshes.append(m)
info.append({'name':name, 'facets':m.CountFacets,
'volume_mm3':m.Volume, 'bounds_mm':bounds(m)})
for i, a in enumerate(meshes):
for b in meshes[i+1:]:
d=distance_xy(a,b)
if d < 8.0:
raise RuntimeError('Parts too close: %.6f mm bounding-box distance' % d)
min_gap=min(distance_xy(a,b) for i,a in enumerate(meshes) for b in meshes[i+1:])
combined.write(str(OUT / (stem+'.stl')))
document.recompute()
document.saveAs(str(OUT / (stem+'.FCStd')))
App.closeDocument(document.Name)
reloaded=Mesh.Mesh(str(OUT / (stem+'.stl')))
parts=reloaded.getSeparateComponents()
if len(parts)!=len(specs) or any(not p.isSolid() for p in parts):
raise RuntimeError('Export does not contain the expected closed components')
for expected in meshes:
candidates=[p for p in parts if abs(p.BoundBox.XMin-expected.BoundBox.XMin)<0.0001
and abs(p.BoundBox.YMin-expected.BoundBox.YMin)<0.0001]
if len(candidates)!=1 or not equivalent(expected,candidates[0]):
raise RuntimeError('Exported mesh differs from the placed original mesh')
if min(p.BoundBox.ZMin for p in parts) < -0.001 or max(abs(p.BoundBox.ZMin) for p in parts) > .001:
raise RuntimeError('Parts are not all at Z=0')
return {'file':stem+'.stl', 'components':len(parts),
'all_closed':True, 'geometry_preserved':True,
'minimum_bounding_box_gap_mm':min_gap,
'bounds_mm':bounds(reloaded), 'objects':info}
main = ([('Bracket',bracket,*bracket_pos)]
+ [('Shim05_%d'%(i+1),shim05,*xy) for i,xy in enumerate(row05)]
+ [('Shim10_%d'%(i+1),shim10,*xy) for i,xy in enumerate(row10)])
result={'main':plate('支架和两种垫片各8个', main)}
(OUT/'布局检查.json').write_text(json.dumps(result,ensure_ascii=False,indent=2),encoding='utf-8')
print(json.dumps(result,ensure_ascii=False,indent=2))

View File

@@ -0,0 +1,128 @@
"""PCD50 3/4-open pillar print trial. Run through FreeCAD GUI macro."""
from pathlib import Path
import math, time, traceback
import FreeCAD as A, FreeCADGui as G, Part, Mesh, Import
from pivy import coin
R=Path(__file__).resolve().parent.parent
O=R/'models'/'pcd50_open_pillar_v1'; O.mkdir(parents=True,exist_ok=True)
V=A.Vector
def cy(r,h,x=0,y=0,z=0): return Part.makeCylinder(r,h,V(x,y,z))
def item(d,n,label,s,c,trans=0):
o=d.addObject('PartDesign::Feature',n);o.Label=label;o.Shape=s
o.ViewObject.ShapeColor=c;o.ViewObject.LineColor=(.1,.1,.1);o.ViewObject.Transparency=trans;return o
def check(n,s):
if s.isNull() or not s.isValid() or len(s.Solids)!=1 or s.Volume<=0:raise RuntimeError('Invalid solid: '+n)
def wedge(x,y,z,h):
# A 90 degree sector points 22.5 degrees from the outward X direction toward
# the nearby corner; mirrored independently for all four quadrants.
sx=1 if x>0 else -1;sy=1 if y>0 else -1
points=[V(x,y,z)]
for i in range(19):
a=math.radians(-22.5+90.0*i/18.0)
points.append(V(x+sx*20*math.cos(a),y+sy*20*math.sin(a),z))
points.append(points[0])
return Part.Face(Part.Wire(Part.makePolygon(points).Edges)).extrude(V(0,0,h))
try:
src=A.openDocument(str(R/'models'/'pcd50_pillar_concept'/'PCD50_Pillar_Assembly.FCStd'))
names=['Motor','Holder','Magnet','PCB','Chip']+['PCBHead%d'%i for i in range(1,4)]+['Corner%d'%i for i in range(4)]+['Rail%d'%i for i in range(2)]+['Mount76_%d'%i for i in range(4)]+['Washer76_%d'%i for i in range(4)]+['Head50_%d'%i for i in range(4)]+['Shaft50_%d'%i for i in range(4)]
sh={n:src.getObject(n).Shape.copy() for n in names}; old=src.getObject('PillarBracket').Shape.copy()
A.closeDocument(src.Name)
pts=[(25*math.cos(math.radians(45+90*i)),25*math.sin(math.radians(45+90*i))) for i in range(4)]
# Retain the already-verified closed-hole 49mm panel and PCB mounting pads.
bracket=old.common(Part.makeBox(120,120,3.2,V(-60,-60,10.9)))
for x,y in pts:
col=cy(6,10.4,x,y,.5).cut(cy(2.2,10.42,x,y,.49)).cut(wedge(x,y,.49,10.42))
check('column',col);bracket=bracket.fuse(col)
bracket=bracket.removeSplitter();check('bracket',bracket)
def shim(x,y,thick,z):
return cy(6,thick,x,y,z).cut(cy(2.2,thick+.02,x,y,z-.01)).cut(wedge(x,y,z-.01,thick+.02)).removeSplitter()
shims=[shim(x,y,.5,0) for x,y in pts]
for i,s in enumerate(shims):check('shim'+str(i),s)
# Printable placement: panel flat on bed, open columns upwards.
parts=A.newDocument('PCD50OpenPillarParts')
orient=bracket.copy();orient.translate(V(0,0,-14.1));orient.rotate(V(),V(1,0,0),180)
neutral05=shim(10,10,.5,0);neutral05.translate(V(-10,-10,0))
neutral10=shim(10,10,1,0);neutral10.translate(V(-10,-10,0))
outparts=[item(parts,'EncoderBracket','PCD50 open pillar bracket',orient,(.1,.42,.85)),item(parts,'Shim05','0.5mm C shim; rotate to opening',neutral05,(.7,.7,.7)),item(parts,'Shim10','1mm C shim; rotate to opening',neutral10,(.7,.7,.7))]
parts.recompute();meshlog=[]
for o in outparts:
check(o.Name,o.Shape);Mesh.export([o],str(O/(o.Name+'.stl')));Import.export([o],str(O/(o.Name+'.step')))
m=Mesh.Mesh(str(O/(o.Name+'.stl')));meshlog.append('%s solid=%s triangles=%d volume=%.5f zmin=%.5f'%(o.Name,m.isSolid(),m.CountFacets,m.Volume,m.BoundBox.ZMin))
if not m.isSolid() or m.Volume<=0 or abs(m.BoundBox.ZMin)>.001:raise RuntimeError('bad mesh '+o.Name)
(O/'mesh_check.txt').write_text('\n'.join(meshlog)+'\n',encoding='utf8')
parts.saveAs(str(O/'PrintParts.FCStd'));A.closeDocument(parts.Name)
# Real assembled orientation, from existing motor and hardware geometry.
d=A.newDocument('PCD50OpenPillarAssembly'); colors={'Motor':(.32,.34,.37),'Holder':(.98,.55,.1),'Magnet':(.8,.8,.8),'PCB':(.04,.65,.19),'Chip':(.1,.1,.1)}
objs={}
for n in names:
col=colors.get(n,(.84,.47,.13) if n.startswith('Corner') else (.55,.56,.60) if n.startswith('Rail') else (.8,.8,.8))
objs[n]=item(d,n,n,sh[n],col,70 if n=='Motor' else 20 if n.startswith('Corner') else 0)
b=item(d,'OpenPillarBracket','four open pillars - print trial',bracket,(.12,.43,.88))
shimobjs=[item(d,'AdjustShim%d'%i,'0.5mm C shim',s,(.7,.7,.75)) for i,s in enumerate(shims)]
d.recompute();hits=[]
# Test all printed parts and all motor screws against every other object;
# intended mating contact is only a shared zero-volume face.
allparts=[o for o in d.Objects if hasattr(o,'Shape')];focus=[b]+shimobjs
for a in focus:
for c in allparts:
if a.Name==c.Name:continue
if not a.Shape.BoundBox.intersect(c.Shape.BoundBox):continue
vol=a.Shape.common(c.Shape).Volume
if vol>.001:hits.append('%s / %s %.6fmm3'%(a.Name,c.Name,vol))
# Motor-side nominal separation of every pillar from every corner.
distances=[]
for i,(x,y) in enumerate(pts):
part=bracket.common(cy(6.01,10.39,x,y,.5))
dmin=min(part.distToShape(sh['Corner%d'%j])[0] for j in range(4))
distances.append(dmin)
# Clearance for an axial 7.5mm diameter tool above each head.
access=[]
for i,(x,y) in enumerate(pts):
tool=cy(3.75,20,x,y,17.11);h=[]
for n in ['Corner%d'%j for j in range(4)]+['Rail%d'%j for j in range(2)]:
v=tool.common(sh[n]).Volume
if v>.001:h.append(n+':%.4f'%v)
access.append('tool%d: %s'%(i,', '.join(h) if h else 'no checked obstruction'))
# Verify top panel material lies under screw head and bores stay open.
heads=[]
for x,y in pts:
count=0;step=.2
for ix in range(-18,19):
for iy in range(-18,19):
dx,dy=ix*step,iy*step
if dx*dx+dy*dy<=3.5**2 and bracket.isInside(V(x+dx,y+dy,14.09),1e-7,True):count+=1
heads.append(count*step*step)
report=['50 PCD; 76 corners flush against motor, 2.5mm outboard in 9.5x5.5 slot (width assumed).',
'3/4 open pillars outer diameter 12mm, M4 bore 4.4mm; nominal radial wall away from opening 3.8mm.',
'Each 90 degree opening faces 22.5 degrees from outward X toward its respective nearby 76mm corner.',
'PCB and magnet positions retain nominal 1mm gap with 0.5mm C shims.',
'M4x20 bolt without a head washer: nominal motor engagement 5.9mm; actual thread depth unknown.',
'minimum pillar/corner distances mm: '+', '.join('%.4f'%x for x in distances),
'Estimated material under 7mm head by sampling mm2: '+', '.join('%.2f'%x for x in heads),
'Positive volume intersections >0.001mm3 for support and shims:']+(hits or ['none in checked pairs'])+['Tool clearance, 7.5mm cylinder:']+access
(O/'geometry_check.txt').write_text('\n'.join(report)+'\n',encoding='utf8')
if hits: raise RuntimeError('Interference in print trial: '+', '.join(hits))
view=G.activeDocument().activeView();G.activateWorkbench('PartWorkbench')
def render(kind,path):
if kind=='rear':view.viewTop()
else:
view.viewAxonometric();G.updateGui();time.sleep(.5)
view.getCameraNode().orientation.setValue(coin.SbRotation(.880476,.115917,-.279848,-.364705))
G.updateGui();time.sleep(.7);view.fitAll();G.updateGui();time.sleep(.7);view.saveImage(str(O/path),1600,1200,'White')
render('rear','rear.png');render('axon','assembly.png')
state={o.Name:o.ViewObject.Visibility for o in d.Objects if hasattr(o,'Shape')}
for o in d.Objects:
if hasattr(o,'Shape'):o.ViewObject.Visibility=False
b.ViewObject.Visibility=True;render('axon','bracket.png')
# Cutting one half through two bores makes the head-bearing face and slots visible.
b.ViewObject.Visibility=False
sec=bracket.cut(Part.makeBox(120,60,25,V(-60,-60-25/math.sqrt(2),0))).removeSplitter()
so=item(d,'SectionPreview','section preview only',sec,(.12,.43,.88));d.recompute();render('axon','section.png');d.removeObject(so.Name)
for o in d.Objects:
if hasattr(o,'Shape'):o.ViewObject.Visibility=state.get(o.Name,True)
render('axon','assembly.png');d.recompute();d.saveAs(str(O/'Assembly.FCStd'))
Import.export([o for o in d.Objects if hasattr(o,'Shape')],str(O/'Assembly.step'))
(O/'DONE.txt').write_text('ok\n',encoding='utf8')
except Exception:
(O/'ERROR.txt').write_text(traceback.format_exc(),encoding='utf8');raise

View File

@@ -0,0 +1,159 @@
"""Closed-bore pillar concept for M9C12 / MT6826S; NOT a print-ready part.
Run with FreeCAD GUI via rebuild_pcd50_pillar_concept.FCMacro. The existing
models are input only. All nominal dimensions and limitations are in the report.
"""
from pathlib import Path
import math, time, traceback
import FreeCAD as A, FreeCADGui as G, Part, Import
from pivy import coin
R=Path(__file__).resolve().parent.parent
O=R/'models'/'pcd50_pillar_concept'; O.mkdir(parents=True,exist_ok=True)
V=A.Vector
def cy(r,h,x,y,z): return Part.makeCylinder(r,h,V(x,y,z))
def add(d,n,label,s,c,t=0):
o=d.addObject('PartDesign::Feature',n);o.Label=label;o.Shape=s
o.ViewObject.ShapeColor=c;o.ViewObject.LineColor=(.1,.1,.1);o.ViewObject.Transparency=t
return o
try:
src=A.openDocument(str(R/'models'/'pcd50_fit_concept'/'PCD50_Assembly.FCStd'))
names=['Motor','Holder','Magnet','PCB','Chip']+['PCBHead%d'%i for i in range(1,4)]+['Corner%d'%i for i in range(4)]+['Rail%d'%i for i in range(2)]
sh={n:src.getObject(n).Shape.copy() for n in names}
old=src.getObject('OpenBracket').Shape.copy()
A.closeDocument(src.Name)
# PCD50 source uses corner offset +2.0mm; move 0.5mm farther out.
for n in ['Corner%d'%i for i in range(4)]+['Rail%d'%i for i in range(2)]:
c=(sh[n].BoundBox.XMin+sh[n].BoundBox.XMax)/2
sh[n].translate(V(.5 if c>0 else -.5,0,0))
pts=[(25*math.cos(math.radians(45+90*i)),25*math.sin(math.radians(45+90*i))) for i in range(4)]
# Rebuild a complete panel rather than inherit the prior U-slot tool gaps.
# Preserve the original three PCB pads, not its open 50mm fastener ears.
pcbpads=old.common(Part.makeBox(120,120,3.2,V(-60,-60,10.9)))
panel=Part.makeBox(49,49,3.2,V(-24.5,-24.5,10.9)).cut(cy(16.8,3.22,0,0,10.89)).fuse(pcbpads)
p76=38/math.sqrt(2)
for sx in (-1,1):
for sy in (-1,1):panel=panel.cut(cy(5.2,3.24,sx*p76,sy*p76,10.88))
columns=[]; cutters=[]
for i in range(4):
bb=sh['Corner%d'%i].BoundBox
cutters.append(Part.makeBox(bb.XLength+.4,bb.YLength+.4,3.22,V(bb.XMin-.2,bb.YMin-.2,-.01)))
bracket=panel
for x,y in pts:
# Carry each column through the panel thickness so the screw head bears on
# a complete closed ring rather than hanging over the old open-panel slot.
col=cy(4.5,13.6,x,y,.5)
for cut in cutters:col=col.cut(cut)
bracket=bracket.fuse(col)
for x,y in pts:bracket=bracket.cut(cy(2.2,13.8,x,y,.4))
bracket=bracket.removeSplitter()
if bracket.isNull() or not bracket.isValid() or len(bracket.Solids)!=1:raise RuntimeError('Invalid / disconnected closed-hole bracket')
# Four identical nominal 0.5mm support shims, not under 76mm angle brackets.
shims=[]
for x,y in pts:
s=cy(4.5,.5,x,y,0).cut(cy(2.2,.52,x,y,-.01))
for cut in cutters:s=s.cut(cut)
s=s.removeSplitter()
if s.isNull() or not s.isValid() or len(s.Solids)!=1:raise RuntimeError('invalid shim')
shims.append(s)
doc=A.newDocument('PCD50_PillarConcept');objs={}
colors={'Motor':(.32,.34,.37),'Holder':(.98,.55,.1),'Magnet':(.8,.8,.8),'PCB':(.04,.65,.19),'Chip':(.1,.1,.1)}
for n in names:
c=colors.get(n,(.84,.47,.13) if n.startswith('Corner') else (.55,.56,.60) if n.startswith('Rail') else (.8,.8,.8))
objs[n]=add(doc,n,n,sh[n],c,70 if n=='Motor' else 20 if n.startswith('Corner') else 0)
b=add(doc,'PillarBracket','PCD50 closed-bore long pillar - conceptual',bracket,(.12,.43,.88))
shimobjs=[add(doc,'Shim%d'%i,'PCD50 0.5mm shim - illustrative',s,(.70,.70,.75)) for i,s in enumerate(shims)]
heads=[];shafts=[]
for i,(x,y) in enumerate(pts):
heads.append(add(doc,'Head50_%d'%i,'M4 head Ø7x3 assumed',cy(3.5,3,x,y,14.1),(.8,.8,.8)))
shafts.append(add(doc,'Shaft50_%d'%i,'M4x20 illustrative - nominal 5.9mm engagement',cy(2,20,x,y,-5.9),(.8,.8,.8)))
p=38/math.sqrt(2);mount76=[];washers76=[]
for sx in (-1,1):
for sy in (-1,1):
x,y=sx*p,sy*p
washer=cy(5,.8,x,y,3).cut(cy(2.15,.82,x,y,2.99)).removeSplitter()
washers76.append(add(doc,'Washer76_%d'%len(washers76),'M4 washer Ø10x0.8 assumed',washer,(.75,.75,.77)))
screw=cy(2,8,x,y,-4.2).fuse(cy(3.5,2.5,x,y,3.8)).removeSplitter()
mount76.append(add(doc,'Mount76_%d'%len(mount76),'M4x8 angle screw conceptual',screw,(.8,.8,.8)))
doc.recompute()
corners=[objs['Corner%d'%i] for i in range(4)];rails=[objs['Rail%d'%i] for i in range(2)]
pairs=[]
for a in [b]+shimobjs+heads+shafts:
for c in corners+rails:pairs.append((a,c))
for a in shafts:
for c in [b]+shimobjs:pairs.append((a,c))
for a in mount76+washers76:
for c in corners+rails+[b]+shimobjs+heads+shafts:pairs.append((a,c))
for a in [b]+shimobjs+heads+shafts:
for n in ['Motor','Holder','Magnet','PCB','Chip']+['PCBHead%d'%i for i in range(1,4)]:pairs.append((a,objs[n]))
hits=[]
for a,c in pairs:
if a.Shape.BoundBox.intersect(c.Shape.BoundBox):
vol=a.Shape.common(c.Shape).Volume
if vol>.001:hits.append('%s / %s %.6fmm3'%(a.Name,c.Name,vol))
# The motor-facing 50mm bores must remain complete; no side exit. Sample
# immediately outside the bore in 72 directions at a low and high Z.
bore=[]
for i,(x,y) in enumerate(pts):
for z in (1.0,13.0):
missing=sum(not bracket.isInside(V(x+2.22*math.cos(a*math.pi/36),y+2.22*math.sin(a*math.pi/36),z),1e-7,True) for a in range(72))
bore.append('Pillar%d z=%.1f closed-bore surrounding shell missing samples: %d/72'%(i,z,missing))
access=[]
for i,(x,y) in enumerate(pts):
tool=cy(3.75,20,x,y,17.11);found=[]
for o in [b]+corners+rails:
if tool.BoundBox.intersect(o.Shape.BoundBox):
v=tool.common(o.Shape).Volume
if v>.001:found.append('%s %.4f'%(o.Name,v))
access.append('M4 tool %d: %s'%(i,', '.join(found) if found else 'no checked obstruction'))
for sx in (-1,1):
for sy in (-1,1):
x,y=sx*p,sy*p;tool=cy(3.75,20,x,y,6.31);found=[]
for o in [b]+corners+rails:
if tool.BoundBox.intersect(o.Shape.BoundBox):
v=tool.common(o.Shape).Volume
if v>.001:found.append('%s %.4f'%(o.Name,v))
access.append('76 M4 tool %+.0f,%+.0f: %s'%(sx,sy,', '.join(found) if found else 'no checked obstruction'))
headareas=[]
for x,y in pts:
count=0;step=.2
for ix in range(-18,19):
for iy in range(-18,19):
dx,dy=ix*step,iy*step
if dx*dx+dy*dy<=3.5**2 and bracket.isInside(V(x+dx,y+dy,14.09),1e-7,True):count+=1
headareas.append(count*step*step)
report=['CONCEPT ONLY: PCD50/PCD76 phase45 and slot9.5x5.5 assumed.',
'Corners flush to motor, moved 2.5mm outward from slot midpoint; 4mm screw nominal slot-end clearance 0.25mm.',
'Pillars 0.5..10.9mm and panel 10.9..14.1mm; closed Ø4.4mm bore.',
'M4x20 nominal screw engagement in motor 20 - 14.1 = 5.9mm, true tapped depth unknown.',
'Pillar cylinders Ø9 extend from z0.5 to 14.1 and merge through panel; clipped at z0.5..3.21 against expanded corner projection 0.2mm.',
'Minimum low-side wall near angle ~0.54mm theoretical, no print/strength guarantee.',
'Estimated head bearing material by sampling (mm2): '+', '.join('%.2f'%x for x in headareas),
'Selected positive-volume intersections >0.001mm3:']+(hits or ['none'])+['Tool envelope Ø7.5:']+access+['Low-bore shell check (positive shell is expected):']+bore
(O/'geometry_check.txt').write_text('\n'.join(report)+'\n',encoding='utf8')
G.activateWorkbench('PartWorkbench');view=G.activeDocument().activeView()
def render(top,path):
if top:view.viewTop()
else:
view.viewAxonometric();G.updateGui();time.sleep(.5)
view.getCameraNode().orientation.setValue(coin.SbRotation(.880476,.115917,-.279848,-.364705))
G.updateGui();time.sleep(.8);view.fitAll();G.updateGui();time.sleep(.8);G.updateGui();view.saveImage(str(O/path),1600,1200,'White')
render(True,'rear.png');render(False,'assembly.png')
states={o.Name:o.ViewObject.Visibility for o in doc.Objects if hasattr(o,'Shape')}
for o in doc.Objects:
if hasattr(o,'Shape'):o.ViewObject.Visibility=False
b.ViewObject.Visibility=True;render(False,'bracket.png')
# Remove the outer half of the two upper pillars to show the bore cross
# section, then discard this object before saving the actual assembly.
b.ViewObject.Visibility=False
section=bracket.cut(Part.makeBox(120,60,25,V(-60,-60-25/math.sqrt(2),0))).removeSplitter()
sectionobj=add(doc,'SectionPreview','Half section for illustration only',section,(.12,.43,.88))
doc.recompute();render(False,'section.png');doc.removeObject(sectionobj.Name)
for o in doc.Objects:
if hasattr(o,'Shape'):o.ViewObject.Visibility=states[o.Name]
render(False,'assembly.png');doc.recompute();doc.saveAs(str(O/'PCD50_Pillar_Assembly.FCStd'))
Import.export([o for o in doc.Objects if hasattr(o,'Shape')],str(O/'PCD50_Pillar_Assembly.step'))
(O/'DONE.txt').write_text('ok\n',encoding='utf8')
except Exception:
(O/'ERROR.txt').write_text(traceback.format_exc(),encoding='utf8');raise

View File

@@ -0,0 +1,134 @@
"""Place existing PCD50 STL meshes on two print plates without changing shapes.
Run with FreeCAD's Python, for example:
& 'C:\\Users\\billy\\scoop\\apps\\freecad\\current\\bin\\python.exe' scripts/build_pcd50_print_plate.py
The main plate is one bracket and four 0.5 mm shims. The second plate holds
four optional 1 mm shims. Output is independent of the input files.
"""
from pathlib import Path
import json
import itertools
import math
import FreeCAD as App
import Mesh
ROOT = Path(__file__).resolve().parent.parent
SOURCE = ROOT / 'models' / 'pcd50_open_pillar_v1'
OUT = ROOT / 'models' / 'pcd50_print_plate'
OUT.mkdir(parents=True, exist_ok=True)
V = App.Vector
def load(name):
mesh = Mesh.Mesh(str(SOURCE / name))
if len(mesh.getSeparateComponents()) != 1 or not mesh.isSolid():
raise RuntimeError('Input is not one closed component: ' + name)
return mesh
bracket = load('EncoderBracket.stl')
shim05 = load('Shim05.stl')
shim10 = load('Shim10.stl')
# X positions are chosen to give at least 8 mm between the shims' actual
# bounding boxes. The 49 mm bracket sits 8 mm above their actual Y maximum.
# Every placement uses the original tessellation; no rotations or rescaling.
row = [(6 + 20*i, 6.0) for i in range(4)]
bracket_pos = (35.77, 44.5)
def moved(mesh, x, y):
result = mesh.copy()
result.translate(x, y, -mesh.BoundBox.ZMin)
return result
def bounds(m):
b = m.BoundBox
return [b.XMin, b.XMax, b.YMin, b.YMax, b.ZMin, b.ZMax]
def distance_xy(a, b):
a, b = a.BoundBox, b.BoundBox
dx = max(a.XMin - b.XMax, b.XMin - a.XMax, 0.0)
dy = max(a.YMin - b.YMax, b.YMin - a.YMax, 0.0)
return math.hypot(dx, dy)
def equivalent(a, b):
"""Compare vertices and facet topology after removing translation.
Float STL coordinates round slightly when translated/reloaded. A small
tolerance is therefore used rather than comparing quantized text.
"""
if a.CountFacets != b.CountFacets:
return False
pa, fa = a.Topology; pb, fb = b.Topology
if len(pa) != len(pb):
return False
ba, bb = a.BoundBox, b.BoundBox
def norm(p, box): return (p.x-box.XMin,p.y-box.YMin,p.z-box.ZMin)
qa=[norm(p,ba) for p in pa]; qb=[norm(p,bb) for p in pb]
tol=0.0001
def key(v): return tuple(math.floor(x/tol) for x in v)
bins={}
for i,v in enumerate(qa):bins.setdefault(key(v),[]).append(i)
matches=[]
for v in qb:
k=key(v); candidates=[]
for offset in itertools.product((-1,0,1),repeat=3):
for i in bins.get(tuple(k[j]+offset[j] for j in range(3)),[]):
dist=max(abs(v[j]-qa[i][j]) for j in range(3))
if dist<tol:candidates.append((dist,i))
if not candidates:return False
matches.append(min(candidates)[1])
if len(set(matches)) != len(qa):return False
af=sorted(tuple(sorted(tri)) for tri in fa)
bf=sorted(tuple(sorted(matches[i] for i in tri)) for tri in fb)
return af==bf
def plate(stem, specs):
document = App.newDocument(stem)
meshes = []
combined = Mesh.Mesh()
info = []
for name, original, x, y in specs:
m = moved(original, x, y)
if not m.isSolid() or len(m.getSeparateComponents()) != 1:
raise RuntimeError('Invalid input mesh after placement: ' + name)
if not equivalent(original, m):
raise RuntimeError('Geometry changed during placement: ' + name)
obj = document.addObject('Mesh::Feature', name)
obj.Label = name
obj.Mesh = m
combined.addMesh(m)
meshes.append(m)
info.append({'name':name, 'facets':m.CountFacets,
'volume_mm3':m.Volume, 'bounds_mm':bounds(m)})
for i, a in enumerate(meshes):
for b in meshes[i+1:]:
d=distance_xy(a,b)
if d < 8.0:
raise RuntimeError('Parts too close: %.6f mm bounding-box distance' % d)
min_gap=min(distance_xy(a,b) for i,a in enumerate(meshes) for b in meshes[i+1:])
combined.write(str(OUT / (stem+'.stl')))
document.recompute()
document.saveAs(str(OUT / (stem+'.FCStd')))
App.closeDocument(document.Name)
reloaded=Mesh.Mesh(str(OUT / (stem+'.stl')))
parts=reloaded.getSeparateComponents()
if len(parts)!=len(specs) or any(not p.isSolid() for p in parts):
raise RuntimeError('Export does not contain the expected closed components')
for expected in meshes:
candidates=[p for p in parts if abs(p.BoundBox.XMin-expected.BoundBox.XMin)<0.0001
and abs(p.BoundBox.YMin-expected.BoundBox.YMin)<0.0001]
if len(candidates)!=1 or not equivalent(expected,candidates[0]):
raise RuntimeError('Exported mesh differs from the placed original mesh')
if min(p.BoundBox.ZMin for p in parts) < -0.001 or max(abs(p.BoundBox.ZMin) for p in parts) > .001:
raise RuntimeError('Parts are not all at Z=0')
return {'file':stem+'.stl', 'components':len(parts),
'all_closed':True, 'geometry_preserved':True,
'minimum_bounding_box_gap_mm':min_gap,
'bounds_mm':bounds(reloaded), 'objects':info}
main=[('Bracket',bracket,*bracket_pos)]+[('Shim05_%d'%(i+1),shim05,*xy) for i,xy in enumerate(row)]
spare=[('Shim10_%d'%(i+1),shim10,*xy) for i,xy in enumerate(row)]
result={'main':plate('支架和四个0.5mm垫片',main),
'spare':plate('四个1mm备用垫片',spare)}
(OUT/'布局检查.json').write_text(json.dumps(result,ensure_ascii=False,indent=2),encoding='utf-8')
print(json.dumps(result,ensure_ascii=False,indent=2))

View File

@@ -0,0 +1,68 @@
"""Illustrative PCB holder attached to 2020 rails; not a print-ready design."""
import math,time,traceback
from pathlib import Path
import FreeCAD as A, FreeCADGui as G, Part, Import
from pivy import coin
R=Path(__file__).resolve().parent.parent
O=R/'models'/'profile_mount_check'
V=A.Vector
source=A.openDocument(str(O/'Direct.FCStd'))
keep=['Motor','Holder','Magnet','PCB','Chip']+['PCBHead%d'%i for i in range(1,4)]+['Corner%d'%i for i in range(4)]+['Rail%d'%i for i in range(2)]+['MountM4_%d'%i for i in range(1,5)]
shape={n:source.getObject(n).Shape.copy() for n in keep}
support=source.getObject('Bracket').Shape.copy()
A.closeDocument(source.Name)
doc=A.newDocument('ProfileMountedEncoderOption')
def item(name,label,s,c,t=0):
o=doc.addObject('PartDesign::Feature',name);o.Label=label;o.Shape=s;o.ViewObject.ShapeColor=c;o.ViewObject.LineColor=(.1,.1,.1);o.ViewObject.Transparency=t;return o
cols={'Motor':(.3,.32,.35),'Holder':(.96,.55,.1),'Magnet':(.8,.8,.8),'PCB':(.04,.65,.19),'Chip':(.1,.1,.1)}
objects={}
for n in keep:
c=cols.get(n,(.84,.47,.13) if n.startswith('Corner') else (.55,.56,.6) if n.startswith('Rail') else (.78,.78,.78))
objects[n]=item(n,n,s=shape[n],c=c,t=70 if n=='Motor' else 20 if n.startswith('Corner') else 0)
# Preserve the original ring, PCB mounting bosses and positions; remove all
# four PCD50 feet, shims and fastening heads. Mount on the extrusion faces.
ring=support.common(Part.makeBox(100,100,3.2,V(-50,-50,10.9)))
face=38/math.sqrt(2)-6+20
bridge=Part.makeBox(face-16,6,3.2,V(16,5,10.9))
flange=Part.makeBox(3,22,12,V(face-3,-11,4))
# Vertical 9.5x5.5 slot; shaft is perpendicular to the 2020 inner face.
axis=V(1,0,0)
slot=Part.makeBox(3.04,5.5,4,V(face-3.02,-2.75,8))
for z in (8,12):slot=slot.fuse(Part.makeCylinder(2.75,3.04,V(face-3.02,0,z),axis))
flange=flange.cut(slot)
arm=bridge.fuse(flange).removeSplitter()
left=arm.mirror(V(),V(1,0,0))
holder=ring.fuse(arm).fuse(left).removeSplitter()
if holder.isNull() or not holder.isValid() or len(holder.Solids)!=1:raise RuntimeError('encoder support not a single valid solid')
h=item('NewHolder','Concept PCB ring - extrusion fasteners and vertical slots',holder,(.18,.47,.86))
# M4 illustration: 7x2.5 head and shank in flange; extrusion T nut is not modelled.
bolts=[]
for side in (1,-1):
head=Part.makeCylinder(3.5,2.5,V(face-5.5,0,10),axis)
shank=Part.makeCylinder(1.95,3.01,V(face-3.01,0,10),axis)
b=head.fuse(shank).removeSplitter()
if side<0:b=b.mirror(V(),V(1,0,0))
bolts.append(item('HolderBolt%d'%len(bolts),'M4 illustrative head, T-nut not modelled',b,(.8,.8,.8)))
doc.recompute()
targets=['Motor','Holder','Magnet','PCB','Chip']+['PCBHead%d'%i for i in range(1,4)]+['Corner%d'%i for i in range(4)]+['Rail%d'%i for i in range(2)]+['MountM4_%d'%i for i in range(1,5)]
res=[]
for n in targets:
other=objects[n]
if not h.Shape.BoundBox.intersect(other.Shape.BoundBox):continue
vol=h.Shape.common(other.Shape).Volume
if vol>0.0001:res.append((h.Name,other.Name,vol))
for b in bolts:
for o in [h]+[objects[n] for n in targets]:
if not b.Shape.BoundBox.intersect(o.Shape.BoundBox):continue
v=b.Shape.common(o.Shape).Volume
if v>.0001:res.append((b.Name,o.Name,v))
(O/'RailHolder_check.txt').write_text('Concept only. The original PCB position is retained (nominal sensor gap 1mm); no PCD50 shims are used.\nNew holder/bolt collisions >0.0001mm3:\n'+('\n'.join('%s / %s %.5f mm3'%r for r in res) if res else 'none in checked pairs')+'\n',encoding='utf8')
view=G.activeDocument().activeView()
def render(kind,name):
if kind=='top':view.viewTop()
else:view.getCameraNode().orientation.setValue(coin.SbRotation(0.880476,0.115917,-.279848,-.364705))
G.updateGui();time.sleep(.3);G.updateGui();view.fitAll();G.updateGui();time.sleep(.3);G.updateGui();view.saveImage(str(O/name),1600,1200,'White')
render('top','RailHolder_rear.png');render('axon','RailHolder_axon.png')
doc.recompute();doc.saveAs(str(O/'RailHolder.FCStd'))
Import.export([o for o in doc.Objects if hasattr(o,'Shape')],str(O/'RailHolder.step'))
(O/'RAIL_DONE.txt').write_text('success\n',encoding='utf8')

View File

@@ -0,0 +1,131 @@
"""2020 corner bracket interference check, not a production drawing."""
import math, time
from pathlib import Path
import FreeCAD as A, FreeCADGui as G, Part, Import
from pivy import coin
ROOT=Path(__file__).resolve().parent.parent
OUT=ROOT/'models'/'profile_mount_check'
OUT.mkdir(parents=True,exist_ok=True)
V=A.Vector
src=A.openDocument(str(ROOT/'models'/'encoder_print_v1'/'EncoderAssembly_Fit.FCStd'))
base={k:src.getObject(k).Shape.copy() for k in ('Motor','Holder','Magnet','Bracket','PCB','Chip','Shim0','Shim1','Shim2','Shim3','PCBHead1','PCBHead2','PCBHead3')}
A.closeDocument(src.Name)
def cylinder(r,h,x,y,z): return Part.makeCylinder(r,h,V(x,y,z))
def corner(x,y,lift,side):
# Max outward fit with 3.9mm screw in assumed 5.5mm slot: offset 2.75mm,
# leaving only 0.05mm clearance at slot end. Extreme tolerance assumption.
start=x-6.0
flat=Part.makeBox(20,18,3,V(start,y-9,lift))
# slot 9.5 long, width assumed 5.5. Fused 4mm rectangle and 2 end arcs.
sc=x+2.75
slot=Part.makeBox(4,5.5,3.04,V(sc-2,y-2.75,lift-.02))
slot=slot.fuse(cylinder(2.75,3.04,sc-2,y,lift-.02)).fuse(cylinder(2.75,3.04,sc+2,y,lift-.02))
flat=flat.cut(slot)
wall=Part.makeBox(3,18,20,V(start+17,y-9,lift))
hole=Part.makeCylinder(2.75,3.04,V(start+16.98,y,lift+10),V(1,0,0))
wall=wall.cut(hole)
shape=flat.fuse(wall).removeSplitter()
if side<0: shape=shape.mirror(V(0,0,0),V(1,0,0))
return shape
def add(doc,name,label,shape,color,trans=0):
obj=doc.addObject('PartDesign::Feature',name); obj.Label=label;obj.Shape=shape
obj.ViewObject.ShapeColor=color;obj.ViewObject.LineColor=(.1,.1,.1);obj.ViewObject.Transparency=trans
return obj
def run(title,lift):
doc=A.newDocument(title)
colors={'Motor':(.3,.32,.35),'Holder':(.98,.55,.1),'Magnet':(.8,.8,.8),'Bracket':(.15,.43,.85),'PCB':(.04,.65,.19),'Chip':(.1,.1,.1)}
objects={}
for k,shape in base.items():
col=colors.get(k,(.75,.75,.75)); objects[k]=add(doc,k,'Encoder existing '+k,shape,col,70 if k=='Motor' else 0)
# Encoder M4 heads were absent from original assembly. Their outer diameter
# 7 mm and height 3 mm are explicit approximations, not measured hardware.
encoderheads=[]
for i in range(4):
a=math.radians(45+i*90); x,y=25*math.cos(a),25*math.sin(a)
shape=cylinder(3.5,3,x,y,2.5)
encoderheads.append(add(doc,'EncoderM4Head%d'%i,'Encoder M4 head 7x3 assumed',shape,(.8,.8,.8)))
p=38/math.sqrt(2); arms=[]; rails=[]; spacers=[]
# Each extrusion face touches the bracket wall at x=start+20. y extends 55
# each way, to span both bolts. Solid 20x20 envelope, channels omitted.
for side in [-1,1]:
for y in [-p,p]:
arm=corner(p,y,lift,side)
arms.append(add(doc,'Corner%d'%len(arms),'2020 corner L slotted to motor',arm,(.84,.47,.13)))
if lift:
sp=cylinder(4,lift,side*p,y,0).cut(cylinder(2.2,lift+.02,side*p,y,-.01))
spacers.append(add(doc,'Spacer%d'%len(spacers),'M4 spacer 7 mm assumed',sp,(.7,.7,.7)))
# M4 fastener shank extends into motor and through spacer+leg.
screw=cylinder(1.95,lift+8,side*p,y,-5)
head=cylinder(3.5,2.5,side*p,y,lift+3)
add(doc,'MountM4_%d'%len(arms),'Motor M4 screw envelope schematic',screw.fuse(head),(.8,.8,.8))
x=p-6.0+20
box=Part.makeBox(20,110,20,V(x,-55,lift))
if side<0:box=box.mirror(V(),V(1,0,0))
rails.append(add(doc,'Rail%d'%len(rails),'2020 solid envelope length 110 assumed',box,(.55,.56,.6)))
doc.recompute()
if any(o.Shape.isNull() or not o.Shape.isValid() for o in doc.Objects if hasattr(o,'Shape')):
raise RuntimeError('Invalid shape')
checks=[]; intersections=[]
targets=[objects['Bracket'],objects['PCB'],objects['Chip'],objects['Holder'],objects['Magnet']]+encoderheads+[objects['Shim%d'%i] for i in range(4)]+[objects['PCBHead%d'%i] for i in range(1,4)]+[objects['Motor']]
for i,c in enumerate(arms+rails):
for t in targets:
if not c.Shape.BoundBox.intersect(t.Shape.BoundBox): continue
intersection=c.Shape.common(t.Shape); volume=intersection.Volume
if volume>0.0001:
checks.append((c.Name,t.Name,volume)); intersections.append((c.Name,t.Name,intersection))
# also compare other corner angles and rails, excluding touching intended faces
for i,a in enumerate(arms):
for b in arms[i+1:]+rails:
if not a.Shape.BoundBox.intersect(b.Shape.BoundBox):continue
vol=a.Shape.common(b.Shape).Volume
if vol>.0001:checks.append((a.Name,b.Name,vol))
report=['Layout: 4 corner brackets and 20x20 solid profile envelopes; lift %.1fmm.'%lift,
'PCD76=45deg, oval width 5.5mm, slot offset +2.75mm outward, profile length 110mm all assumed.',
'M4 shank assumed 3.9mm: extreme position leaves 0.05mm radial clearance.',
'Encoder M4 head diameter 7mm height 3mm, 0.5mm bracket shim assumed.',
'Positive-volume collisions (>0.0001mm3):']
report += ['%s vs %s: %.5f mm3'%x for x in checks] or ['none in the checked part pairs']
(OUT/(title+'_check.txt')).write_text('\n'.join(report)+'\n',encoding='utf8')
for i,(a,b,shape) in enumerate(intersections):
add(doc,'Interference%d'%i,'INTERFERENCE %s / %s'%(a,b),shape,(1.,0.,0.))
# Semi-transparent corner brackets reveal separately selectable collision solids.
for arm in arms: arm.ViewObject.Transparency=30
doc.recompute()
view=G.activeDocument().activeView()
def render(mode,name):
if mode=='top': view.viewTop()
else: view.getCameraNode().orientation.setValue(coin.SbRotation(0.880476,0.115917,-0.279848,-0.364705))
G.updateGui();time.sleep(.5);G.updateGui();view.fitAll();G.updateGui();time.sleep(.5);G.updateGui()
view.saveImage(str(OUT/name),1600,1200,'White')
render('top',title+'_rear.png')
render('axon',title+'_axon.png')
# Focus screenshot: only one corner, original blue bracket and red
# exact intersection solids are visible. No objects are repositioned.
states={o.Name:o.ViewObject.Visibility for o in doc.Objects if hasattr(o,'Shape')}
for o in doc.Objects:
if hasattr(o,'Shape'): o.ViewObject.Visibility=False
objects['Bracket'].ViewObject.Visibility=True
arms[0].ViewObject.Visibility=True
encoderheads[2].ViewObject.Visibility=True
for o in doc.Objects:
if o.Name.startswith('Interference') and 'Corner0 /' in o.Label: o.ViewObject.Visibility=True
render('top',title+'_detail.png')
for o in doc.Objects:
if hasattr(o,'Shape'): o.ViewObject.Visibility=states[o.Name]
render('top',title+'_rear.png')
doc.recompute();doc.saveAs(str(OUT/(title+'.FCStd')))
Import.export([o for o in doc.Objects if hasattr(o,'Shape')],str(OUT/(title+'.step')))
A.closeDocument(doc.Name)
return checks
try:
c1=run('Direct',0)
(OUT/'BUILD_OK.txt').write_text('success\n',encoding='utf8')
except Exception as ex:
import traceback
(OUT/'BUILD_ERROR.txt').write_text(traceback.format_exc(),encoding='utf8')
raise

View File

@@ -0,0 +1,106 @@
"""Illustrative 76 PCD dual-stage standoff mount; all fasteners are assumed."""
import math,time,traceback
from pathlib import Path
import FreeCAD as A, FreeCADGui as G, Part, Import
from pivy import coin
R=Path(__file__).resolve().parent.parent
O=R/'models'/'shared76_concept'; O.mkdir(parents=True,exist_ok=True)
V=A.Vector
def cy(r,h,x=0,y=0,z=0): return Part.makeCylinder(r,h,V(x,y,z))
def item(doc,name,label,s,c,trans=0):
o=doc.addObject('PartDesign::Feature',name);o.Label=label;o.Shape=s
o.ViewObject.ShapeColor=c;o.ViewObject.LineColor=(.1,.1,.1);o.ViewObject.Transparency=trans;return o
def corner(x,y,side):
start=x-8.75
flat=Part.makeBox(20,18,3,V(start,y-9,0))
slot=Part.makeBox(4,5.5,3.04,V(x-2,y-2.75,-.02)).fuse(cy(2.75,3.04,x-2,y,-.02)).fuse(cy(2.75,3.04,x+2,y,-.02))
flat=flat.cut(slot)
wall=Part.makeBox(3,18,20,V(start+17,y-9,0))
wall=wall.cut(Part.makeCylinder(2.75,3.04,V(start+16.98,y,10),V(1,0,0)))
sh=flat.fuse(wall).removeSplitter()
return sh.mirror(V(),V(1,0,0)) if side<0 else sh
def hexprism(x,y,z,height):
pts=[V(x+4.05*math.cos(math.radians(30+i*60)),y+4.05*math.sin(math.radians(30+i*60)),z) for i in range(6)]
pts.append(pts[0]);return Part.Face(Part.Wire(Part.makePolygon(pts).Edges)).extrude(V(0,0,height))
try:
src=A.openDocument(str(R/'models'/'encoder_print_v1'/'EncoderAssembly_Fit.FCStd'))
names=['Motor','Holder','Magnet','PCB','Chip','PCBHead1','PCBHead2','PCBHead3']
base={k:src.getObject(k).Shape.copy() for k in names}
oldring=src.getObject('Bracket').Shape.copy().common(Part.makeBox(100,100,3.2,V(-50,-50,10.9)))
A.closeDocument(src.Name)
doc=A.newDocument('Shared76Concept'); objs={}
colors={'Motor':(.32,.34,.37),'Holder':(.98,.55,.1),'Magnet':(.8,.8,.8),'PCB':(.04,.65,.19),'Chip':(.1,.1,.1)}
for k,s in base.items():objs[k]=item(doc,k,k,s,colors.get(k,(.8,.8,.8)),70 if k=='Motor' else 0)
p=38/math.sqrt(2); rails=[]; corners=[]; washers=[]; columns=[]; bolts=[]
for side in (-1,1):
for y in (-p,p):
x=side*p
c=corner(p,y,side);corners.append(item(doc,'Corner%d'%len(corners),'2020 angle direct against motor',c,(.84,.47,.13),15))
washer=cy(5,.8,x,y,3).cut(cy(2.15,.82,x,y,2.99))
washers.append(item(doc,'Washer%d'%len(washers),'Metal M4 washer, OD10 assumed',washer,(.73,.73,.76)))
body=hexprism(x,y,3.8,7.1).cut(cy(2.0,6.62,x,y,4.29))
stud=cy(1.95,8.8,x,y,-5)
standoff=body.fuse(stud).removeSplitter()
if not standoff.isValid() or len(standoff.Solids)!=1:raise RuntimeError('invalid standoff')
columns.append(item(doc,'Standoff%d'%len(columns),'Metal male-female standoff assumed',standoff,(.85,.75,.38)))
bolt=cy(1.9,6.6,x,y,7.5).fuse(cy(3.5,2.5,x,y,14.1)).removeSplitter()
bolts.append(item(doc,'TopM4%d'%len(bolts),'Separate top M4 head and screw assumed',bolt,(.78,.8,.82)))
x=p-8.75+20
rail=Part.makeBox(20,110,20,V(x,-55,0))
if side<0:rail=rail.mirror(V(),V(1,0,0))
rails.append(item(doc,'Rail%d'%len(rails),'2020 extrusion envelope; length assumed',rail,(.55,.56,.6)))
# Square encoder frame suggested by sketch, PCB and magnet positions unchanged.
# Outer 64mm leaves material outside the 76-PCD nominal screw centres.
frame=Part.makeBox(64,64,3.2,V(-32,-32,10.9)).cut(cy(16.8,3.22,0,0,10.89)).fuse(oldring)
# 4 open slots, to outer x side, directly above the 76 PCD standoff tops.
for side in (-1,1):
for y in (-p,p):
x=side*p; start=x if side>0 else x-10
cut=Part.makeBox(10,4.4,3.24,V(start,y-2.2,10.88)).fuse(cy(2.2,3.24,x,y,10.88))
frame=frame.cut(cut)
frame=frame.removeSplitter()
if frame.isNull() or not frame.isValid() or len(frame.Solids)!=1:raise RuntimeError('Frame invalid')
fr=item(doc,'EncoderFrame','Encoder frame - open slots PCD76, concept',frame,(.13,.43,.86))
doc.recompute()
checks=[]
pairs=[]
for c in corners:
for t in [fr,objs['PCB'],objs['Holder'],objs['Magnet']]+rails:pairs.append((c,t))
for t in [objs['Motor'],objs['Holder'],objs['Magnet'],objs['PCB'],objs['Chip']]+corners+rails+washers+columns+bolts:pairs.append((fr,t))
for col in columns:
for c in corners:pairs.append((col,c))
for w in washers:pairs.append((col,w))
for b in bolts:
for col in columns:pairs.append((b,col))
for a,b in pairs:
if not a.Shape.BoundBox.intersect(b.Shape.BoundBox):continue
vol=a.Shape.common(b.Shape).Volume
if vol>.0001:checks.append('%s / %s: %.5f mm3'%(a.Name,b.Name,vol))
report=['CONCEPT; corners directly contact motor, each held by metal washer and metal male-female column.',
'PCD76 and PCD50 phase=45deg assumed; 2020=20x20x110 solid envelope; angle slot=9.5x5.5 assumed and set at middle.',
'Illustrative washer OD10x0.8; hex column across flats ~7mm height 7.1, male M4 assumed 8.8mm; 5mm enters motor.',
'PCB back z10.9; chip-to-magnet 1mm nominal. Actual fastener, pressure, tool, wiring, rotation tests absent.',
'Checked positive volume intersections >0.0001mm3:']+(checks or ['none in checked pairs'])
(O/'Shared76_check.txt').write_text('\n'.join(report)+'\n',encoding='utf8')
view=G.activeDocument().activeView()
def render(kind,name):
if kind=='rear':view.viewTop()
else:
view.viewAxonometric();G.updateGui();time.sleep(.8)
view.getCameraNode().orientation.setValue(coin.SbRotation(.880476,.115917,-.279848,-.364705))
G.updateGui();time.sleep(1);view.fitAll();G.updateGui();time.sleep(1);G.updateGui();view.saveImage(str(O/name),1600,1200,'White')
render('rear','Shared76_rear.png');render('axon','Shared76_axon.png')
states={o.Name:o.ViewObject.Visibility for o in doc.Objects if hasattr(o,'Shape')}
for o in doc.Objects:
if hasattr(o,'Shape'):o.ViewObject.Visibility=False
fr.ViewObject.Visibility=True
render('rear','Shared76_frame.png')
for o in doc.Objects:
if hasattr(o,'Shape'):o.ViewObject.Visibility=states[o.Name]
render('axon','Shared76_axon.png')
doc.recompute();doc.saveAs(str(O/'Shared76.FCStd'))
Import.export([o for o in doc.Objects if hasattr(o,'Shape')],str(O/'Shared76.step'))
(O/'DONE.txt').write_text('success\n',encoding='utf8')
except Exception:
(O/'ERROR.txt').write_text(traceback.format_exc(),encoding='utf8')
raise

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

View File

@@ -0,0 +1,110 @@
"""First-fit printable PCD76 bracket. Run in FreeCAD GUI via macro.
Geometry is based on a 6xx/7 mm body standoff reference; no load rating is implied.
"""
import math,time,traceback
from pathlib import Path
import FreeCAD as A, FreeCADGui as G, Part, Import, Mesh
from pivy import coin
R=Path(__file__).resolve().parent.parent
O=R/'models'/'encoder_shared76_print_v1'; O.mkdir(parents=True,exist_ok=True)
V=A.Vector
def cy(r,h,x=0,y=0,z=0): return Part.makeCylinder(r,h,V(x,y,z))
def add(doc,name,label,s,c,trans=0):
o=doc.addObject('PartDesign::Feature',name);o.Label=label;o.Shape=s
o.ViewObject.ShapeColor=c;o.ViewObject.LineColor=(.12,.12,.12);o.ViewObject.Transparency=trans;return o
def valid(name,s):
if s.isNull() or not s.isValid() or len(s.Solids)!=1 or s.Volume<=0:raise RuntimeError('Invalid solid '+name)
def hexprism(x,y,z,h):
# 7 mm across flats
rr=7/math.sqrt(3)
pts=[V(x+rr*math.cos(math.radians(30+i*60)),y+rr*math.sin(math.radians(30+i*60)),z) for i in range(6)];pts.append(pts[0])
return Part.Face(Part.Wire(Part.makePolygon(pts).Edges)).extrude(V(0,0,h))
try:
src=A.openDocument(str(R/'models'/'shared76_concept'/'Shared76.FCStd'))
keep=['Motor','Holder','Magnet','PCB','Chip','PCBHead1','PCBHead2','PCBHead3']+['Corner%d'%i for i in range(4)]+['Rail%d'%i for i in range(2)]
shapes={n:src.getObject(n).Shape.copy() for n in keep}
A.closeDocument(src.Name)
src=A.openDocument(str(R/'models'/'encoder_print_v1'/'EncoderAssembly_Fit.FCStd'))
oldring=src.getObject('Bracket').Shape.copy().common(Part.makeBox(100,100,3.2,V(-50,-50,10.9)))
A.closeDocument(src.Name)
# PCB rear/base face z=11.3 with one 0.5mm shim on a 7mm standoff.
board_shift=.4; oldring.translate(V(0,0,board_shift))
for n in ['PCB','Chip','PCBHead1','PCBHead2','PCBHead3']: shapes[n].translate(V(0,0,board_shift))
p=38/math.sqrt(2); frame=Part.makeBox(64,64,3.2,V(-32,-32,11.3)).cut(cy(16.8,3.22,z=11.29)).fuse(oldring)
# U slots face outward: left opens to -X and right to +X. Both pairs differ in direction.
for side in (-1,1):
for y in (-p,p):
x=side*p; start=x if side>0 else x-10
cutter=Part.makeBox(10,4.6,3.24,V(start,y-2.3,11.28)).fuse(cy(2.3,3.24,x,y,11.28))
frame=frame.cut(cutter)
frame=frame.removeSplitter();valid('frame',frame)
shim05=cy(4.5,.5).cut(cy(2.25,.52,z=-.01));valid('shim05',shim05)
shim10=cy(4.5,1).cut(cy(2.25,1.02,z=-.01));valid('shim10',shim10)
parts=A.newDocument('Shared76PrintParts')
local=frame.copy();local.translate(V(0,0,-11.3));valid('flat frame',local)
po=[add(parts,'EncoderBracket','PCD76 encoder bracket print face down',local,(.15,.45,.85)),add(parts,'Shim05','M4 shim 0.5mm',shim05,(.75,.75,.75)),add(parts,'Shim10','M4 shim 1mm',shim10,(.75,.75,.75))]
parts.recompute(); mesh_lines=[]
for ob in po:
Mesh.export([ob],str(O/(ob.Name+'.stl')));Import.export([ob],str(O/(ob.Name+'.step')))
mesh=Mesh.Mesh(str(O/(ob.Name+'.stl')));bb=mesh.BoundBox
mesh_lines.append('%s solid=%s facets=%d volume=%.4f zmin=%.4f'%(ob.Name,mesh.isSolid(),mesh.CountFacets,mesh.Volume,bb.ZMin))
if not mesh.isSolid() or mesh.Volume<=0 or abs(bb.ZMin)>.001:raise RuntimeError('mesh validation failed '+ob.Name)
(O/'mesh_check.txt').write_text('\n'.join(mesh_lines)+'\n',encoding='utf8');parts.saveAs(str(O/'Shared76_Print.FCStd'))
A.closeDocument(parts.Name)
doc=A.newDocument('Shared76Fit');objs={}
colors={'Motor':(.32,.34,.37),'Holder':(.98,.55,.1),'Magnet':(.8,.8,.8),'PCB':(.04,.65,.19),'Chip':(.1,.1,.1)}
for n in keep:
c=colors.get(n,(.84,.47,.13) if n.startswith('Corner') else (.55,.56,.6) if n.startswith('Rail') else (.8,.8,.8))
objs[n]=add(doc,n,n,shapes[n],c,70 if n=='Motor' else 15 if n.startswith('Corner') else 0)
objs['Frame']=add(doc,'EncoderBracket','PCD76 bracket printed part',frame,(.15,.45,.85))
stand=[];bottomwash=[];adjust=[];topwash=[];tops=[]
for side in (-1,1):
for y in (-p,p):
x=side*p; i=len(stand)
# 3mm corner then 0.8 metal washer, then 7mm body; male 8mm long, 4.2mm nominal thread into motor.
washer=cy(5,.8,x,y,3).cut(cy(2.15,.82,x,y,2.99))
bottomwash.append(add(doc,'MetalWasher%d'%i,'Metal angle washer 0.8 assumed',washer,(.78,.8,.82)))
body=hexprism(x,y,3.8,7).cut(cy(2.1,3.52,x,y,7.29))
body=body.fuse(cy(1.95,8,x,y,-4.2)).removeSplitter();valid('standoff',body)
stand.append(add(doc,'Standoff%d'%i,'M4 male female 7mm body 8mm male reference',body,(.82,.70,.4)))
adj=cy(4.5,.5,x,y,10.8).cut(cy(2.25,.52,x,y,10.79))
adjust.append(add(doc,'AdjustmentShim%d'%i,'0.5mm nominal adjustment',adj,(.72,.72,.72)))
tw=cy(4.5,.8,x,y,14.5).cut(cy(2.15,.82,x,y,14.49))
topwash.append(add(doc,'TopWasher%d'%i,'Top M4 washer 9x0.8 assumed',tw,(.8,.8,.8)))
# Chosen illustrative thread penetration 3mm (female minimum reference 3.5mm).
bolt=cy(1.9,8,x,y,7.3).fuse(cy(3.5,2.5,x,y,15.3)).removeSplitter()
tops.append(add(doc,'TopScrew%d'%i,'M4 screw envelope length 8mm verify depth',bolt,(.8,.8,.8)))
doc.recompute()
# All pairs of assembled hardware/PCB/support are checked; intended face contact has zero volume.
group=[objs[k] for k in keep]+[objs['Frame']]+stand+bottomwash+adjust+topwash+tops
intersections=[]
for i,a in enumerate(group):
for b in group[i+1:]:
if a.Name.startswith('PCBHead') and b.Name in ['PCB']: continue # heads bear on board face
if not a.Shape.BoundBox.intersect(b.Shape.BoundBox):continue
volume=a.Shape.common(b.Shape).Volume
if volume>0.001:intersections.append('%s / %s %.5f mm3'%(a.Name,b.Name,volume))
report=['Nominal gap 1.4mm: 7mm standoff body, 0.8mm motor washer, 0.5mm encoder shim.',
'M4 male 8mm; through 3mm angle and 0.8mm metal washer, nominal motor engagement 4.2mm.',
'M4 top shank 8mm through 0.8mm top washer + 3.2mm bracket + 0.5mm shim, female entry 3.5mm.',
'Intersections above 0.001 mm3, selected complete simplified assembly pairs:']+(intersections or ['none'])
(O/'geometry_check.txt').write_text('\n'.join(report)+'\n',encoding='utf8')
# No automatic PASS if a real interference exists: keep report and let human evaluate.
view=G.activeDocument().activeView()
def render(top,name):
if top:view.viewTop()
else:view.viewAxonometric();G.updateGui();time.sleep(.5);view.getCameraNode().orientation.setValue(coin.SbRotation(.880476,.115917,-.279848,-.364705))
G.updateGui();time.sleep(.7);view.fitAll();G.updateGui();time.sleep(.7);view.saveImage(str(O/name),1600,1200,'White')
render(False,'assembly.png');render(True,'rear.png')
states={o.Name:o.ViewObject.Visibility for o in doc.Objects if hasattr(o,'Shape')}
for ob in doc.Objects:
if hasattr(ob,'Shape'):ob.ViewObject.Visibility=False
objs['Frame'].ViewObject.Visibility=True;render(True,'bracket.png')
for ob in doc.Objects:
if hasattr(ob,'Shape'):ob.ViewObject.Visibility=states[ob.Name]
render(False,'assembly.png');doc.recompute();doc.saveAs(str(O/'Shared76_Assembly.FCStd'))
Import.export([o for o in doc.Objects if hasattr(o,'Shape')],str(O/'Shared76_Assembly.step'))
(O/'DONE.txt').write_text('ok\n',encoding='utf8')
except Exception:
(O/'ERROR.txt').write_text(traceback.format_exc(),encoding='utf8');raise

View File

@@ -0,0 +1,110 @@
"""Create two genuinely connected eight-washer sprues for shop review.
Run with the FreeCAD bundled Python. The original single-part files are read
only; all lengths and placements below are in millimetres.
"""
from pathlib import Path
import hashlib
import json
import math
import shutil
import zipfile
import FreeCAD as App
import Part
import Mesh
import MeshPart
ROOT = Path(__file__).resolve().parent.parent
SOURCE = ROOT / 'models' / 'pcd50_open_pillar_v1'
OUT = ROOT / 'models' / 'print_shop_joined_eight'
OUT.mkdir(parents=True, exist_ok=True)
V = App.Vector
# STEP washer geometry is centered on (10,10) in these exported files. These
# coordinates are explicit because imported STEP and STL placements differ.
def centered_shape(name):
shape = Part.read(str(SOURCE / name))
if shape.isNull() or not shape.isValid() or len(shape.Solids) != 1:
raise RuntimeError('Invalid source shape ' + name)
shape.translate(V(-10,-10,0))
return shape
def make_sprue(step_name, doc_name, stem, expected_t):
source = centered_shape(step_name)
if abs(source.BoundBox.ZLength-expected_t) > 1e-5:
raise RuntimeError('Unexpected washer thickness')
pads=[]
# Left column is rotated so both inward edges are solid. Two columns and
# four rows give eight washers with seven straight connecting bars.
for col in (0, 1):
for row in range(4):
pad=source.copy()
if col == 0:
pad.rotate(V(),V(0,0,1),180)
pad.translate(V(24*col,16*row,0))
pads.append(pad)
# Each bar is constant 1.8 x 1.8 mm. Ends overlap the outer edge by
# 0.8 mm on the centerline. The root is sacrificial and needs careful
# trimming and smoothing after printing.
rods=[]
for x in (0,24):
for row in range(3):
rods.append(Part.makeBox(1.8,5.6,1.8,V(x-.9,16*row+5.2,0)))
rods.append(Part.makeBox(13.6,1.8,1.8,V(5.2,-.9,0)))
# Quantify the overlap of each end with its two washers. The rod body is
# never necked down, but its contact height is limited by the washer.
overlaps=[]
for col in (0,1):
for row in range(3):
rod=rods[col*3+row]
overlaps.append([rod.common(pads[col*4+row]).Volume,
rod.common(pads[col*4+row+1]).Volume])
overlaps.append([rods[6].common(pads[0]).Volume,
rods[6].common(pads[4]).Volume])
if any(min(pair) < 0.5 for pair in overlaps):
raise RuntimeError('Insufficient rod/pad intersection')
combined=pads[0]
# All parts touch through each rod, but may not be sequentially connected.
# MultiFuse avoids relying on order of the graph during the operation.
combined = pads[0].multiFuse(pads[1:]+rods).removeSplitter()
if combined.isNull() or not combined.isValid() or len(combined.Solids)!=1:
raise RuntimeError('Sprue is not one valid connected solid: '+stem)
# Confirm every washer's complete original geometry is retained.
for pad in pads:
if pad.cut(combined).Volume > 1e-5:
raise RuntimeError('Source washer material was removed')
bounds=combined.BoundBox
doc=App.newDocument(doc_name)
o=doc.addObject('Part::Feature','ConnectedSprue');o.Label=stem;o.Shape=combined
doc.recompute()
doc.saveAs(str(OUT/(stem+'.FCStd')))
Part.export([o],str(OUT/(stem+'.step')))
mesh=MeshPart.meshFromShape(Shape=combined,LinearDeflection=0.03,
AngularDeflection=0.2,Relative=False)
mesh.write(str(OUT/(stem+'.stl')))
App.closeDocument(doc.Name)
check=Mesh.Mesh(str(OUT/(stem+'.stl')))
components=check.getSeparateComponents()
if len(components)!=1 or not components[0].isSolid():
raise RuntimeError('STL is not one closed connected mesh')
return {'file':stem+'.stl','washer_quantity':8,'nominal_washer_thickness_mm':expected_t,
'rod_count':len(rods),'rod_width_mm':1.8,'rod_height_mm':1.8,
'rod_centerline_radial_overlap_mm':0.8,
'rod_end_intersection_volumes_mm3': [[round(v,5) for v in pair] for pair in overlaps],
'closed_components':len(components),'closed':True,
'bounds_mm':[bounds.XMin,bounds.XMax,bounds.YMin,bounds.YMax,bounds.ZMin,bounds.ZMax],
'nominal_washer_volume_mm3':round(source.Volume,5)}
result=[]
result.append(make_sprue('Shim05.step','WasherSprue05','垫片0.5mm_8个拼件',.5))
result.append(make_sprue('Shim10.step','WasherSprue10','垫片1.0mm_8个拼件',1.0))
source=SOURCE/'EncoderBracket.stl'; target=OUT/'编码器支架_单件.stl'
shutil.copyfile(source,target)
digest=hashlib.sha256(source.read_bytes()).hexdigest()
if hashlib.sha256(target.read_bytes()).hexdigest()!=digest: raise RuntimeError('Bracket copy mismatch')
m=Mesh.Mesh(str(target))
if len(m.getSeparateComponents())!=1 or not m.isSolid(): raise RuntimeError('Invalid bracket mesh')
result.insert(0,{'file':target.name,'quantity':1,'closed_components':1,'sha256':digest})
(OUT/'几何检查.json').write_text(json.dumps(result,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
print(json.dumps(result,ensure_ascii=False,indent=2))

View File

@@ -0,0 +1,132 @@
"""Concept test only: 50mm mounting with open slots, corners flat on motor."""
from pathlib import Path
import time, math, traceback
import FreeCAD as A, FreeCADGui as G, Part, Import
from pivy import coin
R=Path(__file__).resolve().parent.parent
O=R/'models'/'square_encoder_concept'
O.mkdir(parents=True,exist_ok=True)
V=A.Vector
def cyl(r,h,x,y,z): return Part.makeCylinder(r,h,V(x,y,z))
def add(doc,name,label,sh,color,trans=0):
o=doc.addObject('PartDesign::Feature',name); o.Label=label; o.Shape=sh
o.ViewObject.ShapeColor=color; o.ViewObject.LineColor=(.1,.1,.1); o.ViewObject.Transparency=trans
return o
try:
old=A.openDocument(str(R/'models'/'profile_mount_check'/'Direct.FCStd'))
keep=['Motor','Holder','Magnet','PCB','Chip']+['PCBHead%d'%i for i in range(1,4)]+['Corner%d'%i for i in range(4)]+['Rail%d'%i for i in range(2)]+['MountM4_%d'%i for i in range(1,5)]
shapes={n:old.getObject(n).Shape.copy() for n in keep}
orig=old.getObject('Bracket').Shape.copy()
shims=[old.getObject('Shim%d'%i).Shape.copy() for i in range(4)]
A.closeDocument(old.Name)
doc=A.newDocument('SquareOpen50')
colors={'Motor':(.32,.34,.37),'Holder':(.98,.55,.10),'Magnet':(.8,.8,.8),'PCB':(.04,.65,.19),'Chip':(.1,.1,.1)}
objects={}
for n in keep:
color=colors.get(n,(.84,.47,.13) if n.startswith('Corner') else (.55,.56,.60) if n.startswith('Rail') else (.8,.8,.8))
objects[n]=add(doc,n,n,shapes[n],color,70 if n=='Motor' else 20 if n.startswith('Corner') else 0)
pts=[]
for i in range(4):
a=math.radians(45+90*i);pts.append((25*math.cos(a),25*math.sin(a)))
# Existing foot top is z=2.5 with 0.5 shim. Increase clamping surface to 3.5;
# subtract motor-side angle brackets, keeping them directly on motor.
# Blue square outline in user's sketch: retain PCB ring's original height
# and pilot bosses while replacing the outer envelope with a 45mm frame.
square=Part.makeBox(45,45,3.2,V(-22.5,-22.5,10.9)).cut(cyl(16.8,3.22,0,0,10.89))
bracket=orig.fuse(square)
for x,y in pts:
bracket=bracket.fuse(cyl(4.5,1,x,y,2.5))
# Add 0.2mm plan clearance to each metal foot and 0.2mm above it.
cutters=[]
for i in range(4):
bb=shapes['Corner%d'%i].BoundBox
cutters.append(Part.makeBox(bb.XLength+.4,bb.YLength+.4,3.22,V(bb.XMin-.2,bb.YMin-.2,-.01)))
for cutter in cutters: bracket=bracket.cut(cutter)
# Four axial through slots each open outward toward the nearest side.
# Remove a larger opening in the upper frame for M4 head/tool access.
for x,y in pts:
start = x if x>0 else x-10
slot=Part.makeBox(10,4.4,3.7,V(start,y-2.2,-.05)).fuse(cyl(2.2,3.7,x,y,-.05))
upp=Part.makeBox(10,7.4,11,V(start,y-3.7,3.5)).fuse(cyl(3.7,11,x,y,3.5))
bracket=bracket.cut(slot)
bracket=bracket.cut(upp)
for j in range(4):
shims[j]=shims[j].cut(slot)
for j in range(4):
for cutter in cutters: shims[j]=shims[j].cut(cutter)
bracket=bracket.removeSplitter()
if bracket.isNull() or not bracket.isValid() or len(bracket.Solids)!=1:raise RuntimeError('support not a single solid')
blue=add(doc,'OpenBracket','Square PCD50 frame with outward open slots - concept',bracket,(.12,.43,.88))
for i,sh in enumerate(shims):
sh=sh.removeSplitter()
if sh.isNull() or not sh.isValid():raise RuntimeError('shim invalid %d'%i)
add(doc,'SlottedShim%d'%i,'Open 0.5mm shim - concept',sh,(.65,.7,.75))
heads=[];shafts=[]
for i,(x,y) in enumerate(pts):
head=cyl(3.5,3,x,y,3.5);shaft=cyl(1.95,8.5,x,y,-5)
heads.append(add(doc,'EncoderHead%d'%i,'M4 head Ø7 x 3 - assumed',head,(.8,.8,.8)))
shafts.append(add(doc,'EncoderShaft%d'%i,'M4 shaft Ø3.9 - assumed',shaft,(.8,.8,.8)))
doc.recompute()
corners=[objects['Corner%d'%i] for i in range(4)]
rails=[objects['Rail%d'%i] for i in range(2)]
moving=[blue]+[doc.getObject('SlottedShim%d'%i) for i in range(4)]+heads+shafts
checks=[]
for o in moving:
for c in corners+rails:
if not o.Shape.BoundBox.intersect(c.Shape.BoundBox):continue
vol=o.Shape.common(c.Shape).Volume
if vol>.0001:checks.append('%s vs %s %.6f mm3'%(o.Name,c.Name,vol))
for o in shafts:
for c in [blue]+[doc.getObject('SlottedShim%d'%i) for i in range(4)]:
if not o.Shape.BoundBox.intersect(c.Shape.BoundBox): continue
vol=o.Shape.common(c.Shape).Volume
if vol>.0001: checks.append('%s vs %s %.6f mm3'%(o.Name,c.Name,vol))
# Effective supporting material under each head immediately below the top.
areas=[]
for x,y in pts:
# Planar sampling avoids degenerate thin-cylinder OCC intersections.
count=0; step=.1
for ix in range(-35,36):
for iy in range(-35,36):
dx=ix*step;dy=iy*step
if dx*dx+dy*dy<=3.5*3.5 and bracket.isInside(V(x+dx,y+dy,3.49),1e-7,True):count+=1
areas.append(count*step*step)
# Kinematic slide of support from y=+5 to its assembled position: only a
# quick volume test of support vs angles + shafts, not a tool/hand check.
slide=[]
for n in range(11):
dy=5.0-n*.5; moved=bracket.copy();moved.translate(V(0,dy,0));hits=[]
for c in corners+rails+shafts:
if moved.BoundBox.intersect(c.Shape.BoundBox):
v=moved.common(c.Shape).Volume
if v>.0001:hits.append('%s %.4f'%(c.Name,v))
slide.append('dy=%.1f: %s'%(dy,', '.join(hits) if hits else 'no sampled volume overlap'))
report=['CONCEPT ONLY; corners are flush to motor; no spacer under PCD76 corners.',
'Support feet 0.5..3.5mm; shim 0.5mm; screw head 7mm dia x 3mm assumed; 0.2mm corner clearance.',
'PCD50 screw slots open sideways outward; 45mm square frame from sketch.',
'Only 0.05mm theoretical slot-end clearance in motor corner as original direct check.',
'Intersections of support/shims/screw envelope with corners and rails:']+(checks or ['none in checked pairs'])
report+=['Approx material area under 7mm screw head using 0.1mm sampled grid (mm2): '+', '.join('%.3f'%x for x in areas),'Sampled lateral slide +Y to 0:']+slide
(O/'SquareOpen50_check.txt').write_text('\n'.join(report)+'\n',encoding='utf8')
view=G.activeDocument().activeView()
def render(kind,name):
if kind=='rear':view.viewTop()
else:view.getCameraNode().orientation.setValue(coin.SbRotation(.880476,.115917,-.279848,-.364705))
G.updateGui();time.sleep(.5);view.fitAll();G.updateGui();time.sleep(.5);view.saveImage(str(O/name),1600,1200,'White')
render('rear','SquareOpen50_rear.png');render('axon','SquareOpen50_axon.png')
states={o.Name:o.ViewObject.Visibility for o in doc.Objects if hasattr(o,'Shape')}
for o in doc.Objects:
if hasattr(o,'Shape'):o.ViewObject.Visibility=False
blue.ViewObject.Visibility=True
render('rear','SquareOpen50_bracket.png')
for o in doc.Objects:
if hasattr(o,'Shape'):o.ViewObject.Visibility=states[o.Name]
render('axon','SquareOpen50_axon.png')
doc.recompute();doc.saveAs(str(O/'SquareOpen50.FCStd'))
Import.export([o for o in doc.Objects if hasattr(o,'Shape')],str(O/'SquareOpen50.step'))
(O/'SQUARE_DONE.txt').write_text('ok\n',encoding='utf8')
except Exception:
(O/'SQUARE_ERROR.txt').write_text(traceback.format_exc(),encoding='utf8')
raise

View File

@@ -0,0 +1,76 @@
from pathlib import Path
import os,time,traceback
import FreeCAD as A, FreeCADGui as G, Part, Import
from pivy import coin
V=A.Vector
R=Path(r'C:\projects\wheel'); O=R/'models'/'switch_2020_concept';O.mkdir(parents=True,exist_ok=True)
def box(x,y,z,dx,dy,dz): return Part.makeBox(dx,dy,dz,V(x,y,z))
def cy(r,h,x,y,z,d=V(0,0,1)): return Part.makeCylinder(r,h,V(x,y,z),d)
def sy(x,lo,hi,z,r,h): return box(x-r,lo,z,2*r,hi-lo,h).fuse(cy(r,h,x,lo,z)).fuse(cy(r,h,x,hi,z))
def sz(x,y,lo,hi,r,h): return box(x-r,y,lo,2*r,h,hi-lo).fuse(cy(r,h,x,y,lo,V(0,1,0))).fuse(cy(r,h,x,y,hi,V(0,1,0)))
def add(d,n,s,c,label):
o=d.addObject('PartDesign::Feature',n);o.Shape=s.removeSplitter();o.Label=label;o.ViewObject.ShapeColor=c;o.ViewObject.LineColor=(.15,.15,.15);return o
ok=False
try:
d=A.openDocument(str(R/'models'/'kfl_3030_motor_corner_concept'/'Assembly.FCStd'))
d.Label='Switch crossbeam 2020 - dimensional concept with envelope collision'
sw=d.getObject('SwitchEnvelope');sw.ViewObject.Transparency=78
beam=add(d,'SwitchCross2020',box(-80,-55,-142,160,20,20),(.32,.36,.39),'Added 2020 switch crossbeam; solid envelope')
new=[beam];ang=[];con=[];ears=[];heads=[]
for i,(x,y,tag) in enumerate(((-42.,-35.,'A'),(42.,-22.,'B'))):
ax=x+(-.75 if x<0 else .75) # angle offset; M4 ear bolt clearance in 6.5mm slot
# Long leg on the 2020 side; short foot extends away from the 2020 and stays above the 3030.
upright=box(ax-9.5,-55,-145,19,50,3).cut(sy(ax,-45.75,-19.25,-145.02,3.25,3.04))
foot=box(ax-9.5,-55,-176,19,3,31).cut(sz(ax,-55.02,-165.25,-155.75,3.25,3.04))
a=add(d,'SwitchAngle'+tag,upright.fuse(foot),(.98,.52,.05),'Long 50x31 angle '+tag+'; foot extends outward');ang.append(a);new.append(a)
# Separate assumed schematic bolt head and ear on the switch side of the angle.
ear=cy(7,3,x,y,-148).cut(cy(2.2,3.04,x,y,-148.02))
e=add(d,'AssumedEar'+tag,ear,(.21,.67,.24),'ASSUMED ear '+tag+'; illustrative, no actual shell measured');ears.append(e)
h=cy(3.5,2.2,x,y,-150.2)
hd=add(d,'SwitchHead'+tag,h,(.12,.12,.12),'ASSUMED head '+tag+' Ø7');heads.append(hd);new.append(hd)
# Fastener head at the side groove of the 2020 (no shank entering solid extrusion).
railhead=add(d,'SwitchRailHead'+tag,cy(4.25,3,ax,-45,-148),(.12,.12,.12),'Side groove head '+tag+'; bolt and groove not modeled');new.append(railhead)
for i,x in enumerate((-50.6200577,50.6200577)):
# 2020 to 3030 separate L angles on the opposite, plate-facing side.
upright=box(x-9,-55,-122,18,20,3).cut(cy(2.75,3.04,x,-45,-122.02))
foot=box(x-9,-55,-119,18,3,20).cut(cy(2.75,3.04,x,-55.02,-109,V(0,1,0)))
q=add(d,'SwitchCrossJoint%d'%i,upright.fuse(foot),(.91,.23,.18),'2020 to 3030 corner '+str(i+1));con.append(q);new.append(q)
d.recompute()
contextual=[o for o in d.Objects if hasattr(o,'Shape') and not o.Shape.isNull() and o not in new and o not in ears and o.ViewObject.Visibility]
lines=[];bad=[]
for i,a in enumerate(new):
for b in new[i+1:]+contextual:
try:
if not a.Shape.BoundBox.intersect(b.Shape.BoundBox):continue
v=a.Shape.common(b.Shape).Volume
if v>0.001:bad.append((a.Name,b.Name,v))
lines.append('%s / %s : %.6f mm3'%(a.Name,b.Name,v))
except Exception as ex:lines.append('ERROR %s/%s %s'%(a.Name,b.Name,ex))
(O/'interference_report.txt').write_text('\n'.join(lines)+'\n',encoding='utf8')
(O/'positive_intersections.txt').write_text('\n'.join('%s / %s : %.6f mm3'%v for v in bad) or 'none',encoding='utf8')
d.saveAs(str(O/'Assembly.FCStd'));Import.export([o for o in d.Objects if hasattr(o,'Shape') and not o.Shape.isNull() and o.ViewObject.Visibility],str(O/'Assembly.step'))
view=G.activeDocument().activeView();G.activateWorkbench('PartWorkbench')
def snap(name,mode):
if mode=='front':view.viewBottom()
elif mode=='side':view.viewFront()
else:view.viewAxonometric();G.updateGui();view.getCameraNode().orientation.setValue(coin.SbRotation(.880476,.115917,-.279848,-.364705))
G.updateGui();time.sleep(.3);view.fitAll();G.updateGui();time.sleep(.4);view.saveImage(str(O/name),1800,1250,'White')
snap('assembly.png','axon')
# Show surrounding structure and schematic switch, focus the full assembly so the beam is visible.
snap('end.png','front');snap('side.png','side')
vis={o.Name:o.ViewObject.Visibility for o in d.Objects if hasattr(o,'Shape')}
show=set([o.Name for o in new+ears]+['NewRail0','NewRail1','Crossbeam0','Crossbeam1','CompactPlate0','CompactPlate1','KFLHousing0','KFLHousing1','CamShaft','SwitchEnvelope'])
for o in d.Objects:
if hasattr(o,'Shape'):o.ViewObject.Visibility=o.Name in show
snap('focus.png','axon')
simple=set([o.Name for o in new+ears]+['SwitchEnvelope'])
for o in d.Objects:
if hasattr(o,'Shape'):o.ViewObject.Visibility=o.Name in simple
view.viewTop();G.updateGui();time.sleep(.3);view.fitAll();G.updateGui();time.sleep(.3);view.saveImage(str(O/'switch_front.png'),1800,1250,'White')
snap('switch_detail.png','axon')
for o in d.Objects:
if hasattr(o,'Shape'):o.ViewObject.Visibility=vis.get(o.Name,True)
d.recompute();d.saveAs(str(O/'Assembly.FCStd'));A.closeDocument(d.Name)
(O/'DONE.txt').write_text('ok',encoding='utf8');ok=True
except Exception:(O/'ERROR.txt').write_text(traceback.format_exc(),encoding='utf8')
finally:os._exit(0 if ok else 1)

View File

@@ -0,0 +1,87 @@
from pathlib import Path
import os,time,traceback
import FreeCAD as A, FreeCADGui as G, Part, Import
from pivy import coin
V=A.Vector
R=Path(r'C:\projects\wheel'); O=R/'models'/'switch_2020_raise10_concept';O.mkdir(parents=True,exist_ok=True)
def box(x,y,z,dx,dy,dz): return Part.makeBox(dx,dy,dz,V(x,y,z))
def cy(r,h,x,y,z,d=V(0,0,1)): return Part.makeCylinder(r,h,V(x,y,z),d)
def sy(x,lo,hi,z,r,h): return box(x-r,lo,z,2*r,hi-lo,h).fuse(cy(r,h,x,lo,z)).fuse(cy(r,h,x,hi,z))
def sz(x,y,lo,hi,r,h): return box(x-r,y,lo,2*r,h,hi-lo).fuse(cy(r,h,x,y,lo,V(0,1,0))).fuse(cy(r,h,x,y,hi,V(0,1,0)))
def add(d,n,s,c,label):
o=d.addObject('PartDesign::Feature',n);o.Shape=s.removeSplitter();o.Label=label;o.ViewObject.ShapeColor=c;o.ViewObject.LineColor=(.15,.15,.15);return o
ok=False
try:
d=A.openDocument(str(R/'models'/'kfl_3030_motor_corner_concept'/'Assembly.FCStd'))
d.Label='Switch crossbeam 2020 - common shaft raised by 10 mm'
# Every visible component moves together except the fixed 3030 frame, its joints and the long bearing-plate brackets.
fixed_prefix=('NewRail','Crossbeam','PurchasedAngle','BaseJoint','FootHead')
moved=[]
for o in d.Objects:
if hasattr(o,'Shape') and not o.Shape.isNull() and o.ViewObject.Visibility and not o.Name.startswith(fixed_prefix):
p=o.Placement;p.Base.y+=10.0;o.Placement=p;moved.append(o.Name)
d.recompute()
(O/'moved_names.txt').write_text('\n'.join(moved)+'\n',encoding='utf8')
sw=d.getObject('SwitchEnvelope');sw.ViewObject.Transparency=78
beam=add(d,'SwitchCross2020',box(-80,-55,-142,160,20,20),(.32,.36,.39),'Added 2020 switch crossbeam; solid envelope')
new=[beam];ang=[];con=[];ears=[];heads=[]
for i,(x,y,tag) in enumerate(((-42.,-25.,'A'),(42.,-12.,'B'))):
ax=x+(-.75 if x<0 else .75) # angle offset; M4 ear bolt clearance in 6.5mm slot
# 50 mm upright attaches switch; 31 mm foot sits on the top of 2020.
upright=box(ax-9.5,-32,-145,19,47,3).cut(sy(ax,-25.75,.75,-145.02,3.25,3.04))
foot=box(ax-9.5,-35,-142,19,3,31).cut(sz(ax,-35.02,-131.25,-121.75,3.25,3.04))
a=add(d,'SwitchAngle'+tag,upright.fuse(foot),(.98,.52,.05),'Long 50x31 angle '+tag+'; top mounted');ang.append(a);new.append(a)
# Separate assumed schematic bolt head and ear on the switch side of the angle.
ear=cy(7,3,x,y,-148).cut(cy(2.2,3.04,x,y,-148.02))
e=add(d,'AssumedEar'+tag,ear,(.21,.67,.24),'ASSUMED ear '+tag+'; illustrative, no actual shell measured');ears.append(e)
h=cy(3.5,2.2,x,y,-150.2)
hd=add(d,'SwitchHead'+tag,h,(.12,.12,.12),'ASSUMED head '+tag+' Ø7');heads.append(hd);new.append(hd)
# Fastener head at the side groove of the 2020 (no shank entering solid extrusion).
railhead=add(d,'SwitchRailHead'+tag,cy(4.25,3,ax,-32,-132,V(0,1,0)),(.12,.12,.12),'Top groove head '+tag+'; bolt and groove not modeled');new.append(railhead)
for i,x in enumerate((-50.6200577,50.6200577)):
# 2020 to 3030 separate L angles on the opposite, plate-facing side.
upright=box(x-9,-55,-122,18,20,3).cut(cy(2.75,3.04,x,-45,-122.02))
foot=box(x-9,-55,-119,18,3,20).cut(cy(2.75,3.04,x,-55.02,-109,V(0,1,0)))
q=add(d,'SwitchCrossJoint%d'%i,upright.fuse(foot),(.91,.23,.18),'2020 to 3030 corner '+str(i+1));con.append(q);new.append(q)
# Shim bridges the old 2020 end and raised motor corner foot, one 5.5mm fastening hole.
shim=box(x-10,-55,0,20,10,40).cut(cy(2.75,10.04,x,-55.02,30,V(0,1,0)))
q=add(d,'MotorShim%d'%i,shim,(.45,.58,.75),'10mm shim supports motor 2020 and connector foot '+str(i+1));new.append(q)
d.recompute()
contextual=[o for o in d.Objects if hasattr(o,'Shape') and not o.Shape.isNull() and o not in new and o not in ears and o.ViewObject.Visibility]
lines=[];bad=[]
for i,a in enumerate(new):
for b in new[i+1:]+contextual:
try:
if not a.Shape.BoundBox.intersect(b.Shape.BoundBox):continue
v=a.Shape.common(b.Shape).Volume
if v>0.001:bad.append((a.Name,b.Name,v))
lines.append('%s / %s : %.6f mm3'%(a.Name,b.Name,v))
except Exception as ex:lines.append('ERROR %s/%s %s'%(a.Name,b.Name,ex))
(O/'interference_report.txt').write_text('\n'.join(lines)+'\n',encoding='utf8')
(O/'positive_intersections.txt').write_text('\n'.join('%s / %s : %.6f mm3'%v for v in bad) or 'none',encoding='utf8')
d.saveAs(str(O/'Assembly.FCStd'));Import.export([o for o in d.Objects if hasattr(o,'Shape') and not o.Shape.isNull() and o.ViewObject.Visibility],str(O/'Assembly.step'))
view=G.activeDocument().activeView();G.activateWorkbench('PartWorkbench')
def snap(name,mode):
if mode=='front':view.viewBottom()
elif mode=='side':view.viewFront()
else:view.viewAxonometric();G.updateGui();view.getCameraNode().orientation.setValue(coin.SbRotation(.880476,.115917,-.279848,-.364705))
G.updateGui();time.sleep(.3);view.fitAll();G.updateGui();time.sleep(.4);view.saveImage(str(O/name),1800,1250,'White')
snap('assembly.png','axon')
# Show surrounding structure and schematic switch, focus the full assembly so the beam is visible.
snap('end.png','front');snap('side.png','side')
vis={o.Name:o.ViewObject.Visibility for o in d.Objects if hasattr(o,'Shape')}
show=set([o.Name for o in new+ears]+['NewRail0','NewRail1','Crossbeam0','Crossbeam1','CompactPlate0','CompactPlate1','KFLHousing0','KFLHousing1','CamShaft','SwitchEnvelope'])
for o in d.Objects:
if hasattr(o,'Shape'):o.ViewObject.Visibility=o.Name in show
snap('focus.png','axon')
simple=set(['SwitchCross2020','SwitchAngleA','SwitchAngleB','SwitchHeadA','SwitchHeadB','SwitchRailHeadA','SwitchRailHeadB','SwitchCrossJoint0','SwitchCrossJoint1','SwitchEnvelope','AssumedEarA','AssumedEarB'])
for o in d.Objects:
if hasattr(o,'Shape'):o.ViewObject.Visibility=o.Name in simple
view.viewTop();G.updateGui();time.sleep(.3);view.fitAll();G.updateGui();time.sleep(.3);view.saveImage(str(O/'switch_front.png'),1800,1250,'White')
snap('switch_detail.png','axon')
for o in d.Objects:
if hasattr(o,'Shape'):o.ViewObject.Visibility=vis.get(o.Name,True)
d.recompute();d.saveAs(str(O/'Assembly.FCStd'));A.closeDocument(d.Name)
(O/'DONE.txt').write_text('ok',encoding='utf8');ok=True
except Exception:(O/'ERROR.txt').write_text(traceback.format_exc(),encoding='utf8')
finally:os._exit(0 if ok else 1)

View File

@@ -0,0 +1,63 @@
from pathlib import Path
import os, time, json, traceback
import FreeCAD as A, FreeCADGui as G, Part, Import
from pivy import coin
R=Path(r'C:\projects\wheel'); O=R/'models'/'switch_2020_crossbar132'; O.mkdir(parents=True,exist_ok=True)
try:
d=A.openDocument(str(R/'models'/'switch_2020_direct_profiles'/'Assembly.FCStd'))
beam=d.getObject('SwitchCross2020'); old=beam.Shape.copy()
assert abs(old.BoundBox.XLength-160)<1e-5
beam.Shape=Part.makeBox(132,20,20,A.Vector(-66,-55,-142))
beam.Label='Switch crossbeam 2020 x 132 mm'; d.recompute()
new=beam.Shape
assert new.isValid() and abs(new.BoundBox.XLength-132)<1e-6
added=new.cut(old).Volume; removed=old.cut(new).Volume
assert added<1e-7 and abs(removed-11200)<1e-5
result={'crossbar_length_mm':132,'profile_mm':[20,20],'crossbar_x_min_mm':-66,'crossbar_x_max_mm':66,'added_volume_mm3':added,'removed_volume_mm3':removed,'components':{}}
# Replace the two old square bearing plates by the latest V3 machining model.
# V3 coordinates have the same X origin, Y=0 at the plate bottom, and Z=0 at its front face.
v3=Part.read(str(R/'models'/'kfl001_plate_v3_alt'/'KFL001_plate_V3.step'))
result['bearing_plates_v3']={}
for i,z in ((0,-84),(1,-187)):
obj=d.getObject('CompactPlate%d'%i);before=obj.Shape.copy()
revised=v3.copy();revised.translate(A.Vector(0,-41,z))
addedplate=revised.cut(before).Volume
assert addedplate<1e-4,(i,addedplate)
obj.Placement=A.Placement();obj.Shape=revised
obj.Label='Bearing plate V3 %d (76 x 75 x 8)'%(i+1)
result['bearing_plates_v3'][obj.Name]={'added_volume_mm3':addedplate,'removed_volume_mm3':before.cut(revised).Volume,'z_mm':z}
d.recompute()
for n in ['NewRail0','NewRail1','SwitchAngleA','SwitchAngleB','SwitchCrossJoint0','SwitchCrossJoint1','SwitchEnvelope']:
obj=d.getObject(n);bb=obj.Shape.BoundBox
overlap=new.common(obj.Shape).Volume if new.BoundBox.intersect(bb) else 0
distance=new.distToShape(obj.Shape)[0]
result['components'][n]={'x_min_mm':bb.XMin,'x_max_mm':bb.XMax,'distance_mm':distance,'overlap_mm3':overlap}
if n!='SwitchEnvelope':
assert bb.XMin>=-66 and bb.XMax<=66,(n,bb.XMin,bb.XMax)
assert overlap<0.001,(n,overlap)
else: assert distance>4.99
for n in ['SwitchAngleA','SwitchAngleB','SwitchCrossJoint0','SwitchCrossJoint1']:
assert result['components'][n]['distance_mm']<0.0001, n
d.Label='Switch crossbar 2020 x 132 mm - layout'
d.recompute();d.saveAs(str(O/'Assembly.FCStd'))
visible=[o for o in d.Objects if hasattr(o,'Shape') and not o.Shape.isNull() and o.ViewObject.Visibility]
Import.export(visible,str(O/'Assembly.step'))
G.activateWorkbench('PartWorkbench');view=G.activeDocument().activeView()
view.viewAxonometric();G.updateGui();view.getCameraNode().orientation.setValue(coin.SbRotation(.880476,.115917,-.279848,-.364705));G.updateGui();time.sleep(.3);view.fitAll();G.updateGui();time.sleep(.4);view.saveImage(str(O/'assembly.png'),1800,1250,'White')
visibility={o.Name:o.ViewObject.Visibility for o in d.Objects if hasattr(o,'Shape')}
show=set(['SwitchCross2020','SwitchAngleA','SwitchAngleB','SwitchHeadA','SwitchHeadB','SwitchRailHeadA','SwitchRailHeadB','SwitchCrossJoint0','SwitchCrossJoint1','SwitchEnvelope','AssumedEarA','AssumedEarB','NewRail0','NewRail1'])
for o in d.Objects:
if hasattr(o,'Shape'):o.ViewObject.Visibility=o.Name in show
view.viewAxonometric();G.updateGui();time.sleep(.2);view.fitAll();G.updateGui();time.sleep(.4);view.saveImage(str(O/'crossbar_focus.png'),1800,1250,'White')
view.viewTop();G.updateGui();time.sleep(.2);view.fitAll();G.updateGui();time.sleep(.4);view.saveImage(str(O/'crossbar_front.png'),1800,1250,'White')
for o in d.Objects:
if hasattr(o,'Shape'):o.ViewObject.Visibility=visibility.get(o.Name,True)
d.recompute();d.saveAs(str(O/'Assembly.FCStd'));A.closeDocument(d.Name)
dd=A.openDocument(str(O/'Assembly.FCStd'));dd.recompute();v=dd.getObject('SwitchCross2020').Shape
assert v.isValid() and abs(v.BoundBox.XLength-132)<1e-6
step=Part.read(str(O/'Assembly.step'));assert step.isValid() and len(step.Solids)>0
result['fcstd_reopened']=True;result['step_valid']=True;result['step_solid_count']=len(step.Solids)
(O/'validation.json').write_text(json.dumps(result,indent=2),encoding='utf8');A.closeDocument(dd.Name)
(O/'DONE.txt').write_text('ok',encoding='utf8');os._exit(0)
except Exception:
(O/'ERROR.txt').write_text(traceback.format_exc(),encoding='utf8');os._exit(1)

View File

@@ -0,0 +1,184 @@
from pathlib import Path
import FreeCAD as A, FreeCADGui as G, Part, Import, os, math, time, json, traceback
from pivy import coin
V=A.Vector
root=Path(r'C:\projects\wheel'); out=root/'models'/'symmetric_corner_18_concept';out.mkdir(parents=True,exist_ok=True)
def box(x,y,z,dx,dy,dz):return Part.makeBox(dx,dy,dz,V(x,y,z))
def cyl(r,h,x,y,z,axis=V(0,0,1)):return Part.makeCylinder(r,h,V(x,y,z),axis)
def add(doc,n,shape,label,color,trans=0):
o=doc.addObject('PartDesign::Feature',n);o.Shape=shape.removeSplitter();o.Label=label;o.ViewObject.ShapeColor=color;o.ViewObject.LineColor=(.12,.12,.12);o.ViewObject.Transparency=trans;return o
def vol(a,b):return a.common(b).Volume if a.BoundBox.intersect(b.BoundBox) else 0.
def cc(a,b):return {'vol':vol(a,b),'dist':a.distToShape(b)[0]}
try:
d=A.openDocument(str(root/'models'/'fan_direct_motor_side_140'/'Assembly.FCStd'));d.recompute();d.Label='新电机角码与2mm垫片概念装配'
p=38/math.sqrt(2); xinner=p+10.2; xwall=xinner+1.8; xrail=xwall+2; xc=xrail+10
# Reference washer sits on bracket's extrusion facing X surface, never between bracket and motor.
oldlist=['Corner0','Corner1','Corner2','Corner3','Mount76_0','Mount76_1','Mount76_2','Mount76_3','Washer76_0','Washer76_1','Washer76_2','Washer76_3','OpenPillarBracket','Head50_0','Head50_1','Head50_2','Head50_3','Shaft50_0','Shaft50_1','Shaft50_2','Shaft50_3','AdjustShim0','AdjustShim1','AdjustShim2','AdjustShim3','DirectFan','FanBolt0','FanBolt1','Crossbeam0','Crossbeam1','BaseJoint00','BaseJoint01','BaseJoint10','BaseJoint11','PurchasedAngle00','PurchasedAngle01','PurchasedAngle10','PurchasedAngle11','BoardM5_00','BoardM5_01','BoardM5_10','BoardM5_11','FootHead00','FootHead01','FootHead10','FootHead11']
for n in oldlist:
o=d.getObject(n)
if o:o.ViewObject.Visibility=False
# previously purchased profile lengths and axial position, model vertical profiles remain solid envelopes
for i,sgn in enumerate((-1,1)):
rail=d.getObject('Rail%d'%i); rail.Placement=A.Placement();rail.Shape=box(sgn*xc-10,-55,0,20,140,20);rail.Label='Purchased 2020, 140mm'
base=d.getObject('NewRail%d'%i);base.Placement=A.Placement();base.Shape=box(sgn*xc-15,-85,-215,30,30,250);base.Label='Purchased 3030, 250mm'
for stem in ['MotorBaseCorner','SwitchCrossJoint']:
a=d.getObject(stem+str(i));old_center=(-1 if i==0 else 1)*50.6200577
dx=sgn*xc-old_center; s=a.Shape.copy();s.translate(V(dx,0,0));a.Placement=A.Placement();a.Shape=s
# Lighting crossbeam 130mm, fixed by supplied stock; no guessed changes to control profile
sw=d.getObject('SwitchCross2020');sw.Placement=A.Placement();sw.Shape=box(-65,-55,-142,130,20,20);sw.Label='Purchased 2020 switch 130mm'
motor=d.getObject('Motor')
# Cut shape: round end at opposite side of bend. Plan view is x-z extrusion directions; y is 16mm width.
corners=[];washers=[];studs=[];screws=[]
for i,sgn in enumerate((-1,1)):
for j,y in enumerate((10-p,10+p)):
# +X prototype motor leg. 20 length includes 2mm bend wall, 16 width. Round free end R8.
flat=box(xinner-10.2,y-8,0,12,16,1.8)
cap=cyl(8,1.8,xinner-10.2,y,0).common(box(xinner-18.2,y-8,0,8,16,1.8))
leg=flat.fuse(cap).cut(cyl(2.5,1.9,p,y,-.05))
wall=box(xinner,y-8,0,1.8,16,12)
cap2=cyl(8,1.8,xinner,y,12,V(1,0,0)).common(box(xinner,y-8,12,1.8,16,8))
wall=wall.fuse(cap2).cut(cyl(2.5,1.9,xinner-.05,y,12,V(1,0,0)))
corner=leg.fuse(wall).removeSplitter()
washer=cyl(4.5,2,xwall,y,12,V(1,0,0)).cut(cyl(2.2,2.1,xwall-.05,y,12,V(1,0,0)))
# Motor M4 shank and simple head, countersink depth is unknown
motorbolt=cyl(1.95,7,p,y,-5).fuse(cyl(3.5,2.2,p,y,2))
if sgn<0:
corner=corner.mirror(V(),V(1,0,0));washer=washer.mirror(V(),V(1,0,0));motorbolt=motorbolt.mirror(V(),V(1,0,0))
c=add(d,'NewMotorCorner%d%d'%(i,j),corner,'New stainless 20x20x16x1.8 Ø5, round ends assumed',(.9,.55,.13));corners.append(c)
w=add(d,'RailWasher%d%d'%(i,j),washer,'2mm total washer BETWEEN motor corner and 2020 rail',(.95,.2,.12));washers.append(w)
screws.append(add(d,'NewMotorM4%d%d'%(i,j),motorbolt,'M4 motor shank + head schematic',(.65,.65,.68)))
# Encoder bracket as plate-only visualization, 4 hex standoffs per user idea; no claim of finalized printable design
old=d.getObject('OpenPillarBracket').Shape
plate=old.common(box(-70,-70,10.9,140,150,3.2))
bp=add(d,'EncoderPlateConcept',plate,'Encoder panel only; metal standoffs visual concept, not print-ready',(.08,.3,.85))
for i in range(4):
ang=math.radians(45+90*i);x=25*math.cos(ang);y=10+25*math.sin(ang);Rhex=7/(2*math.cos(math.pi/6))
points=[V(x+Rhex*math.cos(math.radians(30+60*k)),y+Rhex*math.sin(math.radians(30+60*k)),0) for k in range(6)];points.append(points[0])
hexshape=Part.Face(Part.makePolygon(points)).extrude(V(0,0,10)).cut(cyl(2.2,10.02,x,y,-.01))
st=add(d,'EncoderHex%d'%i,hexshape,'M4 7mm AF 10mm stand-off ASSUMED',(.86,.72,.3));studs.append(st)
top=cyl(4.5,.9,x,y,10).cut(cyl(2.2,.92,x,y,9.99))
add(d,'EncoderSpacer%d'%i,top,'Encoder metal shim .9mm SCHEMATIC',(.72,.72,.75))
# Two purchased 70mm beams are centred between the 3030 rails.
# The original mounting corners remain at the same ends, translated with the new rails.
conflicts=[]; joint_checks=[]
for i,z in enumerate((-114.,-179.)):
cross=d.getObject('Crossbeam%d'%i); cross.Placement=A.Placement();cross.Shape=box(-35,-85,z,70,30,30);cross.Label='Purchased 3030 crossbeam 70mm';cross.ViewObject.Visibility=True
for j,sgn in enumerate((-1,1)):
rr=d.getObject('NewRail%d'%j).Shape
c=cross.Shape.common(rr)
if c.Volume>1e-5: conflicts.append((i,j,c.Volume))
name='BaseJoint%d%d'%(i,j); a=d.getObject(name)
ss=a.Shape.copy();ss.translate(V(sgn*(xc-50.6200577),0,0));a.Placement=A.Placement();a.Shape=ss;a.ViewObject.Visibility=True
joint_checks.append({'name':name,'cross':cc(a.Shape,cross.Shape),'base':cc(a.Shape,rr)})
for stem in ('PurchasedAngle','BoardM5_','FootHead'):
for j in (0,1):
o=d.getObject(stem+str(i)+str(j))
if o:
o.ViewObject.Visibility=True
if stem in ('PurchasedAngle','FootHead'):
ss=o.Shape.copy();ss.translate(V(0,2,0));o.Placement=A.Placement();o.Shape=ss
# An initial fan frame rotated for new spacing, check conflicts and hide if nonzero.
theta=math.degrees(math.acos((2*xc)/(105*math.sqrt(2))))-45;alpha=math.radians(theta)
fan=box(-60,-60,20,120,120,25).cut(cyl(56,25.04,0,0,19.98))
for x in (-52.5,52.5):
for y in (-52.5,52.5):fan=fan.cut(cyl(2.2,25.1,x,y,19.95))
fan=fan.removeSplitter();fan.rotate(V(),V(0,0,1),theta);fan.translate(V(0,10,0))
f=d.getObject('DirectFan');f.Placement=A.Placement();f.Shape=fan; f.Label='FAN illustrative; rotated to new spacing';f.ViewObject.Visibility=True
for sgn,i in [(-1,0),(1,1)]:
x=sgn*52.5*(math.cos(alpha)-math.sin(alpha));y=10+sgn*52.5*(math.cos(alpha)+math.sin(alpha))
o=d.getObject('FanBolt%d'%i);o.Placement=A.Placement();o.Shape=cyl(2,25,x,y,20).fuse(cyl(3.5,3,x,y,45));o.ViewObject.Visibility=True
d.recompute()
checks=[]
for i,c in enumerate(corners):
j=i//2;rr=d.getObject('Rail%d'%j).Shape
checks.append({'corner':c.Name,'motor_volume':vol(c.Shape,motor.Shape),'washer_corner':cc(c.Shape,washers[i].Shape),'washer_rail':cc(washers[i].Shape,rr),'hex_volume':sum(vol(c.Shape,s.Shape) for s in studs),'hex_min_distance':min(c.Shape.distToShape(s.Shape)[0] for s in studs)})
fanhits=[]
for o in d.Objects:
if o==f or not hasattr(o,'Shape') or o.Shape.isNull() or not o.ViewObject.Visibility or o.Name.startswith('FanBolt'):continue
v=vol(fan,o.Shape)
if v>1e-4:fanhits.append([o.Name,v])
# hide guessed fan when conflicts (never imply it has been fitted)
if fanhits:
f.ViewObject.Visibility=False
for i in (0,1):d.getObject('FanBolt%d'%i).ViewObject.Visibility=False
# Replace every visible old bracket, including the two types of long brackets.
# New L shape made with round distal ends. Its holes are both Ø5 mm. We model
# only sharp-bend nominal geometry; countersink and tool access are unknown.
def l_yz(x, yc, zface, upwards_z, foot_direction):
# width X=16; foot contacts y=yc, upright contacts z=zface.
t=1.8
zz=zface if upwards_z>0 else zface-t
side=box(x-8,yc,zz,16,12,t).fuse(cyl(8,t,x,yc+12,zz).common(box(x-8,yc+12,zz,16,8,t)))
side=side.cut(cyl(2.5,t+.1,x,yc+12,zz-.05))
if foot_direction>0:
foot=box(x-8,yc,zface,16,t,12).fuse(cyl(8,t,x,yc,zface+12,V(0,1,0)).common(box(x-8,yc,zface+12,16,t,8)))
foot=foot.cut(cyl(2.5,t+.1,x,yc-.05,zface+12,V(0,1,0)))
else:
foot=box(x-8,yc,zface-12,16,t,12).fuse(cyl(8,t,x,yc,zface-12,V(0,1,0)).common(box(x-8,yc,zface-20,16,t,8)))
foot=foot.cut(cyl(2.5,t+.1,x,yc-.05,zface-12,V(0,1,0)))
return side.fuse(foot).removeSplitter()
# Same purchased bracket at connections between each motor 2020 and 3030.
for i,sgn in enumerate((-1,1)):
old=d.getObject('MotorBaseCorner%d'%i); old.ViewObject.Visibility=False
add(d,'NewMotorBase%d'%i,l_yz(sgn*xc,-55,0,-1,-1),'20x20x16x1.8 NEW M4 concept',(.12,.42,.15))
# Switch cross-beam connections. Both legs assumed the same end geometry.
for i,sgn in enumerate((-1,1)):
old=d.getObject('SwitchCrossJoint%d'%i);old.ViewObject.Visibility=False
add(d,'NewSwitchBase%d'%i,l_yz(sgn*xc,-55,-122,1,1),'20x20x16x1.8 NEW M4 concept',(.12,.42,.15))
# New small brackets on the four 70 mm cross-beam to 3030 joints. Slot and
# screw centre alignment checked separately: crossbeam and rail centre Y
# differ by 2 mm, larger than the available nominal M4/Ø5 clearance.
def l_xz(side, zface, y):
t=1.8; inner=side*(xc-15)
if side>0:
x0=inner-t; direction=-1
else:
x0=inner; direction=1
if direction>0:
face=box(inner,y-8,zface,12,16,t).fuse(cyl(8,t,inner+12,y,zface).common(box(inner+12,y-8,zface,8,16,t)))
face=face.cut(cyl(2.5,t+.1,inner+12,y,zface-.05))
else:
face=box(inner-12,y-8,zface,12,16,t).fuse(cyl(8,t,inner-12,y,zface).common(box(inner-20,y-8,zface,8,16,t)))
face=face.cut(cyl(2.5,t+.1,inner-12,y,zface-.05))
# Plane on inside of rail; legs extend from zface toward the 3030 slot.
vert=box(x0,y-8,zface, t,16,12).fuse(cyl(8,t,x0,y,zface+12,V(1,0,0)).common(box(x0,y-8,zface+12,t,16,8)))
vert=vert.cut(cyl(2.5,t+.1,x0-.05,y,zface+12,V(1,0,0)))
return face.fuse(vert).removeSplitter()
for i in range(2):
for j,side in enumerate((-1,1)):
d.getObject('BaseJoint%d%d'%(i,j)).ViewObject.Visibility=False
zface=-84 if i==0 else -179
# For the rear bearing make the angle face outward, mirror about zface.
sh=l_xz(side,zface,-70)
if i==1: sh=sh.mirror(V(0,0,zface),V(0,0,1))
add(d,'NewBeamBase%d%d'%(i,j),sh,'20x20x16x1.8: nominal screw-axis alignment',(.12,.42,.15))
# Four bearing and two switch long-slot brackets intentionally remain, per
# user clarification. Beam top and long-angle feet rise 2mm to align beam and
# longitudinal 3030 groove centre lines; plate, shaft, and motor stay fixed.
new_joint_checks=[]
for i in range(2):
for j in range(2):
n='NewBeamBase%d%d'%(i,j);a=d.getObject(n)
new_joint_checks.append({'name':n,'cross':cc(a.Shape,d.getObject('Crossbeam%d'%i).Shape),'base':cc(a.Shape,d.getObject('NewRail%d'%j).Shape)})
d.recompute();d.saveAs(str(out/'Assembly_对称角码装配_概念.FCStd'))
selected=[o for o in d.Objects if hasattr(o,'Shape') and not o.Shape.isNull() and o.ViewObject.Visibility]
Import.export(selected,str(out/'Assembly_对称角码装配_概念.step'))
view=G.activeDocument().activeView();G.activateWorkbench('PartWorkbench');view.viewAxonometric();G.updateGui();view.fitAll();time.sleep(.5);G.updateGui();view.saveImage(str(out/'对称角码_长角码保留_3D.png'),1800,1300,'White')
vis={o.Name:o.ViewObject.Visibility for o in d.Objects if hasattr(o,'Shape')}; show={'Motor','Holder','Magnet','PCB','Chip','EncoderPlateConcept','Rail0','Rail1','NewRail0','NewRail1','MotorBaseCorner0','MotorBaseCorner1'}|{o.Name for o in corners+washers+studs+screws}|{'DirectFan','FanBolt0','FanBolt1'}
for o in d.Objects:
if hasattr(o,'Shape'):o.ViewObject.Visibility=(o.Name in show and (not fanhits or not o.Name.startswith(('DirectFan','FanBolt'))))
view.viewTop();G.updateGui();view.fitAll();time.sleep(.5);G.updateGui();view.saveImage(str(out/'电机端_正视.png'),1800,1300,'White')
view.viewAxonometric();G.updateGui();view.fitAll();time.sleep(.5);G.updateGui();view.saveImage(str(out/'角码与垫片_3D.png'),1800,1300,'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('Rail0').Shape.isValid(); A.closeDocument(d.Name)
step=Part.read(str(out/'Assembly_对称角码装配_概念.step')); assert step.isValid()
info={'all_user_lengths_mm':{'3030_long':250,'3030_cross':70,'2020_motor':140,'2020_switch':130},'hole_centres_76':p,'corner_wall_outside_x':xwall,'rail_inside_x':xrail,'washer_thickness':2,'base_joint_checks':new_joint_checks,'rail_centre_x':xc,'motor_track_centres_mm':2*xc,'3030_inside_gap_mm':2*(xc-15),'70_cross_member_length_mm':70,'crossbar_intersections_mm3':conflicts,'angle_rotation_degrees':theta,'corner_checks':checks,'fan_hits':fanhits,'note':'All 12 small angles replaced with nominal 20x20x16x1.8; 6 long-slot angles retained; real T slots, screws and fan hubs unmodelled.'}
(out/'验证.json').write_text(json.dumps(info,ensure_ascii=False,indent=2),encoding='utf8');(out/'完成.txt').write_text('ok',encoding='utf8');os._exit(0)
except Exception:
(out/'错误.txt').write_text(traceback.format_exc(),encoding='utf8');os._exit(1)

View File

@@ -0,0 +1,45 @@
from pathlib import Path
import FreeCAD as A, Part, json, os, traceback
V=A.Vector
out=Path(r'C:\projects\wheel\models\symmetric_corner_18_concept')
try:
d=A.newDocument('SymmetricCorner');s=d.addObject('Spreadsheet::Sheet','Parameters');s.set('A1','Name');s.set('B1','Value')
for n,(name,val) in enumerate([('Leg','20 mm'),('Width','16 mm'),('Thick','1.8 mm'),('Hole','5 mm'),('EndDistance','8 mm')],2):
s.set('A%d'%n,name);s.set('B%d'%n,val);s.setAlias('B%d'%n,name)
def expr(o,k,v):o.setExpression(k,v)
def box(n,L,W,H,x,y,z):
o=d.addObject('Part::Box',n)
for k,v in [('Length',L),('Width',W),('Height',H),('Placement.Base.x',x),('Placement.Base.y',y),('Placement.Base.z',z)]:expr(o,k,v)
return o
def cyl(n,r,h,x,y,z,ax=False):
o=d.addObject('Part::Cylinder',n)
for k,v in [('Radius',r),('Height',h),('Placement.Base.x',x),('Placement.Base.y',y),('Placement.Base.z',z)]:expr(o,k,v)
if ax:o.Placement.Rotation=A.Rotation(V(0,0,1),V(1,0,0))
return o
r='Parameters.Width/2';v='Parameters.Leg-Parameters.Width/2';xx='-(Parameters.Leg-Parameters.Thick-Parameters.Width/2)';end='-(Parameters.Leg-Parameters.Thick)'
a=box('FlatA',v,'Parameters.Width','Parameters.Thick',xx,'-Parameters.Width/2','0 mm')
ca=cyl('RoundA',r,'Parameters.Thick',xx,'0 mm','0 mm')
limA=box('LimitA',r,'Parameters.Width','Parameters.Thick',end,'-Parameters.Width/2','0 mm')
capA=d.addObject('Part::Common','EndA');capA.Base=ca;capA.Tool=limA
b=box('FlatB','Parameters.Thick','Parameters.Width',v,'0 mm','-Parameters.Width/2','0 mm')
cb=cyl('RoundB',r,'Parameters.Thick','0 mm','0 mm',v,True)
limB=box('LimitB','Parameters.Thick','Parameters.Width',r,'0 mm','-Parameters.Width/2',v)
capB=d.addObject('Part::Common','EndB');capB.Base=cb;capB.Tool=limB
body=d.addObject('Part::MultiFuse','Blank');body.Shapes=[a,capA,b,capB]
h1=cyl('HoleA','Parameters.Hole/2','Parameters.Thick+0.2 mm','-(Parameters.Leg-Parameters.Thick-Parameters.EndDistance)','0 mm','-0.1 mm')
h2=cyl('HoleB','Parameters.Hole/2','Parameters.Thick+0.2 mm','-0.1 mm','0 mm','Parameters.Leg-Parameters.EndDistance',True)
hh=d.addObject('Part::MultiFuse','Holes');hh.Shapes=[h1,h2]
result=d.addObject('Part::Cut','SymmetricCorner');result.Base=body;result.Tool=hh;result.Label='20×20×16 symmetrical angle — approximate bend'
d.recompute();sh=result.Shape;assert sh.isValid() and len(sh.Solids)==1
for o in d.Objects:
if hasattr(o,'ViewObject') and o.ViewObject:o.ViewObject.Visibility=False
result.ViewObject.Visibility=True
d.saveAs(str(out/'新角码_对称参数模型.FCStd'));Part.export([result],str(out/'新角码_对称参数模型.step'))
original=A.openDocument(str(out/'Assembly_对称角码装配_概念.FCStd')); original.recompute();shape=original.getObject('NewMotorCorner10').Shape.copy()
p=38/(2**.5);shape.translate(V(-(p+10.2),-(10-p),0))
diff1=shape.cut(sh).Volume;diff2=sh.cut(shape).Volume;assert diff1<1e-4 and diff2<1e-4,(diff1,diff2)
checks={'valid':True,'solids':len(sh.Solids),'model_to_assembly_mm3':diff1,'assembly_to_model_mm3':diff2,'reference':'20 external, 8 from free ends, 1.8 thick, 10.2 to sharp-corner inner surface; bend radius and countersink omitted'}
(out/'角码参数模型校验.json').write_text(json.dumps(checks,ensure_ascii=False,indent=2),encoding='utf8')
A.closeDocument(original.Name);A.closeDocument(d.Name);os._exit(0)
except Exception:
(out/'角码参数模型_错误.txt').write_text(traceback.format_exc(),encoding='utf8');os._exit(1)

View File

@@ -0,0 +1,77 @@
from pathlib import Path
import math, time, traceback
import FreeCAD as A, FreeCADGui as G, Part, Import
V=A.Vector
R=Path(r'C:\projects\wheel'); O=R/'models'/'turn_signal_cam_v2'; O.mkdir(parents=True,exist_ok=True)
def add(d,n,label,s,c):
o=d.addObject('PartDesign::Feature',n);o.Label=label;o.Shape=s;o.ViewObject.ShapeColor=c;o.ViewObject.LineColor=(.12,.12,.12);return o
def cy(r,h,p,v=V(0,0,1)):return Part.makeCylinder(r,h,p,v)
def limitrad(s):
vertices,_=s.tessellate(.05)
return max((math.hypot(p.x,p.y) for p in vertices),default=0)
def hit(a,b):
return a.common(b).Volume if a.BoundBox.intersect(b.BoundBox) else 0
try:
d=A.newDocument('TurnSignalCamV2')
# Shaft, with local middle key pocket only. End shaft interfaces are NOT modeled.
shaft=cy(6,100,V(0,0,-50)).cut(Part.makeBox(4.1,3,14,V(-2.05,4,-7)))
key=Part.makeBox(4,4,12,V(-2,4,-6))
# Two split semicircular pieces with 0.4 mm total clamping gap.
ring=cy(15.3,20,V(0,0,-10)).cut(cy(6.1,20.04,V(0,0,-10.02)))
upper=ring.common(Part.makeBox(40,20,20,V(-20,.2,-10)))
lower=ring.common(Part.makeBox(40,20,20,V(-20,-20.2,-10)))
# Square-ended ears, overlap semicircle but fit the Ø34 rotating envelope.
for x in (-14.7,8.3):
upper=upper.fuse(Part.makeBox(6.4,7.8,20,V(x,.2,-10)))
lower=lower.fuse(Part.makeBox(6.4,7.8,20,V(x,-8,-10)))
# One radial cam tab, fused; 4x21.3 nominal corner yields Ø43.35 swept.
upper=upper.fuse(Part.makeBox(8,7.2,20,V(-4,14.2,-10)))
# shaft bore again after fusing + flat-bottom key slot in upper half
for name in ('upper','lower'):
s=locals()[name].cut(cy(6.1,20.04,V(0,0,-10.02)))
if name=='upper': upper=s.cut(Part.makeBox(4.2,4.3,20.04,V(-2.1,4.0,-10.02)))
else: lower=s
# Two M3 screws across split; cylindrical recesses open at y side.
for x in (-11,11):
hole=cy(1.7,16.4,V(x,-8.2,0),V(0,1,0))
recessp=cy(3.3,3.02,V(x,5,0),V(0,1,0))
recessn=cy(3.3,3.02,V(x,-8.02,0),V(0,1,0))
upper=upper.cut(hole).cut(recessp)
lower=lower.cut(hole).cut(recessn)
upper=upper.removeSplitter();lower=lower.removeSplitter()
# approximate M3 x 12 screw shaft and 5.5 x 3 head, M3 hex nut.
hardware=[]
for x in (-11,11):
shank=cy(1.5,12,V(x,-7,0),V(0,1,0));head=cy(2.75,3,V(x,5,0),V(0,1,0)); screw=shank.fuse(head).removeSplitter()
# hex, across flats = 5.5, axis y, plane x/z
rad=5.5/math.sqrt(3);pts=[V(x+rad*math.cos(i*math.pi/3),-7.4,rad*math.sin(i*math.pi/3)) for i in range(6)];pts.append(pts[0]); nut=Part.Face(Part.makePolygon(pts)).extrude(V(0,2.4,0)).cut(cy(1.55,2.42,V(x,-7.41,0),V(0,1,0)))
hardware.extend([screw,nut])
for name,s in [('upper',upper),('lower',lower),('shaft',shaft),('key',key)]+[('hardware',s) for s in hardware]:
if s.isNull() or not s.isValid() or len(s.Solids)!=1:raise RuntimeError(name+' invalid')
res={}
for n,s in [('upper',upper),('lower',lower)]+[('hardware%d'%i,s) for i,s in enumerate(hardware)]:res[n]=limitrad(s)
checks=[('upper-lower',hit(upper,lower)),('upper-shaft',hit(upper,shaft)),('lower-shaft',hit(lower,shaft)),('key-shaft',hit(key,shaft)),('key-upper',hit(key,upper)),('key-lower',hit(key,lower))]
for i,s in enumerate(hardware):
checks.extend([('hardware%d-upper'%i,hit(s,upper)),('hardware%d-lower'%i,hit(s,lower)),('hardware%d-shaft'%i,hit(s,shaft))])
if any(v>.001 for _,v in checks):raise RuntimeError('intersections '+str(checks))
if max(res.values())>22.0:raise RuntimeError('whole sweep >44')
# cam excluded to check clamping envelope: split top excluding y>15.3 and z 20.
clamp=upper.cut(Part.makeBox(50,25,22,V(-25,15.3,-11))).fuse(lower)
if max([limitrad(clamp)]+[limitrad(s) for s in hardware])>17:raise RuntimeError('clamp sweep >34')
shafto=add(d,'Shaft','12 x 100 shaft; local mid key slot, end features omitted',shaft,(.55,.58,.62))
ko=add(d,'Key','metal key 4 x 4 x 12 assumed',key,(.95,.68,.15))
uo=add(d,'Upper','upper half and integrated tab',upper,(.1,.39,.78))
lo=add(d,'Lower','lower half',lower,(.2,.61,.88))
hs=[]
for i,s in enumerate(hardware):hs.append(add(d,'Hardware%d'%i,'M3 schematic fastener %d'%i,s,(.18,.19,.22)))
d.recompute();d.saveAs(str(O/'Assembly.FCStd'));Import.export([shafto,ko,uo,lo]+hs,str(O/'Assembly.step'))
report=['PRELIMINARY concept only, do not print.','Shaft: 12x100; middle keyway only, shaft end details omitted.', 'Body axial overall 20; cam corner radius %.4f; total swept diameter %.4f'%(limitrad(upper),2*max(res.values())), 'Clamping envelope including screw/nut and ears %.4f'%(2*max([limitrad(clamp)]+[limitrad(s) for s in hardware])), 'key: 4 x 4 x 12 hypothetical; local keyway depth shaft 2mm and hub 2.3mm, width 4.2mm.', 'clamp gap 0.4mm, bore 12.2mm nominal; M3 clearance dia 3.4; cylindrical 6.6mm relief (nut captured only visually, nut rotation restraint not modeled).', 'Calculated positive-volume intersections (mm3):']+[f'{n} {v:.8f}' for n,v in checks]+['Fastener hex socket and actual switch/cam clearance unmodeled; proposed material, deflection, load and tooling clearance not validated.', '100mm shaft position is schematic, module, both bearings, motor connection and wheel are unmodeled; fit cannot be inferred.']
(O/'geometry_check.txt').write_text('\n'.join(report)+'\n',encoding='utf8')
view=G.activeDocument().activeView();G.activateWorkbench('PartWorkbench');view.viewAxonometric();G.updateGui();time.sleep(.4);view.fitAll();G.updateGui();time.sleep(.4);view.saveImage(str(O/'full.png'),1600,1200,'White')
# close view: temporarily hide shaft ends via a short local reference representation
shafto.ViewObject.Visibility=False
shaftshort=add(d,'ShortShaft','short shaft visual crop',shaft.common(Part.makeBox(24,24,36,V(-12,-12,-18))),(.55,.58,.62)); d.recompute();view.viewAxonometric();G.updateGui();time.sleep(.3);view.fitAll();G.updateGui();time.sleep(.4);view.saveImage(str(O/'detail.png'),1600,1200,'White')
shaftshort.ViewObject.Visibility=False;shafto.ViewObject.Visibility=True;d.recompute();d.saveAs(str(O/'Assembly.FCStd'))
(O/'DONE.txt').write_text('ok\n',encoding='utf8')
except Exception:
(O/'ERROR.txt').write_text(traceback.format_exc(),encoding='utf8');raise

View File

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

View File

@@ -0,0 +1,68 @@
"""Make one solid two-thickness panel containing 16 cut-out C washers.
Run using FreeCAD's bundled Python. Cutting guide is documentation only;
the resulting STL deliberately contains no artificial connecting bars.
"""
from pathlib import Path
import json, math
import FreeCAD as App
import Part, Mesh, MeshPart
ROOT = Path(r'C:\projects\wheel')
OUT = ROOT / 'models' / 'washer_panel_4x2'
OUT.mkdir(parents=True, exist_ok=True)
V = App.Vector
doc = App.newDocument('WasherPanel4x2')
sheet = doc.addObject('Spreadsheet::Sheet', 'Parameters')
sheet.set('A1', 'Parameter'); sheet.set('B1', 'Value'); sheet.set('C1', 'Notes')
for row, name, value, desc in [(2, 'Thin', '0.5 mm', 'Thickness row 1'), (3, 'Thick', '1 mm', 'Thickness row 2')]:
sheet.set('A%d'%row,name); sheet.set('B%d'%row,value); sheet.set('C%d'%row,desc); sheet.setAlias('B%d'%row,name)
blanks=[]
for name, y, p in [('ThinHalf',0,'Parameters.Thin'),('ThickHalf',12,'Parameters.Thick')]:
b=doc.addObject('Part::Box',name); b.Length=48; b.Width=12; b.Placement.Base=V(0,y,0); b.setExpression('Height',p); blanks.append(b)
blank=doc.addObject('Part::MultiFuse','PanelBlank'); blank.Shapes=blanks
cutters=[]
for row in range(2):
for col in range(4):
x,y=12*col+6,12*row+6
circle=doc.addObject('Part::Cylinder',f'Hole{row}{col}'); circle.Radius=2.2; circle.Height=3; circle.Placement.Base=V(x,y,-1); cutters.append(circle)
# Original 90-degree sector after rotating its 22.5-degree centre to 45 degrees.
# NE quadrant within this cell. The cutter is bounded by the cell's
# top and right edges so the cut exposes the notch after separation.
opening=doc.addObject('Part::Box',f'Opening{row}{col}')
opening.Length=6; opening.Width=6; opening.Height=3; opening.Placement.Base=V(x,y,-1); cutters.append(opening)
allcuts=doc.addObject('Part::MultiFuse','HoleAndNotchCutters'); allcuts.Shapes=cutters
panel=doc.addObject('Part::Cut','WasherPanel'); panel.Base=blank; panel.Tool=allcuts
panel.Label='One-piece 4 x 2 washer panel - cut to separate'
doc.recompute()
s=panel.Shape
assert not s.isNull() and s.isValid() and len(s.Solids)==1, (s.isValid(),len(s.Solids))
baseline=s.Volume
for row in range(2):
for col in range(4):
t=.5 if row<1 else 1
cell=s.common(Part.makeBox(12,12,2,V(12*col,12*row,0)))
assert cell.isValid() and len(cell.Solids)==1 and abs(cell.BoundBox.ZLength-t)<1e-6
# At the centre and within the quadrant, the bore and open sector are clear.
for xx,yy in [(0,0),(1,0),(.3,.3),(3,3),(5.9,5.9)]:
assert not cell.isInside(V(12*col+6+xx,12*row+6+yy,t/2),1e-7,False),(row,col,xx,yy)
# Outside the original round bore and notch, the square retained material.
assert cell.isInside(V(12*col+2,12*row+2,t/2),1e-7,False)
for o in doc.Objects:
if hasattr(o,'ViewObject') and o.ViewObject: o.ViewObject.Visibility=False
if panel.ViewObject: panel.ViewObject.Visibility=True
doc.recompute()
fc=OUT/'一体式4x2双厚垫片.FCStd'; step=OUT/'一体式4x2双厚垫片.step'; stl=OUT/'一体式4x2双厚垫片.stl'
doc.saveAs(str(fc)); Part.export([panel],str(step))
MeshPart.meshFromShape(Shape=s,LinearDeflection=.025,AngularDeflection=.15,Relative=False).write(str(stl))
sheet.set('B2','0.6 mm'); doc.recompute(); test=panel.Shape.Volume
assert panel.Shape.isValid() and test>baseline
sheet.set('B2','0.5 mm');doc.recompute();assert abs(panel.Shape.Volume-baseline)<1e-6
App.closeDocument(doc.Name)
dd=App.openDocument(str(fc));dd.recompute();f=dd.getObject('WasherPanel').Shape; ss=Part.read(str(step)); mesh=Mesh.Mesh(str(stl)); components=mesh.getSeparateComponents()
assert f.isValid() and ss.isValid() and len(f.Solids)==len(ss.Solids)==1 and len(components)==1 and components[0].isSolid()
assert f.cut(ss).Volume<1e-5 and ss.cut(f).Volume<1e-5
data={'sheet_size_mm':[48,24], 'cell_mm':[12,12], 'diameter_mm':4.4,'opening_sector_degrees':90,'counts':[4,4], 'thicknesses_mm':[.5,1], 'number_of_solids':len(ss.Solids), 'stl_components':len(components), 'stl_is_solid':components[0].isSolid(), 'part_is_valid':ss.isValid(),'fcstd_reopen':True, 'step_minus_fcstd_volume':ss.cut(f).Volume,'fcstd_minus_step_volume':f.cut(ss).Volume,'original_opening_direction':'northeast, rotate after separation to match each pillar', 'cut_x_mm':[12,24,36], 'cut_y_mm':[12], 'cut_line_is_physical_feature':False,'thickness_parameter_recompute_test':True,'nominal_kerf_mm':0,'nominal_volume_mm3':baseline}
(OUT/'几何验证.json').write_text(json.dumps(data,ensure_ascii=False,indent=2),encoding='utf-8')
print(json.dumps(data,ensure_ascii=False,indent=2))

View File

@@ -0,0 +1,67 @@
"""Make one solid two-thickness panel containing 16 cut-out C washers.
Run using FreeCAD's bundled Python. Cutting guide is documentation only;
the resulting STL deliberately contains no artificial connecting bars.
"""
from pathlib import Path
import json, math
import FreeCAD as App
import Part, Mesh, MeshPart
ROOT = Path(r'C:\projects\wheel')
OUT = ROOT / 'models' / 'washer_panel_4x4'
OUT.mkdir(parents=True, exist_ok=True)
V = App.Vector
doc = App.newDocument('WasherPanel4x4')
sheet = doc.addObject('Spreadsheet::Sheet', 'Parameters')
sheet.set('A1', 'Parameter'); sheet.set('B1', 'Value'); sheet.set('C1', 'Notes')
for row, name, value, desc in [(2, 'Thin', '0.5 mm', 'Thickness rows 1-2'), (3, 'Thick', '1 mm', 'Thickness rows 3-4')]:
sheet.set('A%d'%row,name); sheet.set('B%d'%row,value); sheet.set('C%d'%row,desc); sheet.setAlias('B%d'%row,name)
blanks=[]
for name, y, p in [('ThinHalf',0,'Parameters.Thin'),('ThickHalf',24,'Parameters.Thick')]:
b=doc.addObject('Part::Box',name); b.Length=48; b.Width=24; b.Placement.Base=V(0,y,0); b.setExpression('Height',p); blanks.append(b)
blank=doc.addObject('Part::MultiFuse','PanelBlank'); blank.Shapes=blanks
cutters=[]
for row in range(4):
for col in range(4):
x,y=12*col+6,12*row+6
circle=doc.addObject('Part::Cylinder',f'Hole{row}{col}'); circle.Radius=2.2; circle.Height=3; circle.Placement.Base=V(x,y,-1); cutters.append(circle)
# Original 90-degree sector after rotating its 22.5-degree centre to 45 degrees.
# NE quadrant within this cell. The cutter is bounded by the cell's
# top and right edges so the cut exposes the notch after separation.
opening=doc.addObject('Part::Box',f'Opening{row}{col}')
opening.Length=6; opening.Width=6; opening.Height=3; opening.Placement.Base=V(x,y,-1); cutters.append(opening)
allcuts=doc.addObject('Part::MultiFuse','HoleAndNotchCutters'); allcuts.Shapes=cutters
panel=doc.addObject('Part::Cut','WasherPanel'); panel.Base=blank; panel.Tool=allcuts
panel.Label='One-piece 4 x 4 washer panel - cut to separate'
doc.recompute()
s=panel.Shape
assert not s.isNull() and s.isValid() and len(s.Solids)==1, (s.isValid(),len(s.Solids))
baseline=s.Volume
for row in range(4):
for col in range(4):
t=.5 if row<2 else 1
cell=s.common(Part.makeBox(12,12,2,V(12*col,12*row,0)))
assert cell.isValid() and len(cell.Solids)==1 and abs(cell.BoundBox.ZLength-t)<1e-6
# At the centre and within the quadrant, the bore and open sector are clear.
for xx,yy in [(0,0),(1,0),(.3,.3),(3,3),(5.9,5.9)]:
assert not cell.isInside(V(12*col+6+xx,12*row+6+yy,t/2),1e-7,False),(row,col,xx,yy)
# Outside the original round bore and notch, the square retained material.
assert cell.isInside(V(12*col+2,12*row+2,t/2),1e-7,False)
for o in doc.Objects:
if hasattr(o,'ViewObject') and o.ViewObject: o.ViewObject.Visibility=False
if panel.ViewObject: panel.ViewObject.Visibility=True
doc.recompute()
fc=OUT/'一体式4x4双厚垫片.FCStd'; step=OUT/'一体式4x4双厚垫片.step'; stl=OUT/'一体式4x4双厚垫片.stl'
doc.saveAs(str(fc)); Part.export([panel],str(step))
MeshPart.meshFromShape(Shape=s,LinearDeflection=.025,AngularDeflection=.15,Relative=False).write(str(stl))
sheet.set('B2','0.6 mm'); doc.recompute(); test=panel.Shape.Volume
assert panel.Shape.isValid() and test>baseline
sheet.set('B2','0.5 mm');doc.recompute();assert abs(panel.Shape.Volume-baseline)<1e-6
App.closeDocument(doc.Name)
dd=App.openDocument(str(fc));dd.recompute();f=dd.getObject('WasherPanel').Shape; ss=Part.read(str(step)); mesh=Mesh.Mesh(str(stl)); components=mesh.getSeparateComponents()
assert f.isValid() and ss.isValid() and len(f.Solids)==len(ss.Solids)==1 and len(components)==1 and components[0].isSolid()
assert f.cut(ss).Volume<1e-5 and ss.cut(f).Volume<1e-5
data={'sheet_size_mm':[48,48], 'cell_mm':[12,12], 'diameter_mm':4.4,'opening_sector_degrees':90,'counts':[8,8], 'thicknesses_mm':[.5,1], 'number_of_solids':len(ss.Solids), 'stl_components':len(components), 'stl_is_solid':components[0].isSolid(), 'part_is_valid':ss.isValid(),'fcstd_reopen':True, 'step_minus_fcstd_volume':ss.cut(f).Volume,'fcstd_minus_step_volume':f.cut(ss).Volume,'original_opening_direction':'northeast, rotate after separation to match each pillar', 'cut_boundaries_mm':[12,24,36], 'cut_line_is_physical_feature':False,'thickness_parameter_recompute_test':True,'nominal_kerf_mm':0,'nominal_volume_mm3':baseline}
(OUT/'几何验证.json').write_text(json.dumps(data,ensure_ascii=False,indent=2),encoding='utf-8')
print(json.dumps(data,ensure_ascii=False,indent=2))

View File

@@ -0,0 +1,49 @@
from pathlib import Path
import json
import FreeCAD as A, Part, Mesh, MeshPart
root=Path(r'C:\projects\wheel')
out=root/'models'/'washer_two_panels'
out.mkdir(parents=True,exist_ok=True)
V=A.Vector
reference=Part.read(str(root/'models'/'washer_panel_4x4'/'一体式4x4双厚垫片.step'))
results={}
for tag,thickness,orig_y in [('0.5mm',0.5,0),('1mm',1.0,24)]:
d=A.newDocument('Panel'+tag.replace('.',''))
params=d.addObject('Spreadsheet::Sheet','Parameters')
params.set('A1','Thickness'); params.set('B1',str(thickness)+' mm'); params.setAlias('B1','Thickness')
blank=d.addObject('Part::Box','Blank'); blank.Length=48; blank.Width=24; blank.setExpression('Height','Parameters.Thickness')
tools=[]
for row in range(2):
for col in range(4):
x,y=12*col+6,12*row+6
c=d.addObject('Part::Cylinder','Hole%d%d'%(row,col)); c.Radius=2.2; c.Height=3; c.Placement.Base=V(x,y,-1);tools.append(c)
p=d.addObject('Part::Box','Opening%d%d'%(row,col));p.Length=6;p.Width=6;p.Height=3;p.Placement.Base=V(x,y,-1);tools.append(p)
union=d.addObject('Part::MultiFuse','Cutters');union.Shapes=tools
panel=d.addObject('Part::Cut','Panel');panel.Base=blank;panel.Tool=union
panel.Label='4x2 continuous washer panel '+tag
d.recompute();s=panel.Shape
assert s.isValid() and len(s.Solids)==1
cells=[]
for row in range(2):
for col in range(4):
cell=s.common(Part.makeBox(12,12,2,V(12*col,12*row,0)))
old=reference.common(Part.makeBox(12,12,2,V(12*col,orig_y+12*row,0)))
old.translate(V(0,-orig_y,0))
diff=(cell.cut(old).Volume,old.cut(cell).Volume)
assert cell.isValid() and len(cell.Solids)==1 and max(diff)<1e-6 and abs(cell.BoundBox.ZLength-thickness)<1e-7
cells.append(diff)
filename='一体垫片板_'+tag
fc=out/(filename+'.FCStd'); step=out/(filename+'.step');stl=out/(filename+'.stl')
d.saveAs(str(fc));Part.export([panel],str(step));MeshPart.meshFromShape(Shape=s,LinearDeflection=.025,AngularDeflection=.15,Relative=False).write(str(stl))
A.closeDocument(d.Name)
reopened=A.openDocument(str(fc)); reopened.recompute();f=reopened.getObject('Panel').Shape
ss=Part.read(str(step));m=Mesh.Mesh(str(stl));components=m.getSeparateComponents()
diff=(f.cut(ss).Volume,ss.cut(f).Volume)
assert max(diff)<1e-6 and len(ss.Solids)==1 and ss.isValid() and len(components)==1 and components[0].isSolid()
bb=ss.BoundBox
assert abs(bb.XLength-48)<1e-6 and abs(bb.YLength-24)<1e-6 and abs(bb.ZLength-thickness)<1e-6
results[tag]={'size_mm':[48,24,thickness],'quantity':8,'cells_diff_mm3':cells,'stl_components':len(components),'stl_closed':components[0].isSolid(),'step_fcstd_diff_mm3':diff,'step_valid':ss.isValid()}
A.closeDocument(reopened.Name)
(out/'几何验证.json').write_text(json.dumps(results,ensure_ascii=False,indent=2),encoding='utf8')
print(json.dumps(results,ensure_ascii=False,indent=2))

View File

@@ -0,0 +1,87 @@
from pathlib import Path
root = Path(r'C:\projects\wheel')
source = (root / 'scripts/build_new_motor_corners_2mm_concept.FCMacro').read_text(encoding='utf-8')
source = source.replace("out=root/'models'/'new_motor_corners_2mm_concept'", "out=root/'models'/'all_new_corner_18_concept'")
source = source.replace("p=38/math.sqrt(2); xinner=p+9.5; xwall=xinner+2; xrail=xwall+2; xc=xrail+10", "p=38/math.sqrt(2); xinner=p+10.5; xwall=xinner+1.8; xrail=xwall+2; xc=xrail+10")
source = source.replace("flat=box(xinner-10,y-8,0,12,16,2)", "flat=box(xinner-10.2,y-8,0,12,16,1.8)")
source = source.replace("cap=cyl(8,2,xinner-10,y,0).common(box(xinner-18,y-8,0,8,16,2))", "cap=cyl(8,1.8,xinner-10.2,y,0).common(box(xinner-18.2,y-8,0,8,16,1.8))")
source = source.replace("leg=flat.fuse(cap).cut(cyl(2.5,2.1,p,y,-.05))", "leg=flat.fuse(cap).cut(cyl(2.5,1.9,p,y,-.05))")
source = source.replace("wall=box(xinner,y-8,0,2,16,12)", "wall=box(xinner,y-8,0,1.8,16,12)")
source = source.replace("cap2=cyl(8,2,xinner,y,12,V(1,0,0)).common(box(xinner,y-8,12,2,16,8))", "cap2=cyl(8,1.8,xinner,y,12,V(1,0,0)).common(box(xinner,y-8,12,1.8,16,8))")
source = source.replace("wall=wall.fuse(cap2).cut(cyl(2.5,2.1,xinner-.05,y,11.5,V(1,0,0)))", "wall=wall.fuse(cap2).cut(cyl(2.5,1.9,xinner-.05,y,12,V(1,0,0)))")
source = source.replace("washer=cyl(4.5,2,xwall,y,11.5,V(1,0,0)).cut(cyl(2.2,2.1,xwall-.05,y,11.5,V(1,0,0)))", "washer=cyl(4.5,2,xwall,y,12,V(1,0,0)).cut(cyl(2.2,2.1,xwall-.05,y,12,V(1,0,0)))")
source = source.replace("20x20x16x2 Ø5", "20x20x16x1.8 Ø5")
source = source.replace("box(-35,-87,z,70,30,30)", "box(-35,-85,z,70,30,30)")
source = source.replace("if o:o.ViewObject.Visibility=True\n # An initial fan", "if o:\n o.ViewObject.Visibility=True\n if stem in ('PurchasedAngle','FootHead'):\n ss=o.Shape.copy();ss.translate(V(0,2,0));o.Placement=A.Placement();o.Shape=ss\n # An initial fan")
marker = " d.recompute();d.saveAs(str(out/'Assembly_2mm垫片装配.FCStd'))"
inject = r'''
# Replace every visible old bracket, including the two types of long brackets.
# New L shape made with round distal ends. Its holes are both Ø5 mm. We model
# only sharp-bend nominal geometry; countersink and tool access are unknown.
def l_yz(x, yc, zface, upwards_z, foot_direction):
# width X=16; foot contacts y=yc, upright contacts z=zface.
t=1.8
zz=zface if upwards_z>0 else zface-t
side=box(x-8,yc,zz,16,12,t).fuse(cyl(8,t,x,yc+12,zz).common(box(x-8,yc+12,zz,16,8,t)))
side=side.cut(cyl(2.5,t+.1,x,yc+12,zz-.05))
if foot_direction>0:
foot=box(x-8,yc,zface,16,t,12).fuse(cyl(8,t,x,yc,zface+12,V(0,1,0)).common(box(x-8,yc,zface+12,16,t,8)))
foot=foot.cut(cyl(2.5,t+.1,x,yc-.05,zface+12,V(0,1,0)))
else:
foot=box(x-8,yc,zface-12,16,t,12).fuse(cyl(8,t,x,yc,zface-12,V(0,1,0)).common(box(x-8,yc,zface-20,16,t,8)))
foot=foot.cut(cyl(2.5,t+.1,x,yc-.05,zface-12,V(0,1,0)))
return side.fuse(foot).removeSplitter()
# Same purchased bracket at connections between each motor 2020 and 3030.
for i,sgn in enumerate((-1,1)):
old=d.getObject('MotorBaseCorner%d'%i); old.ViewObject.Visibility=False
add(d,'NewMotorBase%d'%i,l_yz(sgn*xc,-55,0,-1,-1),'20x20x16x1.8 NEW M4 concept',(.12,.42,.15))
# Switch cross-beam connections. Both legs assumed the same end geometry.
for i,sgn in enumerate((-1,1)):
old=d.getObject('SwitchCrossJoint%d'%i);old.ViewObject.Visibility=False
add(d,'NewSwitchBase%d'%i,l_yz(sgn*xc,-55,-122,1,1),'20x20x16x1.8 NEW M4 concept',(.12,.42,.15))
# New small brackets on the four 70 mm cross-beam to 3030 joints. Slot and
# screw centre alignment checked separately: crossbeam and rail centre Y
# differ by 2 mm, larger than the available nominal M4/Ø5 clearance.
def l_xz(side, zface, y):
t=1.8; inner=side*(xc-15)
if side>0:
x0=inner-t; direction=-1
else:
x0=inner; direction=1
if direction>0:
face=box(inner,y-8,zface,12,16,t).fuse(cyl(8,t,inner+12,y,zface).common(box(inner+12,y-8,zface,8,16,t)))
face=face.cut(cyl(2.5,t+.1,inner+12,y,zface-.05))
else:
face=box(inner-12,y-8,zface,12,16,t).fuse(cyl(8,t,inner-12,y,zface).common(box(inner-20,y-8,zface,8,16,t)))
face=face.cut(cyl(2.5,t+.1,inner-12,y,zface-.05))
# Plane on inside of rail; legs extend from zface toward the 3030 slot.
vert=box(x0,y-8,zface, t,16,12).fuse(cyl(8,t,x0,y,zface+12,V(1,0,0)).common(box(x0,y-8,zface+12,t,16,8)))
vert=vert.cut(cyl(2.5,t+.1,x0-.05,y,zface+12,V(1,0,0)))
return face.fuse(vert).removeSplitter()
for i in range(2):
for j,side in enumerate((-1,1)):
d.getObject('BaseJoint%d%d'%(i,j)).ViewObject.Visibility=False
zface=-84 if i==0 else -179
# For the rear bearing make the angle face outward, mirror about zface.
sh=l_xz(side,zface,-70)
if i==1: sh=sh.mirror(V(0,0,zface),V(0,0,1))
add(d,'NewBeamBase%d%d'%(i,j),sh,'20x20x16x1.8: nominal screw-axis mismatch',(.12,.42,.15))
# Four bearing and two switch long-slot brackets intentionally remain, per
# user clarification. Beam top and long-angle feet rise 2mm to align beam and
# longitudinal 3030 groove centre lines; plate, shaft, and motor stay fixed.
new_joint_checks=[]
for i in range(2):
for j in range(2):
n='NewBeamBase%d%d'%(i,j);a=d.getObject(n)
new_joint_checks.append({'name':n,'cross':cc(a.Shape,d.getObject('Crossbeam%d'%i).Shape),'base':cc(a.Shape,d.getObject('NewRail%d'%j).Shape)})
'''
if source.count(marker)<1:
raise RuntimeError('marker mismatch')
source=source.replace(marker,inject+marker,1)
source=source.replace('Assembly_2mm垫片装配','Assembly_新小角码装配_概念')
source=source.replace('2mm方案_总装.png','新小角码_长角码保留_3D.png')
source=source.replace("'base_joint_checks':joint_checks", "'base_joint_checks':new_joint_checks")
source=source.replace('Concept assembly: 70mm crossbeams and original base joints restored; real T slots, screws and fan hubs unmodelled.', 'All 12 small angles replaced with nominal 20x20x16x1.8; 6 long-slot angles retained; real T slots, screws and fan hubs unmodelled.')
(root/'scripts/build_all_new_corner_18_concept.FCMacro').write_text(source,encoding='utf-8')
print('ready')

View File

@@ -0,0 +1,32 @@
from PIL import Image,ImageDraw,ImageFont,ImageChops
from pathlib import Path
root=Path(r'C:\projects\wheel\models\new_motor_corners_1mm_concept')
font='C:/Windows/Fonts/msyh.ttc'
def F(n):return ImageFont.truetype(font,n)
im=Image.new('RGB',(2000,1120),'white');d=ImageDraw.Draw(im)
d.text((55,25),'新电机角码与 1 mm 垫片',font=F(40),fill='#183455')
d.text((58,85),'角码直接贴电机;垫片夹在角码和 2020 型材之间',font=F(24),fill='#314e65')
d.text((60,145),'模型局部(红色为垫片)',font=F(26),fill='#172c43')
base=Image.open(root/'单侧角码垫片特写.png').convert('RGB');diff=ImageChops.difference(base,Image.new('RGB',base.size,'white'));b=diff.getbbox();base=base.crop(b);base.thumbnail((930,750));im.paste(base,(35,205))
d.text((1045,145),'接触层叠剖面(示意,非比例)',font=F(26),fill='#172c43')
# canvas positions: motor z=0 baseline at y=810, x+ to right, z+ upwards
d.rectangle((1050,810,1920,920),fill='#abb4bd',outline='#394b59',width=3)
d.text((1370,845),'电机背面',font=F(27),fill='#172c43')
# L foot contacts motor, leg extends vertically
d.polygon([(1180,790),(1480,790),(1480,345),(1515,345),(1515,810),(1180,810)],fill='#e39c3c',outline='#654a1e')
d.text((1190,738),'新角码',font=F(24),fill='#64400b')
# Rail with exact nominal corner-side face at x1545, visible spacer between x1515 and1545
d.rectangle((1545,335,1850,810),fill='#71899b',outline='#1d384c',width=3)
d.text((1630,490),'2020 型材',font=F(25),fill='white')
# washer represented as full y cross section red of thickness 1 mm
d.rectangle((1515,510,1545,610),fill='#d82f36',outline='#8c1824',width=2)
d.line((1370,560,1650,560),fill='#28303a',width=8)
d.rectangle((1370,540,1398,580),fill='#485b69')
d.text((1085,350),'1 mm 金属垫片',font=F(26),fill='#bd2029')
d.line([(1280,390),(1445,390),(1529,509)],fill='#bd2029',width=4)
d.polygon([(1529,509),(1515,491),(1535,496)],fill='#bd2029')
d.text((1155,690),'角码贴紧电机',font=F(22),fill='#61400d')
d.text((1550,670),'螺丝示意;槽和滑块未建模',font=F(18),fill='#fff')
d.text((55,1000),'每个电机角码的型材侧各 1 片,共 4 片;电机侧不放垫片。',font=F(25),fill='#173756')
d.text((55,1045),'此图用于表达安装方向,未确认实际角码弯曲半径、孔位及五金尺寸。',font=F(21),fill='#b04337')
im.save(root/'角码垫片位置_剖面示意.png')

View File

@@ -0,0 +1,33 @@
from PIL import Image,ImageDraw,ImageFont,ImageChops
from pathlib import Path
root=Path(r'C:\projects\wheel\models\new_motor_corners_2mm_concept')
font='C:/Windows/Fonts/msyh.ttc'
def F(n):return ImageFont.truetype(font,n)
im=Image.new('RGB',(2000,1120),'white');d=ImageDraw.Draw(im)
d.text((55,25),'新电机角码与 2 mm 垫片',font=F(40),fill='#183455')
d.text((58,85),'角码直接贴电机;垫片夹在角码和 2020 型材之间',font=F(24),fill='#314e65')
d.text((60,145),'模型局部(红色为垫片)',font=F(26),fill='#172c43')
base=Image.open(root/'单侧角码垫片特写.png').convert('RGB');diff=ImageChops.difference(base,Image.new('RGB',base.size,'white'));b=diff.getbbox();base=base.crop(b);base.thumbnail((930,750));im.paste(base,(35,205))
d.text((1045,145),'接触层叠剖面(示意,非比例)',font=F(26),fill='#172c43')
# canvas positions: motor z=0 baseline at y=810, x+ to right, z+ upwards
d.rectangle((1050,810,1920,920),fill='#abb4bd',outline='#394b59',width=3)
d.text((1370,845),'电机背面',font=F(27),fill='#172c43')
# L foot contacts motor, leg extends vertically
d.polygon([(1180,790),(1480,790),(1480,345),(1515,345),(1515,810),(1180,810)],fill='#e39c3c',outline='#654a1e')
d.text((1190,738),'新角码',font=F(24),fill='#64400b')
# Rail with exact nominal corner-side face at x1545, visible spacer between x1515 and1545
d.rectangle((1545,335,1850,810),fill='#71899b',outline='#1d384c',width=3)
d.text((1630,490),'2020 型材',font=F(25),fill='white')
# washer represented as full y cross section red of thickness 1 mm
d.rectangle((1515,510,1545,610),fill='#d82f36',outline='#8c1824',width=2)
d.line((1370,560,1650,560),fill='#28303a',width=8)
d.rectangle((1370,540,1398,580),fill='#485b69')
d.text((1085,350),'2 mm 金属垫片',font=F(26),fill='#bd2029')
d.line([(1280,390),(1445,390),(1529,509)],fill='#bd2029',width=4)
d.polygon([(1529,509),(1515,491),(1535,496)],fill='#bd2029')
d.text((1155,690),'角码贴紧电机',font=F(22),fill='#61400d')
d.text((1550,670),'螺丝示意;槽和滑块未建模',font=F(18),fill='#fff')
d.text((55,1000),'每个电机角码型材侧垫片总厚 2 mm,共 4 处;电机侧不放垫片。',font=F(25),fill='#173756')
d.text((55,1045),'此图用于表达安装方向,未确认实际角码弯曲半径、孔位及五金尺寸。',font=F(21),fill='#b04337')
im.save(root/'角码垫片位置_剖面示意.png')

View File

@@ -0,0 +1,14 @@
from PIL import Image, ImageDraw, ImageFont
from pathlib import Path
p=Path(r'C:\projects\wheel\models\switch_2020_crossbar132')
im=Image.new('RGB',(2000,1250),'white');d=ImageDraw.Draw(im)
font=ImageFont.truetype(r'C:\Windows\Fonts\msyh.ttc',42); sub=ImageFont.truetype(r'C:\Windows\Fonts\msyh.ttc',26)
d.text((35,20),'灯光开关 2020 横梁:132 mm 下料方案',font=font,fill='#162c44')
d.text((35,88),'横梁置于两根 3030 纵梁上方;两端各突出约 0.38 mm',font=sub,fill='#415a70')
def paste(source,rect):
pic=Image.open(p/source).convert('RGB');bg=Image.new('RGB',pic.size,'white');diff=__import__('PIL.ImageChops',fromlist=['difference']).difference(pic,bg);box=diff.getbbox();pic=pic.crop(box) if box else pic
x0,y0,x1,y1=rect;pic.thumbnail((x1-x0,y1-y0),Image.Resampling.LANCZOS);im.paste(pic,(x0+(x1-x0-pic.width)//2,y0+(y1-y0-pic.height)//2))
paste('assembly.png',(30,175,1000,1120));paste('crossbar_front.png',(1020,240,1980,1030))
d.text((90,130),'整体装配(转向灯为绿色示意包络)',font=sub,fill='#344d62');d.text((1110,180),'开关横梁正视图',font=sub,fill='#344d62')
d.text((80,1150),'注:灯光开关为示意包络;真实安装孔位置、槽型及滑块须在装配时核对。',font=sub,fill='#8a4141')
im.save(p/'132mm横梁_3D装配示意.png')

View File

@@ -0,0 +1,23 @@
from PIL import Image, ImageDraw, ImageFont, ImageChops
from pathlib import Path
R=Path(r'C:\projects\wheel\models\switch_2020_direct_profiles')
def font(s):return ImageFont.truetype(r'C:\Windows\Fonts\msyh.ttc',s)
def crop(name):
im=Image.open(R/name).convert('RGB');bg=Image.new('RGB',im.size,'white');m=ImageChops.difference(im,bg).convert('L').point(lambda p:255 if p>15 else 0);b=m.getbbox();return im.crop(b) if b else im
def paste(can,im,box):
im.thumbnail((box[2]-box[0],box[3]-box[1]));can.paste(im,(box[0]+(box[2]-box[0]-im.width)//2,box[1]+(box[3]-box[1]-im.height)//2))
out=Image.new('RGB',(1900,1400),'#f4f7fb');d=ImageDraw.Draw(out)
d.text((55,25),'型材直接接触:轴线仍上移 10 mm',font=font(40),fill='#1c2c40')
d.text((58,87),'取消蓝色垫块;电机及角码沿 2020 型材调整,2020 与 3030 直接贴合。',font=font(23),fill='#3c526b')
for x1,x2 in [(35,920),(950,1865)]:d.rounded_rectangle((x1,135,x2,915),radius=16,fill='white',outline='#d5dce5',width=2)
d.text((55,149),'完整装配示意',font=font(23),fill='#25384d');d.text((970,149),'电机与型材连接局部(红色为连接角码)',font=font(24),fill='#25384d')
paste(out,crop('assembly.png'),(55,190,900,890));paste(out,crop('motor_top.png'),(970,190,1845,885))
d.rounded_rectangle((35,945,1865,1360),radius=16,fill='white',outline='#d5dce5',width=2)
d.text((60,967),'取消垫块后,电机安装位置可沿型材调整',font=font(28),fill='#a12e1e')
lines=['两侧电机 2020 与底座 3030 接触;两只红色角码保持连接。',
'电机与四只橙色角码上移 10 mm;侧面圆孔对准 2020 中心槽线。',
'按当前模型,圆孔中心距 2020 端部至少 18.13 mm;实际型材槽未建模。',
'灯光开关为估计包络;角码进入包络,真实塑料外壳和接线干涉仍未确定。',
'未建立电机角码与型材之间的螺丝、滑块螺母;连接强度和实物装配未验证。']
for i,line in enumerate(lines):d.text((60,1020+60*i),line,font=font(22),fill='#31485e')
out.save(R/'型材直接接触_3D装配示意.png')

View File

@@ -0,0 +1,22 @@
from PIL import Image, ImageDraw, ImageFont, ImageChops
from pathlib import Path
p=Path(r'C:\projects\wheel\models\integer_profiles')
im=Image.new('RGB',(2000,1250),'white'); d=ImageDraw.Draw(im)
title=ImageFont.truetype(r'C:\Windows\Fonts\msyh.ttc',43)
font=ImageFont.truetype(r'C:\Windows\Fonts\msyh.ttc',28)
small=ImageFont.truetype(r'C:\Windows\Fonts\msyh.ttc',24)
d.text((35,20),'方向盘底座:型材整数下料尺寸',font=title,fill='#162c44')
d.text((35,90),'保留原电机与纵梁位置;轴承横梁通过角码连接纵梁。',font=font,fill='#415a70')
pic=Image.open(p/'assembly.png').convert('RGB'); bg=Image.new('RGB',pic.size,'white'); box=ImageChops.difference(pic,bg).getbbox()
if box:pic=pic.crop(box)
pic.thumbnail((950,970),Image.Resampling.LANCZOS);im.paste(pic,(35+(950-pic.width)//2,180+(970-pic.height)//2))
d.text((1030,190),'型材下料清单',font=title,fill='#162c44')
items=[('底部纵梁','3030','255 mm','2 根'),('轴承板横梁','3030','71 mm','2 根'),('电机两侧型材','2020','110 mm','2 根'),('开关下方横梁','2020','132 mm','1 根')]
for i,item in enumerate(items):
y=285+i*115
d.rectangle((1020,y,1940,y+83),fill=('#f1f5f8' if i%2==0 else '#fbfcfd'))
for x,t in zip((1040,1350,1540,1760),item):d.text((x,y+20),t,font=font,fill='#172c3f')
d.text((1030,810),'横梁两端保留细小装配间隙,角码连接。',font=font,fill='#344d62')
d.text((1030,870),'孔槽位置和轴承板沿用上一版。',font=font,fill='#344d62')
d.text((65,1170),'注:图中开关为示意包络,真实槽型、滑块和开关外形尚未完整建模。',font=small,fill='#8a4141')
im.save(p/'整数型材_3D装配示意.png')

View File

@@ -0,0 +1,27 @@
from PIL import Image, ImageChops, ImageDraw, ImageFont
from pathlib import Path
R=Path(r'C:\projects\wheel\models'); A=R/'kfl_100_concept'; B=R/'kfl_100_narrow_concept'
font=r'C:\Windows\Fonts\msyh.ttc'
f=ImageFont.truetype(font,42);small=ImageFont.truetype(font,25); tiny=ImageFont.truetype(font,22)
out=Image.new('RGB',(2100,1430),'white'); d=ImageDraw.Draw(out)
d.text((45,18),'100 mm 轴与反装 KFL001 · 3D 布局探索',font=f,fill='#182b45')
d.text((45,78),'两块 8 mm 铝板 / 开关 50 mm 包络 / Ø12 × 100 mm 轴 / 已有两块铝转接盘',font=small,fill='#536478')
d.rounded_rectangle((40,119,2060,174),radius=7,fill='#fff0eb')
d.text((55,129),'空间预算用尽:零间隙;前端 9 mm 插入深度是假设。不能据此认定可装配!',font=small,fill='#9c2a25')
def panel(path,box):
im=Image.open(path).convert('RGB').rotate(90,expand=True)
diff=ImageChops.difference(im,Image.new('RGB',im.size,'white')).convert('L').point(lambda p:255 if p>12 else 0)
bounds=diff.getbbox();im=im.crop(bounds) if bounds else im
im.thumbnail((box[2]-box[0],box[3]-box[1]));out.paste(im,(box[0]+(box[2]-box[0]-im.width)//2,box[1]))
d.text((60,194),'方案 A:假设轴毂外径 20 mm;红色为示意碰撞',font=small,fill='#223955')
panel(A/'side.png',(30,235,2070,650))
d.ellipse((945,370,1030,465),outline='#dc2929',width=5)
d.ellipse((1195,370,1280,465),outline='#dc2929',width=5)
d.text((60,670),'方案 B:假设缩至 18.4 mm;所检查的轴毂碰撞消失,轴向仍无余量',font=small,fill='#223955')
panel(B/'side.png',(30,710,2070,1120))
d.line((42,1150,2058,1150),fill='#d8e0e9',width=2)
d.text((45,1172),'绿色半透明体:开关 50 mm 包络占位,不是实物形状;中心孔 Ø47 mm 仅为示意。',font=small,fill='#3a6350')
d.text((45,1214),'KFL 中央凸起、法兰轮毂尺寸与轴端键槽均未实测;实际白色回正机构未建模。',font=small,fill='#47576b')
d.text((45,1256),'轴向示意:12 + 14.5 + 50 + 14.5 + 9 = 100 mm;20 mm 轴套已包含在开关包络内。',font=small,fill='#47576b')
d.text((45,1298),'缩小轮毂只是几何探索,未验证键槽强度、轴向间隙、装配工具或 100 mm 轴的可用性。',font=small,fill='#9c2a25')
out.save(A/'装配比较示意.png')

View File

@@ -0,0 +1,31 @@
from PIL import Image, ImageChops, ImageDraw, ImageFont
from pathlib import Path
out = Path(r'C:\projects\wheel\models\kfl_125_switch60_concept')
font = r'C:\Windows\Fonts\msyh.ttc'
title = ImageFont.truetype(font, 38)
normal = ImageFont.truetype(font, 24)
small = ImageFont.truetype(font, 21)
im = Image.new('RGB', (1920, 1200), 'white')
d = ImageDraw.Draw(im)
d.text((40, 18), '125 mm 轴 · 开关及接线预留 60 mm', fill='#1a304d', font=title)
d.text((40, 80), '轴 Ø12 × 125 mm;两端相同法兰;两块 8 mm 铝板,KFL001 反装', fill='#52657b', font=normal)
d.rounded_rectangle((40, 123, 1880, 177), radius=8, fill='#fff0eb')
d.text((55, 134), '概念示意:实际开关、轴承、接线和中心螺母未实测;不能认定实物可装', fill='#9b2b22', font=normal)
view = Image.open(out / 'side.png').convert('RGB').rotate(90, expand=True)
mask = ImageChops.difference(view, Image.new('RGB', view.size, 'white')).convert('L').point(lambda p:255 if p>12 else 0)
bounds = mask.getbbox()
if bounds:
view = view.crop(bounds)
view.thumbnail((1760, 550))
d.text((40, 196), '侧视:电机在左,方向盘侧在右', font=normal, fill='#243a55')
im.paste(view, (80 + (1760-view.width)//2, 230 + (550-view.height)//2))
d.line((40, 800, 1880, 800), fill='#d8e0e9', width=2)
d.text((42, 819), '绿色:开关及接线 60 mm 包络占位;灰色 KFL 和中心 Ø47 孔为假设。', font=small, fill='#405b4a')
d.text((42, 862), '电机侧不动;前轴承及前法兰前移 12 mm,轴和轴套前移 6 mm。', font=small, fill='#243a55')
d.text((42, 905), '示意轴承中心距 76.5 mm,开关包络到两侧内圈各预留 1 mm;法兰与轴承座约 0.5 mm。', font=small, fill='#243a55')
d.text((42, 948), '轴两端进入法兰各约 13.5 mm,轴端距法兰底盘内表面各 6.5 mm;键长与强度未知。', font=small, fill='#243a55')
d.text((42, 991), '开关真实外形、白色拨片和接线弯曲、M3 中心螺母、KFL 凸起及工具空间尚未验证。', font=small, fill='#9b2b22')
d.text((42, 1034), '60 mm 不是已验证的实物安全空间;以上间隙和尺寸仅限此假设模型。', font=small, fill='#9b2b22')
im.save(out / '125mm轴_60mm开关_装配示意.png')

View File

@@ -0,0 +1,34 @@
from PIL import Image, ImageChops, ImageDraw, ImageFont
from pathlib import Path
out = Path(r'C:\projects\wheel\models\kfl_150_switch60_concept')
font = r'C:\Windows\Fonts\msyh.ttc'
title = ImageFont.truetype(font, 38)
normal = ImageFont.truetype(font, 24)
small = ImageFont.truetype(font, 21)
im = Image.new('RGB', (1920, 1200), 'white')
d = ImageDraw.Draw(im)
d.text((40, 18), '150 mm 轴 · 开关及接线预留 60 mm', fill='#1a304d', font=title)
d.text((40, 80), '轴 Ø12 × 150 mm;两端相同法兰;两块 8 mm 铝板,KFL001 反装', fill='#52657b', font=normal)
d.rounded_rectangle((40, 123, 1880, 177), radius=8, fill='#fff0eb')
d.text((55, 134), '概念示意:实际开关、轴承、接线和中心螺母未实测;不能认定实物可装', fill='#9b2b22', font=normal)
view = Image.open(out / 'side.png').convert('RGB').rotate(90, expand=True)
mask = ImageChops.difference(view, Image.new('RGB', view.size, 'white')).convert('L').point(lambda p: 255 if p > 12 else 0)
bounds = mask.getbbox()
if bounds:
view = view.crop(bounds)
view.thumbnail((1760, 550))
d.text((40, 196), '侧视:电机在左,方向盘侧在右', font=normal, fill='#243a55')
im.paste(view, (80 + (1760-view.width)//2, 230 + (550-view.height)//2))
d.line((40, 800, 1880, 800), fill='#d8e0e9', width=2)
rows = [
('绿色:开关及接线 60 mm 包络占位;灰色 KFL 和中心 Ø47 孔为假设。', '#405b4a'),
('电机侧和后轴承不动;前轴承与前法兰前移 25 mm,轴和轴套中心前移 12.5 mm。', '#243a55'),
('轴承中心距约 101.5 mm;开关包络到两侧内圈各约 13.5 mm;法兰与轴承座约 0.5 mm。', '#243a55'),
('轴两端进入法兰各约 13.5 mm,距底盘内表面各 6.5 mm;两端键长与强度未知。', '#243a55'),
('下方两根 2020 型材示意延长 25 mm;长度和连接方式未经实物验证。', '#243a55'),
('开关真实外形、白色拨片、接线弯曲、中心 M3 螺母及 KFL 凸起均未验证。', '#9b2b22'),
]
for i, (message, color) in enumerate(rows):
d.text((42, 819 + i * 43), message, font=small, fill=color)
im.save(out / '150mm轴_60mm开关_装配示意.png')

View File

@@ -0,0 +1,22 @@
from PIL import Image,ImageDraw,ImageFont
from pathlib import Path
O=Path(r'C:\projects\wheel\models\kfl_3030_concept')
font=r'C:\Windows\Fonts\msyh.ttc'
def f(s): return ImageFont.truetype(font,s)
canvas=Image.new('RGB',(2400,1740),'#f4f6fa');d=ImageDraw.Draw(canvas)
d.text((70,35),'3030 底座 · 双横梁轴承支撑示意',font=f(50),fill='#18283c')
d.text((70,108),'两块 76 × 80 × 8 mm 板;每块板采用两只 50 × 31 × 19 × 3 mm 长孔角码',font=f(27),fill='#3e5065')
def panel(name,xy,wh,title):
x,y=xy;w,h=wh;d.rounded_rectangle((x,y,x+w,y+h),radius=18,fill='white',outline='#d9e0e9',width=2);d.text((x+20,y+15),title,font=f(28),fill='#20344c')
img=Image.open(O/name).convert('RGB');img.thumbnail((w-28,h-70));canvas.paste(img,(x+(w-img.width)//2,y+58))
panel('assembly.png',(45,165),(1130,1060),'总装:电机、两只轴承、开关空间与 150 mm 轴')
panel('frame.png',(1225,165),(1130,1060),'隐藏电机和开关,查看两根横梁与角码')
d.rounded_rectangle((45,1255,2355,1690),radius=18,fill='white',outline='#d9e0e9',width=2)
d.text((75,1275),'布置与检查范围',font=f(32),fill='#20344c')
rows=['① 底座纵梁、横梁均为 3030;两根横梁夹在纵梁之间并比纵梁低 2 mm,橙色为角码。',
'② 角码的 50 mm 边接板,31 mm 边接横梁;短槽位置按居中假设,实物需核对。',
'③ 灯光开关绿色部分是尺寸包络,未建真实插头、线材和拨杆;型材槽及 T 型螺母未画出。',
'④ 电机侧原有 2020 型材保留,它与底座 3030 的实际连接件未设计,需后续补充。',
'注:此图用于讨论,轴承、螺丝工具空间、型材槽及实际开关均须实物核对,不能直接加工。']
for i,s in enumerate(rows): d.text((75,1338+i*63),s,font=f(25),fill='#9b4939' if i>=3 else '#30445b')
canvas.save(O/'3030底座_双横梁装配示意.png')

View File

@@ -0,0 +1,31 @@
from PIL import Image, ImageChops, ImageDraw, ImageFont
from pathlib import Path
o=Path(r'C:\projects\wheel\models\kfl_compact80_concept')
font=r'C:\Windows\Fonts\msyh.ttc'
t=ImageFont.truetype(font,38); n=ImageFont.truetype(font,24); s=ImageFont.truetype(font,21)
im=Image.new('RGB',(1920,1300),'white');d=ImageDraw.Draw(im)
d.text((40,18),'紧凑轴承板与单根横梁 · 3D 概念装配',font=t,fill='#1a304d')
d.text((40,80),'两块 76 × 80 × 8 mm 铝板;一根横向 2020 加固两根纵梁',font=n,fill='#52657b')
d.rounded_rectangle((40,122,1880,176),radius=8,fill='#fff0eb')
d.text((55,133),'示意方案:加长连接片、轴承开孔、型材和五金均需实物核对,不是 CNC 定稿',font=n,fill='#9b2b22')
def prepare(file,w,h,rotate=0):
v=Image.open(o/file).convert('RGB')
if rotate:v=v.rotate(rotate,expand=True)
mask=ImageChops.difference(v,Image.new('RGB',v.size,'white')).convert('L').point(lambda x:255 if x>12 else 0)
bounds=mask.getbbox()
if bounds:v=v.crop(bounds)
v.thumbnail((w,h));return v
d.text((40,195),'完整总装(电机侧在左)',font=n,fill='#243a55')
d.text((1000,195),'两块板、连接片及一根横梁',font=n,fill='#243a55')
left=prepare('side.png',870,560,90);right=prepare('frame.png',870,560)
im.paste(left,(45+(870-left.width)//2,230+(560-left.height)//2));im.paste(right,(1000+(870-right.width)//2,230+(560-right.height)//2))
d.line((40,810,1880,810),fill='#d8e0e9',width=2)
rows=[
'灰色是 2020 型材及两块铝板;浅黄色为金属连接片;橙色为角码;绿色是开关包络。',
'单根横梁连接两根纵梁以加固框架;两块铝板通过四组角码与连接片分别固定在纵梁内侧。',
'铝板小于 80 × 80 mm;每侧连接片示意 18 × 40 × 3 mm,并使用 3 mm 金属垫柱。',
'两侧纵梁内侧与板边的名义间隙约 2.62 mm;螺栓、垫圈和板槽提供对中调整。',
'轴承孔 Ø39 和凸起 Ø38 为示意包络;开关、接线和轴承顶丝位置未完整建模。',
'横梁、连接件与旧2020的真实槽型、调角连接及强度需核对;型材长度仅为示意。']
for i,x in enumerate(rows):d.text((42,830+i*48),x,font=s,fill='#283f5c' if i<4 else '#8c392c')
im.save(o/'76x80_单横梁_3D示意.png')

View File

@@ -0,0 +1,23 @@
from PIL import Image,ImageDraw,ImageFont
from pathlib import Path
O=Path(r'C:\projects\wheel\models\kfl_double_crossbar_concept')
font=r'C:\Windows\Fonts\msyh.ttc'
def f(size):return ImageFont.truetype(font,size)
canvas=Image.new('RGB',(2400,1830),'#f4f6fa');d=ImageDraw.Draw(canvas)
d.text((70,35),'双横梁方案 · 轴承支撑总装示意',font=f(52),fill='#18283c')
d.text((73,112),'两块 76 × 80 × 8 mm 板;每块下方一根 2020 横梁,使用加长 L 角码直接连接',font=f(27),fill='#3e5065')
def panel(name,xy,wh,title):
x,y=xy;w,h=wh; d.rounded_rectangle((x,y,x+w,y+h),radius=18,fill='white',outline='#d9e0e9',width=2)
d.text((x+22,y+15),title,font=f(29),fill='#20344c')
img=Image.open(O/name).convert('RGB');img.thumbnail((w-28,h-70)); canvas.paste(img,(x+(w-img.width)//2,y+58))
panel('assembly.png',(45,175),(1130,1100),'整机:电机、两只 KFL、开关及 150 mm 轴')
panel('frame.png',(1225,175),(1130,1100),'去掉电机与开关:查看双横梁和角码')
d.rounded_rectangle((45,1305,2355,1765),radius=18,fill='white',outline='#d9e0e9',width=2)
d.text((75,1328),'固定与调整方式',font=f(32),fill='#20344c')
rows=['① 每块轴承板下配一根横向 2020;横梁两端用普通角码固定在两根纵梁外侧。',
'② 每块板用 2 只加长金属 L 角码直接连接横梁;橙色为角码,不需要额外平连接片。',
'③ 板上横向长孔 + 角码竖向长孔用于同轴微调;横梁沿纵梁槽移动调整前后位置。',
'④ 加长角码按 45 × 20 × 18 × 3 mm 假设,长槽 9.5 × 5.5 mm;实物规格及工具空间待核对。',
'注:型材为实体外形示意,槽、螺母、真实折弯半径及开关细节未定;此图不能直接用于加工。']
for i,s in enumerate(rows): d.text((75,1390+i*67),s,font=f(25),fill='#9b4939' if i==4 else '#30445b')
canvas.save(O/'双横梁_3D装配示意.png')

View File

@@ -0,0 +1,35 @@
from PIL import Image, ImageChops, ImageDraw, ImageFont
from pathlib import Path
root = Path(r'C:\projects\wheel\models')
a = root / 'kfl_100_flange57_concept'
b = root / 'kfl_100_flange57_shift_concept'
font = r'C:\Windows\Fonts\msyh.ttc'
title = ImageFont.truetype(font, 38)
normal = ImageFont.truetype(font, 23)
small = ImageFont.truetype(font, 21)
im = Image.new('RGB', (1920, 1300), 'white')
d = ImageDraw.Draw(im)
d.text((40, 20), '更新法兰尺寸:100 mm 轴装配空间检查', fill='#1a304d', font=title)
d.text((40, 82), '电机侧法兰:Ø57 底盘厚 4 mm,Ø30 凸台高 20 mm(从底盘表面量起)', fill='#52657b', font=normal)
d.rounded_rectangle((40, 125, 1880, 180), radius=8, fill='#fff0eb')
d.text((55, 137), '示意布局存在干涉或插入不足风险,不能认定 100 mm 轴可用', fill='#9b2b22', font=normal)
def panel(path, box):
view = Image.open(path).convert('RGB').rotate(90, expand=True)
mask = ImageChops.difference(view, Image.new('RGB', view.size, 'white')).convert('L').point(lambda p:255 if p>12 else 0)
bounds = mask.getbbox()
if bounds: view = view.crop(bounds)
view.thumbnail((box[2]-box[0],box[3]-box[1]))
im.paste(view, (box[0] + (box[2]-box[0]-view.width)//2, box[1]))
d.text((40, 200), 'A:沿用原位置,电机侧轴插入 18 mm;新凸台与后轴承示意外形相交', font=normal, fill='#243a55')
panel(a / 'side.png', (35, 240, 1885, 610))
d.text((40, 625), 'B:轴前移 6 mm、轴承和开关前移 10 mm;电机侧插入 12 mm,前端仅剩 5 mm', font=normal, fill='#243a55')
panel(b / 'side.png', (35, 665, 1885, 1035))
d.line((40, 1060, 1880, 1060), fill='#d8e0e9', width=2)
d.text((42, 1080), '绿色为 50 mm 开关包络占位;开关内孔、白色回正机构未实测。', font=small, fill='#405b4a')
d.text((42, 1120), 'KFL 凸起和板孔尺寸、前端轮毂、键槽及电机中心 M3 螺母均未实测。', font=small, fill='#46586b')
d.text((42, 1160), 'B 的后端轴承仅有约 0.5 mm 名义间隙,前端轮毂仍干涉;5 mm 插入深度未经验证。', font=small, fill='#9b2b22')
d.text((42, 1200), '此前 12 + 14.5 + 50 + 14.5 + 9 的预算不再适用;凸台高 20 不等于必须插入 20 mm。', font=small, fill='#46586b')
im.save(a / '更新尺寸_装配示意.png')

View File

@@ -0,0 +1,27 @@
from PIL import Image, ImageChops, ImageDraw, ImageFont
from pathlib import Path
out=Path(r'C:\projects\wheel\models\kfl_plate_adjust_concept')
F=r'C:\Windows\Fonts\msyh.ttc'; title=ImageFont.truetype(F,38); mid=ImageFont.truetype(F,24); small=ImageFont.truetype(F,22)
im=Image.new('RGB',(1920,1200),'white');d=ImageDraw.Draw(im)
d.text((40,18),'KFL001 反装 · 8 mm 轴承板及对中调整',font=title,fill='#1a304d')
d.text((40,75),'Ø12 × 150 mm 轴;两侧 2020 型材;灯光开关及接线 60 mm 包络',font=mid,fill='#52657b')
d.rounded_rectangle((40,116,1880,167),radius=8,fill='#fff0eb');d.text((52,126),'概念图:轴承径向凸起、铝板孔、开关外形和五金规格需核实,不可用于 CNC',font=mid,fill='#9b2b22')
def panel(filename,x,y,w,h,angle=0):
image=Image.open(out/filename).convert('RGB')
if angle:image=image.rotate(angle,expand=True)
bg=Image.new('RGB',image.size,'white'); mask=ImageChops.difference(image,bg).convert('L').point(lambda p:255 if p>12 else 0); bounds=mask.getbbox()
if bounds:image=image.crop(bounds)
image.thumbnail((w,h));im.paste(image,(x+(w-image.width)//2,y+(h-image.height)//2))
d.text((40,186),'总装(电机在左,方向盘侧在右)',font=mid,fill='#243a55')
d.text((1000,186),'轴承板与角码(隐藏开关、法兰)',font=mid,fill='#243a55')
panel('side.png',30,220,930,560,90); panel('adjustment.png',980,220,900,560,90)
d.line((40,800,1880,800),fill='#d8e0e9',width=2)
rows=[
('两块铝板各 8 mm;KFL 安装耳贴板,凸起进入中心孔;轴承与板作为整体对中。','#243a55'),
('竖向:角码长槽;横向:铝板长槽,交叉槽共用一颗螺丝;前后:2020 槽内 T 型螺母。','#243a55'),
('螺丝基准偏心:竖向约上 1 / 下 2 mm,横向约 ±2 mm,为几何行程,不可硬推轴。','#9b2b22'),
('轴承轴向按 KMT 标称,板孔 Ø30.2、凸起 Ø29 假设;轴承间中心距约 104.5 mm。','#52657b'),
('型材圆孔侧需 Ø9.5×2.75 mm 低头螺丝;工具空间、0.5 mm 法兰间隙未验证。','#9b2b22'),
]
for i,(txt,color) in enumerate(rows):d.text((40,822+i*48),txt,font=small,fill=color)
im.save(out/'KFL反装铝板_3D装配示意.png')

View File

@@ -0,0 +1,35 @@
from PIL import Image, ImageChops, ImageDraw, ImageFont
from pathlib import Path
out=Path(r'C:\projects\wheel\models\kfl_plate_b38_concept')
font=r'C:\Windows\Fonts\msyh.ttc'
title=ImageFont.truetype(font,38); normal=ImageFont.truetype(font,24); small=ImageFont.truetype(font,21)
im=Image.new('RGB',(1920,1300),'white');d=ImageDraw.Draw(im)
d.text((42,18),'KFL001 反装 · 铝板中心孔 Ø39 mm',font=title,fill='#193450')
d.text((42,80),'Ø38 外包络假设;8 mm 铝板;150 mm 轴;60 mm 开关与接线包络',font=normal,fill='#52657b')
d.rounded_rectangle((40,123,1880,177),radius=8,fill='#fff0eb')
d.text((55,134),'概念示意:仅用于讨论;轴承凸起轮廓及承载强度未验证,不可直接 CNC',font=normal,fill='#9b2b22')
def load(name,maxsize,rotation=0):
img=Image.open(out/name).convert('RGB')
if rotation:img=img.rotate(rotation,expand=True)
mask=ImageChops.difference(img,Image.new('RGB',img.size,'white')).convert('L').point(lambda p:255 if p>12 else 0)
b=mask.getbbox()
if b:img=img.crop(b)
img.thumbnail(maxsize);return img
side=load('side.png',(1730,370),90)
d.text((40,194),'整体侧视:电机在左,方向盘侧在右',font=normal,fill='#243a55')
im.paste(side,(95+(1730-side.width)//2,230+(370-side.height)//2))
d.line((40,620,1880,620),fill='#d8e0e9',width=2)
d.text((40,638),'反装结构(外形简化)',font=normal,fill='#243a55')
d.text((980,638),'铝板背面:垫圈与大孔的相对位置',font=normal,fill='#243a55')
detail=load('bearing_detail.png',(800,350));plate=load('plate_detail.png',(800,350))
im.paste(detail,(80+(800-detail.width)//2,680+(350-detail.height)//2))
im.paste(plate,(1020+(800-plate.width)//2,680+(350-plate.height)//2))
d.line((40,1055,1880,1055),fill='#d8e0e9',width=2)
rows=[
('中心孔 Ø39,轴承凸起按 Ø38 圆形包络示意,名义径向间隙 0.5 mm。','#243a55'),
('安装孔距 48、安装孔 Ø7:距中心孔最近材料宽度为 1 mm(几何值,不作强度结论)。','#243a55'),
('板侧 M6 垫圈按 Ø12 示意,靠中心孔一侧约 1.5 mm 悬空;真实承压须核对。','#9b2b22'),
('角码与对中长槽沿用前版;开关形状、轴承顶丝工具空间等尚未实物验证。','#52657b'),
]
for i,(s,c) in enumerate(rows):d.text((42,1073+i*43),s,font=small,fill=c)
im.save(out/'KFL_38凸起_39开孔_3D装配示意.png')

View File

@@ -0,0 +1,33 @@
from PIL import Image, ImageChops, ImageDraw, ImageFont
from pathlib import Path
root = Path(r'C:\projects\wheel\models')
font = r'C:\Windows\Fonts\msyh.ttc'
title = ImageFont.truetype(font, 38)
normal = ImageFont.truetype(font, 24)
small = ImageFont.truetype(font, 21)
im = Image.new('RGB', (1920, 1370), 'white')
d = ImageDraw.Draw(im)
d.text((40, 18), '两端相同法兰:100 mm 与 120 mm 轴布局比较', fill='#1a304d', font=title)
d.text((40, 80), '两端法兰:Ø57 × 4 mm 底盘、Ø30 × 20 mm 凸台;6 孔 PCD48', fill='#52657b', font=normal)
d.rounded_rectangle((40, 123, 1880, 177), radius=8, fill='#fff0eb')
d.text((55, 134), '均为概念示意:KFL 和开关的真实结构未建模,不能证明实物装配通过', fill='#9b2b22', font=normal)
def panel(path, box):
view = Image.open(path).convert('RGB').rotate(90, expand=True)
mask = ImageChops.difference(view, Image.new('RGB', view.size, 'white')).convert('L').point(lambda p:255 if p>12 else 0)
bounds = mask.getbbox()
if bounds: view = view.crop(bounds)
view.thumbnail((box[2]-box[0],box[3]-box[1]))
im.paste(view, (box[0] + (box[2]-box[0]-view.width)//2, box[1]))
d.text((40, 196), 'A:100 mm 轴;在此布局下,电机侧插入 12 mm,方向盘侧只插入 2 mm', font=normal, fill='#243a55')
panel(root / 'kfl_same_flange_100' / 'side.png', (35, 235, 1885, 605))
d.text((40, 620), 'B:120 mm 轴;同一布局下方向盘侧插入 20 mm,仅为对比假设', font=normal, fill='#243a55')
panel(root / 'kfl_same_flange_120' / 'side.png', (35, 660, 1885, 1030))
d.line((40, 1055, 1880, 1055), fill='#d8e0e9', width=2)
d.text((42, 1075), '绿色半透明体:50 mm 开关包络占位,内部 Ø47 孔仅为示意;白色回正件未建模。', font=small, fill='#405b4a')
d.text((42, 1116), 'KFL 凸起、铝板孔、端面键槽和电机中心螺母均未实测;两侧轴承和法兰间隙示意 0.5 mm。', font=small, fill='#46586b')
d.text((42, 1157), '120 mm 是当前布局的比较长度,不是已验证的必需长度或强度结论。', font=small, fill='#9b2b22')
d.text((42, 1198), '原铝转接盘 STEP 孔径约 Ø4.23 mm,法兰 M5 通孔无法直接使用 M5 螺丝贯穿原盘。', font=small, fill='#46586b')
im.save(root / 'kfl_same_flange_100' / '两端相同法兰_装配示意.png')

View File

@@ -0,0 +1,35 @@
from PIL import Image, ImageChops, ImageDraw, ImageFont
from pathlib import Path
out = Path(r'C:\projects\wheel\models\kfl_same_flange_125')
font_path = r'C:\Windows\Fonts\msyh.ttc'
title = ImageFont.truetype(font_path, 38)
normal = ImageFont.truetype(font_path, 24)
small = ImageFont.truetype(font_path, 21)
im = Image.new('RGB', (1920, 1150), 'white')
d = ImageDraw.Draw(im)
d.text((40, 18), '125 mm 轴与两端相同法兰 · 装配示意', fill='#1a304d', font=title)
d.text((40, 80), '轴 Ø12 × 125 mm;两端法兰底盘 Ø57 × 4 mm,凸台 Ø30 × 20 mm', fill='#52657b', font=normal)
d.rounded_rectangle((40, 123, 1880, 177), radius=8, fill='#fff0eb')
d.text((55, 134), '概念布局:轴承、开关及电机中心螺母未实测,不能证明实物装配通过', fill='#9b2b22', font=normal)
def panel(path, box, rotate=False):
view = Image.open(path).convert('RGB')
if rotate:
view = view.rotate(90, expand=True)
mask = ImageChops.difference(view, Image.new('RGB', view.size, 'white')).convert('L').point(lambda p:255 if p>12 else 0)
bounds = mask.getbbox()
if bounds:
view = view.crop(bounds)
view.thumbnail((box[2]-box[0],box[3]-box[1]))
im.paste(view, (box[0] + (box[2]-box[0]-view.width)//2, box[1] + (box[3]-box[1]-view.height)//2))
d.text((40, 196), '侧视图(电机在左,方向盘侧在右)', font=normal, fill='#243a55')
panel(out / 'side.png', (35, 230, 1885, 740), True)
d.line((40, 760, 1880, 760), fill='#d8e0e9', width=2)
d.text((42, 781), '本次几何示意:两端凸台各插入约 19.5 mm;两端轴面离法兰底盘各约 0.5 mm。', font=small, fill='#243a55')
d.text((42, 825), '电机中心 M3 螺母未建模:轴端可能与螺母干涉,不能据此认为此布局能装。', font=small, fill='#9b2b22')
d.text((42, 869), '绿色:灯光开关 50 mm 包络占位;内部 Ø47 孔是示意,白色回正机构未建模。', font=small, fill='#405b4a')
d.text((42, 913), '轴承凸起、铝板孔、端部键槽及连接五金均待核实;当前轴承到法兰名义间隙约 0.5 mm。', font=small, fill='#46586b')
d.text((42, 957), '125 mm 为用户选择的轴长;法兰/轴的配合、位置和强度仍需实物确认。', font=small, fill='#46586b')
im.save(out / '125mm轴_装配示意.png')

View File

@@ -0,0 +1,27 @@
from PIL import Image,ImageDraw,ImageFont
from pathlib import Path
O=Path(r'C:\projects\wheel\models\kfl_double_crossbar_concept'); im=Image.new('RGB',(1700,1150),'white'); d=ImageDraw.Draw(im)
def f(n):return ImageFont.truetype(r'C:\Windows\Fonts\msyh.ttc',n)
def txt(x,y,s,n=28,c='#243349'):d.text((x,y),s,font=f(n),fill=c)
txt(60,35,'加长 L 角码:选购规格示意',45);txt(62,100,'尺寸均为建模假设;确认实物、折弯圆角和孔位后再采购。',25)
# front view of upright; pixel/mm = 11; bottom edge at 790, y 45
x0=170;y0=260;s=11
d.rectangle((x0,y0,x0+18*s,y0+45*s),fill='#f5a245',outline='#623c17',width=4)
sx=x0+9*s;sy=y0+12*s;rad=2.75*s
d.rounded_rectangle((sx-rad,sy-4.75*s,sx+rad,sy+4.75*s),radius=int(rad),fill='white',outline='#704214',width=3)
txt(110,800,'正视:竖腿贴住铝板',25)
d.line((x0+18*s+25,y0,x0+18*s+25,y0+45*s),fill='#335',width=2);txt(x0+18*s+35,460,'45',26)
d.line((x0,y0-25,x0+18*s,y0-25),fill='#335',width=2);txt(x0+50,y0-65,'18',26)
txt(440,315,'长槽 9.5 × 5.5',28);d.line((sx+30,sy,425,sy),fill='#335',width=2)
txt(440,375,'槽中心距顶端 12',25);txt(440,420,'距折弯底端 33',25)
# side diagram scale 10, cutaway L
x=1040;y=260;s=10
d.rectangle((x,y,x+3*s,y+45*s),fill='#f5a245',outline='#623c17',width=3)
d.rectangle((x,y+42*s,x+20*s,y+45*s),fill='#f5a245',outline='#623c17',width=3)
d.line((x-25,y,x-25,y+45*s),fill='#335',width=2);txt(x-95,y+200,'45',26)
d.line((x,y+45*s+28,x+20*s,y+45*s+28),fill='#335',width=2);txt(x+75,y+45*s+40,'20',26)
txt(980,800,'侧视:两腿连续折弯;板厚 3',25)
txt(60,900,'横梁侧:圆孔 Ø5.5,孔中心在 20 mm 腿长度中间。',27)
txt(60,955,'铝板上对应水平槽,长槽交叉用于对中。示意未画真实折弯圆角。',27)
txt(60,1010,'四只长角码;横梁两端另外各用一只普通角码。',27)
im.save(O/'加长角码_尺寸假设.png')

View File

@@ -0,0 +1,25 @@
from PIL import Image,ImageDraw,ImageFont,ImageChops
from pathlib import Path
O=Path(r'C:\projects\wheel\models\kfl_3030_motor_corner_concept')
font=r'C:\Windows\Fonts\msyh.ttc'
def f(s): return ImageFont.truetype(font,s)
canvas=Image.new('RGB',(2400,1760),'#f4f6fa');d=ImageDraw.Draw(canvas)
d.text((65,35),'电机与底座型材连接 · 3D装配示意',font=f(50),fill='#18283c')
d.text((65,106),'左右各增加一只 2020 直角角码;两根 3030 底座纵梁向编码器端各延长 20 mm',font=f(27),fill='#3e5065')
def panel(name,xy,wh,title):
x,y=xy;w,h=wh;d.rounded_rectangle((x,y,x+w,y+h),radius=18,fill='white',outline='#d9e0e9',width=2);d.text((x+20,y+15),title,font=f(28),fill='#20344c')
img=Image.open(O/name).convert('RGB');diff=ImageChops.difference(img,Image.new('RGB',img.size,'white'));bounds=diff.getbbox()
if bounds:
left,top,right,bottom=bounds;img=img.crop((max(0,left-35),max(0,top-35),min(img.width,right+35),min(img.height,bottom+35)))
img.thumbnail((w-35,h-75));canvas.paste(img,(x+(w-img.width)//2,y+60+(h-70-img.height)//2))
panel('assembly.png',(45,165),(1130,1010),'总装:红色为新增角码')
panel('motor_corner_top.png',(1225,165),(1130,1010),'电机端局部:2020、3030 与新增角码')
d.rounded_rectangle((45,1205,2355,1710),radius=18,fill='white',outline='#d9e0e9',width=2)
d.text((75,1225),'静态几何检查与说明',font=f(32),fill='#20344c')
rows=['① 新角码与原电机角码的最小模型间距约 1 mm;实物误差、螺丝与工具空间仍需核对。',
'② 新角码与电机本体约 20 mm、与蓝色编码器支架约 22.15 mm;以上均为简化模型测量。',
'③ 角码与两根型材表面相贴,接触处距离为零;筛查未发现新增实体的正体积重叠。',
'④ 型材 T 槽、螺丝及滑块螺母未建;长孔宽按 5.5 mm 假设,安装与强度需实物验证。',
'⑤ 灯光开关和接线为估计包络,本图不能确认其真实外形或运动时的干涉。']
for i,s in enumerate(rows):d.text((75,1290+i*66),s,font=f(25),fill='#9b4939' if i>=3 else '#30445b')
canvas.save(O/'电机角码_3030加长_3D装配示意.png')

View File

@@ -0,0 +1,24 @@
from PIL import Image, ImageDraw, ImageFont, ImageChops
from pathlib import Path
R=Path(r'C:\projects\wheel\models\switch_2020_concept')
def font(s): return ImageFont.truetype(r'C:\Windows\Fonts\msyh.ttc',s)
def crop(name):
im=Image.open(R/name).convert('RGB'); bg=Image.new('RGB',im.size,'white');d=ImageChops.difference(im,bg).convert('L').point(lambda p:255 if p>15 else 0);bb=d.getbbox();return im.crop(bb) if bb else im
def paste_fit(can,im,box):
w,h=box[2]-box[0],box[3]-box[1]; im.thumbnail((w,h));can.paste(im,(box[0]+(w-im.width)//2,box[1]+(h-im.height)//2))
out=Image.new('RGB',(1900,1400),'#f4f7fb');d=ImageDraw.Draw(out)
d.text((55,26),'灯光开关:横向 2020 与长角码装配示意',font=font(42),fill='#1c2c40')
d.text((58,88),'A 比轴心低 35 mm;B 低 22 mm;孔距 85 mm。两孔左右对称仅为示意假设。',font=font(23),fill='#3c526b')
for x1,x2 in [(35,920),(950,1865)]: d.rounded_rectangle((x1,135,x2,915),radius=16,fill='white',outline='#d5dce5',width=2)
d.text((55,149),'现有整机中的位置',font=font(24),fill='#25384d');d.text((970,149),'沿轴线观察(A 左、B 右)',font=font(24),fill='#25384d')
paste_fit(out,crop('focus.png'),(55,195,900,890));paste_fit(out,crop('switch_front.png'),(970,200,1845,840))
d.rounded_rectangle((35,945,1865,1360),radius=16,fill='white',outline='#d5dce5',width=2)
d.text((60,967),'当前检查:未能确认开关实物无干涉',font=font(28),fill='#a12e1e')
lines=[
'橙色:开关长角码;红色:横梁与纵梁连接角码;绿色:开关估计包络,并非实物外形。',
'新增 2020 横梁与绿色估计包络相交;角码也进入包络。真实开关下缘与安装耳尚未建模。',
'两角码为避开轴承板角码,各偏移约 0.75 mm;与原角码仅约 0.25 mm 净距。',
'A 孔中心与 2020 顶面齐平,螺丝、垫片及安装耳的真实轴向间隙仍须核对。',
'型材 T 槽与实际紧固件未完整建模;此图用于布置讨论,不能证明可装配。']
for i,line in enumerate(lines):d.text((60,1020+60*i),line,font=font(22),fill='#31485e')
out.save(R/'开关2020横梁_装配干涉示意.png')

View File

@@ -0,0 +1,24 @@
from PIL import Image, ImageDraw, ImageFont, ImageChops
from pathlib import Path
R=Path(r'C:\projects\wheel\models\switch_2020_raise10_concept')
def font(s):return ImageFont.truetype(r'C:\Windows\Fonts\msyh.ttc',s)
def crop(name):
im=Image.open(R/name).convert('RGB');bg=Image.new('RGB',im.size,'white');m=ImageChops.difference(im,bg).convert('L').point(lambda p:255 if p>15 else 0);b=m.getbbox();return im.crop(b) if b else im
def paste(can,im,box):
im.thumbnail((box[2]-box[0],box[3]-box[1]));can.paste(im,(box[0]+(box[2]-box[0]-im.width)//2,box[1]+(box[3]-box[1]-im.height)//2))
out=Image.new('RGB',(1900,1400),'#f4f7fb');d=ImageDraw.Draw(out)
d.text((55,25),'灯光开关装配示意:中心轴整体上移 10 mm',font=font(40),fill='#1c2c40')
d.text((58,87),'2020 横梁固定在 3030 纵梁上方;电机端增加 10 mm 支撑垫块。',font=font(23),fill='#3c526b')
for x1,x2 in [(35,920),(950,1865)]:d.rounded_rectangle((x1,135,x2,915),radius=16,fill='white',outline='#d5dce5',width=2)
d.text((55,149),'总装(灰色为型材,蓝色为电机侧垫块)',font=font(23),fill='#25384d');d.text((970,149),'开关局部(A 左、B 右)',font=font(24),fill='#25384d')
paste(out,crop('assembly.png'),(55,190,900,890));paste(out,crop('switch_front.png'),(970,190,1845,885))
d.rounded_rectangle((35,945,1865,1360),radius=16,fill='white',outline='#d5dce5',width=2)
d.text((60,967),'当前是尺寸方案,真实开关仍需实物核对',font=font(28),fill='#a12e1e')
lines=[
'A / B 比轴心分别低 35 / 22 mm;孔距 85 mm;孔组左右对称仅为示意假设。',
'橙色:开关角码;红色:型材角码;绿色:开关估计包络,非真实外壳。',
'横梁与估计包络无实体相交,最小距离 5 mm;开关角码仍穿入估计包络。',
'若 A 到边缘的 7 mm 位于正下方,该处边缘与横梁顶面间约 3 mm;方向未确认。',
'型材槽、全部螺栓与真实安装耳未完整建模;承载、实际装配和工具空间尚需核验。']
for i,line in enumerate(lines):d.text((60,1020+60*i),line,font=font(22),fill='#31485e')
out.save(R/'开关2020横梁_轴心上移10_装配示意.png')

View File

@@ -0,0 +1,15 @@
from PIL import Image, ImageDraw, ImageFont
from pathlib import Path
p=Path(r'C:\projects\wheel\models\turn_signal_cam_v2')
out=Image.new('RGB',(1920,930),'white');d=ImageDraw.Draw(out)
font=r'C:\Windows\Fonts\msyh.ttc'
f=ImageFont.truetype(font,36); medium=ImageFont.truetype(font,24); small=ImageFont.truetype(font,21)
d.text((42,20),'转向灯回正轴套 · 3D 尺寸方案示意',font=f,fill='#15263d')
d.text((42,75),'轴 Ø12 × 100 mm / 轴套轴向 20 mm / 平键宽 4 mm',font=medium,fill='#53647b')
for file,x,label in [('full.png',0,'整根轴(位置示意)'),('detail.png',640,'装配局部'),('exploded.png',1280,'分解结构与平键')]:
im=Image.open(p/file).convert('RGB');im.thumbnail((640,480));out.paste(im,(x,152));d.text((x+32,122),label,font=medium,fill='#15263d')
specs=[('尺寸约束','拨片扫掠 Ø43.54 mm ≤ Ø44 mm','夹紧区含螺丝及螺母约 Ø33.47 mm ≤ Ø34 mm'),('颜色','深蓝:带凸块的半环 浅蓝:另一半环','灰色:轴 金色:4 mm 平键 深灰:夹紧件')]
for i,(title,line1,line2) in enumerate(specs):
x=42+i*920; d.text((x,662),title,font=medium,fill='#15263d');d.text((x,705),line1,font=small,fill='#33465b');d.text((x,744),line2,font=small,fill='#33465b')
d.text((42,832),'示意:开关、两轴承、电机法兰和方向盘未建模;100 mm 轴的实际装配长度尚未验证。',font=medium,fill='#8b3f36')
out.save(p/'3D示意.png')

View File

@@ -0,0 +1,16 @@
from PIL import Image, ImageDraw, ImageFont
from pathlib import Path
p=Path(r'C:\projects\wheel\models\turn_signal_cam_concept')
out=Image.new('RGB',(1800,1040),'white'); d=ImageDraw.Draw(out)
font=r'C:\Windows\Fonts\msyh.ttc'
f=ImageFont.truetype(font,36); small=ImageFont.truetype(font,24); tiny=ImageFont.truetype(font,21)
d.text((44,20),'转向灯自动回正轴套 · 概念示意',font=f,fill='#15263d')
d.text((44,77),'12 mm 轴 / 4 mm 平键槽 · 凸块尺寸和触发位置待实物确认',font=small,fill='#566579')
a=Image.open(p/'perspective.png').convert('RGB'); a.thumbnail((980,735));out.paste(a,(15,170))
b=Image.open(p/'front.png').convert('RGB');b.thumbnail((690,520));out.paste(b,(1040,170))
d.text((50,135),'立体示意',font=small,fill='#15263d');d.text((1055,135),'沿轴向看',font=small,fill='#15263d')
colors=[('#1372b0','蓝色:分体夹紧轴套与一体凸块'),('#777e83','灰色:12 mm 轴(示意)'),('#dfa33c','金色:金属平键(示意)'),('#333b43','深灰:夹紧螺丝与螺母(示意)')]
for i,(c,t) in enumerate(colors):
x=1055;y=720+i*45;d.rounded_rectangle((x,y+5,x+25,y+30),radius=3,fill=c);d.text((x+38,y),t,font=tiny,fill='#243447')
d.text((44,975),'此图只用于讨论结构;未按开关实物建模,不能作为打印文件。',font=small,fill='#8b3f36')
out.save(p/'概念示意.png')

View File

@@ -0,0 +1,21 @@
from pathlib import Path
import FreeCAD as App, FreeCADGui as Gui
import os, traceback
R=Path(r'C:\projects\wheel')
out=R/'models'/'washer_panel_4x2'
try:
p=next(out.glob('*.FCStd'))
doc=App.openDocument(str(p));doc.recompute()
for obj in doc.Objects:
if obj.ViewObject: obj.ViewObject.Visibility=False
panel=doc.getObject('WasherPanel');panel.ViewObject.Visibility=True
panel.ViewObject.ShapeColor=(0.65,0.74,0.86)
Gui.activeDocument().activeView().viewAxonometric();Gui.activeDocument().activeView().fitAll()
doc.recompute();doc.save()
App.closeDocument(doc.Name)
(out/'gui_ready.txt').write_text('OK',encoding='utf-8')
os._exit(0)
except Exception:
(out/'gui_error.txt').write_text(traceback.format_exc(),encoding='utf-8')
os._exit(1)

View File

@@ -0,0 +1,20 @@
from pathlib import Path
import FreeCAD as App, FreeCADGui as Gui
import os, traceback
R=Path(r'C:\projects\wheel')
out=R/'models'/'washer_panel_4x4'
try:
p=next(out.glob('*.FCStd'))
doc=App.openDocument(str(p));doc.recompute()
for obj in doc.Objects:
if obj.ViewObject: obj.ViewObject.Visibility=False
panel=doc.getObject('WasherPanel');panel.ViewObject.Visibility=True
panel.ViewObject.ShapeColor=(0.65,0.74,0.86)
Gui.activeDocument().activeView().viewAxonometric();Gui.activeDocument().activeView().fitAll()
doc.recompute();doc.save()
App.closeDocument(doc.Name)
(out/'gui_ready.txt').write_text('OK',encoding='utf-8')
os._exit(0)
except Exception:
(out/'gui_error.txt').write_text(traceback.format_exc(),encoding='utf-8')
os._exit(1)

View File

@@ -0,0 +1,17 @@
from pathlib import Path
import FreeCAD as A, FreeCADGui as G
import os,traceback
p=Path(r'C:\projects\wheel\models\washer_two_panels')
try:
for name in ['一体垫片板_0.5mm.FCStd','一体垫片板_1mm.FCStd']:
d=A.openDocument(str(p/name));d.recompute()
for o in d.Objects:
if hasattr(o,'ViewObject') and o.ViewObject:o.ViewObject.Visibility=False
d.getObject('Panel').ViewObject.Visibility=True
G.activeDocument().activeView().viewAxonometric();G.activeDocument().activeView().fitAll()
d.recompute();d.save();A.closeDocument(d.Name)
(p/'gui_ready.txt').write_text('ok',encoding='utf8')
os._exit(0)
except Exception:
(p/'gui_error.txt').write_text(traceback.format_exc(),encoding='utf8')
os._exit(1)

View File

@@ -0,0 +1,8 @@
import FreeCAD as A, FreeCADGui as G, json, os
p=r'C:\projects\wheel\models\fan_direct_motor_side_140\Assembly.FCStd'
d=A.openDocument(p);d.recompute();o=[]
for a in d.Objects:
if hasattr(a,'Shape') and not a.Shape.isNull():
b=a.Shape.BoundBox; o.append({'name':a.Name,'label':a.Label,'visible':a.ViewObject.Visibility,'bounds':[b.XMin,b.XMax,b.YMin,b.YMax,b.ZMin,b.ZMax]})
open(r'C:\projects\wheel\models\objects_dump.json','w',encoding='utf8').write(json.dumps(o,ensure_ascii=False,indent=2))
A.closeDocument(d.Name);os._exit(0)

View File

@@ -0,0 +1,47 @@
$src = Get-Content -Raw -Encoding UTF8 'C:\projects\wheel\scripts\build_kfl_same_flange_125.py'
$pairs = @(
@("O=R/'models'/('kfl_same_flange_%d'%shaftlen)", "O=R/'models'/'kfl_125_switch60_concept'"),
@("-108 if n=='Shaft' else -112", "-108 if n=='Shaft' else -118"),
@("-50.5-shaftlen", "-56.5-shaftlen"),
@("4,-119", "4,-125"),
@("20,20,190,V(x-10,-75,-170)", "20,20,200,V(x-10,-75,-180)"),
@("[-84,-150]", "[-84,-162]"),
@("(-150,-153,-1)", "(-162,-165,-1)"),
@("cy(15,20,0,0,-176).cut(cy(6.2,20.02,0,0,-176.01))", "cy(15,20,0,0,-188).cut(cy(6.2,20.02,0,0,-188.01))"),
@("cy(28.5,4,0,0,-180).cut(cy(6.2,4.02,0,0,-180.01))", "cy(28.5,4,0,0,-192).cut(cy(6.2,4.02,0,0,-192.01))"),
@("24*math.sin(a),-180.01", "24*math.sin(a),-192.01"),
@("wheel70.translate(V(0,0,-180))", "wheel70.translate(V(0,0,-192))"),
@("Part.makeBox(110,80,50,V(-55,-40,-138.5)).cut(cy(23.5,50.02,0,0,-138.51))", "Part.makeBox(110,80,60,V(-55,-40,-149.5)).cut(cy(23.5,60.02,0,0,-149.51))"),
@("'50mm switch bounding placeholder; 47mm bore ASSUMED'", "'60mm switch and wiring clearance envelope; 47mm bore ASSUMED'"),
@("24*math.sin(a),-188)", "24*math.sin(a),-200)"),
@("min(-156,-50.5)-max(-176,z_end)", "min(-168,-56.5)-max(-188,z_end)"),
@("min(-176,-50.5)-max(-180,z_end)", "min(-188,-56.5)-max(-192,z_end)"),
@("'Shaft nominal 12 x %d; z=%.1f..-50.5; motor hub z=-70..-50; motor insertion 19.5mm assumed and axial gap to base 0.5mm.'", "'Shaft nominal 12 x %d; z=%.1f..-56.5; motor hub z=-70..-50; motor insertion 13.5mm assumed and axial gap to base 6.5mm.'"),
@("'Wheel hub z=-176..-156, front plate z=-180..-176;", "'Wheel hub z=-188..-168, front plate z=-192..-188;"),
@("'Bearing positions, 50mm switch envelope and cam as previous shifted design;", "'Rear bearing retained; front bearing shifted 12mm to wheel; switch 60mm with 1mm nominal gap on each side, cam shifted 6mm;"),
@("'KFL approximate 48 bolt pitch and 14.5 inner width, housing/protrusion guessed; 8mm plate 30.2 hole guessed; no real module shape, its 47mm hole is an assumption.'", "'KFL approximate 48 bolt pitch and 14.5 inner width, housing/protrusion guessed; 8mm plate 30.2 hole guessed; 60mm switch/wire envelope placeholder, its 47mm hole is an assumption.'"),
@("KFL125_Schematic", "KFL125_Switch60_Schematic")
)
foreach ($pair in $pairs) {
if (-not $src.Contains($pair[0])) { throw "Cannot find required source fragment: $($pair[0])" }
$src = $src.Replace($pair[0], $pair[1])
}
$extraPairs = @'
pairs += [('SwitchEnvelope','Plate0',sw,plates[0].Shape),('SwitchEnvelope','Plate1',sw,plates[1].Shape)]
pairs += [('SwitchEnvelope','KFLHousing0',sw,bearings[0].Shape),('SwitchEnvelope','KFLHousing1',sw,bearings[2].Shape)]
pairs += [('Key','Shaft',cam['Key'],cam['Shaft']),('Key','CamUpper',cam['Key'],cam['Upper']),('Key','CamLower',cam['Key'],cam['Lower'])]
pairs += [('CamUpper','Shaft',cam['Upper'],cam['Shaft']),('CamLower','Shaft',cam['Lower'],cam['Shaft'])]
pairs += [('CamUpper','RearKFL',cam['Upper'],bearings[0].Shape),('CamUpper','FrontKFL',cam['Upper'],bearings[2].Shape)]
pairs += [('CamLower','RearKFL',cam['Lower'],bearings[0].Shape),('CamLower','FrontKFL',cam['Lower'],bearings[2].Shape)]
pairs += [('Shaft','Wheel70',cam['Shaft'],wheel70),('WheelHub','SwitchEnvelope',wh,sw),('MotorFlange','SwitchEnvelope',flange,sw)]
for a,b,sa,sb in pairs:
'@
if (-not $src.Contains(' for a,b,sa,sb in pairs:')) { throw 'Cannot extend collision checks' }
$src = $src.Replace(' for a,b,sa,sb in pairs:', $extraPairs)
[System.IO.File]::WriteAllText('C:\projects\wheel\scripts\build_kfl_125_switch60.py', $src, [System.Text.UTF8Encoding]::new($false))
$macro = @'
from pathlib import Path
script=Path(r'C:\projects\wheel\scripts\build_kfl_125_switch60.py')
exec(compile(script.read_text(encoding='utf8'),str(script),'exec'),{'__name__':'__main__','__file__':str(script)})
'@
[System.IO.File]::WriteAllText('C:\projects\wheel\scripts\rebuild_kfl_125_switch60.FCMacro', $macro, [System.Text.UTF8Encoding]::new($false))

View File

@@ -0,0 +1,31 @@
from pathlib import Path
p=Path(__file__).with_name('build_kfl001_plate_v2.py')
s=p.read_text(encoding='utf8')
s=s.replace('kfl001_plate_v2','kfl001_plate_v3_alt').replace('KFL001_Plate_V2','KFL001_Plate_V3').replace('KFL001_plate_V2','KFL001_plate_V3').replace('Plate_V2','Plate_V3').replace('plate V2','plate V3')
s=s.replace("('EdgeChamfer','1.5 mm','Both outer perimeter edges only'),", "('EdgeChamfer','1 mm','Both outer perimeter edges only'),\n ('CornerRadius','3 mm','Rounded upper transitions and lower plan corners'),")
old=""" outer=[]
for i,e in enumerate(shape.Shape.Edges,1):
b=e.BoundBox
if abs(b.ZMax-b.ZMin)<1e-7 and (abs(b.ZMin)<1e-7 or abs(b.ZMin-8)<1e-7):outer.append('Edge%d'%i)
chamfer=body.newObject('PartDesign::Chamfer','PerimeterChamfer')
chamfer.Base=(chamfer.BaseFeature,outer);expr(chamfer,'Size','Parameters.EdgeChamfer')"""
new=""" vertical=[]
for i,e in enumerate(shape.Shape.Edges,1):
b=e.BoundBox
if abs((b.ZMax-b.ZMin)-8)<1e-7 and (b.XLength<1e-6 and b.YLength<1e-6): vertical.append('Edge%d'%i)
assert len(vertical)==4,vertical
fillet=body.newObject('PartDesign::Fillet','OutlineCornerFillets')
fillet.Base=(fillet.BaseFeature,vertical);expr(fillet,'Radius','Parameters.CornerRadius')
d.recompute()
assert not fillet.Shape.isNull() and fillet.Shape.isValid(),fillet.State
outer=[]
for i,e in enumerate(fillet.Shape.Edges,1):
b=e.BoundBox
if abs(b.ZMax-b.ZMin)<1e-7 and (abs(b.ZMin)<1e-7 or abs(b.ZMin-8)<1e-7):outer.append('Edge%d'%i)
chamfer=body.newObject('PartDesign::Chamfer','PerimeterChamfer')
chamfer.Base=(fillet,outer);expr(chamfer,'Size','Parameters.EdgeChamfer')"""
assert old in s
s=s.replace(old,new)
s=s.replace("'perimeter_chamfer':1.5", "'perimeter_chamfer':1.0,'outline_corner_radius':3.0")
Path(__file__).with_name('build_kfl001_plate_v3_alt.py').write_text(s,encoding='utf8')
Path(__file__).with_name('rebuild_kfl001_plate_v3_alt.FCMacro').write_text("from pathlib import Path\np=Path(__file__).resolve().with_name('build_kfl001_plate_v3_alt.py')\nexec(compile(p.read_text(encoding='utf-8'),str(p),'exec'),{'__name__':'__main__','__file__':str(p)})\n",encoding='utf8')

View File

@@ -0,0 +1,24 @@
from pathlib import Path
p=Path(__file__).with_name('render_kfl_plate_v2.py')
s=p.read_text(encoding='utf8').replace('kfl001_plate_v2','kfl001_plate_v3_alt').replace('V2','V3')
start=s.index('path=c.beginPath();path.moveTo(x0,y0);path.lineTo(x0+w,y0)')
end=s.index("c.setFillColor(HexColor('#ffffff'));",start)
s=s[:start]+'''# Exterior as manufactured: R3 tangent corners and R60 arch, symmetric around x=38.
path=c.beginPath();path.moveTo(x0+3*k,y0);path.lineTo(x0+73*k,y0)
def arc_poly(cx0,cy0,r,a0,a1,n=20):
for j in range(1,n+1):
a=math.radians(a0+(a1-a0)*j/n)
path.lineTo(x0+(cx0+r*math.cos(a))*k,y0+(cy0+r*math.sin(a))*k)
arc_poly(73,3,3,-90,0)
y_corner=15+math.sqrt(57**2-35**2)
path.lineTo(x0+76*k,y0+y_corner*k)
theta=math.degrees(math.atan2(y_corner-15,35))
arc_poly(73,y_corner,3,0,theta)
arc_poly(38,15,60,theta,180-theta,80)
arc_poly(3,y_corner,3,180-theta,180)
path.lineTo(x0,y0+3*k)
arc_poly(3,3,3,180,270)
path.close();c.drawPath(path,fill=1,stroke=1)
''' + s[end:]
s=s.replace("'6 外轮廓两面各 1.5 × 45° 倒角'", "'6 平面过渡 R3;外轮廓双面 C1'")
Path(__file__).with_name('render_kfl_plate_v3_alt.py').write_text(s,encoding='utf8')

View File

@@ -0,0 +1,42 @@
from pathlib import Path
root = Path(r'C:\projects\wheel')
scripts = root / 'scripts'
output = root / 'models' / 'kfl001_plate_v1'
output.mkdir(parents=True, exist_ok=True)
src = (scripts / 'build_kfl001_plate_draft.py').read_text(encoding='utf8')
for before, after in [
('kfl001_plate_draft', 'kfl001_plate_v1'),
('KFL001_Plate_Draft', 'KFL001_Plate_V1'),
('KFL001 8mm plate DRAFT - centre cut unverified', 'KFL001 8mm bearing mounting plate V1'),
('Parameters - centre diameter UNVERIFIED', 'Parameters - dimensions in mm'),
('TEMPORARY ONLY - verify real KFL back geometry', '39 mm clearance for user confirmed 38 mm boss'),
('TEMP_CentreHole', 'CentreHole'),
('Plate_DRAFT', 'Plate_V1'),
('DRAFT bearing plate - centre 39mm NOT verified', 'KFL001 bearing mounting plate V1'),
('KFL001_plate_DRAFT', 'KFL001_plate_V1'),
('center_diameter_provisional', 'center_diameter'),
("'center_UNVERIFIED':True", "'boss_diameter_user_confirmed_mm':38"),
]:
src = src.replace(before, after)
src = src.replace('Editable KFL001 bearing mounting plate -- provisional concept only.', 'Editable KFL001 bearing mounting plate for prototype.')
src = src.replace('The centre cut is an unverified placeholder and MUST NOT be used for CNC.', 'The centre cut provides 0.5 mm radial clearance for a 38 mm boss.')
(scripts / 'build_kfl001_plate_v1.py').write_text(src, encoding='utf8')
(scripts / 'rebuild_kfl001_plate_v1.FCMacro').write_text("from pathlib import Path\nscript = Path(__file__).resolve().with_name('build_kfl001_plate_v1.py')\nexec(compile(script.read_text(encoding='utf-8'), str(script), 'exec'), {'__name__':'__main__','__file__':str(script)})\n", encoding='utf8')
src = (scripts / 'render_kfl_plate_draft.py').read_text(encoding='utf8')
for before, after in [
('kfl001_plate_draft', 'kfl001_plate_v1'),
('KFL001板_尺寸草图_禁止加工.pdf', 'KFL001固定板_尺寸图.pdf'),
('尺寸草图_禁止加工.png', 'KFL001固定板_尺寸图.png'),
('KFL001 mounting plate - provisional dimensions', 'KFL001 mounting plate V1 - prototype'),
('KFL001 固定板尺寸草图', 'KFL001 固定板尺寸图'),
('草图:中心孔仅按直径 39 mm 示意,必须实测后才能加工', '试制版:中心孔直径 39,适配直径 38 凸起'),
('1 中心孔:直径 39 暂定,真实凸起待实测', '1 中心孔:直径 39,适配直径 38 凸起'),
('中心孔可能需要改小或改轮廓,', '轴承及连接件先试装,'),
('请勿用本图或草模直接下单加工。', '紧固和动态强度需实物验证。'),
('注意:圆孔、板边与调节槽均贯穿,轴承真实外形、螺丝及垫圈待核对。', '注:圆孔和长槽均贯穿;两块相同。按孔心尺寸加工,无螺纹。'),
]:
src = src.replace(before, after)
(scripts / 'render_kfl_plate_v1.py').write_text(src, encoding='utf8')

View File

@@ -0,0 +1,14 @@
from pathlib import Path
import FreeCAD as App
import traceback
out = Path(r'C:\projects\wheel\models\70_m4_drill_plain')
out.mkdir(parents=True, exist_ok=True)
try:
source = Path(r'C:\projects\wheel\scripts\build_70_m4_drill_plain.py')
exec(compile(source.read_text(encoding='utf-8'), str(source), 'exec'), {'__name__':'__main__','__file__':str(source)})
(out / 'build_status.txt').write_text('OK', encoding='utf-8')
except:
(out / 'build_status.txt').write_text(traceback.format_exc(), encoding='utf-8')
for name in list(App.listDocuments()):
App.closeDocument(name)
Gui.getMainWindow().close()

View File

@@ -0,0 +1,13 @@
from pathlib import Path
import FreeCAD as App
import traceback
try:
source=Path(r'C:\projects\wheel\scripts\build_70_m4_threaded.py')
exec(compile(source.read_text(encoding='utf-8'),str(source),'exec'),{'__name__':'__main__','__file__':str(source)})
Path(r'C:\projects\wheel\models\70_m4_threaded\build_status.txt').write_text('OK',encoding='utf-8')
except:
Path(r'C:\projects\wheel\models\70_m4_threaded\build_status.txt').write_text(traceback.format_exc(),encoding='utf-8')
for name in list(App.listDocuments()):
App.closeDocument(name)
Gui.getMainWindow().close()

View File

@@ -0,0 +1,10 @@
"""Run in FreeCAD GUI to rebuild conversion_disk_parametric.FCStd.
The .py file and this macro must stay together in the project's scripts folder;
models/转换盘1.step must exist. This overwrites the generated FCStd file.
"""
from pathlib import Path
script = Path(__file__).resolve().with_name('build_conversion_disk.py')
exec(compile(script.read_text(encoding='utf-8'), str(script), 'exec'),
{'__name__': '__main__', '__file__': str(script)})

View File

@@ -0,0 +1,5 @@
"""Rebuild the illustrative encoder assembly in the FreeCAD GUI."""
from pathlib import Path
script = Path(__file__).resolve().with_name('build_encoder_concept.py')
exec(compile(script.read_text(encoding='utf-8'), str(script), 'exec'),
{'__name__': '__main__', '__file__': str(script)})

View File

@@ -0,0 +1,3 @@
from pathlib import Path
script = Path(__file__).resolve().with_name('build_encoder_print_parts.py')
exec(compile(script.read_text(encoding='utf-8'), str(script), 'exec'), {'__name__':'__main__', '__file__':str(script)})

View File

@@ -0,0 +1,3 @@
from pathlib import Path
script=Path(__file__).resolve().with_name('build_fan120_mount.py')
exec(compile(script.read_text(encoding='utf8'),str(script),'exec'),{'__name__':'__main__','__file__':str(script)})

View File

@@ -0,0 +1,3 @@
from pathlib import Path
script = Path(__file__).resolve().with_name('build_kfl001_plate_draft.py')
exec(compile(script.read_text(encoding='utf-8'), str(script), 'exec'), {'__name__':'__main__','__file__':str(script)})

View File

@@ -0,0 +1,3 @@
from pathlib import Path
script = Path(__file__).resolve().with_name('build_kfl001_plate_v1.py')
exec(compile(script.read_text(encoding='utf-8'), str(script), 'exec'), {'__name__':'__main__','__file__':str(script)})

Some files were not shown because too many files have changed in this diff Show More