This commit is contained in:
2026-08-14 10:26:28 +08:00
parent f051146486
commit 7d12459a32
44 changed files with 900211 additions and 15295 deletions
File diff suppressed because it is too large Load Diff
@@ -51,7 +51,7 @@ def scale_from_motor(
reducer_axial_allowance_mm: float = 42.0,
min_radial_scale: float = 0.45,
max_radial_scale: float = 1.50,
min_axial_scale: float = 0.55,
min_axial_scale: float = 0.42,
max_axial_scale: float = 1.80,
) -> HousingScale:
target_inner_radius = motor_radial_diameter_mm / 2.0 + radial_clearance_mm
@@ -0,0 +1,255 @@
from pathlib import Path
import math
import cadquery as cq
AXIS = cq.Vector(0.0, 1.0, 0.0)
def _point_on_xz(radius: float, angle_degrees: float, y: float) -> cq.Vector:
angle = math.radians(angle_degrees)
return cq.Vector(radius * math.cos(angle), y, radius * math.sin(angle))
def _cylinder(radius: float, y0: float, length: float, x: float = 0.0, z: float = 0.0):
return cq.Solid.makeCylinder(radius, length, cq.Vector(x, y0, z), AXIS)
def _ring(outer_radius: float, inner_radius: float, y0: float, length: float):
return _cylinder(outer_radius, y0, length).cut(_cylinder(inner_radius, y0, length))
def _conical_ring(
outer_start: float,
outer_end: float,
inner_start: float,
inner_end: float,
y0: float,
length: float,
):
outer = cq.Solid.makeCone(outer_start, outer_end, length, cq.Vector(0.0, y0, 0.0), AXIS)
inner = cq.Solid.makeCone(inner_start, inner_end, length, cq.Vector(0.0, y0, 0.0), AXIS)
return outer.cut(inner)
def _sector_cut(angle_center: float, angle_width: float, y0: float, length: float, radius: float = 100.0):
start = math.radians(angle_center - angle_width / 2.0)
end = math.radians(angle_center + angle_width / 2.0)
points = [
(0.0, 0.0),
(radius * math.cos(start), radius * math.sin(start)),
(radius * math.cos(end), radius * math.sin(end)),
]
plane = cq.Plane(origin=(0.0, y0, 0.0), xDir=(1.0, 0.0, 0.0), normal=(0.0, 1.0, 0.0))
return cq.Workplane(plane).polyline(points).close().extrude(length).val()
def _annular_sector_cut(
angle_center: float,
angle_width: float,
inner_radius: float,
outer_radius: float,
y0: float,
length: float,
profile_fillet_radius: float = 0.0,
axial_fillet_radius: float = 0.0,
):
start = math.radians(angle_center - angle_width / 2.0)
middle = math.radians(angle_center)
end = math.radians(angle_center + angle_width / 2.0)
inner_start = (inner_radius * math.cos(start), inner_radius * math.sin(start))
inner_middle = (inner_radius * math.cos(middle), inner_radius * math.sin(middle))
inner_end = (inner_radius * math.cos(end), inner_radius * math.sin(end))
outer_start = (outer_radius * math.cos(start), outer_radius * math.sin(start))
outer_middle = (outer_radius * math.cos(middle), outer_radius * math.sin(middle))
outer_end = (outer_radius * math.cos(end), outer_radius * math.sin(end))
plane = cq.Plane(origin=(0.0, y0, 0.0), xDir=(1.0, 0.0, 0.0), normal=(0.0, 1.0, 0.0))
cutter = (
cq.Workplane(plane)
.moveTo(*inner_start)
.lineTo(*outer_start)
.threePointArc(outer_middle, outer_end)
.lineTo(*inner_end)
.threePointArc(inner_middle, inner_start)
.close()
.extrude(length)
)
if profile_fillet_radius > 0.0:
# Round the corners of the annular profile (fillet axes parallel to Y).
cutter = cutter.edges("|Y").fillet(profile_fillet_radius)
if axial_fillet_radius > 0.0:
# Round only the straight radial edges at the two axial ends. This
# produces the R10/R5/R2 window run-outs present in the source STEP.
end_edges = [
edge
for edge in cutter.edges("%LINE").vals()
if abs(edge.Center().y - y0) < 1.0e-6
or abs(edge.Center().y - (y0 + length)) < 1.0e-6
]
cutter = cutter.newObject(end_edges).fillet(axial_fillet_radius)
return cutter.val()
def _radial_bar(angle_degrees: float, y0: float, length: float, inner_radius: float, outer_radius: float, width: float):
radial_length = outer_radius - inner_radius
bar = (
cq.Workplane("XY")
.box(radial_length, length, width)
.translate(((inner_radius + outer_radius) / 2.0, y0 + length / 2.0, 0.0))
)
return bar.rotate((0.0, 0.0, 0.0), (0.0, 1.0, 0.0), -angle_degrees).val()
def _rounded_radial_slot(angle_degrees: float, y0: float, length: float):
"""A 40 mm wide radial window with R2 corners in its axial section."""
plane = cq.Plane(
origin=(40.0, y0 + length / 2.0, 0.0),
xDir=(0.0, 1.0, 0.0),
normal=(1.0, 0.0, 0.0),
)
slot = (
cq.Workplane(plane)
.rect(length - 4.0, 36.0)
.offset2D(2.0)
.extrude(42.0)
.rotate((0.0, 0.0, 0.0), (0.0, 1.0, 0.0), -angle_degrees)
)
return slot.val()
def _support_blend(angle_degrees: float, mirrored: bool):
"""One of the six R2 blends between the R64 and R29.2 support arcs."""
sharp = (-62.80715975198, 12.298808230444)
inner_tangent = (-62.1855700706093, 15.131254904777)
lobe_tangent = (-63.6707499640005, 16.8512614004542)
inner_center = (0.0, 0.0)
blend_center = (-61.6824120316291, 17.0669284278203)
lobe_center = (-34.6410161514, 20.0)
def transform(point):
x, z = point
if mirrored:
axis = math.radians(150.0)
ux, uz = math.cos(axis), math.sin(axis)
projection = x * ux + z * uz
x, z = 2.0 * projection * ux - x, 2.0 * projection * uz - z
angle = math.radians(angle_degrees)
return (
x * math.cos(angle) - z * math.sin(angle),
x * math.sin(angle) + z * math.cos(angle),
)
def arc_middle(center, start, end, radius):
ux = (start[0] - center[0]) / radius + (end[0] - center[0]) / radius
uz = (start[1] - center[1]) / radius + (end[1] - center[1]) / radius
scale = radius / math.hypot(ux, uz)
return center[0] + ux * scale, center[1] + uz * scale
points = [transform(point) for point in (sharp, inner_tangent, lobe_tangent)]
centers = [transform(point) for point in (inner_center, blend_center, lobe_center)]
plane = cq.Plane(origin=(0.0, 90.2, 0.0), xDir=(1.0, 0.0, 0.0), normal=(0.0, -1.0, 0.0))
return (
cq.Workplane(plane)
.moveTo(*points[0])
.threePointArc(arc_middle(centers[0], points[0], points[1], 64.0), points[1])
.threePointArc(arc_middle(centers[1], points[1], points[2], 2.0), points[2])
.threePointArc(arc_middle(centers[2], points[2], points[0], 29.2), points[0])
.close()
.extrude(10.0)
.val()
)
def build():
# Front cylindrical shell and its smaller continuation.
part = cq.Workplane(obj=_ring(79.4, 74.4, -10.0, 22.0))
part = part.union(_ring(78.4, 74.4, 12.0, 43.2))
# Nine radial R2 holes through the front lip, spaced every 40 degrees.
for angle in (90.0, 50.0, 10.0, -30.0, -70.0, -110.0, -150.0, -190.0, -230.0):
start = _point_on_xz(72.0, angle, -5.0)
direction = _point_on_xz(1.0, angle, 0.0)
direction = cq.Vector(direction.x, 0.0, direction.z)
part = part.cut(cq.Solid.makeCylinder(2.0, 10.0, start, direction))
# Six long ventilation windows, 40-degree openings with 20-degree ribs.
for center in (-110.0, -50.0, 10.0, 70.0, 130.0, 190.0):
part = part.cut(
_annular_sector_cut(center, 40.0, 70.0, 82.0, 12.0, 40.0, axial_fillet_radius=10.0)
)
# Taper from the large front shell to the narrower middle shell.
part = part.union(_conical_ring(78.4, 73.2, 74.4, 69.2, 55.2, 20.0))
part = part.union(_ring(73.2, 60.0, 75.2, 5.0))
# Three small axial holes in the first shoulder ring.
for angle in (30.0, 150.0, 270.0):
p = _point_on_xz(64.0, angle, 75.2)
part = part.cut(cq.Solid.makeCylinder(2.0, 5.0, p, AXIS))
# Three-lobed support plate: a disk with three large circular openings.
support = cq.Workplane(obj=_ring(73.2, 64.0, 80.2, 10.0))
for x, z in ((34.6410161514, 20.0), (-34.6410161514, 20.0), (0.0, -40.0)):
support = support.cut(_cylinder(29.2, 80.2, 10.0, x, z))
part = part.union(support)
# Thin middle shell and its second, angularly shifted six-window pattern.
part = part.union(_ring(73.2, 69.2, 90.2, 30.0))
for center in (-150.0, -90.0, -30.0, 30.0, 90.0, 150.0):
part = part.cut(
_annular_sector_cut(center, 40.0, 65.0, 76.0, 90.2, 20.0, axial_fillet_radius=5.0)
)
# Rear shell is one continuous 2:7 taper. Its inner cavity changes from
# a steep R69.2 -> R63.6 cone to the matching shallow taper at Y=125.8.
taper_outer = cq.Solid.makeCone(
73.2, 64.91428571428571, 29.0, cq.Vector(0.0, 120.2, 0.0), AXIS
)
taper_inner_a = cq.Solid.makeCone(69.2, 63.6, 5.6, cq.Vector(0.0, 120.2, 0.0), AXIS)
taper_inner_b = cq.Solid.makeCone(63.6, 59.2, 15.4, cq.Vector(0.0, 125.8, 0.0), AXIS)
part = part.union(taper_outer.cut(taper_inner_a).cut(taper_inner_b))
hub = cq.Workplane(obj=_ring(38.0, 26.0, 129.2, 4.0)).union(
_ring(38.0, 30.0, 133.2, 16.0)
)
part = part.union(hub)
# Stop 0.0001 mm before the coincident rear-plate face so OpenCascade
# keeps the six ribs in one valid solid.
for center in (30.0, 90.0, 150.0, 210.0, 270.0, 330.0):
part = part.cut(_rounded_radial_slot(center, 131.2, 9.9999))
# Central bore through the solid rear plate, then its six fan windows.
part = part.cut(_cylinder(30.0, 141.2, 8.0))
for center in (45.0, 105.0, 165.0, 225.0, 285.0, 345.0):
cutter = _annular_sector_cut(
center, 30.0, 38.0, 59.2, 141.2, 8.0, profile_fillet_radius=2.0
)
part = part.cut(cutter)
# These holes pass through both the end plate and the previously added hub.
for angle in (-90.0, -30.0, 30.0, 90.0, 150.0, 210.0):
p = _point_on_xz(34.0, angle, 141.2)
part = part.cut(cq.Solid.makeCylinder(1.81198, 8.0, p, AXIS))
# Add the small support blends last so they cannot interfere with fusing
# the face-connected rear shell and end plate.
for mirrored in (False, True):
for angle in (0.0, 120.0, 240.0):
part = part.union(_support_blend(angle, mirrored))
# Remove only the six 0.0001 mm run-out residuals after the complete part
# is connected. This reaches the exact Y=141.2 boundary without causing
# the earlier coincident-face split into six solids.
for center in (30.0, 90.0, 150.0, 210.0, 270.0, 330.0):
full_slot = _rounded_radial_slot(center, 131.2, 10.0)
stable_slot = _rounded_radial_slot(center, 131.2, 9.9999)
part = part.cut(full_slot.cut(stable_slot))
return part
if __name__ == "__main__":
output = Path(__file__).resolve().with_name("外壳框架.step")
cq.exporters.export(build(), str(output))
print(output)