/* CR80 / ID-1 CARD HOLDER GENERATOR — V2 -------------------------------------- One-piece FDM-printable holders for CR80 / ISO ID-1 cards. Designed for OpenSCAD Nightly, but deliberately avoids fragile/obscure experimental syntax so files remain easy to maintain and batch-render. Nominal card size: 85.60 x 53.98 x 0.76 mm. Recommended print orientation: back plate flat on the bed. V2 highlights: - multiple retention systems, including XY-flex snap tabs - slide-in and semi-permanent security variants - durable / abuse-proof construction profiles - rounded bumper frame - reinforced lanyard / chain attachments - optional dual attachments - tuneable clearances for PLA / PETG - support-free geometry as far as practical */ /* [Card] */ orientation = "Portrait"; // [Portrait,Landscape] card_width = 85.60; // [80:0.01:90] card_height = 53.98; // [50:0.01:60] card_thickness = 0.76; // [0.50:0.01:1.50] card_corner_radius = 3.18; // [2.0:0.01:4.0] xy_clearance = 0.30; // [0.10:0.05:0.80] z_clearance = 0.20; // [0.05:0.05:0.60] /* [Material / Construction Profile] */ material_profile = "PETG"; // [PLA,PETG,Custom] construction = "Durable"; // [Light,Standard,Durable,Abuse Proof] wall_width = 2.40; // [1.20:0.10:6.00] back_thickness = 1.60; // [0.80:0.10:4.00] outer_corner_radius = 5.00; // [2.0:0.25:12.0] front_edge_height = 0.70; // [0.30:0.10:2.00] bumper_enabled = true; // [true,false] bumper_extra_width = 1.20; // [0.40:0.10:3.50] bumper_height = 1.20; // [0.40:0.10:3.00] /* [Retention] */ retention = "Flex Tabs"; // [Corner Clips,Edge Clips,Side Detents,Flex Tabs,Slide Rails,Security Rails,Full Bezel,Friction Only] clip_overhang = 1.35; // [0.50:0.05:2.80] clip_length = 10.0; // [5:0.5:22] clip_thickness = 0.90; // [0.50:0.05:1.80] clip_entry_chamfer = 0.55; // [0.20:0.05:1.20] detent_depth = 0.45; // [0.15:0.05:1.00] detent_diameter = 3.0; // [1.5:0.25:5.0] slide_rail_overhang = 1.40; // [0.50:0.05:3.00] /* [Flex Tabs] */ flex_tab_length = 15.0; // [8:0.5:28] flex_tab_width = 2.20; // [1.20:0.10:4.00] flex_relief_gap = 0.75; // [0.40:0.05:1.50] flex_hook_depth = 0.70; // [0.25:0.05:1.40] flex_hook_length = 3.20; // [1.5:0.10:6.0] flex_tabs_per_side = 1; // [1,2] /* [Security] */ security_capture = 1.80; // [0.80:0.10:3.50] security_entry_gap = 0.45; // [0.20:0.05:1.20] security_release_notch = true; // [true,false] security_release_width = 7.0; // [4:0.5:14] /* [Access / Removal] */ thumb_notch = true; // [true,false] thumb_notch_side = "Top"; // [Top,Bottom,Left,Right] thumb_notch_diameter = 16.0; // [8:1:28] back_push_hole = false; // [true,false] back_push_hole_diameter = 18.0; // [10:1:30] /* [Attachment] */ attachment = "Slot"; // [None,Round,Slot,Dual Round,Dual Slot,Round + Slot] attachment_side = "Top"; // [Top,Bottom,Left,Right] second_attachment = false; // [true,false] attachment_width = 24.0; // [14:1:44] attachment_extension = 12.0; // [8:1:28] round_hole_diameter = 5.0; // [3:0.25:10] slot_length = 14.0; // [8:0.5:26] slot_width = 4.5; // [3:0.25:9] dual_hole_spacing = 10.0; // [6:0.5:22] attachment_reinforcement = 3.0; // [1.5:0.25:7] attachment_gussets = true; // [true,false] /* [Preview / Quality] */ show_card_preview = true; // [true,false] preview_explode = 0.0; // [0:0.5:12] quality = "Normal"; // [Draft,Normal,Fine] // ---------- Quality ---------- $fn = quality == "Draft" ? 28 : quality == "Fine" ? 96 : 56; // ---------- Material-derived tuning ---------- // These do not override your explicit dimensions; they set safe lower bounds. MAT_FLEX_MIN = material_profile == "PLA" ? 2.40 : material_profile == "PETG" ? 1.80 : 1.20; MAT_GAP_MIN = material_profile == "PLA" ? 0.70 : material_profile == "PETG" ? 0.55 : 0.40; // ---------- Construction profile ---------- PROFILE_WALL = construction == "Light" ? 1.60 : construction == "Standard" ? 2.20 : construction == "Durable" ? 3.00 : 3.80; PROFILE_BACK = construction == "Light" ? 1.00 : construction == "Standard" ? 1.40 : construction == "Durable" ? 2.00 : 2.60; PROFILE_RADIUS = construction == "Light" ? 3.50 : construction == "Standard" ? 4.50 : construction == "Durable" ? 5.50 : 6.50; PROFILE_ATTACH = construction == "Abuse Proof" ? 14.0 : construction == "Durable" ? 12.0 : 9.0; WALL = max(wall_width, PROFILE_WALL); BACK = max(back_thickness, PROFILE_BACK); R_OUT = max(outer_corner_radius, PROFILE_RADIUS); FLEX_W = max(flex_tab_width, MAT_FLEX_MIN); FLEX_GAP = max(flex_relief_gap, MAT_GAP_MIN); ATT_EXT = max(attachment_extension, PROFILE_ATTACH); ATT_REINF = construction == "Abuse Proof" ? max(attachment_reinforcement, 4.2) : construction == "Durable" ? max(attachment_reinforcement, 3.4) : attachment_reinforcement; // ---------- Derived card / holder geometry ---------- raw_w = orientation == "Landscape" ? card_width : card_height; raw_h = orientation == "Landscape" ? card_height : card_width; pocket_w = raw_w + 2 * xy_clearance; pocket_h = raw_h + 2 * xy_clearance; outer_w = pocket_w + 2 * WALL; outer_h = pocket_h + 2 * WALL; card_top_z = BACK + card_thickness + z_clearance; wall_top_z = card_top_z + front_edge_height; attach_t = max(wall_top_z, construction == "Abuse Proof" ? 3.6 : 2.8); bumper_w = construction == "Abuse Proof" ? max(bumper_extra_width, 1.8) : bumper_extra_width; bumper_h = construction == "Abuse Proof" ? max(bumper_height, 1.6) : bumper_height; assert(xy_clearance >= 0.10, "xy_clearance is probably too small for FDM."); assert(WALL > max(clip_overhang, flex_hook_depth), "Wall is too narrow for selected retention geometry."); assert(slot_length > slot_width, "slot_length must be greater than slot_width."); assert(FLEX_W < WALL + 1.5, "Flex tab is unusually thick. Reduce flex_tab_width or increase wall_width."); // ---------- 2D primitives ---------- module rounded_rect_2d(w, h, r) { rr = min(r, min(w,h)/2 - 0.01); offset(r=rr) square([max(0.01,w - 2*rr), max(0.01,h - 2*rr)], center=true); } module slot_2d(len, wid) { hull() { translate([-(len-wid)/2, 0]) circle(d=wid); translate([ +(len-wid)/2, 0]) circle(d=wid); } } // ---------- Attachment positioning ---------- module at_attachment_side(side=attachment_side) { if (side == "Top") translate([0, outer_h/2 + ATT_EXT/2 - 1.0, 0]) children(); else if (side == "Bottom") translate([0, -outer_h/2 - ATT_EXT/2 + 1.0, 0]) rotate([0,0,180]) children(); else if (side == "Left") translate([-outer_w/2 - ATT_EXT/2 + 1.0, 0, 0]) rotate([0,0,90]) children(); else translate([outer_w/2 + ATT_EXT/2 - 1.0, 0, 0]) rotate([0,0,-90]) children(); } function opposite_side(s) = s == "Top" ? "Bottom" : s == "Bottom" ? "Top" : s == "Left" ? "Right" : "Left"; module attachment_plate_2d() { rounded_rect_2d(attachment_width, ATT_EXT + 4.0, min(R_OUT, min(attachment_width, ATT_EXT+4)/2 - 0.1)); } module attachment_holes_2d() { if (attachment == "Round") { circle(d=round_hole_diameter); } else if (attachment == "Slot") { slot_2d(slot_length, slot_width); } else if (attachment == "Dual Round") { translate([-dual_hole_spacing/2,0]) circle(d=round_hole_diameter); translate([ dual_hole_spacing/2,0]) circle(d=round_hole_diameter); } else if (attachment == "Dual Slot") { translate([-dual_hole_spacing/2,0]) rotate(90) slot_2d(slot_length*0.65, slot_width); translate([ dual_hole_spacing/2,0]) rotate(90) slot_2d(slot_length*0.65, slot_width); } else if (attachment == "Round + Slot") { translate([0,-3.5]) slot_2d(slot_length, slot_width); translate([0, 4.5]) circle(d=round_hole_diameter); } } module one_attachment(side=attachment_side) { if (attachment != "None") { at_attachment_side(side) difference() { union() { linear_extrude(attach_t) attachment_plate_2d(); // Local reinforcement rings distribute load around the opening. if (construction == "Durable" || construction == "Abuse Proof") { if (attachment == "Round") cylinder(h=attach_t, d=round_hole_diameter + 2*ATT_REINF); else if (attachment == "Slot") linear_extrude(attach_t) offset(r=ATT_REINF) slot_2d(slot_length, slot_width); else if (attachment == "Dual Round") { translate([-dual_hole_spacing/2,0,0]) cylinder(h=attach_t, d=round_hole_diameter + 2*ATT_REINF); translate([ dual_hole_spacing/2,0,0]) cylinder(h=attach_t, d=round_hole_diameter + 2*ATT_REINF); } } } translate([0,0,-0.1]) linear_extrude(attach_t + 0.2) attachment_holes_2d(); } // Broad triangular-ish roots reduce stress concentration where tab meets body. if (attachment_gussets) at_attachment_side(side) for (sx=[-1,1]) hull() { translate([sx*(attachment_width/2-2.0), -ATT_EXT/2+2.0, 0]) cylinder(h=min(attach_t, BACK+1.0), d=3.2); translate([sx*(outer_w < outer_h ? outer_w : outer_h)*0.10, -ATT_EXT/2-3.5, 0]) cylinder(h=min(attach_t, BACK+1.0), d=3.2); } } } module attachments() { one_attachment(attachment_side); if (second_attachment && attachment != "None") one_attachment(opposite_side(attachment_side)); } // ---------- Base shell ---------- module back_plate() { linear_extrude(BACK) rounded_rect_2d(outer_w, outer_h, R_OUT); } module perimeter_walls(open_top=false) { difference() { translate([0,0,BACK-0.08]) linear_extrude(wall_top_z - BACK + 0.08) rounded_rect_2d(outer_w, outer_h, R_OUT); translate([0,0,BACK-0.05]) linear_extrude(wall_top_z - BACK + 0.10) rounded_rect_2d(pocket_w, pocket_h, card_corner_radius + xy_clearance); if (open_top) translate([0, outer_h/2 - WALL/2, BACK-0.1]) cube([pocket_w + 0.6, WALL + 0.8, wall_top_z-BACK+0.3], center=true); } } module bumper_frame() { if (bumper_enabled) { bh = min(BACK + bumper_h, wall_top_z); if (retention == "Flex Tabs") { // Flex tabs need free space to deflect outward in XY. A continuous // side bumper would mechanically block them, so this mode uses // heavy top/bottom impact rails instead of a closed bumper ring. for (sy=[-1,1]) translate([0, sy*(outer_h/2 + bumper_w/2 - 0.20), 0]) linear_extrude(bh) rounded_rect_2d(outer_w + 2*bumper_w, bumper_w + 0.80, min((bumper_w+0.80)/2-0.01, R_OUT)); } else { // Closed low bumper for rigid retention systems. difference() { linear_extrude(bh) rounded_rect_2d(outer_w + 2*bumper_w, outer_h + 2*bumper_w, R_OUT + bumper_w); translate([0,0,-0.1]) linear_extrude(bh+0.2) rounded_rect_2d(outer_w - 0.20, outer_h - 0.20, max(0.5,R_OUT-0.10)); } } } } // ---------- Traditional roof-style retainers ---------- module edge_roof_x(y, len=clip_length) { translate([0, y, card_top_z + clip_thickness/2]) cube([len, WALL + clip_overhang, clip_thickness], center=true); } module edge_roof_y(x, len=clip_length) { translate([x, 0, card_top_z + clip_thickness/2]) cube([WALL + clip_overhang, len, clip_thickness], center=true); } module corner_clips() { x = pocket_w/2 + (WALL-clip_overhang)/2; y = pocket_h/2 + (WALL-clip_overhang)/2; for (sx=[-1,1], sy=[-1,1]) translate([sx*x, sy*y, card_top_z + clip_thickness/2]) cube([WALL + clip_overhang, WALL + clip_overhang, clip_thickness], center=true); } module edge_clips() { edge_roof_x( pocket_h/2 + (WALL-clip_overhang)/2); edge_roof_x(-pocket_h/2 - (WALL-clip_overhang)/2); edge_roof_y( pocket_w/2 + (WALL-clip_overhang)/2); edge_roof_y(-pocket_w/2 - (WALL-clip_overhang)/2); } // ---------- Side detents ---------- module side_detents() { d = detent_depth; for (sy=[-0.24,0.24]) { translate([ pocket_w/2 + detent_diameter/2 - d, pocket_h*sy, BACK + card_thickness*0.58]) rotate([90,0,0]) cylinder(h=detent_diameter, d=detent_diameter, center=true); translate([-pocket_w/2 - detent_diameter/2 + d, pocket_h*sy, BACK + card_thickness*0.58]) rotate([90,0,0]) cylinder(h=detent_diameter, d=detent_diameter, center=true); } } // ---------- XY flex tabs ---------- // A long leaf is isolated from each side wall by relief cuts. The rounded hook // protrudes into the pocket. During insertion the leaf bends OUTWARD in XY, // so flexing does not rely primarily on Z-layer adhesion. module flex_tab_positive(side=1, y0=0) { xwall = side*(pocket_w/2 + FLEX_W/2); translate([xwall, y0, BACK]) cube([FLEX_W, flex_tab_length, wall_top_z-BACK], center=false); } module flex_hook(side=1, y0=0) { x = side*(pocket_w/2 + flex_hook_depth/2 - 0.08); y = y0 + flex_tab_length/2 - flex_hook_length/2; translate([x,y,BACK + card_thickness*0.58]) rotate([90,0,0]) cylinder(h=flex_hook_length, d=2*flex_hook_depth, center=true); } module flex_tab_relief_cuts() { tab_positions = flex_tabs_per_side == 2 ? [-pocket_h*0.24-flex_tab_length/2, pocket_h*0.24-flex_tab_length/2] : [-flex_tab_length/2]; for (side=[-1,1], y0=tab_positions) { // Along the inner/outer side of the cantilever: leave its root connected. x = side*(pocket_w/2 + WALL/2); translate([x, y0-flex_relief_gap/2, BACK-0.1]) cube([WALL+1.0, FLEX_GAP, wall_top_z-BACK+0.3], center=true); translate([x, y0+flex_tab_length+flex_relief_gap/2, BACK-0.1]) cube([WALL+1.0, FLEX_GAP, wall_top_z-BACK+0.3], center=true); // Outer clearance pocket lets the leaf deflect outward. translate([side*(pocket_w/2 + WALL - FLEX_GAP/2), y0+flex_tab_length*0.58, BACK-0.1]) cube([FLEX_GAP, flex_tab_length*0.78, wall_top_z-BACK+0.3], center=true); } } module flex_tabs() { tab_positions = flex_tabs_per_side == 2 ? [-pocket_h*0.24-flex_tab_length/2, pocket_h*0.24-flex_tab_length/2] : [-flex_tab_length/2]; for (side=[-1,1], y0=tab_positions) flex_hook(side,y0); } // ---------- Slide rails ---------- module slide_rails(security=false) { over = security ? max(slide_rail_overhang, security_capture) : slide_rail_overhang; rail_h = max(clip_thickness, 0.8); x = pocket_w/2 + (WALL-over)/2; for (sx=[-1,1]) translate([sx*x, -WALL*0.15, card_top_z + rail_h/2]) cube([WALL + over, pocket_h - 3.5, rail_h], center=true); // Bottom capture / stop. translate([0, -pocket_h/2 - (WALL-over)/2, card_top_z + rail_h/2]) cube([pocket_w*0.60, WALL + over, rail_h], center=true); // Entrance retention. if (!security) { for (sx=[-1,1]) translate([sx*(pocket_w/2 - 5), pocket_h/2 - 2.0, BACK + card_thickness*0.58]) sphere(d=max(1.6, detent_diameter*0.62)); } else { // Stronger shoulders at entry: semi-permanent but serviceable. for (sx=[-1,1]) translate([sx*(pocket_w/2 - 5.5), pocket_h/2 - 2.4, BACK + card_thickness*0.60]) sphere(d=max(2.0, security_capture*1.55)); } } // ---------- Full bezel ---------- module full_bezel() { // Continuous front frame. Card is intended to slide in from the top opening. bezel_t = max(clip_thickness, 0.9); difference() { translate([0,0,card_top_z]) linear_extrude(bezel_t) rounded_rect_2d(outer_w, outer_h, R_OUT); translate([0,0,card_top_z-0.05]) linear_extrude(bezel_t+0.10) rounded_rect_2d(max(1,pocket_w-2*security_capture), max(1,pocket_h-2*security_capture), max(0.5,card_corner_radius-0.4)); // top loading opening translate([0, outer_h/2-WALL/2, card_top_z-0.1]) cube([pocket_w*0.78, WALL+1.0, bezel_t+0.3], center=true); } } module retention_features() { if (retention == "Corner Clips") corner_clips(); else if (retention == "Edge Clips") edge_clips(); else if (retention == "Side Detents") side_detents(); else if (retention == "Flex Tabs") flex_tabs(); else if (retention == "Slide Rails") slide_rails(false); else if (retention == "Security Rails") slide_rails(true); else if (retention == "Full Bezel") full_bezel(); } // ---------- Access cuts ---------- module thumb_notch_cut() { if (thumb_notch) { h = wall_top_z-BACK + clip_thickness + 0.6; if (thumb_notch_side == "Top") translate([0, pocket_h/2 + WALL*0.30, BACK-0.05]) cylinder(h=h, d=thumb_notch_diameter); else if (thumb_notch_side == "Bottom") translate([0, -pocket_h/2 - WALL*0.30, BACK-0.05]) cylinder(h=h, d=thumb_notch_diameter); else if (thumb_notch_side == "Left") translate([-pocket_w/2 - WALL*0.30, 0, BACK-0.05]) cylinder(h=h, d=thumb_notch_diameter); else translate([pocket_w/2 + WALL*0.30, 0, BACK-0.05]) cylinder(h=h, d=thumb_notch_diameter); } } module back_push_cut() { if (back_push_hole) translate([0,0,-0.1]) cylinder(h=BACK+0.2, d=back_push_hole_diameter); } module security_release_cut() { if (retention == "Security Rails" && security_release_notch) { // Narrow service notch at entry. Enough for a fingernail/small plastic tool, // but much less likely to release accidentally than an open rail system. translate([0, pocket_h/2 + WALL*0.15, BACK + card_thickness*0.35]) cube([security_release_width, WALL+1.2, wall_top_z-BACK+clip_thickness+0.6], center=true); } } // ---------- Final holder ---------- module holder() { open_top = retention == "Slide Rails" || retention == "Security Rails" || retention == "Full Bezel"; difference() { union() { back_plate(); perimeter_walls(open_top); bumper_frame(); retention_features(); attachments(); } // Relief slots are subtractive and belong to Flex Tabs only. if (retention == "Flex Tabs") flex_tab_relief_cuts(); thumb_notch_cut(); back_push_cut(); security_release_cut(); } } module card_preview() { if (show_card_preview) %translate([0,0,BACK + preview_explode]) linear_extrude(card_thickness) rounded_rect_2d(raw_w, raw_h, card_corner_radius); } holder(); card_preview();