Split project into frontend and backend
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
import math
|
||||
|
||||
import cadquery as cq
|
||||
|
||||
|
||||
# All dimensions are millimetres. The main body is a revolved radial section.
|
||||
INNER_BOTTOM_RADIUS = 60.0
|
||||
INNER_STEP_RADIUS = 57.5
|
||||
INNER_SHOULDER_RADIUS = 70.0
|
||||
INNER_WALL_RADIUS = 79.0
|
||||
BOTTOM_OUTER_RADIUS = 77.5
|
||||
BODY_OUTER_RADIUS = 82.5
|
||||
TOP_OUTER_RADIUS = 85.0
|
||||
BASE_MIN_Z = -18.0
|
||||
BASE_MAX_Z = 92.0
|
||||
BASE_HEIGHT = BASE_MAX_Z - BASE_MIN_Z
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class HousingScale:
|
||||
radial_scale: float = 1.0
|
||||
axial_scale: float = 1.0
|
||||
source: str = "default_template_scale"
|
||||
|
||||
|
||||
def scaled_dimensions(scale: HousingScale) -> dict[str, float]:
|
||||
return {
|
||||
"inner_bottom_radius": INNER_BOTTOM_RADIUS * scale.radial_scale,
|
||||
"inner_step_radius": INNER_STEP_RADIUS * scale.radial_scale,
|
||||
"inner_shoulder_radius": INNER_SHOULDER_RADIUS * scale.radial_scale,
|
||||
"inner_wall_radius": INNER_WALL_RADIUS * scale.radial_scale,
|
||||
"bottom_outer_radius": BOTTOM_OUTER_RADIUS * scale.radial_scale,
|
||||
"body_outer_radius": BODY_OUTER_RADIUS * scale.radial_scale,
|
||||
"top_outer_radius": TOP_OUTER_RADIUS * scale.radial_scale,
|
||||
"min_z": BASE_MIN_Z * scale.axial_scale,
|
||||
"max_z": BASE_MAX_Z * scale.axial_scale,
|
||||
"height": BASE_HEIGHT * scale.axial_scale,
|
||||
"radial_scale": scale.radial_scale,
|
||||
"axial_scale": scale.axial_scale,
|
||||
"source": scale.source,
|
||||
}
|
||||
|
||||
|
||||
def scale_from_motor(
|
||||
*,
|
||||
motor_radial_diameter_mm: float,
|
||||
motor_axis_length_mm: float,
|
||||
radial_clearance_mm: float = 14.0,
|
||||
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,
|
||||
max_axial_scale: float = 1.80,
|
||||
) -> HousingScale:
|
||||
target_inner_radius = motor_radial_diameter_mm / 2.0 + radial_clearance_mm
|
||||
radial_scale = target_inner_radius / INNER_WALL_RADIUS
|
||||
target_height = motor_axis_length_mm + reducer_axial_allowance_mm
|
||||
axial_scale = target_height / BASE_HEIGHT
|
||||
return HousingScale(
|
||||
radial_scale=max(min_radial_scale, min(max_radial_scale, radial_scale)),
|
||||
axial_scale=max(min_axial_scale, min(max_axial_scale, axial_scale)),
|
||||
source="computed_from_motor_bbox",
|
||||
)
|
||||
|
||||
|
||||
def _polar_points(radius: float, start_degrees: float, count: int):
|
||||
return [
|
||||
(
|
||||
radius * math.cos(math.radians(start_degrees + 360.0 * index / count)),
|
||||
radius * math.sin(math.radians(start_degrees + 360.0 * index / count)),
|
||||
)
|
||||
for index in range(count)
|
||||
]
|
||||
|
||||
|
||||
def _radial_point(radius: float, angle_degrees: float, z: float) -> cq.Vector:
|
||||
angle = math.radians(angle_degrees)
|
||||
return cq.Vector(radius * math.cos(angle), radius * math.sin(angle), z)
|
||||
|
||||
|
||||
def _radial_direction(angle_degrees: float) -> cq.Vector:
|
||||
angle = math.radians(angle_degrees)
|
||||
return cq.Vector(math.cos(angle), math.sin(angle), 0.0)
|
||||
|
||||
|
||||
def build_body(scale: HousingScale = HousingScale()):
|
||||
rd = scale.radial_scale
|
||||
zd = scale.axial_scale
|
||||
|
||||
def r(value: float) -> float:
|
||||
return value * rd
|
||||
|
||||
def z(value: float) -> float:
|
||||
return value * zd
|
||||
|
||||
# Exact circular-arc midpoints for the two revolved outer blends.
|
||||
lower_arc_mid = (r(78.8397459621556), z(-8.66025403784439))
|
||||
upper_arc_mid = (r(83.1458565330652), z(71.9211556282713))
|
||||
|
||||
radial_section = (
|
||||
cq.Workplane("XZ")
|
||||
.moveTo(r(INNER_BOTTOM_RADIUS), z(-18.0))
|
||||
.lineTo(r(BOTTOM_OUTER_RADIUS), z(-18.0))
|
||||
.lineTo(r(BOTTOM_OUTER_RADIUS), z(-13.6602540378))
|
||||
.threePointArc(lower_arc_mid, (r(BODY_OUTER_RADIUS), z(-5.0)))
|
||||
.lineTo(r(BODY_OUTER_RADIUS), z(68.3856217223))
|
||||
.threePointArc(upper_arc_mid, (r(TOP_OUTER_RADIUS), z(75.0)))
|
||||
.lineTo(r(TOP_OUTER_RADIUS), z(92.0))
|
||||
.lineTo(r(INNER_WALL_RADIUS), z(92.0))
|
||||
.lineTo(r(INNER_WALL_RADIUS), z(0.0))
|
||||
.lineTo(r(INNER_SHOULDER_RADIUS), z(0.0))
|
||||
.lineTo(r(INNER_SHOULDER_RADIUS), z(-3.0))
|
||||
.lineTo(r(INNER_STEP_RADIUS), z(-3.0))
|
||||
.lineTo(r(INNER_STEP_RADIUS), z(-5.0))
|
||||
.lineTo(r(INNER_BOTTOM_RADIUS), z(-5.0))
|
||||
.close()
|
||||
)
|
||||
return radial_section.revolve(360.0, (0.0, 0.0), (0.0, 1.0))
|
||||
|
||||
|
||||
def build(scale: HousingScale = HousingScale()):
|
||||
rd = scale.radial_scale
|
||||
zd = scale.axial_scale
|
||||
|
||||
def r(value: float) -> float:
|
||||
return value * rd
|
||||
|
||||
def z(value: float) -> float:
|
||||
return value * zd
|
||||
|
||||
part = build_body(scale)
|
||||
|
||||
# Ten Ø5 blind drilled holes, equally spaced on a Ø135 pitch circle.
|
||||
drill_tip_height = z(1.5021515475689)
|
||||
for x, y in _polar_points(r(67.5), 18.0, 10):
|
||||
straight_hole = cq.Solid.makeCylinder(
|
||||
r(2.5), z(10.0), cq.Vector(x, y, z(-18.0)), cq.Vector(0.0, 0.0, 1.0)
|
||||
)
|
||||
drill_tip = cq.Solid.makeCone(
|
||||
r(2.5),
|
||||
0.0,
|
||||
drill_tip_height,
|
||||
cq.Vector(x, y, z(-8.0)),
|
||||
cq.Vector(0.0, 0.0, 1.0),
|
||||
)
|
||||
part = part.cut(straight_hole).cut(drill_tip)
|
||||
|
||||
# Four Ø2.5 blind holes opening at z=0, with 118-degree drill tips.
|
||||
small_tip_height = z(0.75107577378445)
|
||||
for x, y in _polar_points(r(71.875), 0.0, 4):
|
||||
straight_hole = cq.Solid.makeCylinder(
|
||||
r(1.25), z(4.24892422621555), cq.Vector(x, y, z(0.0)), cq.Vector(0.0, 0.0, -1.0)
|
||||
)
|
||||
drill_tip = cq.Solid.makeCone(
|
||||
r(1.25),
|
||||
0.0,
|
||||
small_tip_height,
|
||||
cq.Vector(x, y, z(-4.24892422621555)),
|
||||
cq.Vector(0.0, 0.0, -1.0),
|
||||
)
|
||||
part = part.cut(straight_hole).cut(drill_tip)
|
||||
|
||||
# Eight radial Ø6.6 holes with 90-degree countersinks around the top rim.
|
||||
for angle in range(0, 360, 45):
|
||||
direction = _radial_direction(float(angle))
|
||||
straight_hole = cq.Solid.makeCylinder(
|
||||
r(3.3), r(3.9), _radial_point(r(78.0), float(angle), z(85.1)), direction
|
||||
)
|
||||
countersink = cq.Solid.makeCone(
|
||||
r(3.3),
|
||||
r(6.4),
|
||||
r(3.1),
|
||||
_radial_point(r(81.9), float(angle), z(85.1)),
|
||||
direction,
|
||||
)
|
||||
part = part.cut(straight_hole).cut(countersink)
|
||||
|
||||
return part
|
||||
|
||||
|
||||
def export_scaled(output: Path, scale: HousingScale) -> None:
|
||||
cq.exporters.export(build(scale), str(output))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
output = Path(__file__).resolve().with_name("gear_housing.step")
|
||||
export_scaled(output, HousingScale())
|
||||
print(output)
|
||||
@@ -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
Reference in New Issue
Block a user