diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0fc0096 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.FCStd +*.FCBak +*.3mf \ No newline at end of file diff --git a/pointer.FCMacro b/pointer.FCMacro index 8d6b287..57cd876 100644 --- a/pointer.FCMacro +++ b/pointer.FCMacro @@ -1,71 +1,60 @@ -# IMU Pointer Enclosure — FreeCAD Macro v5 -# ========================================== -# Lid drops INSIDE the base. -# Base has a shelf step cut 1.5mm into the inner walls, -# LID_STEP mm from the top. Lid rests on that shelf, flush with base top. -# USB-C notch on front edge of lid keeps port accessible. -# -# Layout (top view, front = left): -# [USB-C][ XIAO on posts ]<-wires->[ AAA holder ][ grip ] -# ========================================== +# IMU Pointer Enclosure import FreeCAD as App import FreeCADGui as Gui import Part -import Mesh -import os from FreeCAD import Base -doc = App.newDocument("IMU_Pointer_v5") +doc = App.newDocument("pointer") -# ---- PARAMETERS ------------------------------------------------------------ +# Global dimensions +L = 115.0 +W = 36.0 +H = 20.0 +WALL = 3.5 +CR = 3.0 +TOL = 0.25 -L = 115.0 -W = 34.0 -H = 28.0 -WALL = 2.5 -CR = 3.0 # corner fillet radius +# Rail and lid +RAIL_H = 4.5 +RAIL_D = 2.0 +LIP_H = 2.0 +LIP_OVER = 1.5 +LIP_EMBED = 0.2 -TOL = 0.25 # fit clearance — lid is this much smaller than the shelf opening +LID_H = RAIL_H - LIP_H - TOL - 0.55 -# Shelf: step cut into inside of base walls near the top -# Lid sits on this shelf, flush with the base rim -SHELF_DEPTH = 1.5 # how far shelf cuts inward from the inner wall face -SHELF_H = 2.0 # how tall the shelf step is (from top of base downward) - # lid height = SHELF_H so it sits exactly flush +# Board dimensions +PCB_T = 1.0 +BRD_L = 21.0 +BRD_W = 17.5 +BRD_X = WALL +BRD_Y = (W - BRD_W) / 2 -LID_H = SHELF_H # lid is exactly as tall as the shelf recess +PLATFORM_H = 0.5 +BRD_Z = WALL + PLATFORM_H -# XIAO board (front of device) -BRD_L = 21.0 -BRD_W = 18.0 -BRD_X = WALL + 4.0 -BRD_Y = (W - BRD_W) / 2 -BRD_Z = WALL + 4.5 +# Clip arms +ARM_LEN = 5.0 +ARM_THICK = 1.6 +ARM_H = BRD_Z + PCB_T + 0.8 +CLIP_TOL = 0.35 -# AAA battery holder -BAT_L = 50.0 -BAT_W = 12.0 -BAT_H = 12.0 -BAT_X = BRD_X + BRD_L + 6.0 -BAT_Y = (W - BAT_W) / 2 +# USB-C cutout +USBC_W = 11.0 +USBC_H = 7.0 +USBC_Z = 4.5 -# L-post mount -POST_OD = 3.5 -POST_ID = 1.6 -POST_H = BRD_Z -L_ARM = 2.2 -L_THICK = 1.2 +# Battery section +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 +BAT_CLIP_Y = 8.0 -# USB-C cutout on front face -USBC_W = 9.5 -USBC_H = 3.5 -USBC_Z = BRD_Z + 0.8 - -MACRO_DIR = os.path.dirname(os.path.abspath(__file__)) - -# ---- HELPERS --------------------------------------------------------------- +# Rounded box helper def rbox(lx, ly, lz, ox=0, oy=0, oz=0, r=CR): b = Part.makeBox(lx, ly, lz, Base.Vector(ox, oy, oz)) try: @@ -77,135 +66,218 @@ def rbox(lx, ly, lz, ox=0, oy=0, oz=0, r=CR): pass return b + +# Simple box helper 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, ox=0, oy=0, oz=0): - return Part.makeCylinder(r, h, Base.Vector(ox, oy, oz)) -def export_stl(shape, name): - path = os.path.join(MACRO_DIR, name) - tess = shape.tessellate(0.06) - m = Mesh.Mesh(list(zip(tess[0], tess[1]))) - m.write(path) - print(f" Saved: {path}") +# Rounded slot helper +def rounded_slot(depth, w, h, ox, oy, oz, r=None): + if r is None: + r = h / 2.0 -# ==================================================================== -# BASE -# ==================================================================== + r = min(r, h / 2.0, w / 2.0) + import math + + cy = oy + w / 2.0 + cz = oz + h / 2.0 + hw = w / 2.0 - r + + def pt(cx, cy_v, cz_v, angle_deg, radius): + a = math.radians(angle_deg) + return Base.Vector( + cx, + cy_v + radius * math.cos(a), + cz_v + radius * math.sin(a) + ) + + l_start = pt(ox, cy - hw, cz, 270, r) + l_mid = pt(ox, cy - hw, cz, 180, r) + l_end = pt(ox, cy - hw, cz, 90, r) + arc_left = Part.Arc(l_start, l_mid, l_end).toShape() + + line_top = Part.makeLine( + l_end, + pt(ox, cy + hw, cz, 90, r) + ) + + r_start = pt(ox, cy + hw, cz, 90, r) + r_mid = pt(ox, cy + hw, cz, 0, r) + r_end = pt(ox, cy + hw, cz, 270, r) + arc_right = Part.Arc(r_start, r_mid, r_end).toShape() + + line_bot = Part.makeLine(r_end, l_start) + + wire = Part.Wire([arc_left, line_top, arc_right, line_bot]) + face = Part.Face(wire) + + return face.extrude(Base.Vector(depth, 0, 0)) + + +# Board clip helper +def make_clip(corner_x, corner_y, inward_x, inward_y): + + plat_w = ARM_THICK + CLIP_TOL + + plat_x = corner_x if inward_x > 0 else corner_x - plat_w + plat_y = corner_y if inward_y > 0 else corner_y - plat_w + + platform = box( + plat_w, plat_w, + PLATFORM_H + PCB_T, + plat_x, plat_y, WALL + ) + + ax_ox = corner_x if inward_x > 0 else corner_x - ARM_LEN + ax_oy = corner_y - ARM_THICK - CLIP_TOL if inward_y > 0 else corner_y + CLIP_TOL + arm_x = box(ARM_LEN, ARM_THICK, ARM_H, ax_ox, ax_oy, WALL) + + ay_oy = corner_y if inward_y > 0 else corner_y - ARM_LEN + ay_ox = corner_x - ARM_THICK - CLIP_TOL if inward_x > 0 else corner_x + CLIP_TOL + arm_y = box(ARM_THICK, ARM_LEN, ARM_H, ay_ox, ay_oy, WALL) + + corner_block_w = ARM_THICK + CLIP_TOL + cb_ox = corner_x - corner_block_w if inward_x > 0 else corner_x + cb_oy = corner_y - corner_block_w if inward_y > 0 else corner_y + + corner_block = box( + corner_block_w, corner_block_w, ARM_H, + cb_ox, cb_oy, WALL + ) + + return platform.fuse(arm_x.fuse(arm_y).fuse(corner_block)) + + +# Base outer body base = rbox(L, W, H) -# Main inner cavity — full height minus floor -base = base.cut(box(L - WALL*2, W - WALL*2, H - WALL, WALL, WALL, WALL)) -# Shelf recess: widen the top SHELF_H of the inner cavity by SHELF_DEPTH -# This creates the step the lid rests on -shelf_cut = box(L - WALL*2 + SHELF_DEPTH*2, - W - WALL*2 + SHELF_DEPTH*2, - SHELF_H + 0.1, - WALL - SHELF_DEPTH, - WALL - SHELF_DEPTH, - H - SHELF_H) -base = base.cut(shelf_cut) +# Inner cavity +base = base.cut( + box(L - WALL * 2, W - WALL * 2, H - WALL, + WALL, WALL, WALL) +) -# USB-C slot on front face (X=0) -base = base.cut(box(WALL*3, USBC_W, USBC_H, - -WALL, W/2 - USBC_W/2, USBC_Z)) +rail_z = H - RAIL_H +groove_h = RAIL_H - LIP_H -# ---- L-shaped mounting posts ---- -brd_cx = BRD_X + BRD_L / 2 -brd_cy = BRD_Y + BRD_W / 2 -post_corners = [ - (BRD_X + 2.0, BRD_Y + 2.0), - (BRD_X + BRD_L - 2.0, BRD_Y + 2.0), - (BRD_X + 2.0, BRD_Y + BRD_W - 2.0), - (BRD_X + BRD_L - 2.0, BRD_Y + BRD_W - 2.0), +# Rail grooves +base = base.cut( + box(L - WALL * 2, RAIL_D, groove_h, + WALL, WALL - RAIL_D, rail_z) +) + +base = base.cut( + box(L - WALL * 2, RAIL_D, groove_h, + WALL, W - WALL, rail_z) +) + +# Lid lips +lip_z = H - LIP_H + +base = base.fuse( + box(L - WALL * 2, LIP_OVER, LIP_H, + WALL, WALL, lip_z) +) + +base = base.fuse( + box(L - WALL * 2, LIP_OVER, LIP_H, + WALL, W - WALL - LIP_OVER, lip_z) +) + +# Back slot +slot_y0 = WALL - RAIL_D +slot_yw = W - WALL * 2 + RAIL_D * 2 + +base = base.cut( + box(WALL + 1.0, slot_yw, RAIL_H, + L - WALL, slot_y0, rail_z) +) + +# Entry bump +BUMP_H = 0.5 +pad_raw = box(WALL, slot_yw, BUMP_H, + L - WALL, slot_y0, rail_z) + +pad_trimmed = pad_raw.common(rbox(L, W, H)) +base = base.fuse(pad_trimmed) + +# Board clips +clip_corners = [ + (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), ] -for (px, py) in post_corners: - post = cyl(POST_OD/2, POST_H, px, py, WALL) - post = post.cut(cyl(POST_ID/2, POST_H + 1.0, px, py, WALL - 0.5)) - dx = brd_cx - px; dy = brd_cy - py - mag = (dx**2 + dy**2)**0.5; dx /= mag; dy /= mag - perp_x = -dy; perp_y = dx; hw = L_THICK / 2 - z0 = WALL + POST_H - L_THICK; z1 = WALL + POST_H - p0 = Base.Vector(px - perp_x*hw, py - perp_y*hw, z0) - p1 = Base.Vector(px + perp_x*hw, py + perp_y*hw, z0) - p2 = Base.Vector(px + perp_x*hw, py + perp_y*hw, z1) - p3 = Base.Vector(px - perp_x*hw, py - perp_y*hw, z1) - arm = Part.Face(Part.Wire([ - Part.makeLine(p0,p1), Part.makeLine(p1,p2), - Part.makeLine(p2,p3), Part.makeLine(p3,p0) - ])).extrude(Base.Vector(dx*L_ARM, dy*L_ARM, 0)) - base = base.fuse(post.fuse(arm)) -# ---- Battery cradle ---- -base = base.cut(box(BAT_L, BAT_W, 3.0, BAT_X, BAT_Y, WALL)) -base = base.fuse(box(2.0, BAT_W+3.0, BAT_H*0.55, BAT_X-2.0, BAT_Y-1.5, WALL)) -base = base.fuse(box(2.0, BAT_W+3.0, BAT_H*0.55, BAT_X+BAT_L, BAT_Y-1.5, WALL)) +for cx, cy, ix, iy in clip_corners: + base = base.fuse(make_clip(cx, cy, ix, iy)) -print("[1/2] Base complete.") +# USB-C opening +base = base.cut( + rounded_slot( + WALL * 3, + USBC_W, + USBC_H, + -WALL, + W / 2 - USBC_W / 2, + USBC_Z + ) +) -# ==================================================================== -# LID — sized to drop inside the shelf opening, built at Z=0 -# ==================================================================== -# Lid outer dimensions match the shelf recess minus tolerance -LID_L = L - WALL*2 + SHELF_DEPTH*2 - TOL*2 -LID_W = W - WALL*2 + SHELF_DEPTH*2 - TOL*2 +# Battery recess +base = base.cut( + box(BAT_L, BAT_W, 3.0, + BAT_X, BAT_Y, WALL) +) -lid = rbox(LID_L, LID_W, LID_H, - WALL - SHELF_DEPTH + TOL, - WALL - SHELF_DEPTH + TOL, - 0, r=1.5) +clip_y_start = BAT_Y + BAT_W / 2 - BAT_CLIP_Y / 2 -# USB-C notch on front edge (X=0 face of lid) so port stays accessible -# The notch is a slot cut from the front edge, aligned with USB-C position -notch_y = W/2 - USBC_W/2 - (WALL - SHELF_DEPTH + TOL) # relative to lid origin -lid = lid.cut(box(WALL*2, USBC_W, USBC_H + 0.5, - -(WALL - SHELF_DEPTH + TOL), # punch through front face - W/2 - USBC_W/2, - 0)) +base = base.fuse( + box(2.0, BAT_CLIP_Y, BAT_H * 0.55, + BAT_X - 2.0, clip_y_start, WALL) +) -print("[2/2] Lid complete.") +base = base.fuse( + box(2.0, BAT_CLIP_Y, BAT_H * 0.55, + BAT_X + BAT_L, clip_y_start, WALL) +) -# Export STLs at Z=0 BEFORE any translation -print("\nExporting STLs...") -export_stl(base, "pointer_base.stl") -export_stl(lid, "pointer_lid.stl") +# Lid +TAB_W = RAIL_D - TOL + 0.5 +LID_L = L - WALL * 2 - TOL +LID_EXTRA_TOL = 0.5 -# Translate lid up to assembled position for FreeCAD viewport only -lid.translate(Base.Vector(0, 0, H - LID_H)) +LID_W = ( + W - WALL * 2 + - (TOL + LID_EXTRA_TOL) * 2 + + TAB_W * 2 +) -# ==================================================================== -# DOCUMENT -# ==================================================================== +lid_y0 = WALL + TOL + LID_EXTRA_TOL - TAB_W + +lid = box(LID_L, LID_W, LID_H, 0, lid_y0, 0) + +lid = lid.cut( + box(WALL * 2, USBC_W + TOL, LID_H + 0.2, + LID_L - WALL, + W / 2 - (USBC_W + TOL) / 2, + -0.1) +) + +lid.translate(Base.Vector(WALL + TOL, 0, rail_z)) + +# Final objects base_obj = doc.addObject("Part::Feature", "Pointer_Base") base_obj.Shape = base base_obj.ViewObject.ShapeColor = (0.12, 0.12, 0.14) lid_obj = doc.addObject("Part::Feature", "Pointer_Lid") lid_obj.Shape = lid -lid_obj.ViewObject.ShapeColor = (0.28, 0.28, 0.34) +lid_obj.ViewObject.ShapeColor = (0.28, 0.28, 0.34) lid_obj.ViewObject.Transparency = 25 doc.recompute() Gui.activeDocument().activeView().viewIsometric() -Gui.SendMsgToActiveView("ViewFit") - -print() -print("=" * 62) -print("IMU Pointer v5 — lid drops INSIDE base") -print(f" Outer : {L:.0f} x {W:.0f} x {H:.0f} mm") -print(f" Shelf step : {SHELF_DEPTH:.1f} mm inward, {SHELF_H:.1f} mm tall") -print(f" Lid : {LID_L:.1f} x {LID_W:.1f} x {LID_H:.1f} mm") -print(f" Tolerance : {TOL:.2f} mm") -print() -print("Assembly:") -print(" Drop lid flat-side-up into the top opening.") -print(" It rests on the shelf, sitting flush with the base rim.") -print(" USB-C notch on front edge keeps port fully accessible.") -print(" To remove: push up from the USB-C hole or pry with fingernail.") -print() -print("Print orientation:") -print(" Base: flat bottom down, no supports") -print(" Lid : either face down, it's a flat plate — no supports") -print("=" * 62) +Gui.SendMsgToActiveView("ViewFit") \ No newline at end of file diff --git a/pointer.FCStd b/pointer.FCStd deleted file mode 100644 index 1ad0e05..0000000 Binary files a/pointer.FCStd and /dev/null differ