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

View File

@ -31,17 +31,17 @@ class Item:
def role(self) -> Optional[Role]: def role(self) -> Optional[Role]:
return None 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 Creates an assembly for this item. Subclass should implement this
""" """
return Cq.Assembly() return Cq.Assembly()
def assembly(self) -> Cq.Assembly: def assembly(self, **kwargs) -> Cq.Assembly:
""" """
Interface for creating assembly with the necessary metadata Interface for creating assembly with the necessary metadata
""" """
a = self.generate() a = self.generate(**kwargs)
if isinstance(a, Cq.Workplane): if isinstance(a, Cq.Workplane):
a = Cq.Assembly(a) a = Cq.Assembly(a)
if role := self.role: 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 3. An outer and an inner annuli which forms a track the rider can move on
""" """
spring: TorsionSpring = field(default_factory=lambda: TorsionSpring( spring: TorsionSpring = field(default_factory=lambda: TorsionSpring(
mass=float('nan'),
radius=10.0, radius=10.0,
thickness=2.0, thickness=2.0,
height=15.0, height=15.0,
@ -306,6 +307,7 @@ class TorsionJoint:
.hole(self.radius_axle * 2) .hole(self.radius_axle * 2)
.cut(slot.moved(Cq.Location((0, 0, self.track_disk_height)))) .cut(slot.moved(Cq.Location((0, 0, self.track_disk_height))))
) )
result.faces("<Z").tag("bot")
# Insert directrix # Insert directrix
result.polyline(self._directrix(self.track_disk_height), result.polyline(self._directrix(self.track_disk_height),
forConstruction=True).tag("dir") forConstruction=True).tag("dir")

View File

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

View File

@ -2,6 +2,25 @@ import unittest
import cadquery as Cq import cadquery as Cq
from nhf.checks import binary_intersection, pairwise_intersection from nhf.checks import binary_intersection, pairwise_intersection
from nhf.parts import joints, handle, metric_threads, springs 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): class TestJoints(unittest.TestCase):

View File

@ -5,6 +5,7 @@ import cadquery as Cq
from nhf import Material, Role from nhf import Material, Role
from nhf.build import Model, target, assembly from nhf.build import Model, target, assembly
from nhf.parts.springs import TorsionSpring from nhf.parts.springs import TorsionSpring
from nhf.parts.fasteners import FlatHeadBolt
from nhf.parts.joints import TorsionJoint from nhf.parts.joints import TorsionJoint
from nhf.parts.box import Hole, MountingBox, box_with_centre_holes from nhf.parts.box import Hole, MountingBox, box_with_centre_holes
import nhf.utils import nhf.utils
@ -14,6 +15,15 @@ TOL = 1e-6
@dataclass @dataclass
class ShoulderJoint(Model): 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 height: float = 60.0
torsion_joint: TorsionJoint = field(default_factory=lambda: TorsionJoint( torsion_joint: TorsionJoint = field(default_factory=lambda: TorsionJoint(
radius_track=18, radius_track=18,
@ -23,9 +33,9 @@ class ShoulderJoint(Model):
groove_radius_inner=13, groove_radius_inner=13,
track_disk_height=5.0, track_disk_height=5.0,
rider_disk_height=5.0, rider_disk_height=5.0,
# M8 Axle
radius_axle=3.0, radius_axle=3.0,
spring=TorsionSpring( spring=TorsionSpring(
mass=float('nan'),
# inner diameter = 9 # inner diameter = 9
radius=9/2 + 1.2, radius=9/2 + 1.2,
thickness=1.3, thickness=1.3,
@ -69,6 +79,16 @@ class ShoulderJoint(Model):
def __post_init__(self): def __post_init__(self):
assert self.parent_lip_length * 2 < self.height 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: def parent(self, top: bool = False) -> Cq.Assembly:
joint = self.torsion_joint joint = self.torsion_joint
# Thickness of the lip connecting this joint to the wing root # Thickness of the lip connecting this joint to the wing root
@ -219,14 +239,18 @@ class ShoulderJoint(Model):
.constrain("child/core", "Fixed") .constrain("child/core", "Fixed")
.addS(self.torsion_joint.spring.generate(deflection=-deflection), name="spring_top", .addS(self.torsion_joint.spring.generate(deflection=-deflection), name="spring_top",
role=Role.DAMPING, material=mat_spring) role=Role.DAMPING, material=mat_spring)
.addS(self.bolt.assembly(), name="bolt_top")
.addS(self.parent_top(), .addS(self.parent_top(),
name="parent_top", name="parent_top",
role=Role.PARENT, material=mat) role=Role.PARENT, material=mat)
.addS(self.torsion_joint.spring.generate(deflection=deflection), name="spring_bot", .addS(self.torsion_joint.spring.generate(deflection=deflection), name="spring_bot",
role=Role.DAMPING, material=mat_spring) role=Role.DAMPING, material=mat_spring)
.addS(self.bolt.assembly(), name="bolt_bot")
.addS(self.parent_bot(), .addS(self.parent_bot(),
name="parent_bot", name="parent_bot",
role=Role.PARENT, material=mat) 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( TorsionJoint.add_constraints(
result, result,
@ -318,6 +342,7 @@ class DiskJoint(Model):
the housing. This provides torsion resistance. the housing. This provides torsion resistance.
""" """
spring: TorsionSpring = field(default_factory=lambda: TorsionSpring( spring: TorsionSpring = field(default_factory=lambda: TorsionSpring(
mass=float('nan'),
radius=9 / 2, radius=9 / 2,
thickness=1.3, thickness=1.3,
height=6.5, height=6.5,
@ -617,7 +642,7 @@ class ElbowJoint(Model):
parent_arm_angle: float = 180.0 parent_arm_angle: float = 180.0
# Size of the mounting holes # Size of the mounting holes
hole_diam: float = 6.0 hole_diam: float = 4.0
material: Material = Material.RESIN_TRANSPERENT material: Material = Material.RESIN_TRANSPERENT
@ -628,6 +653,10 @@ class ElbowJoint(Model):
assert self.parent_arm_radius > self.disk_joint.radius_housing 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 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: def parent_arm_loc(self) -> Cq.Location:
""" """
2d Location of the centre of the arm surface on the parent side, assuming 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_angle_bias: float = 0.0
shoulder_width: float = 36.0 shoulder_width: float = 36.0
shoulder_tip_x: float = -200.0 shoulder_tip_x: float = -260.0
shoulder_tip_y: float = 160.0 shoulder_tip_y: float = 165.0
shoulder_mid_x: float = -105.0 shoulder_mid_x: float = -105.0
shoulder_mid_y: float = 75.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.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) 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_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") @submodel(name="shoulder-joint")
def submodel_shoulder_joint(self) -> Model: def submodel_shoulder_joint(self) -> Model:
@ -225,6 +229,9 @@ class WingProfile(Model):
(-self.base_width, 0), (-self.base_width, 0),
) )
.assemble() .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 return sketch
@ -292,17 +299,12 @@ class WingProfile(Model):
def surface_s0(self, top: bool = False) -> Cq.Workplane: def surface_s0(self, top: bool = False) -> Cq.Workplane:
base_dx = -(self.base_width - self.base_plate_width) / 2 base_dx = -(self.base_width - self.base_plate_width) / 2
base_dy = self.base_joint.joint_height base_dy = self.base_joint.joint_height
sw = self.shoulder_width loc_tip = Cq.Location(0, -self.shoulder_joint.parent_lip_width / 2)
axle_dist = self.shoulder_joint.parent_lip_ext
theta = math.radians(self.shoulder_angle_neutral)
c, s = math.cos(theta), math.sin(theta)
tags = [ tags = [
# transforms [axle_dist, -sw/2] about the centre (tip_x, tip_y - sw/2) ("shoulder",
("shoulder", Cq.Location.from2d( self.shoulder_axle_loc *
self.shoulder_tip_x + axle_dist * c + (-sw/2) * s, self.shoulder_joint.parent_arm_loc() *
self.shoulder_tip_y - sw / 2 - axle_dist * s + (-sw/2) * c, loc_tip),
-self.shoulder_angle_neutral)),
("base", Cq.Location.from2d(base_dx, base_dy, 90)), ("base", Cq.Location.from2d(base_dx, base_dy, 90)),
] ]
result = extrude_with_markers( result = extrude_with_markers(