cosplay: Touhou/Houjuu Nue #4
15
nhf/build.py
15
nhf/build.py
|
@ -69,8 +69,13 @@ class Target:
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def write_to(self, obj, path: str):
|
def write_to(self, obj, path: str) -> bool:
|
||||||
|
"""
|
||||||
|
Returns false if target is `None`
|
||||||
|
"""
|
||||||
x = self._method(obj)
|
x = self._method(obj)
|
||||||
|
if x is None:
|
||||||
|
return False
|
||||||
if self.kind == TargetKind.STL:
|
if self.kind == TargetKind.STL:
|
||||||
assert isinstance(x, Union[
|
assert isinstance(x, Union[
|
||||||
Cq.Workplane, Cq.Shape, Cq.Compound, Cq.Assembly])
|
Cq.Workplane, Cq.Shape, Cq.Compound, Cq.Assembly])
|
||||||
|
@ -92,6 +97,7 @@ class Target:
|
||||||
Cq.exporters.exportDXF(x, path, **self.kwargs)
|
Cq.exporters.exportDXF(x, path, **self.kwargs)
|
||||||
else:
|
else:
|
||||||
assert False, f"Invalid kind: {self.kind}"
|
assert False, f"Invalid kind: {self.kind}"
|
||||||
|
return True
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def methods(cls, subject):
|
def methods(cls, subject):
|
||||||
|
@ -257,7 +263,8 @@ class Model:
|
||||||
"""
|
"""
|
||||||
total = 0
|
total = 0
|
||||||
for t in Target.methods(self).values():
|
for t in Target.methods(self).values():
|
||||||
t(self)
|
result = t(self)
|
||||||
|
if result:
|
||||||
total += 1
|
total += 1
|
||||||
for t in Assembly.methods(self).values():
|
for t in Assembly.methods(self).values():
|
||||||
t.check(self)
|
t.check(self)
|
||||||
|
@ -288,8 +295,8 @@ class Model:
|
||||||
print(f"{Fore.BLUE}Building{Style.RESET_ALL} {output_file}")
|
print(f"{Fore.BLUE}Building{Style.RESET_ALL} {output_file}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
t.write_to(self, str(output_file))
|
flag = t.write_to(self, str(output_file))
|
||||||
if verbose >= 1:
|
if flag and verbose >= 1:
|
||||||
print(f"{Fore.GREEN}Built{Style.RESET_ALL} {output_file}")
|
print(f"{Fore.GREEN}Built{Style.RESET_ALL} {output_file}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"{Fore.RED}Failed to build{Style.RESET_ALL} {output_file}: {e}")
|
print(f"{Fore.RED}Failed to build{Style.RESET_ALL} {output_file}: {e}")
|
||||||
|
|
|
@ -16,7 +16,7 @@ TOL = 1e-6
|
||||||
|
|
||||||
# uxcell 2 Pcs Star Knobs Grips M12 x 30mm Male Thread Steel Zinc Stud Replacement PP
|
# uxcell 2 Pcs Star Knobs Grips M12 x 30mm Male Thread Steel Zinc Stud Replacement PP
|
||||||
HS_JOINT_KNOB = ThreaddedKnob(
|
HS_JOINT_KNOB = ThreaddedKnob(
|
||||||
mass=float('nan'),
|
mass=0.0, # FIXME: Measure
|
||||||
diam_thread=12.0,
|
diam_thread=12.0,
|
||||||
height_thread=30.0,
|
height_thread=30.0,
|
||||||
diam_knob=50.0,
|
diam_knob=50.0,
|
||||||
|
@ -36,11 +36,11 @@ HS_JOINT_HEX_NUT = HexNut(
|
||||||
)
|
)
|
||||||
SHOULDER_AXIS_BOLT = FlatHeadBolt(
|
SHOULDER_AXIS_BOLT = FlatHeadBolt(
|
||||||
# FIXME: measure
|
# FIXME: measure
|
||||||
|
mass=0.0,
|
||||||
diam_head=10.0,
|
diam_head=10.0,
|
||||||
height_head=3.0,
|
height_head=3.0,
|
||||||
diam_thread=6.0,
|
diam_thread=6.0,
|
||||||
height_thread=20.0,
|
height_thread=20.0,
|
||||||
mass=float('nan'),
|
|
||||||
)
|
)
|
||||||
# Hoypeyfiy 10 Pieces Torsion Spring Woodworking DIY 90 Degrees Torsional
|
# Hoypeyfiy 10 Pieces Torsion Spring Woodworking DIY 90 Degrees Torsional
|
||||||
# Springs Repair Maintenance Spring
|
# Springs Repair Maintenance Spring
|
||||||
|
|
|
@ -62,7 +62,7 @@ class WingProfile(Model):
|
||||||
child_arm_radius=23.0,
|
child_arm_radius=23.0,
|
||||||
parent_arm_radius=30.0,
|
parent_arm_radius=30.0,
|
||||||
hole_diam=4.0,
|
hole_diam=4.0,
|
||||||
angle_neutral=30.0,
|
angle_neutral=-20.0,
|
||||||
))
|
))
|
||||||
# Distance between the two spacers on the elbow, halved
|
# Distance between the two spacers on the elbow, halved
|
||||||
wrist_h2: float = 5.0
|
wrist_h2: float = 5.0
|
||||||
|
@ -80,10 +80,10 @@ class WingProfile(Model):
|
||||||
wrist_bot_loc: Cq.Location
|
wrist_bot_loc: Cq.Location
|
||||||
wrist_height: float
|
wrist_height: float
|
||||||
elbow_rotate: float = -5.0
|
elbow_rotate: float = -5.0
|
||||||
wrist_rotate: float = 30.0
|
wrist_rotate: float = -20.0
|
||||||
|
|
||||||
# False for the right side, True for the left side
|
# False for the right side, True for the left side
|
||||||
flip: bool = False
|
flip: bool
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
super().__init__(name=self.name)
|
super().__init__(name=self.name)
|
||||||
|
@ -91,7 +91,10 @@ class WingProfile(Model):
|
||||||
self.elbow_top_loc = self.elbow_bot_loc * Cq.Location.from2d(0, self.elbow_height)
|
self.elbow_top_loc = self.elbow_bot_loc * Cq.Location.from2d(0, self.elbow_height)
|
||||||
self.wrist_top_loc = self.wrist_bot_loc * Cq.Location.from2d(0, self.wrist_height)
|
self.wrist_top_loc = self.wrist_bot_loc * Cq.Location.from2d(0, self.wrist_height)
|
||||||
self.elbow_axle_loc = self.elbow_bot_loc * Cq.Location.from2d(0, self.elbow_height / 2)
|
self.elbow_axle_loc = self.elbow_bot_loc * Cq.Location.from2d(0, self.elbow_height / 2)
|
||||||
|
if self.flip:
|
||||||
self.wrist_axle_loc = self.wrist_bot_loc * Cq.Location.from2d(0, self.wrist_height / 2)
|
self.wrist_axle_loc = self.wrist_bot_loc * Cq.Location.from2d(0, self.wrist_height / 2)
|
||||||
|
else:
|
||||||
|
self.wrist_axle_loc = self.wrist_bot_loc
|
||||||
|
|
||||||
assert self.elbow_joint.total_thickness < min(self.s1_thickness, self.s2_thickness)
|
assert self.elbow_joint.total_thickness < min(self.s1_thickness, self.s2_thickness)
|
||||||
assert self.wrist_joint.total_thickness < min(self.s2_thickness, self.s3_thickness)
|
assert self.wrist_joint.total_thickness < min(self.s2_thickness, self.s3_thickness)
|
||||||
|
@ -329,6 +332,12 @@ class WingProfile(Model):
|
||||||
"""
|
"""
|
||||||
Generates profile from shoulder and above. Subclass should implement
|
Generates profile from shoulder and above. Subclass should implement
|
||||||
"""
|
"""
|
||||||
|
@target(name="profile-s3-extra", kind=TargetKind.DXF)
|
||||||
|
def profile_s3_extra(self) -> Optional[Cq.Sketch]:
|
||||||
|
"""
|
||||||
|
Extra element to be glued on s3. Not needed for left side
|
||||||
|
"""
|
||||||
|
return None
|
||||||
|
|
||||||
def _elbow_joint_retract_cut_polygon(self, loc: Cq.Location) -> Cq.Sketch:
|
def _elbow_joint_retract_cut_polygon(self, loc: Cq.Location) -> Cq.Sketch:
|
||||||
"""
|
"""
|
||||||
|
@ -351,10 +360,15 @@ class WingProfile(Model):
|
||||||
for p in points
|
for p in points
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
def _wrist_joint_retract_cut_polygon(self, loc: Cq.Location) -> Cq.Sketch:
|
def _wrist_joint_retract_cut_polygon(self, loc: Cq.Location) -> Optional[Cq.Sketch]:
|
||||||
"""
|
"""
|
||||||
Creates a cutting polygon for removing the contraction part of a joint
|
Creates a cutting polygon for removing the contraction part of a joint
|
||||||
"""
|
"""
|
||||||
|
if not self.flip:
|
||||||
|
"""
|
||||||
|
No cutting needed on RHS
|
||||||
|
"""
|
||||||
|
return None
|
||||||
theta = math.radians(self.wrist_joint.motion_span)
|
theta = math.radians(self.wrist_joint.motion_span)
|
||||||
dx = self.wrist_height * math.tan(theta)
|
dx = self.wrist_height * math.tan(theta)
|
||||||
dy = self.wrist_height
|
dy = self.wrist_height
|
||||||
|
@ -521,6 +535,10 @@ class WingProfile(Model):
|
||||||
.reset()
|
.reset()
|
||||||
.push([self.elbow_axle_loc])
|
.push([self.elbow_axle_loc])
|
||||||
.each(self._elbow_joint_retract_cut_polygon, mode='s')
|
.each(self._elbow_joint_retract_cut_polygon, mode='s')
|
||||||
|
)
|
||||||
|
if self.flip:
|
||||||
|
profile = (
|
||||||
|
profile
|
||||||
.reset()
|
.reset()
|
||||||
.push([self.wrist_axle_loc])
|
.push([self.wrist_axle_loc])
|
||||||
.each(self._wrist_joint_retract_cut_polygon, mode='s')
|
.each(self._wrist_joint_retract_cut_polygon, mode='s')
|
||||||
|
@ -595,7 +613,6 @@ class WingProfile(Model):
|
||||||
return profile
|
return profile
|
||||||
def surface_s3(self,
|
def surface_s3(self,
|
||||||
front: bool = True) -> Cq.Workplane:
|
front: bool = True) -> Cq.Workplane:
|
||||||
h = self.wrist_height / 2
|
|
||||||
loc_wrist = Cq.Location.rot2d(self.wrist_rotate) * self.wrist_joint.child_arm_loc(flip=not self.flip)
|
loc_wrist = Cq.Location.rot2d(self.wrist_rotate) * self.wrist_joint.child_arm_loc(flip=not self.flip)
|
||||||
tags = [
|
tags = [
|
||||||
("wrist_bot", self.wrist_axle_loc * loc_wrist *
|
("wrist_bot", self.wrist_axle_loc * loc_wrist *
|
||||||
|
@ -605,6 +622,19 @@ class WingProfile(Model):
|
||||||
]
|
]
|
||||||
profile = self.profile_s3()
|
profile = self.profile_s3()
|
||||||
return extrude_with_markers(profile, self.panel_thickness, tags, reverse=front)
|
return extrude_with_markers(profile, self.panel_thickness, tags, reverse=front)
|
||||||
|
def surface_s3_extra(self,
|
||||||
|
front: bool = True) -> Optional[Cq.Workplane]:
|
||||||
|
profile = self.profile_s3_extra()
|
||||||
|
if profile is None:
|
||||||
|
return None
|
||||||
|
loc_wrist = Cq.Location.rot2d(self.wrist_rotate) * self.wrist_joint.child_arm_loc(flip=not self.flip)
|
||||||
|
tags = [
|
||||||
|
("wrist_bot", self.wrist_axle_loc * loc_wrist *
|
||||||
|
Cq.Location.from2d(0, self.wrist_h2)),
|
||||||
|
("wrist_top", self.wrist_axle_loc * loc_wrist *
|
||||||
|
Cq.Location.from2d(0, -self.wrist_h2)),
|
||||||
|
]
|
||||||
|
return extrude_with_markers(profile, self.panel_thickness, tags, reverse=not front)
|
||||||
@submodel(name="spacer-s3-wrist")
|
@submodel(name="spacer-s3-wrist")
|
||||||
def spacer_s3_wrist(self) -> MountingBox:
|
def spacer_s3_wrist(self) -> MountingBox:
|
||||||
return self.spacer_of_joint(
|
return self.spacer_of_joint(
|
||||||
|
@ -624,6 +654,18 @@ class WingProfile(Model):
|
||||||
.constrain("front@faces@>Z", "back@faces@<Z", "Point",
|
.constrain("front@faces@>Z", "back@faces@<Z", "Point",
|
||||||
param=self.s1_thickness)
|
param=self.s1_thickness)
|
||||||
)
|
)
|
||||||
|
if not self.flip:
|
||||||
|
(
|
||||||
|
result
|
||||||
|
.addS(self.surface_s3_extra(front=True), name="extra_front",
|
||||||
|
material=self.mat_panel, role=self.role_panel)
|
||||||
|
.constrain("front?wrist_bot", "extra_front?wrist_bot", "Plane")
|
||||||
|
.constrain("front?wrist_top", "extra_front?wrist_top", "Plane")
|
||||||
|
.addS(self.surface_s3_extra(front=False), name="extra_back",
|
||||||
|
material=self.mat_panel, role=self.role_panel)
|
||||||
|
.constrain("back?wrist_bot", "extra_back?wrist_bot", "Plane")
|
||||||
|
.constrain("back?wrist_top", "extra_back?wrist_top", "Plane")
|
||||||
|
)
|
||||||
for t in ["wrist_bot", "wrist_top"]:
|
for t in ["wrist_bot", "wrist_top"]:
|
||||||
is_top = t.endswith("_top")
|
is_top = t.endswith("_top")
|
||||||
o = self.spacer_s3_wrist()
|
o = self.spacer_s3_wrist()
|
||||||
|
@ -756,10 +798,22 @@ class WingR(WingProfile):
|
||||||
arrow_height: float = 300
|
arrow_height: float = 300
|
||||||
arrow_angle: float = -8
|
arrow_angle: float = -8
|
||||||
|
|
||||||
|
# Underapproximate the wrist tangent angle to leave no gaps on the blade
|
||||||
|
blade_wrist_approx_tangent_angle: float = 40.0
|
||||||
|
blade_overlap_arrow_height: float = 5.0
|
||||||
|
# Some overlap needed to glue the two sides
|
||||||
|
blade_overlap_angle: float = -1
|
||||||
|
blade_hole_angle: float = 3
|
||||||
|
blade_hole_diam: float = 12.0
|
||||||
|
blade_hole_heights: list[float] = field(default_factory=lambda: [230, 260])
|
||||||
|
blade_angle: float = 7
|
||||||
|
|
||||||
# Relative (in wrist coordinate) centre of the ring
|
# Relative (in wrist coordinate) centre of the ring
|
||||||
ring_rel_loc: Cq.Location = Cq.Location.from2d(45.0, 25.0)
|
ring_rel_loc: Cq.Location = Cq.Location.from2d(45.0, 25.0)
|
||||||
ring_radius_inner: float = 22.0
|
ring_radius_inner: float = 22.0
|
||||||
|
|
||||||
|
flip: bool = False
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
super().__post_init__()
|
super().__post_init__()
|
||||||
assert self.arrow_angle < 0, "Arrow angle cannot be positive"
|
assert self.arrow_angle < 0, "Arrow angle cannot be positive"
|
||||||
|
@ -771,6 +825,10 @@ class WingR(WingProfile):
|
||||||
self.ring_loc = self.wrist_top_loc * self.ring_rel_loc
|
self.ring_loc = self.wrist_top_loc * self.ring_rel_loc
|
||||||
assert self.ring_radius > self.ring_radius_inner
|
assert self.ring_radius > self.ring_radius_inner
|
||||||
|
|
||||||
|
assert 0 > self.blade_overlap_angle > self.arrow_angle
|
||||||
|
assert 0 < self.blade_hole_angle < self.blade_angle
|
||||||
|
assert self.blade_wrist_approx_tangent_angle <= self.wrist_bot_loc.to2d_rot()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ring_radius(self) -> float:
|
def ring_radius(self) -> float:
|
||||||
(dx, dy), _ = self.ring_rel_loc.to2d()
|
(dx, dy), _ = self.ring_rel_loc.to2d()
|
||||||
|
@ -829,6 +887,43 @@ class WingR(WingProfile):
|
||||||
)
|
)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def profile_s3_extra(self) -> Cq.Sketch:
|
||||||
|
"""
|
||||||
|
Implements the blade part on Nue's wing
|
||||||
|
"""
|
||||||
|
left_bot_loc = self.arrow_bot_loc * Cq.Location.rot2d(-1)
|
||||||
|
hole_bot_loc = self.arrow_bot_loc * Cq.Location.rot2d(self.blade_hole_angle)
|
||||||
|
right_bot_loc = self.arrow_bot_loc * Cq.Location.rot2d(self.blade_angle)
|
||||||
|
h_loc = Cq.Location.from2d(0, self.arrow_height + self.blade_overlap_arrow_height)
|
||||||
|
|
||||||
|
# Law of sines, uses the triangle of (wrist_bot_loc, arrow_bot_loc, ?)
|
||||||
|
theta_wp = math.radians(90 - self.blade_wrist_approx_tangent_angle)
|
||||||
|
theta_b = math.radians(self.blade_angle)
|
||||||
|
h_blade = math.sin(theta_wp) / math.sin(math.pi - theta_b - theta_wp) * self.arrow_height
|
||||||
|
h_blade_loc = Cq.Location.from2d(0, h_blade)
|
||||||
|
return (
|
||||||
|
Cq.Sketch()
|
||||||
|
.segment(
|
||||||
|
self.arrow_bot_loc.to2d_pos(),
|
||||||
|
(left_bot_loc * h_loc).to2d_pos(),
|
||||||
|
)
|
||||||
|
.segment(
|
||||||
|
(self.arrow_bot_loc * h_loc).to2d_pos(),
|
||||||
|
)
|
||||||
|
.segment(
|
||||||
|
(right_bot_loc * h_blade_loc).to2d_pos(),
|
||||||
|
)
|
||||||
|
.close()
|
||||||
|
.assemble()
|
||||||
|
.reset()
|
||||||
|
.push([
|
||||||
|
(hole_bot_loc * Cq.Location.from2d(0, h)).to2d_pos()
|
||||||
|
for h in self.blade_hole_heights
|
||||||
|
])
|
||||||
|
.circle(self.blade_hole_diam / 2, mode='s')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _mask_elbow(self) -> list[Tuple[float, float]]:
|
def _mask_elbow(self) -> list[Tuple[float, float]]:
|
||||||
l = 200
|
l = 200
|
||||||
elbow_x, _ = self.elbow_bot_loc.to2d_pos()
|
elbow_x, _ = self.elbow_bot_loc.to2d_pos()
|
||||||
|
|
|
@ -72,7 +72,7 @@ def to2d_pos(self: Cq.Location) -> Tuple[float, float]:
|
||||||
return x, y
|
return x, y
|
||||||
Cq.Location.to2d_pos = to2d_pos
|
Cq.Location.to2d_pos = to2d_pos
|
||||||
|
|
||||||
def to2d_rot(self: Cq.Location) -> Tuple[float, float]:
|
def to2d_rot(self: Cq.Location) -> float:
|
||||||
"""
|
"""
|
||||||
Returns position and angle
|
Returns position and angle
|
||||||
"""
|
"""
|
||||||
|
|
Loading…
Reference in New Issue