Files
air-mouse/model/pointer.FCMacro

465 lines
22 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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)