From 590033e4923449f698fa8bf685b08743a946134c Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Mon, 24 Feb 2025 00:21:31 -0800 Subject: [PATCH 01/59] Kanako onbashira barrel --- nhf/materials.py | 1 + nhf/touhou/__init__.py | 1 + nhf/touhou/yasaka_kanako/__init__.py | 0 nhf/touhou/yasaka_kanako/onbashira.py | 79 +++++++++++++++++++++++++++ 4 files changed, 81 insertions(+) create mode 100644 nhf/touhou/__init__.py create mode 100644 nhf/touhou/yasaka_kanako/__init__.py create mode 100644 nhf/touhou/yasaka_kanako/onbashira.py diff --git a/nhf/materials.py b/nhf/materials.py index bc172c3..909e771 100644 --- a/nhf/materials.py +++ b/nhf/materials.py @@ -84,6 +84,7 @@ 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) + ALUMINUM = 2.7, _color('gray', 0.6) def __init__(self, density: float, color: Cq.Color): self.density = density diff --git a/nhf/touhou/__init__.py b/nhf/touhou/__init__.py new file mode 100644 index 0000000..e5a0d9b --- /dev/null +++ b/nhf/touhou/__init__.py @@ -0,0 +1 @@ +#!/usr/bin/env python3 diff --git a/nhf/touhou/yasaka_kanako/__init__.py b/nhf/touhou/yasaka_kanako/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/nhf/touhou/yasaka_kanako/onbashira.py b/nhf/touhou/yasaka_kanako/onbashira.py new file mode 100644 index 0000000..4169081 --- /dev/null +++ b/nhf/touhou/yasaka_kanako/onbashira.py @@ -0,0 +1,79 @@ +from nhf.build import Model, TargetKind, target, assembly, submodel +from nhf.materials import Role, Material +import nhf.utils + +import math +from dataclasses import dataclass +import cadquery as Cq + +@dataclass +class Onbashira(Model): + + n_side: int = 6 + # Dimensions of each side panel + side_width: float = 200.0 + side_length: float = 600.0 + + side_thickness: float = 25.4 / 8 + + material_side: Material = Material.WOOD_BIRCH + material_brace: Material = Material.ALUMINUM + + def __post_init__(self): + assert self.n_side >= 3 + + @property + def angle_side(self) -> float: + return 360 / self.n_side + @property + def angle_dihedral(self) -> float: + return 180 - self.angle_side + @property + def barrel_radius(self) -> float: + """ + Calculate radius of the barrel to the centre + """ + return self.side_width / 2 / math.tan(math.radians(self.angle_side / 2)) + + @target(name="side-panel", kind=TargetKind.DXF) + def profile_side_panel(self) -> Cq.Sketch: + return ( + Cq.Sketch() + .rect(self.side_width, self.side_length) + ) + + def side_panel(self) -> Cq.Workplane: + w = self.side_width + l = self.side_length + result = ( + Cq.Workplane() + .placeSketch(self.profile_side_panel()) + .extrude(self.side_thickness) + ) + intersector = ( + Cq.Workplane('XZ') + .polyline([ + (-w/2, 0), + (w/2, 0), + (0, self.barrel_radius), + ]) + .close() + .extrude(l) + .translate(Cq.Vector(0, l/2,0)) + ) + # Intersect the side panel + return result * intersector + + def assembly(self) -> Cq.Assembly: + a = Cq.Assembly() + side = self.side_panel() + r = self.barrel_radius + for i in range(6): + a = a.addS( + side, + name=f"side{i}", + material=self.material_side, + role=Role.STRUCTURE | Role.DECORATION, + loc=Cq.Location(0,0,0,0,i*60,0) * Cq.Location(0,0,-r) + ) + return a From f4704b9ad604d5506520c0949c7f370704c66fee Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Mon, 24 Feb 2025 00:54:39 -0800 Subject: [PATCH 02/59] fix: Remove shebang in init --- nhf/touhou/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nhf/touhou/__init__.py b/nhf/touhou/__init__.py index e5a0d9b..e69de29 100644 --- a/nhf/touhou/__init__.py +++ b/nhf/touhou/__init__.py @@ -1 +0,0 @@ -#!/usr/bin/env python3 From a74f919a5b70135b1b6a4831abd704603cd32d46 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Tue, 25 Feb 2025 21:04:25 -0800 Subject: [PATCH 03/59] Onbashira rotor-stator mechanism --- nhf/materials.py | 6 ++ nhf/touhou/yasaka_kanako/onbashira.py | 120 +++++++++++++++++++++++++- 2 files changed, 122 insertions(+), 4 deletions(-) diff --git a/nhf/materials.py b/nhf/materials.py index 909e771..86e0ae9 100644 --- a/nhf/materials.py +++ b/nhf/materials.py @@ -25,6 +25,9 @@ class Role(Flag): PARENT = auto() CHILD = auto() CASING = auto() + STATOR = auto() + ROTOR = auto() + BEARING = auto() # Springs, cushions DAMPING = auto() # Main structural support @@ -59,6 +62,9 @@ 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), diff --git a/nhf/touhou/yasaka_kanako/onbashira.py b/nhf/touhou/yasaka_kanako/onbashira.py index 4169081..5dab33e 100644 --- a/nhf/touhou/yasaka_kanako/onbashira.py +++ b/nhf/touhou/yasaka_kanako/onbashira.py @@ -16,11 +16,27 @@ class Onbashira(Model): side_thickness: float = 25.4 / 8 + # Dimensions of gun barrels + barrel_diam: float = 45.0 + barrel_length: float = 300.0 + # Radius from barrel centre to axis + rotation_radius: float = 75.0 + # Radius of ball bearings + bearing_ball_diam: float = 30.0 + bearing_ball_gap: float = 1.0 + bearing_height: float = 40.0 + bearing_thickness: float = 20.0 + material_side: Material = Material.WOOD_BIRCH + material_bearing: Material = Material.PLASTIC_PLA + material_bearing_ball: Material = Material.ACRYLIC_TRANSPARENT material_brace: Material = Material.ALUMINUM def __post_init__(self): assert self.n_side >= 3 + # Bulk must be large enough for the barrel + bearing to rotate + assert self.bulk_radius - self.side_thickness - self.bearing_thickness - self.bearing_diam > self.rotation_radius + self.barrel_diam / 2 + assert self.bearing_height > self.bearing_diam @property def angle_side(self) -> float: @@ -29,11 +45,17 @@ class Onbashira(Model): def angle_dihedral(self) -> float: return 180 - self.angle_side @property - def barrel_radius(self) -> float: + def bulk_radius(self) -> float: """ - Calculate radius of the barrel to the centre + Calculate radius of the bulk to the centre """ return self.side_width / 2 / math.tan(math.radians(self.angle_side / 2)) + @property + def bearing_diam(self) -> float: + return self.bearing_ball_diam + self.bearing_ball_gap + @property + def bearing_radius(self) -> float: + return self.bulk_radius - self.side_thickness - self.bearing_thickness - self.bearing_diam / 2 @target(name="side-panel", kind=TargetKind.DXF) def profile_side_panel(self) -> Cq.Sketch: @@ -55,7 +77,7 @@ class Onbashira(Model): .polyline([ (-w/2, 0), (w/2, 0), - (0, self.barrel_radius), + (0, self.bulk_radius), ]) .close() .extrude(l) @@ -64,10 +86,100 @@ class Onbashira(Model): # Intersect the side panel return result * intersector + def bearing_channel(self) -> Cq.Solid: + """ + Generates a toroidal channel for the ball bearings + """ + return Cq.Solid.makeTorus( + radius1=self.bearing_radius, + radius2=self.bearing_diam/2, + ) + @target(name="inner-rotor") + def inner_rotor(self) -> Cq.Workplane: + r_outer = self.bearing_radius + base = Cq.Solid.makeCylinder( + radius=r_outer, + height=self.bearing_height + ).translate(Cq.Vector(0,0,-self.bearing_height/2)) + r_rot = self.rotation_radius + channel = self.bearing_channel() + return ( + Cq.Workplane() + .add(base - channel) + .faces(">Z") + .workplane() + .polygon( + nSides=self.n_side, + diameter=2 * r_rot, + forConstruction=True + ) + .vertices() + .hole(self.barrel_diam) + ) + return base - channel + @target(name="outer-rotor") + def outer_rotor(self) -> Cq.Workplane: + polygon_radius = (self.bulk_radius - self.side_thickness) / math.cos(math.radians(self.angle_side / 2)) + profile = ( + Cq.Sketch() + .regularPolygon( + r=polygon_radius, + n=self.n_side, + ) + ) + inner = Cq.Solid.makeCylinder( + radius=self.bearing_radius, + height=self.bearing_height, + ) + base = ( + Cq.Workplane() + .placeSketch(profile) + .extrude(self.bearing_height) + .cut(inner) + .translate(Cq.Vector(0,0,-self.bearing_height/2)) + .cut(self.bearing_channel()) + ) + r = self.bearing_radius * 2 + subtractor = Cq.Solid.makeBox( + length=r * 2, + width=r * 2, + height=self.bearing_height, + ).translate(Cq.Vector(-r, -r, -self.bearing_height)) + return base - subtractor + def bearing_ball(self) -> Cq.Solid: + return Cq.Solid.makeSphere(radius=self.bearing_ball_diam/2, angleDegrees1=-90) + + def assembly_bearing(self) -> Cq.Assembly: + a = ( + Cq.Assembly() + .addS( + self.inner_rotor(), + name="inner", + material=self.material_bearing, + role=Role.ROTOR, + ) + .addS( + self.outer_rotor(), + name="outer", + material=self.material_bearing, + role=Role.STATOR, + ) + ) + for i in range(self.n_side): + ball = self.bearing_ball() + a = a.addS( + ball, + name=f"bearing_ball{i}", + material=self.material_bearing_ball, + role=Role.BEARING, + loc=Cq.Location.rot2d(i * 360/self.n_side) * Cq.Location(self.bearing_radius, 0, 0), + ) + return a + def assembly(self) -> Cq.Assembly: a = Cq.Assembly() side = self.side_panel() - r = self.barrel_radius + r = self.bulk_radius for i in range(6): a = a.addS( side, From 7511efa9eea9037fdcf2928ab13fc583b83d5630 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Tue, 22 Apr 2025 11:16:44 -0700 Subject: [PATCH 04/59] Add Kanako set class --- nhf/touhou/yasaka_kanako/__init__.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/nhf/touhou/yasaka_kanako/__init__.py b/nhf/touhou/yasaka_kanako/__init__.py index e69de29..01fde9d 100644 --- a/nhf/touhou/yasaka_kanako/__init__.py +++ b/nhf/touhou/yasaka_kanako/__init__.py @@ -0,0 +1,26 @@ +from dataclasses import dataclass, field +import cadquery as Cq +from nhf.build import Model, TargetKind, target, assembly, submodel +import nhf.touhou.yasaka_kanako.onbashira as MO +import nhf.utils + +@dataclass +class Parameters(Model): + + onbashira: MO.Onbashira = field(default_factory=lambda: MO.Onbashira()) + + def __post_init__(self): + super().__init__(name="yasaka-kanako") + + @submodel(name="onbashira") + def submodel_onbashira(self) -> Model: + return self.onbashira + + +if __name__ == '__main__': + import sys + + p = Parameters() + if len(sys.argv) == 1: + p.build_all() + sys.exit(0) From 878d532890eb4894b7257254f10ee8e09a395a6f Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Fri, 9 May 2025 16:58:14 -0400 Subject: [PATCH 05/59] Use rotor-stator configuration for bearing --- nhf/touhou/yasaka_kanako/onbashira.py | 165 ++++++++++++++------------ 1 file changed, 86 insertions(+), 79 deletions(-) diff --git a/nhf/touhou/yasaka_kanako/onbashira.py b/nhf/touhou/yasaka_kanako/onbashira.py index 73bf69f..5750abb 100644 --- a/nhf/touhou/yasaka_kanako/onbashira.py +++ b/nhf/touhou/yasaka_kanako/onbashira.py @@ -17,15 +17,20 @@ class Onbashira(Model): side_thickness: float = 25.4 / 8 # Dimensions of gun barrels - barrel_diam: float = 45.0 + barrel_diam: float = 25.4 * 2 barrel_length: float = 300.0 # Radius from barrel centre to axis rotation_radius: float = 75.0 - # Radius of ball bearings - bearing_ball_diam: float = 30.0 - bearing_ball_gap: float = 1.0 + n_bearing_balls: int = 24 + # Size of ball bearings + bearing_ball_diam: float = 25.4 * 1/2 + bearing_ball_gap: float = .5 bearing_height: float = 40.0 bearing_thickness: float = 20.0 + bearing_track_radius: float = 120.0 + # Gap between the inner and outer bearing disks + bearing_gap: float = 10.0 + bearing_disk_thickness: float = 25.4 / 8 material_side: Material = Material.WOOD_BIRCH material_bearing: Material = Material.PLASTIC_PLA @@ -37,6 +42,7 @@ class Onbashira(Model): # Bulk must be large enough for the barrel + bearing to rotate assert self.bulk_radius - self.side_thickness - self.bearing_thickness - self.bearing_diam > self.rotation_radius + self.barrel_diam / 2 assert self.bearing_height > self.bearing_diam + assert self.bearing_gap < 0.95 * self.bearing_ball_diam @property def angle_side(self) -> float: @@ -53,9 +59,49 @@ class Onbashira(Model): @property def bearing_diam(self) -> float: return self.bearing_ball_diam + self.bearing_ball_gap + @property - def bearing_radius(self) -> float: - return self.bulk_radius - self.side_thickness - self.bearing_thickness - self.bearing_diam / 2 + def bearing_disk_gap(self) -> float: + diag = self.bearing_ball_diam + dx = self.bearing_gap + return math.sqrt(diag ** 2 - dx ** 2) + + @target(name="bearing-stator", kind=TargetKind.DXF) + def profile_bearing_stator(self) -> Cq.Sketch: + return ( + Cq.Sketch() + .regularPolygon(self.side_width, self.n_side) + .circle(self.bearing_track_radius + self.bearing_gap/2, mode="s") + ) + def bearing_stator(self) -> Cq.Workplane: + return ( + Cq.Workplane() + .placeSketch(self.profile_bearing_stator()) + .extrude(self.bearing_disk_thickness) + ) + @target(name="bearing-rotor", kind=TargetKind.DXF) + def profile_bearing_rotor(self) -> Cq.Sketch: + return ( + Cq.Sketch() + .circle(self.bearing_track_radius - self.bearing_gap/2) + .regularPolygon(self.rotation_radius, self.n_side) + .vertices() + .circle(self.barrel_diam/2, mode="s") + ) + def bearing_rotor(self) -> Cq.Workplane: + return ( + Cq.Workplane() + .placeSketch(self.profile_bearing_rotor()) + .extrude(self.bearing_disk_thickness) + ) + @target(name="bearing-gasket", kind=TargetKind.DXF) + def profile_bearing_gasket(self) -> Cq.Sketch: + pass + + + @target(name="pipe", kind=TargetKind.DXF) + def pipe(self) -> Cq.Sketch: + pass @target(name="side-panel", kind=TargetKind.DXF) def profile_side_panel(self) -> Cq.Sketch: @@ -86,93 +132,53 @@ class Onbashira(Model): # Intersect the side panel return result * intersector - def bearing_channel(self) -> Cq.Solid: - """ - Generates a toroidal channel for the ball bearings - """ - return Cq.Solid.makeTorus( - radius1=self.bearing_radius, - radius2=self.bearing_diam/2, - ) - @target(name="inner-rotor") - def inner_rotor(self) -> Cq.Workplane: - r_outer = self.bearing_radius - base = Cq.Solid.makeCylinder( - radius=r_outer, - height=self.bearing_height - ).translate(Cq.Vector(0,0,-self.bearing_height/2)) - r_rot = self.rotation_radius - channel = self.bearing_channel() - return ( - Cq.Workplane() - .add(base - channel) - .faces(">Z") - .workplane() - .polygon( - nSides=self.n_side, - diameter=2 * r_rot, - forConstruction=True - ) - .vertices() - .hole(self.barrel_diam) - ) - return base - channel - @target(name="outer-rotor") - def outer_rotor(self) -> Cq.Workplane: - polygon_radius = (self.bulk_radius - self.side_thickness) / math.cos(math.radians(self.angle_side / 2)) - profile = ( - Cq.Sketch() - .regularPolygon( - r=polygon_radius, - n=self.n_side, - ) - ) - inner = Cq.Solid.makeCylinder( - radius=self.bearing_radius, - height=self.bearing_height, - ) - base = ( - Cq.Workplane() - .placeSketch(profile) - .extrude(self.bearing_height) - .cut(inner) - .translate(Cq.Vector(0,0,-self.bearing_height/2)) - .cut(self.bearing_channel()) - ) - r = self.bearing_radius * 2 - subtractor = Cq.Solid.makeBox( - length=r * 2, - width=r * 2, - height=self.bearing_height, - ).translate(Cq.Vector(-r, -r, -self.bearing_height)) - return base - subtractor + + def bearing_ball(self) -> Cq.Solid: return Cq.Solid.makeSphere(radius=self.bearing_ball_diam/2, angleDegrees1=-90) - def assembly_bearing(self) -> Cq.Assembly: + def assembly_rotor(self) -> Cq.Assembly: + z_lower = -self.bearing_disk_gap - self.bearing_disk_thickness a = ( Cq.Assembly() .addS( - self.inner_rotor(), - name="inner", - material=self.material_bearing, - role=Role.ROTOR, - ) - .addS( - self.outer_rotor(), - name="outer", + self.bearing_stator(), + name="stator1", material=self.material_bearing, role=Role.STATOR, + loc=Cq.Location(0, 0, self.bearing_disk_gap/2) + ) + .addS( + self.bearing_rotor(), + name="rotor1", + material=self.material_bearing, + role=Role.ROTOR, + loc=Cq.Location(0, 0, self.bearing_disk_gap/2) + ) + .addS( + self.bearing_stator(), + name="stator2", + material=self.material_bearing, + role=Role.STATOR, + loc=Cq.Location(0, 0, z_lower) + ) + .addS( + self.bearing_rotor(), + name="rotor2", + material=self.material_bearing, + role=Role.ROTOR, + loc=Cq.Location(0, 0, z_lower) ) ) - for i in range(self.n_side): + for i in range(self.n_bearing_balls): ball = self.bearing_ball() + loc = Cq.Location.rot2d(i * 360/self.n_bearing_balls) * Cq.Location(self.bearing_track_radius, 0, 0) a = a.addS( ball, name=f"bearing_ball{i}", material=self.material_bearing_ball, role=Role.BEARING, - loc=Cq.Location.rot2d(i * 360/self.n_side) * Cq.Location(self.bearing_radius, 0, 0), + loc=loc, ) return a @@ -180,12 +186,13 @@ class Onbashira(Model): a = Cq.Assembly() side = self.side_panel() r = self.bulk_radius - for i in range(6): + a = a.add(self.assembly_bearing(), name="bearing") + for i in range(self.n_side): a = a.addS( side, name=f"side{i}", material=self.material_side, role=Role.STRUCTURE | Role.DECORATION, - loc=Cq.Location(0,0,0,0,i*60,0) * Cq.Location(0,0,-r) + loc=Cq.Location.rot2d(i*360/self.n_side) * Cq.Location(-r,0,0,90,0,90), ) return a From 74145f88d2803222f412b324216281ff5871d490 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Mon, 12 May 2025 12:24:33 -0700 Subject: [PATCH 06/59] Add bolts on rotor --- nhf/touhou/yasaka_kanako/onbashira.py | 63 ++++++++++++++++++++++++--- 1 file changed, 56 insertions(+), 7 deletions(-) diff --git a/nhf/touhou/yasaka_kanako/onbashira.py b/nhf/touhou/yasaka_kanako/onbashira.py index 5750abb..6a82d0c 100644 --- a/nhf/touhou/yasaka_kanako/onbashira.py +++ b/nhf/touhou/yasaka_kanako/onbashira.py @@ -25,13 +25,17 @@ class Onbashira(Model): # Size of ball bearings bearing_ball_diam: float = 25.4 * 1/2 bearing_ball_gap: float = .5 - bearing_height: float = 40.0 + # Thickness of bearing disks bearing_thickness: float = 20.0 bearing_track_radius: float = 120.0 # Gap between the inner and outer bearing disks bearing_gap: float = 10.0 bearing_disk_thickness: float = 25.4 / 8 + rotor_bind_bolt_diam: float = 10.0 + rotor_bind_radius: float = 50.0 + stator_bind_radius: float = 150.0 + material_side: Material = Material.WOOD_BIRCH material_bearing: Material = Material.PLASTIC_PLA material_bearing_ball: Material = Material.ACRYLIC_TRANSPARENT @@ -41,8 +45,9 @@ class Onbashira(Model): assert self.n_side >= 3 # Bulk must be large enough for the barrel + bearing to rotate assert self.bulk_radius - self.side_thickness - self.bearing_thickness - self.bearing_diam > self.rotation_radius + self.barrel_diam / 2 - assert self.bearing_height > self.bearing_diam assert self.bearing_gap < 0.95 * self.bearing_ball_diam + assert self.rotor_bind_bolt_diam < self.rotor_bind_radius < self.bearing_track_radius + assert self.bearing_track_radius < self.stator_bind_radius @property def angle_side(self) -> float: @@ -62,6 +67,9 @@ class Onbashira(Model): @property def bearing_disk_gap(self) -> float: + """ + Gap between two bearing disks to touch the bearing balls + """ diag = self.bearing_ball_diam dx = self.bearing_gap return math.sqrt(diag ** 2 - dx ** 2) @@ -72,6 +80,12 @@ class Onbashira(Model): Cq.Sketch() .regularPolygon(self.side_width, self.n_side) .circle(self.bearing_track_radius + self.bearing_gap/2, mode="s") + .reset() + .regularPolygon( + self.stator_bind_radius, self.n_side, + mode="c", tag="bolt") + .vertices(tag="bolt") + .circle(self.rotor_bind_bolt_diam/2, mode="s") ) def bearing_stator(self) -> Cq.Workplane: return ( @@ -81,12 +95,22 @@ class Onbashira(Model): ) @target(name="bearing-rotor", kind=TargetKind.DXF) def profile_bearing_rotor(self) -> Cq.Sketch: + bolt_angle = 180 / self.n_side return ( Cq.Sketch() .circle(self.bearing_track_radius - self.bearing_gap/2) - .regularPolygon(self.rotation_radius, self.n_side) - .vertices() + .reset() + .regularPolygon( + self.rotation_radius, self.n_side, + mode="c", tag="corners") + .vertices(tag="corners") .circle(self.barrel_diam/2, mode="s") + .reset() + .regularPolygon( + self.rotor_bind_radius, self.n_side, + mode="c", tag="bolt", angle=bolt_angle) + .vertices(tag="bolt") + .circle(self.rotor_bind_bolt_diam/2, mode="s") ) def bearing_rotor(self) -> Cq.Workplane: return ( @@ -96,7 +120,25 @@ class Onbashira(Model): ) @target(name="bearing-gasket", kind=TargetKind.DXF) def profile_bearing_gasket(self) -> Cq.Sketch: - pass + dr = self.bearing_ball_diam + eps = 0.05 + return ( + Cq.Sketch() + .circle(self.bearing_track_radius + dr) + .circle(self.bearing_track_radius - dr, mode="s") + .reset() + .regularPolygon( + self.bearing_track_radius, self.n_bearing_balls, + mode="c", tag="corners") + .vertices(tag="corners") + .circle(self.bearing_ball_diam/2 * (1+eps), mode="s") + ) + def bearing_gasket(self) -> Cq.Workplane: + return ( + Cq.Workplane() + .placeSketch(self.profile_bearing_gasket()) + .extrude(self.bearing_disk_thickness) + ) @target(name="pipe", kind=TargetKind.DXF) @@ -138,7 +180,7 @@ class Onbashira(Model): return Cq.Solid.makeSphere(radius=self.bearing_ball_diam/2, angleDegrees1=-90) def assembly_rotor(self) -> Cq.Assembly: - z_lower = -self.bearing_disk_gap - self.bearing_disk_thickness + z_lower = -self.bearing_disk_gap/2 - self.bearing_disk_thickness a = ( Cq.Assembly() .addS( @@ -169,6 +211,13 @@ class Onbashira(Model): role=Role.ROTOR, loc=Cq.Location(0, 0, z_lower) ) + .addS( + self.bearing_gasket(), + name="gasket", + material=self.material_bearing, + role=Role.ROTOR, + loc=Cq.Location(0, 0, -self.bearing_disk_thickness/2) + ) ) for i in range(self.n_bearing_balls): ball = self.bearing_ball() @@ -186,7 +235,7 @@ class Onbashira(Model): a = Cq.Assembly() side = self.side_panel() r = self.bulk_radius - a = a.add(self.assembly_bearing(), name="bearing") + a = a.add(self.assembly_rotor(), name="rotor") for i in range(self.n_side): a = a.addS( side, From 97675a2fc8ed31adeea91d47b56209ae13f51f9e Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Mon, 12 May 2025 14:50:59 -0700 Subject: [PATCH 07/59] Add angle joint stub, hole in rotor --- nhf/touhou/yasaka_kanako/onbashira.py | 122 ++++++++++++++++++++++++-- 1 file changed, 113 insertions(+), 9 deletions(-) diff --git a/nhf/touhou/yasaka_kanako/onbashira.py b/nhf/touhou/yasaka_kanako/onbashira.py index 6a82d0c..9263e4c 100644 --- a/nhf/touhou/yasaka_kanako/onbashira.py +++ b/nhf/touhou/yasaka_kanako/onbashira.py @@ -3,7 +3,7 @@ from nhf.materials import Role, Material import nhf.utils import math -from dataclasses import dataclass +from dataclasses import dataclass, field import cadquery as Cq @dataclass @@ -13,28 +13,43 @@ class Onbashira(Model): # Dimensions of each side panel side_width: float = 200.0 side_length: float = 600.0 - side_thickness: float = 25.4 / 8 + # Joints between two sets of side panels + angle_joint_thickness: float = 25.4 / 4 + # Z-axis size of each angle joint + angle_joint_depth: float = 50.0 + # Gap of each angle joint to connect the outside to the inside + angle_joint_gap: float = 10.0 + angle_joint_bolt_length: float = 50.0 + angle_joint_bolt_diam: float = 10.0 + # Position of the holes, with (0, 0) being the centre of each side + angle_joint_hole_position: list[float] = field(default_factory=lambda: [ + (20, 20), + (70, 20), + ]) + # Dimensions of gun barrels barrel_diam: float = 25.4 * 2 barrel_length: float = 300.0 # Radius from barrel centre to axis - rotation_radius: float = 75.0 + rotation_radius: float = 90.0 n_bearing_balls: int = 24 # Size of ball bearings bearing_ball_diam: float = 25.4 * 1/2 bearing_ball_gap: float = .5 # Thickness of bearing disks bearing_thickness: float = 20.0 - bearing_track_radius: float = 120.0 + bearing_track_radius: float = 135.0 # Gap between the inner and outer bearing disks bearing_gap: float = 10.0 bearing_disk_thickness: float = 25.4 / 8 + rotor_inner_radius: float = 55.0 + rotor_bind_bolt_diam: float = 10.0 - rotor_bind_radius: float = 50.0 - stator_bind_radius: float = 150.0 + rotor_bind_radius: float = 110.0 + stator_bind_radius: float = 170.0 material_side: Material = Material.WOOD_BIRCH material_bearing: Material = Material.PLASTIC_PLA @@ -47,18 +62,37 @@ class Onbashira(Model): assert self.bulk_radius - self.side_thickness - self.bearing_thickness - self.bearing_diam > self.rotation_radius + self.barrel_diam / 2 assert self.bearing_gap < 0.95 * self.bearing_ball_diam assert self.rotor_bind_bolt_diam < self.rotor_bind_radius < self.bearing_track_radius - assert self.bearing_track_radius < self.stator_bind_radius + assert self.rotor_inner_radius < self.bearing_track_radius < self.stator_bind_radius + assert self.angle_joint_thickness > self.side_thickness @property def angle_side(self) -> float: return 360 / self.n_side + @property + def side_width_inner(self) -> float: + """ + Interior side width + + If outer width is `wi`, inner width is `wo`, each side's cross section + is a trapezoid with sides `wi`, `wo`, and height `h` (side thickness) + """ + theta = math.pi / self.n_side + dt = self.side_thickness * math.tan(theta) + return self.side_width - dt * 2 + @property + def angle_joint_extra_width(self) -> float: + theta = math.pi / self.n_side + dt = self.angle_joint_thickness * math.tan(theta) + return dt * 2 + + @property def angle_dihedral(self) -> float: return 180 - self.angle_side @property def bulk_radius(self) -> float: """ - Calculate radius of the bulk to the centre + Radius of the bulk (surface of each side) to the centre """ return self.side_width / 2 / math.tan(math.radians(self.angle_side / 2)) @property @@ -78,7 +112,7 @@ class Onbashira(Model): def profile_bearing_stator(self) -> Cq.Sketch: return ( Cq.Sketch() - .regularPolygon(self.side_width, self.n_side) + .regularPolygon(self.side_width - self.side_thickness, self.n_side) .circle(self.bearing_track_radius + self.bearing_gap/2, mode="s") .reset() .regularPolygon( @@ -99,6 +133,7 @@ class Onbashira(Model): return ( Cq.Sketch() .circle(self.bearing_track_radius - self.bearing_gap/2) + .circle(self.rotor_inner_radius, mode="s") .reset() .regularPolygon( self.rotation_radius, self.n_side, @@ -143,6 +178,9 @@ class Onbashira(Model): @target(name="pipe", kind=TargetKind.DXF) def pipe(self) -> Cq.Sketch: + """ + The rotating pipes. Purely for decoration + """ pass @target(name="side-panel", kind=TargetKind.DXF) @@ -160,6 +198,7 @@ class Onbashira(Model): .placeSketch(self.profile_side_panel()) .extrude(self.side_thickness) ) + # Bevel the edges intersector = ( Cq.Workplane('XZ') .polyline([ @@ -174,6 +213,71 @@ class Onbashira(Model): # Intersect the side panel return result * intersector + def angle_joint(self) -> Cq.Workplane: + """ + Angular joint between two side panels. This sits at the intersection of + 4 side panels to provide compressive, shear, and tensile strength. + + To provide tensile strength along the Z-axis, the panels must be bolted + onto the angle joint. + """ + + # Create the slot carving + slot = ( + Cq.Sketch() + .regularPolygon( + self.side_width, + self.n_side + ) + .regularPolygon( + self.side_width_inner, + self.n_side, mode="s", + ) + ) + slot = ( + Cq.Workplane() + .placeSketch(slot) + .extrude(self.angle_joint_depth) + ) + + # Construct the overall shape of the joint, and divide it into sections for printing later. + sketch = ( + Cq.Sketch() + .regularPolygon( + self.side_width + self.angle_joint_extra_width, + self.n_side + ) + .regularPolygon( + self.side_width - self.angle_joint_extra_width, + self.n_side, mode="s" + ) + ) + + h = (self.bulk_radius + self.angle_joint_extra_width) * 2 + # Intersector for 1/n of the ring + intersector = ( + Cq.Workplane() + .sketch() + .polygon([ + (0, 0), + (h, 0), + (h, h * math.tan(2 * math.pi / self.n_side)) + ]) + .finalize() + .extrude(self.angle_joint_depth*4) + .translate((0, 0, -self.angle_joint_depth*2)) + ) + result = ( + Cq.Workplane() + .placeSketch(sketch) + .extrude(self.angle_joint_depth) + .translate((0, 0, -self.angle_joint_depth/2)) + .cut(slot.translate((0, 0, self.angle_joint_gap/2))) + .cut(slot.translate((0, 0, -self.angle_joint_depth-self.angle_joint_gap/2))) + .intersect(intersector) + ) + return result + def bearing_ball(self) -> Cq.Solid: From 4dcd97613b9c5814ba86326d59f473b46480a90c Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Mon, 12 May 2025 22:08:44 -0700 Subject: [PATCH 08/59] Section bracing --- nhf/touhou/yasaka_kanako/onbashira.py | 177 +++++++++++++++++++++++--- nhf/utils.py | 9 ++ 2 files changed, 166 insertions(+), 20 deletions(-) diff --git a/nhf/touhou/yasaka_kanako/onbashira.py b/nhf/touhou/yasaka_kanako/onbashira.py index 9263e4c..76610ca 100644 --- a/nhf/touhou/yasaka_kanako/onbashira.py +++ b/nhf/touhou/yasaka_kanako/onbashira.py @@ -12,21 +12,26 @@ class Onbashira(Model): n_side: int = 6 # Dimensions of each side panel side_width: float = 200.0 - side_length: float = 600.0 + + # Side panels have different lengths + side_length1: float = 200.0 + side_length2: float = 350.0 + side_length3: float = 400.0 + side_thickness: float = 25.4 / 8 # Joints between two sets of side panels - angle_joint_thickness: float = 25.4 / 4 + angle_joint_thickness: float = 10.0 # Z-axis size of each angle joint - angle_joint_depth: float = 50.0 + angle_joint_depth: float = 60.0 # Gap of each angle joint to connect the outside to the inside angle_joint_gap: float = 10.0 angle_joint_bolt_length: float = 50.0 angle_joint_bolt_diam: float = 10.0 # Position of the holes, with (0, 0) being the centre of each side - angle_joint_hole_position: list[float] = field(default_factory=lambda: [ - (20, 20), - (70, 20), + angle_joint_bolt_position: list[float] = field(default_factory=lambda: [ + (20, 15), + (70, 15), ]) # Dimensions of gun barrels @@ -54,7 +59,7 @@ class Onbashira(Model): material_side: Material = Material.WOOD_BIRCH material_bearing: Material = Material.PLASTIC_PLA material_bearing_ball: Material = Material.ACRYLIC_TRANSPARENT - material_brace: Material = Material.METAL_AL + material_brace: Material = Material.PLASTIC_PLA def __post_init__(self): assert self.n_side >= 3 @@ -65,6 +70,9 @@ class Onbashira(Model): assert self.rotor_inner_radius < self.bearing_track_radius < self.stator_bind_radius assert self.angle_joint_thickness > self.side_thickness + for (x, y) in self.angle_joint_bolt_position: + assert y < self.angle_joint_depth / 2 + @property def angle_side(self) -> float: return 360 / self.n_side @@ -183,19 +191,35 @@ class Onbashira(Model): """ pass - @target(name="side-panel", kind=TargetKind.DXF) - def profile_side_panel(self) -> Cq.Sketch: + def profile_side_panel( + self, + length: float, + hasFrontHole: bool = False, + hasBackHole: bool = True) -> Cq.Sketch: + assert hasFrontHole or hasBackHole + signs = ([1] if hasFrontHole else []) + ([-1] if hasBackHole else []) return ( Cq.Sketch() - .rect(self.side_width, self.side_length) + .rect(self.side_width, length) + .push([ + (sx * x, sy * (length/2 - y)) + for (x, y) in self.angle_joint_bolt_position + for sx in [1, -1] + for sy in signs + ]) + .circle(self.angle_joint_bolt_diam/2, mode="s") ) - def side_panel(self) -> Cq.Workplane: + def side_panel(self, length: float, hasFrontHole: bool = True, hasBackHole: bool = True) -> Cq.Workplane: w = self.side_width - l = self.side_length + sketch = self.profile_side_panel( + length=length, + hasFrontHole=hasFrontHole, + hasBackHole=hasBackHole, + ) result = ( Cq.Workplane() - .placeSketch(self.profile_side_panel()) + .placeSketch(sketch) .extrude(self.side_thickness) ) # Bevel the edges @@ -207,11 +231,27 @@ class Onbashira(Model): (0, self.bulk_radius), ]) .close() - .extrude(l) - .translate(Cq.Vector(0, l/2,0)) + .extrude(length) + .translate(Cq.Vector(0, length/2, 0)) ) # Intersect the side panel - return result * intersector + result = result * intersector + + # Mark all attachment points + t = self.side_thickness + for i, (x, y) in enumerate(self.angle_joint_bolt_position): + px = x + py = length / 2 - y + result.tagAbsolute(f"holeFPI{i}", (+px, py, t), direction="+Z") + result.tagAbsolute(f"holeFSI{i}", (-px, py, t), direction="+Z") + result.tagAbsolute(f"holeFPO{i}", (+px, py, 0), direction="-Z") + result.tagAbsolute(f"holeFSO{i}", (-px, py, 0), direction="-Z") + result.tagAbsolute(f"holeBPI{i}", (+px, -py, t), direction="+Z") + result.tagAbsolute(f"holeBSI{i}", (-px, -py, t), direction="+Z") + result.tagAbsolute(f"holeBPO{i}", (+px, -py, 0), direction="-Z") + result.tagAbsolute(f"holeBSO{i}", (-px, -py, 0), direction="-Z") + + return result def angle_joint(self) -> Cq.Workplane: """ @@ -220,6 +260,10 @@ class Onbashira(Model): To provide tensile strength along the Z-axis, the panels must be bolted onto the angle joint. + + The holes are marked hole(L/R)(P/S)(O/I)(i), where L/R corresponds to the two + sections being joined, and P/S corresponds to the two facets + (primary/secondary) being joined. O/I corresponds to the outside/inside """ # Create the slot carving @@ -276,10 +320,45 @@ class Onbashira(Model): .cut(slot.translate((0, 0, -self.angle_joint_depth-self.angle_joint_gap/2))) .intersect(intersector) ) + hole_negative = Cq.Solid.makeCylinder( + radius=self.angle_joint_bolt_diam/2, + height=h, + pnt=(0,0,0), + dir=(1,0,0), + ) + dy = self.angle_joint_gap / 2 + locrot = Cq.Location(0, 0, 0, 0, 0, 360/self.n_side) + for (x, y) in self.angle_joint_bolt_position: + p1 = Cq.Location((0, x, dy+y)) + p2 = Cq.Location((0, x, -dy-y)) + p1r = locrot * Cq.Location((0, -x, dy+y)) + p2r = locrot * Cq.Location((0, -x, -dy-y)) + result = result \ + - hole_negative.moved(p1) \ + - hole_negative.moved(p2) \ + - hole_negative.moved(p1r) \ + - hole_negative.moved(p2r) + # Mark the absolute locations of the mount points + dr = self.bulk_radius + self.angle_joint_thickness + dr0 = self.bulk_radius + dri = self.bulk_radius - self.angle_joint_thickness + for i, (x, y) in enumerate(self.angle_joint_bolt_position): + py = dy + y + result.tagAbsolute(f"holeLPO{i}", (dr, x, py), direction="+X") + result.tagAbsolute(f"holeRPO{i}", (dr, x, -py), direction="+X") + result.tagAbsolute(f"holeLPM{i}", (dr0, x, py), direction="-X") + result.tagAbsolute(f"holeRPM{i}", (dr0, x, -py), direction="-X") + result.tagAbsolute(f"holeLPI{i}", (dri, x, py), direction="-X") + result.tagAbsolute(f"holeRPI{i}", (dri, x, -py), direction="-X") + result.tagAbsolute(f"holeLSO{i}", locrot * Cq.Location(dr, -x, py), direction="+X") + result.tagAbsolute(f"holeRSO{i}", locrot * Cq.Location(dr, -x, -py), direction="+X") + result.tagAbsolute(f"holeLSM{i}", locrot * Cq.Location(dr0, -x, py), direction="-X") + result.tagAbsolute(f"holeRSM{i}", locrot * Cq.Location(dr0, -x, -py), direction="-X") + result.tagAbsolute(f"holeLSI{i}", locrot * Cq.Location(dri, -x, py), direction="-X") + result.tagAbsolute(f"holeRSI{i}", locrot * Cq.Location(dri, -x, -py), direction="-X") return result - def bearing_ball(self) -> Cq.Solid: return Cq.Solid.makeSphere(radius=self.bearing_ball_diam/2, angleDegrees1=-90) @@ -335,11 +414,10 @@ class Onbashira(Model): ) return a - def assembly(self) -> Cq.Assembly: + def assembly_section(self, **kwargs) -> Cq.Assembly: a = Cq.Assembly() - side = self.side_panel() + side = self.side_panel(**kwargs) r = self.bulk_radius - a = a.add(self.assembly_rotor(), name="rotor") for i in range(self.n_side): a = a.addS( side, @@ -349,3 +427,62 @@ class Onbashira(Model): loc=Cq.Location.rot2d(i*360/self.n_side) * Cq.Location(-r,0,0,90,0,90), ) return a + def assembly_ring(self) -> Cq.Assembly: + a = Cq.Assembly() + side = self.angle_joint() + r = self.bulk_radius + for i in range(self.n_side): + a = a.addS( + side, + name=f"side{i}", + material=self.material_brace, + role=Role.CASING | Role.DECORATION, + loc=Cq.Location.rot2d(i*360/self.n_side), + ) + return a + + def assembly(self) -> Cq.Assembly: + a = Cq.Assembly() + a = ( + a + .add( + self.assembly_section(length=self.side_length1, hasFrontHole=False, hasBackHole=True), + name="section1", + ) + .add( + self.assembly_ring(), + name="ring1", + ) + .add( + self.assembly_section(length=self.side_length2, hasFrontHole=True, hasBackHole=True), + name="section2", + ) + .add( + self.assembly_ring(), + name="ring2", + ) + .add( + self.assembly_section(length=self.side_length3, hasFrontHole=True, hasBackHole=True), + name="section3", + ) + ) + for (nl, nc, nr) in [ + ("section1", "ring1", "section2"), + ("section2", "ring2", "section3"), + ]: + for i in range(self.n_side): + j = (i + 1) % self.n_side + for ih in range(len(self.angle_joint_bolt_position)): + a = a.constrain( + f"{nl}/side{i}?holeBSO{ih}", + f"{nc}/side{i}?holeLPM{ih}", + "Plane", + ) + a = a.constrain( + f"{nr}/side{i}?holeFPO{ih}", + f"{nc}/side{i}?holeRSM{ih}", + "Plane", + ) + + a = a.add(self.assembly_rotor(), name="rotor") + return a.solve() diff --git a/nhf/utils.py b/nhf/utils.py index 0c83db7..1234796 100644 --- a/nhf/utils.py +++ b/nhf/utils.py @@ -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 From 44cd6ee960a157a9bc6e8914dbb1f236be3119eb Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Mon, 12 May 2025 23:33:20 -0700 Subject: [PATCH 09/59] Onbashira dimension update and flanges --- nhf/touhou/yasaka_kanako/onbashira.py | 64 ++++++++++++++++++++++----- 1 file changed, 54 insertions(+), 10 deletions(-) diff --git a/nhf/touhou/yasaka_kanako/onbashira.py b/nhf/touhou/yasaka_kanako/onbashira.py index 76610ca..9418628 100644 --- a/nhf/touhou/yasaka_kanako/onbashira.py +++ b/nhf/touhou/yasaka_kanako/onbashira.py @@ -11,12 +11,13 @@ class Onbashira(Model): n_side: int = 6 # Dimensions of each side panel - side_width: float = 200.0 + side_width: float = 170.0 # Side panels have different lengths side_length1: float = 200.0 side_length2: float = 350.0 side_length3: float = 400.0 + side_length4: float = 400.0 side_thickness: float = 25.4 / 8 @@ -31,30 +32,32 @@ class Onbashira(Model): # Position of the holes, with (0, 0) being the centre of each side angle_joint_bolt_position: list[float] = field(default_factory=lambda: [ (20, 15), - (70, 15), + (50, 15), ]) + angle_joint_flange_thickness: float = 7.8 + angle_joint_flange_radius: float = 40.0 # Dimensions of gun barrels barrel_diam: float = 25.4 * 2 barrel_length: float = 300.0 # Radius from barrel centre to axis - rotation_radius: float = 90.0 + rotation_radius: float = 75.0 n_bearing_balls: int = 24 # Size of ball bearings bearing_ball_diam: float = 25.4 * 1/2 bearing_ball_gap: float = .5 # Thickness of bearing disks bearing_thickness: float = 20.0 - bearing_track_radius: float = 135.0 + bearing_track_radius: float = 110.0 # Gap between the inner and outer bearing disks bearing_gap: float = 10.0 bearing_disk_thickness: float = 25.4 / 8 - rotor_inner_radius: float = 55.0 + rotor_inner_radius: float = 40.0 rotor_bind_bolt_diam: float = 10.0 - rotor_bind_radius: float = 110.0 - stator_bind_radius: float = 170.0 + rotor_bind_radius: float = 85.0 + stator_bind_radius: float = 140.0 material_side: Material = Material.WOOD_BIRCH material_bearing: Material = Material.PLASTIC_PLA @@ -253,6 +256,7 @@ class Onbashira(Model): return result + @target(name="angle-joint") def angle_joint(self) -> Cq.Workplane: """ Angular joint between two side panels. This sits at the intersection of @@ -358,6 +362,37 @@ class Onbashira(Model): result.tagAbsolute(f"holeRSI{i}", locrot * Cq.Location(dri, -x, -py), direction="-X") return result + @target(name="angle-joint-flanged") + def angle_joint_flanged(self) -> Cq.Workplane: + result = self.angle_joint() + th = math.pi / self.n_side + r = self.bulk_radius + flange = ( + Cq.Sketch() + .push([ + (r, r * math.tan(th)) + ]) + .circle(self.angle_joint_flange_radius) + .reset() + .regularPolygon(self.side_width_inner, self.n_side, mode="i") + ) + flange = ( + Cq.Workplane() + .placeSketch(flange) + .extrude(self.angle_joint_flange_thickness) + .translate((0, 0, -self.angle_joint_flange_thickness/2)) + ) + ri = self.stator_bind_radius + h = self.angle_joint_flange_thickness + cyl = Cq.Solid.makeCylinder( + radius=self.rotor_bind_bolt_diam/2, + height=h, + pnt=(ri * math.cos(th), ri * math.sin(th), -h/2), + ) + result = result + flange - cyl + result.tagAbsolute("holeStatorL", (ri * math.cos(th), ri * math.sin(th), h/2), direction="+Z") + result.tagAbsolute("holeStatorR", (ri * math.cos(th), ri * math.sin(th), -h/2), direction="-Z") + return result def bearing_ball(self) -> Cq.Solid: return Cq.Solid.makeSphere(radius=self.bearing_ball_diam/2, angleDegrees1=-90) @@ -427,9 +462,9 @@ class Onbashira(Model): loc=Cq.Location.rot2d(i*360/self.n_side) * Cq.Location(-r,0,0,90,0,90), ) return a - def assembly_ring(self) -> Cq.Assembly: + def assembly_ring(self, flanged=False) -> Cq.Assembly: a = Cq.Assembly() - side = self.angle_joint() + side = self.angle_joint_flanged() if flanged else self.angle_joint() r = self.bulk_radius for i in range(self.n_side): a = a.addS( @@ -450,7 +485,7 @@ class Onbashira(Model): name="section1", ) .add( - self.assembly_ring(), + self.assembly_ring(flanged=True), name="ring1", ) .add( @@ -465,10 +500,19 @@ class Onbashira(Model): self.assembly_section(length=self.side_length3, hasFrontHole=True, hasBackHole=True), name="section3", ) + .add( + self.assembly_ring(), + name="ring3", + ) + .add( + self.assembly_section(length=self.side_length4, hasFrontHole=True, hasBackHole=False), + name="section4", + ) ) for (nl, nc, nr) in [ ("section1", "ring1", "section2"), ("section2", "ring2", "section3"), + ("section3", "ring3", "section4"), ]: for i in range(self.n_side): j = (i + 1) % self.n_side From 916ccee2608a97aad27f69665512db2a4fe0db02 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Mon, 12 May 2025 23:52:45 -0700 Subject: [PATCH 10/59] Centre holes --- nhf/touhou/yasaka_kanako/onbashira.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nhf/touhou/yasaka_kanako/onbashira.py b/nhf/touhou/yasaka_kanako/onbashira.py index 9418628..1eb3580 100644 --- a/nhf/touhou/yasaka_kanako/onbashira.py +++ b/nhf/touhou/yasaka_kanako/onbashira.py @@ -31,8 +31,8 @@ class Onbashira(Model): angle_joint_bolt_diam: float = 10.0 # Position of the holes, with (0, 0) being the centre of each side angle_joint_bolt_position: list[float] = field(default_factory=lambda: [ - (20, 15), - (50, 15), + (20, 10), + (60, 10), ]) angle_joint_flange_thickness: float = 7.8 angle_joint_flange_radius: float = 40.0 From ca606c6bc1249ff6e25e43bcf3f0631a2173518b Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Mon, 12 May 2025 23:59:34 -0700 Subject: [PATCH 11/59] Fix rotation radius --- nhf/touhou/yasaka_kanako/onbashira.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nhf/touhou/yasaka_kanako/onbashira.py b/nhf/touhou/yasaka_kanako/onbashira.py index 1eb3580..3007027 100644 --- a/nhf/touhou/yasaka_kanako/onbashira.py +++ b/nhf/touhou/yasaka_kanako/onbashira.py @@ -38,7 +38,7 @@ class Onbashira(Model): angle_joint_flange_radius: float = 40.0 # Dimensions of gun barrels - barrel_diam: float = 25.4 * 2 + barrel_diam: float = 25.4 * 1.5 barrel_length: float = 300.0 # Radius from barrel centre to axis rotation_radius: float = 75.0 From b88d52f4beef64b1c8b36e1903596d6b574f4b72 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Tue, 13 May 2025 00:02:43 -0700 Subject: [PATCH 12/59] Use 8mm bolt --- nhf/touhou/yasaka_kanako/onbashira.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nhf/touhou/yasaka_kanako/onbashira.py b/nhf/touhou/yasaka_kanako/onbashira.py index 3007027..46b1367 100644 --- a/nhf/touhou/yasaka_kanako/onbashira.py +++ b/nhf/touhou/yasaka_kanako/onbashira.py @@ -28,7 +28,7 @@ class Onbashira(Model): # Gap of each angle joint to connect the outside to the inside angle_joint_gap: float = 10.0 angle_joint_bolt_length: float = 50.0 - angle_joint_bolt_diam: float = 10.0 + angle_joint_bolt_diam: float = 8.0 # Position of the holes, with (0, 0) being the centre of each side angle_joint_bolt_position: list[float] = field(default_factory=lambda: [ (20, 10), @@ -55,7 +55,7 @@ class Onbashira(Model): rotor_inner_radius: float = 40.0 - rotor_bind_bolt_diam: float = 10.0 + rotor_bind_bolt_diam: float = 8.0 rotor_bind_radius: float = 85.0 stator_bind_radius: float = 140.0 From 5b5ccee94e701409c6cbd251652f0022034b1056 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Tue, 13 May 2025 09:11:28 -0700 Subject: [PATCH 13/59] Optimize angle joint geometry; Mirror stub --- nhf/touhou/yasaka_kanako/__init__.py | 5 ++++ nhf/touhou/yasaka_kanako/mirror.py | 43 +++++++++++++++++++++++++++ nhf/touhou/yasaka_kanako/onbashira.py | 9 +++--- 3 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 nhf/touhou/yasaka_kanako/mirror.py diff --git a/nhf/touhou/yasaka_kanako/__init__.py b/nhf/touhou/yasaka_kanako/__init__.py index 01fde9d..ab34d4e 100644 --- a/nhf/touhou/yasaka_kanako/__init__.py +++ b/nhf/touhou/yasaka_kanako/__init__.py @@ -2,12 +2,14 @@ from dataclasses import dataclass, field import cadquery as Cq from nhf.build import Model, TargetKind, target, assembly, submodel import nhf.touhou.yasaka_kanako.onbashira as MO +import nhf.touhou.yasaka_kanako.mirror as MM import nhf.utils @dataclass class Parameters(Model): onbashira: MO.Onbashira = field(default_factory=lambda: MO.Onbashira()) + mirror: MM.Mirror = field(default_factory=lambda: MM.Mirror()) def __post_init__(self): super().__init__(name="yasaka-kanako") @@ -15,6 +17,9 @@ class Parameters(Model): @submodel(name="onbashira") def submodel_onbashira(self) -> Model: return self.onbashira + @submodel(name="mirror") + def submodel_mirror(self) -> Model: + return self.mirror if __name__ == '__main__': diff --git a/nhf/touhou/yasaka_kanako/mirror.py b/nhf/touhou/yasaka_kanako/mirror.py new file mode 100644 index 0000000..25eef43 --- /dev/null +++ b/nhf/touhou/yasaka_kanako/mirror.py @@ -0,0 +1,43 @@ +from dataclasses import dataclass, field +import cadquery as Cq +from nhf.build import Model, TargetKind, target, assembly, submodel +import nhf.touhou.yasaka_kanako.onbashira as MO +import nhf.utils + +@dataclass +class Mirror(Model): + """ + Kanako's mirror, made of three levels. + """ + width: float = 50.0 + height: float = 70.0 + + wall_thickness: float = 10.0 + + @target(name="core", kind=TargetKind.DXF) + def profile_core(self) -> Cq.Sketch: + return Cq.Sketch().ellipse(self.width/2, self.height/2) + + @target(name="casing-bot", kind=TargetKind.DXF) + def profile_casing(self) -> Cq.Sketch: + """ + Base of the casing with no holes carved out + """ + dx = self.wall_thickness + return ( + Cq.Sketch() + .ellipse(self.width/2+dx, self.height/2+dx) + ) + @target(name="casing-top", kind=TargetKind.DXF) + def profile_casing_top(self) -> Cq.Sketch: + """ + Base of the casing with no holes carved out + """ + return ( + self.profile_casing() + .ellipse(self.width/2, self.height/2, mode="s") + ) + + @assembly() + def assembly(self) -> Cq.Assembly: + pass diff --git a/nhf/touhou/yasaka_kanako/onbashira.py b/nhf/touhou/yasaka_kanako/onbashira.py index 46b1367..4ce8bb0 100644 --- a/nhf/touhou/yasaka_kanako/onbashira.py +++ b/nhf/touhou/yasaka_kanako/onbashira.py @@ -277,10 +277,10 @@ class Onbashira(Model): self.side_width, self.n_side ) - .regularPolygon( - self.side_width_inner, - self.n_side, mode="s", - ) + #.regularPolygon( + # self.side_width_inner, + # self.n_side, mode="s", + #) ) slot = ( Cq.Workplane() @@ -476,6 +476,7 @@ class Onbashira(Model): ) return a + @assembly() def assembly(self) -> Cq.Assembly: a = Cq.Assembly() a = ( From a684996475c34b18b2af78779c4608563b1c4f83 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Tue, 13 May 2025 14:29:10 -0700 Subject: [PATCH 14/59] Geometry of mirror and rotor spacer --- nhf/touhou/yasaka_kanako/mirror.py | 167 ++++++++++++++++++++++++-- nhf/touhou/yasaka_kanako/onbashira.py | 125 ++++++++++--------- 2 files changed, 224 insertions(+), 68 deletions(-) diff --git a/nhf/touhou/yasaka_kanako/mirror.py b/nhf/touhou/yasaka_kanako/mirror.py index 25eef43..c8133d9 100644 --- a/nhf/touhou/yasaka_kanako/mirror.py +++ b/nhf/touhou/yasaka_kanako/mirror.py @@ -1,6 +1,7 @@ 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 @@ -8,36 +9,178 @@ import nhf.utils 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 = 50.0 - height: float = 70.0 + height: float = 60.0 - wall_thickness: float = 10.0 + inner_gap: float = 3.0 + outer_gap: float = 3.0 + + core_thickness: float = 25.4 / 8 + casing_thickness: float = 25.4 / 16 + + flange_r0: float = 5.0 + flange_r1: float = 15.0 + flange_y1: float = 12.0 + flange_y2: float = 25.0 + flange_hole_r: float = 8.0 + wing_x1: float = 12.0 + wing_x2: float = 20.0 + wing_r1: float = 6.0 + wing_r2: float = 12.0 + tail_r0: float = 5.0 + tail_r1: float = 10.0 + tail_y1: float = 12.0 + tail_y2: float = 25.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: - return Cq.Sketch().ellipse(self.width/2, self.height/2) + 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(self) -> Cq.Sketch: + def profile_casing_bot(self) -> Cq.Sketch: """ Base of the casing with no holes carved out """ - dx = self.wall_thickness return ( Cq.Sketch() - .ellipse(self.width/2+dx, self.height/2+dx) + .ellipse(self.width/2, self.height/2) + ) + 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: + yt = self.height / 2 - self.outer_gap + rx = self.width/2 - self.outer_gap + ry = 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 ( + self.profile_casing_bot() + .ellipse(rx, ry, mode="s") + .boolean(flange, mode="a") + .boolean(tail, mode="a") + .boolean(self.profile_wing(-1), mode="a") + .boolean(self.profile_wing(1), mode="a") + ) + 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: - """ - Base of the casing with no holes carved out - """ + rx = self.width/2 - self.outer_gap - self.inner_gap + ry = self.height/2 - self.outer_gap - self.inner_gap return ( - self.profile_casing() - .ellipse(self.width/2, self.height/2, mode="s") + 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: - pass + 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) + ) + ) diff --git a/nhf/touhou/yasaka_kanako/onbashira.py b/nhf/touhou/yasaka_kanako/onbashira.py index 4ce8bb0..03ad4f6 100644 --- a/nhf/touhou/yasaka_kanako/onbashira.py +++ b/nhf/touhou/yasaka_kanako/onbashira.py @@ -54,9 +54,9 @@ class Onbashira(Model): bearing_disk_thickness: float = 25.4 / 8 rotor_inner_radius: float = 40.0 - rotor_bind_bolt_diam: float = 8.0 rotor_bind_radius: float = 85.0 + rotor_spacer_outer_diam: float = 15.0 stator_bind_radius: float = 140.0 material_side: Material = Material.WOOD_BIRCH @@ -194,6 +194,74 @@ class Onbashira(Model): """ pass + def bearing_ball(self) -> Cq.Solid: + return Cq.Solid.makeSphere(radius=self.bearing_ball_diam/2, angleDegrees1=-90) + + @target(name="rotor-spacer") + def rotor_spacer(self) -> Cq.Solid: + outer = Cq.Solid.makeCylinder( + radius=self.rotor_spacer_outer_diam/2, + height=self.bearing_disk_gap, + ) + inner = Cq.Solid.makeCylinder( + radius=self.rotor_bind_bolt_diam/2, + height=self.bearing_disk_gap + ) + return outer - inner + + def assembly_rotor(self) -> Cq.Assembly: + z_lower = -self.bearing_disk_gap/2 - self.bearing_disk_thickness + a = ( + Cq.Assembly() + .addS( + self.bearing_stator(), + name="stator1", + material=self.material_bearing, + role=Role.STATOR, + loc=Cq.Location(0, 0, self.bearing_disk_gap/2) + ) + .addS( + self.bearing_rotor(), + name="rotor1", + material=self.material_bearing, + role=Role.ROTOR, + loc=Cq.Location(0, 0, self.bearing_disk_gap/2) + ) + .addS( + self.bearing_stator(), + name="stator2", + material=self.material_bearing, + role=Role.STATOR, + loc=Cq.Location(0, 0, z_lower) + ) + .addS( + self.bearing_rotor(), + name="rotor2", + material=self.material_bearing, + role=Role.ROTOR, + loc=Cq.Location(0, 0, z_lower) + ) + .addS( + self.bearing_gasket(), + name="gasket", + material=self.material_bearing, + role=Role.ROTOR, + loc=Cq.Location(0, 0, -self.bearing_disk_thickness/2) + ) + ) + for i in range(self.n_bearing_balls): + ball = self.bearing_ball() + loc = Cq.Location.rot2d(i * 360/self.n_bearing_balls) * Cq.Location(self.bearing_track_radius, 0, 0) + a = a.addS( + ball, + name=f"bearing_ball{i}", + material=self.material_bearing_ball, + role=Role.BEARING, + loc=loc, + ) + return a + + def profile_side_panel( self, length: float, @@ -394,61 +462,6 @@ class Onbashira(Model): result.tagAbsolute("holeStatorR", (ri * math.cos(th), ri * math.sin(th), -h/2), direction="-Z") return result - def bearing_ball(self) -> Cq.Solid: - return Cq.Solid.makeSphere(radius=self.bearing_ball_diam/2, angleDegrees1=-90) - - def assembly_rotor(self) -> Cq.Assembly: - z_lower = -self.bearing_disk_gap/2 - self.bearing_disk_thickness - a = ( - Cq.Assembly() - .addS( - self.bearing_stator(), - name="stator1", - material=self.material_bearing, - role=Role.STATOR, - loc=Cq.Location(0, 0, self.bearing_disk_gap/2) - ) - .addS( - self.bearing_rotor(), - name="rotor1", - material=self.material_bearing, - role=Role.ROTOR, - loc=Cq.Location(0, 0, self.bearing_disk_gap/2) - ) - .addS( - self.bearing_stator(), - name="stator2", - material=self.material_bearing, - role=Role.STATOR, - loc=Cq.Location(0, 0, z_lower) - ) - .addS( - self.bearing_rotor(), - name="rotor2", - material=self.material_bearing, - role=Role.ROTOR, - loc=Cq.Location(0, 0, z_lower) - ) - .addS( - self.bearing_gasket(), - name="gasket", - material=self.material_bearing, - role=Role.ROTOR, - loc=Cq.Location(0, 0, -self.bearing_disk_thickness/2) - ) - ) - for i in range(self.n_bearing_balls): - ball = self.bearing_ball() - loc = Cq.Location.rot2d(i * 360/self.n_bearing_balls) * Cq.Location(self.bearing_track_radius, 0, 0) - a = a.addS( - ball, - name=f"bearing_ball{i}", - material=self.material_bearing_ball, - role=Role.BEARING, - loc=loc, - ) - return a - def assembly_section(self, **kwargs) -> Cq.Assembly: a = Cq.Assembly() side = self.side_panel(**kwargs) From 22a4f4ceec9bc68704e66ba1da6a87c4b6d403c6 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Tue, 13 May 2025 17:20:51 -0700 Subject: [PATCH 15/59] Mirror wing geometry --- README.md | 3 ++ nhf/touhou/yasaka_kanako/mirror.py | 64 +++++++++++++++--------------- 2 files changed, 35 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 1ade17c..e254ea7 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/nhf/touhou/yasaka_kanako/mirror.py b/nhf/touhou/yasaka_kanako/mirror.py index c8133d9..6ba5242 100644 --- a/nhf/touhou/yasaka_kanako/mirror.py +++ b/nhf/touhou/yasaka_kanako/mirror.py @@ -21,7 +21,7 @@ class Mirror(Model): outer_gap: float = 3.0 core_thickness: float = 25.4 / 8 - casing_thickness: float = 25.4 / 16 + casing_thickness: float = 25.4 / 8 flange_r0: float = 5.0 flange_r1: float = 15.0 @@ -61,36 +61,7 @@ class Mirror(Model): """ Base of the casing with no holes carved out """ - return ( - Cq.Sketch() - .ellipse(self.width/2, self.height/2) - ) - 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: yt = self.height / 2 - self.outer_gap - rx = self.width/2 - self.outer_gap - ry = self.height/2 - self.outer_gap yh = (self.flange_y1 + self.flange_y2) / 2 flange = ( Cq.Sketch() @@ -124,13 +95,42 @@ class Mirror(Model): ]) ) return ( - self.profile_casing_bot() - .ellipse(rx, ry, mode="s") + 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() From 670d4a8c2134ba335ec6e77851f0f4ca25046c44 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Wed, 14 May 2025 13:13:44 -0700 Subject: [PATCH 16/59] Improve geometry of angle joint --- nhf/touhou/yasaka_kanako/onbashira.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/nhf/touhou/yasaka_kanako/onbashira.py b/nhf/touhou/yasaka_kanako/onbashira.py index 03ad4f6..afcd26a 100644 --- a/nhf/touhou/yasaka_kanako/onbashira.py +++ b/nhf/touhou/yasaka_kanako/onbashira.py @@ -28,7 +28,9 @@ class Onbashira(Model): # Gap of each angle joint to connect the outside to the inside angle_joint_gap: float = 10.0 angle_joint_bolt_length: float = 50.0 - angle_joint_bolt_diam: float = 8.0 + angle_joint_bolt_diam: float = 6.0 + angle_joint_bolt_head_diam: float = 13.0 + angle_joint_bolt_head_depth: float = 3.0 # Position of the holes, with (0, 0) being the centre of each side angle_joint_bolt_position: list[float] = field(default_factory=lambda: [ (20, 10), @@ -54,7 +56,7 @@ class Onbashira(Model): bearing_disk_thickness: float = 25.4 / 8 rotor_inner_radius: float = 40.0 - rotor_bind_bolt_diam: float = 8.0 + rotor_bind_bolt_diam: float = 6.0 rotor_bind_radius: float = 85.0 rotor_spacer_outer_diam: float = 15.0 stator_bind_radius: float = 140.0 @@ -364,7 +366,7 @@ class Onbashira(Model): self.n_side ) .regularPolygon( - self.side_width - self.angle_joint_extra_width, + self.side_width_inner, self.n_side, mode="s" ) ) @@ -392,11 +394,17 @@ class Onbashira(Model): .cut(slot.translate((0, 0, -self.angle_joint_depth-self.angle_joint_gap/2))) .intersect(intersector) ) + h = self.bulk_radius + self.angle_joint_thickness hole_negative = Cq.Solid.makeCylinder( radius=self.angle_joint_bolt_diam/2, height=h, pnt=(0,0,0), dir=(1,0,0), + ) + Cq.Solid.makeCylinder( + radius=self.angle_joint_bolt_head_diam/2, + height=self.angle_joint_bolt_head_depth, + pnt=(h,0,0), + dir=(-1,0,0), ) dy = self.angle_joint_gap / 2 locrot = Cq.Location(0, 0, 0, 0, 0, 360/self.n_side) @@ -413,21 +421,16 @@ class Onbashira(Model): # Mark the absolute locations of the mount points dr = self.bulk_radius + self.angle_joint_thickness dr0 = self.bulk_radius - dri = self.bulk_radius - self.angle_joint_thickness for i, (x, y) in enumerate(self.angle_joint_bolt_position): py = dy + y result.tagAbsolute(f"holeLPO{i}", (dr, x, py), direction="+X") result.tagAbsolute(f"holeRPO{i}", (dr, x, -py), direction="+X") result.tagAbsolute(f"holeLPM{i}", (dr0, x, py), direction="-X") result.tagAbsolute(f"holeRPM{i}", (dr0, x, -py), direction="-X") - result.tagAbsolute(f"holeLPI{i}", (dri, x, py), direction="-X") - result.tagAbsolute(f"holeRPI{i}", (dri, x, -py), direction="-X") result.tagAbsolute(f"holeLSO{i}", locrot * Cq.Location(dr, -x, py), direction="+X") result.tagAbsolute(f"holeRSO{i}", locrot * Cq.Location(dr, -x, -py), direction="+X") result.tagAbsolute(f"holeLSM{i}", locrot * Cq.Location(dr0, -x, py), direction="-X") result.tagAbsolute(f"holeRSM{i}", locrot * Cq.Location(dr0, -x, -py), direction="-X") - result.tagAbsolute(f"holeLSI{i}", locrot * Cq.Location(dri, -x, py), direction="-X") - result.tagAbsolute(f"holeRSI{i}", locrot * Cq.Location(dri, -x, -py), direction="-X") return result @target(name="angle-joint-flanged") From b83bf5a57d56ea02329e9988b587e9eb34bca9e2 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Wed, 14 May 2025 22:40:53 -0700 Subject: [PATCH 17/59] Use only one bolt for angle bracket --- nhf/touhou/yasaka_kanako/onbashira.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/nhf/touhou/yasaka_kanako/onbashira.py b/nhf/touhou/yasaka_kanako/onbashira.py index afcd26a..8f219e2 100644 --- a/nhf/touhou/yasaka_kanako/onbashira.py +++ b/nhf/touhou/yasaka_kanako/onbashira.py @@ -33,8 +33,7 @@ class Onbashira(Model): angle_joint_bolt_head_depth: float = 3.0 # Position of the holes, with (0, 0) being the centre of each side angle_joint_bolt_position: list[float] = field(default_factory=lambda: [ - (20, 10), - (60, 10), + (40, 12), ]) angle_joint_flange_thickness: float = 7.8 angle_joint_flange_radius: float = 40.0 @@ -63,6 +62,7 @@ class Onbashira(Model): material_side: Material = Material.WOOD_BIRCH material_bearing: Material = Material.PLASTIC_PLA + material_spacer: Material = Material.PLASTIC_PLA material_bearing_ball: Material = Material.ACRYLIC_TRANSPARENT material_brace: Material = Material.PLASTIC_PLA @@ -251,6 +251,24 @@ class Onbashira(Model): loc=Cq.Location(0, 0, -self.bearing_disk_thickness/2) ) ) + z = -self.bearing_disk_gap/2 + for i in range(self.n_side): + loc = Cq.Location.rot2d(i * 360/self.n_side) * Cq.Location(self.rotor_bind_radius, 0, z) + a = a.addS( + self.rotor_spacer(), + name=f"spacerRotor{i}", + material=self.material_spacer, + role=Role.STRUCTURE, + loc=loc + ) + loc = Cq.Location.rot2d((i+0.5) * 360/self.n_side) * Cq.Location(self.stator_bind_radius, 0, z) + a = a.addS( + self.rotor_spacer(), + name=f"spacerStator{i}", + material=self.material_spacer, + role=Role.STRUCTURE, + loc=loc + ) for i in range(self.n_bearing_balls): ball = self.bearing_ball() loc = Cq.Location.rot2d(i * 360/self.n_bearing_balls) * Cq.Location(self.bearing_track_radius, 0, 0) From 4d4e4c7eab388e71e8c266c0cd0e70b087c9476e Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Wed, 14 May 2025 23:08:47 -0700 Subject: [PATCH 18/59] Mating structure for angle joint --- nhf/touhou/yasaka_kanako/onbashira.py | 28 ++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/nhf/touhou/yasaka_kanako/onbashira.py b/nhf/touhou/yasaka_kanako/onbashira.py index 8f219e2..a576050 100644 --- a/nhf/touhou/yasaka_kanako/onbashira.py +++ b/nhf/touhou/yasaka_kanako/onbashira.py @@ -22,7 +22,7 @@ class Onbashira(Model): side_thickness: float = 25.4 / 8 # Joints between two sets of side panels - angle_joint_thickness: float = 10.0 + angle_joint_thickness: float = 15.0 # Z-axis size of each angle joint angle_joint_depth: float = 60.0 # Gap of each angle joint to connect the outside to the inside @@ -37,6 +37,9 @@ class Onbashira(Model): ]) angle_joint_flange_thickness: float = 7.8 angle_joint_flange_radius: float = 40.0 + angle_joint_conn_thickness: float = 6.0 + angle_joint_conn_depth: float = 20.0 + angle_joint_conn_width: float = 20.0 # Dimensions of gun barrels barrel_diam: float = 25.4 * 1.5 @@ -77,6 +80,8 @@ class Onbashira(Model): for (x, y) in self.angle_joint_bolt_position: assert y < self.angle_joint_depth / 2 + assert self.angle_joint_depth / 2 > self.angle_joint_conn_depth + assert self.angle_joint_thickness > self.angle_joint_conn_thickness @property def angle_side(self) -> float: @@ -403,6 +408,23 @@ class Onbashira(Model): .extrude(self.angle_joint_depth*4) .translate((0, 0, -self.angle_joint_depth*2)) ) + # The mating structure + z1 = self.bulk_radius + (self.angle_joint_thickness - self.angle_joint_conn_thickness) / 2 + z2 = z1 + self.angle_joint_conn_thickness + mating1n = ( + Cq.Workplane() + .sketch() + .polygon([ + (z1, 0), + (z1, self.angle_joint_conn_width), + (z2, self.angle_joint_conn_width), + (z2, 0), + ]) + .finalize() + .extrude(self.angle_joint_conn_depth) + ) + mating1p = mating1n.rotate((0,0,0), (1,0,0), 180) + angle = 360 / self.n_side result = ( Cq.Workplane() .placeSketch(sketch) @@ -411,6 +433,10 @@ class Onbashira(Model): .cut(slot.translate((0, 0, self.angle_joint_gap/2))) .cut(slot.translate((0, 0, -self.angle_joint_depth-self.angle_joint_gap/2))) .intersect(intersector) + .cut(mating1n) + .union(mating1p) + .union(mating1n.rotate((0,0,0),(0,0,1),angle)) + .cut(mating1p.rotate((0,0,0),(0,0,1),angle)) ) h = self.bulk_radius + self.angle_joint_thickness hole_negative = Cq.Solid.makeCylinder( From 83d4232ad7ca8c4d57144f7ca681930fe69f1eea Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Wed, 14 May 2025 23:21:13 -0700 Subject: [PATCH 19/59] Add holes for gohei --- nhf/touhou/yasaka_kanako/onbashira.py | 116 ++++++++++++++++++++++---- 1 file changed, 102 insertions(+), 14 deletions(-) diff --git a/nhf/touhou/yasaka_kanako/onbashira.py b/nhf/touhou/yasaka_kanako/onbashira.py index a576050..56e368b 100644 --- a/nhf/touhou/yasaka_kanako/onbashira.py +++ b/nhf/touhou/yasaka_kanako/onbashira.py @@ -21,6 +21,9 @@ class Onbashira(Model): side_thickness: float = 25.4 / 8 + section1_gohei_loc: float = 30.0 + gohei_bolt_diam: float = 6.0 + # Joints between two sets of side panels angle_joint_thickness: float = 15.0 # Z-axis size of each angle joint @@ -349,6 +352,104 @@ class Onbashira(Model): return result + @target(name="side-panel1", kind=TargetKind.DXF) + def profile_side_panel1(self) -> Cq.Sketch: + return ( + self.profile_side_panel( + length=self.side_length1, + hasFrontHole=False, + hasBackHole=True, + ) + .push([ + (0, self.side_length1/2 - self.section1_gohei_loc) + ]) + .circle(self.gohei_bolt_diam/2, mode="s") + ) + def side_panel1(self) -> Cq.Workplane: + l = self.side_length1 + w = self.side_width + sketch = self.profile_side_panel1() + result = ( + Cq.Workplane() + .placeSketch(sketch) + .extrude(self.side_thickness) + ) + # Bevel the edges + intersector = ( + Cq.Workplane('XZ') + .polyline([ + (-w/2, 0), + (w/2, 0), + (0, self.bulk_radius), + ]) + .close() + .extrude(l) + .translate(Cq.Vector(0, l/2, 0)) + ) + # Intersect the side panel + result = result * intersector + + # Mark all attachment points + t = self.side_thickness + for i, (x, y) in enumerate(self.angle_joint_bolt_position): + px = x + py = l / 2 - y + result.tagAbsolute(f"holeFPI{i}", (+px, py, t), direction="+Z") + result.tagAbsolute(f"holeFSI{i}", (-px, py, t), direction="+Z") + result.tagAbsolute(f"holeFPO{i}", (+px, py, 0), direction="-Z") + result.tagAbsolute(f"holeFSO{i}", (-px, py, 0), direction="-Z") + result.tagAbsolute(f"holeBPI{i}", (+px, -py, t), direction="+Z") + result.tagAbsolute(f"holeBSI{i}", (-px, -py, t), direction="+Z") + result.tagAbsolute(f"holeBPO{i}", (+px, -py, 0), direction="-Z") + result.tagAbsolute(f"holeBSO{i}", (-px, -py, 0), direction="-Z") + + return result + @target(name="side-panel2", kind=TargetKind.DXF) + def profile_side_panel2(self) -> Cq.Sketch: + return ( + self.profile_side_panel( + length=self.side_length2, + hasFrontHole=True, + hasBackHole=True, + ) + ) + @target(name="side-panel3", kind=TargetKind.DXF) + def profile_side_panel3(self) -> Cq.Sketch: + return ( + self.profile_side_panel( + length=self.side_length3, + hasFrontHole=True, + hasBackHole=True, + ) + ) + + def assembly_section1(self) -> Cq.Assembly: + a = Cq.Assembly() + side = self.side_panel1() + r = self.bulk_radius + for i in range(self.n_side): + a = a.addS( + side, + name=f"side{i}", + material=self.material_side, + role=Role.STRUCTURE | Role.DECORATION, + loc=Cq.Location.rot2d(i*360/self.n_side) * Cq.Location(-r,0,0,90,0,90), + ) + return a + def assembly_section(self, **kwargs) -> Cq.Assembly: + a = Cq.Assembly() + side = self.side_panel(**kwargs) + r = self.bulk_radius + for i in range(self.n_side): + a = a.addS( + side, + name=f"side{i}", + material=self.material_side, + role=Role.STRUCTURE | Role.DECORATION, + loc=Cq.Location.rot2d(i*360/self.n_side) * Cq.Location(-r,0,0,90,0,90), + ) + return a + @target(name="angle-joint") def angle_joint(self) -> Cq.Workplane: """ @@ -509,19 +610,6 @@ class Onbashira(Model): result.tagAbsolute("holeStatorR", (ri * math.cos(th), ri * math.sin(th), -h/2), direction="-Z") return result - def assembly_section(self, **kwargs) -> Cq.Assembly: - a = Cq.Assembly() - side = self.side_panel(**kwargs) - r = self.bulk_radius - for i in range(self.n_side): - a = a.addS( - side, - name=f"side{i}", - material=self.material_side, - role=Role.STRUCTURE | Role.DECORATION, - loc=Cq.Location.rot2d(i*360/self.n_side) * Cq.Location(-r,0,0,90,0,90), - ) - return a def assembly_ring(self, flanged=False) -> Cq.Assembly: a = Cq.Assembly() side = self.angle_joint_flanged() if flanged else self.angle_joint() @@ -542,7 +630,7 @@ class Onbashira(Model): a = ( a .add( - self.assembly_section(length=self.side_length1, hasFrontHole=False, hasBackHole=True), + self.assembly_section1(), name="section1", ) .add( From 4edad88299f964960820971395b82ab87ebc5f43 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Thu, 15 May 2025 13:23:38 -0700 Subject: [PATCH 20/59] Larger mirror dimensions --- nhf/touhou/yasaka_kanako/mirror.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/nhf/touhou/yasaka_kanako/mirror.py b/nhf/touhou/yasaka_kanako/mirror.py index 6ba5242..905d056 100644 --- a/nhf/touhou/yasaka_kanako/mirror.py +++ b/nhf/touhou/yasaka_kanako/mirror.py @@ -14,8 +14,8 @@ class Mirror(Model): 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 = 50.0 - height: float = 60.0 + width: float = 100.0 + height: float = 120.0 inner_gap: float = 3.0 outer_gap: float = 3.0 @@ -23,19 +23,19 @@ class Mirror(Model): core_thickness: float = 25.4 / 8 casing_thickness: float = 25.4 / 8 - flange_r0: float = 5.0 - flange_r1: float = 15.0 + 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 = 12.0 - wing_x2: float = 20.0 - wing_r1: float = 6.0 - wing_r2: float = 12.0 - tail_r0: float = 5.0 - tail_r1: float = 10.0 - tail_y1: float = 12.0 - tail_y2: float = 25.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 From 0fb88a97d3d16239ef3e418d32dd7e4d1768d4d8 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Thu, 15 May 2025 20:55:17 -0700 Subject: [PATCH 21/59] Chamber connectors --- nhf/touhou/yasaka_kanako/onbashira.py | 438 +++++++++++++++++++++++--- 1 file changed, 386 insertions(+), 52 deletions(-) diff --git a/nhf/touhou/yasaka_kanako/onbashira.py b/nhf/touhou/yasaka_kanako/onbashira.py index 56e368b..b7f8f83 100644 --- a/nhf/touhou/yasaka_kanako/onbashira.py +++ b/nhf/touhou/yasaka_kanako/onbashira.py @@ -17,7 +17,6 @@ class Onbashira(Model): side_length1: float = 200.0 side_length2: float = 350.0 side_length3: float = 400.0 - side_length4: float = 400.0 side_thickness: float = 25.4 / 8 @@ -44,6 +43,9 @@ class Onbashira(Model): angle_joint_conn_depth: float = 20.0 angle_joint_conn_width: float = 20.0 + chamber_side_length: float = 400.0 + chamber_side_width_ex: float = 30.0 + # Dimensions of gun barrels barrel_diam: float = 25.4 * 1.5 barrel_length: float = 300.0 @@ -90,6 +92,14 @@ class Onbashira(Model): def angle_side(self) -> float: return 360 / self.n_side @property + def delta_side_width(self) -> float: + """ + Difference between interior and exterior side width due to side thickness + """ + theta = math.pi / self.n_side + dt = self.side_thickness * math.tan(theta) + return dt * 2 + @property def side_width_inner(self) -> float: """ Interior side width @@ -97,9 +107,7 @@ class Onbashira(Model): If outer width is `wi`, inner width is `wo`, each side's cross section is a trapezoid with sides `wi`, `wo`, and height `h` (side thickness) """ - theta = math.pi / self.n_side - dt = self.side_thickness * math.tan(theta) - return self.side_width - dt * 2 + return self.side_width - self.delta_side_width @property def angle_joint_extra_width(self) -> float: theta = math.pi / self.n_side @@ -117,6 +125,15 @@ class Onbashira(Model): """ return self.side_width / 2 / math.tan(math.radians(self.angle_side / 2)) @property + def chamber_side_width(self) -> float: + return self.side_width + self.chamber_side_width_ex + @property + def chamber_bulk_radius(self) -> float: + """ + Radius of the bulk (surface of each side) to the centre + """ + return self.chamber_side_width / 2 / math.tan(math.radians(self.angle_side / 2)) + @property def bearing_diam(self) -> float: return self.bearing_ball_diam + self.bearing_ball_gap @@ -309,7 +326,12 @@ class Onbashira(Model): .circle(self.angle_joint_bolt_diam/2, mode="s") ) - def side_panel(self, length: float, hasFrontHole: bool = True, hasBackHole: bool = True) -> Cq.Workplane: + def side_panel( + self, + length: float, + hasFrontHole: bool = True, + hasBackHole: bool = True, + ) -> Cq.Workplane: w = self.side_width sketch = self.profile_side_panel( length=length, @@ -450,10 +472,317 @@ class Onbashira(Model): ) return a + @target(name="chamber-side-panel", kind=TargetKind.DXF) + def profile_chamber_side_panel(self) -> Cq.Sketch: + l = self.chamber_side_length + w = self.chamber_side_width + return ( + Cq.Sketch() + .rect(w, l) + .push([ + (sx * x, sy * (l/2 - y)) + for (x, y) in self.angle_joint_bolt_position + for sx in [1, -1] + for sy in [1, -1] + ]) + .circle(self.angle_joint_bolt_diam/2, mode="s") + ) + + def chamber_side_panel(self) -> Cq.Workplane: + w = self.chamber_side_width + l = self.chamber_side_length + sketch = self.profile_chamber_side_panel() + result = ( + Cq.Workplane() + .placeSketch(sketch) + .extrude(self.side_thickness) + ) + # Bevel the edges + intersector = ( + Cq.Workplane('XZ') + .polyline([ + (-w/2, 0), + (w/2, 0), + (0, self.chamber_bulk_radius), + ]) + .close() + .extrude(l) + .translate(Cq.Vector(0, l/2, 0)) + ) + # Intersect the side panel + result = result * intersector + + # Mark all attachment points + t = self.side_thickness + for i, (x, y) in enumerate(self.angle_joint_bolt_position): + px = x + py = l / 2 - y + result.tagAbsolute(f"holeFPI{i}", (+px, py, t), direction="+Z") + result.tagAbsolute(f"holeFSI{i}", (-px, py, t), direction="+Z") + result.tagAbsolute(f"holeFPO{i}", (+px, py, 0), direction="-Z") + result.tagAbsolute(f"holeFSO{i}", (-px, py, 0), direction="-Z") + result.tagAbsolute(f"holeBPI{i}", (+px, -py, t), direction="+Z") + result.tagAbsolute(f"holeBSI{i}", (-px, -py, t), direction="+Z") + result.tagAbsolute(f"holeBPO{i}", (+px, -py, 0), direction="-Z") + result.tagAbsolute(f"holeBSO{i}", (-px, -py, 0), direction="-Z") + + return result + + def assembly_chamber(self) -> Cq.Assembly: + a = Cq.Assembly() + side = self.chamber_side_panel() + r = self.chamber_bulk_radius + for i in range(self.n_side): + a = a.addS( + side, + name=f"side{i}", + material=self.material_side, + role=Role.STRUCTURE | Role.DECORATION, + loc=Cq.Location.rot2d(i*360/self.n_side) * Cq.Location(-r,0,0,90,0,90), + ) + return a + + @target(name="angle-joint-chamber-front") + def angle_joint_chamber_front(self) -> Cq.Workplane: + # This slot cuts the interior of the joint + slot = ( + Cq.Workplane() + .sketch() + .regularPolygon( + self.side_width, + self.n_side + ) + .finalize() + .extrude(self.angle_joint_depth) + ) + thickness = self.chamber_bulk_radius - self.bulk_radius + + h = (self.bulk_radius + self.angle_joint_extra_width) * 2 + # Intersector for 1/n of the ring + intersector = ( + Cq.Workplane() + .sketch() + .polygon([ + (0, 0), + (h, 0), + (h, h * math.tan(2 * math.pi / self.n_side)) + ]) + .finalize() + .extrude(self.angle_joint_depth*4) + .translate((0, 0, -self.angle_joint_depth*2)) + ) + # The mating structure + z1 = self.bulk_radius + (thickness - self.angle_joint_conn_thickness) / 2 + z2 = z1 + self.angle_joint_conn_thickness + mating1n = ( + Cq.Workplane() + .sketch() + .polygon([ + (z1, 0), + (z1, self.angle_joint_conn_width), + (z2, self.angle_joint_conn_width), + (z2, 0), + ]) + .finalize() + .extrude(self.angle_joint_conn_depth) + ) + mating1p = mating1n.rotate((0,0,0), (1,0,0), 180) + angle = 360 / self.n_side + + chamber_intersector = ( + Cq.Workplane() + .sketch() + .regularPolygon(self.chamber_side_width, self.n_side) + .regularPolygon(self.chamber_side_width - self.delta_side_width, self.n_side, mode="s") + .finalize() + .extrude(self.angle_joint_depth) + .translate((0,0,-self.angle_joint_depth-self.angle_joint_gap/2)) + ) + result = ( + Cq.Workplane() + .sketch() + .regularPolygon( + self.chamber_side_width, + self.n_side + ) + .regularPolygon( + self.side_width_inner, + self.n_side, mode="s" + ) + .finalize() + .extrude(self.angle_joint_depth) + .translate((0, 0, -self.angle_joint_depth/2)) + .cut(slot.translate((0, 0, self.angle_joint_gap/2))) + .cut(slot.translate((0, 0, -self.angle_joint_depth-self.angle_joint_gap/2))) + .intersect(intersector) + .cut(chamber_intersector) + .cut(mating1n) + .union(mating1p) + .union(mating1n.rotate((0,0,0),(0,0,1),angle)) + .cut(mating1p.rotate((0,0,0),(0,0,1),angle)) + ) + h = self.chamber_bulk_radius + hole_negative = Cq.Solid.makeCylinder( + radius=self.angle_joint_bolt_diam/2, + height=h, + pnt=(0,0,0), + dir=(1,0,0), + ) + Cq.Solid.makeCylinder( + radius=self.angle_joint_bolt_head_diam/2, + height=self.angle_joint_bolt_head_depth, + pnt=(h,0,0), + dir=(-1,0,0), + ) + dy = self.angle_joint_gap / 2 + locrot = Cq.Location(0, 0, 0, 0, 0, 360/self.n_side) + for (x, y) in self.angle_joint_bolt_position: + p1 = Cq.Location((0, x, dy+y)) + p2 = Cq.Location((0, x, -dy-y)) + p1r = locrot * Cq.Location((0, -x, dy+y)) + p2r = locrot * Cq.Location((0, -x, -dy-y)) + result = result \ + - hole_negative.moved(p1) \ + - hole_negative.moved(p2) \ + - hole_negative.moved(p1r) \ + - hole_negative.moved(p2r) + # Mark the absolute locations of the mount points + dr = self.bulk_radius + self.angle_joint_thickness + dr0 = self.bulk_radius + for i, (x, y) in enumerate(self.angle_joint_bolt_position): + py = dy + y + result.tagAbsolute(f"holeLPO{i}", (dr, x, py), direction="+X") + result.tagAbsolute(f"holeLPM{i}", (dr0, x, py), direction="-X") + result.tagAbsolute(f"holeRPM{i}", (dr0, x, -py), direction="-X") + result.tagAbsolute(f"holeLSO{i}", locrot * Cq.Location(dr, -x, py), direction="+X") + result.tagAbsolute(f"holeLSM{i}", locrot * Cq.Location(dr0, -x, py), direction="-X") + result.tagAbsolute(f"holeRSM{i}", locrot * Cq.Location(dr0, -x, -py), direction="-X") + locrot = Cq.Location(0, 0, 0, 0, 0, 360/self.n_side) + dr = self.chamber_bulk_radius - self.side_thickness + dy = self.angle_joint_gap / 2 + for i, (x, y) in enumerate(self.angle_joint_bolt_position): + py = dy + y + #result.tagAbsolute(f"holeLPO{i}", (dr, x, py), direction="+X") + result.tagAbsolute(f"holeRPO{i}", (dr, x, -py), direction="+X") + #result.tagAbsolute(f"holeLSO{i}", locrot * Cq.Location(dr, -x, py), direction="+X") + result.tagAbsolute(f"holeRSO{i}", locrot * Cq.Location(dr, -x, -py), direction="+X") + return result + + @target(name="angle-joint-chamber-back") + def angle_joint_chamber_back(self) -> Cq.Workplane: + slot = ( + Cq.Workplane() + .sketch() + .regularPolygon( + self.side_width, + self.n_side + ) + .finalize() + .extrude(self.angle_joint_depth) + ) + thickness = self.chamber_bulk_radius - self.bulk_radius + + h = (self.bulk_radius + self.angle_joint_extra_width) * 2 + # Intersector for 1/n of the ring + intersector = ( + Cq.Workplane() + .sketch() + .polygon([ + (0, 0), + (h, 0), + (h, h * math.tan(2 * math.pi / self.n_side)) + ]) + .finalize() + .extrude(self.angle_joint_depth*4) + .translate((0, 0, -self.angle_joint_depth*2)) + ) + # The mating structure + z1 = self.bulk_radius + (thickness - self.angle_joint_conn_thickness) / 2 + z2 = z1 + self.angle_joint_conn_thickness + mating1n = ( + Cq.Workplane() + .sketch() + .polygon([ + (z1, 0), + (z1, self.angle_joint_conn_width), + (z2, self.angle_joint_conn_width), + (z2, 0), + ]) + .finalize() + .extrude(self.angle_joint_conn_depth) + ) + mating1p = mating1n.rotate((0,0,0), (1,0,0), 180) + angle = 360 / self.n_side + + chamber_intersector = ( + Cq.Workplane() + .sketch() + .regularPolygon(self.chamber_side_width, self.n_side) + .regularPolygon(self.chamber_side_width - self.delta_side_width, self.n_side, mode="s") + .finalize() + .extrude(self.angle_joint_depth) + .translate((0,0,self.angle_joint_gap/2)) + ) + result = ( + Cq.Workplane() + .sketch() + .regularPolygon( + self.chamber_side_width, + self.n_side + ) + .regularPolygon( + self.side_width_inner, + self.n_side, mode="s" + ) + .finalize() + .extrude(self.angle_joint_depth) + .translate((0, 0, -self.angle_joint_depth/2)) + .cut(slot.translate((0, 0, self.angle_joint_gap/2))) + .cut(slot.translate((0, 0, -self.angle_joint_depth-self.angle_joint_gap/2))) + .intersect(intersector) + .cut(chamber_intersector) + .cut(mating1n) + .union(mating1p) + .union(mating1n.rotate((0,0,0),(0,0,1),angle)) + .cut(mating1p.rotate((0,0,0),(0,0,1),angle)) + ) + h = self.chamber_bulk_radius + hole_negative = Cq.Solid.makeCylinder( + radius=self.angle_joint_bolt_diam/2, + height=h, + pnt=(0,0,0), + dir=(1,0,0), + ) + Cq.Solid.makeCylinder( + radius=self.angle_joint_bolt_head_diam/2, + height=self.angle_joint_bolt_head_depth, + pnt=(h,0,0), + dir=(-1,0,0), + ) + dy = self.angle_joint_gap / 2 + locrot = Cq.Location(0, 0, 0, 0, 0, 360/self.n_side) + for (x, y) in self.angle_joint_bolt_position: + p1 = Cq.Location((0, x, dy+y)) + p1r = locrot * Cq.Location((0, -x, dy+y)) + result = result \ + - hole_negative.moved(p1) \ + - hole_negative.moved(p1r) + # Mark the absolute locations of the mount points + dr = self.chamber_bulk_radius - self.side_thickness + dr0 = self.bulk_radius + locrot = Cq.Location(0, 0, 0, 0, 0, 360/self.n_side) + dr = self.chamber_bulk_radius - self.side_thickness + dy = self.angle_joint_gap / 2 + for i, (x, y) in enumerate(self.angle_joint_bolt_position): + py = dy + y + #result.tagAbsolute(f"holeLPO{i}", (dr, x, py), direction="+X") + result.tagAbsolute(f"holeLPO{i}", (dr, x, py), direction="+X") + #result.tagAbsolute(f"holeLSO{i}", locrot * Cq.Location(dr, -x, py), direction="+X") + result.tagAbsolute(f"holeLSO{i}", locrot * Cq.Location(dr, -x, py), direction="+X") + return result + @target(name="angle-joint") def angle_joint(self) -> Cq.Workplane: """ - Angular joint between two side panels. This sits at the intersection of + Angular joint between two side panels (excluding chamber). This sits at the intersection of 4 side panels to provide compressive, shear, and tensile strength. To provide tensile strength along the Z-axis, the panels must be bolted @@ -464,37 +793,18 @@ class Onbashira(Model): (primary/secondary) being joined. O/I corresponds to the outside/inside """ - # Create the slot carving + # This slot cuts the interior of the joint slot = ( - Cq.Sketch() + Cq.Workplane() + .sketch() .regularPolygon( self.side_width, self.n_side ) - #.regularPolygon( - # self.side_width_inner, - # self.n_side, mode="s", - #) - ) - slot = ( - Cq.Workplane() - .placeSketch(slot) + .finalize() .extrude(self.angle_joint_depth) ) - # Construct the overall shape of the joint, and divide it into sections for printing later. - sketch = ( - Cq.Sketch() - .regularPolygon( - self.side_width + self.angle_joint_extra_width, - self.n_side - ) - .regularPolygon( - self.side_width_inner, - self.n_side, mode="s" - ) - ) - h = (self.bulk_radius + self.angle_joint_extra_width) * 2 # Intersector for 1/n of the ring intersector = ( @@ -528,7 +838,16 @@ class Onbashira(Model): angle = 360 / self.n_side result = ( Cq.Workplane() - .placeSketch(sketch) + .sketch() + .regularPolygon( + self.side_width + self.angle_joint_extra_width, + self.n_side + ) + .regularPolygon( + self.side_width_inner, + self.n_side, mode="s" + ) + .finalize() .extrude(self.angle_joint_depth) .translate((0, 0, -self.angle_joint_depth/2)) .cut(slot.translate((0, 0, self.angle_joint_gap/2))) @@ -610,13 +929,12 @@ class Onbashira(Model): result.tagAbsolute("holeStatorR", (ri * math.cos(th), ri * math.sin(th), -h/2), direction="-Z") return result - def assembly_ring(self, flanged=False) -> Cq.Assembly: + def assembly_ring(self, base) -> Cq.Assembly: a = Cq.Assembly() - side = self.angle_joint_flanged() if flanged else self.angle_joint() r = self.bulk_radius for i in range(self.n_side): a = a.addS( - side, + base, name=f"side{i}", material=self.material_brace, role=Role.CASING | Role.DECORATION, @@ -634,7 +952,7 @@ class Onbashira(Model): name="section1", ) .add( - self.assembly_ring(flanged=True), + self.assembly_ring(self.angle_joint_flanged()), name="ring1", ) .add( @@ -642,7 +960,7 @@ class Onbashira(Model): name="section2", ) .add( - self.assembly_ring(), + self.assembly_ring(self.angle_joint()), name="ring2", ) .add( @@ -650,32 +968,48 @@ class Onbashira(Model): name="section3", ) .add( - self.assembly_ring(), - name="ring3", + self.assembly_ring(self.angle_joint_chamber_front()), + name="chamber_front", ) .add( - self.assembly_section(length=self.side_length4, hasFrontHole=True, hasBackHole=False), - name="section4", + self.assembly_chamber(), + name="chamber", + ) + .add( + self.assembly_ring(self.angle_joint_chamber_back()), + name="chamber_back", ) ) - for (nl, nc, nr) in [ - ("section1", "ring1", "section2"), - ("section2", "ring2", "section3"), - ("section3", "ring3", "section4"), - ]: - for i in range(self.n_side): - j = (i + 1) % self.n_side - for ih in range(len(self.angle_joint_bolt_position)): + for i in range(self.n_side): + j = (i + 1) % self.n_side + for ih in range(len(self.angle_joint_bolt_position)): + a = a.constrain( + f"chamber/side{i}?holeFPI{ih}", + f"chamber_front/side{i}?holeRSO{ih}", + "Plane", + ) + a = a.constrain( + f"chamber/side{i}?holeBPI{ih}", + f"chamber_back/side{i}?holeLSO{ih}", + "Plane", + ) + for (nl, nc, nr) in [ + ("section1", "ring1", "section2"), + ("section2", "ring2", "section3"), + ("section3", "chamber_front", None), + ]: a = a.constrain( f"{nl}/side{i}?holeBSO{ih}", f"{nc}/side{i}?holeLPM{ih}", "Plane", ) - a = a.constrain( - f"{nr}/side{i}?holeFPO{ih}", - f"{nc}/side{i}?holeRSM{ih}", - "Plane", - ) + if nr: + a = a.constrain( + f"{nr}/side{i}?holeFPO{ih}", + f"{nc}/side{i}?holeRSM{ih}", + "Plane", + ) + a = a.add(self.assembly_rotor(), name="rotor") return a.solve() From 63c2c74e021f58b30ec160ddafbbdf2f29f6425d Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Thu, 15 May 2025 21:20:22 -0700 Subject: [PATCH 22/59] Barrel position solver --- nhf/touhou/yasaka_kanako/onbashira.py | 85 +++++++++++++++++++++++---- 1 file changed, 75 insertions(+), 10 deletions(-) diff --git a/nhf/touhou/yasaka_kanako/onbashira.py b/nhf/touhou/yasaka_kanako/onbashira.py index b7f8f83..8f40f20 100644 --- a/nhf/touhou/yasaka_kanako/onbashira.py +++ b/nhf/touhou/yasaka_kanako/onbashira.py @@ -160,11 +160,22 @@ class Onbashira(Model): .circle(self.rotor_bind_bolt_diam/2, mode="s") ) def bearing_stator(self) -> Cq.Workplane: - return ( + result = ( Cq.Workplane() .placeSketch(self.profile_bearing_stator()) .extrude(self.bearing_disk_thickness) ) + for i in range(self.n_side): + angle = (i+0.5) * math.radians(360 / self.n_side) + result.faces(">Z").moveTo( + self.stator_bind_radius * math.cos(angle), + self.stator_bind_radius * math.sin(angle), + ).tagPlane(f"holeF{i}") + result.faces(" Cq.Sketch: bolt_angle = 180 / self.n_side @@ -236,7 +247,7 @@ class Onbashira(Model): ) return outer - inner - def assembly_rotor(self) -> Cq.Assembly: + def assembly_barrel(self) -> Cq.Assembly: z_lower = -self.bearing_disk_gap/2 - self.bearing_disk_thickness a = ( Cq.Assembly() @@ -665,6 +676,32 @@ class Onbashira(Model): result.tagAbsolute(f"holeRPO{i}", (dr, x, -py), direction="+X") #result.tagAbsolute(f"holeLSO{i}", locrot * Cq.Location(dr, -x, py), direction="+X") result.tagAbsolute(f"holeRSO{i}", locrot * Cq.Location(dr, -x, -py), direction="+X") + + th = math.pi / self.n_side + r = self.bulk_radius + flange = ( + Cq.Workplane() + .sketch() + .push([ + (r, r * math.tan(th)) + ]) + .circle(self.angle_joint_flange_radius) + .reset() + .regularPolygon(self.side_width_inner, self.n_side, mode="i") + .finalize() + .extrude(self.angle_joint_flange_thickness) + .translate((0, 0, -self.angle_joint_flange_thickness/2)) + ) + ri = self.stator_bind_radius + h = self.angle_joint_flange_thickness + cyl = Cq.Solid.makeCylinder( + radius=self.rotor_bind_bolt_diam/2, + height=h, + pnt=(ri * math.cos(th), ri * math.sin(th), -h/2), + ) + result = result + flange - cyl + result.tagAbsolute("holeStatorL", (ri * math.cos(th), ri * math.sin(th), h/2), direction="+Z") + result.tagAbsolute("holeStatorR", (ri * math.cos(th), ri * math.sin(th), -h/2), direction="-Z") return result @target(name="angle-joint-chamber-back") @@ -737,7 +774,6 @@ class Onbashira(Model): .extrude(self.angle_joint_depth) .translate((0, 0, -self.angle_joint_depth/2)) .cut(slot.translate((0, 0, self.angle_joint_gap/2))) - .cut(slot.translate((0, 0, -self.angle_joint_depth-self.angle_joint_gap/2))) .intersect(intersector) .cut(chamber_intersector) .cut(mating1n) @@ -777,6 +813,33 @@ class Onbashira(Model): result.tagAbsolute(f"holeLPO{i}", (dr, x, py), direction="+X") #result.tagAbsolute(f"holeLSO{i}", locrot * Cq.Location(dr, -x, py), direction="+X") result.tagAbsolute(f"holeLSO{i}", locrot * Cq.Location(dr, -x, py), direction="+X") + + th = math.pi / self.n_side + r = self.bulk_radius + flange_z = self.angle_joint_depth / 2 - self.side_thickness + flange = ( + Cq.Workplane() + .sketch() + .push([ + (r, r * math.tan(th)) + ]) + .circle(self.angle_joint_flange_radius) + .reset() + .regularPolygon(self.side_width_inner, self.n_side, mode="i") + .finalize() + .extrude(self.angle_joint_flange_thickness) + .translate((0, 0, -flange_z)) + ) + ri = self.stator_bind_radius + h = self.angle_joint_flange_thickness + cyl = Cq.Solid.makeCylinder( + radius=self.rotor_bind_bolt_diam/2, + height=h, + pnt=(ri * math.cos(th), ri * math.sin(th), -flange_z), + ) + result = result + flange - cyl + result.tagAbsolute("holeStatorL", (ri * math.cos(th), ri * math.sin(th), -flange_z+h/2), direction="+Z") + result.tagAbsolute("holeStatorR", (ri * math.cos(th), ri * math.sin(th), -flange_z-h/2), direction="-Z") return result @target(name="angle-joint") @@ -903,17 +966,15 @@ class Onbashira(Model): th = math.pi / self.n_side r = self.bulk_radius flange = ( - Cq.Sketch() + Cq.Workplane() + .sketch() .push([ (r, r * math.tan(th)) ]) .circle(self.angle_joint_flange_radius) .reset() .regularPolygon(self.side_width_inner, self.n_side, mode="i") - ) - flange = ( - Cq.Workplane() - .placeSketch(flange) + .finalize() .extrude(self.angle_joint_flange_thickness) .translate((0, 0, -self.angle_joint_flange_thickness/2)) ) @@ -979,6 +1040,7 @@ class Onbashira(Model): self.assembly_ring(self.angle_joint_chamber_back()), name="chamber_back", ) + .add(self.assembly_barrel(), name="barrel") ) for i in range(self.n_side): j = (i + 1) % self.n_side @@ -993,6 +1055,11 @@ class Onbashira(Model): f"chamber_back/side{i}?holeLSO{ih}", "Plane", ) + a = a.constrain( + f"barrel/stator2?holeB{i}", + f"ring1/side{i}?holeStatorR", + "Plane", + ) for (nl, nc, nr) in [ ("section1", "ring1", "section2"), ("section2", "ring2", "section3"), @@ -1009,7 +1076,5 @@ class Onbashira(Model): f"{nc}/side{i}?holeRSM{ih}", "Plane", ) - - a = a.add(self.assembly_rotor(), name="rotor") return a.solve() From c5f9e570a676b7e259f381a07a61c12b1dc6619f Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Thu, 15 May 2025 23:16:39 -0700 Subject: [PATCH 23/59] Fit onbashira profile in 12x12 in panel --- nhf/touhou/yasaka_kanako/onbashira.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/nhf/touhou/yasaka_kanako/onbashira.py b/nhf/touhou/yasaka_kanako/onbashira.py index 8f40f20..abc105b 100644 --- a/nhf/touhou/yasaka_kanako/onbashira.py +++ b/nhf/touhou/yasaka_kanako/onbashira.py @@ -11,7 +11,7 @@ class Onbashira(Model): n_side: int = 6 # Dimensions of each side panel - side_width: float = 170.0 + side_width: float = 150.0 # Side panels have different lengths side_length1: float = 200.0 @@ -38,7 +38,7 @@ class Onbashira(Model): (40, 12), ]) angle_joint_flange_thickness: float = 7.8 - angle_joint_flange_radius: float = 40.0 + angle_joint_flange_radius: float = 30.0 angle_joint_conn_thickness: float = 6.0 angle_joint_conn_depth: float = 20.0 angle_joint_conn_width: float = 20.0 @@ -50,23 +50,23 @@ class Onbashira(Model): barrel_diam: float = 25.4 * 1.5 barrel_length: float = 300.0 # Radius from barrel centre to axis - rotation_radius: float = 75.0 - n_bearing_balls: int = 24 + rotation_radius: float = 66.0 + n_bearing_balls: int = 12 # Size of ball bearings bearing_ball_diam: float = 25.4 * 1/2 bearing_ball_gap: float = .5 # Thickness of bearing disks bearing_thickness: float = 20.0 - bearing_track_radius: float = 110.0 + bearing_track_radius: float = 100.0 # Gap between the inner and outer bearing disks bearing_gap: float = 10.0 bearing_disk_thickness: float = 25.4 / 8 rotor_inner_radius: float = 40.0 rotor_bind_bolt_diam: float = 6.0 - rotor_bind_radius: float = 85.0 + rotor_bind_radius: float = 78.0 rotor_spacer_outer_diam: float = 15.0 - stator_bind_radius: float = 140.0 + stator_bind_radius: float = 130.0 material_side: Material = Material.WOOD_BIRCH material_bearing: Material = Material.PLASTIC_PLA @@ -88,6 +88,9 @@ class Onbashira(Model): assert self.angle_joint_depth / 2 > self.angle_joint_conn_depth assert self.angle_joint_thickness > self.angle_joint_conn_thickness + # Ensure the stator could be printed on a 12x12in board + assert self.side_width * 2 < 12 * 25.4 + @property def angle_side(self) -> float: return 360 / self.n_side From bd7e8677c7104cf74d2df42ae02d0c80cb938a3e Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Fri, 16 May 2025 07:15:30 -0700 Subject: [PATCH 24/59] Make all angle joints flanged --- nhf/touhou/yasaka_kanako/onbashira.py | 59 +++++++++++++-------------- 1 file changed, 28 insertions(+), 31 deletions(-) diff --git a/nhf/touhou/yasaka_kanako/onbashira.py b/nhf/touhou/yasaka_kanako/onbashira.py index abc105b..8e79934 100644 --- a/nhf/touhou/yasaka_kanako/onbashira.py +++ b/nhf/touhou/yasaka_kanako/onbashira.py @@ -846,7 +846,7 @@ class Onbashira(Model): return result @target(name="angle-joint") - def angle_joint(self) -> Cq.Workplane: + def angle_joint(self, add_flange=True) -> Cq.Workplane: """ Angular joint between two side panels (excluding chamber). This sits at the intersection of 4 side panels to provide compressive, shear, and tensile strength. @@ -961,36 +961,33 @@ class Onbashira(Model): result.tagAbsolute(f"holeRSO{i}", locrot * Cq.Location(dr, -x, -py), direction="+X") result.tagAbsolute(f"holeLSM{i}", locrot * Cq.Location(dr0, -x, py), direction="-X") result.tagAbsolute(f"holeRSM{i}", locrot * Cq.Location(dr0, -x, -py), direction="-X") - return result - @target(name="angle-joint-flanged") - def angle_joint_flanged(self) -> Cq.Workplane: - result = self.angle_joint() - th = math.pi / self.n_side - r = self.bulk_radius - flange = ( - Cq.Workplane() - .sketch() - .push([ - (r, r * math.tan(th)) - ]) - .circle(self.angle_joint_flange_radius) - .reset() - .regularPolygon(self.side_width_inner, self.n_side, mode="i") - .finalize() - .extrude(self.angle_joint_flange_thickness) - .translate((0, 0, -self.angle_joint_flange_thickness/2)) - ) - ri = self.stator_bind_radius - h = self.angle_joint_flange_thickness - cyl = Cq.Solid.makeCylinder( - radius=self.rotor_bind_bolt_diam/2, - height=h, - pnt=(ri * math.cos(th), ri * math.sin(th), -h/2), - ) - result = result + flange - cyl - result.tagAbsolute("holeStatorL", (ri * math.cos(th), ri * math.sin(th), h/2), direction="+Z") - result.tagAbsolute("holeStatorR", (ri * math.cos(th), ri * math.sin(th), -h/2), direction="-Z") + if add_flange: + th = math.pi / self.n_side + r = self.bulk_radius + flange = ( + Cq.Workplane() + .sketch() + .push([ + (r, r * math.tan(th)) + ]) + .circle(self.angle_joint_flange_radius) + .reset() + .regularPolygon(self.side_width_inner, self.n_side, mode="i") + .finalize() + .extrude(self.angle_joint_flange_thickness) + .translate((0, 0, -self.angle_joint_flange_thickness/2)) + ) + ri = self.stator_bind_radius + h = self.angle_joint_flange_thickness + cyl = Cq.Solid.makeCylinder( + radius=self.rotor_bind_bolt_diam/2, + height=h, + pnt=(ri * math.cos(th), ri * math.sin(th), -h/2), + ) + result = result + flange - cyl + result.tagAbsolute("holeStatorL", (ri * math.cos(th), ri * math.sin(th), h/2), direction="+Z") + result.tagAbsolute("holeStatorR", (ri * math.cos(th), ri * math.sin(th), -h/2), direction="-Z") return result def assembly_ring(self, base) -> Cq.Assembly: @@ -1016,7 +1013,7 @@ class Onbashira(Model): name="section1", ) .add( - self.assembly_ring(self.angle_joint_flanged()), + self.assembly_ring(self.angle_joint()), name="ring1", ) .add( From 0574a767a349bf418f4308aa77fe509883e34989 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Tue, 20 May 2025 08:18:11 -0700 Subject: [PATCH 25/59] Add sanding block --- nhf/touhou/yasaka_kanako/onbashira.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/nhf/touhou/yasaka_kanako/onbashira.py b/nhf/touhou/yasaka_kanako/onbashira.py index 8e79934..53ca1c0 100644 --- a/nhf/touhou/yasaka_kanako/onbashira.py +++ b/nhf/touhou/yasaka_kanako/onbashira.py @@ -149,6 +149,17 @@ class Onbashira(Model): dx = self.bearing_gap return math.sqrt(diag ** 2 - dx ** 2) + @target(name="sanding_block") + def sanding_block(self) -> Cq.Workplane: + x = 50.0 + return ( + Cq.Workplane() + .sketch() + .polygon([(0,0), (0, x), (x, x/2), (x, 0)]) + .finalize() + .extrude(self.side_width * 1.5) + ) + @target(name="bearing-stator", kind=TargetKind.DXF) def profile_bearing_stator(self) -> Cq.Sketch: return ( From bd15f2840321382262d5fe6a13307867274f1568 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Tue, 20 May 2025 08:24:23 -0700 Subject: [PATCH 26/59] Improve grometry --- nhf/touhou/yasaka_kanako/onbashira.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/nhf/touhou/yasaka_kanako/onbashira.py b/nhf/touhou/yasaka_kanako/onbashira.py index 53ca1c0..05be21d 100644 --- a/nhf/touhou/yasaka_kanako/onbashira.py +++ b/nhf/touhou/yasaka_kanako/onbashira.py @@ -24,27 +24,27 @@ class Onbashira(Model): gohei_bolt_diam: float = 6.0 # Joints between two sets of side panels - angle_joint_thickness: float = 15.0 + angle_joint_thickness: float = 10.0 # Z-axis size of each angle joint - angle_joint_depth: float = 60.0 + angle_joint_depth: float = 50.0 # Gap of each angle joint to connect the outside to the inside - angle_joint_gap: float = 10.0 + angle_joint_gap: float = 8.0 angle_joint_bolt_length: float = 50.0 angle_joint_bolt_diam: float = 6.0 angle_joint_bolt_head_diam: float = 13.0 angle_joint_bolt_head_depth: float = 3.0 # Position of the holes, with (0, 0) being the centre of each side angle_joint_bolt_position: list[float] = field(default_factory=lambda: [ - (40, 12), + (40, 10), ]) angle_joint_flange_thickness: float = 7.8 - angle_joint_flange_radius: float = 30.0 - angle_joint_conn_thickness: float = 6.0 - angle_joint_conn_depth: float = 20.0 - angle_joint_conn_width: float = 20.0 + angle_joint_flange_radius: float = 23.0 + angle_joint_conn_thickness: float = 4.0 + angle_joint_conn_depth: float = 15.0 + angle_joint_conn_width: float = 15.0 chamber_side_length: float = 400.0 - chamber_side_width_ex: float = 30.0 + chamber_side_width_ex: float = 20.0 # Dimensions of gun barrels barrel_diam: float = 25.4 * 1.5 @@ -66,7 +66,7 @@ class Onbashira(Model): rotor_bind_bolt_diam: float = 6.0 rotor_bind_radius: float = 78.0 rotor_spacer_outer_diam: float = 15.0 - stator_bind_radius: float = 130.0 + stator_bind_radius: float = 135.0 material_side: Material = Material.WOOD_BIRCH material_bearing: Material = Material.PLASTIC_PLA From b1fe53874787c475ecc2274fcea5193b00e399f5 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Tue, 20 May 2025 19:47:16 -0700 Subject: [PATCH 27/59] Use dihedral angle to calculate sanding block --- nhf/touhou/yasaka_kanako/onbashira.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/nhf/touhou/yasaka_kanako/onbashira.py b/nhf/touhou/yasaka_kanako/onbashira.py index 05be21d..bea144b 100644 --- a/nhf/touhou/yasaka_kanako/onbashira.py +++ b/nhf/touhou/yasaka_kanako/onbashira.py @@ -149,13 +149,16 @@ class Onbashira(Model): dx = self.bearing_gap return math.sqrt(diag ** 2 - dx ** 2) - @target(name="sanding_block") + @target(name="sanding-block") def sanding_block(self) -> Cq.Workplane: + # Dihedral angle / 2 + angle = math.radians(180 / self.n_side) + r = math.sin(angle) x = 50.0 return ( Cq.Workplane() .sketch() - .polygon([(0,0), (0, x), (x, x/2), (x, 0)]) + .polygon([(0,0), (0, x), (x, (1-r) * x), (x, 0)]) .finalize() .extrude(self.side_width * 1.5) ) From a8c80a307f36a4b828e5ee3a54f96eacb40efd18 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Wed, 21 May 2025 20:25:22 -0700 Subject: [PATCH 28/59] Handle stub --- nhf/touhou/yasaka_kanako/onbashira.py | 41 +++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/nhf/touhou/yasaka_kanako/onbashira.py b/nhf/touhou/yasaka_kanako/onbashira.py index bea144b..6df2c2b 100644 --- a/nhf/touhou/yasaka_kanako/onbashira.py +++ b/nhf/touhou/yasaka_kanako/onbashira.py @@ -68,6 +68,11 @@ class Onbashira(Model): rotor_spacer_outer_diam: float = 15.0 stator_bind_radius: float = 135.0 + handle_base_height: float = 10.0 + handle_thickness: float = 12.0 + handle_length: float = 80.0 + handle_height: float = 40.0 + material_side: Material = Material.WOOD_BIRCH material_bearing: Material = Material.PLASTIC_PLA material_spacer: Material = Material.PLASTIC_PLA @@ -1017,6 +1022,42 @@ class Onbashira(Model): ) return a + @target(name="handle") + def handle(self) -> Cq.Workplane: + w = self.side_width + self.angle_joint_extra_width + base = ( + Cq.Workplane() + .box( + length=w, + width=self.angle_joint_depth, + height=self.handle_base_height, + centered=(True, True, False) + ) + .faces(">Z") + .workplane() + .pushPoints([ + (x * sx, y * sy) + for (x, y) in self.angle_joint_bolt_position + for sx in (-1, 1) + for sy in (-1, 1) + ]) + .cboreHole( + self.angle_joint_bolt_diam, + self.angle_joint_bolt_head_diam, + self.angle_joint_bolt_head_depth, + depth=None, + ) + ) + handle = ( + Cq.Workplane(origin=(0, 0, self.handle_height)) + .box( + length=self.handle_length, + width=self.handle_thickness, + height=self.handle_thickness, + ) + ) + return base + handle + @assembly() def assembly(self) -> Cq.Assembly: a = Cq.Assembly() From 40c32213e1713e6e3e2e66f4d1b5d5f8d3c57b8a Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Wed, 28 May 2025 08:01:14 -0700 Subject: [PATCH 29/59] Motor and bolt models --- nhf/parts/fasteners.py | 1 + nhf/touhou/yasaka_kanako/onbashira.py | 52 ++++++++++++++++++++++++++- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/nhf/parts/fasteners.py b/nhf/parts/fasteners.py index ab79802..1489464 100644 --- a/nhf/parts/fasteners.py +++ b/nhf/parts/fasteners.py @@ -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: diff --git a/nhf/touhou/yasaka_kanako/onbashira.py b/nhf/touhou/yasaka_kanako/onbashira.py index 6df2c2b..556cdd9 100644 --- a/nhf/touhou/yasaka_kanako/onbashira.py +++ b/nhf/touhou/yasaka_kanako/onbashira.py @@ -1,11 +1,56 @@ 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 import math from dataclasses import dataclass, field import cadquery as Cq +NUT_COMMON = HexNut( + # FIXME: measure + mass=0.0, + diam_thread=6.0, + pitch=1.0, + thickness=5.0, + width=9.89, +) +WASHER_COMMON = Washer( + # FIXME: measure + mass=0.0, + diam_thread=6.0, + diam_outer=11.68, + thickness=1.5, +) +BOLT_COMMON = FlatHeadBolt( + # FIXME: measure + mass=0.0, + diam_head=12.8, + height_head=2.8, + diam_thread=6.0, + height_thread=30.0, + pitch=1.0, +) + +@dataclass +class Motor(Model): + + mass: float = 589.7 + voltage: float = 12.0 # V + power: float = 30.0 # watts + + diam_thread: float = 4.0 + diam_body: float = 51.82 + height_body: float = 70.87 + height_shaft: float = 37.85 + + def __post_init__(self): + pass + + def model(self) -> Cq.Workplane: + pass + + @dataclass class Onbashira(Model): @@ -48,7 +93,9 @@ class Onbashira(Model): # Dimensions of gun barrels barrel_diam: float = 25.4 * 1.5 - barrel_length: float = 300.0 + barrel_wall_thickness: float = 25.4 / 8 + barrel_length: float = 25.4 * 12 + # Radius from barrel centre to axis rotation_radius: float = 66.0 n_bearing_balls: int = 12 @@ -77,6 +124,7 @@ class Onbashira(Model): material_bearing: Material = Material.PLASTIC_PLA material_spacer: Material = Material.PLASTIC_PLA material_bearing_ball: Material = Material.ACRYLIC_TRANSPARENT + material_barrel: Material = Material.ACRYLIC_BLACK material_brace: Material = Material.PLASTIC_PLA def __post_init__(self): @@ -96,6 +144,8 @@ class Onbashira(Model): # Ensure the stator could be printed on a 12x12in board assert self.side_width * 2 < 12 * 25.4 + assert self.barrel_wall_thickness * 2 < self.barrel_diam + @property def angle_side(self) -> float: return 360 / self.n_side From af82a866526323cf7c83e97de11bb8e9917b8b07 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Wed, 28 May 2025 23:49:04 -0700 Subject: [PATCH 30/59] Eliminate difficult geometry in angle joint --- nhf/materials.py | 1 + nhf/touhou/yasaka_kanako/onbashira.py | 97 +++++++++++++++++++++------ 2 files changed, 76 insertions(+), 22 deletions(-) diff --git a/nhf/materials.py b/nhf/materials.py index 4716e03..09d9ad6 100644 --- a/nhf/materials.py +++ b/nhf/materials.py @@ -90,6 +90,7 @@ 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) diff --git a/nhf/touhou/yasaka_kanako/onbashira.py b/nhf/touhou/yasaka_kanako/onbashira.py index 556cdd9..3ed6b86 100644 --- a/nhf/touhou/yasaka_kanako/onbashira.py +++ b/nhf/touhou/yasaka_kanako/onbashira.py @@ -68,21 +68,22 @@ class Onbashira(Model): section1_gohei_loc: float = 30.0 gohei_bolt_diam: float = 6.0 - # Joints between two sets of side panels + # The angle joint bridges between two sets of side panels. + + # Extra thickness beyond the onbashira's body angle_joint_thickness: float = 10.0 # Z-axis size of each angle joint angle_joint_depth: float = 50.0 # Gap of each angle joint to connect the outside to the inside angle_joint_gap: float = 8.0 angle_joint_bolt_length: float = 50.0 - angle_joint_bolt_diam: float = 6.0 + angle_joint_bolt_diam: float = BOLT_COMMON.diam_thread angle_joint_bolt_head_diam: float = 13.0 angle_joint_bolt_head_depth: float = 3.0 # Position of the holes, with (0, 0) being the centre of each side angle_joint_bolt_position: list[float] = field(default_factory=lambda: [ (40, 10), ]) - angle_joint_flange_thickness: float = 7.8 angle_joint_flange_radius: float = 23.0 angle_joint_conn_thickness: float = 4.0 angle_joint_conn_depth: float = 15.0 @@ -110,7 +111,7 @@ class Onbashira(Model): bearing_disk_thickness: float = 25.4 / 8 rotor_inner_radius: float = 40.0 - rotor_bind_bolt_diam: float = 6.0 + rotor_bind_bolt_diam: float = BOLT_COMMON.diam_thread rotor_bind_radius: float = 78.0 rotor_spacer_outer_diam: float = 15.0 stator_bind_radius: float = 135.0 @@ -126,6 +127,7 @@ class Onbashira(Model): material_bearing_ball: Material = Material.ACRYLIC_TRANSPARENT material_barrel: Material = Material.ACRYLIC_BLACK material_brace: Material = Material.PLASTIC_PLA + material_fastener: Material = Material.STEEL_STAINLESS def __post_init__(self): assert self.n_side >= 3 @@ -320,6 +322,9 @@ class Onbashira(Model): return outer - inner def assembly_barrel(self) -> Cq.Assembly: + """ + The assembly with gun barrels + """ z_lower = -self.bearing_disk_gap/2 - self.bearing_disk_thickness a = ( Cq.Assembly() @@ -611,6 +616,34 @@ class Onbashira(Model): return result + @target(name="chamber-back", kind=TargetKind.DXF) + def profile_chamber_back(self) -> Cq.Sketch: + return ( + Cq.Sketch() + .regularPolygon(self.side_width - self.side_thickness, self.n_side) + .reset() + .regularPolygon( + self.stator_bind_radius, self.n_side, + mode="c", tag="bolt") + .vertices(tag="bolt") + .circle(self.rotor_bind_bolt_diam/2, mode="s") + ) + def chamber_back(self) -> Cq.Workplane: + sketch = self.profile_chamber_back() + result = ( + Cq.Workplane() + .placeSketch(sketch) + .extrude(self.side_thickness) + ) + # Mark all attachment points + for i in range(self.n_side): + angle = (i+0.5) * math.radians(360 / self.n_side) + x = self.stator_bind_radius * math.cos(angle) + y = self.stator_bind_radius * math.sin(angle) + result.tagAbsolute(f"holeF{i}", (x, y, self.side_thickness), direction="+Z") + result.tagAbsolute(f"holeB{i}", (x, -y, 0), direction="-Z") + return result + def assembly_chamber(self) -> Cq.Assembly: a = Cq.Assembly() side = self.chamber_side_panel() @@ -761,11 +794,11 @@ class Onbashira(Model): .reset() .regularPolygon(self.side_width_inner, self.n_side, mode="i") .finalize() - .extrude(self.angle_joint_flange_thickness) - .translate((0, 0, -self.angle_joint_flange_thickness/2)) + .extrude(self.angle_joint_gap) + .translate((0, 0, -self.angle_joint_gap/2)) ) ri = self.stator_bind_radius - h = self.angle_joint_flange_thickness + h = self.angle_joint_gap cyl = Cq.Solid.makeCylinder( radius=self.rotor_bind_bolt_diam/2, height=h, @@ -899,19 +932,19 @@ class Onbashira(Model): .reset() .regularPolygon(self.side_width_inner, self.n_side, mode="i") .finalize() - .extrude(self.angle_joint_flange_thickness) + .extrude(self.angle_joint_gap) .translate((0, 0, -flange_z)) ) ri = self.stator_bind_radius - h = self.angle_joint_flange_thickness + h = self.angle_joint_gap cyl = Cq.Solid.makeCylinder( radius=self.rotor_bind_bolt_diam/2, height=h, pnt=(ri * math.cos(th), ri * math.sin(th), -flange_z), ) result = result + flange - cyl - result.tagAbsolute("holeStatorL", (ri * math.cos(th), ri * math.sin(th), -flange_z+h/2), direction="+Z") - result.tagAbsolute("holeStatorR", (ri * math.cos(th), ri * math.sin(th), -flange_z-h/2), direction="-Z") + result.tagAbsolute("holeStatorO", (ri * math.cos(th), ri * math.sin(th), -flange_z), direction="-Z") + result.tagAbsolute("holeStatorI", (ri * math.cos(th), ri * math.sin(th), -flange_z+h), direction="+Z") return result @target(name="angle-joint") @@ -1044,11 +1077,11 @@ class Onbashira(Model): .reset() .regularPolygon(self.side_width_inner, self.n_side, mode="i") .finalize() - .extrude(self.angle_joint_flange_thickness) - .translate((0, 0, -self.angle_joint_flange_thickness/2)) + .extrude(self.angle_joint_gap) + .translate((0, 0, -self.angle_joint_gap/2)) ) ri = self.stator_bind_radius - h = self.angle_joint_flange_thickness + h = self.angle_joint_gap cyl = Cq.Solid.makeCylinder( radius=self.rotor_bind_bolt_diam/2, height=h, @@ -1135,7 +1168,7 @@ class Onbashira(Model): ) .add( self.assembly_ring(self.angle_joint_chamber_front()), - name="chamber_front", + name="ring3", ) .add( self.assembly_chamber(), @@ -1143,32 +1176,52 @@ class Onbashira(Model): ) .add( self.assembly_ring(self.angle_joint_chamber_back()), - name="chamber_back", + name="ring4", ) - .add(self.assembly_barrel(), name="barrel") + .addS( + self.chamber_back(), + name="chamber_back", + material=self.material_side, + role=Role.STRUCTURE | Role.DECORATION, + ) + #.add(self.assembly_barrel(), name="barrel") ) for i in range(self.n_side): j = (i + 1) % self.n_side for ih in range(len(self.angle_joint_bolt_position)): a = a.constrain( f"chamber/side{i}?holeFPI{ih}", - f"chamber_front/side{i}?holeRSO{ih}", + f"ring3/side{i}?holeRSO{ih}", "Plane", ) a = a.constrain( f"chamber/side{i}?holeBPI{ih}", - f"chamber_back/side{i}?holeLSO{ih}", + f"ring4/side{i}?holeLSO{ih}", "Plane", ) a = a.constrain( - f"barrel/stator2?holeB{i}", - f"ring1/side{i}?holeStatorR", + f"ring4/side{i}?holeStatorO", + f"chamber_back?holeB{i}", "Plane", ) + + name_bolt =f"chamber_back{i}boltFPI{ih}" + a = a.addS( + BOLT_COMMON.generate(), + name=name_bolt, + material=self.material_fastener, + role=Role.CONNECTION, + ) + a = a.constrain( + f"chamber_back?holeF{i}", + f"{name_bolt}?root", + "Plane", + param=0, + ) for (nl, nc, nr) in [ ("section1", "ring1", "section2"), ("section2", "ring2", "section3"), - ("section3", "chamber_front", None), + ("section3", "ring3", None), ]: a = a.constrain( f"{nl}/side{i}?holeBSO{ih}", From b565ab05a08fd66bfbcf52a83c080c448a98abee Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Thu, 29 May 2025 00:13:48 -0700 Subject: [PATCH 31/59] Additional mounting points for machinery on first 3 rings --- nhf/touhou/yasaka_kanako/onbashira.py | 380 ++++++++++++++------------ 1 file changed, 204 insertions(+), 176 deletions(-) diff --git a/nhf/touhou/yasaka_kanako/onbashira.py b/nhf/touhou/yasaka_kanako/onbashira.py index 3ed6b86..fcba7ba 100644 --- a/nhf/touhou/yasaka_kanako/onbashira.py +++ b/nhf/touhou/yasaka_kanako/onbashira.py @@ -84,7 +84,10 @@ class Onbashira(Model): angle_joint_bolt_position: list[float] = field(default_factory=lambda: [ (40, 10), ]) - angle_joint_flange_radius: float = 23.0 + angle_joint_flange_extension: float = 23.0 + angle_joint_extra_hole_offset: float = 20.0 + + # Mating structure on the angle joint angle_joint_conn_thickness: float = 4.0 angle_joint_conn_depth: float = 15.0 angle_joint_conn_width: float = 15.0 @@ -658,8 +661,154 @@ class Onbashira(Model): ) return a + @target(name="angle-joint-chamber-back") + def angle_joint_chamber_back(self) -> Cq.Workplane: + slot = ( + Cq.Workplane() + .sketch() + .regularPolygon( + self.side_width, + self.n_side + ) + .finalize() + .extrude(self.angle_joint_depth) + ) + thickness = self.chamber_bulk_radius - self.bulk_radius + + h = (self.bulk_radius + self.angle_joint_extra_width) * 2 + # Intersector for 1/n of the ring + intersector = ( + Cq.Workplane() + .sketch() + .polygon([ + (0, 0), + (h, 0), + (h, h * math.tan(2 * math.pi / self.n_side)) + ]) + .finalize() + .extrude(self.angle_joint_depth*4) + .translate((0, 0, -self.angle_joint_depth*2)) + ) + # The mating structure + z1 = self.bulk_radius + (thickness - self.angle_joint_conn_thickness) / 2 + z2 = z1 + self.angle_joint_conn_thickness + mating1n = ( + Cq.Workplane() + .sketch() + .polygon([ + (z1, 0), + (z1, self.angle_joint_conn_width), + (z2, self.angle_joint_conn_width), + (z2, 0), + ]) + .finalize() + .extrude(self.angle_joint_conn_depth) + ) + mating1p = mating1n.rotate((0,0,0), (1,0,0), 180) + angle = 360 / self.n_side + + chamber_intersector = ( + Cq.Workplane() + .sketch() + .regularPolygon(self.chamber_side_width, self.n_side) + .regularPolygon(self.chamber_side_width - self.delta_side_width, self.n_side, mode="s") + .finalize() + .extrude(self.angle_joint_depth) + .translate((0,0,self.angle_joint_gap/2)) + ) + result = ( + Cq.Workplane() + .sketch() + .regularPolygon( + self.chamber_side_width, + self.n_side + ) + .regularPolygon( + self.side_width_inner, + self.n_side, mode="s" + ) + .finalize() + .extrude(self.angle_joint_depth) + .translate((0, 0, -self.angle_joint_depth/2)) + .cut(slot.translate((0, 0, self.angle_joint_gap/2))) + .intersect(intersector) + .cut(chamber_intersector) + .cut(mating1n) + .union(mating1p) + .union(mating1n.rotate((0,0,0),(0,0,1),angle)) + .cut(mating1p.rotate((0,0,0),(0,0,1),angle)) + ) + h = self.chamber_bulk_radius + hole_negative = Cq.Solid.makeCylinder( + radius=self.angle_joint_bolt_diam/2, + height=h, + pnt=(0,0,0), + dir=(1,0,0), + ) + Cq.Solid.makeCylinder( + radius=self.angle_joint_bolt_head_diam/2, + height=self.angle_joint_bolt_head_depth, + pnt=(h,0,0), + dir=(-1,0,0), + ) + dy = self.angle_joint_gap / 2 + locrot = Cq.Location(0, 0, 0, 0, 0, 360/self.n_side) + for (x, y) in self.angle_joint_bolt_position: + p1 = Cq.Location((0, x, dy+y)) + p1r = locrot * Cq.Location((0, -x, dy+y)) + result = result \ + - hole_negative.moved(p1) \ + - hole_negative.moved(p1r) + # Mark the absolute locations of the mount points + dr = self.chamber_bulk_radius - self.side_thickness + locrot = Cq.Location(0, 0, 0, 0, 0, 360/self.n_side) + dr = self.chamber_bulk_radius - self.side_thickness + dy = self.angle_joint_gap / 2 + for i, (x, y) in enumerate(self.angle_joint_bolt_position): + py = dy + y + #result.tagAbsolute(f"holeLPO{i}", (dr, x, py), direction="+X") + result.tagAbsolute(f"holeLPO{i}", (dr, x, py), direction="+X") + #result.tagAbsolute(f"holeLSO{i}", locrot * Cq.Location(dr, -x, py), direction="+X") + result.tagAbsolute(f"holeLSO{i}", locrot * Cq.Location(dr, -x, py), direction="+X") + + th = math.pi / self.n_side + r = self.bulk_radius + flange_z = self.angle_joint_depth / 2 - self.side_thickness + flange = ( + Cq.Workplane() + .sketch() + .push([ + (r, r * math.tan(th)) + ]) + .circle(self.angle_joint_flange_extension) + .reset() + .regularPolygon(self.side_width_inner, self.n_side, mode="i") + .finalize() + .extrude(self.angle_joint_gap) + .translate((0, 0, -flange_z)) + ) + ri = self.stator_bind_radius + h = self.angle_joint_gap + # Drill holes for connectors + cyl = Cq.Solid.makeCylinder( + radius=self.rotor_bind_bolt_diam/2, + height=h, + pnt=(0, 0, -flange_z), + ) + result = ( + result + + flange + - cyl.moved(ri * math.cos(th), ri * math.sin(th), 0) + ) + result.tagAbsolute("holeStatorO", (ri * math.cos(th), ri * math.sin(th), -flange_z), direction="-Z") + result.tagAbsolute("holeStatorI", (ri * math.cos(th), ri * math.sin(th), -flange_z+h), direction="+Z") + return result + + @target(name="angle-joint-chamber-front") def angle_joint_chamber_front(self) -> Cq.Workplane: + """ + Angle joint for connecting the chamber to the chassis of the barrel + """ # This slot cuts the interior of the joint slot = ( Cq.Workplane() @@ -782,173 +931,41 @@ class Onbashira(Model): #result.tagAbsolute(f"holeLSO{i}", locrot * Cq.Location(dr, -x, py), direction="+X") result.tagAbsolute(f"holeRSO{i}", locrot * Cq.Location(dr, -x, -py), direction="+X") + # Generate the flange geometry th = math.pi / self.n_side - r = self.bulk_radius flange = ( Cq.Workplane() .sketch() - .push([ - (r, r * math.tan(th)) - ]) - .circle(self.angle_joint_flange_radius) - .reset() - .regularPolygon(self.side_width_inner, self.n_side, mode="i") + .regularPolygon(self.side_width_inner, self.n_side) + .regularPolygon(self.side_width_inner - self.angle_joint_flange_extension, self.n_side, mode="s") .finalize() .extrude(self.angle_joint_gap) .translate((0, 0, -self.angle_joint_gap/2)) ) + flange = flange * intersector ri = self.stator_bind_radius h = self.angle_joint_gap + # Drill holes for connectors cyl = Cq.Solid.makeCylinder( radius=self.rotor_bind_bolt_diam/2, height=h, - pnt=(ri * math.cos(th), ri * math.sin(th), -h/2), + pnt=(0, 0, -h/2), + ) + side_pos = Cq.Location(ri * math.cos(th), self.angle_joint_extra_hole_offset, 0) + side_pos2 = Cq.Location.rot2d(360/self.n_side) * side_pos.flip_y() + result = ( + result + + flange + - cyl.moved(ri * math.cos(th), ri * math.sin(th), 0) + - cyl.moved(side_pos.toTuple()) + - cyl.moved(side_pos2.toTuple()) ) - result = result + flange - cyl result.tagAbsolute("holeStatorL", (ri * math.cos(th), ri * math.sin(th), h/2), direction="+Z") result.tagAbsolute("holeStatorR", (ri * math.cos(th), ri * math.sin(th), -h/2), direction="-Z") return result - @target(name="angle-joint-chamber-back") - def angle_joint_chamber_back(self) -> Cq.Workplane: - slot = ( - Cq.Workplane() - .sketch() - .regularPolygon( - self.side_width, - self.n_side - ) - .finalize() - .extrude(self.angle_joint_depth) - ) - thickness = self.chamber_bulk_radius - self.bulk_radius - - h = (self.bulk_radius + self.angle_joint_extra_width) * 2 - # Intersector for 1/n of the ring - intersector = ( - Cq.Workplane() - .sketch() - .polygon([ - (0, 0), - (h, 0), - (h, h * math.tan(2 * math.pi / self.n_side)) - ]) - .finalize() - .extrude(self.angle_joint_depth*4) - .translate((0, 0, -self.angle_joint_depth*2)) - ) - # The mating structure - z1 = self.bulk_radius + (thickness - self.angle_joint_conn_thickness) / 2 - z2 = z1 + self.angle_joint_conn_thickness - mating1n = ( - Cq.Workplane() - .sketch() - .polygon([ - (z1, 0), - (z1, self.angle_joint_conn_width), - (z2, self.angle_joint_conn_width), - (z2, 0), - ]) - .finalize() - .extrude(self.angle_joint_conn_depth) - ) - mating1p = mating1n.rotate((0,0,0), (1,0,0), 180) - angle = 360 / self.n_side - - chamber_intersector = ( - Cq.Workplane() - .sketch() - .regularPolygon(self.chamber_side_width, self.n_side) - .regularPolygon(self.chamber_side_width - self.delta_side_width, self.n_side, mode="s") - .finalize() - .extrude(self.angle_joint_depth) - .translate((0,0,self.angle_joint_gap/2)) - ) - result = ( - Cq.Workplane() - .sketch() - .regularPolygon( - self.chamber_side_width, - self.n_side - ) - .regularPolygon( - self.side_width_inner, - self.n_side, mode="s" - ) - .finalize() - .extrude(self.angle_joint_depth) - .translate((0, 0, -self.angle_joint_depth/2)) - .cut(slot.translate((0, 0, self.angle_joint_gap/2))) - .intersect(intersector) - .cut(chamber_intersector) - .cut(mating1n) - .union(mating1p) - .union(mating1n.rotate((0,0,0),(0,0,1),angle)) - .cut(mating1p.rotate((0,0,0),(0,0,1),angle)) - ) - h = self.chamber_bulk_radius - hole_negative = Cq.Solid.makeCylinder( - radius=self.angle_joint_bolt_diam/2, - height=h, - pnt=(0,0,0), - dir=(1,0,0), - ) + Cq.Solid.makeCylinder( - radius=self.angle_joint_bolt_head_diam/2, - height=self.angle_joint_bolt_head_depth, - pnt=(h,0,0), - dir=(-1,0,0), - ) - dy = self.angle_joint_gap / 2 - locrot = Cq.Location(0, 0, 0, 0, 0, 360/self.n_side) - for (x, y) in self.angle_joint_bolt_position: - p1 = Cq.Location((0, x, dy+y)) - p1r = locrot * Cq.Location((0, -x, dy+y)) - result = result \ - - hole_negative.moved(p1) \ - - hole_negative.moved(p1r) - # Mark the absolute locations of the mount points - dr = self.chamber_bulk_radius - self.side_thickness - dr0 = self.bulk_radius - locrot = Cq.Location(0, 0, 0, 0, 0, 360/self.n_side) - dr = self.chamber_bulk_radius - self.side_thickness - dy = self.angle_joint_gap / 2 - for i, (x, y) in enumerate(self.angle_joint_bolt_position): - py = dy + y - #result.tagAbsolute(f"holeLPO{i}", (dr, x, py), direction="+X") - result.tagAbsolute(f"holeLPO{i}", (dr, x, py), direction="+X") - #result.tagAbsolute(f"holeLSO{i}", locrot * Cq.Location(dr, -x, py), direction="+X") - result.tagAbsolute(f"holeLSO{i}", locrot * Cq.Location(dr, -x, py), direction="+X") - - th = math.pi / self.n_side - r = self.bulk_radius - flange_z = self.angle_joint_depth / 2 - self.side_thickness - flange = ( - Cq.Workplane() - .sketch() - .push([ - (r, r * math.tan(th)) - ]) - .circle(self.angle_joint_flange_radius) - .reset() - .regularPolygon(self.side_width_inner, self.n_side, mode="i") - .finalize() - .extrude(self.angle_joint_gap) - .translate((0, 0, -flange_z)) - ) - ri = self.stator_bind_radius - h = self.angle_joint_gap - cyl = Cq.Solid.makeCylinder( - radius=self.rotor_bind_bolt_diam/2, - height=h, - pnt=(ri * math.cos(th), ri * math.sin(th), -flange_z), - ) - result = result + flange - cyl - result.tagAbsolute("holeStatorO", (ri * math.cos(th), ri * math.sin(th), -flange_z), direction="-Z") - result.tagAbsolute("holeStatorI", (ri * math.cos(th), ri * math.sin(th), -flange_z+h), direction="+Z") - return result - @target(name="angle-joint") - def angle_joint(self, add_flange=True) -> Cq.Workplane: + def angle_joint(self) -> Cq.Workplane: """ Angular joint between two side panels (excluding chamber). This sits at the intersection of 4 side panels to provide compressive, shear, and tensile strength. @@ -1064,32 +1081,36 @@ class Onbashira(Model): result.tagAbsolute(f"holeLSM{i}", locrot * Cq.Location(dr0, -x, py), direction="-X") result.tagAbsolute(f"holeRSM{i}", locrot * Cq.Location(dr0, -x, -py), direction="-X") - if add_flange: - th = math.pi / self.n_side - r = self.bulk_radius - flange = ( - Cq.Workplane() - .sketch() - .push([ - (r, r * math.tan(th)) - ]) - .circle(self.angle_joint_flange_radius) - .reset() - .regularPolygon(self.side_width_inner, self.n_side, mode="i") - .finalize() - .extrude(self.angle_joint_gap) - .translate((0, 0, -self.angle_joint_gap/2)) - ) - ri = self.stator_bind_radius - h = self.angle_joint_gap - cyl = Cq.Solid.makeCylinder( - radius=self.rotor_bind_bolt_diam/2, - height=h, - pnt=(ri * math.cos(th), ri * math.sin(th), -h/2), - ) - result = result + flange - cyl - result.tagAbsolute("holeStatorL", (ri * math.cos(th), ri * math.sin(th), h/2), direction="+Z") - result.tagAbsolute("holeStatorR", (ri * math.cos(th), ri * math.sin(th), -h/2), direction="-Z") + # Generate the flange geometry + th = math.pi / self.n_side + flange = ( + Cq.Workplane() + .sketch() + .regularPolygon(self.side_width_inner, self.n_side) + .regularPolygon(self.side_width_inner - self.angle_joint_flange_extension, self.n_side, mode="s") + .finalize() + .extrude(self.angle_joint_gap) + .translate((0, 0, -self.angle_joint_gap/2)) + ) + flange = flange * intersector + ri = self.stator_bind_radius + h = self.angle_joint_gap + cyl = Cq.Solid.makeCylinder( + radius=self.rotor_bind_bolt_diam/2, + height=h, + pnt=(0, 0, -h/2), + ) + side_pos = Cq.Location(ri * math.cos(th), self.angle_joint_extra_hole_offset, 0) + side_pos2 = Cq.Location.rot2d(360/self.n_side) * side_pos.flip_y() + result = ( + result + + flange + - cyl.moved(ri * math.cos(th), ri * math.sin(th), 0) + - cyl.moved(side_pos.toTuple()) + - cyl.moved(side_pos2.toTuple()) + ) + result.tagAbsolute("holeStatorL", (ri * math.cos(th), ri * math.sin(th), h/2), direction="+Z") + result.tagAbsolute("holeStatorR", (ri * math.cos(th), ri * math.sin(th), -h/2), direction="-Z") return result def assembly_ring(self, base) -> Cq.Assembly: @@ -1184,7 +1205,7 @@ class Onbashira(Model): material=self.material_side, role=Role.STRUCTURE | Role.DECORATION, ) - #.add(self.assembly_barrel(), name="barrel") + .add(self.assembly_barrel(), name="barrel") ) for i in range(self.n_side): j = (i + 1) % self.n_side @@ -1205,6 +1226,13 @@ class Onbashira(Model): "Plane", ) + a = a.constrain( + f"barrel/stator2?holeB{i}", + f"ring1/side{i}?holeStatorR", + "Plane", + ) + + # Generate bolts for the chamber back name_bolt =f"chamber_back{i}boltFPI{ih}" a = a.addS( BOLT_COMMON.generate(), From bec15c51362bf8317a774a3ac0215c35aae3a928 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Thu, 29 May 2025 00:52:56 -0700 Subject: [PATCH 32/59] Model of the motor --- nhf/touhou/yasaka_kanako/onbashira.py | 43 +++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/nhf/touhou/yasaka_kanako/onbashira.py b/nhf/touhou/yasaka_kanako/onbashira.py index fcba7ba..c2c631b 100644 --- a/nhf/touhou/yasaka_kanako/onbashira.py +++ b/nhf/touhou/yasaka_kanako/onbashira.py @@ -40,15 +40,48 @@ class Motor(Model): power: float = 30.0 # watts diam_thread: float = 4.0 - diam_body: float = 51.82 - height_body: float = 70.87 - height_shaft: float = 37.85 + diam_body: float = 51.0 + height_body: float = 83.5 + diam_ring: float = 25.93 + height_ring: float = 6.55 + height_shaft: float = 38.1 + # Distance between anchor and the body + dx_anchor: float = 20.2 + height_anchor: float = 10.4 def __post_init__(self): + assert self.diam_ring < self.diam_body + assert self.height_ring < self.height_body + assert self.dx_anchor < self.diam_body / 2 pass - def model(self) -> Cq.Workplane: - pass + def generate(self) -> Cq.Workplane: + result = ( + Cq.Workplane() + .cylinder( + radius=self.diam_body/2, + height=self.height_body - self.height_ring, + centered=(True, True, False) + ) + .faces(">Z") + .cylinder( + radius=self.diam_ring/2, + height=self.height_ring, + centered=(True, True, False) + ) + ) + shaft = Cq.Solid.makeCylinder( + radius=self.diam_thread/2, + height=self.height_shaft, + pnt=(0, 0, self.height_body) + ) + anchor = Cq.Solid.makeCylinder( + radius=self.diam_thread/2, + height=self.height_anchor, + pnt=(0, 0, self.height_body - self.height_ring) + ) + result = result + shaft + anchor.moved(self.dx_anchor, 0, 0) + anchor.moved(-self.dx_anchor, 0, 0) + return result @dataclass From a3288ce98f901b6af67ac0510048a4c220c6e618 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Thu, 29 May 2025 01:19:19 -0700 Subject: [PATCH 33/59] Spindle geometry --- nhf/touhou/yasaka_kanako/onbashira.py | 35 +++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/nhf/touhou/yasaka_kanako/onbashira.py b/nhf/touhou/yasaka_kanako/onbashira.py index c2c631b..c7e23ce 100644 --- a/nhf/touhou/yasaka_kanako/onbashira.py +++ b/nhf/touhou/yasaka_kanako/onbashira.py @@ -32,8 +32,12 @@ BOLT_COMMON = FlatHeadBolt( pitch=1.0, ) -@dataclass + +@dataclass(frozen=True) class Motor(Model): + """ + Drive motor for the main barrel + """ mass: float = 589.7 voltage: float = 12.0 # V @@ -133,6 +137,8 @@ class Onbashira(Model): barrel_wall_thickness: float = 25.4 / 8 barrel_length: float = 25.4 * 12 + # Gap between the stator edge and the inner face of the barrel + stator_gap: float = 10.0 # Radius from barrel centre to axis rotation_radius: float = 66.0 n_bearing_balls: int = 12 @@ -144,11 +150,12 @@ class Onbashira(Model): bearing_track_radius: float = 100.0 # Gap between the inner and outer bearing disks bearing_gap: float = 10.0 + bearing_spindle_max_diam: float = 13.0 bearing_disk_thickness: float = 25.4 / 8 rotor_inner_radius: float = 40.0 rotor_bind_bolt_diam: float = BOLT_COMMON.diam_thread - rotor_bind_radius: float = 78.0 + rotor_bind_radius: float = 82.0 rotor_spacer_outer_diam: float = 15.0 stator_bind_radius: float = 135.0 @@ -157,6 +164,8 @@ class Onbashira(Model): handle_length: float = 80.0 handle_height: float = 40.0 + motor: Motor = Motor() + material_side: Material = Material.WOOD_BIRCH material_bearing: Material = Material.PLASTIC_PLA material_spacer: Material = Material.PLASTIC_PLA @@ -357,6 +366,28 @@ class Onbashira(Model): ) return outer - inner + @target(name="bearing-spindle") + def bearing_spindle(self) -> Cq.Solid: + r1 = self.bearing_gap / 2 + r2 = self.bearing_spindle_max_diam + h = self.bearing_disk_gap + cone1 = Cq.Solid.makeCone( + radius1=r2, + radius2=r1, + height=h/2 + ) + cone2 = Cq.Solid.makeCone( + radius1=r1, + radius2=r2, + height=h/2, + ) + hole = Cq.Solid.makeCylinder( + radius=(BOLT_COMMON.diam_thread + 1)/2, + height=h*2 + ).moved(0, 0, -h) + top = (cone1 + cone2.moved(0, 0, h/2)) - hole + return top + top.rotate((0,0,0),(1,0,0),180) + def assembly_barrel(self) -> Cq.Assembly: """ The assembly with gun barrels From d937fc9513c9697b24ed79e8a40175e5314404b1 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Thu, 29 May 2025 09:22:44 -0700 Subject: [PATCH 34/59] Retrofit handle --- nhf/touhou/yasaka_kanako/onbashira.py | 163 +++++++++++++++----------- 1 file changed, 94 insertions(+), 69 deletions(-) diff --git a/nhf/touhou/yasaka_kanako/onbashira.py b/nhf/touhou/yasaka_kanako/onbashira.py index c7e23ce..6ea555b 100644 --- a/nhf/touhou/yasaka_kanako/onbashira.py +++ b/nhf/touhou/yasaka_kanako/onbashira.py @@ -121,6 +121,8 @@ class Onbashira(Model): angle_joint_bolt_position: list[float] = field(default_factory=lambda: [ (40, 10), ]) + angle_joint_flange_thickness: float = 7.8 + angle_joint_flange_radius: float = 23.0 angle_joint_flange_extension: float = 23.0 angle_joint_extra_hole_offset: float = 20.0 @@ -160,9 +162,9 @@ class Onbashira(Model): stator_bind_radius: float = 135.0 handle_base_height: float = 10.0 - handle_thickness: float = 12.0 - handle_length: float = 80.0 - handle_height: float = 40.0 + handle_thickness: float = 17.0 + handle_length: float = 140.0 + handle_height: float = 50.0 motor: Motor = Motor() @@ -725,6 +727,32 @@ class Onbashira(Model): ) return a + def angle_joint_flange(self) -> Cq.Workplane: + th = math.pi / self.n_side + r = self.bulk_radius + flange = ( + Cq.Workplane() + .sketch() + .push([ + (r, r * math.tan(th)) + ]) + .circle(self.angle_joint_flange_radius) + .reset() + .regularPolygon(self.side_width_inner, self.n_side, mode="i") + .finalize() + .extrude(self.angle_joint_flange_thickness) + .translate((0, 0, -self.angle_joint_flange_thickness/2)) + ) + ri = self.stator_bind_radius + h = self.angle_joint_flange_thickness + # drill hole + cyl = Cq.Solid.makeCylinder( + radius=self.rotor_bind_bolt_diam/2, + height=h, + pnt=(ri * math.cos(th), ri * math.sin(th), -h/2), + ) + return flange - cyl + @target(name="angle-joint-chamber-back") def angle_joint_chamber_back(self) -> Cq.Workplane: slot = ( @@ -996,34 +1024,10 @@ class Onbashira(Model): result.tagAbsolute(f"holeRSO{i}", locrot * Cq.Location(dr, -x, -py), direction="+X") # Generate the flange geometry + flange = self.angle_joint_flange() + result = result + self.angle_joint_flange() th = math.pi / self.n_side - flange = ( - Cq.Workplane() - .sketch() - .regularPolygon(self.side_width_inner, self.n_side) - .regularPolygon(self.side_width_inner - self.angle_joint_flange_extension, self.n_side, mode="s") - .finalize() - .extrude(self.angle_joint_gap) - .translate((0, 0, -self.angle_joint_gap/2)) - ) - flange = flange * intersector ri = self.stator_bind_radius - h = self.angle_joint_gap - # Drill holes for connectors - cyl = Cq.Solid.makeCylinder( - radius=self.rotor_bind_bolt_diam/2, - height=h, - pnt=(0, 0, -h/2), - ) - side_pos = Cq.Location(ri * math.cos(th), self.angle_joint_extra_hole_offset, 0) - side_pos2 = Cq.Location.rot2d(360/self.n_side) * side_pos.flip_y() - result = ( - result - + flange - - cyl.moved(ri * math.cos(th), ri * math.sin(th), 0) - - cyl.moved(side_pos.toTuple()) - - cyl.moved(side_pos2.toTuple()) - ) result.tagAbsolute("holeStatorL", (ri * math.cos(th), ri * math.sin(th), h/2), direction="+Z") result.tagAbsolute("holeStatorR", (ri * math.cos(th), ri * math.sin(th), -h/2), direction="-Z") return result @@ -1146,33 +1150,10 @@ class Onbashira(Model): result.tagAbsolute(f"holeRSM{i}", locrot * Cq.Location(dr0, -x, -py), direction="-X") # Generate the flange geometry + flange = self.angle_joint_flange() + result = result + self.angle_joint_flange() th = math.pi / self.n_side - flange = ( - Cq.Workplane() - .sketch() - .regularPolygon(self.side_width_inner, self.n_side) - .regularPolygon(self.side_width_inner - self.angle_joint_flange_extension, self.n_side, mode="s") - .finalize() - .extrude(self.angle_joint_gap) - .translate((0, 0, -self.angle_joint_gap/2)) - ) - flange = flange * intersector ri = self.stator_bind_radius - h = self.angle_joint_gap - cyl = Cq.Solid.makeCylinder( - radius=self.rotor_bind_bolt_diam/2, - height=h, - pnt=(0, 0, -h/2), - ) - side_pos = Cq.Location(ri * math.cos(th), self.angle_joint_extra_hole_offset, 0) - side_pos2 = Cq.Location.rot2d(360/self.n_side) * side_pos.flip_y() - result = ( - result - + flange - - cyl.moved(ri * math.cos(th), ri * math.sin(th), 0) - - cyl.moved(side_pos.toTuple()) - - cyl.moved(side_pos2.toTuple()) - ) result.tagAbsolute("holeStatorL", (ri * math.cos(th), ri * math.sin(th), h/2), direction="+Z") result.tagAbsolute("holeStatorR", (ri * math.cos(th), ri * math.sin(th), -h/2), direction="-Z") return result @@ -1194,7 +1175,9 @@ class Onbashira(Model): def handle(self) -> Cq.Workplane: w = self.side_width + self.angle_joint_extra_width base = ( - Cq.Workplane() + Cq.Workplane( + origin=(0, 0, -self.handle_base_height) + ) .box( length=w, width=self.angle_joint_depth, @@ -1216,16 +1199,40 @@ class Onbashira(Model): depth=None, ) ) - handle = ( - Cq.Workplane(origin=(0, 0, self.handle_height)) - .box( - length=self.handle_length, - width=self.handle_thickness, - height=self.handle_thickness, - ) + dx = self.handle_length / 2 - self.handle_thickness / 2 + assert self.handle_length < w + z = self.handle_height - self.handle_thickness / 2 + handle = Cq.Solid.makeCylinder( + radius=self.handle_thickness/2, + height=dx * 2, + pnt=(-dx, 0, z), + dir=(1, 0, 0), ) - return base + handle + pillar = Cq.Solid.makeCylinder( + radius=self.handle_thickness/2, + height=z, + ) + joint = Cq.Solid.makeSphere(radius=self.handle_thickness/2) + result = ( + base + + handle + + pillar.moved(dx, 0, 0) + + pillar.moved(-dx, 0, 0) + + joint.moved(dx, 0, z) + + joint.moved(-dx, 0, z) + ) + t = self.handle_base_height + for i, (x, y) in enumerate(self.angle_joint_bolt_position): + result.tagAbsolute(f"holeLPO{i}", (+x, y, 0), direction="+Z") + result.tagAbsolute(f"holeLSO{i}", (-x, y, 0), direction="+Z") + result.tagAbsolute(f"holeLPI{i}", (+x, y, -t), direction="-Z") + result.tagAbsolute(f"holeLSI{i}", (-x, y, -t), direction="-Z") + result.tagAbsolute(f"holeRPO{i}", (+x, -y, 0), direction="+Z") + result.tagAbsolute(f"holeRSO{i}", (-x, -y, 0), direction="+Z") + result.tagAbsolute(f"holeRPI{i}", (+x, -y, -t), direction="-Z") + result.tagAbsolute(f"holeRSI{i}", (-x, -y, -t), direction="-Z") + return result @assembly() def assembly(self) -> Cq.Assembly: a = Cq.Assembly() @@ -1269,8 +1276,26 @@ class Onbashira(Model): material=self.material_side, role=Role.STRUCTURE | Role.DECORATION, ) - .add(self.assembly_barrel(), name="barrel") + .addS( + self.handle(), + name="handle", + material=self.material_brace, + role=Role.STRUCTURE, + ) + #.add(self.assembly_barrel(), name="barrel") ) + # Add handle + for ih, (x, y) in enumerate(self.angle_joint_bolt_position): + a = a.constrain( + f"handle?holeLPI{ih}", + f"ring2/side0?holeLPO{ih}", + "Plane", + ) + a = a.constrain( + f"handle?holeRPI{ih}", + f"ring2/side0?holeRPO{ih}", + "Plane", + ) for i in range(self.n_side): j = (i + 1) % self.n_side for ih in range(len(self.angle_joint_bolt_position)): @@ -1290,11 +1315,11 @@ class Onbashira(Model): "Plane", ) - a = a.constrain( - f"barrel/stator2?holeB{i}", - f"ring1/side{i}?holeStatorR", - "Plane", - ) + #a = a.constrain( + # f"barrel/stator2?holeB{i}", + # f"ring1/side{i}?holeStatorR", + # "Plane", + #) # Generate bolts for the chamber back name_bolt =f"chamber_back{i}boltFPI{ih}" From 6709e4f32ea6c0b7fb9633044c40355cdce76156 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Thu, 29 May 2025 14:04:12 -0700 Subject: [PATCH 35/59] Bearing couplers --- nhf/touhou/yasaka_kanako/onbashira.py | 288 +++++++++++++++++++------- 1 file changed, 209 insertions(+), 79 deletions(-) diff --git a/nhf/touhou/yasaka_kanako/onbashira.py b/nhf/touhou/yasaka_kanako/onbashira.py index 6ea555b..93a6223 100644 --- a/nhf/touhou/yasaka_kanako/onbashira.py +++ b/nhf/touhou/yasaka_kanako/onbashira.py @@ -31,6 +31,15 @@ BOLT_COMMON = FlatHeadBolt( height_thread=30.0, pitch=1.0, ) +BOLT_BEARING = FlatHeadBolt( + # FIXME: measure + mass=0.0, + diam_head=12.8, + height_head=2.8, + diam_thread=4.0, + height_thread=30.0, + pitch=1.0, +) @dataclass(frozen=True) @@ -124,12 +133,12 @@ class Onbashira(Model): angle_joint_flange_thickness: float = 7.8 angle_joint_flange_radius: float = 23.0 angle_joint_flange_extension: float = 23.0 - angle_joint_extra_hole_offset: float = 20.0 # Mating structure on the angle joint angle_joint_conn_thickness: float = 4.0 angle_joint_conn_depth: float = 15.0 angle_joint_conn_width: float = 15.0 + angle_joint_bind_radius: float = 135.0 chamber_side_length: float = 400.0 chamber_side_width_ex: float = 20.0 @@ -140,26 +149,33 @@ class Onbashira(Model): barrel_length: float = 25.4 * 12 # Gap between the stator edge and the inner face of the barrel - stator_gap: float = 10.0 + stator_gap: float = 3.0 # Radius from barrel centre to axis - rotation_radius: float = 66.0 + rotation_radius: float = 64.0 n_bearing_balls: int = 12 - # Size of ball bearings - bearing_ball_diam: float = 25.4 * 1/2 - bearing_ball_gap: float = .5 # Thickness of bearing disks bearing_thickness: float = 20.0 - bearing_track_radius: float = 100.0 + bearing_track_radius: float = 97.0 # Gap between the inner and outer bearing disks bearing_gap: float = 10.0 + bearing_disk_gap: float = 10.0 bearing_spindle_max_diam: float = 13.0 - bearing_disk_thickness: float = 25.4 / 8 + bearing_gasket_extend: float = 12.0 + bearing_disk_thickness: float = 25.4 / 16 - rotor_inner_radius: float = 40.0 + # Coupling mechanism onto the chassis + stator_coupler_width: float = 14.0 + stator_coupler_thickness: float = 30.0 + stator_coupler_thickness_inner: float = 10.0 + + stator_bind_radius: float = 117.0 + # Extra bind sites for stator to prevent warping + stator_bind_extra: int = 2 + rotor_inner_radius: float = 36.0 rotor_bind_bolt_diam: float = BOLT_COMMON.diam_thread rotor_bind_radius: float = 82.0 + rotor_bind_extra: int = 1 rotor_spacer_outer_diam: float = 15.0 - stator_bind_radius: float = 135.0 handle_base_height: float = 10.0 handle_thickness: float = 17.0 @@ -171,7 +187,7 @@ class Onbashira(Model): material_side: Material = Material.WOOD_BIRCH material_bearing: Material = Material.PLASTIC_PLA material_spacer: Material = Material.PLASTIC_PLA - material_bearing_ball: Material = Material.ACRYLIC_TRANSPARENT + material_bearing_ball: Material = Material.PLASTIC_PLA material_barrel: Material = Material.ACRYLIC_BLACK material_brace: Material = Material.PLASTIC_PLA material_fastener: Material = Material.STEEL_STAINLESS @@ -179,10 +195,9 @@ class Onbashira(Model): def __post_init__(self): assert self.n_side >= 3 # Bulk must be large enough for the barrel + bearing to rotate - assert self.bulk_radius - self.side_thickness - self.bearing_thickness - self.bearing_diam > self.rotation_radius + self.barrel_diam / 2 - assert self.bearing_gap < 0.95 * self.bearing_ball_diam - assert self.rotor_bind_bolt_diam < self.rotor_bind_radius < self.bearing_track_radius - assert self.rotor_inner_radius < self.bearing_track_radius < self.stator_bind_radius + assert self.bulk_radius - self.side_thickness - self.bearing_thickness > self.rotation_radius + self.barrel_diam / 2 + assert BOLT_COMMON.diam_thread < self.rotor_bind_radius < self.bearing_track_radius + assert self.rotor_inner_radius < self.bearing_track_radius < self.angle_joint_bind_radius assert self.angle_joint_thickness > self.side_thickness for (x, y) in self.angle_joint_bolt_position: @@ -240,18 +255,6 @@ class Onbashira(Model): Radius of the bulk (surface of each side) to the centre """ return self.chamber_side_width / 2 / math.tan(math.radians(self.angle_side / 2)) - @property - def bearing_diam(self) -> float: - return self.bearing_ball_diam + self.bearing_ball_gap - - @property - def bearing_disk_gap(self) -> float: - """ - Gap between two bearing disks to touch the bearing balls - """ - diag = self.bearing_ball_diam - dx = self.bearing_gap - return math.sqrt(diag ** 2 - dx ** 2) @target(name="sanding-block") def sanding_block(self) -> Cq.Workplane: @@ -267,15 +270,64 @@ class Onbashira(Model): .extrude(self.side_width * 1.5) ) + @target(name="stator-coupler") + def stator_coupler(self) -> Cq.Workplane: + """ + Couples the stator to the chassis + """ + r1 = self.angle_joint_bind_radius + r2 = self.stator_bind_radius + assert r1 > r2 + + l = r1 - r2 + w = self.stator_coupler_width + h = self.stator_coupler_thickness + h_step = h - self.stator_coupler_thickness_inner + + intersector = Cq.Solid.makeBox( + length=l + w, + width=w, + height=h_step, + ).moved(0, -w/2, 0) + + profile = ( + Cq.Sketch() + .rect(l, w) + .push([ + (-l/2, 0), + (l/2, 0), + ]) + .circle(w/2, mode="a") + .push([ + (-l/2, 0), + (l/2, 0), + ]) + .circle(BOLT_COMMON.diam_thread/2, mode="s") + ) + result = ( + Cq.Workplane() + .placeSketch(profile) + .extrude(h) + ) + dx = l / 2 + result = result - intersector + result.tagAbsolute(f"holeOB", (-dx, 0, 0), direction="-Z") + result.tagAbsolute(f"holeIB", (+dx, 0, h_step), direction="-Z") + result.tagAbsolute(f"holeOF", (-dx, 0, h), direction="+Z") + result.tagAbsolute(f"holeIF", (+dx, 0, h), direction="+Z") + return result + @target(name="bearing-stator", kind=TargetKind.DXF) def profile_bearing_stator(self) -> Cq.Sketch: + assert self.stator_bind_radius < self.angle_joint_bind_radius return ( Cq.Sketch() - .regularPolygon(self.side_width - self.side_thickness, self.n_side) + .circle(self.bulk_radius - self.side_thickness - self.stator_gap) + #.regularPolygon(self.side_width - self.side_thickness - self.stator_gap, self.n_side*2) .circle(self.bearing_track_radius + self.bearing_gap/2, mode="s") .reset() .regularPolygon( - self.stator_bind_radius, self.n_side, + self.stator_bind_radius, self.n_side * (1 + self.stator_bind_extra), mode="c", tag="bolt") .vertices(tag="bolt") .circle(self.rotor_bind_bolt_diam/2, mode="s") @@ -286,20 +338,30 @@ class Onbashira(Model): .placeSketch(self.profile_bearing_stator()) .extrude(self.bearing_disk_thickness) ) + br = self.stator_bind_radius + th1 = math.radians(360 / self.n_side) + th2 = math.radians(360 / (self.n_side * (1 + self.stator_bind_extra))) for i in range(self.n_side): - angle = (i+0.5) * math.radians(360 / self.n_side) + angle = (i+0.5) * th1 result.faces(">Z").moveTo( - self.stator_bind_radius * math.cos(angle), - self.stator_bind_radius * math.sin(angle), + br * math.cos(angle), + br * math.sin(angle), ).tagPlane(f"holeF{i}") result.faces(" Cq.Sketch: - bolt_angle = 180 / self.n_side + bolt_angle = (180 / self.n_side) * 1.5 + n_binds = 1 + self.rotor_bind_extra return ( Cq.Sketch() .circle(self.bearing_track_radius - self.bearing_gap/2) @@ -312,8 +374,10 @@ class Onbashira(Model): .circle(self.barrel_diam/2, mode="s") .reset() .regularPolygon( - self.rotor_bind_radius, self.n_side, - mode="c", tag="bolt", angle=bolt_angle) + r=self.rotor_bind_radius, + n=self.n_side * n_binds, + mode="c", tag="bolt", + angle=bolt_angle) .vertices(tag="bolt") .circle(self.rotor_bind_bolt_diam/2, mode="s") ) @@ -325,7 +389,7 @@ class Onbashira(Model): ) @target(name="bearing-gasket", kind=TargetKind.DXF) def profile_bearing_gasket(self) -> Cq.Sketch: - dr = self.bearing_ball_diam + dr = self.bearing_gasket_extend eps = 0.05 return ( Cq.Sketch() @@ -336,7 +400,7 @@ class Onbashira(Model): self.bearing_track_radius, self.n_bearing_balls, mode="c", tag="corners") .vertices(tag="corners") - .circle(self.bearing_ball_diam/2 * (1+eps), mode="s") + .circle(BOLT_BEARING.diam_thread, mode="s") ) def bearing_gasket(self) -> Cq.Workplane: return ( @@ -353,11 +417,8 @@ class Onbashira(Model): """ pass - def bearing_ball(self) -> Cq.Solid: - return Cq.Solid.makeSphere(radius=self.bearing_ball_diam/2, angleDegrees1=-90) - - @target(name="rotor-spacer") - def rotor_spacer(self) -> Cq.Solid: + @target(name="stator-spacer") + def stator_spacer(self) -> Cq.Solid: outer = Cq.Solid.makeCylinder( radius=self.rotor_spacer_outer_diam/2, height=self.bearing_disk_gap, @@ -367,16 +428,31 @@ class Onbashira(Model): height=self.bearing_disk_gap ) return outer - inner + @target(name="rotor-spacer") + def rotor_spacer(self) -> Cq.Solid: + outer = Cq.Solid.makeCylinder( + radius=self.rotor_spacer_outer_diam/2, + height=self.bearing_disk_gap, + ) + inner = Cq.Solid.makeCylinder( + radius=BOLT_BEARING.diam_thread/2, + height=self.bearing_disk_gap + ) + return outer - inner + + @property + def bearing_spindle_height(self) -> float: + h = self.bearing_disk_gap + 2 * self.bearing_disk_thickness + return h * 2 @target(name="bearing-spindle") def bearing_spindle(self) -> Cq.Solid: r1 = self.bearing_gap / 2 r2 = self.bearing_spindle_max_diam - h = self.bearing_disk_gap - cone1 = Cq.Solid.makeCone( - radius1=r2, - radius2=r1, - height=h/2 + h = self.bearing_disk_gap + 2 * self.bearing_disk_thickness + cone1 = Cq.Solid.makeCylinder( + radius=r1, + height=h/2, ) cone2 = Cq.Solid.makeCone( radius1=r1, @@ -384,17 +460,33 @@ class Onbashira(Model): height=h/2, ) hole = Cq.Solid.makeCylinder( - radius=(BOLT_COMMON.diam_thread + 1)/2, + radius=(BOLT_BEARING.diam_thread + 1)/2, height=h*2 ).moved(0, 0, -h) top = (cone1 + cone2.moved(0, 0, h/2)) - hole return top + top.rotate((0,0,0),(1,0,0),180) - def assembly_barrel(self) -> Cq.Assembly: + def barrel(self) -> Cq.Compound: + """ + One gun barrel + """ + outer = Cq.Solid.makeCylinder( + radius=self.barrel_diam/2, + height=self.barrel_length, + ) + inner = Cq.Solid.makeCylinder( + radius=self.barrel_diam/2-self.barrel_wall_thickness, + height=self.barrel_length + ) + return outer - inner + + @assembly() + def assembly_machine(self) -> Cq.Assembly: """ The assembly with gun barrels """ z_lower = -self.bearing_disk_gap/2 - self.bearing_disk_thickness + gasket_h = self.bearing_spindle_height / 2 a = ( Cq.Assembly() .addS( @@ -427,36 +519,52 @@ class Onbashira(Model): ) .addS( self.bearing_gasket(), - name="gasket", + name="gasket_bot", material=self.material_bearing, role=Role.ROTOR, - loc=Cq.Location(0, 0, -self.bearing_disk_thickness/2) + loc=Cq.Location(0, 0, -gasket_h-self.bearing_disk_thickness) + ) + .addS( + self.bearing_gasket(), + name="gasket_top", + material=self.material_bearing, + role=Role.ROTOR, + loc=Cq.Location(0, 0, gasket_h) ) ) z = -self.bearing_disk_gap/2 for i in range(self.n_side): + loc_barrel = Cq.Location.rot2d((i+1/2) * 360/self.n_side) * \ + Cq.Location(self.rotation_radius, 0, -self.barrel_length/2) + a = a.addS( + self.barrel(), + name=f"barrel{i}", + material=self.material_barrel, + role=Role.DECORATION, + loc=loc_barrel, + ) loc = Cq.Location.rot2d(i * 360/self.n_side) * Cq.Location(self.rotor_bind_radius, 0, z) - a = a.addS( - self.rotor_spacer(), - name=f"spacerRotor{i}", - material=self.material_spacer, - role=Role.STRUCTURE, - loc=loc - ) - loc = Cq.Location.rot2d((i+0.5) * 360/self.n_side) * Cq.Location(self.stator_bind_radius, 0, z) - a = a.addS( - self.rotor_spacer(), - name=f"spacerStator{i}", - material=self.material_spacer, - role=Role.STRUCTURE, - loc=loc - ) + #a = a.addS( + # self.stator_spacer(), + # name=f"spacerRotor{i}", + # material=self.material_spacer, + # role=Role.STRUCTURE, + # loc=loc + #) + loc = Cq.Location.rot2d((i+0.5) * 360/self.n_side) * Cq.Location(self.angle_joint_bind_radius, 0, z) + #a = a.addS( + # self.rotor_spacer(), + # name=f"spacerStator{i}", + # material=self.material_spacer, + # role=Role.STRUCTURE, + # loc=loc + #) for i in range(self.n_bearing_balls): - ball = self.bearing_ball() + ball = self.bearing_spindle() loc = Cq.Location.rot2d(i * 360/self.n_bearing_balls) * Cq.Location(self.bearing_track_radius, 0, 0) a = a.addS( ball, - name=f"bearing_ball{i}", + name=f"bearing_spindle{i}", material=self.material_bearing_ball, role=Role.BEARING, loc=loc, @@ -692,7 +800,7 @@ class Onbashira(Model): .regularPolygon(self.side_width - self.side_thickness, self.n_side) .reset() .regularPolygon( - self.stator_bind_radius, self.n_side, + self.angle_joint_bind_radius, self.n_side, mode="c", tag="bolt") .vertices(tag="bolt") .circle(self.rotor_bind_bolt_diam/2, mode="s") @@ -707,8 +815,8 @@ class Onbashira(Model): # Mark all attachment points for i in range(self.n_side): angle = (i+0.5) * math.radians(360 / self.n_side) - x = self.stator_bind_radius * math.cos(angle) - y = self.stator_bind_radius * math.sin(angle) + x = self.angle_joint_bind_radius * math.cos(angle) + y = self.angle_joint_bind_radius * math.sin(angle) result.tagAbsolute(f"holeF{i}", (x, y, self.side_thickness), direction="+Z") result.tagAbsolute(f"holeB{i}", (x, -y, 0), direction="-Z") return result @@ -743,7 +851,7 @@ class Onbashira(Model): .extrude(self.angle_joint_flange_thickness) .translate((0, 0, -self.angle_joint_flange_thickness/2)) ) - ri = self.stator_bind_radius + ri = self.angle_joint_bind_radius h = self.angle_joint_flange_thickness # drill hole cyl = Cq.Solid.makeCylinder( @@ -878,7 +986,7 @@ class Onbashira(Model): .extrude(self.angle_joint_gap) .translate((0, 0, -flange_z)) ) - ri = self.stator_bind_radius + ri = self.angle_joint_bind_radius h = self.angle_joint_gap # Drill holes for connectors cyl = Cq.Solid.makeCylinder( @@ -1027,7 +1135,8 @@ class Onbashira(Model): flange = self.angle_joint_flange() result = result + self.angle_joint_flange() th = math.pi / self.n_side - ri = self.stator_bind_radius + ri = self.angle_joint_bind_radius + h = self.angle_joint_gap result.tagAbsolute("holeStatorL", (ri * math.cos(th), ri * math.sin(th), h/2), direction="+Z") result.tagAbsolute("holeStatorR", (ri * math.cos(th), ri * math.sin(th), -h/2), direction="-Z") return result @@ -1153,11 +1262,13 @@ class Onbashira(Model): flange = self.angle_joint_flange() result = result + self.angle_joint_flange() th = math.pi / self.n_side - ri = self.stator_bind_radius + ri = self.angle_joint_bind_radius + h = self.angle_joint_gap result.tagAbsolute("holeStatorL", (ri * math.cos(th), ri * math.sin(th), h/2), direction="+Z") result.tagAbsolute("holeStatorR", (ri * math.cos(th), ri * math.sin(th), -h/2), direction="-Z") return result + @assembly() def assembly_ring(self, base) -> Cq.Assembly: a = Cq.Assembly() r = self.bulk_radius @@ -1280,9 +1391,9 @@ class Onbashira(Model): self.handle(), name="handle", material=self.material_brace, - role=Role.STRUCTURE, + role=Role.HANDLE, ) - #.add(self.assembly_barrel(), name="barrel") + .add(self.assembly_machine(), name="machine") ) # Add handle for ih, (x, y) in enumerate(self.angle_joint_bolt_position): @@ -1298,6 +1409,25 @@ class Onbashira(Model): ) for i in range(self.n_side): j = (i + 1) % self.n_side + ir = (self.n_side - i) % self.n_side + + coupler_name = f"stator_coupler{i}" + a = a.addS( + self.stator_coupler(), + name=coupler_name, + material=self.material_brace, + role=Role.STRUCTURE, + ) + a = a.constrain( + f"{coupler_name}?holeOB", + f"ring1/side{i}?holeStatorL", + "Plane", + ) + a = a.constrain( + f"{coupler_name}?holeIF", + f"machine/stator2?holeB{ir}", + "Plane", + ) for ih in range(len(self.angle_joint_bolt_position)): a = a.constrain( f"chamber/side{i}?holeFPI{ih}", From 0f151bd279822712d4a01c7682d3dea46efbb364 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Thu, 29 May 2025 16:21:01 -0700 Subject: [PATCH 36/59] Motor assembly --- nhf/materials.py | 4 +- nhf/parts/fasteners.py | 2 +- nhf/touhou/yasaka_kanako/onbashira.py | 348 +++++++++++++++++++++++--- 3 files changed, 312 insertions(+), 42 deletions(-) diff --git a/nhf/materials.py b/nhf/materials.py index 09d9ad6..7477e62 100644 --- a/nhf/materials.py +++ b/nhf/materials.py @@ -34,7 +34,7 @@ class Role(Flag): STRUCTURE = auto() DECORATION = auto() ELECTRONIC = auto() - MOTION = auto() + MOTOR = auto() # Fasteners, etc. CONNECTION = auto() @@ -69,7 +69,7 @@ ROLE_COLOR_MAP = { 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), } diff --git a/nhf/parts/fasteners.py b/nhf/parts/fasteners.py index 1489464..492331f 100644 --- a/nhf/parts/fasteners.py +++ b/nhf/parts/fasteners.py @@ -35,7 +35,7 @@ class FlatHeadBolt(Item): centered=(True, True, False)) ) 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 diff --git a/nhf/touhou/yasaka_kanako/onbashira.py b/nhf/touhou/yasaka_kanako/onbashira.py index 93a6223..c2eeed8 100644 --- a/nhf/touhou/yasaka_kanako/onbashira.py +++ b/nhf/touhou/yasaka_kanako/onbashira.py @@ -31,6 +31,15 @@ BOLT_COMMON = FlatHeadBolt( height_thread=30.0, pitch=1.0, ) +BOLT_LONG = FlatHeadBolt( + # FIXME: measure + mass=0.0, + diam_head=12.8, + height_head=2.8, + diam_thread=6.0, + height_thread=50.0, + pitch=1.0, +) BOLT_BEARING = FlatHeadBolt( # FIXME: measure mass=0.0, @@ -41,6 +50,59 @@ BOLT_BEARING = FlatHeadBolt( pitch=1.0, ) +@dataclass(frozen=True) +class FlangeCoupler(Model): + + diam_thread: float = 8.0 + diam_inner: float = 10.0 + diam_outer: float = 22.0 + height: float = 12.0 + height_flange: float = 2.0 + + diam_thread_flange: float = 4.0 + n_hole_flange: int = 4 + r_hole_flange: float = 8.0 # FIXME: Measure! + + def generate(self) -> Cq.Workplane: + result = ( + Cq.Workplane() + .cylinder( + radius=self.diam_outer/2, + height=self.height_flange, + centered=(True, True, False), + ) + .faces(">Z") + .cylinder( + radius=self.diam_inner/2, + height=self.height - self.height_flange, + centered=(True, True, False), + ) + .faces(">Z") + .hole(self.diam_thread) + ) + holes = ( + Cq.Workplane() + .sketch() + .regularPolygon( + self.r_hole_flange, + self.n_hole_flange, + mode="c", + tag="holes", + ) + .vertices(tag="holes") + .circle(self.diam_thread_flange/2) + .finalize() + .extrude(self.height_flange) + ) + result -= holes + result.tagAbsolute("top", (0, 0, self.height), direction="+Z") + result.tagAbsolute("bot", (0, 0, 9), direction="-Z") + for i in range(self.n_hole_flange): + loc = Cq.Location.rot2d(i * 360 / self.n_hole_flange) * Cq.Location(self.r_hole_flange, 0) + result.tagAbsolute(f"holeT{i}", loc * Cq.Location(0, 0, self.height_flange), direction="+Z") + result.tagAbsolute(f"holeB{i}", loc, direction="-Z") + return result + @dataclass(frozen=True) class Motor(Model): @@ -53,13 +115,15 @@ class Motor(Model): power: float = 30.0 # watts diam_thread: float = 4.0 + diam_shaft: float = 8.0 diam_body: float = 51.0 height_body: float = 83.5 diam_ring: float = 25.93 height_ring: float = 6.55 height_shaft: float = 38.1 + height_base_shaft: float = 20.0 # FIXME: Measure # Distance between anchor and the body - dx_anchor: float = 20.2 + dx_anchor: float = 20.2 # FIXME: Measure height_anchor: float = 10.4 def __post_init__(self): @@ -68,6 +132,13 @@ class Motor(Model): assert self.dx_anchor < self.diam_body / 2 pass + @property + def dist_mount_rotor(self): + """ + Distance between mount point and shaft + """ + return self.height_base_shaft + self.height_ring + def generate(self) -> Cq.Workplane: result = ( Cq.Workplane() @@ -83,17 +154,26 @@ class Motor(Model): centered=(True, True, False) ) ) + base_shaft = Cq.Solid.makeBox( + length=self.diam_shaft, + width=self.diam_shaft, + height=self.height_base_shaft, + ).moved(-self.diam_shaft/2, -self.diam_shaft/2, self.height_body) shaft = Cq.Solid.makeCylinder( - radius=self.diam_thread/2, + radius=self.diam_shaft/2, height=self.height_shaft, - pnt=(0, 0, self.height_body) + pnt=(0, 0, self.height_body + self.height_base_shaft) ) + z_anchor = self.height_body - self.height_ring anchor = Cq.Solid.makeCylinder( radius=self.diam_thread/2, height=self.height_anchor, - pnt=(0, 0, self.height_body - self.height_ring) + pnt=(0, 0, z_anchor) ) - result = result + shaft + anchor.moved(self.dx_anchor, 0, 0) + anchor.moved(-self.dx_anchor, 0, 0) + result = result + base_shaft + shaft + anchor.moved(self.dx_anchor, 0, 0) + anchor.moved(-self.dx_anchor, 0, 0) + result.tagAbsolute("anchor1", (self.dx_anchor, 0, z_anchor), direction="+Z") + result.tagAbsolute("anchor2", (-self.dx_anchor, 0, z_anchor), direction="+Z") + result.tagAbsolute("shaft", (0, 0, self.height_body + self.height_base_shaft), direction="+Z") return result @@ -172,10 +252,11 @@ class Onbashira(Model): # Extra bind sites for stator to prevent warping stator_bind_extra: int = 2 rotor_inner_radius: float = 36.0 - rotor_bind_bolt_diam: float = BOLT_COMMON.diam_thread + rotor_bind_bolt_diam: float = BOLT_BEARING.diam_thread rotor_bind_radius: float = 82.0 rotor_bind_extra: int = 1 - rotor_spacer_outer_diam: float = 15.0 + stator_spacer_outer_diam: float = 15.0 + rotor_spacer_outer_diam: float = 12.0 handle_base_height: float = 10.0 handle_thickness: float = 17.0 @@ -183,6 +264,8 @@ class Onbashira(Model): handle_height: float = 50.0 motor: Motor = Motor() + flange_coupler: FlangeCoupler = FlangeCoupler() + auxiliary_thickness: float = 25.4 / 8 material_side: Material = Material.WOOD_BIRCH material_bearing: Material = Material.PLASTIC_PLA @@ -191,6 +274,7 @@ class Onbashira(Model): material_barrel: Material = Material.ACRYLIC_BLACK material_brace: Material = Material.PLASTIC_PLA material_fastener: Material = Material.STEEL_STAINLESS + material_auxiliary: Material = Material.WOOD_BIRCH def __post_init__(self): assert self.n_side >= 3 @@ -235,7 +319,15 @@ class Onbashira(Model): theta = math.pi / self.n_side dt = self.angle_joint_thickness * math.tan(theta) return dt * 2 - + @property + def angle_joint_bind_pos(self) -> Cq.Location: + """ + Planar position of the joint bind position + """ + th = math.pi / self.n_side + x = self.angle_joint_bind_radius * math.cos(th) + y = self.angle_joint_bind_radius * math.sin(th) + return Cq.Location.from2d(x, y) @property def angle_dihedral(self) -> float: @@ -270,6 +362,153 @@ class Onbashira(Model): .extrude(self.side_width * 1.5) ) + @target(name="motor-driver-shaft") + def motor_driver_shaft(self) -> Cq.Workplane: + """ + Driver shaft which connects to each barrel to move them. + + The driver shaft reaches + """ + return ( + Cq.Workplane() + .cylinder( + radius=self.barrel_diam/2, + height=20.0 + ) + ) + + @target(name="motor-driver-disk", kind=TargetKind.DXF) + def profile_motor_driver_disk(self) -> Cq.Sketch: + """ + A drive disk mounts onto the motor, and extends into gun barrels to turn them. + """ + return ( + Cq.Sketch() + .circle(self.rotor_radius) + # Drill out the centre which will accomodate the motor shaft + .circle(self.motor.diam_shaft/2, mode="s") + # Drill out couplers + .reset() + .regularPolygon( + self.flange_coupler.r_hole_flange, + self.flange_coupler.n_hole_flange, + mode="c", + tag="hole", + ) + .vertices(tag="hole") + .circle(self.flange_coupler.diam_thread_flange/2, mode="s") + .reset() + .regularPolygon( + self.rotation_radius, + self.n_side, + mode="c", + tag="const", + ) + .vertices(tag="const") + .circle(BOLT_COMMON.diam_thread/2, mode="s") + ) + def motor_driver_disk(self) -> Cq.Workplane: + result = ( + Cq.Workplane() + .placeSketch(self.profile_motor_driver_disk()) + .extrude(self.auxiliary_thickness) + ) + n = self.flange_coupler.n_hole_flange + for i in range(n): + loc = Cq.Location.rot2d(i * 360 / n) * Cq.Location(self.flange_coupler.r_hole_flange, 0) + result.tagAbsolute(f"holeT{i}", loc * Cq.Location(0, 0, self.auxiliary_thickness), direction="+Z") + result.tagAbsolute(f"holeB{i}", loc, direction="-Z") + return result + + @target(name="motor-mount-plate", kind=TargetKind.DXF) + def profile_motor_mount_plate(self) -> Cq.Sketch: + assert self.n_side == 6 + bx, by = self.angle_joint_bind_pos.to2d_pos() + gap = 10.0 + hole_dx = self.motor.dx_anchor + return ( + Cq.Sketch() + .rect((bx + gap) * 2, (by + gap) * 2) + .reset() + .rect(bx * 2, by * 2, mode="c", tag="corner") + .vertices(tag="corner") + .circle(BOLT_COMMON.diam_thread/2, mode="s") + .reset() + .push([ + (hole_dx, 0), + (-hole_dx, 0), + ]) + .circle(self.motor.diam_thread/2, mode="s") + ) + def motor_mount_plate(self) -> Cq.Workplane: + result = ( + Cq.Workplane() + .placeSketch(self.profile_motor_mount_plate()) + .extrude(self.auxiliary_thickness) + ) + result.tagAbsolute("anchor1", (self.motor.dx_anchor, 0, 0), direction="-Z") + result.tagAbsolute("anchor2", (-self.motor.dx_anchor, 0, 0), direction="-Z") + bp = self.angle_joint_bind_pos + for i in range(self.n_side): + if i in {1, 4}: + continue + angle = i * 360 / self.n_side + x, y = (Cq.Location.rot2d(angle) * bp).to2d_pos() + result.tagAbsolute(f"holeF{i}", (x, y, self.auxiliary_thickness), direction="+Z") + result.tagAbsolute(f"holeB{i}", (x, -y, 0), direction="-Z") + return result + + @assembly() + def assembly_motor(self) -> Cq.Assembly: + a = ( + Cq.Assembly() + .addS( + self.motor.generate(), + name="motor", + role=Role.MOTOR, + ) + .addS( + self.flange_coupler.generate(), + name="flange_coupler", + role=Role.CONNECTION | Role.STRUCTURE, + material=self.material_fastener, + ) + .addS( + self.motor_driver_disk(), + name="driver_disk", + role=Role.CONNECTION | Role.STRUCTURE, + material=self.material_auxiliary, + ) + .addS( + self.motor_mount_plate(), + name="mount_plate", + role=Role.CONNECTION | Role.STRUCTURE, + material=self.material_auxiliary, + ) + .constrain( + "mount_plate?anchor1", + "motor?anchor1", + "Plane", + ) + .constrain( + "mount_plate?anchor2", + "motor?anchor2", + "Plane", + ) + .constrain( + "flange_coupler?top", + "motor?shaft", + "Plane" + ) + ) + for i in range(self.flange_coupler.n_hole_flange): + a = a.constrain( + f"flange_coupler?holeB{i}", + f"driver_disk?holeB{i}", + "Plane", + ) + return a.solve() + @target(name="stator-coupler") def stator_coupler(self) -> Cq.Workplane: """ @@ -358,13 +597,16 @@ class Onbashira(Model): br * math.sin(-angle2), ).tagPlane(f"holeE{i}", direction="-Z") return result + @property + def rotor_radius(self) -> float: + return self.bearing_track_radius - self.bearing_gap/2 @target(name="bearing-rotor", kind=TargetKind.DXF) def profile_bearing_rotor(self) -> Cq.Sketch: bolt_angle = (180 / self.n_side) * 1.5 n_binds = 1 + self.rotor_bind_extra return ( Cq.Sketch() - .circle(self.bearing_track_radius - self.bearing_gap/2) + .circle(self.rotor_radius) .circle(self.rotor_inner_radius, mode="s") .reset() .regularPolygon( @@ -420,7 +662,7 @@ class Onbashira(Model): @target(name="stator-spacer") def stator_spacer(self) -> Cq.Solid: outer = Cq.Solid.makeCylinder( - radius=self.rotor_spacer_outer_diam/2, + radius=self.stator_spacer_outer_diam/2, height=self.bearing_disk_gap, ) inner = Cq.Solid.makeCylinder( @@ -533,6 +775,10 @@ class Onbashira(Model): ) ) z = -self.bearing_disk_gap/2 + da_bind_stator = 360 / self.n_side + da_bind_rotor = 360 / self.n_side + da_bind_stator_minor = 360 / self.n_side / (1 + self.stator_bind_extra) + da_bind_rotor_minor = 360 / self.n_side / (1 + self.rotor_bind_extra) for i in range(self.n_side): loc_barrel = Cq.Location.rot2d((i+1/2) * 360/self.n_side) * \ Cq.Location(self.rotation_radius, 0, -self.barrel_length/2) @@ -543,22 +789,26 @@ class Onbashira(Model): role=Role.DECORATION, loc=loc_barrel, ) - loc = Cq.Location.rot2d(i * 360/self.n_side) * Cq.Location(self.rotor_bind_radius, 0, z) - #a = a.addS( - # self.stator_spacer(), - # name=f"spacerRotor{i}", - # material=self.material_spacer, - # role=Role.STRUCTURE, - # loc=loc - #) - loc = Cq.Location.rot2d((i+0.5) * 360/self.n_side) * Cq.Location(self.angle_joint_bind_radius, 0, z) - #a = a.addS( - # self.rotor_spacer(), - # name=f"spacerStator{i}", - # material=self.material_spacer, - # role=Role.STRUCTURE, - # loc=loc - #) + for j in range(1 + self.rotor_bind_extra): + angle = i * da_bind_rotor + (j+0.5) * da_bind_rotor_minor + loc = Cq.Location.rot2d(angle) * Cq.Location(self.rotor_bind_radius, 0, z) + a = a.addS( + self.rotor_spacer(), + name=f"spacer_rotor{i}_{j}", + material=self.material_spacer, + role=Role.STRUCTURE, + loc=loc + ) + for j in range(1 + self.stator_bind_extra): + angle = i * da_bind_stator + (j+0.5) * da_bind_stator_minor + loc = Cq.Location.rot2d(angle) * Cq.Location(self.stator_bind_radius, 0, z) + a = a.addS( + self.stator_spacer(), + name=f"spacer_stator{i}_{j}", + material=self.material_spacer, + role=Role.STRUCTURE, + loc=loc + ) for i in range(self.n_bearing_balls): ball = self.bearing_spindle() loc = Cq.Location.rot2d(i * 360/self.n_bearing_balls) * Cq.Location(self.bearing_track_radius, 0, 0) @@ -1393,6 +1643,7 @@ class Onbashira(Model): material=self.material_brace, role=Role.HANDLE, ) + .add(self.assembly_motor(), name="motor") .add(self.assembly_machine(), name="machine") ) # Add handle @@ -1428,6 +1679,38 @@ class Onbashira(Model): f"machine/stator2?holeB{ir}", "Plane", ) + if i not in {1, 4}: + a = a.constrain( + f"motor/mount_plate?holeF{i}", + f"ring2/side{(ir+2)%self.n_side}?holeStatorR", + "Plane", + ) + + name_bolt =f"stator_outer_bolt{i}" + a = a.addS( + BOLT_LONG.generate(), + name=name_bolt, + material=self.material_fastener, + role=Role.CONNECTION, + ) + a = a.constrain( + f"{coupler_name}?holeOF", + f"{name_bolt}?root", + "Plane", + ) + + name_bolt =f"chamber_back{i}boltFPI{i}" + a = a.addS( + BOLT_COMMON.generate(), + name=name_bolt, + material=self.material_fastener, + role=Role.CONNECTION, + ) + a = a.constrain( + f"chamber_back?holeF{i}", + f"{name_bolt}?root", + "Plane", + ) for ih in range(len(self.angle_joint_bolt_position)): a = a.constrain( f"chamber/side{i}?holeFPI{ih}", @@ -1452,19 +1735,6 @@ class Onbashira(Model): #) # Generate bolts for the chamber back - name_bolt =f"chamber_back{i}boltFPI{ih}" - a = a.addS( - BOLT_COMMON.generate(), - name=name_bolt, - material=self.material_fastener, - role=Role.CONNECTION, - ) - a = a.constrain( - f"chamber_back?holeF{i}", - f"{name_bolt}?root", - "Plane", - param=0, - ) for (nl, nc, nr) in [ ("section1", "ring1", "section2"), ("section2", "ring2", "section3"), From 2d6d65235dcf6a948c2abada06e4fea9266eff37 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Thu, 29 May 2025 17:24:06 -0700 Subject: [PATCH 37/59] Turning bar --- nhf/touhou/yasaka_kanako/onbashira.py | 93 +++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/nhf/touhou/yasaka_kanako/onbashira.py b/nhf/touhou/yasaka_kanako/onbashira.py index c2eeed8..bfd252a 100644 --- a/nhf/touhou/yasaka_kanako/onbashira.py +++ b/nhf/touhou/yasaka_kanako/onbashira.py @@ -267,6 +267,9 @@ class Onbashira(Model): flange_coupler: FlangeCoupler = FlangeCoupler() auxiliary_thickness: float = 25.4 / 8 + turning_bar_width: float = 15.0 + electronic_mount_dx: float = 50.0 + material_side: Material = Material.WOOD_BIRCH material_bearing: Material = Material.PLASTIC_PLA material_spacer: Material = Material.PLASTIC_PLA @@ -821,6 +824,85 @@ class Onbashira(Model): ) return a + @target(name="turning-bar") + def turning_bar(self) -> Cq.Workplane: + """ + Converts the longitudinal/axial mount points on angle joints to + transverse mount points to make them more suitable for electronics. + """ + _, dx = self.angle_joint_bind_pos.to2d_pos() + t = 8 + w = self.turning_bar_width + result = ( + Cq.Workplane() + .box( + length=dx*2 + w, + width=w, + height=t, + centered=(True, True, False) + ) + ) + flange = Cq.Solid.makeBox( + length=w, + width=t, + height=w/2, + ).moved(-w/2, -t, -w/2) + Cq.Solid.makeCylinder( + radius=w/2, + height=t, + pnt=(0, -t, -w/2), + dir=(0, 1, 0), + ) + remover = Cq.Solid.makeCylinder( + radius=BOLT_COMMON.diam_thread/2, + height=w, + ) + removerf = Cq.Solid.makeCylinder( + radius=BOLT_COMMON.diam_thread/2, + height=w*2, + pnt=(0, -w, -w/2), + dir=(0, 1, 0), + ) + dxe = self.electronic_mount_dx + result = ( + result + + flange.moved(dx, w/2, 0) + + flange.moved(-dx, w/2, 0) + - remover.moved(dxe, 0, 0) + - remover.moved(-dxe, 0, 0) + - removerf.moved(dx, 0, 0) + - removerf.moved(-dx, 0, 0) + ) + result.tagAbsolute("holeBO1", (dx, w/2, -w/2), direction="+Y") + result.tagAbsolute("holeBO2", (-dx, w/2, -w/2), direction="+Y") + result.tagAbsolute("holeMO1", (dxe, 0, t)) + result.tagAbsolute("holeMO2", (-dxe, 0, t)) + return result + @target(name="raising-bar") + def raising_bar(self) -> Cq.Workplane: + """ + Create new longitudinal mount points closer to the centre axis, and a + ring for mounting lights + """ + _, dx = self.angle_joint_bind_pos.to2d_pos() + gap = 10 + t1 = 5 + theta = math.pi / self.n_side + r0 = self.bulk_radius + result = ( + Cq.Workplane() + .sketch() + .circle(self.rotation_radius + gap) + .circle(self.rotation_radius - gap, mode="s") + .polygon([ + (0, 0), + (r0 * math.cos(theta), r0 * math.sin(theta)), + (r0 * math.cos(theta), -r0 * math.sin(theta)), + ], mode="i") + .finalize() + .extrude(t1) + ) + return result + def profile_side_panel( self, @@ -1645,6 +1727,17 @@ class Onbashira(Model): ) .add(self.assembly_motor(), name="motor") .add(self.assembly_machine(), name="machine") + .add(self.turning_bar(), name="turning_bar1") + ) + a = a.constrain( + f"turning_bar1?holeBO2", + f"ring3/side0?holeStatorL", + "Plane", + ) + a = a.constrain( + f"turning_bar1?holeBO1", + f"ring3/side1?holeStatorL", + "Plane", ) # Add handle for ih, (x, y) in enumerate(self.angle_joint_bolt_position): From e94546b01765f0e80e105f1d7723332df96a68cd Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Thu, 29 May 2025 23:40:12 -0700 Subject: [PATCH 38/59] Motor seat and coupler --- nhf/touhou/yasaka_kanako/onbashira.py | 454 +++++++++++++++++++++----- 1 file changed, 369 insertions(+), 85 deletions(-) diff --git a/nhf/touhou/yasaka_kanako/onbashira.py b/nhf/touhou/yasaka_kanako/onbashira.py index bfd252a..e177959 100644 --- a/nhf/touhou/yasaka_kanako/onbashira.py +++ b/nhf/touhou/yasaka_kanako/onbashira.py @@ -3,10 +3,23 @@ from nhf.materials import Role, Material import nhf.utils from nhf.parts.fasteners import FlatHeadBolt, HexNut, Washer +from typing import Optional, Union import math from dataclasses import dataclass, field import cadquery as Cq +def has_part(li: Optional[list[str]], name: Union[str, list[str]]) -> bool: + """ + Check if a part exists in a name list + """ + if li: + if isinstance(name, list): + return all(n in li for n in name) + else: + return name in li + else: + return True + NUT_COMMON = HexNut( # FIXME: measure mass=0.0, @@ -227,6 +240,8 @@ class Onbashira(Model): barrel_diam: float = 25.4 * 1.5 barrel_wall_thickness: float = 25.4 / 8 barrel_length: float = 25.4 * 12 + # Longitudinal shift + barrel_shift: float = -20.0 # Gap between the stator edge and the inner face of the barrel stator_gap: float = 3.0 @@ -267,6 +282,16 @@ class Onbashira(Model): flange_coupler: FlangeCoupler = FlangeCoupler() auxiliary_thickness: float = 25.4 / 8 + # Distance between bind point and motor's mount points + motor_driver_radius: float = 110.0 + motor_seat_depth: float = 95.0 + motor_seat_radius: float = 45.0 + motor_coupler_flange_thickness: float = 10.0 + motor_coupler_flange_radius: float = 8.0 + motor_coupler_height: float = 100.0 + motor_coupler_conn_dx: float = 30.0 + motor_coupler_wall_thickness: float = 5.0 + motor_coupler_inner_gap: float = 1.0 turning_bar_width: float = 15.0 electronic_mount_dx: float = 50.0 @@ -365,29 +390,70 @@ class Onbashira(Model): .extrude(self.side_width * 1.5) ) - @target(name="motor-driver-shaft") - def motor_driver_shaft(self) -> Cq.Workplane: + @target(name="motor-coupler") + def motor_coupler(self) -> Cq.Workplane: """ - Driver shaft which connects to each barrel to move them. - - The driver shaft reaches + Coupler which connects to each barrel to move them. """ - return ( + x = self.motor_coupler_conn_dx + y0 = self.barrel_diam/2 + self.motor_coupler_wall_thickness + y = self.motor_coupler_flange_radius + t = self.motor_coupler_flange_thickness + flange = ( + Cq.Workplane() + .sketch() + .polygon([ + (x, y), + (0, y0), + (-x, y), + (-x, -y), + (0, -y0), + (x, -y), + ]) + .reset() + .push([ + (x, 0), (-x, 0) + ]) + .circle(y, mode="a") + .circle(BOLT_BEARING.diam_thread/2, mode="s") + .reset() + .circle(self.barrel_diam/2, mode="s") + .finalize() + .extrude(t) + ) + body = ( Cq.Workplane() .cylinder( - radius=self.barrel_diam/2, - height=20.0 + radius=self.barrel_diam/2 + self.motor_coupler_wall_thickness, + height=self.motor_coupler_height, + centered=(True, True, False) ) + .faces(">Z") + .hole(self.barrel_diam + self.motor_coupler_inner_gap*2) ) + result = body + flange + result.tagAbsolute("holeT1", (x, 0, t), direction="+Z") + result.tagAbsolute("holeT2", (-x, 0, t), direction="+Z") + result.tagAbsolute("holeB1", (x, 0, 0), direction="-Z") + result.tagAbsolute("holeB2", (-x, 0, 0), direction="-Z") + return result @target(name="motor-driver-disk", kind=TargetKind.DXF) def profile_motor_driver_disk(self) -> Cq.Sketch: """ A drive disk mounts onto the motor, and extends into gun barrels to turn them. """ + hole_diam = self.barrel_diam - self.barrel_wall_thickness * 2 + + coupler_holes = [ + Cq.Location.rot2d(i * 360 / self.n_side) * + Cq.Location.from2d(self.rotation_radius + sx * self.motor_coupler_conn_dx, 0) + for i in range(self.n_side) + for sx in (-1, 1) + ] return ( Cq.Sketch() - .circle(self.rotor_radius) + .circle(self.motor_driver_radius) # Drill out the centre which will accomodate the motor shaft .circle(self.motor.diam_shaft/2, mode="s") # Drill out couplers @@ -404,11 +470,19 @@ class Onbashira(Model): .regularPolygon( self.rotation_radius, self.n_side, + angle=180 / self.n_side, mode="c", tag="const", ) .vertices(tag="const") - .circle(BOLT_COMMON.diam_thread/2, mode="s") + .circle(hole_diam/2, mode="s") + .reset() + # Create coupler holes + .push([ + loc.to2d_pos() + for loc in coupler_holes + ]) + .circle(BOLT_BEARING.diam_thread /2, mode="s") ) def motor_driver_disk(self) -> Cq.Workplane: result = ( @@ -421,19 +495,26 @@ class Onbashira(Model): loc = Cq.Location.rot2d(i * 360 / n) * Cq.Location(self.flange_coupler.r_hole_flange, 0) result.tagAbsolute(f"holeT{i}", loc * Cq.Location(0, 0, self.auxiliary_thickness), direction="+Z") result.tagAbsolute(f"holeB{i}", loc, direction="-Z") + loc_z = Cq.Location(0, 0, self.auxiliary_thickness) + loc_outer = Cq.Location.from2d(self.rotation_radius + self.motor_coupler_conn_dx, 0) + loc_inner = Cq.Location.from2d(self.rotation_radius - self.motor_coupler_conn_dx, 0) + for i in range(self.n_side): + loc_rot = Cq.Location.rot2d(i * 360 / self.n_side) + p_outer, _ = (loc_z * loc_rot * loc_outer).toTuple() + p_inner, _ = (loc_z * loc_rot * loc_inner).toTuple() + result.tagAbsolute(f"holeCOF{i}", p_outer, direction="+Z") + result.tagAbsolute(f"holeCIF{i}", p_inner, direction="+Z") return result @target(name="motor-mount-plate", kind=TargetKind.DXF) def profile_motor_mount_plate(self) -> Cq.Sketch: - assert self.n_side == 6 - bx, by = self.angle_joint_bind_pos.to2d_pos() + r = self.motor_seat_radius gap = 10.0 hole_dx = self.motor.dx_anchor return ( Cq.Sketch() - .rect((bx + gap) * 2, (by + gap) * 2) - .reset() - .rect(bx * 2, by * 2, mode="c", tag="corner") + .circle(r + gap) + .regularPolygon(r, self.n_side, mode="c", tag="corner") .vertices(tag="corner") .circle(BOLT_COMMON.diam_thread/2, mode="s") .reset() @@ -451,12 +532,10 @@ class Onbashira(Model): ) result.tagAbsolute("anchor1", (self.motor.dx_anchor, 0, 0), direction="-Z") result.tagAbsolute("anchor2", (-self.motor.dx_anchor, 0, 0), direction="-Z") - bp = self.angle_joint_bind_pos + r = self.motor_seat_radius for i in range(self.n_side): - if i in {1, 4}: - continue angle = i * 360 / self.n_side - x, y = (Cq.Location.rot2d(angle) * bp).to2d_pos() + x, y = (Cq.Location.rot2d(angle) * Cq.Location.from2d(0, r)).to2d_pos() result.tagAbsolute(f"holeF{i}", (x, y, self.auxiliary_thickness), direction="+Z") result.tagAbsolute(f"holeB{i}", (x, -y, 0), direction="-Z") return result @@ -510,6 +589,49 @@ class Onbashira(Model): f"driver_disk?holeB{i}", "Plane", ) + + # Add the motor seats + assert self.n_side % 2 == 0 + for i in range(self.n_side // 2): + name_seat = f"seat{i}" + a = ( + a.addS( + self.motor_seat(), + name=name_seat, + role=Role.STRUCTURE, + material=self.material_brace + ) + .constrain( + f"{name_seat}?holeMF1", + f"mount_plate?holeB{i*2}", + "Plane" + ) + .constrain( + f"{name_seat}?holeMF2", + f"mount_plate?holeB{i*2+1}", + "Plane" + ) + ) + for i in range(self.n_side): + name_coupler = f"coupler{i}" + a = ( + a.addS( + self.motor_coupler(), + name=name_coupler, + role=Role.CONNECTION, + material=self.material_brace, + ) + .constrain( + f"{name_coupler}?holeB1", + f"driver_disk?holeCOF{i}", + "Plane", + ) + .constrain( + f"{name_coupler}?holeB2", + f"driver_disk?holeCIF{i}", + "Plane", + ) + ) return a.solve() @target(name="stator-coupler") @@ -784,7 +906,7 @@ class Onbashira(Model): da_bind_rotor_minor = 360 / self.n_side / (1 + self.rotor_bind_extra) for i in range(self.n_side): loc_barrel = Cq.Location.rot2d((i+1/2) * 360/self.n_side) * \ - Cq.Location(self.rotation_radius, 0, -self.barrel_length/2) + Cq.Location(self.rotation_radius, 0, self.barrel_shift-self.barrel_length/2) a = a.addS( self.barrel(), name=f"barrel{i}", @@ -877,30 +999,172 @@ class Onbashira(Model): result.tagAbsolute("holeMO1", (dxe, 0, t)) result.tagAbsolute("holeMO2", (-dxe, 0, t)) return result - @target(name="raising-bar") - def raising_bar(self) -> Cq.Workplane: + + @target(name="motor-seat") + def motor_seat(self) -> Cq.Workplane: """ Create new longitudinal mount points closer to the centre axis, and a ring for mounting lights """ - _, dx = self.angle_joint_bind_pos.to2d_pos() + bx, by = self.angle_joint_bind_pos.to2d_pos() gap = 10 - t1 = 5 + t1 = 10 + base_w = 17.0 theta = math.pi / self.n_side + theta2 = theta * 0.5 + track_width = 7.0 r0 = self.bulk_radius - result = ( - Cq.Workplane() - .sketch() - .circle(self.rotation_radius + gap) - .circle(self.rotation_radius - gap, mode="s") + r1 = self.rotation_radius + gap + r2 = self.rotation_radius - gap + profile_arc = ( + Cq.Sketch() + .circle(r1) + .circle(r2, mode="s") .polygon([ (0, 0), (r0 * math.cos(theta), r0 * math.sin(theta)), (r0 * math.cos(theta), -r0 * math.sin(theta)), ], mode="i") + ) + profile_base = ( + profile_arc + .reset() + .polygon([ + (bx - base_w/2, by), + (bx + base_w/2, by), + (bx + base_w/2, -by), + (bx - base_w/2, -by), + ]) + .reset() + .polygon([ + (r1 * math.cos(theta), r1 * math.sin(theta)), + (r1 * math.cos(theta2), r1 * math.sin(theta2)), + (r0 * math.cos(theta2), r0 * math.sin(theta2)), + (r0 * math.cos(theta), r0 * math.sin(theta)), + ]) + .polygon([ + (r1 * math.cos(theta), -r1 * math.sin(theta)), + (r1 * math.cos(theta2), -r1 * math.sin(theta2)), + (r0 * math.cos(theta2), -r0 * math.sin(theta2)), + (r0 * math.cos(theta), -r0 * math.sin(theta)), + ]) + .reset() + .push([ + (bx, by), (bx, -by), + ]) + .circle(base_w/2, mode="a") + .reset() + .push([ + (bx, by), (bx, -by), + ]) + .circle(BOLT_COMMON.diam_thread/2, mode="s") + ) + base = ( + Cq.Workplane() + .placeSketch(profile_base) + .extrude(t1) + ) + r3 = self.motor_seat_radius + r2_5 = r3 + BOLT_COMMON.diam_thread/2 + mount_x = r3 * math.cos(theta) + mount_y = r3 * math.sin(theta) + front = ( + Cq.Workplane() + .sketch() + .circle(r1) + .circle(self.rotation_radius+track_width/2, mode="s") + .circle(self.rotation_radius-track_width/2) + .circle(r2_5, mode="s") + .polygon([ + (0, 0), + (r0 * math.cos(theta), r0 * math.sin(theta)), + (r0 * math.cos(theta), -r0 * math.sin(theta)), + ], mode="i") + .push([ + (mount_x, mount_y), + (mount_x, -mount_y), + ]) + .circle(base_w/2) + .circle(BOLT_COMMON.diam_thread/2, mode="s") .finalize() .extrude(t1) ) + + # Construct the connection between the front and back + + profile_bridge_outer_base = ( + Cq.Sketch() + .polygon([ + (bx - base_w/2, by - base_w/2), + (bx + base_w/2, by - base_w/2), + (bx + base_w/2, by - base_w*1.5), + (bx - base_w/2, by - base_w*1.5), + ]) + .wires() + .val() + .moved(0, 0, t1) + ) + profile_bridge_outer_top = ( + Cq.Sketch() + .circle(r1) + .circle(self.rotation_radius+track_width/2, mode="s") + .polygon([ + (0, 0), + (r0 * math.cos(theta), r0 * math.sin(theta)), + (r0 * math.cos(theta2), r0 * math.sin(theta2)), + ], mode="i") + .wires() + .val() + .moved(0, 0, self.motor_seat_depth) + ) + profile_bridge_inner_base = ( + Cq.Sketch() + .circle(r1) + .circle(r2, mode="s") + .polygon([ + (0, 0), + (r0 * math.cos(theta), r0 * math.sin(theta)), + (r0 * math.cos(theta2), r0 * math.sin(theta2)), + ], mode="i") + .wires() + .val() + ) + profile_bridge_inner_top = ( + Cq.Sketch() + .circle(r1) + .circle(r2, mode="s") + .polygon([ + (0, 0), + (r0 * math.cos(theta), r0 * math.sin(theta)), + (r0 * math.cos(theta2), r0 * math.sin(theta2)), + ], mode="i") + .wires() + .val() + .moved(0, 0, self.motor_seat_depth - t1) + ) + bridge_outer = Cq.Solid.makeLoft([profile_bridge_outer_base, profile_bridge_outer_top]) + bridge_inner = Cq.Solid.makeLoft([profile_bridge_inner_base, profile_bridge_inner_top]) + hole_subtractor = Cq.Solid.makeCylinder( + radius=BOLT_COMMON.diam_thread/2, + height=t1, + pnt=((r1+r2)/2, 0, 0) + ) + result = ( + base + + front.translate((0, 0, self.motor_seat_depth - t1)) + + bridge_outer + + bridge_outer.mirror("XZ") + + bridge_inner + + bridge_inner.mirror("XZ") + - hole_subtractor + ) + + # Mark the mount points + result.tagAbsolute("holeBB1", (bx, +by, 0), direction="-Z") + result.tagAbsolute("holeBB2", (bx, -by, 0), direction="-Z") + result.tagAbsolute("holeMF1", (mount_x, +mount_y, self.motor_seat_depth), direction="+Z") + result.tagAbsolute("holeMF2", (mount_x, -mount_y, self.motor_seat_depth), direction="+Z") + return result @@ -1676,81 +1940,107 @@ class Onbashira(Model): result.tagAbsolute(f"holeRSI{i}", (-x, -y, -t), direction="-Z") return result + @assembly() - def assembly(self) -> Cq.Assembly: + def assembly(self, parts: Optional[list[str]] = None) -> Cq.Assembly: a = Cq.Assembly() - a = ( - a - .add( + if has_part(parts, "section1"): + a = a.add( self.assembly_section1(), name="section1", ) - .add( + if has_part(parts, "ring1"): + a = a.add( self.assembly_ring(self.angle_joint()), name="ring1", ) - .add( + if has_part(parts, "section2"): + a = a.add( self.assembly_section(length=self.side_length2, hasFrontHole=True, hasBackHole=True), name="section2", ) - .add( + if has_part(parts, "ring2"): + a = a.add( self.assembly_ring(self.angle_joint()), name="ring2", ) - .add( - self.assembly_section(length=self.side_length3, hasFrontHole=True, hasBackHole=True), - name="section3", - ) - .add( - self.assembly_ring(self.angle_joint_chamber_front()), - name="ring3", - ) - .add( - self.assembly_chamber(), - name="chamber", - ) - .add( - self.assembly_ring(self.angle_joint_chamber_back()), - name="ring4", - ) - .addS( - self.chamber_back(), - name="chamber_back", - material=self.material_side, - role=Role.STRUCTURE | Role.DECORATION, - ) - .addS( + a = a.addS( self.handle(), name="handle", material=self.material_brace, role=Role.HANDLE, ) - .add(self.assembly_motor(), name="motor") - .add(self.assembly_machine(), name="machine") - .add(self.turning_bar(), name="turning_bar1") - ) - a = a.constrain( - f"turning_bar1?holeBO2", - f"ring3/side0?holeStatorL", - "Plane", - ) - a = a.constrain( - f"turning_bar1?holeBO1", - f"ring3/side1?holeStatorL", - "Plane", - ) - # Add handle - for ih, (x, y) in enumerate(self.angle_joint_bolt_position): + # Handle constrain + for ih, (x, y) in enumerate(self.angle_joint_bolt_position): + a = a.constrain( + f"handle?holeLPI{ih}", + f"ring2/side0?holeLPO{ih}", + "Plane", + ) + a = a.constrain( + f"handle?holeRPI{ih}", + f"ring2/side0?holeRPO{ih}", + "Plane", + ) + if has_part(parts, "section3"): + a = a.add( + self.assembly_section(length=self.side_length3, hasFrontHole=True, hasBackHole=True), + name="section3", + ) + if has_part(parts, "ring3"): + a = a.add( + self.assembly_ring(self.angle_joint_chamber_front()), + name="ring3", + ) + if has_part(parts, "chamber"): + a = a.add( + self.assembly_chamber(), + name="chamber", + ) + if has_part(parts, "ring4"): + a = a.add( + self.assembly_ring(self.angle_joint_chamber_back()), + name="ring4", + ) + if has_part(parts, "chamber_back"): + a = a.addS( + self.chamber_back(), + name="chamber_back", + material=self.material_side, + role=Role.STRUCTURE | Role.DECORATION, + ) + if has_part(parts, "motor"): + a = a.add(self.assembly_motor(), name="motor") + if has_part(parts, "machine"): + a = a.add(self.assembly_machine(), name="machine") + if has_part(parts, "turning_bar"): + a = a.add(self.turning_bar(), name="turning_bar1") + if has_part(parts, ["turning_bar", "ring3"]): a = a.constrain( - f"handle?holeLPI{ih}", - f"ring2/side0?holeLPO{ih}", + f"turning_bar1?holeBO2", + f"ring3/side0?holeStatorL", "Plane", ) a = a.constrain( - f"handle?holeRPI{ih}", - f"ring2/side0?holeRPO{ih}", + f"turning_bar1?holeBO1", + f"ring3/side1?holeStatorL", "Plane", ) + + # FIXME: Filter + if has_part(parts, ["motor", "ring2"]): + for i in range(self.n_side // 2): + j = self.n_side // 2 - 1 - i + a = a.constrain( + f"motor/seat{j}?holeBB1", + f"ring2/side{i*2}?holeStatorL", + "Plane", + ) + #a = a.constrain( + # f"motor/seat{j}?holeBB2", + # f"ring2/side{i*2+1}?holeStatorL", + # "Plane", + #) for i in range(self.n_side): j = (i + 1) % self.n_side ir = (self.n_side - i) % self.n_side @@ -1772,12 +2062,6 @@ class Onbashira(Model): f"machine/stator2?holeB{ir}", "Plane", ) - if i not in {1, 4}: - a = a.constrain( - f"motor/mount_plate?holeF{i}", - f"ring2/side{(ir+2)%self.n_side}?holeStatorR", - "Plane", - ) name_bolt =f"stator_outer_bolt{i}" a = a.addS( From b55dc8caa35699c320f728292a50eba432ee8c7b Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Fri, 30 May 2025 00:28:44 -0700 Subject: [PATCH 39/59] Calibrate measurements --- nhf/parts/electronics.py | 22 +++++++-- nhf/touhou/yasaka_kanako/onbashira.py | 70 +++++++++++++++++++-------- 2 files changed, 70 insertions(+), 22 deletions(-) diff --git a/nhf/parts/electronics.py b/nhf/parts/electronics.py index 0a145b3..8ac43bd 100644 --- a/nhf/parts/electronics.py +++ b/nhf/parts/electronics.py @@ -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) @@ -101,7 +106,7 @@ class BatteryBox18650(Item): 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 +122,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, @@ -133,3 +138,14 @@ class BatteryBox18650(Item): combine=True, ) ) + 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("holeT1", (0, self.hole_dy, self.thickness), direction="+Z") + result.tagAbsolute("holeT2", (0, -self.hole_dy, self.thickness), direction="+Z") + result.tagAbsolute("holeB1", (0, self.hole_dy, 0), direction="-Z") + result.tagAbsolute("holeB2", (0, -self.hole_dy, 0), direction="-Z") + return result diff --git a/nhf/touhou/yasaka_kanako/onbashira.py b/nhf/touhou/yasaka_kanako/onbashira.py index e177959..bd8cd0a 100644 --- a/nhf/touhou/yasaka_kanako/onbashira.py +++ b/nhf/touhou/yasaka_kanako/onbashira.py @@ -2,6 +2,7 @@ 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 @@ -21,7 +22,7 @@ def has_part(li: Optional[list[str]], name: Union[str, list[str]]) -> bool: return True NUT_COMMON = HexNut( - # FIXME: measure + # FIXME: weigh mass=0.0, diam_thread=6.0, pitch=1.0, @@ -29,14 +30,14 @@ NUT_COMMON = HexNut( width=9.89, ) WASHER_COMMON = Washer( - # FIXME: measure + # FIXME: weigh mass=0.0, diam_thread=6.0, diam_outer=11.68, thickness=1.5, ) BOLT_COMMON = FlatHeadBolt( - # FIXME: measure + # FIXME: weigh mass=0.0, diam_head=12.8, height_head=2.8, @@ -45,7 +46,7 @@ BOLT_COMMON = FlatHeadBolt( pitch=1.0, ) BOLT_LONG = FlatHeadBolt( - # FIXME: measure + # FIXME: weigh mass=0.0, diam_head=12.8, height_head=2.8, @@ -54,7 +55,7 @@ BOLT_LONG = FlatHeadBolt( pitch=1.0, ) BOLT_BEARING = FlatHeadBolt( - # FIXME: measure + # FIXME: weigh mass=0.0, diam_head=12.8, height_head=2.8, @@ -63,14 +64,21 @@ BOLT_BEARING = FlatHeadBolt( pitch=1.0, ) +@dataclass(frozen=True) +class Display(Model): + thickness: float = 2.5 + length: float = 38.0 + width: float = 12.0 + @dataclass(frozen=True) class FlangeCoupler(Model): diam_thread: float = 8.0 - diam_inner: float = 10.0 - diam_outer: float = 22.0 + diam_inner: float = 16.0 + diam_outer: float = 32.0 height: float = 12.0 height_flange: float = 2.0 + height_hole: float = 7.0 diam_thread_flange: float = 4.0 n_hole_flange: int = 4 @@ -107,7 +115,14 @@ class FlangeCoupler(Model): .finalize() .extrude(self.height_flange) ) + hole_subtractor = Cq.Solid.makeCylinder( + radius=self.diam_thread_flange/2, + height=self.diam_inner, + pnt=(-self.diam_inner/2, 0, self.height_hole), + dir=(1, 0, 0) + ) result -= holes + result -= hole_subtractor result.tagAbsolute("top", (0, 0, self.height), direction="+Z") result.tagAbsolute("bot", (0, 0, 9), direction="-Z") for i in range(self.n_hole_flange): @@ -133,10 +148,11 @@ class Motor(Model): height_body: float = 83.5 diam_ring: float = 25.93 height_ring: float = 6.55 - height_shaft: float = 38.1 - height_base_shaft: float = 20.0 # FIXME: Measure + height_hole: float = 10.0 + height_shaft: float = 13.0 + height_base_shaft: float = 24.77 # Distance between anchor and the body - dx_anchor: float = 20.2 # FIXME: Measure + dx_anchor: float = 20.0 height_anchor: float = 10.4 def __post_init__(self): @@ -146,6 +162,12 @@ class Motor(Model): pass @property + def dist_mount_hole(self): + """ + Distance between mount point and shaft + """ + return self.height_hole + self.height_ring + @property def dist_mount_rotor(self): """ Distance between mount point and shaft @@ -167,13 +189,19 @@ class Motor(Model): centered=(True, True, False) ) ) - base_shaft = Cq.Solid.makeBox( - length=self.diam_shaft, - width=self.diam_shaft, - height=self.height_base_shaft, - ).moved(-self.diam_shaft/2, -self.diam_shaft/2, self.height_body) - shaft = Cq.Solid.makeCylinder( + hole_subtractor = Cq.Solid.makeCylinder( + radius=self.diam_thread/2, + height=self.diam_shaft, + pnt=(-self.diam_shaft/2, 0, self.height_body + self.height_hole), + dir=(1, 0, 0) + ) + base_shaft = Cq.Solid.makeCylinder( radius=self.diam_shaft/2, + height=self.height_base_shaft, + pnt=(0, 0, self.height_body), + ) + shaft = Cq.Solid.makeCylinder( + radius=self.diam_shaft/2 * 0.9, height=self.height_shaft, pnt=(0, 0, self.height_body + self.height_base_shaft) ) @@ -183,7 +211,7 @@ class Motor(Model): height=self.height_anchor, pnt=(0, 0, z_anchor) ) - result = result + base_shaft + shaft + anchor.moved(self.dx_anchor, 0, 0) + anchor.moved(-self.dx_anchor, 0, 0) + result = result + base_shaft + shaft + anchor.moved(self.dx_anchor, 0, 0) + anchor.moved(-self.dx_anchor, 0, 0) - hole_subtractor result.tagAbsolute("anchor1", (self.dx_anchor, 0, z_anchor), direction="+Z") result.tagAbsolute("anchor2", (-self.dx_anchor, 0, z_anchor), direction="+Z") result.tagAbsolute("shaft", (0, 0, self.height_body + self.height_base_shaft), direction="+Z") @@ -282,13 +310,16 @@ class Onbashira(Model): flange_coupler: FlangeCoupler = FlangeCoupler() auxiliary_thickness: float = 25.4 / 8 + battery_box: BatteryBox18650 = BatteryBox18650() + controller: ArduinoUnoR3 = ArduinoUnoR3() + # Distance between bind point and motor's mount points motor_driver_radius: float = 110.0 motor_seat_depth: float = 95.0 motor_seat_radius: float = 45.0 motor_coupler_flange_thickness: float = 10.0 motor_coupler_flange_radius: float = 8.0 - motor_coupler_height: float = 100.0 + motor_coupler_height: float = 120.0 motor_coupler_conn_dx: float = 30.0 motor_coupler_wall_thickness: float = 5.0 motor_coupler_inner_gap: float = 1.0 @@ -584,9 +615,10 @@ class Onbashira(Model): ) ) for i in range(self.flange_coupler.n_hole_flange): + j = self.flange_coupler.n_hole_flange - i - 1 a = a.constrain( f"flange_coupler?holeB{i}", - f"driver_disk?holeB{i}", + f"driver_disk?holeB{j}", "Plane", ) From 7d3845f3c1f17e90bfd5d3c39f3a3492d009ff3c Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Fri, 30 May 2025 00:32:40 -0700 Subject: [PATCH 40/59] Align coupler holes --- nhf/touhou/yasaka_kanako/onbashira.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/nhf/touhou/yasaka_kanako/onbashira.py b/nhf/touhou/yasaka_kanako/onbashira.py index bd8cd0a..08d2177 100644 --- a/nhf/touhou/yasaka_kanako/onbashira.py +++ b/nhf/touhou/yasaka_kanako/onbashira.py @@ -129,6 +129,7 @@ class FlangeCoupler(Model): loc = Cq.Location.rot2d(i * 360 / self.n_hole_flange) * Cq.Location(self.r_hole_flange, 0) result.tagAbsolute(f"holeT{i}", loc * Cq.Location(0, 0, self.height_flange), direction="+Z") result.tagAbsolute(f"holeB{i}", loc, direction="-Z") + result.tagAbsolute("dir", (0, 0, self.height_hole), direction="+X") return result @@ -215,6 +216,7 @@ class Motor(Model): result.tagAbsolute("anchor1", (self.dx_anchor, 0, z_anchor), direction="+Z") result.tagAbsolute("anchor2", (-self.dx_anchor, 0, z_anchor), direction="+Z") result.tagAbsolute("shaft", (0, 0, self.height_body + self.height_base_shaft), direction="+Z") + result.tagAbsolute("dir", (0, 0, self.height_body + self.height_hole), direction="+X") return result @@ -611,7 +613,13 @@ class Onbashira(Model): .constrain( "flange_coupler?top", "motor?shaft", - "Plane" + "Axis" + ) + .constrain( + "flange_coupler?dir", + "motor?dir", + "Plane", + param=0, ) ) for i in range(self.flange_coupler.n_hole_flange): From 6ad74047bc2f22d04be41b2d1079121890e1395d Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Fri, 30 May 2025 01:22:48 -0700 Subject: [PATCH 41/59] Simplify seat geometry --- nhf/touhou/yasaka_kanako/onbashira.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/nhf/touhou/yasaka_kanako/onbashira.py b/nhf/touhou/yasaka_kanako/onbashira.py index 08d2177..9a31af1 100644 --- a/nhf/touhou/yasaka_kanako/onbashira.py +++ b/nhf/touhou/yasaka_kanako/onbashira.py @@ -318,7 +318,7 @@ class Onbashira(Model): # Distance between bind point and motor's mount points motor_driver_radius: float = 110.0 motor_seat_depth: float = 95.0 - motor_seat_radius: float = 45.0 + motor_seat_radius: float = 50.0 motor_coupler_flange_thickness: float = 10.0 motor_coupler_flange_radius: float = 8.0 motor_coupler_height: float = 120.0 @@ -1047,11 +1047,12 @@ class Onbashira(Model): ring for mounting lights """ bx, by = self.angle_joint_bind_pos.to2d_pos() - gap = 10 + gap = 7 t1 = 10 base_w = 17.0 theta = math.pi / self.n_side theta2 = theta * 0.5 + theta1 = theta * 1.3 track_width = 7.0 r0 = self.bulk_radius r1 = self.rotation_radius + gap @@ -1062,8 +1063,8 @@ class Onbashira(Model): .circle(r2, mode="s") .polygon([ (0, 0), - (r0 * math.cos(theta), r0 * math.sin(theta)), - (r0 * math.cos(theta), -r0 * math.sin(theta)), + (r0 * math.cos(theta1), r0 * math.sin(theta1)), + (r0 * math.cos(theta1), -r0 * math.sin(theta1)), ], mode="i") ) profile_base = ( @@ -1117,8 +1118,8 @@ class Onbashira(Model): .circle(r2_5, mode="s") .polygon([ (0, 0), - (r0 * math.cos(theta), r0 * math.sin(theta)), - (r0 * math.cos(theta), -r0 * math.sin(theta)), + (r0 * math.cos(theta1), r0 * math.sin(theta1)), + (r0 * math.cos(theta1), -r0 * math.sin(theta1)), ], mode="i") .push([ (mount_x, mount_y), @@ -1150,7 +1151,7 @@ class Onbashira(Model): .circle(self.rotation_radius+track_width/2, mode="s") .polygon([ (0, 0), - (r0 * math.cos(theta), r0 * math.sin(theta)), + (r0 * math.cos(theta1), r0 * math.sin(theta1)), (r0 * math.cos(theta2), r0 * math.sin(theta2)), ], mode="i") .wires() @@ -1163,7 +1164,7 @@ class Onbashira(Model): .circle(r2, mode="s") .polygon([ (0, 0), - (r0 * math.cos(theta), r0 * math.sin(theta)), + (r0 * math.cos(theta1), r0 * math.sin(theta1)), (r0 * math.cos(theta2), r0 * math.sin(theta2)), ], mode="i") .wires() @@ -1175,7 +1176,7 @@ class Onbashira(Model): .circle(r2, mode="s") .polygon([ (0, 0), - (r0 * math.cos(theta), r0 * math.sin(theta)), + (r0 * math.cos(theta1), r0 * math.sin(theta1)), (r0 * math.cos(theta2), r0 * math.sin(theta2)), ], mode="i") .wires() From ef0b0dad910f315a7881f792e611fd43fe17c20c Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Fri, 30 May 2025 01:33:14 -0700 Subject: [PATCH 42/59] Add reinforcements to motor seat --- nhf/touhou/yasaka_kanako/onbashira.py | 31 ++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/nhf/touhou/yasaka_kanako/onbashira.py b/nhf/touhou/yasaka_kanako/onbashira.py index 9a31af1..b696ca0 100644 --- a/nhf/touhou/yasaka_kanako/onbashira.py +++ b/nhf/touhou/yasaka_kanako/onbashira.py @@ -1053,6 +1053,7 @@ class Onbashira(Model): theta = math.pi / self.n_side theta2 = theta * 0.5 theta1 = theta * 1.3 + cover_thickness = 4.0 track_width = 7.0 r0 = self.bulk_radius r1 = self.rotation_radius + gap @@ -1113,8 +1114,6 @@ class Onbashira(Model): Cq.Workplane() .sketch() .circle(r1) - .circle(self.rotation_radius+track_width/2, mode="s") - .circle(self.rotation_radius-track_width/2) .circle(r2_5, mode="s") .polygon([ (0, 0), @@ -1130,6 +1129,29 @@ class Onbashira(Model): .finalize() .extrude(t1) ) + channel = ( + Cq.Workplane() + .sketch() + .circle(self.rotation_radius+track_width/2) + .circle(self.rotation_radius-track_width/2, mode="s") + .finalize() + .extrude(t1) + .translate((0, 0, self.motor_seat_depth - t1)) + ) + channel_cover = ( + Cq.Workplane() + .sketch() + .circle(self.rotation_radius+track_width/2) + .circle(self.rotation_radius-track_width/2, mode="s") + .polygon([ + (0, 0), + (r0 * math.cos(theta1), r0 * math.sin(theta1)), + (r0 * math.cos(theta2), r0 * math.sin(theta2)), + ], mode="i") + .finalize() + .extrude(cover_thickness) + .translate((0, 0, self.motor_seat_depth - cover_thickness)) + ) # Construct the connection between the front and back @@ -1148,7 +1170,7 @@ class Onbashira(Model): profile_bridge_outer_top = ( Cq.Sketch() .circle(r1) - .circle(self.rotation_radius+track_width/2, mode="s") + .circle(self.rotation_radius, mode="s") .polygon([ (0, 0), (r0 * math.cos(theta1), r0 * math.sin(theta1)), @@ -1198,6 +1220,9 @@ class Onbashira(Model): + bridge_inner + bridge_inner.mirror("XZ") - hole_subtractor + - channel + + channel_cover + + channel_cover.mirror("XZ") ) # Mark the mount points From 758b51c9db6ba2b62ac6ade16469c09c88fcdd65 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Fri, 30 May 2025 01:35:55 -0700 Subject: [PATCH 43/59] Remove redundant geometry from seat --- nhf/touhou/yasaka_kanako/onbashira.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nhf/touhou/yasaka_kanako/onbashira.py b/nhf/touhou/yasaka_kanako/onbashira.py index b696ca0..7936fb2 100644 --- a/nhf/touhou/yasaka_kanako/onbashira.py +++ b/nhf/touhou/yasaka_kanako/onbashira.py @@ -1051,7 +1051,7 @@ class Onbashira(Model): t1 = 10 base_w = 17.0 theta = math.pi / self.n_side - theta2 = theta * 0.5 + theta2 = theta * 0.7 theta1 = theta * 1.3 cover_thickness = 4.0 track_width = 7.0 @@ -1079,13 +1079,13 @@ class Onbashira(Model): ]) .reset() .polygon([ - (r1 * math.cos(theta), r1 * math.sin(theta)), + (r1 * math.cos(theta1), r1 * math.sin(theta1)), (r1 * math.cos(theta2), r1 * math.sin(theta2)), (r0 * math.cos(theta2), r0 * math.sin(theta2)), (r0 * math.cos(theta), r0 * math.sin(theta)), ]) .polygon([ - (r1 * math.cos(theta), -r1 * math.sin(theta)), + (r1 * math.cos(theta1), -r1 * math.sin(theta1)), (r1 * math.cos(theta2), -r1 * math.sin(theta2)), (r0 * math.cos(theta2), -r0 * math.sin(theta2)), (r0 * math.cos(theta), -r0 * math.sin(theta)), From 67770333839b50ed3d6ea6616712f74e9785d70e Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Fri, 30 May 2025 01:54:19 -0700 Subject: [PATCH 44/59] Remove conflict with base geometry --- nhf/touhou/yasaka_kanako/onbashira.py | 33 +++++++++++++++------------ 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/nhf/touhou/yasaka_kanako/onbashira.py b/nhf/touhou/yasaka_kanako/onbashira.py index 7936fb2..cc711ee 100644 --- a/nhf/touhou/yasaka_kanako/onbashira.py +++ b/nhf/touhou/yasaka_kanako/onbashira.py @@ -1071,24 +1071,17 @@ class Onbashira(Model): profile_base = ( profile_arc .reset() - .polygon([ - (bx - base_w/2, by), - (bx + base_w/2, by), - (bx + base_w/2, -by), - (bx - base_w/2, -by), - ]) - .reset() .polygon([ (r1 * math.cos(theta1), r1 * math.sin(theta1)), (r1 * math.cos(theta2), r1 * math.sin(theta2)), - (r0 * math.cos(theta2), r0 * math.sin(theta2)), - (r0 * math.cos(theta), r0 * math.sin(theta)), + (bx, by - base_w/2), + (bx, by + base_w/2), ]) .polygon([ (r1 * math.cos(theta1), -r1 * math.sin(theta1)), (r1 * math.cos(theta2), -r1 * math.sin(theta2)), - (r0 * math.cos(theta2), -r0 * math.sin(theta2)), - (r0 * math.cos(theta), -r0 * math.sin(theta)), + (bx, -by + base_w/2), + (bx, -by - base_w/2), ]) .reset() .push([ @@ -1155,13 +1148,23 @@ class Onbashira(Model): # Construct the connection between the front and back + x11 = r1 * math.cos(theta1) + y11 = r1 * math.sin(theta1) + x21 = r1 * math.cos(theta2) + y21 = r1 * math.sin(theta2) + x12 = bx + base_w/2 * math.sin(-math.pi * 0.3) + y12 = by + base_w/2 * math.cos(-math.pi * 0.3) + x22 = bx + y22 = by - base_w/2 + a1 = .8 + a2 = .95 profile_bridge_outer_base = ( Cq.Sketch() .polygon([ - (bx - base_w/2, by - base_w/2), - (bx + base_w/2, by - base_w/2), - (bx + base_w/2, by - base_w*1.5), - (bx - base_w/2, by - base_w*1.5), + ((1 - a1) * x11 + a1 * x12, (1 - a1) * y11 + a1 * y12), + ((1 - a1) * x21 + a1 * x22, (1 - a1) * y21 + a1 * y22), + ((1 - a2) * x21 + a2 * x22, (1 - a2) * y21 + a2 * y22), + ((1 - a2) * x11 + a2 * x12, (1 - a2) * y11 + a2 * y12), ]) .wires() .val() From 80730a9c5ac6e68410266d2214e208d84313aea6 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Fri, 30 May 2025 10:20:49 -0700 Subject: [PATCH 45/59] Fix stator coupler hole size --- nhf/touhou/yasaka_kanako/onbashira.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/nhf/touhou/yasaka_kanako/onbashira.py b/nhf/touhou/yasaka_kanako/onbashira.py index cc711ee..c527d5b 100644 --- a/nhf/touhou/yasaka_kanako/onbashira.py +++ b/nhf/touhou/yasaka_kanako/onbashira.py @@ -704,9 +704,13 @@ class Onbashira(Model): .circle(w/2, mode="a") .push([ (-l/2, 0), - (l/2, 0), ]) .circle(BOLT_COMMON.diam_thread/2, mode="s") + .reset() + .push([ + (l/2, 0), + ]) + .circle(BOLT_BEARING.diam_thread/2, mode="s") ) result = ( Cq.Workplane() From e4cfc71f1a4b93fed1986e297626d44eae2ef07c Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Fri, 30 May 2025 14:20:25 -0700 Subject: [PATCH 46/59] Electrnics assembly --- nhf/parts/electronics.py | 15 +- nhf/touhou/yasaka_kanako/onbashira.py | 512 +++++++++++++++++--------- 2 files changed, 341 insertions(+), 186 deletions(-) diff --git a/nhf/parts/electronics.py b/nhf/parts/electronics.py index 8ac43bd..e901276 100644 --- a/nhf/parts/electronics.py +++ b/nhf/parts/electronics.py @@ -100,6 +100,13 @@ 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 @@ -144,8 +151,8 @@ class BatteryBox18650(Item): ) result -= hole.moved(0, self.hole_dy) result -= hole.moved(0, -self.hole_dy) - result.tagAbsolute("holeT1", (0, self.hole_dy, self.thickness), direction="+Z") - result.tagAbsolute("holeT2", (0, -self.hole_dy, self.thickness), direction="+Z") - result.tagAbsolute("holeB1", (0, self.hole_dy, 0), direction="-Z") - result.tagAbsolute("holeB2", (0, -self.hole_dy, 0), direction="-Z") + 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 diff --git a/nhf/touhou/yasaka_kanako/onbashira.py b/nhf/touhou/yasaka_kanako/onbashira.py index c527d5b..d479479 100644 --- a/nhf/touhou/yasaka_kanako/onbashira.py +++ b/nhf/touhou/yasaka_kanako/onbashira.py @@ -284,7 +284,9 @@ class Onbashira(Model): # Gap between the inner and outer bearing disks bearing_gap: float = 10.0 bearing_disk_gap: float = 10.0 - bearing_spindle_max_diam: float = 13.0 + bearing_spindle_max_diam: float = 16.0 + bearing_spindle_ext: float = 5.0 + bearing_spindle_gap: float = 0.2 bearing_gasket_extend: float = 12.0 bearing_disk_thickness: float = 25.4 / 16 @@ -314,6 +316,13 @@ class Onbashira(Model): battery_box: BatteryBox18650 = BatteryBox18650() controller: ArduinoUnoR3 = ArduinoUnoR3() + controller_loc: Cq.Location = Cq.Location.from2d(-30, -35, 90) + battery_box_locs: list[Cq.Location] = field(default_factory=lambda: [ + Cq.Location.from2d(70, -35, 90), + Cq.Location.from2d(140, -35, 90), + Cq.Location.from2d(-70, -35, 90), + Cq.Location.from2d(-140, -35, 90), + ]) # Distance between bind point and motor's mount points motor_driver_radius: float = 110.0 @@ -547,6 +556,7 @@ class Onbashira(Model): return ( Cq.Sketch() .circle(r + gap) + .circle(self.motor.diam_ring/2, mode="s") .regularPolygon(r, self.n_side, mode="c", tag="corner") .vertices(tag="corner") .circle(BOLT_COMMON.diam_thread/2, mode="s") @@ -573,107 +583,6 @@ class Onbashira(Model): result.tagAbsolute(f"holeB{i}", (x, -y, 0), direction="-Z") return result - @assembly() - def assembly_motor(self) -> Cq.Assembly: - a = ( - Cq.Assembly() - .addS( - self.motor.generate(), - name="motor", - role=Role.MOTOR, - ) - .addS( - self.flange_coupler.generate(), - name="flange_coupler", - role=Role.CONNECTION | Role.STRUCTURE, - material=self.material_fastener, - ) - .addS( - self.motor_driver_disk(), - name="driver_disk", - role=Role.CONNECTION | Role.STRUCTURE, - material=self.material_auxiliary, - ) - .addS( - self.motor_mount_plate(), - name="mount_plate", - role=Role.CONNECTION | Role.STRUCTURE, - material=self.material_auxiliary, - ) - .constrain( - "mount_plate?anchor1", - "motor?anchor1", - "Plane", - ) - .constrain( - "mount_plate?anchor2", - "motor?anchor2", - "Plane", - ) - .constrain( - "flange_coupler?top", - "motor?shaft", - "Axis" - ) - .constrain( - "flange_coupler?dir", - "motor?dir", - "Plane", - param=0, - ) - ) - for i in range(self.flange_coupler.n_hole_flange): - j = self.flange_coupler.n_hole_flange - i - 1 - a = a.constrain( - f"flange_coupler?holeB{i}", - f"driver_disk?holeB{j}", - "Plane", - ) - - # Add the motor seats - assert self.n_side % 2 == 0 - for i in range(self.n_side // 2): - name_seat = f"seat{i}" - a = ( - a.addS( - self.motor_seat(), - name=name_seat, - role=Role.STRUCTURE, - material=self.material_brace - ) - .constrain( - f"{name_seat}?holeMF1", - f"mount_plate?holeB{i*2}", - "Plane" - ) - .constrain( - f"{name_seat}?holeMF2", - f"mount_plate?holeB{i*2+1}", - "Plane" - ) - ) - for i in range(self.n_side): - name_coupler = f"coupler{i}" - a = ( - a.addS( - self.motor_coupler(), - name=name_coupler, - role=Role.CONNECTION, - material=self.material_brace, - ) - .constrain( - f"{name_coupler}?holeB1", - f"driver_disk?holeCOF{i}", - "Plane", - ) - .constrain( - f"{name_coupler}?holeB2", - f"driver_disk?holeCIF{i}", - "Plane", - ) - ) - return a.solve() - @target(name="stator-coupler") def stator_coupler(self) -> Cq.Workplane: """ @@ -811,7 +720,7 @@ class Onbashira(Model): self.bearing_track_radius, self.n_bearing_balls, mode="c", tag="corners") .vertices(tag="corners") - .circle(BOLT_BEARING.diam_thread, mode="s") + .circle(BOLT_BEARING.diam_thread/2, mode="s") ) def bearing_gasket(self) -> Cq.Workplane: return ( @@ -821,13 +730,6 @@ class Onbashira(Model): ) - @target(name="pipe", kind=TargetKind.DXF) - def pipe(self) -> Cq.Sketch: - """ - The rotating pipes. Purely for decoration - """ - pass - @target(name="stator-spacer") def stator_spacer(self) -> Cq.Solid: outer = Cq.Solid.makeCylinder( @@ -854,28 +756,29 @@ class Onbashira(Model): @property def bearing_spindle_height(self) -> float: h = self.bearing_disk_gap + 2 * self.bearing_disk_thickness - return h * 2 + return h + self.bearing_spindle_ext * 2 @target(name="bearing-spindle") def bearing_spindle(self) -> Cq.Solid: r1 = self.bearing_gap / 2 - r2 = self.bearing_spindle_max_diam - h = self.bearing_disk_gap + 2 * self.bearing_disk_thickness - cone1 = Cq.Solid.makeCylinder( - radius=r1, + r2 = self.bearing_spindle_max_diam / 2 + h = self.bearing_disk_gap + cone = Cq.Solid.makeCone( + radius1=r2, + radius2=r1, height=h/2, ) - cone2 = Cq.Solid.makeCone( - radius1=r1, - radius2=r2, - height=h/2, + cyl = Cq.Solid.makeCylinder( + radius=r1, + height=self.bearing_spindle_ext + self.bearing_disk_thickness, + pnt=(0, 0, h/2) ) hole = Cq.Solid.makeCylinder( - radius=(BOLT_BEARING.diam_thread + 1)/2, + radius=(BOLT_BEARING.diam_thread + self.bearing_spindle_gap)/2, height=h*2 ).moved(0, 0, -h) - top = (cone1 + cone2.moved(0, 0, h/2)) - hole - return top + top.rotate((0,0,0),(1,0,0),180) + top = cone + cyl - hole + return top + top.mirror("XY") def barrel(self) -> Cq.Compound: """ @@ -990,59 +893,7 @@ class Onbashira(Model): ) return a - @target(name="turning-bar") - def turning_bar(self) -> Cq.Workplane: - """ - Converts the longitudinal/axial mount points on angle joints to - transverse mount points to make them more suitable for electronics. - """ - _, dx = self.angle_joint_bind_pos.to2d_pos() - t = 8 - w = self.turning_bar_width - result = ( - Cq.Workplane() - .box( - length=dx*2 + w, - width=w, - height=t, - centered=(True, True, False) - ) - ) - flange = Cq.Solid.makeBox( - length=w, - width=t, - height=w/2, - ).moved(-w/2, -t, -w/2) + Cq.Solid.makeCylinder( - radius=w/2, - height=t, - pnt=(0, -t, -w/2), - dir=(0, 1, 0), - ) - remover = Cq.Solid.makeCylinder( - radius=BOLT_COMMON.diam_thread/2, - height=w, - ) - removerf = Cq.Solid.makeCylinder( - radius=BOLT_COMMON.diam_thread/2, - height=w*2, - pnt=(0, -w, -w/2), - dir=(0, 1, 0), - ) - dxe = self.electronic_mount_dx - result = ( - result - + flange.moved(dx, w/2, 0) - + flange.moved(-dx, w/2, 0) - - remover.moved(dxe, 0, 0) - - remover.moved(-dxe, 0, 0) - - removerf.moved(dx, 0, 0) - - removerf.moved(-dx, 0, 0) - ) - result.tagAbsolute("holeBO1", (dx, w/2, -w/2), direction="+Y") - result.tagAbsolute("holeBO2", (-dx, w/2, -w/2), direction="+Y") - result.tagAbsolute("holeMO1", (dxe, 0, t)) - result.tagAbsolute("holeMO2", (-dxe, 0, t)) - return result + ### Motor ### @target(name="motor-seat") def motor_seat(self) -> Cq.Workplane: @@ -1240,6 +1091,303 @@ class Onbashira(Model): return result + @assembly() + def assembly_motor(self) -> Cq.Assembly: + a = ( + Cq.Assembly() + .addS( + self.motor.generate(), + name="motor", + role=Role.MOTOR, + ) + .addS( + self.flange_coupler.generate(), + name="flange_coupler", + role=Role.CONNECTION | Role.STRUCTURE, + material=self.material_fastener, + ) + .addS( + self.motor_driver_disk(), + name="driver_disk", + role=Role.CONNECTION | Role.STRUCTURE, + material=self.material_auxiliary, + ) + .addS( + self.motor_mount_plate(), + name="mount_plate", + role=Role.CONNECTION | Role.STRUCTURE, + material=self.material_auxiliary, + ) + .constrain( + "mount_plate?anchor1", + "motor?anchor1", + "Plane", + ) + .constrain( + "mount_plate?anchor2", + "motor?anchor2", + "Plane", + ) + .constrain( + "flange_coupler?top", + "motor?shaft", + "Axis" + ) + .constrain( + "flange_coupler?dir", + "motor?dir", + "Plane", + param=0, + ) + ) + for i in range(self.flange_coupler.n_hole_flange): + j = self.flange_coupler.n_hole_flange - i - 1 + a = a.constrain( + f"flange_coupler?holeB{i}", + f"driver_disk?holeB{j}", + "Plane", + ) + + # Add the motor seats + assert self.n_side % 2 == 0 + for i in range(self.n_side // 2): + name_seat = f"seat{i}" + a = ( + a.addS( + self.motor_seat(), + name=name_seat, + role=Role.STRUCTURE, + material=self.material_brace + ) + .constrain( + f"{name_seat}?holeMF1", + f"mount_plate?holeB{i*2}", + "Plane" + ) + .constrain( + f"{name_seat}?holeMF2", + f"mount_plate?holeB{i*2+1}", + "Plane" + ) + ) + for i in range(self.n_side): + name_coupler = f"coupler{i}" + a = ( + a.addS( + self.motor_coupler(), + name=name_coupler, + role=Role.CONNECTION, + material=self.material_brace, + ) + .constrain( + f"{name_coupler}?holeB1", + f"driver_disk?holeCOF{i}", + "Plane", + ) + .constrain( + f"{name_coupler}?holeB2", + f"driver_disk?holeCIF{i}", + "Plane", + ) + ) + return a.solve() + + ### Electronics ### + + @property + def turning_bar_hole_dy(self) -> float: + """ + Distance between centre of mounting holes in the turning bar and top of + the side panels. + """ + panel_to_mount = self.angle_joint_flange_thickness / 2 - self.angle_joint_gap / 2 + return panel_to_mount + self.turning_bar_width / 2 + + @target(name="turning-bar") + def turning_bar(self) -> Cq.Workplane: + """ + Converts the longitudinal/axial mount points on angle joints to + transverse mount points to make them more suitable for electronics. + """ + _, dx = self.angle_joint_bind_pos.to2d_pos() + t = 8 + w = self.turning_bar_width + result = ( + Cq.Workplane() + .box( + length=dx*2 + w, + width=w, + height=t, + centered=(True, True, False) + ) + ) + flange = Cq.Solid.makeBox( + length=w, + width=t, + height=w/2, + ).moved(-w/2, -t, -w/2) + Cq.Solid.makeCylinder( + radius=w/2, + height=t, + pnt=(0, -t, -w/2), + dir=(0, 1, 0), + ) + remover = Cq.Solid.makeCylinder( + radius=BOLT_COMMON.diam_thread/2, + height=w, + ) + removerf = Cq.Solid.makeCylinder( + radius=BOLT_COMMON.diam_thread/2, + height=w*2, + pnt=(0, -w, -w/2), + dir=(0, 1, 0), + ) + dxe = self.electronic_mount_dx + result = ( + result + + flange.moved(dx, w/2, 0) + + flange.moved(-dx, w/2, 0) + - remover.moved(dxe, 0, 0) + - remover.moved(-dxe, 0, 0) + - removerf.moved(dx, 0, 0) + - removerf.moved(-dx, 0, 0) + ) + result.tagAbsolute("holeBO1", (dx, w/2, -w/2), direction="+Y") + result.tagAbsolute("holeBO2", (-dx, w/2, -w/2), direction="+Y") + result.tagAbsolute("holeMO1", (dxe, 0, t)) + result.tagAbsolute("holeMO2", (-dxe, 0, t)) + return result + + @target(name="electronics-panel1", kind=TargetKind.DXF) + def profile_electronics_panel1(self) -> Cq.Sketch: + hole_dy = self.turning_bar_hole_dy + hole_dx = self.electronic_mount_dx + l = self.side_length3 - hole_dy * 2 + 12 + y = self.side_length3 - hole_dy * 2 + w = self.side_width + controller_holes = [ + self.controller_loc * Cq.Location.from2d(*h).flip_y() + for h in self.controller.holes + ] + battery_box_holes = [ + loc * h + for h in self.battery_box.holes + for loc in self.battery_box_locs + ] + profile = ( + Cq.Sketch() + .rect(l, w) + .rect(y, hole_dx * 2, mode="c", tag="corner") + .vertices(tag="corner") + .circle(BOLT_COMMON.diam_thread/2, mode="s") + .reset() + .push([ + h.to2d_pos() for h in controller_holes + ] + [ + h.to2d_pos() for h in battery_box_holes + ]) + .circle(self.controller.hole_diam/2, mode="s") + ) + return profile + + def electronics_panel1(self) -> Cq.Workplane: + hole_dy = self.turning_bar_hole_dy + hole_dx = self.electronic_mount_dx + l = self.side_length3 + t = self.side_thickness + result = ( + Cq.Workplane() + .placeSketch(self.profile_electronics_panel1()) + .extrude(t) + ) + x = l/2 - hole_dy + for side, z, d in [("T", t, "+Z"), ("B", 0, "-Z")]: + result.tagAbsolute(f"holeLP{side}", (-x, hole_dx, z), direction=d) + result.tagAbsolute(f"holeLS{side}", (-x, -hole_dx, z), direction=d) + result.tagAbsolute(f"holeRP{side}", (x, -hole_dx, z), direction=d) + result.tagAbsolute(f"holeRS{side}", (x, hole_dx, z), direction=d) + for (i, h) in enumerate(self.controller.holes): + loc = self.controller_loc * Cq.Location.from2d(*h).flip_y() + hx, hy = loc.to2d_pos() + result.tagAbsolute(f"holeController{i}", (hx, hy, t), direction="+Z") + for (j, loc) in enumerate(self.battery_box_locs): + for (i, h) in enumerate(self.battery_box.holes): + loch = loc * h + hx, hy = loch.to2d_pos() + result.tagAbsolute(f"holeBB{j}_{i}", (hx, hy, t), direction="+Z") + return result + + @assembly() + def assembly_electronics1(self) -> Cq.Assembly: + name_barL = "barL" + name_barR = "barR" + name_panel = "panel" + name_controller = "controller" + a = ( + Cq.Assembly() + .addS( + self.turning_bar(), + name=name_barL, + material=self.material_brace, + role=Role.STRUCTURE, + ) + .addS( + self.turning_bar(), + name=name_barR, + material=self.material_brace, + role=Role.STRUCTURE, + ) + .addS( + self.electronics_panel1(), + name=name_panel, + material=self.material_auxiliary, + role=Role.STRUCTURE, + ) + .add( + self.controller.assembly(), + name=name_controller, + ) + .constrain( + f"{name_panel}?holeLPB", + f"{name_barL}?holeMO1", + "Plane" + ) + .constrain( + f"{name_panel}?holeLSB", + f"{name_barL}?holeMO2", + "Plane" + ) + .constrain( + f"{name_panel}?holeRPB", + f"{name_barR}?holeMO1", + "Plane" + ) + .constrain( + f"{name_panel}?holeRSB", + f"{name_barR}?holeMO2", + "Plane" + ) + ) + for i in range(len(self.controller.holes)): + a = a.constrain( + f"{name_panel}?holeController{i}", + f"{name_controller}?conn{i}", + "Plane", + ) + for j in range(len(self.battery_box_locs)): + name_box = f"battery_box{j}" + a = a.add( + self.battery_box.assembly(), + name=name_box + ) + for i in range(len(self.battery_box.holes)): + a = a.constrain( + f"{name_panel}?holeBB{j}_{i}", + f"{name_box}?holeB{i}", + "Plane", + ) + return a.solve() + + ### Side Panels def profile_side_panel( self, @@ -2086,17 +2234,17 @@ class Onbashira(Model): a = a.add(self.assembly_motor(), name="motor") if has_part(parts, "machine"): a = a.add(self.assembly_machine(), name="machine") - if has_part(parts, "turning_bar"): - a = a.add(self.turning_bar(), name="turning_bar1") - if has_part(parts, ["turning_bar", "ring3"]): + if has_part(parts, "electronics1"): + a = a.add(self.assembly_electronics1(), name="electronics1") + if has_part(parts, ["electronics1", "ring3"]): a = a.constrain( - f"turning_bar1?holeBO2", - f"ring3/side0?holeStatorL", + f"electronics1/barL?holeBO2", + f"ring3/side1?holeStatorL", "Plane", ) a = a.constrain( - f"turning_bar1?holeBO1", - f"ring3/side1?holeStatorL", + f"electronics1/barL?holeBO1", + f"ring3/side2?holeStatorL", "Plane", ) From a0100f8fb7afe23c5802f02f5ede3fd6fd443086 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Fri, 30 May 2025 14:35:55 -0700 Subject: [PATCH 47/59] Front chamber separator --- nhf/parts/electronics.py | 1 + nhf/touhou/yasaka_kanako/onbashira.py | 74 ++++++++++++++++++++++++--- 2 files changed, 69 insertions(+), 6 deletions(-) diff --git a/nhf/parts/electronics.py b/nhf/parts/electronics.py index e901276..8bb03e8 100644 --- a/nhf/parts/electronics.py +++ b/nhf/parts/electronics.py @@ -144,6 +144,7 @@ class BatteryBox18650(Item): centered=(True, True, False), combine=True, ) + .copyWorkplane(Cq.Workplane('XY')) ) hole = Cq.Solid.makeCylinder( radius=self.diam_thread/2, diff --git a/nhf/touhou/yasaka_kanako/onbashira.py b/nhf/touhou/yasaka_kanako/onbashira.py index d479479..f423b9b 100644 --- a/nhf/touhou/yasaka_kanako/onbashira.py +++ b/nhf/touhou/yasaka_kanako/onbashira.py @@ -318,10 +318,10 @@ class Onbashira(Model): controller: ArduinoUnoR3 = ArduinoUnoR3() controller_loc: Cq.Location = Cq.Location.from2d(-30, -35, 90) battery_box_locs: list[Cq.Location] = field(default_factory=lambda: [ - Cq.Location.from2d(70, -35, 90), - Cq.Location.from2d(140, -35, 90), - Cq.Location.from2d(-70, -35, 90), - Cq.Location.from2d(-140, -35, 90), + Cq.Location.from2d(70, 0, 90), + Cq.Location.from2d(140, 0, 90), + Cq.Location.from2d(-70, 0, 90), + Cq.Location.from2d(-140, 0, 90), ]) # Distance between bind point and motor's mount points @@ -1612,12 +1612,17 @@ class Onbashira(Model): @target(name="chamber-back", kind=TargetKind.DXF) def profile_chamber_back(self) -> Cq.Sketch: + shift = 180 / self.n_side return ( Cq.Sketch() - .regularPolygon(self.side_width - self.side_thickness, self.n_side) + .regularPolygon( + self.side_width - self.side_thickness, + self.n_side, + angle=shift) .reset() .regularPolygon( self.angle_joint_bind_radius, self.n_side, + angle=shift, mode="c", tag="bolt") .vertices(tag="bolt") .circle(self.rotor_bind_bolt_diam/2, mode="s") @@ -1631,7 +1636,7 @@ class Onbashira(Model): ) # Mark all attachment points for i in range(self.n_side): - angle = (i+0.5) * math.radians(360 / self.n_side) + angle = i * math.radians(360 / self.n_side) x = self.angle_joint_bind_radius * math.cos(angle) y = self.angle_joint_bind_radius * math.sin(angle) result.tagAbsolute(f"holeF{i}", (x, y, self.side_thickness), direction="+Z") @@ -1652,6 +1657,38 @@ class Onbashira(Model): ) return a + @target(name="chamber-front", kind=TargetKind.DXF) + def profile_chamber_front(self) -> Cq.Sketch: + """ + Front chamber must allow access to the electronics section + """ + l = self.side_width + h = self.side_width + h2 = 15 + return ( + self.profile_chamber_back() + .reset() + .rect(l, h, mode="s") + .reset() + .push([(0, h/2 + h2)]) + .rect(l/2, h2, mode="s") + ) + def chamber_front(self) -> Cq.Sketch: + sketch = self.profile_chamber_front() + result = ( + Cq.Workplane() + .placeSketch(sketch) + .extrude(self.side_thickness) + ) + # Mark all attachment points + for i in range(self.n_side): + angle = i * math.radians(360 / self.n_side) + x = self.angle_joint_bind_radius * math.cos(angle) + y = self.angle_joint_bind_radius * math.sin(angle) + result.tagAbsolute(f"holeF{i}", (x, y, self.side_thickness), direction="+Z") + result.tagAbsolute(f"holeB{i}", (x, -y, 0), direction="-Z") + return result + def angle_joint_flange(self) -> Cq.Workplane: th = math.pi / self.n_side r = self.bulk_radius @@ -2230,6 +2267,13 @@ class Onbashira(Model): material=self.material_side, role=Role.STRUCTURE | Role.DECORATION, ) + if has_part(parts, "chamber_front"): + a = a.addS( + self.chamber_front(), + name="chamber_front", + material=self.material_side, + role=Role.STRUCTURE | Role.DECORATION, + ) if has_part(parts, "motor"): a = a.add(self.assembly_motor(), name="motor") if has_part(parts, "machine"): @@ -2309,6 +2353,19 @@ class Onbashira(Model): f"{name_bolt}?root", "Plane", ) + + name_bolt =f"chamber_front{i}boltFPI{i}" + a = a.addS( + BOLT_COMMON.generate(), + name=name_bolt, + material=self.material_fastener, + role=Role.CONNECTION, + ) + a = a.constrain( + f"chamber_front?holeF{i}", + f"{name_bolt}?root", + "Plane", + ) for ih in range(len(self.angle_joint_bolt_position)): a = a.constrain( f"chamber/side{i}?holeFPI{ih}", @@ -2325,6 +2382,11 @@ class Onbashira(Model): f"chamber_back?holeB{i}", "Plane", ) + a = a.constrain( + f"ring3/side{i}?holeStatorR", + f"chamber_front?holeB{i}", + "Plane", + ) #a = a.constrain( # f"barrel/stator2?holeB{i}", From 34ecf591246362ae9767bba16f7805bd3f45d3d0 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Fri, 30 May 2025 18:00:48 -0700 Subject: [PATCH 48/59] Add more mounting points on chamber front --- nhf/touhou/yasaka_kanako/onbashira.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/nhf/touhou/yasaka_kanako/onbashira.py b/nhf/touhou/yasaka_kanako/onbashira.py index f423b9b..bb0b797 100644 --- a/nhf/touhou/yasaka_kanako/onbashira.py +++ b/nhf/touhou/yasaka_kanako/onbashira.py @@ -265,6 +265,8 @@ class Onbashira(Model): chamber_side_length: float = 400.0 chamber_side_width_ex: float = 20.0 + # Circular hole to hold a switch + chamber_front_switch_diam: float = 20.0 # Dimensions of gun barrels barrel_diam: float = 25.4 * 1.5 @@ -1664,14 +1666,21 @@ class Onbashira(Model): """ l = self.side_width h = self.side_width - h2 = 15 + gap = 20 return ( self.profile_chamber_back() .reset() .rect(l, h, mode="s") + .push([ + (l/2 + gap + self.chamber_front_switch_diam/2, 0) + ]) + .circle(self.chamber_front_switch_diam/2, mode="s") .reset() - .push([(0, h/2 + h2)]) - .rect(l/2, h2, mode="s") + .push([ + (0, h/2 + gap), + (0, -h/2 - gap), + ]) + .rect(l/4, gap, mode="s") ) def chamber_front(self) -> Cq.Sketch: sketch = self.profile_chamber_front() From c8bbc0de91cee4c8304b873881892586cb467864 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Fri, 30 May 2025 22:31:08 -0700 Subject: [PATCH 49/59] Magnet holder --- nhf/touhou/yasaka_kanako/onbashira.py | 43 +++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/nhf/touhou/yasaka_kanako/onbashira.py b/nhf/touhou/yasaka_kanako/onbashira.py index bb0b797..2032b52 100644 --- a/nhf/touhou/yasaka_kanako/onbashira.py +++ b/nhf/touhou/yasaka_kanako/onbashira.py @@ -82,7 +82,7 @@ class FlangeCoupler(Model): diam_thread_flange: float = 4.0 n_hole_flange: int = 4 - r_hole_flange: float = 8.0 # FIXME: Measure! + r_hole_flange: float = 12.0 # FIXME: Measure! def generate(self) -> Cq.Workplane: result = ( @@ -237,6 +237,8 @@ class Onbashira(Model): section1_gohei_loc: float = 30.0 gohei_bolt_diam: float = 6.0 + magnet_size: float = 6.0 + # The angle joint bridges between two sets of side panels. # Extra thickness beyond the onbashira's body @@ -434,6 +436,41 @@ class Onbashira(Model): .extrude(self.side_width * 1.5) ) + @target(name="magnet-holder") + def magnet_holder(self) -> Cq.Workplane: + magnet_size = self.magnet_size + gap = 1.0 + length1 = 10.0 + width = magnet_size * 3 + height = gap + magnet_size + 1 + result = ( + Cq.Workplane() + .box( + length=length1 + magnet_size, + width=width, + height=height, + centered=(False, True, False) + ) + ) + corner_cut = Cq.Solid.makeBox( + length=height, + width=width, + height=height, + ).moved(0, -width/2, 0) - Cq.Solid.makeCylinder( + radius=height, + height=width, + pnt=(height, -width/2, 0), + dir=(0, 1, 0) + ) + box_cut = Cq.Solid.makeBox( + length=magnet_size, + width=magnet_size, + height=magnet_size + gap, + ).moved(length1, -magnet_size/2, 0) + return result - box_cut - corner_cut + + ### Motor ### + @target(name="motor-coupler") def motor_coupler(self) -> Cq.Workplane: """ @@ -777,8 +814,8 @@ class Onbashira(Model): ) hole = Cq.Solid.makeCylinder( radius=(BOLT_BEARING.diam_thread + self.bearing_spindle_gap)/2, - height=h*2 - ).moved(0, 0, -h) + height=h*4 + ).moved(0, 0, -h*2) top = cone + cyl - hole return top + top.mirror("XY") From d6ccc3496b2102e9eaa470a4251f81a917cd321d Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Fri, 30 May 2025 23:07:25 -0700 Subject: [PATCH 50/59] Use jute rope for handle --- nhf/touhou/yasaka_kanako/onbashira.py | 65 +++++++++++++++------------ 1 file changed, 37 insertions(+), 28 deletions(-) diff --git a/nhf/touhou/yasaka_kanako/onbashira.py b/nhf/touhou/yasaka_kanako/onbashira.py index 2032b52..b9d1ed9 100644 --- a/nhf/touhou/yasaka_kanako/onbashira.py +++ b/nhf/touhou/yasaka_kanako/onbashira.py @@ -310,8 +310,9 @@ class Onbashira(Model): rotor_spacer_outer_diam: float = 12.0 handle_base_height: float = 10.0 - handle_thickness: float = 17.0 - handle_length: float = 140.0 + handle_thickness_x: float = 20.0 + handle_thickness_y: float = 10.0 + handle_radius: float = 20.0 handle_height: float = 50.0 motor: Motor = Motor() @@ -2184,7 +2185,7 @@ class Onbashira(Model): @target(name="handle") def handle(self) -> Cq.Workplane: - w = self.side_width + self.angle_joint_extra_width + w = self.side_width * 0.7 base = ( Cq.Workplane( origin=(0, 0, -self.handle_base_height) @@ -2198,7 +2199,7 @@ class Onbashira(Model): .faces(">Z") .workplane() .pushPoints([ - (x * sx, y * sy) + (x * sx, (y + self.angle_joint_gap/2) * sy) for (x, y) in self.angle_joint_bolt_position for sx in (-1, 1) for sy in (-1, 1) @@ -2210,30 +2211,19 @@ class Onbashira(Model): depth=None, ) ) - dx = self.handle_length / 2 - self.handle_thickness / 2 - assert self.handle_length < w - z = self.handle_height - self.handle_thickness / 2 - handle = Cq.Solid.makeCylinder( - radius=self.handle_thickness/2, - height=dx * 2, - pnt=(-dx, 0, z), - dir=(1, 0, 0), + # Construct the handle + bar = ( + Cq.Workplane(origin=(0, self.handle_radius, 0)) + .rect(self.handle_thickness_x, self.handle_thickness_y) + .revolve(180, (0, -self.handle_radius, 0), (1, -self.handle_radius, 0)) ) - pillar = Cq.Solid.makeCylinder( - radius=self.handle_thickness/2, - height=z, - ) - joint = Cq.Solid.makeSphere(radius=self.handle_thickness/2) result = ( base + - handle + - pillar.moved(dx, 0, 0) + - pillar.moved(-dx, 0, 0) + - joint.moved(dx, 0, z) + - joint.moved(-dx, 0, z) + bar ) t = self.handle_base_height - for i, (x, y) in enumerate(self.angle_joint_bolt_position): + for i, (x, yi) in enumerate(self.angle_joint_bolt_position): + y = yi + self.angle_joint_gap/2 result.tagAbsolute(f"holeLPO{i}", (+x, y, 0), direction="+Z") result.tagAbsolute(f"holeLSO{i}", (-x, y, 0), direction="+Z") result.tagAbsolute(f"holeLPI{i}", (+x, y, -t), direction="-Z") @@ -2268,22 +2258,41 @@ class Onbashira(Model): self.assembly_ring(self.angle_joint()), name="ring2", ) + + name_handle1 = "handle2_1" a = a.addS( self.handle(), - name="handle", + name=name_handle1, + material=self.material_brace, + role=Role.HANDLE, + ) + name_handle2 = "handle2_2" + a = a.addS( + self.handle(), + name=name_handle2, material=self.material_brace, role=Role.HANDLE, ) # Handle constrain for ih, (x, y) in enumerate(self.angle_joint_bolt_position): a = a.constrain( - f"handle?holeLPI{ih}", - f"ring2/side0?holeLPO{ih}", + f"{name_handle1}?holeLPI{ih}", + f"ring2/side2?holeLPO{ih}", "Plane", ) a = a.constrain( - f"handle?holeRPI{ih}", - f"ring2/side0?holeRPO{ih}", + f"{name_handle1}?holeRPI{ih}", + f"ring2/side2?holeRPO{ih}", + "Plane", + ) + a = a.constrain( + f"{name_handle2}?holeLPI{ih}", + f"ring2/side4?holeLPO{ih}", + "Plane", + ) + a = a.constrain( + f"{name_handle2}?holeRPI{ih}", + f"ring2/side4?holeRPO{ih}", "Plane", ) if has_part(parts, "section3"): From 0a4ca64dadf6a90726c6730299e5edf7d1e2794f Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Sun, 1 Jun 2025 22:52:13 -0700 Subject: [PATCH 51/59] Add front stabilization bracket --- nhf/touhou/yasaka_kanako/onbashira.py | 140 +++++++++++++++++++++----- 1 file changed, 115 insertions(+), 25 deletions(-) diff --git a/nhf/touhou/yasaka_kanako/onbashira.py b/nhf/touhou/yasaka_kanako/onbashira.py index b9d1ed9..d2d17aa 100644 --- a/nhf/touhou/yasaka_kanako/onbashira.py +++ b/nhf/touhou/yasaka_kanako/onbashira.py @@ -236,6 +236,10 @@ class Onbashira(Model): section1_gohei_loc: float = 30.0 gohei_bolt_diam: float = 6.0 + # Extension from each gohei bolt's centre + front_bracket_ext: float = 6.0 + front_bracket_depth: float = 15.0 + front_bracket_thickness: float = 6.0 magnet_size: float = 6.0 @@ -275,7 +279,7 @@ class Onbashira(Model): barrel_wall_thickness: float = 25.4 / 8 barrel_length: float = 25.4 * 12 # Longitudinal shift - barrel_shift: float = -20.0 + barrel_shift: float = 30.0 # Gap between the stator edge and the inner face of the barrel stator_gap: float = 3.0 @@ -291,6 +295,8 @@ class Onbashira(Model): bearing_spindle_max_diam: float = 16.0 bearing_spindle_ext: float = 5.0 bearing_spindle_gap: float = 0.2 + bearing_spindle_tail: float = 4.0 + bearing_spindle_tail_diam: float = 6.0 bearing_gasket_extend: float = 12.0 bearing_disk_thickness: float = 25.4 / 16 @@ -373,12 +379,18 @@ class Onbashira(Model): def angle_side(self) -> float: return 360 / self.n_side @property - def delta_side_width(self) -> float: + def ratio_side_width(self) -> float: """ Difference between interior and exterior side width due to side thickness """ theta = math.pi / self.n_side - dt = self.side_thickness * math.tan(theta) + return math.tan(theta) + @property + def delta_side_width(self) -> float: + """ + Difference between interior and exterior side width due to side thickness + """ + dt = self.side_thickness * self.ratio_side_width return dt * 2 @property def side_width_inner(self) -> float: @@ -701,8 +713,8 @@ class Onbashira(Model): for i in range(self.n_side): angle = (i+0.5) * th1 result.faces(">Z").moveTo( - br * math.cos(angle), - br * math.sin(angle), + br * math.cos(-angle), + br * math.sin(-angle), ).tagPlane(f"holeF{i}") result.faces(" BOLT_BEARING.diam_thread hole = Cq.Solid.makeCylinder( radius=(BOLT_BEARING.diam_thread + self.bearing_spindle_gap)/2, - height=h*4 - ).moved(0, 0, -h*2) - top = cone + cyl - hole + height=self.bearing_spindle_height + ).moved(0, 0, -self.bearing_spindle_height/2) + top = cone + cyl + tail - hole return top + top.mirror("XY") def barrel(self) -> Cq.Compound: @@ -1429,6 +1450,51 @@ class Onbashira(Model): ### Side Panels + @target(name="front-bracket") + def front_bracket(self) -> Cq.Workplane: + assert self.front_bracket_ext > self.gohei_bolt_diam / 2 + assert self.front_bracket_depth > self.gohei_bolt_diam + x0 = self.bulk_radius + y0 = x0 * math.tan(2 * math.pi / self.n_side) + s1 = self.side_width_inner + s2 = s1 - self.front_bracket_thickness * self.ratio_side_width + result = ( + Cq.Workplane() + .sketch() + .regularPolygon(s1, self.n_side) + .regularPolygon(s2, self.n_side, mode="s") + .polygon([ + (0, 0), + (x0, 0), + (x0, y0), + ], mode="i") + .finalize() + .extrude(self.front_bracket_depth) + .translate((0, 0, -self.front_bracket_depth/2)) + ) + hole_subtractor = Cq.Solid.makeCylinder( + radius=BOLT_COMMON.diam_thread/2, + height=self.bulk_radius, + dir=(1, 0, 0) + ) + result -= hole_subtractor + angle = 360 / self.n_side + result -= hole_subtractor.rotate((0,0,0), (0, 0, 1), angle) + loc_rot = Cq.Location.rot2d(angle) + r1 = self.bulk_radius - self.side_thickness + result.tagAbsolute("holeT1", (r1, 0, 0), direction="+X") + loc_ht1 = (loc_rot * Cq.Location(r1, 0, 0)).toTuple()[0] + result.tagAbsolute("holeT2", loc_ht1, direction=loc_ht1) + return result + + @target(name="front-bracket-large") + def front_bracket_large(self) -> Cq.Workplane: + """ + Optional alternative that is a bit bigger + """ + result = self.front_bracket() + return result + result.mirror("XZ") + def profile_side_panel( self, length: float, @@ -1546,8 +1612,12 @@ class Onbashira(Model): result.tagAbsolute(f"holeBSI{i}", (-px, -py, t), direction="+Z") result.tagAbsolute(f"holeBPO{i}", (+px, -py, 0), direction="-Z") result.tagAbsolute(f"holeBSO{i}", (-px, -py, 0), direction="-Z") - + # Mark the gohei attachment points + y_gohei = self.side_length1/2 - self.section1_gohei_loc + result.tagAbsolute(f"holeGoheiB", (0, y_gohei, t), direction="+Z") + result.tagAbsolute(f"holeGoheiF", (0, y_gohei, 0), direction="-Z") return result + @target(name="side-panel2", kind=TargetKind.DXF) def profile_side_panel2(self) -> Cq.Sketch: return ( @@ -1579,7 +1649,26 @@ class Onbashira(Model): role=Role.STRUCTURE | Role.DECORATION, loc=Cq.Location.rot2d(i*360/self.n_side) * Cq.Location(-r,0,0,90,0,90), ) - return a + a = a.constrain(f"side{i}", "Fixed") + for i in range(self.n_side): + i1 = (i + 1) % self.n_side + name_bracket = f"front_bracket{i}" + a = a.addS( + self.front_bracket(), + name=name_bracket, + role=Role.STRUCTURE, + ) + a = a.constrain( + f"side{i1}?holeGoheiB", + f"{name_bracket}?holeT1", + "Plane", + ) + a = a.constrain( + f"side{i}?holeGoheiB", + f"{name_bracket}?holeT2", + "Plane", + ) + return a.solve() def assembly_section(self, **kwargs) -> Cq.Assembly: a = Cq.Assembly() side = self.side_panel(**kwargs) @@ -2335,6 +2424,7 @@ class Onbashira(Model): a = a.add(self.assembly_machine(), name="machine") if has_part(parts, "electronics1"): a = a.add(self.assembly_electronics1(), name="electronics1") + a = a.constrain("electronics1/controller", "Fixed") if has_part(parts, ["electronics1", "ring3"]): a = a.constrain( f"electronics1/barL?holeBO2", @@ -2374,27 +2464,27 @@ class Onbashira(Model): ) a = a.constrain( f"{coupler_name}?holeOB", - f"ring1/side{i}?holeStatorL", + f"ring1/side{i}?holeStatorR", "Plane", ) a = a.constrain( f"{coupler_name}?holeIF", - f"machine/stator2?holeB{ir}", + f"machine/stator1?holeF{ir}", "Plane", ) - name_bolt =f"stator_outer_bolt{i}" - a = a.addS( - BOLT_LONG.generate(), - name=name_bolt, - material=self.material_fastener, - role=Role.CONNECTION, - ) - a = a.constrain( - f"{coupler_name}?holeOF", - f"{name_bolt}?root", - "Plane", - ) + #name_bolt =f"stator_outer_bolt{i}" + #a = a.addS( + # BOLT_LONG.generate(), + # name=name_bolt, + # material=self.material_fastener, + # role=Role.CONNECTION, + #) + #a = a.constrain( + # f"{coupler_name}?holeOF", + # f"{name_bolt}?root", + # "Plane", + #) name_bolt =f"chamber_back{i}boltFPI{i}" a = a.addS( From 89c0d88de701bf48cd01d47a7ae4ddb54e19e9c3 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Mon, 2 Jun 2025 22:48:39 -0700 Subject: [PATCH 52/59] Use a much larger spindle gap --- nhf/touhou/yasaka_kanako/onbashira.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/nhf/touhou/yasaka_kanako/onbashira.py b/nhf/touhou/yasaka_kanako/onbashira.py index d2d17aa..fa32752 100644 --- a/nhf/touhou/yasaka_kanako/onbashira.py +++ b/nhf/touhou/yasaka_kanako/onbashira.py @@ -294,7 +294,8 @@ class Onbashira(Model): bearing_disk_gap: float = 10.0 bearing_spindle_max_diam: float = 16.0 bearing_spindle_ext: float = 5.0 - bearing_spindle_gap: float = 0.2 + # Gap on the interior and exterior + bearing_spindle_gap: float = 1.0 bearing_spindle_tail: float = 4.0 bearing_spindle_tail_diam: float = 6.0 bearing_gasket_extend: float = 12.0 @@ -812,7 +813,7 @@ class Onbashira(Model): @target(name="bearing-spindle") def bearing_spindle(self) -> Cq.Solid: - r1 = self.bearing_gap / 2 + r1 = (self.bearing_gap - self.bearing_spindle_gap) / 2 r2 = self.bearing_spindle_max_diam / 2 h = self.bearing_disk_gap h2 = self.bearing_spindle_ext + self.bearing_disk_thickness - self.bearing_spindle_tail From 49d3fa44bf4205ba8a082e82553298d663a3e4e9 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Tue, 3 Jun 2025 00:53:31 -0700 Subject: [PATCH 53/59] Separate interior and exterior gaps --- nhf/touhou/yasaka_kanako/onbashira.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nhf/touhou/yasaka_kanako/onbashira.py b/nhf/touhou/yasaka_kanako/onbashira.py index fa32752..baf7ba0 100644 --- a/nhf/touhou/yasaka_kanako/onbashira.py +++ b/nhf/touhou/yasaka_kanako/onbashira.py @@ -294,10 +294,10 @@ class Onbashira(Model): bearing_disk_gap: float = 10.0 bearing_spindle_max_diam: float = 16.0 bearing_spindle_ext: float = 5.0 - # Gap on the interior and exterior + bearing_spindle_exterior_gap: float = 0.2 bearing_spindle_gap: float = 1.0 bearing_spindle_tail: float = 4.0 - bearing_spindle_tail_diam: float = 6.0 + bearing_spindle_tail_diam: float = 7.0 bearing_gasket_extend: float = 12.0 bearing_disk_thickness: float = 25.4 / 16 @@ -813,7 +813,7 @@ class Onbashira(Model): @target(name="bearing-spindle") def bearing_spindle(self) -> Cq.Solid: - r1 = (self.bearing_gap - self.bearing_spindle_gap) / 2 + r1 = (self.bearing_gap - self.bearing_spindle_exterior_gap) / 2 r2 = self.bearing_spindle_max_diam / 2 h = self.bearing_disk_gap h2 = self.bearing_spindle_ext + self.bearing_disk_thickness - self.bearing_spindle_tail From 5d7137c0377e1fad557c552e13ddab4bf64f0b15 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Tue, 3 Jun 2025 09:11:06 -0700 Subject: [PATCH 54/59] Shimenawa geometry --- nhf/touhou/yasaka_kanako/__init__.py | 22 ++- nhf/touhou/yasaka_kanako/shimenawa.py | 236 ++++++++++++++++++++++++++ 2 files changed, 250 insertions(+), 8 deletions(-) create mode 100644 nhf/touhou/yasaka_kanako/shimenawa.py diff --git a/nhf/touhou/yasaka_kanako/__init__.py b/nhf/touhou/yasaka_kanako/__init__.py index ab34d4e..63364f7 100644 --- a/nhf/touhou/yasaka_kanako/__init__.py +++ b/nhf/touhou/yasaka_kanako/__init__.py @@ -1,25 +1,31 @@ +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 -from nhf.build import Model, TargetKind, target, assembly, submodel -import nhf.touhou.yasaka_kanako.onbashira as MO -import nhf.touhou.yasaka_kanako.mirror as MM -import nhf.utils @dataclass class Parameters(Model): - onbashira: MO.Onbashira = field(default_factory=lambda: MO.Onbashira()) 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="onbashira") - def submodel_onbashira(self) -> Model: - return self.onbashira @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__': diff --git a/nhf/touhou/yasaka_kanako/shimenawa.py b/nhf/touhou/yasaka_kanako/shimenawa.py new file mode 100644 index 0000000..813316e --- /dev/null +++ b/nhf/touhou/yasaka_kanako/shimenawa.py @@ -0,0 +1,236 @@ +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 + +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 = 8.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 = 12.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, + ) + ear_hole = Cq.Solid.makeCylinder( + radius=self.ear_hole_diam/2, + height=self.ear_thickness, + ) + ear = (ear_outer - ear_hole).moved(self.main_radius - r_minor - self.ear_dr, 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 + ) + return result + + @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 + 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) + ) + 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 From 7ec2728a6c810fd7215bfd3784c0570fa66b81eb Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Tue, 3 Jun 2025 18:50:24 -0700 Subject: [PATCH 55/59] Use 0.5 spindle gap for rotation resistance --- nhf/touhou/yasaka_kanako/onbashira.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nhf/touhou/yasaka_kanako/onbashira.py b/nhf/touhou/yasaka_kanako/onbashira.py index baf7ba0..7525663 100644 --- a/nhf/touhou/yasaka_kanako/onbashira.py +++ b/nhf/touhou/yasaka_kanako/onbashira.py @@ -295,7 +295,7 @@ class Onbashira(Model): bearing_spindle_max_diam: float = 16.0 bearing_spindle_ext: float = 5.0 bearing_spindle_exterior_gap: float = 0.2 - bearing_spindle_gap: float = 1.0 + bearing_spindle_gap: float = 0.5 bearing_spindle_tail: float = 4.0 bearing_spindle_tail_diam: float = 7.0 bearing_gasket_extend: float = 12.0 From 52b4e0b3294e276dc8800bcfb2548430c55f3f56 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Tue, 3 Jun 2025 18:50:39 -0700 Subject: [PATCH 56/59] Add ears to the pipe joint --- nhf/touhou/yasaka_kanako/shimenawa.py | 36 ++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/nhf/touhou/yasaka_kanako/shimenawa.py b/nhf/touhou/yasaka_kanako/shimenawa.py index 813316e..5b457c3 100644 --- a/nhf/touhou/yasaka_kanako/shimenawa.py +++ b/nhf/touhou/yasaka_kanako/shimenawa.py @@ -9,6 +9,14 @@ 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, @@ -35,7 +43,7 @@ class Shimenawa(Model): pipe_fitting_angle_span: float = 6.0 pipe_joint_length: float = 120.0 - pipe_joint_outer_thickness: float = 8.0 + pipe_joint_outer_thickness: float = 5.0 pipe_joint_inner_thickness: float = 4.0 pipe_joint_inner_angle_span: float = 120.0 @@ -44,7 +52,7 @@ class Shimenawa(Model): ear_dr: float = 6.0 ear_hole_diam: float = 10.0 - ear_radius: float = 12.0 + ear_radius: float = 15.0 ear_thickness: float = 10.0 main_circumference: float = 3600.0 @@ -81,12 +89,16 @@ class Shimenawa(Model): 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 - self.ear_dr, 0, 0) + ear = (ear_outer - ear_hole).moved(self.main_radius - r_minor, 0, 0) result += ear - inner return result @target(name="pipe-joint-outer") @@ -130,7 +142,18 @@ class Shimenawa(Model): - cut_hole.moved(0, 0, z) - cut_interior ) - return result + 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: @@ -184,12 +207,17 @@ class Shimenawa(Model): 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() From 675f4f995ba1234530e340bd791a0fd274975c46 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Tue, 10 Jun 2025 00:53:41 -0700 Subject: [PATCH 57/59] Add controller code for Kanako --- .../yasaka_kanako/controller/controller.ino | 199 ++++++++++++++++++ 1 file changed, 199 insertions(+) create mode 100644 nhf/touhou/yasaka_kanako/controller/controller.ino diff --git a/nhf/touhou/yasaka_kanako/controller/controller.ino b/nhf/touhou/yasaka_kanako/controller/controller.ino new file mode 100644 index 0000000..bf78e5c --- /dev/null +++ b/nhf/touhou/yasaka_kanako/controller/controller.ino @@ -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 +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 + +#include +#include + +#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(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 +} From db4232f94d179e4303cad6b67b86c156c71be2aa Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Wed, 11 Jun 2025 15:30:28 -0700 Subject: [PATCH 58/59] Add startup sequence --- nhf/touhou/yasaka_kanako/controller/controller.ino | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/nhf/touhou/yasaka_kanako/controller/controller.ino b/nhf/touhou/yasaka_kanako/controller/controller.ino index bf78e5c..bb521db 100644 --- a/nhf/touhou/yasaka_kanako/controller/controller.ino +++ b/nhf/touhou/yasaka_kanako/controller/controller.ino @@ -68,6 +68,9 @@ void setup() { #if USE_LED // Main LED strip FastLED.addLeds(leds, NUM_LEDS); + fill_solid(leds, NUM_LEDS, CRGB::White); + delay(500); + FastLED.show(); #endif #if USE_DISPLAY Serial.begin(9600); @@ -79,9 +82,11 @@ void setup() { 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; @@ -108,8 +113,10 @@ void loop() { if (programChanged) { update_screen(); - programChanged = false; + + digitalWrite(LED_BUILTIN, LOW); } + programChanged = false; } // Utility for updating LEDs @@ -169,6 +176,7 @@ void program_off() FastLED.show(); #endif } + delay(cycle_duration); } void program_still() { @@ -181,6 +189,7 @@ void program_still() FastLED.show(); #endif } + delay(cycle_duration); } void program_rotate() { From 4c0a54dc4ab2fe880ed274d552303b17fcc3283a Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Mon, 16 Jun 2025 10:16:32 -0700 Subject: [PATCH 59/59] Update controller to use internal pullup --- nhf/touhou/yasaka_kanako/README.md | 3 +++ nhf/touhou/yasaka_kanako/controller/controller.ino | 9 ++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 nhf/touhou/yasaka_kanako/README.md diff --git a/nhf/touhou/yasaka_kanako/README.md b/nhf/touhou/yasaka_kanako/README.md new file mode 100644 index 0000000..1e4ff0c --- /dev/null +++ b/nhf/touhou/yasaka_kanako/README.md @@ -0,0 +1,3 @@ +# Yasaka Kanako + +This cosplay won a Judge's favourite award at TouhouFest 2025. diff --git a/nhf/touhou/yasaka_kanako/controller/controller.ino b/nhf/touhou/yasaka_kanako/controller/controller.ino index bb521db..7618e29 100644 --- a/nhf/touhou/yasaka_kanako/controller/controller.ino +++ b/nhf/touhou/yasaka_kanako/controller/controller.ino @@ -1,7 +1,8 @@ #define USE_MOTOR 1 #define USE_LED 1 -#define USE_DISPLAY 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 @@ -52,7 +53,7 @@ bool flag_lighting = false; void setup() { pinMode(LED_BUILTIN, OUTPUT); - pinMode(pinButtonMode, INPUT); + pinMode(pinButtonMode, INPUT_PULLUP); #if USE_LED // Calculate colours hsv2rgb_spectrum(CHSV(4, 255, 100), color_red); @@ -71,6 +72,8 @@ void setup() { 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); @@ -87,7 +90,7 @@ void setup() { void loop() { // Detect a rising edge - int buttonState = digitalRead(pinButtonMode); + int buttonState = !digitalRead(pinButtonMode); if (buttonState && !stateButtonMode) { programId = (programId + 1) % MAX_PROGRAMS; programChanged = true;