Import CAD projects and cad-router v1

This commit is contained in:
Jerry
2026-07-22 19:38:36 +08:00
parent e4e0199bd5
commit af9f630c11
2253 changed files with 1994207 additions and 0 deletions
+48
View File
@@ -0,0 +1,48 @@
# Demo Models
Curated model fixtures and generator assets for text-to-cad workflows.
This tree is intended to be committed with Git LFS for large CAD, mesh, robot,
and G-code artifacts. Source generators and concise documentation remain normal
text files.
## Directory Map
- [benchmarks/](benchmarks/README.md): build123d benchmark generators plus STEP
and mesh outputs.
- [fun/](fun/README.md): standalone generated CAD examples and printable
outputs.
- [gcode/](gcode/README.md): small slicer input/output fixtures.
- [implicits/](implicits/README.md): browser-native implicit CAD examples.
- [mechanisms/](mechanisms/README.md): flattened mechanism STEP demos and
generated render sidecars.
- [robots/](robots/README.md): URDF/SRDF robot fixtures, meshes, printable
outputs, and selected robot STEP sources.
- [simple/](simple/README.md): compact build123d generators and STEP outputs
for basic parts.
The larger `mechbench/` and `mechbench2/` external datasets are intentionally
not included in this committed fixture tree.
## Git LFS Fetching
Repository LFS config excludes `models/**` from default LFS fetches so ordinary
checkout and publish jobs can avoid downloading every model blob. Fetch the
model artifacts explicitly when you need local bytes:
```bash
git lfs pull --include="models/**" --exclude=""
```
## Cleanup Policy
- Keep canonical sources (`*.py`, `*.implicit.js`, `*.urdf`, `*.srdf`, and docs)
readable in normal Git.
- Keep durable generated fixtures (`*.step`, `*.stl`, `*.3mf`, `*.glb`,
`*.gcode`, and `*.dxf`) in Git LFS.
- Do not commit supplementary media or sidecar metadata such as `*.png`,
`*.mp4`, `*.gif`, or `*.json` unless a future workflow defines them as a
required model artifact.
- Do not commit local runtime debris such as `.DS_Store`, `__pycache__/`,
`.cache/`, logs, or one-off timestamped review snapshots.
- Put temporary scratch artifacts under ignored local paths, not in this tree.
+24
View File
@@ -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())
@@ -0,0 +1,94 @@
{
"schema_version": "1.0",
"request": "生成中心距120mm、大头孔36mm、小头孔20mm、厚14mm的发动机连杆,导出STEP和STL,后续可以修改参数",
"route": {
"selected_backend": "build123d",
"runner_skill": "cad",
"confidence": "high",
"fallback_order": [
"simplecadapi",
"cadam"
],
"decision_path": "route-decision.json"
},
"source": {
"path": "connecting_rod.py",
"format": "python",
"backend": "build123d"
},
"artifacts": [
{
"path": "connecting_rod.step",
"role": "primary"
},
{
"path": "connecting_rod.stl",
"role": "secondary"
},
{
"path": "connecting_rod_20260722T071141Z.png",
"role": "verification_snapshot"
}
],
"parameters": {
"center_distance_mm": 120.0,
"thickness_mm": 14.0,
"big_end_outer_diameter_mm": 56.0,
"big_end_bore_diameter_mm": 36.0,
"small_end_outer_diameter_mm": 36.0,
"small_end_bore_diameter_mm": 20.0,
"web_width_mm": 24.0,
"pocket_depth_each_side_mm": 3.0
},
"assumptions": [
"This V1 sample demonstrates routing and editability rather than a production-rated engine component.",
"Material, fatigue loads, bearing fits, lubrication, fasteners, tolerances, and split big-end construction are intentionally out of scope."
],
"validation": [
{
"check": "closed solid count",
"result": 1,
"status": "pass"
},
{
"check": "bounding box millimeters",
"result": [
166.0,
56.0,
14.0
],
"status": "pass"
},
{
"check": "volume cubic millimeters",
"result": 50729.813,
"status": "pass"
},
{
"check": "bore radii millimeters",
"result": [
10.0,
18.0
],
"status": "pass"
},
{
"check": "STEP topology",
"result": {
"shape_count": 1,
"face_count": 18,
"edge_count": 42,
"warnings": []
},
"status": "pass"
},
{
"check": "isometric snapshot review",
"result": "Both bores, the connected web, and the top pocket are visibly present; no disconnected body is visible.",
"status": "pass"
}
],
"viewer_links": [
"http://127.0.0.1:4178/?dir=%2FUsers%2Fjerry%2Flinkhand%2FCadSet%2Ftext-to-cad%2Fmodels%2Fcad-router-v1&file=connecting_rod.step"
]
}
@@ -0,0 +1,59 @@
{
"schema_version": "1.0",
"request": "Generate a parameterized perforated vase for 3D printing",
"route": {
"selected_backend": "cadam",
"project": "CADAM",
"runner_skill": "cad-router",
"adapter": "skills/cad-router/scripts/cadam_compile.mjs",
"fallback_order": [
"build123d",
"simplecadapi"
],
"workflow_profiles": [
"caddesigner_ecip"
]
},
"source": {
"path": "cadam_parametric_vase.scad",
"format": "scad",
"backend": "cadam"
},
"artifacts": [
{
"path": "cadam_parametric_vase.stl",
"role": "primary"
}
],
"parameters": {
"height_mm": 80,
"base_radius_mm": 30,
"top_radius_mm": 24,
"wall_mm": 2.4,
"base_mm": 3
},
"assumptions": [
"The repeated perforations are decorative and are not load-bearing features."
],
"validation": [
{
"check": "CADAM OpenSCAD WASM compile",
"status": "passed",
"details": "exit code 0; manifold backend reported NoError; 3816 vertices and 7868 facets"
},
{
"check": "CAD Viewer review",
"status": "passed",
"details": "STL loaded from the task-scoped cad-router-v1 directory"
}
],
"project_contributions": {
"text-to-cad": "router, artifact contract, validation, and CAD Viewer",
"SimpleCADAPI": "available specialist mechanical fallback",
"CADAM": "SCAD source and OpenSCAD-WASM mesh execution",
"CADDesigner-Code": "ECIP brief and bounded execution/visual-repair workflow"
},
"viewer_links": [
"http://127.0.0.1:4178/?dir=%2FUsers%2Fjerry%2Flinkhand%2FCadSet%2Ftext-to-cad%2Fmodels%2Fcad-router-v1&file=cadam_parametric_vase.stl"
]
}
@@ -0,0 +1,27 @@
// CADAM/OpenSCAD-WASM route sample for CAD Router V1.
$fn = 96;
height = 80;
base_radius = 30;
top_radius = 24;
wall = 2.4;
base = 3;
difference() {
cylinder(h = height, r1 = base_radius, r2 = top_radius);
translate([0, 0, base])
cylinder(
h = height,
r1 = base_radius - wall,
r2 = top_radius - wall
);
for (z = [14 : 14 : 70]) {
for (angle = [0 : 30 : 330]) {
rotate([0, 0, angle + z * 0.75])
translate([base_radius - 1.0 - z * 0.07, 0, z])
rotate([0, 90, 0])
cylinder(h = wall + 5, r = 2.2, center = true, $fn = 24);
}
}
}
@@ -0,0 +1,72 @@
"""Parametric V1 connecting-rod sample routed to the build123d backend."""
from build123d import Align, Box, BuildPart, BuildSketch, Cylinder, Location, Locations, Mode, Plane, SlotOverall, extrude
CENTER_DISTANCE = 120.0
THICKNESS = 14.0
BIG_END_OUTER_DIAMETER = 56.0
BIG_END_BORE_DIAMETER = 36.0
SMALL_END_OUTER_DIAMETER = 36.0
SMALL_END_BORE_DIAMETER = 20.0
WEB_WIDTH = 24.0
POCKET_LENGTH = 72.0
POCKET_WIDTH = 12.0
POCKET_DEPTH = 3.0
def gen_step():
"""Return a closed, STEP-ready connecting-rod solid in millimeters."""
half_distance = CENTER_DISTANCE / 2.0
with BuildPart() as rod:
with Locations(Location((-half_distance, 0.0, 0.0))):
Cylinder(
radius=BIG_END_OUTER_DIAMETER / 2.0,
height=THICKNESS,
align=(Align.CENTER, Align.CENTER, Align.MIN),
)
with Locations(Location((half_distance, 0.0, 0.0))):
Cylinder(
radius=SMALL_END_OUTER_DIAMETER / 2.0,
height=THICKNESS,
align=(Align.CENTER, Align.CENTER, Align.MIN),
)
Box(
CENTER_DISTANCE,
WEB_WIDTH,
THICKNESS,
align=(Align.CENTER, Align.CENTER, Align.MIN),
)
with Locations(Location((-half_distance, 0.0, -1.0))):
Cylinder(
radius=BIG_END_BORE_DIAMETER / 2.0,
height=THICKNESS + 2.0,
align=(Align.CENTER, Align.CENTER, Align.MIN),
mode=Mode.SUBTRACT,
)
with Locations(Location((half_distance, 0.0, -1.0))):
Cylinder(
radius=SMALL_END_BORE_DIAMETER / 2.0,
height=THICKNESS + 2.0,
align=(Align.CENTER, Align.CENTER, Align.MIN),
mode=Mode.SUBTRACT,
)
with BuildSketch(Plane.XY):
SlotOverall(width=POCKET_LENGTH, height=POCKET_WIDTH)
extrude(amount=POCKET_DEPTH, mode=Mode.SUBTRACT)
with BuildSketch(Plane.XY.offset(THICKNESS - POCKET_DEPTH)):
SlotOverall(width=POCKET_LENGTH, height=POCKET_WIDTH)
extrude(amount=POCKET_DEPTH, mode=Mode.SUBTRACT)
result = rod.part
result.label = "cad_router_v1_connecting_rod"
return result
File diff suppressed because it is too large Load Diff
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 410 KiB

@@ -0,0 +1,69 @@
{
"schema_version": "1.0",
"request": "参考图片生成一个8孔圆法兰,中心孔直径20毫米,外径50毫米,厚度4毫米,螺丝孔使用M3",
"route": {
"selected_backend": "build123d",
"runner_skill": "cad",
"project": "text-to-cad",
"fallback_order": [
"simplecadapi",
"cadam"
],
"workflow_profiles": [
"caddesigner_ecip",
"requirement_refinement",
"visual_repair"
],
"downstream_skills": [
"cad-viewer"
]
},
"source": {
"path": "flange_8x_m3.py",
"format": "python",
"backend": "build123d"
},
"artifacts": [
{
"path": "flange_8x_m3.step",
"role": "primary"
},
{
"path": "flange_8x_m3.stl",
"role": "secondary"
}
],
"parameters": {
"outer_diameter_mm": 50.0,
"inner_diameter_mm": 20.0,
"thickness_mm": 4.0,
"bolt_hole_count": 8,
"bolt_hole_diameter_mm": 3.4,
"bolt_circle_diameter_mm": 40.0,
"angular_spacing_deg": 45.0
},
"assumptions": [
"M3螺丝孔按普通间隙通孔处理,孔径采用3.4毫米,不是M3攻丝底孔。",
"未指定分度圆,采用40毫米分度圆,以保持孔到外圆约3.3毫米的最小径向边距。",
"参考图中的车削纹理作为外观参考,不生成削弱4毫米薄法兰的深槽。"
],
"validation": [
{
"path": "validation.json",
"status": "passed",
"checks": {
"step_valid": true,
"solid_count": 1,
"exact_size_mm": [50.0, 50.0, 4.0],
"m3_clearance_hole_count": 8,
"m3_clearance_hole_diameter_mm": 3.4,
"bolt_circle_diameter_mm": 40.0,
"angular_spacing_deg": 45.0,
"visual_review": "passed_iso_opposite_top_front"
}
}
],
"viewer_links": [
"http://127.0.0.1:4178/?dir=%2FUsers%2Fjerry%2Flinkhand%2FCadSet%2Ftext-to-cad%2Fmodels%2Fcad-router-v1%2Fflange_8x_m3&file=flange_8x_m3.step"
]
}
@@ -0,0 +1,51 @@
"""Parametric eight-hole circular flange generated with build123d."""
from math import cos, radians, sin
from build123d import Align, BuildPart, Cylinder, Location, Locations, Mode
OUTER_DIAMETER_MM = 50.0
INNER_DIAMETER_MM = 20.0
THICKNESS_MM = 4.0
BOLT_HOLE_COUNT = 8
M3_CLEARANCE_DIAMETER_MM = 3.4
BOLT_CIRCLE_DIAMETER_MM = 40.0
def polar_point(radius: float, angle_degrees: float) -> tuple[float, float]:
angle = radians(angle_degrees)
return radius * cos(angle), radius * sin(angle)
def gen_step():
"""Return one STEP-ready flange solid in millimeters."""
with BuildPart() as flange:
Cylinder(
radius=OUTER_DIAMETER_MM / 2.0,
height=THICKNESS_MM,
align=(Align.CENTER, Align.CENTER, Align.MIN),
)
with Locations(Location((0.0, 0.0, -1.0))):
Cylinder(
radius=INNER_DIAMETER_MM / 2.0,
height=THICKNESS_MM + 2.0,
align=(Align.CENTER, Align.CENTER, Align.MIN),
mode=Mode.SUBTRACT,
)
for index in range(BOLT_HOLE_COUNT):
angle_degrees = 360.0 * index / BOLT_HOLE_COUNT
x_pos, y_pos = polar_point(BOLT_CIRCLE_DIAMETER_MM / 2.0, angle_degrees)
with Locations(Location((x_pos, y_pos, -1.0))):
Cylinder(
radius=M3_CLEARANCE_DIAMETER_MM / 2.0,
height=THICKNESS_MM + 2.0,
align=(Align.CENTER, Align.CENTER, Align.MIN),
mode=Mode.SUBTRACT,
)
part = flange.part
part.label = "flange_8x_m3_clearance"
return part
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

