#!/usr/bin/env python3
"""gallery_case.py — iPhone 18 Pro case with a grid of little painting frames.

Paintings go in from the INSIDE: drop each card into its pocket, then snap the
phone in; the phone holds every painting against its window lip. Swap them by
popping the phone out. No slots, no glue, no clear windows to scratch.

Phone body: 150.0 x 71.9 x 8.75 mm (Apple spec). Camera plateau, button
positions and corner radius are ESTIMATES; measure your phone and edit PARAMS.
Print a test ring first (see README).
"""
import math, json
import cadquery as cq

P = dict(
    phone_h=150.0, phone_w=71.9, phone_d=8.75,
    corner_r=11.0,          # phone corner radius (estimate; measure)
    fit=0.25,               # gap per side between phone and case
    wall=1.6,               # side wall thickness
    back=2.4,               # back thickness (pocket 1.2 + window wall 1.2)
    lip=1.1,                # how far the front lip wraps over the screen edge
    lip_t=1.2,              # front lip thickness (height above the glass)
    # camera plateau cutout, measured from the top edge of the phone (estimates!)
    plateau_w=63.0, plateau_h=42.0, plateau_top=4.5, plateau_r=8.0, plateau_clear=1.5,
    # painting grid: cols x rows of card_w x card_h, with pocket depth card_t
    cols=2, rows=3, card_w=26.0, card_h=28.0, card_t=1.2, gap=3.0, frame=1.2,
    grid_top_margin=4.0,    # below the plateau cutout
    # side cutouts (y from top of phone): left = action + volume, right = side button, camera control
    left_cut=(32.0, 86.0), right_cuts=((38.0, 66.0), (96.0, 124.0)), cut_depth_margin=1.0,
    bottom_cut_w=52.0,
)

def build(P):
    H, W, D = P["phone_h"] + 2*P["fit"], P["phone_w"] + 2*P["fit"], P["phone_d"]
    wall, back, lip, lip_t = P["wall"], P["back"], P["lip"], P["lip_t"]
    R = P["corner_r"] + P["fit"]
    total_h = back + D + lip_t                      # z: 0 = outside of back
    OH, OW, OR = H + 2*wall, W + 2*wall, R + wall

    # outer shell
    body = cq.Workplane("XY").rect(OW, OH).extrude(total_h).edges("|Z").fillet(OR)
    # phone cavity (from z=back up through the top)
    cav = (cq.Workplane("XY").workplane(offset=back).rect(W, H).extrude(D + lip_t + 1)
           .edges("|Z").fillet(R))
    body = body.cut(cav)
    # front lip: re-add a frame over the screen edge, leaving the screen open
    lip_ring = (cq.Workplane("XY").workplane(offset=back + D).rect(OW, OH).extrude(lip_t)
                .edges("|Z").fillet(OR))
    lip_hole = (cq.Workplane("XY").workplane(offset=back + D).rect(W - 2*lip, H - 2*lip).extrude(lip_t + 1)
                .edges("|Z").fillet(max(0.5, R - lip)))
    body = body.union(lip_ring.cut(lip_hole))

    # soften outer edges
    try:
        body = body.faces("<Z").edges().fillet(1.0)
    except Exception:
        pass

    # camera plateau cutout (through back), positioned from the top edge
    pw, ph = P["plateau_w"] + 2*P["plateau_clear"], P["plateau_h"] + 2*P["plateau_clear"]
    py = H/2 - P["plateau_top"] - P["plateau_h"]/2     # centre y (phone centred at origin, +y = top)
    plateau = (cq.Workplane("XY").center(0, py).rect(pw, ph).extrude(back + 1)
               .edges("|Z").fillet(P["plateau_r"]))
    body = body.cut(plateau)

    # painting grid below the plateau
    cols, rows = P["cols"], P["rows"]
    cw, ch, ct, g, fr = P["card_w"], P["card_h"], P["card_t"], P["gap"], P["frame"]
    grid_w = cols*cw + (cols-1)*g
    grid_h = rows*ch + (rows-1)*g
    top_y = py - ph/2 - P["grid_top_margin"]
    pockets = []
    for c in range(cols):
        for r in range(rows):
            cx = -grid_w/2 + cw/2 + c*(cw + g)
            cy = top_y - ch/2 - r*(ch + g)
            # pocket from the inside, card-deep
            pocket = (cq.Workplane("XY").workplane(offset=back - ct).center(cx, cy)
                      .rect(cw, ch).extrude(ct + 0.01).edges("|Z").fillet(1.0))
            # window through the remaining wall, smaller by the frame lip
            window = (cq.Workplane("XY").center(cx, cy).rect(cw - 2*fr, ch - 2*fr)
                      .extrude(back).edges("|Z").fillet(1.0))
            body = body.cut(pocket).cut(window)
            pockets.append((cx, cy))
    if top_y - grid_h < -H/2 + 4:
        raise ValueError("painting grid runs off the bottom of the phone; reduce rows or card_h")

    # side cutouts (buttons): full wall height above the back, from the lip down
    def side_cut(x_sign, y0, y1):
        yc = H/2 - (y0 + y1)/2; ln = y1 - y0
        return (cq.Workplane("XY").workplane(offset=back + P["cut_depth_margin"])
                .center(x_sign*(W/2 + wall/2), yc).rect(wall + 2, ln)
                .extrude(D + lip_t + 1))
    body = body.cut(side_cut(-1, *P["left_cut"]))
    for yc in P["right_cuts"]:
        body = body.cut(side_cut(+1, *yc))
    # bottom cutout (USB-C, speakers, mic): through bottom wall, open to the front
    bottom = (cq.Workplane("XY").workplane(offset=back + P["cut_depth_margin"])
              .center(0, -(H/2 + wall/2)).rect(P["bottom_cut_w"], wall + 2)
              .extrude(D + lip_t + 1))
    body = body.cut(bottom)
    return body, dict(outer_mm=[round(OW,1), round(OH,1), round(total_h,2)],
                      grid=f"{cols}x{rows} of {cw}x{ch} mm, cards up to {ct} mm thick",
                      window_mm=[cw - 2*fr, ch - 2*fr], pockets=len(pockets))

