# @author Arnaud Morin # SPDX-License-Identifier: Apache-2.0 # from math import cos, radians, sin import cadquery as cq from frame_params import (CF_DENSITY, FIT, TBONE_D, TENON_L, THICKNESS) def _custom_filets(wp, corners, tol=1e-4): """The edges that run through the flat pattern (parallel to Z) at the given (x, y) corners.""" picked = [] for e in wp.edges().vals(): vs = e.Vertices() if len(vs) != 2: continue a, b = vs[0].toTuple(), vs[1].toTuple() if abs(a[0] - b[0]) > 1e-6 or abs(a[1] - b[1]) > 1e-6: continue if any(abs(a[0] - cx) < tol and abs(a[1] - cy) < tol for cx, cy in corners): picked.append(e) return wp.newObject(picked) def _mortises(): """(x, y, angle) of the four arm root mortises, in the plate's own frame. angle is the mortise's long axis, which is radial.""" return [(25.3 * cos(radians(a)), 25.3 * sin(radians(a)), a) for a in (45.0, 135.0, 225.0, 315.0)] def plate(): # one quadrant, from the +X axis to the +Y axis quadrant = [ (21, 0.0), (15, 7.0), (28.5, 22.9), (13.9, 23.7), (7.5, 20.5), (0.0, 20.5), ] # mirrored across the Y axis (x -> -x) gives quadrant 2, joining quadrant # 1 into the top half -- both ends now sit on the X axis, so mirrorX() # below can close the loop by mirroring that half across it in turn top_half = quadrant + [(-x, y) for x, y in reversed(quadrant)][1:] p = ( cq.Workplane("XY") .polyline(top_half) .mirrorX() .extrude(THICKNESS) ) # Filets p = _custom_filets(p, [(21, 0), (-21, 0), (-21, -0), (21, -0)]).fillet(2) p = _custom_filets(p, [(15, 7), (-15, 7), (-15, -7), (15, -7)]).fillet(2) p = _custom_filets(p, [(28.5, 22.9), (-28.5, 22.9), (-28.5, -22.9), (28.5, -22.9)]).fillet(4.5) p = _custom_filets(p, [(13.9, 23.7), (-13.9, 23.7), (-13.9, -23.7), (13.9, -23.7)]).fillet(2) p = _custom_filets(p, [(7.5, 20.5), (-7.5, 20.5), (-7.5, -20.5), (7.5, -20.5)]).fillet(6.5) # Add holes for 20x20 FC p = ( p.faces(">Z") .workplane() .pushPoints([(10, 10), (-10, 10), (-10, -10), (10, -10)]) # Holes are 2.2 to fit 2.0 screws .hole(2.2) ) # Add holes for 25.5x25.5 FC p = ( p.faces(">Z") .workplane() .pushPoints([(18.03, 0), (-18.03, 0), (0, -18.03), (0, 18.03)]) # Holes are 2.2 to fit 2.0 screws .hole(2.2) ) # Add holes for standoff x, y = (14, 20) p = p.faces(">Z").workplane().pushPoints( [(sx * x, sy * y) for sx in (-1.0, 1.0) for sy in (-1.0, 1.0)] ).hole(2.2) # Arm mortises length, width = TENON_L + FIT, THICKNESS r = TBONE_D / 2.0 hl, hw = length / 2.0, width / 2.0 body = (cq.Workplane("XY") .polyline([(-hl, -hw), (hl, -hw), (hl, hw), (-hl, hw)]) .close().extrude(THICKNESS)) bones = cq.Workplane("XY") for u in (-(hl - r), hl - r): for v in (-hw, hw): bones = bones.moveTo(u, v).circle(r) bones = bones.extrude(THICKNESS) for x, y, a in _mortises(): # two separate cuts, not one combined cutter -- a compound of the # rectangle plus 4 tangent circles confuses the boolean into leaving # sliver solids behind p = p.cut(body.rotate((0, 0, 0), (0, 0, 1), a).translate((x, y, 0))) p = p.cut(bones.rotate((0, 0, 0), (0, 0, 1), a).translate((x, y, 0))) # Triangle cuts to reduce weight cut1 = ( cq.Workplane("XY") .polyline([ (6.5, -9), (6.5, 9), (1, 0), ]).close() .extrude(THICKNESS) .edges("|Z") .fillet(1.0) ) cut2 = ( cq.Workplane("XY") .polyline([ (9.5, -9), (9.5, 9), (15, 0), ]).close() .extrude(THICKNESS) .edges("|Z") .fillet(1.0) ) cut3 = ( cq.Workplane("XY") .polyline([ (11, 15), (-11, 15), (0, 4), ]).close() .extrude(THICKNESS) .edges("|Z") .fillet(1.0) ) p = ( p.cut(cut1) .cut(cut1.mirror("YZ")) ) p = ( p.cut(cut2) .cut(cut2.mirror("YZ")) ) p = ( p.cut(cut3) .cut(cut3.mirror("XZ")) ) return p result = plate() assert result.solids().size() == 1, "plate is not one solid" if "show_object" not in globals(): # running outside CQ-editor def show_object(*args, **kwargs): pass show_object(result) if __name__ == "__main__": v = result.val().Volume() print("plate mass %.2f g each, %.2f g for two" % (v * CF_DENSITY, 2 * v * CF_DENSITY))