# club_sofa — volledige reverse-engineering + eigen productiepijplijn

> Doel van deze sessie: **niet** Habbo documenteren, maar bewijzen dat we met
> moderne tools zelf isometrische furniture kunnen ontwerpen en produceren.
> `club_sofa` is puur de technische referentie: het laat zien welk *contract*
> een renderer verwacht (schaal, perspectief, richtingen, lagen, offsets,
> placement, interactie, z-order).
>
> Alles hieronder is afgeleid uit de **echte assetbestanden** in
> `reference/club_sofa/` en uit de broncode van `xabbo.io/nx` (de imager) en
> `billsonnn/nitro-renderer` (de officiële open renderer). Niets is geraden.

---

## 0. Antwoord op de drie vragen (samenvatting)

**1. Kunnen we één volledig eigen furniture-item technisch correct maken?**
Ja. Het contract is klein en volledig in kaart gebracht. Een furniture-item =
`N` PNG-sprites + 4 XML-bestanden (`index`, `logic`, `visualization`, `assets`)
+ een regel in de furnidata. De renderer heeft niets magisch nodig: sprite op
`origin + (-offsetX, -offsetY)`, lagen sorteren op een z-getal, klaar. Onze
canvas-preview (`/preview`) rendert `club_sofa` uit de losse sprites en is
pixel-identiek aan wat `nx imager` produceert — dat is het bewijs dat we het
contract volledig begrijpen.

**2. Welke stappen kan AI betrouwbaar automatiseren?**

| Stap | Automatiseerbaar? | Hoe |
|---|---|---|
| Concept → 4 richtingen isometrische pixel-art | **Grotendeels** | image-gen met vaste iso-constraints (dimetrisch 2:1, 26.57°), per richting 1 render, style-lock via referentiebeelden |
| Sprite-splitsing in lagen (a/b/c/d) | **Deels** | segmentatie op diepte-hint (zit vóór avatar / rug achter avatar); of lagen apart genereren |
| Offsets / anchor berekenen | **Volledig** | deterministisch uit de bounding box + een vaste origin-conventie (zie §5) |
| `visualization.xml` / `logic.xml` / `assets.xml` schrijven | **Volledig** | template + de footprint/z-keuzes; puur codegen |
| flipH-richtingen (0↔6, 4↔2) | **Volledig** | gratis, één regel in `assets.xml` |
| Schaduw genereren | **Volledig** | footprint-diamant → geblurde ellips op alpha 46 |
| 32px-variant | **Volledig** | downscale 64px met nearest + handmatige touch-up (of los genereren) |
| QA: rendert het correct, staat de avatar goed? | **Volledig** | headless render + diff tegen verwachting (zie §9) |

**3. Hoe ziet een herhaalbare pijplijn eruit?** Zie §8. Kort:
`brief → art-gen (4 richtingen) → laag-split → auto-slice/trim → offset+anchor bereken →
XML/furnidata codegen → .nitro pakken → headless render-QA → mens keurt 1 plaatje goed`.
Na dit ene voorbeeld hoeft niemand nog iets handmatig uit te zoeken; alleen de
art-review per item blijft mensenwerk (1 blik, geen pixel-arbeid).

---

## 1. Wat is `club_sofa` volgens de data

Uit `nx furni info` + `metadata/`:

| veld | waarde | betekenis |
|---|---|---|
| `kind` | 267 | numerieke type-ID in de furnidata |
| `identifier` / class | `club_sofa` | naam van de library (SWF/`.nitro`) |
| `type` | `s` (floor) | staat op de vloer, niet aan de muur |
| `revision` | 45508 | asset-versie op de CDN |
| `category` | `chair` | UI-categorie |
| `line` | `old_hc_gifts` | furni-lijn |
| `xdim` × `ydim` | **2 × 1** | footprint in tegels |
| `z` (logic) | **1.0** | hoogte in tegel-eenheden |
| `defaultdir` | 0 | richting bij plaatsen |
| `cansiton` | **true** | avatars kunnen erop zitten |
| `canstandon` / `canlayon` | false / false | — |
| `partcolors` | `0,0,0` | geen kleurvarianten |
| `specialtype` | 1 | "gewoon" furni |
| `index.visualization` | `furniture_static` | render-klasse: geen animatie |
| `index.logic` | `furniture_basic` | gedrag-klasse: alleen afmetingen + richtingen |

