cosplay: Touhou/Shiki Eiki #7

Open
aniva wants to merge 11 commits from touhou/shiki-eiki into main
5 changed files with 323 additions and 1 deletions
Showing only changes of commit d910326096 - Show all commits

View File

@ -2,12 +2,16 @@ from dataclasses import dataclass, field
import cadquery as Cq import cadquery as Cq
from nhf.build import Model, TargetKind, target, assembly, submodel from nhf.build import Model, TargetKind, target, assembly, submodel
import nhf.touhou.shiki_eiki.rod as MR import nhf.touhou.shiki_eiki.rod as MR
import nhf.touhou.shiki_eiki.crown as MC
import nhf.touhou.shiki_eiki.epaulette as ME
import nhf.utils import nhf.utils
@dataclass @dataclass
class Parameters(Model): class Parameters(Model):
rod: MR.Rod = field(default_factory=lambda: MR.Rod()) rod: MR.Rod = field(default_factory=lambda: MR.Rod())
crown: MC.Crown = field(default_factory=lambda: MC.Crown())
epaulette: ME.Epaulette = field(default_factory=lambda: ME.Epaulette())
def __post_init__(self): def __post_init__(self):
super().__init__(name="shiki-eiki") super().__init__(name="shiki-eiki")
@ -15,6 +19,12 @@ class Parameters(Model):
@submodel(name="rod") @submodel(name="rod")
def submodel_rod(self) -> Model: def submodel_rod(self) -> Model:
return self.rod return self.rod
@submodel(name="crown")
def submodel_crown(self) -> Model:
return self.crown
@submodel(name="epaulette")
def submodel_epaulette(self) -> Model:
return self.epaulette
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -0,0 +1,294 @@
import math
from dataclasses import dataclass, field
import cadquery as Cq
from nhf import Material, Role
from nhf.build import Model, target, assembly
import nhf.utils
@dataclass
class Crown(Model):
facets: int = 5
# Lower circumference
base_circ: float = 570.0
# Upper circumference
tilt_circ: float = 670.0
height: float = 120.0
margin: float = 10.0
def __post_init__(self):
super().__init__(name="crown")
assert self.tilt_circ > self.base_circ
@property
def facet_width_lower(self):
return self.base_circ / self.facets
@property
def facet_width_upper(self):
return self.tilt_circ / self.facets
@target(name="side")
def profile_base(self) -> Cq.Sketch:
# Generate the pentagonal shape
dx_l = self.facet_width_lower
dx_u = self.facet_width_upper
dy = self.height
return (
Cq.Sketch()
.polygon([
(dx_l/2, 0),
(dx_u/2, dy/2),
(0, dy),
(-dx_u/2, dy/2),
(-dx_l/2, 0),
])
)
@target(name="front")
def profile_front(self) -> Cq.Sketch:
dx_l = self.facet_width_lower
dx_u = self.facet_width_upper
dy = self.height
window_length = dy / 5
window_height = self.margin / 2
window = (
Cq.Sketch()
.rect(window_length, window_height)
)
window_p1 = Cq.Location.from2d(
dx_u/2 - self.margin - window_length * 0.4,
dy/2 + self.margin,
math.degrees(math.atan2(dy/2, -dx_u/2)),
)
window_p2 = Cq.Location.from2d(
dx_l/2 - self.margin + window_length * 0.15,
window_length/2 + self.margin,
math.degrees(math.atan2(dy/2, (dx_u-dx_l)/2)),
)
# Carve the scale
z = dy * 1/64 # "Pen" Thickness
scale_pan_x = dx_l / 2 * 0.6
scale_pan_y = dy / 2 * 0.7
pan_dx = dx_l * 1/4
pan_dy = dy * 1/16
scale_pan = (
Cq.Sketch()
.arc(
(- pan_dx/2, pan_dy),
(0, 0),
(+ pan_dx/2, pan_dy),
)
.segment(
(+pan_dx/2, pan_dy),
(+pan_dx/2 - z, pan_dy),
)
.arc(
(-pan_dx/2 + z, pan_dy),
(0, z),
(+pan_dx/2 - z, pan_dy),
)
.segment(
(-pan_dx/2, pan_dy),
(-pan_dx/2 + z, pan_dy),
)
.assemble()
)
loc_scale_pan = Cq.Location.from2d(scale_pan_x, scale_pan_y)
loc_scale_pan2 = Cq.Location.from2d(-scale_pan_x, scale_pan_y)
scale_base_y = dy / 2 * 0.36
scale_base_x = dx_l / 10
assert scale_base_y < scale_pan_y
assert scale_base_x < scale_pan_x
scale_body = (
Cq.Sketch()
.arc(
(scale_pan_x, scale_pan_y),
(0, scale_base_y),
(-scale_pan_x, scale_pan_y),
)
.segment(
(-scale_pan_x, scale_pan_y),
(-scale_pan_x+z, scale_pan_y+z),
)
.arc(
(scale_pan_x - z, scale_pan_y+z),
(0, scale_base_y + z),
(-scale_pan_x + z, scale_pan_y+z),
)
.segment(
(scale_pan_x, scale_pan_y),
(scale_pan_x-z, scale_pan_y+z),
)
.assemble()
.polygon([
(scale_base_x, scale_base_y + z/2),
(scale_base_x, self.margin),
(scale_base_x-z, self.margin),
(scale_base_x-z, scale_base_y-z),
(-scale_base_x+z, scale_base_y-z),
(-scale_base_x+z, self.margin),
(-scale_base_x, self.margin),
(-scale_base_x, scale_base_y + z/2),
], mode='a')
)
# Needle
needle_y_top = dy - self.margin
needle_y_mid = dy * 0.7
needle_dx = scale_base_x * 2
needle = (
Cq.Sketch()
.segment(
(z, needle_y_mid),
(z, scale_base_y),
)
.segment(
(z, scale_base_y),
(-z, scale_base_y),
)
.segment(
(-z, scale_base_y),
(-z, needle_y_mid),
)
.bezier([
(0, needle_y_top),
(0, (needle_y_top + needle_y_mid)/2),
(needle_dx, (needle_y_top + needle_y_mid)/2),
(z, needle_y_mid),
])
.bezier([
(0, needle_y_top),
(0, (needle_y_top + needle_y_mid)/2),
(-needle_dx, (needle_y_top + needle_y_mid)/2),
(-z, needle_y_mid),
])
.assemble()
)
z2 = z * 2
needle_inner = (
Cq.Sketch()
.segment(
(z2, needle_y_mid - z2),
(-z2, needle_y_mid - z2)
)
.segment(
(z2, needle_y_mid - z2),
(z2, needle_y_mid + z2),
)
.segment(
(-z2, needle_y_mid - z2),
(-z2, needle_y_mid + z2),
)
.bezier([
(0, needle_y_top - z2),
(0, (needle_y_top + needle_y_mid)/2),
(needle_dx-z2*2, (needle_y_top + needle_y_mid)/2),
(z2, needle_y_mid + z2),
])
.bezier([
(0, needle_y_top - z2),
(0, (needle_y_top + needle_y_mid)/2),
(-needle_dx+z2*2, (needle_y_top + needle_y_mid)/2),
(-z2, needle_y_mid + z2),
])
.assemble()
)
return (
self.profile_base()
.boolean(window.moved(window_p1), mode='s')
.boolean(window.moved(window_p1.flip_x()), mode='s')
.boolean(window.moved(window_p2), mode='s')
.boolean(window.moved(window_p2.flip_x()), mode='s')
.boolean(scale_pan.moved(loc_scale_pan), mode='s')
.boolean(scale_pan.moved(loc_scale_pan2), mode='s')
.boolean(scale_body, mode='s')
.boolean(needle, mode='s')
.boolean(needle_inner, mode='a')
.clean()
)
@target(name="side-guard")
def profile_side_guard(self) -> Cq.Sketch:
dx = self.facet_width_lower / 2
dy = self.height
# Main control points
p_mid = Cq.Location.from2d(0, 0.5 * dy)
p_mid_v = Cq.Location.from2d(10/57 * dx, 0)
p_top1 = Cq.Location.from2d(0.408 * dx, 5/24 * dy)
p_top1_v = Cq.Location.from2d(0.13 * dx, 0)
p_top2 = Cq.Location.from2d(0.737 * dx, 0.255 * dy)
p_top2_c1 = p_top2 * Cq.Location.from2d(-0.105 * dx, 0.033 * dy)
p_top2_c2 = p_top2 * Cq.Location.from2d(-0.053 * dx, -0.09 * dy)
p_top3 = Cq.Location.from2d(0.929 * dx, 0.145 * dy)
p_top3_v = Cq.Location.from2d(0.066 * dx, 0.033 * dy)
p_top4 = Cq.Location.from2d(0.85 * dx, 0.374 * dy)
p_top4_v = Cq.Location.from2d(-0.053 * dx, 0.008 * dy)
p_top5 = Cq.Location.from2d(0.54 * dx, 0.349 * dy)
p_top5_c1 = p_top5 * Cq.Location.from2d(0.103 * dx, 0.017 * dy)
p_top5_c2 = p_top5 * Cq.Location.from2d(0.158 * dx, 0.034 * dy)
p_base_c = Cq.Location.from2d(1.245 * dx, 0.55 * dy)
p_base = Cq.Location.from2d(dx, 0)
bezier_groups = [
[
p_base,
p_base_c,
p_top5_c2,
p_top5,
],
[
p_top5,
p_top5_c1,
p_top4 * p_top4_v,
p_top4,
],
[
p_top4,
p_top4 * p_top4_v.inverse.scale(4),
p_top3 * p_top3_v,
p_top3,
],
[
p_top3,
p_top3 * p_top3_v.inverse,
p_top2_c2,
p_top2,
],
[
p_top2,
p_top2_c1,
p_top1 * p_top1_v,
p_top1,
],
[
p_top1,
p_top1 * p_top1_v.inverse,
p_mid * p_mid_v,
p_mid,
],
]
sketch = (
Cq.Sketch()
.segment(
p_base.to2d_pos(),
p_base.flip_x().to2d_pos(),
)
)
for bezier_group in bezier_groups:
sketch = (
sketch
.bezier([p.to2d_pos() for p in bezier_group])
.bezier([p.flip_x().to2d_pos() for p in bezier_group])
)
return sketch.assemble()

