/* strap-beam-3x.scad — Three-strap longitudinal beam enclosures ============================================================= Revision 8. Replaces triangle-strap-enclosure_v7.scad. Three pallet-strap bundles run parallel to one longitudinal axis (Z). A profile decides only how the three cross-sections are arranged in XY; that arrangement is swept along Z. A "member" here is the end view of one 10-100 ft strap bundle, so its length in the cross-section is the strap's WIDTH. Profiles 1. Equilateral Triangle ring, three equal sides 2. General Triangle ring, caller-shaped triangle 3. A Frame two legs meeting at a gable apex, plus a crossbar butted between them 4. Y three radial spokes about a plugged centre 5. T two-strap flange with a perpendicular stem 6. Three-Fin tangential triangle whose members are slid cyclically so each projects one fin Everything reusable lives in lib/sb-core.scad and is shared unchanged with strap-beam-4x.scad. This file contains only the six arrangements and their parameters. --------------------------------------------------------------------- Changes from v7 that alter dimensions --------------------------------------------------------------------- * Every gap parameter is now a WEB: the PLA+ that survives between two strap cavities. v7 stated gaps strap-to-strap and then inflated the cavities by the fit clearance afterwards, which quietly removed 2 x clearance from every junction. A declared 1.2 mm web is now 1.2 mm of plastic. * Ring profiles size themselves. 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 three at once because each edge shares its budget with two corners. The polygon is therefore grown about its centroid until the tightest corner reaches the declared web. Caller-supplied triangle dimensions set the SHAPE; the size follows from the strap width. * Ring members are centred on their edges rather than shifted cyclically, so the triangle profiles are now mirror-symmetric. * Junctions are structural. Members butt through their neighbours and are cut flush on the far face; fillets are applied on top of that overlap rather than in place of it. In v7 the Three-Fin was held together by a 0.4 mm corner touch that the fillet happened to bridge. * Members with no enclosed side (Y spokes, T stem and flange) take the outside wall on both faces. v7 hard-coded an interior direction for these, which made the Y a pinwheel whenever the two wall thicknesses differed. * Validation measures the finished section. Connectivity is necessary but not sufficient - v7 passed cross-sections joined by 0.13 mm. * Checks are scoped to the selected profile. In v7 every assert was unconditional, so an A Frame slider could kill the Y profile. */ include // --------------------------------------------------------------------------- // Parameters // --------------------------------------------------------------------------- /* [Profile] */ profile_type = "Equilateral Triangle"; // ["Equilateral Triangle","General Triangle","A Frame","Y","T","Three-Fin"] /* [Pallet strap] */ strap_width_mm = 15.875; // ULINE 5/8 in nominal width strap_thickness_mm = 0.508; // ULINE .020 in nominal thickness bundle_count = 1; // [1:1:3] /* [Longitudinal axis] */ member_length_ft = 10; // [10:1:100] length_view = "Preview"; // ["Preview","Full Length"] preview_length_mm = 100; /* [PLA+ enclosure] */ // Clearance is added to every cavity face. Declared webs are unaffected by // it; the library compensates internally. fit_clearance_mm = 0.25; inside_wall_thickness_mm = 1.20; // faces that look into an enclosed bore outside_wall_thickness_mm = 1.20; // faces exposed to the outside world edge_wall_thickness_mm = 1.20; // caps over a strap's narrow edges // Nothing anywhere in the section is allowed to be thinner than this. min_wall_mm = 1.20; /* [Ring profiles] */ // Applies to both triangle profiles. The polygon grows until its tightest // corner reaches this web. ring_corner_web_mm = 1.20; // Rounding of the complete outer envelope, never of individual sleeves. ring_corner_radius_mm = 2.00; /* [General Triangle shape] */ // These set proportions only; absolute size is solved from the strap width. general_triangle_base = 17.50; general_triangle_height = 15.50; general_triangle_apex_offset = 1.50; /* [A Frame] */ a_frame_leg_angle_deg = 45; // leg angle measured from horizontal a_frame_apex_web_mm = 1.20; // PLA+ between the two leg cavities a_frame_crossbar_web_mm = 1.20; // PLA+ between crossbar and leg cavities a_frame_crossbar_offset_mm = 0; a_frame_junction_round_mm = 1.50; /* [Y Profile] */ y_rotation_deg = 90; y_junction_web_mm = 1.20; // PLA+ between neighbouring spoke cavities y_junction_round_mm = 1.50; /* [T Profile] */ t_stem_web_mm = 1.20; // PLA+ between stem and flange cavities t_flange_web_mm = 1.20; // PLA+ between the two flange cavities t_junction_round_mm = 1.50; /* [Three-Fin Profile] */ // Fin projection past the corner it overhangs. The core triangle is solved // from this plus the web and bore minimums below. three_fin_fin_mm = 6.25; three_fin_web_mm = 1.20; // PLA+ at each cyclic junction three_fin_bore_side_mm = 7.50; // minimum central opening three_fin_rotation_deg = 0; three_fin_junction_round_mm = 2.00; /* [Display] */ render_mode = "3D"; // ["3D","Section"] show_straps = true; show_enclosure = true; /* [Quality] */ facets = 48; // [12:4:128] /* [Reporting] */ material_density_g_cm3 = 1.24; // PLA+ nominal $fn = facets; // --------------------------------------------------------------------------- // Derived // --------------------------------------------------------------------------- SB_FAMILY = "3x"; // Generator revision. Qualification attestations bind to this, so any change // that alters emitted geometry MUST bump it. Cosmetic or comment-only edits // must not. See sb-report.scad for what is published. SB_REVISION = "8.0.0"; SB_MEMBER_COUNT = 3; geo = sb_geo( strap_width_mm, strap_thickness_mm, bundle_count, fit_clearance_mm, inside_wall_thickness_mm, outside_wall_thickness_mm, edge_wall_thickness_mm, min_wall_mm ); model_length_mm = length_view == "Full Length" ? member_length_ft * 304.8 : preview_length_mm; // --------------------------------------------------------------------------- // 1-2. Ring profiles // --------------------------------------------------------------------------- /* Both triangle profiles are the same construction: a centreline polygon, grown until its corners are legal, wrapped in one envelope with solid rounded corners, with the bore taken from the members' real inside walls. The 4x generator gets these for free by handing sb_ring_profile() a quadrilateral instead. */ function sb3_equilateral(g) = let(s = sb_width(g), a = s / (2 * SB_SQRT3)) sb_ring_polygon_profile([[-s / 2, -a], [s / 2, -a], [0, 2 * a]], g, ring_corner_web_mm, ring_corner_radius_mm, "Equilateral Triangle"); function sb3_general_triangle(g) = sb_ring_polygon_profile( [[-general_triangle_base / 2, -general_triangle_height / 2], [ general_triangle_base / 2, -general_triangle_height / 2], [ general_triangle_apex_offset, general_triangle_height / 2]], g, ring_corner_web_mm, ring_corner_radius_mm, "General Triangle"); // --------------------------------------------------------------------------- // 3. A Frame // --------------------------------------------------------------------------- /* The apex is a real meeting point: both leg centrelines pass through it and each leg is set back along its own axis until the two cavities are exactly a_frame_apex_web_mm apart. v7 instead offset both legs from the origin by a fixed radius, which delivered apex_gap x cos(leg_angle) and collapsed to nothing as the legs approached vertical. The crossbar is likewise solved by depth against the measured web and then butted flush between the two legs' outer faces. v7 measured to the leg CENTRELINE, so its declared 1.40 mm arrived as 0.16 mm. */ function sb3_a_frame_legs(g, setback) = let( a = a_frame_leg_angle_deg, target = [0, -sb_width(g)], // deep inside the counter d = sb_cavity_w(g) / 2 + setback, angL = 180 + a, angR = -a, cL = [d * cos(angL), d * sin(angL)], cR = [d * cos(angR), d * sin(angR)] ) [ sb_member(cL.x, cL.y, angL, sb_face_toward(cL, angL, target)), sb_member(cR.x, cR.y, angR, sb_face_toward(cR, angR, target)) ]; function sb3_a_frame_apex_web(g, setback) = let(l = sb3_a_frame_legs(g, setback)) sb_path_gap(sb_cavity_path(l[0], g), sb_cavity_path(l[1], g)); function sb3_a_frame_crossbar(g, depth) = let(c = [a_frame_crossbar_offset_mm, -depth]) sb_member(c.x, c.y, 0, sb_face_toward(c, 0, [0, 0])); function sb3_a_frame_bar_web(g, legs, depth) = let(cb = sb_cavity_path(sb3_a_frame_crossbar(g, depth), g)) min(sb_path_gap(cb, sb_cavity_path(legs[0], g)), sb_path_gap(cb, sb_cavity_path(legs[1], g))); function sb3_a_frame(g) = let( W = sb_width(g), f_apex = function(s) sb3_a_frame_apex_web(g, s), feasible = sb_solvable(f_apex, 6 * W, a_frame_apex_web_mm) ) !feasible ? sb_profile_failed(str( "A Frame: at ", a_frame_leg_angle_deg, " degrees the legs are too close to parallel to open a ", a_frame_apex_web_mm, " mm apex web. Reduce a_frame_leg_angle_deg.")) : let( setback = sb_solve(f_apex, 0, 6 * W, a_frame_apex_web_mm), legs = sb3_a_frame_legs(g, setback), f_bar = function(d) sb3_a_frame_bar_web(g, legs, d), bar_ok = sb_solvable(f_bar, 8 * W, a_frame_crossbar_web_mm) ) !bar_ok ? sb_profile_failed( "A Frame: the crossbar cannot reach a legal web against the legs. Reduce a_frame_crossbar_web_mm or the crossbar offset.") : let( depth = sb_solve(f_bar, 0, 8 * W, a_frame_crossbar_web_mm), bar = sb3_a_frame_crossbar(g, depth), ms = [legs[0], bar, legs[1]], // Legs run up to the apex; the wedge between their end faces // is plugged with their convex hull, which leaves no spike // and no V-notch. ext = max(0, setback - sb_wall_edge(g)), legL = sb_sleeve_path(legs[0], g, 0, ext), legR = sb_sleeve_path(legs[1], g, 0, ext), cap = sb_hull_cap([sb_end_face(legs[0], g, -1, ext), sb_end_face(legs[1], g, -1, ext)]), // Crossbar spans flush between the legs' outer faces. lineL = sb_far_face_line(legs[0], g, sb_mc(bar)), lineR = sb_far_face_line(legs[1], g, sb_mc(bar)), barP = sb_sleeve_span(bar, g, lineL[0], lineL[1], lineR[0], lineR[1]), paths = [legL, barP, legR], raw = union(concat([[legL], [legR], [barP]], len(cap) > 0 ? [cap] : [])), shell = sb_fillet_junctions(raw, [[0, 1], [1, 2], [0, 2]], paths, a_frame_junction_round_mm), bore = sb_bore_from_members(ms, g), leg_end_depth = (setback + sb_cavity_w(g)) * sin(a_frame_leg_angle_deg) ) sb_profile( ms, shell, bore, [ sb_check(depth < leg_end_depth, str("A Frame: the crossbar sits at ", depth, " mm below the apex but the legs only reach ", leg_end_depth, " mm. Reduce a_frame_crossbar_web_mm or the leg angle.")), sb_check(sb_bore_valid(bore, ms, g), "A Frame: the enclosed counter has collapsed. Reduce inside_wall_thickness_mm or open the frame out."), sb_check(a_frame_junction_round_mm > 0, "A Frame: a_frame_junction_round_mm 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_check(abs(a_frame_crossbar_offset_mm) < sb_width(g) / 2, "A Frame: a_frame_crossbar_offset_mm has pushed the crossbar off the frame.") ], [ sb_kv("AF_APEX_SETBACK_MM", setback), sb_kv("AF_CROSSBAR_DEPTH_MM", depth), sb_kv("AF_APEX_WEB_MM", sb3_a_frame_apex_web(g, setback)), sb_kv("AF_CROSSBAR_WEB_MM", sb3_a_frame_bar_web(g, legs, depth)) ] ); // --------------------------------------------------------------------------- // 4. Y // --------------------------------------------------------------------------- /* Three spokes pointing away from a common centre. A spoke's broad faces both look at open air, so both get the outside wall - the profile stays three-fold symmetric and non-chiral under any wall settings. The centre is plugged with the convex hull of the three inner end faces rather than left to whatever the crossing rectangles happened to produce. */ function sb3_y(g) = sb_spoke_profile(3, y_rotation_deg, y_junction_web_mm, y_junction_round_mm, g, "Y"); // --------------------------------------------------------------------------- // 5. T // --------------------------------------------------------------------------- /* Two straps form the flange, separated by their own web so each keeps a private channel - in v7 they touched edge to edge and shared one slot. The stem is placed so its cavity clears the flange cavities by exactly t_stem_web_mm and is then butted flush into the flange's far face. */ function sb3_t_members(g) = let( half = t_flange_web_mm / 2 + sb_cavity_w(g) / 2, top = -sb_cavity_t(g) / 2 - t_stem_web_mm, stemy = top - sb_cavity_w(g) / 2 ) [ sb_member(-half, 0, 0, SB_FACE_BOTH_OUT), sb_member( half, 0, 0, SB_FACE_BOTH_OUT), sb_member(0, stemy, -90, SB_FACE_BOTH_OUT) // leading end points down ]; function sb3_t(g) = let( ms = sb3_t_members(g), flL = sb_sleeve_path(ms[0], g), flR = sb_sleeve_path(ms[1], g), stem = sb_sleeve_butt(ms[2], g, ms[0]), paths = [flL, flR, stem], raw = union([[flL], [flR], [stem]]), shell = sb_fillet_junctions(raw, [[0, 2], [1, 2]], paths, t_junction_round_mm), flange_overlap = 2 * sb_wall_edge(g) - t_flange_web_mm ) sb_profile(ms, shell, [], [ sb_check(flange_overlap > 0, str("T: t_flange_web_mm of ", t_flange_web_mm, " mm exceeds twice the edge wall, so the two flange sleeves no longer meet. Reduce it or raise edge_wall_thickness_mm.")), sb_check(t_stem_web_mm >= sb_min_wall(g) - 1e-9, str("T: t_stem_web_mm of ", t_stem_web_mm, " mm is below min_wall_mm.")), sb_check(t_flange_web_mm >= sb_min_wall(g) - 1e-9, str("T: t_flange_web_mm of ", t_flange_web_mm, " mm is below min_wall_mm.")), sb_check(t_junction_round_mm > 0, "T: t_junction_round_mm must be greater than zero. A butt joint with no fillet meets the flange along an exactly tangent boundary, which is a valid outline but cannot be tessellated.") ], [ sb_kv("T_FLANGE_SPAN_MM", 2 * sb_cavity_w(g) + t_flange_web_mm + 2 * sb_wall_edge(g)), sb_kv("T_NOTE", "inside_wall unused: no enclosed bore") ]); // --------------------------------------------------------------------------- // 6. Three-Fin // --------------------------------------------------------------------------- /* Tangential, not radial. Three straight members lie on the sides of a core triangle and are slid cyclically along those sides, so each stops short of the corner behind it and overhangs the corner ahead of it. The three overhangs are the fins. Y three radial arms leaving a common centre Three-Fin three tangential sides around a core, one fin each The core triangle is not a free parameter. It is solved so that the cyclic junctions carry the declared web AND the bore reaches its declared minimum, whichever demands more. The fin projection stays exactly as asked, because it is measured against the same solved triangle. Each member's trailing end is run through the member behind it and cut off flush on its far face. That gives every junction a full-width overlap; the fillet that follows is cosmetic. In v7 the junctions were a 0.4 mm corner touch and the fillet was the only thing holding the part together. */ function sb3_three_fin(g) = sb_fin_profile(3, three_fin_fin_mm, three_fin_web_mm, three_fin_bore_side_mm, three_fin_rotation_deg, three_fin_junction_round_mm, g, "Three-Fin"); // --------------------------------------------------------------------------- // Catalogue // --------------------------------------------------------------------------- /* Only the selected builder runs, so a parameter belonging to one entry can never invalidate another. A 4x generator supplies its own table here and nothing else changes. */ function sb3_build(name, g) = name == "Equilateral Triangle" ? sb3_equilateral(g) : name == "General Triangle" ? sb3_general_triangle(g) : name == "A Frame" ? sb3_a_frame(g) : name == "Y" ? sb3_y(g) : name == "T" ? sb3_t(g) : name == "Three-Fin" ? sb3_three_fin(g) : sb_profile_failed(str("Unknown profile_type: ", name)); profile = sb3_build(profile_type, geo); // Parameters that apply to every profile, so they are checked once here. base_checks = [ sb_check(strap_width_mm > 0, "strap_width_mm must be greater than zero."), sb_check(strap_thickness_mm > 0, "strap_thickness_mm must be greater than zero."), sb_check(bundle_count >= 1 && bundle_count == floor(bundle_count), "bundle_count must be a whole number of at least 1."), sb_check(fit_clearance_mm >= 0, "fit_clearance_mm cannot be negative."), sb_check(inside_wall_thickness_mm > 0, "inside_wall_thickness_mm must be positive."), sb_check(outside_wall_thickness_mm > 0, "outside_wall_thickness_mm must be positive."), sb_check(edge_wall_thickness_mm > 0, "edge_wall_thickness_mm must be positive."), sb_check(min_wall_mm > 0, "min_wall_mm must be positive."), sb_check(preview_length_mm > 0, "preview_length_mm must be greater than zero.") ]; // Fail on the profile's own checks before touching geometry, so an // unbuildable arrangement reports its cause instead of a library error. build_status = sb_require(concat(base_checks, sb_p_checks(profile))); centred = sb_centred(profile, geo); section = centred[SB_C_SECTION]; members = sb_centred_members(profile, centred[SB_C_SHIFT]); metrics = sb_metrics(section, centred[SB_C_SHELL], members, geo); status = sb_require(sb_universal_checks(section, metrics, SB_MEMBER_COUNT, geo)); // --------------------------------------------------------------------------- // Report // --------------------------------------------------------------------------- sb_report(SB_FAMILY, profile_type, status, geo, metrics, model_length_mm, material_density_g_cm3, concat([sb_kv("REVISION", SB_REVISION), sb_kv("LENGTH_FT", member_length_ft), sb_kv("LENGTH_VIEW", length_view)], sb_p_info(profile))); // --------------------------------------------------------------------------- // Output // --------------------------------------------------------------------------- if (render_mode == "Section") { if (show_enclosure) color([0.20, 0.55, 0.95]) sb_draw_section(section); if (show_straps) color([0.96, 0.72, 0.05]) sb_draw_section(centred[SB_C_STRAPS]); } else { if (show_enclosure) color([0.20, 0.55, 0.95, 0.55]) sb_extrude_section(section, model_length_mm); if (show_straps) color([0.96, 0.72, 0.05, 0.82]) sb_extrude_straps(members, geo, model_length_mm); }