588 lines
18 KiB
Python
588 lines
18 KiB
Python
import math
|
|
from dataclasses import dataclass, field
|
|
from typing import Tuple
|
|
import cadquery as Cq
|
|
from nhf import Material, Role
|
|
from nhf.build import Model, target, assembly, TargetKind
|
|
import nhf.utils
|
|
|
|
@dataclass
|
|
class Rod(Model):
|
|
|
|
width: float = 120.0
|
|
length: float = 550.0
|
|
length_tip: float = 100.0
|
|
width_tail: float = 60.0
|
|
margin: float = 10.0
|
|
|
|
thickness_top: float = 25.4 / 8
|
|
# The side which has mounted hinges must be thicker
|
|
thickness_side: float = 25.4 / 4
|
|
|
|
height_internal: float = 30.0
|
|
|
|
material_shell: Material = Material.WOOD_BIRCH
|
|
|
|
# Considering the glyph on the top ...
|
|
|
|
# counted from middle to the bottom
|
|
fac_bar_top: float = 0.1
|
|
# counted from bottom to top
|
|
fac_window_tsumi_bot: float = 0.63
|
|
fac_window_tsumi_top: float = 0.88
|
|
|
|
fac_window_footer_bot: float = 0.36
|
|
fac_window_footer_top: float = 0.6
|
|
|
|
# Considering the side ...
|
|
hinge_plate_pos: list[float] = field(default_factory=lambda: [0.1, 0.9])
|
|
hinge_plate_length: float = 30.0
|
|
hinge_hole_diam: float = 2.5
|
|
# Hole distance to axis
|
|
hinge_hole_axis_dist: float = 12.5 / 2
|
|
# Distance between holes
|
|
hinge_hole_sep: float = 15.89
|
|
|
|
# Consider the reference objects
|
|
ref_object_width: float = 50.0
|
|
ref_object_length: float = 50.0
|
|
|
|
def __post_init__(self):
|
|
super().__init__(name="rod")
|
|
self.loc_core = Cq.Location.from2d(self.length - self.length_tip, 0)
|
|
assert self.length_tip * 2 < self.length
|
|
#assert self.fac_bar_top + self.fac_window_tsumi_top < 1
|
|
assert self.fac_window_tsumi_bot < self.fac_window_tsumi_top
|
|
|
|
@property
|
|
def length_tail(self):
|
|
return self.length - self.length_tip
|
|
|
|
@property
|
|
def _reduced_tip_x(self):
|
|
return self.length_tip - self.margin
|
|
@property
|
|
def _reduced_y(self):
|
|
return self.width / 2 - self.margin
|
|
@property
|
|
def _reduced_tail_y(self):
|
|
return self.width_tail / 2 - self.margin
|
|
|
|
def profile_points(self) -> list[Tuple[str, Tuple[float, float]]]:
|
|
"""
|
|
Points in polygon line order, labaled
|
|
"""
|
|
return [
|
|
("tip", (self.length, 0)),
|
|
("mid_r", (self.length - self.length_tip, self.width/2)),
|
|
("bot_r", (0, self.width_tail / 2)),
|
|
("bot_l", (0, -self.width_tail / 2)),
|
|
("mid_l", (self.length - self.length_tip, -self.width/2)),
|
|
]
|
|
|
|
def _window_tip(self) -> Cq.Sketch:
|
|
dxh = self._reduced_tip_x
|
|
dy = self._reduced_y
|
|
return (
|
|
Cq.Sketch()
|
|
.segment(
|
|
(dxh, 0),
|
|
(dxh / 2, dy / 2),
|
|
)
|
|
.bezier([
|
|
(dxh / 2, dy / 2),
|
|
(dxh * 0.6, dy * 0.4),
|
|
(dxh * 0.6, -dy * 0.4),
|
|
(dxh / 2, -dy / 2),
|
|
])
|
|
.segment(
|
|
(dxh, 0),
|
|
)
|
|
.assemble()
|
|
.moved(self.loc_core.to2d_pos())
|
|
)
|
|
def _window_eye(self, refl: bool = False) -> Cq.Sketch:
|
|
|
|
sign = -1 if refl else 1
|
|
dxh = self._reduced_tip_x
|
|
xm = dxh * 0.45
|
|
dy = sign * self._reduced_y
|
|
fac = 0.05
|
|
|
|
p1 = Cq.Location.from2d(xm, sign * self.margin / 2)
|
|
p2 = Cq.Location.from2d(dxh * 0.1, sign * self.margin / 2)
|
|
p3 = Cq.Location.from2d(dxh * 0.15, dy * 0.55)
|
|
p4 = Cq.Location.from2d(dxh * 0.4, dy * 0.45)
|
|
d4 = Cq.Location.from2d(dxh * fac, -dy * fac)
|
|
|
|
return (
|
|
Cq.Sketch()
|
|
.segment(
|
|
p1.to2d_pos(),
|
|
p2.to2d_pos(),
|
|
)
|
|
.bezier([
|
|
p2.to2d_pos(),
|
|
(p2 * Cq.Location.from2d(0, dy * fac)).to2d_pos(),
|
|
(p3 * Cq.Location.from2d(-dxh * fac, -dy * fac)).to2d_pos(),
|
|
p3.to2d_pos(),
|
|
])
|
|
.bezier([
|
|
p3.to2d_pos(),
|
|
(p3 * Cq.Location.from2d(0, dy * fac)).to2d_pos(),
|
|
(p4 * d4.inverse).to2d_pos(),
|
|
p4.to2d_pos(),
|
|
])
|
|
.bezier([
|
|
p4.to2d_pos(),
|
|
(p4 * d4).to2d_pos(),
|
|
(p1 * Cq.Location.from2d(0, dy * fac)).to2d_pos(),
|
|
p1.to2d_pos(),
|
|
])
|
|
.assemble()
|
|
.moved(self.loc_core.to2d_pos())
|
|
)
|
|
|
|
def _window_bar(self) -> Cq.Sketch():
|
|
dxh = self._reduced_tip_x
|
|
dy = self._reduced_y
|
|
dyt = self._reduced_tail_y
|
|
dxt = self.length_tail
|
|
|
|
ext_fac = self.fac_bar_top
|
|
|
|
p_corner = Cq.Location.from2d(0, dy)
|
|
p_top = Cq.Location.from2d(0.3 * dxh, 0.7 * dy)
|
|
p_bot = Cq.Location.from2d(-ext_fac * dxt, dy + ext_fac * (dyt - dy))
|
|
p_top_int = p_corner * Cq.Location.from2d(.05 * dxh, -.2 * dy)
|
|
p_top_ctrl = Cq.Location.from2d(0, .3 * dy)
|
|
p_bot_int = p_corner * Cq.Location.from2d(-.15 * dxh, -.2 * dy)
|
|
p_bot_ctrl = Cq.Location.from2d(-.25 * dxh, .3 * dy)
|
|
|
|
return (
|
|
Cq.Sketch()
|
|
.segment(
|
|
p_corner.to2d_pos(),
|
|
p_top.to2d_pos(),
|
|
)
|
|
.segment(p_top_int.to2d_pos())
|
|
.bezier([
|
|
p_top_int.to2d_pos(),
|
|
p_top_ctrl.to2d_pos(),
|
|
p_top_ctrl.flip_y().to2d_pos(),
|
|
p_top_int.flip_y().to2d_pos(),
|
|
])
|
|
.segment(p_top.flip_y().to2d_pos())
|
|
.segment(p_corner.flip_y().to2d_pos())
|
|
.segment(p_bot.flip_y().to2d_pos())
|
|
.segment(p_bot_int.flip_y().to2d_pos())
|
|
.bezier([
|
|
p_bot_int.flip_y().to2d_pos(),
|
|
p_bot_ctrl.flip_y().to2d_pos(),
|
|
p_bot_ctrl.to2d_pos(),
|
|
p_bot_int.to2d_pos(),
|
|
])
|
|
.segment(p_bot.to2d_pos())
|
|
.segment(p_corner.to2d_pos())
|
|
.assemble()
|
|
.moved(self.loc_core.to2d_pos())
|
|
)
|
|
|
|
def _window_tsumi(self) -> Cq.Sketch:
|
|
dx = (self.fac_window_tsumi_top - self.fac_window_tsumi_bot) * self.length_tail
|
|
dy = 2 * self._reduced_y * 0.8
|
|
loc = Cq.Location(self.fac_window_tsumi_bot * self.length_tail, 0)
|
|
|
|
# Construction of the top part of the kanji
|
|
|
|
dx_top = dx * 0.3
|
|
x_top = dx - dx_top / 2
|
|
dy_top = dy
|
|
dy_eye = dy * 0.2
|
|
dy_border = (dy_top - 3 * dy_eye) / 4
|
|
# The skip must follow 3 * eye + 4 * border = dy_top
|
|
y_skip = dy_eye + dy_border
|
|
|
|
# Construction of the bottom part
|
|
x_bot = dx * 0.65
|
|
y3 = dy * 0.4
|
|
y2 = dy * 0.2
|
|
y1 = dy * 0.1
|
|
# x/y-centers of the legs
|
|
x_leg0 = x_bot / 14
|
|
dx_leg = x_bot / 7
|
|
y_leg = (y3 + y1) / 2
|
|
|
|
return (
|
|
Cq.Sketch()
|
|
.push([(x_top, 0)])
|
|
.rect(dx_top, dy_top)
|
|
.push([
|
|
(x_top, -y_skip),
|
|
(x_top, 0),
|
|
(x_top, y_skip),
|
|
])
|
|
.rect(dx_top / 3, dy_eye, mode='s')
|
|
|
|
# Construct the two sides
|
|
.push([
|
|
(x_bot / 2, (y2 + y1) / 2),
|
|
(x_bot / 2, -(y2 + y1) / 2),
|
|
])
|
|
.rect(x_bot, y2 - y1, mode='a')
|
|
.push([
|
|
(x_leg0 + dx_leg, y_leg),
|
|
(x_leg0 + 3 * dx_leg, y_leg),
|
|
(x_leg0 + 5 * dx_leg, y_leg),
|
|
(x_leg0 + dx_leg, -y_leg),
|
|
(x_leg0 + 3 * dx_leg, -y_leg),
|
|
(x_leg0 + 5 * dx_leg, -y_leg),
|
|
])
|
|
.rect(dx_leg, y3 - y1, mode='a')
|
|
|
|
.moved(loc)
|
|
)
|
|
|
|
def _window_footer(self) -> Cq.Sketch:
|
|
x_bot = self.fac_window_footer_bot * self.length_tail
|
|
dx = (self.fac_window_footer_top - self.fac_window_footer_bot) * self.length_tail
|
|
loc = Cq.Location(x_bot, 0)
|
|
|
|
dy = self._reduced_y * 0.8
|
|
|
|
# eyes
|
|
eye_y2 = dy * .5
|
|
eye_y1 = dy * .2
|
|
eye_width = eye_y2 - eye_y1
|
|
eye_x = dx - eye_width / 2
|
|
|
|
# bar polygon
|
|
bar_x0 = dx * 0.65
|
|
bar_dx = dx * 0.1
|
|
bar_x1 = bar_x0 + bar_dx
|
|
bar_x2 = bar_x0 + bar_dx * 2
|
|
bar_x3 = bar_x0 + bar_dx * 3
|
|
bar_y1 = dy * .75
|
|
assert bar_y1 > eye_y2
|
|
bar_y2 = dy * .9
|
|
assert bar_y1 < bar_y2
|
|
|
|
# Construction of the cross
|
|
cross_dx = dx * 0.7 / math.sqrt(2)
|
|
cross_dy = dy * 0.2
|
|
|
|
cross = (
|
|
Cq.Sketch()
|
|
.rect(cross_dx, cross_dy)
|
|
.rect(cross_dy, cross_dx, mode='a')
|
|
.moved(Cq.Location.from2d(dx * 0.5, 0, 45))
|
|
)
|
|
return (
|
|
Cq.Sketch()
|
|
# eyes
|
|
.push([
|
|
(eye_x, (eye_y1 + eye_y2)/2),
|
|
(eye_x, -(eye_y1 + eye_y2)/2),
|
|
])
|
|
.rect(eye_width, eye_width, mode='a')
|
|
# middle bar
|
|
.push([(0,0)])
|
|
.polygon([
|
|
(bar_x1, bar_y1),
|
|
(bar_x0, bar_y1),
|
|
(bar_x0, bar_y2),
|
|
(bar_x3, bar_y2),
|
|
(bar_x3, bar_y1),
|
|
(bar_x2, bar_y1),
|
|
|
|
(bar_x2, -bar_y1),
|
|
(bar_x3, -bar_y1),
|
|
(bar_x3, -bar_y2),
|
|
(bar_x0, -bar_y2),
|
|
(bar_x0, -bar_y1),
|
|
(bar_x1, -bar_y1),
|
|
], mode='a')
|
|
# cross
|
|
.boolean(cross, mode='a')
|
|
|
|
#.push([(0,0)])
|
|
#.rect(10, 10)
|
|
|
|
.moved(loc)
|
|
)
|
|
|
|
@target(name="bottom", kind=TargetKind.DXF)
|
|
def profile_bottom(self) -> Cq.Sketch:
|
|
return (
|
|
Cq.Sketch()
|
|
.polygon([p for _, p in self.profile_points()])
|
|
)
|
|
|
|
@target(name="top", kind=TargetKind.DXF)
|
|
def profile_top(self) -> Cq.Sketch:
|
|
return (
|
|
self.profile_bottom()
|
|
.boolean(self._window_tip(), mode='s')
|
|
.boolean(self._window_eye(True), mode='s')
|
|
.boolean(self._window_eye(False), mode='s')
|
|
.boolean(self._window_bar(), mode='s')
|
|
.boolean(self._window_tsumi(), mode='s')
|
|
.boolean(self._window_footer(), mode='s')
|
|
)
|
|
|
|
def surface_top(self) -> Cq.Workplane:
|
|
return (
|
|
Cq.Workplane('XY')
|
|
.placeSketch(self.profile_top())
|
|
.extrude(self.thickness_top)
|
|
)
|
|
|
|
def surface_bottom(self) -> Cq.Workplane:
|
|
surface = (
|
|
Cq.Workplane('XY')
|
|
.placeSketch(self.profile_bottom())
|
|
.extrude(self.thickness_top)
|
|
)
|
|
plane = surface.faces(">Z").workplane()
|
|
|
|
for (name, p) in self.profile_points():
|
|
plane.moveTo(*p).tagPlane(name)
|
|
|
|
return surface
|
|
|
|
# Properties of the side surfaces
|
|
|
|
@property
|
|
def length_edge_tip(self):
|
|
return math.sqrt(self.length_tip ** 2 + (self.width / 2) ** 2)
|
|
@property
|
|
def length_edge_tail(self):
|
|
dw = (self.width - self.width_tail) / 2
|
|
return math.sqrt(self.length_tail ** 2 + dw ** 2)
|
|
@property
|
|
def tip_incident_angle(self):
|
|
"""
|
|
Angle (measuring from vertical) at which the tip edge pieces must be
|
|
sanded in order to make them not collide into each other.
|
|
"""
|
|
return math.atan2(self.length_tip, self.width / 2)
|
|
@property
|
|
def shoulder_incident_angle(self) -> float:
|
|
angle_tip = math.atan2(self.width / 2, self.length_tip)
|
|
angle_tail = math.atan2((self.width - self.width_tail) / 2, self.length_tail)
|
|
return (angle_tip + angle_tail) / 2
|
|
|
|
@target(name="ref-tip")
|
|
def ref_tip(self) -> Cq.Workplane:
|
|
angle = self.tip_incident_angle
|
|
w = self.ref_object_width
|
|
drop = math.sin(angle) * w
|
|
profile = (
|
|
Cq.Sketch()
|
|
.polygon([
|
|
(0, 0),
|
|
(0, w),
|
|
(w, w),
|
|
(w - drop, 0),
|
|
])
|
|
)
|
|
return (
|
|
Cq.Workplane()
|
|
.placeSketch(profile)
|
|
.extrude(self.ref_object_length)
|
|
)
|
|
@target(name="ref-shoulder")
|
|
def ref_shoulder(self) -> Cq.Workplane:
|
|
angle = self.shoulder_incident_angle
|
|
w = self.ref_object_width
|
|
drop = math.sin(angle) * w
|
|
profile = (
|
|
Cq.Sketch()
|
|
.polygon([
|
|
(0, 0),
|
|
(0, w),
|
|
(w, w),
|
|
(w - drop, 0),
|
|
])
|
|
)
|
|
return (
|
|
Cq.Workplane()
|
|
.placeSketch(profile)
|
|
.extrude(self.ref_object_length)
|
|
)
|
|
|
|
@target(name="side-tip-2x", kind=TargetKind.DXF)
|
|
def profile_side_tip(self):
|
|
l = self.length_edge_tip
|
|
w = self.height_internal
|
|
return (
|
|
Cq.Sketch()
|
|
.push([(l/2, w/2)])
|
|
.rect(l, w)
|
|
)
|
|
@target(name="side-tail", kind=TargetKind.DXF)
|
|
def profile_side_tail(self):
|
|
"""
|
|
Plain side 2 with no hinge
|
|
"""
|
|
l = self.length_edge_tail
|
|
w = self.height_internal
|
|
return (
|
|
Cq.Sketch()
|
|
.push([(l/2, w/2)])
|
|
.rect(l, w)
|
|
)
|
|
@target(name="side-hinge-plate", kind=TargetKind.DXF)
|
|
def profile_side_hinge_plate(self):
|
|
l = self.hinge_plate_length
|
|
w = self.height_internal / 2
|
|
return (
|
|
Cq.Sketch()
|
|
.push([(0, w/2)])
|
|
.rect(l, w)
|
|
.push([
|
|
(self.hinge_hole_sep / 2, self.hinge_hole_axis_dist),
|
|
(-self.hinge_hole_sep / 2, self.hinge_hole_axis_dist),
|
|
])
|
|
.circle(self.hinge_hole_diam / 2, mode='s')
|
|
)
|
|
@target(name="side-tail-hinged", kind=TargetKind.DXF)
|
|
def profile_side_tail_hinged(self):
|
|
"""
|
|
Plain side 2 with no hinge
|
|
"""
|
|
l = self.length_edge_tail
|
|
w = self.height_internal
|
|
|
|
# Holes for hinge
|
|
plate_pos = [
|
|
(t * l, w * 3/4) for t in self.hinge_plate_pos
|
|
]
|
|
hole_pos = [
|
|
(self.hinge_hole_sep / 2, self.hinge_hole_axis_dist),
|
|
(-self.hinge_hole_sep / 2, self.hinge_hole_axis_dist),
|
|
]
|
|
return (
|
|
self.profile_side_tail()
|
|
.push(plate_pos)
|
|
.rect(self.hinge_plate_length, w/2, mode='s')
|
|
.push([
|
|
(hx + px, w/2 - hy)
|
|
for hx, hy in hole_pos
|
|
for px, _ in plate_pos
|
|
])
|
|
.circle(self.hinge_hole_diam / 2, mode='s')
|
|
)
|
|
@target(name="side-bot", kind=TargetKind.DXF)
|
|
def profile_side_bot(self):
|
|
l = self.width_tail - self.thickness_side * 2
|
|
w = self.height_internal
|
|
return (
|
|
Cq.Sketch()
|
|
.rect(l, w)
|
|
)
|
|
|
|
def surface_side_tip(self):
|
|
result = (
|
|
Cq.Workplane('XY')
|
|
.placeSketch(self.profile_side_tip())
|
|
.extrude(self.thickness_side)
|
|
)
|
|
plane = result.faces(">Y").workplane()
|
|
plane.moveTo(0, 0).tagPlane("bot")
|
|
plane.moveTo(-self.length_edge_tip, 0).tagPlane("top")
|
|
return result
|
|
def surface_side_tail(self):
|
|
result = (
|
|
Cq.Workplane('XY')
|
|
.placeSketch(self.profile_side_tail())
|
|
.extrude(self.thickness_side)
|
|
)
|
|
plane = result.faces(">Y").workplane()
|
|
plane.moveTo(0, 0).tagPlane("bot")
|
|
plane.moveTo(-self.length_edge_tail, 0).tagPlane("top")
|
|
return result
|
|
def surface_side_tail_hinged(self):
|
|
result = (
|
|
Cq.Workplane('XY')
|
|
.placeSketch(self.profile_side_tail_hinged())
|
|
.extrude(self.thickness_side)
|
|
)
|
|
plane = result.faces(">Y").workplane()
|
|
plane.moveTo(0, 0).tagPlane("bot")
|
|
plane.moveTo(-self.length_edge_tail, 0).tagPlane("top")
|
|
return result
|
|
def surface_side_bot(self):
|
|
result = (
|
|
Cq.Workplane('XY')
|
|
.placeSketch(self.profile_side_bot())
|
|
.extrude(self.thickness_side)
|
|
)
|
|
plane = result.faces(">Y").workplane()
|
|
plane.moveTo(self.width_tail / 2, 0).tagPlane("bot")
|
|
plane.moveTo(-self.width_tail / 2, 0).tagPlane("top")
|
|
return result
|
|
|
|
@assembly()
|
|
def assembly(self) -> Cq.Assembly:
|
|
a = (
|
|
Cq.Assembly()
|
|
.addS(
|
|
self.surface_top(),
|
|
name="top",
|
|
material=self.material_shell,
|
|
role=Role.STRUCTURE | Role.DECORATION
|
|
)
|
|
.constrain("top", "Fixed")
|
|
.addS(
|
|
self.surface_bottom(),
|
|
name="bottom",
|
|
material=self.material_shell,
|
|
role=Role.STRUCTURE,
|
|
loc=Cq.Location(0, 0, -self.thickness_top - self.height_internal)
|
|
)
|
|
.constrain("bottom", "Fixed")
|
|
.addS(
|
|
self.surface_side_tip(),
|
|
name="side_tip_l",
|
|
material=self.material_shell,
|
|
role=Role.STRUCTURE,
|
|
)
|
|
.constrain("bottom?tip", "side_tip_l?top", "Plane")
|
|
.constrain("bottom?mid_l", "side_tip_l?bot", "Plane")
|
|
.addS(
|
|
self.surface_side_tip(),
|
|
name="side_tip_r",
|
|
material=self.material_shell,
|
|
role=Role.STRUCTURE,
|
|
)
|
|
.constrain("bottom?tip", "side_tip_r?bot", "Plane")
|
|
.constrain("bottom?mid_r", "side_tip_r?top", "Plane")
|
|
.addS(
|
|
self.surface_side_tail(),
|
|
name="side_tail_l",
|
|
material=self.material_shell,
|
|
role=Role.STRUCTURE,
|
|
)
|
|
.constrain("bottom?mid_l", "side_tail_l?top", "Plane")
|
|
.constrain("bottom?bot_l", "side_tail_l?bot", "Plane")
|
|
.addS(
|
|
self.surface_side_tail_hinged(),
|
|
name="side_tail_r",
|
|
material=self.material_shell,
|
|
role=Role.STRUCTURE,
|
|
)
|
|
.constrain("bottom?mid_r", "side_tail_r?bot", "Plane")
|
|
.constrain("bottom?bot_r", "side_tail_r?top", "Plane")
|
|
.addS(
|
|
self.surface_side_bot(),
|
|
name="side_bot",
|
|
material=self.material_shell,
|
|
role=Role.STRUCTURE,
|
|
)
|
|
.constrain("bottom?bot_l", "side_bot?top", "Plane")
|
|
.constrain("bottom?bot_r", "side_bot?bot", "Plane")
|
|
.solve()
|
|
)
|
|
return a
|