cosplay: Touhou/Houjuu Nue #4
|
@ -15,6 +15,7 @@ class Role(Enum):
|
||||||
|
|
||||||
# Parent and child components in a load bearing joint
|
# Parent and child components in a load bearing joint
|
||||||
PARENT = _color('blue4', 0.6)
|
PARENT = _color('blue4', 0.6)
|
||||||
|
CASING = _color('dodgerblue3', 0.6)
|
||||||
CHILD = _color('darkorange2', 0.6)
|
CHILD = _color('darkorange2', 0.6)
|
||||||
DAMPING = _color('springgreen', 0.5)
|
DAMPING = _color('springgreen', 0.5)
|
||||||
STRUCTURE = _color('gray', 0.4)
|
STRUCTURE = _color('gray', 0.4)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from typing import Optional, Tuple
|
||||||
import cadquery as Cq
|
import cadquery as Cq
|
||||||
from nhf import Role
|
from nhf import Role
|
||||||
from nhf.build import Model, target
|
from nhf.build import Model, target
|
||||||
|
@ -25,8 +26,12 @@ class DiskJoint(Model):
|
||||||
spring_thickness: float = 1.3
|
spring_thickness: float = 1.3
|
||||||
spring_height: float = 6.5
|
spring_height: float = 6.5
|
||||||
spring_tail_length: float = 45.0
|
spring_tail_length: float = 45.0
|
||||||
spring_angle: float = 90.0
|
|
||||||
spring_angle_shift: float = 0
|
# Spring angle at 0 degrees of movement
|
||||||
|
spring_angle: float = 30.0
|
||||||
|
# Angle at which the spring exerts no torque
|
||||||
|
spring_angle_neutral: float = 90.0
|
||||||
|
spring_angle_shift: float = 30
|
||||||
wall_inset: float = 2.0
|
wall_inset: float = 2.0
|
||||||
|
|
||||||
# Angular span of movement
|
# Angular span of movement
|
||||||
|
@ -55,15 +60,22 @@ class DiskJoint(Model):
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def total_thickness(self):
|
def neutral_movement_angle(self) -> Optional[float]:
|
||||||
|
a = self.spring_angle_neutral - self.spring_angle
|
||||||
|
if 0 <= a and a <= self.movement_angle:
|
||||||
|
return a
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def total_thickness(self) -> float:
|
||||||
return self.housing_thickness * 2 + self.disk_thickness
|
return self.housing_thickness * 2 + self.disk_thickness
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def opening_span(self):
|
def opening_span(self) -> float:
|
||||||
return self.movement_angle + self.tongue_span
|
return self.movement_angle + self.tongue_span
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def housing_upper_carve_offset(self):
|
def housing_upper_carve_offset(self) -> float:
|
||||||
return self.housing_thickness + self.disk_thickness - self.spring_height
|
return self.housing_thickness + self.disk_thickness - self.spring_height
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -147,8 +159,7 @@ class DiskJoint(Model):
|
||||||
result = result.cut(
|
result = result.cut(
|
||||||
self
|
self
|
||||||
.wall()
|
.wall()
|
||||||
.mirror("XY")
|
.located(Cq.Location((0, 0, self.disk_thickness - self.wall_inset)))
|
||||||
.located(Cq.Location((0, 0, self.housing_thickness + self.disk_thickness)))
|
|
||||||
#.rotate((0, 0, 0), (1, 0, 0), 180)
|
#.rotate((0, 0, 0), (1, 0, 0), 180)
|
||||||
#.located(Cq.Location((0, 0, self.disk_thickness + self.housing_thickness)))
|
#.located(Cq.Location((0, 0, self.disk_thickness + self.housing_thickness)))
|
||||||
)
|
)
|
||||||
|
@ -165,7 +176,7 @@ class DiskJoint(Model):
|
||||||
width=self.spring_thickness,
|
width=self.spring_thickness,
|
||||||
height=self.housing_thickness
|
height=self.housing_thickness
|
||||||
).located(Cq.Location((0, self.radius_spring_internal, 0))))
|
).located(Cq.Location((0, self.radius_spring_internal, 0))))
|
||||||
).rotate((0, 0, 0), (0, 0, 1), self.spring_angle - self.spring_angle_shift)
|
).rotate((0, 0, 0), (0, 0, 1), 180 + self.spring_angle - self.spring_angle_shift)
|
||||||
result = (
|
result = (
|
||||||
Cq.Workplane('XY')
|
Cq.Workplane('XY')
|
||||||
.cylinder(
|
.cylinder(
|
||||||
|
@ -192,7 +203,8 @@ class DiskJoint(Model):
|
||||||
result = result.union(tube)
|
result = result.union(tube)
|
||||||
wall = (
|
wall = (
|
||||||
self.wall()
|
self.wall()
|
||||||
.rotate((0, 0, 0), (1, 0, 0), 180)
|
.rotate((0, 0, 0), (0, 0, 1), self.tongue_span)
|
||||||
|
.mirror("XY")
|
||||||
.located(Cq.Location((0, 0, self.disk_thickness + self.housing_thickness + self.wall_inset)))
|
.located(Cq.Location((0, 0, self.disk_thickness + self.housing_thickness + self.wall_inset)))
|
||||||
)
|
)
|
||||||
result = (
|
result = (
|
||||||
|
@ -202,18 +214,47 @@ class DiskJoint(Model):
|
||||||
)
|
)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def add_constraints(self,
|
||||||
|
assembly: Cq.Assembly,
|
||||||
|
housing_lower: str,
|
||||||
|
housing_upper: str,
|
||||||
|
disk: str,
|
||||||
|
angle: Tuple[float, float, float] = (0, 0, 0),
|
||||||
|
) -> Cq.Assembly:
|
||||||
|
"""
|
||||||
|
The angle supplied must be perpendicular to the disk normal.
|
||||||
|
"""
|
||||||
|
(
|
||||||
|
assembly
|
||||||
|
.constrain(f"{disk}?mate_bot", f"{housing_lower}?mate", "Plane")
|
||||||
|
.constrain(f"{disk}?mate_top", f"{housing_upper}?mate", "Plane")
|
||||||
|
.constrain(f"{housing_lower}?dir", f"{housing_upper}?dir", "Axis")
|
||||||
|
.constrain(f"{disk}?dir", "FixedRotation", angle)
|
||||||
|
)
|
||||||
|
|
||||||
def assembly(self) -> Cq.Assembly:
|
|
||||||
|
def assembly(self, angle: Optional[float] = 0) -> Cq.Assembly:
|
||||||
|
if angle is None:
|
||||||
|
angle = self.movement_angle
|
||||||
|
if angle is None:
|
||||||
|
angle = 0
|
||||||
|
else:
|
||||||
|
assert 0 <= angle <= self.movement_angle
|
||||||
result = (
|
result = (
|
||||||
Cq.Assembly()
|
Cq.Assembly()
|
||||||
.add(self.disk(), name="disk", color=Role.CHILD.color)
|
.add(self.disk(), name="disk", color=Role.CHILD.color)
|
||||||
.add(self.housing_lower(), name="housing_lower", color=Role.PARENT.color)
|
.add(self.housing_lower(), name="housing_lower", color=Role.PARENT.color)
|
||||||
.add(self.housing_upper(), name="housing_upper", color=Role.PARENT.color)
|
.add(self.housing_upper(), name="housing_upper", color=Role.CASING.color)
|
||||||
.constrain("disk?mate_bot", "housing_lower?mate", "Plane")
|
.constrain("housing_lower", "Fixed")
|
||||||
.constrain("disk?mate_top", "housing_upper?mate", "Plane")
|
|
||||||
.solve()
|
|
||||||
)
|
)
|
||||||
return result
|
self.add_constraints(
|
||||||
|
result,
|
||||||
|
housing_lower="housing_lower",
|
||||||
|
housing_upper="housing_upper",
|
||||||
|
disk="disk",
|
||||||
|
angle=(0, 0, angle),
|
||||||
|
)
|
||||||
|
return result.solve()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
p = DiskJoint()
|
p = DiskJoint()
|
||||||
|
|
|
@ -1,8 +1,24 @@
|
||||||
import unittest
|
import unittest
|
||||||
import cadquery as Cq
|
import cadquery as Cq
|
||||||
import nhf.touhou.houjuu_nue as M
|
import nhf.touhou.houjuu_nue as M
|
||||||
|
import nhf.touhou.houjuu_nue.parts as MP
|
||||||
from nhf.checks import pairwise_intersection
|
from nhf.checks import pairwise_intersection
|
||||||
|
|
||||||
|
class TestDiskJoint(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_collision_0(self):
|
||||||
|
j = MP.DiskJoint()
|
||||||
|
assembly = j.assembly(angle=0)
|
||||||
|
self.assertEqual(pairwise_intersection(assembly), [])
|
||||||
|
def test_collision_mid(self):
|
||||||
|
j = MP.DiskJoint()
|
||||||
|
assembly = j.assembly(angle=j.movement_angle / 2)
|
||||||
|
self.assertEqual(pairwise_intersection(assembly), [])
|
||||||
|
def test_collision_max(self):
|
||||||
|
j = MP.DiskJoint()
|
||||||
|
assembly = j.assembly(angle=j.movement_angle)
|
||||||
|
self.assertEqual(pairwise_intersection(assembly), [])
|
||||||
|
|
||||||
class Test(unittest.TestCase):
|
class Test(unittest.TestCase):
|
||||||
|
|
||||||
def test_hs_joint_parent(self):
|
def test_hs_joint_parent(self):
|
||||||
|
|
Loading…
Reference in New Issue