Import CAD projects and cad-router v1
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
# Benchmark Models
|
||||
|
||||
Benchmark CAD fixtures for repeatable geometry generation, import/export, and
|
||||
viewer behavior checks. Each benchmark is intentionally more structured than the
|
||||
small examples in `../simple/` and is meant to exercise a distinct modeling
|
||||
surface.
|
||||
|
||||
## Contents
|
||||
|
||||
- `benchmark_*.py`: build123d generator source for each benchmark.
|
||||
- `benchmark_*.step`: canonical generated STEP output for each benchmark.
|
||||
- `.benchmark_*.step.glb`: generated render/selector sidecar paired with the
|
||||
STEP file.
|
||||
- `benchmark_common.py`: shared helper functions for benchmark generators.
|
||||
- `validate_benchmark.py`: local benchmark validation helper.
|
||||
|
||||
## Coverage
|
||||
|
||||
The benchmark set covers calibration blocks, flanges, brackets, shafts,
|
||||
enclosures, clevises, engine-like fins, impellers, spiral stairs, and planetary
|
||||
gear stages.
|
||||
|
||||
Keep benchmark examples here when they are intended as regression fixtures or
|
||||
represent a richer version of a simpler shape.
|
||||
@@ -0,0 +1,31 @@
|
||||
from build123d import Align, Box, BuildPart, Cylinder, Location, Locations, Mode
|
||||
|
||||
from benchmark_common import line_edges_at_z, safe_chamfer
|
||||
|
||||
|
||||
LENGTH = 100.0
|
||||
WIDTH = 60.0
|
||||
HEIGHT = 20.0
|
||||
HOLE_DIAMETER = 8.0
|
||||
HOLE_LOCATIONS = ((-35.0, -20.0), (-35.0, 20.0), (35.0, -20.0), (35.0, 20.0))
|
||||
TOP_CHAMFER = 2.0
|
||||
|
||||
|
||||
def gen_step():
|
||||
"""Return the rectangular calibration block benchmark model in millimeters."""
|
||||
with BuildPart() as block:
|
||||
Box(LENGTH, WIDTH, HEIGHT, align=(Align.CENTER, Align.CENTER, Align.MIN))
|
||||
|
||||
for x_pos, y_pos in HOLE_LOCATIONS:
|
||||
with Locations(Location((x_pos, y_pos, -1.0))):
|
||||
Cylinder(
|
||||
radius=HOLE_DIAMETER / 2.0,
|
||||
height=HEIGHT + 2.0,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
mode=Mode.SUBTRACT,
|
||||
)
|
||||
|
||||
part = block.part
|
||||
part = safe_chamfer(part, line_edges_at_z(part, HEIGHT), length=TOP_CHAMFER)
|
||||
part.label = "benchmark_01_rectangular_calibration_block"
|
||||
return part
|
||||
@@ -0,0 +1,50 @@
|
||||
from math import tau
|
||||
|
||||
from build123d import Align, BuildPart, Cylinder, Location, Locations, Mode
|
||||
|
||||
from benchmark_common import circular_edges, polar_point, safe_fillet
|
||||
|
||||
|
||||
OUTER_DIAMETER = 80.0
|
||||
THICKNESS = 10.0
|
||||
CENTRAL_BORE_DIAMETER = 30.0
|
||||
BOLT_HOLE_COUNT = 6
|
||||
BOLT_HOLE_DIAMETER = 6.0
|
||||
BOLT_CIRCLE_DIAMETER = 60.0
|
||||
OUTER_FILLET = 1.5
|
||||
|
||||
|
||||
def gen_step():
|
||||
"""Return the circular flange benchmark model in millimeters."""
|
||||
with BuildPart() as flange:
|
||||
Cylinder(
|
||||
radius=OUTER_DIAMETER / 2.0,
|
||||
height=THICKNESS,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
)
|
||||
|
||||
with Locations(Location((0.0, 0.0, -1.0))):
|
||||
Cylinder(
|
||||
radius=CENTRAL_BORE_DIAMETER / 2.0,
|
||||
height=THICKNESS + 2.0,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
mode=Mode.SUBTRACT,
|
||||
)
|
||||
|
||||
for index in range(BOLT_HOLE_COUNT):
|
||||
x_pos, y_pos = polar_point(BOLT_CIRCLE_DIAMETER / 2.0, tau * index / BOLT_HOLE_COUNT)
|
||||
with Locations(Location((x_pos, y_pos, -1.0))):
|
||||
Cylinder(
|
||||
radius=BOLT_HOLE_DIAMETER / 2.0,
|
||||
height=THICKNESS + 2.0,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
mode=Mode.SUBTRACT,
|
||||
)
|
||||
|
||||
part = flange.part
|
||||
edges = []
|
||||
edges.extend(circular_edges(part, radius=OUTER_DIAMETER / 2.0, axis="z", coordinate=0.0))
|
||||
edges.extend(circular_edges(part, radius=OUTER_DIAMETER / 2.0, axis="z", coordinate=THICKNESS))
|
||||
part = safe_fillet(part, edges, radius=OUTER_FILLET)
|
||||
part.label = "benchmark_02_circular_flange_bolt_pattern"
|
||||
return part
|
||||
@@ -0,0 +1,113 @@
|
||||
from build123d import (
|
||||
Align,
|
||||
Box,
|
||||
BuildPart,
|
||||
BuildSketch,
|
||||
Cylinder,
|
||||
Location,
|
||||
Locations,
|
||||
Mode,
|
||||
Plane,
|
||||
Polygon,
|
||||
add,
|
||||
extrude,
|
||||
)
|
||||
|
||||
from benchmark_common import safe_fillet
|
||||
|
||||
|
||||
BASE_LENGTH = 80.0
|
||||
BASE_WIDTH = 50.0
|
||||
BASE_THICKNESS = 8.0
|
||||
|
||||
BACK_LENGTH = 80.0
|
||||
BACK_THICKNESS = 8.0
|
||||
BACK_HEIGHT = 50.0
|
||||
BACK_CENTER_Y = BASE_WIDTH / 2.0 - BACK_THICKNESS / 2.0
|
||||
|
||||
HOLE_DIAMETER = 6.0
|
||||
BASE_HOLE_LOCATIONS = ((-25.0, -10.0), (25.0, -10.0))
|
||||
BACK_HOLE_LOCATIONS = ((-25.0, 30.0), (25.0, 30.0))
|
||||
|
||||
GUSSET_X_THICKNESS = 8.0
|
||||
GUSSET_X_LOCATIONS = (-20.0, 20.0)
|
||||
GUSSET_DEPTH = 30.0
|
||||
GUSSET_HEIGHT = 30.0
|
||||
TRANSITION_FILLET = 2.0
|
||||
|
||||
|
||||
def _make_gusset(x_center: float):
|
||||
back_front_y = BACK_CENTER_Y - BACK_THICKNESS / 2.0
|
||||
front_y = back_front_y - GUSSET_DEPTH
|
||||
top_z = BASE_THICKNESS + GUSSET_HEIGHT
|
||||
|
||||
with BuildPart() as gusset:
|
||||
with BuildSketch(Plane.YZ):
|
||||
Polygon(
|
||||
[
|
||||
(back_front_y, BASE_THICKNESS),
|
||||
(front_y, BASE_THICKNESS),
|
||||
(back_front_y, top_z),
|
||||
],
|
||||
align=None,
|
||||
)
|
||||
extrude(amount=GUSSET_X_THICKNESS)
|
||||
|
||||
part = gusset.part.moved(Location((x_center - GUSSET_X_THICKNESS / 2.0, 0.0, 0.0)))
|
||||
part.label = f"gusset_x_{x_center:.0f}"
|
||||
return part
|
||||
|
||||
|
||||
def _transition_edges(part):
|
||||
edges = []
|
||||
for edge in part.edges():
|
||||
bbox = edge.bounding_box()
|
||||
if str(edge.geom_type).endswith("LINE"):
|
||||
if abs(bbox.min.Z - BASE_THICKNESS) < 0.05 and abs(bbox.max.Z - BASE_THICKNESS) < 0.05:
|
||||
center_y = bbox.center().Y
|
||||
if BACK_CENTER_Y - BACK_THICKNESS / 2.0 - 0.05 <= center_y <= BASE_WIDTH / 2.0 + 0.05:
|
||||
if bbox.size.X > 20.0:
|
||||
edges.append(edge)
|
||||
return edges
|
||||
|
||||
|
||||
def gen_step():
|
||||
"""Return the L-bracket benchmark model in millimeters."""
|
||||
with BuildPart() as bracket:
|
||||
Box(BASE_LENGTH, BASE_WIDTH, BASE_THICKNESS, align=(Align.CENTER, Align.CENTER, Align.MIN))
|
||||
|
||||
with Locations(Location((0.0, BACK_CENTER_Y, BASE_THICKNESS))):
|
||||
Box(
|
||||
BACK_LENGTH,
|
||||
BACK_THICKNESS,
|
||||
BACK_HEIGHT,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
mode=Mode.ADD,
|
||||
)
|
||||
|
||||
for x_pos, y_pos in BASE_HOLE_LOCATIONS:
|
||||
with Locations(Location((x_pos, y_pos, -1.0))):
|
||||
Cylinder(
|
||||
radius=HOLE_DIAMETER / 2.0,
|
||||
height=BASE_THICKNESS + 2.0,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
mode=Mode.SUBTRACT,
|
||||
)
|
||||
|
||||
for x_pos, z_pos in BACK_HOLE_LOCATIONS:
|
||||
with Locations(Location((x_pos, BACK_CENTER_Y, z_pos))):
|
||||
Cylinder(
|
||||
radius=HOLE_DIAMETER / 2.0,
|
||||
height=BACK_THICKNESS + 2.0,
|
||||
rotation=(90.0, 0.0, 0.0),
|
||||
align=(Align.CENTER, Align.CENTER, Align.CENTER),
|
||||
mode=Mode.SUBTRACT,
|
||||
)
|
||||
|
||||
for x_center in GUSSET_X_LOCATIONS:
|
||||
add(_make_gusset(x_center), mode=Mode.ADD)
|
||||
|
||||
part = bracket.part
|
||||
part = safe_fillet(part, _transition_edges(part), radius=TRANSITION_FILLET)
|
||||
part.label = "benchmark_03_l_bracket_gussets_two_hole_directions"
|
||||
return part
|
||||
@@ -0,0 +1,53 @@
|
||||
from build123d import Align, Box, BuildPart, Cylinder, Location, Locations, Mode
|
||||
|
||||
from benchmark_common import circular_edges, safe_chamfer
|
||||
|
||||
|
||||
TOTAL_LENGTH = 120.0
|
||||
LEFT_LENGTH = 30.0
|
||||
MIDDLE_LENGTH = 60.0
|
||||
RIGHT_LENGTH = 30.0
|
||||
SMALL_DIAMETER = 20.0
|
||||
MIDDLE_DIAMETER = 30.0
|
||||
END_CHAMFER = 1.0
|
||||
|
||||
KEYWAY_X_START = 40.0
|
||||
KEYWAY_X_END = 80.0
|
||||
KEYWAY_WIDTH = 6.0
|
||||
KEYWAY_DEPTH = 3.0
|
||||
|
||||
|
||||
def _add_shaft_section(x_start: float, x_end: float, diameter: float):
|
||||
with Locations(Location(((x_start + x_end) / 2.0, 0.0, 0.0))):
|
||||
Cylinder(
|
||||
radius=diameter / 2.0,
|
||||
height=x_end - x_start,
|
||||
rotation=(0.0, 90.0, 0.0),
|
||||
align=(Align.CENTER, Align.CENTER, Align.CENTER),
|
||||
mode=Mode.ADD,
|
||||
)
|
||||
|
||||
|
||||
def gen_step():
|
||||
"""Return the stepped shaft with keyway benchmark model in millimeters."""
|
||||
with BuildPart() as shaft:
|
||||
_add_shaft_section(0.0, LEFT_LENGTH, SMALL_DIAMETER)
|
||||
_add_shaft_section(LEFT_LENGTH, LEFT_LENGTH + MIDDLE_LENGTH, MIDDLE_DIAMETER)
|
||||
_add_shaft_section(LEFT_LENGTH + MIDDLE_LENGTH, TOTAL_LENGTH, SMALL_DIAMETER)
|
||||
|
||||
with Locations(Location(((KEYWAY_X_START + KEYWAY_X_END) / 2.0, 0.0, MIDDLE_DIAMETER / 2.0 - KEYWAY_DEPTH))):
|
||||
Box(
|
||||
KEYWAY_X_END - KEYWAY_X_START,
|
||||
KEYWAY_WIDTH,
|
||||
KEYWAY_DEPTH + 2.0,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
mode=Mode.SUBTRACT,
|
||||
)
|
||||
|
||||
part = shaft.part
|
||||
end_edges = []
|
||||
end_edges.extend(circular_edges(part, radius=SMALL_DIAMETER / 2.0, axis="x", coordinate=0.0))
|
||||
end_edges.extend(circular_edges(part, radius=SMALL_DIAMETER / 2.0, axis="x", coordinate=TOTAL_LENGTH))
|
||||
part = safe_chamfer(part, end_edges, length=END_CHAMFER)
|
||||
part.label = "benchmark_04_stepped_shaft_keyway"
|
||||
return part
|
||||
@@ -0,0 +1,75 @@
|
||||
from build123d import (
|
||||
Align,
|
||||
Box,
|
||||
BuildPart,
|
||||
BuildSketch,
|
||||
Cylinder,
|
||||
Location,
|
||||
Locations,
|
||||
Mode,
|
||||
Plane,
|
||||
RectangleRounded,
|
||||
extrude,
|
||||
)
|
||||
|
||||
|
||||
OUTER_LENGTH = 100.0
|
||||
OUTER_WIDTH = 70.0
|
||||
OUTER_HEIGHT = 30.0
|
||||
WALL_THICKNESS = 3.0
|
||||
FLOOR_THICKNESS = 3.0
|
||||
OUTER_CORNER_RADIUS = 2.0
|
||||
|
||||
STANDOFF_DIAMETER = 10.0
|
||||
STANDOFF_HEIGHT = 12.0
|
||||
STANDOFF_HOLE_DIAMETER = 3.0
|
||||
STANDOFF_HOLE_DEPTH = 8.0
|
||||
STANDOFF_LOCATIONS = ((-35.0, -25.0), (-35.0, 25.0), (35.0, -25.0), (35.0, 25.0))
|
||||
|
||||
|
||||
def gen_step():
|
||||
"""Return the open-top electronics enclosure benchmark model in millimeters."""
|
||||
inner_length = OUTER_LENGTH - 2.0 * WALL_THICKNESS
|
||||
inner_width = OUTER_WIDTH - 2.0 * WALL_THICKNESS
|
||||
cavity_height = OUTER_HEIGHT - FLOOR_THICKNESS
|
||||
standoff_top_z = FLOOR_THICKNESS + STANDOFF_HEIGHT
|
||||
|
||||
with BuildPart() as enclosure:
|
||||
with BuildSketch(Plane.XY):
|
||||
RectangleRounded(
|
||||
OUTER_LENGTH,
|
||||
OUTER_WIDTH,
|
||||
OUTER_CORNER_RADIUS,
|
||||
align=(Align.CENTER, Align.CENTER),
|
||||
)
|
||||
extrude(amount=OUTER_HEIGHT)
|
||||
|
||||
with Locations(Location((0.0, 0.0, FLOOR_THICKNESS))):
|
||||
Box(
|
||||
inner_length,
|
||||
inner_width,
|
||||
cavity_height,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
mode=Mode.SUBTRACT,
|
||||
)
|
||||
|
||||
for x_pos, y_pos in STANDOFF_LOCATIONS:
|
||||
with Locations(Location((x_pos, y_pos, FLOOR_THICKNESS))):
|
||||
Cylinder(
|
||||
radius=STANDOFF_DIAMETER / 2.0,
|
||||
height=STANDOFF_HEIGHT,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
mode=Mode.ADD,
|
||||
)
|
||||
|
||||
with Locations(Location((x_pos, y_pos, standoff_top_z))):
|
||||
Cylinder(
|
||||
radius=STANDOFF_HOLE_DIAMETER / 2.0,
|
||||
height=STANDOFF_HOLE_DEPTH,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MAX),
|
||||
mode=Mode.SUBTRACT,
|
||||
)
|
||||
|
||||
part = enclosure.part
|
||||
part.label = "benchmark_05_open_top_electronics_enclosure_bosses"
|
||||
return part
|
||||
@@ -0,0 +1,164 @@
|
||||
from math import cos, radians, sin
|
||||
|
||||
from build123d import (
|
||||
Align,
|
||||
Box,
|
||||
BuildPart,
|
||||
BuildSketch,
|
||||
Cylinder,
|
||||
Location,
|
||||
Locations,
|
||||
Mode,
|
||||
Plane,
|
||||
Polygon,
|
||||
add,
|
||||
extrude,
|
||||
)
|
||||
|
||||
from benchmark_common import rounded_triangle_points, safe_fillet
|
||||
|
||||
|
||||
BASE_LENGTH = 120.0
|
||||
BASE_WIDTH = 60.0
|
||||
BASE_THICKNESS = 10.0
|
||||
BASE_FILLET = 3.0
|
||||
|
||||
LUG_X_EXTENT = 36.0
|
||||
LUG_RADIUS = 18.0
|
||||
LUG_HEIGHT_ABOVE_BASE = 42.0
|
||||
LUG_THICKNESS_Y = 18.0
|
||||
LUG_GAP_Y = 16.0
|
||||
LUG_FILLET = 2.0
|
||||
|
||||
CLEVIS_HOLE_DIAMETER = 14.0
|
||||
CLEVIS_HOLE_CENTER_Z = 34.0
|
||||
|
||||
BASE_HOLE_DIAMETER = 7.0
|
||||
BASE_HOLE_LOCATIONS = ((-45.0, -20.0), (-45.0, 20.0), (45.0, -20.0), (45.0, 20.0))
|
||||
|
||||
CUTOUT_CORNER_RADIUS = 3.0
|
||||
RIB_THICKNESS_X = 6.0
|
||||
|
||||
|
||||
def _lug_profile_points() -> list[tuple[float, float]]:
|
||||
half_x = LUG_X_EXTENT / 2.0
|
||||
arc_center_z = BASE_THICKNESS + LUG_HEIGHT_ABOVE_BASE - LUG_RADIUS
|
||||
points = [(-half_x, BASE_THICKNESS), (half_x, BASE_THICKNESS), (half_x, arc_center_z)]
|
||||
|
||||
for step in range(1, 12):
|
||||
angle = radians(180.0 * step / 12.0)
|
||||
points.append((LUG_RADIUS * cos(angle), arc_center_z + LUG_RADIUS * sin(angle)))
|
||||
|
||||
points.append((-half_x, arc_center_z))
|
||||
return points
|
||||
|
||||
|
||||
def _make_lug(y_min: float, label: str):
|
||||
with BuildPart() as lug:
|
||||
with BuildSketch(Plane.XZ):
|
||||
Polygon(_lug_profile_points(), align=None)
|
||||
extrude(amount=LUG_THICKNESS_Y)
|
||||
|
||||
part = lug.part.moved(Location((0.0, y_min, 0.0)))
|
||||
part.label = label
|
||||
return part
|
||||
|
||||
|
||||
def _make_rib(sign: float):
|
||||
outer_y = sign * (LUG_GAP_Y / 2.0 + LUG_THICKNESS_Y)
|
||||
foot_y = sign * (BASE_WIDTH / 2.0)
|
||||
top_z = BASE_THICKNESS + 28.0
|
||||
|
||||
with BuildPart() as rib:
|
||||
with BuildSketch(Plane.YZ):
|
||||
Polygon(
|
||||
[
|
||||
(outer_y, BASE_THICKNESS),
|
||||
(foot_y, BASE_THICKNESS),
|
||||
(outer_y, top_z),
|
||||
],
|
||||
align=None,
|
||||
)
|
||||
extrude(amount=RIB_THICKNESS_X)
|
||||
|
||||
part = rib.part.moved(Location((-RIB_THICKNESS_X / 2.0, 0.0, 0.0)))
|
||||
part.label = "positive_y_diagonal_rib" if sign > 0.0 else "negative_y_diagonal_rib"
|
||||
return part
|
||||
|
||||
|
||||
def _outer_base_edges(part):
|
||||
edges = []
|
||||
for edge in part.edges():
|
||||
bbox = edge.bounding_box()
|
||||
center = bbox.center()
|
||||
on_outer_x = abs(abs(center.X) - BASE_LENGTH / 2.0) < 0.05
|
||||
on_outer_y = abs(abs(center.Y) - BASE_WIDTH / 2.0) < 0.05
|
||||
on_top_or_bottom = abs(center.Z - 0.0) < 0.05 or abs(center.Z - BASE_THICKNESS) < 0.05
|
||||
if (on_outer_x or on_outer_y) and on_top_or_bottom:
|
||||
edges.append(edge)
|
||||
return edges
|
||||
|
||||
|
||||
def _lug_base_edges(part):
|
||||
edges = []
|
||||
for edge in part.edges():
|
||||
bbox = edge.bounding_box()
|
||||
center = bbox.center()
|
||||
if abs(center.Z - BASE_THICKNESS) > 0.05:
|
||||
continue
|
||||
if abs(abs(center.Y) - LUG_GAP_Y / 2.0) < 0.05 and bbox.size.X > LUG_X_EXTENT - 1.0:
|
||||
edges.append(edge)
|
||||
return edges
|
||||
|
||||
|
||||
def _subtract_rounded_triangle(vertices: list[tuple[float, float]]):
|
||||
with BuildSketch(Plane.XY) as cutout:
|
||||
Polygon(
|
||||
rounded_triangle_points(vertices, radius=CUTOUT_CORNER_RADIUS, segments=5),
|
||||
align=None,
|
||||
)
|
||||
extrude(cutout.sketch, amount=BASE_THICKNESS + 2.0, mode=Mode.SUBTRACT)
|
||||
|
||||
|
||||
def gen_step():
|
||||
"""Return the clevis bracket benchmark model in millimeters."""
|
||||
positive_lug_y_min = LUG_GAP_Y / 2.0 + LUG_THICKNESS_Y
|
||||
negative_lug_y_min = -LUG_GAP_Y / 2.0
|
||||
|
||||
with BuildPart() as bracket:
|
||||
Box(BASE_LENGTH, BASE_WIDTH, BASE_THICKNESS, align=(Align.CENTER, Align.CENTER, Align.MIN))
|
||||
|
||||
base = safe_fillet(bracket.part, _outer_base_edges(bracket.part), radius=BASE_FILLET)
|
||||
|
||||
with BuildPart() as bracket:
|
||||
add(base, mode=Mode.ADD)
|
||||
add(_make_lug(positive_lug_y_min, "positive_y_clevis_lug"), mode=Mode.ADD)
|
||||
add(_make_lug(negative_lug_y_min, "negative_y_clevis_lug"), mode=Mode.ADD)
|
||||
add(_make_rib(1.0), mode=Mode.ADD)
|
||||
add(_make_rib(-1.0), mode=Mode.ADD)
|
||||
|
||||
with Locations(Location((0.0, 0.0, CLEVIS_HOLE_CENTER_Z))):
|
||||
Cylinder(
|
||||
radius=CLEVIS_HOLE_DIAMETER / 2.0,
|
||||
height=BASE_WIDTH + 8.0,
|
||||
rotation=(90.0, 0.0, 0.0),
|
||||
align=(Align.CENTER, Align.CENTER, Align.CENTER),
|
||||
mode=Mode.SUBTRACT,
|
||||
)
|
||||
|
||||
for x_pos, y_pos in BASE_HOLE_LOCATIONS:
|
||||
with Locations(Location((x_pos, y_pos, -1.0))):
|
||||
Cylinder(
|
||||
radius=BASE_HOLE_DIAMETER / 2.0,
|
||||
height=BASE_THICKNESS + 2.0,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
mode=Mode.SUBTRACT,
|
||||
)
|
||||
|
||||
_subtract_rounded_triangle([(-54.0, -13.0), (-25.0, 0.0), (-54.0, 13.0)])
|
||||
_subtract_rounded_triangle([(54.0, -13.0), (25.0, 0.0), (54.0, 13.0)])
|
||||
|
||||
part = bracket.part
|
||||
part = safe_fillet(part, _lug_base_edges(part), radius=LUG_FILLET)
|
||||
part.label = "benchmark_06_aerospace_clevis_bracket_lightening_cutouts"
|
||||
return part
|
||||
@@ -0,0 +1,122 @@
|
||||
from math import cos, radians, sin, tau
|
||||
|
||||
from build123d import Align, BuildPart, Cylinder, Location, Locations, Mode
|
||||
|
||||
from benchmark_common import circular_edges, polar_point, safe_fillet
|
||||
|
||||
|
||||
BARREL_DIAMETER = 36.0
|
||||
BARREL_HEIGHT = 70.0
|
||||
|
||||
FIN_COUNT = 12
|
||||
FIN_DIAMETER = 62.0
|
||||
FIN_THICKNESS = 2.0
|
||||
FIN_Z_VALUES = tuple(10.0 + 5.0 * index for index in range(FIN_COUNT))
|
||||
|
||||
BASE_FLANGE_DIAMETER = 70.0
|
||||
BASE_FLANGE_THICKNESS = 8.0
|
||||
MOUNT_HOLE_COUNT = 6
|
||||
MOUNT_HOLE_DIAMETER = 5.0
|
||||
MOUNT_BOLT_CIRCLE_DIAMETER = 56.0
|
||||
|
||||
TOP_CAP_DIAMETER = 44.0
|
||||
TOP_CAP_BOTTOM_Z = 70.0
|
||||
TOP_CAP_HEIGHT = 8.0
|
||||
|
||||
BOSS_DIAMETER = 12.0
|
||||
BOSS_LENGTH = 24.0
|
||||
BOSS_ANGLE_DEGREES = 35.0
|
||||
BOSS_BORE_DIAMETER = 5.0
|
||||
|
||||
EDGE_FILLET = 0.95
|
||||
|
||||
|
||||
def _boss_center() -> tuple[float, float, float]:
|
||||
angle = radians(BOSS_ANGLE_DEGREES)
|
||||
start = (TOP_CAP_DIAMETER / 2.0 - 1.0, 0.0, TOP_CAP_BOTTOM_Z + TOP_CAP_HEIGHT / 2.0)
|
||||
direction = (cos(angle), 0.0, sin(angle))
|
||||
return (
|
||||
start[0] + direction[0] * BOSS_LENGTH / 2.0,
|
||||
0.0,
|
||||
start[2] + direction[2] * BOSS_LENGTH / 2.0,
|
||||
)
|
||||
|
||||
|
||||
def _edge_fillets(part):
|
||||
edges = []
|
||||
for z_pos in FIN_Z_VALUES:
|
||||
edges.extend(circular_edges(part, radius=FIN_DIAMETER / 2.0, axis="z", coordinate=z_pos))
|
||||
edges.extend(circular_edges(part, radius=FIN_DIAMETER / 2.0, axis="z", coordinate=z_pos + FIN_THICKNESS))
|
||||
edges.extend(circular_edges(part, radius=BASE_FLANGE_DIAMETER / 2.0, axis="z", coordinate=0.0))
|
||||
edges.extend(circular_edges(part, radius=BASE_FLANGE_DIAMETER / 2.0, axis="z", coordinate=BASE_FLANGE_THICKNESS))
|
||||
return edges
|
||||
|
||||
|
||||
def gen_step():
|
||||
"""Return the radial-engine-style cylinder benchmark model in millimeters."""
|
||||
boss_rotation_y = 90.0 - BOSS_ANGLE_DEGREES
|
||||
boss_center = _boss_center()
|
||||
|
||||
with BuildPart() as cylinder:
|
||||
Cylinder(
|
||||
radius=BARREL_DIAMETER / 2.0,
|
||||
height=BARREL_HEIGHT,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
)
|
||||
|
||||
Cylinder(
|
||||
radius=BASE_FLANGE_DIAMETER / 2.0,
|
||||
height=BASE_FLANGE_THICKNESS,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
mode=Mode.ADD,
|
||||
)
|
||||
|
||||
for z_pos in FIN_Z_VALUES:
|
||||
with Locations(Location((0.0, 0.0, z_pos))):
|
||||
Cylinder(
|
||||
radius=FIN_DIAMETER / 2.0,
|
||||
height=FIN_THICKNESS,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
mode=Mode.ADD,
|
||||
)
|
||||
|
||||
with Locations(Location((0.0, 0.0, TOP_CAP_BOTTOM_Z))):
|
||||
Cylinder(
|
||||
radius=TOP_CAP_DIAMETER / 2.0,
|
||||
height=TOP_CAP_HEIGHT,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
mode=Mode.ADD,
|
||||
)
|
||||
|
||||
with Locations(Location(boss_center)):
|
||||
Cylinder(
|
||||
radius=BOSS_DIAMETER / 2.0,
|
||||
height=BOSS_LENGTH,
|
||||
rotation=(0.0, boss_rotation_y, 0.0),
|
||||
align=(Align.CENTER, Align.CENTER, Align.CENTER),
|
||||
mode=Mode.ADD,
|
||||
)
|
||||
|
||||
for index in range(MOUNT_HOLE_COUNT):
|
||||
x_pos, y_pos = polar_point(MOUNT_BOLT_CIRCLE_DIAMETER / 2.0, tau * index / MOUNT_HOLE_COUNT)
|
||||
with Locations(Location((x_pos, y_pos, -1.0))):
|
||||
Cylinder(
|
||||
radius=MOUNT_HOLE_DIAMETER / 2.0,
|
||||
height=BASE_FLANGE_THICKNESS + 2.0,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
mode=Mode.SUBTRACT,
|
||||
)
|
||||
|
||||
with Locations(Location(boss_center)):
|
||||
Cylinder(
|
||||
radius=BOSS_BORE_DIAMETER / 2.0,
|
||||
height=BOSS_LENGTH + 6.0,
|
||||
rotation=(0.0, boss_rotation_y, 0.0),
|
||||
align=(Align.CENTER, Align.CENTER, Align.CENTER),
|
||||
mode=Mode.SUBTRACT,
|
||||
)
|
||||
|
||||
part = cylinder.part
|
||||
part = safe_fillet(part, _edge_fillets(part), radius=EDGE_FILLET)
|
||||
part.label = "benchmark_07_radial_engine_cylinder_cooling_fins"
|
||||
return part
|
||||
@@ -0,0 +1,152 @@
|
||||
from math import asin, cos, hypot, radians, sin, tau
|
||||
|
||||
from build123d import (
|
||||
Align,
|
||||
BuildPart,
|
||||
BuildSketch,
|
||||
Cylinder,
|
||||
Location,
|
||||
Locations,
|
||||
Mode,
|
||||
Plane,
|
||||
Polygon,
|
||||
add,
|
||||
extrude,
|
||||
)
|
||||
|
||||
from benchmark_common import circular_edges, polar_point, safe_fillet
|
||||
|
||||
|
||||
BACKPLATE_DIAMETER = 90.0
|
||||
BACKPLATE_THICKNESS = 6.0
|
||||
BACKPLATE_FILLET = 1.5
|
||||
HUB_DIAMETER = 26.0
|
||||
HUB_HEIGHT = 22.0
|
||||
BORE_DIAMETER = 8.0
|
||||
|
||||
BLADE_COUNT = 12
|
||||
BLADE_ROOT_RADIUS = 18.0
|
||||
BLADE_TIP_RADIUS = 43.0
|
||||
BLADE_THICKNESS = 3.0
|
||||
BLADE_HEIGHT = 16.0
|
||||
BLADE_BACKWARD_SWEEP = radians(45.0)
|
||||
BLADE_SEGMENTS = 5
|
||||
BLADE_ROOT_FILLET = 1.0
|
||||
|
||||
|
||||
def _clamp_radius(point: tuple[float, float]) -> tuple[float, float]:
|
||||
radius = hypot(point[0], point[1])
|
||||
if radius < BLADE_ROOT_RADIUS:
|
||||
scale = BLADE_ROOT_RADIUS / radius
|
||||
return (point[0] * scale, point[1] * scale)
|
||||
if radius > BLADE_TIP_RADIUS:
|
||||
scale = BLADE_TIP_RADIUS / radius
|
||||
return (point[0] * scale, point[1] * scale)
|
||||
return point
|
||||
|
||||
|
||||
def _blade_center(base_angle: float, t: float) -> tuple[float, float]:
|
||||
radius = BLADE_ROOT_RADIUS + (BLADE_TIP_RADIUS - BLADE_ROOT_RADIUS) * t
|
||||
angle = base_angle - BLADE_BACKWARD_SWEEP * t
|
||||
return polar_point(radius, angle)
|
||||
|
||||
|
||||
def _blade_normal(base_angle: float, t: float) -> tuple[float, float]:
|
||||
radius = BLADE_ROOT_RADIUS + (BLADE_TIP_RADIUS - BLADE_ROOT_RADIUS) * t
|
||||
dr_dt = BLADE_TIP_RADIUS - BLADE_ROOT_RADIUS
|
||||
dtheta_dt = -BLADE_BACKWARD_SWEEP
|
||||
angle = base_angle + dtheta_dt * t
|
||||
dx_dt = dr_dt * cos(angle) - radius * sin(angle) * dtheta_dt
|
||||
dy_dt = dr_dt * sin(angle) + radius * cos(angle) * dtheta_dt
|
||||
length = hypot(dx_dt, dy_dt)
|
||||
return (-dy_dt / length, dx_dt / length)
|
||||
|
||||
|
||||
def _blade_end_points(base_angle: float, t: float) -> tuple[tuple[float, float], tuple[float, float]]:
|
||||
radius = BLADE_ROOT_RADIUS if t == 0.0 else BLADE_TIP_RADIUS
|
||||
angle = base_angle - BLADE_BACKWARD_SWEEP * t
|
||||
half_angle = asin((BLADE_THICKNESS / 2.0) / radius)
|
||||
return (polar_point(radius, angle + half_angle), polar_point(radius, angle - half_angle))
|
||||
|
||||
|
||||
def _blade_outline(base_angle: float) -> list[tuple[float, float]]:
|
||||
left_side = []
|
||||
right_side = []
|
||||
half_thickness = BLADE_THICKNESS / 2.0
|
||||
|
||||
for index in range(BLADE_SEGMENTS + 1):
|
||||
t = index / BLADE_SEGMENTS
|
||||
if index == 0 or index == BLADE_SEGMENTS:
|
||||
left, right = _blade_end_points(base_angle, t)
|
||||
else:
|
||||
center = _blade_center(base_angle, t)
|
||||
normal = _blade_normal(base_angle, t)
|
||||
left = _clamp_radius((center[0] + normal[0] * half_thickness, center[1] + normal[1] * half_thickness))
|
||||
right = _clamp_radius((center[0] - normal[0] * half_thickness, center[1] - normal[1] * half_thickness))
|
||||
left_side.append(left)
|
||||
right_side.append(right)
|
||||
|
||||
return left_side + list(reversed(right_side))
|
||||
|
||||
|
||||
def _make_blade(index: int):
|
||||
base_angle = tau * index / BLADE_COUNT
|
||||
with BuildPart() as blade:
|
||||
with BuildSketch(Plane.XY):
|
||||
Polygon(_blade_outline(base_angle), align=None)
|
||||
extrude(amount=BLADE_HEIGHT)
|
||||
|
||||
part = blade.part.moved(Location((0.0, 0.0, BACKPLATE_THICKNESS)))
|
||||
part.label = f"backward_curved_blade_{index + 1:02d}"
|
||||
return part
|
||||
|
||||
|
||||
def _blade_root_edges(part):
|
||||
edges = []
|
||||
for edge in part.edges():
|
||||
bbox = edge.bounding_box()
|
||||
center = bbox.center()
|
||||
if abs(center.Z - BACKPLATE_THICKNESS) > 0.05:
|
||||
continue
|
||||
radial_distance = hypot(center.X, center.Y)
|
||||
if BLADE_ROOT_RADIUS - 3.0 <= radial_distance <= 30.0:
|
||||
if str(edge.geom_type).endswith("LINE"):
|
||||
edges.append(edge)
|
||||
return edges
|
||||
|
||||
|
||||
def gen_step():
|
||||
"""Return the centrifugal impeller benchmark model in millimeters."""
|
||||
total_height = BACKPLATE_THICKNESS + HUB_HEIGHT
|
||||
|
||||
with BuildPart() as impeller:
|
||||
Cylinder(
|
||||
radius=BACKPLATE_DIAMETER / 2.0,
|
||||
height=BACKPLATE_THICKNESS,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
)
|
||||
for blade_index in range(BLADE_COUNT):
|
||||
add(_make_blade(blade_index), mode=Mode.ADD)
|
||||
with Locations(Location((0.0, 0.0, BACKPLATE_THICKNESS))):
|
||||
Cylinder(
|
||||
radius=HUB_DIAMETER / 2.0,
|
||||
height=HUB_HEIGHT,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
mode=Mode.ADD,
|
||||
)
|
||||
with Locations(Location((0.0, 0.0, -1.0))):
|
||||
Cylinder(
|
||||
radius=BORE_DIAMETER / 2.0,
|
||||
height=total_height + 2.0,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
mode=Mode.SUBTRACT,
|
||||
)
|
||||
|
||||
part = impeller.part
|
||||
backplate_edges = []
|
||||
backplate_edges.extend(circular_edges(part, radius=BACKPLATE_DIAMETER / 2.0, axis="z", coordinate=0.0))
|
||||
backplate_edges.extend(circular_edges(part, radius=BACKPLATE_DIAMETER / 2.0, axis="z", coordinate=BACKPLATE_THICKNESS))
|
||||
part = safe_fillet(part, backplate_edges, radius=BACKPLATE_FILLET)
|
||||
part = safe_fillet(part, _blade_root_edges(part), radius=BLADE_ROOT_FILLET)
|
||||
part.label = "benchmark_08_centrifugal_impeller_backward_curved_blades"
|
||||
return part
|
||||
@@ -0,0 +1,149 @@
|
||||
from math import cos, radians, sin
|
||||
|
||||
from build123d import (
|
||||
Align,
|
||||
BuildLine,
|
||||
BuildPart,
|
||||
BuildSketch,
|
||||
CenterArc,
|
||||
Circle,
|
||||
Color,
|
||||
Compound,
|
||||
Cylinder,
|
||||
Edge,
|
||||
Line,
|
||||
Location,
|
||||
Plane,
|
||||
extrude,
|
||||
make_face,
|
||||
sweep,
|
||||
)
|
||||
|
||||
|
||||
BASE_DIAMETER = 90.0
|
||||
BASE_THICKNESS = 5.0
|
||||
COLUMN_DIAMETER = 14.0
|
||||
COLUMN_HEIGHT = 140.0
|
||||
|
||||
TREAD_COUNT = 20
|
||||
TREAD_THICKNESS = 4.0
|
||||
TREAD_INNER_RADIUS = 10.0
|
||||
TREAD_OUTER_RADIUS = 62.0
|
||||
TREAD_SWEEP_DEGREES = 24.0
|
||||
TREAD_FIRST_BOTTOM_Z = 4.0
|
||||
TREAD_RISE = 6.0
|
||||
TREAD_ROTATION_DEGREES = 18.0
|
||||
|
||||
HANDRAIL_RADIUS = 66.0
|
||||
HANDRAIL_DIAMETER = 5.0
|
||||
HANDRAIL_START_Z = 14.0
|
||||
HANDRAIL_END_Z = 130.0
|
||||
BALUSTER_DIAMETER = 3.0
|
||||
|
||||
|
||||
def _polar_point(radius: float, angle_degrees: float) -> tuple[float, float]:
|
||||
angle = radians(angle_degrees)
|
||||
return (radius * cos(angle), radius * sin(angle))
|
||||
|
||||
|
||||
def _make_base():
|
||||
with BuildPart() as base:
|
||||
Cylinder(
|
||||
radius=BASE_DIAMETER / 2.0,
|
||||
height=BASE_THICKNESS,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
)
|
||||
part = base.part
|
||||
part.label = "base_disk"
|
||||
part.color = Color(0.42, 0.43, 0.45, 1.0)
|
||||
return part
|
||||
|
||||
|
||||
def _make_column():
|
||||
with BuildPart() as column:
|
||||
Cylinder(
|
||||
radius=COLUMN_DIAMETER / 2.0,
|
||||
height=COLUMN_HEIGHT,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
)
|
||||
part = column.part
|
||||
part.label = "central_column"
|
||||
part.color = Color(0.70, 0.72, 0.74, 1.0)
|
||||
return part
|
||||
|
||||
|
||||
def _make_tread_profile():
|
||||
half_sweep = TREAD_SWEEP_DEGREES / 2.0
|
||||
with BuildPart() as tread:
|
||||
with BuildSketch(Plane.XY):
|
||||
with BuildLine():
|
||||
CenterArc((0.0, 0.0), TREAD_OUTER_RADIUS, -half_sweep, TREAD_SWEEP_DEGREES)
|
||||
Line(_polar_point(TREAD_OUTER_RADIUS, half_sweep), _polar_point(TREAD_INNER_RADIUS, half_sweep))
|
||||
CenterArc((0.0, 0.0), TREAD_INNER_RADIUS, half_sweep, -TREAD_SWEEP_DEGREES)
|
||||
Line(_polar_point(TREAD_INNER_RADIUS, -half_sweep), _polar_point(TREAD_OUTER_RADIUS, -half_sweep))
|
||||
make_face()
|
||||
extrude(amount=TREAD_THICKNESS)
|
||||
return tread.part
|
||||
|
||||
|
||||
def _make_tread(index: int):
|
||||
angle = index * TREAD_ROTATION_DEGREES
|
||||
bottom_z = TREAD_FIRST_BOTTOM_Z + index * TREAD_RISE
|
||||
part = _make_tread_profile().moved(Location((0.0, 0.0, bottom_z), (0.0, 0.0, angle)))
|
||||
part.label = f"helical_tread_{index + 1:02d}"
|
||||
part.color = Color(0.78, 0.58, 0.35, 1.0)
|
||||
return part
|
||||
|
||||
|
||||
def _handrail_center_z(angle_degrees: float) -> float:
|
||||
return HANDRAIL_START_Z + (HANDRAIL_END_Z - HANDRAIL_START_Z) * angle_degrees / 360.0
|
||||
|
||||
|
||||
def _make_handrail():
|
||||
rail_height = HANDRAIL_END_Z - HANDRAIL_START_Z
|
||||
path = Edge.make_helix(
|
||||
pitch=rail_height,
|
||||
height=rail_height,
|
||||
radius=HANDRAIL_RADIUS,
|
||||
center=(0.0, 0.0, HANDRAIL_START_Z),
|
||||
)
|
||||
profile_plane = Plane(
|
||||
origin=(HANDRAIL_RADIUS, 0.0, HANDRAIL_START_Z),
|
||||
x_dir=(1.0, 0.0, 0.0),
|
||||
z_dir=path.tangent_at(0.0),
|
||||
)
|
||||
|
||||
with BuildPart() as rail:
|
||||
with BuildSketch(profile_plane):
|
||||
Circle(HANDRAIL_DIAMETER / 2.0)
|
||||
sweep(path=path, is_frenet=True)
|
||||
part = rail.part
|
||||
part.label = "helical_handrail"
|
||||
part.color = Color(0.22, 0.28, 0.34, 1.0)
|
||||
return part
|
||||
|
||||
|
||||
def _make_baluster(index: int):
|
||||
angle = index * TREAD_ROTATION_DEGREES
|
||||
tread_top_z = TREAD_FIRST_BOTTOM_Z + index * TREAD_RISE + TREAD_THICKNESS
|
||||
rail_center_z = _handrail_center_z(angle)
|
||||
x_pos, y_pos = _polar_point(TREAD_OUTER_RADIUS, angle)
|
||||
with BuildPart() as baluster:
|
||||
Cylinder(
|
||||
radius=BALUSTER_DIAMETER / 2.0,
|
||||
height=rail_center_z - tread_top_z,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
)
|
||||
part = baluster.part.moved(Location((x_pos, y_pos, tread_top_z)))
|
||||
part.label = f"baluster_{index + 1:02d}"
|
||||
part.color = Color(0.30, 0.33, 0.36, 1.0)
|
||||
return part
|
||||
|
||||
|
||||
def gen_step():
|
||||
"""Return the miniature spiral staircase benchmark model in millimeters."""
|
||||
parts = [_make_base(), _make_column()]
|
||||
parts.extend(_make_tread(index) for index in range(TREAD_COUNT))
|
||||
parts.append(_make_handrail())
|
||||
parts.extend(_make_baluster(index) for index in range(TREAD_COUNT))
|
||||
return Compound(obj=parts, children=parts, label="benchmark_09_spiral_staircase_helical_handrail")
|
||||
@@ -0,0 +1,181 @@
|
||||
from math import tau
|
||||
|
||||
from build123d import (
|
||||
Align,
|
||||
BuildPart,
|
||||
BuildSketch,
|
||||
Color,
|
||||
Compound,
|
||||
Cylinder,
|
||||
Location,
|
||||
Locations,
|
||||
Mode,
|
||||
Plane,
|
||||
Polygon,
|
||||
extrude,
|
||||
)
|
||||
|
||||
from benchmark_common import polar_point, trapezoid_tooth_profile
|
||||
|
||||
|
||||
GEAR_THICKNESS = 8.0
|
||||
SUN_TEETH = 24
|
||||
SUN_PITCH_DIAMETER = 48.0
|
||||
SUN_ROOT_DIAMETER = 42.0
|
||||
SUN_OUTSIDE_DIAMETER = 54.0
|
||||
SUN_BORE_DIAMETER = 10.0
|
||||
|
||||
PLANET_COUNT = 3
|
||||
PLANET_TEETH = 18
|
||||
PLANET_PITCH_DIAMETER = 36.0
|
||||
PLANET_ROOT_DIAMETER = 31.0
|
||||
PLANET_OUTSIDE_DIAMETER = 41.0
|
||||
PLANET_CENTER_RADIUS = 42.0
|
||||
PLANET_BORE_DIAMETER = 6.8
|
||||
|
||||
RING_TEETH = 60
|
||||
RING_INTERNAL_PITCH_DIAMETER = 120.0
|
||||
RING_INTERNAL_ROOT_DIAMETER = 126.0
|
||||
RING_INTERNAL_TIP_DIAMETER = 114.0
|
||||
RING_OUTSIDE_DIAMETER = 140.0
|
||||
|
||||
CARRIER_DIAMETER = 105.0
|
||||
CARRIER_BOTTOM_Z = -5.0
|
||||
CARRIER_TOP_Z = -1.0
|
||||
CARRIER_THICKNESS = CARRIER_TOP_Z - CARRIER_BOTTOM_Z
|
||||
|
||||
PIN_DIAMETER = 6.0
|
||||
PIN_HEIGHT = 14.0
|
||||
PIN_BOTTOM_Z = -5.0
|
||||
|
||||
|
||||
def _make_external_gear(
|
||||
*,
|
||||
label: str,
|
||||
teeth: int,
|
||||
root_diameter: float,
|
||||
outside_diameter: float,
|
||||
phase: float,
|
||||
center: tuple[float, float] = (0.0, 0.0),
|
||||
bore_diameter: float | None = None,
|
||||
color: Color | None = None,
|
||||
):
|
||||
with BuildPart() as gear:
|
||||
with BuildSketch(Plane.XY):
|
||||
Polygon(
|
||||
trapezoid_tooth_profile(
|
||||
teeth=teeth,
|
||||
root_radius=root_diameter / 2.0,
|
||||
tip_radius=outside_diameter / 2.0,
|
||||
phase=phase,
|
||||
),
|
||||
align=None,
|
||||
)
|
||||
extrude(amount=GEAR_THICKNESS)
|
||||
|
||||
if bore_diameter is not None:
|
||||
with Locations(Location((0.0, 0.0, -0.5))):
|
||||
Cylinder(
|
||||
radius=bore_diameter / 2.0,
|
||||
height=GEAR_THICKNESS + 1.0,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
mode=Mode.SUBTRACT,
|
||||
)
|
||||
|
||||
part = gear.part.moved(Location((center[0], center[1], 0.0)))
|
||||
part.label = label
|
||||
part.color = color
|
||||
return part
|
||||
|
||||
|
||||
def _make_internal_ring_gear():
|
||||
with BuildPart() as ring:
|
||||
Cylinder(
|
||||
radius=RING_OUTSIDE_DIAMETER / 2.0,
|
||||
height=GEAR_THICKNESS,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
)
|
||||
with BuildSketch(Plane.XY):
|
||||
Polygon(
|
||||
trapezoid_tooth_profile(
|
||||
teeth=RING_TEETH,
|
||||
root_radius=RING_INTERNAL_ROOT_DIAMETER / 2.0,
|
||||
tip_radius=RING_INTERNAL_TIP_DIAMETER / 2.0,
|
||||
phase=-tau / RING_TEETH / 2.0,
|
||||
root_span_fraction=0.68,
|
||||
tip_span_fraction=0.34,
|
||||
),
|
||||
align=None,
|
||||
)
|
||||
extrude(amount=GEAR_THICKNESS, mode=Mode.SUBTRACT)
|
||||
part = ring.part
|
||||
part.label = "ring_gear_60_internal_teeth"
|
||||
part.color = Color(0.66, 0.68, 0.70, 1.0)
|
||||
return part
|
||||
|
||||
|
||||
def _make_carrier_plate():
|
||||
with BuildPart() as carrier:
|
||||
with Locations(Location((0.0, 0.0, CARRIER_BOTTOM_Z))):
|
||||
Cylinder(
|
||||
radius=CARRIER_DIAMETER / 2.0,
|
||||
height=CARRIER_THICKNESS,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
)
|
||||
part = carrier.part
|
||||
part.label = "carrier_plate"
|
||||
part.color = Color(0.52, 0.62, 0.58, 1.0)
|
||||
return part
|
||||
|
||||
|
||||
def _make_pin(label: str, center: tuple[float, float]):
|
||||
with BuildPart() as pin:
|
||||
with Locations(Location((center[0], center[1], PIN_BOTTOM_Z))):
|
||||
Cylinder(
|
||||
radius=PIN_DIAMETER / 2.0,
|
||||
height=PIN_HEIGHT,
|
||||
align=(Align.CENTER, Align.CENTER, Align.MIN),
|
||||
)
|
||||
part = pin.part
|
||||
part.label = label
|
||||
part.color = Color(0.22, 0.25, 0.29, 1.0)
|
||||
return part
|
||||
|
||||
|
||||
def _planet_center(index: int) -> tuple[float, float]:
|
||||
return polar_point(PLANET_CENTER_RADIUS, tau * index / PLANET_COUNT)
|
||||
|
||||
|
||||
def gen_step():
|
||||
"""Return the simplified planetary gear benchmark assembly in millimeters."""
|
||||
parts = [
|
||||
_make_carrier_plate(),
|
||||
_make_internal_ring_gear(),
|
||||
_make_external_gear(
|
||||
label="sun_gear_24_external_teeth",
|
||||
teeth=SUN_TEETH,
|
||||
root_diameter=SUN_ROOT_DIAMETER,
|
||||
outside_diameter=SUN_OUTSIDE_DIAMETER,
|
||||
phase=-tau / SUN_TEETH / 2.0,
|
||||
bore_diameter=SUN_BORE_DIAMETER,
|
||||
color=Color(0.94, 0.60, 0.22, 1.0),
|
||||
),
|
||||
]
|
||||
|
||||
for index in range(PLANET_COUNT):
|
||||
center = _planet_center(index)
|
||||
parts.append(
|
||||
_make_external_gear(
|
||||
label=f"planet_gear_{index + 1}_18_external_teeth",
|
||||
teeth=PLANET_TEETH,
|
||||
root_diameter=PLANET_ROOT_DIAMETER,
|
||||
outside_diameter=PLANET_OUTSIDE_DIAMETER,
|
||||
phase=tau * index / PLANET_COUNT,
|
||||
center=center,
|
||||
bore_diameter=PLANET_BORE_DIAMETER,
|
||||
color=Color(0.24, 0.51, 0.76, 1.0),
|
||||
)
|
||||
)
|
||||
parts.append(_make_pin(f"planet_pin_{index + 1}", center))
|
||||
|
||||
return Compound(obj=parts, children=parts, label="benchmark_10_simplified_planetary_gear_stage")
|
||||
@@ -0,0 +1,177 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from math import cos, hypot, sin, tau
|
||||
from typing import Iterable
|
||||
|
||||
from build123d import Axis, BuildPart, Color, Location, Mode, chamfer, fillet
|
||||
|
||||
|
||||
TOLERANCE = 1e-6
|
||||
|
||||
|
||||
def polar_point(radius: float, angle: float) -> tuple[float, float]:
|
||||
return (radius * cos(angle), radius * sin(angle))
|
||||
|
||||
|
||||
def safe_fillet(part, edges: Iterable, radius: float):
|
||||
selected = list(edges)
|
||||
if not selected:
|
||||
return part
|
||||
try:
|
||||
return fillet(selected, radius=radius)
|
||||
except Exception:
|
||||
return part
|
||||
|
||||
|
||||
def safe_chamfer(part, edges: Iterable, length: float):
|
||||
selected = list(edges)
|
||||
if not selected:
|
||||
return part
|
||||
try:
|
||||
return chamfer(selected, length=length)
|
||||
except Exception:
|
||||
return part
|
||||
|
||||
|
||||
def line_edges_at_z(part, z_value: float, *, tol: float = 0.02):
|
||||
edges = []
|
||||
for edge in part.edges():
|
||||
bbox = edge.bounding_box()
|
||||
if abs(bbox.min.Z - z_value) <= tol and abs(bbox.max.Z - z_value) <= tol:
|
||||
if str(edge.geom_type).endswith("LINE"):
|
||||
edges.append(edge)
|
||||
return edges
|
||||
|
||||
|
||||
def circular_edges(
|
||||
part,
|
||||
*,
|
||||
radius: float | None = None,
|
||||
axis: str | None = None,
|
||||
coordinate: float | None = None,
|
||||
tol: float = 0.05,
|
||||
):
|
||||
edges = []
|
||||
for edge in part.edges():
|
||||
try:
|
||||
edge_radius = edge.radius
|
||||
edge_center = edge.arc_center
|
||||
edge_normal = edge.normal()
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if radius is not None and abs(edge_radius - radius) > tol:
|
||||
continue
|
||||
|
||||
if axis is not None:
|
||||
components = {
|
||||
"x": abs(edge_normal.X),
|
||||
"y": abs(edge_normal.Y),
|
||||
"z": abs(edge_normal.Z),
|
||||
}
|
||||
if components[axis] < 1.0 - 1e-4:
|
||||
continue
|
||||
|
||||
if coordinate is not None:
|
||||
coord = {"x": edge_center.X, "y": edge_center.Y, "z": edge_center.Z}[axis or "z"]
|
||||
if abs(coord - coordinate) > tol:
|
||||
continue
|
||||
|
||||
edges.append(edge)
|
||||
|
||||
return edges
|
||||
|
||||
|
||||
def cylinder_axis_x(radius: float, length: float, center: tuple[float, float, float], *, mode=Mode.ADD):
|
||||
from build123d import Align, Cylinder
|
||||
|
||||
return Cylinder(
|
||||
radius=radius,
|
||||
height=length,
|
||||
rotation=(0.0, 90.0, 0.0),
|
||||
align=(Align.CENTER, Align.CENTER, Align.CENTER),
|
||||
mode=mode,
|
||||
).locate(Location(center))
|
||||
|
||||
|
||||
def cylinder_axis_y(radius: float, length: float, center: tuple[float, float, float], *, mode=Mode.ADD):
|
||||
from build123d import Align, Cylinder
|
||||
|
||||
return Cylinder(
|
||||
radius=radius,
|
||||
height=length,
|
||||
rotation=(90.0, 0.0, 0.0),
|
||||
align=(Align.CENTER, Align.CENTER, Align.CENTER),
|
||||
mode=mode,
|
||||
).locate(Location(center))
|
||||
|
||||
|
||||
def rounded_triangle_points(
|
||||
vertices: list[tuple[float, float]],
|
||||
*,
|
||||
radius: float,
|
||||
segments: int = 5,
|
||||
) -> list[tuple[float, float]]:
|
||||
"""Approximate a rounded triangular loop with quadratic corner arcs."""
|
||||
points: list[tuple[float, float]] = []
|
||||
count = len(vertices)
|
||||
|
||||
for index, vertex in enumerate(vertices):
|
||||
previous = vertices[(index - 1) % count]
|
||||
next_vertex = vertices[(index + 1) % count]
|
||||
|
||||
to_previous = (previous[0] - vertex[0], previous[1] - vertex[1])
|
||||
to_next = (next_vertex[0] - vertex[0], next_vertex[1] - vertex[1])
|
||||
previous_length = hypot(*to_previous)
|
||||
next_length = hypot(*to_next)
|
||||
offset = min(radius, previous_length * 0.35, next_length * 0.35)
|
||||
|
||||
start = (
|
||||
vertex[0] + to_previous[0] / previous_length * offset,
|
||||
vertex[1] + to_previous[1] / previous_length * offset,
|
||||
)
|
||||
end = (
|
||||
vertex[0] + to_next[0] / next_length * offset,
|
||||
vertex[1] + to_next[1] / next_length * offset,
|
||||
)
|
||||
|
||||
for step in range(segments + 1):
|
||||
t = step / segments
|
||||
x_pos = (1.0 - t) ** 2 * start[0] + 2.0 * (1.0 - t) * t * vertex[0] + t**2 * end[0]
|
||||
y_pos = (1.0 - t) ** 2 * start[1] + 2.0 * (1.0 - t) * t * vertex[1] + t**2 * end[1]
|
||||
points.append((x_pos, y_pos))
|
||||
|
||||
return points
|
||||
|
||||
|
||||
def trapezoid_tooth_profile(
|
||||
*,
|
||||
teeth: int,
|
||||
root_radius: float,
|
||||
tip_radius: float,
|
||||
phase: float,
|
||||
root_span_fraction: float = 0.72,
|
||||
tip_span_fraction: float = 0.38,
|
||||
) -> list[tuple[float, float]]:
|
||||
points: list[tuple[float, float]] = []
|
||||
pitch_angle = tau / teeth
|
||||
|
||||
for tooth_index in range(teeth):
|
||||
center_angle = phase + tooth_index * pitch_angle
|
||||
points.extend(
|
||||
(
|
||||
polar_point(root_radius, center_angle - root_span_fraction * pitch_angle / 2.0),
|
||||
polar_point(tip_radius, center_angle - tip_span_fraction * pitch_angle / 2.0),
|
||||
polar_point(tip_radius, center_angle + tip_span_fraction * pitch_angle / 2.0),
|
||||
polar_point(root_radius, center_angle + root_span_fraction * pitch_angle / 2.0),
|
||||
)
|
||||
)
|
||||
|
||||
return points
|
||||
|
||||
|
||||
def label(part, name: str, color: Color | None = None):
|
||||
part.label = name
|
||||
if color is not None:
|
||||
part.color = color
|
||||
return part
|
||||
@@ -0,0 +1,269 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from math import atan2, degrees, hypot
|
||||
from pathlib import Path
|
||||
|
||||
from build123d import import_step
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
BASIC_TOL = 0.2
|
||||
COMPLEX_TOL = 0.5
|
||||
ANGLE_TOL = 2.0
|
||||
|
||||
|
||||
class BenchmarkFailure(AssertionError):
|
||||
pass
|
||||
|
||||
|
||||
def close(actual: float, expected: float, tol: float = BASIC_TOL) -> bool:
|
||||
return abs(actual - expected) <= tol
|
||||
|
||||
|
||||
def check(condition: bool, message: str) -> None:
|
||||
if not condition:
|
||||
raise BenchmarkFailure(message)
|
||||
|
||||
|
||||
def bbox(shape):
|
||||
box = shape.bounding_box()
|
||||
return box.min, box.max, (
|
||||
box.max.X - box.min.X,
|
||||
box.max.Y - box.min.Y,
|
||||
box.max.Z - box.min.Z,
|
||||
)
|
||||
|
||||
|
||||
def assert_bbox(shape, expected_size, expected_min=None, tol: float = BASIC_TOL):
|
||||
min_v, max_v, size = bbox(shape)
|
||||
for actual, expected, axis in zip(size, expected_size, "xyz"):
|
||||
check(close(actual, expected, tol), f"bbox {axis} size {actual:.3f} != {expected:.3f}")
|
||||
if expected_min is not None:
|
||||
for actual, expected, axis in zip((min_v.X, min_v.Y, min_v.Z), expected_min, "xyz"):
|
||||
check(close(actual, expected, tol), f"bbox min {axis} {actual:.3f} != {expected:.3f}")
|
||||
|
||||
|
||||
def axis_name(direction) -> str:
|
||||
components = {"x": abs(direction.X), "y": abs(direction.Y), "z": abs(direction.Z)}
|
||||
return max(components, key=components.get)
|
||||
|
||||
|
||||
def cyl_faces(shape, *, radius: float | None = None, axis: str | None = None, tol: float = BASIC_TOL):
|
||||
faces = []
|
||||
for face in shape.faces():
|
||||
if "CYLINDER" not in str(face.geom_type):
|
||||
continue
|
||||
if radius is not None and not close(face.radius, radius, tol):
|
||||
continue
|
||||
if axis is not None and axis_name(face.axis_of_rotation.direction) != axis:
|
||||
continue
|
||||
faces.append(face)
|
||||
return faces
|
||||
|
||||
|
||||
def torus_count(shape) -> int:
|
||||
return sum(1 for face in shape.faces() if "TORUS" in str(face.geom_type))
|
||||
|
||||
|
||||
def cone_count(shape) -> int:
|
||||
return sum(1 for face in shape.faces() if "CONE" in str(face.geom_type))
|
||||
|
||||
|
||||
def cylinder_centers(shape, *, radius: float, axis: str):
|
||||
centers = []
|
||||
for face in cyl_faces(shape, radius=radius, axis=axis):
|
||||
pos = face.axis_of_rotation.position
|
||||
centers.append((pos.X, pos.Y, pos.Z))
|
||||
return centers
|
||||
|
||||
|
||||
def expect_cylinder_at(shape, *, radius: float, axis: str, x=None, y=None, z=None, tol: float = BASIC_TOL):
|
||||
for center in cylinder_centers(shape, radius=radius, axis=axis):
|
||||
if x is not None and not close(center[0], x, tol):
|
||||
continue
|
||||
if y is not None and not close(center[1], y, tol):
|
||||
continue
|
||||
if z is not None and not close(center[2], z, tol):
|
||||
continue
|
||||
return
|
||||
raise BenchmarkFailure(f"missing cylinder radius {radius} axis {axis} at x={x} y={y} z={z}")
|
||||
|
||||
|
||||
def plane_faces_at(shape, *, axis: str, coordinate: float, tol: float = BASIC_TOL):
|
||||
index = "xyz".index(axis)
|
||||
faces = []
|
||||
for face in shape.faces():
|
||||
if "PLANE" not in str(face.geom_type):
|
||||
continue
|
||||
box = face.bounding_box()
|
||||
mins = (box.min.X, box.min.Y, box.min.Z)
|
||||
maxs = (box.max.X, box.max.Y, box.max.Z)
|
||||
if close(mins[index], coordinate, tol) and close(maxs[index], coordinate, tol):
|
||||
faces.append(face)
|
||||
return faces
|
||||
|
||||
|
||||
def assert_common(shape, *, solids: int | None = None, min_solids: int | None = None):
|
||||
check(shape.is_valid, "STEP imports but imported B-rep is invalid")
|
||||
check(shape.is_manifold, "STEP imports but B-rep is non-manifold")
|
||||
if solids is not None:
|
||||
check(len(shape.solids()) == solids, f"solid count {len(shape.solids())} != {solids}")
|
||||
if min_solids is not None:
|
||||
check(len(shape.solids()) >= min_solids, f"solid count {len(shape.solids())} < {min_solids}")
|
||||
|
||||
|
||||
def validate_01(shape):
|
||||
assert_common(shape, solids=1)
|
||||
assert_bbox(shape, (100.0, 60.0, 20.0), (-50.0, -30.0, 0.0))
|
||||
for x in (-35.0, 35.0):
|
||||
for y in (-20.0, 20.0):
|
||||
expect_cylinder_at(shape, radius=4.0, axis="z", x=x, y=y)
|
||||
check(len(cyl_faces(shape, radius=4.0, axis="z")) == 4, "expected four 8 mm through holes")
|
||||
top_faces = plane_faces_at(shape, axis="z", coordinate=20.0)
|
||||
check(any(close(face.bounding_box().size.X, 96.0) and close(face.bounding_box().size.Y, 56.0) for face in top_faces), "top chamfered face is not inset by 2 mm")
|
||||
check(cone_count(shape) == 0 and torus_count(shape) == 0, "unexpected hole chamfers/roundovers")
|
||||
|
||||
|
||||
def validate_02(shape):
|
||||
assert_common(shape, solids=1)
|
||||
assert_bbox(shape, (80.0, 80.0, 10.0), (-40.0, -40.0, 0.0))
|
||||
expect_cylinder_at(shape, radius=15.0, axis="z", x=0.0, y=0.0)
|
||||
bolt_centers = cylinder_centers(shape, radius=3.0, axis="z")
|
||||
check(len(bolt_centers) == 6, "expected six bolt holes")
|
||||
angles = sorted(degrees(atan2(y, x)) % 360.0 for x, y, _ in bolt_centers)
|
||||
for x, y, _ in bolt_centers:
|
||||
check(close(hypot(x, y), 30.0, COMPLEX_TOL), "bolt hole is off 60 mm bolt circle")
|
||||
gaps = [(angles[(i + 1) % 6] - angles[i]) % 360.0 for i in range(6)]
|
||||
check(all(close(gap, 60.0, ANGLE_TOL) for gap in gaps), "bolt holes are not spaced 60 degrees apart")
|
||||
check(torus_count(shape) >= 2, "missing top/bottom outer edge fillets")
|
||||
|
||||
|
||||
def validate_03(shape):
|
||||
assert_common(shape, solids=1)
|
||||
assert_bbox(shape, (80.0, 50.0, 58.0), (-40.0, -25.0, 0.0))
|
||||
for x in (-25.0, 25.0):
|
||||
expect_cylinder_at(shape, radius=3.0, axis="z", x=x, y=-10.0)
|
||||
expect_cylinder_at(shape, radius=3.0, axis="y", x=x, z=30.0)
|
||||
check(len(cyl_faces(shape, radius=3.0, axis="z")) == 2, "base hole count is not two")
|
||||
check(len(cyl_faces(shape, radius=3.0, axis="y")) == 2, "back plate hole count is not two")
|
||||
check(len(shape.faces()) >= 20, "missing gusset/fillet detail")
|
||||
|
||||
|
||||
def validate_04(shape):
|
||||
assert_common(shape, solids=1)
|
||||
assert_bbox(shape, (120.0, 30.0, 30.0), (0.0, -15.0, -15.0))
|
||||
check(len(cyl_faces(shape, radius=10.0, axis="x")) == 2, "expected two 20 mm shaft sections")
|
||||
check(len(cyl_faces(shape, radius=15.0, axis="x")) == 1, "expected one 30 mm shaft section")
|
||||
keyway_planes = plane_faces_at(shape, axis="z", coordinate=12.0)
|
||||
check(any(close(face.bounding_box().size.X, 40.0) and close(face.bounding_box().size.Y, 6.0) for face in keyway_planes), "keyway floor is not 40 x 6 mm at 3 mm depth")
|
||||
check(cone_count(shape) >= 2, "missing 1 mm end chamfers")
|
||||
|
||||
|
||||
def validate_05(shape):
|
||||
assert_common(shape, solids=1)
|
||||
assert_bbox(shape, (100.0, 70.0, 30.0), (-50.0, -35.0, 0.0))
|
||||
check(any(close(face.bounding_box().size.X, 94.0) and close(face.bounding_box().size.Y, 64.0) for face in plane_faces_at(shape, axis="z", coordinate=3.0)), "inside floor/wall thickness check failed")
|
||||
for x in (-35.0, 35.0):
|
||||
for y in (-25.0, 25.0):
|
||||
expect_cylinder_at(shape, radius=5.0, axis="z", x=x, y=y)
|
||||
expect_cylinder_at(shape, radius=1.5, axis="z", x=x, y=y)
|
||||
check(len(cyl_faces(shape, radius=5.0, axis="z")) == 4, "standoff count is not four")
|
||||
check(len(cyl_faces(shape, radius=1.5, axis="z")) == 4, "blind hole count is not four")
|
||||
check(len(cyl_faces(shape, radius=2.0, axis="z")) >= 4, "missing 2 mm outside vertical corner fillets")
|
||||
|
||||
|
||||
def validate_06(shape):
|
||||
assert_common(shape, solids=1)
|
||||
assert_bbox(shape, (120.0, 60.0, 52.0), (-60.0, -30.0, 0.0))
|
||||
expect_cylinder_at(shape, radius=7.0, axis="y", x=0.0, z=34.0)
|
||||
for x in (-45.0, 45.0):
|
||||
for y in (-20.0, 20.0):
|
||||
expect_cylinder_at(shape, radius=3.5, axis="z", x=x, y=y)
|
||||
check(len(cyl_faces(shape, radius=7.0, axis="y")) == 2, "clevis through-hole should pass through two separate lugs")
|
||||
check(len(cyl_faces(shape, radius=3.5, axis="z")) == 4, "base mounting hole count is not four")
|
||||
check(len(cyl_faces(shape, radius=2.0, axis="x")) >= 2, "missing lug-to-base radius detail")
|
||||
check(len(shape.faces()) >= 90, "missing cutout/rib/lug detail")
|
||||
|
||||
|
||||
def validate_07(shape):
|
||||
assert_common(shape, solids=1)
|
||||
assert_bbox(shape, (79.1, 70.0, 92.7), (-35.0, -35.0, 0.0), tol=COMPLEX_TOL)
|
||||
check(len(cyl_faces(shape, radius=31.0, axis="z")) == 12, "cooling fin count is not 12")
|
||||
check(len(cyl_faces(shape, radius=2.5, axis="z")) == 6, "mounting hole count is not six")
|
||||
check(len(cyl_faces(shape, radius=18.0, axis="z")) >= 1, "missing 36 mm barrel")
|
||||
check(len(cyl_faces(shape, radius=22.0, axis="z")) == 1, "missing 44 mm top cap")
|
||||
boss = cyl_faces(shape, radius=6.0)
|
||||
bore = [face for face in cyl_faces(shape, radius=2.5) if axis_name(face.axis_of_rotation.direction) == "x"]
|
||||
check(len(boss) == 1 and len(bore) == 1, "missing angled boss or boss bore")
|
||||
for face in (boss[0], bore[0]):
|
||||
direction = face.axis_of_rotation.direction
|
||||
angle = degrees(atan2(abs(direction.Z), abs(direction.X)))
|
||||
check(close(angle, 35.0, ANGLE_TOL), "boss axis is not 35 degrees upward")
|
||||
check(torus_count(shape) >= 24, "missing fin/flange edge fillets")
|
||||
|
||||
|
||||
def validate_08(shape):
|
||||
assert_common(shape, solids=1)
|
||||
assert_bbox(shape, (90.0, 90.0, 28.0), (-45.0, -45.0, 0.0), tol=COMPLEX_TOL)
|
||||
expect_cylinder_at(shape, radius=4.0, axis="z", x=0.0, y=0.0)
|
||||
check(len(cyl_faces(shape, radius=13.0, axis="z")) == 1, "missing 26 mm hub")
|
||||
check(len(shape.faces()) >= 220, "missing repeated curved blade detail")
|
||||
check(torus_count(shape) >= 2, "missing backplate edge fillets")
|
||||
|
||||
|
||||
def validate_09(shape):
|
||||
assert_common(shape, min_solids=43)
|
||||
assert_bbox(shape, (137.0, 137.0, 140.0), (-68.5, -68.5, 0.0), tol=COMPLEX_TOL)
|
||||
check(len(cyl_faces(shape, radius=7.0, axis="z")) == 1, "missing central column")
|
||||
check(len(cyl_faces(shape, radius=45.0, axis="z")) == 1, "missing base disk")
|
||||
check(len(cyl_faces(shape, radius=62.0, axis="z")) == 20, "tread outer radius/count failed")
|
||||
check(len(cyl_faces(shape, radius=10.0, axis="z")) == 20, "tread inner radius/count failed")
|
||||
check(len(cyl_faces(shape, radius=1.5, axis="z")) == 20, "baluster count failed")
|
||||
check(len(shape.solids()) == 43, "staircase assembly should have 43 solids")
|
||||
|
||||
|
||||
def validate_10(shape):
|
||||
assert_common(shape, solids=9)
|
||||
assert_bbox(shape, (140.0, 140.0, 14.0), (-70.0, -70.0, -5.0), tol=COMPLEX_TOL)
|
||||
check(len(cyl_faces(shape, radius=3.0, axis="z")) == 3, "planet pin count failed")
|
||||
check(len(cyl_faces(shape, radius=5.0, axis="z")) == 1, "sun bore failed")
|
||||
check(len(shape.faces()) >= 550 and len(shape.edges()) >= 1600, "gear tooth detail is missing")
|
||||
|
||||
|
||||
VALIDATORS = [
|
||||
("1. Rectangular calibration block with four holes", "benchmark_01_rectangular_calibration_block.step", validate_01),
|
||||
("2. Circular flange with bolt-hole pattern", "benchmark_02_circular_flange.step", validate_02),
|
||||
("3. L-bracket with gussets and two hole directions", "benchmark_03_l_bracket.step", validate_03),
|
||||
("4. Stepped shaft with keyway", "benchmark_04_stepped_shaft_keyway.step", validate_04),
|
||||
("5. Open-top electronics enclosure with bosses", "benchmark_05_open_top_electronics_enclosure.step", validate_05),
|
||||
("6. Aerospace-style clevis bracket with lightening cutouts", "benchmark_06_clevis_bracket_lightening_cutouts.step", validate_06),
|
||||
("7. Radial-engine-style cylinder with cooling fins", "benchmark_07_radial_engine_cylinder.step", validate_07),
|
||||
("8. Centrifugal impeller with backward-curved blades", "benchmark_08_centrifugal_impeller.step", validate_08),
|
||||
("9. Spiral staircase with helical handrail", "benchmark_09_spiral_staircase.step", validate_09),
|
||||
("10. Simplified planetary gear stage", "benchmark_10_planetary_gear_stage.step", validate_10),
|
||||
]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
failures = []
|
||||
for title, filename, validator in VALIDATORS:
|
||||
step_path = ROOT / filename
|
||||
try:
|
||||
shape = import_step(step_path)
|
||||
validator(shape)
|
||||
except Exception as exc:
|
||||
failures.append((title, exc))
|
||||
print(f"FAIL {title}: {exc}")
|
||||
else:
|
||||
print(f"PASS {title}")
|
||||
|
||||
if failures:
|
||||
print(f"\n{len(failures)} benchmark(s) failed.")
|
||||
return 1
|
||||
|
||||
print("\nAll benchmark checks passed.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user