cosplay: Touhou/Houjuu Nue #4
|
@ -1,12 +0,0 @@
|
||||||
import cadquery as Cq
|
|
||||||
|
|
||||||
def mystery():
|
|
||||||
return (
|
|
||||||
Cq.Workplane("XY")
|
|
||||||
.box(10, 5, 5)
|
|
||||||
.faces(">Z")
|
|
||||||
.workplane()
|
|
||||||
.hole(1)
|
|
||||||
.edges("|Z")
|
|
||||||
.fillet(2)
|
|
||||||
)
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
#+title: Cosplay: Houjuu Nue
|
||||||
|
|
||||||
|
* Controller
|
||||||
|
|
||||||
|
This part describes the electrical connections and the microcontroller code.
|
||||||
|
|
||||||
|
* Structure
|
||||||
|
|
||||||
|
This part describes the 3d printed and laser cut structures. ~structure.blend~
|
||||||
|
is an overall sketch of the shapes and looks of the wing.
|
||||||
|
|
||||||
|
* Pattern
|
||||||
|
|
||||||
|
This part describes the sewing patterns.
|
|
@ -0,0 +1,204 @@
|
||||||
|
"""
|
||||||
|
To build, execute
|
||||||
|
```
|
||||||
|
python3 nhf/touhou/houjuu_nue/__init__.py
|
||||||
|
```
|
||||||
|
|
||||||
|
This cosplay consists of 3 components:
|
||||||
|
|
||||||
|
## Trident
|
||||||
|
|
||||||
|
The trident is composed of individual segments, made of acrylic, and a 3D
|
||||||
|
printed head (convention rule prohibits metal) with a metallic paint. To ease
|
||||||
|
transportation, the trident handle has individual segments with threads and can
|
||||||
|
be assembled on site.
|
||||||
|
|
||||||
|
## Snake
|
||||||
|
|
||||||
|
A 3D printed snake with a soft material so it can wrap around and bend
|
||||||
|
|
||||||
|
## Wings
|
||||||
|
|
||||||
|
This is the crux of the cosplay and the most complex component. The wings mount
|
||||||
|
on a wearable harness. Each wing consists of 4 segments with 3 joints. Parts of
|
||||||
|
the wing which demands transluscency are created from 1/16" acrylic panels.
|
||||||
|
These panels serve double duty as the exoskeleton.
|
||||||
|
|
||||||
|
The wings are labeled r1,r2,r3,l1,l2,l3. The segments of the wings are labeled
|
||||||
|
from root to tip s0 (root),
|
||||||
|
s1, s2, s3. The joints are named (from root to tip)
|
||||||
|
shoulder, elbow, wrist in analogy with human anatomy.
|
||||||
|
"""
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Optional
|
||||||
|
import cadquery as Cq
|
||||||
|
from nhf.build import Model, TargetKind, target, assembly, submodel
|
||||||
|
import nhf.touhou.houjuu_nue.wing as MW
|
||||||
|
import nhf.touhou.houjuu_nue.trident as MT
|
||||||
|
import nhf.touhou.houjuu_nue.joints as MJ
|
||||||
|
import nhf.touhou.houjuu_nue.harness as MH
|
||||||
|
import nhf.touhou.houjuu_nue.electronics as ME
|
||||||
|
from nhf.parts.item import Item
|
||||||
|
import nhf.utils
|
||||||
|
|
||||||
|
WING_DEFLECT_ODD = 0.0
|
||||||
|
WING_DEFLECT_EVEN = 25.0
|
||||||
|
@dataclass
|
||||||
|
class Parameters(Model):
|
||||||
|
"""
|
||||||
|
Defines dimensions for the Houjuu Nue cosplay
|
||||||
|
"""
|
||||||
|
|
||||||
|
harness: MH.Harness = field(default_factory=lambda: MH.Harness())
|
||||||
|
|
||||||
|
wing_r1: MW.WingR = field(default_factory=lambda: MW.WingR(
|
||||||
|
name="r1",
|
||||||
|
root_joint=MJ.RootJoint(
|
||||||
|
parent_substrate_cull_corners=(0,1,1,1),
|
||||||
|
parent_substrate_cull_edges=(0,0,1,0),
|
||||||
|
),
|
||||||
|
shoulder_angle_bias=WING_DEFLECT_ODD,
|
||||||
|
s0_top_hole=False,
|
||||||
|
s0_bot_hole=True,
|
||||||
|
arrow_height=350.0
|
||||||
|
))
|
||||||
|
wing_r2: MW.WingR = field(default_factory=lambda: MW.WingR(
|
||||||
|
name="r2",
|
||||||
|
root_joint=MJ.RootJoint(
|
||||||
|
parent_substrate_cull_corners=(1,1,1,1),
|
||||||
|
parent_substrate_cull_edges=(0,0,1,0),
|
||||||
|
),
|
||||||
|
electronic_board=ME.ElectronicBoardControl(),
|
||||||
|
shoulder_angle_bias=WING_DEFLECT_EVEN,
|
||||||
|
s0_top_hole=True,
|
||||||
|
s0_bot_hole=True,
|
||||||
|
))
|
||||||
|
wing_r3: MW.WingR = field(default_factory=lambda: MW.WingR(
|
||||||
|
name="r3",
|
||||||
|
root_joint=MJ.RootJoint(
|
||||||
|
parent_substrate_cull_corners=(1,1,1,0),
|
||||||
|
parent_substrate_cull_edges=(0,0,1,0),
|
||||||
|
),
|
||||||
|
shoulder_angle_bias=WING_DEFLECT_ODD,
|
||||||
|
s0_top_hole=True,
|
||||||
|
s0_bot_hole=False,
|
||||||
|
))
|
||||||
|
wing_l1: MW.WingL = field(default_factory=lambda: MW.WingL(
|
||||||
|
name="l1",
|
||||||
|
root_joint=MJ.RootJoint(
|
||||||
|
parent_substrate_cull_corners=(1,0,1,1),
|
||||||
|
parent_substrate_cull_edges=(1,0,0,0),
|
||||||
|
),
|
||||||
|
shoulder_angle_bias=WING_DEFLECT_EVEN,
|
||||||
|
wrist_angle=-60.0,
|
||||||
|
s0_top_hole=False,
|
||||||
|
s0_bot_hole=True,
|
||||||
|
))
|
||||||
|
wing_l2: MW.WingL = field(default_factory=lambda: MW.WingL(
|
||||||
|
name="l2",
|
||||||
|
root_joint=MJ.RootJoint(
|
||||||
|
parent_substrate_cull_corners=(1,1,1,1),
|
||||||
|
parent_substrate_cull_edges=(1,0,0,0),
|
||||||
|
),
|
||||||
|
wrist_angle=-30.0,
|
||||||
|
shoulder_angle_bias=WING_DEFLECT_ODD,
|
||||||
|
s0_top_hole=True,
|
||||||
|
s0_bot_hole=True,
|
||||||
|
))
|
||||||
|
wing_l3: MW.WingL = field(default_factory=lambda: MW.WingL(
|
||||||
|
name="l3",
|
||||||
|
root_joint=MJ.RootJoint(
|
||||||
|
parent_substrate_cull_corners=(1,1,0,1),
|
||||||
|
parent_substrate_cull_edges=(1,0,0,0),
|
||||||
|
),
|
||||||
|
shoulder_angle_bias=WING_DEFLECT_EVEN,
|
||||||
|
wrist_angle=-0.0,
|
||||||
|
s0_top_hole=True,
|
||||||
|
s0_bot_hole=False,
|
||||||
|
))
|
||||||
|
|
||||||
|
trident: MT.Trident = field(default_factory=lambda: MT.Trident())
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
super().__init__(name="houjuu-nue")
|
||||||
|
|
||||||
|
@submodel(name="harness")
|
||||||
|
def submodel_harness(self) -> Model:
|
||||||
|
return self.harness
|
||||||
|
|
||||||
|
@submodel(name="wing-r1")
|
||||||
|
def submodel_wing_r1(self) -> Model:
|
||||||
|
return self.wing_r1
|
||||||
|
@submodel(name="wing-r2")
|
||||||
|
def submodel_wing_r2(self) -> Model:
|
||||||
|
return self.wing_r2
|
||||||
|
@submodel(name="wing-r3")
|
||||||
|
def submodel_wing_r3(self) -> Model:
|
||||||
|
return self.wing_r3
|
||||||
|
@submodel(name="wing-l1")
|
||||||
|
def submodel_wing_l1(self) -> Model:
|
||||||
|
return self.wing_l1
|
||||||
|
@submodel(name="wing-l2")
|
||||||
|
def submodel_wing_l2(self) -> Model:
|
||||||
|
return self.wing_l2
|
||||||
|
@submodel(name="wing-l3")
|
||||||
|
def submodel_wing_l3(self) -> Model:
|
||||||
|
return self.wing_l3
|
||||||
|
|
||||||
|
@assembly()
|
||||||
|
def wings_harness_assembly(self,
|
||||||
|
parts: Optional[list[str]] = None,
|
||||||
|
**kwargs) -> Cq.Assembly:
|
||||||
|
"""
|
||||||
|
Assembly of harness with all the wings
|
||||||
|
"""
|
||||||
|
result = (
|
||||||
|
Cq.Assembly()
|
||||||
|
.add(self.harness.assembly(), name="harness", loc=Cq.Location((0, 0, 0)))
|
||||||
|
.add(self.wing_r1.assembly(parts, root_offset=9, **kwargs), name="wing_r1")
|
||||||
|
.add(self.wing_r2.assembly(parts, root_offset=7, **kwargs), name="wing_r2")
|
||||||
|
.add(self.wing_r3.assembly(parts, root_offset=6, **kwargs), name="wing_r3")
|
||||||
|
.add(self.wing_l1.assembly(parts, root_offset=19, **kwargs), name="wing_l1")
|
||||||
|
.add(self.wing_l2.assembly(parts, root_offset=20, **kwargs), name="wing_l2")
|
||||||
|
.add(self.wing_l3.assembly(parts, root_offset=21, **kwargs), name="wing_l3")
|
||||||
|
)
|
||||||
|
for tag in ["r1", "r2", "r3", "l1", "l2", "l3"]:
|
||||||
|
self.harness.add_root_joint_constraint(
|
||||||
|
result,
|
||||||
|
"harness/base",
|
||||||
|
f"wing_{tag}/root",
|
||||||
|
tag
|
||||||
|
)
|
||||||
|
return result.solve()
|
||||||
|
|
||||||
|
@submodel(name="trident")
|
||||||
|
def submodel_trident(self) -> Model:
|
||||||
|
return self.trident
|
||||||
|
|
||||||
|
def stat(self) -> dict[str, float]:
|
||||||
|
a = self.wings_harness_assembly()
|
||||||
|
bbox = a.toCompound().BoundingBox()
|
||||||
|
return {
|
||||||
|
"wing-span": bbox.xlen,
|
||||||
|
"wing-depth": bbox.ylen,
|
||||||
|
"wing-height": bbox.zlen,
|
||||||
|
"wing-mass": a.total_mass(),
|
||||||
|
"wing-centre-of-mass": a.centre_of_mass().toTuple(),
|
||||||
|
"items": Item.count(a),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
|
||||||
|
p = Parameters()
|
||||||
|
if len(sys.argv) == 1:
|
||||||
|
p.build_all()
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
if sys.argv[1] == 'stat':
|
||||||
|
print(p.stat())
|
||||||
|
elif sys.argv[1] == 'model':
|
||||||
|
file_name = sys.argv[2]
|
||||||
|
a = p.wings_harness_assembly()
|
||||||
|
a.save(file_name, exportType='STEP')
|
|
@ -0,0 +1,18 @@
|
||||||
|
from nhf.parts.fasteners import FlatHeadBolt, HexNut, ThreaddedKnob
|
||||||
|
|
||||||
|
NUT_COMMON = HexNut(
|
||||||
|
# FIXME: measure
|
||||||
|
mass=0.0,
|
||||||
|
diam_thread=4.0,
|
||||||
|
pitch=0.7,
|
||||||
|
thickness=3.2,
|
||||||
|
width=7.0,
|
||||||
|
)
|
||||||
|
BOLT_COMMON = FlatHeadBolt(
|
||||||
|
# FIXME: measure
|
||||||
|
mass=0.0,
|
||||||
|
diam_head=8.0,
|
||||||
|
height_head=2.0,
|
||||||
|
diam_thread=4.0,
|
||||||
|
height_thread=20.0,
|
||||||
|
)
|
|
@ -0,0 +1,68 @@
|
||||||
|
#include <FastLED.h>
|
||||||
|
|
||||||
|
// Main LED strip setup
|
||||||
|
#define LED_PIN 5
|
||||||
|
#define NUM_LEDS 100
|
||||||
|
#define LED_PART 50
|
||||||
|
#define BRIGHTNESS 250
|
||||||
|
#define LED_TYPE WS2811
|
||||||
|
CRGB leds[NUM_LEDS];
|
||||||
|
|
||||||
|
CRGB color_red;
|
||||||
|
CRGB color_blue;
|
||||||
|
CRGB color_green;
|
||||||
|
|
||||||
|
#define DIAG_PIN 6
|
||||||
|
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
// Calculate colors
|
||||||
|
hsv2rgb_spectrum(CHSV(4, 255, 100), color_red);
|
||||||
|
hsv2rgb_spectrum(CHSV(170, 255, 100), color_blue);
|
||||||
|
hsv2rgb_spectrum(CHSV(90, 255, 100), color_green);
|
||||||
|
pinMode(LED_BUILTIN, OUTPUT);
|
||||||
|
pinMode(LED_PIN, OUTPUT);
|
||||||
|
pinMode(DIAG_PIN, OUTPUT);
|
||||||
|
|
||||||
|
// Main LED strip
|
||||||
|
FastLED.addLeds<LED_TYPE, LED_PIN, RGB>(leds, NUM_LEDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
fill_segmented(CRGB::Green, CRGB::Orange);
|
||||||
|
delay(500);
|
||||||
|
|
||||||
|
flash(leds, NUM_LEDS, color_red, 10, 20);
|
||||||
|
delay(500);
|
||||||
|
flash(leds, NUM_LEDS, color_blue, 10, 20);
|
||||||
|
delay(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
void fill_segmented(CRGB c1, CRGB c2)
|
||||||
|
{
|
||||||
|
//fill_solid(leds, LED_PART, c1);
|
||||||
|
fill_gradient_RGB(leds, LED_PART, CRGB::Black ,c1);
|
||||||
|
fill_gradient_RGB(leds + LED_PART, NUM_LEDS - LED_PART, CRGB::Black, c2);
|
||||||
|
FastLED.show();
|
||||||
|
}
|
||||||
|
void flash(CRGB *ptr, uint16_t num, CRGB const& lead, int steps, int step_time)
|
||||||
|
{
|
||||||
|
digitalWrite(LED_BUILTIN, LOW);
|
||||||
|
|
||||||
|
//fill_solid(leds, NUM_LEDS, CRGB::Black);
|
||||||
|
for (int i = 0; i < steps; ++i)
|
||||||
|
{
|
||||||
|
uint8_t factor = 255 * i / steps;
|
||||||
|
analogWrite(DIAG_PIN, factor);
|
||||||
|
CRGB tail = blend(lead, CRGB::Black, factor);
|
||||||
|
uint16_t front = factor * (int) num / 255;
|
||||||
|
fill_solid(ptr, front, tail);
|
||||||
|
//fill_gradient_RGB(ptr, front, tail, lead);
|
||||||
|
//fill_solid(leds + front, NUM_LEDS - front, CRGB::Black);
|
||||||
|
FastLED.show();
|
||||||
|
delay(step_time);
|
||||||
|
}
|
||||||
|
fill_gradient_RGB(ptr, num, CRGB::Black, lead);
|
||||||
|
FastLED.show();
|
||||||
|
analogWrite(DIAG_PIN, LOW);
|
||||||
|
}
|
|
@ -0,0 +1,540 @@
|
||||||
|
"""
|
||||||
|
Electronic components
|
||||||
|
"""
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Optional, Tuple
|
||||||
|
import math
|
||||||
|
import cadquery as Cq
|
||||||
|
from nhf.build import Model, TargetKind, target, assembly, submodel
|
||||||
|
from nhf.materials import Role, Material
|
||||||
|
from nhf.parts.box import MountingBox, Hole
|
||||||
|
from nhf.parts.fibre import tension_fibre
|
||||||
|
from nhf.parts.item import Item
|
||||||
|
from nhf.parts.fasteners import FlatHeadBolt, HexNut
|
||||||
|
from nhf.parts.electronics import ArduinoUnoR3, BatteryBox18650
|
||||||
|
from nhf.touhou.houjuu_nue.common import NUT_COMMON, BOLT_COMMON
|
||||||
|
import nhf.utils
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class LinearActuator(Item):
|
||||||
|
stroke_length: float
|
||||||
|
shaft_diam: float = 9.04
|
||||||
|
|
||||||
|
front_hole_ext: float = 4.41
|
||||||
|
front_hole_diam: float = 4.41
|
||||||
|
front_length: float = 9.55
|
||||||
|
front_width: float = 9.24
|
||||||
|
front_height: float = 5.98
|
||||||
|
|
||||||
|
segment1_length: float = 37.54
|
||||||
|
segment1_width: float = 15.95
|
||||||
|
segment1_height: float = 11.94
|
||||||
|
|
||||||
|
segment2_length: float = 37.37
|
||||||
|
segment2_width: float = 20.03
|
||||||
|
segment2_height: float = 15.03
|
||||||
|
|
||||||
|
back_hole_ext: float = 4.58
|
||||||
|
back_hole_diam: float = 4.18
|
||||||
|
back_length: float = 9.27
|
||||||
|
back_width: float = 10.16
|
||||||
|
back_height: float = 8.12
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return f"LinearActuator {self.stroke_length}mm"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def role(self) -> Role:
|
||||||
|
return Role.MOTION
|
||||||
|
|
||||||
|
@property
|
||||||
|
def conn_length(self):
|
||||||
|
return self.segment1_length + self.segment2_length + self.front_hole_ext + self.back_hole_ext
|
||||||
|
|
||||||
|
def generate(self, pos: float=0) -> Cq.Assembly:
|
||||||
|
assert -1e-6 <= pos <= 1 + 1e-6, f"Illegal position: {pos}"
|
||||||
|
stroke_x = pos * self.stroke_length
|
||||||
|
front = (
|
||||||
|
Cq.Workplane('XZ')
|
||||||
|
.cylinder(
|
||||||
|
radius=self.front_width / 2,
|
||||||
|
height=self.front_height,
|
||||||
|
centered=True,
|
||||||
|
)
|
||||||
|
.box(
|
||||||
|
length=self.front_hole_ext,
|
||||||
|
width=self.front_width,
|
||||||
|
height=self.front_height,
|
||||||
|
combine=True,
|
||||||
|
centered=(False, True, True)
|
||||||
|
)
|
||||||
|
.copyWorkplane(Cq.Workplane('XZ'))
|
||||||
|
.cylinder(
|
||||||
|
radius=self.front_hole_diam / 2,
|
||||||
|
height=self.front_height,
|
||||||
|
centered=True,
|
||||||
|
combine='cut',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
front.copyWorkplane(Cq.Workplane('XZ')).tagPlane('conn')
|
||||||
|
if stroke_x > 0:
|
||||||
|
shaft = (
|
||||||
|
Cq.Workplane('YZ')
|
||||||
|
.cylinder(
|
||||||
|
radius=self.shaft_diam / 2,
|
||||||
|
height=stroke_x,
|
||||||
|
centered=(True, True, False)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
shaft = None
|
||||||
|
segment1 = (
|
||||||
|
Cq.Workplane()
|
||||||
|
.box(
|
||||||
|
length=self.segment1_length,
|
||||||
|
height=self.segment1_width,
|
||||||
|
width=self.segment1_height,
|
||||||
|
centered=(False, True, True),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
segment2 = (
|
||||||
|
Cq.Workplane()
|
||||||
|
.box(
|
||||||
|
length=self.segment2_length,
|
||||||
|
height=self.segment2_width,
|
||||||
|
width=self.segment2_height,
|
||||||
|
centered=(False, True, True),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
back = (
|
||||||
|
Cq.Workplane('XZ')
|
||||||
|
.cylinder(
|
||||||
|
radius=self.back_width / 2,
|
||||||
|
height=self.back_height,
|
||||||
|
centered=True,
|
||||||
|
)
|
||||||
|
.box(
|
||||||
|
length=self.back_hole_ext,
|
||||||
|
width=self.back_width,
|
||||||
|
height=self.back_height,
|
||||||
|
combine=True,
|
||||||
|
centered=(False, True, True)
|
||||||
|
)
|
||||||
|
.copyWorkplane(Cq.Workplane('XZ'))
|
||||||
|
.cylinder(
|
||||||
|
radius=self.back_hole_diam / 2,
|
||||||
|
height=self.back_height,
|
||||||
|
centered=True,
|
||||||
|
combine='cut',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
back.faces(">X").tag("dir")
|
||||||
|
back.copyWorkplane(Cq.Workplane('XZ')).tagPlane('conn')
|
||||||
|
result = (
|
||||||
|
Cq.Assembly()
|
||||||
|
.add(front, name="front",
|
||||||
|
loc=Cq.Location((-self.front_hole_ext, 0, 0)))
|
||||||
|
.add(segment1, name="segment1",
|
||||||
|
loc=Cq.Location((stroke_x, 0, 0)))
|
||||||
|
.add(segment2, name="segment2",
|
||||||
|
loc=Cq.Location((stroke_x + self.segment1_length, 0, 0)))
|
||||||
|
.add(back, name="back",
|
||||||
|
loc=Cq.Location((stroke_x + self.segment1_length + self.segment2_length + self.back_hole_ext, 0, 0), (0, 1, 0), 180))
|
||||||
|
)
|
||||||
|
if shaft:
|
||||||
|
result.add(shaft, name="shaft")
|
||||||
|
return result
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class MountingBracket(Item):
|
||||||
|
"""
|
||||||
|
Mounting bracket for a linear actuator
|
||||||
|
"""
|
||||||
|
mass: float = 1.6
|
||||||
|
hole_diam: float = 4.0
|
||||||
|
width: float = 8.0
|
||||||
|
height: float = 12.20
|
||||||
|
thickness: float = 0.98
|
||||||
|
length: float = 13.00
|
||||||
|
hole_to_side_ext: float = 8.25
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
assert self.hole_to_side_ext - self.hole_diam / 2 > 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return f"MountingBracket M{int(self.hole_diam)}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def role(self) -> Role:
|
||||||
|
return Role.MOTION
|
||||||
|
|
||||||
|
def generate(self) -> Cq.Workplane:
|
||||||
|
result = (
|
||||||
|
Cq.Workplane('XY')
|
||||||
|
.box(
|
||||||
|
length=self.hole_to_side_ext,
|
||||||
|
width=self.width,
|
||||||
|
height=self.height,
|
||||||
|
centered=(False, True, True)
|
||||||
|
)
|
||||||
|
.copyWorkplane(Cq.Workplane('XY'))
|
||||||
|
.cylinder(
|
||||||
|
height=self.height,
|
||||||
|
radius=self.width / 2,
|
||||||
|
combine=True,
|
||||||
|
)
|
||||||
|
.copyWorkplane(Cq.Workplane('XY'))
|
||||||
|
.box(
|
||||||
|
length=2 * (self.hole_to_side_ext - self.thickness),
|
||||||
|
width=self.width,
|
||||||
|
height=self.height - self.thickness * 2,
|
||||||
|
combine='cut',
|
||||||
|
)
|
||||||
|
.copyWorkplane(Cq.Workplane('XY'))
|
||||||
|
.cylinder(
|
||||||
|
height=self.height,
|
||||||
|
radius=self.hole_diam / 2,
|
||||||
|
combine='cut'
|
||||||
|
)
|
||||||
|
.copyWorkplane(Cq.Workplane('YZ'))
|
||||||
|
.cylinder(
|
||||||
|
height=self.hole_to_side_ext * 2,
|
||||||
|
radius=self.hole_diam / 2,
|
||||||
|
combine='cut'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
result.copyWorkplane(Cq.Workplane('YZ', origin=(self.hole_to_side_ext, 0, 0))).tagPlane("conn_side")
|
||||||
|
result.copyWorkplane(Cq.Workplane('XY', origin=(0, 0, self.height/2))).tagPlane("conn_top")
|
||||||
|
result.copyWorkplane(Cq.Workplane('YX', origin=(0, 0, -self.height/2))).tagPlane("conn_bot")
|
||||||
|
result.copyWorkplane(Cq.Workplane('XY')).tagPlane("conn_mid")
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
LINEAR_ACTUATOR_50 = LinearActuator(
|
||||||
|
mass=40.8,
|
||||||
|
stroke_length=50,
|
||||||
|
shaft_diam=9.05,
|
||||||
|
front_hole_ext=4.32,
|
||||||
|
back_hole_ext=4.54,
|
||||||
|
segment1_length=57.35,
|
||||||
|
segment1_width=15.97,
|
||||||
|
segment1_height=11.95,
|
||||||
|
segment2_length=37.69,
|
||||||
|
segment2_width=19.97,
|
||||||
|
segment2_height=14.96,
|
||||||
|
|
||||||
|
front_length=9.40,
|
||||||
|
front_width=9.17,
|
||||||
|
front_height=6.12,
|
||||||
|
back_length=9.18,
|
||||||
|
back_width=10.07,
|
||||||
|
back_height=8.06,
|
||||||
|
)
|
||||||
|
LINEAR_ACTUATOR_30 = LinearActuator(
|
||||||
|
mass=34.0,
|
||||||
|
stroke_length=30,
|
||||||
|
)
|
||||||
|
LINEAR_ACTUATOR_21 = LinearActuator(
|
||||||
|
# FIXME: Measure
|
||||||
|
mass=0.0,
|
||||||
|
stroke_length=21,
|
||||||
|
front_hole_ext=4,
|
||||||
|
back_hole_ext=4,
|
||||||
|
segment1_length=34,
|
||||||
|
segment2_length=34,
|
||||||
|
)
|
||||||
|
LINEAR_ACTUATOR_10 = LinearActuator(
|
||||||
|
mass=41.3,
|
||||||
|
stroke_length=10,
|
||||||
|
front_hole_ext=4.02,
|
||||||
|
back_hole_ext=4.67,
|
||||||
|
segment1_length=13.29,
|
||||||
|
segment1_width=15.88,
|
||||||
|
segment1_height=12.07,
|
||||||
|
segment2_length=42.52,
|
||||||
|
segment2_width=20.98,
|
||||||
|
segment2_height=14.84,
|
||||||
|
)
|
||||||
|
LINEAR_ACTUATOR_HEX_NUT = HexNut(
|
||||||
|
mass=0.8,
|
||||||
|
diam_thread=4,
|
||||||
|
pitch=0.7,
|
||||||
|
thickness=4.16,
|
||||||
|
width=6.79,
|
||||||
|
)
|
||||||
|
LINEAR_ACTUATOR_BOLT = FlatHeadBolt(
|
||||||
|
mass=1.7,
|
||||||
|
diam_head=6.68,
|
||||||
|
height_head=2.98,
|
||||||
|
diam_thread=4.0,
|
||||||
|
height_thread=15.83,
|
||||||
|
)
|
||||||
|
LINEAR_ACTUATOR_BRACKET = MountingBracket()
|
||||||
|
|
||||||
|
BATTERY_BOX = BatteryBox18650()
|
||||||
|
|
||||||
|
# Acrylic hex nut
|
||||||
|
ELECTRONIC_MOUNT_HEXNUT = HexNut(
|
||||||
|
mass=0.8,
|
||||||
|
diam_thread=4,
|
||||||
|
pitch=0.7,
|
||||||
|
thickness=3.57,
|
||||||
|
width=6.81,
|
||||||
|
)
|
||||||
|
|
||||||
|
@dataclass(kw_only=True, frozen=True)
|
||||||
|
class Winch:
|
||||||
|
linear_motion_span: float
|
||||||
|
|
||||||
|
actuator: LinearActuator = LINEAR_ACTUATOR_21
|
||||||
|
nut: HexNut = LINEAR_ACTUATOR_HEX_NUT
|
||||||
|
bolt: FlatHeadBolt = LINEAR_ACTUATOR_BOLT
|
||||||
|
bracket: MountingBracket = LINEAR_ACTUATOR_BRACKET
|
||||||
|
|
||||||
|
@dataclass(kw_only=True)
|
||||||
|
class Flexor:
|
||||||
|
"""
|
||||||
|
Actuator assembly which flexes, similar to biceps
|
||||||
|
"""
|
||||||
|
motion_span: float
|
||||||
|
arm_radius: Optional[float] = None
|
||||||
|
pos_smaller: bool = True
|
||||||
|
|
||||||
|
actuator: LinearActuator = LINEAR_ACTUATOR_50
|
||||||
|
nut: HexNut = LINEAR_ACTUATOR_HEX_NUT
|
||||||
|
bolt: FlatHeadBolt = LINEAR_ACTUATOR_BOLT
|
||||||
|
bracket: MountingBracket = LINEAR_ACTUATOR_BRACKET
|
||||||
|
# Length of line attached to the flexor
|
||||||
|
line_length: float = 0.0
|
||||||
|
line_thickness: float = 0.5
|
||||||
|
# By how much is the line permitted to slack. This reduces the effective stroke length
|
||||||
|
line_slack: float = 0.0
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
assert self.line_slack <= self.line_length, f"Insufficient length: {self.line_slack} >= {self.line_length}"
|
||||||
|
assert self.line_slack < self.actuator.stroke_length
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mount_height(self):
|
||||||
|
return self.bracket.hole_to_side_ext
|
||||||
|
|
||||||
|
@property
|
||||||
|
def d_open(self):
|
||||||
|
return self.actuator.conn_length + self.actuator.stroke_length + self.line_length - self.line_slack
|
||||||
|
@property
|
||||||
|
def d_closed(self):
|
||||||
|
return self.actuator.conn_length + self.line_length
|
||||||
|
|
||||||
|
def open_pos(self) -> Tuple[float, float, float]:
|
||||||
|
r, phi, r_ = nhf.geometry.contraction_span_pos_from_radius(
|
||||||
|
d_open=self.d_open,
|
||||||
|
d_closed=self.d_closed,
|
||||||
|
theta=math.radians(self.motion_span),
|
||||||
|
r=self.arm_radius,
|
||||||
|
smaller=self.pos_smaller,
|
||||||
|
)
|
||||||
|
return r, math.degrees(phi), r_
|
||||||
|
|
||||||
|
def target_length_at_angle(
|
||||||
|
self,
|
||||||
|
angle: float = 0.0
|
||||||
|
) -> float:
|
||||||
|
"""
|
||||||
|
Length of the actuator at some angle
|
||||||
|
"""
|
||||||
|
assert 0 <= angle <= self.motion_span
|
||||||
|
r, phi, rp = self.open_pos()
|
||||||
|
th = math.radians(phi - angle)
|
||||||
|
|
||||||
|
result = math.sqrt(r * r + rp * rp - 2 * r * rp * math.cos(th))
|
||||||
|
#result = math.sqrt((r * math.cos(th) - rp) ** 2 + (r * math.sin(th)) ** 2)
|
||||||
|
assert self.d_closed -1e-6 <= result <= self.d_open + 1e-6,\
|
||||||
|
f"Illegal length: {result} not in [{self.d_closed}, {self.d_open}]"
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def add_to(
|
||||||
|
self,
|
||||||
|
a: Cq.Assembly,
|
||||||
|
target_length: float,
|
||||||
|
tag_prefix: Optional[str] = None,
|
||||||
|
tag_hole_front: Optional[str] = None,
|
||||||
|
tag_hole_back: Optional[str] = None,
|
||||||
|
tag_dir: Optional[str] = None):
|
||||||
|
"""
|
||||||
|
Adds the necessary mechanical components to this assembly. Does not
|
||||||
|
invoke `a.solve()`.
|
||||||
|
"""
|
||||||
|
draft = max(0, target_length - self.d_closed - self.line_length)
|
||||||
|
pos = draft / self.actuator.stroke_length
|
||||||
|
line_l = target_length - draft - self.actuator.conn_length
|
||||||
|
if tag_prefix:
|
||||||
|
tag_prefix = tag_prefix + "_"
|
||||||
|
else:
|
||||||
|
tag_prefix = ""
|
||||||
|
name_actuator = f"{tag_prefix}actuator"
|
||||||
|
name_bracket_front = f"{tag_prefix}bracket_front"
|
||||||
|
name_bracket_back = f"{tag_prefix}bracket_back"
|
||||||
|
name_bolt_front = f"{tag_prefix}front_bolt"
|
||||||
|
name_bolt_back = f"{tag_prefix}back_bolt"
|
||||||
|
name_nut_front = f"{tag_prefix}front_nut"
|
||||||
|
name_nut_back = f"{tag_prefix}back_nut"
|
||||||
|
(
|
||||||
|
a
|
||||||
|
.add(self.actuator.assembly(pos=pos), name=name_actuator)
|
||||||
|
.add(self.bracket.assembly(), name=name_bracket_front)
|
||||||
|
.add(self.bolt.assembly(), name=name_bolt_front)
|
||||||
|
.add(self.nut.assembly(), name=name_nut_front)
|
||||||
|
.constrain(f"{name_bolt_front}?root", f"{name_bracket_front}?conn_top",
|
||||||
|
"Plane", param=0)
|
||||||
|
.constrain(f"{name_nut_front}?bot", f"{name_bracket_front}?conn_bot",
|
||||||
|
"Plane")
|
||||||
|
.add(self.bracket.assembly(), name=name_bracket_back)
|
||||||
|
.add(self.bolt.assembly(), name=name_bolt_back)
|
||||||
|
.add(self.nut.assembly(), name=name_nut_back)
|
||||||
|
.constrain(f"{name_actuator}/back?conn", f"{name_bracket_back}?conn_mid",
|
||||||
|
"Plane", param=0)
|
||||||
|
.constrain(f"{name_bolt_back}?root", f"{name_bracket_back}?conn_top",
|
||||||
|
"Plane", param=0)
|
||||||
|
.constrain(f"{name_nut_back}?bot", f"{name_bracket_back}?conn_bot",
|
||||||
|
"Plane")
|
||||||
|
)
|
||||||
|
if self.line_length == 0.0:
|
||||||
|
a.constrain(
|
||||||
|
f"{name_actuator}/front?conn",
|
||||||
|
f"{name_bracket_front}?conn_mid",
|
||||||
|
"Plane", param=0)
|
||||||
|
else:
|
||||||
|
(
|
||||||
|
a
|
||||||
|
.addS(tension_fibre(
|
||||||
|
length=line_l,
|
||||||
|
hole_diam=self.nut.diam_thread,
|
||||||
|
thickness=self.line_thickness,
|
||||||
|
), name="fibre", role=Role.CONNECTION)
|
||||||
|
.constrain(
|
||||||
|
f"{name_actuator}/front?conn",
|
||||||
|
"fibre?male",
|
||||||
|
"Plane"
|
||||||
|
)
|
||||||
|
.constrain(
|
||||||
|
f"{name_bracket_front}?conn_mid",
|
||||||
|
"fibre?female",
|
||||||
|
"Plane"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if tag_hole_front:
|
||||||
|
a.constrain(tag_hole_front, f"{name_bracket_front}?conn_side", "Plane")
|
||||||
|
if tag_hole_back:
|
||||||
|
a.constrain(tag_hole_back, f"{name_bracket_back}?conn_side", "Plane")
|
||||||
|
if tag_dir:
|
||||||
|
a.constrain(tag_dir, f"{name_bracket_front}?conn_mid", "Axis", param=0)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ElectronicBoard(Model):
|
||||||
|
|
||||||
|
name: str = "electronic-board"
|
||||||
|
nut: HexNut = NUT_COMMON
|
||||||
|
bolt: FlatHeadBolt = BOLT_COMMON
|
||||||
|
length: float = 70.0
|
||||||
|
width: float = 170.0
|
||||||
|
mount_holes: list[Hole] = field(default_factory=lambda: [
|
||||||
|
Hole(x=25, y=75),
|
||||||
|
Hole(x=25, y=-75),
|
||||||
|
Hole(x=-25, y=75),
|
||||||
|
Hole(x=-25, y=-75),
|
||||||
|
])
|
||||||
|
panel_thickness: float = 25.4 / 16
|
||||||
|
mount_panel_thickness: float = 25.4 / 4
|
||||||
|
material: Material = Material.WOOD_BIRCH
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mount_hole_diam(self) -> float:
|
||||||
|
return self.bolt.diam_thread
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
super().__init__(name=self.name)
|
||||||
|
|
||||||
|
def panel(self) -> MountingBox:
|
||||||
|
return MountingBox(
|
||||||
|
holes=self.mount_holes,
|
||||||
|
hole_diam=self.mount_hole_diam,
|
||||||
|
length=self.length,
|
||||||
|
width=self.width,
|
||||||
|
centred=(True, True),
|
||||||
|
thickness=self.panel_thickness,
|
||||||
|
generate_reverse_tags=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def assembly(self) -> Cq.Assembly:
|
||||||
|
panel = self.panel()
|
||||||
|
result = (
|
||||||
|
Cq.Assembly()
|
||||||
|
.addS(panel.generate(), name="panel",
|
||||||
|
role=Role.ELECTRONIC | Role.STRUCTURE, material=self.material)
|
||||||
|
)
|
||||||
|
for hole in self.mount_holes:
|
||||||
|
bolt_name = f"{hole.tag}_bolt"
|
||||||
|
(
|
||||||
|
result
|
||||||
|
.add(self.bolt.assembly(), name=bolt_name)
|
||||||
|
.constrain(
|
||||||
|
f"{bolt_name}?root",
|
||||||
|
f"panel?{hole.tag}",
|
||||||
|
"Plane", param=0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return result.solve()
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ElectronicBoardBattery(ElectronicBoard):
|
||||||
|
name: str = "electronic-board-battery"
|
||||||
|
battery_box: BatteryBox18650 = BATTERY_BOX
|
||||||
|
|
||||||
|
@submodel(name="panel")
|
||||||
|
def panel_out(self) -> MountingBox:
|
||||||
|
return self.panel()
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ElectronicBoardControl(ElectronicBoard):
|
||||||
|
name: str = "electronic-board-control"
|
||||||
|
|
||||||
|
controller_datum: Cq.Location = Cq.Location.from2d(-25, 23, -90)
|
||||||
|
|
||||||
|
controller: ArduinoUnoR3 = ArduinoUnoR3()
|
||||||
|
|
||||||
|
def panel(self) -> MountingBox:
|
||||||
|
box = super().panel()
|
||||||
|
def transform(i, x, y):
|
||||||
|
pos = self.controller_datum * Cq.Location.from2d(x, self.controller.width - y)
|
||||||
|
x, y = pos.to2d_pos()
|
||||||
|
return Hole(
|
||||||
|
x=x, y=y,
|
||||||
|
diam=self.controller.hole_diam,
|
||||||
|
tag=f"controller_conn{i}",
|
||||||
|
)
|
||||||
|
box.holes = box.holes.copy() + [
|
||||||
|
transform(i, x, y)
|
||||||
|
for i, (x, y) in enumerate(self.controller.holes)
|
||||||
|
]
|
||||||
|
return box
|
||||||
|
|
||||||
|
@submodel(name="panel")
|
||||||
|
def panel_out(self) -> MountingBox:
|
||||||
|
return self.panel()
|
||||||
|
|
||||||
|
def assembly(self) -> Cq.Assembly:
|
||||||
|
result = super().assembly()
|
||||||
|
result.add(self.controller.assembly(), name="controller")
|
||||||
|
for i in range(len(self.controller.holes)):
|
||||||
|
result.constrain(f"controller?conn{i}", f"panel?controller_conn{i}", "Plane")
|
||||||
|
return result.solve()
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class LightStrip:
|
||||||
|
|
||||||
|
width: float = 10.0
|
||||||
|
height: float = 4.5
|
|
@ -0,0 +1,204 @@
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
import cadquery as Cq
|
||||||
|
from nhf.parts.joints import HirthJoint
|
||||||
|
from nhf import Material, Role
|
||||||
|
from nhf.build import Model, TargetKind, target, assembly, submodel
|
||||||
|
from nhf.touhou.houjuu_nue.joints import RootJoint
|
||||||
|
from nhf.parts.box import MountingBox
|
||||||
|
import nhf.utils
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class Mannequin:
|
||||||
|
"""
|
||||||
|
A mannequin for calibration
|
||||||
|
"""
|
||||||
|
|
||||||
|
shoulder_width: float = 400
|
||||||
|
shoulder_to_waist: float = 440
|
||||||
|
waist_width: float = 250
|
||||||
|
head_height: float = 220.0
|
||||||
|
neck_height: float = 105.0
|
||||||
|
neck_diam: float = 140
|
||||||
|
head_diam: float = 210
|
||||||
|
torso_thickness: float = 150
|
||||||
|
|
||||||
|
def generate(self) -> Cq.Workplane:
|
||||||
|
head_neck = (
|
||||||
|
Cq.Workplane("XY")
|
||||||
|
.cylinder(
|
||||||
|
radius=self.neck_diam/2,
|
||||||
|
height=self.neck_height,
|
||||||
|
centered=(True, True, False))
|
||||||
|
.faces(">Z")
|
||||||
|
.workplane()
|
||||||
|
.cylinder(
|
||||||
|
radius=self.head_diam/2,
|
||||||
|
height=self.head_height,
|
||||||
|
combine=True, centered=(True, True, False))
|
||||||
|
)
|
||||||
|
result = (
|
||||||
|
Cq.Workplane("XY")
|
||||||
|
.rect(self.waist_width, self.torso_thickness)
|
||||||
|
.workplane(offset=self.shoulder_to_waist)
|
||||||
|
.rect(self.shoulder_width, self.torso_thickness)
|
||||||
|
.loft(combine=True)
|
||||||
|
.union(head_neck.translate((0, 0, self.shoulder_to_waist)))
|
||||||
|
)
|
||||||
|
return result.translate((0, self.torso_thickness / 2, 0))
|
||||||
|
|
||||||
|
|
||||||
|
BASE_POS_X = 70.0
|
||||||
|
BASE_POS_Y = 100.0
|
||||||
|
|
||||||
|
@dataclass(kw_only=True)
|
||||||
|
class Harness(Model):
|
||||||
|
thickness: float = 25.4 / 8
|
||||||
|
width: float = 200.0
|
||||||
|
height: float = 304.8
|
||||||
|
fillet: float = 10.0
|
||||||
|
|
||||||
|
wing_base_pos: list[tuple[str, float, float]] = field(default_factory=lambda: [
|
||||||
|
("r1", BASE_POS_X, BASE_POS_Y),
|
||||||
|
("l1", -BASE_POS_X, BASE_POS_Y),
|
||||||
|
("r2", BASE_POS_X, 0),
|
||||||
|
("l2", -BASE_POS_X, 0),
|
||||||
|
("r3", BASE_POS_X, -BASE_POS_Y),
|
||||||
|
("l3", -BASE_POS_X, -BASE_POS_Y),
|
||||||
|
])
|
||||||
|
|
||||||
|
root_joint: RootJoint = field(default_factory=lambda: RootJoint())
|
||||||
|
|
||||||
|
mannequin: Mannequin = Mannequin()
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
super().__init__(name="harness")
|
||||||
|
|
||||||
|
@submodel(name="bridge-pair-horizontal")
|
||||||
|
def bridge_pair_horizontal(self) -> MountingBox:
|
||||||
|
return self.root_joint.bridge_pair_horizontal(centre_dx=BASE_POS_X * 2)
|
||||||
|
@submodel(name="bridge-pair-vertical")
|
||||||
|
def bridge_pair_vertical(self) -> MountingBox:
|
||||||
|
return self.root_joint.bridge_pair_vertical(centre_dy=BASE_POS_Y)
|
||||||
|
|
||||||
|
@target(name="profile", kind=TargetKind.DXF)
|
||||||
|
def profile(self) -> Cq.Sketch:
|
||||||
|
"""
|
||||||
|
Creates the harness shape
|
||||||
|
"""
|
||||||
|
w, h = self.width / 2, self.height / 2
|
||||||
|
sketch = (
|
||||||
|
Cq.Sketch()
|
||||||
|
.polygon([
|
||||||
|
(w, h),
|
||||||
|
(w, -h),
|
||||||
|
(-w, -h),
|
||||||
|
(-w, h),
|
||||||
|
#(0.7 * w, h),
|
||||||
|
#(w, 0),
|
||||||
|
#(0.7 * w, -h),
|
||||||
|
#(0.7 * -w, -h),
|
||||||
|
#(-w, 0),
|
||||||
|
#(0.7 * -w, h),
|
||||||
|
])
|
||||||
|
#.rect(self.harness_width, self.harness_height)
|
||||||
|
.vertices()
|
||||||
|
.fillet(self.fillet)
|
||||||
|
)
|
||||||
|
for tag, x, y in self.wing_base_pos:
|
||||||
|
conn = [(px + x, py + y) for px, py in self.root_joint.corner_pos()]
|
||||||
|
sketch = (
|
||||||
|
sketch
|
||||||
|
.push(conn)
|
||||||
|
.tag(tag)
|
||||||
|
.circle(self.root_joint.corner_hole_diam / 2, mode='s')
|
||||||
|
.reset()
|
||||||
|
)
|
||||||
|
return sketch
|
||||||
|
|
||||||
|
def surface(self) -> Cq.Workplane:
|
||||||
|
"""
|
||||||
|
Creates the harness shape
|
||||||
|
"""
|
||||||
|
result = (
|
||||||
|
Cq.Workplane('XZ')
|
||||||
|
.placeSketch(self.profile())
|
||||||
|
.extrude(self.thickness)
|
||||||
|
)
|
||||||
|
result.faces(">Y").tag("mount")
|
||||||
|
plane = result.faces(">Y").workplane()
|
||||||
|
for tag, x, y in self.wing_base_pos:
|
||||||
|
conn = [(px + x, py + y) for px, py
|
||||||
|
in self.root_joint.corner_pos()]
|
||||||
|
for i, (px, py) in enumerate(conn):
|
||||||
|
plane.moveTo(px, py).tagPlane(f"{tag}_{i}")
|
||||||
|
return result
|
||||||
|
|
||||||
|
def add_root_joint_constraint(
|
||||||
|
self,
|
||||||
|
a: Cq.Assembly,
|
||||||
|
harness_tag: str,
|
||||||
|
joint_tag: str,
|
||||||
|
mount_tag: str):
|
||||||
|
for i in range(4):
|
||||||
|
a.constrain(f"{harness_tag}?{mount_tag}_{i}", f"{joint_tag}/parent?h{i}", "Point")
|
||||||
|
|
||||||
|
|
||||||
|
@assembly()
|
||||||
|
def assembly(self, with_root_joint: bool = False) -> Cq.Assembly:
|
||||||
|
harness = self.surface()
|
||||||
|
mannequin_z = self.mannequin.shoulder_to_waist * 0.6
|
||||||
|
|
||||||
|
result = (
|
||||||
|
Cq.Assembly()
|
||||||
|
.addS(
|
||||||
|
harness, name="base",
|
||||||
|
material=Material.WOOD_BIRCH,
|
||||||
|
role=Role.STRUCTURE)
|
||||||
|
.constrain("base", "Fixed")
|
||||||
|
.addS(
|
||||||
|
self.mannequin.generate(),
|
||||||
|
name="mannequin",
|
||||||
|
role=Role.FIXTURE,
|
||||||
|
loc=Cq.Location((0, -self.thickness, -mannequin_z), (0, 0, 1), 180))
|
||||||
|
.constrain("mannequin", "Fixed")
|
||||||
|
)
|
||||||
|
bridge_h = self.bridge_pair_horizontal().generate()
|
||||||
|
for i in [1,2,3]:
|
||||||
|
name = f"r{i}l{i}_bridge"
|
||||||
|
(
|
||||||
|
result
|
||||||
|
.addS(
|
||||||
|
bridge_h, name=name,
|
||||||
|
role=Role.FIXTURE,
|
||||||
|
material=Material.WOOD_BIRCH,
|
||||||
|
)
|
||||||
|
.constrain(f"{name}?conn0_rev", f"base?r{i}_1", "Point")
|
||||||
|
.constrain(f"{name}?conn1_rev", f"base?l{i}_0", "Point")
|
||||||
|
.constrain(f"{name}?conn2_rev", f"base?l{i}_3", "Point")
|
||||||
|
.constrain(f"{name}?conn3_rev", f"base?r{i}_2", "Point")
|
||||||
|
)
|
||||||
|
bridge_v = self.bridge_pair_vertical().generate()
|
||||||
|
(
|
||||||
|
result
|
||||||
|
.addS(bridge_v, name="r1_bridge", role=Role.FIXTURE, material=Material.WOOD_BIRCH)
|
||||||
|
.constrain("r1_bridge?conn0_rev", "base?r1_3", 'Plane')
|
||||||
|
.constrain("r1_bridge?conn1_rev", "base?r2_0", 'Plane')
|
||||||
|
.addS(bridge_v, name="r2_bridge", role=Role.FIXTURE, material=Material.WOOD_BIRCH)
|
||||||
|
.constrain("r2_bridge?conn0_rev", "base?r2_3", 'Plane')
|
||||||
|
.constrain("r2_bridge?conn1_rev", "base?r3_0", 'Plane')
|
||||||
|
.addS(bridge_v, name="l1_bridge", role=Role.FIXTURE, material=Material.WOOD_BIRCH)
|
||||||
|
.constrain("l1_bridge?conn0_rev", "base?l1_2", 'Plane')
|
||||||
|
.constrain("l1_bridge?conn1_rev", "base?l2_1", 'Plane')
|
||||||
|
.addS(bridge_v, name="l2_bridge", role=Role.FIXTURE, material=Material.WOOD_BIRCH)
|
||||||
|
.constrain("l2_bridge?conn0_rev", "base?l2_2", 'Plane')
|
||||||
|
.constrain("l2_bridge?conn1_rev", "base?l3_1", 'Plane')
|
||||||
|
)
|
||||||
|
if with_root_joint:
|
||||||
|
for name in ["l1", "l2", "l3", "r1", "r2", "r3"]:
|
||||||
|
result.addS(
|
||||||
|
self.root_joint.assembly(), name=name,
|
||||||
|
role=Role.PARENT,
|
||||||
|
material=Material.PLASTIC_PLA)
|
||||||
|
self.add_root_joint_constraint(result, "base", name, name)
|
||||||
|
result.solve()
|
||||||
|
return result
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,130 @@
|
||||||
|
import unittest
|
||||||
|
import cadquery as Cq
|
||||||
|
import nhf.touhou.houjuu_nue as M
|
||||||
|
import nhf.touhou.houjuu_nue.joints as MJ
|
||||||
|
import nhf.touhou.houjuu_nue.electronics as ME
|
||||||
|
from nhf.checks import pairwise_intersection
|
||||||
|
|
||||||
|
class TestElectronics(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_actuator_length(self):
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
ME.LINEAR_ACTUATOR_50.conn_length, 103.9
|
||||||
|
)
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
ME.LINEAR_ACTUATOR_30.conn_length, 83.9
|
||||||
|
)
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
ME.LINEAR_ACTUATOR_10.conn_length, 64.5
|
||||||
|
)
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
ME.LINEAR_ACTUATOR_21.conn_length, 76.0
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_flexor(self):
|
||||||
|
flexor = ME.Flexor(
|
||||||
|
motion_span=60,
|
||||||
|
)
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
flexor.target_length_at_angle(0),
|
||||||
|
flexor.actuator.stroke_length + flexor.actuator.conn_length)
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
flexor.target_length_at_angle(flexor.motion_span),
|
||||||
|
flexor.actuator.conn_length)
|
||||||
|
|
||||||
|
|
||||||
|
class TestJoints(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_shoulder_collision_of_torsion_joint(self):
|
||||||
|
j = MJ.ShoulderJoint()
|
||||||
|
assembly = j.torsion_joint.rider_track_assembly()
|
||||||
|
self.assertEqual(pairwise_intersection(assembly), [])
|
||||||
|
|
||||||
|
def test_shoulder_collision_0(self):
|
||||||
|
j = MJ.ShoulderJoint()
|
||||||
|
assembly = j.assembly()
|
||||||
|
self.assertEqual(pairwise_intersection(assembly), [])
|
||||||
|
|
||||||
|
def test_shoulder_align(self):
|
||||||
|
j = MJ.ShoulderJoint()
|
||||||
|
a = j.assembly()
|
||||||
|
l_t_c0 = a.get_abs_location("parent_top/lip?conn0")
|
||||||
|
l_b_c0 = a.get_abs_location("parent_bot/lip?conn0")
|
||||||
|
v = l_t_c0 - l_b_c0
|
||||||
|
self.assertAlmostEqual(v.x, 0)
|
||||||
|
self.assertAlmostEqual(v.y, 0)
|
||||||
|
|
||||||
|
def test_shoulder_joint_dist(self):
|
||||||
|
"""
|
||||||
|
Tests the arm radius
|
||||||
|
"""
|
||||||
|
j = MJ.ShoulderJoint()
|
||||||
|
for deflection in [0, 40, j.angle_max_deflection]:
|
||||||
|
with self.subTest(deflection=deflection):
|
||||||
|
a = j.assembly(deflection=deflection)
|
||||||
|
# Axle
|
||||||
|
o = a.get_abs_location("parent_top/track?spring")
|
||||||
|
l_c1 = a.get_abs_location("parent_top/lip?conn0")
|
||||||
|
l_c2= a.get_abs_location("parent_top/lip?conn1")
|
||||||
|
v_c = 0.5 * ((l_c1 - o) + (l_c2 - o))
|
||||||
|
v_c.z = 0
|
||||||
|
self.assertAlmostEqual(v_c.Length, j.parent_lip_ext)
|
||||||
|
|
||||||
|
def test_disk_collision_0(self):
|
||||||
|
j = MJ.DiskJoint()
|
||||||
|
assembly = j.assembly(angle=0)
|
||||||
|
self.assertEqual(pairwise_intersection(assembly), [])
|
||||||
|
def test_disk_collision_mid(self):
|
||||||
|
j = MJ.DiskJoint()
|
||||||
|
assembly = j.assembly(angle=j.movement_angle / 2)
|
||||||
|
self.assertEqual(pairwise_intersection(assembly), [])
|
||||||
|
def test_disk_collision_max(self):
|
||||||
|
j = MJ.DiskJoint()
|
||||||
|
assembly = j.assembly(angle=j.movement_angle)
|
||||||
|
self.assertEqual(pairwise_intersection(assembly), [])
|
||||||
|
|
||||||
|
def test_elbow_joint_dist(self):
|
||||||
|
"""
|
||||||
|
Tests the arm radius
|
||||||
|
"""
|
||||||
|
j = MJ.ElbowJoint()
|
||||||
|
for angle in [0, 10, 20, j.disk_joint.movement_angle]:
|
||||||
|
with self.subTest(angle=angle):
|
||||||
|
a = j.assembly(angle=angle)
|
||||||
|
o = a.get_abs_location("child/disk?mate_bot")
|
||||||
|
l_c1 = a.get_abs_location("child/lip?conn_top0")
|
||||||
|
l_c2 = a.get_abs_location("child/lip?conn_bot0")
|
||||||
|
v_c = 0.5 * ((l_c1 - o) + (l_c2 - o))
|
||||||
|
v_c.z = 0
|
||||||
|
self.assertAlmostEqual(v_c.Length, j.child_arm_radius)
|
||||||
|
|
||||||
|
l_p1 = a.get_abs_location("parent_upper/lip?conn_top0")
|
||||||
|
l_p2 = a.get_abs_location("parent_upper/lip?conn_bot0")
|
||||||
|
v_p = 0.5 * ((l_p1 - o) + (l_p2 - o))
|
||||||
|
v_p.z = 0
|
||||||
|
self.assertAlmostEqual(v_p.Length, j.parent_arm_radius)
|
||||||
|
|
||||||
|
|
||||||
|
class Test(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_hs_joint_parent(self):
|
||||||
|
p = M.Parameters()
|
||||||
|
obj = p.harness.hs_joint_parent()
|
||||||
|
self.assertIsInstance(obj.val().solids(), Cq.Solid, msg="H-S joint must be in one piece")
|
||||||
|
|
||||||
|
def test_wings_assembly(self):
|
||||||
|
p = M.Parameters()
|
||||||
|
p.wings_harness_assembly()
|
||||||
|
def test_trident_assembly(self):
|
||||||
|
p = M.Parameters()
|
||||||
|
assembly = p.trident.assembly()
|
||||||
|
bbox = assembly.toCompound().BoundingBox()
|
||||||
|
length = bbox.zlen
|
||||||
|
self.assertGreater(length, 1300)
|
||||||
|
self.assertLess(length, 1700)
|
||||||
|
#def test_assemblies(self):
|
||||||
|
# p = M.Parameters()
|
||||||
|
# p.check_all()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
|
@ -0,0 +1,88 @@
|
||||||
|
import math
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
import cadquery as Cq
|
||||||
|
from nhf import Material, Role
|
||||||
|
from nhf.parts.handle import Handle, BayonetMount
|
||||||
|
from nhf.build import Model, target, assembly
|
||||||
|
import nhf.utils
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Trident(Model):
|
||||||
|
handle: Handle = field(default_factory=lambda: Handle(
|
||||||
|
diam=38,
|
||||||
|
diam_inner=38-2 * 25.4/8,
|
||||||
|
diam_connector_internal=18,
|
||||||
|
simplify_geometry=False,
|
||||||
|
mount=BayonetMount(n_pin=3),
|
||||||
|
))
|
||||||
|
terminal_height: float = 80
|
||||||
|
terminal_hole_diam: float = 24
|
||||||
|
terminal_bottom_thickness: float = 10
|
||||||
|
segment_length: float = 24 * 25.4
|
||||||
|
|
||||||
|
@target(name="handle-connector")
|
||||||
|
def handle_connector(self):
|
||||||
|
return self.handle.connector()
|
||||||
|
@target(name="handle-insertion")
|
||||||
|
def handle_insertion(self):
|
||||||
|
return self.handle.insertion()
|
||||||
|
@target(name="proto-handle-terminal-connector", prototype=True)
|
||||||
|
def proto_handle_connector(self):
|
||||||
|
return self.handle.one_side_connector(height=15)
|
||||||
|
|
||||||
|
@target(name="handle-terminal-connector")
|
||||||
|
def handle_terminal_connector(self):
|
||||||
|
result = self.handle.one_side_connector(height=self.terminal_height)
|
||||||
|
#result.faces("<Z").circle(radius=25/2).cutThruAll()
|
||||||
|
h = self.terminal_height + self.handle.insertion_length - self.terminal_bottom_thickness
|
||||||
|
result = result.faces(">Z").hole(self.terminal_hole_diam, depth=h)
|
||||||
|
return result
|
||||||
|
|
||||||
|
@assembly()
|
||||||
|
def assembly(self):
|
||||||
|
def segment():
|
||||||
|
return self.handle.segment(self.segment_length)
|
||||||
|
|
||||||
|
terminal = (
|
||||||
|
self.handle
|
||||||
|
.one_side_connector(height=self.terminal_height)
|
||||||
|
.faces(">Z")
|
||||||
|
.hole(15, self.terminal_height + self.handle.insertion_length - 10)
|
||||||
|
)
|
||||||
|
mat_c = Material.PLASTIC_PLA
|
||||||
|
mat_i = Material.RESIN_TOUGH_1500
|
||||||
|
mat_s = Material.ACRYLIC_BLACK
|
||||||
|
role_i = Role.CONNECTION
|
||||||
|
role_c = Role.CONNECTION
|
||||||
|
role_s = Role.STRUCTURE
|
||||||
|
a = (
|
||||||
|
Cq.Assembly()
|
||||||
|
.addS(self.handle.insertion(), name="i0",
|
||||||
|
material=mat_i, role=role_i)
|
||||||
|
.constrain("i0", "Fixed")
|
||||||
|
.addS(segment(), name="s1",
|
||||||
|
material=mat_s, role=role_s)
|
||||||
|
.constrain("i0?rim", "s1?mate1", "Plane", param=0)
|
||||||
|
.addS(self.handle.insertion(), name="i1",
|
||||||
|
material=mat_i, role=role_i)
|
||||||
|
.addS(self.handle.connector(), name="c1",
|
||||||
|
material=mat_c, role=role_c)
|
||||||
|
.addS(self.handle.insertion(), name="i2",
|
||||||
|
material=mat_i, role=role_i)
|
||||||
|
.constrain("s1?mate2", "i1?rim", "Plane", param=0)
|
||||||
|
.constrain("i1?mate", "c1?mate1", "Plane")
|
||||||
|
.constrain("i2?mate", "c1?mate2", "Plane")
|
||||||
|
.addS(segment(), name="s2",
|
||||||
|
material=mat_s, role=role_s)
|
||||||
|
.constrain("i2?rim", "s2?mate1", "Plane", param=0)
|
||||||
|
.addS(self.handle.insertion(), name="i3",
|
||||||
|
material=mat_i, role=role_i)
|
||||||
|
.constrain("s2?mate2", "i3?rim", "Plane", param=0)
|
||||||
|
.addS(self.handle.one_side_connector(), name="head",
|
||||||
|
material=mat_c, role=role_c)
|
||||||
|
.constrain("i3?mate", "head?mate", "Plane")
|
||||||
|
.addS(terminal, name="terminal",
|
||||||
|
material=mat_c, role=role_c)
|
||||||
|
.constrain("i0?mate", "terminal?mate", "Plane")
|
||||||
|
)
|
||||||
|
return a.solve()
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue