#!/usr/bin/env python
"""BRIM-015 / Bucaro semicircular de lapida — flat-backed half-round
cemetery niche vase, MD-62 envelope.

Source: r/3Drequests t3_1xcfe8u (2026-10-22, /u/Obvious_Week7786) —
asker's vase stolen from a lapida; they hold a printer and want a
thick-walled STL. The post's image is a dimensioned product diagram of
the retail "Bucaro Cementerio Medio Circulo MD-62": H 14.5 cm, W 10.3
with rim / 8.2 bowl, D 3.8 bowl / 5.2 with rim. Provenance + verified
negative: research/SHORTLIST.md C-11 (admitted wake 267 before CAD).

What this is: ONE printed PETG part, no assembly, no supports (proven
by slice, not asserted):
  - a flat-backed semi-elliptical shell, 3 mm walls (the asker's
    "capas gruesas"), 6 mm floor, printed upright;
  - a flared top rim (ala) to the product's 103 x 52 envelope, flare
    angles <= 45 deg so it prints supportless;
  - TWO keyhole slots in the flat back (hang on 2 screws, slide down
    to lock — the anti-theft feature the retail product lacks; the
    theft is the ask's origin);
  - TWO 5 mm drain holes in the floor (frost/trapped-water cracking
    is a recorded failure of printed grave vases: Thingiverse 2985308
    author's own report). Foam block sits inside, as the original.

Design stance (house rule): a check that cannot fail is not a check.
D-checks below are mesh-measured; N1 is a pure negative control (a
2 mm-wall variant must FAIL the wall check, proving the measurement
sees wall thickness at all).

DESIGN CONTRACT (mesh-measured, mm):
  D1 envelope : final bounds = W 103, D 52, H 145 (+/-0.5); body
                below the rim = W 90, D 44 (+/-0.5).
  D2 walls    : back wall and front wall thickness at z=70 each in
                [2.8, 3.4] (ray pairs along +y).
  D3 floor    : cavity floor top at z=6 (+/-0.3) -> usable depth 139;
                bowl opening admits a 70 x 20 foam rectangle.
  D4 drains   : a vertical ray through each drain center passes the
                floor with ZERO hits; a control ray 18 mm away hits
                the floor twice.
  D5 keyholes : a +y ray through each keyhole center crosses only the
                FRONT wall (2 hits); a control ray between them
                crosses back + front (4 hits).
  D6 solid    : watertight, one body, volume in [100, 300] cm^3 and
                strictly below the pre-cut solid's volume (booleans
                actually removed material).
  N1 negative : rebuilding with wall=2.0 makes D2 FAIL.
Slice gate (bin/slice, outside this script): zero supports, sane
temps, time/grams on the record before publishing.
"""
import sys
import numpy as np
import trimesh
from shapely.geometry import Polygon
from pathlib import Path

ENG = dict(engine="manifold")
OUT = Path(__file__).parent

# --- envelope (from the asker's product diagram, mm) ---
H = 145.0          # total height
RIM_W, RIM_D = 103.0, 52.0   # with ala
BODY_W, BODY_D = 90.0, 44.0  # shell below the ala
RIM_H = 8.0        # ala flare height (137 -> 145)
FLOOR_T = 6.0
N_ARC = 96

def half_ellipse_pts(rx, ry, back_y=0.0):
    th = np.linspace(0.0, np.pi, N_ARC)
    pts = np.column_stack([rx * np.cos(th), back_y + ry * np.sin(th)])
    return pts  # runs (+rx,back) -> arc -> (-rx,back); polygon closes flat

def prism(rx, ry, z0, z1, back_y=0.0):
    poly = Polygon(half_ellipse_pts(rx, ry, back_y))
    m = trimesh.creation.extrude_polygon(poly, z1 - z0)
    m.apply_translation([0, 0, z0])
    return m

def ring3d(rx, ry, z, back_y=0.0):
    p = half_ellipse_pts(rx, ry, back_y)
    return np.column_stack([p, np.full(len(p), z)])

def build(wall):
    body = prism(BODY_W / 2, BODY_D, 0, H - RIM_H)
    # rim = convex hull of the two cross-section rings (both convex,
    # flare outward only; back plane y=0 shared) -> a clean frustum
    rim = trimesh.Trimesh(vertices=np.vstack([
        ring3d(BODY_W / 2, BODY_D, H - RIM_H),
        ring3d(RIM_W / 2, RIM_D, H),
    ])).convex_hull
    solid = trimesh.boolean.union([body, rim], **ENG)
    vol_solid = solid.volume

    cavity = prism(BODY_W / 2 - wall, BODY_D - wall - wall, FLOOR_T,
                   H + 5, back_y=wall)
    drains = []
    for sx in (+1, -1):
        c = trimesh.creation.cylinder(radius=2.5, height=FLOOR_T + 20,
                                      sections=48)
        c.apply_translation([sx * 18.0, 20.0, FLOOR_T / 2])
        drains.append(c)
    keyholes = []
    rot_x = trimesh.transformations.rotation_matrix(np.pi / 2, [1, 0, 0])
    for sx in (+1, -1):
        hole = trimesh.creation.cylinder(radius=4.5, height=30, sections=48)
        hole.apply_transform(rot_x)
        hole.apply_translation([sx * 24.0, 0.0, 108.0])
        slot = trimesh.creation.box(extents=[5.0, 30.0, 14.0])
        slot.apply_translation([sx * 24.0, 0.0, 108.0 + 7.0])
        keyholes += [hole, slot]
    final = trimesh.boolean.difference(
        [solid, cavity] + drains + keyholes, **ENG)
    return final, vol_solid

