Files
Freddy-Fazbear-Digital-Eyes/hardware/eye_mount/first_fit.py
T
2026-10-02 12:23:58 -04:00

160 lines
6.6 KiB
Python

"""Export a *test-fit* eye-board carrier and cosmetic trim in mask STEP coordinates.
Run: uv run --no-project --with build123d python hardware/eye_mount/first_fit.py
The three mask pads per eye are modeled against the *CAD* shell, not the glued
print. Do not bond them to the mask until board/cable clearance and fit are
checked on the real assembly. Units are millimetres.
"""
import math
from pathlib import Path
from build123d import (
Align, Box, Compound, Cylinder, Pos, Rot, Vector, export_step, export_stl,
import_step,
)
ROOT = Path(__file__).resolve().parents[2]
OUT = Path(__file__).resolve().parent
MASK = import_step(str(ROOT / "stls/freddy_fazbear_mask.step"))
HEAD_HALVES = MASK.solids()[:2]
# The official Waveshare CAD has 2 mm nominal bores in the four rear posts.
# Their centers are not a square. These are in the vendor board's local XY.
BOARD_POSTS = [(-29.5, 13.63818, -6.0), (29.5, 13.63818, -6.0),
(-18.0, -27.06012, -6.1), (18.0, -27.06012, -6.1)]
BOARD_ORIGIN_Y = -15.0
EYE_CENTERS = [(-30.0, 196.0), (56.0, 196.0)]
def cyl(radius, height, x=0, y=0, z=0):
return Pos(x, y, z) * Cylinder(
radius, height, align=(Align.CENTER, Align.CENTER, Align.MIN)
)
def arm(a, b, width, z0, height):
"""A straight XY rib joining a point on the carrier ring to an anchor."""
d = Vector(b[0] - a[0], b[1] - a[1])
angle = math.degrees(math.atan2(d.Y, d.X))
return (Pos((a[0] + b[0]) / 2, (a[1] + b[1]) / 2, z0 + height / 2)
* Rot(0, 0, angle) * Box(d.length, width, height))
def radial_slot(x, y, radius=1.8, travel=5.0):
"""Through-slot, with 5 mm adjustment along its radial direction."""
r = (x * x + y * y) ** 0.5
ux, uy = x / r, y / r
a = (x - ux * travel / 2, y - uy * travel / 2)
b = (x + ux * travel / 2, y + uy * travel / 2)
return (cyl(radius, 5, *a, -13)
+ cyl(radius, 5, *b, -13)
+ arm(a, b, 2 * radius, -13, 5))
def carrier(outer_sign):
# Rear carrier is an open ring, well behind the PCB parts. Its four small
# stand-offs reach the vendor board posts at local Z=-6.0. Screw from back.
part = cyl(37.0, 3.0, z=-12.0) - cyl(29.0, 5.0, z=-13.0)
for x, y, back_z in BOARD_POSTS:
part += cyl(3.5, back_z + 12.0, x, y, -12.0)
# Two brow attachments and one lower/outer cheek attachment. The lateral
# temple surface changes depth by >20 mm under a 12 mm pad; the lower
# cheek is smoother, though it needs a much longer stand-off sleeve.
anchors = [(-20.0, 42.0), (20.0, 42.0), (outer_sign * 20.0, -38.0)]
for x, y in anchors:
r = (x * x + y * y) ** 0.5
start = (x * 34.0 / r, y * 34.0 / r)
part += arm(start, (x, y), 8.0, -12.0, 3.0)
part += cyl(6.0, 3.0, x, y, -12.0)
for x, y, _ in BOARD_POSTS:
part -= cyl(1.15, 8.0, x, y, -13.0) # 2.3 mm M2 clearance
for x, y in anchors:
part -= radial_slot(x, y) # 3.6 mm M3 clearance
return part, anchors
def trim():
# A simple first-fit matte/paintable annulus sits 0.5 mm in front of the
# Waveshare face; ID 54.5 mm does not press on the ~53 mm lit circle.
# It is a placement/appearance gauge, not a finished snap-fit surround.
return cyl(37.1, 1.2, z=3.9) - cyl(27.25, 2.2, z=3.4)
def place_at_eye(shape, center):
return Pos(center[0], BOARD_ORIGIN_Y, center[1]) * Rot(90, 0, 0) * shape
def mask_pad(shell, x, z):
# The rear surface is found along Y at this X/Z; the farthest intersection
# is the surface a pad placed from behind will meet.
from build123d import Axis
def rear_at(px, pz):
hits = shell.find_intersection_points(Axis((px, -200, pz), (0, 1, 0)))
return max((point.Y for point, _ in hits), default=None)
rear_y = rear_at(x, z)
samples = [rear_y]
for radius in (3.0, 6.0):
for step in range(24):
theta = step * math.tau / 24
hit = rear_at(x + radius * math.cos(theta),
z + radius * math.sin(theta))
if hit is not None:
samples.append(hit)
# Size the *rear* plane from the farthest-back point under the whole pad so
# its sleeve stays off the mask.
back_y = max(samples) + 2.0
front_y = min(samples) - 1.0
# A small sacrificial pad is cut to the nominal shell surface. Actual
# print/glue variation needs filler or a foam/epoxy bond after dry fit.
blank = (Pos(x, back_y, z) * Rot(90, 0, 0)
* cyl(6.0, back_y - front_y))
pad = blank - shell
# 4.2 mm pilot for a test M3 heat-set insert, inserted from behind.
bore = (Pos(x, back_y + 0.1, z) * Rot(90, 0, 0)
* cyl(2.1, 4.1))
pad -= bore
return pad, rear_y, back_y
pieces = []
for index, (shell, center) in enumerate(zip(HEAD_HALVES, EYE_CENTERS)):
side = "left" if index == 0 else "right" # mask STEP negative/positive X
sign = -1 if index == 0 else 1
local_carrier, anchors = carrier(sign)
world_carrier = place_at_eye(local_carrier, center)
world_trim = place_at_eye(trim(), center)
export_step(world_carrier, str(OUT / f"{side}_carrier.step"))
export_stl(local_carrier, str(OUT / f"{side}_carrier.stl"))
export_step(world_trim, str(OUT / f"{side}_trim_gauge.step"))
export_stl(trim(), str(OUT / f"{side}_trim_gauge.stl"))
pieces.extend([world_carrier, world_trim])
print(f"{side}: carrier {world_carrier.volume:.1f} mm^3, "
f"trim {world_trim.volume:.1f} mm^3")
for number, (lx, ly) in enumerate(anchors, 1):
pad, rear_y, back_y = mask_pad(shell, center[0] + lx,
center[1] + ly)
export_step(pad, str(OUT / f"{side}_pad_{number}.step"))
export_stl(pad, str(OUT / f"{side}_pad_{number}.stl"))
pieces.append(pad)
gap = -6.0 - back_y # carrier front to pad rear face
sleeve_outer_radius = 5.0 if gap > 30.0 else 3.5
sleeve = (Pos(center[0] + lx, -6.0, center[1] + ly)
* Rot(90, 0, 0)
* (cyl(sleeve_outer_radius, gap) - cyl(1.8, gap)))
export_step(sleeve, str(OUT / f"{side}_sleeve_{number}.step"))
export_stl(cyl(sleeve_outer_radius, gap) - cyl(1.8, gap),
str(OUT / f"{side}_sleeve_{number}.stl"))
pieces.append(sleeve)
print(f" pad {number}: X={center[0]+lx:.1f}, Z={center[1]+ly:.1f}, "
f"rear shell Y={rear_y:.1f}, sleeve={gap:.1f} mm, "
f"pad volume={pad.volume:.1f} mm^3")
export_step(Compound(children=pieces), str(OUT / "first_fit_mounts_in_mask_coordinates.step"))
export_step(Compound(children=[*HEAD_HALVES, *pieces]),
str(OUT / "mask_with_first_fit_mounts.step"))
print("Exported test-fit STEP and STL parts to", OUT)