From 848b176953b24979bebe0edad24622935611bd83 Mon Sep 17 00:00:00 2001 From: Savya Bikram Shah Date: Wed, 27 May 2026 15:07:13 +0545 Subject: [PATCH] docs(readme): drop Paper feature, simplify gallery, add scripts inventory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Major restructure: - Remove Paper feature entirely (IPaperSurface, PaperSurface, PaperSurfaceModule). Paper is just RectTransforms in the ColorBook scene, exposed via a ColorBookSceneRefs MonoBehaviour the scope registers as a singleton. - Simplify IGalleryService to a single SaveToDeviceAsync(byte[], albumName) shim over a native gallery plugin. Drop ListAsync / LoadFullAsync / LoadThumbnailAsync / DeleteAsync / GetLatestThumbnailAsync / SavedArtworkDTO / sidecar JSON / persistentDataPath gallery folder. - Drop ArtBook feature (no in-app gallery — users view captures in phone Photos). Removes ArtBookLifetimeScope, GalleryPresenter, IExternalShareService. - Replace ArtworkCapturedSignal / ArtworkSavedSignal with PaperCapturedSignal / PaperSavedSignal (templateId only). - Capture and Gallery are now independent: ICaptureService produces PNG bytes, IGalleryService writes them to native Photos. CaptureController orchestrates the chain. - Rewrite §11 Persistence — only ProtectedPlayerPrefs for settings/progression; no app-side image store. - Remove §28 SavedArtwork JSON Schema; replace with §28 Native Gallery Integration (plugin recommendations, permission flows). - Add §31b: Scripts Inventory by Domain — comprehensive path-by-path table of every script (existing and planned) across Core / Libs / Services / Features / App, with status markers. - Update Readme.docx — better formatting (page break before each numbered section, blue heading underlines, alternating row shading on tables, monospace code blocks with left accent bar, expanded title page). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Services/Gallery/IGalleryService.cs | 2 +- .../Services/Gallery/Core/GalleryService.cs | 30 +- Readme.docx | Bin 0 -> 68737 bytes Readme.md | 705 +++++++++--------- 4 files changed, 383 insertions(+), 354 deletions(-) create mode 100644 Readme.docx 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 0000000000000000000000000000000000000000..a2cd684864dca9223279e77bb6a0e6aa34e73708 GIT binary patch literal 68737 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

IwO+oU%@|$-8=nFxr6zgTI9r4&~ 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 Vfc`;5m;jgn!SCNT-T?pr{|mW?!#Dr{ literal 0 HcmV?d00001 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.