View File

@ -0,0 +1,12 @@
import math
from dataclasses import dataclass, field
import cadquery as Cq
from nhf import Material, Role
from nhf.build import Model, target, assembly
import nhf.utils
@dataclass
class Epaulette(Model):
def __post_init__(self):
super().__init__(name="epaulette")

View File

@ -24,6 +24,7 @@ class Rod(Model):
fac_window_footer_top: float = 0.59 fac_window_footer_top: float = 0.59
def __post_init__(self): def __post_init__(self):
super().__init__(name="rod")
self.loc_core = Cq.Location.from2d(self.length - self.length_tip, 0) self.loc_core = Cq.Location.from2d(self.length - self.length_tip, 0)
assert self.length_tip * 2 < self.length assert self.length_tip * 2 < self.length
#assert self.fac_bar_top + self.fac_window_tsumi_top < 1 #assert self.fac_bar_top + self.fac_window_tsumi_top < 1

View File

@ -53,6 +53,11 @@ def is2d(self: Cq.Location) -> bool:
return z == 0 and rx == 0 and ry == 0 return z == 0 and rx == 0 and ry == 0
Cq.Location.is2d = is2d Cq.Location.is2d = is2d
def scale(self: Cq.Location, fac: float) -> bool:
(x, y, z), (rx, ry, rz) = self.toTuple()
return Cq.Location(x*fac, y*fac, z*fac, rx, ry, rz)
Cq.Location.scale = scale
def to2d(self: Cq.Location) -> Tuple[Tuple[float, float], float]: def to2d(self: Cq.Location) -> Tuple[Tuple[float, float], float]:
""" """
Returns position and angle Returns position and angle
@ -91,7 +96,7 @@ Cq.Location.with_angle_2d = with_angle_2d
def flip_x(self: Cq.Location) -> Cq.Location: def flip_x(self: Cq.Location) -> Cq.Location:
(x, y), a = self.to2d() (x, y), a = self.to2d()
return Cq.Location.from2d(-x, y, 90 - a) return Cq.Location.from2d(-x, y, 180 - a)
Cq.Location.flip_x = flip_x Cq.Location.flip_x = flip_x
def flip_y(self: Cq.Location) -> Cq.Location: def flip_y(self: Cq.Location) -> Cq.Location:
(x, y), a = self.to2d() (x, y), a = self.to2d()