Compare commits
1 Commits
main
...
touhou/clo
Author | SHA1 | Date |
---|---|---|
|
35a44e5be3 |
|
@ -3,9 +3,6 @@
|
||||||
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,16 +25,13 @@ 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()
|
||||||
MOTOR = auto()
|
MOTION = auto()
|
||||||
|
|
||||||
# Fasteners, etc.
|
# Fasteners, etc.
|
||||||
CONNECTION = auto()
|
CONNECTION = auto()
|
||||||
|
@ -62,14 +59,11 @@ 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.MOTOR: _color('thistle3', 0.7),
|
Role.MOTION: _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),
|
||||||
}
|
}
|
||||||
|
@ -90,8 +84,6 @@ 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,16 +83,11 @@ 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 = 2.28
|
thickness: float = 1.66
|
||||||
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)
|
||||||
|
|
||||||
|
@ -100,20 +95,13 @@ 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
|
||||||
result = (
|
return (
|
||||||
Cq.Workplane('XY')
|
Cq.Workplane('XY')
|
||||||
.box(
|
.box(
|
||||||
length=self.length,
|
length=self.length,
|
||||||
|
@ -129,7 +117,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 + self.battery_gap)))
|
.copyWorkplane(Cq.Workplane('XY', origin=(-self.battery_height/2, 0, self.thickness + self.battery_diam/2)))
|
||||||
.rarray(
|
.rarray(
|
||||||
xSpacing=1,
|
xSpacing=1,
|
||||||
ySpacing=self.battery_dist,
|
ySpacing=self.battery_dist,
|
||||||
|
@ -144,16 +132,4 @@ 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,7 +11,6 @@ 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:
|
||||||
|
@ -35,7 +34,7 @@ class FlatHeadBolt(Item):
|
||||||
centered=(True, True, False))
|
centered=(True, True, False))
|
||||||
)
|
)
|
||||||
rod.faces("<Z").tag("tip")
|
rod.faces("<Z").tag("tip")
|
||||||
rod.tagAbsolute("root", (0, 0, self.height_thread), direction="-Z")
|
rod.faces(">Z").tag("root")
|
||||||
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,27 @@
|
||||||
|
import nhf.touhou.clownpiece.torch as MT
|
||||||
|
|
||||||
|
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):
|
||||||
|
|
||||||
|
torch: MT.Torch = field(default_factory=lambda: MT.Torch())
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
super().__init__(name="clownpiece")
|
||||||
|
|
||||||
|
@submodel(name="torch")
|
||||||
|
def submodel_torch(self) -> Model:
|
||||||
|
return self.torch
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
|
||||||
|
p = Parameters()
|
||||||
|
if len(sys.argv) == 1:
|
||||||
|
p.build_all()
|
||||||
|
sys.exit(0)
|
|
@ -0,0 +1,23 @@
|
||||||
|
from nhf import Material, Role
|
||||||
|
from nhf.build import Model, target, assembly, TargetKind
|
||||||
|
import nhf.utils
|
||||||
|
|
||||||
|
import math
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Tuple
|
||||||
|
import cadquery as Cq
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Torch(Model):
|
||||||
|
"""
|
||||||
|
The torch modeled after Lady Liberty's Torch
|
||||||
|
"""
|
||||||
|
diam_pan: float = 200.0
|
||||||
|
diam_handle: float = 80.0
|
||||||
|
height_pan: float = 30.0
|
||||||
|
height_handle: float = 300.0
|
||||||
|
|
||||||
|
material_handle: Material = Material.PLASTIC_PLA
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
pass
|
|
@ -1,3 +0,0 @@
|
||||||
# Yasaka Kanako
|
|
||||||
|
|
||||||
This cosplay won a Judge's favourite award at TouhouFest 2025.
|
|
|
@ -1,37 +0,0 @@
|
||||||
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)
|
|
|
@ -1,211 +0,0 @@
|
||||||
#define USE_MOTOR 1
|
|
||||||
#define USE_LED 1
|
|
||||||
#define USE_DISPLAY 0
|
|
||||||
|
|
||||||
// The mode switch button should be wired to the ground with an internal pullup resistor.
|
|
||||||
#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_PULLUP);
|
|
||||||
#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();
|
|
||||||
fill_solid(leds, NUM_LEDS, CRGB::Black);
|
|
||||||
delay(500);
|
|
||||||
#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
|
|
||||||
}
|
|
|
@ -1,186 +0,0 @@
|
||||||
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
|
@ -1,264 +0,0 @@
|
||||||
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,15 +162,6 @@ 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