# Color Book — Architecture Guide

A toddler-targeted (ages 2–6) coloring book game built on the same **Strict Modular Monolith** pattern as the Bus Game. Powered by **VContainer** for DI, **UniTask** for async, **Addressables** for shipped content, and a **Canvas-only UI** render strategy.

This document is the canonical reference for the Color Book game's structure. The Bus Game's [Darkmatter Architecture Guide](../Assets/Darkmatter_Architecture_Guide.md) is the parent contract; this doc only adds game-specific structure.

---

## 1. Game Flow

Four scenes. Each gets its own scope. Root services persist across all of them.

```
┌─────────────────┐
│  Boot.unity     │  RootLifetimeScope — services + cross-scene singletons
│                 │  AppBoot:  Addressables.Init → Progression.Load → Catalog.Init
└────────┬────────┘
         │ Scenes.LoadAsync("MainMenu")
         ▼
┌─────────────────┐
│ MainMenu.unity  │  Spine mascot looping idle. Single "Play" button.
│                 │  Tap → SetLastOpened(null) → load Colorbook
└────────┬────────┘
         │ Scenes.LoadAsync("Colorbook")
         ▼
┌─────────────────┐  ◀─────────────────────────┐
│ Colorbook.unity │  Catalog grid of drawings. │  (back from Gameplay returns here;
│                 │  Each cell shows: cached  │   catalog cells refresh with cached
│                 │  user thumbnail if any,   │   thumbnails written during gameplay)
│                 │  else DefaultThumbnail.   │
│                 │  Tap cell →                │
│                 │  Progression.SetLastOpened(id) → load Gameplay
└────────┬────────┘                            │
         │ Scenes.LoadAsync("Gameplay")        │
         ▼                                     │
┌─────────────────┐                            │
│ Gameplay.unity  │  Active drawing experience │
│                 │                            │
│  Reads Progression.LastOpenedTemplateId      │
│  Reads Progression.GetProgress(id) → null,   │
│        Building, or Coloring                 │
│                                              │
│  ┌──────────────────────────────────────┐    │
│  │  GameplayState.Building              │    │
│  │   • Pieces in tray, drag → snap      │    │
│  │   • Pre-snapped pieces auto-locked   │    │
│  │     if resuming                      │    │
│  │   • Back tap → save partial state   ─┼────┤
│  │                + load Colorbook      │    │
│  └────────────────┬─────────────────────┘    │
│                   │ ShapeAssembledSignal      │
│                   │ (save phase + thumb)      │
│                   ▼                           │
│  ┌──────────────────────────────────────┐    │
│  │  GameplayState.Coloring              │    │
│  │   • Tap color → tap region → paint   │    │
│  │   • Undo / Redo any time             │    │
│  │   • Autosave (debounced 500 ms)      │    │
│  │   • Save tap → capture + native     ─┼────┐
│  │     Photos save + cache thumb        │    │
│  │   • Next tap → save + mark complete  │    │
│  │     + advance to next drawing  ──────┼─┐  │
│  │   • Back tap → save + load Colorbook─┼────┤
│  └──────────────────────────────────────┘ │  │
│                                            │  │
└────────────────────────────────────────────┼──┘
                                             │
                  ┌──────────────────────────┘
                  │ AdvanceToNextDrawing:
                  │ Catalog.NextUnseen(currentId) → reload Gameplay
                  └─ stays in Gameplay.unity, no scene transition
```

The user views captured drawings inside the phone's native **Photos** app — there is no in-app gallery viewer. `ICaptureService` produces PNG bytes; `IGalleryService` is a thin shim over a native plugin that writes those bytes into the device's photo library. The Save System (§13) decides *when* to capture and save.

---

## 2. Philosophy

Identical to Bus Game:

- **Vertical slices** — code grouped by Feature, not by type.
- **Strict layering** — dependencies flow downward only.
- **Composition over inheritance** — wired by DI.
- **Code vs Content** — `Code/` for logic, `Contents/` for assets.

Game-specific additions:

- **Toddler-first UX** — large hitboxes, forgiving snap radii, no fail states, no timers.
- **Stateless replay** — every action (paint, snap) is an `ICommand` so undo/redo and auto-save are trivial.
- **Capture-as-truth** — the gallery is a folder of PNGs on disk, not a serialized scene graph. What the child sees is what gets saved.

---

## 3. Dependency Graph

```
App   ──────────┐
                ▼
Features ──► Core ◄── Services
                ▲
                └── Libs
```

### Forbidden actions

- Services cannot reference Features.
- Features cannot reference Service implementations — only Core contracts via DI.
- Features cannot reference other Features — use `IEventBus` signals or Core contracts.
- Core may not contain logic. Interfaces, enums, DTOs, signal records only.
- Core may reference `UniTask` for async contract signatures. Nothing else.

---

## 4. Folder Structure

This section reflects the **actual project on disk today**. Empty folders that have been reserved for upcoming work are marked `(planned)`; everything else has at least one file in it. Aspirational additions for the rest of the game are listed in §4c at the bottom.

### 4a. Actual layout on disk

```
Assets/Darkmatter/
├── Scenes/
│   └── Boot.unity                                          ← only scene wired so far
│
├── Content/                                                ← singular ("Content", not "Contents")
│   └── Gameplay/
│       └── PaperRig/                                       ← stale folder — paper rig dropped; safe to delete
│
├── Data/
│   ├── Inputs/                                             (Input System .inputactions)
│   └── Settings/
│       ├── Persistance/Resources/                          (ProtectedPlayerPrefs settings)
│       ├── Rendering/                                      (URP renderer + asset)
│       └── Scenes/URP2DSceneTemplate.unity
│
└── Code/
    ├── App/
    │   └── LifetimeScopes/
    │       └── RootLifetimeScope.cs                        ← scope loads serialized IServiceModule list
    │   Darkmatter.App.asmdef
    │
    ├── Core/                                               (asmdef name: `Core`, namespace root `Darkmatter.Core.*`)
    │   ├── Compatibility/
    │   │   └── IsExternalInit.cs                           (C#9 init shim for older runtimes)
    │   ├── Contracts/
    │   │   ├── Paper/                                      ← stale empty folder — delete (Paper is no longer a feature)
    │   │   └── Services/
    │   │       ├── Assets/IAssetProviderService.cs
    │   │       ├── Audio/IAudioService.cs, ISfxPlayer.cs
    │   │       ├── Camera/ICameraService.cs
    │   │       ├── Capture/                                ← (planned — ICaptureService)
    │   │       ├── Inputs/IInputReader.cs
    │   │       └── Scenes/ISceneService.cs
    │   ├── Data/
    │   │   ├── Dynamic/Services/Audio/                     (AudioHandle, AudioRequest)
    │   │   └── Static/Services/Audio/                      (SfxCatalogSO)
    │   └── Enums/
    │       └── Services/
    │           ├── Audio/                                  (AudioChannel, AudioPlayMode, SfxId)
    │           ├── Camera/CameraType.cs                    (MainCamera, UICamera — ArtCamera not added yet)
    │           └── Scenes/GameScene.cs
    │
    ├── Features/                                           ← (planned — empty folder today)
    │
    ├── Libs/
    │   ├── FSM/                                            (IState, State, StateMachine + Docs)
    │   │   Libs.FSM.asmdef
    │   ├── Installers/                                     (IServiceModule — Register(IContainerBuilder))
    │   │   Libs.Installers.asmdef
    │   ├── Observer/                                       (IEventBus, EventBus — note: not named "EventBus")
    │   │   Libs.Observer.asmdef
    │   ├── PlayerPrefs/                                    (ProtectedPlayerPrefs — used in place of a Persistence service)
    │   │   ├── Editor/   Libs.PlayerPrefs.Editor.asmdef
    │   │   └── Runtime/  Libs.PlayerPrefs.asmdef
    │   └── UI/                                             (ToggleButton, ToggleButtonGroup)
    │       Libs.UI.asmdef
    │
    └── Services/
        ├── Analytics/
        │   ├── Installers/AnalyticsServiceModule.cs
        │   └── Systems/FirebaseAnalyticsSystem.cs
        │   Services.Analytics.asmdef
        ├── Assets/
        │   ├── AddressableAssetProviderService.cs
        │   └── AddressableLoadHandleTracker.cs
        │   Services.Assets.asmdef
        ├── Audio/
        │   ├── AudioService.cs
        │   └── SfxPlayer.cs
        │   Services.Audio.asmdef
        ├── Camera/
        │   ├── Installers/CameraServiceModule.cs
        │   └── Service/CameraService.cs
        │   Services.Camera.asmdef
        ├── Inputs/
        │   ├── Generated/GameInputs.cs                     (Input System codegen)
        │   ├── Installers/InputServiceModule.cs
        │   └── Readers/InputReaderSO.cs
        │   Services.Inputs.asmdef
        └── Scenes/
            └── SceneService.cs
            Services.Scenes.asmdef
```

### 4b. Conventions visible in current code

- **Asmdef per Service / Lib / App / Core.** No Feature asmdefs yet (folder is empty).
- **Core sub-tree shape:** `Core/{Compatibility, Contracts, Data, Enums}` — deeply nested by Service rather than by topic. Game-specific Core types (Drawing, Coloring, Paper, Gallery, History, Progression, Signals) will be added under either `Core/Contracts/<Area>/` or new top-level `Core/<Area>/` — pick one convention before adding the first one.
- **Service sub-shape varies** — some are flat (`Services/Audio/*.cs`), some split (`Services/Camera/Installers/`, `Services/Camera/Service/`). The "official" per-feature shape (§4d) hasn't been validated against real code yet.
- **Installer pattern:** `MonoBehaviour, IServiceModule` with `Register(IContainerBuilder builder)`. **Not** `IInstaller.Install(...)` as older sections of this doc imply. New installers must follow `IServiceModule`.
- **No `AppBoot`** entry point exists yet. `RootLifetimeScope` only iterates a serialized `MonoBehaviour[] serviceModules` and calls `Register` on each `IServiceModule`. Boot sequence in §29 is aspirational.
- **No Persistence service.** `Libs/PlayerPrefs` (the `ProtectedPlayerPrefs` library) is in place for small-key state. JSON persistence for gallery sidecars will need a separate service when Gallery lands.
- **Camera service is a registry**, not a fitter. `ICameraService.Register/Get` only — no aspect-fit logic.

### 4c. Planned additions (not on disk yet)

All new game code follows the same nesting pattern as existing Services — `Contracts/Features/<Name>/`, `Data/{Dynamic,Static}/Features/<Name>/`, `Features/<Name>/` — and asmdefs drop the `Darkmatter.` prefix to match `Core`, `Services.Audio`, `Libs.Observer`.

Rough landing order for ColorBook scene to be playable:

| Path | Role |
|---|---|
| `Core/Contracts/Services/Capture/ICaptureService.cs` | Capture service contract — returns PNG bytes |
| `Core/Contracts/Services/Gallery/IGalleryService.cs` | Native-gallery save shim |
| `Core/Contracts/Features/Drawing/IDrawingTemplate.cs`, `IDrawingTemplateCatalog.cs` | Drawing template contracts |
| `Core/Contracts/Features/Coloring/IColorPalette.cs` | Palette contract |
| `Core/Contracts/Features/Progression/IProgressionService.cs` | Progression contract |
| `Core/Data/Static/Features/Drawing/DrawingTemplateSO.cs` | Authored drawing data |
| `Core/Data/Static/Features/Drawing/ShapeSO.cs` | Authored shape (sprite + snap params, reusable) |
| `Core/Data/Static/Features/Coloring/ColorPaletteSO.cs` | Authored palette data |
| `Core/Data/Dynamic/Features/Drawing/ColorRegionDTO.cs` | Runtime region struct |
| `Core/Data/Dynamic/Features/Coloring/PaintCommandDTO.cs` | Runtime coloring struct |
| `Core/Data/Dynamic/Features/Signals/*.cs` (DrawingSelectedSignal, ShapeAssembledSignal, ColorAppliedSignal, PaperSavedSignal) | Cross-feature signal structs |
| `Core/Enums/Services/Camera/CameraType.cs` | Add `CaptureCamera` enum value to existing file |
| `Services/Capture/` (+ `Services.Capture.asmdef`) | `RenderTextureCaptureService` drives the disabled `CaptureCamera` |
| `Services/Gallery/` (+ `Services.Gallery.asmdef`) | `NativeGallerySaveService` — wraps the native gallery plugin |
| `Features/MainMenu/` (+ `Features.MainMenu.asmdef`) | Spine mascot + Play button |
| `Features/{DrawingCatalog,ShapeBuilder,Coloring,Capture,Progression,ColorBookFlow}/` (+ asmdefs each) | Game features |
| `App/LifetimeScopes/{MainMenu,ColorBook}LifetimeScope.cs` | Per-scene scopes |
| `App/Boot/AppBoot.cs` | Bootstrap entry point |
| `Assets/Darkmatter/Scenes/{MainMenu,ColorBook}.unity` | Scenes (no ArtBook — captures go to phone Photos) |
| `Content/Gameplay/Drawings/<theme>/<id>/{Template.asset, Drawing.prefab, Regions/, PaperBackground.png}` | Authored drawings — `Drawing.prefab` holds `SlotMarker`s at slot poses with `ShapeSO` refs |
| `Content/Gameplay/Shapes/*.asset` | Reusable `ShapeSO`s (one per shape; shared across drawings) |
| `Content/Gameplay/Prefabs/ShapePiece.prefab` | The single piece prefab (`ShapePieceUI` MB on root) |
| `Content/Gameplay/Palettes/*.asset` | Color palettes |
| `Content/Audio/{UI,Coloring}/` | SFX banks |

### 4d. Per-feature folder layout (matches existing Services pattern)

Look at `Services/Camera/` (`Installers/` + `Service/`) and `Services/Analytics/` (`Installers/` + `Systems/`) — that's the convention. Features adopt the same shape, adding `UI/` or `Views/` only when there's something to put in them.

```
Features/[Name]/
├── Installers/                IServiceModule — VContainer registration
├── Systems/   (or Service/)   Controllers, repositories, factories (pure C#)
├── UI/                        (only if the feature has Canvas UI)
│   ├── *Presenter.cs          Pure C#, listens to model, drives view
│   └── *View.cs               MonoBehaviour, setters only
├── Views/                     (only if the feature has world-space MonoBehaviours)
└── Features.<Name>.asmdef
```

Rules of thumb pulled from current Services:
- **Use `Service/` (singular)** when the feature has exactly one main implementation class (like `Services/Camera/Service/CameraService.cs`).
- **Use `Systems/` (plural)** when there are multiple pure-C# coordinators (like `Services/Analytics/Systems/`).
- **Skip nesting entirely** when the feature has only 1–2 files at root (like `Services/Audio/AudioService.cs` + `SfxPlayer.cs` flat).
- **`Docs/` is per-folder** in current code — drop a `Docs/` inside any sub-folder that needs notes, don't make a global feature-level Docs.

---

## 5. Namespaces & Asmdef naming

C# namespace pattern is `Darkmatter.<Layer>.<Module>[.<SubArea>]` — the `Darkmatter.` prefix stays on namespaces. Examples already in code:

- `Darkmatter.Core.Contracts.Services.Camera` ([ICameraService.cs](Assets/Darkmatter/Code/Core/Contracts/Services/Camera/ICameraService.cs))
- `Darkmatter.Services.Camera` ([CameraService.cs](Assets/Darkmatter/Code/Services/Camera/Service/CameraService.cs))
- `Darkmatter.Services.Camera.Installers` ([CameraServiceModule.cs](Assets/Darkmatter/Code/Services/Camera/Installers/CameraServiceModule.cs))
- `Darkmatter.Libs.Installers` ([IServiceModule.cs](Assets/Darkmatter/Code/Libs/Installers/IServiceModule.cs))

