# 3D product models

Every category has a real model. They are wired up by category rather than by
product, in `src/app/core/data/catalog.data.ts`:

```ts
const CATEGORY_MODELS: Partial<Record<ProductCategory, string>> = {
  tshirt: '/assets/models/tshirt.glb',
  hoodie: '/assets/models/hoodie.glb',
  mug:    '/assets/models/mug.glb',
  cap:    '/assets/models/cap.glb',
};
```

A premium tee and a classic tee are the same shape — the difference is in the
fabric, not the silhouette — so they share a model the same way they share a
mockup photo. A product that needs its own can still set `model` on itself and
that wins.

## What is installed

| File | Size | Meshes | Notes |
| --- | --- | --- | --- |
| `tshirt.glb` | 4.7 MB | 1 | |
| `hoodie.glb` | 14 MB | 4 | Split into panels; **the heaviest asset in the app** |
| `mug.glb` | 275 KB | 1 | |
| `cap.glb` | 7.1 MB | 3 | Declares `KHR_materials_pbrSpecularGlossiness` |

Two things worth knowing before swapping any of them out:

**`hoodie.glb` is 14 MB.** That only downloads when a customer opens the 3D tab,
so it never touches the first paint of a page, but it is still a slow first turn
on a phone. Running it through `gltf-transform optimize` (Draco geometry plus
WebP textures) typically takes a model like this under 3 MB with no visible
difference. Worth doing before launch; not worth blocking on.

**`cap.glb` uses a retired material extension.** `KHR_materials_pbrSpecularGlossiness`
was deprecated in favour of metallic-roughness and modern three.js no longer
reads it natively, so the cap loads with plain colours instead of its authored
maps. It looks fine — a white cap is close to plain white — but it will never
gain fabric texture until the model is converted. `gltf-transform metalrough`
does the conversion in one command.

## Requirements for a replacement

| | Requirement | Why |
| --- | --- | --- |
| **Format** | glTF binary (`.glb`) | One file, textures embedded, loads natively |
| **Scale** | Real-world metres | The viewer normalises size, but physical scale keeps proportions honest between products |
| **Orientation** | Y up, front facing +Z | The print is projected from +Z; a model facing sideways prints on its ribs |
| **Origin** | Centre of the object | Not at the feet — an off-centre origin makes it orbit around a point outside itself |
| **Materials** | The printable surface named `fabric` | The colour swatches recolour that material. Without the name every material is recoloured except things named like hardware (`metal`, `buckle`, `zip`, `eyelet`, …), which is right far more often than it is wrong |
| **Maps** | Base colour, normal, roughness | The normal map is what makes cotton read as cotton rather than plastic |
| **Budget** | Roughly 30–80k triangles | Enough for a soft silhouette; past that it is only download size |

Optional but worth having: a lightly wrinkled garment rather than a perfectly
smooth one. Real cloth is never flat, and the wrinkles are most of what tells a
viewer they are looking at fabric.

## What you do *not* need to worry about

**UV layout.** The design is applied as a decal — projected onto the surface
from the front and clipped to whatever it lands on — rather than drawn into the
model's texture atlas. An off-the-shelf model whose UVs were laid out for
texturing works exactly as it comes.

**How many meshes it is cut into.** The print is projected onto every mesh in
the model, not the largest one. The hoodie is four separate meshes sharing one
material, and aiming at a single mesh put the design on a body panel while the
chest belonged to another — what you saw was a couple of stray slivers where the
chosen mesh happened to cross the projector.

## Two things that *are* easy to get wrong

**Which way it faces.** The viewer squares up a model that arrived a quarter
turn out by comparing its width against its depth: a garment is much wider than
it is deep, so one deeper than it is wide must be side-on. A cap breaks that —
with the peak sticking out it really is longer front-to-back than across, and
the heuristic turns a cap that was already facing forward. That is what
`CATEGORY_MODEL_ROTATION` is for, and why `cap: 0` in it is a real instruction
rather than a missing value.

**Where the print is centred.** Not on the bounding box. Anything with a part
sticking out of one side drags the box centre away from the surface being
printed — a mug's handle moves it off the barrel by half the handle's width, and
the design then runs off the left edge of the only surface it was meant for. The
viewer centres on the vertices nearest the front instead, which leaves the
handle out because it sits at the mug's waist, not on its face.

## Checking one

Open `/customize/<slug>` and press **3D**. If the model loads it appears
immediately; if the file is missing or malformed the viewer falls back to
primitive geometry and says so on screen rather than showing an empty frame.
Then upload any design and turn the product all the way round: the print should
stay on the surface, wrap where the surface curves, and not appear on the back.
