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(