建立方向盘项目初始版本
This commit is contained in:
172
scripts/build_70.py
Normal file
172
scripts/build_70.py
Normal 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))
|
||||
46
scripts/build_70_m4_drill.FCMacro
Normal file
46
scripts/build_70_m4_drill.FCMacro
Normal 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()
|
||||
38
scripts/build_70_m4_drill_plain.py
Normal file
38
scripts/build_70_m4_drill_plain.py
Normal 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()
|
||||
60
scripts/build_70_m4_threaded.py
Normal file
60
scripts/build_70_m4_threaded.py
Normal 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__'})
|
||||
|
||||
|
||||
|
||||
|
||||
183
scripts/build_all_new_corner_18_concept.FCMacro
Normal file
183
scripts/build_all_new_corner_18_concept.FCMacro
Normal 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)
|
||||
|
||||
|
||||
109
scripts/build_conversion_disk.py
Normal file
109
scripts/build_conversion_disk.py
Normal 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))
|
||||
82
scripts/build_conversion_disk_m4.FCMacro
Normal file
82
scripts/build_conversion_disk_m4.FCMacro
Normal 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()
|
||||
70
scripts/build_conversion_disk_m4_drill.FCMacro
Normal file
70
scripts/build_conversion_disk_m4_drill.FCMacro
Normal 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()
|
||||
65
scripts/build_direct_profiles.FCMacro
Normal file
65
scripts/build_direct_profiles.FCMacro
Normal 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)
|
||||
147
scripts/build_encoder_concept.py
Normal file
147
scripts/build_encoder_concept.py
Normal 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')
|
||||
|
||||
146
scripts/build_encoder_print_parts.py
Normal file
146
scripts/build_encoder_print_parts.py
Normal 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')
|
||||
52
scripts/build_fan120_integer_v2.FCMacro
Normal file
52
scripts/build_fan120_integer_v2.FCMacro
Normal 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)
|
||||
98
scripts/build_fan120_mount.py
Normal file
98
scripts/build_fan120_mount.py
Normal 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
|
||||
103
scripts/build_fan_direct145.FCMacro
Normal file
103
scripts/build_fan_direct145.FCMacro
Normal 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)
|
||||
74
scripts/build_fan_direct_motor_side_125.FCMacro
Normal file
74
scripts/build_fan_direct_motor_side_125.FCMacro
Normal 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)
|
||||
75
scripts/build_fan_direct_motor_side_130.FCMacro
Normal file
75
scripts/build_fan_direct_motor_side_130.FCMacro
Normal 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)
|
||||
|
||||
76
scripts/build_fan_direct_motor_side_140.FCMacro
Normal file
76
scripts/build_fan_direct_motor_side_140.FCMacro
Normal 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)
|
||||
|
||||
|
||||
55
scripts/build_integer_profiles.FCMacro
Normal file
55
scripts/build_integer_profiles.FCMacro
Normal 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)
|
||||
88
scripts/build_kfl001_plate_draft.py
Normal file
88
scripts/build_kfl001_plate_draft.py
Normal 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)
|
||||
88
scripts/build_kfl001_plate_v1.py
Normal file
88
scripts/build_kfl001_plate_v1.py
Normal 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)
|
||||
111
scripts/build_kfl001_plate_v2.py
Normal file
111
scripts/build_kfl001_plate_v2.py
Normal 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)
|
||||
121
scripts/build_kfl001_plate_v3_alt.py
Normal file
121
scripts/build_kfl001_plate_v3_alt.py
Normal 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)
|
||||
127
scripts/build_kfl_100_concept.py
Normal file
127
scripts/build_kfl_100_concept.py
Normal 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
|
||||
145
scripts/build_kfl_125_switch60.py
Normal file
145
scripts/build_kfl_125_switch60.py
Normal 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
|
||||
30
scripts/build_kfl_150_switch60.py
Normal file
30
scripts/build_kfl_150_switch60.py
Normal 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)})
|
||||
99
scripts/build_kfl_3030_concept.py
Normal file
99
scripts/build_kfl_3030_concept.py
Normal 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)
|
||||
160
scripts/build_kfl_compact80_concept.py
Normal file
160
scripts/build_kfl_compact80_concept.py
Normal 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
|
||||
106
scripts/build_kfl_double_crossbar_concept.py
Normal file
106
scripts/build_kfl_double_crossbar_concept.py
Normal 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)
|
||||
127
scripts/build_kfl_flange57.py
Normal file
127
scripts/build_kfl_flange57.py
Normal 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
|
||||
128
scripts/build_kfl_flange57_shift.py
Normal file
128
scripts/build_kfl_flange57_shift.py
Normal 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
|
||||
155
scripts/build_kfl_plate_adjust_concept.py
Normal file
155
scripts/build_kfl_plate_adjust_concept.py
Normal 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
|
||||
118
scripts/build_kfl_plate_b38_concept.py
Normal file
118
scripts/build_kfl_plate_b38_concept.py
Normal 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
|
||||
138
scripts/build_kfl_same_flange.py
Normal file
138
scripts/build_kfl_same_flange.py
Normal 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
|
||||
138
scripts/build_kfl_same_flange_125.py
Normal file
138
scripts/build_kfl_same_flange_125.py
Normal 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
134
scripts/build_mad_m9c12.py
Normal 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)
|
||||
71
scripts/build_motor_base_corner.py
Normal file
71
scripts/build_motor_base_corner.py
Normal 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)
|
||||
110
scripts/build_new_motor_corners_1mm_concept.FCMacro
Normal file
110
scripts/build_new_motor_corners_1mm_concept.FCMacro
Normal 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)
|
||||
120
scripts/build_new_motor_corners_2mm_concept.FCMacro
Normal file
120
scripts/build_new_motor_corners_2mm_concept.FCMacro
Normal 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)
|
||||
|
||||
|
||||
116
scripts/build_open_encoder_option.py
Normal file
116
scripts/build_open_encoder_option.py
Normal 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
|
||||
147
scripts/build_pcd50_fit_concept.py
Normal file
147
scripts/build_pcd50_fit_concept.py
Normal 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
|
||||
135
scripts/build_pcd50_full_print_plate.py
Normal file
135
scripts/build_pcd50_full_print_plate.py
Normal 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))
|
||||
128
scripts/build_pcd50_open_pillar.py
Normal file
128
scripts/build_pcd50_open_pillar.py
Normal 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
|
||||
159
scripts/build_pcd50_pillar_concept.py
Normal file
159
scripts/build_pcd50_pillar_concept.py
Normal 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
|
||||
134
scripts/build_pcd50_print_plate.py
Normal file
134
scripts/build_pcd50_print_plate.py
Normal 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))
|
||||
68
scripts/build_profile_encoder_option.py
Normal file
68
scripts/build_profile_encoder_option.py
Normal 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')
|
||||
131
scripts/build_profile_mount_check.py
Normal file
131
scripts/build_profile_mount_check.py
Normal 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
|
||||
106
scripts/build_shared76_concept.py
Normal file
106
scripts/build_shared76_concept.py
Normal 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
|
||||
165
scripts/build_shared76_outward_check.py
Normal file
165
scripts/build_shared76_outward_check.py
Normal 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
|
||||
110
scripts/build_shared76_print.py
Normal file
110
scripts/build_shared76_print.py
Normal 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
|
||||
110
scripts/build_shop_sprues.py
Normal file
110
scripts/build_shop_sprues.py
Normal 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))
|
||||
132
scripts/build_square_open_encoder.py
Normal file
132
scripts/build_square_open_encoder.py
Normal 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
|
||||
76
scripts/build_switch2020_concept.FCMacro
Normal file
76
scripts/build_switch2020_concept.FCMacro
Normal 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)
|
||||
87
scripts/build_switch2020_raise10.FCMacro
Normal file
87
scripts/build_switch2020_raise10.FCMacro
Normal 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)
|
||||
63
scripts/build_switch_crossbar_132.FCMacro
Normal file
63
scripts/build_switch_crossbar_132.FCMacro
Normal 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)
|
||||
184
scripts/build_symmetric_corner_18_concept.FCMacro
Normal file
184
scripts/build_symmetric_corner_18_concept.FCMacro
Normal 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)
|
||||
|
||||
|
||||
|
||||
45
scripts/build_symmetric_corner_part.FCMacro
Normal file
45
scripts/build_symmetric_corner_part.FCMacro
Normal 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)
|
||||
77
scripts/build_turn_signal_cam_v2.py
Normal file
77
scripts/build_turn_signal_cam_v2.py
Normal 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
|
||||
89
scripts/build_washer_grid_4x4.py
Normal file
89
scripts/build_washer_grid_4x4.py
Normal 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))
|
||||
68
scripts/build_washer_panel_4x2.py
Normal file
68
scripts/build_washer_panel_4x2.py
Normal 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))
|
||||
|
||||
67
scripts/build_washer_panel_4x4.py
Normal file
67
scripts/build_washer_panel_4x4.py
Normal 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))
|
||||
49
scripts/build_washer_two_panels.py
Normal file
49
scripts/build_washer_two_panels.py
Normal 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))
|
||||
87
scripts/compose_all_new_corner_18.py
Normal file
87
scripts/compose_all_new_corner_18.py
Normal 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')
|
||||
32
scripts/compose_corner_1mm.py
Normal file
32
scripts/compose_corner_1mm.py
Normal 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')
|
||||
33
scripts/compose_corner_2mm.py
Normal file
33
scripts/compose_corner_2mm.py
Normal 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')
|
||||
|
||||
14
scripts/compose_crossbar_132.py
Normal file
14
scripts/compose_crossbar_132.py
Normal 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')
|
||||
23
scripts/compose_direct_profiles.py
Normal file
23
scripts/compose_direct_profiles.py
Normal 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')
|
||||
22
scripts/compose_integer_profiles.py
Normal file
22
scripts/compose_integer_profiles.py
Normal 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')
|
||||
27
scripts/compose_kfl_100_concept.py
Normal file
27
scripts/compose_kfl_100_concept.py
Normal 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')
|
||||
31
scripts/compose_kfl_125_switch60.py
Normal file
31
scripts/compose_kfl_125_switch60.py
Normal 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')
|
||||
34
scripts/compose_kfl_150_switch60.py
Normal file
34
scripts/compose_kfl_150_switch60.py
Normal 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')
|
||||
22
scripts/compose_kfl_3030.py
Normal file
22
scripts/compose_kfl_3030.py
Normal 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')
|
||||
31
scripts/compose_kfl_compact80_concept.py
Normal file
31
scripts/compose_kfl_compact80_concept.py
Normal 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')
|
||||
23
scripts/compose_kfl_double_crossbar.py
Normal file
23
scripts/compose_kfl_double_crossbar.py
Normal 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')
|
||||
35
scripts/compose_kfl_flange57.py
Normal file
35
scripts/compose_kfl_flange57.py
Normal 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')
|
||||
27
scripts/compose_kfl_plate_adjust_concept.py
Normal file
27
scripts/compose_kfl_plate_adjust_concept.py
Normal 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')
|
||||
35
scripts/compose_kfl_plate_b38_concept.py
Normal file
35
scripts/compose_kfl_plate_b38_concept.py
Normal 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')
|
||||
33
scripts/compose_kfl_same_flange.py
Normal file
33
scripts/compose_kfl_same_flange.py
Normal 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')
|
||||
35
scripts/compose_kfl_same_flange_125.py
Normal file
35
scripts/compose_kfl_same_flange_125.py
Normal 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')
|
||||
27
scripts/compose_long_angle_concept.py
Normal file
27
scripts/compose_long_angle_concept.py
Normal 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')
|
||||
25
scripts/compose_motor_base_corner.py
Normal file
25
scripts/compose_motor_base_corner.py
Normal 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')
|
||||
24
scripts/compose_switch2020.py
Normal file
24
scripts/compose_switch2020.py
Normal 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')
|
||||
24
scripts/compose_switch2020_raise10.py
Normal file
24
scripts/compose_switch2020_raise10.py
Normal 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')
|
||||
15
scripts/compose_turn_signal_cam_v2.py
Normal file
15
scripts/compose_turn_signal_cam_v2.py
Normal 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')
|
||||
16
scripts/compose_turn_signal_concept.py
Normal file
16
scripts/compose_turn_signal_concept.py
Normal 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')
|
||||
21
scripts/finish_washer_panel_4x2_gui.FCMacro
Normal file
21
scripts/finish_washer_panel_4x2_gui.FCMacro
Normal 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)
|
||||
|
||||
20
scripts/finish_washer_panel_gui.FCMacro
Normal file
20
scripts/finish_washer_panel_gui.FCMacro
Normal 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)
|
||||
17
scripts/fix_washer_two_panel_gui.FCMacro
Normal file
17
scripts/fix_washer_two_panel_gui.FCMacro
Normal 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)
|
||||
8
scripts/inspect_mount.FCmacro
Normal file
8
scripts/inspect_mount.FCmacro
Normal 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)
|
||||
47
scripts/make_kfl_125_switch60.ps1
Normal file
47
scripts/make_kfl_125_switch60.ps1
Normal 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))
|
||||
31
scripts/make_plate_v3_alt.py
Normal file
31
scripts/make_plate_v3_alt.py
Normal 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')
|
||||
24
scripts/make_plate_v3_render_alt.py
Normal file
24
scripts/make_plate_v3_render_alt.py
Normal 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')
|
||||
42
scripts/prepare_kfl001_plate_v1.py
Normal file
42
scripts/prepare_kfl001_plate_v1.py
Normal 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')
|
||||
14
scripts/rebuild_70_m4_drill_plain.FCMacro
Normal file
14
scripts/rebuild_70_m4_drill_plain.FCMacro
Normal 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()
|
||||
13
scripts/rebuild_70_m4_threaded.FCMacro
Normal file
13
scripts/rebuild_70_m4_threaded.FCMacro
Normal 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()
|
||||
|
||||
10
scripts/rebuild_conversion_disk.FCMacro
Normal file
10
scripts/rebuild_conversion_disk.FCMacro
Normal 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)})
|
||||
5
scripts/rebuild_encoder_concept.FCMacro
Normal file
5
scripts/rebuild_encoder_concept.FCMacro
Normal 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)})
|
||||
3
scripts/rebuild_encoder_print_parts.FCMacro
Normal file
3
scripts/rebuild_encoder_print_parts.FCMacro
Normal 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)})
|
||||
3
scripts/rebuild_fan120_mount.FCMacro
Normal file
3
scripts/rebuild_fan120_mount.FCMacro
Normal 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)})
|
||||
3
scripts/rebuild_kfl001_plate_draft.FCMacro
Normal file
3
scripts/rebuild_kfl001_plate_draft.FCMacro
Normal 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)})
|
||||
3
scripts/rebuild_kfl001_plate_v1.FCMacro
Normal file
3
scripts/rebuild_kfl001_plate_v1.FCMacro
Normal 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
Reference in New Issue
Block a user