@@ -0,0 +1,29 @@
{
"input": "models/cad-router-v1/flange_8x_m3/flange_8x_m3.step",
"mode": "view",
"outputs": [
{
"path": "models/cad-router-v1/flange_8x_m3/review/iso.png",
"camera": "iso"
},
{
"path": "models/cad-router-v1/flange_8x_m3/review/iso_opposite.png",
"camera": {
"direction": [-1, 1, -0.8]
}
},
{
"path": "models/cad-router-v1/flange_8x_m3/review/top.png",
"camera": "top"
},
{
"path": "models/cad-router-v1/flange_8x_m3/review/front.png",
"camera": "front"
}
],
"render": {
"viewLabels": true,
"padding": 0.12,
"sizeProfile": "diagnostic"
}
}
@@ -0,0 +1,12 @@
{
"step_valid": true,
"solid_count": 1,
"exact_size_mm": [50.0, 50.0, 4.0],
"outer_radius_mm": 25.0,
"inner_radius_mm": 10.0,
"m3_clearance_hole_radius_mm": 1.7,
"m3_clearance_hole_count": 8,
"bolt_circle_radius_mm": 20.0,
"angular_spacing_deg": [45.0, 45.0, 45.0, 45.0, 45.0, 45.0, 45.0, 45.0],
"visual_review": "passed_iso_opposite_top_front"
}
@@ -0,0 +1,82 @@
{
"schema_version": "1.0",
"request": "生成模数2、20齿、12毫米厚、中心孔12毫米的人字齿轮,导出STEP并保留可重放模型图",
"route": {
"selected_backend": "simplecadapi",
"runner_skill": "simplecadapi",
"confidence": "high",
"fallback_order": [
"build123d",
"cadam"
],
"decision_path": "gear-route-decision.json"
},
"source": {
"path": "herringbone_gear_simplecad.py",
"format": "python",
"backend": "simplecadapi",
"replay_model": "herringbone_gear_simplecad.json"
},
"artifacts": [
{
"path": "herringbone_gear_simplecad.step",
"role": "primary"
},
{
"path": "herringbone_gear_simplecad.stl",
"role": "secondary"
},
{
"path": "herringbone_gear_simplecad_20260722T072647Z.png",
"role": "verification_snapshot"
}
],
"parameters": {
"teeth": 20,
"module_mm": 2.0,
"pressure_angle_deg": 20.0,
"helix_angle_deg": 30.0,
"height_mm": 12.0,
"bore_diameter_mm": 12.0,
"backlash_mm": 0.05
},
"validation": [
{
"check": "SimpleCAD operation graph nodes",
"result": 270,
"status": "pass"
},
{
"check": "volume cubic millimeters",
"result": 13213.684,
"status": "pass"
},
{
"check": "STEP topology",
"result": {
"shape_count": 1,
"face_count": 963,
"edge_count": 2043,
"bounds_mm": [
43.99775,
43.99775,
12.0
],
"warnings": []
},
"status": "pass"
},
{
"check": "isometric snapshot review",
"result": "Twenty double-helical teeth, the center bore, and the herringbone ridge are visibly present.",
"status": "pass"
}
],
"assumptions": [
"This sample validates backend routing and parametric generation; it is not a rated transmission design.",
"Material, heat treatment, profile shift, tolerance class, strength, lubrication, and mating gear checks remain out of scope."
],
"viewer_links": [
"http://127.0.0.1:4178/?dir=%2FUsers%2Fjerry%2Flinkhand%2FCadSet%2Ftext-to-cad%2Fmodels%2Fcad-router-v1&file=herringbone_gear_simplecad.step"
]
}
@@ -0,0 +1,64 @@
{
"schema_version": "1.0",
"request": "生成模数2、20齿、12毫米厚、中心孔12毫米的人字齿轮,导出STEP并保留可重放模型图",
"selected_backend": "simplecadapi",
"runner_skill": "simplecadapi",
"confidence": "high",
"backend_scores": [
{
"backend": "simplecadapi",
"score": 175,
"reasons": [
"+20 specialist mechanical baseline",
"+65 supported standard mechanical family",
"+60 replay, semantic graph, or FreeCAD requirement",
"+30 OCP STEP export requested"
]
},
{
"backend": "build123d",
"score": 78,
"reasons": [
"+35 general STEP-first default",
"+8 can model the family generically",
"+35 editable STEP requested"
]
},
{
"backend": "cadam",
"score": -60,
"reasons": [
"+10 printable CSG baseline",
"-70 no native editable STEP output"
]
}
],
"fallback_order": [
"build123d",
"cadam"
],
"workflow_profiles": [
"caddesigner_ecip"
],
"downstream_skills": [
"cad-viewer"
],
"requested_outputs": [
"step"
],
"source_of_truth": "SimpleCADAPI Python generator and model JSON",
"availability": {
"build123d": {
"available": true,
"probe": "python=/Users/jerry/linkhand/CadSet/text-to-cad/.venv/bin/python"
},
"simplecadapi": {
"available": true,
"probe": "python=/Users/jerry/linkhand/CadSet/text-to-cad/.venv/bin/python"
},
"cadam": {
"available": true,
"probe": "/Users/jerry/linkhand/CadSet/CADAM"
}
}
}
@@ -0,0 +1,75 @@
{
"schema_version": "1.0",
"request": "生成一个28齿、厚度4毫米的齿轮",
"route": {
"selected_backend": "simplecadapi",
"runner_skill": "simplecadapi",
"project": "SimpleCADAPI",
"fallback_order": [
"build123d",
"cadam"
],
"workflow_profiles": [
"caddesigner_ecip"
],
"downstream_skills": [
"cad-viewer"
]
},
"source": {
"path": "gear_28t_4mm.py",
"format": "python",
"backend": "simplecadapi"
},
"artifacts": [
{
"path": "gear_28t_4mm.model.json",
"role": "replayable_model"
},
{
"path": "gear_28t_4mm.step",
"role": "primary"
},
{
"path": "gear_28t_4mm.stl",
"role": "secondary"
}
],
"parameters": {
"n_teeth": 28,
"module_mm": 1.0,
"pressure_angle_deg": 20.0,
"thickness_mm": 4.0,
"bore_diameter_mm": 5.0,
"addendum_factor": 1.0,
"clearance_factor": 0.25,
"backlash_mm": 0.0
},
"assumptions": [
"未指定模数,V1采用1.0毫米。",
"未指定压力角,采用标准20度。",
"未指定中心孔,采用5.0毫米通孔。",
"未指定齿轮类型,采用直齿渐开线外齿轮。"
],
"validation": [
{
"path": "validation.json",
"status": "passed",
"checks": {
"step_valid": true,
"solid_count": 1,
"bounds_mm": [
30.0,
30.0,
4.0
],
"pitch_diameter_mm": 28.0,
"outside_diameter_mm": 30.0,
"visual_review": "passed_iso_top_front"
}
}
],
"viewer_links": [
"http://127.0.0.1:4178/?dir=%2FUsers%2Fjerry%2Flinkhand%2FCadSet%2Ftext-to-cad%2Fmodels%2Fcad-router-v1%2Fgear_28t_4mm&file=gear_28t_4mm.step"
]
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,78 @@
"""Parametric 28-tooth spur gear generated with SimpleCADAPI."""
import json
from pathlib import Path
import simplecadapi as scad
N_TEETH = 28
MODULE_MM = 1.0
PRESSURE_ANGLE_DEG = 20.0
THICKNESS_MM = 4.0
BORE_DIAMETER_MM = 5.0
ADDENDUM_FACTOR = 1.0
CLEARANCE_FACTOR = 0.25
BACKLASH_MM = 0.0
def generate() -> None:
output_dir = Path(__file__).resolve().parent
with scad.GraphSession() as session:
gear_blank = scad.std.gear.make_spur_gear_rsolid(
n_teeth=N_TEETH,
module=MODULE_MM,
pressure_angle=PRESSURE_ANGLE_DEG,
gear_height=THICKNESS_MM,
addendum_factor=ADDENDUM_FACTOR,
clearance_factor=CLEARANCE_FACTOR,
backlash=BACKLASH_MM,
)
blank_faces = scad.ql.select(items=gear_blank.get_faces()).all()
print("gear_blank_faces", len(blank_faces))
bore = scad.make_cylinder_rsolid(
radius=BORE_DIAMETER_MM / 2.0,
height=THICKNESS_MM + 2.0,
bottom_face_center=(0.0, 0.0, -1.0),
axis=(0.0, 0.0, 1.0),
)
gear = scad.cut_rsolid(gear_blank, bore, skip_non_intersecting=False)
gear = scad.apply_tag(shape=gear, tag="role.power_transmission.gear")
final_faces = scad.ql.select(items=gear.get_faces()).all()
final_edges = scad.ql.select(items=gear.get_edges()).all()
print("final_topology", {"faces": len(final_faces), "edges": len(final_edges)})
model_json = scad.export_model_json(session=session, indent=2)
(output_dir / "gear_28t_4mm.model.json").write_text(model_json, encoding="utf-8")
scad.export_step(shapes=gear, filename=str(output_dir / "gear_28t_4mm.step"))
scad.export_stl(shapes=gear, filename=str(output_dir / "gear_28t_4mm.stl"))
pitch_diameter_mm = MODULE_MM * N_TEETH
outside_diameter_mm = MODULE_MM * (N_TEETH + 2.0 * ADDENDUM_FACTOR)
root_diameter_mm = MODULE_MM * (
N_TEETH - 2.0 * (ADDENDUM_FACTOR + CLEARANCE_FACTOR)
)
validation = {
"n_teeth": N_TEETH,
"pitch_diameter_mm": pitch_diameter_mm,
"outside_diameter_mm": outside_diameter_mm,
"root_diameter_mm": root_diameter_mm,
"thickness_mm": THICKNESS_MM,
"bore_diameter_mm": BORE_DIAMETER_MM,
"volume_mm3": round(gear.get_volume(), 6),
"faces": len(final_faces),
"edges": len(final_edges),
"tags": scad.list_tags(shape=gear),
"graph_nodes": session.graph.node_count,
}
(output_dir / "validation.json").write_text(
json.dumps(validation, indent=2, ensure_ascii=False) + "\n",
encoding="utf-8",
)
print("validation", validation)
if __name__ == "__main__":
generate()
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,19 @@
{
"n_teeth": 28,
"pitch_diameter_mm": 28.0,
"outside_diameter_mm": 30.0,
"root_diameter_mm": 25.5,
"thickness_mm": 4.0,
"bore_diameter_mm": 5.0,
"volume_mm3": 2343.649578,
"faces": 171,
"edges": 507,
"tags": [
"role.power_transmission.gear",
"sketch.gear_28t_m1.id_0",
"sketch_profile.arc_root_0",
"solid.boolean.cut",
"solid.extrusion"
],
"graph_nodes": 351
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,50 @@
"""SimpleCADAPI specialist-backend smoke test for CAD Router V1."""
from pathlib import Path
import simplecadapi as scad
N_TEETH = 20
MODULE = 2.0
PRESSURE_ANGLE = 20.0
HELIX_ANGLE = 30.0
GEAR_HEIGHT = 12.0
BORE_DIAMETER = 12.0
def generate() -> None:
output_root = Path(__file__).resolve().parent
with scad.GraphSession() as session:
gear = scad.std.gear.make_herringbone_gear_rsolid(
n_teeth=N_TEETH,
module=MODULE,
pressure_angle=PRESSURE_ANGLE,
helix_angle=HELIX_ANGLE,
gear_height=GEAR_HEIGHT,
addendum_factor=1.0,
clearance_factor=0.25,
backlash=0.05,
)
bore = scad.make_cylinder_rsolid(
radius=BORE_DIAMETER / 2.0,
height=GEAR_HEIGHT + 2.0,
bottom_face_center=(0.0, 0.0, -1.0),
axis=(0.0, 0.0, 1.0),
)
gear = scad.cut_rsolid(gear, bore, skip_non_intersecting=False)
gear = scad.apply_tag(shape=gear, tag="role.power_transmission.gear")
model_json = scad.export_model_json(session=session, indent=2)
(output_root / "herringbone_gear_simplecad.json").write_text(model_json, encoding="utf-8")
scad.export_step(shapes=gear, filename=str(output_root / "herringbone_gear_simplecad.step"))
scad.export_stl(shapes=gear, filename=str(output_root / "herringbone_gear_simplecad.stl"))
print("volume_mm3", round(gear.get_volume(), 3))
print("tags", scad.list_tags(shape=gear))
print("graph_nodes", session.graph.node_count)
if __name__ == "__main__":
generate()
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 953 KiB

@@ -0,0 +1,56 @@
{
"schema_version": "1.0",
"request": "生成中心距120mm、大头孔36mm、小头孔20mm、厚14mm的发动机连杆,导出STEP和STL,后续可以修改参数",
"selected_backend": "build123d",
"runner_skill": "cad",
"confidence": "high",
"backend_scores": [
{
"backend": "build123d",
"score": 135,
"reasons": [
"+35 general STEP-first default",
"+35 general mechanical part with direct parametric features",
"+25 machining favors STEP-first B-Rep",
"+35 editable STEP requested",
"+5 secondary mesh export is available"
]
},
{
"backend": "simplecadapi",
"score": 78,
"reasons": [
"+20 specialist mechanical baseline",
"+10 general OCP modeling is available",
"+18 OCP STEP geometry suits machining",
"+30 OCP STEP export requested"
]
},
{
"backend": "cadam",
"score": -73,
"reasons": [
"+10 printable CSG baseline",
"-25 mesh/CSG route is not preferred for machining",
"-70 no native editable STEP output",
"+12 print mesh requested"
]
}
],
"fallback_order": [
"simplecadapi",
"cadam"
],
"workflow_profiles": [
"caddesigner_ecip"
],
"downstream_skills": [
"cad-viewer"
],
"requested_outputs": [
"step",
"stl"
],
"source_of_truth": "build123d Python generator",
"availability": null
}
@@ -0,0 +1,24 @@
"""Approximate long multi-collar shaft inferred from reference 001957.png."""
from shaft_common import cut_end_bore, cut_side_flat, export_horizontal, stepped_body
SEGMENTS = [
(10.0, 34.0), # reduced drive tip
(14.0, 218.0), # long shaft
(19.0, 18.0), # transition shoulder
(30.0, 4.0),
(25.0, 7.0),
(32.0, 5.0),
(25.0, 8.0),
(31.0, 5.0),
(23.0, 8.0),
(14.0, 30.0), # hollow output nose
]
def gen_step():
body, total = stepped_body(SEGMENTS)
body = cut_side_flat(body, diameter=10.0, z_start=27.0, length=7.0, flat_depth=1.4)
body = cut_end_bore(body, total, diameter=8.0, depth=18.0)
return export_horizontal(body, "shaft_001957_like")
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,15 @@
"""Approximate long cylindrical pin with a shallow axial end recess."""
from shaft_common import cut_end_bore, export_horizontal, stepped_body
OUTER_DIAMETER = 18.0
LENGTH = 120.0
RECESS_DIAMETER = 6.0
RECESS_DEPTH = 5.0
def gen_step():
body, total = stepped_body([(OUTER_DIAMETER, LENGTH)])
body = cut_end_bore(body, total, RECESS_DIAMETER, RECESS_DEPTH)
return export_horizontal(body, "pin_004910_like")
@@ -0,0 +1,296 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('Open CASCADE Model'),'2;1');
FILE_NAME('Open CASCADE Shape Model','2026-07-22T18:43:40',('Author'),(
'Open CASCADE'),'Open CASCADE STEP processor 7.9','Open CASCADE 7.9'
,'Unknown');
FILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }'));
ENDSEC;
DATA;
#1 = APPLICATION_PROTOCOL_DEFINITION('international standard',
'automotive_design',2000,#2);
#2 = APPLICATION_CONTEXT(
'core data for automotive mechanical design processes');
#3 = SHAPE_DEFINITION_REPRESENTATION(#4,#10);
#4 = PRODUCT_DEFINITION_SHAPE('','',#5);
#5 = PRODUCT_DEFINITION('design','',#6,#9);
#6 = PRODUCT_DEFINITION_FORMATION('','',#7);
#7 = PRODUCT('=>[0:1:1:2]','=>[0:1:1:2]','',(#8));
#8 = PRODUCT_CONTEXT('',#2,'mechanical');
#9 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design');
#10 = SHAPE_REPRESENTATION('',(#11,#15),#19);
#11 = AXIS2_PLACEMENT_3D('',#12,#13,#14);
#12 = CARTESIAN_POINT('',(0.,0.,0.));
#13 = DIRECTION('',(0.,0.,1.));
#14 = DIRECTION('',(1.,0.,-0.));
#15 = AXIS2_PLACEMENT_3D('',#16,#17,#18);
#16 = CARTESIAN_POINT('',(0.,0.,0.));
#17 = DIRECTION('',(1.,-0.,1.110223024625E-16));
#18 = DIRECTION('',(1.110223024625E-16,0.,-1.));
#19 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3)
GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#23)) GLOBAL_UNIT_ASSIGNED_CONTEXT(
(#20,#21,#22)) REPRESENTATION_CONTEXT('Context #1',
'3D Context with UNIT and UNCERTAINTY') );
#20 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) );
#21 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) );
#22 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() );
#23 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#20,
'distance_accuracy_value','confusion accuracy');
#24 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#7));
#25 = SHAPE_DEFINITION_REPRESENTATION(#26,#32);
#26 = PRODUCT_DEFINITION_SHAPE('','',#27);
#27 = PRODUCT_DEFINITION('design','',#28,#31);
#28 = PRODUCT_DEFINITION_FORMATION('','',#29);
#29 = PRODUCT('pin_004910_like','pin_004910_like','',(#30));
#30 = PRODUCT_CONTEXT('',#2,'mechanical');
#31 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design');
#32 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#33),#221);
#33 = MANIFOLD_SOLID_BREP('',#34);
#34 = CLOSED_SHELL('',(#35,#123,#158,#162,#217));
#35 = ADVANCED_FACE('',(#36),#49,.T.);
#36 = FACE_BOUND('',#37,.T.);
#37 = EDGE_LOOP('',(#38,#72,#95,#122));
#38 = ORIENTED_EDGE('',*,*,#39,.F.);
#39 = EDGE_CURVE('',#40,#40,#42,.T.);
#40 = VERTEX_POINT('',#41);
#41 = CARTESIAN_POINT('',(9.,-2.204364238465E-15,120.));
#42 = SURFACE_CURVE('',#43,(#48,#60),.PCURVE_S1.);
#43 = CIRCLE('',#44,9.);
#44 = AXIS2_PLACEMENT_3D('',#45,#46,#47);
#45 = CARTESIAN_POINT('',(0.,0.,120.));
#46 = DIRECTION('',(0.,0.,1.));
#47 = DIRECTION('',(1.,0.,-0.));
#48 = PCURVE('',#49,#54);
#49 = CYLINDRICAL_SURFACE('',#50,9.);
#50 = AXIS2_PLACEMENT_3D('',#51,#52,#53);
#51 = CARTESIAN_POINT('',(0.,0.,0.));
#52 = DIRECTION('',(0.,0.,1.));
#53 = DIRECTION('',(1.,0.,-0.));
#54 = DEFINITIONAL_REPRESENTATION('',(#55),#59);
#55 = LINE('',#56,#57);
#56 = CARTESIAN_POINT('',(0.,120.));
#57 = VECTOR('',#58,1.);
#58 = DIRECTION('',(1.,0.));
#59 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#60 = PCURVE('',#61,#66);
#61 = PLANE('',#62);
#62 = AXIS2_PLACEMENT_3D('',#63,#64,#65);
#63 = CARTESIAN_POINT('',(0.,0.,120.));
#64 = DIRECTION('',(0.,0.,1.));
#65 = DIRECTION('',(1.,0.,-0.));
#66 = DEFINITIONAL_REPRESENTATION('',(#67),#71);
#67 = CIRCLE('',#68,9.);
#68 = AXIS2_PLACEMENT_2D('',#69,#70);
#69 = CARTESIAN_POINT('',(0.,0.));
#70 = DIRECTION('',(1.,0.));
#71 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#72 = ORIENTED_EDGE('',*,*,#73,.F.);
#73 = EDGE_CURVE('',#74,#40,#76,.T.);
#74 = VERTEX_POINT('',#75);
#75 = CARTESIAN_POINT('',(9.,-2.204364238465E-15,0.));
#76 = SEAM_CURVE('',#77,(#81,#88),.PCURVE_S1.);
#77 = LINE('',#78,#79);
#78 = CARTESIAN_POINT('',(9.,-2.204364238465E-15,0.));
#79 = VECTOR('',#80,1.);
#80 = DIRECTION('',(0.,0.,1.));
#81 = PCURVE('',#49,#82);
#82 = DEFINITIONAL_REPRESENTATION('',(#83),#87);
#83 = LINE('',#84,#85);
#84 = CARTESIAN_POINT('',(6.28318530718,-0.));
#85 = VECTOR('',#86,1.);
#86 = DIRECTION('',(0.,1.));
#87 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#88 = PCURVE('',#49,#89);
#89 = DEFINITIONAL_REPRESENTATION('',(#90),#94);
#90 = LINE('',#91,#92);
#91 = CARTESIAN_POINT('',(0.,-0.));
#92 = VECTOR('',#93,1.);
#93 = DIRECTION('',(0.,1.));
#94 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#95 = ORIENTED_EDGE('',*,*,#96,.T.);
#96 = EDGE_CURVE('',#74,#74,#97,.T.);
#97 = SURFACE_CURVE('',#98,(#103,#110),.PCURVE_S1.);
#98 = CIRCLE('',#99,9.);
#99 = AXIS2_PLACEMENT_3D('',#100,#101,#102);
#100 = CARTESIAN_POINT('',(0.,0.,0.));
#101 = DIRECTION('',(0.,0.,1.));
#102 = DIRECTION('',(1.,0.,-0.));
#103 = PCURVE('',#49,#104);
#104 = DEFINITIONAL_REPRESENTATION('',(#105),#109);
#105 = LINE('',#106,#107);
#106 = CARTESIAN_POINT('',(0.,0.));
#107 = VECTOR('',#108,1.);
#108 = DIRECTION('',(1.,0.));
#109 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#110 = PCURVE('',#111,#116);
#111 = PLANE('',#112);
#112 = AXIS2_PLACEMENT_3D('',#113,#114,#115);
#113 = CARTESIAN_POINT('',(0.,0.,0.));
#114 = DIRECTION('',(0.,0.,1.));
#115 = DIRECTION('',(1.,0.,-0.));
#116 = DEFINITIONAL_REPRESENTATION('',(#117),#121);
#117 = CIRCLE('',#118,9.);
#118 = AXIS2_PLACEMENT_2D('',#119,#120);
#119 = CARTESIAN_POINT('',(0.,0.));
#120 = DIRECTION('',(1.,0.));
#121 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#122 = ORIENTED_EDGE('',*,*,#73,.T.);
#123 = ADVANCED_FACE('',(#124,#127),#61,.T.);
#124 = FACE_BOUND('',#125,.T.);
#125 = EDGE_LOOP('',(#126));
#126 = ORIENTED_EDGE('',*,*,#39,.T.);
#127 = FACE_BOUND('',#128,.T.);
#128 = EDGE_LOOP('',(#129));
#129 = ORIENTED_EDGE('',*,*,#130,.F.);
#130 = EDGE_CURVE('',#131,#131,#133,.T.);
#131 = VERTEX_POINT('',#132);
#132 = CARTESIAN_POINT('',(3.,-7.347880794884E-16,120.));
#133 = SURFACE_CURVE('',#134,(#139,#146),.PCURVE_S1.);
#134 = CIRCLE('',#135,3.);
#135 = AXIS2_PLACEMENT_3D('',#136,#137,#138);
#136 = CARTESIAN_POINT('',(0.,0.,120.));
#137 = DIRECTION('',(0.,0.,1.));
#138 = DIRECTION('',(1.,0.,-0.));
#139 = PCURVE('',#61,#140);
#140 = DEFINITIONAL_REPRESENTATION('',(#141),#145);
#141 = CIRCLE('',#142,3.);
#142 = AXIS2_PLACEMENT_2D('',#143,#144);
#143 = CARTESIAN_POINT('',(0.,0.));
#144 = DIRECTION('',(1.,0.));
#145 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#146 = PCURVE('',#147,#152);
#147 = CYLINDRICAL_SURFACE('',#148,3.);
#148 = AXIS2_PLACEMENT_3D('',#149,#150,#151);
#149 = CARTESIAN_POINT('',(0.,0.,115.));
#150 = DIRECTION('',(0.,0.,1.));
#151 = DIRECTION('',(1.,0.,-0.));
#152 = DEFINITIONAL_REPRESENTATION('',(#153),#157);
#153 = LINE('',#154,#155);
#154 = CARTESIAN_POINT('',(0.,5.));
#155 = VECTOR('',#156,1.);
#156 = DIRECTION('',(1.,0.));
#157 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#158 = ADVANCED_FACE('',(#159),#111,.F.);
#159 = FACE_BOUND('',#160,.T.);
#160 = EDGE_LOOP('',(#161));
#161 = ORIENTED_EDGE('',*,*,#96,.F.);
#162 = ADVANCED_FACE('',(#163),#147,.F.);
#163 = FACE_BOUND('',#164,.F.);
#164 = EDGE_LOOP('',(#165,#166,#189,#216));
#165 = ORIENTED_EDGE('',*,*,#130,.F.);
#166 = ORIENTED_EDGE('',*,*,#167,.F.);
#167 = EDGE_CURVE('',#168,#131,#170,.T.);
#168 = VERTEX_POINT('',#169);
#169 = CARTESIAN_POINT('',(3.,-7.347880794884E-16,115.));
#170 = SEAM_CURVE('',#171,(#175,#182),.PCURVE_S1.);
#171 = LINE('',#172,#173);
#172 = CARTESIAN_POINT('',(3.,-7.347880794884E-16,115.));
#173 = VECTOR('',#174,1.);
#174 = DIRECTION('',(0.,0.,1.));
#175 = PCURVE('',#147,#176);
#176 = DEFINITIONAL_REPRESENTATION('',(#177),#181);
#177 = LINE('',#178,#179);
#178 = CARTESIAN_POINT('',(6.28318530718,-0.));
#179 = VECTOR('',#180,1.);
#180 = DIRECTION('',(0.,1.));
#181 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#182 = PCURVE('',#147,#183);
#183 = DEFINITIONAL_REPRESENTATION('',(#184),#188);
#184 = LINE('',#185,#186);
#185 = CARTESIAN_POINT('',(0.,-0.));
#186 = VECTOR('',#187,1.);
#187 = DIRECTION('',(0.,1.));
#188 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#189 = ORIENTED_EDGE('',*,*,#190,.T.);
#190 = EDGE_CURVE('',#168,#168,#191,.T.);
#191 = SURFACE_CURVE('',#192,(#197,#204),.PCURVE_S1.);
#192 = CIRCLE('',#193,3.);
#193 = AXIS2_PLACEMENT_3D('',#194,#195,#196);
#194 = CARTESIAN_POINT('',(0.,0.,115.));
#195 = DIRECTION('',(0.,0.,1.));
#196 = DIRECTION('',(1.,0.,-0.));
#197 = PCURVE('',#147,#198);
#198 = DEFINITIONAL_REPRESENTATION('',(#199),#203);
#199 = LINE('',#200,#201);
#200 = CARTESIAN_POINT('',(0.,0.));
#201 = VECTOR('',#202,1.);
#202 = DIRECTION('',(1.,0.));
#203 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#204 = PCURVE('',#205,#210);
#205 = PLANE('',#206);
#206 = AXIS2_PLACEMENT_3D('',#207,#208,#209);
#207 = CARTESIAN_POINT('',(0.,0.,115.));
#208 = DIRECTION('',(0.,0.,1.));
#209 = DIRECTION('',(1.,0.,-0.));
#210 = DEFINITIONAL_REPRESENTATION('',(#211),#215);
#211 = CIRCLE('',#212,3.);
#212 = AXIS2_PLACEMENT_2D('',#213,#214);
#213 = CARTESIAN_POINT('',(0.,0.));
#214 = DIRECTION('',(1.,0.));
#215 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#216 = ORIENTED_EDGE('',*,*,#167,.T.);
#217 = ADVANCED_FACE('',(#218),#205,.T.);
#218 = FACE_BOUND('',#219,.F.);
#219 = EDGE_LOOP('',(#220));
#220 = ORIENTED_EDGE('',*,*,#190,.F.);
#221 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3)
GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#225)) GLOBAL_UNIT_ASSIGNED_CONTEXT
((#222,#223,#224)) REPRESENTATION_CONTEXT('Context #1',
'3D Context with UNIT and UNCERTAINTY') );
#222 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) );
#223 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) );
#224 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() );
#225 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#222,
'distance_accuracy_value','confusion accuracy');
#226 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#227,#229);
#227 = ( REPRESENTATION_RELATIONSHIP('','',#32,#10)
REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#228)
SHAPE_REPRESENTATION_RELATIONSHIP() );
#228 = ITEM_DEFINED_TRANSFORMATION('','',#11,#15);
#229 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item',#230
);
#230 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('1','=>[0:1:1:2]','',#5,#27,$);
#231 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#29));
#232=DESCRIPTIVE_REPRESENTATION_ITEM('cadpy:generator','cadpy');
#233=REPRESENTATION('cadpy:generator',(#232),#19);
#234=PROPERTY_DEFINITION('cadpy metadata','cadpy:generator',#5);
#235=PROPERTY_DEFINITION_REPRESENTATION(#234,#233);
#236=DESCRIPTIVE_REPRESENTATION_ITEM('cadpy:entryKind','part');
#237=REPRESENTATION('cadpy:entryKind',(#236),#19);
#238=PROPERTY_DEFINITION('cadpy metadata','cadpy:entryKind',#5);
#239=PROPERTY_DEFINITION_REPRESENTATION(#238,#237);
#240=DESCRIPTIVE_REPRESENTATION_ITEM('cadpy:sourcePath','004910_like.py');
#241=REPRESENTATION('cadpy:sourcePath',(#240),#19);
#242=PROPERTY_DEFINITION('cadpy metadata','cadpy:sourcePath',#5);
#243=PROPERTY_DEFINITION_REPRESENTATION(#242,#241);
#244=DESCRIPTIVE_REPRESENTATION_ITEM('cadpy:sourceHash','d6933bc68c9b9ae8b4ffafc77016167fc587342bd4ff83b33f41060611757bc6');
#245=REPRESENTATION('cadpy:sourceHash',(#244),#19);
#246=PROPERTY_DEFINITION('cadpy metadata','cadpy:sourceHash',#5);
#247=PROPERTY_DEFINITION_REPRESENTATION(#246,#245);
ENDSEC;
END-ISO-10303-21;
@@ -0,0 +1,12 @@
"""Approximate stepped shaft with a broad axial key/wrench flat."""
from shaft_common import cut_side_flat, export_horizontal, stepped_body
SEGMENTS = [(16.0, 68.0), (24.0, 22.0)]
def gen_step():
body, _ = stepped_body(SEGMENTS)
body = cut_side_flat(body, diameter=16.0, z_start=0.0, length=30.0, flat_depth=2.7)
return export_horizontal(body, "stepped_shaft_054964_like")
@@ -0,0 +1,479 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('Open CASCADE Model'),'2;1');
FILE_NAME('Open CASCADE Shape Model','2026-07-22T18:43:40',('Author'),(
'Open CASCADE'),'Open CASCADE STEP processor 7.9','Open CASCADE 7.9'
,'Unknown');
FILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }'));
ENDSEC;
DATA;
#1 = APPLICATION_PROTOCOL_DEFINITION('international standard',
'automotive_design',2000,#2);
#2 = APPLICATION_CONTEXT(
'core data for automotive mechanical design processes');
#3 = SHAPE_DEFINITION_REPRESENTATION(#4,#10);
#4 = PRODUCT_DEFINITION_SHAPE('','',#5);
#5 = PRODUCT_DEFINITION('design','',#6,#9);
#6 = PRODUCT_DEFINITION_FORMATION('','',#7);
#7 = PRODUCT('=>[0:1:1:2]','=>[0:1:1:2]','',(#8));
#8 = PRODUCT_CONTEXT('',#2,'mechanical');
#9 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design');
#10 = SHAPE_REPRESENTATION('',(#11,#15),#19);
#11 = AXIS2_PLACEMENT_3D('',#12,#13,#14);
#12 = CARTESIAN_POINT('',(0.,0.,0.));
#13 = DIRECTION('',(0.,0.,1.));
#14 = DIRECTION('',(1.,0.,-0.));
#15 = AXIS2_PLACEMENT_3D('',#16,#17,#18);
#16 = CARTESIAN_POINT('',(0.,0.,0.));
#17 = DIRECTION('',(1.,-0.,1.110223024625E-16));
#18 = DIRECTION('',(1.110223024625E-16,0.,-1.));
#19 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3)
GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#23)) GLOBAL_UNIT_ASSIGNED_CONTEXT(
(#20,#21,#22)) REPRESENTATION_CONTEXT('Context #1',
'3D Context with UNIT and UNCERTAINTY') );
#20 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) );
#21 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) );
#22 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() );
#23 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#20,
'distance_accuracy_value','confusion accuracy');
#24 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#7));
#25 = SHAPE_DEFINITION_REPRESENTATION(#26,#32);
#26 = PRODUCT_DEFINITION_SHAPE('','',#27);
#27 = PRODUCT_DEFINITION('design','',#28,#31);
#28 = PRODUCT_DEFINITION_FORMATION('','',#29);
#29 = PRODUCT('stepped_shaft_054964_like','stepped_shaft_054964_like',''
,(#30));
#30 = PRODUCT_CONTEXT('',#2,'mechanical');
#31 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design');
#32 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#33),#375);
#33 = MANIFOLD_SOLID_BREP('',#34);
#34 = CLOSED_SHELL('',(#35,#223,#249,#276,#281,#316,#371));
#35 = ADVANCED_FACE('',(#36),#50,.T.);
#36 = FACE_BOUND('',#37,.T.);
#37 = EDGE_LOOP('',(#38,#68,#95,#96,#125,#151,#180,#201));
#38 = ORIENTED_EDGE('',*,*,#39,.T.);
#39 = EDGE_CURVE('',#40,#42,#44,.T.);
#40 = VERTEX_POINT('',#41);
#41 = CARTESIAN_POINT('',(8.,-1.959434878636E-15,30.));
#42 = VERTEX_POINT('',#43);
#43 = CARTESIAN_POINT('',(8.,-1.959434878636E-15,67.85));
#44 = SEAM_CURVE('',#45,(#49,#61),.PCURVE_S1.);
#45 = LINE('',#46,#47);
#46 = CARTESIAN_POINT('',(8.,-1.959434878636E-15,0.));
#47 = VECTOR('',#48,1.);
#48 = DIRECTION('',(0.,0.,1.));
#49 = PCURVE('',#50,#55);
#50 = CYLINDRICAL_SURFACE('',#51,8.);
#51 = AXIS2_PLACEMENT_3D('',#52,#53,#54);
#52 = CARTESIAN_POINT('',(0.,0.,0.));
#53 = DIRECTION('',(0.,0.,1.));
#54 = DIRECTION('',(1.,0.,-0.));
#55 = DEFINITIONAL_REPRESENTATION('',(#56),#60);
#56 = LINE('',#57,#58);
#57 = CARTESIAN_POINT('',(6.28318530718,-0.));
#58 = VECTOR('',#59,1.);
#59 = DIRECTION('',(0.,1.));
#60 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#61 = PCURVE('',#50,#62);
#62 = DEFINITIONAL_REPRESENTATION('',(#63),#67);
#63 = LINE('',#64,#65);
#64 = CARTESIAN_POINT('',(0.,-0.));
#65 = VECTOR('',#66,1.);
#66 = DIRECTION('',(0.,1.));
#67 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#68 = ORIENTED_EDGE('',*,*,#69,.F.);
#69 = EDGE_CURVE('',#42,#42,#70,.T.);
#70 = SURFACE_CURVE('',#71,(#76,#83),.PCURVE_S1.);
#71 = CIRCLE('',#72,8.);
#72 = AXIS2_PLACEMENT_3D('',#73,#74,#75);
#73 = CARTESIAN_POINT('',(0.,0.,67.85));
#74 = DIRECTION('',(0.,0.,1.));
#75 = DIRECTION('',(1.,0.,-0.));
#76 = PCURVE('',#50,#77);
#77 = DEFINITIONAL_REPRESENTATION('',(#78),#82);
#78 = LINE('',#79,#80);
#79 = CARTESIAN_POINT('',(0.,67.85));
#80 = VECTOR('',#81,1.);
#81 = DIRECTION('',(1.,0.));
#82 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#83 = PCURVE('',#84,#89);
#84 = PLANE('',#85);
#85 = AXIS2_PLACEMENT_3D('',#86,#87,#88);
#86 = CARTESIAN_POINT('',(0.,0.,67.85));
#87 = DIRECTION('',(0.,0.,1.));
#88 = DIRECTION('',(1.,0.,-0.));
#89 = DEFINITIONAL_REPRESENTATION('',(#90),#94);
#90 = CIRCLE('',#91,8.);
#91 = AXIS2_PLACEMENT_2D('',#92,#93);
#92 = CARTESIAN_POINT('',(0.,0.));
#93 = DIRECTION('',(1.,0.));
#94 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#95 = ORIENTED_EDGE('',*,*,#39,.F.);
#96 = ORIENTED_EDGE('',*,*,#97,.T.);
#97 = EDGE_CURVE('',#40,#98,#100,.T.);
#98 = VERTEX_POINT('',#99);
#99 = CARTESIAN_POINT('',(5.3,5.992495306631,30.));
#100 = SURFACE_CURVE('',#101,(#106,#113),.PCURVE_S1.);
#101 = CIRCLE('',#102,8.);
#102 = AXIS2_PLACEMENT_3D('',#103,#104,#105);
#103 = CARTESIAN_POINT('',(0.,0.,30.));
#104 = DIRECTION('',(0.,0.,1.));
#105 = DIRECTION('',(1.,0.,-0.));
#106 = PCURVE('',#50,#107);
#107 = DEFINITIONAL_REPRESENTATION('',(#108),#112);
#108 = LINE('',#109,#110);
#109 = CARTESIAN_POINT('',(0.,30.));
#110 = VECTOR('',#111,1.);
#111 = DIRECTION('',(1.,0.));
#112 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#113 = PCURVE('',#114,#119);
#114 = PLANE('',#115);
#115 = AXIS2_PLACEMENT_3D('',#116,#117,#118);
#116 = CARTESIAN_POINT('',(5.3,-12.,30.));
#117 = DIRECTION('',(0.,0.,1.));
#118 = DIRECTION('',(1.,0.,-0.));
#119 = DEFINITIONAL_REPRESENTATION('',(#120),#124);
#120 = CIRCLE('',#121,8.);
#121 = AXIS2_PLACEMENT_2D('',#122,#123);
#122 = CARTESIAN_POINT('',(-5.3,12.));
#123 = DIRECTION('',(1.,0.));
#124 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#125 = ORIENTED_EDGE('',*,*,#126,.F.);
#126 = EDGE_CURVE('',#127,#98,#129,.T.);
#127 = VERTEX_POINT('',#128);
#128 = CARTESIAN_POINT('',(5.3,5.992495306632,0.));
#129 = SURFACE_CURVE('',#130,(#134,#140),.PCURVE_S1.);
#130 = LINE('',#131,#132);
#131 = CARTESIAN_POINT('',(5.3,5.992495306631,0.));
#132 = VECTOR('',#133,1.);
#133 = DIRECTION('',(0.,0.,1.));
#134 = PCURVE('',#50,#135);
#135 = DEFINITIONAL_REPRESENTATION('',(#136),#139);
#136 = B_SPLINE_CURVE_WITH_KNOTS('',1,(#137,#138),.UNSPECIFIED.,.F.,.F.,
(2,2),(-6.E-07,33.),.PIECEWISE_BEZIER_KNOTS.);
#137 = CARTESIAN_POINT('',(0.846644963322,-6.E-07));
#138 = CARTESIAN_POINT('',(0.846644963322,33.));
#139 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#140 = PCURVE('',#141,#146);
#141 = PLANE('',#142);
#142 = AXIS2_PLACEMENT_3D('',#143,#144,#145);
#143 = CARTESIAN_POINT('',(5.3,-12.,0.));
#144 = DIRECTION('',(1.,0.,-0.));
#145 = DIRECTION('',(0.,0.,1.));
#146 = DEFINITIONAL_REPRESENTATION('',(#147),#150);
#147 = B_SPLINE_CURVE_WITH_KNOTS('',1,(#148,#149),.UNSPECIFIED.,.F.,.F.,
(2,2),(-6.E-07,33.),.PIECEWISE_BEZIER_KNOTS.);
#148 = CARTESIAN_POINT('',(-6.E-07,-17.99249530663));
#149 = CARTESIAN_POINT('',(33.,-17.99249530663));
#150 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#151 = ORIENTED_EDGE('',*,*,#152,.T.);
#152 = EDGE_CURVE('',#127,#153,#155,.T.);
#153 = VERTEX_POINT('',#154);
#154 = CARTESIAN_POINT('',(5.3,-5.992495306632,0.));
#155 = SURFACE_CURVE('',#156,(#161,#168),.PCURVE_S1.);
#156 = CIRCLE('',#157,8.);
#157 = AXIS2_PLACEMENT_3D('',#158,#159,#160);
#158 = CARTESIAN_POINT('',(0.,0.,0.));
#159 = DIRECTION('',(0.,0.,1.));
#160 = DIRECTION('',(1.,0.,-0.));
#161 = PCURVE('',#50,#162);
#162 = DEFINITIONAL_REPRESENTATION('',(#163),#167);
#163 = LINE('',#164,#165);
#164 = CARTESIAN_POINT('',(0.,0.));
#165 = VECTOR('',#166,1.);
#166 = DIRECTION('',(1.,0.));
#167 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#168 = PCURVE('',#169,#174);
#169 = PLANE('',#170);
#170 = AXIS2_PLACEMENT_3D('',#171,#172,#173);
#171 = CARTESIAN_POINT('',(0.,0.,0.));
#172 = DIRECTION('',(0.,0.,1.));
#173 = DIRECTION('',(1.,0.,-0.));
#174 = DEFINITIONAL_REPRESENTATION('',(#175),#179);
#175 = CIRCLE('',#176,8.);
#176 = AXIS2_PLACEMENT_2D('',#177,#178);
#177 = CARTESIAN_POINT('',(0.,0.));
#178 = DIRECTION('',(1.,0.));
#179 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#180 = ORIENTED_EDGE('',*,*,#181,.T.);
#181 = EDGE_CURVE('',#153,#182,#184,.T.);
#182 = VERTEX_POINT('',#183);
#183 = CARTESIAN_POINT('',(5.3,-5.992495306631,30.));
#184 = SURFACE_CURVE('',#185,(#189,#195),.PCURVE_S1.);
#185 = LINE('',#186,#187);
#186 = CARTESIAN_POINT('',(5.3,-5.992495306631,0.));
#187 = VECTOR('',#188,1.);
#188 = DIRECTION('',(0.,0.,1.));
#189 = PCURVE('',#50,#190);
#190 = DEFINITIONAL_REPRESENTATION('',(#191),#194);
#191 = B_SPLINE_CURVE_WITH_KNOTS('',1,(#192,#193),.UNSPECIFIED.,.F.,.F.,
(2,2),(-6.E-07,33.),.PIECEWISE_BEZIER_KNOTS.);
#192 = CARTESIAN_POINT('',(5.436540343858,-6.E-07));
#193 = CARTESIAN_POINT('',(5.436540343858,33.));
#194 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#195 = PCURVE('',#141,#196);
#196 = DEFINITIONAL_REPRESENTATION('',(#197),#200);
#197 = B_SPLINE_CURVE_WITH_KNOTS('',1,(#198,#199),.UNSPECIFIED.,.F.,.F.,
(2,2),(-6.E-07,33.),.PIECEWISE_BEZIER_KNOTS.);
#198 = CARTESIAN_POINT('',(-6.E-07,-6.007504693369));
#199 = CARTESIAN_POINT('',(33.,-6.007504693369));
#200 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#201 = ORIENTED_EDGE('',*,*,#202,.T.);
#202 = EDGE_CURVE('',#182,#40,#203,.T.);
#203 = SURFACE_CURVE('',#204,(#209,#216),.PCURVE_S1.);
#204 = CIRCLE('',#205,8.);
#205 = AXIS2_PLACEMENT_3D('',#206,#207,#208);
#206 = CARTESIAN_POINT('',(0.,0.,30.));
#207 = DIRECTION('',(0.,0.,1.));
#208 = DIRECTION('',(1.,0.,-0.));
#209 = PCURVE('',#50,#210);
#210 = DEFINITIONAL_REPRESENTATION('',(#211),#215);
#211 = LINE('',#212,#213);
#212 = CARTESIAN_POINT('',(0.,30.));
#213 = VECTOR('',#214,1.);
#214 = DIRECTION('',(1.,0.));
#215 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#216 = PCURVE('',#114,#217);
#217 = DEFINITIONAL_REPRESENTATION('',(#218),#222);
#218 = CIRCLE('',#219,8.);
#219 = AXIS2_PLACEMENT_2D('',#220,#221);
#220 = CARTESIAN_POINT('',(-5.3,12.));
#221 = DIRECTION('',(1.,0.));
#222 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#223 = ADVANCED_FACE('',(#224),#114,.F.);
#224 = FACE_BOUND('',#225,.F.);
#225 = EDGE_LOOP('',(#226,#247,#248));
#226 = ORIENTED_EDGE('',*,*,#227,.F.);
#227 = EDGE_CURVE('',#182,#98,#228,.T.);
#228 = SURFACE_CURVE('',#229,(#233,#240),.PCURVE_S1.);
#229 = LINE('',#230,#231);
#230 = CARTESIAN_POINT('',(5.3,-12.,30.));
#231 = VECTOR('',#232,1.);
#232 = DIRECTION('',(-0.,1.,0.));
#233 = PCURVE('',#114,#234);
#234 = DEFINITIONAL_REPRESENTATION('',(#235),#239);
#235 = LINE('',#236,#237);
#236 = CARTESIAN_POINT('',(0.,0.));
#237 = VECTOR('',#238,1.);
#238 = DIRECTION('',(0.,1.));
#239 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#240 = PCURVE('',#141,#241);
#241 = DEFINITIONAL_REPRESENTATION('',(#242),#246);
#242 = LINE('',#243,#244);
#243 = CARTESIAN_POINT('',(30.,0.));
#244 = VECTOR('',#245,1.);
#245 = DIRECTION('',(0.,-1.));
#246 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#247 = ORIENTED_EDGE('',*,*,#202,.T.);
#248 = ORIENTED_EDGE('',*,*,#97,.T.);
#249 = ADVANCED_FACE('',(#250),#141,.T.);
#250 = FACE_BOUND('',#251,.T.);
#251 = EDGE_LOOP('',(#252,#253,#254,#275));
#252 = ORIENTED_EDGE('',*,*,#227,.F.);
#253 = ORIENTED_EDGE('',*,*,#181,.F.);
#254 = ORIENTED_EDGE('',*,*,#255,.T.);
#255 = EDGE_CURVE('',#153,#127,#256,.T.);
#256 = SURFACE_CURVE('',#257,(#261,#268),.PCURVE_S1.);
#257 = LINE('',#258,#259);
#258 = CARTESIAN_POINT('',(5.3,-12.,0.));
#259 = VECTOR('',#260,1.);
#260 = DIRECTION('',(-0.,1.,0.));
#261 = PCURVE('',#141,#262);
#262 = DEFINITIONAL_REPRESENTATION('',(#263),#267);
#263 = LINE('',#264,#265);
#264 = CARTESIAN_POINT('',(0.,0.));
#265 = VECTOR('',#266,1.);
#266 = DIRECTION('',(0.,-1.));
#267 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#268 = PCURVE('',#169,#269);
#269 = DEFINITIONAL_REPRESENTATION('',(#270),#274);
#270 = LINE('',#271,#272);
#271 = CARTESIAN_POINT('',(5.3,-12.));
#272 = VECTOR('',#273,1.);
#273 = DIRECTION('',(0.,1.));
#274 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#275 = ORIENTED_EDGE('',*,*,#126,.T.);
#276 = ADVANCED_FACE('',(#277),#169,.F.);
#277 = FACE_BOUND('',#278,.F.);
#278 = EDGE_LOOP('',(#279,#280));
#279 = ORIENTED_EDGE('',*,*,#255,.T.);
#280 = ORIENTED_EDGE('',*,*,#152,.T.);
#281 = ADVANCED_FACE('',(#282,#313),#84,.F.);
#282 = FACE_BOUND('',#283,.F.);
#283 = EDGE_LOOP('',(#284));
#284 = ORIENTED_EDGE('',*,*,#285,.T.);
#285 = EDGE_CURVE('',#286,#286,#288,.T.);
#286 = VERTEX_POINT('',#287);
#287 = CARTESIAN_POINT('',(12.,-2.939152317954E-15,67.85));
#288 = SURFACE_CURVE('',#289,(#294,#301),.PCURVE_S1.);
#289 = CIRCLE('',#290,12.);
#290 = AXIS2_PLACEMENT_3D('',#291,#292,#293);
#291 = CARTESIAN_POINT('',(0.,0.,67.85));
#292 = DIRECTION('',(0.,0.,1.));
#293 = DIRECTION('',(1.,0.,-0.));
#294 = PCURVE('',#84,#295);
#295 = DEFINITIONAL_REPRESENTATION('',(#296),#300);
#296 = CIRCLE('',#297,12.);
#297 = AXIS2_PLACEMENT_2D('',#298,#299);
#298 = CARTESIAN_POINT('',(0.,0.));
#299 = DIRECTION('',(1.,0.));
#300 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#301 = PCURVE('',#302,#307);
#302 = CYLINDRICAL_SURFACE('',#303,12.);
#303 = AXIS2_PLACEMENT_3D('',#304,#305,#306);
#304 = CARTESIAN_POINT('',(0.,0.,67.85));
#305 = DIRECTION('',(0.,0.,1.));
#306 = DIRECTION('',(1.,0.,-0.));
#307 = DEFINITIONAL_REPRESENTATION('',(#308),#312);
#308 = LINE('',#309,#310);
#309 = CARTESIAN_POINT('',(0.,0.));
#310 = VECTOR('',#311,1.);
#311 = DIRECTION('',(1.,0.));
#312 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#313 = FACE_BOUND('',#314,.F.);
#314 = EDGE_LOOP('',(#315));
#315 = ORIENTED_EDGE('',*,*,#69,.F.);
#316 = ADVANCED_FACE('',(#317),#302,.T.);
#317 = FACE_BOUND('',#318,.T.);
#318 = EDGE_LOOP('',(#319,#348,#369,#370));
#319 = ORIENTED_EDGE('',*,*,#320,.F.);
#320 = EDGE_CURVE('',#321,#321,#323,.T.);
#321 = VERTEX_POINT('',#322);
#322 = CARTESIAN_POINT('',(12.,-2.939152317954E-15,90.));
#323 = SURFACE_CURVE('',#324,(#329,#336),.PCURVE_S1.);
#324 = CIRCLE('',#325,12.);
#325 = AXIS2_PLACEMENT_3D('',#326,#327,#328);
#326 = CARTESIAN_POINT('',(0.,0.,90.));
#327 = DIRECTION('',(0.,0.,1.));
#328 = DIRECTION('',(1.,0.,-0.));
#329 = PCURVE('',#302,#330);
#330 = DEFINITIONAL_REPRESENTATION('',(#331),#335);
#331 = LINE('',#332,#333);
#332 = CARTESIAN_POINT('',(0.,22.15));
#333 = VECTOR('',#334,1.);
#334 = DIRECTION('',(1.,0.));
#335 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#336 = PCURVE('',#337,#342);
#337 = PLANE('',#338);
#338 = AXIS2_PLACEMENT_3D('',#339,#340,#341);
#339 = CARTESIAN_POINT('',(0.,0.,90.));
#340 = DIRECTION('',(0.,0.,1.));
#341 = DIRECTION('',(1.,0.,-0.));
#342 = DEFINITIONAL_REPRESENTATION('',(#343),#347);
#343 = CIRCLE('',#344,12.);
#344 = AXIS2_PLACEMENT_2D('',#345,#346);
#345 = CARTESIAN_POINT('',(0.,0.));
#346 = DIRECTION('',(1.,0.));
#347 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#348 = ORIENTED_EDGE('',*,*,#349,.F.);
#349 = EDGE_CURVE('',#286,#321,#350,.T.);
#350 = SEAM_CURVE('',#351,(#355,#362),.PCURVE_S1.);
#351 = LINE('',#352,#353);
#352 = CARTESIAN_POINT('',(12.,-2.939152317954E-15,67.85));
#353 = VECTOR('',#354,1.);
#354 = DIRECTION('',(0.,0.,1.));
#355 = PCURVE('',#302,#356);
#356 = DEFINITIONAL_REPRESENTATION('',(#357),#361);
#357 = LINE('',#358,#359);
#358 = CARTESIAN_POINT('',(6.28318530718,-0.));
#359 = VECTOR('',#360,1.);
#360 = DIRECTION('',(0.,1.));
#361 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#362 = PCURVE('',#302,#363);
#363 = DEFINITIONAL_REPRESENTATION('',(#364),#368);
#364 = LINE('',#365,#366);
#365 = CARTESIAN_POINT('',(0.,-0.));
#366 = VECTOR('',#367,1.);
#367 = DIRECTION('',(0.,1.));
#368 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#369 = ORIENTED_EDGE('',*,*,#285,.T.);
#370 = ORIENTED_EDGE('',*,*,#349,.T.);
#371 = ADVANCED_FACE('',(#372),#337,.T.);
#372 = FACE_BOUND('',#373,.T.);
#373 = EDGE_LOOP('',(#374));
#374 = ORIENTED_EDGE('',*,*,#320,.T.);
#375 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3)
GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#379)) GLOBAL_UNIT_ASSIGNED_CONTEXT
((#376,#377,#378)) REPRESENTATION_CONTEXT('Context #1',
'3D Context with UNIT and UNCERTAINTY') );
#376 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) );
#377 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) );
#378 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() );
#379 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#376,
'distance_accuracy_value','confusion accuracy');
#380 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#381,#383);
#381 = ( REPRESENTATION_RELATIONSHIP('','',#32,#10)
REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#382)
SHAPE_REPRESENTATION_RELATIONSHIP() );
#382 = ITEM_DEFINED_TRANSFORMATION('','',#11,#15);
#383 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item',#384
);
#384 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('1','=>[0:1:1:2]','',#5,#27,$);
#385 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#29));
#386=DESCRIPTIVE_REPRESENTATION_ITEM('cadpy:generator','cadpy');
#387=REPRESENTATION('cadpy:generator',(#386),#19);
#388=PROPERTY_DEFINITION('cadpy metadata','cadpy:generator',#5);
#389=PROPERTY_DEFINITION_REPRESENTATION(#388,#387);
#390=DESCRIPTIVE_REPRESENTATION_ITEM('cadpy:entryKind','part');
#391=REPRESENTATION('cadpy:entryKind',(#390),#19);
#392=PROPERTY_DEFINITION('cadpy metadata','cadpy:entryKind',#5);
#393=PROPERTY_DEFINITION_REPRESENTATION(#392,#391);
#394=DESCRIPTIVE_REPRESENTATION_ITEM('cadpy:sourcePath','054964_like.py');
#395=REPRESENTATION('cadpy:sourcePath',(#394),#19);
#396=PROPERTY_DEFINITION('cadpy metadata','cadpy:sourcePath',#5);
#397=PROPERTY_DEFINITION_REPRESENTATION(#396,#395);
#398=DESCRIPTIVE_REPRESENTATION_ITEM('cadpy:sourceHash','d870b9d4f0a40ccc7cb87d885258286165cd8974ab07b50164d030406abf6de8');
#399=REPRESENTATION('cadpy:sourceHash',(#398),#19);
#400=PROPERTY_DEFINITION('cadpy metadata','cadpy:sourceHash',#5);
#401=PROPERTY_DEFINITION_REPRESENTATION(#400,#399);
ENDSEC;
END-ISO-10303-21;
@@ -0,0 +1,18 @@
"""Approximate stepped plunger / valve pin inferred from reference 061119."""
from shaft_common import cut_end_bore, export_horizontal, stepped_body
SEGMENTS = [
(9.0, 20.0),
(18.0, 72.0),
(12.0, 10.0),
(16.0, 7.0),
(18.0, 6.0),
]
def gen_step():
body, total = stepped_body(SEGMENTS)
body = cut_end_bore(body, total, diameter=8.0, depth=2.5)
return export_horizontal(body, "plunger_061119_like")
@@ -0,0 +1,705 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('Open CASCADE Model'),'2;1');
FILE_NAME('Open CASCADE Shape Model','2026-07-22T18:43:40',('Author'),(
'Open CASCADE'),'Open CASCADE STEP processor 7.9','Open CASCADE 7.9'
,'Unknown');
FILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }'));
ENDSEC;
DATA;
#1 = APPLICATION_PROTOCOL_DEFINITION('international standard',
'automotive_design',2000,#2);
#2 = APPLICATION_CONTEXT(
'core data for automotive mechanical design processes');
#3 = SHAPE_DEFINITION_REPRESENTATION(#4,#10);
#4 = PRODUCT_DEFINITION_SHAPE('','',#5);
#5 = PRODUCT_DEFINITION('design','',#6,#9);
#6 = PRODUCT_DEFINITION_FORMATION('','',#7);
#7 = PRODUCT('=>[0:1:1:2]','=>[0:1:1:2]','',(#8));
#8 = PRODUCT_CONTEXT('',#2,'mechanical');
#9 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design');
#10 = SHAPE_REPRESENTATION('',(#11,#15),#19);
#11 = AXIS2_PLACEMENT_3D('',#12,#13,#14);
#12 = CARTESIAN_POINT('',(0.,0.,0.));
#13 = DIRECTION('',(0.,0.,1.));
#14 = DIRECTION('',(1.,0.,-0.));
#15 = AXIS2_PLACEMENT_3D('',#16,#17,#18);
#16 = CARTESIAN_POINT('',(0.,0.,0.));
#17 = DIRECTION('',(1.,-0.,1.110223024625E-16));
#18 = DIRECTION('',(1.110223024625E-16,0.,-1.));
#19 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3)
GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#23)) GLOBAL_UNIT_ASSIGNED_CONTEXT(
(#20,#21,#22)) REPRESENTATION_CONTEXT('Context #1',
'3D Context with UNIT and UNCERTAINTY') );
#20 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) );
#21 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) );
#22 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() );
#23 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#20,
'distance_accuracy_value','confusion accuracy');
#24 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#7));
#25 = SHAPE_DEFINITION_REPRESENTATION(#26,#32);
#26 = PRODUCT_DEFINITION_SHAPE('','',#27);
#27 = PRODUCT_DEFINITION('design','',#28,#31);
#28 = PRODUCT_DEFINITION_FORMATION('','',#29);
#29 = PRODUCT('plunger_061119_like','plunger_061119_like','',(#30));
#30 = PRODUCT_CONTEXT('',#2,'mechanical');
#31 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design');
#32 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#33),#581);
#33 = MANIFOLD_SOLID_BREP('',#34);
#34 = CLOSED_SHELL('',(#35,#123,#158,#162,#217,#252,#307,#342,#397,#432,
#487,#522,#577));
#35 = ADVANCED_FACE('',(#36),#49,.T.);
#36 = FACE_BOUND('',#37,.T.);
#37 = EDGE_LOOP('',(#38,#72,#95,#122));
#38 = ORIENTED_EDGE('',*,*,#39,.F.);
#39 = EDGE_CURVE('',#40,#40,#42,.T.);
#40 = VERTEX_POINT('',#41);
#41 = CARTESIAN_POINT('',(4.5,-1.102182119233E-15,19.85));
#42 = SURFACE_CURVE('',#43,(#48,#60),.PCURVE_S1.);
#43 = CIRCLE('',#44,4.5);
#44 = AXIS2_PLACEMENT_3D('',#45,#46,#47);
#45 = CARTESIAN_POINT('',(0.,0.,19.85));
#46 = DIRECTION('',(0.,0.,1.));
#47 = DIRECTION('',(1.,0.,-0.));
#48 = PCURVE('',#49,#54);
#49 = CYLINDRICAL_SURFACE('',#50,4.5);
#50 = AXIS2_PLACEMENT_3D('',#51,#52,#53);
#51 = CARTESIAN_POINT('',(0.,0.,0.));
#52 = DIRECTION('',(0.,0.,1.));
#53 = DIRECTION('',(1.,0.,-0.));
#54 = DEFINITIONAL_REPRESENTATION('',(#55),#59);
#55 = LINE('',#56,#57);
#56 = CARTESIAN_POINT('',(0.,19.85));
#57 = VECTOR('',#58,1.);
#58 = DIRECTION('',(1.,0.));
#59 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#60 = PCURVE('',#61,#66);
#61 = PLANE('',#62);
#62 = AXIS2_PLACEMENT_3D('',#63,#64,#65);
#63 = CARTESIAN_POINT('',(0.,0.,19.85));
#64 = DIRECTION('',(0.,0.,1.));
#65 = DIRECTION('',(1.,0.,-0.));
#66 = DEFINITIONAL_REPRESENTATION('',(#67),#71);
#67 = CIRCLE('',#68,4.5);
#68 = AXIS2_PLACEMENT_2D('',#69,#70);
#69 = CARTESIAN_POINT('',(0.,0.));
#70 = DIRECTION('',(1.,0.));
#71 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#72 = ORIENTED_EDGE('',*,*,#73,.F.);
#73 = EDGE_CURVE('',#74,#40,#76,.T.);
#74 = VERTEX_POINT('',#75);
#75 = CARTESIAN_POINT('',(4.5,-1.102182119233E-15,0.));
#76 = SEAM_CURVE('',#77,(#81,#88),.PCURVE_S1.);
#77 = LINE('',#78,#79);
#78 = CARTESIAN_POINT('',(4.5,-1.102182119233E-15,0.));
#79 = VECTOR('',#80,1.);
#80 = DIRECTION('',(0.,0.,1.));
#81 = PCURVE('',#49,#82);
#82 = DEFINITIONAL_REPRESENTATION('',(#83),#87);
#83 = LINE('',#84,#85);
#84 = CARTESIAN_POINT('',(6.28318530718,-0.));
#85 = VECTOR('',#86,1.);
#86 = DIRECTION('',(0.,1.));
#87 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#88 = PCURVE('',#49,#89);
#89 = DEFINITIONAL_REPRESENTATION('',(#90),#94);
#90 = LINE('',#91,#92);
#91 = CARTESIAN_POINT('',(0.,-0.));
#92 = VECTOR('',#93,1.);
#93 = DIRECTION('',(0.,1.));
#94 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#95 = ORIENTED_EDGE('',*,*,#96,.T.);
#96 = EDGE_CURVE('',#74,#74,#97,.T.);
#97 = SURFACE_CURVE('',#98,(#103,#110),.PCURVE_S1.);
#98 = CIRCLE('',#99,4.5);
#99 = AXIS2_PLACEMENT_3D('',#100,#101,#102);
#100 = CARTESIAN_POINT('',(0.,0.,0.));
#101 = DIRECTION('',(0.,0.,1.));
#102 = DIRECTION('',(1.,0.,-0.));
#103 = PCURVE('',#49,#104);
#104 = DEFINITIONAL_REPRESENTATION('',(#105),#109);
#105 = LINE('',#106,#107);
#106 = CARTESIAN_POINT('',(0.,0.));
#107 = VECTOR('',#108,1.);
#108 = DIRECTION('',(1.,0.));
#109 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#110 = PCURVE('',#111,#116);
#111 = PLANE('',#112);
#112 = AXIS2_PLACEMENT_3D('',#113,#114,#115);
#113 = CARTESIAN_POINT('',(0.,0.,0.));
#114 = DIRECTION('',(0.,0.,1.));
#115 = DIRECTION('',(1.,0.,-0.));
#116 = DEFINITIONAL_REPRESENTATION('',(#117),#121);
#117 = CIRCLE('',#118,4.5);
#118 = AXIS2_PLACEMENT_2D('',#119,#120);
#119 = CARTESIAN_POINT('',(0.,0.));
#120 = DIRECTION('',(1.,0.));
#121 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#122 = ORIENTED_EDGE('',*,*,#73,.T.);
#123 = ADVANCED_FACE('',(#124,#155),#61,.F.);
#124 = FACE_BOUND('',#125,.F.);
#125 = EDGE_LOOP('',(#126));
#126 = ORIENTED_EDGE('',*,*,#127,.T.);
#127 = EDGE_CURVE('',#128,#128,#130,.T.);
#128 = VERTEX_POINT('',#129);
#129 = CARTESIAN_POINT('',(9.,-2.204364238465E-15,19.85));
#130 = SURFACE_CURVE('',#131,(#136,#143),.PCURVE_S1.);
#131 = CIRCLE('',#132,9.);
#132 = AXIS2_PLACEMENT_3D('',#133,#134,#135);
#133 = CARTESIAN_POINT('',(0.,0.,19.85));
#134 = DIRECTION('',(0.,0.,1.));
#135 = DIRECTION('',(1.,0.,-0.));
#136 = PCURVE('',#61,#137);
#137 = DEFINITIONAL_REPRESENTATION('',(#138),#142);
#138 = CIRCLE('',#139,9.);
#139 = AXIS2_PLACEMENT_2D('',#140,#141);
#140 = CARTESIAN_POINT('',(0.,0.));
#141 = DIRECTION('',(1.,0.));
#142 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#143 = PCURVE('',#144,#149);
#144 = CYLINDRICAL_SURFACE('',#145,9.);
#145 = AXIS2_PLACEMENT_3D('',#146,#147,#148);
#146 = CARTESIAN_POINT('',(0.,0.,19.85));
#147 = DIRECTION('',(0.,0.,1.));
#148 = DIRECTION('',(1.,0.,-0.));
#149 = DEFINITIONAL_REPRESENTATION('',(#150),#154);
#150 = LINE('',#151,#152);
#151 = CARTESIAN_POINT('',(0.,0.));
#152 = VECTOR('',#153,1.);
#153 = DIRECTION('',(1.,0.));
#154 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#155 = FACE_BOUND('',#156,.F.);
#156 = EDGE_LOOP('',(#157));
#157 = ORIENTED_EDGE('',*,*,#39,.F.);
#158 = ADVANCED_FACE('',(#159),#111,.F.);
#159 = FACE_BOUND('',#160,.T.);
#160 = EDGE_LOOP('',(#161));
#161 = ORIENTED_EDGE('',*,*,#96,.F.);
#162 = ADVANCED_FACE('',(#163),#144,.T.);
#163 = FACE_BOUND('',#164,.T.);
#164 = EDGE_LOOP('',(#165,#194,#215,#216));
#165 = ORIENTED_EDGE('',*,*,#166,.F.);
#166 = EDGE_CURVE('',#167,#167,#169,.T.);
#167 = VERTEX_POINT('',#168);
#168 = CARTESIAN_POINT('',(9.,-2.204364238465E-15,92.));
#169 = SURFACE_CURVE('',#170,(#175,#182),.PCURVE_S1.);
#170 = CIRCLE('',#171,9.);
#171 = AXIS2_PLACEMENT_3D('',#172,#173,#174);
#172 = CARTESIAN_POINT('',(0.,0.,92.));
#173 = DIRECTION('',(0.,0.,1.));
#174 = DIRECTION('',(1.,0.,-0.));
#175 = PCURVE('',#144,#176);
#176 = DEFINITIONAL_REPRESENTATION('',(#177),#181);
#177 = LINE('',#178,#179);
#178 = CARTESIAN_POINT('',(0.,72.15));
#179 = VECTOR('',#180,1.);
#180 = DIRECTION('',(1.,0.));
#181 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#182 = PCURVE('',#183,#188);
#183 = PLANE('',#184);
#184 = AXIS2_PLACEMENT_3D('',#185,#186,#187);
#185 = CARTESIAN_POINT('',(0.,0.,92.));
#186 = DIRECTION('',(0.,0.,1.));
#187 = DIRECTION('',(1.,0.,-0.));
#188 = DEFINITIONAL_REPRESENTATION('',(#189),#193);
#189 = CIRCLE('',#190,9.);
#190 = AXIS2_PLACEMENT_2D('',#191,#192);
#191 = CARTESIAN_POINT('',(0.,0.));
#192 = DIRECTION('',(1.,0.));
#193 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#194 = ORIENTED_EDGE('',*,*,#195,.F.);
#195 = EDGE_CURVE('',#128,#167,#196,.T.);
#196 = SEAM_CURVE('',#197,(#201,#208),.PCURVE_S1.);
#197 = LINE('',#198,#199);
#198 = CARTESIAN_POINT('',(9.,-2.204364238465E-15,19.85));
#199 = VECTOR('',#200,1.);
#200 = DIRECTION('',(0.,0.,1.));
#201 = PCURVE('',#144,#202);
#202 = DEFINITIONAL_REPRESENTATION('',(#203),#207);
#203 = LINE('',#204,#205);
#204 = CARTESIAN_POINT('',(6.28318530718,-0.));
#205 = VECTOR('',#206,1.);
#206 = DIRECTION('',(0.,1.));
#207 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#208 = PCURVE('',#144,#209);
#209 = DEFINITIONAL_REPRESENTATION('',(#210),#214);
#210 = LINE('',#211,#212);
#211 = CARTESIAN_POINT('',(0.,-0.));
#212 = VECTOR('',#213,1.);
#213 = DIRECTION('',(0.,1.));
#214 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#215 = ORIENTED_EDGE('',*,*,#127,.T.);
#216 = ORIENTED_EDGE('',*,*,#195,.T.);
#217 = ADVANCED_FACE('',(#218,#221),#183,.T.);
#218 = FACE_BOUND('',#219,.T.);
#219 = EDGE_LOOP('',(#220));
#220 = ORIENTED_EDGE('',*,*,#166,.T.);
#221 = FACE_BOUND('',#222,.T.);
#222 = EDGE_LOOP('',(#223));
#223 = ORIENTED_EDGE('',*,*,#224,.F.);
#224 = EDGE_CURVE('',#225,#225,#227,.T.);
#225 = VERTEX_POINT('',#226);
#226 = CARTESIAN_POINT('',(6.,-1.469576158977E-15,92.));
#227 = SURFACE_CURVE('',#228,(#233,#240),.PCURVE_S1.);
#228 = CIRCLE('',#229,6.);
#229 = AXIS2_PLACEMENT_3D('',#230,#231,#232);
#230 = CARTESIAN_POINT('',(0.,0.,92.));
#231 = DIRECTION('',(0.,0.,1.));
#232 = DIRECTION('',(1.,0.,-0.));
#233 = PCURVE('',#183,#234);
#234 = DEFINITIONAL_REPRESENTATION('',(#235),#239);
#235 = CIRCLE('',#236,6.);
#236 = AXIS2_PLACEMENT_2D('',#237,#238);
#237 = CARTESIAN_POINT('',(0.,0.));
#238 = DIRECTION('',(1.,0.));
#239 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#240 = PCURVE('',#241,#246);
#241 = CYLINDRICAL_SURFACE('',#242,6.);
#242 = AXIS2_PLACEMENT_3D('',#243,#244,#245);
#243 = CARTESIAN_POINT('',(0.,0.,91.85));
#244 = DIRECTION('',(0.,0.,1.));
#245 = DIRECTION('',(1.,0.,-0.));
#246 = DEFINITIONAL_REPRESENTATION('',(#247),#251);
#247 = LINE('',#248,#249);
#248 = CARTESIAN_POINT('',(0.,0.15));
#249 = VECTOR('',#250,1.);
#250 = DIRECTION('',(1.,0.));
#251 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#252 = ADVANCED_FACE('',(#253),#241,.T.);
#253 = FACE_BOUND('',#254,.T.);
#254 = EDGE_LOOP('',(#255,#256,#279,#306));
#255 = ORIENTED_EDGE('',*,*,#224,.T.);
#256 = ORIENTED_EDGE('',*,*,#257,.T.);
#257 = EDGE_CURVE('',#225,#258,#260,.T.);
#258 = VERTEX_POINT('',#259);
#259 = CARTESIAN_POINT('',(6.,-1.469576158977E-15,101.85));
#260 = SEAM_CURVE('',#261,(#265,#272),.PCURVE_S1.);
#261 = LINE('',#262,#263);
#262 = CARTESIAN_POINT('',(6.,-1.469576158977E-15,91.85));
#263 = VECTOR('',#264,1.);
#264 = DIRECTION('',(0.,0.,1.));
#265 = PCURVE('',#241,#266);
#266 = DEFINITIONAL_REPRESENTATION('',(#267),#271);
#267 = LINE('',#268,#269);
#268 = CARTESIAN_POINT('',(0.,-0.));
#269 = VECTOR('',#270,1.);
#270 = DIRECTION('',(0.,1.));
#271 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#272 = PCURVE('',#241,#273);
#273 = DEFINITIONAL_REPRESENTATION('',(#274),#278);
#274 = LINE('',#275,#276);
#275 = CARTESIAN_POINT('',(6.28318530718,-0.));
#276 = VECTOR('',#277,1.);
#277 = DIRECTION('',(0.,1.));
#278 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#279 = ORIENTED_EDGE('',*,*,#280,.F.);
#280 = EDGE_CURVE('',#258,#258,#281,.T.);
#281 = SURFACE_CURVE('',#282,(#287,#294),.PCURVE_S1.);
#282 = CIRCLE('',#283,6.);
#283 = AXIS2_PLACEMENT_3D('',#284,#285,#286);
#284 = CARTESIAN_POINT('',(0.,0.,101.85));
#285 = DIRECTION('',(0.,0.,1.));
#286 = DIRECTION('',(1.,0.,-0.));
#287 = PCURVE('',#241,#288);
#288 = DEFINITIONAL_REPRESENTATION('',(#289),#293);
#289 = LINE('',#290,#291);
#290 = CARTESIAN_POINT('',(0.,10.));
#291 = VECTOR('',#292,1.);
#292 = DIRECTION('',(1.,0.));
#293 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#294 = PCURVE('',#295,#300);
#295 = PLANE('',#296);
#296 = AXIS2_PLACEMENT_3D('',#297,#298,#299);
#297 = CARTESIAN_POINT('',(0.,0.,101.85));
#298 = DIRECTION('',(0.,0.,1.));
#299 = DIRECTION('',(1.,0.,-0.));
#300 = DEFINITIONAL_REPRESENTATION('',(#301),#305);
#301 = CIRCLE('',#302,6.);
#302 = AXIS2_PLACEMENT_2D('',#303,#304);
#303 = CARTESIAN_POINT('',(0.,0.));
#304 = DIRECTION('',(1.,0.));
#305 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#306 = ORIENTED_EDGE('',*,*,#257,.F.);
#307 = ADVANCED_FACE('',(#308,#339),#295,.F.);
#308 = FACE_BOUND('',#309,.F.);
#309 = EDGE_LOOP('',(#310));
#310 = ORIENTED_EDGE('',*,*,#311,.T.);
#311 = EDGE_CURVE('',#312,#312,#314,.T.);
#312 = VERTEX_POINT('',#313);
#313 = CARTESIAN_POINT('',(8.,-1.959434878636E-15,101.85));
#314 = SURFACE_CURVE('',#315,(#320,#327),.PCURVE_S1.);
#315 = CIRCLE('',#316,8.);
#316 = AXIS2_PLACEMENT_3D('',#317,#318,#319);
#317 = CARTESIAN_POINT('',(0.,0.,101.85));
#318 = DIRECTION('',(0.,0.,1.));
#319 = DIRECTION('',(1.,0.,-0.));
#320 = PCURVE('',#295,#321);
#321 = DEFINITIONAL_REPRESENTATION('',(#322),#326);
#322 = CIRCLE('',#323,8.);
#323 = AXIS2_PLACEMENT_2D('',#324,#325);
#324 = CARTESIAN_POINT('',(0.,0.));
#325 = DIRECTION('',(1.,0.));
#326 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#327 = PCURVE('',#328,#333);
#328 = CYLINDRICAL_SURFACE('',#329,8.);
#329 = AXIS2_PLACEMENT_3D('',#330,#331,#332);
#330 = CARTESIAN_POINT('',(0.,0.,101.85));
#331 = DIRECTION('',(0.,0.,1.));
#332 = DIRECTION('',(1.,0.,-0.));
#333 = DEFINITIONAL_REPRESENTATION('',(#334),#338);
#334 = LINE('',#335,#336);
#335 = CARTESIAN_POINT('',(0.,0.));
#336 = VECTOR('',#337,1.);
#337 = DIRECTION('',(1.,0.));
#338 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#339 = FACE_BOUND('',#340,.F.);
#340 = EDGE_LOOP('',(#341));
#341 = ORIENTED_EDGE('',*,*,#280,.F.);
#342 = ADVANCED_FACE('',(#343),#328,.T.);
#343 = FACE_BOUND('',#344,.T.);
#344 = EDGE_LOOP('',(#345,#374,#395,#396));
#345 = ORIENTED_EDGE('',*,*,#346,.F.);
#346 = EDGE_CURVE('',#347,#347,#349,.T.);
#347 = VERTEX_POINT('',#348);
#348 = CARTESIAN_POINT('',(8.,-1.959434878636E-15,108.85));
#349 = SURFACE_CURVE('',#350,(#355,#362),.PCURVE_S1.);
#350 = CIRCLE('',#351,8.);
#351 = AXIS2_PLACEMENT_3D('',#352,#353,#354);
#352 = CARTESIAN_POINT('',(0.,0.,108.85));
#353 = DIRECTION('',(0.,0.,1.));
#354 = DIRECTION('',(1.,0.,-0.));
#355 = PCURVE('',#328,#356);
#356 = DEFINITIONAL_REPRESENTATION('',(#357),#361);
#357 = LINE('',#358,#359);
#358 = CARTESIAN_POINT('',(0.,7.));
#359 = VECTOR('',#360,1.);
#360 = DIRECTION('',(1.,0.));
#361 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#362 = PCURVE('',#363,#368);
#363 = PLANE('',#364);
#364 = AXIS2_PLACEMENT_3D('',#365,#366,#367);
#365 = CARTESIAN_POINT('',(0.,0.,108.85));
#366 = DIRECTION('',(0.,0.,1.));
#367 = DIRECTION('',(1.,0.,-0.));
#368 = DEFINITIONAL_REPRESENTATION('',(#369),#373);
#369 = CIRCLE('',#370,8.);
#370 = AXIS2_PLACEMENT_2D('',#371,#372);
#371 = CARTESIAN_POINT('',(0.,0.));
#372 = DIRECTION('',(1.,0.));
#373 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#374 = ORIENTED_EDGE('',*,*,#375,.F.);
#375 = EDGE_CURVE('',#312,#347,#376,.T.);
#376 = SEAM_CURVE('',#377,(#381,#388),.PCURVE_S1.);
#377 = LINE('',#378,#379);
#378 = CARTESIAN_POINT('',(8.,-1.959434878636E-15,101.85));
#379 = VECTOR('',#380,1.);
#380 = DIRECTION('',(0.,0.,1.));
#381 = PCURVE('',#328,#382);
#382 = DEFINITIONAL_REPRESENTATION('',(#383),#387);
#383 = LINE('',#384,#385);
#384 = CARTESIAN_POINT('',(6.28318530718,-0.));
#385 = VECTOR('',#386,1.);
#386 = DIRECTION('',(0.,1.));
#387 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#388 = PCURVE('',#328,#389);
#389 = DEFINITIONAL_REPRESENTATION('',(#390),#394);
#390 = LINE('',#391,#392);
#391 = CARTESIAN_POINT('',(0.,-0.));
#392 = VECTOR('',#393,1.);
#393 = DIRECTION('',(0.,1.));
#394 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#395 = ORIENTED_EDGE('',*,*,#311,.T.);
#396 = ORIENTED_EDGE('',*,*,#375,.T.);
#397 = ADVANCED_FACE('',(#398,#429),#363,.F.);
#398 = FACE_BOUND('',#399,.F.);
#399 = EDGE_LOOP('',(#400));
#400 = ORIENTED_EDGE('',*,*,#401,.T.);
#401 = EDGE_CURVE('',#402,#402,#404,.T.);
#402 = VERTEX_POINT('',#403);
#403 = CARTESIAN_POINT('',(9.,-2.204364238465E-15,108.85));
#404 = SURFACE_CURVE('',#405,(#410,#417),.PCURVE_S1.);
#405 = CIRCLE('',#406,9.);
#406 = AXIS2_PLACEMENT_3D('',#407,#408,#409);
#407 = CARTESIAN_POINT('',(0.,0.,108.85));
#408 = DIRECTION('',(0.,0.,1.));
#409 = DIRECTION('',(1.,0.,-0.));
#410 = PCURVE('',#363,#411);
#411 = DEFINITIONAL_REPRESENTATION('',(#412),#416);
#412 = CIRCLE('',#413,9.);
#413 = AXIS2_PLACEMENT_2D('',#414,#415);
#414 = CARTESIAN_POINT('',(0.,0.));
#415 = DIRECTION('',(1.,0.));
#416 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#417 = PCURVE('',#418,#423);
#418 = CYLINDRICAL_SURFACE('',#419,9.);
#419 = AXIS2_PLACEMENT_3D('',#420,#421,#422);
#420 = CARTESIAN_POINT('',(0.,0.,108.85));
#421 = DIRECTION('',(0.,0.,1.));
#422 = DIRECTION('',(1.,0.,-0.));
#423 = DEFINITIONAL_REPRESENTATION('',(#424),#428);
#424 = LINE('',#425,#426);
#425 = CARTESIAN_POINT('',(0.,0.));
#426 = VECTOR('',#427,1.);
#427 = DIRECTION('',(1.,0.));
#428 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#429 = FACE_BOUND('',#430,.F.);
#430 = EDGE_LOOP('',(#431));
#431 = ORIENTED_EDGE('',*,*,#346,.F.);
#432 = ADVANCED_FACE('',(#433),#418,.T.);
#433 = FACE_BOUND('',#434,.T.);
#434 = EDGE_LOOP('',(#435,#464,#485,#486));
#435 = ORIENTED_EDGE('',*,*,#436,.F.);
#436 = EDGE_CURVE('',#437,#437,#439,.T.);
#437 = VERTEX_POINT('',#438);
#438 = CARTESIAN_POINT('',(9.,-2.204364238465E-15,115.));
#439 = SURFACE_CURVE('',#440,(#445,#452),.PCURVE_S1.);
#440 = CIRCLE('',#441,9.);
#441 = AXIS2_PLACEMENT_3D('',#442,#443,#444);
#442 = CARTESIAN_POINT('',(0.,0.,115.));
#443 = DIRECTION('',(0.,0.,1.));
#444 = DIRECTION('',(1.,0.,-0.));
#445 = PCURVE('',#418,#446);
#446 = DEFINITIONAL_REPRESENTATION('',(#447),#451);
#447 = LINE('',#448,#449);
#448 = CARTESIAN_POINT('',(0.,6.15));
#449 = VECTOR('',#450,1.);
#450 = DIRECTION('',(1.,0.));
#451 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#452 = PCURVE('',#453,#458);
#453 = PLANE('',#454);
#454 = AXIS2_PLACEMENT_3D('',#455,#456,#457);
#455 = CARTESIAN_POINT('',(0.,0.,115.));
#456 = DIRECTION('',(0.,0.,1.));
#457 = DIRECTION('',(1.,0.,-0.));
#458 = DEFINITIONAL_REPRESENTATION('',(#459),#463);
#459 = CIRCLE('',#460,9.);
#460 = AXIS2_PLACEMENT_2D('',#461,#462);
#461 = CARTESIAN_POINT('',(0.,0.));
#462 = DIRECTION('',(1.,0.));
#463 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#464 = ORIENTED_EDGE('',*,*,#465,.F.);
#465 = EDGE_CURVE('',#402,#437,#466,.T.);
#466 = SEAM_CURVE('',#467,(#471,#478),.PCURVE_S1.);
#467 = LINE('',#468,#469);
#468 = CARTESIAN_POINT('',(9.,-2.204364238465E-15,108.85));
#469 = VECTOR('',#470,1.);
#470 = DIRECTION('',(0.,0.,1.));
#471 = PCURVE('',#418,#472);
#472 = DEFINITIONAL_REPRESENTATION('',(#473),#477);
#473 = LINE('',#474,#475);
#474 = CARTESIAN_POINT('',(6.28318530718,-0.));
#475 = VECTOR('',#476,1.);
#476 = DIRECTION('',(0.,1.));
#477 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#478 = PCURVE('',#418,#479);
#479 = DEFINITIONAL_REPRESENTATION('',(#480),#484);
#480 = LINE('',#481,#482);
#481 = CARTESIAN_POINT('',(0.,-0.));
#482 = VECTOR('',#483,1.);
#483 = DIRECTION('',(0.,1.));
#484 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#485 = ORIENTED_EDGE('',*,*,#401,.T.);
#486 = ORIENTED_EDGE('',*,*,#465,.T.);
#487 = ADVANCED_FACE('',(#488,#491),#453,.T.);
#488 = FACE_BOUND('',#489,.T.);
#489 = EDGE_LOOP('',(#490));
#490 = ORIENTED_EDGE('',*,*,#436,.T.);
#491 = FACE_BOUND('',#492,.T.);
#492 = EDGE_LOOP('',(#493));
#493 = ORIENTED_EDGE('',*,*,#494,.F.);
#494 = EDGE_CURVE('',#495,#495,#497,.T.);
#495 = VERTEX_POINT('',#496);
#496 = CARTESIAN_POINT('',(4.,-9.797174393179E-16,115.));
#497 = SURFACE_CURVE('',#498,(#503,#510),.PCURVE_S1.);
#498 = CIRCLE('',#499,4.);
#499 = AXIS2_PLACEMENT_3D('',#500,#501,#502);
#500 = CARTESIAN_POINT('',(0.,0.,115.));
#501 = DIRECTION('',(0.,0.,1.));
#502 = DIRECTION('',(1.,0.,-0.));
#503 = PCURVE('',#453,#504);
#504 = DEFINITIONAL_REPRESENTATION('',(#505),#509);
#505 = CIRCLE('',#506,4.);
#506 = AXIS2_PLACEMENT_2D('',#507,#508);
#507 = CARTESIAN_POINT('',(0.,0.));
#508 = DIRECTION('',(1.,0.));
#509 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#510 = PCURVE('',#511,#516);
#511 = CYLINDRICAL_SURFACE('',#512,4.);
#512 = AXIS2_PLACEMENT_3D('',#513,#514,#515);
#513 = CARTESIAN_POINT('',(0.,0.,112.5));
#514 = DIRECTION('',(0.,0.,1.));
#515 = DIRECTION('',(1.,0.,-0.));
#516 = DEFINITIONAL_REPRESENTATION('',(#517),#521);
#517 = LINE('',#518,#519);
#518 = CARTESIAN_POINT('',(0.,2.5));
#519 = VECTOR('',#520,1.);
#520 = DIRECTION('',(1.,0.));
#521 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#522 = ADVANCED_FACE('',(#523),#511,.F.);
#523 = FACE_BOUND('',#524,.F.);
#524 = EDGE_LOOP('',(#525,#526,#549,#576));
#525 = ORIENTED_EDGE('',*,*,#494,.F.);
#526 = ORIENTED_EDGE('',*,*,#527,.F.);
#527 = EDGE_CURVE('',#528,#495,#530,.T.);
#528 = VERTEX_POINT('',#529);
#529 = CARTESIAN_POINT('',(4.,-9.797174393179E-16,112.5));
#530 = SEAM_CURVE('',#531,(#535,#542),.PCURVE_S1.);
#531 = LINE('',#532,#533);
#532 = CARTESIAN_POINT('',(4.,-9.797174393179E-16,112.5));
#533 = VECTOR('',#534,1.);
#534 = DIRECTION('',(0.,0.,1.));
#535 = PCURVE('',#511,#536);
#536 = DEFINITIONAL_REPRESENTATION('',(#537),#541);
#537 = LINE('',#538,#539);
#538 = CARTESIAN_POINT('',(6.28318530718,-0.));
#539 = VECTOR('',#540,1.);
#540 = DIRECTION('',(0.,1.));
#541 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#542 = PCURVE('',#511,#543);
#543 = DEFINITIONAL_REPRESENTATION('',(#544),#548);
#544 = LINE('',#545,#546);
#545 = CARTESIAN_POINT('',(0.,-0.));
#546 = VECTOR('',#547,1.);
#547 = DIRECTION('',(0.,1.));
#548 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#549 = ORIENTED_EDGE('',*,*,#550,.T.);
#550 = EDGE_CURVE('',#528,#528,#551,.T.);
#551 = SURFACE_CURVE('',#552,(#557,#564),.PCURVE_S1.);
#552 = CIRCLE('',#553,4.);
#553 = AXIS2_PLACEMENT_3D('',#554,#555,#556);
#554 = CARTESIAN_POINT('',(0.,0.,112.5));
#555 = DIRECTION('',(0.,0.,1.));
#556 = DIRECTION('',(1.,0.,-0.));
#557 = PCURVE('',#511,#558);
#558 = DEFINITIONAL_REPRESENTATION('',(#559),#563);
#559 = LINE('',#560,#561);
#560 = CARTESIAN_POINT('',(0.,0.));
#561 = VECTOR('',#562,1.);
#562 = DIRECTION('',(1.,0.));
#563 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#564 = PCURVE('',#565,#570);
#565 = PLANE('',#566);
#566 = AXIS2_PLACEMENT_3D('',#567,#568,#569);
#567 = CARTESIAN_POINT('',(0.,0.,112.5));
#568 = DIRECTION('',(0.,0.,1.));
#569 = DIRECTION('',(1.,0.,-0.));
#570 = DEFINITIONAL_REPRESENTATION('',(#571),#575);
#571 = CIRCLE('',#572,4.);
#572 = AXIS2_PLACEMENT_2D('',#573,#574);
#573 = CARTESIAN_POINT('',(0.,0.));
#574 = DIRECTION('',(1.,0.));
#575 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2)
PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE',''
) );
#576 = ORIENTED_EDGE('',*,*,#527,.T.);
#577 = ADVANCED_FACE('',(#578),#565,.T.);
#578 = FACE_BOUND('',#579,.F.);
#579 = EDGE_LOOP('',(#580));
#580 = ORIENTED_EDGE('',*,*,#550,.F.);
#581 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3)
GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#585)) GLOBAL_UNIT_ASSIGNED_CONTEXT
((#582,#583,#584)) REPRESENTATION_CONTEXT('Context #1',
'3D Context with UNIT and UNCERTAINTY') );
#582 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) );
#583 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) );
#584 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() );
#585 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#582,
'distance_accuracy_value','confusion accuracy');
#586 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#587,#589);
#587 = ( REPRESENTATION_RELATIONSHIP('','',#32,#10)
REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#588)
SHAPE_REPRESENTATION_RELATIONSHIP() );
#588 = ITEM_DEFINED_TRANSFORMATION('','',#11,#15);
#589 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item',#590
);
#590 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('1','=>[0:1:1:2]','',#5,#27,$);
#591 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#29));
#592=DESCRIPTIVE_REPRESENTATION_ITEM('cadpy:generator','cadpy');
#593=REPRESENTATION('cadpy:generator',(#592),#19);
#594=PROPERTY_DEFINITION('cadpy metadata','cadpy:generator',#5);
#595=PROPERTY_DEFINITION_REPRESENTATION(#594,#593);
#596=DESCRIPTIVE_REPRESENTATION_ITEM('cadpy:entryKind','part');
#597=REPRESENTATION('cadpy:entryKind',(#596),#19);
#598=PROPERTY_DEFINITION('cadpy metadata','cadpy:entryKind',#5);
#599=PROPERTY_DEFINITION_REPRESENTATION(#598,#597);
#600=DESCRIPTIVE_REPRESENTATION_ITEM('cadpy:sourcePath','061119_like.py');
#601=REPRESENTATION('cadpy:sourcePath',(#600),#19);
#602=PROPERTY_DEFINITION('cadpy metadata','cadpy:sourcePath',#5);
#603=PROPERTY_DEFINITION_REPRESENTATION(#602,#601);
#604=DESCRIPTIVE_REPRESENTATION_ITEM('cadpy:sourceHash','2ccee2227148c8f8001212325b0b068dd74de453f1afcbaba34e1bac35ec765d');
#605=REPRESENTATION('cadpy:sourceHash',(#604),#19);
#606=PROPERTY_DEFINITION('cadpy metadata','cadpy:sourceHash',#5);
#607=PROPERTY_DEFINITION_REPRESENTATION(#606,#605);
ENDSEC;
END-ISO-10303-21;
@@ -0,0 +1,28 @@
"""Approximate threaded stud, roller body, and hex-end silhouette.
The image does not define a thread standard. Rings represent the visible
thread rhythm while keeping the concept model lightweight and editable.
"""
from build123d import BuildPart, BuildSketch, Cylinder, Mode, Plane, Pos, RegularPolygon, extrude
from shaft_common import add_thread_rings, cut_end_bore, export_horizontal, stepped_body
def _hex_prism(z_start, across_corners, length):
with BuildPart() as prism:
with BuildSketch(Plane.XY.offset(z_start)):
RegularPolygon(radius=across_corners / 2.0, side_count=6)
extrude(amount=length)
return prism.part
def gen_step():
# Minor thread stem, neck, roller, and short nut-supporting neck.
body, total = stepped_body([(8.2, 40.0), (9.5, 7.0), (28.0, 25.0), (10.0, 4.0)])
body = add_thread_rings(body, z_start=0.5, length=38.5, major_diameter=10.0, pitch=2.0)
hex_body = _hex_prism(total - 0.15, across_corners=22.0, length=11.0)
body = body + hex_body
total += 10.85
body = cut_end_bore(body, total, diameter=8.0, depth=8.0)
return export_horizontal(body, "threaded_roller_067249_like")
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,20 @@
"""Approximate long slim stepped shaft inferred from reference 086237."""
from shaft_common import cut_side_flat, export_horizontal, stepped_body
SEGMENTS = [
(7.0, 20.0),
(10.0, 278.0),
(14.0, 10.0),
(18.0, 4.0),
(22.0, 8.0),
(18.0, 4.0),
(12.0, 18.0),
]
def gen_step():
body, _ = stepped_body(SEGMENTS)
body = cut_side_flat(body, diameter=7.0, z_start=14.0, length=6.0, flat_depth=1.0)
return export_horizontal(body, "shaft_086237_like")
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,65 @@
{
"schema_version": "1.0",
"request": "根据六张无尺寸参考图生成六个外形相似的轴类零件;尺寸按图片比例推定,不要求 1:1。",
"route": {
"selected_backend": "build123d",
"runner_skill": "cad",
"project": "text-to-cad",
"fallback_order": ["simplecadapi", "cadam"],
"workflow_profiles": ["caddesigner_ecip", "requirement_refinement", "visual_repair"]
},
"source": {
"paths": [
"001957_like.py",
"004910_like.py",
"054964_like.py",
"061119_like.py",
"067249_like.py",
"086237_like.py"
],
"format": "python",
"backend": "build123d"
},
"artifacts": [
{"path": "001957_like.step", "role": "primary"},
{"path": "001957_like.stl", "role": "secondary"},
{"path": "004910_like.step", "role": "primary"},
{"path": "004910_like.stl", "role": "secondary"},
{"path": "054964_like.step", "role": "primary"},
{"path": "054964_like.stl", "role": "secondary"},
{"path": "061119_like.step", "role": "primary"},
{"path": "061119_like.stl", "role": "secondary"},
{"path": "067249_like.step", "role": "primary"},
{"path": "067249_like.stl", "role": "secondary"},
{"path": "086237_like.step", "role": "primary"},
{"path": "086237_like.stl", "role": "secondary"}
],
"parameters": {
"001957_like": {"overall_length": 337.0, "max_diameter": 32.0},
"004910_like": {"overall_length": 120.0, "diameter": 18.0, "recess_diameter": 6.0},
"054964_like": {"overall_length": 90.0, "shaft_diameter": 16.0, "head_diameter": 24.0},
"061119_like": {"overall_length": 115.0, "body_diameter": 18.0},
"067249_like": {"overall_length": 86.85, "roller_diameter": 28.0, "nominal_thread_diameter": 10.0},
"086237_like": {"overall_length": 342.0, "max_diameter": 22.0}
},
"assumptions": [
"All dimensions are inferred in millimetres from image proportions; the images contain no reliable scale.",
"The models are shape studies, not production drawings or tolerance specifications.",
"067249 uses annular ribs as a lightweight visual thread indication, not a standards-compliant helical thread.",
"Small edge rounds and chamfers visible in renders are omitted unless they materially control the silhouette."
],
"validation": [
"All six build123d generators returned one valid solid before export.",
"All six STEP artifacts passed inspect refs --facts --planes --positioning with ok=true and no warnings.",
"Exported STEP bounds matched the inferred design envelopes: 337x32x32, 120x18x18, 90x24x24, 115x18x18, 86.85x28x28, and 342x22x22 mm (mesh-derived inspection values differ by under 0.02 mm on circular axes).",
"ISO and front snapshot packets were rendered and reviewed for all six parts."
],
"viewer_links": [
"http://127.0.0.1:4178/?dir=%2FUsers%2Fjerry%2Flinkhand%2FCadSet%2Ftext-to-cad%2Fmodels%2Fcad-router-v1%2Fshaft_reference_set_v1&file=001957_like.step",
"http://127.0.0.1:4178/?dir=%2FUsers%2Fjerry%2Flinkhand%2FCadSet%2Ftext-to-cad%2Fmodels%2Fcad-router-v1%2Fshaft_reference_set_v1&file=004910_like.step",
"http://127.0.0.1:4178/?dir=%2FUsers%2Fjerry%2Flinkhand%2FCadSet%2Ftext-to-cad%2Fmodels%2Fcad-router-v1%2Fshaft_reference_set_v1&file=054964_like.step",
"http://127.0.0.1:4178/?dir=%2FUsers%2Fjerry%2Flinkhand%2FCadSet%2Ftext-to-cad%2Fmodels%2Fcad-router-v1%2Fshaft_reference_set_v1&file=061119_like.step",
"http://127.0.0.1:4178/?dir=%2FUsers%2Fjerry%2Flinkhand%2FCadSet%2Ftext-to-cad%2Fmodels%2Fcad-router-v1%2Fshaft_reference_set_v1&file=067249_like.step",
"http://127.0.0.1:4178/?dir=%2FUsers%2Fjerry%2Flinkhand%2FCadSet%2Ftext-to-cad%2Fmodels%2Fcad-router-v1%2Fshaft_reference_set_v1&file=086237_like.step"
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 438 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

@@ -0,0 +1,67 @@
"""Shared helpers for the six image-derived shaft studies.
All source dimensions are millimetres. Parts are authored on +Z and rotated
onto +X at export so their long axis matches the supplied reference sheets.
"""
from build123d import Align, Axis, Box, Cylinder, Pos
JOIN_OVERLAP = 0.15
def stepped_body(segments):
"""Fuse [(diameter, length), ...] into one robust coaxial solid."""
body = None
z = 0.0
for index, (diameter, length) in enumerate(segments):
overlap = JOIN_OVERLAP if index else 0.0
piece = Pos(0, 0, z - overlap) * Cylinder(
diameter / 2.0,
length + overlap,
align=(Align.CENTER, Align.CENTER, Align.MIN),
)
body = piece if body is None else body + piece
z += length
return body, z
def cut_end_bore(body, total_length, diameter, depth):
tool = Pos(0, 0, total_length - depth) * Cylinder(
diameter / 2.0,
depth + 1.0,
align=(Align.CENTER, Align.CENTER, Align.MIN),
)
return body - tool
def cut_side_flat(body, diameter, z_start, length, flat_depth):
"""Cut one axial wrench/key flat into the +X side of a round shaft."""
x_start = diameter / 2.0 - flat_depth
tool = Pos(x_start, 0, z_start) * Box(
diameter,
diameter * 1.5,
length,
align=(Align.MIN, Align.CENTER, Align.MIN),
)
return body - tool
def add_thread_rings(body, z_start, length, major_diameter, pitch=2.0, rib_width=0.65):
"""Add simplified annular thread indication (not a true helical thread)."""
count = max(1, int(length // pitch))
for index in range(count):
z = z_start + index * pitch
ring = Pos(0, 0, z) * Cylinder(
major_diameter / 2.0,
min(rib_width, z_start + length - z),
align=(Align.CENTER, Align.CENTER, Align.MIN),
)
body = body + ring
return body
def export_horizontal(body, label):
result = body.rotate(Axis.Y, 90)
result.label = label
return result
@@ -0,0 +1,178 @@
{
"jobs": [
{
"input": "models/cad-router-v1/shaft_reference_set_v1/001957_like.step",
"mode": "view",
"outputs": [
{
"path": "models/cad-router-v1/shaft_reference_set_v1/review/001957_like_iso.png",
"camera": {
"direction": [
1,
-1,
0.65
]
}
},
{
"path": "models/cad-router-v1/shaft_reference_set_v1/review/001957_like_front.png",
"camera": "front"
}
],
"render": {
"viewLabels": true,
"padding": 0.14,
"sizeProfile": "diagnostic"
},
"display": {
"mode": "solid",
"projection": "orthographic"
}
},
{
"input": "models/cad-router-v1/shaft_reference_set_v1/004910_like.step",
"mode": "view",
"outputs": [
{
"path": "models/cad-router-v1/shaft_reference_set_v1/review/004910_like_iso.png",
"camera": {
"direction": [
1,
-1,
0.65
]
}
},
{
"path": "models/cad-router-v1/shaft_reference_set_v1/review/004910_like_front.png",
"camera": "front"
}
],
"render": {
"viewLabels": true,
"padding": 0.14,
"sizeProfile": "diagnostic"
},
"display": {
"mode": "solid",
"projection": "orthographic"
}
},
{
"input": "models/cad-router-v1/shaft_reference_set_v1/054964_like.step",
"mode": "view",
"outputs": [
{
"path": "models/cad-router-v1/shaft_reference_set_v1/review/054964_like_iso.png",
"camera": {
"direction": [
1,
-1,
0.65
]
}
},
{
"path": "models/cad-router-v1/shaft_reference_set_v1/review/054964_like_front.png",
"camera": "front"
}
],
"render": {
"viewLabels": true,
"padding": 0.14,
"sizeProfile": "diagnostic"
},
"display": {
"mode": "solid",
"projection": "orthographic"
}
},
{
"input": "models/cad-router-v1/shaft_reference_set_v1/061119_like.step",
"mode": "view",
"outputs": [
{
"path": "models/cad-router-v1/shaft_reference_set_v1/review/061119_like_iso.png",
"camera": {
"direction": [
1,
-1,
0.65
]
}
},
{
"path": "models/cad-router-v1/shaft_reference_set_v1/review/061119_like_front.png",
"camera": "front"
}
],
"render": {
"viewLabels": true,
"padding": 0.14,
"sizeProfile": "diagnostic"
},
"display": {
"mode": "solid",
"projection": "orthographic"
}
},
{
"input": "models/cad-router-v1/shaft_reference_set_v1/067249_like.step",
"mode": "view",
"outputs": [
{
"path": "models/cad-router-v1/shaft_reference_set_v1/review/067249_like_iso.png",
"camera": {
"direction": [
1,
-1,
0.65
]
}
},
{
"path": "models/cad-router-v1/shaft_reference_set_v1/review/067249_like_front.png",
"camera": "front"
}
],
"render": {
"viewLabels": true,
"padding": 0.14,
"sizeProfile": "diagnostic"
},
"display": {
"mode": "solid",
"projection": "orthographic"
}
},
{
"input": "models/cad-router-v1/shaft_reference_set_v1/086237_like.step",
"mode": "view",
"outputs": [
{
"path": "models/cad-router-v1/shaft_reference_set_v1/review/086237_like_iso.png",
"camera": {
"direction": [
1,
-1,
0.65
]
}
},
{
"path": "models/cad-router-v1/shaft_reference_set_v1/review/086237_like_front.png",
"camera": "front"
}
],
"render": {
"viewLabels": true,
"padding": 0.14,
"sizeProfile": "diagnostic"
},
"display": {
"mode": "solid",
"projection": "orthographic"
}
}
]
}
@@ -0,0 +1,205 @@
const Z_AXIS = [0, 0, 1];
const ORIGIN = [0, 0, 0];
const PLANETS = ["planet1", "planet2", "planet3"];
const PINS = ["pin1", "pin2", "pin3"];
const SUN_TEETH = 24;
const PLANET_TEETH = 18;
const RING_TEETH = 60;
// Fixed-ring planetary kinematics: carrier follows the sun by Ns / (Ns + Nr).
const CARRIER_RATIO_FIXED_RING = SUN_TEETH / (SUN_TEETH + RING_TEETH);
// 3.5 sun revolutions returns the carrier to 360 deg and each gear to an equivalent tooth phase.
const FULL_MESH_CYCLE_DEG = 1260;
let orbitGuide = null;
function finite(value, fallback = 0) {
const numericValue = Number(value);
return Number.isFinite(numericValue) ? numericValue : fallback;
}
function radialUnit(feature) {
const center = Array.isArray(feature?.center) ? feature.center : ORIGIN;
const length = Math.hypot(center[0] || 0, center[1] || 0);
if (length <= 1e-6) {
return [1, 0, 0];
}
return [(center[0] || 0) / length, (center[1] || 0) / length, 0];
}
function scaled(vector, scale) {
return vector.map((value) => value * scale);
}
function createOrbitGuide(THREE) {
const radius = 42;
const points = [];
for (let index = 0; index < 144; index += 1) {
const angle = (index / 144) * Math.PI * 2;
points.push(new THREE.Vector3(Math.cos(angle) * radius, Math.sin(angle) * radius, 4));
}
const geometry = new THREE.BufferGeometry().setFromPoints(points);
const material = new THREE.LineBasicMaterial({
color: 0x94a3b8,
transparent: true,
opacity: 0.34,
depthWrite: false
});
const line = new THREE.LineLoop(geometry, material);
line.name = "planetary-gear-orbit-guide";
line.renderOrder = 30;
return line;
}
export default {
manifest: {
schemaVersion: 1,
step: {
path: "models/fun/planetary_gear_assembly.step"
},
units: {
length: "mm",
angle: "deg",
time: "s"
},
features: {
carrier: { ref: "#o1.1.1", label: "Carrier plate", description: "The plate that carries planet pins around the sun axis." },
ring: { ref: "#o1.2.1", label: "Ring gear", description: "Stationary internal gear, visually faded or hidden by controls." },
sun: { ref: "#o1.3.1", label: "Sun gear", description: "Central driven gear." },
planet1: { ref: "#o1.4.1", label: "Planet gear 1" },
pin1: { ref: "#o1.5.1", label: "Planet pin 1" },
planet2: { ref: "#o1.6.1", label: "Planet gear 2" },
pin2: { ref: "#o1.7.1", label: "Planet pin 2" },
planet3: { ref: "#o1.8.1", label: "Planet gear 3" },
pin3: { ref: "#o1.9.1", label: "Planet pin 3" }
},
parameters: {
drive: { type: "number", label: "Drive", description: "Sun gear input angle across one closed mesh cycle.", default: 0, min: 0, max: FULL_MESH_CYCLE_DEG, step: 1, unit: "deg" },
explode: { type: "number", label: "Explode", description: "Radially separates meshing features for inspection.", default: 0, min: 0, max: 1, step: 0.01 },
ringVisible: { type: "boolean", label: "Ring gear", default: true },
orbitGuides: { type: "boolean", label: "Orbit guide", default: false },
highlightMeshing: { type: "boolean", label: "Mesh highlight", default: false },
viewMode: {
type: "select",
label: "View",
default: "mesh",
options: [
{ value: "mesh", label: "Mesh study" },
{ value: "cutaway", label: "Cutaway" },
{ value: "carrier", label: "Carrier focus" }
]
}
},
animations: {
meshCycle: {
label: "Mesh cycle",
duration: 6,
loop: true,
update({ cycle, set }) {
set("drive", (cycle % 1) * FULL_MESH_CYCLE_DEG);
}
},
inspectExplode: {
label: "Explode inspect",
duration: 5,
loop: true,
update({ progress, set }) {
set("drive", progress * FULL_MESH_CYCLE_DEG);
set("explode", Math.sin(progress * Math.PI));
}
}
}
},
setup({ THREE, modelGroup, cleanup }) {
if (!THREE || !modelGroup) {
return;
}
orbitGuide = createOrbitGuide(THREE);
modelGroup.add(orbitGuide);
cleanup(() => {
modelGroup.remove(orbitGuide);
orbitGuide.geometry?.dispose?.();
orbitGuide.material?.dispose?.();
orbitGuide = null;
});
},
update({ params, features, effects, time }) {
const drive = finite(params.drive);
const explode = finite(params.explode);
const sunAngle = drive;
const carrierAngle = sunAngle * CARRIER_RATIO_FIXED_RING;
const planetSpinRelativeToCarrier = -(SUN_TEETH / PLANET_TEETH) * (sunAngle - carrierAngle);
const lift = explode * 7;
const cutaway = params.viewMode === "cutaway";
const carrierFocus = params.viewMode === "carrier";
const highlight = params.highlightMeshing === true &&
(!time.playing || (time.progress > 0.45 && time.progress < 0.6));
if (orbitGuide) {
orbitGuide.visible = params.orbitGuides !== false;
orbitGuide.rotation.z = (carrierAngle * Math.PI) / 180;
}
effects.visible("ring", params.ringVisible !== false);
effects.style("ring", {
opacity: cutaway ? 0.22 : 1,
edgeOpacity: cutaway ? 0.38 : 1
});
effects.style("sun", {
emissive: highlight ? "#7c2d12" : "",
emissiveIntensity: highlight ? 0.32 : 0
});
effects.style(PLANETS, {
emissive: highlight ? "#075985" : "",
emissiveIntensity: highlight ? 0.22 : 0
});
effects.style(PINS, {
opacity: 1
});
effects.style("carrier", {
opacity: 1,
emissive: carrierFocus ? "#14532d" : "",
emissiveIntensity: carrierFocus ? 0.2 : 0
});
if (highlight) {
effects.highlight(["sun", ...PLANETS], true);
}
effects.transform("sun", {
transforms: [
{ rotate: { axis: Z_AXIS, origin: features.sun.center, angleDeg: sunAngle } },
{ translate: [0, 0, lift] }
]
});
effects.transform("carrier", {
transforms: [
{ rotate: { axis: Z_AXIS, origin: ORIGIN, angleDeg: carrierAngle } },
{ translate: [0, 0, -explode * 4] }
]
});
for (const planetId of PLANETS) {
const radial = radialUnit(features[planetId]);
effects.transform(planetId, {
transforms: [
{ rotate: { axis: Z_AXIS, origin: features[planetId].center, angleDeg: planetSpinRelativeToCarrier } },
{ translate: scaled(radial, explode * 16) },
{ rotate: { axis: Z_AXIS, origin: ORIGIN, angleDeg: carrierAngle } }
]
});
}
for (const pinId of PINS) {
const radial = radialUnit(features[pinId]);
effects.transform(pinId, {
transforms: [
{ translate: scaled(radial, explode * 16) },
{ rotate: { axis: Z_AXIS, origin: ORIGIN, angleDeg: carrierAngle } },
{ translate: [0, 0, -explode * 4] }
]
});
}
}
};
+17
View File
@@ -0,0 +1,17 @@
# Fun Models
Standalone generated CAD examples used for demos, visual inspection, and
occasional print/export experiments. These models are more expressive than the
benchmark fixtures and are not expected to form a systematic test suite.
## Contents
- `*.py`: build123d generator source for authored models.
- `*.step`: canonical generated STEP output.
- `.*.step.glb`: generated render/selector sidecars paired with STEP files.
- `.*.step.js`: optional interaction sidecars paired with STEP files.
- `*.stl`, `*.3mf`, and direct `*.glb`: durable exported/printable artifacts
when the export itself is useful as a fixture.
Avoid adding screenshots, videos, throwaway slicer profiles, or timestamped
review captures here.
@@ -0,0 +1,229 @@
from math import asin, cos, hypot, radians, sin, tau
from build123d import (
Align,
BuildPart,
BuildSketch,
Color,
Cylinder,
Location,
Locations,
Mode,
Plane,
Polygon,
add,
extrude,
fillet,
)
# Units: millimeters.
# Origin: impeller axis centered at X=0, Y=0.
# XY: backplate plane. +Z: vertical impeller axis.
# Assumed rotation direction: counter-clockwise viewed from +Z, so blade tips
# trail clockwise by the specified backward sweep.
BACKPLATE_DIAMETER = 90.0
BACKPLATE_RADIUS = BACKPLATE_DIAMETER / 2.0
BACKPLATE_THICKNESS = 6.0
BACKPLATE_OUTER_FILLET_RADIUS = 1.5
HUB_DIAMETER = 26.0
HUB_RADIUS = HUB_DIAMETER / 2.0
HUB_HEIGHT_ABOVE_BACKPLATE = 22.0
BORE_DIAMETER = 8.0
BORE_RADIUS = BORE_DIAMETER / 2.0
BLADE_COUNT = 12
BLADE_ROOT_RADIUS = 18.0
BLADE_TIP_RADIUS = 43.0
BLADE_THICKNESS = 3.0
BLADE_HEIGHT_ABOVE_BACKPLATE = 16.0
BLADE_BACKWARD_SWEEP_ANGLE = radians(45.0)
BLADE_CURVE_SEGMENTS = 5
BLADE_ROOT_FILLET_RADIUS = 1.0
BLADE_ROOT_FILLET_MAX_RADIUS = 30.0
MODEL_LABEL = "centrifugal_impeller_12_backward_curved_blades"
def _polar_point(radius: float, angle: float) -> tuple[float, float]:
return (radius * cos(angle), radius * sin(angle))
def _clamp_radius(point: tuple[float, float]) -> tuple[float, float]:
x_pos, y_pos = point
radius = hypot(x_pos, y_pos)
if radius < BLADE_ROOT_RADIUS:
scale = BLADE_ROOT_RADIUS / radius
return (x_pos * scale, y_pos * scale)
if radius > BLADE_TIP_RADIUS:
scale = BLADE_TIP_RADIUS / radius
return (x_pos * scale, y_pos * scale)
return point
def _blade_center_point(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_ANGLE * 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
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_endpoint_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_ANGLE * 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: list[tuple[float, float]] = []
right_side: list[tuple[float, float]] = []
half_thickness = BLADE_THICKNESS / 2.0
for index in range(BLADE_CURVE_SEGMENTS + 1):
t = index / BLADE_CURVE_SEGMENTS
if index == 0 or index == BLADE_CURVE_SEGMENTS:
left_point, right_point = _blade_endpoint_points(base_angle, t)
left_side.append(left_point)
right_side.append(right_point)
continue
x_pos, y_pos = _blade_center_point(base_angle, t)
nx, ny = _blade_normal(base_angle, t)
left_side.append(
_clamp_radius((x_pos + nx * half_thickness, y_pos + ny * half_thickness))
)
right_side.append(
_clamp_radius((x_pos - nx * half_thickness, y_pos - ny * half_thickness))
)
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_ABOVE_BACKPLATE)
part = blade.part.moved(Location((0.0, 0.0, BACKPLATE_THICKNESS)))
part.label = f"blade_{index + 1:02d}"
return part
def _make_backplate():
with BuildPart() as backplate:
Cylinder(
radius=BACKPLATE_RADIUS,
height=BACKPLATE_THICKNESS,
align=(Align.CENTER, Align.CENTER, Align.MIN),
)
part = backplate.part
outer_edges = []
outer_edges.extend(_edges_near_z_radius(part, 0.0, BACKPLATE_RADIUS))
outer_edges.extend(_edges_near_z_radius(part, BACKPLATE_THICKNESS, BACKPLATE_RADIUS))
part = fillet(outer_edges, radius=BACKPLATE_OUTER_FILLET_RADIUS)
part.label = "filleted_backplate"
return part
def _edge_z(edge) -> float:
return edge.bounding_box().center().Z
def _edge_xy_radius(edge) -> float:
center = edge.bounding_box().center()
return hypot(center.X, center.Y)
def _circle_radius(edge) -> float | None:
try:
return edge.radius
except ValueError:
return None
def _edges_near_z_radius(part, z_value: float, radius: float, tolerance: float = 0.02):
edges = []
for edge in part.edges():
circle_radius = _circle_radius(edge)
if circle_radius is None:
continue
if abs(_edge_z(edge) - z_value) <= tolerance and abs(circle_radius - radius) <= tolerance:
edges.append(edge)
return edges
def _blade_root_edges(part):
min_radius = BLADE_ROOT_RADIUS - BLADE_THICKNESS
edges = []
for edge in part.edges():
if _circle_radius(edge) is not None:
continue
if abs(_edge_z(edge) - BACKPLATE_THICKNESS) > 0.02:
continue
if min_radius <= _edge_xy_radius(edge) <= BLADE_ROOT_FILLET_MAX_RADIUS:
edges.append(edge)
return edges
def gen_step():
"""Return a single solid centrifugal impeller in millimeters."""
total_height = BACKPLATE_THICKNESS + HUB_HEIGHT_ABOVE_BACKPLATE
backplate = _make_backplate()
with BuildPart() as impeller:
add(backplate, mode=Mode.ADD)
with Locations(Location((0.0, 0.0, BACKPLATE_THICKNESS))):
Cylinder(
radius=HUB_RADIUS,
height=HUB_HEIGHT_ABOVE_BACKPLATE,
align=(Align.CENTER, Align.CENTER, Align.MIN),
mode=Mode.ADD,
)
for blade_index in range(BLADE_COUNT):
add(_make_blade(blade_index), mode=Mode.ADD)
with Locations(Location((0.0, 0.0, -1.0))):
Cylinder(
radius=BORE_RADIUS,
height=total_height + 2.0,
align=(Align.CENTER, Align.CENTER, Align.MIN),
mode=Mode.SUBTRACT,
)
part = impeller.part
root_edges = _blade_root_edges(part)
root_edges.extend(_edges_near_z_radius(part, BACKPLATE_THICKNESS, HUB_RADIUS))
if root_edges:
part = fillet(root_edges, radius=BLADE_ROOT_FILLET_RADIUS)
part.label = MODEL_LABEL
part.color = Color(0.62, 0.66, 0.70, 1.0)
return part
@@ -0,0 +1,646 @@
from __future__ import annotations
from math import atan2, cos, degrees, hypot, radians, sin
from build123d import (
Align,
Axis,
BuildPart,
BuildSketch,
Color,
Compound,
Cone,
Cylinder,
Face,
Location,
Locations,
Mode,
Plane,
Polygon,
RectangleRounded,
Torus,
Wire,
add,
extrude,
fillet,
loft,
)
DISPLAY_NAME = "Cutaway miniature turbofan jet engine"
# Units: millimeters.
# Origin: front intake center at X=0 on the engine axis.
# Engine axis: +X, centered through Y=0, Z=0.
MODEL_LABEL = "cutaway_miniature_turbofan_engine"
CUTAWAY_START_DEG = -5.0
CUTAWAY_END_DEG = 95.0
VISIBLE_SECTOR_START_DEG = CUTAWAY_END_DEG
VISIBLE_SECTOR_END_DEG = 360.0 + CUTAWAY_START_DEG
NACELLE_LENGTH = 180.0
NACELLE_OUTER_RADIUS = 55.0
NACELLE_INNER_RADIUS = 48.0
BLADE_ROOT_OVERLAP = 1.5
STATOR_TIP_OVERLAP = 1.0
STRUT_OVERLAP = 0.8
BASE_CENTER_X = 90.0
BASE_LENGTH = 140.0
BASE_WIDTH = 70.0
BASE_THICKNESS = 8.0
BASE_TOP_Z = -110.0
BASE_BOTTOM_Z = BASE_TOP_Z - BASE_THICKNESS
def _polar_yz(radius: float, angle_rad: float) -> tuple[float, float]:
return (radius * cos(angle_rad), radius * sin(angle_rad))
def _point_yz(x_pos: float, radius: float, angle_rad: float) -> tuple[float, float, float]:
y_pos, z_pos = _polar_yz(radius, angle_rad)
return (x_pos, y_pos, z_pos)
def _annular_sector_points(
*,
outer_radius: float,
inner_radius: float,
start_deg: float = VISIBLE_SECTOR_START_DEG,
end_deg: float = VISIBLE_SECTOR_END_DEG,
segments: int = 96,
) -> list[tuple[float, float]]:
outer_points = []
inner_points = []
for index in range(segments + 1):
angle = radians(start_deg + (end_deg - start_deg) * index / segments)
outer_points.append(_polar_yz(outer_radius, angle))
inner_points.append(_polar_yz(inner_radius, angle))
return outer_points + list(reversed(inner_points))
def _annular_sector_face(
*,
x_pos: float,
outer_radius: float,
inner_radius: float,
start_deg: float = VISIBLE_SECTOR_START_DEG,
end_deg: float = VISIBLE_SECTOR_END_DEG,
segments: int = 72,
) -> Face:
points = []
for index in range(segments + 1):
angle = radians(start_deg + (end_deg - start_deg) * index / segments)
points.append(_point_yz(x_pos, outer_radius, angle))
for index in range(segments, -1, -1):
angle = radians(start_deg + (end_deg - start_deg) * index / segments)
points.append(_point_yz(x_pos, inner_radius, angle))
return Face.make_surface(Wire.make_polygon(points, close=True))
def _yz_angle_degrees(y_pos: float, z_pos: float) -> float:
return degrees(atan2(z_pos, y_pos)) % 360.0
def _angle_delta_deg(a: float, b: float) -> float:
return abs((a - b + 180.0) % 360.0 - 180.0)
def _in_cutaway(angle_deg: float) -> bool:
angle = angle_deg % 360.0
start = CUTAWAY_START_DEG % 360.0
end = CUTAWAY_END_DEG % 360.0
if start > end:
return angle >= start or angle <= end
return start <= angle <= end
def _safe_fillet(part, edges, radius: float):
if not edges:
return part
try:
return fillet(edges, radius=radius)
except Exception:
return part
def _label(part, label: str, color: Color | None = None):
part.label = label
if color is not None:
part.color = color
return part
def _make_compound(label: str, parts: list, color: Color | None = None) -> Compound:
compound = Compound(obj=parts, children=parts, label=label)
if color is not None:
compound.color = color
return compound
def _make_cylinder_x(
*,
label: str,
x_start: float,
x_end: float,
radius: float,
color: Color,
) -> object:
with BuildPart() as cylinder_part:
with Locations(Location(((x_start + x_end) / 2.0, 0.0, 0.0))):
Cylinder(
radius=radius,
height=x_end - x_start,
rotation=(0.0, 90.0, 0.0),
align=(Align.CENTER, Align.CENTER, Align.CENTER),
)
return _label(cylinder_part.part, label, color)
def _make_annular_sector(
*,
label: str,
x_start: float,
x_end: float,
outer_radius: float,
inner_radius: float,
color: Color,
fillet_cut_edges: float | None = None,
) -> object:
with BuildPart() as sector:
with BuildSketch(Plane.YZ):
Polygon(
_annular_sector_points(
outer_radius=outer_radius,
inner_radius=inner_radius,
),
align=None,
)
extrude(amount=x_end - x_start)
part = sector.part.moved(Location((x_start, 0.0, 0.0)))
if fillet_cut_edges is not None:
cut_edges = []
for edge in part.edges():
bbox = edge.bounding_box()
center = bbox.center()
if bbox.size.X < (x_end - x_start) * 0.85:
continue
angle = _yz_angle_degrees(center.Y, center.Z)
radius = hypot(center.Y, center.Z)
if (
min(inner_radius, outer_radius) - 1.0
<= radius
<= max(inner_radius, outer_radius) + 1.0
and (
_angle_delta_deg(angle, CUTAWAY_START_DEG) < 1.0
or _angle_delta_deg(angle, CUTAWAY_END_DEG) < 1.0
)
):
cut_edges.append(edge)
part = _safe_fillet(part, cut_edges, fillet_cut_edges)
return _label(part, label, color)
def _make_tapered_annular_sector(
*,
label: str,
x_start: float,
x_end: float,
outer_start_radius: float,
inner_start_radius: float,
outer_end_radius: float,
inner_end_radius: float,
color: Color,
) -> object:
part = loft(
[
_annular_sector_face(
x_pos=x_start,
outer_radius=outer_start_radius,
inner_radius=inner_start_radius,
),
_annular_sector_face(
x_pos=x_end,
outer_radius=outer_end_radius,
inner_radius=inner_end_radius,
),
]
)
return _label(part, label, color)
def _make_intake_lip() -> object:
with BuildPart() as lip:
with Locations(Location((5.0, 0.0, 0.0))):
Torus(
major_radius=50.0,
minor_radius=5.0,
major_angle=260.0,
rotation=(0.0, 90.0, 0.0),
)
part = lip.part.rotate(Axis.X, -175.0)
return _label(part, "rounded_intake_lip_5mm_radius", Color(0.74, 0.77, 0.78, 1.0))
def _make_nacelle_shell() -> Compound:
parts = [
_make_annular_sector(
label="nacelle_hollow_duct_100deg_cutaway",
x_start=0.0,
x_end=NACELLE_LENGTH,
outer_radius=NACELLE_OUTER_RADIUS,
inner_radius=NACELLE_INNER_RADIUS,
color=Color(0.73, 0.76, 0.78, 1.0),
fillet_cut_edges=2.0,
),
_make_intake_lip(),
_make_tapered_annular_sector(
label="rear_exhaust_ring_od92_id74",
x_start=160.0,
x_end=180.0,
outer_start_radius=48.0,
inner_start_radius=39.0,
outer_end_radius=46.0,
inner_end_radius=37.0,
color=Color(0.62, 0.65, 0.67, 1.0),
),
]
return _make_compound("nacelle_shell", parts)
def _normalized(vector: tuple[float, float, float]) -> tuple[float, float, float]:
length = hypot(hypot(vector[0], vector[1]), vector[2])
return (vector[0] / length, vector[1] / length, vector[2] / length)
def _scale(vector: tuple[float, float, float], amount: float) -> tuple[float, float, float]:
return (vector[0] * amount, vector[1] * amount, vector[2] * amount)
def _add_vec(
a: tuple[float, float, float],
b: tuple[float, float, float],
) -> tuple[float, float, float]:
return (a[0] + b[0], a[1] + b[1], a[2] + b[2])
def _section_face(
*,
center: tuple[float, float, float],
chord_direction: tuple[float, float, float],
thickness_direction: tuple[float, float, float],
chord: float,
thickness: float,
) -> Face:
chord_half = _scale(chord_direction, chord / 2.0)
thickness_half = _scale(thickness_direction, thickness / 2.0)
points = [
_add_vec(_add_vec(center, chord_half), thickness_half),
_add_vec(_add_vec(center, _scale(chord_half, -1.0)), thickness_half),
_add_vec(_add_vec(center, _scale(chord_half, -1.0)), _scale(thickness_half, -1.0)),
_add_vec(_add_vec(center, chord_half), _scale(thickness_half, -1.0)),
]
return Face.make_surface(Wire.make_polygon(points, close=True))
def _make_radial_blade(
*,
label: str,
x_center: float,
base_angle: float,
root_radius: float,
tip_radius: float,
chord: float,
thickness: float,
sweep_deg: float,
twist_root_deg: float,
twist_tip_deg: float,
color: Color,
attachment_radius: float | None = None,
axial_sweep: float = 0.0,
) -> object:
span = tip_radius - root_radius
stations = []
if attachment_radius is not None and attachment_radius < root_radius:
stations.append((attachment_radius, 0.0, 0.52, 1.08))
stations.extend(
[
(root_radius, 0.0, 1.00, 1.00),
(root_radius + span * 0.38, 0.38, 1.04, 0.92),
(root_radius + span * 0.72, 0.72, 0.96, 0.84),
(tip_radius, 1.0, 0.78, 0.68),
]
)
faces = []
for radius, t, chord_scale, thickness_scale in stations:
angle = base_angle + radians(sweep_deg) * (t**1.25)
twist = radians(twist_root_deg + (twist_tip_deg - twist_root_deg) * t)
tangent = (0.0, -sin(angle), cos(angle))
x_axis = (1.0, 0.0, 0.0)
chord_direction = _normalized(
(
cos(twist) * x_axis[0] + sin(twist) * tangent[0],
cos(twist) * x_axis[1] + sin(twist) * tangent[1],
cos(twist) * x_axis[2] + sin(twist) * tangent[2],
)
)
thickness_direction = _normalized(
(
-sin(twist) * x_axis[0] + cos(twist) * tangent[0],
-sin(twist) * x_axis[1] + cos(twist) * tangent[1],
-sin(twist) * x_axis[2] + cos(twist) * tangent[2],
)
)
center = (
x_center + axial_sweep * (t - 0.5),
radius * cos(angle),
radius * sin(angle),
)
faces.append(
_section_face(
center=center,
chord_direction=chord_direction,
thickness_direction=thickness_direction,
chord=chord * chord_scale,
thickness=thickness * thickness_scale,
)
)
part = loft(faces, ruled=False)
return _label(part, label, color)
def _make_fan_rotor() -> Compound:
parts = [
_make_cylinder_x(
label="fan_hub_d26_l18",
x_start=19.0,
x_end=37.0,
radius=13.0,
color=Color(0.31, 0.38, 0.43, 1.0),
)
]
for index in range(24):
parts.append(
_make_radial_blade(
label=f"fan_blade_{index + 1:02d}",
x_center=28.0,
base_angle=radians(index * 360.0 / 24.0),
root_radius=16.0,
tip_radius=45.0,
chord=18.0,
thickness=2.0,
sweep_deg=24.0,
twist_root_deg=-20.0,
twist_tip_deg=15.0,
attachment_radius=13.0 - BLADE_ROOT_OVERLAP,
axial_sweep=3.0,
color=Color(0.77, 0.82, 0.86, 1.0),
)
)
return _make_compound("fan_rotor", parts)
def _make_compressor_rotor() -> Compound:
parts = []
stage_centers = [57.0, 69.0, 81.0, 93.0]
for stage_index, x_center in enumerate(stage_centers):
parts.append(
_make_cylinder_x(
label=f"compressor_stage_{stage_index + 1}_disk_d42",
x_start=x_center - 2.0,
x_end=x_center + 2.0,
radius=21.0,
color=Color(0.45, 0.50, 0.54, 1.0),
)
)
phase = radians(stage_index * 10.0)
stage_angle_offset = -4.0 if stage_index % 2 else 4.0
for blade_index in range(18):
parts.append(
_make_radial_blade(
label=f"compressor_s{stage_index + 1}_blade_{blade_index + 1:02d}",
x_center=x_center,
base_angle=phase + radians(blade_index * 360.0 / 18.0),
root_radius=22.0,
tip_radius=34.0,
chord=8.0,
thickness=1.2,
sweep_deg=8.0 + stage_angle_offset,
twist_root_deg=26.0 + stage_angle_offset,
twist_tip_deg=12.0 + stage_angle_offset,
attachment_radius=21.0 - BLADE_ROOT_OVERLAP,
axial_sweep=1.2,
color=Color(0.69, 0.73, 0.76, 1.0),
)
)
return _make_compound("compressor_rotor_four_stages", parts)
def _make_stator_vanes() -> Compound:
parts = []
for ring_index, x_center in enumerate([63.0, 75.0, 87.0]):
phase = radians(360.0 / 32.0 + ring_index * 6.0)
for vane_index in range(16):
parts.append(
_make_radial_blade(
label=f"stator_r{ring_index + 1}_vane_{vane_index + 1:02d}",
x_center=x_center,
base_angle=phase + radians(vane_index * 360.0 / 16.0),
root_radius=36.0,
tip_radius=NACELLE_INNER_RADIUS + STATOR_TIP_OVERLAP,
chord=7.5,
thickness=1.0,
sweep_deg=-7.0,
twist_root_deg=-30.0,
twist_tip_deg=-18.0,
axial_sweep=-0.8,
color=Color(0.79, 0.65, 0.37, 1.0),
)
)
return _make_compound("stator_vanes_three_rings", parts)
def _make_turbine_rotor() -> Compound:
parts = []
for stage_index, x_center in enumerate([128.0, 145.0]):
parts.append(
_make_cylinder_x(
label=f"turbine_stage_{stage_index + 1}_disk_d46",
x_start=x_center - 2.5,
x_end=x_center + 2.5,
radius=23.0,
color=Color(0.49, 0.45, 0.41, 1.0),
)
)
phase = radians(stage_index * 12.0)
for blade_index in range(22):
parts.append(
_make_radial_blade(
label=f"turbine_s{stage_index + 1}_blade_{blade_index + 1:02d}",
x_center=x_center,
base_angle=phase + radians(blade_index * 360.0 / 22.0),
root_radius=24.0,
tip_radius=40.0,
chord=12.0,
thickness=2.0,
sweep_deg=-34.0,
twist_root_deg=-44.0,
twist_tip_deg=-8.0,
attachment_radius=23.0 - BLADE_ROOT_OVERLAP,
axial_sweep=2.2,
color=Color(0.74, 0.59, 0.42, 1.0),
)
)
return _make_compound("turbine_rotor_two_stages", parts)
def _make_exhaust_cone() -> object:
part = loft(
[
Face.make_surface(
Wire.make_circle(
18.0,
plane=Plane((145.0, 0.0, 0.0), x_dir=(0.0, 1.0, 0.0), z_dir=(1.0, 0.0, 0.0)),
)
),
Face.make_surface(
Wire.make_circle(
7.0,
plane=Plane((178.0, 0.0, 0.0), x_dir=(0.0, 1.0, 0.0), z_dir=(1.0, 0.0, 0.0)),
)
),
]
)
return _label(part, "exhaust_cone_taper_d36_to_d14", Color(0.38, 0.40, 0.42, 1.0))
def _make_support_strut(index: int, angle_deg: float, *, trimmed: bool = False) -> object:
angle = radians(angle_deg)
inner_radius = 4.0 - STRUT_OVERLAP
outer_radius = 31.0 if trimmed else NACELLE_INNER_RADIUS + STRUT_OVERLAP
radial_direction = (0.0, cos(angle), sin(angle))
tangent_direction = (0.0, -sin(angle), cos(angle))
x_direction = (1.0, 0.0, 0.0)
faces = []
for radius in (inner_radius, outer_radius):
center = (110.0, radius * radial_direction[1], radius * radial_direction[2])
faces.append(
_section_face(
center=center,
chord_direction=x_direction,
thickness_direction=tangent_direction,
chord=5.0,
thickness=4.0,
)
)
part = loft(faces, ruled=True)
suffix = "_trimmed" if trimmed else ""
return _label(part, f"support_strut_{index}_{int(angle_deg)}deg{suffix}", Color(0.33, 0.36, 0.38, 1.0))
def _make_support_struts() -> Compound:
parts = []
for index, angle_deg in enumerate([5.0, 95.0, 185.0, 275.0], start=1):
trimmed = _in_cutaway(angle_deg) and _angle_delta_deg(angle_deg, CUTAWAY_END_DEG) > 0.5
parts.append(_make_support_strut(index, angle_deg, trimmed=trimmed))
return _make_compound("support_struts_cutaway_trimmed", parts)
def _make_mounting_pylon() -> object:
def pylon_face(z_pos: float, x_width: float, y_width: float, x_center: float) -> Face:
points = [
(x_center - x_width / 2.0, 0.0, z_pos),
(x_center - x_width * 0.18, y_width / 2.0, z_pos),
(x_center + x_width * 0.35, y_width / 2.0, z_pos),
(x_center + x_width / 2.0, 0.0, z_pos),
(x_center + x_width * 0.35, -y_width / 2.0, z_pos),
(x_center - x_width * 0.18, -y_width / 2.0, z_pos),
]
return Face.make_surface(Wire.make_polygon(points, close=True))
part = loft(
[
pylon_face(BASE_TOP_Z, 22.0, 10.0, 90.0),
pylon_face(-82.0, 19.0, 9.0, 90.0),
pylon_face(-55.0, 18.0, 8.0, 90.0),
],
ruled=False,
)
return _label(part, "mounting_pylon_swept_aero_support", Color(0.46, 0.49, 0.50, 1.0))
def _make_display_base() -> object:
with BuildPart() as base:
with BuildSketch(Plane.XY):
RectangleRounded(
BASE_LENGTH,
BASE_WIDTH,
5.0,
align=(Align.CENTER, Align.CENTER),
)
extrude(amount=BASE_THICKNESS)
for x_offset in (-50.0, 50.0):
for y_pos in (-22.0, 22.0):
x_pos = BASE_CENTER_X + x_offset
with Locations(Location((x_pos - BASE_CENTER_X, y_pos, -0.5))):
Cylinder(
radius=3.0,
height=BASE_THICKNESS + 1.0,
align=(Align.CENTER, Align.CENTER, Align.MIN),
mode=Mode.SUBTRACT,
)
with Locations(Location((x_pos - BASE_CENTER_X, y_pos, BASE_THICKNESS))):
Cone(
bottom_radius=3.0,
top_radius=5.5,
height=2.6,
align=(Align.CENTER, Align.CENTER, Align.MAX),
mode=Mode.SUBTRACT,
)
part = base.part.moved(Location((BASE_CENTER_X, 0.0, BASE_BOTTOM_Z)))
outer_edges = []
for edge in part.edges():
bbox = edge.bounding_box()
center = bbox.center()
if abs(center.Z - BASE_BOTTOM_Z) > 0.03 and abs(center.Z - BASE_TOP_Z) > 0.03:
continue
if (
abs(center.X - BASE_CENTER_X) > BASE_LENGTH / 2.0 - 6.0
or abs(center.Y) > BASE_WIDTH / 2.0 - 6.0
):
outer_edges.append(edge)
part = _safe_fillet(part, outer_edges, 3.0)
return _label(part, "display_base_rounded_rectangle_countersunk_holes", Color(0.22, 0.24, 0.25, 1.0))
def gen_step():
"""Return a labeled multi-body cutaway turbofan display model."""
bodies = [
_make_nacelle_shell(),
_make_fan_rotor(),
_make_compressor_rotor(),
_make_turbine_rotor(),
_make_cylinder_x(
label="central_shaft_d8_x15_to_x165",
x_start=15.0,
x_end=165.0,
radius=4.0,
color=Color(0.18, 0.20, 0.22, 1.0),
),
_make_stator_vanes(),
_make_exhaust_cone(),
_make_support_struts(),
_make_mounting_pylon(),
_make_display_base(),
]
return _make_compound(MODEL_LABEL, bodies)
@@ -0,0 +1,79 @@
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_FILLET_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 a single solid open-top electronics enclosure base 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_FILLET_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 = "electronics_enclosure_base"
return part
+426
View File
@@ -0,0 +1,426 @@
from __future__ import annotations
import math
from collections.abc import Sequence
from build123d import (
Box,
Color,
Compound,
Cylinder,
Face,
Location,
Plane,
Polygon,
Shape,
Torus,
Wire,
BuildPart,
BuildSketch,
chamfer,
extrude,
fillet,
loft,
)
from OCP.BRepPrimAPI import BRepPrimAPI_MakeCylinder
from OCP.gp import gp_Ax2, gp_Dir, gp_Pnt
DISPLAY_NAME = "Roadable VTOL flying car concept"
# Units: millimeters.
# Origin: center of the road car cabin footprint.
# +X: forward/nose, +Y: driver-left/port side, +Z: up.
MODEL_LABEL = "roadable_vtol_flying_car_concept"
COLORS = {
"pearl_body": (0.82, 0.86, 0.84, 1.0),
"graphite": (0.03, 0.035, 0.04, 1.0),
"dark_glass": (0.08, 0.16, 0.24, 0.58),
"warm_lens": (1.0, 0.74, 0.28, 1.0),
"red_lens": (0.95, 0.08, 0.05, 1.0),
"titanium": (0.52, 0.56, 0.58, 1.0),
"brushed_aluminum": (0.72, 0.74, 0.73, 1.0),
"rotor_blue": (0.24, 0.42, 0.56, 1.0),
"safety_orange": (1.0, 0.35, 0.06, 1.0),
"rubber": (0.005, 0.006, 0.007, 1.0),
}
Vector3 = tuple[float, float, float]
BODY_STATIONS = [
(-182.0, 48.0, 24.0, 43.0),
(-146.0, 82.0, 39.0, 42.0),
(-78.0, 102.0, 50.0, 42.0),
(20.0, 104.0, 54.0, 43.0),
(105.0, 82.0, 44.0, 43.0),
(162.0, 42.0, 28.0, 43.0),
(183.0, 10.0, 10.0, 43.0),
]
CANOPY_STATIONS = [
(-62.0, 45.0, 10.0, 65.0),
(-22.0, 56.0, 24.0, 72.0),
(32.0, 52.0, 25.0, 72.0),
(74.0, 30.0, 10.0, 65.0),
]
DUCT_OUTER_RADIUS = 48.0
DUCT_INNER_RADIUS = 36.0
DUCT_HEIGHT = 12.0
DUCT_Z = 58.0
DUCT_FRONT_X = 112.0
DUCT_REAR_X = -114.0
DUCT_CENTER_Y = 112.0
ROTOR_Z = DUCT_Z + 0.9
def style(shape: Shape, label: str, color_name: str) -> Shape:
shape.label = label
shape.color = Color(*COLORS[color_name])
return shape
def group(label: str, children: Sequence[Shape], color_name: str | None = None) -> Compound:
compound = Compound(obj=list(children), children=list(children), label=label)
if color_name is not None:
compound.color = Color(*COLORS[color_name])
return compound
def softened(shape: Shape, radius: float) -> Shape:
if radius <= 0.0:
return shape
try:
return fillet(shape.edges(), radius=radius)
except Exception:
try:
return chamfer(shape.edges(), length=radius * 0.35)
except Exception:
return shape
def superellipse_section(
x_pos: float,
width: float,
height: float,
z_center: float,
*,
exponent: float = 2.65,
segments: int = 40,
) -> Face:
points: list[Vector3] = []
half_width = width / 2.0
half_height = height / 2.0
for index in range(segments):
angle = 2.0 * math.pi * index / segments
y_pos = math.copysign(abs(math.cos(angle)) ** (2.0 / exponent), math.cos(angle)) * half_width
z_pos = z_center + math.copysign(abs(math.sin(angle)) ** (2.0 / exponent), math.sin(angle)) * half_height
points.append((x_pos, y_pos, z_pos))
return Face.make_surface(Wire.make_polygon(points, close=True))
def lofted_superellipse(label: str, stations: Sequence[tuple[float, float, float, float]], color_name: str) -> Shape:
faces = [superellipse_section(*station) for station in stations]
return style(softened(loft(faces, ruled=False), 0.75), label, color_name)
def cylinder_x(label: str, center: Vector3, radius: float, length: float, color_name: str) -> Shape:
return style(Cylinder(radius, length, rotation=(0.0, 90.0, 0.0)).moved(Location(center)), label, color_name)
def cylinder_y(label: str, center: Vector3, radius: float, length: float, color_name: str) -> Shape:
return style(Cylinder(radius, length, rotation=(90.0, 0.0, 0.0)).moved(Location(center)), label, color_name)
def cylinder_z(label: str, center: Vector3, radius: float, length: float, color_name: str) -> Shape:
return style(Cylinder(radius, length).moved(Location(center)), label, color_name)
def cylinder_between(label: str, p1: Vector3, p2: Vector3, radius: float, color_name: str) -> Shape:
axis = (p2[0] - p1[0], p2[1] - p1[1], p2[2] - p1[2])
length = math.sqrt(axis[0] ** 2 + axis[1] ** 2 + axis[2] ** 2)
direction = (axis[0] / length, axis[1] / length, axis[2] / length)
wrapped = BRepPrimAPI_MakeCylinder(
gp_Ax2(gp_Pnt(*p1), gp_Dir(*direction)),
radius,
length,
).Shape()
return style(Shape(obj=wrapped), label, color_name)
def annular_z(label: str, center: Vector3, outer_radius: float, inner_radius: float, height: float, color_name: str) -> Shape:
outer = Cylinder(outer_radius, height).moved(Location(center))
inner = Cylinder(inner_radius, height + 3.0).moved(Location(center))
return style(softened(outer - inner, 0.65), label, color_name)
def annular_x(label: str, center: Vector3, outer_radius: float, inner_radius: float, length: float, color_name: str) -> Shape:
outer = Cylinder(outer_radius, length, rotation=(0.0, 90.0, 0.0)).moved(Location(center))
inner = Cylinder(inner_radius, length + 3.0, rotation=(0.0, 90.0, 0.0)).moved(Location(center))
return style(softened(outer - inner, 0.65), label, color_name)
def annular_y(label: str, center: Vector3, outer_radius: float, inner_radius: float, length: float, color_name: str) -> Shape:
outer = Cylinder(outer_radius, length, rotation=(90.0, 0.0, 0.0)).moved(Location(center))
inner = Cylinder(inner_radius, length + 3.0, rotation=(90.0, 0.0, 0.0)).moved(Location(center))
return style(softened(outer - inner, 0.55), label, color_name)
def prism_from_xz(label: str, y_center: float, points_xz: Sequence[tuple[float, float]], thickness: float, color_name: str) -> Shape:
faces = []
for y_pos in (y_center - thickness / 2.0, y_center + thickness / 2.0):
points = [(x_pos, y_pos, z_pos) for x_pos, z_pos in points_xz]
faces.append(Face.make_surface(Wire.make_polygon(points, close=True)))
return style(softened(loft(faces, ruled=True), 0.45), label, color_name)
def prism_from_yz(label: str, x_center: float, points_yz: Sequence[tuple[float, float]], thickness: float, color_name: str) -> Shape:
faces = []
for x_pos in (x_center - thickness / 2.0, x_center + thickness / 2.0):
points = [(x_pos, y_pos, z_pos) for y_pos, z_pos in points_yz]
faces.append(Face.make_surface(Wire.make_polygon(points, close=True)))
return style(softened(loft(faces, ruled=True), 0.35), label, color_name)
def body_shell() -> Compound:
fuselage = lofted_superellipse("one_piece_aero_road_car_fuselage", BODY_STATIONS, "pearl_body")
belly = style(
softened(Box(225.0, 42.0, 8.0).moved(Location((-18.0, 0.0, 18.0))), 1.2),
"flat_structural_battery_spine",
"graphite",
)
canopy = lofted_superellipse("teardrop_panoramic_canopy", CANOPY_STATIONS, "dark_glass")
splitter = style(
softened(Box(42.0, 54.0, 3.6).moved(Location((172.0, 0.0, 32.0))), 0.8),
"front_aero_splitter_under_nose",
"graphite",
)
rear_diffuser = style(
softened(Box(58.0, 68.0, 5.0).moved(Location((-167.0, 0.0, 27.0))), 0.8),
"rear_diffuser_plane",
"graphite",
)
lights = [
cylinder_x("left_warm_headlamp", (184.0, 17.5, 43.5), 4.2, 3.4, "warm_lens"),
cylinder_x("right_warm_headlamp", (184.0, -17.5, 43.5), 4.2, 3.4, "warm_lens"),
cylinder_x("left_rear_marker_lamp", (-184.0, 28.0, 42.0), 3.2, 3.4, "red_lens"),
cylinder_x("right_rear_marker_lamp", (-184.0, -28.0, 42.0), 3.2, 3.4, "red_lens"),
]
return group(
"aero_car_body",
[fuselage, belly, canopy, splitter, rear_diffuser, *lights],
"pearl_body",
)
def wing_panel(label: str, side: float) -> Shape:
base_points = [
(74.0, 43.0),
(96.0, 138.0),
(-92.0, 128.0),
(-66.0, 43.0),
]
points = [(x_pos, side * y_pos) for x_pos, y_pos in base_points]
if side < 0:
points = list(reversed(points))
with BuildPart() as wing:
with BuildSketch(Plane.XY):
Polygon(points, align=None)
extrude(amount=5.2)
panel = wing.part.moved(Location((0.0, 0.0, 51.5)))
return style(softened(panel, 0.7), label, "brushed_aluminum")
def stabilizers() -> Compound:
fin_profile = [(-174.0, 54.0), (-129.0, 55.0), (-114.0, 108.0), (-166.0, 91.0)]
return group(
"twin_rear_vertical_stabilizers",
[
prism_from_xz("port_vertical_tail_fin", 36.0, fin_profile, 5.0, "safety_orange"),
prism_from_xz("starboard_vertical_tail_fin", -36.0, fin_profile, 5.0, "safety_orange"),
],
"safety_orange",
)
def lifting_wings() -> Compound:
center_fairing = style(
softened(Box(120.0, 96.0, 7.0).moved(Location((-2.0, 0.0, 53.0))), 1.0),
"flush_wing_centerbox_between_ducts",
"brushed_aluminum",
)
return group(
"folding_lifting_wing_set",
[
center_fairing,
wing_panel("port_deployable_lifting_wing", 1.0),
wing_panel("starboard_deployable_lifting_wing", -1.0),
stabilizers(),
],
"brushed_aluminum",
)
def rotor_blade_z(label: str, center: Vector3, angle_deg: float, color_name: str) -> Shape:
root_radius = 9.0
tip_radius = DUCT_INNER_RADIUS - 4.0
root_width = 9.0
tip_width = 4.4
angle = math.radians(angle_deg)
radial = (math.cos(angle), math.sin(angle))
tangent = (-math.sin(angle), math.cos(angle))
corners = []
for distance, half_width in (
(root_radius, root_width / 2.0),
(tip_radius, tip_width / 2.0),
(tip_radius, -tip_width / 2.0),
(root_radius, -root_width / 2.0),
):
corners.append(
(
center[0] + radial[0] * distance + tangent[0] * half_width,
center[1] + radial[1] * distance + tangent[1] * half_width,
)
)
with BuildPart() as blade:
with BuildSketch(Plane.XY):
Polygon(corners, align=None)
extrude(amount=1.8)
return style(blade.part.moved(Location((0.0, 0.0, center[2] - 0.9))), label, color_name)
def lift_duct_module(label: str, center: Vector3, front: bool, side: float) -> Compound:
duct_label = "front" if front else "rear"
side_label = "port" if side > 0 else "starboard"
parts: list[Shape] = [
annular_z(f"{side_label}_{duct_label}_protective_lift_duct_ring", center, DUCT_OUTER_RADIUS, DUCT_INNER_RADIUS, DUCT_HEIGHT, "graphite"),
style(
Torus(DUCT_OUTER_RADIUS - 2.4, 1.3).moved(Location((center[0], center[1], center[2] + DUCT_HEIGHT / 2.0))),
f"{side_label}_{duct_label}_upper_guard_lip",
"titanium",
),
cylinder_z(f"{side_label}_{duct_label}_rotor_hub", (center[0], center[1], ROTOR_Z), 8.5, 6.0, "titanium"),
]
for index in range(6):
parts.append(
rotor_blade_z(
f"{side_label}_{duct_label}_lift_rotor_blade_{index + 1}",
(center[0], center[1], ROTOR_Z + 1.6),
index * 60.0 + (15.0 if front else -15.0),
"rotor_blue",
)
)
for index, angle_deg in enumerate((0.0, 90.0, 180.0, 270.0), start=1):
angle = math.radians(angle_deg)
p1 = (center[0], center[1], center[2])
p2 = (
center[0] + math.cos(angle) * (DUCT_INNER_RADIUS - 2.0),
center[1] + math.sin(angle) * (DUCT_INNER_RADIUS - 2.0),
center[2],
)
parts.append(cylinder_between(f"{side_label}_{duct_label}_duct_cross_strut_{index}", p1, p2, 1.8, "titanium"))
body_attach = (center[0] + (-31.0 if front else 31.0), side * 50.0, center[2] - 3.0)
parts.append(cylinder_between(f"{side_label}_{duct_label}_faired_duct_boom", body_attach, center, 4.8, "brushed_aluminum"))
return group(label, parts, "graphite")
def lift_system() -> Compound:
duct_centers = [
("port_front_lift_fan_module", (DUCT_FRONT_X, DUCT_CENTER_Y, DUCT_Z), True, 1.0),
("starboard_front_lift_fan_module", (DUCT_FRONT_X, -DUCT_CENTER_Y, DUCT_Z), True, -1.0),
("port_rear_lift_fan_module", (DUCT_REAR_X, DUCT_CENTER_Y, DUCT_Z), False, 1.0),
("starboard_rear_lift_fan_module", (DUCT_REAR_X, -DUCT_CENTER_Y, DUCT_Z), False, -1.0),
]
return group(
"quad_tilt_lift_duct_system",
[lift_duct_module(label, center, front, side) for label, center, front, side in duct_centers],
"graphite",
)
def rear_blade_x(label: str, center: Vector3, angle_deg: float) -> Shape:
root_radius = 7.0
tip_radius = 20.5
root_width = 6.0
tip_width = 2.8
angle = math.radians(angle_deg)
radial = (math.cos(angle), math.sin(angle))
tangent = (-math.sin(angle), math.cos(angle))
points: list[tuple[float, float]] = []
for distance, half_width in (
(root_radius, root_width / 2.0),
(tip_radius, tip_width / 2.0),
(tip_radius, -tip_width / 2.0),
(root_radius, -root_width / 2.0),
):
y_pos = center[1] + radial[0] * distance + tangent[0] * half_width
z_pos = center[2] + radial[1] * distance + tangent[1] * half_width
points.append((y_pos, z_pos))
return prism_from_yz(label, center[0], points, 1.8, "rotor_blue")
def rear_thruster() -> Compound:
center = (-201.0, 0.0, 56.0)
parts: list[Shape] = [
annular_x("rear_cruise_thrust_duct", center, 27.0, 20.5, 28.0, "graphite"),
cylinder_x("rear_cruise_rotor_hub", center, 6.0, 16.0, "titanium"),
cylinder_between("upper_rear_thruster_pylon", (-174.0, 0.0, 61.0), (-190.0, 0.0, 72.0), 3.4, "brushed_aluminum"),
cylinder_between("lower_rear_thruster_pylon", (-174.0, 0.0, 39.0), (-190.0, 0.0, 42.0), 3.0, "brushed_aluminum"),
]
for index in range(7):
parts.append(rear_blade_x(f"rear_cruise_propulsor_blade_{index + 1}", center, index * 360.0 / 7.0 + 8.0))
return group("rear_cruise_propulsor", parts, "graphite")
def wheel_module(label: str, x_pos: float, side: float) -> Compound:
y_pos = side * 61.0
wheel_center = (x_pos, y_pos, 20.0)
side_label = "port" if side > 0 else "starboard"
axle_end = (x_pos, side * 45.0, 20.0)
fairing = style(
softened(Box(37.0, 17.0, 14.0).moved(Location((x_pos, side * 52.0, 31.0))), 2.0),
f"{side_label}_{label}_flush_retractable_wheel_fairing",
"pearl_body",
)
parts = [
fairing,
cylinder_between(f"{side_label}_{label}_short_axle", axle_end, wheel_center, 3.4, "titanium"),
annular_y(f"{side_label}_{label}_low_profile_tire", wheel_center, 20.0, 11.0, 12.0, "rubber"),
cylinder_y(f"{side_label}_{label}_silver_wheel_hub", wheel_center, 7.0, 15.0, "brushed_aluminum"),
]
return group(f"{side_label}_{label}_road_wheel_module", parts, "rubber")
def road_gear() -> Compound:
return group(
"retractable_road_wheel_set",
[
wheel_module("front", 108.0, 1.0),
wheel_module("front", 108.0, -1.0),
wheel_module("rear", -105.0, 1.0),
wheel_module("rear", -105.0, -1.0),
],
"rubber",
)
def gen_step():
return group(
MODEL_LABEL,
[
body_shell(),
lifting_wings(),
lift_system(),
rear_thruster(),
road_gear(),
],
)
@@ -0,0 +1,552 @@
from __future__ import annotations
import math
from collections.abc import Iterable, Sequence
from build123d import *
from build123d import Shape
from OCP.BRepBuilderAPI import BRepBuilderAPI_Transform
from OCP.BRepPrimAPI import BRepPrimAPI_MakeCylinder
from OCP.gp import gp_Ax2, gp_Dir, gp_Pnt, gp_Trsf
DISPLAY_NAME = "Futuristic lunar rover wheel suspension corner"
# Coordinate convention:
# - millimeters
# - wheel axis is global Y
# - visible outside wheel face points toward negative Y
# - wheel center is fixed at X=0, Y=0, Z=80
WHEEL_CENTER = (0.0, 0.0, 80.0)
BASE_CENTER_X = -20.0
BASE_TOP_Z = 8.0
COLORS = {
"dark_graphite": (0.005, 0.006, 0.007, 1.0),
"matte_black": (0.002, 0.002, 0.002, 1.0),
"satin_titanium": (0.42, 0.43, 0.40, 1.0),
"pale_aluminum": (0.78, 0.80, 0.78, 1.0),
"brushed_steel": (0.50, 0.52, 0.54, 1.0),
"anodized_orange": (1.00, 0.34, 0.08, 1.0),
"spring_orange": (1.00, 0.42, 0.02, 1.0),
"brushed_aluminum": (0.66, 0.68, 0.66, 1.0),
"fastener_steel": (0.18, 0.19, 0.20, 1.0),
}
Vector3 = tuple[float, float, float]
def v_add(a: Vector3, b: Vector3) -> Vector3:
return (a[0] + b[0], a[1] + b[1], a[2] + b[2])
def v_sub(a: Vector3, b: Vector3) -> Vector3:
return (a[0] - b[0], a[1] - b[1], a[2] - b[2])
def v_mul(a: Vector3, scalar: float) -> Vector3:
return (a[0] * scalar, a[1] * scalar, a[2] * scalar)
def v_dot(a: Vector3, b: Vector3) -> float:
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
def v_cross(a: Vector3, b: Vector3) -> Vector3:
return (
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
)
def v_len(a: Vector3) -> float:
return math.sqrt(v_dot(a, a))
def v_norm(a: Vector3) -> Vector3:
length = v_len(a)
if length <= 1e-9:
raise ValueError("zero-length vector")
return (a[0] / length, a[1] / length, a[2] / length)
def style(shape: Shape, label: str, color_name: str) -> Shape:
shape.label = label
shape.color = Color(*COLORS[color_name])
return shape
def group(label: str, children: Sequence[Shape], color_name: str | None = None) -> Compound:
compound = Compound(obj=list(children), children=list(children), label=label)
if color_name is not None:
compound.color = Color(*COLORS[color_name])
return compound
def transformed(shape: Shape, center: Vector3, axis_x: Vector3, axis_y: Vector3, axis_z: Vector3) -> Shape:
x_dir = v_norm(axis_x)
y_dir = v_norm(axis_y)
z_dir = v_norm(axis_z)
trsf = gp_Trsf()
trsf.SetValues(
x_dir[0],
y_dir[0],
z_dir[0],
center[0],
x_dir[1],
y_dir[1],
z_dir[1],
center[1],
x_dir[2],
y_dir[2],
z_dir[2],
center[2],
)
return Shape(obj=BRepBuilderAPI_Transform(shape.wrapped, trsf, True).Shape())
def oriented_box(
label: str,
center: Vector3,
size_x: float,
size_y: float,
size_z: float,
axis_x: Vector3 = (1.0, 0.0, 0.0),
axis_y: Vector3 = (0.0, 1.0, 0.0),
axis_z: Vector3 = (0.0, 0.0, 1.0),
color_name: str = "pale_aluminum",
fillet_radius: float = 0.0,
) -> Shape:
shape = transformed(Box(size_x, size_y, size_z), center, axis_x, axis_y, axis_z)
if fillet_radius > 0.0:
shape = softened(shape, fillet_radius)
return style(shape, label, color_name)
def cylinder_between(label: str, p1: Vector3, p2: Vector3, radius: float, color_name: str) -> Shape:
axis = v_sub(p2, p1)
length = v_len(axis)
if length <= 1e-6:
return style(Sphere(radius).moved(Location(p1)), label, color_name)
direction = v_norm(axis)
wrapped = BRepPrimAPI_MakeCylinder(
gp_Ax2(gp_Pnt(*p1), gp_Dir(*direction)),
radius,
length,
).Shape()
return style(Shape(obj=wrapped), label, color_name)
def cylinder_x(label: str, center: Vector3, radius: float, length: float, color_name: str) -> Shape:
return style(Cylinder(radius, length, rotation=(0, 90, 0)).moved(Location(center)), label, color_name)
def cylinder_y(label: str, center: Vector3, radius: float, length: float, color_name: str) -> Shape:
return style(Cylinder(radius, length, rotation=(90, 0, 0)).moved(Location(center)), label, color_name)
def cylinder_z(label: str, center: Vector3, radius: float, length: float, color_name: str) -> Shape:
return style(Cylinder(radius, length).moved(Location(center)), label, color_name)
def softened(shape: Shape, radius: float) -> Shape:
# Small repeated pads, spokes, tubes, and bolts dominate export time if every
# edge receives a true BREP fillet. Major machined bodies still get rounded
# edges below; small bodies rely on cylindrical profiles and crisp bevel-like
# proportions to keep the STEP robust.
if radius < 0.95:
return shape
try:
return fillet(shape.edges(), radius=radius)
except Exception:
try:
return chamfer(shape.edges(), length=radius * 0.35)
except Exception:
return shape
def annular_y(label: str, center: Vector3, outer_radius: float, inner_radius: float, length: float, color_name: str) -> Shape:
outer = Cylinder(outer_radius, length, rotation=(90, 0, 0)).moved(Location(center))
inner = Cylinder(inner_radius, length + 4.0, rotation=(90, 0, 0)).moved(Location(center))
return style(softened(outer - inner, 1.2), label, color_name)
def wheel_lattice() -> Compound:
center = WHEEL_CENTER
parts: list[Shape] = [
annular_y("wheel_outer_rim_ring", center, 65.0, 56.0, 42.0, "dark_graphite"),
annular_y("wheel_inner_rim_ring", center, 38.0, 25.0, 40.0, "dark_graphite"),
style(Torus(65.0, 1.8, rotation=(90, 0, 0)).moved(Location((0.0, -21.4, 80.0))), "wheel_outer_bead_visible", "dark_graphite"),
style(Torus(65.0, 1.8, rotation=(90, 0, 0)).moved(Location((0.0, 21.4, 80.0))), "wheel_outer_bead_inner", "dark_graphite"),
]
for index in range(16):
theta_inner = (2.0 * math.pi * index) / 16.0
theta_outer = theta_inner + math.radians(5.0)
p1 = (
math.cos(theta_inner) * 19.0,
-13.5,
80.0 + math.sin(theta_inner) * 19.0,
)
p2 = (
math.cos(theta_outer) * 56.0,
13.5,
80.0 + math.sin(theta_outer) * 56.0,
)
spoke = cylinder_between(f"wheel_angular_spoke_{index + 1:02d}", p1, p2, 2.8, "dark_graphite")
parts.append(style(softened(spoke, 0.6), spoke.label or f"wheel_angular_spoke_{index + 1:02d}", "dark_graphite"))
return group("wheel", parts, "dark_graphite")
def tread_elements() -> Compound:
parts: list[Shape] = []
rim_radius = 65.0
tread_height = 5.0
# The diagonal chevron bars have tangent-wise corners, so the pad center is
# tucked slightly below the nominal 65 mm rim surface to keep the full tread
# envelope close to the requested 140 mm outside diameter.
radial_center = rim_radius + tread_height / 2.0 - 1.1
long_size = math.hypot(18.0, 10.0)
for index in range(32):
theta = (2.0 * math.pi * index) / 32.0
normal = (math.cos(theta), 0.0, math.sin(theta))
tangent = (-math.sin(theta), 0.0, math.cos(theta))
chevron_sign = 1.0 if index % 2 == 0 else -1.0
for side_name, y_sign in (("outside", -1.0), ("inside", 1.0)):
axis_x = v_norm(v_add(v_mul((0.0, 1.0, 0.0), y_sign * 18.0), v_mul(tangent, chevron_sign * 10.0)))
axis_z = normal
axis_y = v_norm(v_cross(axis_z, axis_x))
center = v_add(
WHEEL_CENTER,
v_add(
v_mul(normal, radial_center),
v_add(v_mul((0.0, 1.0, 0.0), y_sign * 9.0), v_mul(tangent, chevron_sign * 5.0)),
),
)
pad = oriented_box(
f"tread_chevron_{index + 1:02d}_{side_name}",
center,
long_size,
5.8,
tread_height,
axis_x,
axis_y,
axis_z,
"matte_black",
fillet_radius=0.75,
)
parts.append(pad)
return group("tire_tread_elements", parts, "matte_black")
def hub_and_axle() -> tuple[Shape, Compound]:
hub = cylinder_y("central_hub", WHEEL_CENTER, 17.0, 50.0, "pale_aluminum")
hub = style(softened(hub, 1.2), "central_hub", "pale_aluminum")
axle_parts: list[Shape] = [
cylinder_y("axle_stub", (0.0, 8.0, 80.0), 8.0, 78.0, "fastener_steel"),
cylinder_y("visible_axle_cap", (0.0, -29.0, 80.0), 12.0, 6.0, "fastener_steel"),
]
axle_parts = [style(softened(part, 0.6), part.label or "axle_part", "fastener_steel") for part in axle_parts]
return hub, group("axle_hardware", axle_parts, "fastener_steel")
def brake_disc() -> Shape:
disc = Cylinder(41.0, 5.0, rotation=(90, 0, 0)).moved(Location((0.0, 29.0, 80.0)))
for index in range(24):
theta = (2.0 * math.pi * index) / 24.0
hole_center = (
math.cos(theta) * 31.0,
29.0,
80.0 + math.sin(theta) * 31.0,
)
disc = disc - Cylinder(2.2, 8.0, rotation=(90, 0, 0)).moved(Location(hole_center))
disc = softened(disc, 0.4)
return style(disc, "brake_disc_82mm_vented", "brushed_steel")
def brake_caliper() -> Compound:
angle = math.radians(42.0)
radial = (math.cos(angle), 0.0, math.sin(angle))
tangent = (-math.sin(angle), 0.0, math.cos(angle))
base = v_add(WHEEL_CENTER, v_mul(radial, 45.0))
parts = [
oriented_box(
"brake_caliper_outer_half",
v_add(base, (0.0, 20.8, 0.0)),
30.0,
7.0,
17.0,
tangent,
(0.0, 1.0, 0.0),
radial,
"anodized_orange",
fillet_radius=2.0,
),
oriented_box(
"brake_caliper_inner_half",
v_add(base, (0.0, 37.2, 0.0)),
30.0,
7.0,
17.0,
tangent,
(0.0, 1.0, 0.0),
radial,
"anodized_orange",
fillet_radius=2.0,
),
oriented_box(
"brake_caliper_bridge",
v_add(WHEEL_CENTER, v_add(v_mul(radial, 53.0), (0.0, 29.0, 0.0))),
28.0,
18.0,
5.5,
tangent,
(0.0, 1.0, 0.0),
radial,
"anodized_orange",
fillet_radius=1.3,
),
]
return group("brake_caliper", parts, "anodized_orange")
def upright() -> Compound:
parts: list[Shape] = [
oriented_box("upright_machined_web", (-18.0, 40.0, 80.0), 15.0, 18.0, 72.0, color_name="pale_aluminum", fillet_radius=1.5),
oriented_box("upright_upper_fork_lug", (-10.0, 40.0, 111.0), 30.0, 20.0, 17.0, color_name="pale_aluminum", fillet_radius=1.5),
oriented_box("upright_lower_fork_lug", (-10.0, 40.0, 49.0), 30.0, 20.0, 17.0, color_name="pale_aluminum", fillet_radius=1.5),
annular_y("upright_central_bearing", (0.0, 40.0, 80.0), 19.0, 9.0, 20.0, "pale_aluminum"),
cylinder_y("upright_upper_bushing", (-21.0, 40.0, 111.0), 6.5, 24.0, "satin_titanium"),
cylinder_y("upright_lower_bushing", (-23.0, 40.0, 49.0), 6.5, 24.0, "satin_titanium"),
]
return group("upright", parts, "pale_aluminum")
def bushing(label: str, center: Vector3, length: float = 15.0) -> Shape:
return style(softened(cylinder_y(label, center, 6.2, length, "satin_titanium"), 0.5), label, "satin_titanium")
def control_arms() -> tuple[Compound, Compound]:
upper_upright = (-21.0, 40.0, 111.0)
upper_front = (-94.0, -16.0, 118.0)
upper_rear = (-94.0, 52.0, 118.0)
lower_upright = (-23.0, 40.0, 49.0)
lower_front = (-94.0, -20.0, 45.0)
lower_rear = (-94.0, 52.0, 45.0)
upper_parts = [
cylinder_between("upper_control_arm_front_tube", upper_front, upper_upright, 3.5, "satin_titanium"),
cylinder_between("upper_control_arm_rear_tube", upper_rear, upper_upright, 3.5, "satin_titanium"),
cylinder_between("upper_control_arm_cross_tube", upper_front, upper_rear, 2.8, "satin_titanium"),
bushing("upper_front_chassis_bushing", upper_front),
bushing("upper_rear_chassis_bushing", upper_rear),
bushing("upper_upright_outer_bushing", upper_upright, 18.0),
]
upper_parts = [
style(softened(part, 0.45), part.label or "upper_control_arm_part", "satin_titanium")
if "tube" in (part.label or "")
else part
for part in upper_parts
]
lower_parts = [
cylinder_between("lower_control_arm_front_tube", lower_front, lower_upright, 3.5, "satin_titanium"),
cylinder_between("lower_control_arm_rear_tube", lower_rear, lower_upright, 3.5, "satin_titanium"),
cylinder_between("lower_control_arm_cross_tube", lower_front, lower_rear, 2.8, "satin_titanium"),
bushing("lower_front_chassis_bushing", lower_front),
bushing("lower_rear_chassis_bushing", lower_rear),
bushing("lower_upright_outer_bushing", lower_upright, 18.0),
]
lower_parts = [
style(softened(part, 0.45), part.label or "lower_control_arm_part", "satin_titanium")
if "tube" in (part.label or "")
else part
for part in lower_parts
]
return (
group("upper_control_arm", upper_parts, "satin_titanium"),
group("lower_control_arm", lower_parts, "satin_titanium"),
)
def spring_along_axis(label: str, start: Vector3, end: Vector3) -> Compound:
axis = v_sub(end, start)
length = v_len(axis)
axis_z = v_norm(axis)
fallback = (0.0, 0.0, 1.0)
if abs(v_dot(axis_z, fallback)) > 0.92:
fallback = (1.0, 0.0, 0.0)
axis_x = v_norm(v_cross(fallback, axis_z))
axis_y = v_norm(v_cross(axis_z, axis_x))
wire_radius = 1.5
helix_radius = 10.5
turns = 8
segments_per_turn = 8
total_segments = turns * segments_per_turn
def helix_point(step: int) -> Vector3:
angle = 2.0 * math.pi * turns * step / total_segments
local = (
math.cos(angle) * helix_radius,
math.sin(angle) * helix_radius,
length * step / total_segments,
)
return v_add(
start,
v_add(
v_mul(axis_x, local[0]),
v_add(v_mul(axis_y, local[1]), v_mul(axis_z, local[2])),
),
)
segments: list[Shape] = []
previous = helix_point(0)
for index in range(1, total_segments + 1):
current = helix_point(index)
segments.append(
cylinder_between(
f"{label}_segment_{index:02d}",
previous,
current,
wire_radius,
"spring_orange",
)
)
previous = current
return group(label, segments, "spring_orange")
def coilover_shock() -> Compound:
top = (-90.0, -8.0, 118.0)
bottom = (-48.0, -24.0, 58.0)
axis = v_norm(v_sub(top, bottom))
length = v_len(v_sub(top, bottom))
damper_start = v_add(bottom, v_mul(axis, 9.0))
damper_end = v_add(bottom, v_mul(axis, length * 0.62))
rod_start = v_add(bottom, v_mul(axis, length * 0.52))
rod_end = v_add(top, v_mul(axis, -7.0))
spring_start = v_add(bottom, v_mul(axis, 7.0))
spring_end = v_add(top, v_mul(axis, -7.0))
parts = [
cylinder_between("coilover_damper_body", damper_start, damper_end, 5.0, "brushed_steel"),
cylinder_between("coilover_piston_rod", rod_start, rod_end, 2.5, "fastener_steel"),
spring_along_axis("coilover_helical_spring_8_turns", spring_start, spring_end),
cylinder_y("coilover_top_eyelet", top, 6.0, 14.0, "satin_titanium"),
cylinder_y("coilover_lower_eyelet", bottom, 6.0, 14.0, "satin_titanium"),
]
return group("coilover_shock", parts, "satin_titanium")
def triangular_cutout_tool(label: str, x_center: float, vertices_yz: Sequence[tuple[float, float]]) -> Shape:
plane = Plane(origin=(x_center, 0.0, 0.0), x_dir=(0.0, 1.0, 0.0), z_dir=(1.0, 0.0, 0.0))
with BuildPart() as tool:
with BuildSketch(plane):
Polygon(*vertices_yz)
extrude(amount=13.0, both=True)
tool.part.label = label
return tool.part
def chassis_bulkhead() -> tuple[Shape, Compound]:
plate = Box(8.0, 70.0, 95.0).moved(Location((-95.0, 20.0, 80.0)))
cutouts = [
triangular_cutout_tool("bulkhead_upper_triangular_cutout", -95.0, [(3.0, 86.0), (37.0, 86.0), (20.0, 117.0)]),
triangular_cutout_tool("bulkhead_lower_triangular_cutout", -95.0, [(3.0, 74.0), (37.0, 74.0), (20.0, 43.0)]),
]
for tool in cutouts:
plate = plate - tool
for y, z in [(3.0, 86.0), (37.0, 86.0), (20.0, 117.0), (3.0, 74.0), (37.0, 74.0), (20.0, 43.0)]:
plate = plate - cylinder_x("bulkhead_cutout_corner_radius_tool", (-95.0, y, z), 5.0, 13.0, "pale_aluminum")
plate = style(softened(plate, 1.0), "chassis_bulkhead", "pale_aluminum")
supports = group(
"bulkhead_display_support_blocks",
[
oriented_box("bulkhead_left_support_block", (-95.0, -8.0, 20.25), 17.0, 12.0, 24.5, color_name="brushed_aluminum", fillet_radius=1.0),
oriented_box("bulkhead_right_support_block", (-95.0, 48.0, 20.25), 17.0, 12.0, 24.5, color_name="brushed_aluminum", fillet_radius=1.0),
],
"brushed_aluminum",
)
return plate, supports
def countersunk_display_base() -> Shape:
with BuildPart() as part:
with BuildSketch():
RectangleRounded(170.0, 110.0, 6.0)
extrude(amount=8.0)
base = part.part.moved(Location((BASE_CENTER_X, 0.0, 0.0)))
hole_positions = [
(BASE_CENTER_X - 70.0, -43.0),
(BASE_CENTER_X + 70.0, -43.0),
(BASE_CENTER_X - 70.0, 43.0),
(BASE_CENTER_X + 70.0, 43.0),
]
for x, y in hole_positions:
base = base - Cylinder(3.2, 12.0).moved(Location((x, y, 4.0)))
base = base - Cone(3.2, 6.4, 5.0).moved(Location((x, y, 6.3)))
base = softened(base, 1.1)
return style(base, "display_base_rounded_countersunk", "brushed_aluminum")
def fasteners() -> Compound:
parts: list[Shape] = []
for index in range(6):
theta = (2.0 * math.pi * index) / 6.0
center = (math.cos(theta) * 22.0, -28.5, 80.0 + math.sin(theta) * 22.0)
parts.append(style(softened(cylinder_y(f"hub_bolt_{index + 1:02d}", center, 2.6, 5.0, "fastener_steel"), 0.35), f"hub_bolt_{index + 1:02d}", "fastener_steel"))
caliper_bolt_centers = [
v_add(WHEEL_CENTER, (23.0, 17.0, 44.0)),
v_add(WHEEL_CENTER, (43.0, 17.0, 24.0)),
]
for index, center in enumerate(caliper_bolt_centers, start=1):
parts.append(style(softened(cylinder_y(f"caliper_socket_bolt_{index}", center, 2.2, 4.0, "fastener_steel"), 0.3), f"caliper_socket_bolt_{index}", "fastener_steel"))
for index, (y, z) in enumerate(
[
(-16.0, 118.0),
(52.0, 118.0),
(-20.0, 45.0),
(52.0, 45.0),
(-8.0, 34.0),
(48.0, 34.0),
(-8.0, 126.0),
(48.0, 126.0),
],
start=1,
):
parts.append(style(softened(cylinder_x(f"bulkhead_bolt_head_{index:02d}", (-89.6, y, z), 2.8, 3.2, "fastener_steel"), 0.25), f"bulkhead_bolt_head_{index:02d}", "fastener_steel"))
return group("fasteners", parts, "fastener_steel")
def gen_step():
upper_arm, lower_arm = control_arms()
chassis_plate, support_blocks = chassis_bulkhead()
hub, axle = hub_and_axle()
children: list[Shape] = [
countersunk_display_base(),
support_blocks,
wheel_lattice(),
tread_elements(),
hub,
axle,
brake_disc(),
brake_caliper(),
upright(),
upper_arm,
lower_arm,
coilover_shock(),
chassis_plate,
fasteners(),
]
return group("lunar_rover_wheel_suspension_corner", children)
@@ -0,0 +1,546 @@
from __future__ import annotations
from math import cos, degrees, radians, sin, tau
from typing import Iterable
from build123d import (
Align,
Box,
BuildPart,
BuildSketch,
Color,
Compound,
Cone,
Cylinder,
Location,
Locations,
Mode,
Plane,
Polygon,
RegularPolygon,
chamfer,
extrude,
fillet,
)
# Units: millimeters.
# Origin: center of the aperture on the Z axis.
# XY: ring/blade plane viewed from above. +Z: stack direction upward.
MODEL_LABEL = "mechanical_iris_aperture_assembly"
BRUSHED_STEEL = Color(0.64, 0.67, 0.68, 1.0)
STEEL_HIGHLIGHT = Color(0.76, 0.78, 0.78, 1.0)
GRAPHITE = Color(0.0, 0.0, 0.0, 1.0)
BRASS = Color(0.95, 0.68, 0.25, 1.0)
GUNMETAL = Color(0.13, 0.15, 0.17, 1.0)
DARK_GRAY = Color(0.06, 0.065, 0.07, 1.0)
APERTURE_BLACK = Color(0.0, 0.0, 0.0, 1.0)
BASE_OD = 160.0
BASE_ID = 52.0
BASE_THICKNESS = 8.0
BASE_TOP_Z = BASE_THICKNESS
BASE_MOUNT_HOLE_COUNT = 6
BASE_MOUNT_HOLE_DIAMETER = 5.0
BASE_MOUNT_BCD = 130.0
BASE_COUNTERSINK_TOP_DIAMETER = 10.5
BASE_COUNTERSINK_DEPTH = 2.4
RETAINING_ROOT_OD = 150.0
RETAINING_TOOTH_TIP_OD = 154.0
RETAINING_ID = 70.0
RETAINING_Z_BOTTOM = 8.0
RETAINING_Z_TOP = 13.0
RETAINING_HEIGHT = RETAINING_Z_TOP - RETAINING_Z_BOTTOM
RETAINING_TEETH = 120
BLADE_COUNT = 12
BLADE_THICKNESS = 1.2
BLADE_Z_BOTTOM = 13.05
BLADE_Z_STAGGER = 0.15
BLADE_INNER_NOSE_RADIUS = 18.0
BLADE_NOMINAL_INNER_RADIUS = 24.0
BLADE_OUTER_RADIUS = 68.0
BLADE_SPAN_DEGREES = 38.0
BLADE_PIVOT_RADIUS = 62.0
BLADE_PIVOT_LOCAL_ANGLE = radians(13.0)
BLADE_PIVOT_HOLE_DIAMETER = 3.0
BLADE_DRIVE_PIN_RADIUS = 46.0
BLADE_DRIVE_PIN_LOCAL_ANGLE = radians(-8.0)
ACTUATING_OD = 138.0
ACTUATING_ID = 86.0
ACTUATING_Z_BOTTOM = 14.0
ACTUATING_Z_TOP = 17.0
ACTUATING_HEIGHT = ACTUATING_Z_TOP - ACTUATING_Z_BOTTOM
CAM_SLOT_RADIUS = BLADE_DRIVE_PIN_RADIUS
CAM_SLOT_LENGTH = 18.0
CAM_SLOT_WIDTH = 4.0
FRONT_SCREW_COUNT = 6
FRONT_SCREW_BCD = 120.0
FRONT_SCREW_HEAD_DIAMETER = 7.0
FRONT_SCREW_HEAD_HEIGHT = 3.0
def _polar_point(radius: float, angle: float) -> tuple[float, float]:
return (radius * cos(angle), radius * sin(angle))
def _offset_point(
radius: float,
angle: float,
tangent_offset: float,
) -> tuple[float, float]:
radial_x, radial_y = cos(angle), sin(angle)
tangent_x, tangent_y = -sin(angle), cos(angle)
return (
radius * radial_x + tangent_offset * tangent_x,
radius * radial_y + tangent_offset * tangent_y,
)
def _set_metadata(part, label: str, color: Color):
part.label = label
part.color = color
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 _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 _circular_edges(
part,
*,
radius: float | None = None,
coordinate: float | None = None,
axis: str = "z",
tolerance: float = 0.06,
):
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) > tolerance:
continue
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]
if abs(coord - coordinate) > tolerance:
continue
edges.append(edge)
return edges
def _edges_at_z(part, z_value: float, *, tolerance: float = 0.04):
edges = []
for edge in part.edges():
bbox = edge.bounding_box()
if abs(bbox.min.Z - z_value) <= tolerance and abs(bbox.max.Z - z_value) <= tolerance:
edges.append(edge)
return edges
def _tooth_profile(
*,
teeth: int,
root_radius: float,
tip_radius: float,
phase: float = 0.0,
root_span_fraction: float = 0.72,
tip_span_fraction: float = 0.34,
) -> 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 _tick_rectangle_points(angle: float, inner_radius: float, outer_radius: float, width: float):
tangent_x, tangent_y = -sin(angle), cos(angle)
p1 = _polar_point(inner_radius, angle)
p2 = _polar_point(outer_radius, angle)
half_width = width / 2.0
return [
(p1[0] - tangent_x * half_width, p1[1] - tangent_y * half_width),
(p2[0] - tangent_x * half_width, p2[1] - tangent_y * half_width),
(p2[0] + tangent_x * half_width, p2[1] + tangent_y * half_width),
(p1[0] + tangent_x * half_width, p1[1] + tangent_y * half_width),
]
def _make_base_ring():
with BuildPart() as base:
Cylinder(
radius=BASE_OD / 2.0,
height=BASE_THICKNESS,
align=(Align.CENTER, Align.CENTER, Align.MIN),
)
with Locations(Location((0.0, 0.0, -0.75))):
Cylinder(
radius=BASE_ID / 2.0,
height=BASE_THICKNESS + 1.5,
align=(Align.CENTER, Align.CENTER, Align.MIN),
mode=Mode.SUBTRACT,
)
for index in range(BASE_MOUNT_HOLE_COUNT):
x_pos, y_pos = _polar_point(BASE_MOUNT_BCD / 2.0, tau * index / BASE_MOUNT_HOLE_COUNT)
with Locations(Location((x_pos, y_pos, -0.75))):
Cylinder(
radius=BASE_MOUNT_HOLE_DIAMETER / 2.0,
height=BASE_THICKNESS + 1.5,
align=(Align.CENTER, Align.CENTER, Align.MIN),
mode=Mode.SUBTRACT,
)
with Locations(Location((x_pos, y_pos, BASE_TOP_Z - BASE_COUNTERSINK_DEPTH))):
Cone(
bottom_radius=BASE_MOUNT_HOLE_DIAMETER / 2.0,
top_radius=BASE_COUNTERSINK_TOP_DIAMETER / 2.0,
height=BASE_COUNTERSINK_DEPTH + 0.2,
align=(Align.CENTER, Align.CENTER, Align.MIN),
mode=Mode.SUBTRACT,
)
with BuildSketch(Plane.XY.offset(BASE_TOP_Z - 0.22)):
for index in range(72):
angle = tau * index / 72
major = index % 6 == 0
inner_radius = 76.0 if major else 77.2
outer_radius = 79.35
width = 0.72 if major else 0.44
Polygon(_tick_rectangle_points(angle, inner_radius, outer_radius, width), align=None)
extrude(amount=0.36, mode=Mode.SUBTRACT)
part = base.part
chamfer_edges = []
for radius in (BASE_OD / 2.0, BASE_ID / 2.0):
chamfer_edges.extend(_circular_edges(part, radius=radius, coordinate=0.0))
chamfer_edges.extend(_circular_edges(part, radius=radius, coordinate=BASE_THICKNESS))
part = _safe_chamfer(part, chamfer_edges, 1.0)
return _set_metadata(part, "base_ring_annular_disk_160od_52id_countersunk", BRUSHED_STEEL)
def _make_tick_marks() -> list[object]:
ticks = []
for index in range(72):
angle = tau * index / 72
major = index % 6 == 0
inner_radius = 76.0 if major else 77.2
outer_radius = 79.35
width = 0.72 if major else 0.44
with BuildPart() as tick:
with BuildSketch(Plane.XY.offset(BASE_TOP_Z - 0.18)):
Polygon(_tick_rectangle_points(angle, inner_radius, outer_radius, width), align=None)
extrude(amount=0.12)
ticks.append(_set_metadata(tick.part, f"dark_engraved_radial_tick_{index + 1:02d}", DARK_GRAY))
return ticks
def _make_retaining_ring():
root_radius = RETAINING_ROOT_OD / 2.0
tip_radius = RETAINING_TOOTH_TIP_OD / 2.0
with BuildPart() as retaining:
with BuildSketch(Plane.XY.offset(RETAINING_Z_BOTTOM)):
Polygon(
_tooth_profile(
teeth=RETAINING_TEETH,
root_radius=root_radius,
tip_radius=tip_radius,
phase=-tau / RETAINING_TEETH / 2.0,
root_span_fraction=0.72,
tip_span_fraction=0.34,
),
align=None,
)
extrude(amount=RETAINING_HEIGHT)
with Locations(Location((0.0, 0.0, RETAINING_Z_BOTTOM - 0.5))):
Cylinder(
radius=RETAINING_ID / 2.0,
height=RETAINING_HEIGHT + 1.0,
align=(Align.CENTER, Align.CENTER, Align.MIN),
mode=Mode.SUBTRACT,
)
part = retaining.part
outer_chamfers = []
outer_chamfers.extend(_edges_at_z(part, RETAINING_Z_BOTTOM))
outer_chamfers.extend(_edges_at_z(part, RETAINING_Z_TOP))
part = _safe_chamfer(part, outer_chamfers, 0.45)
return _set_metadata(part, "front_retaining_ring_120_external_teeth_150od_70id", STEEL_HIGHLIGHT)
def _blade_outline_points(center_angle: float) -> list[tuple[float, float]]:
local_points: list[tuple[float, float]] = []
for step in range(10):
local_angle = radians(-BLADE_SPAN_DEGREES / 2.0) + radians(BLADE_SPAN_DEGREES) * step / 9
local_points.append((BLADE_OUTER_RADIUS, local_angle))
local_points.extend(
(
(54.0, radians(18.8)),
(36.0, radians(18.2)),
(BLADE_NOMINAL_INNER_RADIUS, radians(18.0)),
(20.0, radians(17.5)),
(BLADE_INNER_NOSE_RADIUS, radians(14.5)),
(BLADE_INNER_NOSE_RADIUS, radians(10.5)),
(BLADE_INNER_NOSE_RADIUS, radians(6.5)),
(BLADE_INNER_NOSE_RADIUS, radians(2.5)),
(BLADE_INNER_NOSE_RADIUS, radians(-1.5)),
(BLADE_INNER_NOSE_RADIUS, radians(-5.5)),
(BLADE_INNER_NOSE_RADIUS, radians(-9.5)),
(BLADE_INNER_NOSE_RADIUS, radians(-13.5)),
(20.0, radians(-16.8)),
(BLADE_NOMINAL_INNER_RADIUS, radians(-18.0)),
(38.0, radians(-18.4)),
(56.0, radians(-18.8)),
)
)
return [_polar_point(radius, center_angle + local_angle) for radius, local_angle in local_points]
def _blade_pivot_center(blade_angle: float) -> tuple[float, float]:
return _polar_point(BLADE_PIVOT_RADIUS, blade_angle + BLADE_PIVOT_LOCAL_ANGLE)
def _blade_drive_pin_center(blade_angle: float) -> tuple[float, float]:
return _polar_point(BLADE_DRIVE_PIN_RADIUS, blade_angle + BLADE_DRIVE_PIN_LOCAL_ANGLE)
def _make_iris_blade(index: int):
blade_angle = tau * index / BLADE_COUNT
z_bottom = BLADE_Z_BOTTOM + index * BLADE_Z_STAGGER
pivot_x, pivot_y = _blade_pivot_center(blade_angle)
with BuildPart() as blade:
with BuildSketch(Plane.XY.offset(z_bottom)):
Polygon(_blade_outline_points(blade_angle), align=None)
extrude(amount=BLADE_THICKNESS)
with Locations(Location((pivot_x, pivot_y, z_bottom - 0.2))):
Cylinder(
radius=BLADE_PIVOT_HOLE_DIAMETER / 2.0,
height=BLADE_THICKNESS + 0.4,
align=(Align.CENTER, Align.CENTER, Align.MIN),
mode=Mode.SUBTRACT,
)
part = blade.part
part = _safe_chamfer(part, part.edges(), 0.28)
return _set_metadata(part, f"iris_blade_{index + 1:02d}_matte_black_graphite", GRAPHITE)
def _curved_slot_points(center_angle: float) -> list[tuple[float, float]]:
half_angle = (CAM_SLOT_LENGTH / CAM_SLOT_RADIUS) / 2.0
inner_radius = CAM_SLOT_RADIUS - CAM_SLOT_WIDTH / 2.0
outer_radius = CAM_SLOT_RADIUS + CAM_SLOT_WIDTH / 2.0
start_angle = center_angle - half_angle
end_angle = center_angle + half_angle
points = []
for step in range(8):
angle = start_angle + (end_angle - start_angle) * step / 7
points.append(_polar_point(outer_radius, angle))
for step in range(7, -1, -1):
angle = start_angle + (end_angle - start_angle) * step / 7
points.append(_polar_point(inner_radius, angle))
return points
def _make_actuating_ring():
with BuildPart() as actuator:
with Locations(Location((0.0, 0.0, ACTUATING_Z_BOTTOM))):
Cylinder(
radius=ACTUATING_OD / 2.0,
height=ACTUATING_HEIGHT,
align=(Align.CENTER, Align.CENTER, Align.MIN),
)
with Locations(Location((0.0, 0.0, ACTUATING_Z_BOTTOM - 0.5))):
Cylinder(
radius=ACTUATING_ID / 2.0,
height=ACTUATING_HEIGHT + 1.0,
align=(Align.CENTER, Align.CENTER, Align.MIN),
mode=Mode.SUBTRACT,
)
with BuildSketch(Plane.XY.offset(ACTUATING_Z_BOTTOM - 0.2)):
for index in range(BLADE_COUNT):
blade_angle = tau * index / BLADE_COUNT
slot_angle = blade_angle + BLADE_DRIVE_PIN_LOCAL_ANGLE
Polygon(_curved_slot_points(slot_angle), align=None)
extrude(amount=ACTUATING_HEIGHT + 0.4, mode=Mode.SUBTRACT)
part = actuator.part
chamfer_edges = []
for radius in (ACTUATING_OD / 2.0, ACTUATING_ID / 2.0):
chamfer_edges.extend(_circular_edges(part, radius=radius, coordinate=ACTUATING_Z_BOTTOM))
chamfer_edges.extend(_circular_edges(part, radius=radius, coordinate=ACTUATING_Z_TOP))
part = _safe_chamfer(part, chamfer_edges, 0.8)
return _set_metadata(part, "actuating_ring_12_curved_cam_slots_138od_86id", BRUSHED_STEEL)
def _make_pivot_screw(index: int):
blade_angle = tau * index / BLADE_COUNT
x_pos, y_pos = _blade_pivot_center(blade_angle)
shaft_height = ACTUATING_Z_TOP - BLADE_Z_BOTTOM + 0.2
head_height = 1.6
with BuildPart() as screw:
Cylinder(
radius=1.25,
height=shaft_height,
align=(Align.CENTER, Align.CENTER, Align.MIN),
)
with Locations(Location((0.0, 0.0, shaft_height))):
Cylinder(
radius=2.5,
height=head_height,
align=(Align.CENTER, Align.CENTER, Align.MIN),
)
with BuildSketch(Plane.XY.offset(shaft_height + head_height - 0.72)):
RegularPolygon(radius=1.35, side_count=6, major_radius=True, rotation=30.0)
extrude(amount=0.82, mode=Mode.SUBTRACT)
part = screw.part
head_edges = []
head_edges.extend(_circular_edges(part, radius=2.5, coordinate=shaft_height))
head_edges.extend(_circular_edges(part, radius=2.5, coordinate=shaft_height + head_height))
part = _safe_fillet(part, head_edges, 0.35)
part = part.moved(Location((x_pos, y_pos, BLADE_Z_BOTTOM - 0.1)))
return _set_metadata(part, f"pivot_screw_{index + 1:02d}_brass_head_and_shaft", BRASS)
def _make_drive_pin(index: int):
blade_angle = tau * index / BLADE_COUNT
x_pos, y_pos = _blade_drive_pin_center(blade_angle)
with BuildPart() as pin:
Cylinder(
radius=1.3,
height=ACTUATING_Z_TOP - BLADE_Z_BOTTOM + 0.75,
align=(Align.CENTER, Align.CENTER, Align.MIN),
)
part = pin.part
part = _safe_fillet(part, _circular_edges(part, radius=1.3, coordinate=ACTUATING_Z_TOP - BLADE_Z_BOTTOM + 0.75), 0.45)
part = part.moved(Location((x_pos, y_pos, BLADE_Z_BOTTOM - 0.05)))
return _set_metadata(part, f"drive_pin_{index + 1:02d}_brass_cam_follower", BRASS)
def _make_front_socket_screw(index: int):
angle = tau * index / FRONT_SCREW_COUNT + radians(30.0)
x_pos, y_pos = _polar_point(FRONT_SCREW_BCD / 2.0, angle)
shaft_radius = 2.5
shaft_height = ACTUATING_Z_TOP - RETAINING_Z_BOTTOM + 0.2
head_radius = FRONT_SCREW_HEAD_DIAMETER / 2.0
socket_depth = 1.45
with BuildPart() as screw:
Cylinder(
radius=shaft_radius,
height=shaft_height,
align=(Align.CENTER, Align.CENTER, Align.MIN),
)
with Locations(Location((0.0, 0.0, shaft_height))):
Cylinder(
radius=head_radius,
height=FRONT_SCREW_HEAD_HEIGHT,
align=(Align.CENTER, Align.CENTER, Align.MIN),
)
with BuildSketch(Plane.XY.offset(shaft_height + FRONT_SCREW_HEAD_HEIGHT - socket_depth)):
RegularPolygon(radius=2.05, side_count=6, major_radius=True, rotation=30.0)
extrude(amount=socket_depth + 0.15, mode=Mode.SUBTRACT)
part = screw.part
head_edges = []
head_edges.extend(_circular_edges(part, radius=head_radius, coordinate=shaft_height))
head_edges.extend(_circular_edges(part, radius=head_radius, coordinate=shaft_height + FRONT_SCREW_HEAD_HEIGHT))
part = _safe_fillet(part, head_edges, 0.5)
part = part.moved(Location((x_pos, y_pos, RETAINING_Z_BOTTOM - 0.1)))
return _set_metadata(part, f"front_socket_head_cap_screw_{index + 1:02d}_gunmetal", GUNMETAL)
def _make_aperture_shadow():
with BuildPart() as shadow:
with Locations(Location((0.0, 0.0, BLADE_Z_BOTTOM - 0.22))):
Cylinder(
radius=18.0,
height=0.12,
align=(Align.CENTER, Align.CENTER, Align.MIN),
)
return _set_metadata(shadow.part, "dark_central_aperture_void_36mm_visual_shadow", APERTURE_BLACK)
def gen_step():
"""Return a labeled mechanical iris aperture assembly in millimeters."""
parts = [
_make_aperture_shadow(),
_make_base_ring(),
*_make_tick_marks(),
_make_retaining_ring(),
]
for index in range(BLADE_COUNT):
parts.append(_make_iris_blade(index))
parts.append(_make_actuating_ring())
for index in range(BLADE_COUNT):
parts.append(_make_drive_pin(index))
parts.append(_make_pivot_screw(index))
for index in range(FRONT_SCREW_COUNT):
parts.append(_make_front_socket_screw(index))
assembly = Compound(
obj=parts,
children=parts,
label=MODEL_LABEL,
)
return assembly
@@ -0,0 +1,189 @@
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,
)
# Units: millimeters.
# Origin: centered on the staircase axis; base/column bottom starts at Z=0.
# XY: plan view. +Z: staircase rise direction.
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_TUBE_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 = radians(angle_degrees)
return (radius * cos(angle_radians), radius * sin(angle_radians))
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 = "circular_base_disk_diameter_90mm_thickness_5mm"
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_diameter_14mm_height_140mm"
part.color = Color(0.70, 0.72, 0.74, 1.0)
return part
def _make_tread_profile():
half_sweep = TREAD_SWEEP_DEGREES / 2.0
start_angle = -half_sweep
end_angle = half_sweep
with BuildPart() as tread:
with BuildSketch(Plane.XY):
with BuildLine():
CenterArc((0.0, 0.0), TREAD_OUTER_RADIUS, start_angle, TREAD_SWEEP_DEGREES)
Line(
_polar_point(TREAD_OUTER_RADIUS, end_angle),
_polar_point(TREAD_INNER_RADIUS, end_angle),
)
CenterArc((0.0, 0.0), TREAD_INNER_RADIUS, end_angle, -TREAD_SWEEP_DEGREES)
Line(
_polar_point(TREAD_INNER_RADIUS, start_angle),
_polar_point(TREAD_OUTER_RADIUS, start_angle),
)
make_face()
extrude(amount=TREAD_THICKNESS)
part = tread.part
part.label = "tread_profile_inner_10mm_outer_62mm_sweep_24deg"
part.color = Color(0.78, 0.58, 0.35, 1.0)
return part
def _make_tread(index: int):
angle_degrees = index * TREAD_ROTATION_DEGREES
bottom_z = TREAD_FIRST_BOTTOM_Z + index * TREAD_RISE
tread_profile = _make_tread_profile()
tread = tread_profile.moved(Location((0.0, 0.0, bottom_z), (0.0, 0.0, angle_degrees)))
tread.label = f"tread_{index + 1:02d}_bottom_z_{bottom_z:.0f}mm_angle_{angle_degrees:.0f}deg"
return tread
def _handrail_center_z(angle_degrees: float) -> float:
rail_rise = HANDRAIL_END_Z - HANDRAIL_START_Z
return HANDRAIL_START_Z + rail_rise * 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),
)
start_tangent = path.tangent_at(0.0)
profile_plane = Plane(
origin=(HANDRAIL_RADIUS, 0.0, HANDRAIL_START_Z),
x_dir=(1.0, 0.0, 0.0),
z_dir=start_tangent,
)
with BuildPart() as rail:
with BuildSketch(profile_plane):
Circle(HANDRAIL_TUBE_DIAMETER / 2.0)
sweep(path=path, is_frenet=True)
part = rail.part
part.label = "helical_outer_handrail_radius_66mm_diameter_5mm"
part.color = Color(0.22, 0.28, 0.34, 1.0)
return part
def _make_baluster(index: int):
angle_degrees = index * TREAD_ROTATION_DEGREES
tread_top_z = TREAD_FIRST_BOTTOM_Z + index * TREAD_RISE + TREAD_THICKNESS
rail_center_z = _handrail_center_z(angle_degrees)
x_pos, y_pos = _polar_point(TREAD_OUTER_RADIUS, angle_degrees)
height = rail_center_z - tread_top_z
with BuildPart() as baluster:
Cylinder(
radius=BALUSTER_DIAMETER / 2.0,
height=height,
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}_diameter_3mm"
part.color = Color(0.30, 0.33, 0.36, 1.0)
return part
def gen_step():
"""Return a labeled miniature spiral staircase STEP compound in millimeters."""
parts = [
_make_base(),
_make_column(),
]
for index in range(TREAD_COUNT):
parts.append(_make_tread(index))
parts.append(_make_handrail())
for index in range(TREAD_COUNT):
parts.append(_make_baluster(index))
assembly = Compound(
obj=parts,
children=parts,
label="miniature_spiral_staircase",
)
return assembly
@@ -0,0 +1,338 @@
from __future__ import annotations
import math
from collections.abc import Sequence
from build123d import (
Box,
Color,
Compound,
Cone,
Cylinder,
Location,
Shape,
Sphere,
Torus,
chamfer,
fillet,
scale,
)
from OCP.BRepBuilderAPI import BRepBuilderAPI_Transform
from OCP.BRepPrimAPI import BRepPrimAPI_MakeCylinder
from OCP.gp import gp_Ax2, gp_Dir, gp_Pnt, gp_Trsf
DISPLAY_NAME = "Pelican riding a bicycle"
# Units: millimeters.
# Origin: halfway between the wheel hubs on the bicycle center plane.
# X: rear-to-front bicycle direction, Y: wheel axle direction, +Z: up.
COLORS = {
"rubber_black": (0.003, 0.003, 0.003, 1.0),
"rim_silver": (0.70, 0.72, 0.70, 1.0),
"spoke_steel": (0.82, 0.84, 0.82, 1.0),
"frame_blue": (0.05, 0.28, 0.75, 1.0),
"seat_brown": (0.28, 0.12, 0.05, 1.0),
"pelican_white": (0.96, 0.95, 0.88, 1.0),
"wing_gray": (0.66, 0.68, 0.64, 1.0),
"feather_dark": (0.08, 0.08, 0.075, 1.0),
"bill_yellow": (1.00, 0.78, 0.26, 1.0),
"pouch_orange": (0.95, 0.45, 0.12, 1.0),
"leg_orange": (0.98, 0.50, 0.12, 1.0),
"eye_black": (0.0, 0.0, 0.0, 1.0),
}
Vector3 = tuple[float, float, float]
def _v_add(a: Vector3, b: Vector3) -> Vector3:
return (a[0] + b[0], a[1] + b[1], a[2] + b[2])
def _v_sub(a: Vector3, b: Vector3) -> Vector3:
return (a[0] - b[0], a[1] - b[1], a[2] - b[2])
def _v_mul(a: Vector3, scalar: float) -> Vector3:
return (a[0] * scalar, a[1] * scalar, a[2] * scalar)
def _v_dot(a: Vector3, b: Vector3) -> float:
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
def _v_cross(a: Vector3, b: Vector3) -> Vector3:
return (
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
)
def _v_len(a: Vector3) -> float:
return math.sqrt(_v_dot(a, a))
def _v_norm(a: Vector3) -> Vector3:
length = _v_len(a)
if length <= 1e-9:
raise ValueError("zero-length vector")
return (a[0] / length, a[1] / length, a[2] / length)
def _style(shape: Shape, label: str, color_name: str) -> Shape:
shape.label = label
shape.color = Color(*COLORS[color_name])
return shape
def _group(label: str, children: Sequence[Shape], color_name: str | None = None) -> Compound:
compound = Compound(obj=list(children), children=list(children), label=label)
if color_name is not None:
compound.color = Color(*COLORS[color_name])
return compound
def _softened(shape: Shape, radius: float) -> Shape:
try:
return fillet(shape.edges(), radius=radius)
except Exception:
try:
return chamfer(shape.edges(), length=radius * 0.45)
except Exception:
return shape
def _transformed(shape: Shape, center: Vector3, axis_x: Vector3, axis_y: Vector3, axis_z: Vector3) -> Shape:
x_dir = _v_norm(axis_x)
y_dir = _v_norm(axis_y)
z_dir = _v_norm(axis_z)
trsf = gp_Trsf()
trsf.SetValues(
x_dir[0],
y_dir[0],
z_dir[0],
center[0],
x_dir[1],
y_dir[1],
z_dir[1],
center[1],
x_dir[2],
y_dir[2],
z_dir[2],
center[2],
)
return Shape(obj=BRepBuilderAPI_Transform(shape.wrapped, trsf, True).Shape())
def _oriented_box(
label: str,
center: Vector3,
size_x: float,
size_y: float,
size_z: float,
axis_x: Vector3,
axis_y: Vector3,
axis_z: Vector3,
color_name: str,
*,
radius: float = 0.0,
) -> Shape:
shape = _transformed(Box(size_x, size_y, size_z), center, axis_x, axis_y, axis_z)
if radius > 0.0:
shape = _softened(shape, radius)
return _style(shape, label, color_name)
def _cylinder_between(label: str, p1: Vector3, p2: Vector3, radius: float, color_name: str) -> Shape:
axis = _v_sub(p2, p1)
length = _v_len(axis)
if length <= 1e-6:
return _style(Sphere(radius).moved(Location(p1)), label, color_name)
direction = _v_norm(axis)
wrapped = BRepPrimAPI_MakeCylinder(
gp_Ax2(gp_Pnt(*p1), gp_Dir(*direction)),
radius,
length,
).Shape()
return _style(Shape(obj=wrapped), label, color_name)
def _cylinder_y(label: str, center: Vector3, radius: float, length: float, color_name: str) -> Shape:
return _style(Cylinder(radius, length, rotation=(90, 0, 0)).moved(Location(center)), label, color_name)
def _cylinder_x(label: str, center: Vector3, radius: float, length: float, color_name: str) -> Shape:
return _style(Cylinder(radius, length, rotation=(0, 90, 0)).moved(Location(center)), label, color_name)
def _ellipsoid(label: str, center: Vector3, radii: Vector3, color_name: str) -> Shape:
shape = scale(Sphere(1.0), by=radii).moved(Location(center))
return _style(shape, label, color_name)
def _flat_cone_x(
label: str,
center: Vector3,
length: float,
base_radius: float,
tip_radius: float,
y_scale: float,
z_scale: float,
color_name: str,
) -> Shape:
cone = Cone(base_radius, tip_radius, length, rotation=(0, 90, 0))
shape = scale(cone, by=(1.0, y_scale, z_scale)).moved(Location(center))
return _style(shape, label, color_name)
def _make_wheel(label: str, center: Vector3) -> Compound:
cx, cy, cz = center
tire_major_radius = 40.0
tire_minor_radius = 4.0
rim_radius = 31.0
spoke_radius = 0.72
hub_radius = 4.2
spoke_count = 20
parts: list[Shape] = [
_style(
Torus(tire_major_radius, tire_minor_radius, rotation=(90, 0, 0)).moved(Location(center)),
f"{label}_continuous_black_tire",
"rubber_black",
),
_style(
Torus(rim_radius, 1.45, rotation=(90, 0, 0)).moved(Location(center)),
f"{label}_silver_rim",
"rim_silver",
),
_cylinder_y(f"{label}_hub_barrel", center, hub_radius, 14.0, "rim_silver"),
_cylinder_y(f"{label}_hub_axle_pin", center, 1.8, 24.0, "spoke_steel"),
]
for index in range(spoke_count):
theta = index * math.tau / spoke_count
rim_point = (cx + math.cos(theta) * (rim_radius - 1.8), cy, cz + math.sin(theta) * (rim_radius - 1.8))
opposite_offset = -0.9 if index % 2 == 0 else 0.9
hub_point = (cx, cy + opposite_offset, cz)
parts.append(_cylinder_between(f"{label}_radial_spoke_{index + 1:02d}", hub_point, rim_point, spoke_radius, "spoke_steel"))
return _group(label, parts)
def _make_bicycle() -> Compound:
rear = (-55.0, 0.0, 44.0)
front = (55.0, 0.0, 44.0)
bottom = (0.0, 0.0, 49.0)
seat_lug = (-15.0, 0.0, 86.0)
head_low = (43.0, 0.0, 66.0)
head_high = (38.0, 0.0, 88.0)
handle_stem = (57.0, 0.0, 99.0)
parts: list[Shape] = [
_make_wheel("rear_wheel", rear),
_make_wheel("front_wheel", front),
_cylinder_between("frame_seat_tube", bottom, seat_lug, 2.3, "frame_blue"),
_cylinder_between("frame_top_tube", seat_lug, head_high, 2.2, "frame_blue"),
_cylinder_between("frame_down_tube", bottom, head_low, 2.5, "frame_blue"),
_cylinder_between("frame_head_tube", head_low, head_high, 2.6, "frame_blue"),
_cylinder_between("frame_left_chain_stay", (bottom[0], -4.2, bottom[2]), (rear[0], -4.2, rear[2]), 1.7, "frame_blue"),
_cylinder_between("frame_right_chain_stay", (bottom[0], 4.2, bottom[2]), (rear[0], 4.2, rear[2]), 1.7, "frame_blue"),
_cylinder_between("frame_left_seat_stay", (seat_lug[0], -3.8, seat_lug[2]), (rear[0], -3.8, rear[2]), 1.55, "frame_blue"),
_cylinder_between("frame_right_seat_stay", (seat_lug[0], 3.8, seat_lug[2]), (rear[0], 3.8, rear[2]), 1.55, "frame_blue"),
_cylinder_between("front_left_fork_blade", (head_low[0], -4.0, head_low[2]), (front[0], -4.0, front[2]), 1.7, "frame_blue"),
_cylinder_between("front_right_fork_blade", (head_low[0], 4.0, head_low[2]), (front[0], 4.0, front[2]), 1.7, "frame_blue"),
_cylinder_between("handlebar_stem", head_high, handle_stem, 1.8, "frame_blue"),
_cylinder_y("handlebar_crossbar", (64.0, 0.0, 101.0), 1.6, 38.0, "frame_blue"),
_cylinder_y("left_black_grip", (64.0, -22.5, 101.0), 2.2, 8.0, "rubber_black"),
_cylinder_y("right_black_grip", (64.0, 22.5, 101.0), 2.2, 8.0, "rubber_black"),
_cylinder_between("seat_post", seat_lug, (-17.0, 0.0, 95.0), 1.75, "spoke_steel"),
_style(_softened(Box(24.0, 17.0, 5.0).moved(Location((-20.0, 0.0, 97.0))), 1.2), "brown_saddle", "seat_brown"),
_style(Torus(7.5, 0.9, rotation=(90, 0, 0)).moved(Location(bottom)), "crank_ring", "spoke_steel"),
_cylinder_y("bottom_bracket_axle", bottom, 3.3, 20.0, "rim_silver"),
]
pedal_front = (12.5, -11.0, 38.5)
pedal_back = (-12.5, 11.0, 60.5)
parts.extend(
[
_cylinder_between("front_crank_arm", bottom, pedal_front, 1.0, "spoke_steel"),
_cylinder_between("rear_crank_arm", bottom, pedal_back, 1.0, "spoke_steel"),
_style(_softened(Box(15.0, 4.0, 2.5).moved(Location(pedal_front)), 0.7), "front_pedal_flat", "rubber_black"),
_style(_softened(Box(15.0, 4.0, 2.5).moved(Location(pedal_back)), 0.7), "rear_pedal_flat", "rubber_black"),
]
)
return _group("bicycle", parts, "frame_blue")
def _make_pelican() -> Compound:
parts: list[Shape] = [
_ellipsoid("pelican_round_body", (-12.0, 0.0, 120.0), (24.0, 15.0, 29.0), "pelican_white"),
_ellipsoid("pelican_chest_belly_highlight", (1.0, 0.0, 112.0), (18.0, 13.0, 20.0), "pelican_white"),
_ellipsoid("left_folded_wing", (-22.0, -13.0, 120.0), (19.0, 3.2, 25.0), "wing_gray"),
_ellipsoid("right_folded_wing", (-22.0, 13.0, 120.0), (19.0, 3.2, 25.0), "wing_gray"),
_ellipsoid("left_dark_wing_tip", (-33.0, -15.7, 104.0), (12.0, 2.2, 10.0), "feather_dark"),
_ellipsoid("right_dark_wing_tip", (-33.0, 15.7, 104.0), (12.0, 2.2, 10.0), "feather_dark"),
_cylinder_between("curved_neck_lower_segment", (-1.0, 0.0, 139.0), (12.0, 0.0, 151.0), 5.0, "pelican_white"),
_cylinder_between("curved_neck_upper_segment", (12.0, 0.0, 151.0), (24.0, 0.0, 157.0), 4.4, "pelican_white"),
_ellipsoid("pelican_head", (28.0, 0.0, 160.0), (10.5, 8.1, 9.5), "pelican_white"),
_flat_cone_x("long_upper_bill", (52.0, 0.0, 160.5), 48.0, 5.2, 1.1, 1.45, 0.55, "bill_yellow"),
_ellipsoid("rounded_lower_bill_pouch", (49.0, 0.0, 151.5), (23.0, 7.0, 7.2), "pouch_orange"),
_cylinder_between("dark_bill_seam_left", (31.0, -7.3, 156.2), (70.0, -2.7, 157.0), 0.45, "feather_dark"),
_cylinder_between("dark_bill_seam_right", (31.0, 7.3, 156.2), (70.0, 2.7, 157.0), 0.45, "feather_dark"),
_ellipsoid("left_eye", (31.5, -7.4, 162.8), (1.45, 0.75, 1.45), "eye_black"),
_ellipsoid("right_eye", (31.5, 7.4, 162.8), (1.45, 0.75, 1.45), "eye_black"),
_ellipsoid("soft_head_crest", (19.0, 0.0, 168.0), (4.0, 4.0, 5.0), "pelican_white"),
]
tail_roots = [(-34.0, -4.5, 108.0), (-38.0, 0.0, 110.0), (-34.0, 4.5, 108.0)]
for index, center in enumerate(tail_roots, start=1):
parts.append(_ellipsoid(f"short_tail_feather_{index}", center, (11.0, 2.0, 5.2), "feather_dark"))
# Bent orange legs and webbed feet make the bird visibly seated on the cycle.
left_hip = (-8.0, -5.7, 96.0)
left_knee = (2.5, -7.5, 74.0)
left_pedal = (12.5, -11.0, 38.5)
right_hip = (-13.0, 5.7, 96.0)
right_knee = (-19.0, 8.4, 74.0)
right_pedal = (-12.5, 11.0, 60.5)
parts.extend(
[
_cylinder_between("left_upper_leg", left_hip, left_knee, 1.8, "leg_orange"),
_cylinder_between("left_lower_leg", left_knee, left_pedal, 1.6, "leg_orange"),
_cylinder_between("right_upper_leg", right_hip, right_knee, 1.8, "leg_orange"),
_cylinder_between("right_lower_leg", right_knee, right_pedal, 1.6, "leg_orange"),
_ellipsoid("left_webbed_foot_on_forward_pedal", _v_add(left_pedal, (1.5, 0.0, 2.0)), (8.5, 3.5, 2.2), "leg_orange"),
_ellipsoid("right_webbed_foot_on_rear_pedal", _v_add(right_pedal, (-1.5, 0.0, 2.0)), (8.5, 3.5, 2.2), "leg_orange"),
]
)
feather_line_specs = [
("left_wing_feather_line_1", (-17.0, -16.2, 136.0), (-33.0, -16.2, 112.0)),
("left_wing_feather_line_2", (-11.0, -16.4, 126.0), (-34.0, -16.4, 101.0)),
("right_wing_feather_line_1", (-17.0, 16.2, 136.0), (-33.0, 16.2, 112.0)),
("right_wing_feather_line_2", (-11.0, 16.4, 126.0), (-34.0, 16.4, 101.0)),
]
for label, p1, p2 in feather_line_specs:
parts.append(_cylinder_between(label, p1, p2, 0.55, "feather_dark"))
return _group("pelican", parts, "pelican_white")
def _make_ground_shadow() -> Compound:
# Thin reference pads give the model a stable visual base without merging
# into the wheels.
front_pad = _style(_softened(Box(92.0, 28.0, 1.0).moved(Location((55.0, 0.0, -2.0))), 1.0), "front_wheel_ground_shadow", "rubber_black")
rear_pad = _style(_softened(Box(92.0, 28.0, 1.0).moved(Location((-55.0, 0.0, -2.0))), 1.0), "rear_wheel_ground_shadow", "rubber_black")
return _group("subtle_ground_shadow", [front_pad, rear_pad])
def gen_step() -> Compound:
bicycle = _make_bicycle()
pelican = _make_pelican()
ground = _make_ground_shadow()
assembly = _group("pelican_riding_bicycle", [bicycle, pelican, ground])
assembly.label = "pelican_riding_bicycle"
return assembly
+537
View File
@@ -0,0 +1,537 @@
from __future__ import annotations
from math import degrees, cos, sin, sqrt
from build123d import (
Align,
BuildPart,
BuildSketch,
Color,
Compound,
Cone,
Cylinder,
Location,
Locations,
Mode,
Plane,
Rectangle,
RectangleRounded,
SlotOverall,
Solid,
Text,
TextAlign,
Torus,
extrude,
)
# Units: millimeters.
# Origin: center of the cup base footprint, XY on table plane, +Z upward.
MODEL_LABEL = "photo_reference_takeaway_coffee_cup"
PAPER = Color(0.86, 0.78, 0.64, 1.0)
PAPER_EDGE = Color(0.90, 0.82, 0.69, 1.0)
LID_WHITE = Color(0.96, 0.94, 0.90, 1.0)
LID_HIGHLIGHT = Color(1.0, 0.985, 0.955, 1.0)
LID_SHADOW = Color(0.82, 0.80, 0.76, 1.0)
BLUE_LOGO = Color(0.00, 0.62, 0.78, 1.0)
DARK_OPENING = Color(0.08, 0.075, 0.065, 1.0)
BODY_HEIGHT = 108.0
BODY_BOTTOM_RADIUS = 30.0
BODY_TOP_RADIUS = 43.0
BODY_WALL = 2.2
BODY_BOTTOM_THICKNESS = 2.8
LID_SKIRT_BOTTOM_Z = 103.5
LID_TOP_Z = 127.0
LOGO_LOCAL_BOTTOM_Z = -2.0
LOGO_LOCAL_TOP_Z = 86.4
LOGO_WORLD_BOTTOM_Z = 34.0
LOGO_Z_SCALE = 0.56
LOGO_X_SCALE = 0.70
LOGO_WORLD_TOP_Z = LOGO_WORLD_BOTTOM_Z + (LOGO_LOCAL_TOP_Z - LOGO_LOCAL_BOTTOM_Z) * LOGO_Z_SCALE
def _set_metadata(part, label: str, color: Color):
part.label = label
part.color = color
return part
def _radius_at_z(z_pos: float) -> float:
slope = (BODY_TOP_RADIUS - BODY_BOTTOM_RADIUS) / BODY_HEIGHT
return BODY_BOTTOM_RADIUS + slope * z_pos
def _annular_cylinder(
*,
label: str,
outer_radius: float,
inner_radius: float,
height: float,
z_bottom: float,
color: Color,
) -> object:
with BuildPart() as ring:
with Locations(Location((0.0, 0.0, z_bottom))):
Cylinder(
radius=outer_radius,
height=height,
align=(Align.CENTER, Align.CENTER, Align.MIN),
)
with Locations(Location((0.0, 0.0, z_bottom - 0.75))):
Cylinder(
radius=inner_radius,
height=height + 1.5,
align=(Align.CENTER, Align.CENTER, Align.MIN),
mode=Mode.SUBTRACT,
)
return _set_metadata(ring.part, label, color)
def _solid_cylinder(
*,
label: str,
radius: float,
height: float,
z_bottom: float,
color: Color,
) -> object:
with BuildPart() as cylinder:
with Locations(Location((0.0, 0.0, z_bottom))):
Cylinder(
radius=radius,
height=height,
align=(Align.CENTER, Align.CENTER, Align.MIN),
)
return _set_metadata(cylinder.part, label, color)
def _make_paper_cup_body() -> object:
extra_cut_height = 4.0
slope = (BODY_TOP_RADIUS - BODY_BOTTOM_RADIUS) / BODY_HEIGHT
inner_bottom_radius = _radius_at_z(BODY_BOTTOM_THICKNESS) - BODY_WALL
inner_top_radius = BODY_TOP_RADIUS - BODY_WALL + slope * extra_cut_height
with BuildPart() as cup:
Cone(
bottom_radius=BODY_BOTTOM_RADIUS,
top_radius=BODY_TOP_RADIUS,
height=BODY_HEIGHT,
align=(Align.CENTER, Align.CENTER, Align.MIN),
)
with Locations(Location((0.0, 0.0, BODY_BOTTOM_THICKNESS))):
Cone(
bottom_radius=inner_bottom_radius,
top_radius=inner_top_radius,
height=BODY_HEIGHT - BODY_BOTTOM_THICKNESS + extra_cut_height,
align=(Align.CENTER, Align.CENTER, Align.MIN),
mode=Mode.SUBTRACT,
)
return _set_metadata(
cup.part,
"tapered_beige_paper_cup_open_shell_12oz",
PAPER,
)
def _make_bottom_foot_ring() -> object:
with BuildPart() as foot:
with Locations(Location((0.0, 0.0, 3.2))):
Torus(major_radius=BODY_BOTTOM_RADIUS - 2.0, minor_radius=1.25)
return _set_metadata(foot.part, "subtle_rolled_bottom_paper_foot_ring", PAPER_EDGE)
def _make_lid_skirt_parts() -> list[object]:
parts: list[object] = []
parts.append(
_annular_cylinder(
label="white_plastic_lid_lower_snap_skirt_overhanging_cup",
outer_radius=47.8,
inner_radius=38.8,
height=8.0,
z_bottom=LID_SKIRT_BOTTOM_Z,
color=LID_WHITE,
)
)
parts.append(
_annular_cylinder(
label="thin_shadow_groove_between_lid_beads",
outer_radius=46.4,
inner_radius=42.3,
height=1.0,
z_bottom=112.5,
color=LID_SHADOW,
)
)
for label, z_pos, major_radius, minor_radius in [
("rounded_lower_snap_lip_bead", 108.8, 45.9, 2.1),
("rounded_middle_lid_bead", 113.6, 45.2, 1.55),
("rounded_upper_lid_bead_under_dome", 117.2, 43.4, 1.25),
]:
with BuildPart() as bead:
with Locations(Location((0.0, 0.0, z_pos))):
Torus(major_radius=major_radius, minor_radius=minor_radius)
parts.append(_set_metadata(bead.part, label, LID_WHITE))
return parts
def _make_lid_top() -> list[object]:
parts: list[object] = []
with BuildPart() as dome:
with Locations(Location((0.0, 0.0, 115.5))):
Cone(
bottom_radius=42.8,
top_radius=39.0,
height=9.4,
align=(Align.CENTER, Align.CENTER, Align.MIN),
)
parts.append(_set_metadata(dome.part, "shallow_sloped_lid_dome", LID_WHITE))
parts.append(
_solid_cylinder(
label="flat_raised_top_lid_disk",
radius=38.4,
height=2.1,
z_bottom=124.6,
color=LID_HIGHLIGHT,
)
)
with BuildPart() as top_outer_ring:
with Locations(Location((0.0, 0.0, 126.9))):
Torus(major_radius=36.4, minor_radius=0.85)
parts.append(_set_metadata(top_outer_ring.part, "fine_raised_outer_top_emboss_ring", LID_HIGHLIGHT))
with BuildPart() as top_inner_ring:
with Locations(Location((0.0, 0.0, 127.15))):
Torus(major_radius=18.0, minor_radius=0.38)
parts.append(_set_metadata(top_inner_ring.part, "faint_central_emboss_circle", LID_HIGHLIGHT))
return parts
def _make_sip_feature() -> list[object]:
parts: list[object] = []
top_plane = Plane.XY.offset(LID_TOP_Z)
with BuildPart() as raised_sip:
with BuildSketch(top_plane):
with Locations((11.0, 18.4)):
SlotOverall(width=31.0, height=12.2, rotation=-9.0)
extrude(amount=1.2)
parts.append(_set_metadata(raised_sip.part, "raised_oval_sipping_platform_on_lid", LID_HIGHLIGHT))
with BuildPart() as sip_opening:
with BuildSketch(Plane.XY.offset(LID_TOP_Z + 1.22)):
with Locations((15.2, 20.0)):
SlotOverall(width=16.0, height=4.0, rotation=-9.0)
extrude(amount=0.45)
parts.append(_set_metadata(sip_opening.part, "dark_recessed_sip_slot_opening", DARK_OPENING))
with BuildPart() as nose_ridge:
with BuildSketch(Plane.XY.offset(LID_TOP_Z + 0.7)):
with Locations((0.5, 7.5)):
RectangleRounded(width=10.0, height=22.0, radius=2.0, rotation=-8.0)
extrude(amount=0.75)
parts.append(_set_metadata(nose_ridge.part, "low_centered_lid_drink_ridge", LID_HIGHLIGHT))
return parts
def _make_top_embossing() -> list[object]:
parts: list[object] = []
emboss_plane = Plane.XY.offset(LID_TOP_Z + 0.18)
with BuildPart() as caution_text:
with BuildSketch(emboss_plane):
with Locations((-2.5, -10.5)):
Text(
"CAUTION HOT",
font_size=4.0,
text_align=(TextAlign.CENTER, TextAlign.CENTER),
rotation=3.0,
)
with Locations((-4.0, 6.0)):
Text(
"PLEASE RECYCLE",
font_size=3.0,
text_align=(TextAlign.CENTER, TextAlign.CENTER),
rotation=-4.0,
)
extrude(amount=0.28)
parts.append(_set_metadata(caution_text.part, "low_raised_lid_embossed_warning_text", LID_HIGHLIGHT))
for index, (x_pos, y_pos, width, height, rotation) in enumerate(
[
(-18.0, 4.5, 10.0, 1.1, 13.0),
(18.0, 4.5, 10.0, 1.1, -13.0),
(-15.0, -18.0, 8.5, 1.0, -16.0),
(15.0, -18.0, 8.5, 1.0, 16.0),
],
start=1,
):
with BuildPart() as tick:
with BuildSketch(Plane.XY.offset(LID_TOP_Z + 0.16)):
with Locations((x_pos, y_pos)):
Rectangle(width=width, height=height, rotation=rotation)
extrude(amount=0.24)
parts.append(_set_metadata(tick.part, f"short_emboss_tick_mark_{index:02d}", LID_HIGHLIGHT))
return parts
def _make_blue_logo() -> list[object]:
bottle_cells: list[object] = []
z_bottom = LOGO_WORLD_BOTTOM_Z
z_top = LOGO_WORLD_TOP_Z
dz = 0.58
z_pos = z_bottom
cell_index = 1
while z_pos < z_top:
z_next = min(z_pos + dz, z_top)
for interval in _blue_bottle_intervals(z_pos, z_next):
cell = _make_curved_decal_cell(
label=f"blue_bottle_logo_curved_surface_cell_{cell_index:03d}",
x_left_bottom=interval[0],
x_right_bottom=interval[1],
z_bottom=z_pos,
x_left_top=interval[2],
x_right_top=interval[3],
z_top=z_next,
color=BLUE_LOGO,
)
if cell is not None:
bottle_cells.append(cell)
cell_index += 1
z_pos = z_next
mark_cells: list[object] = []
z_pos = 34.65
while z_pos < 37.35:
z_next = min(z_pos + 0.36, 37.35)
interval = _registered_mark_interval((z_pos + z_next) / 2.0)
if interval is not None:
cell = _make_curved_decal_cell(
label=f"blue_bottle_registered_mark_surface_cell_{len(mark_cells) + 1:02d}",
x_left_bottom=interval[0],
x_right_bottom=interval[1],
z_bottom=z_pos,
x_left_top=interval[0],
x_right_top=interval[1],
z_top=z_next,
color=BLUE_LOGO,
inner_offset=0.18,
outer_offset=0.72,
)
if cell is not None:
mark_cells.append(cell)
z_pos = z_next
logo = Compound(
obj=bottle_cells + mark_cells,
children=bottle_cells + mark_cells,
label="front_blue_bottle_logo_wrapped_on_cup_surface",
)
logo.color = BLUE_LOGO
return [logo]
def _blue_bottle_intervals(
z_bottom: float,
z_top: float,
) -> list[tuple[float, float, float, float]]:
intervals: list[tuple[float, float, float, float]] = []
bottom_intervals = _logo_outline_intervals_at(z_bottom + 1.0e-4)
top_intervals = _logo_outline_intervals_at(z_top - 1.0e-4)
for bottom_interval, top_interval in zip(bottom_intervals, top_intervals):
intervals.append((bottom_interval[0], bottom_interval[1], top_interval[0], top_interval[1]))
return intervals
def _logo_outline_intervals_at(z_pos: float) -> list[tuple[float, float]]:
local_z = (z_pos - LOGO_WORLD_BOTTOM_Z) / LOGO_Z_SCALE + LOGO_LOCAL_BOTTOM_Z
if local_z < LOGO_LOCAL_BOTTOM_Z or local_z > LOGO_LOCAL_TOP_Z:
return []
outline = [
(-4.6, 86.4),
(-3.5, 86.1),
(-2.1, 84.6),
(-0.1, 83.7),
(3.0, 83.2),
(7.4, 83.1),
(8.7, 83.4),
(8.3, 82.2),
(5.9, 81.4),
(1.8, 81.2),
(-2.7, 81.7),
(-5.8, 82.0),
(-6.7, 83.0),
(-6.8, 84.8),
(-6.2, 85.9),
(-3.4, 80.7),
(-3.4, 73.5),
(-3.7, 68.2),
(-4.7, 64.3),
(-6.3, 61.1),
(-8.6, 58.7),
(-11.4, 56.8),
(-13.4, 54.4),
(-14.5, 50.8),
(-15.1, 45.2),
(-15.4, 37.0),
(-15.2, 28.4),
(-14.5, 20.0),
(-13.5, 11.9),
(-12.1, 4.1),
(-10.7, 0.7),
(-8.6, -0.8),
(-4.6, -1.7),
(-0.5, -2.0),
(4.0, -1.8),
(8.1, -0.9),
(10.7, 0.8),
(12.1, 4.5),
(13.5, 11.2),
(14.7, 19.4),
(15.4, 28.1),
(15.7, 37.2),
(15.5, 45.9),
(14.6, 52.0),
(13.0, 56.3),
(10.7, 59.8),
(8.0, 62.2),
(5.4, 63.7),
(2.8, 64.8),
(1.1, 66.5),
(0.3, 69.8),
(0.2, 74.9),
(0.2, 80.4),
]
xs: list[float] = []
for index, (x1, z1) in enumerate(outline):
x2, z2 = outline[(index + 1) % len(outline)]
if z1 == z2:
continue
if (z1 <= local_z < z2) or (z2 <= local_z < z1):
t = (local_z - z1) / (z2 - z1)
xs.append(x1 + (x2 - x1) * t)
xs.sort()
return [
(xs[index] * LOGO_X_SCALE, xs[index + 1] * LOGO_X_SCALE)
for index in range(0, len(xs) - 1, 2)
]
def _registered_mark_interval(z_pos: float) -> tuple[float, float] | None:
center_x = 14.2
center_z = 36.0
radius = 1.25
dy = z_pos - center_z
if abs(dy) > radius:
return None
half_width = radius * sqrt(max(0.0, 1.0 - dy * dy / (radius * radius)))
return (center_x - half_width, center_x + half_width)
def _cup_surface_point(arc_x: float, z_pos: float, radial_offset: float) -> tuple[float, float, float]:
radius = _radius_at_z(z_pos) + radial_offset
angle = arc_x / radius
return (radius * sin(angle), -radius * cos(angle), z_pos)
def _make_curved_decal_cell(
*,
label: str,
x_left_bottom: float,
x_right_bottom: float,
z_bottom: float,
x_left_top: float,
x_right_top: float,
z_top: float,
color: Color,
inner_offset: float = 0.16,
outer_offset: float = 0.78,
) -> object | None:
x_left = (x_left_bottom + x_left_top) / 2.0
x_right = (x_right_bottom + x_right_top) / 2.0
width = x_right - x_left
height = z_top - z_bottom
if width < 0.18 or height <= 0.0:
return None
z_center = (z_bottom + z_top) / 2.0
thickness = outer_offset - inner_offset
max_segment_width = 3.2
segment_count = max(1, int(width / max_segment_width + 0.999))
segment_parts = []
for segment_index in range(segment_count):
segment_left = x_left + width * segment_index / segment_count
segment_right = x_left + width * (segment_index + 1) / segment_count
segment_width = segment_right - segment_left
x_center = (segment_left + segment_right) / 2.0
segment_parts.append(
_make_surface_decal_box(
label=f"{label}_segment_{segment_index + 1:02d}",
x_center=x_center,
z_center=z_center,
width=segment_width + 0.04,
height=height + 0.035,
thickness=thickness,
radial_offset=inner_offset + thickness / 2.0,
color=color,
)
)
if len(segment_parts) == 1:
return segment_parts[0]
compound = Compound(obj=segment_parts, children=segment_parts, label=label)
compound.color = color
return compound
def _make_surface_decal_box(
*,
label: str,
x_center: float,
z_center: float,
width: float,
height: float,
thickness: float,
radial_offset: float,
color: Color,
) -> object:
radius = _radius_at_z(z_center) + radial_offset
angle = x_center / radius
center = _cup_surface_point(x_center, z_center, radial_offset)
box = Solid.make_box(width, thickness, height)
box = box.moved(Location((-width / 2.0, -thickness / 2.0, -height / 2.0)))
box = box.moved(Location(center, (0.0, 0.0, degrees(angle))))
return _set_metadata(box, label, color)
def gen_step():
"""Return a photo-inspired takeaway coffee cup model in millimeters."""
parts: list[object] = [
_make_paper_cup_body(),
_make_bottom_foot_ring(),
]
parts.extend(_make_lid_skirt_parts())
parts.extend(_make_lid_top())
parts.extend(_make_sip_feature())
parts.extend(_make_top_embossing())
parts.extend(_make_blue_logo())
return Compound(obj=parts, children=parts, label=MODEL_LABEL)

Some files were not shown because too many files have changed in this diff Show More