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

497 lines
16 KiB
Python

#!/usr/bin/env python3
"""
Bent-sheet end bracket for wrapping a 2020 extrusion end and two side faces.
"""
from __future__ import annotations
import math
import os
import sys
from dataclasses import dataclass
from pathlib import Path
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 build123d
import ezdxf
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_HALF_SPAN_MM,
KEYED_CONNECTION_REF,
KEYED_CONNECTION_ROTATION_DEG,
make_keyed_connection_face,
sample_keyed_connection_outline,
)
PART_NAME = Path(__file__).stem
DISPLAY_NAME = "Extrusion Bracket 2020"
EXTRUSION_X_SPAN_MM = 20.0
EXTRUSION_X_HALF_SPAN_MM = 0.5 * EXTRUSION_X_SPAN_MM
EXTRUSION_Y_SPAN_MM = 20.0
EXTRUSION_Y_HALF_SPAN_MM = 0.5 * EXTRUSION_Y_SPAN_MM
# Keep the bracket seated tightly on the 2020 extrusion; any additional offset
# should come from a future face redesign rather than opening the full wrap gap.
EXTRUSION_CLEARANCE_MM = 0.5
SHORT_SIDE_MM = EXTRUSION_X_SPAN_MM
SHORT_SIDE_HALF_MM = 0.5 * SHORT_SIDE_MM
SHEET_THICKNESS_MM = 1.6
BRACKET_DEPTH_MM = 30.0
FREE_END_CORNER_RADIUS_MM = 1.0
M5_CLEARANCE_RADIUS_MM = 2.75
# Keep the inner row a bit farther off the bend die region for SendCutSend.
SIDE_FASTENER_Z_POSITIONS_MM = (10.0, 22.0)
CUT_EXTENSION_MM = 1.0
EDGE_MATCH_TOLERANCE_MM = 1e-3
SENDCUTSEND_EFFECTIVE_BEND_RADIUS_MM = 25.4 * 0.038
SENDCUTSEND_K_FACTOR = 0.48
SENDCUTSEND_BEND_DEDUCTION_90_MM = (
2.0 * (SENDCUTSEND_EFFECTIVE_BEND_RADIUS_MM + SHEET_THICKNESS_MM)
) - (
(math.pi * 0.5)
* (SENDCUTSEND_EFFECTIVE_BEND_RADIUS_MM + (SENDCUTSEND_K_FACTOR * SHEET_THICKNESS_MM))
)
SENDCUTSEND_MIN_FLANGE_FORMED_MM = 25.4 * 0.313
SENDCUTSEND_MIN_FLANGE_FLAT_MM = 25.4 * 0.255
SENDCUTSEND_HALF_DIE_WIDTH_MM = 0.5 * (25.4 * 0.472)
INNER_X_MIN = -(EXTRUSION_X_HALF_SPAN_MM + EXTRUSION_CLEARANCE_MM)
INNER_X_MAX = -INNER_X_MIN
INNER_Y_MIN = -(EXTRUSION_Y_HALF_SPAN_MM + EXTRUSION_CLEARANCE_MM)
INNER_Y_MAX = -INNER_Y_MIN
OUTER_X_MIN = -SHORT_SIDE_HALF_MM
OUTER_X_MAX = SHORT_SIDE_HALF_MM
BASE_Z_MIN = -SHEET_THICKNESS_MM
BASE_Z_MAX = 0.0
SIDE_Z_MIN = BASE_Z_MIN
SIDE_Z_MAX = BRACKET_DEPTH_MM
LOWER_Y_MIN = INNER_Y_MIN - SHEET_THICKNESS_MM
LOWER_Y_MAX = INNER_Y_MIN
UPPER_Y_MIN = INNER_Y_MAX
UPPER_Y_MAX = INNER_Y_MAX + SHEET_THICKNESS_MM
@dataclass(frozen=True)
class FlatPattern:
total_length_mm: float
width_mm: float
flange_flat_length_mm: float
web_flat_start_mm: float
web_flat_end_mm: float
bend_line_positions_mm: tuple[float, float]
cut_profile_centers: tuple[tuple[float, float], ...]
cut_circles: tuple[tuple[float, float, float], ...]
rounded_corner_radius_mm: float
min_feature_to_bend_mm: float
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 _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 _make_keyed_connection_cutter_along_z(
*,
center_x: float,
center_y: float,
start_z: float,
height: float,
) -> build123d.Solid:
face = make_keyed_connection_face(
origin=build123d.Vector(center_x, center_y, start_z),
u_dir=build123d.Vector(1.0, 0.0, 0.0),
v_dir=build123d.Vector(0.0, 1.0, 0.0),
)
return build123d.Solid.extrude(face, build123d.Vector(0.0, 0.0, height))
def _sendcutsend_outside_setback_90_mm() -> float:
return SENDCUTSEND_EFFECTIVE_BEND_RADIUS_MM + SHEET_THICKNESS_MM
def _sendcutsend_bend_allowance_90_mm() -> float:
return (math.pi * 0.5) * (
SENDCUTSEND_EFFECTIVE_BEND_RADIUS_MM + (SENDCUTSEND_K_FACTOR * SHEET_THICKNESS_MM)
)
def _is_close(value: float, expected: float, tolerance: float = EDGE_MATCH_TOLERANCE_MM) -> bool:
return abs(value - expected) <= tolerance
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 build_bracket() -> build123d.Shape:
end_face = _make_box(
x_min=OUTER_X_MIN,
x_max=OUTER_X_MAX,
y_min=INNER_Y_MIN,
y_max=INNER_Y_MAX,
z_min=BASE_Z_MIN,
z_max=BASE_Z_MAX,
)
lower_flange = _make_box(
x_min=OUTER_X_MIN,
x_max=OUTER_X_MAX,
y_min=LOWER_Y_MIN,
y_max=LOWER_Y_MAX,
z_min=SIDE_Z_MIN,
z_max=SIDE_Z_MAX,
)
upper_flange = _make_box(
x_min=OUTER_X_MIN,
x_max=OUTER_X_MAX,
y_min=UPPER_Y_MIN,
y_max=UPPER_Y_MAX,
z_min=SIDE_Z_MIN,
z_max=SIDE_Z_MAX,
)
bracket = end_face.fuse(lower_flange).fuse(upper_flange)
hole_length = SHEET_THICKNESS_MM + (2.0 * CUT_EXTENSION_MM)
bracket = bracket.cut(
_make_keyed_connection_cutter_along_z(
center_x=0.0,
center_y=0.0,
start_z=BASE_Z_MIN - CUT_EXTENSION_MM,
height=hole_length,
)
)
for z_position in SIDE_FASTENER_Z_POSITIONS_MM:
bracket = bracket.cut(
_make_cylinder_along(
radius=M5_CLEARANCE_RADIUS_MM,
height=hole_length,
origin=build123d.Vector(0.0, LOWER_Y_MIN - CUT_EXTENSION_MM, z_position),
direction=build123d.Vector(0.0, 1.0, 0.0),
)
)
bracket = bracket.cut(
_make_cylinder_along(
radius=M5_CLEARANCE_RADIUS_MM,
height=hole_length,
origin=build123d.Vector(0.0, UPPER_Y_MAX + CUT_EXTENSION_MM, z_position),
direction=build123d.Vector(0.0, -1.0, 0.0),
)
)
target_edge_centers = (
build123d.Vector(
-SHORT_SIDE_HALF_MM,
LOWER_Y_MIN + (0.5 * SHEET_THICKNESS_MM),
BRACKET_DEPTH_MM,
),
build123d.Vector(
SHORT_SIDE_HALF_MM,
LOWER_Y_MIN + (0.5 * SHEET_THICKNESS_MM),
BRACKET_DEPTH_MM,
),
build123d.Vector(
-SHORT_SIDE_HALF_MM,
UPPER_Y_MAX - (0.5 * SHEET_THICKNESS_MM),
BRACKET_DEPTH_MM,
),
build123d.Vector(
SHORT_SIDE_HALF_MM,
UPPER_Y_MAX - (0.5 * SHEET_THICKNESS_MM),
BRACKET_DEPTH_MM,
),
)
target_edges: list[build123d.Edge] = []
for edge in bracket.edges():
if edge.geom_type != build123d.GeomType.LINE:
continue
center = edge.center()
if any(
_is_close(center.X, target_center.X)
and _is_close(center.Y, target_center.Y)
and _is_close(center.Z, target_center.Z)
for target_center in target_edge_centers
):
target_edges.append(edge)
if len(target_edges) != len(target_edge_centers):
raise RuntimeError(
"Expected to find the four flange free-end corners for rounding, "
f"found {len(target_edges)}"
)
bracket = build123d.fillet(target_edges, FREE_END_CORNER_RADIUS_MM)
solids = list(bracket.solids())
if len(solids) != 1:
raise RuntimeError(f"Expected one wrap bracket solid, found {len(solids)}")
bracket = solids[0]
bracket.label = PART_NAME
bbox = bracket.bounding_box()
print(
"Bent-sheet 2020 end bracket: "
f"clearance={EXTRUSION_CLEARANCE_MM:.3f} mm, "
f"thickness={SHEET_THICKNESS_MM:.3f} mm, "
f"depth={BRACKET_DEPTH_MM:.3f} mm, "
f"free_corner_radius={FREE_END_CORNER_RADIUS_MM:.3f} mm, "
f"size=({bbox.size.X:.3f}, {bbox.size.Y:.3f}, {bbox.size.Z:.3f}) mm, "
f"plate_fasteners={2 * len(SIDE_FASTENER_Z_POSITIONS_MM)}, "
f"connection_ref={KEYED_CONNECTION_REF}, "
f"connection_rotation={KEYED_CONNECTION_ROTATION_DEG:.1f} deg, "
f"hole_diameter={2.0 * M5_CLEARANCE_RADIUS_MM:.3f} mm"
)
return bracket
def _build_flat_pattern() -> FlatPattern:
_validate_sendcutsend_bend_rule()
outside_setback = _sendcutsend_outside_setback_90_mm()
bend_allowance = _sendcutsend_bend_allowance_90_mm()
flange_outside_length = BRACKET_DEPTH_MM + SHEET_THICKNESS_MM
flange_flat_length = flange_outside_length - outside_setback
if flange_outside_length < SENDCUTSEND_MIN_FLANGE_FORMED_MM:
raise RuntimeError(
"Formed flange length is below the configured bend-rule minimum: "
f"{flange_outside_length:.3f} mm < {SENDCUTSEND_MIN_FLANGE_FORMED_MM:.3f} mm"
)
if flange_flat_length < SENDCUTSEND_MIN_FLANGE_FLAT_MM:
raise RuntimeError(
"Flat flange length is below the configured bend-rule minimum: "
f"{flange_flat_length:.3f} mm < {SENDCUTSEND_MIN_FLANGE_FLAT_MM:.3f} mm"
)
web_outside_length = INNER_Y_MAX - INNER_Y_MIN
web_flat_length = web_outside_length - (2.0 * SENDCUTSEND_EFFECTIVE_BEND_RADIUS_MM)
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 = flange_flat_length + bend_allowance
web_flat_end = web_flat_start + web_flat_length
total_length = web_flat_end + bend_allowance + flange_flat_length
width = OUTER_X_MAX - OUTER_X_MIN
bend_line_positions = (
flange_flat_length + (0.5 * bend_allowance),
web_flat_end + (0.5 * bend_allowance),
)
def _v_from_x(x_coord: float) -> float:
return x_coord - OUTER_X_MIN
keyed_connection_center_u = web_flat_start + (-INNER_Y_MIN - SENDCUTSEND_EFFECTIVE_BEND_RADIUS_MM)
cut_profile_centers = ((keyed_connection_center_u, _v_from_x(0.0)),)
cut_circles: list[tuple[float, float, float]] = []
for z_position in SIDE_FASTENER_Z_POSITIONS_MM:
lower_u = BRACKET_DEPTH_MM - z_position
upper_u = total_length - (BRACKET_DEPTH_MM - z_position)
cut_circles.append((lower_u, _v_from_x(0.0), M5_CLEARANCE_RADIUS_MM))
cut_circles.append((upper_u, _v_from_x(0.0), M5_CLEARANCE_RADIUS_MM))
min_feature_to_bend = min(
abs(keyed_connection_center_u - bend_u) - KEYED_CONNECTION_HALF_SPAN_MM
for bend_u in bend_line_positions
)
min_feature_to_bend = min(
min_feature_to_bend,
min(
abs(center_u - bend_u) - radius
for center_u, _center_v, radius in cut_circles
for bend_u in bend_line_positions
),
)
return FlatPattern(
total_length_mm=total_length,
width_mm=width,
flange_flat_length_mm=flange_flat_length,
web_flat_start_mm=web_flat_start,
web_flat_end_mm=web_flat_end,
bend_line_positions_mm=bend_line_positions,
cut_profile_centers=cut_profile_centers,
cut_circles=tuple(cut_circles),
rounded_corner_radius_mm=FREE_END_CORNER_RADIUS_MM,
min_feature_to_bend_mm=min_feature_to_bend,
)
def build_dxf() -> ezdxf.document.Drawing:
flat_pattern = _build_flat_pattern()
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()
corner_radius = flat_pattern.rounded_corner_radius_mm
if corner_radius <= 0.0:
outline = (
(0.0, 0.0),
(flat_pattern.total_length_mm, 0.0),
(flat_pattern.total_length_mm, flat_pattern.width_mm),
(0.0, flat_pattern.width_mm),
)
msp.add_lwpolyline(outline, format="xy", close=True, dxfattribs={"layer": "CUT"})
else:
if (2.0 * corner_radius) >= flat_pattern.width_mm:
raise RuntimeError(
"Rounded flange corners exceed the flat-pattern width: "
f"diameter {2.0 * corner_radius:.3f} mm >= width {flat_pattern.width_mm:.3f} mm"
)
msp.add_line(
(corner_radius, 0.0),
(flat_pattern.total_length_mm - corner_radius, 0.0),
dxfattribs={"layer": "CUT"},
)
msp.add_arc(
(corner_radius, corner_radius),
corner_radius,
180.0,
270.0,
dxfattribs={"layer": "CUT"},
)
msp.add_arc(
(flat_pattern.total_length_mm - corner_radius, corner_radius),
corner_radius,
270.0,
360.0,
dxfattribs={"layer": "CUT"},
)
msp.add_line(
(flat_pattern.total_length_mm, corner_radius),
(flat_pattern.total_length_mm, flat_pattern.width_mm - corner_radius),
dxfattribs={"layer": "CUT"},
)
msp.add_arc(
(flat_pattern.total_length_mm - corner_radius, flat_pattern.width_mm - corner_radius),
corner_radius,
0.0,
90.0,
dxfattribs={"layer": "CUT"},
)
msp.add_line(
(flat_pattern.total_length_mm - corner_radius, flat_pattern.width_mm),
(corner_radius, flat_pattern.width_mm),
dxfattribs={"layer": "CUT"},
)
msp.add_arc(
(corner_radius, flat_pattern.width_mm - corner_radius),
corner_radius,
90.0,
180.0,
dxfattribs={"layer": "CUT"},
)
msp.add_line(
(0.0, flat_pattern.width_mm - corner_radius),
(0.0, corner_radius),
dxfattribs={"layer": "CUT"},
)
for center_u, center_v, radius in flat_pattern.cut_circles:
msp.add_circle((center_u, center_v), radius, dxfattribs={"layer": "CUT"})
for center_u, center_v in flat_pattern.cut_profile_centers:
msp.add_lwpolyline(
sample_keyed_connection_outline(center_u=center_u, center_v=center_v, points_per_arc=16),
format="xy",
close=True,
dxfattribs={"layer": "CUT"},
)
for bend_line_u in flat_pattern.bend_line_positions_mm:
msp.add_line(
(bend_line_u, 0.0),
(bend_line_u, flat_pattern.width_mm),
dxfattribs={"layer": "BEND", "linetype": "DASHED"},
)
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"cut_profiles={len(flat_pattern.cut_profile_centers)}, "
f"cut_circles={len(flat_pattern.cut_circles)}"
)
print(
"DXF bend checks "
f"flange_flat={flat_pattern.flange_flat_length_mm:.3f} mm, "
f"flange_flat_required={SENDCUTSEND_MIN_FLANGE_FLAT_MM:.3f} mm, "
f"feature_to_bend_min={flat_pattern.min_feature_to_bend_mm:.3f} mm, "
f"feature_to_bend_required={SENDCUTSEND_HALF_DIE_WIDTH_MM:.3f} mm"
)
if flat_pattern.min_feature_to_bend_mm < SENDCUTSEND_HALF_DIE_WIDTH_MM:
print(
"Warning: flat-pattern keyed feature enters the bend die region by "
f"{SENDCUTSEND_HALF_DIE_WIDTH_MM - flat_pattern.min_feature_to_bend_mm:.3f} mm"
)
return doc
def gen_step() -> dict[str, object]:
return {
"shape": build_bracket(),
}
def gen_dxf() -> dict[str, object]:
return {
"document": build_dxf(),
}
if __name__ == "__main__":
gen_step()