From 0f151bd279822712d4a01c7682d3dea46efbb364 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Thu, 29 May 2025 16:21:01 -0700 Subject: [PATCH] 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"),