2024-07-19 21:00:10 -07:00
|
|
|
"""
|
|
|
|
Electronic components
|
|
|
|
"""
|
2024-07-22 15:02:26 -07:00
|
|
|
from dataclasses import dataclass, field
|
2024-07-21 18:45:13 -07:00
|
|
|
from typing import Optional, Tuple
|
|
|
|
import math
|
2024-07-19 21:00:10 -07:00
|
|
|
import cadquery as Cq
|
2024-07-22 15:02:26 -07:00
|
|
|
from nhf.build import Model, TargetKind, target, assembly, submodel
|
|
|
|
from nhf.materials import Role, Material
|
|
|
|
from nhf.parts.box import MountingBox, Hole
|
2024-07-19 21:00:10 -07:00
|
|
|
from nhf.parts.item import Item
|
|
|
|
from nhf.parts.fasteners import FlatHeadBolt, HexNut
|
2024-07-22 15:02:26 -07:00
|
|
|
from nhf.touhou.houjuu_nue.common import NUT_COMMON, BOLT_COMMON
|
2024-07-21 05:46:18 -07:00
|
|
|
import nhf.utils
|
2024-07-19 21:00:10 -07:00
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
class LinearActuator(Item):
|
|
|
|
stroke_length: float
|
|
|
|
shaft_diam: float = 9.04
|
|
|
|
|
|
|
|
front_hole_ext: float = 4.41
|
|
|
|
front_hole_diam: float = 4.41
|
|
|
|
front_length: float = 9.55
|
|
|
|
front_width: float = 9.24
|
|
|
|
front_height: float = 5.98
|
|
|
|
|
2024-07-24 18:17:39 -07:00
|
|
|
segment1_length: float = 37.54
|
2024-07-19 21:00:10 -07:00
|
|
|
segment1_width: float = 15.95
|
|
|
|
segment1_height: float = 11.94
|
|
|
|
|
2024-07-24 18:17:39 -07:00
|
|
|
segment2_length: float = 37.37
|
2024-07-19 21:00:10 -07:00
|
|
|
segment2_width: float = 20.03
|
|
|
|
segment2_height: float = 15.03
|
|
|
|
|
|
|
|
back_hole_ext: float = 4.58
|
|
|
|
back_hole_diam: float = 4.18
|
|
|
|
back_length: float = 9.27
|
|
|
|
back_width: float = 10.16
|
|
|
|
back_height: float = 8.12
|
|
|
|
|
|
|
|
@property
|
|
|
|
def name(self) -> str:
|
|
|
|
return f"LinearActuator {self.stroke_length}mm"
|
|
|
|
|
|
|
|
@property
|
|
|
|
def role(self) -> Role:
|
|
|
|
return Role.MOTION
|
|
|
|
|
|
|
|
@property
|
|
|
|
def conn_length(self):
|
|
|
|
return self.segment1_length + self.segment2_length + self.front_hole_ext + self.back_hole_ext
|
|
|
|
|
|
|
|
def generate(self, pos: float=0) -> Cq.Assembly:
|
2024-07-24 21:49:54 -07:00
|
|
|
assert -1e-6 <= pos <= 1 + 1e-6, f"Illegal position: {pos}"
|
2024-07-19 21:00:10 -07:00
|
|
|
stroke_x = pos * self.stroke_length
|
|
|
|
front = (
|
|
|
|
Cq.Workplane('XZ')
|
|
|
|
.cylinder(
|
|
|
|
radius=self.front_width / 2,
|
|
|
|
height=self.front_height,
|
|
|
|
centered=True,
|
|
|
|
)
|
|
|
|
.box(
|
|
|
|
length=self.front_hole_ext,
|
|
|
|
width=self.front_width,
|
|
|
|
height=self.front_height,
|
|
|
|
combine=True,
|
|
|
|
centered=(False, True, True)
|
|
|
|
)
|
|
|
|
.copyWorkplane(Cq.Workplane('XZ'))
|
|
|
|
.cylinder(
|
|
|
|
radius=self.front_hole_diam / 2,
|
|
|
|
height=self.front_height,
|
|
|
|
centered=True,
|
|
|
|
combine='cut',
|
|
|
|
)
|
|
|
|
)
|
2024-07-21 05:46:18 -07:00
|
|
|
front.copyWorkplane(Cq.Workplane('XZ')).tagPlane('conn')
|
2024-07-19 21:00:10 -07:00
|
|
|
if stroke_x > 0:
|
|
|
|
shaft = (
|
|
|
|
Cq.Workplane('YZ')
|
|
|
|
.cylinder(
|
|
|
|
radius=self.shaft_diam / 2,
|
|
|
|
height=stroke_x,
|
|
|
|
centered=(True, True, False)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
shaft = None
|
|
|
|
segment1 = (
|
|
|
|
Cq.Workplane()
|
|
|
|
.box(
|
|
|
|
length=self.segment1_length,
|
|
|
|
height=self.segment1_width,
|
|
|
|
width=self.segment1_height,
|
|
|
|
centered=(False, True, True),
|
|
|
|
)
|
|
|
|
)
|
|
|
|
segment2 = (
|
|
|
|
Cq.Workplane()
|
|
|
|
.box(
|
|
|
|
length=self.segment2_length,
|
|
|
|
height=self.segment2_width,
|
|
|
|
width=self.segment2_height,
|
|
|
|
centered=(False, True, True),
|
|
|
|
)
|
|
|
|
)
|
|
|
|
back = (
|
|
|
|
Cq.Workplane('XZ')
|
|
|
|
.cylinder(
|
|
|
|
radius=self.back_width / 2,
|
|
|
|
height=self.back_height,
|
|
|
|
centered=True,
|
|
|
|
)
|
|
|
|
.box(
|
|
|
|
length=self.back_hole_ext,
|
|
|
|
width=self.back_width,
|
|
|
|
height=self.back_height,
|
|
|
|
combine=True,
|
|
|
|
centered=(False, True, True)
|
|
|
|
)
|
|
|
|
.copyWorkplane(Cq.Workplane('XZ'))
|
|
|
|
.cylinder(
|
|
|
|
radius=self.back_hole_diam / 2,
|
|
|
|
height=self.back_height,
|
|
|
|
centered=True,
|
|
|
|
combine='cut',
|
|
|
|
)
|
|
|
|
)
|
2024-07-21 05:46:18 -07:00
|
|
|
back.copyWorkplane(Cq.Workplane('XZ')).tagPlane('conn')
|
2024-07-19 21:00:10 -07:00
|
|
|
result = (
|
|
|
|
Cq.Assembly()
|
|
|
|
.add(front, name="front",
|
|
|
|
loc=Cq.Location((-self.front_hole_ext, 0, 0)))
|
|
|
|
.add(segment1, name="segment1",
|
|
|
|
loc=Cq.Location((stroke_x, 0, 0)))
|
|
|
|
.add(segment2, name="segment2",
|
|
|
|
loc=Cq.Location((stroke_x + self.segment1_length, 0, 0)))
|
|
|
|
.add(back, name="back",
|
|
|
|
loc=Cq.Location((stroke_x + self.segment1_length + self.segment2_length + self.back_hole_ext, 0, 0), (0, 1, 0), 180))
|
|
|
|
)
|
|
|
|
if shaft:
|
|
|
|
result.add(shaft, name="shaft")
|
|
|
|
return result
|
|
|
|
|
2024-07-21 05:46:18 -07:00
|
|
|
@dataclass(frozen=True)
|
|
|
|
class MountingBracket(Item):
|
|
|
|
"""
|
|
|
|
Mounting bracket for a linear actuator
|
|
|
|
"""
|
|
|
|
mass: float = 1.6
|
|
|
|
hole_diam: float = 4.0
|
|
|
|
width: float = 8.0
|
|
|
|
height: float = 12.20
|
|
|
|
thickness: float = 0.98
|
|
|
|
length: float = 13.00
|
2024-07-24 18:17:39 -07:00
|
|
|
hole_to_side_ext: float = 8.25
|
2024-07-21 05:46:18 -07:00
|
|
|
|
|
|
|
def __post_init__(self):
|
|
|
|
assert self.hole_to_side_ext - self.hole_diam / 2 > 0
|
|
|
|
|
|
|
|
@property
|
|
|
|
def name(self) -> str:
|
|
|
|
return f"MountingBracket M{int(self.hole_diam)}"
|
|
|
|
|
|
|
|
@property
|
|
|
|
def role(self) -> Role:
|
|
|
|
return Role.MOTION
|
|
|
|
|
|
|
|
def generate(self) -> Cq.Workplane:
|
|
|
|
result = (
|
|
|
|
Cq.Workplane('XY')
|
|
|
|
.box(
|
|
|
|
length=self.hole_to_side_ext,
|
|
|
|
width=self.width,
|
|
|
|
height=self.height,
|
|
|
|
centered=(False, True, True)
|
|
|
|
)
|
|
|
|
.copyWorkplane(Cq.Workplane('XY'))
|
|
|
|
.cylinder(
|
|
|
|
height=self.height,
|
|
|
|
radius=self.width / 2,
|
|
|
|
combine=True,
|
|
|
|
)
|
|
|
|
.copyWorkplane(Cq.Workplane('XY'))
|
|
|
|
.box(
|
|
|
|
length=2 * (self.hole_to_side_ext - self.thickness),
|
|
|
|
width=self.width,
|
|
|
|
height=self.height - self.thickness * 2,
|
|
|
|
combine='cut',
|
|
|
|
)
|
|
|
|
.copyWorkplane(Cq.Workplane('XY'))
|
|
|
|
.cylinder(
|
|
|
|
height=self.height,
|
|
|
|
radius=self.hole_diam / 2,
|
|
|
|
combine='cut'
|
|
|
|
)
|
|
|
|
.copyWorkplane(Cq.Workplane('YZ'))
|
|
|
|
.cylinder(
|
|
|
|
height=self.hole_to_side_ext * 2,
|
|
|
|
radius=self.hole_diam / 2,
|
|
|
|
combine='cut'
|
|
|
|
)
|
|
|
|
)
|
|
|
|
result.copyWorkplane(Cq.Workplane('YZ', origin=(self.hole_to_side_ext, 0, 0))).tagPlane("conn_side")
|
|
|
|
result.copyWorkplane(Cq.Workplane('XY', origin=(0, 0, self.height/2))).tagPlane("conn_top")
|
|
|
|
result.copyWorkplane(Cq.Workplane('YX', origin=(0, 0, -self.height/2))).tagPlane("conn_bot")
|
|
|
|
result.copyWorkplane(Cq.Workplane('XY')).tagPlane("conn_mid")
|
|
|
|
return result
|
|
|
|
|
2024-07-21 22:34:19 -07:00
|
|
|
@dataclass(frozen=True)
|
|
|
|
class BatteryBox18650(Item):
|
|
|
|
"""
|
|
|
|
A number of 18650 batteries in series
|
|
|
|
"""
|
|
|
|
mass: float = 17.4 + 68.80 * 3
|
|
|
|
length: float = 75.70
|
|
|
|
width_base: float = 61.46 - 18.48 - 20.18 * 2
|
|
|
|
battery_dist: float = 20.18
|
|
|
|
height: float = 19.66
|
|
|
|
# space from bottom to battery begin
|
|
|
|
thickness: float = 1.66
|
|
|
|
battery_diam: float = 18.48
|
|
|
|
battery_height: float = 68.80
|
|
|
|
n_batteries: int = 3
|
|
|
|
|
|
|
|
def __post_init__(self):
|
|
|
|
assert 2 * self.thickness < min(self.length, self.height)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def name(self) -> str:
|
|
|
|
return f"BatteryBox 18650*{self.n_batteries}"
|
|
|
|
|
|
|
|
@property
|
|
|
|
def role(self) -> Role:
|
|
|
|
return Role.ELECTRONIC
|
|
|
|
|
|
|
|
def generate(self) -> Cq.Workplane:
|
|
|
|
width = self.width_base + self.battery_dist * (self.n_batteries - 1) + self.battery_diam
|
|
|
|
return (
|
|
|
|
Cq.Workplane('XY')
|
|
|
|
.box(
|
|
|
|
length=self.length,
|
|
|
|
width=width,
|
|
|
|
height=self.height,
|
|
|
|
centered=(True, True, False),
|
|
|
|
)
|
|
|
|
.copyWorkplane(Cq.Workplane('XY', origin=(0, 0, self.thickness)))
|
|
|
|
.box(
|
|
|
|
length=self.length - self.thickness*2,
|
|
|
|
width=width - self.thickness*2,
|
|
|
|
height=self.height - self.thickness,
|
|
|
|
centered=(True, True, False),
|
|
|
|
combine='cut',
|
|
|
|
)
|
|
|
|
.copyWorkplane(Cq.Workplane('XY', origin=(-self.battery_height/2, 0, self.thickness + self.battery_diam/2)))
|
|
|
|
.rarray(
|
|
|
|
xSpacing=1,
|
|
|
|
ySpacing=self.battery_dist,
|
|
|
|
xCount=1,
|
|
|
|
yCount=self.n_batteries,
|
|
|
|
center=True,
|
|
|
|
)
|
|
|
|
.cylinder(
|
|
|
|
radius=self.battery_diam/2,
|
|
|
|
height=self.battery_height,
|
|
|
|
direct=(1, 0, 0),
|
|
|
|
centered=(True, True, False),
|
|
|
|
combine=True,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2024-07-21 18:45:13 -07:00
|
|
|
LINEAR_ACTUATOR_50 = LinearActuator(
|
2024-07-24 18:17:39 -07:00
|
|
|
mass=40.8,
|
2024-07-21 18:45:13 -07:00
|
|
|
stroke_length=50,
|
2024-07-24 18:17:39 -07:00
|
|
|
shaft_diam=9.05,
|
|
|
|
front_hole_ext=4.32,
|
|
|
|
back_hole_ext=4.54,
|
|
|
|
segment1_length=57.35,
|
|
|
|
segment1_width=15.97,
|
|
|
|
segment1_height=11.95,
|
|
|
|
segment2_length=37.69,
|
|
|
|
segment2_width=19.97,
|
|
|
|
segment2_height=14.96,
|
|
|
|
|
|
|
|
front_length=9.40,
|
|
|
|
front_width=9.17,
|
|
|
|
front_height=6.12,
|
|
|
|
back_length=9.18,
|
|
|
|
back_width=10.07,
|
|
|
|
back_height=8.06,
|
2024-07-21 18:45:13 -07:00
|
|
|
)
|
|
|
|
LINEAR_ACTUATOR_30 = LinearActuator(
|
2024-07-19 21:00:10 -07:00
|
|
|
mass=34.0,
|
|
|
|
stroke_length=30,
|
|
|
|
)
|
2024-07-21 18:45:13 -07:00
|
|
|
LINEAR_ACTUATOR_21 = LinearActuator(
|
|
|
|
# FIXME: Measure
|
|
|
|
mass=0.0,
|
|
|
|
stroke_length=21,
|
|
|
|
front_hole_ext=4,
|
|
|
|
back_hole_ext=4,
|
|
|
|
segment1_length=75/2,
|
|
|
|
segment2_length=75/2,
|
|
|
|
)
|
|
|
|
LINEAR_ACTUATOR_10 = LinearActuator(
|
|
|
|
# FIXME: Measure
|
|
|
|
mass=0.0,
|
|
|
|
stroke_length=10,
|
|
|
|
front_hole_ext=4.5/2,
|
|
|
|
back_hole_ext=4.5/2,
|
|
|
|
segment1_length=30.0,
|
|
|
|
segment2_length=30.0,
|
2024-07-22 09:49:16 -07:00
|
|
|
segment1_width=15.0,
|
|
|
|
segment2_width=21.0,
|
2024-07-21 18:45:13 -07:00
|
|
|
)
|
2024-07-19 21:00:10 -07:00
|
|
|
LINEAR_ACTUATOR_HEX_NUT = HexNut(
|
|
|
|
mass=0.8,
|
|
|
|
diam_thread=4,
|
|
|
|
pitch=0.7,
|
|
|
|
thickness=4.16,
|
|
|
|
width=6.79,
|
|
|
|
)
|
|
|
|
LINEAR_ACTUATOR_BOLT = FlatHeadBolt(
|
|
|
|
mass=1.7,
|
2024-07-21 05:46:18 -07:00
|
|
|
diam_head=6.68,
|
2024-07-19 21:00:10 -07:00
|
|
|
height_head=2.98,
|
|
|
|
diam_thread=4.0,
|
|
|
|
height_thread=15.83,
|
|
|
|
)
|
2024-07-21 05:46:18 -07:00
|
|
|
LINEAR_ACTUATOR_BRACKET = MountingBracket()
|
|
|
|
|
2024-07-21 22:34:19 -07:00
|
|
|
BATTERY_BOX = BatteryBox18650()
|
|
|
|
|
2024-07-21 18:45:13 -07:00
|
|
|
@dataclass
|
|
|
|
class Flexor:
|
|
|
|
"""
|
|
|
|
Actuator assembly which flexes, similar to biceps
|
|
|
|
"""
|
|
|
|
motion_span: float
|
2024-07-24 21:49:54 -07:00
|
|
|
arm_radius: Optional[float] = None
|
2024-07-24 22:24:23 -07:00
|
|
|
pos_smaller: bool = True
|
2024-07-21 05:46:18 -07:00
|
|
|
|
2024-07-21 21:49:28 -07:00
|
|
|
actuator: LinearActuator = LINEAR_ACTUATOR_50
|
2024-07-21 05:46:18 -07:00
|
|
|
nut: HexNut = LINEAR_ACTUATOR_HEX_NUT
|
|
|
|
bolt: FlatHeadBolt = LINEAR_ACTUATOR_BOLT
|
|
|
|
bracket: MountingBracket = LINEAR_ACTUATOR_BRACKET
|
|
|
|
|
2024-07-21 18:45:13 -07:00
|
|
|
# FIXME: Add a compression spring so the serviceable distances are not as fixed
|
|
|
|
|
|
|
|
@property
|
|
|
|
def mount_height(self):
|
|
|
|
return self.bracket.hole_to_side_ext
|
|
|
|
|
2024-07-22 13:26:37 -07:00
|
|
|
def open_pos(self) -> Tuple[float, float, float]:
|
2024-07-24 21:49:54 -07:00
|
|
|
r, phi, r_ = nhf.geometry.contraction_span_pos_from_radius(
|
2024-07-22 13:26:37 -07:00
|
|
|
d_open=self.actuator.conn_length + self.actuator.stroke_length,
|
|
|
|
d_closed=self.actuator.conn_length,
|
|
|
|
theta=math.radians(self.motion_span),
|
2024-07-24 21:49:54 -07:00
|
|
|
r=self.arm_radius,
|
2024-07-24 22:24:23 -07:00
|
|
|
smaller=self.pos_smaller,
|
2024-07-22 13:26:37 -07:00
|
|
|
)
|
|
|
|
return r, math.degrees(phi), r_
|
2024-07-22 01:28:58 -07:00
|
|
|
|
2024-07-21 18:45:13 -07:00
|
|
|
def target_length_at_angle(
|
|
|
|
self,
|
|
|
|
angle: float = 0.0
|
|
|
|
) -> float:
|
2024-07-22 13:26:37 -07:00
|
|
|
"""
|
|
|
|
Length of the actuator at some angle
|
|
|
|
"""
|
2024-07-24 21:49:54 -07:00
|
|
|
assert 0 <= angle <= self.motion_span
|
2024-07-22 13:26:37 -07:00
|
|
|
r, phi, rp = self.open_pos()
|
2024-07-22 01:28:58 -07:00
|
|
|
th = math.radians(phi - angle)
|
2024-07-24 21:49:54 -07:00
|
|
|
|
|
|
|
result = math.sqrt(r * r + rp * rp - 2 * r * rp * math.cos(th))
|
|
|
|
#result = math.sqrt((r * math.cos(th) - rp) ** 2 + (r * math.sin(th)) ** 2)
|
|
|
|
assert self.actuator.conn_length <= result <= self.actuator.conn_length + self.actuator.stroke_length, \
|
|
|
|
f"Illegal length: {result} in {self.actuator.conn_length}+{self.actuator.stroke_length}"
|
|
|
|
return result
|
2024-07-21 18:45:13 -07:00
|
|
|
|
|
|
|
|
2024-07-21 05:46:18 -07:00
|
|
|
def add_to(
|
|
|
|
self,
|
|
|
|
a: Cq.Assembly,
|
2024-07-21 18:45:13 -07:00
|
|
|
target_length: float,
|
2024-07-21 05:46:18 -07:00
|
|
|
tag_prefix: Optional[str] = None,
|
|
|
|
tag_hole_front: Optional[str] = None,
|
|
|
|
tag_hole_back: Optional[str] = None,
|
|
|
|
tag_dir: Optional[str] = None):
|
|
|
|
"""
|
|
|
|
Adds the necessary mechanical components to this assembly. Does not
|
|
|
|
invoke `a.solve()`.
|
|
|
|
"""
|
2024-07-21 18:45:13 -07:00
|
|
|
pos = (target_length - self.actuator.conn_length) / self.actuator.stroke_length
|
2024-07-21 05:46:18 -07:00
|
|
|
if tag_prefix:
|
|
|
|
tag_prefix = tag_prefix + "_"
|
2024-07-23 22:40:49 -07:00
|
|
|
else:
|
|
|
|
tag_prefix = ""
|
2024-07-21 05:46:18 -07:00
|
|
|
name_actuator = f"{tag_prefix}actuator"
|
|
|
|
name_bracket_front = f"{tag_prefix}bracket_front"
|
|
|
|
name_bracket_back = f"{tag_prefix}bracket_back"
|
|
|
|
name_bolt_front = f"{tag_prefix}front_bolt"
|
|
|
|
name_bolt_back = f"{tag_prefix}back_bolt"
|
|
|
|
name_nut_front = f"{tag_prefix}front_nut"
|
|
|
|
name_nut_back = f"{tag_prefix}back_nut"
|
|
|
|
(
|
|
|
|
a
|
2024-07-21 18:45:13 -07:00
|
|
|
.add(self.actuator.assembly(pos=pos), name=name_actuator)
|
2024-07-21 05:46:18 -07:00
|
|
|
.add(self.bracket.assembly(), name=name_bracket_front)
|
|
|
|
.add(self.bolt.assembly(), name=name_bolt_front)
|
|
|
|
.add(self.nut.assembly(), name=name_nut_front)
|
|
|
|
.constrain(f"{name_actuator}/front?conn", f"{name_bracket_front}?conn_mid",
|
|
|
|
"Plane", param=0)
|
|
|
|
.constrain(f"{name_bolt_front}?root", f"{name_bracket_front}?conn_top",
|
|
|
|
"Plane", param=0)
|
|
|
|
.constrain(f"{name_nut_front}?bot", f"{name_bracket_front}?conn_bot",
|
|
|
|
"Plane")
|
|
|
|
.add(self.bracket.assembly(), name=name_bracket_back)
|
|
|
|
.add(self.bolt.assembly(), name=name_bolt_back)
|
|
|
|
.add(self.nut.assembly(), name=name_nut_back)
|
|
|
|
.constrain(f"{name_actuator}/back?conn", f"{name_bracket_back}?conn_mid",
|
|
|
|
"Plane", param=0)
|
|
|
|
.constrain(f"{name_bolt_back}?root", f"{name_bracket_back}?conn_top",
|
|
|
|
"Plane", param=0)
|
|
|
|
.constrain(f"{name_nut_back}?bot", f"{name_bracket_back}?conn_bot",
|
|
|
|
"Plane")
|
|
|
|
)
|
|
|
|
if tag_hole_front:
|
|
|
|
a.constrain(tag_hole_front, f"{name_bracket_front}?conn_side", "Plane")
|
|
|
|
if tag_hole_back:
|
|
|
|
a.constrain(tag_hole_back, f"{name_bracket_back}?conn_side", "Plane")
|
|
|
|
if tag_dir:
|
|
|
|
a.constrain(tag_dir, f"{name_bracket_front}?conn_mid", "Axis", param=0)
|
2024-07-21 23:34:02 -07:00
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
2024-07-22 15:02:26 -07:00
|
|
|
class ElectronicBoard(Model):
|
|
|
|
|
|
|
|
name: str = "electronic-board"
|
|
|
|
nut: HexNut = NUT_COMMON
|
|
|
|
bolt: FlatHeadBolt = BOLT_COMMON
|
|
|
|
length: float = 70.0
|
|
|
|
width: float = 170.0
|
|
|
|
mount_holes: list[Hole] = field(default_factory=lambda: [
|
|
|
|
Hole(x=30, y=80),
|
|
|
|
Hole(x=30, y=-80),
|
|
|
|
Hole(x=-30, y=80),
|
|
|
|
Hole(x=-30, y=-80),
|
|
|
|
])
|
|
|
|
panel_thickness: float = 25.4 / 16
|
|
|
|
mount_panel_thickness: float = 25.4 / 4
|
|
|
|
material: Material = Material.WOOD_BIRCH
|
2024-07-21 23:34:02 -07:00
|
|
|
|
2024-07-22 15:02:26 -07:00
|
|
|
@property
|
|
|
|
def mount_hole_diam(self) -> float:
|
|
|
|
return self.bolt.diam_thread
|
|
|
|
|
|
|
|
def __post_init__(self):
|
|
|
|
super().__init__(name=self.name)
|
|
|
|
|
|
|
|
@submodel(name="panel")
|
|
|
|
def panel(self) -> MountingBox:
|
|
|
|
return MountingBox(
|
|
|
|
holes=self.mount_holes,
|
|
|
|
hole_diam=self.mount_hole_diam,
|
|
|
|
length=self.length,
|
|
|
|
width=self.width,
|
|
|
|
centred=(True, True),
|
|
|
|
thickness=self.panel_thickness,
|
|
|
|
generate_reverse_tags=True,
|
|
|
|
)
|
|
|
|
|
|
|
|
def assembly(self) -> Cq.Assembly:
|
|
|
|
panel = self.panel()
|
|
|
|
result = (
|
|
|
|
Cq.Assembly()
|
|
|
|
.addS(panel.generate(), name="panel",
|
|
|
|
role=Role.STRUCTURE, material=self.material)
|
|
|
|
)
|
|
|
|
for hole in panel.holes:
|
|
|
|
spacer_name = f"{hole.tag}_spacer"
|
|
|
|
bolt_name = f"{hole.tag}_bolt"
|
|
|
|
(
|
|
|
|
result
|
|
|
|
.add(self.nut.assembly(), name=spacer_name)
|
|
|
|
.add(self.bolt.assembly(), name=bolt_name)
|
|
|
|
.constrain(
|
|
|
|
f"{spacer_name}?top",
|
|
|
|
f"panel?{hole.rev_tag}",
|
|
|
|
"Plane"
|
|
|
|
)
|
|
|
|
.constrain(
|
|
|
|
f"{bolt_name}?root",
|
|
|
|
f"panel?{hole.tag}",
|
|
|
|
"Plane", param=0
|
|
|
|
)
|
|
|
|
)
|
|
|
|
return result.solve()
|