if __name__ == "__main__":
    case, info = build(P)
    cq.exporters.export(case, "gallery_case.stl", tolerance=0.02, angularTolerance=0.2)
    cq.exporters.export(case, "gallery_case.step")
    # fit-test ring: a 20 mm band of the case around the phone's lower half, prints in minutes
    ring = (build(P)[0].intersect(cq.Workplane("XY").workplane(offset=0)
            .center(0, -(P["phone_h"]/2) + 25).rect(200, 50).extrude(50)))
    cq.exporters.export(ring, "fit_test_ring.stl", tolerance=0.02, angularTolerance=0.2)
    info["volume_cm3"] = round(case.val().Volume()/1000, 1)
    json.dump(info, open("info.json", "w"), indent=1); print(json.dumps(info, indent=1))

    # painting template: the cards to paint and cut out
    cw, ch = P["card_w"], P["card_h"]; n = P["cols"]*P["rows"]
    cells = "".join(
        f'<rect x="{15 + (i%4)*(cw+6)}" y="{15 + (i//4)*(ch+6)}" width="{cw}" height="{ch}" '
        f'fill="none" stroke="#999" stroke-width="0.2"/>'
        f'<rect x="{15 + (i%4)*(cw+6) + P["frame"]}" y="{15 + (i//4)*(ch+6) + P["frame"]}" '
        f'width="{cw-2*P["frame"]}" height="{ch-2*P["frame"]}" fill="none" stroke="#bbb" stroke-width="0.15" stroke-dasharray="1 1"/>'
        for i in range(n + 2))
    open("painting_template.svg", "w").write(
        f'<svg xmlns="http://www.w3.org/2000/svg" width="150mm" height="100mm" viewBox="0 0 150 100">'
        f'<text x="15" y="10" font-size="4" font-family="sans-serif">cut on solid line ({cw}x{ch} mm); '
        f'dashed = visible area through the frame; print at 100%</text>{cells}</svg>')
