cosplay: Touhou/Yasaka Kanako #11
|
@ -3,6 +3,9 @@
|
||||||
This is the design repository for NorCal Hakkero Factory No. 1, where we use
|
This is the design repository for NorCal Hakkero Factory No. 1, where we use
|
||||||
parametric CAD to make cosplay props.
|
parametric CAD to make cosplay props.
|
||||||
|
|
||||||
|
> NorCal Hakkero Factory № 1
|
||||||
|
> 北加国営八卦炉第一工場
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
Most cosplay schematics are created with Blender, CadQuery, and Inkscape. To
|
Most cosplay schematics are created with Blender, CadQuery, and Inkscape. To
|
||||||
|
|
|
@ -25,13 +25,16 @@ class Role(Flag):
|
||||||
PARENT = auto()
|
PARENT = auto()
|
||||||
CHILD = auto()
|
CHILD = auto()
|
||||||
CASING = auto()
|
CASING = auto()
|
||||||
|
STATOR = auto()
|
||||||
|
ROTOR = auto()
|
||||||
|
BEARING = auto()
|
||||||
# Springs, cushions
|
# Springs, cushions
|
||||||
DAMPING = auto()
|
DAMPING = auto()
|
||||||
# Main structural support
|
# Main structural support
|
||||||
STRUCTURE = auto()
|
STRUCTURE = auto()
|
||||||
DECORATION = auto()
|
DECORATION = auto()
|
||||||
ELECTRONIC = auto()
|
ELECTRONIC = auto()
|
||||||
MOTION = auto()
|
MOTOR = auto()
|
||||||
|
|
||||||
# Fasteners, etc.
|
# Fasteners, etc.
|
||||||
CONNECTION = auto()
|
CONNECTION = auto()
|
||||||
|
@ -59,11 +62,14 @@ ROLE_COLOR_MAP = {
|
||||||
Role.PARENT: _color('blue4', 0.6),
|
Role.PARENT: _color('blue4', 0.6),
|
||||||
Role.CASING: _color('dodgerblue3', 0.6),
|
Role.CASING: _color('dodgerblue3', 0.6),
|
||||||
Role.CHILD: _color('darkorange2', 0.6),
|
Role.CHILD: _color('darkorange2', 0.6),
|
||||||
|
Role.STATOR: _color('gray', 0.5),
|
||||||
|
Role.ROTOR: _color('blue3', 0.5),
|
||||||
|
Role.BEARING: _color('green3', 0.8),
|
||||||
Role.DAMPING: _color('springgreen', 1.0),
|
Role.DAMPING: _color('springgreen', 1.0),
|
||||||
Role.STRUCTURE: _color('gray', 0.4),
|
Role.STRUCTURE: _color('gray', 0.4),
|
||||||
Role.DECORATION: _color('lightseagreen', 0.4),
|
Role.DECORATION: _color('lightseagreen', 0.4),
|
||||||
Role.ELECTRONIC: _color('mediumorchid', 0.7),
|
Role.ELECTRONIC: _color('mediumorchid', 0.7),
|
||||||
Role.MOTION: _color('thistle3', 0.7),
|
Role.MOTOR: _color('thistle3', 0.7),
|
||||||
Role.CONNECTION: _color('steelblue3', 0.8),
|
Role.CONNECTION: _color('steelblue3', 0.8),
|
||||||
Role.HANDLE: _color('tomato4', 0.8),
|
Role.HANDLE: _color('tomato4', 0.8),
|
||||||
}
|
}
|
||||||
|
@ -84,6 +90,8 @@ class Material(Enum):
|
||||||
ACRYLIC_TRANSLUSCENT = 1.18, _color('ivory2', 0.8)
|
ACRYLIC_TRANSLUSCENT = 1.18, _color('ivory2', 0.8)
|
||||||
ACRYLIC_TRANSPARENT = 1.18, _color('ghostwhite', 0.5)
|
ACRYLIC_TRANSPARENT = 1.18, _color('ghostwhite', 0.5)
|
||||||
STEEL_SPRING = 7.8, _color('gray', 0.8)
|
STEEL_SPRING = 7.8, _color('gray', 0.8)
|
||||||
|
STEEL_STAINLESS = 7.8, _color('gray', 0.9)
|
||||||
|
METAL_AL = 2.7, _color('gray', 0.6)
|
||||||
METAL_BRASS = 8.5, _color('gold1', 0.8)
|
METAL_BRASS = 8.5, _color('gold1', 0.8)
|
||||||
|
|
||||||
def __init__(self, density: float, color: Cq.Color):
|
def __init__(self, density: float, color: Cq.Color):
|
||||||
|
|
|
@ -83,11 +83,16 @@ class BatteryBox18650(Item):
|
||||||
battery_dist: float = 20.18
|
battery_dist: float = 20.18
|
||||||
height: float = 19.66
|
height: float = 19.66
|
||||||
# space from bottom to battery begin
|
# space from bottom to battery begin
|
||||||
thickness: float = 1.66
|
thickness: float = 2.28
|
||||||
battery_diam: float = 18.48
|
battery_diam: float = 18.48
|
||||||
battery_height: float = 68.80
|
battery_height: float = 68.80
|
||||||
n_batteries: int = 3
|
n_batteries: int = 3
|
||||||
|
|
||||||
|
battery_gap: float = 2.0
|
||||||
|
|
||||||
|
diam_thread: float = 3.0
|
||||||
|
hole_dy: float = 39.50 / 2
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
assert 2 * self.thickness < min(self.length, self.height)
|
assert 2 * self.thickness < min(self.length, self.height)
|
||||||
|
|
||||||
|
@ -95,13 +100,20 @@ class BatteryBox18650(Item):
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
return f"BatteryBox 18650*{self.n_batteries}"
|
return f"BatteryBox 18650*{self.n_batteries}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def holes(self) -> list[Cq.Location]:
|
||||||
|
return [
|
||||||
|
Cq.Location.from2d(0, self.hole_dy),
|
||||||
|
Cq.Location.from2d(0, -self.hole_dy),
|
||||||
|
]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def role(self) -> Role:
|
def role(self) -> Role:
|
||||||
return Role.ELECTRONIC
|
return Role.ELECTRONIC
|
||||||
|
|
||||||
def generate(self) -> Cq.Workplane:
|
def generate(self) -> Cq.Workplane:
|
||||||
width = self.width_base + self.battery_dist * (self.n_batteries - 1) + self.battery_diam
|
width = self.width_base + self.battery_dist * (self.n_batteries - 1) + self.battery_diam
|
||||||
return (
|
result = (
|
||||||
Cq.Workplane('XY')
|
Cq.Workplane('XY')
|
||||||
.box(
|
.box(
|
||||||
length=self.length,
|
length=self.length,
|
||||||
|
@ -117,7 +129,7 @@ class BatteryBox18650(Item):
|
||||||
centered=(True, True, False),
|
centered=(True, True, False),
|
||||||
combine='cut',
|
combine='cut',
|
||||||
)
|
)
|
||||||
.copyWorkplane(Cq.Workplane('XY', origin=(-self.battery_height/2, 0, self.thickness + self.battery_diam/2)))
|
.copyWorkplane(Cq.Workplane('XY', origin=(-self.battery_height/2, 0, self.thickness + self.battery_diam/2 + self.battery_gap)))
|
||||||
.rarray(
|
.rarray(
|
||||||
xSpacing=1,
|
xSpacing=1,
|
||||||
ySpacing=self.battery_dist,
|
ySpacing=self.battery_dist,
|
||||||
|
@ -132,4 +144,16 @@ class BatteryBox18650(Item):
|
||||||
centered=(True, True, False),
|
centered=(True, True, False),
|
||||||
combine=True,
|
combine=True,
|
||||||
)
|
)
|
||||||
|
.copyWorkplane(Cq.Workplane('XY'))
|
||||||
)
|
)
|
||||||
|
hole = Cq.Solid.makeCylinder(
|
||||||
|
radius=self.diam_thread/2,
|
||||||
|
height=self.thickness,
|
||||||
|
)
|
||||||
|
result -= hole.moved(0, self.hole_dy)
|
||||||
|
result -= hole.moved(0, -self.hole_dy)
|
||||||
|
result.tagAbsolute("holeT0", (0, self.hole_dy, self.thickness), direction="+Z")
|
||||||
|
result.tagAbsolute("holeT1", (0, -self.hole_dy, self.thickness), direction="+Z")
|
||||||
|
result.tagAbsolute("holeB0", (0, self.hole_dy, 0), direction="-Z")
|
||||||
|
result.tagAbsolute("holeB1", (0, -self.hole_dy, 0), direction="-Z")
|
||||||
|
return result
|
||||||
|
|
|
@ -11,6 +11,7 @@ class FlatHeadBolt(Item):
|
||||||
height_head: float
|
height_head: float
|
||||||
diam_thread: float
|
diam_thread: float
|
||||||
height_thread: float
|
height_thread: float
|
||||||
|
pitch: float = 1.0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
|
@ -34,7 +35,7 @@ class FlatHeadBolt(Item):
|
||||||
centered=(True, True, False))
|
centered=(True, True, False))
|
||||||
)
|
)
|
||||||
rod.faces("<Z").tag("tip")
|
rod.faces("<Z").tag("tip")
|
||||||
rod.faces(">Z").tag("root")
|
rod.tagAbsolute("root", (0, 0, self.height_thread), direction="-Z")
|
||||||
rod = rod.union(head.located(Cq.Location((0, 0, self.height_thread))))
|
rod = rod.union(head.located(Cq.Location((0, 0, self.height_thread))))
|
||||||
return rod
|
return rod
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
import nhf.touhou.yasaka_kanako.mirror as MM
|
||||||
|
import nhf.touhou.yasaka_kanako.onbashira as MO
|
||||||
|
import nhf.touhou.yasaka_kanako.shimenawa as MS
|
||||||
|
from nhf.build import Model, TargetKind, target, assembly, submodel
|
||||||
|
import nhf.utils
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
import cadquery as Cq
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Parameters(Model):
|
||||||
|
|
||||||
|
mirror: MM.Mirror = field(default_factory=lambda: MM.Mirror())
|
||||||
|
onbashira: MO.Onbashira = field(default_factory=lambda: MO.Onbashira())
|
||||||
|
shimenawa: MS.Shimenawa = field(default_factory=lambda: MS.Shimenawa())
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
super().__init__(name="yasaka-kanako")
|
||||||
|
|
||||||
|
@submodel(name="mirror")
|
||||||
|
def submodel_mirror(self) -> Model:
|
||||||
|
return self.mirror
|
||||||
|
@submodel(name="onbashira")
|
||||||
|
def submodel_onbashira(self) -> Model:
|
||||||
|
return self.onbashira
|
||||||
|
@submodel(name="shimenawa")
|
||||||
|
def submodel_shimenawa(self) -> Model:
|
||||||
|
return self.shimenawa
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
|
||||||
|
p = Parameters()
|
||||||
|
if len(sys.argv) == 1:
|
||||||
|
p.build_all()
|
||||||
|
sys.exit(0)
|
|
@ -0,0 +1,208 @@
|
||||||
|
#define USE_MOTOR 1
|
||||||
|
#define USE_LED 1
|
||||||
|
#define USE_DISPLAY 1
|
||||||
|
|
||||||
|
#define pinButtonMode 9
|
||||||
|
#define pinDiag 6
|
||||||
|
|
||||||
|
// Main LED strip setup
|
||||||
|
#define pinLED 3
|
||||||
|
#define NUM_LEDS 20
|
||||||
|
#define LED_PART 10
|
||||||
|
#define BRIGHTNESS 250
|
||||||
|
#define LED_TYPE WS2811
|
||||||
|
|
||||||
|
// Relay controlled motor
|
||||||
|
#define pinMotor 7
|
||||||
|
|
||||||
|
#if USE_LED
|
||||||
|
#include <FastLED.h>
|
||||||
|
int cycles = 100;
|
||||||
|
int cycle_duration = 100;
|
||||||
|
CRGB leds[NUM_LEDS];
|
||||||
|
|
||||||
|
CRGB color_red;
|
||||||
|
CRGB color_blue;
|
||||||
|
CRGB color_green;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if USE_DISPLAY
|
||||||
|
#include <Wire.h>
|
||||||
|
|
||||||
|
#include <Adafruit_GFX.h>
|
||||||
|
#include <Adafruit_SSD1306.h>
|
||||||
|
|
||||||
|
#define SCREEN_WIDTH 128
|
||||||
|
#define SCREEN_HEIGHT 32
|
||||||
|
|
||||||
|
#define OLED_RESET -1
|
||||||
|
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Program state
|
||||||
|
|
||||||
|
bool stateButtonMode = false;
|
||||||
|
int programId = 0;
|
||||||
|
bool programChanged = true;
|
||||||
|
#define MAX_PROGRAMS 3
|
||||||
|
bool flag_motor = false;
|
||||||
|
bool flag_lighting = false;
|
||||||
|
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
pinMode(LED_BUILTIN, OUTPUT);
|
||||||
|
pinMode(pinButtonMode, INPUT);
|
||||||
|
#if USE_LED
|
||||||
|
// Calculate colours
|
||||||
|
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(pinLED, OUTPUT);
|
||||||
|
#endif
|
||||||
|
pinMode(pinDiag, OUTPUT);
|
||||||
|
#if USE_MOTOR
|
||||||
|
pinMode(pinMotor, OUTPUT);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if USE_LED
|
||||||
|
// Main LED strip
|
||||||
|
FastLED.addLeds<LED_TYPE, pinLED, RGB>(leds, NUM_LEDS);
|
||||||
|
fill_solid(leds, NUM_LEDS, CRGB::White);
|
||||||
|
delay(500);
|
||||||
|
FastLED.show();
|
||||||
|
#endif
|
||||||
|
#if USE_DISPLAY
|
||||||
|
Serial.begin(9600);
|
||||||
|
// SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
|
||||||
|
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Address 0x3D for 128x64
|
||||||
|
Serial.println(F("SSD1306 allocation failed"));
|
||||||
|
for(;;); // Don't proceed, loop forever
|
||||||
|
pinMode(pinLED, OUTPUT);
|
||||||
|
digitalWrite(pinLED, HIGH);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
digitalWrite(LED_BUILTIN, HIGH);
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
// Detect a rising edge
|
||||||
|
int buttonState = digitalRead(pinButtonMode);
|
||||||
|
if (buttonState && !stateButtonMode) {
|
||||||
|
programId = (programId + 1) % MAX_PROGRAMS;
|
||||||
|
programChanged = true;
|
||||||
|
stateButtonMode = true;
|
||||||
|
}
|
||||||
|
if (!buttonState) {
|
||||||
|
stateButtonMode = false;
|
||||||
|
}
|
||||||
|
switch (programId)
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
program_off();
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
program_still();
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
program_rotate();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (programChanged) {
|
||||||
|
update_screen();
|
||||||
|
|
||||||
|
digitalWrite(LED_BUILTIN, LOW);
|
||||||
|
}
|
||||||
|
programChanged = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility for updating LEDs
|
||||||
|
void fill_segmented(CRGB c1, CRGB c2)
|
||||||
|
{
|
||||||
|
#if USE_LED
|
||||||
|
//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();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
void set_motor(bool flag)
|
||||||
|
{
|
||||||
|
#if USE_MOTOR
|
||||||
|
if (flag) {
|
||||||
|
digitalWrite(pinMotor, HIGH);
|
||||||
|
flag_motor = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
digitalWrite(pinMotor, LOW);
|
||||||
|
flag_motor = false;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update current display status
|
||||||
|
void update_screen()
|
||||||
|
{
|
||||||
|
#if USE_DISPLAY
|
||||||
|
display.clearDisplay();
|
||||||
|
display.setTextSize(2);
|
||||||
|
display.setTextColor(SSD1306_WHITE);
|
||||||
|
display.setCursor(0,0);
|
||||||
|
display.println("Yasaka K.");
|
||||||
|
display.print("P");
|
||||||
|
display.print(programId);
|
||||||
|
display.print(" ");
|
||||||
|
if (flag_motor) {
|
||||||
|
display.print("M");
|
||||||
|
}
|
||||||
|
if (flag_lighting) {
|
||||||
|
display.print("L");
|
||||||
|
}
|
||||||
|
display.display();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void program_off()
|
||||||
|
{
|
||||||
|
if (programChanged)
|
||||||
|
{
|
||||||
|
set_motor(false);
|
||||||
|
#if USE_LED
|
||||||
|
flag_lighting = false;
|
||||||
|
fill_solid(leds, NUM_LEDS, CRGB::Black);
|
||||||
|
FastLED.show();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
delay(cycle_duration);
|
||||||
|
}
|
||||||
|
void program_still()
|
||||||
|
{
|
||||||
|
if (programChanged)
|
||||||
|
{
|
||||||
|
set_motor(false);
|
||||||
|
#if USE_LED
|
||||||
|
flag_lighting = true;
|
||||||
|
fill_segmented(CRGB::Green, CRGB::Orange);
|
||||||
|
FastLED.show();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
delay(cycle_duration);
|
||||||
|
}
|
||||||
|
void program_rotate()
|
||||||
|
{
|
||||||
|
if (programChanged)
|
||||||
|
{
|
||||||
|
set_motor(true);
|
||||||
|
}
|
||||||
|
#if USE_LED
|
||||||
|
flag_lighting = true;
|
||||||
|
fill_segmented(CRGB::Green, CRGB::Orange);
|
||||||
|
delay(cycle_duration/2);
|
||||||
|
fill_solid(leds, NUM_LEDS, CRGB::Black);
|
||||||
|
FastLED.show();
|
||||||
|
delay(cycle_duration/2);
|
||||||
|
#endif
|
||||||
|
}
|
|
@ -0,0 +1,186 @@
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
import cadquery as Cq
|
||||||
|
from nhf.build import Model, TargetKind, target, assembly, submodel
|
||||||
|
from nhf.materials import Role, Material
|
||||||
|
import nhf.touhou.yasaka_kanako.onbashira as MO
|
||||||
|
import nhf.utils
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Mirror(Model):
|
||||||
|
"""
|
||||||
|
Kanako's mirror, made of three levels.
|
||||||
|
|
||||||
|
The mirror suface is sandwiched between two layers of wood. As such, its
|
||||||
|
dimensions have to sit in between that of the aperature on the surface, and
|
||||||
|
the outer walls. The width/height here refers to the outer edge's width and height
|
||||||
|
"""
|
||||||
|
width: float = 100.0
|
||||||
|
height: float = 120.0
|
||||||
|
|
||||||
|
inner_gap: float = 3.0
|
||||||
|
outer_gap: float = 3.0
|
||||||
|
|
||||||
|
core_thickness: float = 25.4 / 8
|
||||||
|
casing_thickness: float = 25.4 / 8
|
||||||
|
|
||||||
|
flange_r0: float = 8.0
|
||||||
|
flange_r1: float = 20.0
|
||||||
|
flange_y1: float = 12.0
|
||||||
|
flange_y2: float = 25.0
|
||||||
|
flange_hole_r: float = 8.0
|
||||||
|
wing_x1: float = 15.0
|
||||||
|
wing_x2: float = 24.0
|
||||||
|
wing_r1: float = 10.0
|
||||||
|
wing_r2: float = 16.0
|
||||||
|
tail_r0: float = 8.0
|
||||||
|
tail_r1: float = 13.0
|
||||||
|
tail_y1: float = 16.0
|
||||||
|
tail_y2: float = 29.0
|
||||||
|
|
||||||
|
# Necklace hole
|
||||||
|
hole_diam: float = 5.0
|
||||||
|
|
||||||
|
material_mirror: Material = Material.ACRYLIC_TRANSPARENT
|
||||||
|
material_casing: Material = Material.WOOD_BIRCH
|
||||||
|
|
||||||
|
@target(name="core", kind=TargetKind.DXF)
|
||||||
|
def profile_core(self) -> Cq.Sketch:
|
||||||
|
rx = self.width/2 - self.outer_gap
|
||||||
|
ry = self.height/2 - self.outer_gap
|
||||||
|
return Cq.Sketch().ellipse(rx, ry)
|
||||||
|
|
||||||
|
def core(self) -> Cq.Workplane:
|
||||||
|
return (
|
||||||
|
Cq.Workplane()
|
||||||
|
.placeSketch(self.profile_core())
|
||||||
|
.extrude(self.core_thickness)
|
||||||
|
)
|
||||||
|
|
||||||
|
@target(name="casing-bot", kind=TargetKind.DXF)
|
||||||
|
def profile_casing_bot(self) -> Cq.Sketch:
|
||||||
|
"""
|
||||||
|
Base of the casing with no holes carved out
|
||||||
|
"""
|
||||||
|
yt = self.height / 2 - self.outer_gap
|
||||||
|
yh = (self.flange_y1 + self.flange_y2) / 2
|
||||||
|
flange = (
|
||||||
|
Cq.Sketch()
|
||||||
|
.polygon([
|
||||||
|
(self.flange_r0, yt),
|
||||||
|
(self.flange_r0, yt + self.flange_y1),
|
||||||
|
(self.flange_r1, yt + self.flange_y1),
|
||||||
|
(self.flange_r1, yt + self.flange_y2),
|
||||||
|
(-self.flange_r1, yt + self.flange_y2),
|
||||||
|
(-self.flange_r1, yt + self.flange_y1),
|
||||||
|
(-self.flange_r0, yt + self.flange_y1),
|
||||||
|
(-self.flange_r0, yt),
|
||||||
|
])
|
||||||
|
.push([
|
||||||
|
(self.flange_hole_r, yt+yh),
|
||||||
|
(-self.flange_hole_r, yt+yh),
|
||||||
|
])
|
||||||
|
.circle(self.hole_diam/2, mode="s")
|
||||||
|
)
|
||||||
|
tail = (
|
||||||
|
Cq.Sketch()
|
||||||
|
.polygon([
|
||||||
|
(+self.tail_r0, -yt),
|
||||||
|
(+self.tail_r0, -yt - self.tail_y1),
|
||||||
|
(+self.tail_r1, -yt - self.tail_y1),
|
||||||
|
(+self.tail_r1, -yt - self.tail_y2),
|
||||||
|
(-self.tail_r1, -yt - self.tail_y2),
|
||||||
|
(-self.tail_r1, -yt - self.tail_y1),
|
||||||
|
(-self.tail_r0, -yt - self.tail_y1),
|
||||||
|
(-self.tail_r0, -yt),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
Cq.Sketch()
|
||||||
|
.ellipse(self.width/2, self.height/2)
|
||||||
|
.boolean(flange, mode="a")
|
||||||
|
.boolean(tail, mode="a")
|
||||||
|
.boolean(self.profile_wing(-1), mode="a")
|
||||||
|
.boolean(self.profile_wing(1), mode="a")
|
||||||
|
)
|
||||||
|
def casing_bot(self) -> Cq.Workplane:
|
||||||
|
return (
|
||||||
|
Cq.Workplane()
|
||||||
|
.placeSketch(self.profile_casing_bot())
|
||||||
|
.extrude(self.casing_thickness)
|
||||||
|
)
|
||||||
|
def profile_wing(self, sign: float=1) -> Cq.Sketch:
|
||||||
|
xt = self.width / 2 - self.outer_gap
|
||||||
|
return (
|
||||||
|
Cq.Sketch()
|
||||||
|
.polygon([
|
||||||
|
(sign*xt, self.wing_r1),
|
||||||
|
(sign*(xt+self.wing_x1), self.wing_r1),
|
||||||
|
(sign*(xt+self.wing_x1), self.wing_r2),
|
||||||
|
(sign*(xt+self.wing_x2), self.wing_r2),
|
||||||
|
(sign*(xt+self.wing_x2), -self.wing_r2),
|
||||||
|
(sign*(xt+self.wing_x1), -self.wing_r2),
|
||||||
|
(sign*(xt+self.wing_x1), -self.wing_r1),
|
||||||
|
(sign*xt, -self.wing_r1),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
@target(name="casing-mid", kind=TargetKind.DXF)
|
||||||
|
def profile_casing_mid(self) -> Cq.Sketch:
|
||||||
|
rx = self.width/2 - self.outer_gap
|
||||||
|
ry = self.height/2 - self.outer_gap
|
||||||
|
return (
|
||||||
|
self.profile_casing_bot()
|
||||||
|
.ellipse(rx, ry, mode="s")
|
||||||
|
)
|
||||||
|
def casing_mid(self) -> Cq.Workplane:
|
||||||
|
return (
|
||||||
|
Cq.Workplane()
|
||||||
|
.placeSketch(self.profile_casing_mid())
|
||||||
|
.extrude(self.core_thickness)
|
||||||
|
)
|
||||||
|
@target(name="casing-top", kind=TargetKind.DXF)
|
||||||
|
def profile_casing_top(self) -> Cq.Sketch:
|
||||||
|
rx = self.width/2 - self.outer_gap - self.inner_gap
|
||||||
|
ry = self.height/2 - self.outer_gap - self.inner_gap
|
||||||
|
return (
|
||||||
|
self.profile_casing_bot()
|
||||||
|
.ellipse(rx, ry, mode="s")
|
||||||
|
)
|
||||||
|
def casing_top(self) -> Cq.Workplane:
|
||||||
|
return (
|
||||||
|
Cq.Workplane()
|
||||||
|
.placeSketch(self.profile_casing_top())
|
||||||
|
.extrude(self.casing_thickness)
|
||||||
|
)
|
||||||
|
|
||||||
|
@assembly()
|
||||||
|
def assembly(self) -> Cq.Assembly:
|
||||||
|
return (
|
||||||
|
Cq.Assembly()
|
||||||
|
.addS(
|
||||||
|
self.core(),
|
||||||
|
name="core",
|
||||||
|
material=self.material_mirror,
|
||||||
|
role=Role.DECORATION,
|
||||||
|
loc=Cq.Location(0, 0, self.casing_thickness)
|
||||||
|
)
|
||||||
|
.addS(
|
||||||
|
self.casing_bot(),
|
||||||
|
name="casing_bot",
|
||||||
|
material=self.material_casing,
|
||||||
|
role=Role.CASING,
|
||||||
|
)
|
||||||
|
.addS(
|
||||||
|
self.casing_mid(),
|
||||||
|
name="casing_mid",
|
||||||
|
material=self.material_casing,
|
||||||
|
role=Role.CASING,
|
||||||
|
loc=Cq.Location(0, 0, self.casing_thickness)
|
||||||
|
)
|
||||||
|
.addS(
|
||||||
|
self.casing_top(),
|
||||||
|
name="casing_top",
|
||||||
|
material=self.material_casing,
|
||||||
|
role=Role.CASING,
|
||||||
|
loc=Cq.Location(0, 0, self.core_thickness + self.casing_thickness)
|
||||||
|
)
|
||||||
|
)
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,264 @@
|
||||||
|
from nhf.build import Model, TargetKind, target, assembly, submodel
|
||||||
|
from nhf.materials import Role, Material
|
||||||
|
import nhf.utils
|
||||||
|
from nhf.parts.fasteners import FlatHeadBolt, HexNut, Washer
|
||||||
|
from nhf.parts.electronics import ArduinoUnoR3, BatteryBox18650
|
||||||
|
|
||||||
|
from typing import Optional, Union
|
||||||
|
import math
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
import cadquery as Cq
|
||||||
|
|
||||||
|
NUT_COMMON = HexNut(
|
||||||
|
# FIXME: weigh
|
||||||
|
mass=0.0,
|
||||||
|
diam_thread=6.0,
|
||||||
|
pitch=1.0,
|
||||||
|
thickness=5.0,
|
||||||
|
width=9.89,
|
||||||
|
)
|
||||||
|
BOLT_COMMON = FlatHeadBolt(
|
||||||
|
# FIXME: weigh
|
||||||
|
mass=0.0,
|
||||||
|
diam_head=12.8,
|
||||||
|
height_head=2.8,
|
||||||
|
diam_thread=6.0,
|
||||||
|
height_thread=30.0,
|
||||||
|
pitch=1.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Shimenawa(Model):
|
||||||
|
"""
|
||||||
|
The ring
|
||||||
|
"""
|
||||||
|
|
||||||
|
diam_inner: float = 43.0
|
||||||
|
diam_outer: float = 43.0 + 9 * 2
|
||||||
|
|
||||||
|
diam_hole_outer: float = 8.0
|
||||||
|
hole_ext: float = 2.0
|
||||||
|
hole_z: float = 15.0
|
||||||
|
|
||||||
|
pipe_fitting_angle_span: float = 6.0
|
||||||
|
|
||||||
|
pipe_joint_length: float = 120.0
|
||||||
|
pipe_joint_outer_thickness: float = 5.0
|
||||||
|
pipe_joint_inner_thickness: float = 4.0
|
||||||
|
|
||||||
|
pipe_joint_inner_angle_span: float = 120.0
|
||||||
|
pipe_joint_taper: float = 5.0
|
||||||
|
pipe_joint_taper_length: float = 10.0
|
||||||
|
|
||||||
|
ear_dr: float = 6.0
|
||||||
|
ear_hole_diam: float = 10.0
|
||||||
|
ear_radius: float = 15.0
|
||||||
|
ear_thickness: float = 10.0
|
||||||
|
|
||||||
|
main_circumference: float = 3600.0
|
||||||
|
|
||||||
|
material_fitting: Material = Material.PLASTIC_PLA
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
assert self.diam_inner < self.diam_outer
|
||||||
|
|
||||||
|
@property
|
||||||
|
def main_radius(self) -> float:
|
||||||
|
return self.main_circumference / (2 * math.pi)
|
||||||
|
|
||||||
|
@target(name="pipe-fitting-curved")
|
||||||
|
def pipe_fitting_curved(self) -> Cq.Workplane:
|
||||||
|
r_minor = self.diam_outer/2 + self.pipe_joint_outer_thickness
|
||||||
|
a1 = self.pipe_fitting_angle_span
|
||||||
|
outer = Cq.Solid.makeTorus(
|
||||||
|
radius1=self.main_radius,
|
||||||
|
radius2=r_minor,
|
||||||
|
)
|
||||||
|
inner = Cq.Solid.makeTorus(
|
||||||
|
radius1=self.main_radius,
|
||||||
|
radius2=self.diam_outer/2,
|
||||||
|
)
|
||||||
|
angle_intersector = Cq.Solid.makeCylinder(
|
||||||
|
radius=self.main_radius + r_minor,
|
||||||
|
height=r_minor*2,
|
||||||
|
angleDegrees=a1,
|
||||||
|
pnt=(0,0,-r_minor)
|
||||||
|
).rotate((0,0,0),(0,0,1),-a1/2)
|
||||||
|
result = (outer - inner) * angle_intersector
|
||||||
|
|
||||||
|
ear_outer = Cq.Solid.makeCylinder(
|
||||||
|
radius=self.ear_radius,
|
||||||
|
height=self.ear_thickness,
|
||||||
|
pnt=(0,-self.ear_thickness/2,0),
|
||||||
|
dir=(0,1,0),
|
||||||
|
)
|
||||||
|
ear_hole = Cq.Solid.makeCylinder(
|
||||||
|
radius=self.ear_hole_diam/2,
|
||||||
|
height=self.ear_thickness,
|
||||||
|
pnt=(-self.ear_dr,-self.ear_thickness/2,0),
|
||||||
|
dir=(0,1,0),
|
||||||
|
)
|
||||||
|
ear = (ear_outer - ear_hole).moved(self.main_radius - r_minor, 0, 0)
|
||||||
|
result += ear - inner
|
||||||
|
return result
|
||||||
|
@target(name="pipe-joint-outer")
|
||||||
|
def pipe_joint_outer(self) -> Cq.Workplane:
|
||||||
|
"""
|
||||||
|
Used to joint two pipes together (outside)
|
||||||
|
"""
|
||||||
|
r1 = self.diam_outer / 2 + self.pipe_joint_outer_thickness
|
||||||
|
h = self.pipe_joint_length
|
||||||
|
result = (
|
||||||
|
Cq.Workplane()
|
||||||
|
.cylinder(
|
||||||
|
radius=r1,
|
||||||
|
height=self.pipe_joint_length,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
cut_interior = Cq.Solid.makeCylinder(
|
||||||
|
radius=self.diam_outer/2,
|
||||||
|
height=h,
|
||||||
|
pnt=(0, 0, -h/2)
|
||||||
|
)
|
||||||
|
rh = r1 + self.hole_ext
|
||||||
|
add_hole = Cq.Solid.makeCylinder(
|
||||||
|
radius=self.diam_hole_outer/2,
|
||||||
|
height=rh*2,
|
||||||
|
pnt=(-rh, 0, 0),
|
||||||
|
dir=(1, 0, 0),
|
||||||
|
)
|
||||||
|
cut_hole = Cq.Solid.makeCylinder(
|
||||||
|
radius=BOLT_COMMON.diam_thread/2,
|
||||||
|
height=rh*2,
|
||||||
|
pnt=(-rh, 0, 0),
|
||||||
|
dir=(1, 0, 0),
|
||||||
|
)
|
||||||
|
z = self.hole_z
|
||||||
|
result = (
|
||||||
|
result
|
||||||
|
+ add_hole.moved(0, 0, -z)
|
||||||
|
+ add_hole.moved(0, 0, z)
|
||||||
|
- cut_hole.moved(0, 0, -z)
|
||||||
|
- cut_hole.moved(0, 0, z)
|
||||||
|
- cut_interior
|
||||||
|
)
|
||||||
|
ear_outer = Cq.Solid.makeCylinder(
|
||||||
|
radius=self.ear_radius,
|
||||||
|
height=self.ear_thickness,
|
||||||
|
pnt=(0, r1, -self.ear_thickness/2),
|
||||||
|
)
|
||||||
|
ear_hole = Cq.Solid.makeCylinder(
|
||||||
|
radius=self.ear_hole_diam/2,
|
||||||
|
height=self.ear_thickness,
|
||||||
|
pnt=(0,r1+self.ear_dr,-self.ear_thickness/2),
|
||||||
|
)
|
||||||
|
ear = ear_outer - ear_hole - cut_interior
|
||||||
|
return result + ear
|
||||||
|
|
||||||
|
@target(name="pipe-joint-inner")
|
||||||
|
def pipe_joint_inner(self) -> Cq.Workplane:
|
||||||
|
"""
|
||||||
|
Used to joint two pipes together (inside)
|
||||||
|
"""
|
||||||
|
r1 = self.diam_inner / 2
|
||||||
|
r2 = r1 - self.pipe_joint_taper
|
||||||
|
r3 = r2 - self.pipe_joint_inner_thickness
|
||||||
|
h = self.pipe_joint_length
|
||||||
|
h0 = h - self.pipe_joint_taper_length*2
|
||||||
|
core = Cq.Solid.makeCylinder(
|
||||||
|
radius=r2,
|
||||||
|
height=h0/2,
|
||||||
|
)
|
||||||
|
centre_cut = Cq.Solid.makeCylinder(
|
||||||
|
radius=r3,
|
||||||
|
height=h0/2,
|
||||||
|
)
|
||||||
|
taper = Cq.Solid.makeCone(
|
||||||
|
radius1=r2,
|
||||||
|
radius2=r1,
|
||||||
|
height=(h - h0) / 2,
|
||||||
|
pnt=(0, 0, h0/2),
|
||||||
|
)
|
||||||
|
centre_cut_taper = Cq.Solid.makeCone(
|
||||||
|
radius1=r3,
|
||||||
|
radius2=r3 + self.pipe_joint_taper,
|
||||||
|
height=(h - h0) / 2,
|
||||||
|
pnt=(0, 0, h0/2),
|
||||||
|
)
|
||||||
|
angle_intersector = Cq.Solid.makeCylinder(
|
||||||
|
radius=r1,
|
||||||
|
height=h,
|
||||||
|
angleDegrees=self.pipe_joint_inner_angle_span
|
||||||
|
).rotate((0,0,0), (0,0,1), -self.pipe_joint_inner_angle_span/2)
|
||||||
|
result = (taper + core - centre_cut - centre_cut_taper) * angle_intersector
|
||||||
|
|
||||||
|
result += result.mirror("XY")
|
||||||
|
|
||||||
|
add_hole = Cq.Solid.makeCylinder(
|
||||||
|
radius=self.diam_hole_outer/2,
|
||||||
|
height=self.hole_ext,
|
||||||
|
pnt=(r3, 0, 0),
|
||||||
|
dir=(-1, 0, 0),
|
||||||
|
)
|
||||||
|
cut_hole = Cq.Solid.makeCylinder(
|
||||||
|
radius=BOLT_COMMON.diam_thread/2,
|
||||||
|
height=r1,
|
||||||
|
pnt=(0, 0, 0),
|
||||||
|
dir=(r1, 0, 0),
|
||||||
|
)
|
||||||
|
z = self.hole_z
|
||||||
|
# avoid collisions
|
||||||
|
nut_x = r3 - self.hole_ext - NUT_COMMON.thickness
|
||||||
|
nut = NUT_COMMON.generate().val().rotate((0,0,0),(0,1,0),90)
|
||||||
|
result = (
|
||||||
|
result
|
||||||
|
+ add_hole.moved(0, 0, z)
|
||||||
|
+ add_hole.moved(0, 0, -z)
|
||||||
|
- cut_hole.moved(0, 0, z)
|
||||||
|
- cut_hole.moved(0, 0, -z)
|
||||||
|
- nut.moved(nut_x, 0, z)
|
||||||
|
- nut.moved(nut_x, 0, -z)
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
@assembly()
|
||||||
|
def assembly_pipe_joint(self) -> Cq.Assembly:
|
||||||
|
a = (
|
||||||
|
Cq.Assembly()
|
||||||
|
.addS(
|
||||||
|
self.pipe_joint_outer(),
|
||||||
|
name="joint_outer",
|
||||||
|
material=self.material_fitting,
|
||||||
|
role=Role.STRUCTURE,
|
||||||
|
)
|
||||||
|
.addS(
|
||||||
|
self.pipe_joint_inner(),
|
||||||
|
name="joint_inner1",
|
||||||
|
material=self.material_fitting,
|
||||||
|
role=Role.STRUCTURE,
|
||||||
|
)
|
||||||
|
.addS(
|
||||||
|
self.pipe_joint_inner(),
|
||||||
|
name="joint_inner2",
|
||||||
|
material=self.material_fitting,
|
||||||
|
role=Role.STRUCTURE,
|
||||||
|
loc=Cq.Location.rot2d(180),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return a
|
||||||
|
|
||||||
|
@assembly()
|
||||||
|
def assembly(self) -> Cq.Assembly:
|
||||||
|
a = (
|
||||||
|
Cq.Assembly()
|
||||||
|
.addS(
|
||||||
|
self.pipe_fitting_curved(),
|
||||||
|
name="fitting1",
|
||||||
|
material=self.material_fitting,
|
||||||
|
role=Role.STRUCTURE,
|
||||||
|
)
|
||||||
|
.add(
|
||||||
|
self.assembly_pipe_joint(),
|
||||||
|
name="pipe_joint",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return a
|
|
@ -162,6 +162,15 @@ def tagPlane(self, tag: str,
|
||||||
|
|
||||||
Cq.Workplane.tagPlane = tagPlane
|
Cq.Workplane.tagPlane = tagPlane
|
||||||
|
|
||||||
|
def tag_absolute(
|
||||||
|
self,
|
||||||
|
tag: str,
|
||||||
|
loc: Union[Cq.Location, Tuple[float, float, float]],
|
||||||
|
direction: Union[str, Cq.Vector, Tuple[float, float, float]] = '+Z'):
|
||||||
|
return self.pushPoints([loc]).tagPlane(tag, direction=direction)
|
||||||
|
|
||||||
|
Cq.Workplane.tagAbsolute = tag_absolute
|
||||||
|
|
||||||
def make_sphere(r: float = 2) -> Cq.Solid:
|
def make_sphere(r: float = 2) -> Cq.Solid:
|
||||||
"""
|
"""
|
||||||
Makes a full sphere. The default function makes a hemisphere
|
Makes a full sphere. The default function makes a hemisphere
|
||||||
|
|
Loading…
Reference in New Issue