Cosplay/nhf/touhou/shiki_eiki/crown.py

389 lines
11 KiB
Python
Raw Normal View History

2024-11-18 00:36:39 -08:00
import math
from dataclasses import dataclass, field
import cadquery as Cq
from nhf import Material, Role
2024-11-18 14:54:29 -08:00
from nhf.build import Model, target, assembly, TargetKind
2024-11-18 00:36:39 -08:00
import nhf.utils
@dataclass
class Crown(Model):
facets: int = 5
# Lower circumference
2024-11-19 16:01:06 -08:00
base_circ: float = 538.0
2024-11-18 00:36:39 -08:00
# Upper circumference
2024-11-19 16:01:06 -08:00
tilt_circ: float = 640.0
2024-11-18 00:36:39 -08:00
height: float = 120.0
margin: float = 10.0
2024-11-18 14:16:35 -08:00
thickness: float = 0.4 # 26 Gauge
material: Material = Material.METAL_BRASS
2024-11-18 00:36:39 -08:00
def __post_init__(self):
super().__init__(name="crown")
assert self.tilt_circ > self.base_circ
2024-11-19 16:01:06 -08:00
assert self.facet_width_upper / 2 > self.height / 2, "Top angle must be > 90 degrees"
2024-11-18 00:36:39 -08:00
@property
def facet_width_lower(self):
return self.base_circ / self.facets
@property
def facet_width_upper(self):
return self.tilt_circ / self.facets
2024-11-19 16:01:06 -08:00
def profile_base(self) -> Cq.Sketch:
2024-11-18 00:36:39 -08:00
# 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),
])
)
2024-11-19 16:01:06 -08:00
@target(name="side", kind=TargetKind.DXF)
def profile_side(self) -> Cq.Sketch:
dy = self.facet_width_upper * 0.1
x_side = self.facet_width_upper
y_tip = self.height - self.margin
eye = (
Cq.Sketch()
.segment(
(0, y_tip),
(dy, y_tip - dy),
)
.segment(
(0, y_tip),
(-dy, y_tip - dy),
)
.bezier([
(dy, y_tip - dy),
(0, y_tip - dy/2),
(0, y_tip - dy/2),
(-dy, y_tip - dy),
])
.assemble()
)
return (
self.profile_base()
.boolean(eye, mode='s')
)
2024-11-18 20:54:06 -08:00
@target(name="dot", kind=TargetKind.DXF)
def profile_dot(self) -> Cq.Sketch:
return (
Cq.Sketch()
.circle(self.margin / 2)
)
2024-11-18 14:54:29 -08:00
@target(name="front", kind=TargetKind.DXF)
2024-11-18 00:36:39 -08:00
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,
2024-11-19 16:01:06 -08:00
dy/2 + self.margin/2,
2024-11-18 00:36:39 -08:00
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
2024-11-19 16:01:06 -08:00
y_shoulder = needle_y_mid - z * 2
2024-11-18 00:36:39 -08:00
needle = (
Cq.Sketch()
.segment(
2024-11-19 16:01:06 -08:00
(0, needle_y_mid),
(z, y_shoulder),
)
.segment(
(z, y_shoulder),
2024-11-18 00:36:39 -08:00
(z, scale_base_y),
)
.segment(
(z, scale_base_y),
(-z, scale_base_y),
)
.segment(
2024-11-19 16:01:06 -08:00
(-z, y_shoulder),
2024-11-18 00:36:39 -08:00
(-z, scale_base_y),
2024-11-19 16:01:06 -08:00
)
.segment(
(-z, y_shoulder),
(0, needle_y_mid),
)
.assemble()
)
z2 = z * 2
y1 = needle_y_mid + z2
needle_head = (
Cq.Sketch()
.segment(
(z, needle_y_mid),
(z, y1),
)
.segment(
2024-11-18 00:36:39 -08:00
(-z, needle_y_mid),
2024-11-19 16:01:06 -08:00
(-z, y1),
2024-11-18 00:36:39 -08:00
)
2024-11-19 16:01:06 -08:00
# Outer edge
2024-11-18 00:36:39 -08:00
.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),
])
2024-11-19 16:01:06 -08:00
# Inner edge
2024-11-18 00:36:39 -08:00
.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),
2024-11-19 16:01:06 -08:00
(z, y1),
2024-11-18 00:36:39 -08:00
])
.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),
2024-11-19 16:01:06 -08:00
(-z, y1),
2024-11-18 00:36:39 -08:00
])
.assemble()
)
return (
2024-11-19 16:01:06 -08:00
self.profile_base()
2024-11-18 00:36:39 -08:00
.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')
2024-11-19 16:01:06 -08:00
.boolean(needle_head, mode='s')
2024-11-18 00:36:39 -08:00
.clean()
)
2024-11-18 14:54:29 -08:00
@target(name="side-guard", kind=TargetKind.DXF)
2024-11-18 00:36:39 -08:00
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()
2024-11-18 14:16:35 -08:00
def assembly(self) -> Cq.Assembly:
front = (
Cq.Workplane('XY')
.placeSketch(self.profile_front())
.extrude(self.thickness)
)
side = (
Cq.Workplane('XY')
.placeSketch(self.profile_side())
.extrude(self.thickness)
)
side_guard = (
Cq.Workplane('XY')
.placeSketch(self.profile_side_guard())
.extrude(self.thickness)
)
assembly = (
Cq.Assembly()
.addS(
front,
name="front",
material=self.material,
role=Role.DECORATION,
)
)
for i, pos in enumerate([-2, -1, 1, 2]):
x = self.facet_width_upper * pos
assembly = (
assembly
.addS(
side,
name=f"side{i}",
material=self.material,
role=Role.DECORATION,
loc=Cq.Location.from2d(x, 0),
)
.addS(
side_guard,
name=f"guard{i}",
material=self.material,
role=Role.DECORATION,
loc=Cq.Location(x, 0, self.thickness),
)
)
return assembly