**Asmdef names drop the `Darkmatter.` prefix.** Existing pattern:

| Namespace | Asmdef |
|---|---|
| `Darkmatter.Core.*` | `Core` |
| `Darkmatter.App` | `Darkmatter.App` (one exception — keep as-is, don't churn) |
| `Darkmatter.Libs.Observer` | `Libs.Observer` |
| `Darkmatter.Libs.FSM` | `Libs.FSM` |
| `Darkmatter.Libs.Installers` | `Libs.Installers` |
| `Darkmatter.Libs.PlayerPrefs` | `Libs.PlayerPrefs` (+ `Libs.PlayerPrefs.Editor`) |
| `Darkmatter.Libs.UI` | `Libs.UI` |
| `Darkmatter.Services.Audio` | `Services.Audio` |
| `Darkmatter.Services.Assets` | `Services.Assets` |
| `Darkmatter.Services.Camera` | `Services.Camera` |
| `Darkmatter.Services.Inputs` | `Services.Inputs` |
| `Darkmatter.Services.Scenes` | `Services.Scenes` |
| `Darkmatter.Services.Analytics` | `Services.Analytics` |

New asmdefs follow the same convention: `Services.Capture`, `Services.Gallery`, `Libs.CommandStack`, `Features.Paper`, `Features.Coloring`, etc.

---

## 6. Scenes & Lifetime Scopes

Four scenes, each with its own scope. Root scope persists across all scene changes.

| Scene | Scope | Status | Contents |
|---|---|---|---|
| `Boot.unity` | `RootLifetimeScope` | ✅ exists | All Services + Libs + cross-scene singletons (`IProgressionSystem`, `IDrawingTemplateCatalog`). Persists forever. |
| `MainMenu.unity` | `MainMenuLifetimeScope` | ⚠️ planned | Spine mascot, single "Play" button. |
| `Colorbook.unity` | `ColorbookLifetimeScope` | ⚠️ planned | `ColorbookFlow` + `DrawingCatalog`. Catalog grid where the player picks a drawing. |
| `Gameplay.unity` | `GameplayLifetimeScope` | ⚠️ scene exists, scope empty | `ShapeBuilder` + `Coloring` + `History` + `Capture` (the feature wrapper) + `GameplayFlow`. The active drawing experience. |

Only `Boot.unity` and an empty `Gameplay.unity` exist today; the four scene scope classes need full implementation (only `RootLifetimeScope` is functional).

Scopes nest: `Root → (MainMenu | Colorbook | Gameplay)`. Services and cross-scene features (Progression, DrawingTemplate) resolve from the root parent. Scene scopes only register their own features.

**No in-app gallery** — captured drawings go to the phone's native Photos app via `IGalleryService`. The catalog grid in `Colorbook.unity` shows the user's progress by reading cached thumbnails from `IProgressionSystem`.

### Boot chain (planned)

No `AppBoot` class exists yet — today `RootLifetimeScope` only registers services and stops. When `AppBoot` is added (as an `IAsyncStartable` registered via `RootLifetimeScope`), it should run once, in order:

1. `IAssetProviderService.InitializeAsync()` — Addressables init.
2. `IProgressionSystem.LoadAsync()` — hydrate per-template state from PlayerPrefs.
3. `IDrawingTemplateCatalog.InitializeAsync()` — batch-load all `DrawingTemplateSO`s by Addressables label.
4. Optional: preload UI sounds, palette assets.
5. `ISceneService.LoadAsync(MainMenu)`.

Failures show a child-friendly retry screen; never crash.

---

## 7. Rendering Strategy

**Full Canvas UI.** No `SpriteRenderer`, no `Physics2D`, no offscreen `RenderTexture` for the live view. The paper, slots, pieces, and color regions are all `Image` components on a Screen-Space-Camera canvas. Standard Unity UI eventing (`IPointerDownHandler`, `IDragHandler`) handles all input.

```
┌──────────────────────────────────────────────────────────┐
│ PaperCanvas (Screen Space - Camera, UICamera)           │
│   layer: PaperUI                                         │
│                                                          │
│   ┌──────────────────────────────────────────────────┐   │
│   │ PaperPanel (RectTransform, 2048×2048 ref units)  │   │
│   │   ├─ BackgroundImage                             │   │
│   │   ├─ SlotsPanel       (slot Image outlines)      │   │
│   │   ├─ PiecesPanel      (draggable piece Images)   │   │
│   │   └─ RegionsPanel     (colorable region Images)  │   │
│   └──────────────────────────────────────────────────┘   │
│                                                          │
└──────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────┐
│ HUDCanvas (Screen Space - Overlay, OR separate camera)  │
│   layer: HUDUI                                           │
│   ├─ Palette panel                                       │
│   ├─ Undo / Redo buttons                                 │
│   ├─ Capture / Next buttons                              │
│   └─ Tray panel (during build phase)                     │
└──────────────────────────────────────────────────────────┘

CaptureCamera (disabled by default, one-shot Render() on capture)
   orthographic, projection cloned from UICamera
   cullingMask = PaperUI only
   targetTexture = temp RT allocated per capture (2048×2048)
```

### Cameras

| Camera | Render mode | Culling Mask | Render Target | Purpose |
|---|---|---|---|---|
| `UICamera` | Screen-Space - Camera (orthographic) | `PaperUI`, `HUDUI` | Screen | Normal display each frame. |
| `CaptureCamera` | Orthographic, disabled | `PaperUI` only | temp `RenderTexture` | One-shot `Render()` invoked by `ICaptureService.CaptureAsync()`. |

`CaptureCamera` shares `UICamera`'s position, orthographic size, and clip planes so the captured frame matches what the player sees — minus the HUD because of the culling mask.

### Layers

| Layer | Used by |
|---|---|
| `PaperUI` | `PaperCanvas` and all of its children (background, slots, pieces, regions, completion FX). Visible in capture. |
| `HUDUI` | `HUDCanvas` and tray panel (palette, undo/redo, capture button, drawing catalog grid, etc.). Excluded from capture. |
| `EventSystem` | Unity's input layer — managed automatically. |

### Why full UI

| Need | Choice | Why |
|---|---|---|
| Tap-to-paint region | `Image` + `Image.alphaHitTestMinimumThreshold` + `IPointerClickHandler` | Tight alpha-based hit shape per region. No mesh / collider authoring. Tap events route through `EventSystem` natively. |
| Drag/drop shape pieces | `Image` + `IBeginDragHandler` / `IDragHandler` / `IEndDragHandler` | Standard Unity UI drag. Pointer events come in canvas-local coords already. No screen→world math anywhere. |
| Visual transition during drag → snap | `DOAnchorPos`, `DOSizeDelta`, `DOLocalRotate` | All pose is in `RectTransform` units. The "transition" is a tween over canvas-local values — no swap of render context. |
| Capture to PNG | Dedicated `CaptureCamera` with `cullingMask = PaperUI` | One `Render()` call into a temp RT. HUD physically can't appear. |
| Multi-resolution support | `CanvasScaler` on `PaperCanvas` (Scale With Screen Size) | Reference resolution `2048 × 2048`, Match = 1 (height). All `anchoredPosition` units are constant across devices. |
| HUD layout independent of paper | `HUDCanvas` (separate Canvas) | HUD scales/anchors per its own rules without affecting the paper layout. |
| Drawing catalog grid, palette, etc. | Standard UI (`GridLayoutGroup`, `ScrollRect`, `Button`) | Anchors handle aspect ratios. Async thumbnail loader. |

### Multi-resolution rule

The paper content is **canvas-unit-stable.** Author every drawing against a fixed 2048 × 2048 reference resolution. Slot positions, piece sizes, region rects, hit shapes — all expressed in `anchoredPosition` / `sizeDelta` units. `CanvasScaler` on `PaperCanvas` does the screen mapping.

`PaperPanel` is anchored center, fixed 2048×2048 (or whatever you pick for the reference). On a wider screen, `CanvasScaler` pillarboxes the panel; on a narrower screen, it letterboxes. The panel's contents never resize relative to each other.

If you want a backdrop (wood/cloth behind the paper area), it's a sibling `Image` of `PaperPanel` (still on `PaperUI` layer) sized to fill the canvas. The backdrop *is* captured into the PNG by default — set its layer to `HUDUI` if you want it excluded.

### Tradeoff vs the old RT-paper-rig design

| Concern | RT-paper-rig (old) | Canvas-only (current) |
|---|---|---|
| Paper contracts | 2 contracts + ~80 lines of math | gone (paper is just RectTransforms in scene) |
| Input pipeline | `IInputReader` → bridge → `Physics2D.OverlapPoint` | native `EventSystem` (`IPointerDownHandler` etc.) |
| Coloring hit shape | `PolygonCollider2D` from `Sprite.Editor` physics shape | `Image.alphaHitTestMinimumThreshold = 0.5f` on the region sprite |
| Per-frame render passes | 2 (ArtCamera into RT + UICamera draws RawImage) | 1 (UICamera draws everything) |
| Capture | read persistent RT | one-shot `CaptureCamera.Render()` |
| Coordinate gotchas | mismatches between screen / RT / world | none — everything is canvas-local |

If you ever need world-space effects (particle sparkles that physically explode outside the paper, free-draw brush stroke, pinch zoom on the artwork), revisit the RT approach. For the v1 tap-to-fill + drag-to-snap design, Canvas-only is correct.

---

## 8. Core Contracts

All Core types are pure data or interfaces.

### Drawing

> Contracts live in `Darkmatter.Core.Contracts.Features.Drawing`; DTOs in `Darkmatter.Core.Data.Dynamic.Features.Drawing`.

```csharp
namespace Darkmatter.Core.Contracts.Features.Drawing;

public interface IDrawingTemplate {
    string Id { get; }
    string DisplayName { get; }
    Sprite DefaultThumbnail { get; }         // authored fallback (used when user has no captures for this template)
    Sprite PaperBackground { get; }
    IReadOnlyList<ShapeSO> Pieces { get; }   // shapes that get spawned in the tray for this drawing
    IReadOnlyList<ColorRegionDTO> Regions { get; }
}

public readonly struct ColorRegionDTO {
    public string RegionId { get; }
    public Sprite Sprite { get; }               // assigned to Image.sprite
    public Vector2 AnchoredPosition { get; }    // canvas units, relative to RegionsParent
    public Vector2 SizeDelta { get; }           // canvas units
    public Color InitialColor { get; }          // usually white
    // Hit shape comes from the sprite alpha — set Image.alphaHitTestMinimumThreshold = 0.5.
    // No polygon path needed; sprite import settings ("Read/Write Enabled") provide it.
}
```

### Shape authoring (`ShapeSO` + one prefab)

Shapes are authored as ScriptableObject assets via the Project Create menu (`Assets > Create > Darkmatter > Drawing > Shape`). One asset per shape — reusable across many drawings.

```csharp
namespace Darkmatter.Core.Data.Static.Features.Drawing;

[CreateAssetMenu(menuName = "Darkmatter/Drawing/Shape", fileName = "Shape_")]
public sealed class ShapeSO : ScriptableObject
{
    [field: SerializeField] public string Id { get; private set; }
    [field: SerializeField] public Sprite Sprite { get; private set; }
    [field: SerializeField] public Vector2 DefaultSizeDelta { get; private set; } = new(256, 256);
    [field: SerializeField] public float SnapRadius { get; private set; } = 100f;
    [field: SerializeField] public float PreviewRadius { get; private set; } = 200f;
}
```

How the runtime uses it:

1. **One piece prefab.** A `ShapePiecePrefab` in `Content/Gameplay/Prefabs/` carries `Image` + `ShapePieceUI`. The `ShapePieceUI` MonoBehaviour has a `[SerializeField] ShapeSO _shape` field — empty on the raw prefab, filled in by the controller at spawn time (or pre-assigned in inspector for testing scenes).
2. **`SlotMarker`** lives in the drawing's per-drawing scene/prefab at the slot's authored pose. Its `[SerializeField] ShapeSO _shape` field tells which shape fits this slot. The slot's `RectTransform` (`anchoredPosition`, `sizeDelta`, `localRotation`) *is* the target snap pose.
3. **Matching is by `ShapeSO` reference equality.** Piece P matches slot S iff `P._shape == S._shape`. No string-id lookups at runtime.
4. **Identity follows the asset.** Whenever `_shape` changes (inspector edit or runtime assign), `ShapePieceUI` re-applies `_shape.Sprite` to its `Image`, sets `RectTransform.sizeDelta = _shape.DefaultSizeDelta`, and exposes `PieceId => _shape.Id`. Done via `OnValidate` (editor) + `Awake` (runtime) + an explicit `Assign(ShapeSO)` method (controller-driven).

> Optional future editor tool: a wizard window for bulk-creating `ShapeSO`s from a folder of sprites — sets `Id` from filename, assigns sprite, applies sensible default radii. For v1, the plain Create-Asset-Menu is enough.

### Coloring

> Contracts in `Darkmatter.Core.Contracts.Features.Coloring`; DTOs in `Darkmatter.Core.Data.Dynamic.Features.Coloring`.

```csharp
namespace Darkmatter.Core.Contracts.Features.Coloring;

public interface IColorPalette {
    string Id { get; }
    IReadOnlyList<Color> Colors { get; }
}

public readonly struct PaintCommandDTO {
    public string RegionId { get; }
    public Color FromColor { get; }
    public Color ToColor { get; }
}
```

### Scene composition (no Paper feature)

The paper area is just a `PaperPanel` GameObject in the `ColorBook.unity` scene with child `RectTransform`s for slots, pieces, and regions. There is **no `IPaperSurface` contract** and no Paper feature. Features that need to parent UI under one of these roots read them from a small `ColorBookSceneRefs : MonoBehaviour` that the scene scope registers as a singleton:

```csharp
public sealed class ColorBookSceneRefs : MonoBehaviour {
    [SerializeField] public RectTransform PaperRoot;
    [SerializeField] public RectTransform SlotsParent;
    [SerializeField] public RectTransform PiecesParent;
    [SerializeField] public RectTransform RegionsParent;
    [SerializeField] public Camera CaptureCamera;       // disabled, used by ICaptureService
    [SerializeField] public RectTransform HudRoot;
    [SerializeField] public RectTransform TrayPanel;
}
```

Registered once via `ColorBookLifetimeScope`:

```csharp
builder.RegisterInstance(_sceneRefs);
```

Features inject `ColorBookSceneRefs` directly and read the rects they need. This keeps the scene refs visible in the inspector while avoiding a one-feature wrapper that adds nothing.

### History

> Contracts in `Darkmatter.Core.Contracts.Features.History`.

```csharp
namespace Darkmatter.Core.Contracts.Features.History;

public interface ICommand {
    void Execute();
    void Undo();
}

public interface IUndoStack {
    bool CanUndo { get; }
    bool CanRedo { get; }
    void Push(ICommand cmd);   // executes + appends
    void Undo();
    void Redo();
    void Clear();
}
```

### Capture & Gallery

Two separate, independent services. **Capture** produces PNG bytes. **Gallery** writes those bytes into the device's native photo library. Neither service knows about the other; the orchestration lives in the Capture feature.

```csharp
namespace Darkmatter.Core.Contracts.Services.Capture;

public interface ICaptureService {
    // Renders the disabled CaptureCamera into a temp RT, ReadPixels into a Texture2D,
    // encodes PNG, releases the RT. Returns the encoded bytes.
    UniTask<byte[]> CaptureAsync();
}

namespace Darkmatter.Core.Contracts.Services.Gallery;

public interface IGalleryService {
    // Saves the given PNG bytes into the device's native photo library
    // under the given album. Native plugin handles platform permissions.
    UniTask SaveToDeviceAsync(byte[] png, string albumName = "Color Book");
}
```

- `ICaptureService` owns the `CaptureCamera` reference (a disabled `Camera` in the ColorBook scene). The camera's `cullingMask` is set to `PaperUI` so the HUD physically cannot appear in the PNG. Paper background is just an `Image` on `PaperUI` — no compositing pass.
- `IGalleryService` is a **thin shim over a native gallery plugin** (NativeGallery, NativeShare, or a custom plugin). It does **not** save thumbnails, does **not** maintain a file list, does **not** delete entries. Everything in the device library is owned by the OS.
- There is no `SavedArtworkDTO`, no sidecar JSON, no in-app gallery file system. The user views their drawings in the phone's Photos app.

### Signals

> Signal structs live in `Darkmatter.Core.Data.Dynamic.Features.Signals` (runtime data, cross-feature).

```csharp
namespace Darkmatter.Core.Data.Dynamic.Features.Signals;

public readonly struct DrawingSelectedSignal {
    public string TemplateId { get; }
}

public readonly struct ShapeAssembledSignal {
    public string TemplateId { get; }
}

public readonly struct ColorAppliedSignal {
    public string RegionId { get; }
    public Color Color { get; }
}

public readonly struct PaperCapturedSignal {
    public string TemplateId { get; }      // captured, before native-gallery save
}

public readonly struct PaperSavedSignal {
    public string TemplateId { get; }      // PNG written to phone library
}
```

---

## 9. Feature Responsibilities

### `MainMenu`

- Lives in `MainMenu.unity`. Single primary action: **Play** (→ `ColorBook` scene). There is no in-app gallery viewer — captured drawings live in the phone's Photos app.
- Hosts a **Spine character mascot** (via `SkeletonGraphic` for Canvas). Authored animations include idle loop, wave, react-to-button, victory dance.
- `MenuMascotPresenter` (pure C#) drives the mascot from code: subscribes to Play-button hover / click events and the model's idle timer, calls `IMenuMascotView.Play(animName, loop)`.
- View is setter-only. Spine-Unity's `SkeletonGraphic.AnimationState.SetAnimation(track, name, loop)` is encapsulated behind `IMenuMascotView`.
- Mascot's skeleton + atlas ship via Addressables (see §10).

### `DrawingCatalog`

- Loads the catalog manifest (list of available template IDs).
- Presents a scrollable grid of thumbnails (Canvas), one per template.
- Each cell shows `IDrawingTemplate.DefaultThumbnail` (the authored fallback sprite). The user's captured drawings live in the phone's Photos app, not in the catalog grid.
- On select → fires `DrawingSelectedSignal(templateId)` and unloads the catalog UI.

### `ShapeBuilder`

- Listens to `DrawingSelectedSignal` (raised by the Colorbook scene before scene change; resume reads `LastOpenedTemplateId` in Gameplay scope startup).
- Loads the per-drawing prefab via `IDrawingTemplateCatalog`, instantiates it under `GameplaySceneRefs.PaperRoot`. The prefab carries the `SlotMarker`s at their authored poses.
- Spawns one **`ShapePiece` MonoBehaviour** per `ShapeSO` in the template via `Instantiate(piecePrefab, tray)` and calls `piece.Setup(shape, slot, cfg, sfx, bus, trayPos, preSnapped)`. If `progress.Phase == ShapeBuilding`, pieces in `progress.SnappedPieces` are pre-snapped (start locked).
- `ShapePiece` is a single MB handling all three behaviors inline: drag (Unity UI `IBeginDrag / IDrag / IEndDrag`), reactive preview lerp when within `cfg.PreviewRadius`, snap (PrimeTween — `Tween.UIAnchoredPosition` / `UISizeDelta` / `LocalRotation`) on release inside `cfg.SnapRadius`, otherwise tween back to tray. No FSM, no factory — just the MB.
- Publishes `PieceSnappedSignal(pieceId)` on lock. Controller counts against expected; fires `ShapeAssembledSignal(templateId)` when all locked.

### `Coloring`

- Listens to `ShapeAssembledSignal`.
- Spawns one UI `Image` per `ColorRegionDTO` under `GameplaySceneRefs.RegionsParent`. Each region's `Image.alphaHitTestMinimumThreshold = 0.5f` so taps on transparent pixels pass through to the next region below.
- Each region has `IPointerClickHandler`. On click → `ColoringController.PaintRegion(view)`.
- Listens to palette selection (current color held in `ColoringStateRepository`).
- Controller builds `PaintRegionCommand(regionId, oldColor, newColor)` and pushes to `IUndoStack`. Command sets `Image.color` on Execute/Undo.
- Publishes `ColorAppliedSignal` for SFX / sparkle effects.
- **Resume:** if `progress.RegionColors` is non-empty, spawned regions are initialized with those saved colors instead of `region.InitialColor`.
- **Autosave hook:** after each `PaintRegion`, debounces 500 ms then calls `GameplayFlowController.AutosaveAsync` so the colors hit disk without thumbnail re-render. See §13.

### `History`

- Owns the scoped `IUndoStack` for the current Gameplay session.
- Cleared on Gameplay scope startup (new drawing = fresh history).
- Capped at 20 entries.
- UI: two big arrow buttons; disabled state when `CanUndo` / `CanRedo` is false.

### `Capture`

- Wraps `ICaptureService.CaptureAsync()` (one-shot `CaptureCamera.Render()` into a temp RT, ReadPixels, EncodeToPNG). Returns raw PNG bytes.
- The **Capture feature does NOT decide what to do with the bytes** — `GameplayFlowController` calls it, then routes to gallery + thumbnail cache depending on the trigger. See §13.

### `Progression`

- Single source of truth for per-template user state. Implements `IProgressionSystem`, persists `DrawingProgress` records via `Libs.PlayerPrefs` (single JSON blob under `PlayerPrefsKeys.Progression`).
- Internally stores thumbnails per template as PNG files in `Application.persistentDataPath/thumbs/{safeId}.png` (large blobs don't belong in PlayerPrefs).
- `ProgressionRepository` does the IO; `ProgressionSystem` keeps an in-memory cache and exposes a clean API.
- Exposes: `GetProgress(id)`, `SaveProgressAsync(progress)`, `SaveProgressAsync(progress, png)`, `ClearProgressAsync(id)`, `IsCompleted(id)`, `CompletedTemplateIds`, `LastOpenedTemplateId / SetLastOpened`, `GetCachedThumbnailAsync(id)`.
- See §13 for the full save matrix.

### `ColorbookFlow` (in `Colorbook.unity`)

- The orchestrator for the catalog scene.
- On scope start: calls `_drawingCatalog.InitializeAsync()` to populate the visible-IDs list.
- Subscribes to `DrawingSelectedSignal`: `_progression.SetLastOpened(id)` + `_scenes.LoadAsync(Gameplay)`.

### `GameplayFlow` (in `Gameplay.unity`)

- The orchestrator for the active drawing scene.
- On scope start: reads `_progression.LastOpenedTemplateId`, fetches its `DrawingProgress`, enters either Building (no progress / Phase==ShapeBuilding) or Coloring (Phase==Coloring) state.
- Handles all save points (§13): Back button, ShapeAssembled transition, Save button, Next button, app lifecycle pause/quit.
- Uses `Libs.FSM` (StateMachine) for `Building` ↔ `Coloring` runtime states.

---

## 10. Addressables Strategy

Mirror the Bus Game pattern via `IAssetProviderService`.

### What ships through Addressables

| Asset | Why |
|---|---|
| `DrawingTemplate` ScriptableObject (per drawing) | Many; load on demand. |
| `ShapeSO` assets | Reused across drawings; load once per drawing batch. |
| Region sprites | Heavy; loaded per drawing. |
| Paper backgrounds | Per template, sometimes shared. |
| Color palette SOs | Swap per theme. |
| Audio clips (tap, snap, complete, sparkle) | Shared SFX bank. |
| Spine mascot (`SkeletonDataAsset` + atlas) | Heavy textures; load with `MainMenu` scene, release on scene exit. |

### What does NOT use Addressables

- HUD prefabs (palette button, undo icon) — always loaded with scene.
- Core UI canvases.
- Boot scene assets.
- User-saved gallery PNGs — those live in `Application.persistentDataPath`.

### Group layout

```
Drawings_Animals       (label: drawing, animals)
Drawings_Vehicles      (label: drawing, vehicles)
Drawings_Shapes        (label: drawing, shapes)
Shapes_Library         (label: shape)         — reusable ShapeSO assets
Palettes               (label: palette)
Audio_UI               (label: sfx, ui)
Audio_Coloring         (label: sfx, coloring)
Spine_MainMenu         (label: spine, menu)   — mascot skeleton + atlas
```

### Lifecycle

- Catalog loads **thumbnail handles only** (cheap).
- On select → full template loads (pieces + regions + paper).
- On "Next" or scene exit → previous template `Release`d before next loads.
- This bound matters on toddler tablets with limited RAM.

### Remote groups (future)

Drawing packs ship as remote bundles. New theme packs (Christmas, Dinosaurs) update without an app store release.

---

## 11. Persistence

Only one in-app persistent store — small settings + progression. Captured drawings go to the device's native photo library (managed by the OS, not by the app).

### Settings + progression via `Libs.PlayerPrefs`

`ProtectedPlayerPrefs` (in `Libs/PlayerPrefs/`) is a lightweight encrypted-string wrapper around Unity's `PlayerPrefs`. Used for:

- Completed template IDs (JSON-encoded list).
- Last opened drawing.
- Audio volume, simple toggles.

A higher-level `IProgressionService` reads/writes these keys; consumers never touch `PlayerPrefs` directly.

### Native photo library (gallery)

Captured PNGs go to the phone's Photos app via `IGalleryService.SaveToDeviceAsync(bytes, albumName)`. The app does **not**:

- Write `.png` files to `persistentDataPath`.
- Generate or store thumbnails locally.
- Maintain any sidecar JSON / index.
- Provide list / load / delete operations.

The user opens the phone's Photos app to view, share, or delete their drawings. iOS / Android handle permissions and album organization.

---

## 12. Capture Pipeline

A dedicated `CaptureCamera` lives in the ColorBook scene, disabled by default. It renders only the `PaperUI` layer into a temp `RenderTexture` when capture fires. The PNG bytes are then handed to the native gallery plugin — no local file IO.

```
[Save button or Next button]
        │
        ▼
CaptureController.SaveAsync(templateId)
        │
        ▼
ICaptureService.CaptureAsync()
        │
        ├─ rt = RenderTexture.GetTemporary(2048, 2048, 0, ARGB32)
        ├─ _captureCam.targetTexture = rt
        ├─ _captureCam.Render()                  (one-shot; cullingMask = PaperUI only)
        ├─ _captureCam.targetTexture = null
        ├─ prev = RenderTexture.active
        ├─ RenderTexture.active = rt
        ├─ tex = new Texture2D(2048, 2048, RGBA32, false)
        ├─ tex.ReadPixels(full rect, 0, 0); tex.Apply()
        ├─ RenderTexture.active = prev
        ├─ RenderTexture.ReleaseTemporary(rt)
        ├─ bytes = tex.EncodeToPNG()             (on worker via UniTask.RunOnThreadPool)
        ├─ Object.Destroy(tex)
        └─ return bytes
        ▼
EventBus.Publish(new PaperCapturedSignal(templateId))
        │
        ▼
IGalleryService.SaveToDeviceAsync(bytes, "Color Book")
        │
        ├─ native plugin handles platform permissions
        ├─ writes PNG into the device's Photos album
        └─ (no return — fire and forget; throws on failure)
        ▼
EventBus.Publish(new PaperSavedSignal(templateId))
```

Notes:

- HUD never appears in capture because `CaptureCamera.cullingMask` excludes `HUDUI`. Layer mask, not coincidence — even if you accidentally parent a HUD element under `PaperPanel`, putting it on the wrong layer keeps it out.
- Paper background is just an `Image` on `PaperUI`. Already in the right layer; no special compositing.
- Saved PNGs are 2048×2048 on every device. `CaptureCamera` has fixed `orthographicSize` and aspect, independent of screen size.
- `CaptureAsync` is safe to call repeatedly. The CaptureCamera's transform / projection are set once at scene start and never modified.
- The temp RT is allocated via `RenderTexture.GetTemporary` so successive captures don't leak GPU memory.
- `IGalleryService` and `ICaptureService` are independent — `IGalleryService` knows nothing about the camera; `ICaptureService` knows nothing about the native plugin. The chain is the `CaptureController`'s sole responsibility.

---

## 12b. Save System

Everything the user does that affects their drawing state must end up persisted. `GameplayFlowController` is the **single owner** of all save calls — feature controllers expose getters; the flow controller assembles the `DrawingProgress` record and hands it to `IProgressionSystem`.

### What is saved

| Field on `DrawingProgress` | Meaning |
|---|---|
| `templateId` | Which drawing this record is about |
| `phase` | `ShapeBuilding` or `Coloring` — where to resume |
| `snappedPieces` | Pieces locked into slots (relevant in ShapeBuilding) |
| `regionColors` | Per-region color (relevant in Coloring) |
| `hasThumbnail` | Whether a thumbnail PNG exists on disk for catalog display |
| `hasBeenCompleted` | Flipped true on first Next; never flips back |
| `completionCount` | Number of times Next was pressed (optional stats) |
| `updatedUtcIso` / `firstCompletedUtcIso` | Timestamps (ISO 8601 strings for JsonUtility) |

### Save matrix

| Trigger | Phase saved | Snapped pieces | Region colors | Thumbnail? | Native gallery? |
|---|---|---|---|---|---|
| **ShapeAssembledSignal** (Building → Coloring transition) | `Coloring` | all | empty | **yes** (bare-assembled paper) | no |
| **Each paint** (debounced 500 ms) | `Coloring` | all | current | no | no |
| **Save button** | `Coloring` | all | current | **yes** | **yes** |
| **Next button** | `Coloring` | all | current | **yes** | **yes** |
| **Back button (during Building)** | `ShapeBuilding` | current | empty | no | no |
| **Back button (during Coloring)** | `Coloring` | all | current | **yes** | no |
| **OnApplicationPause(true) / OnApplicationQuit** | current phase | current | current | no | no |

Two design principles drive the matrix:

- **Thumbnail capture is expensive** (render + ReadPixels + PNG encode). Skip it on partial-assembly saves and per-paint autosaves. Only generate a thumbnail when the user takes an explicit save-style action.
- **Defensive saves never block UX.** App pause/quit saves whatever is in memory without capturing — fast path, no awaitable IO holding up shutdown.

### `Next` adds two extras

- Flips `hasBeenCompleted = true` (preserves first `firstCompletedUtcIso`); increments `completionCount`.
- Plays completion animation, then calls `AdvanceToNextDrawing` → `Catalog.NextUnseen(currentId)` → reload Gameplay scope for the new drawing.

### Storage layout

| What | Where |
|---|---|
| `DrawingProgress` records + `lastOpened` | One JSON blob in `ProtectedPlayerPrefs[PlayerPrefsKeys.Progression]` (see `ProgressionRootDto`) |
| Thumbnail PNGs | `Application.persistentDataPath/thumbs/{safeId}.png` (one file per template that has a thumbnail) |

`safeId` replaces `/` and `\` with `_` so `animals/elephant` becomes `animals_elephant.png`.

### Resume / load decision

On Gameplay scope startup:

```csharp
var id       = _progression.LastOpenedTemplateId;
var progress = _progression.GetProgress(id);  // null if untouched

if (progress == null || progress.Value.phase == DrawingPhase.ShapeBuilding) {
    fsm.Go(Building);   // spawn pieces in tray, pre-snap any in progress.snappedPieces
} else {
    fsm.Go(Coloring);   // skip ShapeBuilder; auto-snap pieces; spawn regions with progress.regionColors
}
```

### Catalog cells reflect saves automatically

The Colorbook scene reloads on Back. Its presenter calls `_progression.GetCachedThumbnailAsync(id)` per cell → returns the most recent save's PNG. Drawings the user touched display their progress; untouched drawings fall back to `IDrawingTemplate.DefaultThumbnail`. No live-update plumbing needed — re-entry is the refresh.

### Files touching the save system

| Path | Role |
|---|---|
| `Core/Contracts/Features/Progression/IProgressionSystem.cs` | Contract |
| `Core/Data/Dynamic/Features/Progression/DrawingProgress.cs` | The struct |
| `Core/Data/Dynamic/Features/Progression/ProgressionRootDto.cs` | JSON root (records + lastOpened) |
| `Core/Data/Dynamic/Features/Progression/RegionColorEntry.cs` | Flattened color entry (JsonUtility-friendly) |
| `Core/Enums/Features/Progression/DrawingPhase.cs` | Phase enum |
| `Features/Progression/Systems/ProgressionSystem.cs` | In-memory cache + write serialization (SemaphoreSlim) |
| `Features/Progression/Systems/ProgressionRepository.cs` | PlayerPrefs JSON + thumbnail file IO |
| `Features/Progression/Installers/ProgressionFeatureModule.cs` | Registers `IProgressionSystem` as Singleton in Root scope |

### Single rule

> Only `GameplayFlowController` calls `_progression.SaveProgressAsync(...)`. Feature controllers expose getters; they never touch the tracker themselves. This means there is exactly one place to audit when save behavior changes.

---

## 13. Communication Rules

| Use case | Mechanism |
|---|---|
| Load template, return result | Direct DI call (`IDrawingTemplateCatalog.LoadAsync`). |
| Capture → save chain | Direct DI calls, sequenced. |
| Notify HUD that a region was painted | `IEventBus` signal. |
| Notify Progression that a drawing was completed | `IEventBus` signal. |
| Tell ColorBookFlow that pieces are assembled | `IEventBus` signal. |
| Tell Coloring which color is currently selected | Direct DI on `ColoringStateRepository`. |

**Never** use signals for request/response. If you need a return value or guaranteed single handler, define a Core interface.

---

## 14. UI (MVP — Passive View)

Identical to Bus Game.

- **Model** — controller / repository, fires C# events.
- **View** — `MonoBehaviour`, only setters (`SetColors(IReadOnlyList<Color>)`).
- **Presenter** — pure C#, subscribes to model events, calls view setters.

### Inspector bridge

For palette icons, undo buttons, region prefabs:

```csharp
[SerializeField, RequireInterface(typeof(IColorButtonView))]
private MonoBehaviour[] _colorButtons;
```

---

## 15. Toddler UX Constraints

These shape several design decisions and are **non-negotiable**:

- **No fail states.** Drawings cannot be "wrong".
- **No timers.** Nothing decays or runs out.
- **No tiny hitboxes.** Drag tolerance ≥ 40 canvas units; snap radius ≥ 80 canvas units for shape pieces. (Canvas reference resolution is 2048×2048 — these are anchored-position deltas, not screen px.)
- **Auto-snap on near-miss.** If a piece is dropped within `1.5 × SnapRadius`, snap anyway and play a happy sound.
- **No text-heavy UI.** Icons everywhere. Single-word labels max.
- **Loud, immediate feedback.** Every tap plays a sound; every fill bursts a small particle effect.
- **Undo cap = 20.** Toddlers will mash undo. Bound the memory.
- **Long-press = quick menu off.** Avoid surprise modals.

---

## 16. Testing

| Layer | Test type | Location |
|---|---|---|
| `Libs/CommandStack` | EditMode unit tests | `Libs/CommandStack/Tests/` |
| `Core` DTOs | EditMode | rarely needed; mostly type-only checks. |
| `Services/Gallery` | EditMode w/ temp directory | mocks `Application.persistentDataPath`. |
| `Services/Capture` | PlayMode | requires a Camera in the test scene. |
| `Features/*/Systems` | EditMode w/ DI test container | inject fakes for `IUndoStack`, signals captured by a fake `IEventBus`. |
| Full flow | PlayMode smoke test | one drawing → assemble → color → capture → assert gallery has 1 file. |

---

## 17. "Where do I put this?" Checklist

1. **Is it a cross-assembly interface / enum / DTO?** → `Core/`
2. **Is it a generic, sellable utility?** → `Libs/`
3. **Is it infrastructure (input, audio, file IO, addressables, capture)?** → `Services/`
4. **Is it gameplay logic specific to coloring books?** → `Features/`
5. **Is it composition / scene wiring?** → `App/`

When in doubt, ask: *would deleting this feature break Core?* If yes, the dependency is wrong.

---

## 18. Open Questions / Future Work

- **Pencil/brush mode** — currently the design is tap-to-fill regions. A free-draw brush mode would need a `BrushStrokeCommand` and a dynamic texture per region; out of scope for v1.
- **Multi-child profiles** — single-profile for v1; multi-profile would slot in behind `IProgressionService` and `IGalleryService` keyed by `profileId`.
- **Cloud sync** — gallery sync would happen behind `IGalleryService` (decorator pattern); local-first stays the source of truth.
- **Sticker / decoration layer** — additive sprite layer above coloring, also `ICommand`-driven so it integrates with undo/redo cleanly.

---

## 19. Quick Reference — Feature ↔ Signal Map

| Feature | Subscribes to | Publishes |
|---|---|---|
| `MainMenu` | — | — (Play tap loads a scene directly) |
| `DrawingCatalog` | — | `DrawingSelectedSignal` |
| `ShapeBuilder` | `DrawingSelectedSignal` | `ShapeAssembledSignal` |
| `Coloring` | `ShapeAssembledSignal` | `ColorAppliedSignal` |
| `History` | `DrawingSelectedSignal` (to clear) | — |
| `Capture` | — (button-driven) | `PaperCapturedSignal`, `PaperSavedSignal` |
| `Progression` | `PaperSavedSignal` | — |
| `ColorBookFlow` | `ShapeAssembledSignal`, `PaperSavedSignal` | — |

---

Maintained alongside the [Darkmatter Architecture Guide](../Assets/Darkmatter_Architecture_Guide.md). Do not break the dependency arrows.

---

## 20. Assembly Definition Map

Every Lib / Service / Feature is its own `.asmdef`. The `Darkmatter.` prefix is **only** on the App asmdef; everything else uses bare `<Layer>.<Module>` names. References follow the layer rules.

### On disk today

| Asmdef | Path | References |
|---|---|---|
| `Darkmatter.App` | `App/` | All Services, Libs, Core (Features when they land) |
| `Core` | `Core/` | (none — `UniTask` allowed in async signatures) |
| `Libs.FSM` | `Libs/FSM/` | `Core` |
| `Libs.Installers` | `Libs/Installers/` | (VContainer only) |
| `Libs.Observer` | `Libs/Observer/` | `Core` |
| `Libs.PlayerPrefs` | `Libs/PlayerPrefs/Runtime/` | (standalone) |
| `Libs.PlayerPrefs.Editor` | `Libs/PlayerPrefs/Editor/` | `Libs.PlayerPrefs` |
| `Libs.UI` | `Libs/UI/` | `Core` |
| `Services.Analytics` | `Services/Analytics/` | `Core`, `Libs.Installers` |
| `Services.Assets` | `Services/Assets/` | `Core`, `Libs.Installers` |
| `Services.Audio` | `Services/Audio/` | `Core`, `Libs.Installers` |
| `Services.Camera` | `Services/Camera/` | `Core`, `Libs.Installers` |
| `Services.Inputs` | `Services/Inputs/` | `Core`, `Libs.Installers` |
| `Services.Scenes` | `Services/Scenes/` | `Core`, `Libs.Installers` |

### Planned (not on disk yet)

| Asmdef | Path | References |
|---|---|---|
| `Services.Capture` | `Services/Capture/` | `Core`, `Libs.Installers` |
| `Services.Gallery` | `Services/Gallery/` | `Core`, `Libs.Installers` |
| `Features.MainMenu` | `Features/MainMenu/` | `Core`, `Libs.Installers` |
| `Features.DrawingCatalog` | `Features/DrawingCatalog/` | `Core`, `Libs.Installers` |
| `Features.ShapeBuilder` | `Features/ShapeBuilder/` | `Core`, `Libs.Installers` |
| `Features.Coloring` | `Features/Coloring/` | `Core`, `Libs.Installers` |
| `Features.Capture` | `Features/Capture/` | `Core`, `Libs.Installers` |
| `Features.Progression` | `Features/Progression/` | `Core`, `Libs.Installers`, `Libs.PlayerPrefs` |
| `Features.ColorBookFlow` | `Features/ColorBookFlow/` | `Core`, `Libs.Installers`, `Libs.FSM` |

> `Libs.CommandStack`, `Features.ArtBook`, and `Features.Paper` were previously planned but cut. Undo lives inside `Features.History` (already on disk). Art-book is removed because captures save to phone Photos. Paper is just RectTransforms in the scene — no contract needed. `ICommand` / `IUndoStack` live in `Core`, so `Features.Coloring` reaches the undo stack without referencing `Features.History`.

**Hard rules:**
- No Service asmdef references any Feature asmdef.
- No Feature asmdef references another Feature asmdef.
- All Services and Features depend on `Libs.Installers` so they can implement `IServiceModule`.
- The compiler enforces this — if a `using` won't resolve, the dependency is wrong.

---

## 21. LifetimeScope Concrete Sample

All scopes use the same pattern: a serialized `MonoBehaviour[]` list of `IServiceModule` installers. Each installer is a MonoBehaviour on a child GameObject of the scope. Scope iterates and calls `Register`. **No hardcoded registrations in the scope itself.** This is exactly what [RootLifetimeScope.cs](Assets/Darkmatter/Code/App/LifetimeScopes/RootLifetimeScope.cs) already does today.

### `RootLifetimeScope` (Boot scene, persists forever) — actual code

```csharp
using Darkmatter.Libs.Installers;
using UnityEngine;
using VContainer;
using VContainer.Unity;

public class RootLifetimeScope : LifetimeScope {
    [SerializeField] private MonoBehaviour[] serviceModules;

    protected override void Configure(IContainerBuilder builder) {
        foreach (var module in serviceModules) {
            if (module is IServiceModule serviceModule)
                serviceModule.Register(builder);
        }
    }
}
```

The inspector lists the installer MonoBehaviours in `serviceModules[]`. Drag the children of the Boot scope GameObject (e.g. `AudioServiceModule`, `CameraServiceModule`, `InputServiceModule`, `AssetProviderServiceModule`, `AnalyticsServiceModule`, `SceneServiceModule`) into that slot. Each is a `MonoBehaviour, IServiceModule`.

### `ColorBookLifetimeScope` (per-scene, child of Root) — same pattern

```csharp
namespace Darkmatter.App.LifetimeScopes;

public sealed class ColorBookLifetimeScope : LifetimeScope {
    [SerializeField] private MonoBehaviour[] sceneModules;

    protected override void Configure(IContainerBuilder builder) {
        foreach (var module in sceneModules) {
            if (module is IServiceModule serviceModule)
                serviceModule.Register(builder);
        }
    }
}
```

Drag the scene's installer MonoBehaviours into `sceneModules[]`:
- `DrawingCatalogModule`
- `ShapeBuilderModule`
- `ColoringModule`
- `HistoryModule`
- `CaptureModule`
- `ProgressionModule`
- `ColorBookFlowModule`

Each registers its own classes via `IServiceModule.Register(IContainerBuilder)`.

> If a scope needs a non-installer reference (e.g. a `ColorBookSceneRefs` MB holding camera + roots), expose it as a separate `[SerializeField]` and `builder.RegisterInstance(...)` it inside the scope's `Configure`. Don't put scene refs inside an installer — keep installers stateless across scenes.

---

## 22. Installer Pattern — Concrete Coloring Sample

Mirrors the existing [CameraServiceModule.cs](Assets/Darkmatter/Code/Services/Camera/Installers/CameraServiceModule.cs) and [InputServiceModule.cs](Assets/Darkmatter/Code/Services/Inputs/Installers/InputServiceModule.cs) — a `MonoBehaviour` implementing `IServiceModule.Register`.

```csharp
namespace Darkmatter.Features.Coloring.Installers;

public sealed class ColoringModule : MonoBehaviour, IServiceModule {
    [SerializeField] private ColoringConfig _config;

    public void Register(IContainerBuilder builder) {
        builder.RegisterInstance(_config);
        builder.Register<ColoringStateRepository>(Lifetime.Scoped).AsSelf();
        builder.Register<ColoringController>(Lifetime.Scoped)
               .As<IColoringController>()
               .AsSelf();
        builder.Register<ColorPalettePresenter>(Lifetime.Scoped).AsSelf();
        builder.RegisterEntryPoint<ColoringInputBinder>();
    }
}
```

Convention:
- One `IServiceModule` per feature, named `<Feature>Module` (matches `CameraServiceModule`, `InputServiceModule`, `AnalyticsServiceModule` already in the project).
- `MonoBehaviour` lives on a GameObject under the scope's hierarchy; dragged into the scope's `serviceModules[]` / `sceneModules[]` inspector list.
- Method name is `Register`, not `Install`. There is **no `IInstaller`** in this project — uses `IServiceModule` from [Libs.Installers](Assets/Darkmatter/Code/Libs/Installers/IServiceModule.cs).
- Registers only its own types. Never touches another feature's types.
- If the installer needs to wire scene-bound MonoBehaviours into DI, expose them as `[SerializeField]` fields on the installer itself and `builder.RegisterInstance<IFoo>(_foo)` them. `ColorBookSceneRefs` (§32.13) is registered this way directly from the scope's serialized field.

---

## 23. Command Pattern — `PaintRegionCommand`

```csharp
namespace Darkmatter.Features.Coloring.Commands;

internal sealed class PaintRegionCommand : ICommand {
    private readonly ColorRegionView _view;
    private readonly Color _fromColor;
    private readonly Color _toColor;
    private readonly IEventBus _bus;

    public PaintRegionCommand(ColorRegionView view, Color from, Color to, IEventBus bus) {
        _view = view;
        _fromColor = from;
        _toColor = to;
        _bus = bus;
    }

    public void Execute() {
        _view.SetColor(_toColor);
        _bus.Publish(new ColorAppliedSignal(_view.RegionId, _toColor));
    }

    public void Undo() {
        _view.SetColor(_fromColor);
        _bus.Publish(new ColorAppliedSignal(_view.RegionId, _fromColor));
    }
}
```

Usage in controller:

```csharp
public void PaintRegion(ColorRegionView view) {
    var current = _state.CurrentColor;
    if (view.Color == current) return;            // no-op
    var cmd = new PaintRegionCommand(view, view.Color, current, _bus);
    _undoStack.Push(cmd);                          // Push executes + records
}
```

Same pattern applies to `SnapPieceCommand` if shape-builder steps should be undoable (optional for v1).

---

## 24. CommandStack — `Libs/CommandStack`

```csharp
namespace Darkmatter.Lib.CommandStack;

public sealed class BoundedUndoStack : IUndoStack {
    private readonly Deque<ICommand> _undo = new();
    private readonly Stack<ICommand> _redo = new();
    private readonly int _capacity;

    public BoundedUndoStack(int capacity = 20) => _capacity = capacity;

    public bool CanUndo => _undo.Count > 0;
    public bool CanRedo => _redo.Count > 0;

    public void Push(ICommand cmd) {
        cmd.Execute();
        _undo.AddLast(cmd);
        if (_undo.Count > _capacity) _undo.RemoveFirst();
        _redo.Clear();
    }

    public void Undo() {
        if (!CanUndo) return;
        var cmd = _undo.Last;
        _undo.RemoveLast();
        cmd.Undo();
        _redo.Push(cmd);
    }

    public void Redo() {
        if (!CanRedo) return;
        var cmd = _redo.Pop();
        cmd.Execute();
        _undo.AddLast(cmd);
    }

    public void Clear() {
        _undo.Clear();
        _redo.Clear();
    }
}
```

`Deque<T>` keeps the oldest entry cheap to evict when the cap fires.

---

## 25. View / Presenter Pair — Color Palette

### View (MonoBehaviour, setters only)

```csharp
namespace Darkmatter.Features.Coloring.UI;

public sealed class ColorPaletteView : MonoBehaviour, IColorPaletteView {
    [SerializeField, RequireInterface(typeof(IColorButtonView))]
    private MonoBehaviour[] _buttonsRaw;

    private IColorButtonView[] _buttons;

    public event Action<int> OnColorButtonClicked;

    private void Awake() {
        _buttons = _buttonsRaw.Cast<IColorButtonView>().ToArray();
        for (var i = 0; i < _buttons.Length; i++) {
            var idx = i;
            _buttons[i].OnClicked += () => OnColorButtonClicked?.Invoke(idx);
        }
    }

    public void SetColors(IReadOnlyList<Color> colors) {
        for (var i = 0; i < _buttons.Length; i++)
            _buttons[i].SetVisible(i < colors.Count);
        for (var i = 0; i < colors.Count; i++)
            _buttons[i].SetColor(colors[i]);
    }

    public void SetSelected(int index) {
        for (var i = 0; i < _buttons.Length; i++)
            _buttons[i].SetSelected(i == index);
    }
}
```

### Presenter (pure C#)

```csharp
namespace Darkmatter.Features.Coloring.UI;

public sealed class ColorPalettePresenter : IStartable, IDisposable {
    private readonly IColorPaletteView _view;
    private readonly ColoringStateRepository _state;

    public ColorPalettePresenter(IColorPaletteView view, ColoringStateRepository state) {
        _view = view;
        _state = state;
    }

    public void Start() {
        _view.SetColors(_state.Palette.Colors);
        _view.SetSelected(_state.SelectedIndex);
        _view.OnColorButtonClicked += OnClicked;
        _state.SelectedIndexChanged += OnIndexChanged;
    }

    private void OnClicked(int index) => _state.SelectColor(index);
    private void OnIndexChanged(int index) => _view.SetSelected(index);

    public void Dispose() {
        _view.OnColorButtonClicked -= OnClicked;
        _state.SelectedIndexChanged -= OnIndexChanged;
    }
}
```

Same shape repeats for every feature's UI.

---

## 26. ShapeBuilder — Snap Algorithm

All math is in canvas-local space — `anchoredPosition`, `sizeDelta`, `localRotation`. No world coords. Behavior lives inline in `ShapePiece : MonoBehaviour` — no FSM, no factory, no state classes. Three behaviors expressed across three Unity drag handlers.

### `OnDrag` — reactive preview lerp

```csharp
public void OnDrag(PointerEventData e)
{
    if (_locked) return;

    var pointerLocal = ScreenToLocal(e.position) + _grabOffset;
    var slotPos = _slot.RectTransform.anchoredPosition;
    float dist = Vector2.Distance(pointerLocal, slotPos);

    if (dist <= _cfg.PreviewRadius)
    {
        if (!_inPreview) { _inPreview = true; _sfx.Play(SfxId.ShapeHover); }
        ApplyPreviewLerp(pointerLocal, dist);
    }
    else
    {
        _inPreview = false;
        RectTransform.anchoredPosition = pointerLocal;
        RectTransform.sizeDelta        = _traySize;
        RectTransform.localRotation    = Quaternion.identity;
    }
}

private void ApplyPreviewLerp(Vector2 pointerLocal, float dist)
{
    float t = Mathf.Clamp01(1f - dist / _cfg.PreviewRadius);
    if (_cfg.PreviewCurve != null) t = _cfg.PreviewCurve.Evaluate(t);
    var slot = _slot.RectTransform;
    RectTransform.anchoredPosition = Vector2.Lerp(pointerLocal, slot.anchoredPosition, t);
    RectTransform.sizeDelta        = Vector2.Lerp(_traySize,    slot.sizeDelta,        t);
    RectTransform.localRotation    = Quaternion.Slerp(Quaternion.identity, slot.localRotation, t);
}
```

### `OnEndDrag` — snap or return

```csharp
public void OnEndDrag(PointerEventData e)
{
    if (_locked) return;
    float dist = Vector2.Distance(
        RectTransform.anchoredPosition,
        _slot.RectTransform.anchoredPosition);
    if (dist <= _cfg.SnapRadius) Snap();
    else                         ReturnToTray();
}

private void Snap()
{
    Lock();                                                   // reparent + raycast off + _locked = true
    var slot = _slot.RectTransform;
    Tween.UIAnchoredPosition(RectTransform, slot.anchoredPosition, _cfg.SnapDuration, Ease.OutBack);
    Tween.UISizeDelta       (RectTransform, slot.sizeDelta,        _cfg.SnapDuration, Ease.OutBack);
    Tween.LocalRotation     (RectTransform, slot.localRotation,    _cfg.SnapDuration, Ease.OutBack);
    _sfx.Play(SfxId.ShapeSnap);
    _bus.Publish(new PieceSnappedSignal(_shape.Id));
}
```

### Four things worth noting

1. **Reparent on lock** — `Lock()` calls `RectTransform.SetParent(_slot.RectTransform.parent, false)`. The piece moves from the HUD-side tray to the per-drawing slot parent so it travels with the paper and gets included in the captured PNG.
2. **Three parallel PrimeTween calls** — position, size, rotation. Tweens start together so the piece visually snaps as one motion. Zero allocations per tween.
3. **`SnapRadius` is in canvas units** (from `ShapeBuilderConfig`, e.g. 80–120), not world units. Same `CanvasScaler` reference resolution across devices = same hit feel.
4. **Preview hover sound fires once per drag**, on the boundary cross into the preview radius. `_inPreview` flag resets on `OnBeginDrag`.

### Pre-snapped resume

If the user closes the app mid-assembly (or after completing the drawing), the saved `DrawingProgress.snappedPieces` lists which pieces were locked. On resume, the controller passes `preSnapped: true` to `Setup` for those, and `ShapePiece.SnapInstantly()` puts them straight into their slots — no tween, no input. The user can keep snapping the remaining pieces.

```csharp
private void SnapInstantly()
{
    Lock();
    var slot = _slot.RectTransform;
    RectTransform.anchoredPosition = slot.anchoredPosition;
    RectTransform.sizeDelta        = slot.sizeDelta;
    RectTransform.localRotation    = slot.localRotation;
}
```

### Spawn loop in `ShapeBuilderController.BuildAsync`

```csharp
var preSnappedIds = progress?.snappedPieces;
foreach (var (shape, idx) in template.Pieces.Select((s, i) => (s, i)))
{
    var go    = Instantiate(_piecePrefab, _refs.TrayPanel);
    var piece = go.GetComponent<ShapePiece>();
    var slot  = FindSlotForShape(slots, shape);
    var trayPos = _trayLayout.GetSlotPosition(idx, template.Pieces.Count);
    var preSnapped = preSnappedIds != null && preSnappedIds.Contains(shape.Id);

    piece.Setup(shape, slot, _cfg, _sfx, _bus, trayPos, preSnapped);
    _alive.Add(piece);
}
```

Controller listens for `PieceSnappedSignal`, counts against expected piece count, fires `ShapeAssembledSignal` when complete → `GameplayFlowController` captures bare-assembled thumbnail, transitions to Coloring (see §13).

---

## 27. Rendering Order & Sorting

Canvas-only — order is sibling index inside `PaperPanel` (front-most is last in hierarchy). No URP 2D sorting layers.

`PaperPanel` children (bottom → top):

```
PaperPanel
├─ BackgroundImage          (paper texture)
├─ RegionsPanel             (colorable region Images)
├─ SlotsPanel               (slot outline Images — under pieces so snapped pieces hide them)
├─ PiecesPanel              (draggable / snapped piece Images)
└─ EffectsPanel             (sparkle / particle UI for completion FX, optional)
```

`HUDCanvas` is a separate Canvas at a higher sorting order (or Screen Space - Overlay). It never sorts against `PaperCanvas` because they're different canvases.

`CaptureCamera` renders only the `PaperUI` layer. The HUD physically cannot leak into the saved PNG because of the culling mask, regardless of sibling order.

> If you ever need particles outside the canvas (e.g. confetti falling across the full screen on completion), use a separate Canvas above the HUD with its own sub-tree of UI particles. Don't add `ParticleSystem`s under PaperPanel — they don't render in UI canvases without `UIParticleSystem` or similar packages.

---

## 28. Native Gallery Integration

`IGalleryService.SaveToDeviceAsync(byte[] png, string albumName)` is the only operation. Implementations wrap a native plugin — recommended packages:

| Platform | Library |
|---|---|
| Cross-platform | [Native Gallery for Android & iOS](https://github.com/yasirkula/UnityNativeGallery) |
| iOS-only fallback | `PHPhotoLibrary` direct bindings |
| Android-only fallback | `MediaStore` direct bindings via `AndroidJavaClass` |

Permission handling:

- **iOS** — `NSPhotoLibraryAddUsageDescription` in `Info.plist`. iOS prompts on first save.
- **Android 13+** — no permission required for writes that target a public collection via the plugin.
- **Android 11–12** — `WRITE_EXTERNAL_STORAGE` declared but not requested at runtime; plugin uses scoped storage.
- **Android ≤ 10** — `WRITE_EXTERNAL_STORAGE` runtime permission requested by the plugin.

`NativeGallerySaveService` (the planned concrete) catches plugin permission denials and either silently no-ops (toddler app) or surfaces a child-friendly retry prompt via the HUD.

No app-side data is persisted about saved drawings. Once `SaveToDeviceAsync` returns, the PNG is the OS's responsibility.

---

## 29. Boot & Error Handling

> **Status: not implemented.** No `AppBoot` class exists. Today, [RootLifetimeScope.cs](Assets/Darkmatter/Code/App/LifetimeScopes/RootLifetimeScope.cs) only iterates installer MonoBehaviours and registers them — nothing runs after that. The block below is the *target* sequence when `AppBoot` is added as an `IAsyncStartable` entry point under `App/Boot/`.

```
AppBoot.StartAsync()           (planned — Features/Boot/AppBoot.cs, registered via builder.RegisterEntryPoint<AppBoot>())
  ├─ try Addressables.InitializeAsync()
  │     fail → show "Tap to retry" splash
  ├─ try preload palette + UI sounds (Addressables labels)
  │     fail → log + continue (non-fatal)
  ├─ try _persistence.LoadAsync()
  │     fail → start with empty progression (don't crash)
  ├─ _scenes.LoadAsync("MainMenu")
  └─ done
```

Toddler-mode error UI:
- One large smiling icon.
- One big "tap" button.
- No text, no error codes.
- A small upper-right gear opens a parent-only diagnostic screen (long-press 3 s to unlock).

---

## 30. Setup Checklist (new dev, day one)

1. Open `Colorbook.sln` at the repo root.
2. Verify required Unity packages are installed (check `Packages/manifest.json`): VContainer, UniTask, Addressables, Input System, URP, **Spine-Unity runtime** (`com.esotericsoftware.spine.spine-unity`) for the main-menu mascot, DOTween (for snap/return tweens).
3. Open `Assets/Darkmatter/Scenes/Boot.unity` (currently the only scene wired).
4. Inspect the `RootLifetimeScope` GameObject — confirm its `serviceModules[]` list references the child installer MonoBehaviours (`AudioServiceModule`, `CameraServiceModule`, `InputServiceModule`, etc.).
5. Hit Play from `Boot.unity`. Other scenes (`MainMenu`, `ColorBook`) don't exist yet — they're listed in §6 / §4c as planned work.
6. When new scene scopes land, the same rule applies: never start a scene mid-flow, always enter from `Boot.unity` so the root scope exists.
7. When drawings are authored: duplicate the template folder under `Content/Gameplay/Drawings/<theme>/<id>/`, edit `Template.asset` (pieces + regions), add to the appropriate Addressables group.
8. Run `Tests > EditMode` and `Tests > PlayMode` before pushing (test infra not set up yet — see §16).

---

## 31. Quick Reference — Class ↔ Layer ↔ Asmdef

| Class | Layer | Asmdef |
|---|---|---|
| `IDrawingTemplate`, `IDrawingTemplateCatalog`, `IDrawingCatalogController` | Core | `Core` |
| `ColorRegionDTO`, `PaintCommandDTO`, `ColorPaletteSO` | Core | `Core` |
| `ShapeSO`, `ShapeBuilderConfig` (ScriptableObjects) | Core | `Core` |
| `DrawingProgress`, `DrawingPhase`, `ProgressionRootDto`, `RegionColorEntry` | Core | `Core` |
| `ICommand`, `IUndoStack`, `IProgressionSystem` | Core | `Core` |
| `UndoStack`, `HistoryButtonsView`, `HistoryButtonsPresenter` | Features | `Features.History` |
| `AddressableAssetProviderService` | Services | `Services.Assets` |
| `NativeGallerySaveService` | Services | `Services.Gallery` |
| `CaptureService` | Services | `Services.Capture` |
| `ColoringController`, `ColoringStateRepository`, `ColorRegionView`, `PaintRegionCommand` | Features | `Features.Coloring` |
| `ShapePiece`, `SlotMarker`, `ShapeBuilderController`, `TrayLayout` | Features | `Features.ShapeBuilder` |
| `AddressableDrawingTemplateCatalog` | Features | `Features.DrawingTemplate` |
| `DrawingCatalogController`, `DrawingCatalogPresenter`, `DrawingCatalogView`, `CatalogItemVM` | Features | `Features.DrawingCatalog` |
| `ColorbookFlowController` | Features | `Features.Colorbook` |
| `GameplayFlowController` | Features | `Features.GameplayFlow` |
| `ProgressionSystem`, `ProgressionRepository` | Features | `Features.Progression` |
| `MenuMascotView`, `MenuMascotPresenter` | Features | `Features.MainMenu` |
| `RootLifetimeScope`, `MainMenuLifetimeScope`, `ColorbookLifetimeScope`, `GameplayLifetimeScope`, `GameplaySceneRefs`, `AppBoot` | App | `Darkmatter.App` |

If a class's natural home doesn't match its asmdef, the architecture is bent — fix the placement, don't add a reference.

### 31b. Scripts inventory by domain

Comprehensive index — every script (existing or planned) grouped by its module. Use this as the single-page mental map. Status column: ✅ exists on disk, ⚠️ planned.

#### Core

| Module (path) | Scripts | Status |
|---|---|---|
| `Core/Compatibility/` | `IsExternalInit` | ✅ |
| `Core/Contracts/Services/Assets/` | `IAssetProviderService` | ✅ |
| `Core/Contracts/Services/Audio/` | `IAudioService`, `ISfxPlayer` | ✅ |
| `Core/Contracts/Services/Camera/` | `ICameraService` | ✅ |
| `Core/Contracts/Services/Inputs/` | `IInputReader` | ✅ |
| `Core/Contracts/Services/Scenes/` | `ISceneService` | ✅ |
| `Core/Contracts/Services/Capture/` | `ICaptureService` | ⚠️ |
| `Core/Contracts/Services/Gallery/` | `IGalleryService` | ⚠️ |
| `Core/Contracts/Features/DrawingCatalog/` | `IDrawingCatalogController`, `IDrawingTemplate`, `IDrawingTemplateCatalog` | ✅ |
| `Core/Contracts/Features/Coloring/` | `IColorPalette` | ✅ |
| `Core/Contracts/Features/History/` | `ICommand`, `IUndoStack` | ✅ |
| `Core/Contracts/Features/Progression/` | `IProgressionSystem` | ✅ |
| `Core/Data/Dynamic/Services/Audio/` | `AudioHandle`, `AudioRequest` | ✅ |
| `Core/Data/Static/Services/Audio/` | `SfxCatalogSO` | ✅ |
| `Core/Data/Static/Features/DrawingTemplate/` | `DrawingTemplateSO` | ✅ |
| `Core/Data/Static/Features/ShapeBuilder/` | `ShapeSO`, `ShapeBuilderConfig` | ✅ |
| `Core/Data/Static/Features/Coloring/` | `ColorPaletteSO` | ✅ |
| `Core/Data/Dynamic/Features/Coloring/` | `ColorRegionDTO`, `PaintCommandDTO` | ✅ |
| `Core/Data/Dynamic/Features/Progression/` | `DrawingProgress`, `ProgressionRootDto`, `RegionColorEntry` | ✅ |
| `Core/Data/Signals/Features/DrawingCatalog/` | `DrawingSelectedSignal` | ✅ |
| `Core/Data/Signals/Features/ShapeBuilder/` | `ShapeAssembledSignal`, `PieceSnappedSignal` | ✅ |
| `Core/Data/Signals/Features/Coloring/` | `ColorAppliedSignal` | ⚠️ |
| `Core/Data/Signals/Features/Capture/` | `PaperCapturedSignal`, `PaperSavedSignal` | ⚠️ |
| `Core/Enums/Features/Progression/` | `DrawingPhase` (`ShapeBuilding`, `Coloring`) | ✅ |
| `Core/Enums/Services/Audio/` | `SfxId` (`None`, `ShapeHover`, `ShapeSnap`, `ShapeNiceTry`, `ShapeReturn`) | ✅ |
| `Core/Enums/Services/Camera/` | `CameraType` (add `CaptureCamera` value) | ✅ |
| `Core/Enums/Services/Scenes/` | `GameScene` | ✅ |

#### Libs

| Module (path) | Scripts | Status |
|---|---|---|
| `Libs/FSM/` | `IState`, `State<T>`, `StateMachine` (abstract) | ✅ |
| `Libs/Installers/` | `IModule` | ✅ |
| `Libs/Observer/` | `IEventBus`, `EventBus` | ✅ |
| `Libs/PlayerPrefs/Runtime/` | `ProtectedPlayerPrefs`, `ProtectedPlayerPrefsSettings`, `PlayerPrefsKeys`, `PlayerPrefsKeyRegistry`, `LocalWriteTracker`, `PendingWriteResync` | ✅ |
| `Libs/PlayerPrefs/Editor/` | `PlayerPrefsEditorWindow`, `ProtectedPlayerPrefsGettingStartedWindow`, `ProtectedPlayerPrefsSettingsUtility`, `ProtectedPlayerPrefsSetupBootstrap` | ✅ |
| `Libs/UI/` | `ToggleButton`, `ToggleButtonGroup` | ✅ |

#### Services

| Module (path) | Scripts | Status |
|---|---|---|
| `Services/Analytics/Installers/` | `AnalyticsModule` | ✅ |
| `Services/Analytics/Systems/` | `FirebaseAnalyticsSystem` | ✅ |
| `Services/Assets/` | `AddressableAssetProviderService`, `AddressableLoadHandleTracker` | ✅ |
| `Services/Audio/` | `AudioService`, `SfxPlayer` | ✅ |
| `Services/Camera/Service/` | `CameraService` | ✅ |
| `Services/Camera/Installers/` | `CameraModule` | ✅ |
| `Services/Inputs/Generated/` | `GameInputs` (Input System codegen) | ✅ |
| `Services/Inputs/` | (Inputs feature partial — Reader + Installer location TBD) | ⚠️ |
| `Services/Scenes/` | `SceneService` | ✅ |
| `Services/Capture/Systems/` | `CaptureService` | ✅ (stub) |
| `Services/Capture/Installers/` | `CaptureModule` | ✅ |
| `Services/Gallery/Core/` | `GalleryService` | ✅ (stub — needs native plugin wiring) |
| `Services/Gallery/Installers/` | `GalleryModule` | ✅ |

#### Features

| Module (path) | Scripts | Status |
|---|---|---|
| `Features/History/Stack/` | `UndoStack` | ✅ |
| `Features/History/Installers/` | `HistoryFeatureModule` | ✅ |
| `Features/History/UI/` | `HistoryButtonsView`, `HistoryButtonsPresenter` | ✅ |
| `Features/MainMenu/Installers/` | `MainMenuFeatureModule` | ⚠️ |
| `Features/MainMenu/Systems/` | `MainMenuModel`, `MenuMascotPresenter` | ⚠️ |
| `Features/MainMenu/UI/` | `MenuMascotView`, `IMenuMascotView` | ⚠️ |
| `Features/DrawingCatalog/Systems/` | `DrawingCatalogController` | ✅ |
| `Features/DrawingCatalog/UI/` | `DrawingCatalogPresenter`, `DrawingCatalogView`, `DrawingCatalogButton`, `CatalogItemVM` | ✅ |
| `Features/DrawingCatalog/Installers/` | `DrawingCatalogFeatureModule` | ✅ |
| `Features/DrawingTemplate/Systems/` | `AddressableDrawingTemplateCatalog` | ✅ |
| `Features/DrawingTemplate/Installers/` | `DrawingTemplateFeatureModule` | ✅ |
| `Features/ShapeBuilder/UI/` | `ShapePiece`, `SlotMarker` | ✅ |
| `Features/ShapeBuilder/Systems/` | `ShapeBuilderController`, `TrayLayout` | ⚠️ |
| `Features/ShapeBuilder/Installers/` | `ShapeBuilderFeatureModule` | ✅ |
| `Features/Coloring/Systems/` | `ColoringController`, `ColoringStateRepository`, `ColorRegionFactory` | ⚠️ |
| `Features/Coloring/UI/` | `ColorRegionView`, `ColorPaletteView`, `ColorPalettePresenter` | ⚠️ |
| `Features/Coloring/Commands/` | `PaintRegionCommand` | ⚠️ |
| `Features/Coloring/Installers/` | `ColoringFeatureModule` | ⚠️ |
| `Features/Capture/Systems/` | `CaptureController` (light wrapper around `ICaptureService`) | ⚠️ |
| `Features/Capture/UI/` | `CaptureButtonPresenter`, `SaveToastView` | ⚠️ |
| `Features/Capture/Installers/` | `CaptureFeatureModule` | ⚠️ |
| `Features/Progression/Systems/` | `ProgressionSystem`, `ProgressionRepository` | ✅ (stubs) |
| `Features/Progression/Installers/` | `ProgressionFeatureModule` | ✅ |
| `Features/ColorbookFlow/System/` | `ColorbookFlowController` | ✅ (needs constructor injection) |
| `Features/ColorbookFlow/Installers/` | `ColorbookFlowFeatureModule` | ✅ |
| `Features/GameplayFlow/Systems/` | `GameplayFlowController` (single owner of all saves — §13) | ⚠️ |
| `Features/GameplayFlow/Installers/` | `GameplayFlowFeatureModule` | ⚠️ |

#### App

| Module (path) | Scripts | Status |
|---|---|---|
| `App/LifetimeScopes/` | `BaseLifetimeScope` (abstract), `RootLifetimeScope` | ✅ |
| `App/LifetimeScopes/` | `GameLifetimeScope` (placeholder, empty), `GameplayLifetimescope` (typo — needs rename to `GameplayLifetimeScope`) | ⚠️ |
| `App/LifetimeScopes/` | `MainMenuLifetimeScope`, `ColorbookLifetimeScope`, `GameplayLifetimeScope` (final) | ⚠️ planned |
| `App/Boot/` | `AppBoot` | ⚠️ planned |
| `App/SceneRefs/` | `GameplaySceneRefs` (PaperRoot, SlotsParent, PiecesParent, RegionsParent, TrayPanel, CaptureCamera) | ⚠️ planned |

---

## 32. Class Reference (Detailed)

> **Status: target spec, mostly unimplemented.** Existing on disk: `AddressableAssetProviderService`, `AudioService` / `SfxPlayer`, `CameraService`, `SceneService`, `InputReaderSO`, `FirebaseAnalyticsSystem`, plus the History feature (`UndoStack`, `HistoryServiceModule`, `ICommand`, `IUndoStack`). Everything else (MainMenu, DrawingCatalog, ShapeBuilder, Coloring, Capture, Gallery, Progression, ColorBookFlow, AppBoot, scene scopes) is the target shape for when those classes are written. Treat this section as a contract for new code, not documentation of current state.

Canonical breakdown of every concrete class and interface. For each: **purpose**, **public surface** (signatures), **injected dependencies**, and **collaborators** (signals or interfaces it talks to).

> Convention used below
> - `// fields:` = constructor-injected dependencies
> - `// pub:` = events / signals fired
> - `// sub:` = events / signals consumed
> - All async returns are `UniTask` unless noted.
> - Folder labels follow the actual nesting pattern: `Core/Contracts/Features/<Name>/`, `Core/Contracts/Services/<Name>/`, `Core/Data/Dynamic/Features/<Name>/`, `Features/<Name>/<Sub>/`, `Services/<Name>/<Sub>/`.

---

### 32.1 Core Contracts

Pure interfaces and DTOs. Zero logic.

#### `IDrawingTemplate` *(Core/Contracts/Features/Drawing — planned)*
Immutable view of a single drawing's authored data.
```csharp
public interface IDrawingTemplate {
    string Id { get; }                              // e.g. "animals/elephant"
    string DisplayName { get; }                     // user-facing
    Sprite DefaultThumbnail { get; }                // 256×256 authored fallback for the catalog grid
    Sprite PaperBackground { get; }                 // image under all paper content
    IReadOnlyList<ShapeSO> Pieces { get; }          // shapes spawned in tray (reusable across drawings)
    IReadOnlyList<ColorRegionDTO> Regions { get; }  // for Coloring
}
```
Implemented by `DrawingTemplateSO` (ScriptableObject) loaded via Addressables. The per-drawing slot positions live in the drawing's authored scene/prefab as `SlotMarker` MonoBehaviours, not in the template SO.

> The catalog grid always uses `DefaultThumbnail`. The user's captured drawings live in the phone's Photos app, not the catalog cell.

#### `IDrawingTemplateCatalog` *(Core/Contracts/Features/Drawing — planned)*
Authority on which drawings exist, completion state, and "next" selection.
```csharp
public interface IDrawingTemplateCatalog {
    UniTask InitializeAsync();                       // pulls manifest from Addressables
    IReadOnlyList<string> AllTemplateIds { get; }
    UniTask<Sprite> GetThumbnailAsync(string id);    // cheap; for grid
    UniTask<IDrawingTemplate> LoadAsync(string id);  // expensive; full template
    void Release(string id);                         // Addressables ref count down
    string NextUnseen(string currentId);             // progression hint
}
```

#### `IColorPalette` *(Core/Contracts/Features/Coloring — planned)*
Set of colors offered to the child. Authored as `ColorPaletteSO`.
```csharp
public interface IColorPalette {
    string Id { get; }
    IReadOnlyList<Color> Colors { get; }             // 6–10 entries typical
}
```

#### `ICommand` & `IUndoStack` *(Core/Contracts/Features/History — planned)*
Already shown in section 8. Each undoable user action is one `ICommand`; the stack is bounded.

#### `IGalleryService` *(Core/Contracts/Services/Gallery — planned)*
Thin shim over a native gallery plugin. Saves PNG bytes into the phone's Photos app. **Does not** track files, thumbnails, or sidecar metadata — the OS owns the file once it's saved.
```csharp
public interface IGalleryService {
    UniTask SaveToDeviceAsync(byte[] png, string albumName = "Color Book");
}
```
- No list / load / delete operations. The user uses the phone's Photos app for those.
- Implementation (`NativeGallerySaveService`) wraps a third-party native plugin and handles platform permission prompts.

#### `ICaptureService` *(Core/Contracts/Services/Capture — planned)*
Snapshots the paper area to a PNG blob. No arguments — implementation owns the disabled `CaptureCamera` reference.
```csharp
public interface ICaptureService {
    UniTask<byte[]> CaptureAsync();
}
```
- Independent of `IGalleryService`. Returns raw PNG bytes; what happens next is the caller's call (save, share, discard).

#### Removed contracts

- `IPaperRig`, `IArtInputBridge`, `IPaperSurface` — paper is just RectTransforms in the scene now, exposed via `ColorBookSceneRefs`. No contract.
- `SavedArtworkDTO`, `IGalleryService.ListAsync/LoadFullAsync/LoadThumbnailAsync/DeleteAsync/GetLatestThumbnailAsync` — no app-side gallery store.
- `ArtworkCapturedSignal`, `ArtworkSavedSignal` — replaced by `PaperCapturedSignal` / `PaperSavedSignal` (templateId only, no DTO).

#### `IProgressionService` *(Core/Contracts/Features/Progression — planned)*
Tracks which templates the child has completed and what they last opened.
```csharp
public interface IProgressionService {
    UniTask LoadAsync();
    UniTask SaveAsync();
    IReadOnlyCollection<string> CompletedTemplateIds { get; }
    string LastOpenedTemplateId { get; }
    void MarkCompleted(string templateId);
    void SetLastOpened(string templateId);
}
```

#### `IAssetProviderService` *(Core/Contracts/Services/Assets — ✅ exists)*
Addressables wrapper. Hides handle bookkeeping from features.
```csharp
public interface IAssetProviderService {
    UniTask InitializeAsync();
    UniTask<T> LoadAsync<T>(string address) where T : UnityEngine.Object;
    UniTask<IReadOnlyList<T>> LoadByLabelAsync<T>(string label) where T : UnityEngine.Object;
    void Release(string address);
    void ReleaseAll();
}
```

#### `IEventBus` *(Libs/Observer — ✅ exists; note the folder is `Observer`, not `EventBus`)*
```csharp
public interface IEventBus {
    void Publish<T>(T signal) where T : struct;
    IDisposable Subscribe<T>(Action<T> handler) where T : struct;
}
```
Signals are structs to avoid GC. Disposable subscription so presenters can unsubscribe in `Dispose()`.

---

### 32.2 Services Layer

Concrete infrastructure. One implementation each. All singletons in `RootLifetimeScope`, registered via per-service `MonoBehaviour, IServiceModule` installers.

#### `AddressableAssetProviderService` *(Services/Assets — ✅ exists)*
Implements `IAssetProviderService`.
- **Responsibility:** Wrap `Addressables.LoadAssetAsync<T>` and ref-count handles by address.
- **State:** `Dictionary<string, AsyncOperationHandle>` keyed by address.
- **Notes:** `Release(address)` decrements; `ReleaseAll()` for scene teardown. Initialization must complete before any other service may load.

#### `NativeGallerySaveService` *(Services/Gallery — planned)*
Implements `IGalleryService` as a thin wrapper around a native gallery plugin.
```csharp
// fields:
//   INativeGalleryPlugin _plugin     (third-party native bridge)
//   IEventBus _bus                   (optional — for failure surfacing)

public sealed class NativeGallerySaveService : IGalleryService {
    public async UniTask SaveToDeviceAsync(byte[] png, string albumName = "Color Book") {
        var permission = await _plugin.RequestWritePermissionAsync();
        if (permission != Permission.Granted) {
            // toddler app — silently skip; or publish a failure signal for HUD retry prompt
            return;
        }
        await _plugin.SaveImageToAlbumAsync(png, albumName, fileName: $"colorbook_{DateTime.UtcNow:yyyyMMdd_HHmmss}.png");
    }
}
```
- **No file IO**, no thumbnails, no sidecars. The native plugin owns everything past the call.
- **Permission flow** runs once per session; the plugin caches the grant.
- **Failure handling**: a toddler app shouldn't crash or block on denial — silently skip and let the user keep playing.

#### `RenderTextureCaptureService` *(Services/Capture — planned)*
Implements `ICaptureService`. Drives the scene's disabled `CaptureCamera` once per capture.
```csharp
// fields:
//   Camera _captureCam            (scene-bound, registered via CaptureServiceModule)
//   int    _surfaceSize = 2048
//   IPathProvider _paths          (only if you want to expose paths — usually not needed here)
```
- **Steps:** `RenderTexture.GetTemporary(size, size, 0, ARGB32)` → set `_captureCam.targetTexture = rt` → `_captureCam.Render()` → `ReadPixels` into a `Texture2D` → null out the target texture → `RenderTexture.ReleaseTemporary(rt)` → `EncodeToPNG` → return bytes.
- **Threading:** PNG encode happens on `UniTask.RunOnThreadPool` to avoid hitching the main thread on tablets.
- **Camera setup:** `_captureCam` has `cullingMask = PaperUI`, `clearFlags = SolidColor` (white or paper color), `orthographicSize` and `aspect` cloned from `UICamera` once at scene start. Stays disabled — `Render()` is the only call site.
- **Sizing:** default 2048², overridable. Capped at device max texture size.

#### Persistence (no dedicated service)
There is no `IPersistenceService` / JSON file writer. `Libs/PlayerPrefs` (`ProtectedPlayerPrefs`) is the only persistent storage in the app. `IProgressionService` consumes it directly.

- **Backing store:** `PlayerPrefs` via the encrypted wrapper.
- **Keys:** namespaced strings registered in `PlayerPrefsKeyRegistry`.
- **Format per value:** JSON-encoded primitives (`ScriptableObject.CreateInstance` not needed at this layer).
- **Atomicity:** `PlayerPrefs` is already atomic per key on iOS/Android.

#### `SceneService` *(Services/Scenes — ✅ exists)*
Implements `ISceneService`. Wraps `SceneManager.LoadSceneAsync` with `UniTask` plus a fade-curtain.
```csharp
public interface ISceneService {
    UniTask LoadAsync(string sceneName, LoadSceneMode mode = LoadSceneMode.Single);
    UniTask UnloadAsync(string sceneName);
}
```

#### `AudioService` *(Services/Audio — ✅ exists; see also `SfxPlayer`)*
Implements `IAudioService`. Plays SFX clips loaded by address, mixes via Unity AudioMixer groups.
```csharp
public interface IAudioService {
    UniTask PreloadAsync(string label);              // e.g. "sfx,ui"
    void PlayOneShot(string clipId, float volume = 1f);
    void SetCategoryVolume(AudioCategory cat, float v01);
}
```
Holds an internal `Dictionary<string, AudioClip>` populated at preload.

#### `InputReaderSO` *(Services/Inputs/Readers — ✅ exists)*
ScriptableObject wrapping the new Input System; exposes events.
```csharp
public interface IInputReader {
    event Action<Vector2> PointerDown;     // world pos
    event Action<Vector2> PointerDrag;
    event Action<Vector2> PointerUp;
}
```
- **Why an SO:** assignable in inspector and survives scene loads, but still resolvable via DI (`builder.RegisterInstance(_inputReader).As<IInputReader>()`).

---

### 32.3 Libs

Generic, project-agnostic utilities.

#### `BoundedUndoStack` *(Libs/CommandStack — planned)*
Implements `IUndoStack`. Source already in section 24.
- **Capacity:** default 20.
- **Invariant:** `_redo` cleared on any new `Push`.
- **Edge cases:** `Undo`/`Redo` on empty stack is a no-op (never throws).

#### `EventBus` *(Libs/Observer — ✅ exists)*
Implements `IEventBus` with a `Dictionary<Type, Delegate>` of `Action<T>` per signal type.
- **Subscribe** returns an `IDisposable` that removes the handler on `Dispose`.
- **Publish** snapshots the invocation list before iterating (so handlers may safely unsubscribe during dispatch).

#### `StateMachine` / `IState` / `State` *(Libs/FSM — ✅ exists)*
Generic state machine. Current shape on disk uses `IState` / `State` / `StateMachine` (see [Libs/FSM/](Assets/Darkmatter/Code/Libs/FSM/)). `ColorBookFlowController` (planned) will use this. The generic sketch below is the target shape if you decide to make it strongly-typed via an enum — verify against actual API before consuming.
```csharp
public sealed class Fsm<TState> where TState : struct, Enum {
    public TState Current { get; }
    public event Action<TState, TState> Transitioned;
    public void Bind(TState state, IFsmState handler);
    public void Go(TState next);                     // calls Exit on old, Enter on new
}

public interface IFsmState { void Enter(); void Exit(); }
```

---

### 32.3b Feature — `MainMenu` *(planned)*

Lives in `MainMenu.unity`. Hosts the **Play** / **Art Book** entry buttons plus the **Spine character mascot**.

#### `IMenuMascotView` *(UI contract)*
Setter-only view interface. Hides Spine-Unity's API behind a tiny surface.
```csharp
public interface IMenuMascotView {
    event Action<string> AnimationComplete;       // fires when a non-looping anim ends
    void Play(string animName, bool loop);
    void SetSkin(string skinName);                // optional — character variants
}
```

#### `MenuMascotView : MonoBehaviour, IMenuMascotView` *(UI)*
Concrete view. Wraps a `SkeletonGraphic` component (Spine-Unity's Canvas-compatible renderer).
```csharp
public sealed class MenuMascotView : MonoBehaviour, IMenuMascotView {
    [SerializeField] private SkeletonGraphic _skeleton;     // Spine UI component

    public event Action<string> AnimationComplete;

    public void Play(string animName, bool loop) {
        var track = _skeleton.AnimationState.SetAnimation(0, animName, loop);
        track.Complete += _ => AnimationComplete?.Invoke(animName);
    }

    public void SetSkin(string skinName) {
        _skeleton.Skeleton.SetSkin(skinName);
        _skeleton.Skeleton.SetSlotsToSetupPose();
        _skeleton.AnimationState.Apply(_skeleton.Skeleton);
    }
}
```
- `SkeletonGraphic` lives on a child of `MainMenuCanvas`. It's a `Graphic`, so it interacts with `CanvasRenderer` just like an `Image`.
- The Spine asset (`SkeletonDataAsset`) is loaded via Addressables, assigned at scene setup, and released on scene exit.

#### `MenuMascotPresenter` *(UI)* — `IStartable, IDisposable`
Pure C#. Subscribes to button events + idle timer, drives the view.
```csharp
// fields: IMenuMascotView _view, MainMenuModel _model, IInputReader _input
public sealed class MenuMascotPresenter : IStartable, IDisposable {
    public void Start() {
        _view.Play("idle", loop: true);
        _model.PlayButtonHovered  += () => _view.Play("hover_play", loop: false);
        _model.PlayButtonPressed  += () => _view.Play("wave",       loop: false);
        _view.AnimationComplete   += OnAnimationComplete;
    }

    private void OnAnimationComplete(string anim) {
        if (anim != "idle") _view.Play("idle", loop: true);   // always return to idle
    }
}
```
- Mascot reactions are pure presenter logic — the view never decides what to play.
- If you want randomized idle variants, add an idle timer in the model + a list of clip names.

#### `MainMenuModel` *(Repository)*
Holds menu state — current selected skin, fires hover/click events from button presenters.

#### `MainMenuModule : MonoBehaviour, IServiceModule` *(Installers)*
Registers the view (`RegisterInstance<IMenuMascotView>(_view)`), the presenter as a startable entry point, and the model.

> **Package dependency:** [Spine-Unity runtime](http://esotericsoftware.com/spine-unity) (`com.esotericsoftware.spine.spine-unity`). Add to `Packages/manifest.json`. The `SkeletonGraphic` component lives in `Spine.Unity` namespace.

---

### 32.4 Feature — `DrawingCatalog` *(planned)*

#### `DrawingCatalogController` *(Systems)*
Headless logic. Owns the list of template IDs visible in the grid.
```csharp
// fields: IDrawingTemplateCatalog _catalog, IEventBus _bus
public sealed class DrawingCatalogController : IAsyncStartable {
    public IReadOnlyList<string> VisibleIds { get; }
    public event Action ListChanged;
    public UniTask StartAsync(CancellationToken ct);     // pulls catalog, refreshes list
    public void OnTemplateSelected(string id);           // bus.Publish(new DrawingSelectedSignal(id))
}
// pub: DrawingSelectedSignal
```

#### `DrawingCatalogPresenter` *(UI)*
Bridges controller ↔ view.
```csharp
// fields: IDrawingCatalogView _view, DrawingCatalogController _ctrl, IDrawingTemplateCatalog _catalog
public sealed class DrawingCatalogPresenter : IStartable, IDisposable {
    public void Start();         // wires view.OnItemClicked → _ctrl.OnTemplateSelected
    public void Dispose();
}
```
- **Thumbnail load:** `_catalog.GetThumbnailAsync(id)` per visible cell, with placeholder while loading.

#### `DrawingCatalogView : MonoBehaviour` *(UI)*
Implements `IDrawingCatalogView`. Pure setters + click event.
```csharp
public interface IDrawingCatalogView {
    event Action<string> OnItemClicked;
    void SetItems(IReadOnlyList<CatalogItemVM> items);   // vm = id + Sprite thumbnail
}
```

---

### 32.5 Feature — `ShapeBuilder` *(planned controller, MBs exist)*

The simplified post-FSM design. **No state machine per piece, no factory class.** Three roles: piece MB, slot MB, controller. Plus a tunables SO.

#### `ShapePiece : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler` *(UI — ✅ exists)*
The single MonoBehaviour that handles drag, reactive preview lerp, snap (PrimeTween), and return-to-tray. Spawned by the controller; `Setup` binds dependencies and starting pose.
```csharp
public sealed class ShapePiece : MonoBehaviour,
    IBeginDragHandler, IDragHandler, IEndDragHandler
{
    [SerializeField] private Image image;
    private ShapeSO _shape;            // bound by Setup
    private SlotMarker _slot;
    private ShapeBuilderConfig _cfg;
    private ISfxPlayer _sfx;
    private IEventBus _bus;
    private Vector2 _trayPos;
    private Vector2 _traySize;
    private bool _locked;

    public ShapeSO Shape => _shape;
    public string PieceId => _shape != null ? _shape.Id : null;
    public bool IsLocked => _locked;
    public RectTransform RectTransform { get; }

    public void Setup(
        ShapeSO shape, SlotMarker slot, ShapeBuilderConfig cfg,
        ISfxPlayer sfx, IEventBus bus,
        Vector2 trayPos, bool preSnapped);
}
```
- Drag handlers run inline in this MB — no separate FSM class.
- `Snap()` is a PrimeTween triple (`Tween.UIAnchoredPosition` / `UISizeDelta` / `LocalRotation`); `SnapInstantly()` is the resume path that puts a pre-snapped piece directly into its slot pose without animation.
- `ReturnToTray()` builds a PrimeTween `Sequence` of three parallel tweens.
- See §26 for the snap algorithm walkthrough.

#### `SlotMarker : MonoBehaviour` *(UI — ✅ exists)*
Authored per drawing — designer places one in the per-drawing prefab at each slot location with the `RectTransform` set to the target pose and `_shape` field assigned to the matching `ShapeSO`. The `RectTransform` itself **is** the target pose.
```csharp
public sealed class SlotMarker : MonoBehaviour
{
    [SerializeField] private ShapeSO shape;
    [SerializeField] private Image outline;     // optional faint outline UI

    public ShapeSO Shape => shape;
    public string SlotId => shape != null ? shape.Id : null;
    public RectTransform RectTransform => (RectTransform)transform;

    public void SetOutlineVisible(bool visible);
}
```
**Matching is by `ShapeSO` reference equality** — the controller pairs each `ShapePiece.Shape` with the `SlotMarker.Shape` of the same SO asset.

#### `ShapeBuilderConfig : ScriptableObject` *(Static data — ✅ exists)*
```csharp
[CreateAssetMenu(menuName = "Darkmatter/ShapeBuilder/Config")]
public sealed class ShapeBuilderConfig : ScriptableObject
{
    public float SnapRadius;             // 80–120 canvas units
    public float SnapGraceMultiplier;    // (currently unused — grace zone removed)
    public float PreviewRadius;          // ~2× SnapRadius
    public float SnapDuration;           // 0.25s
    public float ReturnDuration;         // 0.25s
    public AnimationCurve PreviewCurve;  // easing for the reactive lerp

    public Vector2 DragSizeDelta(ShapeSO shape);
}
```

#### `ShapeBuilderController` *(Systems — planned)*
Spawns pieces for the selected template, tracks snap progress, fires `ShapeAssembledSignal` when complete. **Replaces the old `ShapePieceFactory` — spawning is now a 5-line inline loop, not a separate class.**
```csharp
// fields: IDrawingTemplateCatalog _catalog, IAssetProviderService _assets,
//         GameplaySceneRefs _refs, TrayLayout _trayLayout,
//         ShapeBuilderConfig _cfg, ISfxPlayer _sfx, IEventBus _bus
public sealed class ShapeBuilderController : IAsyncStartable, IDisposable
{
    private readonly List<ShapePiece> _alive = new();
    private GameObject _piecePrefab;

    public async UniTask StartAsync(CancellationToken ct) {
        _piecePrefab = await _assets.LoadAssetAsync<GameObject>(
            "shapebuilder/piece", null, ct);
        _pieceSnappedSub = _bus.Subscribe<PieceSnappedSignal>(OnPieceSnapped);
    }

    public IReadOnlyList<ShapePiece> Alive => _alive;
    public IReadOnlyCollection<string> GetSnappedPieceIds();   // for save records

    public async UniTask BuildAsync(
        IDrawingTemplate template,
        IReadOnlyCollection<string> preSnappedIds = null);

    public void Reset();   // despawns all pieces; called by GameplayFlowController on Teardown
}
// sub: PieceSnappedSignal
// pub: ShapeAssembledSignal
```
- **Slot discovery:** after the per-drawing prefab is instantiated, `GetComponentsInChildren<SlotMarker>(includeInactive: true)` finds all slots. Each slot's `_shape` tells which `ShapeSO` it expects.
- **Pre-snap on resume:** if `preSnappedIds.Contains(shape.Id)`, the spawned `ShapePiece` is initialized with `preSnapped: true` → `SnapInstantly()` lands it in the slot at scope start.
- **Snapped count:** subscribes to `PieceSnappedSignal`, counts against expected, fires `ShapeAssembledSignal` when count == `template.Pieces.Count`.

#### `TrayLayout` *(Systems — planned)*
Tiny stateless helper. Given (`index`, `total`), returns the tray slot's `anchoredPosition`. Either uses a `HorizontalLayoutGroup`'s computed positions or a hand-rolled even spacing across the tray's width.
```csharp
public sealed class TrayLayout
{
    [SerializeField] private RectTransform trayRect;
    public Vector2 GetSlotPosition(int index, int total);
}
```

#### Removed / not needed

- **`ShapePieceFsm`** — was a per-piece state machine. Replaced by inline drag handlers + a single `_locked` bool on `ShapePiece`.
- **`ShapePieceFactory`** — was a wrapper around `Instantiate` + FSM wiring. Replaced by a 5-line inline loop in `ShapeBuilderController.BuildAsync`.
- **Five state classes** (`InTray`, `Dragging`, `Preview`, `Snapped`, `Returning`) — gone. Their behavior maps to: `_locked = false` (idle/dragging/preview all share the same handlers), `_inPreview` flag (preview boundary detection), `Snap()` method, `ReturnToTray()` method.
- **`ShapeBuilderInputBinder`** — never needed; UI handlers on the piece are sufficient.

---


### 32.6 Feature — `Coloring` *(planned)*

#### `ColoringStateRepository` *(Repository)*
In-memory model. Owns "currently selected color" and the palette in use.
```csharp
public sealed class ColoringStateRepository {
    public IColorPalette Palette { get; private set; }
    public int SelectedIndex { get; private set; }
    public Color CurrentColor => Palette.Colors[SelectedIndex];
    public event Action<int> SelectedIndexChanged;
    public void SetPalette(IColorPalette palette);   // resets SelectedIndex to 0
    public void SelectColor(int index);
}
```
- **Why a repository:** presenter and controller both need to read/write current color; an event-emitting POCO is simpler than wiring two signals.

#### `ColoringController` *(Systems)* — implements `IColoringController`
Builds and pushes `PaintRegionCommand` instances; spawns `ColorRegionView` per region.
```csharp
// fields: IUndoStack _undo, ColoringStateRepository _state, ColorRegionFactory _factory,
//         GameplaySceneRefs _refs, IEventBus _bus
public interface IColoringController {
    // Spawn regions on the paper. Pass non-null savedColors to restore colors
    // from a DrawingProgress record; null = use ColorRegionDTO.InitialColor.
    UniTask SpawnRegionsAsync(
        IDrawingTemplate template,
        IReadOnlyDictionary<string, Color> savedColors = null);

    void PaintRegion(ColorRegionView view);             // builds command, pushes to undo stack

    // Snapshot current paint state for save records (see §13).
    IReadOnlyDictionary<string, Color> GetCurrentColors();

    void Clear();                                       // despawn all regions
}
// sub: ShapeAssembledSignal (via flow controller, not direct)
// pub: ColorAppliedSignal (via PaintRegionCommand)
```
Spawns each region as a UI `Image` under `_refs.RegionsParent`. No `Physics2D`.

**Autosave integration:** after each successful `PaintRegion`, the controller calls a debounced `GameplayFlowController.ScheduleAutosave()` so the flow can write the new color state to `IProgressionSystem` 500 ms later (no thumbnail, cheap). The flow controller cancels and resets the timer on each paint — only the last paint in a burst triggers the write.

#### `ColorRegionView : MonoBehaviour, IPointerClickHandler` *(UI)*
UI Image with alpha-based hit detection. Tap routes through Unity's EventSystem directly to `OnPointerClick`.
```csharp
public sealed class ColorRegionView : MonoBehaviour, IPointerClickHandler {
    public string RegionId { get; }
    public Color Color => _image.color;
    public void Initialize(ColorRegionDTO dto, IColoringController controller);
    public void SetColor(Color c);                   // setter only; no logic
    public void OnPointerClick(PointerEventData e) => _controller.PaintRegion(this);
}
```
- **Required sprite setup:** sprite import inspector → **Read/Write Enabled = on**, **Generate Physics Shape = off** (not needed). `Image.alphaHitTestMinimumThreshold = 0.5f` on Initialize so taps on transparent pixels pass through to the next region below.
- **Sibling order matters** for stacked regions — top sibling gets first crack at the click; with alpha hit-test, transparent areas defer correctly.

No `ColoringInputBinder` class needed. Unity's EventSystem fires `OnPointerClick` on the topmost UI element under the pointer that consumes it — exactly what we want.

#### `PaintRegionCommand` *(Commands)*
Source in section 23. Holds `view`, `fromColor`, `toColor`, `bus`. Symmetrical execute/undo.

#### `ColorPaletteView`, `ColorPalettePresenter` *(UI)*
Sources in section 25. Presenter binds `ColoringStateRepository.SelectedIndexChanged` ↔ `IColorPaletteView`.

#### `ColorRegionFactory` *(Systems)*
Mirror of `ShapePieceFactory` for regions. Pool-friendly.

---

### 32.7 Feature — `History` *(planned)*

#### `HistoryController` *(Systems)* — `IStartable, IDisposable`
Owns the per-session `IUndoStack` (registered scoped, so a new ColorBook scene = new stack).
```csharp
// fields: IUndoStack _stack, IEventBus _bus
public sealed class HistoryController : IStartable, IDisposable {
    public bool CanUndo => _stack.CanUndo;
    public bool CanRedo => _stack.CanRedo;
    public event Action StateChanged;
    public void Undo();                              // _stack.Undo() + StateChanged
    public void Redo();
    // sub: DrawingSelectedSignal → _stack.Clear()
}
```

#### `HistoryButtonsView : MonoBehaviour` *(UI)*
Two big arrow buttons. Setters only.
```csharp
public interface IHistoryButtonsView {
    event Action UndoClicked;
    event Action RedoClicked;
    void SetUndoEnabled(bool enabled);
    void SetRedoEnabled(bool enabled);
}
```

#### `HistoryPresenter` *(UI)*
Wires controller `StateChanged` ↔ view enable/disable; view click events → controller.

---

### 32.8 Feature — `Capture` *(planned)*

#### `CaptureController` *(Systems)*
The orchestrator behind the "Save" button. Owns the capture-then-save chain. Stateless other than guarding against concurrent captures.
```csharp
// fields: ICaptureService _capture, IGalleryService _gallery, IEventBus _bus
public sealed class CaptureController {
    public bool IsBusy { get; }
    public UniTask SaveAsync(string templateId);
}
// pub: PaperCapturedSignal (mid-flow), PaperSavedSignal (after native save)
```
- **Flow:** `_capture.CaptureAsync()` → publish `PaperCapturedSignal` → `_gallery.SaveToDeviceAsync(bytes)` → publish `PaperSavedSignal`.
- **Concurrency:** sets `IsBusy = true` on entry; UI binds button enabled to `!IsBusy` to prevent double-tap.
- **No camera args** — `ICaptureService` owns the `CaptureCamera` reference.
- **No file-IO awareness** — `IGalleryService` handles the native plugin handoff.

#### `CaptureButtonPresenter` *(UI)*
Wires button click → `CaptureController.SaveAsync(currentTemplateId)`. Disables button while `IsBusy`. Shows a "Saved to Photos" toast on `PaperSavedSignal`.

---

### 32.9 Feature — `Progression` *(planned)*

#### `ProgressionService` *(Systems)* — implements `IProgressionService`
The only place that knows what "completed" means.
- **Persistence:** delegates to `ProtectedPlayerPrefs` (`Libs.PlayerPrefs`) under key `"progression"`.
- **Load order:** `AppBoot` calls `LoadAsync()` early.
- **Save trigger:** after `MarkCompleted`, debounced 500 ms to coalesce a burst of "Next" presses.

#### `ProgressionRepository` *(Repository)*
Pure in-memory holder used by the service. Separated so tests can inspect state without going through file IO.

---

### 32.10 Feature — `ColorBookFlow` *(planned)*

#### `ColorBookFlowController` *(Systems)* — `IStartable, IDisposable`
**The only orchestrator inside the ColorBook scene.** Drives the panel FSM: `Catalog → Building → Coloring → Done`.
```csharp
// fields:
//   IEventBus _bus
//   IDrawingTemplateCatalog _catalog
//   ShapeBuilderController _builder
//   IColoringController _coloring
//   CaptureController _capture
//   IProgressionService _progression
//   ColorBookSceneRefs _refs        (panel roots to enable/disable)
//   Fsm<ColorBookState> _fsm
```
- **State table:**

  | State      | On enter                                       | Triggers exit                  |
  |------------|-----------------------------------------------|--------------------------------|
  | `Catalog`  | Show catalog panel                            | `DrawingSelectedSignal`        |
  | `Building` | `_builder.BuildAsync(id)`                     | `ShapeAssembledSignal`         |
  | `Coloring` | `_coloring.SpawnRegionsAsync(template)`       | "Next" or "Capture" pressed    |
  | `Done`     | Run autosave capture, mark completed, `Go(Catalog)` for next | always advances        |

- **"Next" sequence:** `_capture.CaptureCurrentAsync` → `_progression.MarkCompleted` → `_catalog.Release(current)` → `_catalog.LoadAsync(_catalog.NextUnseen(current))` → re-enter `Building`.

---

### 32.12 App Layer

#### `AppBoot` *(App/Boot — planned; folder doesn't exist yet)* — `IAsyncStartable`
Single entry point. Steps in section 29.
```csharp
// fields: IAssetProviderService _assets, IProgressionService _progress,
//         IAudioService _audio, ISceneService _scenes, BootConfig _cfg
public sealed class AppBoot : IAsyncStartable {
    public UniTask StartAsync(CancellationToken ct);
}
```

#### LifetimeScopes
- `RootLifetimeScope` — ✅ exists ([source](Assets/Darkmatter/Code/App/LifetimeScopes/RootLifetimeScope.cs)). Iterates a serialized `MonoBehaviour[] serviceModules` and calls `Register` on each `IServiceModule`. Persists for app lifetime.
- `MainMenuLifetimeScope` — planned. Same pattern as Root (serialized installer list, no hardcoded registrations).
- `ColorBookLifetimeScope` — planned. Same pattern; installer list includes feature installers + the flow controller installer. Also has a `[SerializeField] ColorBookSceneRefs _sceneRefs;` and registers it via `builder.RegisterInstance(_sceneRefs)`.

All scope classes are thin: a serialized installer-MonoBehaviour list (+ optional scene refs as separate fields) and a `Configure(IContainerBuilder)` that iterates and calls `Register`.

---

### 32.13 Cross-cutting types

#### `ColorBookSceneRefs : MonoBehaviour` *(App — planned)*
The single source of scene-bound Unity references for the ColorBook scene. Registered in `ColorBookLifetimeScope` via `builder.RegisterInstance(_sceneRefs)` so features don't `Find` things.

```csharp
public sealed class ColorBookSceneRefs : MonoBehaviour {
    public RectTransform PaperRoot;
    public RectTransform SlotsParent;
    public RectTransform PiecesParent;
    public RectTransform RegionsParent;
    public RectTransform HudRoot;
    public RectTransform TrayPanel;
    public Camera CaptureCamera;          // disabled — used by ICaptureService
    public ColorPaletteView PaletteView;  // optional inline ref
    public HistoryButtonsView HistoryButtons;
}
```
Replaces the dropped `IPaperSurface` contract — features that need a paper-area RectTransform read it off this MB.

#### `IServiceModule` *(Libs/Installers — ✅ exists)*
```csharp
public interface IServiceModule {
    void Register(IContainerBuilder builder);
}
```
Implemented as `MonoBehaviour` per feature/service so scopes can drag them in the inspector ([CameraServiceModule.cs](Assets/Darkmatter/Code/Services/Camera/Installers/CameraServiceModule.cs) shows the pattern). The method is `Register`, not `Install` — there is no `IInstaller` in this project.

---

### 32.14 Class summary table

| Class | Layer | Role | Key dependencies |
|---|---|---|---|
| `AppBoot` | App | Startup sequencer | assets, progression, audio, scenes |
| `RootLifetimeScope` | App | Root DI | service modules |
| `MainMenuLifetimeScope` | App | Menu scene DI | feature modules |
| `ColorbookLifetimeScope` | App | Catalog scene DI | feature modules |
| `GameplayLifetimeScope` | App | Active drawing scene DI | scene refs, feature modules |
| `GameplaySceneRefs` | App | Scene-bound RectTransform + CaptureCamera holder | — |
| `MenuMascotView` | Feature.MainMenu | Spine mascot UI (`SkeletonGraphic` wrapper) | — |
| `MenuMascotPresenter` | Feature.MainMenu | Drives mascot animations from model events | view, model |
| `DrawingCatalogController` | Feature.DrawingCatalog | Visible-ID list + selection signal | catalog, progression, bus |
| `DrawingCatalogPresenter` | Feature.DrawingCatalog | UI bridge | view, controller, catalog, progression |
| `DrawingCatalogView` | Feature.DrawingCatalog | UI; renders cells | — |
| `CatalogItemVM` | Feature.DrawingCatalog | View-model per cell | — |
| `AddressableDrawingTemplateCatalog` | Feature.DrawingTemplate | Loads `DrawingTemplateSO`s, exposes `NextUnseen` | assets, progression |
| `ShapeSO` | Core asset | Authored shape (id + sprite + DefaultSizeDelta) | — |
| `ShapeBuilderConfig` | Core asset | Tunables (radii, durations, curve) | — |
| `ShapePiece` | Feature.ShapeBuilder | Draggable piece MB (drag + preview lerp + snap + return) | shape, slot, cfg, sfx, bus |
| `SlotMarker` | Feature.ShapeBuilder | Slot anchor MB; `RectTransform` == target pose | — |
| `ShapeBuilderController` | Feature.ShapeBuilder | Spawns pieces, tracks snap count | catalog, assets, refs, tray, cfg, sfx, bus |
| `TrayLayout` | Feature.ShapeBuilder | Computes piece tray positions | — |
| `ColoringStateRepository` | Feature.Coloring | Current color model | — |
| `ColoringController` | Feature.Coloring | Region spawn + paint cmd + autosave hook | undo, state, factory, refs, flow, bus |
| `ColorRegionView` | Feature.Coloring | Region UI Image + `IPointerClickHandler` | controller |
| `PaintRegionCommand` | Feature.Coloring | Undoable paint (sets `Image.color`) | view, bus |
| `HistoryController` | Feature.History | Undo/redo facade | undo stack, bus |
| `UndoStack` | Feature.History | Bounded undo store | — |
| `CaptureController` | Feature.Capture | (light wrapper) calls `ICaptureService.CaptureAsync` | capture svc |
| `ColorbookFlowController` | Feature.ColorbookFlow | Catalog scene orchestrator (init + selection→scene-load) | catalog, progression, scenes, bus |
| `GameplayFlowController` | Feature.GameplayFlow | Active drawing FSM + **single owner of all saves** (see §13) | catalog, builder, coloring, capture, gallery, progression, scenes, bus |
| `ProgressionSystem` | Feature.Progression | Per-template state + completed view | repository |
| `ProgressionRepository` | Feature.Progression | PlayerPrefs JSON + thumbnail file IO | — |
| `EventBus` | Lib.Observer | Pub/sub | — |
| `StateMachine` (abstract) + `State<T>` | Lib.FSM | Generic FSM base (Enter/Tick/Exit, ChangeState) | — |
| `IModule` | Lib.Installers | DI installer interface | — |
| `ProtectedPlayerPrefs` | Lib.PlayerPrefs | Encrypted PlayerPrefs wrapper | — |
| `AddressableAssetProviderService` | Service.Assets | Addressables wrapper | — |
| `CaptureService` | Service.Capture | One-shot PNG render via `CaptureCamera` | refs |
| `NativeGallerySaveService` | Service.Gallery | Native gallery save (thin plugin shim) | — |
| `SceneService` | Service.Scenes | Async scene loads | — |
| `AudioService`, `SfxPlayer` | Service.Audio | SFX playback | assets |
| `CameraService` | Service.Camera | Camera registry | — |
| `InputReaderSO` | Service.Inputs | New Input System reader | — |
| `FirebaseAnalyticsSystem` | Service.Analytics | Analytics events | — |

If you add a class not in this table, add it here in the same PR. This table is the cheap mental-model index — keep it honest. See §31b for the full path-by-path inventory.

> **What's real on disk today (2026-05):** All Service classes (`AddressableAssetProviderService`, `AudioService`/`SfxPlayer`, `CameraService`, `SceneService`, `InputReaderSO`, `FirebaseAnalyticsSystem`, stub `CaptureService`, stub `GalleryService`), all Lib classes, `ShapePiece` + `SlotMarker` + `ShapeBuilderConfig`, `UndoStack` + `HistoryServiceModule`, `ProgressionSystem` + `ProgressionRepository` (stubs), `AddressableDrawingTemplateCatalog` + module, `DrawingCatalogController` + presenter + view, `ColorbookFlowController` (partial — needs constructor injection wired). Empty / planned: `MainMenu` feature, `GameplayFlow` feature, `Coloring` feature, `MainMenu`/`Colorbook`/`Gameplay` scene scopes, all scenes except `Boot.unity`.
