// Orbis Darts rig, https://orbisdarts.com/#rig // Licensed CC BY-NC-SA 4.0 (https://creativecommons.org/licenses/by-nc-sa/4.0/): print, change and // share it with credit to Orbis Darts, not for sale. Share changes under the same licence. // // Ring light for the dartboard: a COB LED strip on a printed ring that sits behind the three // cameras (camera_mount.stl, imported for the preview) and is carried by their arms. // // Behind the cameras the light reaches the board from 15-40 degrees up instead of skimming // across it, so there is more light on the board and dart shadows are short. The strip sits in // a throat under a visor that cuts off everything flatter than `cutoff` below horizontal: every // camera sees the far side of the ring at about its own height, so no camera ever looks into // the LEDs, and the ring can never come between a camera and the board. // // The board hangs on a wall, so the ring's weight acts in its own plane, where it is stiffest. // The arms carry the light, so a hard knock on the ring can shift a camera: recalibrate then. // // Parts (set `part`, F6 Render, File > Export > STL). Each exports in its print orientation. // segment x17 20-degree section of the ring, printed upside down (top face on the bed) // segment_wire x1 the same with a hole for the strip's lead // key x18 joins two sections at the back, 2 M3 x 8 screws each // bracket x3 clamps onto a camera arm and holds the ring behind that camera: // 1 M3 x 10 locks it on the arm, 2 M3 x 12 go up into the ring // Print the sections with 2 walls and 10-15% infill to keep the ring light for the arms. // // Fitting: slide a bracket sideways onto each arm (the arm's ends are the foot and the camera // box), all three the same way round, and tighten its locking screw. Join the sections with the // keys, stick the strip in the throat, lift the ring onto the three brackets' plates and screw // it on from below. Then calibrate the cameras. // // Coordinate system: board centre at the origin, z = 0 is the playing face, z goes out towards // the thrower, y is up (12 o'clock). Bearings are clockwise from 12 o'clock. /* [Part to export] */ part = "preview"; // [preview, section, segment, segment_wire, key, bracket] /* [Board, surround and camera arms] */ board_d = 451; // standard bristle board surround_z = -30; // surround face, behind the playing face (the board stands 30 proud) surround_d = 710; // surround outer diameter arms = [0, 120, 240]; // camera arm bearings: 12, 4 and 8 o'clock // The arm (camera_mount.stl, profile in the arm's own x/z with x = 0 at the board edge): its // underside leaves the foot at (6, -9) and meets the camera box at (185.1, 139.5); its top runs // from (0, 0) to (166.9, 141.7), a little steeper, so the arm thickens from about 12 to 12.7 mm // along its length (measured by slicing the STL). arm_p0 = [6, -9]; arm_p1 = [185.1, 139.5]; arm_q0 = [0, 0]; arm_q1 = [166.9, 141.7]; arm_w = 43.6; // arm (and camera box) width // Camera (camera_mount.scad: cam_radius 385, arm_angle 45): lens at x = z = 159.5, the box // tilted 22.5 degrees towards the bull, 16 deep and as wide as the arm. lens = [159.5, 159.5]; cam_tilt = 22.5; box_d = 16; /* [LED strip and its throat] */ strip_w = 8; // COB strip width face_w = 10; // face the strip sticks to strip_r = 445; // radius of the strip's centre line strip_z = 180; // height of the strip's centre above the playing face aim = 32; // the strip faces in towards the board, tipped down by this much cutoff = 10; // the visor stops every ray flatter than this below horizontal visor_slope = 40; // the visor's underside slopes down at this (steeper: shorter visor, less light out) visor_t = 3; // visor thickness at its root lip = 6; // short lip below the strip back_w = 10; // ring body behind the strip (the key screws need it) under = 4; // ring body below the lip /* [Ring] */ segs = 18; // sections round the ring (20 degrees each fit a 220 x 220 bed) /* [Keys between sections] */ key_d = 3; // key thickness (pocket depth in the back face) key_l = 15; // key reaches this far into each section key_h = 14; // key height key_fit = 0.3; key_screw = 5; // M3 pilot depth beyond the pocket (M3 x 8 screws) key_drop = 2; // keys sit this far below the body's middle, where it is deepest /* [Arm brackets] */ clamp_x = 115; // where the clamp sits on the arm (distance out from the board edge) clamp_l = 26; // clamp length along the arm clamp_wall = 4; clamp_fit = 0.4; // clearance round the arm strut_t = 6; // strut thickness beside the arm (it takes the ring's sideways weight at 4 and 8) strut_end = 416; // the strut along the arm stops at this radius, clear of the ring's visor pad_t = 6; // plate under the ring pad_w = 52; // plate width along the ring ring_screw_y = 12; // the ring screws, either side of the joint ring_pilot = 8; // pilot depth in the ring (M3 x 12 through the plate) pilot = 2.6; // M3 into plastic clear = 3.4; // M3 clearance $fn = 96; eps = 0.01; R = board_d / 2; seg_a = 360 / segs; // ---------------------------------------------------------------- ring profile (r, z) nrm = [-cos(aim), -sin(aim)]; // out of the strip face, towards the board up = [-sin(aim), cos(aim)]; // up the strip face S = [strip_r, strip_z]; B = S - face_w / 2 * up; // lower (outer) edge of the face T = S + face_w / 2 * up; // upper (inner) edge vdir = [-cos(visor_slope), -sin(visor_slope)]; // the visor's underside slopes down from the face // long enough that the ray from the face's lower edge past the visor tip is at the cut-off angle // (the visor must slope more steeply than the cut-off, or that ray would never meet it) visor_len = face_w * (cos(aim) + tan(cutoff) * sin(aim)) * cos(cutoff) / sin(visor_slope - cutoff) + 2; V = T + visor_len * vdir; // visor tip, underside top_z = T[1] + visor_t; W = B + lip * [-cos(55), -sin(55)]; // lip tip below the strip back_r = B[0] + back_w; bot_z = W[1] - under; module ring_2d() { difference() { polygon([B, T, V, [V[0], top_z], [back_r, top_z], [back_r, bot_z], [W[0], bot_z], W]); // shallow channel the strip sits in hw = (strip_w + 0.6) / 2; polygon([S - hw * up - 0.5 * nrm, S + hw * up - 0.5 * nrm, S + hw * up + 1 * nrm, S - hw * up + 1 * nrm]); } } // ---------------------------------------------------------------- the camera and arm, in (r, z) arm_k = (arm_p1[1] - arm_p0[1]) / (arm_p1[0] - arm_p0[0]); arm_ang = atan(arm_k); function arm_z(r) = arm_p0[1] + (r - R - arm_p0[0]) * arm_k; // the camera's sight line to the near edge of the board, at radius r function sight_z(r) = (r - R) * lens[1] / lens[0]; // a point in the camera box's frame (x back from the lens face, z up its face) function box_pt(x, z) = [R + lens[0] + x * cos(cam_tilt) - z * sin(cam_tilt), lens[1] + x * sin(cam_tilt) + z * cos(cam_tilt)]; box_back = [box_pt(box_d, -arm_w / 2), box_pt(box_d, arm_w / 2)]; function box_back_r(z) = box_back[0][0] + (z - box_back[0][1]) / (box_back[1][1] - box_back[0][1]) * (box_back[1][0] - box_back[0][0]); // ---------------------------------------------------------------- checks lens_rz = [R + lens[0], lens[1]]; top_ray = atan2(V[1] - B[1], B[0] - V[0]); // flattest ray the visor lets out (negative: below horizontal) // how far below its own level a camera sees the strip's upper edge, from the parts of the ring // in front of its lens (behind the lens the camera can't see the ring at all) function look(phi) = let(p = [strip_r * sin(phi), strip_r * cos(phi)], c = [0, lens_rz[0]], dist = norm(p - c), front = (p[1] - c[1]) < 0) front ? atan2(lens_rz[1] - T[1], dist) : 90; worst = min([for (phi = [1 : 179]) look(phi)]); echo(str("Strip ", round(2 * PI * strip_r), " mm long. Light leaves the ring from ", round(-top_ray), " degrees below horizontal down; the board is ", round(atan2(strip_z, strip_r + R)), "-", round(atan2(strip_z, strip_r - R)), " degrees below it. Cameras look at the strip from ", round(-worst), " degrees below, so the visor hides it")); echo(str("Visor tip at r ", V[0], "; camera box back at that height r ", box_back_r(V[1]), " (gap ", V[0] - box_back_r(V[1]), ")")); assert(-top_ray >= cutoff - 0.01, "visor too short for the cut-off"); assert(worst > top_ray + 2, "a camera could look into the strip"); assert(V[0] - box_back_r(V[1]) >= 8, "ring too close behind the camera box"); assert(bot_z - pad_t > box_pt(box_d, -arm_w / 2)[1] + 8, "ring too low behind the camera box"); // ---------------------------------------------------------------- segment // at bearing b (clockwise from 12), radius r, height z; local x radial, y along the ring module at(b, r = 0, z = 0) { rotate([0, 0, 90 - b]) translate([r, 0, z]) children(); } key_z = (top_z + bot_z) / 2 - key_drop; ring_screw_r = (W[0] + back_r) / 2 - 1; // A section runs clockwise, which is local -y at its start and +y at its end. module joint_cuts(end) { // end = 0: start of the section, 1: its end s = end ? 1 : -1; k = key_l + key_fit; translate([back_r - key_d - key_fit, end ? -eps : -k, key_z - key_h / 2 - key_fit]) cube([key_d + key_fit + 1, k + eps, key_h + 2 * key_fit]); translate([back_r - key_d - key_fit - key_screw, s * key_l / 2, key_z]) rotate([0, 90, 0]) cylinder(d = pilot, h = key_screw + 1, $fn = 24); // pilot for a bracket's screw, up into the underside (only the joints at an arm use it) translate([ring_screw_r, s * ring_screw_y, bot_z - 1]) cylinder(d = pilot, h = ring_pilot + 1, $fn = 24); } module segment(b0, wire = false) { difference() { at(b0 + seg_a) rotate_extrude(angle = seg_a, $fn = 540) ring_2d(); at(b0) joint_cuts(0); at(b0 + seg_a) joint_cuts(1); if (wire) at(b0 + seg_a / 2) translate([S[0] - 4, 0, S[1]]) rotate([0, 90, 0]) cylinder(d = 6, h = back_r - S[0] + 8, $fn = 32); } } module key() { l = 2 * key_l - 2 * key_fit; difference() { cube([key_d - 0.2, l, key_h - key_fit]); for (s = [-1, 1]) translate([-1, l / 2 + s * key_l / 2, key_h / 2]) rotate([0, 90, 0]) cylinder(d = clear, h = key_d + 2, $fn = 24); } } // ---------------------------------------------------------------- bracket // // In (r, z) with y across the arm. A C round the arm, open on the +y side so it slides on // sideways; its closed side carries on along the arm as a strut, beside the arm and the camera // box, to a plate under the ring behind the camera. It prints on that closed side. top_k = (arm_q1[1] - arm_q0[1]) / (arm_q1[0] - arm_q0[0]); top_ang = atan(top_k); function arm_top(r) = arm_q0[1] + (r - R - arm_q0[0]) * top_k; cr = R + clamp_x; AW = arm_w + clamp_fit; // slot width fv = clamp_fit / 2 / cos(arm_ang); // clearance above and below the arm, measured upright tv = clamp_wall / cos(arm_ang); // wall thickness, measured upright cl = [cr - clamp_l / 2 * cos(arm_ang), cr + clamp_l / 2 * cos(arm_ang)]; // clamp's ends (r) pad = [W[0] - 14, back_r + 2]; // plate under the ring, r from/to // the arm's own shape between radii r0 and r1, grown by `below` and `above` module band(r0, r1, below, above) polygon([[r0, arm_z(r0) - below], [r1, arm_z(r1) - below], [r1, arm_top(r1) + above], [r0, arm_top(r0) + above]]); module clamp_2d() band(cl[0], cl[1], fv + tv, fv + tv); module pad_2d() { translate([pad[0], bot_z - pad_t]) square([pad[1] - pad[0], pad_t]); } module strut_2d() { // along the arm, the arm's own depth plus the clamp's walls band(cl[0], strut_end, fv + tv, fv + tv); // down and out to the plate under the ring hull() { band(strut_end - 9, strut_end, fv + tv, fv + tv); translate([pad[0], bot_z - pad_t]) square([12, pad_t]); } pad_2d(); } // the clamp's top jaw stays well below the camera's sight line to the board edge jaw_top = arm_top(cl[1]) + fv + tv; echo(str("Clamp: arm ", (arm_top(cr) - arm_z(cr)) * cos(arm_ang), " mm thick there; top jaw ", sight_z(cl[1]) - jaw_top, " mm below the camera's sight line")); assert(sight_z(cl[1]) - jaw_top >= 8, "clamp in the camera's view of the board edge"); // the strut along the arm ends before the visor, or under it assert(strut_end <= V[0] - 4 || arm_top(strut_end) + fv + tv <= V[1] - 2, "bracket strut runs into the ring's visor"); // plastic left between each screw pilot and the strip's channel or the lip (the profile's inner edge) function inner_r(z) = z <= W[1] ? W[0] : z <= B[1] ? W[0] + (z - W[1]) / (B[1] - W[1]) * (B[0] - W[0]) : B[0] - (z - B[1]) * tan(aim) + 1; key_wall = (back_r - key_d - key_fit - key_screw) - inner_r(key_z) ; ring_wall = (ring_screw_r - pilot / 2) - inner_r(bot_z + ring_pilot); echo(str("Ring ", back_r - V[0], " x ", top_z - bot_z, " mm; plastic round the key screws ", key_wall, " mm, round the bracket screws ", ring_wall, " mm")); assert(key_wall >= 2 && ring_wall >= 1.2, "a screw pilot breaks into the strip channel"); // y as the third dimension: extrude a profile from y0 to y1 module across(y0, y1) { translate([0, y1, 0]) rotate([90, 0, 0]) linear_extrude(y1 - y0) children(); } module bracket() { t = clamp_wall; difference() { union() { across(-AW / 2 - t, AW / 2) clamp_2d(); // the C // strut beside the arm and the camera box, 0.6 mm further out than the clamp's // wall so it never rubs on the box across(-AW / 2 - strut_t - 0.6, -AW / 2 - 0.6) strut_2d(); across(-pad_w / 2, pad_w / 2) pad_2d(); // plate across under the ring } // slot the arm slides into, open on the +y side across(-AW / 2, AW / 2 + 10) band(cr - 60, cr + 60, fv, fv); // locking screw through the top jaw onto the arm translate([cr, 0, arm_top(cr) + fv]) rotate([0, -top_ang, 0]) cylinder(d = pilot, h = t + 3, $fn = 24); // screws up into the ring, either side of the joint for (s = [-1, 1]) translate([ring_screw_r, s * ring_screw_y, bot_z - pad_t - 1]) cylinder(d = clear, h = pad_t + 2, $fn = 24); } } // ---------------------------------------------------------------- preview module board_and_surround() { color("tan") translate([0, 0, -38]) cylinder(r = R, h = 38, $fn = 180); color("firebrick") translate([0, 0, surround_z - 20]) difference() { cylinder(r = surround_d / 2, h = 20, $fn = 180); translate([0, 0, -1]) cylinder(r = R, h = 22, $fn = 180); } } // the assembled ring; the colours are for previews and renders only (rig_render.scad sets its own) module ring(c_seg = ["white", "gainsboro"], c_key = "steelblue", c_bracket = "orange", c_strip = "yellow") { // the lead's section at the bottom, so it runs down with the camera cables for (i = [0 : segs - 1]) color(c_seg[i % 2]) segment(arms[0] + i * seg_a, arms[0] + i * seg_a == 180); for (i = [0 : segs - 1]) color(c_key) at(arms[0] + i * seg_a) translate([back_r - key_d + 0.1, -key_l + key_fit, key_z - key_h / 2 + key_fit / 2]) key(); for (b = arms) color(c_bracket) at(b) bracket(); color(c_strip) rotate_extrude($fn = 180) polygon([S - strip_w / 2 * up, S + strip_w / 2 * up, S + strip_w / 2 * up + 0.4 * nrm, S - strip_w / 2 * up + 0.4 * nrm]); } module preview() { board_and_surround(); ring(); for (b = arms) color("dimgray") at(b, R) import("camera_mount.stl"); } // Cross-section through an arm: ring, bracket, arm and camera box, the camera's sight line to // the board edge, and the light's cut-off ray module section() { color("white") ring_2d(); color("orange", 0.8) { clamp_2d(); strut_2d(); } // the arm, from its foot to the camera box color("gray") polygon([[R + arm_p0[0], arm_p0[1]], [R + arm_p1[0], arm_p1[1]], [R + arm_q1[0], arm_q1[1]], [R + arm_q0[0], arm_q0[1]]]); color("dimgray") polygon([box_pt(0, -arm_w / 2), box_pt(box_d, -arm_w / 2), box_pt(box_d, arm_w / 2), box_pt(0, arm_w / 2)]); color("red") translate([R, 0]) rotate(atan2(lens[1], lens[0])) square([norm(lens), 0.8]); color("gold") translate(B) rotate(180 - top_ray) square([500, 0.8]); color("tan") translate([0, -38]) square([R, 38]); color("firebrick") translate([R, surround_z - 20]) square([surround_d / 2 - R, 20]); } // ---------------------------------------------------------------- print orientations if (part == "preview") preview(); else if (part == "section") section(); else if (part == "segment" || part == "segment_wire") // upside down, top face on the bed, centred on the origin translate([-(V[0] + back_r) / 2, 0, top_z]) rotate([180, 0, 0]) rotate([0, 0, seg_a / 2 - 90]) segment(0, part == "segment_wire"); else if (part == "key") rotate([0, -90, 0]) key(); else if (part == "bracket") // closed side (the strut) on the bed translate([-(cr + strut_end) / 2, 0, AW / 2 + strut_t + 0.6]) rotate([90, 0, 0]) bracket();