cosplay: Touhou/Houjuu Nue #4

Open
aniva wants to merge 189 commits from touhou/houjuu-nue into main
7 changed files with 132 additions and 41 deletions
Showing only changes of commit 3e5fe7bc5e - Show all commits

View File

@ -4,6 +4,42 @@ import cadquery as Cq
from nhf import Item, Role
import nhf.utils
@dataclass(frozen=True)
class FlatHeadBolt(Item):
diam_head: float
height_head: float
diam_thread: float
height_thread: float
@property
def name(self) -> str:
return f"Bolt M{int(self.diam_thread)} h{int(self.height_thread)}mm"
def generate(self) -> Cq.Assembly:
print(self.name)
head = Cq.Solid.makeCylinder(
radius=self.diam_head / 2,
height=self.height_head,
)
rod = (
Cq.Workplane('XY')
.cylinder(
radius=self.diam_thread/ 2,
height=self.height_thread,
centered=(True, True, False))
)
rod.faces("<Z").tag("tip")
rod.faces(">Z").tag("root")
return (
Cq.Assembly()
.addS(rod, name="thread", role=Role.CONNECTION)
.addS(head, name="head", role=Role.CONNECTION,
loc=Cq.Location((0, 0, self.height_thread)))
)
@dataclass(frozen=True)
class ThreaddedKnob(Item):
"""
@ -12,8 +48,8 @@ class ThreaddedKnob(Item):
> Othmro Black 12mm(M12) x 50mm Thread Replacement Star Hand Knob Tightening
> Screws
"""
diam_rod: float
height_rod: float
diam_thread: float
height_thread: float
diam_knob: float
diam_neck: float
@ -22,7 +58,7 @@ class ThreaddedKnob(Item):
@property
def name(self) -> str:
return f"Knob-M{int(self.diam_rod)}-{int(self.height_rod)}mm"
return f"Knob M{int(self.diam_thread)} h{int(self.height_thread)}mm"
def generate(self) -> Cq.Assembly:
print(self.name)
@ -34,41 +70,40 @@ class ThreaddedKnob(Item):
radius=self.diam_neck / 2,
height=self.height_neck,
)
rod = (
thread = (
Cq.Workplane('XY')
.cylinder(
radius=self.diam_rod / 2,
height=self.height_rod,
radius=self.diam_thread / 2,
height=self.height_thread,
centered=(True, True, False))
)
rod.faces("<Z").tag("tip")
rod.faces(">Z").tag("root")
thread.faces("<Z").tag("tip")
thread.faces(">Z").tag("root")
return (
Cq.Assembly()
.addS(rod, name="rod", role=Role.CONNECTION)
.addS(thread, name="thread", role=Role.CONNECTION)
.addS(neck, name="neck", role=Role.HANDLE,
loc=Cq.Location((0, 0, self.height_rod)))
loc=Cq.Location((0, 0, self.height_thread)))
.addS(knob, name="knob", role=Role.HANDLE,
loc=Cq.Location((0, 0, self.height_rod + self.height_neck)))
loc=Cq.Location((0, 0, self.height_thread + self.height_neck)))
)
@dataclass(frozen=True)
class HexNut(Item):
diam: float
diam_thread: float
pitch: float
# FIXME: Measure these
m: float
s: float
thickness: float
width: float
def __post_init__(self):
assert self.s > self.diam
assert self.width > self.diam_thread
@property
def name(self):
return f"HexNut-M{int(self.diam)}-{self.pitch}"
return f"HexNut M{int(self.diam_thread)}-{self.pitch}"
@property
def role(self):
@ -76,14 +111,14 @@ class HexNut(Item):
def generate(self) -> Cq.Workplane:
print(self.name)
r = self.s / math.sqrt(3)
r = self.width / math.sqrt(3)
result = (
Cq.Workplane("XY")
.sketch()
.regularPolygon(r=r, n=6)
.circle(r=self.diam/2, mode='s')
.circle(r=self.diam_thread/2, mode='s')
.finalize()
.extrude(self.m)
.extrude(self.thickness)
)
result.faces("<Z").tag("bot")
result.faces(">Z").tag("top")

View File

@ -31,17 +31,17 @@ class Item:
def role(self) -> Optional[Role]:
return None
def generate(self) -> Union[Cq.Assembly, Cq.Workplane]:
def generate(self, **kwargs) -> Union[Cq.Assembly, Cq.Workplane]:
"""
Creates an assembly for this item. Subclass should implement this
"""
return Cq.Assembly()
def assembly(self) -> Cq.Assembly:
def assembly(self, **kwargs) -> Cq.Assembly:
"""
Interface for creating assembly with the necessary metadata
"""
a = self.generate()
a = self.generate(**kwargs)
if isinstance(a, Cq.Workplane):
a = Cq.Assembly(a)
if role := self.role:

