cosplay: Touhou/Shiki Eiki #7

Merged
aniva merged 25 commits from touhou/shiki-eiki into main 2025-04-08 23:01:05 -07:00
10 changed files with 5505 additions and 6 deletions

View File

@ -84,6 +84,7 @@ class Material(Enum):
ACRYLIC_TRANSLUSCENT = 1.18, _color('ivory2', 0.8)
ACRYLIC_TRANSPARENT = 1.18, _color('ghostwhite', 0.5)
STEEL_SPRING = 7.8, _color('gray', 0.8)
METAL_BRASS = 8.5, _color('gold1', 0.8)
def __init__(self, density: float, color: Cq.Color):
self.density = density
@ -116,6 +117,9 @@ def add_with_material_role(
Cq.Assembly.addS = add_with_material_role
def color_by_material(self: Cq.Assembly) -> Cq.Assembly:
"""
Set colours in an assembly by material
"""
for _, a in self.traverse():
if KEY_MATERIAL not in a.metadata:
continue
@ -123,6 +127,9 @@ def color_by_material(self: Cq.Assembly) -> Cq.Assembly:
return self
Cq.Assembly.color_by_material = color_by_material
def color_by_role(self: Cq.Assembly, avg: bool = True) -> Cq.Assembly:
"""
Set colours in an assembly by role
"""
for _, a in self.traverse():
if KEY_ROLE not in a.metadata:
continue

0
nhf/touhou/__init__.py Normal file
View File

View File

@ -0,0 +1,40 @@
from dataclasses import dataclass, field
import cadquery as Cq
from nhf.build import Model, TargetKind, target, assembly, submodel
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
@dataclass
class Parameters(Model):
rod: MR.Rod = field(default_factory=lambda: MR.Rod())
crown: MC.Crown = field(default_factory=lambda: MC.Crown())
epaulette_ze: ME.Epaulette = field(default_factory=lambda: ME.Epaulette(side="ze"))
epaulette_hi: ME.Epaulette = field(default_factory=lambda: ME.Epaulette(side="hi"))
def __post_init__(self):
super().__init__(name="shiki-eiki")
@submodel(name="rod")
def submodel_rod(self) -> Model:
return self.rod
@submodel(name="crown")
def submodel_crown(self) -> Model:
return self.crown
@submodel(name="epaulette_ze")
def submodel_epaulette_ze(self) -> Model:
return self.epaulette_ze
@submodel(name="epaulette_hi")
def submodel_epaulette_hi(self) -> Model:
return self.epaulette_hi
if __name__ == '__main__':
import sys
p = Parameters()
if len(sys.argv) == 1:
p.build_all()
sys.exit(0)

View File

@ -0,0 +1,790 @@
from nhf import Material, Role
from nhf.build import Model, target, assembly, TargetKind
import nhf.utils
import math
from typing import Optional
from dataclasses import dataclass, field
from enum import Enum
import cadquery as Cq
class AttachPoint(Enum):
DOVETAIL_IN = 1
DOVETAIL_OUT = 2
NONE = 3
# Inset slot for front surface attachment j
SLOT = 4
@dataclass
class Crown(Model):
facets: int = 5
# Lower circumference
base_circ: float = 538.0
# Upper circumference, at the middle
tilt_circ: float = 640.0
front_base_circ: float = (640.0 + 538.0) / 2
# Total height
height: float = 120.0
# Front guard has a wing that inserts into the side guards.
front_wing_angle: float = 9.0
front_wing_dh: float = 40.0
front_wing_height: float = 20.0
margin: float = 10.0
thickness: float = 0.4 # 26 Gauge
side_guard_thickness: float = 15.0
side_guard_channel_radius: float = 90
side_guard_channel_height: float = 10
side_guard_hole_height: float = 15.0
side_guard_hole_diam: float = 1.5
side_guard_dovetail_height: float = 30.0
side_guard_slot_width: float = 22.0
side_guard_slot_angle: float = 18.0
# brass insert thickness
slot_thickness: float = 2.0
slot_width: float = 20.0
slot_tilt: float = 60
material: Material = Material.METAL_BRASS
material_side: Material = Material.PLASTIC_PLA
def __post_init__(self):
super().__init__(name="crown")
assert self.tilt_circ > self.base_circ
assert self.facet_width_upper / 2 > self.height / 2, "Top angle must be > 90 degrees"
assert self.side_guard_channel_radius > self.radius_lower
assert self.front_wing_angle < 180 / self.facets
assert self.front_wing_dh + self.front_wing_height < self.height
assert self.slot_phi < 2 * math.pi / self.facets
@property
def facet_width_lower(self):
return self.base_circ / self.facets
@property
def facet_width_upper(self):
return self.tilt_circ / self.facets
@property
def radius_lower(self):
return self.base_circ / (2 * math.pi)
@property
def radius_middle(self):
return self.tilt_circ / (2 * math.pi)
@property
def radius_upper(self):
return (self.tilt_circ + (self.tilt_circ - self.base_circ)) / (2 * math.pi)
@property
def radius_lower_front(self):
return self.front_base_circ / (2 * math.pi)
@property
def radius_middle_front(self):
return self.radius_lower_front + (self.radius_middle - self.radius_lower)
@property
def radius_upper_front(self):
return self.radius_lower_front + (self.radius_upper - self.radius_lower)
@property
def slot_r0(self):
return self.radius_lower + self.thickness / 2
@property
def slot_r1(self):
return self.radius_upper + self.thickness / 2
@property
def slot_h0(self) -> float:
"""
Phantom height formed by similar triangle, i.e. h0 in
(h0 + h) / r2 = h0 / r1
"""
rat = self.slot_r0 / (self.slot_r1 - self.slot_r0)
return self.height * rat
@property
def slot_theta(self) -> float:
"""
Cone tilt, related to other quantities by
h0 = r1 * cot theta
"""
h = self.height
return math.atan(self.slot_r0 / (self.height + self.slot_h0))
@property
def slot_phi(self) -> float:
"""
When a slice of the crown is expanded (via Gauss's Theorema Egregium),
it does not form a full circle. phi is the angle of one of the slices.
Note that on the cone itself, the angular slice is `2 pi / n` which `n`
is the number of sides.
"""
arc = self.slot_r0 * math.pi * 2 / self.facets
rho = self.slot_h0 / math.cos(self.slot_theta)
return arc / rho
def profile_base(self) -> Cq.Sketch:
# Generate a conical pentagonal shape
y0 = self.slot_h0 / math.cos(self.slot_theta)
yh = (self.height/2 + self.slot_h0) / math.cos(self.slot_theta)
yq = (self.height*3/4 + self.slot_h0) / math.cos(self.slot_theta)
y1 = (self.height + self.slot_h0) / math.cos(self.slot_theta)
phi2 = self.slot_phi / 2
return (
Cq.Sketch()
.segment(
(y0 * math.sin(phi2), y0 * (-1 + math.cos(phi2))),
(yh * math.sin(phi2), -y0 + yh * math.cos(phi2)),
)
.arc(
(yh * math.sin(phi2), -y0 + yh * math.cos(phi2)),
(yq * math.sin(phi2/2), -y0 + yq * math.cos(phi2/2)),
(0, y1 - y0),
)
.arc(
(-yh * math.sin(phi2), -y0 + yh * math.cos(phi2)),
(-yq * math.sin(phi2/2), -y0 + yq * math.cos(phi2/2)),
(0, y1 - y0),
)
.segment(
(-y0 * math.sin(phi2), y0 * (-1 + math.cos(phi2))),
(-yh * math.sin(phi2), -y0 + yh * math.cos(phi2)),
)
.arc(
(y0 * math.sin(phi2), -y0 + y0 * math.cos(phi2)),
(0, 0),
(-y0 * math.sin(phi2), y0 * (-1 + math.cos(phi2))),
)
.assemble()
)
@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')
)
@target(name="dot", kind=TargetKind.DXF)
def profile_dot(self) -> Cq.Sketch:
return (
Cq.Sketch()
.circle(self.margin / 2)
)
def profile_front_wing(self, mirror: bool) -> Cq.Sketch:
# Add the two wings to the base profile
hw = self.front_wing_height / math.cos(self.slot_theta)
hw0 = (self.front_wing_dh + self.slot_h0) / math.cos(self.slot_theta)
hw1 = hw0 + hw
y0 = self.slot_h0 / math.cos(self.slot_theta)
# Calculate angle of wing analogously to `this.slot_phi`. This arc's
# radius is hw0.
wing_arc = self.slot_r0 * math.radians(self.front_wing_angle)
phi_w = wing_arc / hw0
sign = -1 if mirror else 1
phi2 = self.slot_phi / 2
return (
Cq.Sketch()
.segment(
(sign * hw0 * math.sin(phi2), -y0 + hw0 * math.cos(phi2)),
(sign * hw1 * math.sin(phi2), -y0 + hw1 * math.cos(phi2)),
)
.segment(
(sign * hw0 * math.sin(phi2+phi_w), -y0 + hw0 * math.cos(phi2+phi_w)),
(sign * hw1 * math.sin(phi2+phi_w), -y0 + hw1 * math.cos(phi2+phi_w)),
)
.arc(
(sign * hw0 * math.sin(phi2), -y0 + hw0 * math.cos(phi2)),
(sign * hw0 * math.sin(phi2+phi_w/2), -y0 + hw0 * math.cos(phi2+phi_w/2)),
(sign * hw0 * math.sin(phi2+phi_w), -y0 + hw0 * math.cos(phi2+phi_w)),
)
.arc(
(sign * hw1 * math.sin(phi2), -y0 + hw1 * math.cos(phi2)),
(sign * hw1 * math.sin(phi2+phi_w/2), -y0 + hw1 * math.cos(phi2+phi_w/2)),
(sign * hw1 * math.sin(phi2+phi_w), -y0 + hw1 * math.cos(phi2+phi_w)),
)
.assemble()
)
@target(name="front", kind=TargetKind.DXF)
def profile_front(self) -> Cq.Sketch:
profile_base = (
self.profile_base()
.boolean(self.profile_front_wing(False), mode='a')
.boolean(self.profile_front_wing(True), mode='a')
)
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/2,
math.degrees(math.atan2(dy/2, -dx_u/2) * 0.95),
)
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/32 # "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
y_shoulder = needle_y_mid - z * 2
needle = (
Cq.Sketch()
.segment(
(0, needle_y_mid),
(z, y_shoulder),
)
.segment(
(z, y_shoulder),
(z, scale_base_y),
)
.segment(
(z, scale_base_y),
(-z, scale_base_y),
)
.segment(
(-z, y_shoulder),
(-z, scale_base_y),
)
.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(
(-z, needle_y_mid),
(-z, y1),
)
# Outer edge
.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),
])
# Inner edge
.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),
(z, y1),
])
.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),
(-z, y1),
])
.assemble()
)
return (
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_head, mode='s')
.clean()
)
@target(name="side-guard", kind=TargetKind.DXF)
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)
y0 = self.slot_h0 / math.cos(self.slot_theta)
phi2 = self.slot_phi / 2
p_base = Cq.Location.from2d(y0 * math.sin(phi2), -y0 + y0 * math.cos(phi2))
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()
.arc(
p_base.to2d_pos(),
(0, 0),
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()
def side_guard_dovetail(self) -> Cq.Solid:
"""
Generates a dovetail coupling for the side guard
"""
dx = self.side_guard_thickness / 2
wire = Cq.Wire.makePolygon([
(dx * 0.5, 0),
(dx * 0.7, dx),
(-dx * 0.7, dx),
(-dx * 0.5, 0),
], close=True)
return Cq.Solid.extrudeLinear(
wire,
[],
(0,0,dx + self.side_guard_dovetail_height),
).moved((0, 0, -dx))
def side_guard_frontal_slot(self) -> Cq.Workplane:
angle = 360 / self.facets
inner_d = self.thickness / 2 - self.slot_thickness / 2
outer_d = self.thickness / 2 + self.slot_thickness / 2
outer = Cq.Solid.makeCone(
radius1=self.radius_lower_front + outer_d,
radius2=self.radius_upper_front + outer_d,
height=self.height,
angleDegrees=angle,
)
inner = Cq.Solid.makeCone(
radius1=self.radius_lower_front + inner_d,
radius2=self.radius_upper_front + inner_d,
height=self.height,
angleDegrees=angle,
)
shell = (
outer.cut(inner)
.rotate((0,0,0), (0,0,1), -angle/2)
)
# Generate the sector intersector
intersector = Cq.Solid.makeCylinder(
radius=self.radius_upper + self.side_guard_thickness,
height=self.front_wing_height,
angleDegrees=self.front_wing_angle,
).moved(Cq.Location(0,0,self.front_wing_dh,0,0,-self.front_wing_angle/2))
return shell * intersector
def side_guard(
self,
attach_left: AttachPoint,
attach_right: AttachPoint,
) -> Cq.Workplane:
"""
Constructs the side guard using a cone. Via Gauss's Theorema Egregium,
the surface of the cone can be deformed into a plane.
"""
angle_span = 360 / self.facets
outer = Cq.Solid.makeCone(
radius1=self.radius_lower + self.side_guard_thickness,
radius2=self.radius_upper + self.side_guard_thickness,
height=self.height,
angleDegrees=angle_span,
)
inner = Cq.Solid.makeCone(
radius1=self.radius_lower,
radius2=self.radius_upper,
height=self.height,
angleDegrees=angle_span,
)
shell = (outer - inner).rotate((0,0,0), (0,0,1), -angle_span/2)
dx = math.sin(math.radians(angle_span / 2)) * (self.radius_middle + self.side_guard_thickness)
profile = (
Cq.Workplane('YZ')
.polyline([
(0, self.height),
(-dx, self.height / 2),
(-dx, 0),
(dx, 0),
(dx, self.height / 2),
])
.close()
.extrude(self.radius_upper + self.side_guard_thickness)
.val()
)
#channel = (
# Cq.Solid.makeCylinder(
# radius=self.side_guard_channel_radius + 1.0,
# height=self.side_guard_channel_height,
# ) - Cq.Solid.makeCylinder(
# radius=self.side_guard_channel_radius,
# height=self.side_guard_channel_height,
# )
#)
result = shell * profile# - channel
# Create the downward slots
for sign in [-1, 1]:
slot_box = Cq.Solid.makeBox(
length=self.height,
width=self.slot_width,
height=self.slot_thickness,
).moved(
Cq.Location(-self.slot_thickness,-self.slot_width/2, -self.slot_thickness/2)
)
# keyhole for threads to stay in place
slot_cyl = Cq.Solid.makeCylinder(
radius=self.slot_thickness/2,
height=self.height,
pnt=(0,0,self.slot_thickness/2),
dir=(1,0,0),
)
slot = slot_box + slot_cyl
slot = slot.moved(
Cq.Location.rot2d(sign * self.side_guard_slot_angle) *
Cq.Location(self.radius_lower + self.side_guard_thickness/2, 0, 0) *
Cq.Location(0,0,0,0,-180 + self.slot_tilt,0)
)
result = result - slot
radius_attach = self.radius_lower + self.side_guard_thickness / 2
# tilt the dovetail by radius differential
angle_tilt = math.degrees(math.atan2(self.radius_middle - self.radius_lower, self.height / 2))
dovetail = self.side_guard_dovetail()
loc_dovetail_left = Cq.Location.rot2d(angle_span / 2) * Cq.Location(radius_attach, 0, 0, 0, angle_tilt, 0)
loc_dovetail_right = Cq.Location.rot2d(-angle_span / 2) * Cq.Location(radius_attach, 0, 0, 0, angle_tilt, 0)
angle_slot = 180 / self.facets - self.front_wing_angle / 2
match attach_left:
case AttachPoint.DOVETAIL_IN:
loc_dovetail_left *= Cq.Location.rot2d(180)
result = result - dovetail.moved(loc_dovetail_left)
case AttachPoint.DOVETAIL_OUT:
result = result + dovetail.moved(loc_dovetail_left)
case AttachPoint.SLOT:
result = result - self.side_guard_frontal_slot().moved(Cq.Location.rot2d(angle_slot))
case AttachPoint.NONE:
pass
match attach_right:
case AttachPoint.DOVETAIL_IN:
result = result - dovetail.moved(loc_dovetail_right)
case AttachPoint.DOVETAIL_OUT:
loc_dovetail_right *= Cq.Location.rot2d(180)
result = result + dovetail.moved(loc_dovetail_right)
case AttachPoint.SLOT:
result = result - self.side_guard_frontal_slot().moved(Cq.Location.rot2d(-angle_slot))
case AttachPoint.NONE:
pass
# Remove parts below the horizontal
cut_h = self.radius_lower
result -= Cq.Solid.makeCylinder(
radius=self.radius_lower + self.side_guard_thickness,
height=cut_h).moved((0,0,-cut_h))
return result
@target(name="side_guard_1", angularTolerance=0.01)
def side_guard_1(self) -> Cq.Workplane:
return self.side_guard(
attach_left=AttachPoint.SLOT,
attach_right=AttachPoint.DOVETAIL_IN,
)
@target(name="side_guard_2", angularTolerance=0.01)
def side_guard_2(self) -> Cq.Workplane:
return self.side_guard(
attach_left=AttachPoint.DOVETAIL_OUT,
attach_right=AttachPoint.DOVETAIL_IN,
)
@target(name="side_guard_3", angularTolerance=0.01)
def side_guard_3(self) -> Cq.Workplane:
return self.side_guard(
attach_left=AttachPoint.DOVETAIL_OUT,
attach_right=AttachPoint.DOVETAIL_IN,
)
@target(name="side_guard_4", angularTolerance=0.01)
def side_guard_4(self) -> Cq.Workplane:
return self.side_guard(
attach_left=AttachPoint.DOVETAIL_OUT,
attach_right=AttachPoint.SLOT,
)
def front_surrogate(self) -> Cq.Workplane:
"""
Create a surrogate cylindrical section structure for the front since we
cannot bend extrusions
"""
angle = 360 / 5
outer = Cq.Solid.makeCone(
radius1=self.radius_lower_front + self.thickness,
radius2=self.radius_upper_front + self.thickness,
height=self.height,
angleDegrees=angle,
)
inner = Cq.Solid.makeCone(
radius1=self.radius_lower_front,
radius2=self.radius_upper_front,
height=self.height,
angleDegrees=angle,
)
shell = (
outer.cut(inner)
.rotate((0,0,0), (0,0,1), -angle/2)
)
dx = math.sin(math.radians(angle / 2)) * self.radius_middle_front
profile = (
Cq.Workplane('YZ')
.polyline([
(0, self.height),
(-dx, self.height / 2),
(-dx, 0),
(dx, 0),
(dx, self.height / 2),
])
.close()
.extrude(self.radius_upper_front + self.side_guard_thickness)
.val()
)
return shell * profile
def assembly(self) -> Cq.Assembly:
"""
New assembly using conformal mapping on the cone.
"""
side_guards = [
self.side_guard_1(),
self.side_guard_2(),
self.side_guard_3(),
self.side_guard_4(),
]
a = Cq.Assembly()
for i,side_guard in enumerate(side_guards):
angle = -(i+1) * 360 / self.facets
a = a.addS(
side_guard,
name=f"side-{i}",
material=self.material_side,
loc=Cq.Location(rz=angle)
)
a.addS(
self.front_surrogate(),
name="front",
material=self.material,
)
return a
def old_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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,36 @@
import math
from dataclasses import dataclass, field
from pathlib import Path
import cadquery as Cq
from nhf import Material, Role
from nhf.build import Model, target, assembly
import nhf.utils
@dataclass
class Epaulette(Model):
side: str
diam: float = 100.0
thickness_brass: float = 0.4 # 26 Gauge
thickness_fabric: float = 0.3
material: Material = Material.METAL_BRASS
def __post_init__(self):
super().__init__(name=f"epaulette-{self.side}")
def surface(self) -> Cq.Solid:
path = Path(__file__).resolve().parent / f"epaulette-{self.side}.dxf"
return (
Cq.importers.importDXF(path).wires().toPending().extrude(self.thickness_brass)
)
def assembly(self) -> Cq.Assembly:
assembly = (
Cq.Assembly()
.addS(
self.surface(),
name="surface",
material=self.material,
role=Role.DECORATION,
)
)
return assembly

View File

@ -0,0 +1,587 @@
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

View File

@ -0,0 +1,139 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="100mm"
height="100mm"
viewBox="0 0 100 100"
version="1.1"
id="svg1"
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
sodipodi:docname="zehi.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="1.8706489"
inkscape:cx="171.86549"
inkscape:cy="207.68194"
inkscape:window-width="1640"
inkscape:window-height="962"
inkscape:window-x="20"
inkscape:window-y="40"
inkscape:window-maximized="0"
inkscape:current-layer="layer3"
showguides="true">
<sodipodi:guide
position="50,90.756846"
orientation="-1,0"
id="guide1"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="32.08421,55"
orientation="0,1"
id="guide2"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="60,78.19709"
orientation="-1,0"
id="guide3"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="40,74.842796"
orientation="-1,0"
id="guide4"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="4.9999993,79.999999"
orientation="0,1"
id="guide5"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="-9.7833569,45"
orientation="0,1"
id="guide6"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
</sodipodi:namedview>
<defs
id="defs1" />
<g
inkscape:label="Outline"
inkscape:groupmode="layer"
id="layer1">
<path
id="path1"
style="fill:none;stroke:#000000;stroke-width:0.264453;stroke-opacity:1;-inkscape-stroke:none"
inkscape:label="outer"
d="M 50.000049 0.13229167 A 49.867775 49.867775 0 0 0 0.13229167 50.000049 A 49.867775 49.867775 0 0 0 50.000049 99.867806 A 49.867775 49.867775 0 0 0 99.867806 50.000049 A 49.867775 49.867775 0 0 0 50.000049 0.13229167 z M 50.000049 5.1190674 A 44.880997 44.880997 0 0 1 94.88103 50.000049 A 44.880997 44.880997 0 0 1 50.000049 94.88103 A 44.880997 44.880997 0 0 1 5.1190674 50.000049 A 44.880997 44.880997 0 0 1 50.000049 5.1190674 z " />
</g>
<g
inkscape:groupmode="layer"
id="layer4"
inkscape:label="Cut"
style="display:none">
<circle
style="fill:none;stroke:#000000;stroke-width:0.264453;stroke-opacity:1;-inkscape-stroke:none"
id="circle14"
cx="50"
cy="50"
inkscape:label="outer"
r="49.867775" />
</g>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Ze"
style="display:inline">
<path
id="path10"
style="fill:none;stroke:#144e16;stroke-width:0.290227;stroke-opacity:1;-inkscape-stroke:none"
d="M 50.000049,12.645223 A 37.354885,37.354885 0 0 0 13.002824,44.999837 h 2.546614 2.469617 63.965088 1.438155 3.574976 A 37.354885,37.354885 0 0 0 50.000049,12.645223 Z m 0,4.980575 a 32.374233,32.374233 0 0 1 22.160404,8.817549 H 27.842745 A 32.374233,32.374233 0 0 1 50.000049,17.625798 Z M 23.725167,31.201713 h 52.552864 a 32.374233,32.374233 0 0 1 4.547526,9.039758 H 19.177641 a 32.374233,32.374233 0 0 1 4.547526,-9.039758 z"
inkscape:label="top" />
<path
style="fill:none;stroke:#144e16;stroke-width:0.261252;stroke-opacity:1;-inkscape-stroke:none"
d="m 30.468424,65.042542 -8.764322,8.764322 a 37.141727,37.141731 0 0 0 28.295947,13.12168 37.141727,37.141731 0 0 0 23.236308,-8.258411 L 70.147139,75.252771 C 64.43218,79.841996 57.3295,82.357002 50.000049,82.386702 42.031806,82.356509 34.403936,79.387593 28.529008,74.129842 l 5.513359,-5.513358 z"
id="path14" />
<path
style="fill:none;stroke:#144e16;stroke-width:0.261252;stroke-opacity:1;-inkscape-stroke:none"
d="m 53.049475,54.972872 v 0.02687 H 13.231234 a 37.141727,37.141731 0 0 0 1.076937,5.000211 H 53.049475 V 75.366976 H 58.10343 V 67.69716 H 73.684908 V 62.643205 H 58.10343 v -2.64325 h 27.588497 a 37.141727,37.141731 0 0 0 1.076937,-5.000211 H 58.10343 v -0.02687 z"
id="path11" />
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Hi"
style="display:inline">
<path
id="rect6"
style="fill:none;stroke:#053efb;stroke-width:0.264583;stroke-opacity:1;-inkscape-stroke:none"
d="m 54.786837,19.999813 v 59.999955 h 5.213118 v -8.94364 H 79.70883 V 62.775496 H 59.999955 V 54.140365 H 79.70883 V 45.859733 H 59.999955 V 37.224601 H 79.70883 V 28.94397 H 59.999955 v -8.944157 z"
inkscape:label="right" />
<path
id="path9"
style="fill:none;stroke:#053efb;stroke-width:0.264583;stroke-opacity:1;-inkscape-stroke:none"
d="M 45.245359,79.999977 V 20.000022 h -5.213118 v 8.94364 H 20.323366 v 8.280632 h 19.708875 v 8.635131 H 20.323366 v 8.280632 h 19.708875 v 8.635132 H 20.323366 v 8.280631 h 19.708875 v 8.944157 z"
inkscape:label="left" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@ -1,13 +1,11 @@
"""
Utility functions for cadquery objects
"""
import functools
import math
from typing import Optional
import functools, math
from typing import Optional, Union, Tuple, cast
import cadquery as Cq
from cadquery.occ_impl.solver import ConstraintSpec
from nhf import Role
from typing import Union, Tuple, cast
from nhf.materials import KEY_ITEM, KEY_MATERIAL
# Bug fixes
@ -55,6 +53,11 @@ def is2d(self: Cq.Location) -> bool:
return z == 0 and rx == 0 and ry == 0
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]:
"""
Returns position and angle
@ -93,17 +96,24 @@ Cq.Location.with_angle_2d = with_angle_2d
def flip_x(self: Cq.Location) -> Cq.Location:
(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
def flip_y(self: Cq.Location) -> Cq.Location:
(x, y), a = self.to2d()
return Cq.Location.from2d(x, -y, -a)
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 (
self
.reset()
# Has to be 0, 0. Translation doesn't work.
.push([(0, 0)])
.each(lambda _: obj, **kwargs)
)