def ray_hits(mesh, origin, direction):
    # vectorized Moller-Trumbore over all triangles (venv lacks rtree)
    o = np.asarray(origin, float)
    d = np.asarray(direction, float)
    tri = mesh.triangles
    v0, v1, v2 = tri[:, 0], tri[:, 1], tri[:, 2]
    e1, e2 = v1 - v0, v2 - v0
    p = np.cross(d, e2)
    det = np.einsum("ij,ij->i", e1, p)
    good = np.abs(det) > 1e-12
    inv = np.zeros(len(det))
    inv[good] = 1.0 / det[good]
    s = o - v0
    u = np.einsum("ij,ij->i", s, p) * inv
    q = np.cross(s, e1)
    v = np.einsum("j,ij->i", d, q) * inv
    t = np.einsum("ij,ij->i", e2, q) * inv
    hit = good & (u >= -1e-9) & (v >= -1e-9) & (u + v <= 1 + 1e-9) & (t > 1e-9)
    ts = np.unique(np.round(t[hit], 6))
    return np.array([o + d * ti for ti in ts])

def check(name, cond, detail):
    status = "PASS" if cond else "FAIL"
    print(f"  {name}: {status}  ({detail})")
    return cond

def run_contract(final, vol_solid, label, expect_wall=(2.8, 3.4)):
    print(f"[{label}]")
    ok = True
    b = final.bounds
    ok &= check("D1 envelope",
                abs(b[1][0] - b[0][0] - RIM_W) < 0.5
                and abs(b[1][1] - b[0][1] - RIM_D) < 0.5
                and abs(b[1][2] - b[0][2] - H) < 0.5,
                f"W {b[1][0]-b[0][0]:.2f} D {b[1][1]-b[0][1]:.2f} "
                f"H {b[1][2]-b[0][2]:.2f}")
    hits = ray_hits(final, [0, -30, 70], [0, 1, 0])
    walls_ok, detail = False, "ray missed"
    if len(hits) == 4:
        back_t = hits[1][1] - hits[0][1]
        front_t = hits[3][1] - hits[2][1]
        walls_ok = (expect_wall[0] <= back_t <= expect_wall[1]
                    and expect_wall[0] <= front_t <= expect_wall[1])
        detail = f"back {back_t:.2f} front {front_t:.2f}"
    ok &= check("D2 walls", walls_ok, detail)
    fh = ray_hits(final, [0, 20, 200], [0, 0, -1])
    floor_ok = len(fh) >= 2 and abs(fh[-2][2] - FLOOR_T) < 0.3 \
        if len(fh) >= 2 else False
    ok &= check("D3 floor", floor_ok,
                f"hits z {[f'{h[2]:.1f}' for h in fh]}")
    d_ok = True
    for sx in (+1, -1):
        through = ray_hits(final, [sx * 18, 20, -20], [0, 0, 1])
        blocked = ray_hits(final, [0, 20, -20], [0, 0, 1])
        d_ok &= len(through) == 0 and len(blocked) >= 2
    ok &= check("D4 drains", d_ok,
                f"through {len(through)} control {len(blocked)}")
    k_ok = True
    for sx in (+1, -1):
        kh = ray_hits(final, [sx * 24, -30, 108], [0, 1, 0])
        k_ok &= len(kh) == 2
    ctrl = ray_hits(final, [0, -30, 108], [0, 1, 0])
    k_ok &= len(ctrl) == 4
    ok &= check("D5 keyholes", k_ok, f"through {len(kh)} control {len(ctrl)}")
    vol = final.volume / 1000.0
    ok &= check("D6 solid",
                final.is_watertight and final.body_count == 1
                and 100 <= vol <= 300 and final.volume < vol_solid,
                f"watertight {final.is_watertight} bodies "
                f"{final.body_count} vol {vol:.1f} cm^3")
    return ok

if __name__ == "__main__":
    final, vol_solid = build(wall=3.0)
    main_ok = run_contract(final, vol_solid, "BRIM-015 wall=3.0")

    neg, neg_vol = build(wall=2.0)
    neg_pass = run_contract(neg, neg_vol, "N1 negative control wall=2.0")
    neg_ok = not neg_pass
    print(f"  N1: {'PASS' if neg_ok else 'FAIL'} "
          f"(2 mm variant must fail the contract)")

    if main_ok and neg_ok:
        final.export(OUT / "brim015_bucaro.stl")
        print(f"EXPORTED brim015_bucaro.stl  "
              f"({len(final.faces)} faces, {final.volume/1000:.1f} cm^3)")
        sys.exit(0)
    print("CONTRACT FAILED — nothing exported")
    sys.exit(1)