---

## 2. Bestanden in de library

`nx get furni club_sofa` → `club_sofa.swf` (11 KB). `nx extract -i -d` pakt uit:

### 2a. Metadata (4 XML)

| bestand | inhoud |
|---|---|
| `club_sofa_index.xml` | koppelt visualization-klasse (`furniture_static`) + logic-klasse (`furniture_basic`) |
| `club_sofa_club_sofa_logic.xml` | `<dimensions x=2 y=1 z=1.0>` + toegestane richtingen `0,90,180,270` |
| `club_sofa_club_sofa_visualization.xml` | per size (1/32/64): `layerCount`, `angle`, laag-z, per-richting laag-overrides |
| `club_sofa_club_sofa_assets.xml` | per sprite: `x`, `y` offset, `flipH`, `source`-alias |
| `club_sofa_manifest.xml` | lijst van alle assets + mime-types (nitro-manifest) |

### 2b. Sprites (PNG, RGBA)

18 losse PNG's + 1 icoon. Naampatroon:

```
club_sofa_club_sofa_{size}_{layerletter}_{direction}_{frame}.png
                     └64/32┘  └a b c d sd┘  └0 2 4 6┘   └0┘
```

`a`=laag 0, `b`=laag 1, `c`=laag 2, `d`=laag 3, `sd`=schaduw (laag −1).
`icon_a` = het inventaris-icoon (size 1).

**Alleen richting 2 en 6 zijn echt getekend.** Richting 0 = 6 gespiegeld,
richting 4 = 2 gespiegeld (zie `assets.xml`, `flipH="1"` + `source=`).
Dat halveert het teken-werk — belangrijk voor onze pijplijn.

| sprite (size 64) | px (b-box) | rol |
|---|---|---|
| `64_a_2_0` | 67×64 | **zitkussens** (deel dat vóór de zittende avatar valt) |
| `64_b_2_0` | 70×66 | **rug + armleuningen + frame** (basisstructuur) |
| `64_c_2_0` | 50×47 | **los geel kussen** + stukje arm (ligt altijd bovenop) |
| `64_d_2_0` | 1×1 | leeg (alias naar `64_d_4_0`) — deze richting heeft geen laag 3 |
| `64_sd_2_0` | 105×35 | schaduw-ellips (footprint op de vloer) |
| `64_a_6_0` | 6×4 | vrijwel leeg — zitting is van achteren niet zichtbaar |
| `64_b_6_0` | 51×43 | zichtbare bovenrand rugleuning van achteren |
| `64_c_6_0` | 73×62 | geel kussen + arm van achteren |
| `64_d_6_0` | 45×53 | **bruine achterkant van de bank** (van achteren gezien) |

Size 32 = dezelfde structuur, ~halve resolutie, eigen (grovere) sprites — geen
kale downscale, ze zijn apart nagewerkt.

![lagen-grid](img/00_layers_grid_64.png)

---

## 3. Directions

- `visualization angle = 45` → richtingen in stappen van 45°. `club_sofa`
  gebruikt er **4**: `0, 2, 4, 6` (dus 90°-stappen). Renderers kennen 0–7.
- Mapping (Habbo-conventie, vanuit de default-camera):
  `0 = NO`, `2 = ZO`, `4 = ZW`, `6 = NW`.
- `logic.xml` noteert dezelfde 4 als `0/90/180/270` graden.
- De footprint (2×1) draait mee: richting **0 & 4** = lange kant NO–ZW;
  richting **2 & 6** = lange kant NW–ZO.

![4 richtingen met origin](img/01_directions_with_origin.png)

---

## 4. Layers

