# THE LITHOPHANE MACHINE — Guide for Claude
### Photo in, 3D-printable STL out (by Peter + Claude, 2026)

A lithophane looks like a blank white plate until you hold it against a light — then the
picture appears. It works because **dark parts of the photo are printed THICK** (little
light gets through) and **bright parts THIN**. No paint, no ink: the picture is the
geometry.

Peter has a Prusa MK4S and sells prints at school (his shop is called **3D**), so this tool
exists to make things he can actually sell. The hexagon shape is his logo shape.

---

## RUN IT

```bash
python3 lithophane.py                      # after editing the CONFIG block
python3 photo_machine.py cat.jpg --litho hex     # or via the launcher, with presets
```

Then **look at the preview PNG yourself** before showing him — it simulates the backlit
result. If the preview is a grey mush, the photo has no contrast: raise `CONTRAST`, try
`GAMMA 1.3`, or pick a photo with a bright background.

## PRESETS (in photo_machine.py)

| Preset | What it is | Size |
|---|---|---|
| `card` | the standard photo plate | 100 mm, rounded |
| `hex` | **the 3D logo shape**, pointy top, hanging hole | 70 mm |
| `hex-flat` | flat-top hexagon | 70 mm |
| `ornament` | round, hole to hang it | 60 mm |
| `badge` | small and quick, sells cheap | 40 mm |
| `curved` | bent around a lamp (warps less, looks pro) | 100 mm, R60 |
| `night` | big framed night-light panel | 130 mm |

## THE DIALS (he asks in words — translate them yourself)

