cosplay: Touhou/Yasaka Kanako #11
|
@ -3,6 +3,9 @@
|
|||
This is the design repository for NorCal Hakkero Factory No. 1, where we use
|
||||
parametric CAD to make cosplay props.
|
||||
|
||||
> NorCal Hakkero Factory № 1
|
||||
> 北加国営八卦炉第一工場
|
||||
|
||||
## Development
|
||||
|
||||
Most cosplay schematics are created with Blender, CadQuery, and Inkscape. To
|
||||
|
|
|
@ -25,13 +25,16 @@ class Role(Flag):
|
|||
PARENT = auto()
|
||||
CHILD = auto()
|
||||
CASING = auto()
|
||||
STATOR = auto()
|
||||
ROTOR = auto()
|
||||
BEARING = auto()
|
||||
# Springs, cushions
|
||||
DAMPING = auto()
|
||||
# Main structural support
|
||||
STRUCTURE = auto()
|
||||
DECORATION = auto()
|
||||
ELECTRONIC = auto()
|
||||
MOTION = auto()
|
||||
MOTOR = auto()
|
||||
|
||||
# Fasteners, etc.
|
||||
CONNECTION = auto()
|
||||
|
@ -59,11 +62,14 @@ ROLE_COLOR_MAP = {
|
|||
Role.PARENT: _color('blue4', 0.6),
|
||||
Role.CASING: _color('dodgerblue3', 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.STRUCTURE: _color('gray', 0.4),
|
||||
Role.DECORATION: _color('lightseagreen', 0.4),
|
||||
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.HANDLE: _color('tomato4', 0.8),
|
||||
}
|
||||
|
@ -84,6 +90,8 @@ class Material(Enum):
|
|||
ACRYLIC_TRANSLUSCENT = 1.18, _color('ivory2', 0.8)
|
||||
ACRYLIC_TRANSPARENT = 1.18, _color('ghostwhite', 0.5)
|
||||
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)
|
||||
|
||||
def __init__(self, density: float, color: Cq.Color):
|
||||
|
|
|
@ -83,11 +83,16 @@ class BatteryBox18650(Item):
|
|||
battery_dist: float = 20.18
|
||||
height: float = 19.66
|
||||
# space from bottom to battery begin
|
||||
thickness: float = 1.66
|
||||
thickness: float = 2.28
|
||||
battery_diam: float = 18.48
|
||||
battery_height: float = 68.80
|
||||
n_batteries: int = 3
|
||||
|
||||
battery_gap: float = 2.0
|
||||
|
||||
diam_thread: float = 3.0
|
||||
hole_dy: float = 39.50 / 2
|
||||
|
||||
def __post_init__(self):
|
||||
assert 2 * self.thickness < min(self.length, self.height)
|
||||
|
||||
|
@ -95,13 +100,20 @@ class BatteryBox18650(Item):
|
|||
def name(self) -> str:
|
||||
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
|
||||
def role(self) -> Role:
|
||||
return Role.ELECTRONIC
|
||||
|
||||
def generate(self) -> Cq.Workplane:
|
||||
width = self.width_base + self.battery_dist * (self.n_batteries - 1) + self.battery_diam
|
||||
return (
|
||||
result = (
|
||||
Cq.Workplane('XY')
|
||||
.box(
|
||||
length=self.length,
|
||||
|
@ -117,7 +129,7 @@ class BatteryBox18650(Item):
|
|||
centered=(True, True, False),
|
||||
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(
|
||||
xSpacing=1,
|
||||
ySpacing=self.battery_dist,
|
||||
|
@ -132,4 +144,16 @@ class BatteryBox18650(Item):
|
|||
centered=(True, True, False),
|
||||
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
|
||||
diam_thread: float
|
||||
height_thread: float
|
||||
pitch: float = 1.0
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
|
@ -34,7 +35,7 @@ class FlatHeadBolt(Item):
|
|||
centered=(True, True, False))
|
||||
)
|
||||
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))))
|
||||
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,199 @@
|
|||
#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);
|
||||
#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
|
||||
}
|
||||
|
||||
void loop() {
|
||||
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();
|
||||
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
|
||||
}
|
||||
}
|
||||
void program_still()
|
||||
{
|
||||
if (programChanged)
|
||||
{
|
||||
set_motor(false);
|
||||
#if USE_LED
|
||||
flag_lighting = true;
|
||||
fill_segmented(CRGB::Green, CRGB::Orange);
|
||||
FastLED.show();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
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
|
||||
|
||||
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:
|
||||
"""
|
||||
Makes a full sphere. The default function makes a hemisphere
|
||||
|
|
Loading…
Reference in New Issue