`layerCount = 4` (a,b,c,d) + de schaduw (−1). Per (richting) wordt elke laag
één keer geplaatst. `club_sofa` heeft geen animatie, dus per laag precies 1 frame.

**z-order** — dit is de kern van "hoe blijft de bank kloppen met een avatar erop":

Nitro (`FurnitureVisualizationData` → `SizeData`):

1. default `zOffset` van elke laag = `0`
2. `<layers><layer id=.. z=..>` zet het voor **alle** richtingen
3. `<directions><direction id=..><layer id=.. z=..>` overschrijft het voor
   **die** richting
4. XML-`z` → nitro: `zOffset = z / -1000`

Dan (`FurnitureVisualization.updateSprite`):

```
relativeDepth      = zOffset - layerId * 0.001
sprite.relativeDepth = relativeDepth * sqrt(0.5)      // DEPTH_MULTIPLIER
```

**Lager (meer negatief) relativeDepth = dichter bij de kijker = bovenop.**
De schaduw krijgt `relativeDepth = 1` (altijd onderaan).

Voor `club_sofa`:

| laag | `z` (xml) | zOffset | geldt voor | rol in de z-splitsing |
|---|---|---|---|---|
| c (2) | **1500** | −1.5 | alle richtingen | los kussen: **altijd helemaal vooraan** |
| a (0) | **1000** | −1.0 | **alleen dir 2 & 4** | zitkussen: **vóór** de zittende avatar |
| d (3) | **500** | −0.5 | **alleen dir 0 & 6** | bruine achterkant: **vóór** de avatar (die zit er "in") |
| b (1) | — | 0 | alle richtingen | rug/frame: **achter** de avatar |
| sd (−1) | — | (rd=1) | alle richtingen | schaduw op de vloer |

De **zittende avatar** is een los room-object. De room diepte-sorteert zijn
sprites tussen de furni-lagen: alles met sterk negatieve z (a, c, of d
afhankelijk van richting) tekent vóór de avatar, `b` (z≈0) erachter. Zo zit de
avatar visueel *in* de bank in plaats van erop geplakt.

**Tekenvolgorde per richting** (achter → voor), letterlijk uit `contract.json`:

| dir | volgorde |
|---|---|
| 0 | `sd → a → b → d → c` |
| 2 | `sd → b → d(leeg) → a → c` |
| 4 | `sd → b → d(leeg) → a → c` |
| 6 | `sd → a → b → d → c` |

> ⚠️ `nx imager` implementeert de **per-richting** z-overrides niet (het ziet
> alleen `c z=1500` en sorteert de rest op layer-ID). Voor `club_sofa` valt het
> resultaat toevallig samen omdat de overlap klein is. Onze preview volgt de
> **echte** nitro-regel. Voor eigen furni: reken op de nitro-regel, niet op nx.

![explode dir 2](img/02_explode_dir2.png)
![opbouw dir 2](img/03_buildup_dir2.png)

---

## 5. Offsets, anchor & alignment

`assets.xml` geeft per sprite een `x` en `y`. Betekenis (bevestigd in
`nx/imager/sprite.go` en de nitro-renderer):

```
sprite_top_left_op_canvas = furni_origin + (-x, -y)
```

Dus `x`/`y` = "hoeveel pixels staat de linkerbovenhoek van deze sprite links
van / boven de origin". De **origin `(0,0)`** is het projectiepunt van de
furni op het scherm — voor een floor-furni het schermpunt van de
plaatsings­tegel.

`club_sofa` size 64:

| dir | laag a | laag b | laag c | laag d | schaduw |
|---|---|---|---|---|---|
| 2 | (67,39) | (30,52) | (73,14) | — | (73,1) |
| 6 | (42,16) | (10,44) | (73,29) | (8,39) | (73,1) |

**flipH**: een gespiegelde sprite hergebruikt het bronbeeld; de offset wordt
`x' = -x + bronbreedte`, `y` blijft (`nx` `flipOffsetFurni`, nitro idem).

**Origin-conventie voor eigen furni** (deterministisch, geen giswerk nodig):

