Files
Colorbook/Readme.md
Savya Bikram Shah 8d599a6396 docs added
2026-05-26 17:35:06 +05:45

1593 lines
57 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Color Book — Architecture Guide
A toddler-targeted (ages 26) 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 **hybrid Sprites + Canvas** 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
```
App launch
└─ Boot scene (RootLifetimeScope)
└─ MainMenu scene
├─ Press "Play" → ColorBook scene
│ ├─ Drawing catalog (grid of templates)
│ ├─ Select drawing
│ ├─ Shape Builder panel (drag pieces → snap to slots)
│ ├─ ↓ on assembly complete
│ ├─ Color panel (tap color → tap region)
│ ├─ Undo / Redo any time
│ ├─ "Capture" → save to Gallery with paper background
│ └─ "Next" → auto-save + load next drawing
└─ Press "Art Book" → ArtBook scene (gallery viewer)
├─ Grid of saved artworks
├─ View / share / delete
└─ Save to device camera roll
```
---
## 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
```
Assets/Darkmatter/Code/
├── App/
│ ├── Boot/
│ │ └── AppBoot.cs
│ └── LifetimeScopes/
│ ├── RootLifetimeScope.cs
│ ├── MainMenuLifetimeScope.cs
│ ├── ColorBookLifetimeScope.cs
│ └── ArtBookLifetimeScope.cs
├── Core/
│ ├── Drawing/
│ │ ├── IDrawingTemplate.cs
│ │ ├── IDrawingTemplateCatalog.cs
│ │ ├── ShapePieceDTO.cs
│ │ └── ColorRegionDTO.cs
│ ├── Coloring/
│ │ ├── IColorPalette.cs
│ │ └── PaintCommandDTO.cs
│ ├── History/
│ │ ├── ICommand.cs
│ │ └── IUndoStack.cs
│ ├── Gallery/
│ │ ├── IGalleryService.cs
│ │ └── SavedArtworkDTO.cs
│ ├── Capture/
│ │ └── ICaptureService.cs
│ ├── Progression/
│ │ └── IProgressionService.cs
│ └── Signals/
│ ├── DrawingSelectedSignal.cs
│ ├── ShapeAssembledSignal.cs
│ ├── ColorAppliedSignal.cs
│ ├── ArtworkCapturedSignal.cs
│ └── ArtworkSavedSignal.cs
├── Libs/
│ ├── CommandStack/ (generic bounded undo/redo)
│ ├── EventBus/ (shared with bus game if monorepo)
│ └── FSM/ (optional, for ColorBookFlow)
├── Services/
│ ├── Audio/
│ ├── Inputs/
│ ├── Assets/ (Addressables wrapper — IAssetProviderService)
│ ├── Scenes/
│ ├── Persistence/ (JSON / PlayerPrefs for non-image state)
│ ├── Gallery/ (file IO — PNG + sidecar JSON)
│ └── Capture/ (RenderTexture → PNG, paper bg compositing)
└── Features/
├── MainMenu/
├── DrawingCatalog/
├── ShapeBuilder/
├── Coloring/
├── History/
├── Capture/
├── Progression/
├── ColorBookFlow/ (orchestrates panel swap, next, capture chain)
└── ArtBook/
```
### Per-feature folder layout
Every feature follows the same internal shape:
```
Features/[Name]/
├── Installers/ IInstaller — VContainer registrations
├── Systems/ Controllers, services (pure C#)
├── Repository/ In-memory state holders
├── Commands/ ICommand implementations (if feature mutates undoable state)
├── UI/
│ ├── *Presenter.cs Pure C#, listens to model, drives view
│ └── *View.cs MonoBehaviour, setters only
├── Views/ World-space MonoBehaviours (sprites, colliders)
└── Docs/ Feature-specific markdown
```
### Asset folder parallel
```
Assets/Darkmatter/Contents/
├── Drawings/
│ ├── Animals/<id>/
│ │ ├── Template.asset (DrawingTemplateSO)
│ │ ├── Pieces/*.png
│ │ ├── Regions/*.png
│ │ └── PaperBackground.png
│ └── Vehicles/...
├── Palettes/*.asset (ColorPaletteSO)
├── Audio/
│ ├── UI/ (tap, swipe, button)
│ └── Coloring/ (fill, complete, sparkle)
└── UI/ (HUD prefabs, fonts, icons)
```
---
## 5. Namespaces
`Darkmatter.[Layer].[Module]`
- `Darkmatter.Features.Coloring`
- `Darkmatter.Features.ShapeBuilder`
- `Darkmatter.Services.Gallery`
- `Darkmatter.Services.Capture`
- `Darkmatter.Core.Drawing`
- `Darkmatter.Lib.CommandStack`
Each maps 1:1 to a `.asmdef`.
---
## 6. Scenes & Lifetime Scopes
| Scene | Scope | Contents |
|---|---|---|
| `Boot.unity` | `RootLifetimeScope` | All Services + `IEventBus`. Persists forever. |
| `MainMenu.unity` | `MainMenuLifetimeScope` | Menu presenter, art book entry. |
| `ColorBook.unity` | `ColorBookLifetimeScope` | DrawingCatalog, ShapeBuilder, Coloring, History, Capture, ColorBookFlow. |
| `ArtBook.unity` | `ArtBookLifetimeScope` | Gallery presenter, viewer, share. |
Scopes nest: `Root → (MainMenu | ColorBook | ArtBook)`. Services resolved from the root parent. Scene scopes only register their own features.
### Boot chain
`AppBoot` runs once, in order:
1. Initialize `IAssetProviderService` (Addressables init).
2. Preload essential bundles (palettes, UI sounds).
3. Load `IProgressionService` from disk.
4. Load `MainMenu` scene.
Failures show a child-friendly retry screen; never crash.
---
## 7. Rendering Strategy
Hybrid: **Sprites for artwork, Canvas for HUD**.
### Cameras
| Camera | Type | Culling Mask | Purpose |
|---|---|---|---|
| `ArtCamera` | Orthographic | `Artwork`, `PaperBackground` | Renders the drawing only. Source for capture. |
| `UICamera` | Overlay (or screen-space) | `UI` | HUD canvas, palette, buttons. |
### Layers
| Layer | Used by |
|---|---|
| `Artwork` | Drawing region sprites, shape pieces, paper bg |
| `UI` | All Canvas elements |
| `Effects` | Particle bursts, sparkles on completion |
### Why hybrid
| Need | Choice | Why |
|---|---|---|
| Per-region tap-to-fill | Sprites + `PolygonCollider2D` | Clean `Physics2D.OverlapPoint`; deterministic; no shader work for the toddler region count (520). |
| Drag/drop shape pieces | Sprites + Physics2D | Natural world bounds, easy snap distance checks. |
| Capture to PNG with paper bg | Sprites under dedicated Camera | `RenderTexture` from `ArtCamera` excludes HUD automatically. |
| Color palette, buttons | Canvas | Anchors handle aspect ratios. Buttons + ScrollRect free. |
| Drawing catalog grid | Canvas | `GridLayoutGroup` + ScrollRect, async thumbnail loader. |
---
## 8. Core Contracts
All Core types are pure data or interfaces.
### Drawing
```csharp
namespace Darkmatter.Core.Drawing;
public interface IDrawingTemplate {
string Id { get; }
string DisplayName { get; }
Sprite Thumbnail { get; }
Sprite PaperBackground { get; }
IReadOnlyList<ShapePieceDTO> Pieces { get; }
IReadOnlyList<ColorRegionDTO> Regions { get; }
}
public readonly struct ShapePieceDTO {
public string PieceId { get; }
public Sprite Sprite { get; }
public Vector2 SlotPosition { get; }
public float SlotRotation { get; }
public float SnapRadius { get; } // generous for toddlers
}
public readonly struct ColorRegionDTO {
public string RegionId { get; }
public Sprite Sprite { get; } // sprite renderer source
public Vector2[] ColliderPath { get; } // polygon collider points
public Color InitialColor { get; } // usually white
}
```
### Coloring
```csharp
namespace Darkmatter.Core.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; }
}
```
### History
```csharp
namespace Darkmatter.Core.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();
}
```
### Gallery & Capture
```csharp
namespace Darkmatter.Core.Gallery;
public readonly struct SavedArtworkDTO {
public string Id { get; }
public string TemplateId { get; }
public DateTime CreatedUtc { get; }
public string ImagePath { get; } // persistentDataPath PNG
public string ThumbnailPath { get; }
}
public interface IGalleryService {
UniTask<SavedArtworkDTO> SaveAsync(byte[] png, string templateId);
UniTask<IReadOnlyList<SavedArtworkDTO>> ListAsync();
UniTask<Texture2D> LoadFullAsync(string artworkId);
UniTask DeleteAsync(string artworkId);
}
namespace Darkmatter.Core.Capture;
public interface ICaptureService {
UniTask<byte[]> CaptureAsync(Camera artCamera, Sprite paperBackground, int width = 2048, int height = 2048);
}
```
### Signals
```csharp
namespace Darkmatter.Core.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 ArtworkCapturedSignal {
public string ArtworkId { get; }
}
public readonly struct ArtworkSavedSignal {
public SavedArtworkDTO Artwork { get; }
}
```
---
## 9. Feature Responsibilities
### `DrawingCatalog`
- Loads the catalog manifest (list of available template IDs + thumbnail addresses).
- Presents a scrollable grid of thumbnails (Canvas).
- On select → fires `DrawingSelectedSignal(templateId)` and unloads the catalog UI.
### `ShapeBuilder`
- Listens to `DrawingSelectedSignal`.
- Loads template via `IDrawingTemplateLoader`, instantiates shape pieces at random off-slot positions.
- Per piece: drag with `ShapePieceView` (sprite + collider). On drop, check distance to `SlotPosition` against `SnapRadius`; if within, snap and lock.
- Fires `ShapeAssembledSignal` when all pieces locked.
### `Coloring`
- Listens to `ShapeAssembledSignal`.
- Spawns one `ColorRegionView` per `ColorRegionDTO` (sprite + polygon collider on Artwork layer).
- Listens to palette selection (current color held in `ColoringStateRepository`).
- On region tap: builds `PaintRegionCommand(regionId, oldColor, newColor)`, pushes to `IUndoStack`.
- Command sets `SpriteRenderer.color` on undo/redo.
- Fires `ColorAppliedSignal` for SFX / sparkle effects.
### `History`
- Owns the singleton `IUndoStack` for the current ColorBook session.
- Cleared on `DrawingSelectedSignal` (new drawing = fresh history).
- Capped at ~20 entries (memory + cognitive simplicity).
- UI: two big arrow buttons; disabled state when `CanUndo / CanRedo` is false.
### `Capture`
- Bound to the "Capture" button.
- Calls `ICaptureService.CaptureAsync(artCamera, template.PaperBackground)` → PNG bytes.
- Hands bytes to `IGalleryService.SaveAsync(...)`.
- Fires `ArtworkCapturedSignal` then `ArtworkSavedSignal`.
- Shows a quick "saved!" toast with a thumbnail of the new entry.
### `Progression`
- Tracks completed template IDs and the in-progress draft.
- On "Next" button: silently runs Capture pipeline (auto-save), marks current as completed, calls `IDrawingTemplateCatalog.NextUnseen()`.
- Persists JSON via `IPersistenceService`.
### `ColorBookFlow`
- The only orchestrator inside ColorBook scope.
- Subscribes to flow-relevant signals and toggles UI panels (catalog → builder → coloring).
- Coordinates "Next" sequence: `IProgressionService.MarkCompleted``ICaptureService` autosave → `IDrawingTemplateLoader.Release(currentId)` → load next.
- Built as a small FSM (`Catalog → Building → Coloring → Done`).
### `ArtBook`
- Separate scene.
- `GalleryPresenter` calls `IGalleryService.ListAsync()` → grid of thumbnails.
- Tap → fullscreen view, share-sheet button, delete.
- Saved-to-device-camera-roll uses an optional platform plugin behind `IExternalShareService` (Core contract).
---
## 10. Addressables Strategy
Mirror the Bus Game pattern via `IAssetProviderService`.
### What ships through Addressables
| Asset | Why |
|---|---|
| `DrawingTemplate` ScriptableObject (per drawing) | Many; load on demand. |
| Shape piece sprites | Only needed when active. |
| Region sprites + polygon paths | 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. |
### 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)
Palettes (label: palette)
Audio_UI (label: sfx, ui)
Audio_Coloring (label: sfx, coloring)
```
### 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
Two distinct stores, each behind its own Core contract.
### `IPersistenceService` (JSON / PlayerPrefs)
Holds:
- Completed template IDs.
- Last opened drawing.
- Audio volume, simple settings.
Path: `Application.persistentDataPath/save.json`.
### `IGalleryService` (file IO)
Holds user artworks:
```
persistentDataPath/Gallery/
├── {guid}.png full-res render (~2048×2048)
├── {guid}.thumb.png 256×256 for grid
└── {guid}.json SavedArtworkDTO sidecar
```
- Writes are atomic (`.tmp` → rename).
- `ListAsync` enumerates sidecar JSONs sorted by `CreatedUtc desc`.
- Thumbnail generation happens once at save time on a worker thread.
---
## 12. Capture Pipeline
```
[Capture button or Next button]
ICaptureService.CaptureAsync(artCamera, paperBg)
├─ Allocate RenderTexture (2048×2048, ARGB32)
├─ artCamera.targetTexture = rt
├─ Force render (artCamera.Render())
├─ ReadPixels into Texture2D
├─ Composite paperBg underneath (single shader pass or CPU blend)
├─ Encode PNG (Texture2D.EncodeToPNG)
├─ Release RT + temp texture
└─ return byte[]
IGalleryService.SaveAsync(bytes, templateId)
├─ Write .png atomically
├─ Generate + write thumbnail
├─ Write sidecar JSON
└─ return SavedArtworkDTO
EventBus.Publish(new ArtworkSavedSignal(dto))
```
Notes:
- HUD never appears in capture because `ArtCamera` only renders the `Artwork` layer.
- Paper background can either be already present in the scene (cheap) or composited at capture time (lets the same drawing be saved with different papers).
---
## 13. Communication Rules
| Use case | Mechanism |
|---|---|
| Load template, return result | Direct DI call (`IDrawingTemplateLoader.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 px; snap radius ≥ 60 px for shape pieces.
- **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, but for `SavedArtworkDTO` serialization, yes. |
| `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 |
|---|---|---|
| `DrawingCatalog` | — | `DrawingSelectedSignal` |
| `ShapeBuilder` | `DrawingSelectedSignal` | `ShapeAssembledSignal` |
| `Coloring` | `ShapeAssembledSignal` | `ColorAppliedSignal` |
| `History` | `DrawingSelectedSignal` (to clear) | — |
| `Capture` | — (button-driven) | `ArtworkCapturedSignal`, `ArtworkSavedSignal` |
| `Progression` | `ArtworkSavedSignal` | — |
| `ColorBookFlow` | `ShapeAssembledSignal`, `ArtworkSavedSignal` | — |
| `ArtBook (Gallery)` | `ArtworkSavedSignal` (if open) | — |
---
Maintained alongside the [Darkmatter Architecture Guide](../Assets/Darkmatter_Architecture_Guide.md). Do not break the dependency arrows.
---
## 20. Assembly Definition Map
Every folder under `Code/` is its own `.asmdef`. References follow the layer rules exactly.
| Asmdef | Path | References |
|---|---|---|
| `Darkmatter.App` | `App/` | All Features, all Services, Core, Libs |
| `Darkmatter.Core` | `Core/` | (none — `UniTask` allowed in async signatures) |
| `Darkmatter.Lib.CommandStack` | `Libs/CommandStack/` | `Darkmatter.Core` |
| `Darkmatter.Lib.EventBus` | `Libs/EventBus/` | `Darkmatter.Core` |
| `Darkmatter.Lib.FSM` | `Libs/FSM/` | `Darkmatter.Core` |
| `Darkmatter.Services.Audio` | `Services/Audio/` | `Darkmatter.Core` |
| `Darkmatter.Services.Inputs` | `Services/Inputs/` | `Darkmatter.Core` |
| `Darkmatter.Services.Assets` | `Services/Assets/` | `Darkmatter.Core` |
| `Darkmatter.Services.Scenes` | `Services/Scenes/` | `Darkmatter.Core` |
| `Darkmatter.Services.Persistence` | `Services/Persistence/` | `Darkmatter.Core` |
| `Darkmatter.Services.Gallery` | `Services/Gallery/` | `Darkmatter.Core` |
| `Darkmatter.Services.Capture` | `Services/Capture/` | `Darkmatter.Core` |
| `Darkmatter.Features.MainMenu` | `Features/MainMenu/` | `Darkmatter.Core`, Libs |
| `Darkmatter.Features.DrawingCatalog` | `Features/DrawingCatalog/` | `Darkmatter.Core`, Libs |
| `Darkmatter.Features.ShapeBuilder` | `Features/ShapeBuilder/` | `Darkmatter.Core`, Libs |
| `Darkmatter.Features.Coloring` | `Features/Coloring/` | `Darkmatter.Core`, `Lib.CommandStack` |
| `Darkmatter.Features.History` | `Features/History/` | `Darkmatter.Core`, `Lib.CommandStack` |
| `Darkmatter.Features.Capture` | `Features/Capture/` | `Darkmatter.Core` |
| `Darkmatter.Features.Progression` | `Features/Progression/` | `Darkmatter.Core` |
| `Darkmatter.Features.ColorBookFlow` | `Features/ColorBookFlow/` | `Darkmatter.Core`, `Lib.FSM` |
| `Darkmatter.Features.ArtBook` | `Features/ArtBook/` | `Darkmatter.Core` |
**Hard rule:** No Service asmdef references any Feature asmdef. No Feature asmdef references another Feature asmdef. Compiler enforces the architecture.
---
## 21. LifetimeScope Concrete Sample
### `RootLifetimeScope` (Boot scene, persists forever)
```csharp
namespace Darkmatter.App.LifetimeScopes;
public sealed class RootLifetimeScope : LifetimeScope {
[SerializeField] private AudioServiceConfig _audioConfig;
[SerializeField] private InputReaderSO _inputReader;
protected override void Configure(IContainerBuilder builder) {
// EventBus
builder.Register<IEventBus, EventBus>(Lifetime.Singleton);
// Services
builder.RegisterInstance(_inputReader).As<IInputReader>();
builder.Register<IAudioService, AudioService>(Lifetime.Singleton)
.WithParameter(_audioConfig);
builder.Register<IAssetProviderService, AddressableAssetProviderService>(Lifetime.Singleton);
builder.Register<ISceneService, SceneService>(Lifetime.Singleton);
builder.Register<IPersistenceService, JsonPersistenceService>(Lifetime.Singleton);
builder.Register<IGalleryService, FileGalleryService>(Lifetime.Singleton);
builder.Register<ICaptureService, RenderTextureCaptureService>(Lifetime.Singleton);
// App entry
builder.RegisterEntryPoint<AppBoot>();
}
}
```
### `ColorBookLifetimeScope` (per-scene, child of Root)
```csharp
namespace Darkmatter.App.LifetimeScopes;
public sealed class ColorBookLifetimeScope : LifetimeScope {
[SerializeField] private ColorBookSceneRefs _sceneRefs; // ArtCamera, panel roots, prefabs
[SerializeField] private IInstaller[] _installers; // assigned in inspector
protected override void Configure(IContainerBuilder builder) {
builder.RegisterInstance(_sceneRefs);
// Each feature ships an IInstaller
foreach (var installer in _installers) installer.Install(builder);
// Scene-scoped orchestrator
builder.RegisterEntryPoint<ColorBookFlowController>();
}
}
```
Drag these installers in the inspector:
- `DrawingCatalogServiceModule`
- `ShapeBuilderServiceModule`
- `ColoringServiceModule`
- `HistoryServiceModule`
- `CaptureFeatureModule`
- `ProgressionServiceModule`
---
## 22. Installer Pattern — Concrete Coloring Sample
```csharp
namespace Darkmatter.Features.Coloring.Installers;
[CreateAssetMenu(menuName = "Darkmatter/Installers/Coloring")]
public sealed class ColoringServiceModule : ScriptableObject, IInstaller {
[SerializeField] private ColoringConfig _config;
public void Install(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 `IInstaller` per feature.
- `ScriptableObject` so it can be referenced by scene scope inspector.
- Registers only its own types. Never touches another feature's types.
---
## 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
```csharp
// In ShapePieceView.OnPointerUp:
public void OnDragEnd(Vector2 worldPos) {
var slot = transform.position; // assigned target slot
var d = Vector2.Distance(worldPos, slot);
if (d <= _piece.SnapRadius) {
SnapToSlot();
} else if (d <= _piece.SnapRadius * 1.5f) {
// Toddler grace zone — snap anyway, play happy sound
SnapToSlot();
_audio.PlayOneShot(_clips.NiceTry);
} else {
ReturnToTrayAnimated();
}
}
private void SnapToSlot() {
_locked = true;
transform.DOMove(_piece.SlotPosition, 0.25f).SetEase(Ease.OutBack);
_audio.PlayOneShot(_clips.Snap);
_bus.Publish(new PieceSnappedSignal(_piece.PieceId));
}
```
Controller listens for `PieceSnappedSignal`, counts against expected piece count, fires `ShapeAssembledSignal` when complete.
---
## 27. Rendering Order & Sorting
URP 2D with a single `ArtCamera` ortho cam.
| Sorting Layer | Order | Contents |
|---|---|---|
| `PaperBackground` | 0 | Paper bg sprite (under everything) |
| `ArtworkRegions` | 100 | `ColorRegionView` sprites (the colorable shapes) |
| `ArtworkPieces` | 200 | `ShapePieceView` sprites (during build) |
| `Effects` | 300 | Particle bursts, sparkles |
| `UIWorld` | 400 | World-space prompts (rare; mostly Canvas) |
Canvas HUD lives on `UICamera` (Overlay), never sorts against `ArtCamera`. Capture renders only `ArtCamera`'s layers → HUD physically cannot leak into saved PNG.
---
## 28. SavedArtwork JSON Schema
```json
{
"id": "f3a8e2d4-...",
"templateId": "animals/elephant",
"createdUtc": "2026-05-26T16:42:11Z",
"imagePath": "Gallery/f3a8e2d4-....png",
"thumbnailPath": "Gallery/f3a8e2d4-....thumb.png",
"regions": [
{ "regionId": "body", "color": "#FFB347" },
{ "regionId": "ears", "color": "#FF6961" }
]
}
```
`regions[]` lets the gallery reopen an artwork for further edits in a future version (out of scope v1, but the schema reserves the field now to avoid migration later).
Paths are **relative** to `persistentDataPath`. Never store absolute paths — they change between OS updates on some platforms.
---
## 29. Boot & Error Handling
```
AppBoot.StartAsync()
├─ 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 `Bus Game.sln` (color book lives in same repo / Unity project per plan).
2. Verify Addressables groups exist: `Drawings_*`, `Palettes`, `Audio_*`.
3. Open `Boot.unity` → confirm `RootLifetimeScope` references the right configs.
4. Open `ColorBook.unity` → confirm `ColorBookLifetimeScope._installers[]` is fully populated.
5. Hit Play from `Boot.unity` (entry scene). Never start mid-flow — DI parent scope must exist.
6. To author a new drawing: duplicate `Animals/elephant/`, edit `Template.asset` (pieces + regions), add to the appropriate Addressables group.
7. Run `Tests > EditMode` and `Tests > PlayMode` before pushing.
---
## 31. Quick Reference — Class ↔ Layer ↔ Asmdef
| Class | Layer | Asmdef |
|---|---|---|
| `IDrawingTemplate`, `ShapePieceDTO`, `ColorRegionDTO` | Core | `Darkmatter.Core` |
| `ICommand`, `IUndoStack` | Core | `Darkmatter.Core` |
| `BoundedUndoStack` | Libs | `Darkmatter.Lib.CommandStack` |
| `AddressableAssetProviderService` | Services | `Darkmatter.Services.Assets` |
| `FileGalleryService` | Services | `Darkmatter.Services.Gallery` |
| `RenderTextureCaptureService` | Services | `Darkmatter.Services.Capture` |
| `ColoringController`, `PaintRegionCommand` | Features | `Darkmatter.Features.Coloring` |
| `ShapeBuilderController`, `ShapePieceView` | Features | `Darkmatter.Features.ShapeBuilder` |
| `HistoryController` | Features | `Darkmatter.Features.History` |
| `ColorBookFlowController` | Features | `Darkmatter.Features.ColorBookFlow` |
| `GalleryPresenter`, `GalleryGridView` | Features | `Darkmatter.Features.ArtBook` |
| `ColorBookLifetimeScope`, `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.
---
## 32. Class Reference (Detailed)
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.
---
### 32.1 Core Contracts
Pure interfaces and DTOs. Zero logic.
#### `IDrawingTemplate` *(Core/Drawing)*
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 Thumbnail { get; } // 256×256 for catalog grid
Sprite PaperBackground { get; } // composited under artwork
IReadOnlyList<ShapePieceDTO> Pieces { get; } // for ShapeBuilder
IReadOnlyList<ColorRegionDTO> Regions { get; } // for Coloring
}
```
Implemented by `DrawingTemplateSO` (ScriptableObject) loaded via Addressables.
#### `IDrawingTemplateCatalog` *(Core/Drawing)*
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/Coloring)*
Set of colors offered to the child. Authored as `ColorPaletteSO`.
```csharp
public interface IColorPalette {
string Id { get; }
IReadOnlyList<Color> Colors { get; } // 610 entries typical
}
```
#### `ICommand` & `IUndoStack` *(Core/History)*
Already shown in section 8. Each undoable user action is one `ICommand`; the stack is bounded.
#### `IGalleryService` *(Core/Gallery)*
Persistent store of saved artwork PNGs.
```csharp
public interface IGalleryService {
UniTask<SavedArtworkDTO> SaveAsync(byte[] png, string templateId);
UniTask<IReadOnlyList<SavedArtworkDTO>> ListAsync(); // sorted newest first
UniTask<Texture2D> LoadFullAsync(string artworkId); // for fullscreen view
UniTask<Texture2D> LoadThumbnailAsync(string artworkId);
UniTask DeleteAsync(string artworkId);
}
```
#### `ICaptureService` *(Core/Capture)*
Snapshots the artwork camera to a PNG blob.
```csharp
public interface ICaptureService {
UniTask<byte[]> CaptureAsync(
Camera artCamera,
Sprite paperBackground,
int width = 2048,
int height = 2048);
}
```
#### `IProgressionService` *(Core/Progression)*
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/Assets)*
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/EventBus, also referenced from Core)*
```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`.
#### `AddressableAssetProviderService` *(Services/Assets)*
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.
#### `FileGalleryService` *(Services/Gallery)*
Implements `IGalleryService`.
```csharp
// fields:
// IPathProvider _paths (wraps Application.persistentDataPath for tests)
// IThumbnailGenerator _thumb (downscale + encode)
// IEventBus _bus
// pub: ArtworkSavedSignal, ArtworkDeletedSignal
```
- **Save flow:** write `{guid}.png.tmp` → fsync → rename; generate thumbnail on a worker; write sidecar JSON last (so partial saves are detectable by absence of JSON).
- **List flow:** enumerate `*.json` in `Gallery/`, deserialize, sort by `CreatedUtc desc`.
- **Delete flow:** delete png + thumb + json; missing files ignored (idempotent).
#### `RenderTextureCaptureService` *(Services/Capture)*
Implements `ICaptureService`.
- **Steps:** allocate `RenderTexture(width, height, 0, ARGB32)` → bind to `artCamera.targetTexture``artCamera.Render()``ReadPixels` into `Texture2D` → composite `paperBackground` underneath (single shader blit) → `EncodeToPNG` → release RT + textures.
- **Threading:** PNG encode happens on a `UniTask.RunOnThreadPool` to avoid hitching the main thread on tablets.
- **Sizing:** default 2048², overridable. Capped at device max texture size.
#### `JsonPersistenceService` *(Services/Persistence)*
Implements `IPersistenceService` (small JSON blob; not the gallery).
```csharp
public interface IPersistenceService {
UniTask<T> LoadAsync<T>(string key) where T : class, new();
UniTask SaveAsync<T>(string key, T value);
}
```
- **Path:** `Application.persistentDataPath/save.json`.
- **Format:** single JSON object keyed by `key` so multiple services can share one file.
- **Atomicity:** write to `save.json.tmp` → rename.
#### `SceneService` *(Services/Scenes)*
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)*
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)*
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)*
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/EventBus)*
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).
#### `Fsm<TState>` *(Libs/FSM)*
Generic state machine used by `ColorBookFlowController`.
```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.4 Feature — `DrawingCatalog`
#### `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`
#### `ShapeBuilderController` *(Systems)*
Spawns shape pieces for the selected template, tracks snap progress, fires `ShapeAssembledSignal` when complete.
```csharp
// fields: IDrawingTemplateCatalog _catalog, ShapePieceFactory _factory, IEventBus _bus, ShapeBuilderConfig _cfg
public sealed class ShapeBuilderController : IDisposable {
public IReadOnlyList<ShapePieceView> Active { get; }
public UniTask BuildAsync(string templateId); // load template, spawn pieces in tray
public void Reset(); // clear, unsubscribe
}
// sub: DrawingSelectedSignal
// pub: ShapeAssembledSignal
```
- **Internal:** counts `PieceSnappedSignal` against expected piece count.
#### `ShapePieceView : MonoBehaviour` *(Views)*
World-space draggable sprite with collider. Source for snap-or-return logic shown in section 26.
```csharp
public sealed class ShapePieceView : MonoBehaviour {
public string PieceId { get; }
public bool IsLocked { get; }
public event Action<string> Snapped; // raised when piece locks into slot
public void Initialize(ShapePieceDTO dto, IInputReader input, IAudioService audio);
}
```
- **No public mutators** for position once locked — controller treats `IsLocked` as the source of truth.
#### `ShapePieceFactory` *(Systems)*
Instantiates `ShapePieceView` prefabs from a pool. Avoids re-instantiating across "Next" cycles on the same template family.
```csharp
public sealed class ShapePieceFactory {
public ShapePieceView Spawn(ShapePieceDTO dto, Transform parent);
public void Despawn(ShapePieceView view);
}
```
---
### 32.6 Feature — `Coloring`
#### `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, IEventBus _bus
public interface IColoringController {
UniTask SpawnRegionsAsync(IDrawingTemplate template);
void PaintRegion(ColorRegionView view); // builds command, pushes to undo stack
void Clear();
}
// sub: ShapeAssembledSignal (via flow controller, not direct)
// pub: ColorAppliedSignal (via PaintRegionCommand)
```
#### `ColorRegionView : MonoBehaviour` *(Views)*
Sprite + `PolygonCollider2D`, on `Artwork` layer. Tapped via `Physics2D.OverlapPoint` from `ColoringInputBinder`.
```csharp
public sealed class ColorRegionView : MonoBehaviour {
public string RegionId { get; }
public Color Color { get; } // current paint
public void Initialize(ColorRegionDTO dto);
public void SetColor(Color c); // setter only; no logic
}
```
#### `ColoringInputBinder` *(Systems)* — `IStartable, IDisposable`
Subscribes to `IInputReader.PointerDown`, raycasts on the `Artwork` layer mask, calls `ColoringController.PaintRegion(view)` on hit.
#### `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`
#### `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`
#### `CaptureController` *(Systems)*
The orchestrator behind the "Capture" button. Stateless other than guarding against concurrent captures.
```csharp
// fields: ICaptureService _capture, IGalleryService _gallery, IEventBus _bus, ColorBookSceneRefs _refs
public sealed class CaptureController {
public bool IsCapturing { get; }
public UniTask<SavedArtworkDTO> CaptureCurrentAsync(string templateId, Sprite paperBg);
}
// pub: ArtworkCapturedSignal (mid-flow), ArtworkSavedSignal (post-save)
```
- **Concurrency:** sets `IsCapturing = true` on entry; UI binds button enabled to `!IsCapturing` to prevent double-tap.
#### `CaptureButtonPresenter` *(UI)*
Wires button click → `CaptureController.CaptureCurrentAsync`. Disables button while in progress. Shows toast on `ArtworkSavedSignal`.
---
### 32.9 Feature — `Progression`
#### `ProgressionService` *(Systems)* — implements `IProgressionService`
The only place that knows what "completed" means.
- **Persistence:** delegates to `IPersistenceService` 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`
#### `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.11 Feature — `ArtBook`
#### `GalleryPresenter` *(UI)* — `IAsyncStartable, IDisposable`
Lists artworks, opens fullscreen view, deletes, shares.
```csharp
// fields: IGalleryService _gallery, IGalleryView _view, IExternalShareService _share, IEventBus _bus
```
- **Start:** `_gallery.ListAsync()``_view.SetItems(...)`.
- **Subscribes** to `ArtworkSavedSignal` to live-refresh if the user pops back in.
#### `IGalleryView` *(UI)*
```csharp
public interface IGalleryView {
event Action<string> OnArtworkTapped;
event Action<string> OnDeleteRequested;
event Action<string> OnShareRequested;
void SetItems(IReadOnlyList<GalleryItemVM> items);
void ShowFullscreen(Texture2D full);
void HideFullscreen();
}
```
#### `IExternalShareService` *(Core)*
Platform plugin shim (iOS Photos / Android MediaStore).
```csharp
public interface IExternalShareService {
UniTask SaveToCameraRollAsync(byte[] png);
UniTask ShareAsync(byte[] png, string subject);
}
```
---
### 32.12 App Layer
#### `AppBoot` *(App/Boot)* — `IAsyncStartable`
Single entry point. Steps in section 29.
```csharp
// fields: IAssetProviderService _assets, IPersistenceService _persist, IProgressionService _progress,
// IAudioService _audio, ISceneService _scenes, BootConfig _cfg
public sealed class AppBoot : IAsyncStartable {
public UniTask StartAsync(CancellationToken ct);
}
```
#### LifetimeScopes
- `RootLifetimeScope` — section 21. Registers all services + `IEventBus`. Persists for app lifetime.
- `MainMenuLifetimeScope` — registers `MainMenuPresenter` and view.
- `ColorBookLifetimeScope` — section 21. Registers feature installers + `ColorBookFlowController` as entry point.
- `ArtBookLifetimeScope` — registers `GalleryPresenter` + view + `IExternalShareService`.
All scope classes are thin: serialized fields for scene refs, `Configure(IContainerBuilder)` only.
---
### 32.13 Cross-cutting types
#### `ColorBookSceneRefs : MonoBehaviour` *(App)*
Aggregates all scene-bound Unity references that features need: `Camera artCamera`, `Transform catalogRoot`, `Transform builderRoot`, `Transform coloringRoot`, `RectTransform hudRoot`, `ColorPaletteView paletteView`, `HistoryButtonsView historyView`. Registered as a singleton in `ColorBookLifetimeScope` so features don't `Find` things.
#### `IInstaller` *(App)*
```csharp
public interface IInstaller { void Install(IContainerBuilder builder); }
```
Implemented as `ScriptableObject` per feature so scopes can drag them in the inspector (section 22).
---
### 32.14 Class summary table
| Class | Layer | Role | Key dependencies |
|---|---|---|---|
| `AppBoot` | App | Startup sequencer | assets, persist, progression, scenes |
| `RootLifetimeScope` | App | Root DI | configs |
| `ColorBookLifetimeScope` | App | Scene DI | scene refs, installers |
| `DrawingCatalogController` | Feature | Grid logic | catalog, bus |
| `DrawingCatalogPresenter` | Feature | UI bridge | view, controller, catalog |
| `ShapeBuilderController` | Feature | Piece spawn + snap tracking | catalog, factory, bus, cfg |
| `ShapePieceView` | Feature | Draggable piece MB | input, audio |
| `ColoringStateRepository` | Feature | Current color model | — |
| `ColoringController` | Feature | Region spawn + paint cmd | undo, state, factory, bus |
| `ColorRegionView` | Feature | Region sprite MB | — |
| `PaintRegionCommand` | Feature | Undoable paint | view, bus |
| `HistoryController` | Feature | Undo/redo facade | undo stack, bus |
| `CaptureController` | Feature | Capture+save orchestration | capture svc, gallery, bus, refs |
| `ColorBookFlowController` | Feature | Scene FSM | bus, catalog, builder, coloring, capture, progression |
| `GalleryPresenter` | Feature | Art book listing | gallery, share, view, bus |
| `BoundedUndoStack` | Lib | Capped undo store | — |
| `EventBus` | Lib | Pub/sub | — |
| `Fsm<TState>` | Lib | Generic FSM | — |
| `AddressableAssetProviderService` | Service | Addressables wrapper | — |
| `FileGalleryService` | Service | Gallery file IO | paths, thumb gen, bus |
| `RenderTextureCaptureService` | Service | PNG render | — |
| `JsonPersistenceService` | Service | Settings/progression IO | — |
| `SceneService` | Service | Async scene loads | — |
| `AudioService` | Service | SFX playback | assets |
| `ProgressionService` | Service | Completion tracking | persistence |
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.