diff --git a/Assets/Darkmatter/Code/Core/Contracts/Services/Gallery/IGalleryService.cs b/Assets/Darkmatter/Code/Core/Contracts/Services/Gallery/IGalleryService.cs index 743fe61..4274987 100644 --- a/Assets/Darkmatter/Code/Core/Contracts/Services/Gallery/IGalleryService.cs +++ b/Assets/Darkmatter/Code/Core/Contracts/Services/Gallery/IGalleryService.cs @@ -5,6 +5,6 @@ namespace Darkmatter.Core.Contracts.Services.Gallery { public interface IGalleryService { - void SaveImageAsync(Texture2D sprite, string fileName); + UniTask SaveImageAsync(Texture2D sprite, string fileName, string albumName = "Colorbook"); } } \ No newline at end of file diff --git a/Assets/Darkmatter/Code/Services/Gallery/Core/GalleryService.cs b/Assets/Darkmatter/Code/Services/Gallery/Core/GalleryService.cs index 99a2b31..f3a454a 100644 --- a/Assets/Darkmatter/Code/Services/Gallery/Core/GalleryService.cs +++ b/Assets/Darkmatter/Code/Services/Gallery/Core/GalleryService.cs @@ -1,3 +1,5 @@ +using System; +using Cysharp.Threading.Tasks; using Darkmatter.Core.Contracts.Services.Gallery; using UnityEngine; @@ -5,9 +7,33 @@ namespace Darkmatter.Services.Gallery { public class GalleryService : IGalleryService { - public void SaveImageAsync(Texture2D image, string fileName) + public async UniTask SaveImageAsync(Texture2D image, string fileName, string albumName = "Colorbook") { - NativeGallery.SaveImageToGallery(image, "ColorBook", fileName); + var permission = await NativeGallery.RequestPermissionAsync(NativeGallery.PermissionType.Write, + NativeGallery.MediaType.Image); + + if (permission != NativeGallery.Permission.Granted) + { + return; + } + + var tcs = new UniTaskCompletionSource(); + + NativeGallery.SaveImageToGallery(image, albumName, + filename: $"colorbook_{DateTime.UtcNow:yyyyMMdd_HHmmss}.png", callback: (success, path) => + { + if (!success) + { + Debug.LogError("Failed to save image to gallery."); + } + else + { + Debug.Log($"Image saved to gallery at: {path}"); + } + tcs.TrySetResult(); + }); + + await tcs.Task; } } } \ No newline at end of file diff --git a/Readme.docx b/Readme.docx new file mode 100644 index 0000000..a2cd684 Binary files /dev/null and b/Readme.docx differ diff --git a/Readme.md b/Readme.md index 0ac09b9..9786bbe 100644 --- a/Readme.md +++ b/Readme.md @@ -1,6 +1,6 @@ # 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 **hybrid Sprites + Canvas** render strategy. +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. @@ -11,22 +11,21 @@ This document is the canonical reference for the Color Book game's structure. Th ``` 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 + └─ MainMenu scene (Spine mascot, Play button) + └─ 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 + ├─ "Save" → screenshot via CaptureCamera → native gallery plugin + │ saves PNG to phone's Photos album. Toast confirmation. + └─ "Next" → auto-save + load next drawing ``` +The user views their captured drawings inside the phone's native **Photos** app — there is no in-app gallery viewer. Capture and gallery-save are two independent services: `ICaptureService` produces PNG bytes; `IGalleryService` is a thin shim over a native plugin that writes those bytes into the device's photo library. + --- ## 2. Philosophy @@ -79,7 +78,7 @@ Assets/Darkmatter/ │ ├── Content/ ← singular ("Content", not "Contents") │ └── Gameplay/ -│ └── PaperRig/ ← (planned — paper rig prefabs) +│ └── PaperRig/ ← stale folder — paper rig dropped; safe to delete │ ├── Data/ │ ├── Inputs/ (Input System .inputactions) @@ -98,7 +97,7 @@ Assets/Darkmatter/ │ ├── Compatibility/ │ │ └── IsExternalInit.cs (C#9 init shim for older runtimes) │ ├── Contracts/ - │ │ ├── Paper/ ← misplaced empty folder — move to Contracts/Features/Paper/ when IPaperSurface lands + │ │ ├── Paper/ ← stale empty folder — delete (Paper is no longer a feature) │ │ └── Services/ │ │ ├── Assets/IAssetProviderService.cs │ │ ├── Audio/IAudioService.cs, ISfxPlayer.cs @@ -175,29 +174,25 @@ Rough landing order for ColorBook scene to be playable: | Path | Role | |---|---| -| `Core/Contracts/Features/Paper/IPaperSurface.cs` | Paper surface contract (canvas roots) | -| `Core/Contracts/Services/Capture/ICaptureService.cs` | Capture service contract | -| `Core/Contracts/Services/Gallery/IGalleryService.cs` | Gallery service contract | +| `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/History/ICommand.cs`, `IUndoStack.cs` | Undo/redo contracts | -| `Core/Contracts/Features/Progression/IProgressionService.cs` | Progression contract (despite the name, it's a feature contract since it's game-specific) | -| `Core/Data/Static/Features/Drawing/` (DrawingTemplateSO) | Authored drawing data | -| `Core/Data/Static/Features/Coloring/` (ColorPaletteSO) | Authored palette data | -| `Core/Data/Dynamic/Features/Drawing/ColorRegionDTO.cs` | Runtime drawing struct (regions only — pieces use `ShapeSO`) | -| `Core/Data/Static/Features/Drawing/ShapeSO.cs` | Authored shape ScriptableObject (sprite + snap params) | +| `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/Gallery/SavedArtworkDTO.cs` | Runtime gallery struct | -| `Core/Data/Dynamic/Features/Signals/` (DrawingSelectedSignal, ShapeAssembledSignal, ColorAppliedSignal, ArtworkCapturedSignal, ArtworkSavedSignal) | Cross-feature signal structs | -| `Core/Enums/Services/Camera/CameraType.cs` | Add `ArtCamera` enum value to existing file | -| `Libs/CommandStack/` (+ `Libs.CommandStack.asmdef`) | Bounded undo/redo | +| `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`) | `FileGalleryService` — PNG + sidecar JSON IO | -| `Features/Paper/` (+ `Features.Paper.asmdef`) | Scene-bound `PaperSurface` MB + module | -| `Features/{MainMenu,DrawingCatalog,ShapeBuilder,Coloring,History,Capture,Progression,ColorBookFlow,ArtBook}/` (+ asmdefs each) | Game features | -| `App/LifetimeScopes/{MainMenu,ColorBook,ArtBook}LifetimeScope.cs` | Per-scene scopes | +| `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,ArtBook}.unity` | Scenes | +| `Assets/Darkmatter/Scenes/{MainMenu,ColorBook}.unity` | Scenes (no ArtBook — captures go to phone Photos) | | `Content/Gameplay/Drawings///{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) | @@ -263,13 +258,12 @@ New asmdefs follow the same convention: `Services.Capture`, `Services.Gallery`, | Scene | Scope | Status | Contents | |---|---|---|---| | `Boot.unity` | `RootLifetimeScope` | ✅ exists | All Services + Libs. Persists forever. | -| `MainMenu.unity` | `MainMenuLifetimeScope` | ⚠️ planned | Menu presenter, art book entry. | -| `ColorBook.unity` | `ColorBookLifetimeScope` | ⚠️ planned | `PaperRig`, DrawingCatalog, ShapeBuilder, Coloring, History, Capture, ColorBookFlow. | -| `ArtBook.unity` | `ArtBookLifetimeScope` | ⚠️ planned | Gallery presenter, viewer, share. | +| `MainMenu.unity` | `MainMenuLifetimeScope` | ⚠️ planned | Spine mascot, Play button. | +| `ColorBook.unity` | `ColorBookLifetimeScope` | ⚠️ planned | DrawingCatalog, ShapeBuilder, Coloring, History, Capture, ColorBookFlow. | -Only `Boot.unity` exists today; the three scene scope classes haven't been written yet either (only `RootLifetimeScope` exists in [App/LifetimeScopes/](Assets/Darkmatter/Code/App/LifetimeScopes/)). +Only `Boot.unity` exists today; the two scene scope classes haven't been written yet either (only `RootLifetimeScope` exists in [App/LifetimeScopes/](Assets/Darkmatter/Code/App/LifetimeScopes/)). -Scopes nest: `Root → (MainMenu | ColorBook | ArtBook)`. Services resolved from the root parent. Scene scopes only register their own features. +Scopes nest: `Root → (MainMenu | ColorBook)`. Services resolved from the root parent. Scene scopes only register their own features. There is **no in-app gallery** — captured drawings go to the phone's native Photos app via the gallery service. ### Boot chain (planned) @@ -359,7 +353,7 @@ If you want a backdrop (wood/cloth behind the paper area), it's a sibling `Image | Concern | RT-paper-rig (old) | Canvas-only (current) | |---|---|---| -| Files in `IPaperRig` / `IArtInputBridge` | 2 contracts + ~80 lines of math | gone | +| 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) | @@ -447,25 +441,29 @@ public readonly struct PaintCommandDTO { } ``` -### Paper (canvas surface root) +### Scene composition (no Paper feature) -> Contracts live in `Darkmatter.Core.Contracts.Features.Paper`. Files at `Core/Contracts/Features/Paper/`. +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 -namespace Darkmatter.Core.Contracts.Features.Paper; - -public interface IPaperSurface { - RectTransform Root { get; } // PaperPanel — parent of slots/pieces/regions - RectTransform SlotsParent { get; } // parent for slot Images - RectTransform PiecesParent { get; } // parent for piece Images - RectTransform RegionsParent { get; } // parent for region Images - float DesignHalfSize { get; } // half of the reference square (e.g. 1024) +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; } ``` -- Implemented by `PaperSurface : MonoBehaviour` in the ColorBook scene (sits on the `PaperPanel` GameObject). -- All paper-side features (`Coloring`, `ShapeBuilder`, `Capture`) parent their UI under one of these `RectTransform` slots and use canvas-local coords throughout. -- No `IPaperRig`. No `IArtInputBridge`. Input runs through Unity's `EventSystem` directly on the UI children. +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 @@ -489,45 +487,31 @@ public interface IUndoStack { } ``` -### Gallery & Capture +### Capture & Gallery -> `IGalleryService` is a Service contract → `Darkmatter.Core.Contracts.Services.Gallery`. `SavedArtworkDTO` is a runtime data struct → `Darkmatter.Core.Data.Dynamic.Features.Gallery`. `ICaptureService` → `Darkmatter.Core.Contracts.Services.Capture`. `CaptureAsync` takes no args — implementation owns the `CaptureCamera` reference and renders the `PaperUI` layer to a one-shot RT. +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.Data.Dynamic.Features.Gallery; +namespace Darkmatter.Core.Contracts.Services.Capture; -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 ICaptureService { + // Renders the disabled CaptureCamera into a temp RT, ReadPixels into a Texture2D, + // encodes PNG, releases the RT. Returns the encoded bytes. + UniTask CaptureAsync(); } namespace Darkmatter.Core.Contracts.Services.Gallery; public interface IGalleryService { - UniTask SaveAsync(byte[] png, string templateId); - UniTask> ListAsync(); - UniTask LoadFullAsync(string artworkId); - UniTask LoadThumbnailAsync(string artworkId); - UniTask DeleteAsync(string artworkId); - - // Newest captured thumbnail for the given template, or null if none exist. - // Catalog cells fall back to IDrawingTemplate.DefaultThumbnail when this returns null. - UniTask GetLatestThumbnailAsync(string templateId); -} - -namespace Darkmatter.Core.Contracts.Services.Capture; - -public interface ICaptureService { - // Allocates a temp RT, renders the CaptureCamera once (PaperUI layer only), - // ReadPixels into a Texture2D, encodes PNG, releases the RT. - UniTask CaptureAsync(); + // 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 a reference to `CaptureCamera` (a disabled `Camera` in the ColorBook scene). The capture camera's `cullingMask` is set to `PaperUI` so the HUD physically cannot appear in the PNG. The paper background is part of `PaperPanel`, so it's already in the right layer — no compositing pass. +- `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 @@ -549,12 +533,12 @@ public readonly struct ColorAppliedSignal { public Color Color { get; } } -public readonly struct ArtworkCapturedSignal { - public string ArtworkId { get; } +public readonly struct PaperCapturedSignal { + public string TemplateId { get; } // captured, before native-gallery save } -public readonly struct ArtworkSavedSignal { - public SavedArtworkDTO Artwork { get; } +public readonly struct PaperSavedSignal { + public string TemplateId { get; } // PNG written to phone library } ``` @@ -564,40 +548,33 @@ public readonly struct ArtworkSavedSignal { ### `MainMenu` -- Lives in `MainMenu.unity`. Two main entry buttons: **Play** (→ `ColorBook` scene) and **Art Book** (→ `ArtBook` scene). -- Hosts a **Spine character mascot** (via `SkeletonGraphic` for Canvas). The mascot has multiple authored animations — idle loop, wave, react-to-button, victory dance. -- `MenuMascotPresenter` (pure C#) drives the mascot from code: subscribes to button hover / click events and the model's idle timer, calls `IMenuMascotView.Play(animName, loop)`. +- 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). -- **Each cell shows the latest captured thumbnail** for that template via `IGalleryService.GetLatestThumbnailAsync(templateId)`. If the user has no captures yet for that template, falls back to `IDrawingTemplate.DefaultThumbnail` (the authored sprite). -- Subscribes to `ArtworkSavedSignal` — re-fetches the cell's thumbnail for the affected `TemplateId` so the grid reflects user progress without a reopen. +- 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. -### `Paper` - -- Scene-scoped infrastructure. Lives in `ColorBook.unity` only. -- Owns `PaperSurface` (MonoBehaviour) on the `PaperPanel` GameObject. Implements `IPaperSurface`, exposes `Root`, `SlotsParent`, `PiecesParent`, `RegionsParent`, `DesignHalfSize`. -- Registered in `ColorBookLifetimeScope` via `PaperSurfaceModule`. Other features resolve `IPaperSurface` from DI when they need to parent their UI under one of the role-specific `RectTransform`s. -- No render-target ownership. No input bridge. No coordinate conversion. The paper *is* the canvas children — nothing more. - ### `ShapeBuilder` - Listens to `DrawingSelectedSignal`. -- Loads template, spawns UI `Image` per piece under either `IPaperSurface.PiecesParent` or the HUD tray (depending on the FSM start state — usually tray). -- Each piece has `IBeginDragHandler` / `IDragHandler` / `IEndDragHandler` plus a per-piece `ShapePieceFsm`. Drag updates `RectTransform.anchoredPosition` directly from `PointerEventData` (converted to canvas-local via `RectTransformUtility.ScreenPointToLocalPointInRectangle`). +- Loads template, instantiates the single piece prefab once per `ShapeSO` in the template, parents under the HUD tray panel (`ColorBookSceneRefs.TrayPanel`). Each instance is `Assign(shape)`ed to its `ShapeSO`. +- `SlotMarker`s in the drawing's per-drawing prefab (under `ColorBookSceneRefs.SlotsParent`) provide target poses + matching `ShapeSO` refs. +- Each piece has `IBeginDragHandler` / `IDragHandler` / `IEndDragHandler` plus a per-piece `ShapePieceFsm`. Drag updates `RectTransform.anchoredPosition` directly from `PointerEventData`, converted to canvas-local via `RectTransformUtility.ScreenPointToLocalPointInRectangle`. - On entering preview radius of the matching slot: reactive `Lerp` of `anchoredPosition` / `sizeDelta` / `localRotation` toward `SlotMarker`'s `RectTransform`. Drives off pointer distance, not time. -- On `OnEndDrag` inside snap radius: DOTween ease-out to exact slot pose, disable input. Otherwise DOTween back to tray slot. +- On `OnEndDrag` inside snap radius: piece reparents to `ColorBookSceneRefs.PiecesParent`, DOTween ease-out to exact slot pose, disable input. Otherwise DOTween back to tray slot. - Fires `ShapeAssembledSignal` when all pieces locked. ### `Coloring` - Listens to `ShapeAssembledSignal`. -- Spawns one UI `Image` per `ColorRegionDTO` under `IPaperSurface.RegionsParent`. Each region's `Image.alphaHitTestMinimumThreshold = 0.5f` so taps on transparent pixels pass through to the next region. +- Spawns one UI `Image` per `ColorRegionDTO` under `ColorBookSceneRefs.RegionsParent`. Each region's `Image.alphaHitTestMinimumThreshold = 0.5f` so taps on transparent pixels pass through to the next region. - 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`. @@ -606,39 +583,35 @@ public readonly struct ArtworkSavedSignal { ### `History` -- Owns the singleton `IUndoStack` for the current ColorBook session. +- Owns the scoped `IUndoStack` for the current ColorBook session. - Cleared on `DrawingSelectedSignal` (new drawing = fresh history). -- Capped at ~20 entries (memory + cognitive simplicity). +- 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()` → PNG bytes. Implementation owns the disabled `CaptureCamera`, sets its `targetTexture` to a temp RT, calls `Render()` once, reads pixels, releases. -- Hands bytes to `IGalleryService.SaveAsync(...)`. -- Fires `ArtworkCapturedSignal` then `ArtworkSavedSignal`. -- Shows a quick "saved!" toast with a thumbnail of the new entry. +- Bound to the "Save" button (and triggered silently by "Next"). +- `CaptureController.SaveAsync(templateId)`: + 1. `_capture.CaptureAsync()` → PNG bytes (one-shot `CaptureCamera.Render()` into a temp RT) + 2. Publish `PaperCapturedSignal(templateId)` + 3. `_gallery.SaveToDeviceAsync(bytes, "Color Book")` → native plugin writes into phone's Photos + 4. Publish `PaperSavedSignal(templateId)` +- HUD shows a brief "Saved to Photos" toast on `PaperSavedSignal`. +- `CaptureController` is the only place that orchestrates capture-then-save. Other features never call `IGalleryService` directly. ### `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`. +- On "Next" button: silently runs `CaptureController.SaveAsync`, marks current as completed, calls `IDrawingTemplateCatalog.NextUnseen()`. +- Persists JSON via `Libs.PlayerPrefs` (`ProtectedPlayerPrefs`). ### `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. +- Coordinates "Next" sequence: `CaptureController.SaveAsync` → `IProgressionService.MarkCompleted` → `IDrawingTemplateCatalog.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 @@ -692,41 +665,40 @@ Drawing packs ship as remote bundles. New theme packs (Christmas, Dinosaurs) upd ## 11. Persistence -Two distinct stores, each behind its own Core contract. +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). -### `IPersistenceService` (JSON / PlayerPrefs) +### Settings + progression via `Libs.PlayerPrefs` -Holds: +`ProtectedPlayerPrefs` (in `Libs/PlayerPrefs/`) is a lightweight encrypted-string wrapper around Unity's `PlayerPrefs`. Used for: -- Completed template IDs. +- Completed template IDs (JSON-encoded list). - Last opened drawing. -- Audio volume, simple settings. +- Audio volume, simple toggles. -Path: `Application.persistentDataPath/save.json`. +A higher-level `IProgressionService` reads/writes these keys; consumers never touch `PlayerPrefs` directly. -### `IGalleryService` (file IO) +### Native photo library (gallery) -Holds user artworks: +Captured PNGs go to the phone's Photos app via `IGalleryService.SaveToDeviceAsync(bytes, albumName)`. The app does **not**: -``` -persistentDataPath/Gallery/ -├── {guid}.png full-res render (~2048×2048) -├── {guid}.thumb.png 256×256 for grid -└── {guid}.json SavedArtworkDTO sidecar -``` +- Write `.png` files to `persistentDataPath`. +- Generate or store thumbnails locally. +- Maintain any sidecar JSON / index. +- Provide list / load / delete operations. -- Writes are atomic (`.tmp` → rename). -- `ListAsync` enumerates sidecar JSONs sorted by `CreatedUtc desc`. -- Thumbnail generation happens once at save time on a worker thread. +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 scene, disabled by default. It renders only the `PaperUI` layer into a temp `RenderTexture` when capture fires. +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. ``` -[Capture button or Next button] +[Save button or Next button] + │ + ▼ +CaptureController.SaveAsync(templateId) │ ▼ ICaptureService.CaptureAsync() @@ -745,14 +717,16 @@ ICaptureService.CaptureAsync() ├─ Object.Destroy(tex) └─ return bytes ▼ -IGalleryService.SaveAsync(bytes, templateId) +EventBus.Publish(new PaperCapturedSignal(templateId)) │ - ├─ Write .png atomically - ├─ Generate + write thumbnail - ├─ Write sidecar JSON - └─ return SavedArtworkDTO ▼ -EventBus.Publish(new ArtworkSavedSignal(dto)) +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: @@ -762,6 +736,7 @@ Notes: - 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. --- @@ -769,7 +744,7 @@ Notes: | Use case | Mechanism | |---|---| -| Load template, return result | Direct DI call (`IDrawingTemplateLoader.LoadAsync`). | +| 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. | @@ -819,7 +794,7 @@ These shape several design decisions and are **non-negotiable**: | Layer | Test type | Location | |---|---|---| | `Libs/CommandStack` | EditMode unit tests | `Libs/CommandStack/Tests/` | -| `Core` DTOs | EditMode | rarely needed, but for `SavedArtworkDTO` serialization, yes. | +| `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`. | @@ -852,14 +827,14 @@ When in doubt, ask: *would deleting this feature break Core?* If yes, the depend | 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) | `ArtworkCapturedSignal`, `ArtworkSavedSignal` | -| `Progression` | `ArtworkSavedSignal` | — | -| `ColorBookFlow` | `ShapeAssembledSignal`, `ArtworkSavedSignal` | — | -| `ArtBook (Gallery)` | `ArtworkSavedSignal` (if open) | — | +| `Capture` | — (button-driven) | `PaperCapturedSignal`, `PaperSavedSignal` | +| `Progression` | `PaperSavedSignal` | — | +| `ColorBookFlow` | `ShapeAssembledSignal`, `PaperSavedSignal` | — | --- @@ -894,19 +869,17 @@ Every Lib / Service / Feature is its own `.asmdef`. The `Darkmatter.` prefix is | Asmdef | Path | References | |---|---|---| -| `Libs.CommandStack` | `Libs/CommandStack/` | `Core` | | `Services.Capture` | `Services/Capture/` | `Core`, `Libs.Installers` | | `Services.Gallery` | `Services/Gallery/` | `Core`, `Libs.Installers` | -| `Features.Paper` | `Features/Paper/` | `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`, `Libs.CommandStack` | -| `Features.History` | `Features/History/` | `Core`, `Libs.Installers`, `Libs.CommandStack` | +| `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` | -| `Features.ArtBook` | `Features/ArtBook/` | `Core`, `Libs.Installers` | + +> `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. @@ -960,7 +933,6 @@ public sealed class ColorBookLifetimeScope : LifetimeScope { ``` Drag the scene's installer MonoBehaviours into `sceneModules[]`: -- `PaperRigModule` - `DrawingCatalogModule` - `ShapeBuilderModule` - `ColoringModule` @@ -1002,7 +974,7 @@ Convention: - `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(_foo)` them. See the planned `PaperRigModule` in §32.5b for an example. +- If the installer needs to wire scene-bound MonoBehaviours into DI, expose them as `[SerializeField]` fields on the installer itself and `builder.RegisterInstance(_foo)` them. `ColorBookSceneRefs` (§32.13) is registered this way directly from the scope's serialized field. --- @@ -1206,7 +1178,7 @@ private void SnapToSlot() { Three things to note: -1. **Reparent** the piece from `TrayPanel` (HUD canvas) to `IPaperSurface.PiecesParent` (PaperCanvas) so it'll be included in capture. `worldPositionStays: false` because we want the new `anchoredPosition` to be relative to the new parent, not the world. +1. **Reparent** the piece from `TrayPanel` (HUD canvas) to `ColorBookSceneRefs.PiecesParent` (PaperCanvas) so it'll be included in capture. `worldPositionStays: false` because we want the new `anchoredPosition` to be relative to the new parent, not the world. 2. **Three simultaneous tweens** — position, size, rotation. Use `DOAnchorPos`, `DOSizeDelta`, `DOLocalRotateQuaternion`. They start together so the piece visually snaps as one motion. 3. **`SnapRadius` is in canvas units** (from `ShapeBuilderConfig`, e.g. 80–120), not world units. Same `CanvasScaler` reference resolution across devices = same hit feel. @@ -1237,25 +1209,26 @@ PaperPanel --- -## 28. SavedArtwork JSON Schema +## 28. Native Gallery Integration -```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" } - ] -} -``` +`IGalleryService.SaveToDeviceAsync(byte[] png, string albumName)` is the only operation. Implementations wrap a native plugin — recommended packages: -`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). +| 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` | -Paths are **relative** to `persistentDataPath`. Never store absolute paths — they change between OS updates on some platforms. +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. --- @@ -1289,7 +1262,7 @@ Toddler-mode error UI: 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`, `ArtBook`) don't exist yet — they're listed in §6 / §4c as planned work. +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///`, 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). @@ -1302,28 +1275,121 @@ Toddler-mode error UI: |---|---|---| | `IDrawingTemplate`, `ColorRegionDTO` | Core | `Core` | | `ShapeSO` (ScriptableObject) | Core | `Core` | -| `IPaperSurface` | Core | `Core` | | `ICommand`, `IUndoStack` | Core | `Core` | -| `BoundedUndoStack` | Libs | `Libs.CommandStack` | +| `UndoStack` | Features | `Features.History` | | `AddressableAssetProviderService` | Services | `Services.Assets` | -| `FileGalleryService` | Services | `Services.Gallery` | +| `NativeGallerySaveService` | Services | `Services.Gallery` | | `RenderTextureCaptureService` | Services | `Services.Capture` | -| `PaperSurface`, `PaperSurfaceModule` | Features | `Features.Paper` | | `ColoringController`, `PaintRegionCommand` | Features | `Features.Coloring` | -| `ShapeBuilderController`, `ShapePieceView` | Features | `Features.ShapeBuilder` | +| `ShapeBuilderController`, `ShapePieceUI` | Features | `Features.ShapeBuilder` | | `HistoryController` | Features | `Features.History` | | `ColorBookFlowController` | Features | `Features.ColorBookFlow` | -| `GalleryPresenter`, `GalleryGridView` | Features | `Features.ArtBook` | | `MenuMascotView`, `MenuMascotPresenter` | Features | `Features.MainMenu` | -| `ColorBookLifetimeScope`, `AppBoot` | App | `Darkmatter.App` | +| `ColorBookSceneRefs`, `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. +### 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/Drawing/` | `IDrawingTemplate`, `IDrawingTemplateCatalog` | ⚠️ | +| `Core/Contracts/Features/Coloring/` | `IColorPalette` | ⚠️ | +| `Core/Contracts/Features/History/` | `ICommand`, `IUndoStack` | ✅ | +| `Core/Contracts/Features/Progression/` | `IProgressionService` | ⚠️ | +| `Core/Data/Dynamic/Services/Audio/` | `AudioHandle`, `AudioRequest` | ✅ | +| `Core/Data/Static/Services/Audio/` | `SfxCatalogSO` | ✅ | +| `Core/Data/Static/Features/Drawing/` | `DrawingTemplateSO`, `ShapeSO` | ⚠️ | +| `Core/Data/Static/Features/Coloring/` | `ColorPaletteSO` | ⚠️ | +| `Core/Data/Dynamic/Features/Drawing/` | `ColorRegionDTO` | ⚠️ | +| `Core/Data/Dynamic/Features/Coloring/` | `PaintCommandDTO` | ⚠️ | +| `Core/Data/Dynamic/Features/Signals/` | `DrawingSelectedSignal`, `ShapeAssembledSignal`, `ColorAppliedSignal`, `PieceSnappedSignal`, `PaperCapturedSignal`, `PaperSavedSignal` | ⚠️ | +| `Core/Enums/Services/Audio/` | `AudioChannel`, `AudioPlayMode`, `SfxId` | ✅ | +| `Core/Enums/Services/Camera/` | `CameraType` (add `CaptureCamera` value) | ✅ | +| `Core/Enums/Services/Scenes/` | `GameScene` | ✅ | + +#### Libs + +| Module (path) | Scripts | Status | +|---|---|---| +| `Libs/FSM/` | `IState`, `State`, `StateMachine` | ✅ | +| `Libs/Installers/` | `IServiceModule` | ✅ | +| `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/` | `AnalyticsServiceModule` | ✅ | +| `Services/Analytics/Systems/` | `FirebaseAnalyticsSystem` | ✅ | +| `Services/Assets/` | `AddressableAssetProviderService`, `AddressableLoadHandleTracker` | ✅ | +| `Services/Audio/` | `AudioService`, `SfxPlayer` | ✅ | +| `Services/Camera/Service/` | `CameraService` | ✅ | +| `Services/Camera/Installers/` | `CameraServiceModule` | ✅ | +| `Services/Inputs/Generated/` | `GameInputs` (Input System codegen) | ✅ | +| `Services/Inputs/Readers/` | `InputReaderSO` | ✅ | +| `Services/Inputs/Installers/` | `InputServiceModule` | ✅ | +| `Services/Scenes/` | `SceneService` | ✅ | +| `Services/Capture/` | `RenderTextureCaptureService`, `CaptureServiceModule` | ⚠️ | +| `Services/Gallery/` | `NativeGallerySaveService`, `GalleryServiceModule` | ⚠️ | + +#### Features + +| Module (path) | Scripts | Status | +|---|---|---| +| `Features/History/Stack/` | `UndoStack` | ✅ | +| `Features/History/Installers/` | `HistoryServiceModule` | ✅ | +| `Features/History/UI/` | `HistoryButtonsView`, `HistoryPresenter`, `HistoryController` | ⚠️ | +| `Features/MainMenu/Installers/` | `MainMenuModule` | ⚠️ | +| `Features/MainMenu/Systems/` | `MainMenuModel`, `MenuMascotPresenter` | ⚠️ | +| `Features/MainMenu/UI/` | `MenuMascotView`, `IMenuMascotView` | ⚠️ | +| `Features/DrawingCatalog/Systems/` | `DrawingCatalogController` | ⚠️ | +| `Features/DrawingCatalog/UI/` | `DrawingCatalogPresenter`, `DrawingCatalogView`, `IDrawingCatalogView`, `CatalogItemVM` | ⚠️ | +| `Features/DrawingCatalog/Installers/` | `DrawingCatalogModule` | ⚠️ | +| `Features/ShapeBuilder/Systems/` | `ShapeBuilderController`, `ShapePieceFsm`, `ShapePieceFactory`, `TrayLayout` | ⚠️ | +| `Features/ShapeBuilder/UI/` | `ShapePieceUI`, `SlotMarker`, `TrayPanel` | ⚠️ | +| `Features/ShapeBuilder/Installers/` | `ShapeBuilderModule` | ⚠️ | +| `Features/Coloring/Systems/` | `ColoringController`, `ColoringStateRepository`, `ColorRegionFactory` | ⚠️ | +| `Features/Coloring/UI/` | `ColorRegionView`, `ColorPaletteView`, `ColorPalettePresenter` | ⚠️ | +| `Features/Coloring/Commands/` | `PaintRegionCommand` | ⚠️ | +| `Features/Coloring/Installers/` | `ColoringModule` | ⚠️ | +| `Features/Capture/Systems/` | `CaptureController` | ⚠️ | +| `Features/Capture/UI/` | `CaptureButtonPresenter`, `SaveToastView` | ⚠️ | +| `Features/Capture/Installers/` | `CaptureFeatureModule` | ⚠️ | +| `Features/Progression/Systems/` | `ProgressionService`, `ProgressionRepository` | ⚠️ | +| `Features/Progression/Installers/` | `ProgressionModule` | ⚠️ | +| `Features/ColorBookFlow/Systems/` | `ColorBookFlowController` | ⚠️ | +| `Features/ColorBookFlow/Installers/` | `ColorBookFlowModule` | ⚠️ | + +#### App + +| Module (path) | Scripts | Status | +|---|---|---| +| `App/LifetimeScopes/` | `RootLifetimeScope` | ✅ | +| `App/LifetimeScopes/` | `MainMenuLifetimeScope`, `ColorBookLifetimeScope` | ⚠️ | +| `App/Boot/` | `AppBoot` | ⚠️ | +| `App/SceneRefs/` | `ColorBookSceneRefs` | ⚠️ | + --- ## 32. Class Reference (Detailed) -> **Status: target spec, mostly unimplemented.** Of everything below, only the following Service classes exist on disk today: `AddressableAssetProviderService`, `AudioService` / `SfxPlayer`, `CameraService`, `SceneService`, `InputReaderSO`, `FirebaseAnalyticsSystem`. Everything else (Paper, Drawing, Coloring, History, Capture, Gallery, Progression, ColorBookFlow, ArtBook, AppBoot) is the target shape for when those classes are written. Treat this section as a contract for new code, not documentation of current state. +> **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). @@ -1354,7 +1420,7 @@ public interface IDrawingTemplate { ``` 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 shows the latest user-captured thumbnail (via `IGalleryService.GetLatestThumbnailAsync`) when available, falling back to `DefaultThumbnail` when the user hasn't completed this template yet. The template itself stays immutable. +> 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. @@ -1382,43 +1448,29 @@ public interface IColorPalette { Already shown in section 8. Each undoable user action is one `ICommand`; the stack is bounded. #### `IGalleryService` *(Core/Contracts/Services/Gallery — planned)* -Persistent store of saved artwork PNGs. +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 SaveAsync(byte[] png, string templateId); - UniTask> ListAsync(); // sorted newest first - UniTask LoadFullAsync(string artworkId); // fullscreen view - UniTask LoadThumbnailAsync(string artworkId); - UniTask DeleteAsync(string artworkId); - - // Newest captured thumbnail for the given template, or null if the user has - // no captures for it. Used by the catalog grid; null → caller falls back to - // IDrawingTemplate.DefaultThumbnail. - UniTask GetLatestThumbnailAsync(string templateId); + UniTask SaveToDeviceAsync(byte[] png, string albumName = "Color Book"); } ``` -For v1 the latest-thumbnail lookup can list-and-filter (tens of templates max). Add an in-memory `Dictionary` cache later if perf becomes a concern. +- 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 RT to a PNG blob. No arguments — dimensions and content come from `IPaperRig.Surface`. +Snapshots the paper area to a PNG blob. No arguments — implementation owns the disabled `CaptureCamera` reference. ```csharp public interface ICaptureService { UniTask CaptureAsync(); } ``` +- Independent of `IGalleryService`. Returns raw PNG bytes; what happens next is the caller's call (save, share, discard). -#### `IPaperSurface` *(Core/Contracts/Features/Paper — planned)* -The paper is just RectTransform real estate. Features parent their UI children under one of the role-specific roots. -```csharp -public interface IPaperSurface { - RectTransform Root { get; } // PaperPanel itself - RectTransform SlotsParent { get; } // child of Root — for ShapeBuilder slot outlines - RectTransform PiecesParent { get; } // child of Root — for ShapeBuilder pieces (post-snap) - RectTransform RegionsParent { get; } // child of Root — for Coloring region Images - float DesignHalfSize { get; } // half the reference resolution side, in canvas units -} -``` -No render-target ownership. No coordinate conversion. The contract just hands out RectTransforms so features don't have to `Find` them. +#### 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. @@ -1466,18 +1518,27 @@ Implements `IAssetProviderService`. - **State:** `Dictionary` keyed by address. - **Notes:** `Release(address)` decrements; `ReleaseAll()` for scene teardown. Initialization must complete before any other service may load. -#### `FileGalleryService` *(Services/Gallery — planned)* -Implements `IGalleryService`. +#### `NativeGallerySaveService` *(Services/Gallery — planned)* +Implements `IGalleryService` as a thin wrapper around a native gallery plugin. ```csharp // fields: -// IPathProvider _paths (wraps Application.persistentDataPath for tests) -// IThumbnailGenerator _thumb (downscale + encode) -// IEventBus _bus -// pub: ArtworkSavedSignal, ArtworkDeletedSignal +// 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"); + } +} ``` -- **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). +- **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. @@ -1492,17 +1553,13 @@ Implements `ICaptureService`. Drives the scene's disabled `CaptureCamera` once p - **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. -#### `JsonPersistenceService` *(Services/Persistence — planned; today `Libs/PlayerPrefs` covers small-key state)* -Implements `IPersistenceService` (small JSON blob; not the gallery). -```csharp -public interface IPersistenceService { - UniTask LoadAsync(string key) where T : class, new(); - UniTask SaveAsync(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. +#### 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. @@ -1611,9 +1668,9 @@ Pure C#. Subscribes to button events + idle timer, drives the view. public sealed class MenuMascotPresenter : IStartable, IDisposable { public void Start() { _view.Play("idle", loop: true); - _model.PlayButtonHovered += () => _view.Play("hover_play", loop: false); - _model.ArtBookButtonHovered += () => _view.Play("hover_artbook", loop: false); - _view.AnimationComplete += OnAnimationComplete; + _model.PlayButtonHovered += () => _view.Play("hover_play", loop: false); + _model.PlayButtonPressed += () => _view.Play("wave", loop: false); + _view.AnimationComplete += OnAnimationComplete; } private void OnAnimationComplete(string anim) { @@ -1677,7 +1734,7 @@ public interface IDrawingCatalogView { Spawns shape pieces for the selected template, tracks snap progress, fires `ShapeAssembledSignal` when complete. ```csharp // fields: IDrawingTemplateCatalog _catalog, ShapePieceFactory _factory, -// IPaperSurface _paper, TrayPanel _tray, IEventBus _bus, ShapeBuilderConfig _cfg +// ColorBookSceneRefs _refs, TrayPanel _tray, IEventBus _bus, ShapeBuilderConfig _cfg public sealed class ShapeBuilderController : IDisposable { public IReadOnlyList Active { get; } public UniTask BuildAsync(string templateId); // load template, spawn pieces in tray @@ -1687,7 +1744,7 @@ public sealed class ShapeBuilderController : IDisposable { // pub: ShapeAssembledSignal ``` - **Internal:** counts `PieceSnappedSignal` against expected piece count. -- **Slot discovery:** after a drawing's per-drawing prefab is instantiated under `IPaperSurface.Root`, the controller queries `GetComponentsInChildren()` to discover all slots in the loaded drawing. Each slot's `_shape` field tells which `ShapeSO` it expects; matching pieces are spawned in the tray. +- **Slot discovery:** after a drawing's per-drawing prefab is instantiated under `ColorBookSceneRefs.PaperRoot`, the controller queries `GetComponentsInChildren()` to discover all slots in the loaded drawing. Each slot's `_shape` field tells which `ShapeSO` it expects; matching pieces are spawned in the tray. #### `ShapePieceUI : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler` *(UI)* The UI Image that the toddler drags. One prefab; the assigned `ShapeSO` determines visual identity and snap params. @@ -1743,7 +1800,7 @@ public sealed class ShapePieceFsm { - **Returning enter**: DOTween back to tray slot (`anchoredPosition` from `TrayLayout`). #### `SlotMarker : MonoBehaviour` *(UI)* -The outline `Image` on `IPaperSurface.SlotsParent` showing where a piece should snap. Authored per drawing — designer places one in the scene at each slot location, with its `RectTransform` set to the target pose and `_shape` field assigned to the matching `ShapeSO`. +The outline `Image` under `ColorBookSceneRefs.SlotsParent` showing where a piece should snap. Authored per drawing — designer places one in the per-drawing prefab at each slot location, with its `RectTransform` set to the target pose and `_shape` field assigned to the matching `ShapeSO`. ```csharp public sealed class SlotMarker : MonoBehaviour { [SerializeField] private ShapeSO _shape; // which shape fits here @@ -1777,43 +1834,6 @@ With UI handlers on the piece itself, an explicit input binder isn't strictly ne --- -### 32.5b Feature — `Paper` *(planned)* - -A tiny feature. Just exposes the paper RectTransforms via DI so consumers don't `Find` them. - -#### `PaperSurface : MonoBehaviour, IPaperSurface` *(Surface)* -Scene-bound component placed on the `PaperPanel` GameObject in `ColorBook.unity`. -```csharp -// inspector fields: -// RectTransform _slotsParent -// RectTransform _piecesParent -// RectTransform _regionsParent -// float _designHalfSize = 1024f // half of 2048 reference resolution - -public sealed class PaperSurface : MonoBehaviour, IPaperSurface { - public RectTransform Root => (RectTransform)transform; - public RectTransform SlotsParent => _slotsParent; - public RectTransform PiecesParent => _piecesParent; - public RectTransform RegionsParent => _regionsParent; - public float DesignHalfSize => _designHalfSize; -} -``` -- No `Awake` / `OnDestroy` logic. The component is a pure pass-through to the RectTransforms. -- All four child rects share the same anchors and size as `Root` (anchored center, stretched to fill). - -#### `PaperSurfaceModule : MonoBehaviour, IServiceModule` *(Installers)* -Scene-scoped installer. Dragged into `ColorBookLifetimeScope.sceneModules[]`. -```csharp -// inspector fields: -// PaperSurface _surface - -public void Register(IContainerBuilder builder) { - builder.RegisterInstance(_surface); -} -``` -Registers as `Instance` because `PaperSurface` is a MonoBehaviour already in the scene. Lifetime tied to the scene. - ---- ### 32.6 Feature — `Coloring` *(planned)* @@ -1835,7 +1855,7 @@ public sealed class ColoringStateRepository { Builds and pushes `PaintRegionCommand` instances; spawns `ColorRegionView` per region. ```csharp // fields: IUndoStack _undo, ColoringStateRepository _state, ColorRegionFactory _factory, -// IPaperSurface _paper, IEventBus _bus +// ColorBookSceneRefs _refs, IEventBus _bus public interface IColoringController { UniTask SpawnRegionsAsync(IDrawingTemplate template); void PaintRegion(ColorRegionView view); // builds command, pushes to undo stack @@ -1908,21 +1928,22 @@ Wires controller `StateChanged` ↔ view enable/disable; view click events → c ### 32.8 Feature — `Capture` *(planned)* #### `CaptureController` *(Systems)* -The orchestrator behind the "Capture" button. Stateless other than guarding against concurrent captures. +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 IsCapturing { get; } - public UniTask CaptureCurrentAsync(string templateId); + public bool IsBusy { get; } + public UniTask SaveAsync(string templateId); } -// pub: ArtworkCapturedSignal (mid-flow), ArtworkSavedSignal (post-save) +// pub: PaperCapturedSignal (mid-flow), PaperSavedSignal (after native save) ``` -- **Flow:** `_capture.CaptureAsync()` → `_gallery.SaveAsync(bytes, templateId)` → publish signals. -- **Concurrency:** sets `IsCapturing = true` on entry; UI binds button enabled to `!IsCapturing` to prevent double-tap. -- **No camera or sprite args** — the implementation owns a reference to the disabled `CaptureCamera` and drives the one-shot render internally. +- **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.CaptureCurrentAsync`. Disables button while in progress. Shows toast on `ArtworkSavedSignal`. +Wires button click → `CaptureController.SaveAsync(currentTemplateId)`. Disables button while `IsBusy`. Shows a "Saved to Photos" toast on `PaperSavedSignal`. --- @@ -1930,7 +1951,7 @@ Wires button click → `CaptureController.CaptureCurrentAsync`. Disables button #### `ProgressionService` *(Systems)* — implements `IProgressionService` The only place that knows what "completed" means. -- **Persistence:** delegates to `IPersistenceService` under key `"progression"`. +- **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. @@ -1967,45 +1988,12 @@ Pure in-memory holder used by the service. Separated so tests can inspect state --- -### 32.11 Feature — `ArtBook` *(planned)* - -#### `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 OnArtworkTapped; - event Action OnDeleteRequested; - event Action OnShareRequested; - void SetItems(IReadOnlyList 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 — planned; folder doesn't exist yet)* — `IAsyncStartable` Single entry point. Steps in section 29. ```csharp -// fields: IAssetProviderService _assets, IPersistenceService _persist, IProgressionService _progress, +// fields: IAssetProviderService _assets, IProgressionService _progress, // IAudioService _audio, ISceneService _scenes, BootConfig _cfg public sealed class AppBoot : IAsyncStartable { public UniTask StartAsync(CancellationToken ct); @@ -2015,8 +2003,7 @@ public sealed class AppBoot : IAsyncStartable { #### 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 `PaperRigModule`, feature installers, and the flow controller installer. -- `ArtBookLifetimeScope` — planned. +- `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`. @@ -2024,10 +2011,23 @@ All scope classes are thin: a serialized installer-MonoBehaviour list (+ optiona ### 32.13 Cross-cutting types -#### `ColorBookSceneRefs : MonoBehaviour` *(App — planned, optional)* -Aggregates HUD-side scene-bound Unity references that don't fit any single feature. Examples: `Camera captureCamera`, `RectTransform hudRoot`, `ColorPaletteView paletteView`, `HistoryButtonsView historyView`, `TrayPanel trayPanel`. Registered in `ColorBookLifetimeScope` via `builder.RegisterInstance(_sceneRefs)` so features don't `Find` things. +#### `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. -> Paper-side refs are subsumed by `IPaperSurface` (which exposes the four canvas RectTransform roots). `CaptureCamera` could either live here or be exposed via its own dedicated `ICaptureCameraSource` contract — for v1, putting it on `ColorBookSceneRefs` is fine. +```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 @@ -2043,40 +2043,43 @@ Implemented as `MonoBehaviour` per feature/service so scopes can drag them in th | 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, paper, tray, bus, cfg | -| `ShapeSO` | Core asset | Authored shape (sprite + snap params) | — | -| `ShapePieceUI` | Feature | Draggable UI piece prefab; holds `[SerializeField] ShapeSO _shape` | fsm | -| `ShapePieceFsm` | Feature | Per-piece state machine | ui, slot, cfg, audio, bus | -| `SlotMarker` | Feature | Slot outline UI Image at target pose | — | -| `TrayPanel` | Feature | HUD-side tray with LayoutGroup | — | -| `ColoringStateRepository` | Feature | Current color model | — | -| `ColoringController` | Feature | Region spawn + paint cmd | undo, state, factory, paper, bus | -| `ColorRegionView` | Feature | Region UI Image + IPointerClickHandler | controller | -| `PaintRegionCommand` | Feature | Undoable paint (sets Image.color) | view, bus | -| `PaperSurface` | Feature | IPaperSurface (Root + child rects) | — | -| `PaperSurfaceModule` | Feature | DI registration | surface | -| `HistoryController` | Feature | Undo/redo facade | undo stack, bus | -| `CaptureController` | Feature | Capture+save orchestration | capture svc, gallery, bus | -| `ColorBookFlowController` | Feature | Scene FSM | bus, catalog, builder, coloring, capture, progression | -| `GalleryPresenter` | Feature | Art book listing | gallery, share, view, bus | +| `AppBoot` | App | Startup sequencer | assets, progression, audio, scenes | +| `RootLifetimeScope` | App | Root DI | service modules | +| `ColorBookLifetimeScope` | App | Scene DI | scene refs, feature modules | +| `MainMenuLifetimeScope` | App | Menu scene DI | feature modules | +| `ColorBookSceneRefs` | App | Scene-bound RectTransform / Camera holder | — | | `MenuMascotView` | Feature | Spine mascot UI (SkeletonGraphic wrapper) | — | | `MenuMascotPresenter` | Feature | Drives mascot animations from model events | view, model | -| `BoundedUndoStack` | Lib | Capped undo store | — | +| `DrawingCatalogController` | Feature | Grid logic | catalog, bus | +| `DrawingCatalogPresenter` | Feature | UI bridge | view, controller, catalog | +| `ShapeSO` | Core asset | Authored shape (sprite + snap params, reusable) | — | +| `ShapeBuilderController` | Feature | Piece spawn + snap tracking | catalog, factory, refs, tray, bus, cfg | +| `ShapePieceUI` | Feature | Draggable UI piece prefab; holds `[SerializeField] ShapeSO _shape` | fsm | +| `ShapePieceFsm` | Feature | Per-piece state machine (Tray/Drag/Preview/Snapped/Returning) | ui, slot, cfg, audio, bus | +| `SlotMarker` | Feature | Slot outline UI Image at target pose; holds `_shape` | — | +| `TrayPanel` | Feature | HUD-side tray with LayoutGroup | — | +| `ColoringStateRepository` | Feature | Current color model | — | +| `ColoringController` | Feature | Region spawn + paint cmd | undo, state, factory, refs, bus | +| `ColorRegionView` | Feature | Region UI Image + IPointerClickHandler | controller | +| `PaintRegionCommand` | Feature | Undoable paint (sets Image.color) | view, bus | +| `HistoryController` | Feature | Undo/redo facade | undo stack, bus | +| `UndoStack` | Feature | Bounded undo store | — | +| `CaptureController` | Feature | Capture-then-save orchestration | capture svc, gallery svc, bus | +| `ColorBookFlowController` | Feature | Scene FSM (Catalog → Building → Coloring → Done) | bus, catalog, builder, coloring, capture, progression | +| `ProgressionService` | Feature | Completion tracking | PlayerPrefs lib | | `EventBus` | Lib | Pub/sub | — | -| `Fsm` | Lib | Generic FSM | — | +| `StateMachine` | Lib | Generic FSM | — | +| `IServiceModule` | Lib | DI installer interface | — | +| `ProtectedPlayerPrefs` | Lib | Encrypted PlayerPrefs wrapper | — | | `AddressableAssetProviderService` | Service | Addressables wrapper | — | -| `FileGalleryService` | Service | Gallery file IO | paths, thumb gen, bus | -| `RenderTextureCaptureService` | Service | PNG render from rig.Surface | paper rig | -| `JsonPersistenceService` | Service | Settings/progression IO | — | +| `RenderTextureCaptureService` | Service | One-shot PNG render via CaptureCamera | scene refs | +| `NativeGallerySaveService` | Service | Native gallery save (thin plugin shim) | — | | `SceneService` | Service | Async scene loads | — | -| `AudioService` | Service | SFX playback | assets | -| `ProgressionService` | Service | Completion tracking | persistence | +| `AudioService`, `SfxPlayer` | Service | SFX playback | assets | +| `CameraService` | Service | Camera registry (MainCamera, UICamera, CaptureCamera) | — | +| `InputReaderSO` | Service | New Input System reader | — | +| `FirebaseAnalyticsSystem` | Service | 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. +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. -> Today only these rows are real on disk: `RootLifetimeScope` (App), `AddressableAssetProviderService` (Service), `AudioService` (Service), `CameraService` (Service), `SceneService` (Service), `InputReaderSO` (Service), plus the Firebase analytics class, plus the `Libs.*` entries (`EventBus`, `StateMachine`, `IServiceModule`, PlayerPrefs lib, UI toggles). Everything else is the target. +> Today only these rows are real on disk: `RootLifetimeScope` (App), `AddressableAssetProviderService`, `AudioService`/`SfxPlayer`, `CameraService`, `SceneService`, `InputReaderSO`, `FirebaseAnalyticsSystem` (Services), `UndoStack` + `HistoryServiceModule` (Features.History), plus `Libs.*` entries (`EventBus`, `StateMachine`, `IServiceModule`, PlayerPrefs lib, UI toggles). Everything else is the target.