- Kies één canoniek canvas per size (bv. 64px: de iso-tegel is 64×32).
- De origin ligt op de **horizontale helft, verticaal op de vloerlijn** van de
  *anker*-tegel van de furni.
- Voor elke gerenderde laag: `offset = origin_in_art - laag_bbox_topleft`.
  Puur rekenen zodra je weet waar in je art-canvas de origin zit — daarom
  render je de art op een vast canvas met een zichtbaar origin-kruis.
- De schaduw-sprite ís de footprint; leg zijn midden op het footprint-midden
  en de offset volgt. (Zo doet onze preview de placement, en het klopt — zie
  `preview/preview-mockup.png`.)

![preview mockup](../preview/preview-mockup.png)

---

## 6. Footprint

- `xdim=2, ydim=1` → **2 tegels**, `z=1.0` hoog.
- 2 zit-plekken (`cansiton` + 2 tegels → 1 avatar per tegel).
- De footprint roteert met de richting (§3).
- Tegel = 64×32 px (size 64) of 32×16 px (size 32) — klassieke 2:1 dimetrische
  ("isometrische") projectie, hoek 26.565°.

---

## 7. Sitting behaviour

- `logic = furniture_basic` → geen eigen zit-hoogte in de XML.
- Habbo/nitro zet de avatar op de stapelhoogte van de tegel, posture `"sit"`
  (de sit-animatie verlaagt de figuur zelf ~0.5), avatar kijkt **dezelfde kant
  op als de bank**.
- De z-splitsing van §4 doet de rest: rug achter de avatar, zitkussen +
  los kussen ervoor.
- Wat een eigen sittable furni dus moet leveren:
  1. `cansiton=true` in de furnidata,
  2. minstens één laag met sterk negatieve z (het zit-deel) en één met z≈0
     (de rug),
  3. per richting de juiste laag naar voren halen via `<direction><layer z>`.

---

## 8. States / animaties / kleuren

- `club_sofa`: **1 state (0)**, geen `<animations>`, geen `<colors>`.
- Het contract ondersteunt wel:
  - **animaties**: `<animations><animation id><animationLayer><frameSequence>`
    → per laag een frame-reeks; sprites krijgen `_1_`, `_2_`, … in de naam.
  - **kleurvarianten**: `<colors><color id><colorLayer id color="RRGGBB">`
    → dezelfde sprite, gemultiplied met een kleur (bv. bankstellen in 6 tinten
    uit één set grijswaarde-sprites — zeer relevant voor volume-productie).

---

## 9. Wat de renderer minimaal nodig heeft

1. de sprite-PNG's per `(size, layer, direction, frame)`
2. `assets.xml`: `offset (x,y)` + `flipH` + `source` per sprite
3. `visualization.xml`: `layerCount`, `angle`, laag-z (basis + per richting),
   evt. `alpha`/`ink`
4. `logic.xml`: footprint `x/y/z` + toegestane richtingen
5. `index.xml`: welke visualization-/logic-klasse
6. furnidata-regel: `cansiton/canstandon/canlayon`, `xdim/ydim`, `defaultdir`,
   `category`, `specialtype`
7. een origin→scherm iso-projectie; daarna elke sprite op `origin + (-x,-y)`
8. diepte-sortering die de avatar tussen de lagen kan mengen

Alle 8 staan machine-leesbaar in [`contract.json`](contract.json).

---

## 10. De eigen furniture-generation pijplijn

