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

80 lines
3.4 KiB
Python

"""Measure straight-on clear eye apertures in the Shapr3D mask export.
Run with: uv run --no-project --with build123d python hardware/analyze_mask_step.py
This is read-only. A 0.5 mm sightline grid gives approximate, not print-fit,
measurements; faceted CAD and the assembled print may differ.
"""
from collections import deque
from pathlib import Path
from build123d import Axis, import_step
from scipy.ndimage import distance_transform_edt
import numpy as np
STEP = Path(__file__).resolve().parents[1] / "stls/freddy_fazbear_mask.step"
GRID_MM = 0.5
mask = import_step(str(STEP))
solids = mask.solids()
def clear_aperture(solid_index: int, x_start: int, seed_x: int) -> None:
"""Flood-fill only the enclosed eye opening, not empty space off-mask."""
solid = solids[solid_index]
xs = [x_start + i * GRID_MM for i in range(241)]
zs = [160 + i * GRID_MM for i in range(141)]
open_cells = set()
for zi, z in enumerate(zs):
for xi, x in enumerate(xs):
axis = Axis((x, -200, z), (0, 1, 0))
if not solid.find_intersection_points(axis):
open_cells.add((xi, zi))
seed = (round((seed_x - x_start) / GRID_MM), round((195 - 160) / GRID_MM))
assert seed in open_cells, f"Expected opening seed is not clear: {seed}"
component = {seed}
queue = deque([seed])
while queue:
xi, zi = queue.popleft()
for neighbor in ((xi - 1, zi), (xi + 1, zi), (xi, zi - 1), (xi, zi + 1)):
if neighbor in open_cells and neighbor not in component:
component.add(neighbor)
queue.append(neighbor)
assert all(0 < xi < len(xs) - 1 and 0 < zi < len(zs) - 1 for xi, zi in component), (
"The eye component reached the scan boundary; expand the grid or inspect the model."
)
x_coords = [xs[xi] for xi, _ in component]
z_coords = [zs[zi] for _, zi in component]
rows = {}
columns = {}
for xi, zi in component:
rows.setdefault(zi, []).append(xi)
columns.setdefault(xi, []).append(zi)
widest_row = max(rows, key=lambda row: max(rows[row]) - min(rows[row]))
tallest_column = max(columns, key=lambda col: max(columns[col]) - min(columns[col]))
print(f"Eye half {solid_index}: {len(component)} clear samples, grid {GRID_MM} mm")
print(f" projected clear bounds: X {min(x_coords):.1f}..{max(x_coords):.1f} mm, "
f"Z {min(z_coords):.1f}..{max(z_coords):.1f} mm")
print(f" max horizontal clear width: "
f"{(max(rows[widest_row]) - min(rows[widest_row])) * GRID_MM:.1f} mm "
f"at Z={zs[widest_row]:.1f} mm")
print(f" max vertical clear height: "
f"{(max(columns[tallest_column]) - min(columns[tallest_column])) * GRID_MM:.1f} mm "
f"at X={xs[tallest_column]:.1f} mm")
print(f" clear-area centroid: X={sum(x_coords)/len(x_coords):.1f}, "
f"Z={sum(z_coords)/len(z_coords):.1f} mm")
aperture = np.zeros((len(zs), len(xs)), dtype=bool)
for xi, zi in component:
aperture[zi, xi] = True
distances = distance_transform_edt(aperture) * GRID_MM
center_zi, center_xi = np.unravel_index(np.argmax(distances), distances.shape)
print(f" largest straight-on inscribed circle: radius about "
f"{distances[center_zi, center_xi]:.1f} mm at "
f"X={xs[center_xi]:.1f}, Z={zs[center_zi]:.1f} mm")
clear_aperture(0, -110, -30)
clear_aperture(1, 15, 60)