Cleanup macro

This commit is contained in:
2026-03-01 17:41:48 +01:00
parent 8c95ea0480
commit 2cd4d272d6
3 changed files with 229 additions and 154 deletions
+3
View File
@@ -0,0 +1,3 @@
*.FCStd
*.FCBak
*.3mf
+222 -150
View File
@@ -1,71 +1,60 @@
# IMU Pointer Enclosure — FreeCAD Macro v5 # IMU Pointer Enclosure
# ==========================================
# 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 ]
# ==========================================
import FreeCAD as App import FreeCAD as App
import FreeCADGui as Gui import FreeCADGui as Gui
import Part import Part
import Mesh
import os
from FreeCAD import Base from FreeCAD import Base
doc = App.newDocument("IMU_Pointer_v5") doc = App.newDocument("pointer")
# ---- PARAMETERS ------------------------------------------------------------
# Global dimensions
L = 115.0 L = 115.0
W = 34.0 W = 36.0
H = 28.0 H = 20.0
WALL = 2.5 WALL = 3.5
CR = 3.0 # corner fillet radius CR = 3.0
TOL = 0.25
TOL = 0.25 # fit clearance — lid is this much smaller than the shelf opening # Rail and lid
RAIL_H = 4.5
RAIL_D = 2.0
LIP_H = 2.0
LIP_OVER = 1.5
LIP_EMBED = 0.2
# Shelf: step cut into inside of base walls near the top LID_H = RAIL_H - LIP_H - TOL - 0.55
# 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
LID_H = SHELF_H # lid is exactly as tall as the shelf recess # Board dimensions
PCB_T = 1.0
# XIAO board (front of device)
BRD_L = 21.0 BRD_L = 21.0
BRD_W = 18.0 BRD_W = 17.5
BRD_X = WALL + 4.0 BRD_X = WALL
BRD_Y = (W - BRD_W) / 2 BRD_Y = (W - BRD_W) / 2
BRD_Z = WALL + 4.5
# AAA battery holder PLATFORM_H = 0.5
BRD_Z = WALL + PLATFORM_H
# Clip arms
ARM_LEN = 5.0
ARM_THICK = 1.6
ARM_H = BRD_Z + PCB_T + 0.8
CLIP_TOL = 0.35
# USB-C cutout
USBC_W = 11.0
USBC_H = 7.0
USBC_Z = 4.5
# Battery section
BAT_L = 50.0 BAT_L = 50.0
BAT_W = 12.0 BAT_W = 12.0
BAT_H = 12.0 BAT_H = 12.0
BAT_X = BRD_X + BRD_L + 6.0 BAT_X = BRD_X + BRD_L + 8.0
BAT_Y = (W - BAT_W) / 2 BAT_Y = (W - BAT_W) / 2
BAT_CLIP_Y = 8.0
# L-post mount
POST_OD = 3.5
POST_ID = 1.6
POST_H = BRD_Z
L_ARM = 2.2
L_THICK = 1.2
# 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): def rbox(lx, ly, lz, ox=0, oy=0, oz=0, r=CR):
b = Part.makeBox(lx, ly, lz, Base.Vector(ox, oy, oz)) b = Part.makeBox(lx, ly, lz, Base.Vector(ox, oy, oz))
try: try:
@@ -77,107 +66,209 @@ def rbox(lx, ly, lz, ox=0, oy=0, oz=0, r=CR):
pass pass
return b return b
# Simple box helper
def box(lx, ly, lz, ox=0, oy=0, oz=0): def box(lx, ly, lz, ox=0, oy=0, oz=0):
return Part.makeBox(lx, ly, lz, Base.Vector(ox, oy, oz)) 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): # Rounded slot helper
path = os.path.join(MACRO_DIR, name) def rounded_slot(depth, w, h, ox, oy, oz, r=None):
tess = shape.tessellate(0.06) if r is None:
m = Mesh.Mesh(list(zip(tess[0], tess[1]))) r = h / 2.0
m.write(path)
print(f" Saved: {path}")
# ==================================================================== r = min(r, h / 2.0, w / 2.0)
# BASE
# ====================================================================
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) 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 # Inner cavity
# This creates the step the lid rests on base = base.cut(
shelf_cut = box(L - WALL*2 + SHELF_DEPTH*2, box(L - WALL * 2, W - WALL * 2, H - WALL,
W - WALL*2 + SHELF_DEPTH*2, WALL, WALL, WALL)
SHELF_H + 0.1, )
WALL - SHELF_DEPTH,
WALL - SHELF_DEPTH,
H - SHELF_H)
base = base.cut(shelf_cut)
# USB-C slot on front face (X=0) rail_z = H - RAIL_H
base = base.cut(box(WALL*3, USBC_W, USBC_H, groove_h = RAIL_H - LIP_H
-WALL, W/2 - USBC_W/2, USBC_Z))
# ---- L-shaped mounting posts ---- # Rail grooves
brd_cx = BRD_X + BRD_L / 2 base = base.cut(
brd_cy = BRD_Y + BRD_W / 2 box(L - WALL * 2, RAIL_D, groove_h,
post_corners = [ WALL, WALL - RAIL_D, rail_z)
(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), base = base.cut(
(BRD_X + BRD_L - 2.0, BRD_Y + BRD_W - 2.0), 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 ---- for cx, cy, ix, iy in clip_corners:
base = base.cut(box(BAT_L, BAT_W, 3.0, BAT_X, BAT_Y, WALL)) base = base.fuse(make_clip(cx, cy, ix, iy))
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))
print("[1/2] Base complete.") # USB-C opening
base = base.cut(
# ==================================================================== rounded_slot(
# LID — sized to drop inside the shelf opening, built at Z=0 WALL * 3,
# ==================================================================== USBC_W,
# Lid outer dimensions match the shelf recess minus tolerance USBC_H,
LID_L = L - WALL*2 + SHELF_DEPTH*2 - TOL*2 -WALL,
LID_W = W - WALL*2 + SHELF_DEPTH*2 - TOL*2
lid = rbox(LID_L, LID_W, LID_H,
WALL - SHELF_DEPTH + TOL,
WALL - SHELF_DEPTH + TOL,
0, r=1.5)
# 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, W / 2 - USBC_W / 2,
0)) USBC_Z
)
)
print("[2/2] Lid complete.") # Battery recess
base = base.cut(
box(BAT_L, BAT_W, 3.0,
BAT_X, BAT_Y, WALL)
)
# Export STLs at Z=0 BEFORE any translation clip_y_start = BAT_Y + BAT_W / 2 - BAT_CLIP_Y / 2
print("\nExporting STLs...")
export_stl(base, "pointer_base.stl")
export_stl(lid, "pointer_lid.stl")
# Translate lid up to assembled position for FreeCAD viewport only base = base.fuse(
lid.translate(Base.Vector(0, 0, H - LID_H)) box(2.0, BAT_CLIP_Y, BAT_H * 0.55,
BAT_X - 2.0, clip_y_start, WALL)
)
# ==================================================================== base = base.fuse(
# DOCUMENT box(2.0, BAT_CLIP_Y, BAT_H * 0.55,
# ==================================================================== BAT_X + BAT_L, clip_y_start, WALL)
)
# Lid
TAB_W = RAIL_D - TOL + 0.5
LID_L = L - WALL * 2 - TOL
LID_EXTRA_TOL = 0.5
LID_W = (
W - WALL * 2
- (TOL + LID_EXTRA_TOL) * 2
+ TAB_W * 2
)
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 = doc.addObject("Part::Feature", "Pointer_Base")
base_obj.Shape = base base_obj.Shape = base
base_obj.ViewObject.ShapeColor = (0.12, 0.12, 0.14) base_obj.ViewObject.ShapeColor = (0.12, 0.12, 0.14)
@@ -190,22 +281,3 @@ lid_obj.ViewObject.Transparency = 25
doc.recompute() doc.recompute()
Gui.activeDocument().activeView().viewIsometric() Gui.activeDocument().activeView().viewIsometric()
Gui.SendMsgToActiveView("ViewFit") 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)
BIN
View File
Binary file not shown.