Files
cadSet/text-to-cad/models/robots/tom/v1/parts/servo_end_mount.py
T
2026-07-22 19:38:36 +08:00

1226 lines
45 KiB
Python

#!/usr/bin/env python3
"""
Generate the localized end mount.
The part is authored in the imported STS3250 local frame so assemblies can place
it with the same transform as the reference servo geometry.
Usage:
python STEP/servo_end_mount.py
"""
from __future__ import annotations
import math
import os
import sys
from dataclasses import dataclass
from pathlib import Path
# Keep CAD library caches inside the workspace to avoid permission issues.
V1_DIR = Path(__file__).resolve().parents[1]
TOM_DIR = V1_DIR.parent
_CACHE_HOME = V1_DIR / ".cache"
_CACHE_HOME.mkdir(parents=True, exist_ok=True)
os.environ.setdefault("XDG_CACHE_HOME", str(_CACHE_HOME))
os.environ.setdefault("EZDXF_CACHE_HOME", str(_CACHE_HOME / "ezdxf"))
import ezdxf
import build123d
from ezdxf.units import MM as DXF_UNIT_MM
if str(TOM_DIR) not in sys.path:
sys.path.insert(0, str(TOM_DIR))
from robot_common.keyed_connection import (
KEYED_CONNECTION_REF,
KEYED_CONNECTION_ROTATION_DEG,
cut_keyed_connection_x_aligned,
sample_keyed_connection_outline,
)
from robot_common.step_import import import_as_shape
CAD_DIR = Path(__file__).resolve().parent
PART_NAME = Path(__file__).stem
DISPLAY_NAME = "Servo End Mount"
SERVO_STEP = V1_DIR / "parts" / "imports" / "sts3250.step"
SENDCUTSEND_MIN_BEND_LINE_EDGE_CLEARANCE_MM = 25.4 * 0.255
SENDCUTSEND_SHEET_THICKNESS_MM = 25.4 * 0.063
CENTER_BRIDGE_BEND_LINE_EDGE_BUFFER_MM = 25.4 * 0.010
CENTER_BRIDGE_BEND_LINE_EDGE_TARGET_MM = (
SENDCUTSEND_MIN_BEND_LINE_EDGE_CLEARANCE_MM
+ CENTER_BRIDGE_BEND_LINE_EDGE_BUFFER_MM
)
BRACKET_FACE_CLEARANCE_MM = CENTER_BRIDGE_BEND_LINE_EDGE_TARGET_MM - SENDCUTSEND_SHEET_THICKNESS_MM
SENDCUTSEND_REFERENCE_SHEET_THICKNESS_MM = 25.4 * 0.080
SENDCUTSEND_DIE_REGION_RULE_SCALE = SENDCUTSEND_SHEET_THICKNESS_MM / SENDCUTSEND_REFERENCE_SHEET_THICKNESS_MM
SENDCUTSEND_EFFECTIVE_BEND_RADIUS_MM = 25.4 * 0.035
SENDCUTSEND_K_FACTOR = 0.48
SENDCUTSEND_BEND_DEDUCTION_90_MM = (2.0 * (SENDCUTSEND_EFFECTIVE_BEND_RADIUS_MM + SENDCUTSEND_SHEET_THICKNESS_MM)) - (
(math.pi * 0.5)
* (SENDCUTSEND_EFFECTIVE_BEND_RADIUS_MM + (SENDCUTSEND_K_FACTOR * SENDCUTSEND_SHEET_THICKNESS_MM))
)
SENDCUTSEND_DIE_WIDTH_MM = 25.4 * 0.472
SENDCUTSEND_HALF_DIE_WIDTH_MM = (0.5 * SENDCUTSEND_DIE_WIDTH_MM) * SENDCUTSEND_DIE_REGION_RULE_SCALE
SENDCUTSEND_MIN_FLANGE_FLAT_MM = SENDCUTSEND_MIN_BEND_LINE_EDGE_CLEARANCE_MM
SENDCUTSEND_MIN_FLANGE_FORMED_MM = (
SENDCUTSEND_MIN_FLANGE_FLAT_MM
+ SENDCUTSEND_EFFECTIVE_BEND_RADIUS_MM
+ SENDCUTSEND_SHEET_THICKNESS_MM
)
SENDCUTSEND_BEND_RELIEF_DEPTH_MM = 25.4 * 0.118
SENDCUTSEND_MIN_CORNER_RELIEF_DISTANCE_MM = 25.4 * 0.037
SENDCUTSEND_MIN_ANODIZE_SHORTEST_DIM_MM = 25.4
BEND_DIE_REGION_TOLERANCE_MM = 0.1
# The theoretical outer radius is inner radius + thickness, but the split tab
# geometry cannot accept the full value as a direct solid fillet. Clamp the
# modeled exterior bend to a stable radius that still reads as a formed bend.
BEND_OUTER_RADIUS_MM = min(2.0, SENDCUTSEND_EFFECTIVE_BEND_RADIUS_MM + SENDCUTSEND_SHEET_THICKNESS_MM)
FLANGE_FREE_CORNER_RADIUS_MM = 1.0
FLANGE_FREE_CORNER_SAMPLE_COUNT = 8
MOUNT_PLANE_CLEARANCE_MM = 0.25
MOUNT_HOLE_EDGE_X_WEB_MM = 1.55
MOUNT_HOLE_OUTBOARD_Z_WEB_MM = 2.15
MOUNT_HOLE_INBOARD_Z_WEB_MM = 2.15
MOUNT_HOLE_TRIMMED_OUTBOARD_Z_WEB_WARN_MM = 1.0
CUT_EXTENSION_MM = 2.0
MAX_FACE_TARGET_ERROR_MM = 1.0
FACE_TARGET_CENTERS_MM: dict[str, tuple[float, float, float]] = {
"end_face": (9.61, -7.65, 0.0),
"end_wrap_neg": (8.843, -7.65, -11.593),
"end_wrap_pos": (8.843, -7.65, 11.593),
"upper_top_neg": (-6.561, 6.4, -9.484),
"upper_top_pos": (-6.561, 6.4, 9.484),
"main_bottom": (-0.881, -25.6, 0.0),
}
@dataclass(frozen=True)
class BracketLayout:
z_min: float
z_max: float
top_holes: tuple[tuple[build123d.Vector, float], ...]
bottom_holes: tuple[tuple[build123d.Vector, float], ...]
top_tab_x_min: float
bottom_tab_x_min: float
bracket_face_inner_x: float
bracket_face_outer_x: float
top_tab_y_min: float
top_tab_y_max: float
bottom_tab_y_min: float
bottom_tab_y_max: float
tab_bands: tuple[tuple[float, float], ...]
bend_bands: tuple[tuple[float, float], ...]
center_bridge_x_min: float | None
front_face_center_y: float
outer_flush_gap: float
@dataclass(frozen=True)
class FlatPattern:
total_length_mm: float
width_mm: float
top_flat_length_mm: float
web_flat_start_mm: float
web_flat_end_mm: float
bottom_flat_start_mm: float
bend_line_positions_mm: tuple[float, float]
bend_line_segments_mm: tuple[tuple[float, float, float], ...]
cut_circles: tuple[tuple[float, float, float], ...]
cut_profile_centers: tuple[tuple[float, float], ...]
cut_segments: tuple[tuple[str, tuple[tuple[float, float], ...]], ...]
def _geom_type_name(shape: build123d.Shape) -> str:
geom_type = shape.geom_type
return geom_type.name if hasattr(geom_type, "name") else str(geom_type)
def _face_from_wire(
exterior: build123d.Wire,
interior_wires: list[build123d.Wire] | None = None,
) -> build123d.Face:
holes = list(interior_wires or [])
return build123d.Face(exterior, holes) if holes else build123d.Face(exterior)
def _make_cylinder_along(
*,
radius: float,
height: float,
origin: build123d.Vector,
direction: build123d.Vector,
) -> build123d.Solid:
return build123d.Solid.make_cylinder(
radius,
height,
plane=build123d.Plane(origin=origin, z_dir=direction),
)
def _vector_to_tuple(vector: build123d.Vector) -> tuple[float, float, float]:
return (vector.X, vector.Y, vector.Z)
def _make_box(*, x_min: float, x_max: float, y_min: float, y_max: float, z_min: float, z_max: float) -> build123d.Solid:
return build123d.Solid.make_box(
x_max - x_min,
y_max - y_min,
z_max - z_min,
plane=build123d.Plane(origin=build123d.Vector(x_min, y_min, z_min)),
)
def _assemble_wire(edges: list[build123d.Edge]) -> build123d.Wire:
wires = list(build123d.Wire.combine(edges))
if len(wires) != 1:
raise RuntimeError(f"Expected a single assembled wire, found {len(wires)}")
return wires[0]
def _arc_angles_xz(
*,
center_x: float,
center_z: float,
start_point: tuple[float, float],
end_point: tuple[float, float],
) -> tuple[float, float, build123d.AngularDirection]:
start_angle = math.degrees(math.atan2(start_point[1] - center_z, start_point[0] - center_x))
end_angle = math.degrees(math.atan2(end_point[1] - center_z, end_point[0] - center_x))
ccw_span = (end_angle - start_angle) % 360.0
if ccw_span <= 180.0:
return start_angle, end_angle, build123d.AngularDirection.COUNTER_CLOCKWISE
return start_angle, end_angle, build123d.AngularDirection.CLOCKWISE
def _make_arc_edge_xz(
*,
center_x: float,
center_z: float,
radius: float,
start_point: tuple[float, float],
end_point: tuple[float, float],
) -> build123d.Edge:
start_angle, end_angle, angular_direction = _arc_angles_xz(
center_x=center_x,
center_z=center_z,
start_point=start_point,
end_point=end_point,
)
return build123d.Edge.make_circle(
radius,
plane=build123d.Plane(origin=(center_x, 0.0, center_z), x_dir=(1.0, 0.0, 0.0), z_dir=(0.0, -1.0, 0.0)),
start_angle=start_angle,
end_angle=end_angle,
angular_direction=angular_direction,
)
def _make_wire_prism_y(
*,
edges_xz: list[build123d.Edge],
y_min: float,
y_max: float,
) -> build123d.Solid:
face = _face_from_wire(_assemble_wire(edges_xz))
solid = build123d.Solid.extrude(face, build123d.Vector(0.0, y_max - y_min, 0.0))
return solid.translate((0.0, y_min, 0.0))
def _make_rounded_flange_tab(
*,
x_min: float,
x_max: float,
y_min: float,
y_max: float,
z_min: float,
z_max: float,
radius: float,
) -> build123d.Shape:
radius = min(radius, 0.5 * (z_max - z_min), x_max - x_min)
if radius <= 1e-6:
return _make_box(x_min=x_min, x_max=x_max, y_min=y_min, y_max=y_max, z_min=z_min, z_max=z_max)
lower_tangent = (x_min + radius, z_min)
right_lower = (x_max, z_min)
right_upper = (x_max, z_max)
upper_tangent = (x_min + radius, z_max)
left_upper = (x_min, z_max - radius)
left_lower = (x_min, z_min + radius)
return _make_wire_prism_y(
edges_xz=[
build123d.Edge.make_line((lower_tangent[0], 0.0, lower_tangent[1]), (right_lower[0], 0.0, right_lower[1])),
build123d.Edge.make_line((right_lower[0], 0.0, right_lower[1]), (right_upper[0], 0.0, right_upper[1])),
build123d.Edge.make_line((right_upper[0], 0.0, right_upper[1]), (upper_tangent[0], 0.0, upper_tangent[1])),
_make_arc_edge_xz(
center_x=x_min + radius,
center_z=z_max - radius,
radius=radius,
start_point=upper_tangent,
end_point=left_upper,
),
build123d.Edge.make_line((left_upper[0], 0.0, left_upper[1]), (left_lower[0], 0.0, left_lower[1])),
_make_arc_edge_xz(
center_x=x_min + radius,
center_z=z_min + radius,
radius=radius,
start_point=left_lower,
end_point=lower_tangent,
),
],
y_min=y_min,
y_max=y_max,
)
def _find_face_near(shape: build123d.Shape, target: tuple[float, float, float], *, geom_type: str | None = None) -> build123d.Face:
target_vec = build123d.Vector(*target)
best: tuple[float, build123d.Face] | None = None
for face in shape.faces():
if geom_type is not None and _geom_type_name(face) != geom_type:
continue
dist = face.center().sub(target_vec).length
if best is None or dist < best[0]:
best = (dist, face)
if best is None:
raise RuntimeError(f"No planar face found near {target}")
if best[0] > MAX_FACE_TARGET_ERROR_MM:
raise RuntimeError(
f"Resolved face near {target} was {best[0]:.3f} mm away, which exceeds the tolerance"
)
return best[1]
def _wire_area(wire: build123d.Wire) -> float:
return abs(_face_from_wire(wire).area)
def _outer_wire_and_inner_wires(face: build123d.Face) -> tuple[build123d.Wire, list[build123d.Wire]]:
wires = list(face.wires())
if not wires:
raise RuntimeError("Face has no wires")
outer_wire = max(wires, key=_wire_area)
inner_wires = [wire for wire in wires if not wire.is_same(outer_wire)]
return outer_wire, inner_wires
def _extract_inner_hole_profiles(face: build123d.Face) -> list[tuple[build123d.Vector, float]]:
_outer_wire, inner_wires = _outer_wire_and_inner_wires(face)
profiles: list[tuple[build123d.Vector, float]] = []
for wire in inner_wires:
circle_edges = [edge for edge in wire.edges() if _geom_type_name(edge) == "CIRCLE"]
if not circle_edges:
continue
center = _face_from_wire(wire).center()
radius = max(edge.radius for edge in circle_edges)
profiles.append((center, radius))
return profiles
def _select_near_end_holes(
profiles: list[tuple[build123d.Vector, float]],
*,
count: int,
) -> list[tuple[build123d.Vector, float]]:
if len(profiles) < count:
raise RuntimeError(f"Expected at least {count} mount holes, found {len(profiles)}")
selected = sorted(profiles, key=lambda item: (item[0].X, abs(item[0].Z)), reverse=True)[:count]
selected.sort(key=lambda item: item[0].Z)
return selected
def _cut_mount_holes(
body: build123d.Shape,
*,
profiles: list[tuple[build123d.Vector, float]],
start_y: float,
direction_y: float,
) -> build123d.Shape:
hole_depth = SENDCUTSEND_SHEET_THICKNESS_MM + (2.0 * CUT_EXTENSION_MM)
for center, radius in profiles:
cutter = _make_cylinder_along(
radius=radius,
height=hole_depth,
origin=build123d.Vector(center.X, start_y, center.Z),
direction=build123d.Vector(0.0, direction_y, 0.0),
)
body = body.cut(cutter)
return body
def _cut_front_face_connection_frame(
body: build123d.Shape,
*,
face_center_y: float,
x_max: float,
) -> build123d.Shape:
body_bb = body.bounding_box()
return cut_keyed_connection_x_aligned(
body,
center=build123d.Vector(x_max, face_center_y, 0.0),
start_x=x_max + CUT_EXTENSION_MM,
direction_x=-1.0,
thickness_mm=body_bb.size.X,
cut_extension_mm=CUT_EXTENSION_MM,
)
def _apply_bends_and_end_rounds(
bracket: build123d.Shape,
*,
bracket_face_inner_x: float,
bracket_face_outer_x: float,
top_tab_x_min: float,
bottom_tab_x_min: float,
top_tab_y_min: float,
top_tab_y_max: float,
bottom_tab_y_min: float,
bottom_tab_y_max: float,
bend_bands: tuple[tuple[float, float], ...],
) -> build123d.Shape:
return bracket
def _min_mount_hole_distance_to_bend_line(
profiles: list[tuple[build123d.Vector, float]],
*,
bend_line_x: float,
) -> float:
return min(abs(center.X - bend_line_x) for center, _radius in profiles)
def _merge_z_bands(
bands: list[tuple[float, float]] | tuple[tuple[float, float], ...],
*,
tolerance: float = 1e-6,
) -> tuple[tuple[float, float], ...]:
merged: list[tuple[float, float]] = []
for start, end in sorted(bands, key=lambda band: band[0]):
if end - start <= tolerance:
continue
if not merged or start > merged[-1][1] + tolerance:
merged.append((start, end))
continue
merged[-1] = (merged[-1][0], max(merged[-1][1], end))
return tuple(merged)
def _build_layout(
servo_shape: build123d.Shape,
*,
include_top_center_bridge: bool = True,
) -> BracketLayout:
faces = {
"end_face": _find_face_near(servo_shape, FACE_TARGET_CENTERS_MM["end_face"], geom_type="PLANE"),
"end_wrap_neg": _find_face_near(servo_shape, FACE_TARGET_CENTERS_MM["end_wrap_neg"]),
"end_wrap_pos": _find_face_near(servo_shape, FACE_TARGET_CENTERS_MM["end_wrap_pos"]),
"upper_top_neg": _find_face_near(servo_shape, FACE_TARGET_CENTERS_MM["upper_top_neg"], geom_type="PLANE"),
"upper_top_pos": _find_face_near(servo_shape, FACE_TARGET_CENTERS_MM["upper_top_pos"], geom_type="PLANE"),
"main_bottom": _find_face_near(servo_shape, FACE_TARGET_CENTERS_MM["main_bottom"], geom_type="PLANE"),
}
end_face_bb = faces["end_face"].bounding_box()
end_wrap_neg_bb = faces["end_wrap_neg"].bounding_box()
end_wrap_pos_bb = faces["end_wrap_pos"].bounding_box()
z_min = min(
end_face_bb.min.Z,
end_wrap_neg_bb.min.Z,
end_wrap_pos_bb.min.Z,
)
z_max = max(
end_face_bb.max.Z,
end_wrap_neg_bb.max.Z,
end_wrap_pos_bb.max.Z,
)
top_holes = _select_near_end_holes(_extract_inner_hole_profiles(faces["upper_top_neg"]), count=1)
top_holes += _select_near_end_holes(_extract_inner_hole_profiles(faces["upper_top_pos"]), count=1)
top_holes.sort(key=lambda item: item[0].Z)
bottom_holes = _select_near_end_holes(_extract_inner_hole_profiles(faces["main_bottom"]), count=2)
top_mount_y = sum(center.Y for center, _radius in top_holes) / len(top_holes)
bottom_mount_y = sum(center.Y for center, _radius in bottom_holes) / len(bottom_holes)
top_tab_y_min = top_mount_y + MOUNT_PLANE_CLEARANCE_MM
top_tab_y_max = top_tab_y_min + SENDCUTSEND_SHEET_THICKNESS_MM
bottom_tab_y_max = bottom_mount_y - MOUNT_PLANE_CLEARANCE_MM
bottom_tab_y_min = bottom_tab_y_max - SENDCUTSEND_SHEET_THICKNESS_MM
side_profiles = {
"neg": [top_holes[0], bottom_holes[0]],
"pos": [top_holes[1], bottom_holes[1]],
}
tab_bands: list[tuple[float, float]] = []
for top_profile, bottom_profile in side_profiles.values():
profiles = (top_profile, bottom_profile)
if sum(center.Z for center, _radius in profiles) < 0.0:
z_band_min = min(center.Z - radius - MOUNT_HOLE_OUTBOARD_Z_WEB_MM for center, radius in profiles)
z_band_max = max(center.Z + radius + MOUNT_HOLE_INBOARD_Z_WEB_MM for center, radius in profiles)
else:
z_band_min = min(center.Z - radius - MOUNT_HOLE_INBOARD_Z_WEB_MM for center, radius in profiles)
z_band_max = max(center.Z + radius + MOUNT_HOLE_OUTBOARD_Z_WEB_MM for center, radius in profiles)
z_band_min = max(z_band_min, z_min)
z_band_max = min(z_band_max, z_max)
tab_bands.append((z_band_min, z_band_max))
sorted_tab_bands = sorted(tab_bands, key=lambda band: band[0])
bend_bands = _merge_z_bands(sorted_tab_bands)
top_tab_x_min = min(center.X - radius - MOUNT_HOLE_EDGE_X_WEB_MM for center, radius in top_holes)
bottom_tab_x_min = min(center.X - radius - MOUNT_HOLE_EDGE_X_WEB_MM for center, radius in bottom_holes)
bracket_face_clearance = BRACKET_FACE_CLEARANCE_MM
bracket_face_inner_x = end_face_bb.max.X + bracket_face_clearance
bracket_face_outer_x = bracket_face_inner_x + SENDCUTSEND_SHEET_THICKNESS_MM
center_bridge_x_min: float | None = None
if include_top_center_bridge and len(sorted_tab_bands) >= 2:
center_gap_z_min = sorted_tab_bands[0][1]
center_gap_z_max = sorted_tab_bands[-1][0]
if center_gap_z_max - center_gap_z_min > 1e-3:
candidate_center_bridge_x_min = max(
bracket_face_outer_x - CENTER_BRIDGE_BEND_LINE_EDGE_TARGET_MM,
top_tab_x_min,
)
if candidate_center_bridge_x_min < bracket_face_outer_x - 1e-3:
center_bridge_x_min = candidate_center_bridge_x_min
bend_bands = _merge_z_bands((*sorted_tab_bands, (center_gap_z_min, center_gap_z_max)))
min_hole_to_bend_line = min(
_min_mount_hole_distance_to_bend_line(top_holes, bend_line_x=bracket_face_inner_x),
_min_mount_hole_distance_to_bend_line(bottom_holes, bend_line_x=bracket_face_inner_x),
)
if min_hole_to_bend_line + BEND_DIE_REGION_TOLERANCE_MM < SENDCUTSEND_HALF_DIE_WIDTH_MM:
raise RuntimeError(
"Mount holes are too close to the bend line for the configured sheet-metal bend rule: "
f"{min_hole_to_bend_line:.3f} mm < {SENDCUTSEND_HALF_DIE_WIDTH_MM:.3f} mm"
)
front_face_center_y = 0.5 * (bottom_tab_y_min + top_tab_y_max)
outer_flush_gap = min(
min(
z_max - (center.Z + radius) if center.Z >= 0.0 else (center.Z - radius) - z_min
for center, radius in top_holes
),
min(
z_max - (center.Z + radius) if center.Z >= 0.0 else (center.Z - radius) - z_min
for center, radius in bottom_holes
),
)
if outer_flush_gap <= 0.0:
raise RuntimeError(f"Trimmed mount width cuts into a mounting hole: outboard web {outer_flush_gap:.3f} mm")
return BracketLayout(
z_min=z_min,
z_max=z_max,
top_holes=tuple(top_holes),
bottom_holes=tuple(bottom_holes),
top_tab_x_min=top_tab_x_min,
bottom_tab_x_min=bottom_tab_x_min,
bracket_face_inner_x=bracket_face_inner_x,
bracket_face_outer_x=bracket_face_outer_x,
top_tab_y_min=top_tab_y_min,
top_tab_y_max=top_tab_y_max,
bottom_tab_y_min=bottom_tab_y_min,
bottom_tab_y_max=bottom_tab_y_max,
tab_bands=tuple(sorted_tab_bands),
bend_bands=bend_bands,
center_bridge_x_min=center_bridge_x_min,
front_face_center_y=front_face_center_y,
outer_flush_gap=outer_flush_gap,
)
def build_bracket(
servo_shape: build123d.Shape,
*,
include_top_center_bridge: bool = True,
) -> tuple[build123d.Shape, BracketLayout]:
layout = _build_layout(servo_shape, include_top_center_bridge=include_top_center_bridge)
bracket: build123d.Shape = _make_box(
x_min=layout.bracket_face_inner_x,
x_max=layout.bracket_face_outer_x,
y_min=layout.bottom_tab_y_min,
y_max=layout.top_tab_y_max,
z_min=layout.z_min,
z_max=layout.z_max,
)
for z_band_min, z_band_max in layout.tab_bands:
bracket = bracket.fuse(
_make_rounded_flange_tab(
x_min=layout.top_tab_x_min,
x_max=layout.bracket_face_outer_x,
y_min=layout.top_tab_y_min,
y_max=layout.top_tab_y_max,
z_min=z_band_min,
z_max=z_band_max,
radius=FLANGE_FREE_CORNER_RADIUS_MM,
)
)
if layout.center_bridge_x_min is not None and len(layout.tab_bands) >= 2:
center_gap_z_min = layout.tab_bands[0][1]
center_gap_z_max = layout.tab_bands[-1][0]
bracket = bracket.fuse(
_make_box(
x_min=layout.center_bridge_x_min,
x_max=layout.bracket_face_outer_x,
y_min=layout.top_tab_y_min,
y_max=layout.top_tab_y_max,
z_min=center_gap_z_min,
z_max=center_gap_z_max,
)
)
bracket = bracket.fuse(
_make_rounded_flange_tab(
x_min=layout.bottom_tab_x_min,
x_max=layout.bracket_face_outer_x,
y_min=layout.bottom_tab_y_min,
y_max=layout.bottom_tab_y_max,
z_min=layout.z_min,
z_max=layout.z_max,
radius=FLANGE_FREE_CORNER_RADIUS_MM,
)
)
bracket = _apply_bends_and_end_rounds(
bracket,
bracket_face_inner_x=layout.bracket_face_inner_x,
bracket_face_outer_x=layout.bracket_face_outer_x,
top_tab_x_min=layout.top_tab_x_min,
bottom_tab_x_min=layout.bottom_tab_x_min,
top_tab_y_min=layout.top_tab_y_min,
top_tab_y_max=layout.top_tab_y_max,
bottom_tab_y_min=layout.bottom_tab_y_min,
bottom_tab_y_max=layout.bottom_tab_y_max,
bend_bands=layout.bend_bands,
)
bracket = _cut_front_face_connection_frame(
bracket,
face_center_y=layout.front_face_center_y,
x_max=layout.bracket_face_outer_x,
)
bracket = _cut_mount_holes(
bracket,
profiles=list(layout.top_holes),
start_y=layout.top_tab_y_max + CUT_EXTENSION_MM,
direction_y=-1.0,
)
bracket = _cut_mount_holes(
bracket,
profiles=list(layout.bottom_holes),
start_y=layout.bottom_tab_y_min - CUT_EXTENSION_MM,
direction_y=1.0,
)
return bracket, layout
def _sendcutsend_outside_setback_90_mm() -> float:
return SENDCUTSEND_EFFECTIVE_BEND_RADIUS_MM + SENDCUTSEND_SHEET_THICKNESS_MM
def _sendcutsend_bend_allowance_90_mm() -> float:
return (math.pi * 0.5) * (
SENDCUTSEND_EFFECTIVE_BEND_RADIUS_MM + (SENDCUTSEND_K_FACTOR * SENDCUTSEND_SHEET_THICKNESS_MM)
)
def _validate_sendcutsend_bend_rule() -> None:
bend_deduction_error = abs(
SENDCUTSEND_BEND_DEDUCTION_90_MM
- ((2.0 * _sendcutsend_outside_setback_90_mm()) - _sendcutsend_bend_allowance_90_mm())
)
if bend_deduction_error > 0.05:
raise RuntimeError(
"SendCutSend bend-rule constants are inconsistent: "
f"bend deduction error {bend_deduction_error:.3f} mm is too large"
)
def _min_flange_outside_length(layout: BracketLayout) -> float:
return min(
layout.bracket_face_outer_x - layout.top_tab_x_min,
layout.bracket_face_outer_x - layout.bottom_tab_x_min,
)
def _supported_bend_fraction(layout: BracketLayout) -> float:
bend_width = layout.z_max - layout.z_min
if bend_width <= 0.0:
return 0.0
supported_width = sum(z_band_max - z_band_min for z_band_min, z_band_max in layout.bend_bands)
return supported_width / bend_width
def _min_bend_segment_length(layout: BracketLayout) -> float:
return min(z_band_max - z_band_min for z_band_min, z_band_max in layout.bend_bands)
def _center_bridge_z_range(layout: BracketLayout) -> tuple[float, float] | None:
if layout.center_bridge_x_min is None or len(layout.tab_bands) < 2:
return None
z_min = layout.tab_bands[0][1]
z_max = layout.tab_bands[-1][0]
if z_max - z_min <= 1e-6:
return None
return z_min, z_max
def _center_bridge_formed_length(layout: BracketLayout) -> float | None:
if layout.center_bridge_x_min is None:
return None
return layout.bracket_face_inner_x - layout.center_bridge_x_min
def _center_bridge_bend_edge_distance(layout: BracketLayout) -> float | None:
if layout.center_bridge_x_min is None:
return None
return layout.bracket_face_outer_x - layout.center_bridge_x_min
def _center_bridge_flat_length(layout: BracketLayout, flat_pattern: FlatPattern) -> float | None:
formed_length = _center_bridge_formed_length(layout)
if formed_length is None:
return None
# Keep the flat-pattern edge at least the required distance from the drawn
# bend line while preserving the formed center-bridge length in the 3D part.
bend_line_clearance_flat_length = (
CENTER_BRIDGE_BEND_LINE_EDGE_TARGET_MM
- (flat_pattern.bend_line_positions_mm[0] - flat_pattern.top_flat_length_mm)
)
return max(formed_length, bend_line_clearance_flat_length)
def _center_bridge_flat_bend_line_to_edge_distance(layout: BracketLayout, flat_pattern: FlatPattern) -> float | None:
flat_length = _center_bridge_flat_length(layout, flat_pattern)
if flat_length is None:
return None
return flat_length + (flat_pattern.bend_line_positions_mm[0] - flat_pattern.top_flat_length_mm)
def _build_flat_pattern(
layout: BracketLayout,
) -> FlatPattern:
_validate_sendcutsend_bend_rule()
outside_setback = _sendcutsend_outside_setback_90_mm()
bend_allowance = _sendcutsend_bend_allowance_90_mm()
top_outside_length = layout.bracket_face_outer_x - layout.top_tab_x_min
bottom_outside_length = layout.bracket_face_outer_x - layout.bottom_tab_x_min
top_flat_length = top_outside_length - outside_setback
bottom_flat_length = bottom_outside_length - outside_setback
if top_flat_length <= 0.0 or bottom_flat_length <= 0.0:
raise RuntimeError(
"Flat flange length implied by the bend rule is too short: "
f"top={top_flat_length:.3f} mm, bottom={bottom_flat_length:.3f} mm"
)
flange_outside_min = _min_flange_outside_length(layout)
flange_flat_min = flange_outside_min - outside_setback
center_bridge_bend_edge_distance = _center_bridge_bend_edge_distance(layout)
if (
center_bridge_bend_edge_distance is not None
and center_bridge_bend_edge_distance < CENTER_BRIDGE_BEND_LINE_EDGE_TARGET_MM
):
raise RuntimeError(
"Top center bridge bend-edge distance is below the configured clearance target: "
f"{center_bridge_bend_edge_distance:.3f} mm < {CENTER_BRIDGE_BEND_LINE_EDGE_TARGET_MM:.3f} mm"
)
if flange_outside_min < SENDCUTSEND_MIN_FLANGE_FORMED_MM:
raise RuntimeError(
"Formed flange length is below the configured bend-rule minimum: "
f"{flange_outside_min:.3f} mm < {SENDCUTSEND_MIN_FLANGE_FORMED_MM:.3f} mm"
)
if flange_flat_min < SENDCUTSEND_MIN_FLANGE_FLAT_MM:
raise RuntimeError(
"Flat flange length is below the configured bend-rule minimum: "
f"{flange_flat_min:.3f} mm < {SENDCUTSEND_MIN_FLANGE_FLAT_MM:.3f} mm"
)
top_web_tangent_y = layout.top_tab_y_min - SENDCUTSEND_EFFECTIVE_BEND_RADIUS_MM
bottom_web_tangent_y = layout.bottom_tab_y_max + SENDCUTSEND_EFFECTIVE_BEND_RADIUS_MM
web_flat_length = top_web_tangent_y - bottom_web_tangent_y
if web_flat_length <= 0.0:
raise RuntimeError(f"Web flat length implied by the bend rule is too short: {web_flat_length:.3f} mm")
web_flat_start = top_flat_length + bend_allowance
web_flat_end = web_flat_start + web_flat_length
bottom_flat_start = web_flat_end + bend_allowance
total_length = bottom_flat_start + bottom_flat_length
width = layout.z_max - layout.z_min
top_bend_line_x = top_flat_length + (0.5 * bend_allowance)
bottom_bend_line_x = web_flat_end + (0.5 * bend_allowance)
bend_line_segments = (
(top_bend_line_x, 0.0, width),
(bottom_bend_line_x, 0.0, width),
)
cut_circles: list[tuple[float, float, float]] = []
for center, radius in layout.top_holes:
cut_circles.append(
(
center.X - layout.top_tab_x_min,
center.Z - layout.z_min,
radius,
)
)
for center, radius in layout.bottom_holes:
cut_circles.append(
(
total_length - (center.X - layout.bottom_tab_x_min),
center.Z - layout.z_min,
radius,
)
)
cut_profile_centers = (
(
web_flat_start + (top_web_tangent_y - layout.front_face_center_y),
-layout.z_min,
),
)
return FlatPattern(
total_length_mm=total_length,
width_mm=width,
top_flat_length_mm=top_flat_length,
web_flat_start_mm=web_flat_start,
web_flat_end_mm=web_flat_end,
bottom_flat_start_mm=bottom_flat_start,
bend_line_positions_mm=(top_bend_line_x, bottom_bend_line_x),
bend_line_segments_mm=bend_line_segments,
cut_circles=tuple(cut_circles),
cut_profile_centers=cut_profile_centers,
cut_segments=(),
)
def _flat_rectangles(layout: BracketLayout, flat_pattern: FlatPattern) -> tuple[tuple[float, float, float, float], ...]:
rectangles: list[tuple[float, float, float, float]] = [
(
flat_pattern.top_flat_length_mm,
0.0,
flat_pattern.bottom_flat_start_mm,
flat_pattern.width_mm,
)
]
for z_band_min, z_band_max in layout.tab_bands:
y_min = z_band_min - layout.z_min
y_max = z_band_max - layout.z_min
rectangles.append((0.0, y_min, flat_pattern.top_flat_length_mm, y_max))
center_bridge_z_range = _center_bridge_z_range(layout)
center_bridge_flat_length = _center_bridge_flat_length(layout, flat_pattern)
if center_bridge_z_range is not None and center_bridge_flat_length is not None:
y_min = center_bridge_z_range[0] - layout.z_min
y_max = center_bridge_z_range[1] - layout.z_min
x_min = flat_pattern.top_flat_length_mm - center_bridge_flat_length
rectangles.append((x_min, y_min, flat_pattern.top_flat_length_mm, y_max))
rectangles.append((flat_pattern.bottom_flat_start_mm, 0.0, flat_pattern.total_length_mm, flat_pattern.width_mm))
return tuple(rectangle for rectangle in rectangles if rectangle[2] - rectangle[0] > 1e-6 and rectangle[3] - rectangle[1] > 1e-6)
def _flange_free_corner_points(
layout: BracketLayout,
flat_pattern: FlatPattern,
) -> set[tuple[float, float]]:
points: set[tuple[float, float]] = set()
for z_band_min, z_band_max in layout.tab_bands:
flat_z_min = z_band_min - layout.z_min
flat_z_max = z_band_max - layout.z_min
points.add((0.0, flat_z_min))
points.add((0.0, flat_z_max))
points.add((flat_pattern.total_length_mm, 0.0))
points.add((flat_pattern.total_length_mm, flat_pattern.width_mm))
return points
def _sample_short_arc_2d(
*,
center: tuple[float, float],
start: tuple[float, float],
end: tuple[float, float],
count: int,
) -> list[tuple[float, float]]:
start_angle = math.atan2(start[1] - center[1], start[0] - center[0])
end_angle = math.atan2(end[1] - center[1], end[0] - center[0])
ccw_span = (end_angle - start_angle) % (2.0 * math.pi)
if ccw_span <= math.pi:
span = ccw_span
else:
span = -((start_angle - end_angle) % (2.0 * math.pi))
samples = max(count, 2)
radius = math.hypot(start[0] - center[0], start[1] - center[1])
return [
(
center[0] + (radius * math.cos(start_angle + (span * index / (samples - 1)))),
center[1] + (radius * math.sin(start_angle + (span * index / (samples - 1)))),
)
for index in range(samples)
]
def _rounded_outline_loop_points(
loop: list[tuple[float, float]],
*,
rounded_corners: set[tuple[float, float]],
radius: float,
) -> list[tuple[float, float]]:
if radius <= 1e-6:
return loop
rounded_loop: list[tuple[float, float]] = []
for index, point in enumerate(loop):
if point not in rounded_corners:
rounded_loop.append(point)
continue
previous_point = loop[index - 1]
next_point = loop[(index + 1) % len(loop)]
prev_vec = (previous_point[0] - point[0], previous_point[1] - point[1])
next_vec = (next_point[0] - point[0], next_point[1] - point[1])
prev_len = math.hypot(*prev_vec)
next_len = math.hypot(*next_vec)
corner_radius = min(radius, 0.5 * prev_len, 0.5 * next_len)
if corner_radius <= 1e-6:
rounded_loop.append(point)
continue
prev_unit = (prev_vec[0] / prev_len, prev_vec[1] / prev_len)
next_unit = (next_vec[0] / next_len, next_vec[1] / next_len)
tangent_in = (
point[0] + (prev_unit[0] * corner_radius),
point[1] + (prev_unit[1] * corner_radius),
)
tangent_out = (
point[0] + (next_unit[0] * corner_radius),
point[1] + (next_unit[1] * corner_radius),
)
center = (
point[0] + ((prev_unit[0] + next_unit[0]) * corner_radius),
point[1] + ((prev_unit[1] + next_unit[1]) * corner_radius),
)
arc_points = _sample_short_arc_2d(
center=center,
start=tangent_in,
end=tangent_out,
count=FLANGE_FREE_CORNER_SAMPLE_COUNT,
)
rounded_loop.extend(arc_points)
return rounded_loop
def _outline_loops_from_rectangles(
rectangles: tuple[tuple[float, float, float, float], ...],
) -> list[list[tuple[float, float]]]:
x_coords = sorted({coord for x0, _y0, x1, _y1 in rectangles for coord in (x0, x1)})
y_coords = sorted({coord for _x0, y0, _x1, y1 in rectangles for coord in (y0, y1)})
filled_cells: set[tuple[int, int]] = set()
for x_index in range(len(x_coords) - 1):
for y_index in range(len(y_coords) - 1):
x_mid = 0.5 * (x_coords[x_index] + x_coords[x_index + 1])
y_mid = 0.5 * (y_coords[y_index] + y_coords[y_index + 1])
if any(x0 < x_mid < x1 and y0 < y_mid < y1 for x0, y0, x1, y1 in rectangles):
filled_cells.add((x_index, y_index))
edges: dict[tuple[tuple[float, float], tuple[float, float]], None] = {}
def add_edge(start: tuple[float, float], end: tuple[float, float]) -> None:
reverse = (end, start)
if reverse in edges:
del edges[reverse]
return
edges[(start, end)] = None
for x_index, y_index in filled_cells:
x0 = x_coords[x_index]
x1 = x_coords[x_index + 1]
y0 = y_coords[y_index]
y1 = y_coords[y_index + 1]
add_edge((x0, y0), (x1, y0))
add_edge((x1, y0), (x1, y1))
add_edge((x1, y1), (x0, y1))
add_edge((x0, y1), (x0, y0))
outgoing: dict[tuple[float, float], list[tuple[float, float]]] = {}
for start, end in edges:
outgoing.setdefault(start, []).append(end)
loops: list[list[tuple[float, float]]] = []
while outgoing:
start = min(outgoing)
loop = [start]
current = start
while True:
next_points = outgoing.get(current)
if not next_points:
raise RuntimeError("Flat-pattern outline could not be assembled from panel rectangles")
next_point = next_points.pop()
if not next_points:
del outgoing[current]
if next_point == start:
break
loop.append(next_point)
current = next_point
area = _polygon_area(loop)
if area < 0.0:
loop.reverse()
loops.append(loop)
return loops
def _polygon_area(points: list[tuple[float, float]]) -> float:
return 0.5 * sum(
(points[index][0] * points[(index + 1) % len(points)][1])
- (points[(index + 1) % len(points)][0] * points[index][1])
for index in range(len(points))
)
def _add_reference_arc(
msp: ezdxf.layouts.Modelspace,
*,
start: tuple[float, float],
midpoint: tuple[float, float],
end: tuple[float, float],
) -> None:
from ezdxf.math import ConstructionArc
arc = ConstructionArc.from_3p(start, end, def_point=midpoint, ccw=True)
center = (arc.center.x, arc.center.y)
start_angle = math.atan2(start[1] - center[1], start[0] - center[0])
mid_angle = math.atan2(midpoint[1] - center[1], midpoint[0] - center[0])
end_angle = math.atan2(end[1] - center[1], end[0] - center[0])
ccw_sweep = (end_angle - start_angle) % (2.0 * math.pi)
mid_sweep = (mid_angle - start_angle) % (2.0 * math.pi)
if mid_sweep <= ccw_sweep + 1e-6:
msp.add_arc(
center=center,
radius=arc.radius,
start_angle=math.degrees(start_angle),
end_angle=math.degrees(end_angle),
dxfattribs={"layer": "CUT"},
)
return
msp.add_arc(
center=center,
radius=arc.radius,
start_angle=math.degrees(end_angle),
end_angle=math.degrees(start_angle),
dxfattribs={"layer": "CUT"},
)
def _build_flat_pattern_dxf(flat_pattern: FlatPattern, layout: BracketLayout) -> ezdxf.document.Drawing:
doc = ezdxf.new("R2010", setup=True)
doc.units = DXF_UNIT_MM
if "CUT" not in doc.layers:
doc.layers.add("CUT")
if "BEND" not in doc.layers:
doc.layers.add("BEND", linetype="DASHED")
msp = doc.modelspace()
rounded_corners = _flange_free_corner_points(layout, flat_pattern)
for loop in _outline_loops_from_rectangles(_flat_rectangles(layout, flat_pattern)):
rounded_loop = _rounded_outline_loop_points(
loop,
rounded_corners=rounded_corners,
radius=FLANGE_FREE_CORNER_RADIUS_MM,
)
for index, point in enumerate(rounded_loop):
msp.add_line(point, rounded_loop[(index + 1) % len(rounded_loop)], dxfattribs={"layer": "CUT"})
for x, y, cut_radius in flat_pattern.cut_circles:
msp.add_circle((x, y), cut_radius, dxfattribs={"layer": "CUT"})
for center_x, center_y in flat_pattern.cut_profile_centers:
msp.add_lwpolyline(
sample_keyed_connection_outline(center_u=center_x, center_v=center_y, points_per_arc=16),
format="xy",
close=True,
dxfattribs={"layer": "CUT"},
)
for segment_type, points in flat_pattern.cut_segments:
if segment_type == "LINE":
msp.add_line(points[0], points[-1], dxfattribs={"layer": "CUT"})
elif segment_type == "ARC":
_add_reference_arc(msp, start=points[0], midpoint=points[1], end=points[-1])
else:
raise RuntimeError(f"Unsupported flat-pattern cut segment type: {segment_type}")
for bend_line_x, bend_y_min, bend_y_max in flat_pattern.bend_line_segments_mm:
msp.add_line(
(bend_line_x, bend_y_min),
(bend_line_x, bend_y_max),
dxfattribs={"layer": "BEND", "linetype": "DASHED"},
)
return doc
def build_step(
*,
include_top_center_bridge: bool = True,
) -> build123d.Shape:
CAD_DIR.mkdir(parents=True, exist_ok=True)
if not SERVO_STEP.exists():
raise FileNotFoundError(f"Missing STS3250 servo STEP: {SERVO_STEP}")
servo_shape = import_as_shape(SERVO_STEP)
bracket, layout = build_bracket(servo_shape, include_top_center_bridge=include_top_center_bridge)
flat_pattern = _build_flat_pattern(layout)
solids = bracket.solids()
if len(solids) != 1:
raise RuntimeError(f"Expected one connected bracket solid, found {len(solids)}")
intersection_volume = servo_shape.intersect(bracket).volume
if intersection_volume > 1e-3:
raise RuntimeError(f"Bracket intersects the servo by {intersection_volume:.6f} mm^3")
bracket.label = PART_NAME
bracket_bb = bracket.bounding_box()
print(
"Bracket envelope "
f"X={bracket_bb.size.X:.3f} mm, "
f"Y={bracket_bb.size.Y:.3f} mm, "
f"Z={bracket_bb.size.Z:.3f} mm"
)
print(
"SendCutSend setup "
f"material=5052-H32, thickness={SENDCUTSEND_SHEET_THICKNESS_MM:.3f} mm, "
f"bend_radius={SENDCUTSEND_EFFECTIVE_BEND_RADIUS_MM:.3f} mm, "
f"k_factor={SENDCUTSEND_K_FACTOR:.2f}, "
f"bend_deduction_90={SENDCUTSEND_BEND_DEDUCTION_90_MM:.3f} mm, "
f"anodize_min_short_dim={SENDCUTSEND_MIN_ANODIZE_SHORTEST_DIM_MM:.3f} mm"
)
sendcutsend_checks = (
"SendCutSend checks "
f"hole_to_bend_min={min(abs(center.X - layout.bracket_face_inner_x) for center, _radius in [*layout.top_holes, *layout.bottom_holes]):.3f} mm, "
f"bend_segment_min={_min_bend_segment_length(layout):.3f} mm, "
f"bend_support={_supported_bend_fraction(layout):.1%}, "
f"outboard_hole_edge_web={layout.outer_flush_gap:.3f} mm, "
f"flange_free_corner_radius={FLANGE_FREE_CORNER_RADIUS_MM:.3f} mm"
)
print(sendcutsend_checks)
if layout.outer_flush_gap < MOUNT_HOLE_TRIMMED_OUTBOARD_Z_WEB_WARN_MM:
print(
"Warning: trimmed outer side leaves a small hole edge web: "
f"{layout.outer_flush_gap:.3f} mm < {MOUNT_HOLE_TRIMMED_OUTBOARD_Z_WEB_WARN_MM:.3f} mm"
)
end_face_x = _find_face_near(servo_shape, FACE_TARGET_CENTERS_MM["end_face"], geom_type="PLANE").bounding_box().max.X
print(f"Bracket face clearance: {layout.bracket_face_inner_x - end_face_x:.3f} mm")
center_bridge_bend_edge_distance = _center_bridge_bend_edge_distance(layout)
center_bridge_flat_clearance = _center_bridge_flat_bend_line_to_edge_distance(layout, flat_pattern)
if center_bridge_bend_edge_distance is not None and center_bridge_flat_clearance is not None:
print(
"Top center bridge "
f"bend_edge_distance={center_bridge_bend_edge_distance:.3f} mm, "
f"flat_bend_line_to_edge={center_bridge_flat_clearance:.3f} mm"
)
print(f"Bracket face inner X: {layout.bracket_face_inner_x:.3f} mm")
print(
"Front-face keyed connection "
f"ref={KEYED_CONNECTION_REF}, "
f"center=(x=max, y={layout.front_face_center_y:.6f}, z=0.000000) mm, "
f"rotation={KEYED_CONNECTION_ROTATION_DEG:.1f} deg, "
"profiles=1"
)
print(
"Top mount holes used (x, y, z) mm: "
+ ", ".join(str(tuple(round(value, 6) for value in _vector_to_tuple(center))) for center, _radius in layout.top_holes)
)
print(
"Bottom mount holes used (x, y, z) mm: "
+ ", ".join(str(tuple(round(value, 6) for value in _vector_to_tuple(center))) for center, _radius in layout.bottom_holes)
)
print(f"Servo/bracket interference volume (mm^3): {intersection_volume:.6f}")
return bracket
def build_dxf(
*,
include_top_center_bridge: bool = True,
) -> ezdxf.document.Drawing:
CAD_DIR.mkdir(parents=True, exist_ok=True)
if not SERVO_STEP.exists():
raise FileNotFoundError(f"Missing STS3250 servo STEP: {SERVO_STEP}")
servo_shape = import_as_shape(SERVO_STEP)
layout = _build_layout(servo_shape, include_top_center_bridge=include_top_center_bridge)
flat_pattern = _build_flat_pattern(layout)
doc = _build_flat_pattern_dxf(flat_pattern, layout)
outside_setback = _sendcutsend_outside_setback_90_mm()
flange_outside_min = _min_flange_outside_length(layout)
print(
"Flat pattern "
f"length={flat_pattern.total_length_mm:.3f} mm, "
f"width={flat_pattern.width_mm:.3f} mm, "
f"bend_lines=({flat_pattern.bend_line_positions_mm[0]:.3f}, {flat_pattern.bend_line_positions_mm[1]:.3f}) mm, "
f"bend_segments={len(flat_pattern.bend_line_segments_mm)}, "
f"cut_circles={len(flat_pattern.cut_circles)}, "
f"front_cut_profiles={len(flat_pattern.cut_profile_centers)}, "
f"front_cut_segments={len(flat_pattern.cut_segments)}"
)
dxf_bend_checks = (
"DXF bend checks "
f"flange_outside_min={flange_outside_min:.3f} mm, "
f"flange_flat_min={flange_outside_min - outside_setback:.3f} mm, "
f"flange_flat_required={SENDCUTSEND_MIN_FLANGE_FLAT_MM:.3f} mm, "
f"bend_segment_min={_min_bend_segment_length(layout):.3f} mm, "
f"bend_support={_supported_bend_fraction(layout):.1%}, "
f"hole_to_bend_min={min(abs(center.X - layout.bracket_face_inner_x) for center, _radius in [*layout.top_holes, *layout.bottom_holes]):.3f} mm, "
f"outboard_hole_edge_web={layout.outer_flush_gap:.3f} mm"
)
print(dxf_bend_checks)
center_bridge_bend_edge_distance = _center_bridge_bend_edge_distance(layout)
center_bridge_flat_clearance = _center_bridge_flat_bend_line_to_edge_distance(layout, flat_pattern)
if center_bridge_bend_edge_distance is not None and center_bridge_flat_clearance is not None:
print(
"DXF center bridge "
f"bend_edge_distance={center_bridge_bend_edge_distance:.3f} mm, "
f"flat_bend_line_to_edge={center_bridge_flat_clearance:.3f} mm"
)
return doc
def gen_step() -> dict[str, object]:
return {
"shape": build_step(),
}
def gen_dxf() -> dict[str, object]:
return {
"document": build_dxf(),
}
def gen() -> None:
gen_step()
gen_dxf()
if __name__ == "__main__":
gen()