```
┌─ 1. BRIEF ──────────────────────────────────────────────────────────────┐
│  mens of Astra: "2-zits fauteuil, mosterdgeel fluweel, art-deco".        │
│  + gekozen: footprint (xdim,ydim,z), cansiton?, category, #states.       │
└────────────────────────────────────────────────────────────────────────┘
        ↓
┌─ 2. ART-GEN (AI) ──────────────────────────────────────────────────────┐
│  image-gen met vaste constraints:                                       │
│   • dimetrisch 2:1, hoek 26.57°, camera vast                            │
│   • canvas 64×64+ met zichtbaar origin-kruis + tegel-diamant            │
│   • style-lock via 3–5 referentiebeelden (onze eigen huisstijl)         │
│   • render richting 2 en 6 apart (0 en 4 = later gratis gespiegeld)     │
│   • per richting óf 1 plat beeld óf de lagen los (rug / zit / los kussen)│
│  output: 2–4 PNG per item.                                              │
└────────────────────────────────────────────────────────────────────────┘
        ↓
┌─ 3. LAAG-SPLIT (AI + regels) ─────────────────────────────────────────┐
│  als art plat kwam: segmenteer op diepte-hint                          │
│   → "wat overlapt een zittende figuur van voren" = laag a (z 1000)     │
│   → "rug/frame" = laag b (z 0)                                         │
│   → losse objecten (kussen) = laag c (z 1500)                          │
│   → van-achteren-paneel = laag d (z 500, alleen dir 0/6)              │
│  schaduw: footprint-diamant → blur → alpha 46.                         │
└────────────────────────────────────────────────────────────────────────┘
        ↓
┌─ 4. SLICE / TRIM / OFFSET (deterministisch, geen AI) ─────────────────┐
│  • autocrop elke laag naar zijn alpha-bbox                            │
│  • offset = origin_in_canvas − bbox_topleft                           │
│  • dir 0 = flipH(dir 6), dir 4 = flipH(dir 2): offset' = -x + w       │
│  • 32px-set: downscale + (optioneel) AI-touch-up                      │
└────────────────────────────────────────────────────────────────────────┘
        ↓
┌─ 5. CODEGEN (templates) ─────────────────────────────────────────────┐
│  schrijf index.xml, logic.xml, visualization.xml, assets.xml,         │
│  manifest.xml + furnidata-regel uit de brief + de offsets.            │
│  pak alles als .nitro (of SWF-vervanger) — 1 script.                  │
└────────────────────────────────────────────────────────────────────────┘
        ↓
┌─ 6. HEADLESS QA ────────────────────────────────────────────────────┐
│  • render alle richtingen/states headless (onze preview-renderer of  │
│    nitro headless)                                                   │
│  • checks: geen laag buiten canvas, footprint == shadow-bbox,        │
│    silhouet-continuïteit tussen richtingen, avatar-z-test (plaats    │
│    testfiguur, controleer dat rug achter / zit voor valt)            │
│  • diff 0↔6 en 2↔4 spiegel-consistentie                             │
└────────────────────────────────────────────────────────────────────────┘
        ↓
┌─ 7. MENS KEURT ────────────────────────────────────────────────────┐
│  1 contactsheet (4 richtingen + avatar erop) → go/no-go.            │
│  geen pixel-arbeid; alleen smaak/branding.                          │
└───────────────────────────────────────────────────────────────────┘
```

**Wat blijft mensenwerk:** de art-review in stap 7 (één blik), en soms een
lichte retouche in stap 3 als de laag-split rafelt. Al het rekenwerk (4, 5, 6)
is puur deterministisch en herbruikbaar — dat is precies wat deze analyse
oplevert.

**Wat we hierna één keer moeten bouwen:**
- `tools/pack.py` — codegen + `.nitro`-packer (stap 5) — het contract staat al in `contract.json`
- `tools/qa.py` — headless render + de checks uit stap 6 (de preview-renderer is de basis)
- een style-guide + referentieset voor de image-gen (stap 2)
- een klein promptsjabloon voor de laag-split (stap 3)

---

## 11. Reproduceren

```bash
go install xabbo.io/nx/cmd/nx@latest
cd reference/club_sofa && nx get furni club_sofa
nx extract -i -d club_sofa.swf
cd ../.. && python3 tools/analyze.py      # → contract.json + img/ + preview/data
cd preview && python3 -m http.server 8899 # → open http://localhost:8899
```

Bestanden: `reference/club_sofa/{asset,extracted,sprites,metadata,directions,renders}`,
`analysis/{README.md,contract.json,img/}`, `preview/{index.html,data/}`.