View File

@ -180,6 +180,7 @@ class TorsionJoint:
3. An outer and an inner annuli which forms a track the rider can move on
"""
spring: TorsionSpring = field(default_factory=lambda: TorsionSpring(
mass=float('nan'),
radius=10.0,
thickness=2.0,
height=15.0,
@ -306,6 +307,7 @@ class TorsionJoint:
.hole(self.radius_axle * 2)
.cut(slot.moved(Cq.Location((0, 0, self.track_disk_height))))
)
result.faces("<Z").tag("bot")
# Insert directrix
result.polyline(self._directrix(self.track_disk_height),
forConstruction=True).tag("dir")

View File

@ -2,9 +2,10 @@ import math
from typing import Optional
from dataclasses import dataclass
import cadquery as Cq
from nhf import Item, Role
@dataclass
class TorsionSpring:
@dataclass(frozen=True)
class TorsionSpring(Item):
"""
A torsion spring with abridged geometry (since sweep is slow)
"""
@ -21,6 +22,10 @@ class TorsionSpring:
torsion_rate: Optional[float] = None
@property
def name(self) -> str:
return f"TorsionSpring-{int(self.radius)}-{int(self.height)}"
@property
def radius_inner(self) -> float:
return self.radius - self.thickness
@ -28,7 +33,7 @@ class TorsionSpring:
def torque_at(self, theta: float) -> float:
return self.torsion_rate * theta
def generate(self, deflection: float = 0):
def generate(self, deflection: float = 0) -> Cq.Workplane:
omega = self.angle_neutral + deflection
omega = -omega if self.right_handed else omega
base = (
@ -39,7 +44,6 @@ class TorsionSpring:
base.faces(">Z").tag("top")
base.faces("<Z").tag("bot")
box_shift = -self.radius if self.right_handed else self.radius-self.thickness
tail = Cq.Solid.makeCylinder(
height=self.tail_length,
radius=self.thickness / 2)

View File

@ -2,6 +2,25 @@ import unittest
import cadquery as Cq
from nhf.checks import binary_intersection, pairwise_intersection
from nhf.parts import joints, handle, metric_threads, springs
import nhf.parts.fasteners as fasteners
class TestFasteners(unittest.TestCase):
def test_hex_nut(self):
width = 18.9
height = 9.8
item = fasteners.HexNut(
mass=float('nan'),
diam_thread=12.0,
pitch=1.75,
thickness=9.8,
width=width,
)
obj = item.generate()
self.assertEqual(len(obj.vals()), 1)
bbox = obj.val().BoundingBox()
self.assertAlmostEqual(bbox.xlen, width)
self.assertAlmostEqual(bbox.zlen, height)
class TestJoints(unittest.TestCase):

View File

@ -5,6 +5,7 @@ import cadquery as Cq
from nhf import Material, Role
from nhf.build import Model, target, assembly
from nhf.parts.springs import TorsionSpring
from nhf.parts.fasteners import FlatHeadBolt
from nhf.parts.joints import TorsionJoint
from nhf.parts.box import Hole, MountingBox, box_with_centre_holes
import nhf.utils
@ -14,6 +15,15 @@ TOL = 1e-6
@dataclass
class ShoulderJoint(Model):
bolt: FlatHeadBolt = FlatHeadBolt(
# FIXME: measure
diam_head=10.0,
height_head=3.0,
diam_thread=6.0,
height_thread=20.0,
mass=float('nan'),
)
height: float = 60.0
torsion_joint: TorsionJoint = field(default_factory=lambda: TorsionJoint(
radius_track=18,
@ -23,9 +33,9 @@ class ShoulderJoint(Model):
groove_radius_inner=13,
track_disk_height=5.0,
rider_disk_height=5.0,
# M8 Axle
radius_axle=3.0,
spring=TorsionSpring(
mass=float('nan'),
# inner diameter = 9
radius=9/2 + 1.2,
thickness=1.3,
@ -69,6 +79,16 @@ class ShoulderJoint(Model):
def __post_init__(self):
assert self.parent_lip_length * 2 < self.height
@property
def radius(self):
return self.torsion_joint.radius
def parent_arm_loc(self) -> Cq.Location:
"""
2d location of the arm surface on the parent side, relative to axle
"""
return Cq.Location.rot2d(self.angle_neutral) * Cq.Location.from2d(self.parent_lip_ext, 0, 0)
def parent(self, top: bool = False) -> Cq.Assembly:
joint = self.torsion_joint
# Thickness of the lip connecting this joint to the wing root
@ -219,14 +239,18 @@ class ShoulderJoint(Model):
.constrain("child/core", "Fixed")
.addS(self.torsion_joint.spring.generate(deflection=-deflection), name="spring_top",
role=Role.DAMPING, material=mat_spring)
.addS(self.bolt.assembly(), name="bolt_top")
.addS(self.parent_top(),
name="parent_top",
role=Role.PARENT, material=mat)
.addS(self.torsion_joint.spring.generate(deflection=deflection), name="spring_bot",
role=Role.DAMPING, material=mat_spring)
.addS(self.bolt.assembly(), name="bolt_bot")
.addS(self.parent_bot(),
name="parent_bot",
role=Role.PARENT, material=mat)
.constrain("bolt_top/thread?root", "parent_top/track?bot", "Plane", param=0)
.constrain("bolt_bot/thread?root", "parent_bot/track?bot", "Plane", param=0)
)
TorsionJoint.add_constraints(
result,
@ -318,6 +342,7 @@ class DiskJoint(Model):
the housing. This provides torsion resistance.
"""
spring: TorsionSpring = field(default_factory=lambda: TorsionSpring(
mass=float('nan'),
radius=9 / 2,
thickness=1.3,
height=6.5,
@ -617,7 +642,7 @@ class ElbowJoint(Model):
parent_arm_angle: float = 180.0
# Size of the mounting holes
hole_diam: float = 6.0
hole_diam: float = 4.0
material: Material = Material.RESIN_TRANSPERENT
@ -628,6 +653,10 @@ class ElbowJoint(Model):
assert self.parent_arm_radius > self.disk_joint.radius_housing
self.disk_joint.tongue_length = self.child_arm_radius - self.disk_joint.radius_disk - self.lip_thickness / 2
@property
def total_thickness(self):
return self.disk_joint.total_thickness
def parent_arm_loc(self) -> Cq.Location:
"""
2d Location of the centre of the arm surface on the parent side, assuming

View File

@ -41,8 +41,8 @@ class WingProfile(Model):
))
shoulder_angle_bias: float = 0.0
shoulder_width: float = 36.0
shoulder_tip_x: float = -200.0
shoulder_tip_y: float = 160.0
shoulder_tip_x: float = -260.0
shoulder_tip_y: float = 165.0
shoulder_mid_x: float = -105.0
shoulder_mid_y: float = 75.0
@ -102,7 +102,11 @@ class WingProfile(Model):
self.elbow_axle_loc = self.elbow_bot_loc * Cq.Location.from2d(0, self.elbow_height / 2)
self.wrist_axle_loc = self.wrist_bot_loc * Cq.Location.from2d(0, self.wrist_height / 2)
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)
self.shoulder_joint.angle_neutral = -self.shoulder_angle_neutral - self.shoulder_angle_bias
self.shoulder_axle_loc = Cq.Location.from2d(self.shoulder_tip_x, self.shoulder_tip_y - self.shoulder_width / 2, -self.shoulder_angle_bias)
@submodel(name="shoulder-joint")
def submodel_shoulder_joint(self) -> Model:
@ -225,6 +229,9 @@ class WingProfile(Model):
(-self.base_width, 0),
)
.assemble()
.push([self.shoulder_axle_loc.to2d_pos()])
.circle(self.shoulder_joint.radius, mode='a')
.circle(self.shoulder_joint.bolt.diam_head / 2, mode='s')
)
return sketch
@ -292,17 +299,12 @@ class WingProfile(Model):
def surface_s0(self, top: bool = False) -> Cq.Workplane:
base_dx = -(self.base_width - self.base_plate_width) / 2
base_dy = self.base_joint.joint_height
sw = self.shoulder_width
axle_dist = self.shoulder_joint.parent_lip_ext
theta = math.radians(self.shoulder_angle_neutral)
c, s = math.cos(theta), math.sin(theta)
loc_tip = Cq.Location(0, -self.shoulder_joint.parent_lip_width / 2)
tags = [
# transforms [axle_dist, -sw/2] about the centre (tip_x, tip_y - sw/2)
("shoulder", Cq.Location.from2d(
self.shoulder_tip_x + axle_dist * c + (-sw/2) * s,
self.shoulder_tip_y - sw / 2 - axle_dist * s + (-sw/2) * c,
-self.shoulder_angle_neutral)),
("shoulder",
self.shoulder_axle_loc *
self.shoulder_joint.parent_arm_loc() *
loc_tip),
("base", Cq.Location.from2d(base_dx, base_dy, 90)),
]
result = extrude_with_markers(