""" IMU Pointer Enclosure — v8 ============================ Changes from v7: 1. SNAP-LOCK JOINT: Removed the flex notches that were cutting through the tongue (they were weakening it without providing any positive lock). Replaced with a snap ridge system: a small rectangular bump is added to the OUTER face of the tongue on each long side at mid-tongue height. The top-shell groove gets a matching recess (undercut) so the ridge clicks in when the top is pushed down. To release, flex the shell sides slightly outward. 2. LARGER BUTTON APERTURE: BTN_HOLE_R increased from 8 mm → 10 mm (⌀20 mm contact face). Cap shaft/rim scale accordingly. Makes it much harder to miss the button. 3. FOUR SCREW POSTS FOR BUTTON DAUGHTERBOARD: Previous design had 2 posts only in Y. Replaced with 4 posts in a 2×2 grid centred under the button. Pin pitch 2 mm c-to-c → posts at (BTN_X ± 1 mm, BTN_CY ± 1 mm). Screw hole ⌀0.9 mm (just under 1 mm), post OD 4 mm for adequate wall thickness around the hole. Split joint design: - Bottom shell TONGUE projects UP from SPLIT_Z (thin rectangular frame). - SNAP RIDGE: small box bump on the outer long-side faces of the tongue, at ~mid tongue height. Ridge stands proud of the tongue outer face by RIDGE_PROUD (0.4 mm), so the groove wall deflects it inward during insertion and snaps into RIDGE_RECESS cut into the groove wall. - Top shell GROOVE cut into the inside of its lower edge. - RIDGE RECESS: matching pocket cut into the outer face of the groove to receive the snap ridge. """ import FreeCAD as App import FreeCADGui as Gui import Part import math from FreeCAD import Base doc = App.newDocument("pointer_v8") # ───────────────────────────────────────────────────────────────────────────── # DIMENSIONS # ───────────────────────────────────────────────────────────────────────────── L = 115.0 # length (X): front=0, back=L W = 36.0 # width (Y) H = 22.0 # height (Z): bottom=0, top=H WALL = 4.5 # wall thickness CR = 5.0 # corner fillet radius (vertical edges) TOL = 0.25 # fit tolerance # Taper: removed per user request TAPER_RISE = 0.0 TAPER_LEN = 100.0 # unused but kept to avoid NameError # Split plane USBC_W = 11.0 USBC_H = 7.0 USBC_Z = 5.0 SPLIT_Z = USBC_Z + USBC_H + 2.5 # = 14.5 mm # Tongue/groove clip joint TONGUE_H = 2.5 # how far tongue projects above SPLIT_Z TONGUE_T = 1.2 # tongue wall thickness GROOVE_H = TONGUE_H + TOL GROOVE_T = TONGUE_T + TOL # Snap ridge on tongue long sides # Ridge sits on the OUTER face of the tongue (the face against the groove wall) # at mid-tongue height. The groove wall has a matching recess. RIDGE_W = 12.0 # length of ridge along X (centred on enclosure) RIDGE_H = 0.8 # height of the ridge bump RIDGE_PROUD = 0.4 # how far ridge protrudes beyond tongue outer face RIDGE_Z_OFF = (TONGUE_H - RIDGE_H) / 2.0 # centres ridge vertically in tongue # ───────────────────────────────────────────────────────────────────────────── # IMU BOARD # ───────────────────────────────────────────────────────────────────────────── PCB_T = 1.0 BRD_L = 21.0 BRD_W = 17.5 BRD_X = WALL BRD_Y = (W - BRD_W) / 2.0 PLATFORM_H = 0.5 BRD_Z = WALL + PLATFORM_H ARM_LEN = 5.0 ARM_THICK = 1.6 ARM_H = BRD_Z + PCB_T + 0.8 CLIP_TOL = 0.35 # ───────────────────────────────────────────────────────────────────────────── # BATTERY BAY # ───────────────────────────────────────────────────────────────────────────── BAT_L = 50.0 BAT_W = 12.0 BAT_H = 12.0 BAT_X = BRD_X + BRD_L + 8.0 BAT_Y = (W - BAT_W) / 2.0 BAT_CLIP_Y = 8.0 # ───────────────────────────────────────────────────────────────────────────── # BUTTON — enlarged aperture for easier contact # ───────────────────────────────────────────────────────────────────────────── BTN_X = 28.0 BTN_CY = W / 2.0 BTN_HOLE_R = 10.0 # ⌀20 mm — up from ⌀16 mm for easier press CAP_SHAFT_R = BTN_HOLE_R - 0.4 # 0.4 mm radial clearance CAP_SHAFT_H = WALL # shaft fills top wall → top face flush CAP_RIM_R = BTN_HOLE_R + 2.0 # 2 mm wider than hole → retention CAP_RIM_H = 1.5 NUBBIN_R = 2.2 # slightly larger nubbin to match bigger cap NUBBIN_H = 2.0 # Switch geometry (adjust to match your Omron) SWITCH_BODY_H = 5.0 STEM_H = 2.5 GAP = 0.5 # PCB position derived top-down PCB_TOP_Z = H - WALL - CAP_RIM_H - NUBBIN_H - GAP - SWITCH_BODY_H - STEM_H PCB_BOT_Z = PCB_TOP_Z - PCB_T # Clamp: must be inside the top shell cavity floor_top_shell = SPLIT_Z + WALL if PCB_BOT_Z < floor_top_shell + 0.5: PCB_BOT_Z = floor_top_shell + 0.5 PCB_TOP_Z = PCB_BOT_Z + PCB_T POST_H = PCB_BOT_Z - floor_top_shell # ── Screw posts ────────────────────────────────────────────────────────────── # 4 posts in a 2×2 grid centred under the button. # Pin pitch 2 mm c-to-c → offsets ±1 mm in both X and Y from button centre. POST_OD = 4.0; POST_R = POST_OD / 2.0 POST_ID = 0.9; POST_IR = POST_ID / 2.0 # just under ⌀1 mm POST_PITCH = 2.0 # 2 mm centre-to-centre POST_OFFS = POST_PITCH / 2.0 # ±1 mm from centre BPCB_L = 16.0 BPCB_W = 16.0 SHELF_WALL = 2.0 # ───────────────────────────────────────────────────────────────────────────── # HELPERS # ───────────────────────────────────────────────────────────────────────────── def box(lx, ly, lz, ox=0.0, oy=0.0, oz=0.0): return Part.makeBox(lx, ly, lz, Base.Vector(ox, oy, oz)) def cyl(r, h, cx=0.0, cy=0.0, cz=0.0): return Part.makeCylinder(r, h, Base.Vector(cx, cy, cz)) def rounded_slot(depth, sw, sh, ox, oy, oz): """Stadium slot extruded in +X.""" r = min(sh / 2.0, sw / 2.0) cy = oy + sw / 2.0 cz = oz + sh / 2.0 hw = max(sw / 2.0 - r, 0.0) if hw < 1e-6: circ = Part.makeCircle(r, Base.Vector(ox, cy, cz), Base.Vector(1, 0, 0)) return Part.Face(Part.Wire(circ)).extrude(Base.Vector(depth, 0, 0)) l_s = Base.Vector(ox, cy - hw, cz - r) l_m = Base.Vector(ox, cy - hw - r, cz) l_e = Base.Vector(ox, cy - hw, cz + r) r_s = Base.Vector(ox, cy + hw, cz + r) r_m = Base.Vector(ox, cy + hw + r, cz) r_e = Base.Vector(ox, cy + hw, cz - r) wire = Part.Wire([Part.Arc(l_s, l_m, l_e).toShape(), Part.makeLine(l_e, r_s), Part.Arc(r_s, r_m, r_e).toShape(), Part.makeLine(r_e, l_s)]) return Part.Face(wire).extrude(Base.Vector(depth, 0, 0)) def fillet_vert(solid, r, min_len=4.0): """Fillet edges that are primarily vertical (parallel to Z).""" try: edges = [e for e in solid.Edges if len(e.Vertexes) == 2 and abs(e.Vertexes[0].X - e.Vertexes[1].X) < 1e-3 and abs(e.Vertexes[0].Y - e.Vertexes[1].Y) < 1e-3 and e.Length >= min_len] if edges: return solid.makeFillet(r, edges) except Exception as e: print(f" fillet_vert skipped: {e}") return solid def make_clip(cx, cy, ix, iy): plat_w = ARM_THICK + CLIP_TOL plat_x = cx if ix > 0 else cx - plat_w plat_y = cy if iy > 0 else cy - plat_w plat = box(plat_w, plat_w, PLATFORM_H + PCB_T, plat_x, plat_y, WALL) ax_ox = cx if ix > 0 else cx - ARM_LEN ax_oy = cy - ARM_THICK - CLIP_TOL if iy > 0 else cy + CLIP_TOL arm_x = box(ARM_LEN, ARM_THICK, ARM_H, ax_ox, ax_oy, WALL) ay_oy = cy if iy > 0 else cy - ARM_LEN ay_ox = cx - ARM_THICK - CLIP_TOL if ix > 0 else cx + CLIP_TOL arm_y = box(ARM_THICK, ARM_LEN, ARM_H, ay_ox, ay_oy, WALL) cb_w = ARM_THICK + CLIP_TOL cb_ox = cx - cb_w if ix > 0 else cx cb_oy = cy - cb_w if iy > 0 else cy cb = box(cb_w, cb_w, ARM_H, cb_ox, cb_oy, WALL) return plat.fuse(arm_x.fuse(arm_y).fuse(cb)) # ═════════════════════════════════════════════════════════════════════════════ # BOTTOM SHELL (Z = 0 → SPLIT_Z, open on top) # ═════════════════════════════════════════════════════════════════════════════ # 1. Outer solid — built ONLY to SPLIT_Z height bot_outer = box(L, W, SPLIT_Z) bot_outer = fillet_vert(bot_outer, CR, min_len=SPLIT_Z * 0.4) # Fillet the bottom long horizontal edges (comfort grip) EDGE_FILLET = 2.5 try: h_edges = [] for e in bot_outer.Edges: if len(e.Vertexes) != 2: continue v0, v1 = e.Vertexes[0], e.Vertexes[1] dx = abs(v0.X - v1.X) dz = abs(v0.Z - v1.Z) dy = abs(v0.Y - v1.Y) z_mid = (v0.Z + v1.Z) / 2.0 if dx > L * 0.5 and dz < 0.5 and dy < 0.5 and z_mid < 1.0: h_edges.append(e) if h_edges: bot_outer = bot_outer.makeFillet(EDGE_FILLET, h_edges) print(f"Bottom shell: filleted {len(h_edges)} horizontal edge(s) R={EDGE_FILLET}") else: print("Bottom shell: no horizontal edges found — skipped") except Exception as exc: print(f"Bottom shell horizontal fillet skipped: {exc}") # 3. Inner cavity bot_cav_lx = L - WALL * 2 bot_cav_ly = W - WALL * 2 bot_cav_lz = SPLIT_Z - WALL bot_inner = box(bot_cav_lx, bot_cav_ly, bot_cav_lz, WALL, WALL, WALL) bot_shell = bot_outer.cut(bot_inner) # 4. Tongue (projects UP from SPLIT_Z, inner perimeter frame) t_slab = box(bot_cav_lx, bot_cav_ly, TONGUE_H, WALL, WALL, SPLIT_Z) t_cut = box(bot_cav_lx - TONGUE_T*2, bot_cav_ly - TONGUE_T*2, TONGUE_H + 1, WALL + TONGUE_T, WALL + TONGUE_T, SPLIT_Z - 0.5) tongue = t_slab.cut(t_cut) # 5. Snap ridges on the tongue outer long-side faces # The tongue outer face sits at Y=WALL and Y=W-WALL (bottom shell outer wall # inner face). We add a small proud bump to each long side (the Y faces). # # Long sides run in X. The tongue outer face in -Y direction is at Y=WALL, # outer face in +Y direction is at Y=W-WALL. # # Ridge box for -Y side: # X: centred on L/2, width RIDGE_W # Y: from (WALL - RIDGE_PROUD) to WALL (proud outward = -Y direction) # Z: from (SPLIT_Z + RIDGE_Z_OFF) height RIDGE_H # # Ridge box for +Y side: # Y: from (W - WALL) to (W - WALL + RIDGE_PROUD) # ridge_x0 = L / 2.0 - RIDGE_W / 2.0 ridge_z0 = SPLIT_Z + RIDGE_Z_OFF ridge_neg_y = box(RIDGE_W, RIDGE_PROUD, RIDGE_H, ridge_x0, WALL - RIDGE_PROUD, ridge_z0) ridge_pos_y = box(RIDGE_W, RIDGE_PROUD, RIDGE_H, ridge_x0, W - WALL, ridge_z0) tongue = tongue.fuse(ridge_neg_y).fuse(ridge_pos_y) bot_shell = bot_shell.fuse(tongue) # 6. IMU clips for cx, cy, ix, iy in [ (BRD_X, BRD_Y, +1, +1), (BRD_X + BRD_L, BRD_Y, -1, +1), (BRD_X, BRD_Y + BRD_W, +1, -1), (BRD_X + BRD_L, BRD_Y + BRD_W, -1, -1), ]: bot_shell = bot_shell.fuse(make_clip(cx, cy, ix, iy)) # 7. USB-C slot bot_shell = bot_shell.cut( rounded_slot(WALL * 6, USBC_W, USBC_H, -WALL * 3, W / 2.0 - USBC_W / 2.0, USBC_Z)) # 8. Battery bay bot_shell = bot_shell.cut(box(BAT_L, BAT_W, 3.0, BAT_X, BAT_Y, WALL)) cy0 = BAT_Y + BAT_W / 2.0 - BAT_CLIP_Y / 2.0 bot_shell = bot_shell.fuse(box(2.0, BAT_CLIP_Y, BAT_H * 0.55, BAT_X - 2.0, cy0, WALL)) bot_shell = bot_shell.fuse(box(2.0, BAT_CLIP_Y, BAT_H * 0.55, BAT_X + BAT_L, cy0, WALL)) # ═════════════════════════════════════════════════════════════════════════════ # TOP SHELL (Z = SPLIT_Z → H, open on bottom, closed ceiling at H) # ═════════════════════════════════════════════════════════════════════════════ top_h = H - SPLIT_Z # = 7.5 mm # 1. Outer solid spans SPLIT_Z → H top_outer = box(L, W, top_h, 0, 0, SPLIT_Z) top_outer = fillet_vert(top_outer, CR, min_len=top_h * 0.4) # Fillet the top long horizontal edges try: th_edges = [] for e in top_outer.Edges: if len(e.Vertexes) != 2: continue v0, v1 = e.Vertexes[0], e.Vertexes[1] dx = abs(v0.X - v1.X) dz = abs(v0.Z - v1.Z) dy = abs(v0.Y - v1.Y) z_mid = (v0.Z + v1.Z) / 2.0 if dx > L * 0.5 and dz < 0.5 and dy < 0.5 and z_mid > H - 1.0: th_edges.append(e) if th_edges: top_outer = top_outer.makeFillet(EDGE_FILLET, th_edges) print(f"Top shell: filleted {len(th_edges)} horizontal edge(s) R={EDGE_FILLET}") else: print("Top shell: no horizontal edges found — skipped") except Exception as exc: print(f"Top shell horizontal fillet skipped: {exc}") # 2. Inner cavity top_cav_lx = L - WALL * 2 top_cav_ly = W - WALL * 2 top_cav_lz = top_h - WALL top_inner = box(top_cav_lx, top_cav_ly, top_cav_lz, WALL, WALL, SPLIT_Z) top_shell = top_outer.cut(top_inner) # 3. Groove at the bottom of the top shell g_slab = box(top_cav_lx, top_cav_ly, GROOVE_H, WALL, WALL, SPLIT_Z) g_cut = box(top_cav_lx - GROOVE_T*2, top_cav_ly - GROOVE_T*2, GROOVE_H + 1, WALL + GROOVE_T, WALL + GROOVE_T, SPLIT_Z - 0.5) groove = g_slab.cut(g_cut) top_shell = top_shell.cut(groove) # 4. Ridge recess in the groove wall — matches the snap ridges on the tongue. # The groove outer wall in -Y direction is the inner face of the top shell # wall at Y=WALL. The recess is cut OUTWARD from the groove wall face, # i.e. the wall material between groove and outside. # # Recess for -Y groove wall face (inner face sits at Y = WALL + GROOVE_T): # We need to cut from the groove outer face inward. # The groove outer face (toward -Y) is at Y = WALL. # Recess: box RIDGE_W wide in X, RIDGE_PROUD + TOL deep in Y (into wall), # RIDGE_H + TOL tall, at ridge_z0. # # Note: the groove wall is only GROOVE_T = 1.45 mm wide; cutting RIDGE_PROUD # (0.4 mm) + TOL (0.25 mm) = 0.65 mm is well within that. # recess_depth = RIDGE_PROUD + TOL recess_h = RIDGE_H + TOL recess_z0 = ridge_z0 - TOL / 2.0 # slight vertical oversize for tolerance recess_neg_y = box(RIDGE_W, recess_depth, recess_h, ridge_x0, WALL - recess_depth, recess_z0) recess_pos_y = box(RIDGE_W, recess_depth, recess_h, ridge_x0, W - WALL, recess_z0) top_shell = top_shell.cut(recess_neg_y).cut(recess_pos_y) # 5. Button aperture — brute-force punch through ceiling top_shell = top_shell.cut( cyl(BTN_HOLE_R, H - SPLIT_Z + 2, BTN_X, BTN_CY, SPLIT_Z)) # 6. Button PCB shelf frame shelf_ox = BTN_X - BPCB_L / 2.0 shelf_oy = BTN_CY - BPCB_W / 2.0 shelf_h = 1.5 shelf_slab = box(BPCB_L + SHELF_WALL*2, BPCB_W + SHELF_WALL*2, shelf_h, shelf_ox - SHELF_WALL, shelf_oy - SHELF_WALL, PCB_BOT_Z - shelf_h) shelf_hole = box(BPCB_L, BPCB_W, shelf_h + 2.0, shelf_ox, shelf_oy, PCB_BOT_Z - shelf_h - 1.0) shelf = shelf_slab.cut(shelf_hole) if floor_top_shell < PCB_BOT_Z < H - WALL: top_shell = top_shell.fuse(shelf) # 7. Four screw posts (2×2 grid) for button daughterboard # Grid centred at (BTN_X, BTN_CY), 2 mm pitch → offsets ±POST_OFFS if POST_H > 0.5: for px_off in [-POST_OFFS, +POST_OFFS]: for py_off in [-POST_OFFS, +POST_OFFS]: px = BTN_X + px_off py = BTN_CY + py_off p = cyl(POST_R, POST_H, px, py, floor_top_shell) ph = cyl(POST_IR, POST_H + 1.0, px, py, floor_top_shell) top_shell = top_shell.fuse(p) top_shell = top_shell.cut(ph) # ═════════════════════════════════════════════════════════════════════════════ # BUTTON CAP (separate printed part) # # Geometry at origin: # Shaft: Z = 0 (bottom/inner) → Z = CAP_SHAFT_H = WALL (top/flush) # Rim: Z = -CAP_RIM_H → Z = 0 (hangs inside cavity) # Nubbin: Z = -CAP_RIM_H-NUBBIN_H → Z = -CAP_RIM_H # # Placed so shaft top = H → flush with top face. # ═════════════════════════════════════════════════════════════════════════════ cap_shaft = cyl(CAP_SHAFT_R, CAP_SHAFT_H) cap_rim = cyl(CAP_RIM_R, CAP_RIM_H, 0, 0, -CAP_RIM_H) cap_nub = cyl(NUBBIN_R, NUBBIN_H, 0, 0, -CAP_RIM_H - NUBBIN_H) cap_raw = cap_shaft.fuse(cap_rim).fuse(cap_nub) cap_placed = cap_raw.copy() cap_placed.translate(Base.Vector(BTN_X, BTN_CY, H - CAP_SHAFT_H)) # ═════════════════════════════════════════════════════════════════════════════ # REGISTER OBJECTS # ═════════════════════════════════════════════════════════════════════════════ bot_obj = doc.addObject("Part::Feature", "Shell_Bottom") bot_obj.Shape = bot_shell bot_obj.ViewObject.ShapeColor = (0.12, 0.12, 0.14) bot_obj.ViewObject.Transparency = 0 top_obj = doc.addObject("Part::Feature", "Shell_Top") top_obj.Shape = top_shell top_obj.ViewObject.ShapeColor = (0.20, 0.20, 0.26) top_obj.ViewObject.Transparency = 0 cap_obj = doc.addObject("Part::Feature", "Button_Cap") cap_obj.Shape = cap_placed cap_obj.ViewObject.ShapeColor = (0.80, 0.80, 0.86) cap_obj.ViewObject.Transparency = 0 doc.recompute() Gui.activeDocument().activeView().viewIsometric() Gui.SendMsgToActiveView("ViewFit") # ═════════════════════════════════════════════════════════════════════════════ # SUMMARY # ═════════════════════════════════════════════════════════════════════════════ print("=" * 62) print("IMU Pointer v8") print("=" * 62) print(f"Body: {L:.0f} × {W:.0f} mm") print(f"Height: {H:.0f} mm uniform (no taper)") print(f"Wall: {WALL:.1f} mm CR = {CR:.1f} mm Edge fillet = {EDGE_FILLET:.1f} mm") print(f"Split Z: {SPLIT_Z:.1f} mm " f"(USB-C top = {USBC_Z + USBC_H:.1f} mm)") print(f"Top shell interior height: {top_cav_lz:.1f} mm (Z {SPLIT_Z:.1f} → {H - WALL:.1f})") print() print(f"Tongue H/T: {TONGUE_H:.1f} / {TONGUE_T:.1f} mm") print(f"Groove H/T: {GROOVE_H:.2f} / {GROOVE_T:.2f} mm") print(f"Snap ridge: {RIDGE_W:.0f} mm long × {RIDGE_H:.1f} mm tall × {RIDGE_PROUD:.1f} mm proud") print(f"Ridge recess: {RIDGE_W:.0f} mm long × {recess_h:.2f} mm tall × {recess_depth:.2f} mm deep") print() print(f"Button hole: ⌀{BTN_HOLE_R*2:.0f} mm X={BTN_X} Y={BTN_CY:.0f} (was ⌀16 mm)") print(f"Cap shaft: ⌀{CAP_SHAFT_R*2:.1f} mm × {CAP_SHAFT_H:.1f} mm (flush, Z {H-WALL:.1f}→{H:.1f})") print(f"Cap rim: ⌀{CAP_RIM_R*2:.0f} mm × {CAP_RIM_H:.1f} mm (retention)") print() print(f"PCB top Z: {PCB_TOP_Z:.2f} mm (above split floor {floor_top_shell:.1f} mm)") print(f"PCB bot Z: {PCB_BOT_Z:.2f} mm") print(f"Post H: {POST_H:.2f} mm ⌀{POST_OD:.0f}/{POST_ID:.1f} mm pitch={POST_PITCH:.0f} mm") print(f"Posts (4×): (BTN_X±{POST_OFFS:.0f}, BTN_CY±{POST_OFFS:.0f})") print() print(f"Switch stack: body={SWITCH_BODY_H} + stem={STEM_H} + gap={GAP} mm") print(" Adjust SWITCH_BODY_H / STEM_H if your Omron differs.") print("=" * 62)