Compare commits

...

62 Commits

Author SHA1 Message Date
Leni Aniva 2a405f916a Merge pull request 'cosplay: Touhou/Yasaka Kanako' (#11) from touhou/yasaka-kanako into main
Reviewed-on: #11
2025-06-16 10:16:57 -07:00
Leni Aniva 4c0a54dc4a
Update controller to use internal pullup 2025-06-16 10:16:32 -07:00
Leni Aniva db4232f94d
Add startup sequence 2025-06-11 15:30:28 -07:00
Leni Aniva 675f4f995b
Add controller code for Kanako 2025-06-10 00:53:41 -07:00
Leni Aniva 52b4e0b329
Add ears to the pipe joint 2025-06-03 18:50:39 -07:00
Leni Aniva 7ec2728a6c
Use 0.5 spindle gap for rotation resistance 2025-06-03 18:50:24 -07:00
Leni Aniva 5d7137c037
Shimenawa geometry 2025-06-03 09:11:06 -07:00
Leni Aniva 49d3fa44bf
Separate interior and exterior gaps 2025-06-03 00:53:31 -07:00
Leni Aniva 89c0d88de7
Use a much larger spindle gap 2025-06-02 22:48:39 -07:00
Leni Aniva 0a4ca64dad
Add front stabilization bracket 2025-06-01 22:52:13 -07:00
Leni Aniva d6ccc3496b
Use jute rope for handle 2025-05-31 07:41:54 -07:00
Leni Aniva c8bbc0de91
Magnet holder 2025-05-30 22:31:08 -07:00
Leni Aniva 34ecf59124
Add more mounting points on chamber front 2025-05-30 18:00:48 -07:00
Leni Aniva a0100f8fb7
Front chamber separator 2025-05-30 14:35:55 -07:00
Leni Aniva e4cfc71f1a
Electrnics assembly 2025-05-30 14:20:25 -07:00
Leni Aniva 80730a9c5a
Fix stator coupler hole size 2025-05-30 10:20:49 -07:00
Leni Aniva 6777033383
Remove conflict with base geometry 2025-05-30 01:54:19 -07:00
Leni Aniva 758b51c9db
Remove redundant geometry from seat 2025-05-30 01:35:55 -07:00
Leni Aniva ef0b0dad91
Add reinforcements to motor seat 2025-05-30 01:33:14 -07:00
Leni Aniva 6ad74047bc
Simplify seat geometry 2025-05-30 01:22:48 -07:00
Leni Aniva 7d3845f3c1
Align coupler holes 2025-05-30 00:32:40 -07:00
Leni Aniva b55dc8caa3
Calibrate measurements 2025-05-30 00:28:44 -07:00
Leni Aniva e94546b017
Motor seat and coupler 2025-05-29 23:40:12 -07:00
Leni Aniva 2d6d65235d
Turning bar 2025-05-29 17:24:06 -07:00
Leni Aniva 0f151bd279
Motor assembly 2025-05-29 16:21:01 -07:00
Leni Aniva 6709e4f32e
Bearing couplers 2025-05-29 14:04:12 -07:00
Leni Aniva d937fc9513
Retrofit handle 2025-05-29 09:22:44 -07:00
Leni Aniva a3288ce98f
Spindle geometry 2025-05-29 01:19:19 -07:00
Leni Aniva bec15c5136
Model of the motor 2025-05-29 00:52:56 -07:00
Leni Aniva b565ab05a0
Additional mounting points for machinery on first 3 rings 2025-05-29 00:13:48 -07:00
Leni Aniva af82a86652
Eliminate difficult geometry in angle joint 2025-05-28 23:49:04 -07:00
Leni Aniva 40c32213e1
Motor and bolt models 2025-05-28 08:01:14 -07:00
Leni Aniva a8c80a307f
Handle stub 2025-05-21 20:25:57 -07:00
Leni Aniva b1fe538747
Use dihedral angle to calculate sanding block 2025-05-20 19:47:16 -07:00
Leni Aniva bd15f28403
Improve grometry 2025-05-20 08:24:41 -07:00
Leni Aniva 0574a767a3
Add sanding block 2025-05-20 08:18:11 -07:00
Leni Aniva bd7e8677c7
Make all angle joints flanged 2025-05-16 07:15:30 -07:00
Leni Aniva c5f9e570a6
Fit onbashira profile in 12x12 in panel 2025-05-15 23:16:39 -07:00
Leni Aniva 63c2c74e02
Barrel position solver 2025-05-15 21:20:22 -07:00
Leni Aniva 0fb88a97d3
Chamber connectors 2025-05-15 20:55:17 -07:00
Leni Aniva 4edad88299
Larger mirror dimensions 2025-05-15 13:23:52 -07:00
Leni Aniva 83d4232ad7
Add holes for gohei 2025-05-14 23:21:13 -07:00
Leni Aniva 4d4e4c7eab
Mating structure for angle joint 2025-05-14 23:08:47 -07:00
Leni Aniva b83bf5a57d
Use only one bolt for angle bracket 2025-05-14 22:40:53 -07:00
Leni Aniva 670d4a8c21
Improve geometry of angle joint 2025-05-14 13:13:44 -07:00
Leni Aniva 22a4f4ceec
Mirror wing geometry 2025-05-13 17:20:51 -07:00
Leni Aniva a684996475
Geometry of mirror and rotor spacer 2025-05-13 14:29:10 -07:00
Leni Aniva 5b5ccee94e
Optimize angle joint geometry; Mirror stub 2025-05-13 09:11:28 -07:00
Leni Aniva b88d52f4be
Use 8mm bolt 2025-05-13 00:02:43 -07:00
Leni Aniva ca606c6bc1
Fix rotation radius 2025-05-12 23:59:34 -07:00
Leni Aniva 916ccee260
Centre holes 2025-05-12 23:52:45 -07:00
Leni Aniva 44cd6ee960
Onbashira dimension update and flanges 2025-05-12 23:33:20 -07:00
Leni Aniva 4dcd97613b
Section bracing 2025-05-12 22:08:44 -07:00
Leni Aniva 97675a2fc8
Add angle joint stub, hole in rotor 2025-05-12 14:50:59 -07:00
Leni Aniva 74145f88d2
Add bolts on rotor 2025-05-12 12:24:33 -07:00
Leni Aniva 878d532890
Use rotor-stator configuration for bearing 2025-05-09 16:58:14 -04:00
Leni Aniva 7511efa9ee
Add Kanako set class 2025-04-22 11:16:44 -07:00
Leni Aniva 3e0eab0cec
Merge branch 'main' into touhou/yasaka-kanako 2025-04-20 12:49:17 -07:00
Leni Aniva b15db172a0
Merge branch 'main' into touhou/yasaka-kanako 2025-04-08 23:28:30 -07:00
Leni Aniva a74f919a5b
Onbashira rotor-stator mechanism 2025-02-25 21:04:25 -08:00
Leni Aniva f4704b9ad6
fix: Remove shebang in init 2025-02-24 00:54:39 -08:00
Leni Aniva 590033e492
Kanako onbashira barrel 2025-02-24 00:21:31 -08:00
11 changed files with 3313 additions and 6 deletions

View File

@ -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

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,3 @@
# Yasaka Kanako
This cosplay won a Judge's favourite award at TouhouFest 2025.

View File

@ -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)

View File

@ -0,0 +1,211 @@
#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
}

View File

@ -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

View File

@ -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

View File

@ -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