From 5ab13a525aedb0fc441a89debf8743259d016efc Mon Sep 17 00:00:00 2001 From: Nik Rozman Date: Tue, 24 Mar 2026 19:29:40 +0100 Subject: [PATCH] Redesign MCU and PCB mounting --- model/pointer.FCMacro | 574 +++++++++++------------------------------- 1 file changed, 150 insertions(+), 424 deletions(-) diff --git a/model/pointer.FCMacro b/model/pointer.FCMacro index 9211f14..c310bc1 100644 --- a/model/pointer.FCMacro +++ b/model/pointer.FCMacro @@ -1,464 +1,190 @@ """ -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. +IMU Pointer Enclosure — v11.7 (Slimmed Corners & Rounded USB-C) """ import FreeCAD as App import FreeCADGui as Gui import Part -import math from FreeCAD import Base -doc = App.newDocument("pointer_v8") +doc = App.newDocument("pointer_v11_7") -# ───────────────────────────────────────────────────────────────────────────── -# 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 +# ─── DIMENSIONS ─────────────────────────────────────────────────────────────── +L, W, H = 115.0, 36.0, 22.0 +WALL = 3.5 +CR, CR_I = 8.0, 4.5 +TOL = 0.25 +EDGE_FILLET = 3.0 -# Taper: removed per user request -TAPER_RISE = 0.0 -TAPER_LEN = 100.0 # unused but kept to avoid NameError +USBC_W, USBC_H, USBC_Z = 12.0, 7.0, 5.0 +SPLIT_Z = USBC_Z + USBC_H + 2.5 -# 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 +# MICRO-DETENT Snap Logic +TONGUE_H, TONGUE_T = 2.5, 2.0 +GROOVE_H, GROOVE_T = TONGUE_H + TOL, TONGUE_T + TOL +RIDGE_W = 12.0 +RIDGE_H = 1.2 +RIDGE_PROUD = 1.0 # Snap ridge protrusion +RIDGE_Z_OFF = (TONGUE_H - RIDGE_H) / 2.0 -# 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 +# ─── IMU BOARD (+1mm Spacing & Slim Corners) ────────────────────────────────── +PCB_T, BRD_L, BRD_W = 3.0, 22.6, 19.6 +BRD_X, BRD_Y = WALL, (W - BRD_W) / 2.0 +PLATFORM_H, MIC_EXTRA = 1.5, 2.0 +MIC_PCB_T = 2.5 # Thicker PCB section (MEMS mic), rounded up from 2.2 +BUMP_PROUD = 0.3 # Press-fit nub protrusion into board cavity +BUMP_R = 0.6 # Nub radius (half-sphere) +BRD_Z = WALL + PLATFORM_H -# 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 +# ─── BUTTON & BATTERY ───────────────────────────────────────────────────────── +BAT_L, BAT_W, BAT_H = 50.0, 12.0, 12.0 +BAT_X, BAT_Y = BRD_X + BRD_L + 8.0 + 5.0, (W - BAT_W) / 2.0 +BAT_CLIP_Y = 8.0 +BTN_X, BTN_CY, BTN_HOLE_R = 28.0, W / 2.0, 10.0 +CAP_SHAFT_R, CAP_SHAFT_H = 9.6, WALL +CAP_RIM_R, CAP_RIM_H = 12.0, 1.5 +NUBBIN_R, NUBBIN_H = 4.2, 1.0 +BTN_DOME_R, BTN_DOME_SAG = 14.0, 0.6 -# ───────────────────────────────────────────────────────────────────────────── -# 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 +PCB_BOT_Z = SPLIT_Z + 1.5 +POST_H = BRD_Z + PCB_T + MIC_EXTRA + 3.0 - 4.0 # Lowered 4mm for button PCB + button thickness +POST_OFFS_X, POST_OFFS_Y = 4.0, 11.0 +LH_R, LH_X, LH_Y_OFFS = 1.5, L - WALL - 3.0, 4.0 +BPCB_L, BPCB_W = 16.0, 16.0 -# ───────────────────────────────────────────────────────────────────────────── -# 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 +# ─── HELPERS ────────────────────────────────────────────────────────────────── -# ───────────────────────────────────────────────────────────────────────────── -# 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): +def box(lx, ly, lz, ox=0, oy=0, oz=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): +def rbox(lx, ly, lz, ox=0, oy=0, oz=0, r=CR): + b = box(lx, ly, lz, ox, oy, oz) + try: + edges = [e for e in b.Edges if abs(e.Vertexes[0].X - e.Vertexes[1].X) < 1e-3 and abs(e.Vertexes[0].Y - e.Vertexes[1].Y) < 1e-3] + return b.makeFillet(r, edges) if edges else b + except: return b + +def cyl(r, h, cx=0, cy=0, cz=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).""" +def fillet_horiz(solid, r, z_test): 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 + edges = [e for e in solid.Edges if abs(e.Vertexes[0].Z - e.Vertexes[1].Z) < 0.2 and abs((e.Vertexes[0].Z + e.Vertexes[1].Z)/2 - z_test) < 1.5] + return solid.makeFillet(r, edges) if edges else solid + except: 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)) +def make_slim_corner(cx, cy, ix, iy): + pw = 0.8 # Much slimmer wall thickness (was 1.5/1.6) + sl = 4.0 # Slightly shorter side length + h = PLATFORM_H + PCB_T + 0.5 + + x0, y0 = (cx if ix>0 else cx-sl), (cy if iy>0 else cy-pw) + w1 = box(sl, pw, h, x0, y0, WALL) + + x1, y1 = (cx if ix>0 else cx-pw), (cy if iy>0 else cy-sl) + w2 = box(pw, sl, h, x1, y1, WALL) + + px, py = (cx if ix>0 else cx-sl), (cy if iy>0 else cy-sl) + plat = box(sl, sl, PLATFORM_H, px, py, WALL) + + return plat.fuse(w1).fuse(w2) # ═════════════════════════════════════════════════════════════════════════════ -# BOTTOM SHELL (Z = 0 → SPLIT_Z, open on top) +# CONSTRUCTION # ═════════════════════════════════════════════════════════════════════════════ -# 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) +# BOTTOM SHELL +bot_shell = fillet_horiz(rbox(L, W, SPLIT_Z + TONGUE_H), EDGE_FILLET, 0.0) +bot_shell = bot_shell.cut(rbox(L-WALL*2, W-WALL*2, SPLIT_Z, WALL, WALL, WALL, r=CR_I)) +bot_shell = bot_shell.cut(rbox(L-TONGUE_T*2, W-TONGUE_T*2, TONGUE_H+2, TONGUE_T, TONGUE_T, SPLIT_Z, r=CR-TONGUE_T)) -# 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}") +# Internal Fusions (Using Slim L-bracket style for MCU) +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_slim_corner(cx, cy, ix, iy)) + +# Press-fit nubs — half-sphere on each L-bracket's inner Y-facing wall (w1) +bump_z = BRD_Z + 1.0 + BUMP_R # Bottom of nub sits 1mm above platform +pw = 0.8; sl = 4.0 # Must match make_slim_corner +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)]: + # w1 wall centre X: midpoint of the sl-long wall extending from corner + mid_x = cx + ix * sl / 2.0 + # w1 inner face Y: the face that looks toward the board centre + face_y = cy if iy > 0 else cy - pw # wall origin Y + inner_y = face_y + pw if iy > 0 else face_y # the side facing inward + # iy>0 → bump faces +Y (inward), iy<0 → bump faces -Y (inward) + # Actually: iy>0 means corner is at low-Y side, wall inner face = face_y+pw, bump goes +Y + # iy<0 means corner is at high-Y side, wall inner face = face_y, bump goes -Y + sph = Part.makeSphere(BUMP_R, Base.Vector(mid_x, inner_y, bump_z)) + cs = BUMP_R + 0.5 + # Clip: keep only the half protruding inward (toward board centre) + if iy > 0: + clip = box(cs*2, cs, cs*2, mid_x - cs, inner_y, bump_z - cs) else: - print("Bottom shell: no horizontal edges found — skipped") -except Exception as exc: - print(f"Bottom shell horizontal fillet skipped: {exc}") + clip = box(cs*2, cs, cs*2, mid_x - cs, inner_y - cs, bump_z - cs) + half_sph = sph.common(clip) + bot_shell = bot_shell.fuse(half_sph) -# 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) +POST_R = 1.75 +POST_TAPER_EXTRA = 0.3 # Extra radius at base +POST_TAPER_H = 6.0 # Height over which the taper blends to nominal radius +for px, py in [(BTN_X+ox, BTN_CY+oy) for ox in [-POST_OFFS_X, POST_OFFS_X] for oy in [-POST_OFFS_Y, POST_OFFS_Y]]: + post = cyl(POST_R, POST_H, px, py, WALL) + # Tapered cone base: wider at bottom, blends to post radius at POST_TAPER_H + taper = Part.makeCone(POST_R + POST_TAPER_EXTRA, POST_R, POST_TAPER_H, + Base.Vector(px, py, WALL)) + post = post.fuse(taper) + post = post.cut(cyl(0.5, POST_H + 1, px, py, WALL)) + bot_shell = bot_shell.fuse(post) -# 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) +# Rounded USB-C Cut (Pill Shape) +usbc_r = USBC_H / 2.0 +usbc_box = box(WALL*4, USBC_W - 2*usbc_r, USBC_H, -1, W/2 - USBC_W/2 + usbc_r, USBC_Z) +usbc_cyl1 = Part.makeCylinder(usbc_r, WALL*4, Base.Vector(-1, W/2 - USBC_W/2 + usbc_r, USBC_Z + usbc_r), Base.Vector(1, 0, 0)) +usbc_cyl2 = Part.makeCylinder(usbc_r, WALL*4, Base.Vector(-1, W/2 + USBC_W/2 - usbc_r, USBC_Z + usbc_r), Base.Vector(1, 0, 0)) +usbc_rounded = usbc_box.fuse(usbc_cyl1).fuse(usbc_cyl2) +bot_shell = bot_shell.cut(usbc_rounded) -# 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 +# Battery bay + retaining tabs 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)) +bat_clip_cy = 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, bat_clip_cy, WALL)) +bot_shell = bot_shell.fuse(box(2.0, BAT_CLIP_Y, BAT_H * 0.55, BAT_X + BAT_L, bat_clip_cy, WALL)) -# ═════════════════════════════════════════════════════════════════════════════ -# TOP SHELL (Z = SPLIT_Z → H, open on bottom, closed ceiling at H) -# ═════════════════════════════════════════════════════════════════════════════ -top_h = H - SPLIT_Z # = 7.5 mm +# MICRO-DETENT RIDGES: Buried deep, barely protruding +rx0, rz0 = L/2 - RIDGE_W/2, SPLIT_Z + RIDGE_Z_OFF +ridge_bury = 1.5 +ridge_total_t = ridge_bury + RIDGE_PROUD +bot_shell = bot_shell.fuse(box(RIDGE_W, ridge_total_t, RIDGE_H, rx0, TONGUE_T - ridge_bury, rz0)) +bot_shell = bot_shell.fuse(box(RIDGE_W, ridge_total_t, RIDGE_H, rx0, W - TONGUE_T - RIDGE_PROUD, rz0)) -# 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) +# TOP SHELL +top_shell = fillet_horiz(rbox(L, W, H-SPLIT_Z, 0, 0, SPLIT_Z), EDGE_FILLET, H) +top_shell = top_shell.cut(rbox(L-WALL*2, W-WALL*2, H-SPLIT_Z-WALL, WALL, WALL, SPLIT_Z, r=CR_I)) -# 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}") +# Groove and Matching Recesses +g_band = rbox(L, W, GROOVE_H, 0, 0, SPLIT_Z, r=CR).cut(rbox(L-GROOVE_T*2, W-GROOVE_T*2, GROOVE_H+2, GROOVE_T, GROOVE_T, SPLIT_Z-1, r=CR-GROOVE_T)) +top_shell = top_shell.cut(g_band) -# 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) +# Recesses in groove wall — bottom ridges click into these +rec_w = RIDGE_W + TOL*2 +rec_d = RIDGE_PROUD + TOL # Slightly deeper than ridge protrusion +top_shell = top_shell.cut(box(rec_w, rec_d, RIDGE_H+TOL, L/2-rec_w/2, GROOVE_T, rz0-TOL/2)) +top_shell = top_shell.cut(box(rec_w, rec_d, RIDGE_H+TOL, L/2-rec_w/2, W-GROOVE_T-rec_d, rz0-TOL/2)) -# 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) +# Button & Cap +top_shell = top_shell.cut(cyl(BTN_HOLE_R, H, BTN_X, BTN_CY, SPLIT_Z)) +top_shell = top_shell.cut(Part.makeSphere(BTN_DOME_R, Base.Vector(BTN_X, BTN_CY, H - WALL - BTN_DOME_R + BTN_DOME_SAG))) +cap = cyl(CAP_SHAFT_R, CAP_SHAFT_H).fuse(cyl(CAP_RIM_R, CAP_RIM_H, 0, 0, -CAP_RIM_H)).fuse(cyl(NUBBIN_R, NUBBIN_H, 0, 0, -CAP_RIM_H - NUBBIN_H)) +cap_placed = cap.copy(); cap_placed.translate(Base.Vector(BTN_X, BTN_CY, H - CAP_SHAFT_H)) -# 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 +# ─── REGISTER ──────────────────────────────────────────────────────────────── +for name, shape, color in [("Shell_Bottom", bot_shell, (0.15, 0.15, 0.18)), + ("Shell_Top", top_shell, (0.25, 0.25, 0.32)), + ("Button_Cap", cap_placed, (0.7, 0.7, 0.7))]: + obj = doc.addObject("Part::Feature", name) + obj.Shape = shape + obj.ViewObject.ShapeColor = color 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) +Gui.SendMsgToActiveView("ViewFit") \ No newline at end of file