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
File diff suppressed because it is too large Load Diff
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)
File diff suppressed because one or more lines are too long
@@ -0,0 +1,6 @@
{
"platform_id": "actuator_v2_fixed_platform",
"motor_step": "../motors/3500_motor_part2_standard.step",
"housing_step": "../housings/actuator_v2_knee_abd_main_case_2.step",
"reference_assembly_step": "../references/actuator_v2_knee_abd_reference.step"
}
File diff suppressed because one or more lines are too long
@@ -1,14 +1,14 @@
{
"module_type": "planetary_joint_module",
"motor_step": "../../assets/motors/3500_motor_part2.step",
"housing_step": "../../assets/housings/generated_housing_baseline.step",
"reducer_requirement": "../reducers/simple_2k_h/simple_2kh_industrial_ratio_7p5_spur.json",
"platform_profile": "../../assets/platforms/actuator_v2_fixed_platform.json",
"reducer_requirement": "../reducers/simple_2k_h/simple_2kh_industrial_ratio_7p5_spur_compact.json",
"placement_policy": "infer_then_write_editable_json",
"motor_to_reducer_relation": "motor_output_to_sun_keyed_input",
"housing_relation": "ring_fixed_to_housing",
"joint_output": "carrier_output_shaft",
"output_extension_mm": 8.0,
"coupling_clearance_mm": 1.0,
"motor_placement_policy": "external_flush_input_end",
"output_extension_mm": 2.0,
"coupling_clearance_mm": 0.5,
"input_shaft_insertion_depth_mm": 4.0,
"motor_output_side": "axis_max",
"axis_angle_tolerance_deg": 0.5,
@@ -1,15 +1,15 @@
{
"module_type": "planetary_joint_module",
"motor_step": "../../assets/motors/3500_motor_part2.step",
"housing_step": "../../assets/housings/generated_housing_baseline.step",
"reducer_requirement": "../reducers/simple_2k_h_cascade/simple_2kh_cascade_ratio_9_spur.json",
"platform_profile": "../../assets/platforms/actuator_v2_fixed_platform.json",
"reducer_requirement": "../reducers/simple_2k_h_cascade/simple_2kh_cascade_ratio_9_spur_compact.json",
"placement_policy": "infer_then_write_editable_json",
"motor_to_reducer_relation": "motor_output_to_s1_sun_input",
"housing_relation": "stage_rings_fixed_to_housing",
"joint_output": "s2_carrier_output",
"motor_placement_policy": "external_flush_input_end",
"output_extension_mm": 2.0,
"coupling_clearance_mm": 0.5,
"input_shaft_insertion_depth_mm": 1.0,
"input_shaft_insertion_depth_mm": 4.0,
"motor_output_side": "axis_max",
"axis_angle_tolerance_deg": 0.5,
"axis_distance_tolerance_mm": 0.25,
@@ -0,0 +1,26 @@
{
"target_ratio": 7.5,
"ratio_tolerance": 0.01,
"topology_family": "simple_2k_h",
"detail_level": "industrial_core",
"input": "sun",
"fixed": "ring",
"output": "carrier",
"tooth_form": "spur",
"planet_count_candidates": [3],
"module_candidates_mm": [0.3],
"pressure_angle_deg": 20.0,
"face_width_mm": 6.0,
"backlash_mm": 0.03,
"addendum_coeff": 1.0,
"clearance_coeff": 0.25,
"ring_rim_thickness_mm": 1.5,
"constraints": {
"max_outer_diameter_mm": 60.0,
"min_planet_neighbor_clearance_mm": 0.3,
"min_sun_teeth": 20,
"max_sun_teeth": 20,
"min_planet_teeth": 55,
"max_planet_teeth": 55
}
}
@@ -0,0 +1,44 @@
{
"target_ratio": 9.0,
"ratio_tolerance": 0.000001,
"topology_family": "simple_2k_h_cascade",
"tooth_form": "spur",
"planet_count_candidates": [3],
"module_candidates_mm": [0.5],
"pressure_angle_deg": 20.0,
"face_width_mm": 6.0,
"backlash_mm": 0.03,
"addendum_coeff": 1.0,
"clearance_coeff": 0.25,
"ring_rim_thickness_mm": 2.5,
"constraints": {
"max_outer_diameter_mm": 100.0,
"min_planet_neighbor_clearance_mm": 0.3,
"min_sun_teeth": 36,
"max_sun_teeth": 36,
"min_planet_teeth": 18,
"max_planet_teeth": 18
},
"stages": [
{
"stage_id": "s1",
"input": "sun",
"fixed": "ring",
"output": "carrier",
"planet_count_candidates": [3],
"module_candidates_mm": [0.5]
},
{
"stage_id": "s2",
"input": "sun",
"fixed": "ring",
"output": "carrier",
"planet_count_candidates": [3],
"module_candidates_mm": [0.5]
}
],
"cascade": {
"coupling_policy": "stage_output_to_next_stage_input",
"stage_gap_mm": 2.0
}
}