766 lines
25 KiB
Python
766 lines
25 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Generate a bent base clamp that wraps under the base servo footprint.
|
|
|
|
The upper plate stays in the servo base frame, a side wall rises from the
|
|
positive-x side, and a lower return closes the U so the part can clamp to a
|
|
desk or fixture from below.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
import math
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
from OCP.BRepFilletAPI import BRepFilletAPI_MakeFillet
|
|
|
|
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))
|
|
|
|
|
|
DISPLAY_NAME = "Base Clamp"
|
|
PLATE_THICKNESS_MM = 25.4 * 0.125
|
|
SADDLE_FOOTPRINT_X_BUFFER_MM = 2.0
|
|
SIDE_WALL_THICKNESS_MM = PLATE_THICKNESS_MM
|
|
BOOLEAN_OVERLAP_MM = 0.05
|
|
CUT_EXTENSION_MM = 0.5
|
|
CORNER_RADIUS_MM = 3.0
|
|
RETURN_FACE_LENGTH_RATIO = 2.0 / 3.0
|
|
M3_CLEARANCE_RADIUS_MM = 1.6
|
|
BASE_CENTER_HORN_FACE_RADIUS_MM = 2.8
|
|
BASE_CENTER_HORN_FACE_CLEARANCE_BUFFER_MM = 0.25
|
|
BASE_CENTER_HORN_FACE_CLEARANCE_RADIUS_MM = (
|
|
BASE_CENTER_HORN_FACE_RADIUS_MM + BASE_CENTER_HORN_FACE_CLEARANCE_BUFFER_MM
|
|
)
|
|
M8_CLEARANCE_DIAMETER_MM = 8.5
|
|
M8_CLEARANCE_RADIUS_MM = 0.5 * M8_CLEARANCE_DIAMETER_MM
|
|
SENDCUTSEND_EFFECTIVE_BEND_RADIUS_MM = 25.4 * 0.125
|
|
SENDCUTSEND_K_FACTOR = 0.44
|
|
SENDCUTSEND_BEND_DEDUCTION_90_MM = 25.4 * 0.216
|
|
SENDCUTSEND_HALF_DIE_WIDTH_MM = 0.5 * (25.4 * 0.630)
|
|
SENDCUTSEND_MIN_FLANGE_FORMED_MM = 25.4 * 0.476
|
|
SENDCUTSEND_MOUNT_HOLE_TO_BEND_BUFFER_MM = 0.1
|
|
|
|
# Base servo footprint bounds in robot-arm local coordinates.
|
|
SADDLE_BASE_FACE_Y_MM = -29.8002
|
|
SADDLE_BASE_X_MIN_MM = -35.61
|
|
SADDLE_BASE_X_MAX_MM = 6.338516
|
|
SADDLE_BASE_Z_MIN_MM = -15.960226
|
|
SADDLE_BASE_Z_MAX_MM = 15.960224
|
|
|
|
# 5x M3 lower-horn clearance pattern on the base plate.
|
|
BASE_HORN_CONNECTION_HOLE_SPECS_MM: tuple[tuple[float, float, float], ...] = (
|
|
(-32.5, 0.0, M3_CLEARANCE_RADIUS_MM),
|
|
(-25.5, -7.0, M3_CLEARANCE_RADIUS_MM),
|
|
(-25.5, 0.0, BASE_CENTER_HORN_FACE_CLEARANCE_RADIUS_MM),
|
|
(-25.5, 7.0, M3_CLEARANCE_RADIUS_MM),
|
|
(-18.5, 0.0, M3_CLEARANCE_RADIUS_MM),
|
|
)
|
|
|
|
# Resolved from:
|
|
# @cad[STEP/imports/sts3250#o1.2.e920,o1.2.e922]
|
|
# @cad[STEP/imports/sts3250#o1.2.e921,o1.2.e923]
|
|
M2_CLEARANCE_RADIUS_MM = 1.1
|
|
BASE_SERVO_M2_HOLE_SPECS_MM: tuple[tuple[float, float, float], ...] = (
|
|
(-17.2, -10.25, M2_CLEARANCE_RADIUS_MM),
|
|
(7.25, -10.25, M2_CLEARANCE_RADIUS_MM),
|
|
(-17.2, 10.25, M2_CLEARANCE_RADIUS_MM),
|
|
(7.25, 10.25, M2_CLEARANCE_RADIUS_MM),
|
|
)
|
|
BASE_ATTACHMENT_HOLES_MM: tuple[tuple[float, float, float], ...] = (
|
|
BASE_HORN_CONNECTION_HOLE_SPECS_MM + BASE_SERVO_M2_HOLE_SPECS_MM
|
|
)
|
|
|
|
# Resolved from the four PCB spacer hole centers in the original SO-101 bus board.
|
|
SERVO_BUS_BOARD_SPACER_CENTERS_XY_MM: tuple[tuple[float, float], ...] = (
|
|
(2.4930065, 2.4999875),
|
|
(2.4930065, 30.4999925),
|
|
(39.493009, 2.4999875),
|
|
(39.505008000000004, 30.4999925),
|
|
)
|
|
SERVO_BUS_SPACER_CLEARANCE_RADIUS_MM = M3_CLEARANCE_RADIUS_MM
|
|
SERVO_BUS_MOUNT_LOCAL_X_MIN_MM = min(x for x, _y in SERVO_BUS_BOARD_SPACER_CENTERS_XY_MM)
|
|
SERVO_BUS_MOUNT_LOCAL_X_MAX_MM = max(x for x, _y in SERVO_BUS_BOARD_SPACER_CENTERS_XY_MM)
|
|
SERVO_BUS_MOUNT_LOCAL_X_CENTER_MM = 0.5 * (SERVO_BUS_MOUNT_LOCAL_X_MIN_MM + SERVO_BUS_MOUNT_LOCAL_X_MAX_MM)
|
|
SERVO_BUS_MOUNT_LOCAL_Y_MIN_MM = min(y for _x, y in SERVO_BUS_BOARD_SPACER_CENTERS_XY_MM)
|
|
SERVO_BUS_MOUNT_LOCAL_Y_MAX_MM = max(y for _x, y in SERVO_BUS_BOARD_SPACER_CENTERS_XY_MM)
|
|
SERVO_BUS_MOUNT_LOCAL_Y_CENTER_MM = 0.5 * (SERVO_BUS_MOUNT_LOCAL_Y_MIN_MM + SERVO_BUS_MOUNT_LOCAL_Y_MAX_MM)
|
|
SENDCUTSEND_MIN_HOLE_TO_EDGE_QUOTE_MM = 0.97
|
|
BASE_CLAMP_HOLE_TO_EDGE_BUFFER_MM = 0.53
|
|
BASE_CLAMP_HOLE_TO_EDGE_TARGET_MM = (
|
|
SENDCUTSEND_MIN_HOLE_TO_EDGE_QUOTE_MM + BASE_CLAMP_HOLE_TO_EDGE_BUFFER_MM
|
|
)
|
|
BASE_CLAMP_SERVO_BUS_SPACER_WIDTH_MM = (
|
|
(SERVO_BUS_MOUNT_LOCAL_Y_MAX_MM - SERVO_BUS_MOUNT_LOCAL_Y_MIN_MM)
|
|
+ (2.0 * (SERVO_BUS_SPACER_CLEARANCE_RADIUS_MM + BASE_CLAMP_HOLE_TO_EDGE_TARGET_MM))
|
|
)
|
|
|
|
# SendCutSend's current .125" 5052 guideline uses a 0.630" die width for
|
|
# bending; cut features should stay outside half of that die width from the
|
|
# bend line. We size the wall from the mount-hole span plus that offset.
|
|
SENDCUTSEND_5052_125_DIE_WIDTH_MM = 0.630 * 25.4
|
|
SENDCUTSEND_FEATURE_TO_BEND_MIN_MM = (0.5 * SENDCUTSEND_5052_125_DIE_WIDTH_MM) + SERVO_BUS_SPACER_CLEARANCE_RADIUS_MM
|
|
U_BRACKET_CLEARANCE_MM = (
|
|
(SERVO_BUS_MOUNT_LOCAL_X_MAX_MM - SERVO_BUS_MOUNT_LOCAL_X_MIN_MM)
|
|
+ (2.0 * SENDCUTSEND_FEATURE_TO_BEND_MIN_MM)
|
|
- (2.0 * PLATE_THICKNESS_MM)
|
|
)
|
|
|
|
# Resolved from @cad[STEP/imports/servo_bus].
|
|
SERVO_BUS_LOCAL_X_MIN_MM = -0.390013
|
|
SERVO_BUS_LOCAL_X_MAX_MM = 42.401989
|
|
SERVO_BUS_LOCAL_Y_MIN_MM = -0.012095
|
|
SERVO_BUS_LOCAL_Y_MAX_MM = 33.012075
|
|
SERVO_BUS_LOCAL_Z_MIN_MM = -6.0
|
|
SERVO_BUS_LOCAL_Z_MAX_MM = 1.599999
|
|
BASE_PLATE_Z_WIDTH_MM = max(
|
|
SERVO_BUS_LOCAL_Y_MAX_MM - SERVO_BUS_LOCAL_Y_MIN_MM,
|
|
BASE_CLAMP_SERVO_BUS_SPACER_WIDTH_MM,
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class BracketLayout:
|
|
x_min: float
|
|
x_max: float
|
|
y_min: float
|
|
y_max: float
|
|
z_min: float
|
|
z_max: float
|
|
width: float
|
|
height: float
|
|
return_width: float
|
|
return_x_min: float
|
|
return_y_min: float
|
|
return_y_max: float
|
|
side_x_min: float
|
|
side_x_max: float
|
|
overall_y_min: float
|
|
face_y: float
|
|
attachment_holes: tuple[tuple[float, float, float], ...]
|
|
return_mount_holes: tuple[tuple[float, float, float], ...]
|
|
servo_bus_mount_holes_yzr: tuple[tuple[float, float, float], ...]
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class FlatPattern:
|
|
total_length_mm: float
|
|
width_mm: float
|
|
top_flange_flat_length_mm: float
|
|
return_flange_flat_length_mm: float
|
|
web_flat_start_mm: float
|
|
web_flat_end_mm: float
|
|
bend_line_positions_mm: 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 _sendcutsend_outside_setback_90_mm() -> float:
|
|
return SENDCUTSEND_EFFECTIVE_BEND_RADIUS_MM + PLATE_THICKNESS_MM
|
|
|
|
|
|
def _sendcutsend_bend_allowance_90_mm() -> float:
|
|
return (math.pi * 0.5) * (
|
|
SENDCUTSEND_EFFECTIVE_BEND_RADIUS_MM + (SENDCUTSEND_K_FACTOR * PLATE_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 _cast_shape(obj: object) -> build123d.Shape:
|
|
from build123d.topology import downcast
|
|
from build123d.topology.shape_core import shapetype
|
|
|
|
occt_shape = downcast(obj)
|
|
shape_name = build123d.Shape.shape_LUT[shapetype(occt_shape)]
|
|
shape_type = getattr(build123d, shape_name)
|
|
return shape_type.cast(occt_shape)
|
|
|
|
|
|
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 _build_bracket_layout() -> BracketLayout:
|
|
x_min = SADDLE_BASE_X_MIN_MM - SADDLE_FOOTPRINT_X_BUFFER_MM
|
|
x_max = SADDLE_BASE_X_MAX_MM + SADDLE_FOOTPRINT_X_BUFFER_MM
|
|
required_x_max_for_mount_holes = max(
|
|
x
|
|
+ radius
|
|
+ SENDCUTSEND_HALF_DIE_WIDTH_MM
|
|
+ SENDCUTSEND_MOUNT_HOLE_TO_BEND_BUFFER_MM
|
|
for x, _z, radius in BASE_ATTACHMENT_HOLES_MM
|
|
)
|
|
x_max = max(x_max, required_x_max_for_mount_holes)
|
|
z_center = 0.5 * (SADDLE_BASE_Z_MIN_MM + SADDLE_BASE_Z_MAX_MM)
|
|
z_min = z_center - (0.5 * BASE_PLATE_Z_WIDTH_MM)
|
|
z_max = z_center + (0.5 * BASE_PLATE_Z_WIDTH_MM)
|
|
y_min = SADDLE_BASE_FACE_Y_MM - PLATE_THICKNESS_MM
|
|
y_max = SADDLE_BASE_FACE_Y_MM
|
|
return_y_max = y_min - U_BRACKET_CLEARANCE_MM
|
|
return_y_min = return_y_max - PLATE_THICKNESS_MM
|
|
side_x_min = x_max - BOOLEAN_OVERLAP_MM
|
|
side_x_max = x_max + SIDE_WALL_THICKNESS_MM
|
|
return_x_min = side_x_min - (RETURN_FACE_LENGTH_RATIO * (side_x_max - x_min))
|
|
return_mount_holes = (
|
|
(
|
|
0.5 * (return_x_min + side_x_min),
|
|
0.5 * (z_min + z_max),
|
|
M8_CLEARANCE_RADIUS_MM,
|
|
),
|
|
)
|
|
|
|
servo_bus_mount_holes_yzr = _servo_bus_mount_holes_yzr(
|
|
y_min=return_y_min,
|
|
y_max=y_max,
|
|
z_min=z_min,
|
|
z_max=z_max,
|
|
)
|
|
|
|
return BracketLayout(
|
|
x_min=x_min,
|
|
x_max=x_max,
|
|
y_min=y_min,
|
|
y_max=y_max,
|
|
z_min=z_min,
|
|
z_max=z_max,
|
|
width=x_max - x_min,
|
|
height=z_max - z_min,
|
|
return_width=side_x_max - return_x_min,
|
|
return_x_min=return_x_min,
|
|
return_y_min=return_y_min,
|
|
return_y_max=return_y_max,
|
|
side_x_min=side_x_min,
|
|
side_x_max=side_x_max,
|
|
overall_y_min=return_y_min,
|
|
face_y=y_max,
|
|
attachment_holes=BASE_ATTACHMENT_HOLES_MM,
|
|
return_mount_holes=return_mount_holes,
|
|
servo_bus_mount_holes_yzr=servo_bus_mount_holes_yzr,
|
|
)
|
|
|
|
|
|
def _fillet_plan_corners(
|
|
plate: build123d.Solid,
|
|
*,
|
|
x_min: float,
|
|
x_max: float,
|
|
y_min: float,
|
|
y_max: float,
|
|
z_min: float,
|
|
z_max: float,
|
|
include_min_x: bool = True,
|
|
include_max_x: bool = True,
|
|
) -> build123d.Shape:
|
|
matches: list[build123d.Edge] = []
|
|
for edge in plate.edges():
|
|
if _geom_type_name(edge) != "LINE":
|
|
continue
|
|
bb = edge.bounding_box()
|
|
if abs((bb.max.Y - bb.min.Y) - (y_max - y_min)) > 1e-3:
|
|
continue
|
|
if (bb.max.X - bb.min.X) > 1e-3 or (bb.max.Z - bb.min.Z) > 1e-3:
|
|
continue
|
|
center = edge.center()
|
|
if abs(abs(center.X - ((x_min + x_max) * 0.5)) - ((x_max - x_min) * 0.5)) > 1e-3:
|
|
continue
|
|
if abs(abs(center.Z - ((z_min + z_max) * 0.5)) - ((z_max - z_min) * 0.5)) > 1e-3:
|
|
continue
|
|
at_min_x = abs(center.X - x_min) <= 1e-3
|
|
at_max_x = abs(center.X - x_max) <= 1e-3
|
|
if at_min_x and not include_min_x:
|
|
continue
|
|
if at_max_x and not include_max_x:
|
|
continue
|
|
matches.append(edge)
|
|
|
|
expected_count = (2 if include_min_x else 0) + (2 if include_max_x else 0)
|
|
if len(matches) != expected_count:
|
|
raise RuntimeError(f"Expected {expected_count} plan-view corner edges to fillet, found {len(matches)}")
|
|
|
|
mk = BRepFilletAPI_MakeFillet(plate.wrapped)
|
|
for edge in matches:
|
|
mk.Add(CORNER_RADIUS_MM, edge.wrapped)
|
|
mk.Build()
|
|
if not mk.IsDone():
|
|
raise RuntimeError("Failed to round U-bracket plate corners")
|
|
return _cast_shape(mk.Shape())
|
|
|
|
|
|
def _cut_y_holes(
|
|
plate: build123d.Shape,
|
|
*,
|
|
holes_xzr: tuple[tuple[float, float, float], ...],
|
|
x_min: float,
|
|
x_max: float,
|
|
y_min: float,
|
|
y_max: float,
|
|
z_min: float,
|
|
z_max: float,
|
|
) -> build123d.Shape:
|
|
hole_depth = (y_max - y_min) + (2.0 * CUT_EXTENSION_MM)
|
|
for x, z, radius in holes_xzr:
|
|
if not (x_min + radius <= x <= x_max - radius):
|
|
raise RuntimeError(f"Hole at x={x:.3f} mm does not fit within the bracket x span")
|
|
if not (z_min + radius <= z <= z_max - radius):
|
|
raise RuntimeError(f"Hole at z={z:.3f} mm does not fit within the bracket z span")
|
|
plate = plate.cut(
|
|
_make_cylinder_along(
|
|
radius=radius,
|
|
height=hole_depth,
|
|
origin=build123d.Vector(x, y_min - CUT_EXTENSION_MM, z),
|
|
direction=build123d.Vector(0.0, 1.0, 0.0),
|
|
)
|
|
)
|
|
return plate
|
|
|
|
|
|
def _cut_x_holes(
|
|
bracket: build123d.Shape,
|
|
*,
|
|
holes_yzr: tuple[tuple[float, float, float], ...],
|
|
x_min: float,
|
|
x_max: float,
|
|
y_min: float,
|
|
y_max: float,
|
|
z_min: float,
|
|
z_max: float,
|
|
) -> build123d.Shape:
|
|
hole_depth = (x_max - x_min) + (2.0 * CUT_EXTENSION_MM)
|
|
for y, z, radius in holes_yzr:
|
|
if not (y_min + radius <= y <= y_max - radius):
|
|
raise RuntimeError(f"Hole at y={y:.3f} mm does not fit within the bracket y span")
|
|
if not (z_min + radius <= z <= z_max - radius):
|
|
raise RuntimeError(f"Hole at z={z:.3f} mm does not fit within the bracket z span")
|
|
bracket = bracket.cut(
|
|
_make_cylinder_along(
|
|
radius=radius,
|
|
height=hole_depth,
|
|
origin=build123d.Vector(x_min - CUT_EXTENSION_MM, y, z),
|
|
direction=build123d.Vector(1.0, 0.0, 0.0),
|
|
)
|
|
)
|
|
return bracket
|
|
|
|
|
|
def _cut_base_plate_mount_features(
|
|
plate: build123d.Shape,
|
|
*,
|
|
holes_xzr: tuple[tuple[float, float, float], ...],
|
|
x_min: float,
|
|
x_max: float,
|
|
y_min: float,
|
|
y_max: float,
|
|
z_min: float,
|
|
z_max: float,
|
|
) -> build123d.Shape:
|
|
return _cut_y_holes(
|
|
plate,
|
|
holes_xzr=holes_xzr,
|
|
x_min=x_min,
|
|
x_max=x_max,
|
|
y_min=y_min,
|
|
y_max=y_max,
|
|
z_min=z_min,
|
|
z_max=z_max,
|
|
)
|
|
|
|
|
|
def _as_single_solid(shape: build123d.Shape) -> build123d.Solid:
|
|
solids = list(shape.solids())
|
|
if len(solids) != 1:
|
|
raise RuntimeError(f"Expected a single solid, found {len(solids)}")
|
|
return solids[0]
|
|
|
|
|
|
def _make_lower_plate(layout: BracketLayout) -> build123d.Shape:
|
|
plate = _make_box(
|
|
x_min=layout.x_min,
|
|
x_max=layout.x_max,
|
|
y_min=layout.y_min,
|
|
y_max=layout.y_max,
|
|
z_min=layout.z_min,
|
|
z_max=layout.z_max,
|
|
)
|
|
plate = _fillet_plan_corners(
|
|
plate,
|
|
x_min=layout.x_min,
|
|
x_max=layout.x_max,
|
|
y_min=layout.y_min,
|
|
y_max=layout.y_max,
|
|
z_min=layout.z_min,
|
|
z_max=layout.z_max,
|
|
include_min_x=True,
|
|
include_max_x=False,
|
|
)
|
|
plate = _cut_base_plate_mount_features(
|
|
plate,
|
|
holes_xzr=layout.attachment_holes,
|
|
x_min=layout.x_min,
|
|
x_max=layout.x_max,
|
|
y_min=layout.y_min,
|
|
y_max=layout.y_max,
|
|
z_min=layout.z_min,
|
|
z_max=layout.z_max,
|
|
)
|
|
return _as_single_solid(plate)
|
|
|
|
|
|
def _servo_bus_mount_holes_yzr(
|
|
*,
|
|
y_min: float,
|
|
y_max: float,
|
|
z_min: float,
|
|
z_max: float,
|
|
) -> tuple[tuple[float, float, float], ...]:
|
|
target_center_y = 0.5 * (y_min + y_max)
|
|
target_center_z = 0.5 * (z_min + z_max)
|
|
bus_to_bracket_y_offset = target_center_y - SERVO_BUS_MOUNT_LOCAL_X_CENTER_MM
|
|
bus_to_bracket_z_offset = target_center_z + SERVO_BUS_MOUNT_LOCAL_Y_CENTER_MM
|
|
|
|
return tuple(
|
|
(
|
|
bus_x + bus_to_bracket_y_offset,
|
|
bus_to_bracket_z_offset - bus_y,
|
|
SERVO_BUS_SPACER_CLEARANCE_RADIUS_MM,
|
|
)
|
|
for bus_x, bus_y in SERVO_BUS_BOARD_SPACER_CENTERS_XY_MM
|
|
)
|
|
|
|
|
|
def _make_return_plate(layout: BracketLayout) -> build123d.Shape:
|
|
plate = _make_box(
|
|
x_min=layout.return_x_min,
|
|
x_max=layout.side_x_max,
|
|
y_min=layout.return_y_min,
|
|
y_max=layout.return_y_max,
|
|
z_min=layout.z_min,
|
|
z_max=layout.z_max,
|
|
)
|
|
plate = _fillet_plan_corners(
|
|
plate,
|
|
x_min=layout.return_x_min,
|
|
x_max=layout.side_x_max,
|
|
y_min=layout.return_y_min,
|
|
y_max=layout.return_y_max,
|
|
z_min=layout.z_min,
|
|
z_max=layout.z_max,
|
|
include_min_x=True,
|
|
include_max_x=False,
|
|
)
|
|
plate = _cut_y_holes(
|
|
plate,
|
|
holes_xzr=layout.return_mount_holes,
|
|
x_min=layout.return_x_min,
|
|
x_max=layout.side_x_max,
|
|
y_min=layout.return_y_min,
|
|
y_max=layout.return_y_max,
|
|
z_min=layout.z_min,
|
|
z_max=layout.z_max,
|
|
)
|
|
return _as_single_solid(plate)
|
|
|
|
|
|
def _make_side_wall(layout: BracketLayout) -> build123d.Shape:
|
|
return _make_box(
|
|
x_min=layout.side_x_min,
|
|
x_max=layout.side_x_max,
|
|
y_min=layout.return_y_min,
|
|
y_max=layout.y_max,
|
|
z_min=layout.z_min,
|
|
z_max=layout.z_max,
|
|
)
|
|
|
|
|
|
def build_bracket() -> build123d.Solid:
|
|
layout = _build_bracket_layout()
|
|
|
|
bracket = _make_lower_plate(layout)
|
|
bracket = _as_single_solid(bracket.fuse(_make_side_wall(layout)))
|
|
bracket = _as_single_solid(bracket.fuse(_make_return_plate(layout)))
|
|
bracket = _as_single_solid(
|
|
_cut_x_holes(
|
|
bracket,
|
|
holes_yzr=layout.servo_bus_mount_holes_yzr,
|
|
x_min=layout.side_x_min,
|
|
x_max=layout.side_x_max,
|
|
y_min=layout.return_y_min,
|
|
y_max=layout.y_max,
|
|
z_min=layout.z_min,
|
|
z_max=layout.z_max,
|
|
)
|
|
)
|
|
|
|
print(
|
|
f"{DISPLAY_NAME}: "
|
|
f"lower_width={layout.width:.3f} mm, "
|
|
f"return_width={layout.return_width:.3f} mm, "
|
|
f"lower_height={layout.height:.3f} mm, "
|
|
f"plate_thickness={PLATE_THICKNESS_MM:.3f} mm, "
|
|
f"clearance={U_BRACKET_CLEARANCE_MM:.3f} mm, "
|
|
f"overall_y_min={layout.overall_y_min:.4f} mm, "
|
|
f"horn_m3_holes={len(BASE_HORN_CONNECTION_HOLE_SPECS_MM)}, "
|
|
f"m2_mount_holes={len(BASE_SERVO_M2_HOLE_SPECS_MM)}, "
|
|
f"return_mount_holes={len(layout.return_mount_holes)}, "
|
|
f"servo_bus_holes={len(layout.servo_bus_mount_holes_yzr)}, "
|
|
f"flush_y={layout.face_y:.4f} mm"
|
|
)
|
|
return bracket
|
|
|
|
|
|
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_flange_outside_length = layout.side_x_max - layout.x_min
|
|
return_flange_outside_length = layout.return_width
|
|
top_flange_flat_length = top_flange_outside_length - outside_setback
|
|
return_flange_flat_length = return_flange_outside_length - outside_setback
|
|
min_flange_outside_length = min(top_flange_outside_length, return_flange_outside_length)
|
|
min_flange_flat_length = min(top_flange_flat_length, return_flange_flat_length)
|
|
if min_flange_outside_length < SENDCUTSEND_MIN_FLANGE_FORMED_MM:
|
|
raise RuntimeError(
|
|
"Flange length is below the configured bend-rule minimum: "
|
|
f"{min_flange_outside_length:.3f} mm < {SENDCUTSEND_MIN_FLANGE_FORMED_MM:.3f} mm"
|
|
)
|
|
if min_flange_flat_length <= 0.0:
|
|
raise RuntimeError(
|
|
"Flat flange length implied by the bend rule is too short: "
|
|
f"{min_flange_flat_length:.3f} mm"
|
|
)
|
|
|
|
web_outside_length = layout.y_max - layout.return_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 = top_flange_flat_length + bend_allowance
|
|
web_flat_end = web_flat_start + web_flat_length
|
|
total_length = web_flat_end + bend_allowance + return_flange_flat_length
|
|
width = layout.z_max - layout.z_min
|
|
bend_line_positions = (
|
|
top_flange_flat_length + (0.5 * bend_allowance),
|
|
web_flat_end + (0.5 * bend_allowance),
|
|
)
|
|
|
|
top_tangent_y = layout.y_max - SENDCUTSEND_EFFECTIVE_BEND_RADIUS_MM
|
|
bottom_tangent_y = layout.return_y_min + SENDCUTSEND_EFFECTIVE_BEND_RADIUS_MM
|
|
|
|
feature_extents: list[tuple[float, float]] = []
|
|
cut_circles: list[tuple[float, float, float]] = []
|
|
for x, z, radius in layout.attachment_holes:
|
|
circle = (x - layout.x_min, z - layout.z_min, radius)
|
|
cut_circles.append(circle)
|
|
feature_extents.append((circle[0], radius))
|
|
for y, z, radius in layout.servo_bus_mount_holes_yzr:
|
|
circle = (web_flat_start + (top_tangent_y - y), z - layout.z_min, radius)
|
|
cut_circles.append(circle)
|
|
feature_extents.append((circle[0], radius))
|
|
for x, z, radius in layout.return_mount_holes:
|
|
circle = (total_length - (x - layout.return_x_min), z - layout.z_min, radius)
|
|
cut_circles.append(circle)
|
|
feature_extents.append((circle[0], radius))
|
|
|
|
min_feature_to_bend = min(
|
|
min(abs(center_u - bend_u) - feature_radius for bend_u in bend_line_positions)
|
|
for center_u, feature_radius in feature_extents
|
|
)
|
|
|
|
return FlatPattern(
|
|
total_length_mm=total_length,
|
|
width_mm=width,
|
|
top_flange_flat_length_mm=top_flange_flat_length,
|
|
return_flange_flat_length_mm=return_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_circles=tuple(cut_circles),
|
|
rounded_corner_radius_mm=CORNER_RADIUS_MM,
|
|
min_feature_to_bend_mm=min_feature_to_bend,
|
|
)
|
|
|
|
|
|
def _add_rounded_rect_outline(
|
|
msp: ezdxf.layouts.Modelspace,
|
|
*,
|
|
length: float,
|
|
width: float,
|
|
radius: float,
|
|
) -> None:
|
|
if radius <= 0.0:
|
|
raise RuntimeError(f"Corner radius must be positive, got {radius:.3f} mm")
|
|
if width <= (2.0 * radius):
|
|
raise RuntimeError(
|
|
"Corner radius is too large for the base clamp flat-pattern width: "
|
|
f"{radius:.3f} mm"
|
|
)
|
|
if length <= (2.0 * radius):
|
|
raise RuntimeError(
|
|
"Corner radius is too large for the base clamp flat-pattern length: "
|
|
f"{radius:.3f} mm"
|
|
)
|
|
|
|
msp.add_line((radius, 0.0), (length - radius, 0.0), dxfattribs={"layer": "CUT"})
|
|
msp.add_arc(
|
|
center=(length - radius, radius),
|
|
radius=radius,
|
|
start_angle=-90.0,
|
|
end_angle=0.0,
|
|
dxfattribs={"layer": "CUT"},
|
|
)
|
|
msp.add_line((length, radius), (length, width - radius), dxfattribs={"layer": "CUT"})
|
|
msp.add_arc(
|
|
center=(length - radius, width - radius),
|
|
radius=radius,
|
|
start_angle=0.0,
|
|
end_angle=90.0,
|
|
dxfattribs={"layer": "CUT"},
|
|
)
|
|
msp.add_line((length - radius, width), (radius, width), dxfattribs={"layer": "CUT"})
|
|
msp.add_arc(
|
|
center=(radius, width - radius),
|
|
radius=radius,
|
|
start_angle=90.0,
|
|
end_angle=180.0,
|
|
dxfattribs={"layer": "CUT"},
|
|
)
|
|
msp.add_line((0.0, width - radius), (0.0, radius), dxfattribs={"layer": "CUT"})
|
|
msp.add_arc(
|
|
center=(radius, radius),
|
|
radius=radius,
|
|
start_angle=180.0,
|
|
end_angle=270.0,
|
|
dxfattribs={"layer": "CUT"},
|
|
)
|
|
|
|
|
|
def build_dxf() -> ezdxf.document.Drawing:
|
|
layout = _build_bracket_layout()
|
|
flat_pattern = _build_flat_pattern(layout)
|
|
|
|
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()
|
|
_add_rounded_rect_outline(
|
|
msp,
|
|
length=flat_pattern.total_length_mm,
|
|
width=flat_pattern.width_mm,
|
|
radius=flat_pattern.rounded_corner_radius_mm,
|
|
)
|
|
for center_u, center_v, radius in flat_pattern.cut_circles:
|
|
msp.add_circle((center_u, center_v), radius, 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(
|
|
f"{DISPLAY_NAME} DXF: "
|
|
f"stock_thickness={PLATE_THICKNESS_MM:.3f} mm, "
|
|
f"flat_length={flat_pattern.total_length_mm:.3f} mm, "
|
|
f"flat_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_circles={len(flat_pattern.cut_circles)}"
|
|
)
|
|
print(
|
|
"DXF bend checks "
|
|
f"top_flange_flat={flat_pattern.top_flange_flat_length_mm:.3f} mm, "
|
|
f"return_flange_flat={flat_pattern.return_flange_flat_length_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 features enter 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(),
|
|
}
|
|
|
|
|
|
def gen() -> None:
|
|
gen_step()
|
|
gen_dxf()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
gen()
|