""" IMU Pointer Enclosure — v6 ============================ Fixes from v5 (diagnosed from rendered images): 1. BOTTOM HOLE: v5 built the bottom shell from a full-height box (0→H) then trimmed at SPLIT_Z. The taper wedge interacted badly with the split cut near X=0 leaving a hole in the floor. Fix: build bottom outer solid only to SPLIT_Z height — no trimming step needed, no interaction. 2. TOP SHELL WALLS PROTRUDING BELOW SPLIT: v5 top_outer started at SPLIT_Z but the cavity inside started at SPLIT_Z + WALL, leaving WALL=3.5mm of solid wall below the groove — visually protruding past the bottom shell. Fix: the top shell outer solid starts at SPLIT_Z. The groove is cut starting exactly at SPLIT_Z so there is zero protrusion below the split line. 3. BUTTON CAP NOT FLUSH / SITTING PROUD: Aperture cylinder had arithmetic-derived Z that could miss the top wall faces after filleting. Fix: aperture runs from Z=SPLIT_Z (well inside the cavity) all the way to Z=H+2 — brute-force punch, impossible to miss. 4. CAP RIM SITS UNDER OUTER FACE (not above it): Cap is placed so shaft top = H (flush). Rim hangs BELOW the top face inside the cavity — correct retention geometry. The cap shaft top is exactly flush with H. No part of the cap protrudes above H. Split joint design: - Bottom shell has a TONGUE that projects UP from SPLIT_Z. The tongue is a thin rectangular frame (inner perimeter of the walls). - Top shell has a matching GROOVE cut into the inside of its lower edge, starting exactly at SPLIT_Z (the bottom face of the top shell). - Two flex notches cut through the tongue on the long sides allow snap fit. """ import FreeCAD as App import FreeCADGui as Gui import Part import math from FreeCAD import Base doc = App.newDocument("pointer_v7") # ───────────────────────────────────────────────────────────────────────────── # 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 (+1 mm vs v6 — closes taper floor gap) CR = 5.0 # corner fillet radius (vertical edges) TOL = 0.25 # fit tolerance # Taper: front of bottom shell is TAPER_RISE mm shorter than back. # Applied only to the BOTTOM shell (it's where the ergonomic taper lives). TAPER_RISE = 0.0 # no taper — removed per user request 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 # Flex notch through tongue (for snap release) NOTCH_W = 8.0 NOTCH_H = TONGUE_H + 0.5 # ───────────────────────────────────────────────────────────────────────────── # 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 # ───────────────────────────────────────────────────────────────────────────── BTN_X = 28.0 BTN_CY = W / 2.0 BTN_HOLE_R = 8.0 CAP_SHAFT_R = BTN_HOLE_R - 0.4 # 0.4 mm radial clearance in hole 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 = 1.8 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 POST_OD = 4.0; POST_R = POST_OD / 2.0 POST_ID = 1.9; POST_IR = POST_ID / 2.0 POST_SEP = 3.0 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 long horizontal edges the user holds. # These are the 4 edges running in X at Z≈0 and Z≈SPLIT_Z, on both long sides. # Same fillet applied to equivalent edges on the top shell later. EDGE_FILLET = 2.5 # mm — soft and comfortable, visible but not decorative 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) # Long edge in X, horizontal, on a long side face — # but ONLY at Z≈0 (bottom face). Exclude Z≈SPLIT_Z (the join edge). 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}") # No taper wedge — removed per user request # 3. Inner cavity — floor at WALL, ceiling at SPLIT_Z (open top, no ceiling) bot_cav_lx = L - WALL * 2 bot_cav_ly = W - WALL * 2 bot_cav_lz = SPLIT_Z - WALL # floor(WALL) → SPLIT_Z 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) # Outer edge of tongue = inner face of outer wall = WALL from outside # Inner edge of tongue = WALL + TONGUE_T from outside 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) # Flex notches on the two long sides (parallel to X) nx0 = L / 2.0 - NOTCH_W / 2.0 for ny_start in [WALL, W - WALL - TONGUE_T]: tongue = tongue.cut( box(NOTCH_W, TONGUE_T + 0.5, NOTCH_H, nx0, ny_start - 0.1, SPLIT_Z - 0.1)) bot_shell = bot_shell.fuse(tongue) # 5. 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)) # 6. USB-C slot — starts at X = -WALL*3 so it punches through the rounded # front face cleanly regardless of fillet radius bot_shell = bot_shell.cut( rounded_slot(WALL * 6, USBC_W, USBC_H, -WALL * 3, W / 2.0 - USBC_W / 2.0, USBC_Z)) # 7. 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 long horizontal edges of the top shell — # the top edges (Z≈H) are the ones selected in blue in the user's screenshot. 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) # Long edge in X, horizontal, on a long side face — # ONLY at Z≈H (top face). Exclude Z≈SPLIT_Z (the join edge). 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: side walls WALL thick, CEILING at H-WALL (WALL-thick roof), # FLOOR open (starts at SPLIT_Z — nothing blocks the bottom opening). # Cavity box: X from WALL→L-WALL, Y from WALL→W-WALL, Z from SPLIT_Z→H-WALL top_cav_lx = L - WALL * 2 top_cav_ly = W - WALL * 2 top_cav_lz = top_h - WALL # = 7.5 - 3.5 = 4.0 mm interior height top_inner = box(top_cav_lx, top_cav_ly, top_cav_lz, WALL, WALL, SPLIT_Z) # starts exactly at SPLIT_Z top_shell = top_outer.cut(top_inner) # 3. Groove at the bottom of the top shell, starting at SPLIT_Z # The groove is a frame-shaped recess cut into the inner face of the walls. # It goes from Z=SPLIT_Z up to Z=SPLIT_Z+GROOVE_H. # Width = GROOVE_T (slightly wider than tongue). 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. Button aperture — brute-force: run cylinder from Z=SPLIT_Z to Z=H+2. # It will punch through the ceiling regardless of any topology. top_shell = top_shell.cut( cyl(BTN_HOLE_R, H - SPLIT_Z + 2, BTN_X, BTN_CY, SPLIT_Z)) # 5. 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) # 6. Screw posts on top-shell floor if POST_H > 0.5: for py in [BTN_CY - POST_SEP/2.0, BTN_CY + POST_SEP/2.0]: p = cyl(POST_R, POST_H, BTN_X, py, floor_top_shell) ph = cyl(POST_IR, POST_H + 1.0, BTN_X, 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. # Rim is entirely inside the cavity. No part protrudes above H. # ═════════════════════════════════════════════════════════════════════════════ 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) # No fillet on cap top rim — cap sits flush inside aperture so a fillet # would create a visible chamfer ring against the hole edge. 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 v7") 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() print(f"Button hole: ⌀{BTN_HOLE_R*2:.0f} mm X={BTN_X} Y={BTN_CY:.0f}") 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, below top face)") 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 sep={POST_SEP:.0f} mm c-to-c") 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)