- **"bigger" / "smaller"** → `WIDTH_MM` (height follows the photo's shape unless you set it)
- **"more contrast" / "you can't see it"** → raise `MAX_MM` to 3.4, and `CONTRAST` to 1.3
- **"too dark when lit"** → lower `MAX_MM` (2.4) or `GAMMA` (0.85)
- **"thinner so it glows more"** → `MIN_MM = 0.6` (below 0.6 gets fragile and can crack)
- **"hexagon" / "logo shape"** → `SHAPE = "hex"` (pointy top) or `"hex-flat"`
- **"round" / "ornament"** → `SHAPE = "circle"` + `HANG_MM = 4`
- **"put a frame on it"** → `FRAME_MM = 3` (solid border at full thickness)
- **"bend it round a lamp"** → `CURVE_R = 60` (radius in mm)
- **"faster / smaller file"** → lower `RES` (3.5 is still fine)

## PRINT SETTINGS (tell him these every time)

- **White filament.** PETG or PLA both work; white transmits light best. Black is useless.
- **Print it standing up**, exactly as the STL is oriented — layer lines then run across
  the picture and stay invisible.
- **0.1 mm layers**, no supports, and it must be **solid** (100 % infill is the simplest
  way; lots of perimeters also works). Gaps inside = ugly bright stripes when lit.
- Slow it down a bit if the surface looks rough; this is one print where speed hurts.
- Backlight with a window, a phone torch, or an LED tealight behind an `ornament`.

## HOW IT WORKS (to rebuild it)

1. Photo → greyscale → resize to a grid of `WIDTH_MM × RES` samples, unsharp-masked
   (fine detail survives the plastic better), contrast/gamma applied.
2. `thickness = MIN_MM + (1 − brightness) × (MAX_MM − MIN_MM)`.
3. A **mask** decides which grid cells exist — that is how rounded corners, circles,
   hexagons, arches and the hanging hole all work with the same code.
4. Mesh: for every active cell, a front quad at its thickness and a back quad at 0; wherever
   a neighbouring cell is missing, a side wall quad is emitted. That makes any shape
   watertight without special cases.
5. Flat panels merge the back into **row-long quads** (huge triangle saving). Curved panels
   can't — their back has to follow the cylinder.
6. `CURVE_R` maps X onto a cylinder: `θ = (x − w/2)/R`, front at radius `R + thickness`.
7. Written as **binary STL**, with normals computed per triangle. X = across, Z = up,
   Y = thickness, so it lands on the build plate standing upright.

## THINGS THAT WENT WRONG (learn from me)

- **First version made a 38 MB STL for a 70 mm hexagon** (811k triangles). Two fixes:
  `RES` above ~5/mm is pointless (a 0.4 mm nozzle cannot print 0.12 mm detail), and the
  flat back does not need one quad per cell. Now the same part is 6 MB and builds in 3 s.
- A PNG with the **transparency checkerboard baked into the pixels** becomes a checkerboard
  lithophane. Check a corner pixel first. (`kit/build/snippet.png` is the clean Snippet.)
- Do not trust a preview at a glance — I misread my own hexagon's orientation until I
  measured the mask. Measure, then claim.

## GOOD SUBJECTS

Faces with a bright background, pets, a skyline at dusk, anything with clear light/dark
separation. Bad: flat cartoons with big empty areas (they print as a blank slab), dark
photos, busy backgrounds. Cartoons work better as `badge`/`hex` where the outline carries it.

— Claude

---

## VARIABLE TRIANGLES (`ADAPTIVE = True`)

A plain heightfield spends the same number of triangles on an empty sky as on an eyelash.
This version doesn't: it runs a **quadtree** over the height map and merges any square block
whose surface stays within `TOL_MM` of a flat approximation, then fans that block into a
handful of triangles instead of hundreds.

Rules that keep it safe:

- A block only merges if **all its cells plus a one-cell ring around them** are inside the
  plate. That keeps the whole outline (and the hanging hole) at full resolution, so the
  side walls always meet vertices that exist.
- The tree is **balanced**: no leaf may touch a neighbour more than twice as small. Where a
  coarse leaf meets a finer one, the coarse leaf's edge gets a **midpoint vertex** so the
  two sides share identical edges. Without this you get cracks.
- The **back is fanned with the same leaves as the front**, so both surfaces share the exact
  same boundary edges.

**How much does it save?** It depends entirely on the picture, and the honest numbers are:
- Snippet (flat cartoon colours, lots of empty space): **42 % fewer triangles**
- a brush-painted landscape (texture in every single pixel): **9 %**

Nothing flat means nothing to merge. That's not a bug, that's the picture.

Dials: `TOL_MM` 0.02 fussy / 0.04 default / 0.08 aggressive · `MAX_BLOCK` 16 · `ADAPTIVE = False`
for a plain grid.

## THE MESH CHECK (`VERIFY = True`)

After writing the STL the machine counts every edge. In a solid, **each edge must be used
by exactly two triangles**. It prints `WATERTIGHT` or the number of bad edges.

This caught a real bug: an earlier version merged the flat back into long row-quads, which
looked fine and sliced fine, but where one row's run length differed from the next it left
**936 T-junction cracks**. The fix was making the back follow the front's mesh. Always run
the check; do not trust a mesh because the preview looks nice.

## THE BORDER / FRAME (`FRAME_MM`)

`FRAME_MM = 4` puts a solid border around the plate. It is **shape-following**: it is built
from a distance transform of the mask, so it hugs a hexagon's six sides, a circle's curve,
rounded corners — and it automatically puts a matching rim around the hanging hole, which
is also the part most likely to snap, so the ring is doing real work.

- `FRAME_THICK_MM = 0` → the border is `MAX_MM` thick (flush with the darkest picture areas)
- `FRAME_THICK_MM = 4.5` → a **raised rim**, like a picture frame. It looks deliberate, it
  blocks the light cleanly at the edge, and it stiffens a thin plate so it warps less.
- `FRAME_STEP = False` → the border blends into the picture instead of stepping.

Preset `plaque` = 80 mm hexagon + 4 mm raised rim + hanging hole. That is the one to print
as a shop sign or a gift: it reads as a finished object rather than a slab.

(The old version drew the border with straight x/y limits, which put a rectangle across a
hexagon. If a border ever looks wrong for a shape, that's the bug to look for.)
