Cosplay/nhf/touhou/shiki_eiki/rod.py

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