#!/usr/bin/env python3 # type: ignore # ************************************************************************** # Copyright (C) 2025, Paul Lutus * # * # This program is free software; you can redistribute it and/or modify * # it under the terms of the GNU General Public License as published by * # the Free Software Foundation; either version 2 of the License, or * # (at your option) any later version. * # * # This program is distributed in the hope that it will be useful, * # but WITHOUT ANY WARRANTY; without even the implied warranty of * # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * # GNU General Public License for more details. * # * # You should have received a copy of the GNU General Public License * # along with this program; if not, write to the * # Free Software Foundation, Inc., * # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * # ************************************************************************** # this script displays results using # CQ-Editor if launched within it import cadquery as cq import copy import math, sys # a classic interpolation algorithm def ntrp(x, xa, xb, ya, yb): return (x - xa) * (yb - ya) / (xb - xa) + ya # create perforations like those in the classic # Roman dodecahedron, with decorative rings def make_perf(assembly, R, scale, radius, N): # generate variery of radii # circ_radius = ntrp(N, 0, 11, 15, 10) * scale assembly -= cq.Workplane("XY").circle(radius).extrude(16 * scale) rings = cq.Workplane("XZ") for r in (2, 4, 6): rings += ( cq.Workplane("XZ").moveTo(radius + r * scale).circle(0.5 * scale).revolve() ) return assembly.cut(rings) # rotate and duplicate a polygon section def polyside(section, sides, N): # workplane for results polygon = cq.Workplane("XY") # generate N polygons # sides = N angle = 0 step = 360 / sides for i in range(N): # print(f"sweep angle : {angle}") polygon.add(section.rotate((0, 0, 0), (0, 0, 1), angle)) angle += step # if show: # show_results(polygon) return polygon # build_poly: # show, used in CQ-editor # single = one edge, otherwise full figure # R = radius x_offset to vertex, # scale: sets all polygon proportions # sides = sides of a polygon # N = generate N segments def build_poly(show, R, wallv, scale, sides, N): radians = math.pi / 180.0 degrees = 1 / radians # new design 2025.07.22: build the base and connectors # for the entire polygon separately, then join wall = scale * wallv # wall thickness mm # X/Y values define the pie slice xv = R * math.sin(math.pi / sides) # diagonal length, X coordinate yv = R * math.cos(math.pi / sides) # diagonal height, Y coordinate # construct primary slice polygon_points = ( (0, 0), (xv, yv), (-xv, yv), (0, 0), ) polygon = cq.Workplane("XY").polyline(polygon_points).close().extrude(wall) # perforate polygon canter, add decorative rings polygon = make_perf(polygon, R, scale * sides * 0.2, scale * 10, N) # create mating-surface angled edge profile adj = wall * 5 # just to remain consistent with other polygons dihedral = math.acos(-math.sqrt(5) / 3) face_angle = math.pi - dihedral rs = math.tan(face_angle / 2) print( f"dihedral: {dihedral * degrees:.2f} , face angle: {face_angle * degrees:.2f}, rs: {rs:.4f}" ) adj = wall * 5 edge_points = ( (yv, 0), # initial +Y (yv, adj), # +Y +Z (yv - adj * rs, adj), # -Y +Z (yv, 0), # initial +Y ) edge = ( cq.Workplane("YZ") .polyline(edge_points) .close() .extrude(xv * 2) .translate((-xv, 0, 0)) ) polygon = polygon.cut(edge) polygon = polyside(polygon, sides, sides) section = cq.Workplane("XY") # show_results(section) # return section x_offset = 0.35 box_width = xv * 0.3 # create core of "male" connection side junct_height = wall + scale * 7 iw = yv - junct_height * rs # socket Z arg ib = yv - wall * rs # surface height # create core of "male" connection side plug_box = ( cq.Workplane("XY") .box(box_width, scale * 6, scale * 12) # reverse the sign of xv to test alignment .translate((-xv * x_offset, ib - scale * 3, junct_height * 0.7)) .edges() .fillet(0.8 * scale) ) section.add(plug_box) sphere_size = scale * 2 # important for tight fit: this tunes # the collision angle between tang and sphere sphere_x_offset = box_width * .42 # default: 0.37 # create two "male" connection pivots pivot_sphere = cq.Workplane("XY").sphere(sphere_size) # this rotate deals with a cadquery bug pivot_sphere_a = pivot_sphere.rotate((0, 0, 0), (0, 0, 1), 180).translate( (-(xv * x_offset) + sphere_x_offset, iw, junct_height) ) # duplicate existing part pivot_sphere_b = pivot_sphere.translate( (-(xv * x_offset) - sphere_x_offset, iw, junct_height) ) section.add(pivot_sphere_a + pivot_sphere_b) # NOTE: fitness control variables: tang_thickness = xv * 0.04 # tangs should meet box # sides with no gap tang_x_offset = (box_width + tang_thickness * 2) * 0.45 tang_y_size = scale * 13 # at wall height tang_y_delta = scale * 2 # extension above tang_z_size = scale * 11.5 tang_z_offset = scale * 0.3 tang_profile = ( (0,0), (0,tang_z_size), (tang_y_size,tang_z_size), (tang_y_size + tang_y_delta,tang_z_size * 3/4), (tang_y_size + tang_y_delta,tang_z_size/3), (tang_y_size,0), (0,0), ) # create two "female" support tangs left_tang = ( cq.Workplane("YZ") .polyline(tang_profile) .close() #.box(tang_thickness, tang_y_size, tang_z_size) .extrude(tang_thickness) .translate( ( -xv * -x_offset - tang_x_offset - tang_thickness/2, ib - tang_y_size, wall + tang_z_offset, ) ) ) # duplicate existing part right_tang = left_tang.translate((tang_x_offset * 2, 0, 0)) # create a cylindrical opening between the tangs cylinder = ( cq.Workplane("YZ") .circle(sphere_size * 0.66) .extrude(-xv) .translate((xv, iw, junct_height)) ) left_tang = left_tang.cut(cylinder).edges().fillet(tang_thickness * 0.49) right_tang = right_tang.cut(cylinder).edges().fillet(tang_thickness * 0.49) section.add(left_tang) section.add(right_tang) rear_support = ( cq.Workplane("XY") .box( #box_width + tang_thickness * 2, tang_x_offset * 2 + tang_thickness, yv * 0.15, tang_z_size * 1.15, ) .translate((xv * x_offset, ib + 1 - tang_y_size, wall + scale * 5.1)) ) section.add(rear_support) #section = section.cut(cylinder) #section = section.edges("Z").fillet(scale * 0.5) polygon += polyside(section, sides, sides) # polygon = polyside(section,sides,sides) if show: show_results(polygon) return polygon def show_results(item): try: show_object(item) except: print(" The show_object() function only works in CQ-Editor.") def main(mode): show = True # this is for CQ-editor # radius, distance between x_offset and vertex, mm R = 45 # default value # maintain overall scale proportional to R, # the x_offset to vertex distance scale = R / 45 # polygon sides sides = 3 wall = 2 # mm match mode: case 0: # create working models print("Creating prototypes:") polygon = build_poly(show, R, wall, scale, sides, 1) polygon.export(f"icosahedron_element_{sides}_sides.stl") case 1: print("Mode 1 not used.") case 2: print("Mode 2 not used.") print("Done.") # if __name__ == "__main__": main(0) # create test items # main(1) # create a full set of 12 polygons # main(2) # create prototype peg and array