/* sb-profiles.scad — Strap-Beam shared profile constructions ========================================================== Part of the Strap-Beam library. Three complete arrangements, each written for N members and each returning a finished PROFILE record. sb_ring_polygon_profile members on the edges of a closed polygon, wrapped in one envelope with solid rounded corners and an enclosed bore sb_spoke_profile N members radiating from a plugged centre sb_fin_profile N members lying tangentially on the sides of a regular core polygon, slid cyclically so each overhangs one corner A family generator supplies N and the parameters; nothing below changes between the 3x and 4x files. strap-beam-3x.scad calls all three (Triangles, Y, Three-Fin) and strap-beam-4x.scad calls the same three (Quadrilaterals, Cross, Four-Fin) with N = 4. Requires sb-geom.scad, sb-join.scad and sb-report.scad. */ // --------------------------------------------------------------------------- // Ring: members on the edges of a closed polygon // --------------------------------------------------------------------------- /* Straps have a fixed width, so on a polygon of a given size the corner webs are whatever they are - sliding members along their edges cannot raise all N at once, because every edge shares its budget with two corners. The one free variable that lifts them together is the polygon's size, so the caller's outline is treated as a SHAPE and grown about its centroid until the tightest corner reaches `web`. Members are centred on their edges, which keeps the result mirror- symmetric whenever the outline is. */ function sb_ring_polygon_profile(seed_path, g, web, corner_r, label) = abs(sb_signed_area(seed_path)) < 1e-6 ? sb_profile_failed(str(label, ": the supplied outline is degenerate.")) : let( k = sb_ring_fit_scale(seed_path, g, web), path = is_undef(k) ? undef : sb_scale_about_centroid(seed_path, k) ) is_undef(path) ? sb_profile_failed(str(label, ": no polygon size gives a ", web, " mm corner web. Reduce the web, the wall thicknesses, or the strap width.")) : let( ms = sb_ring_members(path, g), max_r = sb_ring_max_corner_r(path, g), // Build with a radius the envelope can actually accept; if the // caller asked for more, the check below reports it rather than // letting the rounding routine fail with a library error. shell = [sb_ring_shell(path, g, min(corner_r, max_r))], bore = sb_bore_from_members(ms, g), edges = [for (i = [0 : len(path) - 1]) sb_dist(path[i], path[(i + 1) % len(path)])] ) sb_profile(ms, shell, bore, [ sb_check(len(shell[0]) >= 3, str(label, ": the outer envelope collapsed.")), sb_check(sb_bore_valid(bore, ms, g), str(label, ": the central bore has collapsed. Reduce inside_wall_thickness_mm or the bundle thickness.")), sb_check(corner_r <= max_r + 1e-6, str(label, ": corner radius of ", corner_r, " mm is not usable here - it would cut the outer wall below ", sb_min_wall(g), " mm at the corners, or exceed what the envelope can accept. Maximum is ", max_r, " mm.")) ], [ sb_kv("RING_SCALE", k), sb_kv("RING_EDGES_MM", edges), sb_kv("RING_CORNER_WEB_MM", sb_ring_web(path, g)), sb_kv("RING_CORNER_R_MAX_MM", max_r) ]); // --------------------------------------------------------------------------- // Spokes: N members radiating from a common centre // --------------------------------------------------------------------------- /* A spoke's two broad faces both look at open air, so both take the outside wall. There is no interior to face and therefore no bore; forcing an interior direction on these members is what made earlier revisions chiral under asymmetric wall settings. The centre is plugged with the convex hull of the N inner end faces rather than left as whatever the crossing rectangles happened to produce. The spoke radius is solved against the measured web between neighbours. */ function sb_spoke_members(n, radius, rotation) = [ for (i = [0 : n - 1]) sb_member_radial(radius, rotation + 360 * i / n, SB_FACE_BOTH_OUT) ]; function sb_spoke_web(n, radius, rotation, g) = let(cv = [for (m = sb_spoke_members(n, radius, rotation)) sb_cavity_path(m, g)]) min([for (i = [0 : n - 1]) sb_path_gap(cv[i], cv[(i + 1) % n])]); function sb_spoke_profile(n, rotation, web, fillet_r, g, label) = let( W = sb_width(g), f = function(r) sb_spoke_web(n, r, rotation, g), hi = 6 * W ) !sb_solvable(f, hi, web) ? sb_profile_failed(str(label, ": cannot open a ", web, " mm web between neighbouring spokes. Reduce the web or the wall thicknesses.")) : let( radius = sb_solve(f, W / 2, hi, web), ms = sb_spoke_members(n, radius, rotation), paths = [for (m = ms) sb_sleeve_path(m, g)], cap = sb_hull_cap([for (m = ms) sb_end_face(m, g, -1)]), raw = union(concat([for (p = paths) [p]], len(cap) > 0 ? [cap] : [])), pairs = [for (i = [0 : n - 1]) [i, (i + 1) % n]], shell = sb_fillet_junctions(raw, pairs, paths, fillet_r) ) sb_profile(ms, shell, [], [ sb_check(fillet_r > 0, str(label, ": the junction fillet radius must be greater than zero. A hull-plugged centre with no fillet meets the spokes along an exactly tangent boundary, which is a valid outline but cannot be tessellated.")) ], [ sb_kv("SPOKE_RADIUS_MM", radius), sb_kv("SPOKE_WEB_MM", sb_spoke_web(n, radius, rotation, g)), sb_kv("NOTE", "inside wall unused: no enclosed bore") ]); // --------------------------------------------------------------------------- // Fins: N members lying tangentially on a regular core polygon // --------------------------------------------------------------------------- /* Tangential, not radial. Each member lies along one side of a regular core polygon and is slid cyclically along that side, so it stops short of the corner behind it and overhangs the corner ahead of it. Those N overhangs are the fins. spokes N members leaving a common centre, ends pointing outward fins N members wrapping a core, each with one cyclic overhang The core size is not a free parameter. It is solved so the cyclic junctions carry the declared web AND the bore reaches its declared minimum, whichever demands more; the fin projection is then exact because it is measured against that same solved polygon. Each member's trailing end is run through the member behind it and cut off flush on its far face, so every junction has a full-width overlap and the fillet that follows is cosmetic. Junctions that merely touch at a corner and rely on a fillet to bridge them are not load paths. */ function sb_regular_polygon(n, side, rotation = 0) = let(R = side / (2 * sin(180 / n))) [for (k = [0 : n - 1]) let(a = -90 + 180 / n + 360 * k / n + rotation) [R * cos(a), R * sin(a)]]; function sb_fin_members(n, side, fin, rotation, g) = let( path = sb_regular_polygon(n, side, rotation), c = sb_centroid(path), shift = fin + (side - sb_width(g)) / 2 ) [for (i = [0 : n - 1]) sb_member_on_edge(path[i], path[(i + 1) % n], c, shift)]; function sb_fin_web(n, side, fin, rotation, g) = let(cv = [for (m = sb_fin_members(n, side, fin, rotation, g)) sb_cavity_path(m, g)]) min([for (i = [0 : n - 1]) sb_path_gap(cv[i], cv[(i + n - 1) % n])]); // Side length of the bore left by N inside walls around a regular core. function sb_fin_bore_side(n, side, g) = let(t = tan(180 / n)) 2 * t * (side / (2 * t) - sb_reach_inside(g)); function sb_fin_profile(n, fin, web, bore_side, rotation, fillet_r, g, label) = let( W = sb_width(g), fweb = function(s) sb_fin_web(n, s, fin, rotation, g), fbor = function(s) sb_fin_bore_side(n, s, g), hi = 10 * W ) !sb_solvable(fweb, hi, web) ? sb_profile_failed(str(label, ": cannot open a ", web, " mm junction web. Reduce the web or the wall thicknesses.")) : !sb_solvable(fbor, hi, bore_side) ? sb_profile_failed(str(label, ": cannot reach a ", bore_side, " mm bore. Reduce the requested bore.")) : let( side = max(sb_solve(fweb, 0.1, hi, web), sb_solve(fbor, 0.1, hi, bore_side)), ms = sb_fin_members(n, side, fin, rotation, g), // Member i butts through member i-1, the one whose fin crosses the // corner that member i stops short of. paths = [for (i = [0 : n - 1]) sb_sleeve_butt(ms[i], g, ms[(i + n - 1) % n])], pairs = [for (i = [0 : n - 1]) [(i + n - 1) % n, i]], shell = sb_fillet_junctions(sb_sleeve_shell(paths), pairs, paths, fillet_r), bore = sb_bore_from_members(ms, g), setback = fin + side - W ) sb_profile(ms, shell, bore, [ sb_check(sb_bore_valid(bore, ms, g), str(label, ": the central bore has collapsed. Raise the requested bore or reduce inside_wall_thickness_mm.")), sb_check(setback > 0, str(label, ": the solved trailing setback is ", setback, " mm, so the members overlap instead of stepping cyclically. Increase the fin projection.")), sb_check(fillet_r > 0, str(label, ": the junction fillet radius must be greater than zero. A butt joint with no fillet meets its neighbour along an exactly tangent boundary, which is a valid outline but cannot be tessellated.")) ], [ sb_kv("FIN_CORE_SIDE_MM", side), sb_kv("FIN_PROJECTION_MM", fin), sb_kv("FIN_SETBACK_MM", setback), sb_kv("FIN_BORE_SIDE_MM", sb_fin_bore_side(n, side, g)), sb_kv("FIN_JUNCTION_WEB_MM", sb_fin_web(n, side, fin, rotation, g)) ]);