cosplay: Touhou/Shiki Eiki #7

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

View File

@ -1,3 +1,4 @@
import math
from dataclasses import dataclass, field from dataclasses import dataclass, field
import cadquery as Cq import cadquery as Cq
from nhf import Material, Role from nhf import Material, Role
@ -11,6 +12,270 @@ class Rod(Model):
length: float = 550.0 length: float = 550.0
length_tip: float = 100.0 length_tip: float = 100.0
width_tail: float = 60.0 width_tail: float = 60.0
margin: float = 10.0
# 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.33
fac_window_footer_top: float = 0.59
def __post_init__(self):
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 _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.6
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="surface") @target(name="surface")
def top_profile(self) -> Cq.Sketch: def top_profile(self) -> Cq.Sketch:
@ -24,6 +289,16 @@ class Rod(Model):
(self.length - self.length_tip, -self.width/2), (self.length - self.length_tip, -self.width/2),
]) ])
) )
sketch = (
sketch
.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')
)
return sketch return sketch
@assembly() @assembly()

View File

@ -1,13 +1,11 @@
""" """
Utility functions for cadquery objects Utility functions for cadquery objects
""" """
import functools import functools, math
import math from typing import Optional, Union, Tuple, cast
from typing import Optional
import cadquery as Cq import cadquery as Cq
from cadquery.occ_impl.solver import ConstraintSpec from cadquery.occ_impl.solver import ConstraintSpec
from nhf import Role from nhf import Role
from typing import Union, Tuple, cast
from nhf.materials import KEY_ITEM, KEY_MATERIAL from nhf.materials import KEY_ITEM, KEY_MATERIAL
# Bug fixes # Bug fixes
@ -100,10 +98,17 @@ def flip_y(self: Cq.Location) -> Cq.Location:
return Cq.Location.from2d(x, -y, -a) return Cq.Location.from2d(x, -y, -a)
Cq.Location.flip_y = flip_y Cq.Location.flip_y = flip_y
def boolean(self: Cq.Sketch, obj, **kwargs) -> Cq.Sketch: def boolean(
self: Cq.Sketch,
obj: Union[Cq.Face, Cq.Sketch, Cq.Compound],
**kwargs) -> Cq.Sketch:
"""
Performs Boolean operation between a sketch and a sketch-like object
"""
return ( return (
self self
.reset() .reset()
# Has to be 0, 0. Translation doesn't work.
.push([(0, 0)]) .push([(0, 0)])
.each(lambda _: obj, **kwargs) .each(lambda _: obj, **kwargs)
) )