Readme asset

This commit is contained in:
Savya Bikram Shah
2026-05-27 13:13:47 +05:45
parent f18b78db24
commit d3d16439b6

491
Readme.md
View File

@@ -98,7 +98,7 @@ Assets/Darkmatter/
│ ├── Compatibility/ │ ├── Compatibility/
│ │ └── IsExternalInit.cs (C#9 init shim for older runtimes) │ │ └── IsExternalInit.cs (C#9 init shim for older runtimes)
│ ├── Contracts/ │ ├── Contracts/
│ │ ├── Paper/ ← misplaced empty folder — should be Contracts/Features/Paper/ (delete or move) │ │ ├── Paper/ ← misplaced empty folder — move to Contracts/Features/Paper/ when IPaperSurface lands
│ │ └── Services/ │ │ └── Services/
│ │ ├── Assets/IAssetProviderService.cs │ │ ├── Assets/IAssetProviderService.cs
│ │ ├── Audio/IAudioService.cs, ISfxPlayer.cs │ │ ├── Audio/IAudioService.cs, ISfxPlayer.cs
@@ -175,7 +175,7 @@ Rough landing order for ColorBook scene to be playable:
| Path | Role | | Path | Role |
|---|---| |---|---|
| `Core/Contracts/Features/Paper/IPaperRig.cs`, `IArtInputBridge.cs` | Paper rig contracts | | `Core/Contracts/Features/Paper/IPaperSurface.cs` | Paper surface contract (canvas roots) |
| `Core/Contracts/Services/Capture/ICaptureService.cs` | Capture service contract | | `Core/Contracts/Services/Capture/ICaptureService.cs` | Capture service contract |
| `Core/Contracts/Services/Gallery/IGalleryService.cs` | Gallery service contract | | `Core/Contracts/Services/Gallery/IGalleryService.cs` | Gallery service contract |
| `Core/Contracts/Features/Drawing/IDrawingTemplate.cs`, `IDrawingTemplateCatalog.cs` | Drawing template contracts | | `Core/Contracts/Features/Drawing/IDrawingTemplate.cs`, `IDrawingTemplateCatalog.cs` | Drawing template contracts |
@@ -190,9 +190,9 @@ Rough landing order for ColorBook scene to be playable:
| `Core/Data/Dynamic/Features/Signals/` (DrawingSelectedSignal, ShapeAssembledSignal, ColorAppliedSignal, ArtworkCapturedSignal, ArtworkSavedSignal) | Cross-feature signal structs | | `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 | | `Core/Enums/Services/Camera/CameraType.cs` | Add `ArtCamera` enum value to existing file |
| `Libs/CommandStack/` (+ `Libs.CommandStack.asmdef`) | Bounded undo/redo | | `Libs/CommandStack/` (+ `Libs.CommandStack.asmdef`) | Bounded undo/redo |
| `Services/Capture/` (+ `Services.Capture.asmdef`) | `RenderTextureCaptureService` reads `IPaperRig.Surface` | | `Services/Capture/` (+ `Services.Capture.asmdef`) | `RenderTextureCaptureService` drives the disabled `CaptureCamera` |
| `Services/Gallery/` (+ `Services.Gallery.asmdef`) | `FileGalleryService` — PNG + sidecar JSON IO | | `Services/Gallery/` (+ `Services.Gallery.asmdef`) | `FileGalleryService` — PNG + sidecar JSON IO |
| `Features/Paper/` (+ `Features.Paper.asmdef`) | Scene-bound RT rig | | `Features/Paper/` (+ `Features.Paper.asmdef`) | Scene-bound `PaperSurface` MB + module |
| `Features/{MainMenu,DrawingCatalog,ShapeBuilder,Coloring,History,Capture,Progression,ColorBookFlow,ArtBook}/` (+ asmdefs each) | Game features | | `Features/{MainMenu,DrawingCatalog,ShapeBuilder,Coloring,History,Capture,Progression,ColorBookFlow,ArtBook}/` (+ asmdefs each) | Game features |
| `App/LifetimeScopes/{MainMenu,ColorBook,ArtBook}LifetimeScope.cs` | Per-scene scopes | | `App/LifetimeScopes/{MainMenu,ColorBook,ArtBook}LifetimeScope.cs` | Per-scene scopes |
| `App/Boot/AppBoot.cs` | Bootstrap entry point | | `App/Boot/AppBoot.cs` | Bootstrap entry point |
@@ -283,59 +283,87 @@ Failures show a child-friendly retry screen; never crash.
## 7. Rendering Strategy ## 7. Rendering Strategy
**RT-as-paper.** ArtCamera renders the drawing world to an offscreen `RenderTexture`. A Canvas `RawImage` displays that RT. HUD lives on the same Canvas, above the RawImage. The RT *is* the paper — same fixed coordinate system on every device. **Full Canvas UI.** No `SpriteRenderer`, no `Physics2D`, no offscreen `RenderTexture` for the live view. The paper, slots, pieces, and color regions are all `Image` components on a Screen-Space-Camera canvas. Standard Unity UI eventing (`IPointerDownHandler`, `IDragHandler`) handles all input.
``` ```
┌──────────────────────────────────────────────────────┐ ┌──────────────────────────────────────────────────────────
UICanvas (Screen-Space - Camera, UICamera) │ PaperCanvas (Screen Space - Camera, UICamera)
layer: PaperUI
┌────────────────────────────────────┐
│ RawImage (AspectRatioFitter 1:1) │ [HUD] ┌──────────────────────────────────────────────────┐
│ │ └─ texture = PaperRig.Surface │ palette │ │ PaperPanel (RectTransform, 2048×2048 ref units) │
│ │ undo etc │ │ ├─ BackgroundImage │ │
│ │ ArtCamera renders → here │ │ │ ├─ SlotsPanel (slot Image outlines)
└────────────────────────────────────┘ │ ├─ PiecesPanel (draggable piece Images)
│ └─ RegionsPanel (colorable region Images)
└────────────────────────────────────────────────────── └──────────────────────────────────────────────────┘
│ rendered offscreen └──────────────────────────────────────────────────────────┘
ArtCamera (orthographicSize fixed, aspect = 1f) ┌──────────────────────────────────────────────────────────┐
culling mask: Artwork, PaperBackground, Effects │ HUDCanvas (Screen Space - Overlay, OR separate camera) │
target texture: PaperRig.Surface (2048×2048 ARGB32) layer: HUDUI │
│ ├─ Palette panel │
│ ├─ Undo / Redo buttons │
│ ├─ Capture / Next buttons │
│ └─ Tray panel (during build phase) │
└──────────────────────────────────────────────────────────┘
CaptureCamera (disabled by default, one-shot Render() on capture)
orthographic, projection cloned from UICamera
cullingMask = PaperUI only
targetTexture = temp RT allocated per capture (2048×2048)
``` ```
### Cameras ### Cameras
| Camera | Type | Culling Mask | Render Target | Purpose | | Camera | Render mode | Culling Mask | Render Target | Purpose |
|---|---|---|---|---| |---|---|---|---|---|
| `ArtCamera` | Orthographic, **fixed ortho size**, aspect = 1 | `Artwork`, `PaperBackground`, `Effects` | `PaperRig.Surface` (offscreen RT) | Renders the drawing world. Never sees the screen. | | `UICamera` | Screen-Space - Camera (orthographic) | `PaperUI`, `HUDUI` | Screen | Normal display each frame. |
| `UICamera` | Camera (Screen-Space Camera) | `UI` | Screen | Displays the paper RawImage + HUD. | | `CaptureCamera` | Orthographic, disabled | `PaperUI` only | temp `RenderTexture` | One-shot `Render()` invoked by `ICaptureService.CaptureAsync()`. |
`CaptureCamera` shares `UICamera`'s position, orthographic size, and clip planes so the captured frame matches what the player sees — minus the HUD because of the culling mask.
### Layers ### Layers
| Layer | Used by | | Layer | Used by |
|---|---| |---|---|
| `Artwork` | Drawing region sprites, shape pieces, paper bg, all in ArtCamera world | | `PaperUI` | `PaperCanvas` and all of its children (background, slots, pieces, regions, completion FX). Visible in capture. |
| `Effects` | Particle bursts, sparkles — also in ArtCamera world (so they're captured into the PNG) | | `HUDUI` | `HUDCanvas` and tray panel (palette, undo/redo, capture button, drawing catalog grid, etc.). Excluded from capture. |
| `UI` | All Canvas elements (RawImage paper + HUD) | | `EventSystem` | Unity's input layer — managed automatically. |
### Why RT-as-paper ### Why full UI
| Need | Choice | Why | | Need | Choice | Why |
|---|---|---| |---|---|---|
| Per-region tap-to-fill | Sprites + `PolygonCollider2D` in ArtCamera world; tapped via `IArtInputBridge` | Coordinate system is fixed (RT space). One `Physics2D.OverlapPoint` call after screen→art-world conversion. | | Tap-to-paint region | `Image` + `Image.alphaHitTestMinimumThreshold` + `IPointerClickHandler` | Tight alpha-based hit shape per region. No mesh / collider authoring. Tap events route through `EventSystem` natively. |
| Drag/drop shape pieces | Sprites + Physics2D in art world | Same fixed bounds on every device — no per-aspect tray layout. | | Drag/drop shape pieces | `Image` + `IBeginDragHandler` / `IDragHandler` / `IEndDragHandler` | Standard Unity UI drag. Pointer events come in canvas-local coords already. No screen→world math anywhere. |
| Capture to PNG | `RT → Texture2D → PNG` | The RT *is* the saved image. No camera state override, no compositing pass, no determinism worries. | | Visual transition during drag → snap | `DOAnchorPos`, `DOSizeDelta`, `DOLocalRotate` | All pose is in `RectTransform` units. The "transition" is a tween over canvas-local values — no swap of render context. |
| Multi-resolution support | `AspectRatioFitter (1:1, FitInParent)` on the RawImage | The "fit camera" problem reduces to a single Canvas property. Letterbox/pillarbox = whatever the Canvas around the RawImage looks like. | | Capture to PNG | Dedicated `CaptureCamera` with `cullingMask = PaperUI` | One `Render()` call into a temp RT. HUD physically can't appear. |
| Color palette, buttons | Canvas above the RawImage | Anchors handle aspect ratios. Buttons + ScrollRect free. | | Multi-resolution support | `CanvasScaler` on `PaperCanvas` (Scale With Screen Size) | Reference resolution `2048 × 2048`, Match = 1 (height). All `anchoredPosition` units are constant across devices. |
| Drawing catalog grid | Canvas | `GridLayoutGroup` + ScrollRect, async thumbnail loader. | | HUD layout independent of paper | `HUDCanvas` (separate Canvas) | HUD scales/anchors per its own rules without affecting the paper layout. |
| Drawing catalog grid, palette, etc. | Standard UI (`GridLayoutGroup`, `ScrollRect`, `Button`) | Anchors handle aspect ratios. Async thumbnail loader. |
### Multi-resolution rule ### Multi-resolution rule
The artwork world is **screen-size-independent by construction.** Author every drawing in a fixed 2048×2048 design rect (or 20×20 world units at PPU=100). Pieces, regions, snap radii, slot positions — all expressed in this space and never scaled at runtime. Different screen sizes only change how the *RawImage* is laid out on the Canvas; the contents of the RT stay identical. The paper content is **canvas-unit-stable.** Author every drawing against a fixed 2048 × 2048 reference resolution. Slot positions, piece sizes, region rects, hit shapes — all expressed in `anchoredPosition` / `sizeDelta` units. `CanvasScaler` on `PaperCanvas` does the screen mapping.
If you need a backdrop (wood/cloth behind the paper), it's a sibling Canvas Image *outside* the RawImage, sized to fill the screen. The RT itself has a transparent or paper-colored background. `PaperPanel` is anchored center, fixed 2048×2048 (or whatever you pick for the reference). On a wider screen, `CanvasScaler` pillarboxes the panel; on a narrower screen, it letterboxes. The panel's contents never resize relative to each other.
If you want a backdrop (wood/cloth behind the paper area), it's a sibling `Image` of `PaperPanel` (still on `PaperUI` layer) sized to fill the canvas. The backdrop *is* captured into the PNG by default — set its layer to `HUDUI` if you want it excluded.
### Tradeoff vs the old RT-paper-rig design
| Concern | RT-paper-rig (old) | Canvas-only (current) |
|---|---|---|
| Files in `IPaperRig` / `IArtInputBridge` | 2 contracts + ~80 lines of math | gone |
| Input pipeline | `IInputReader` → bridge → `Physics2D.OverlapPoint` | native `EventSystem` (`IPointerDownHandler` etc.) |
| Coloring hit shape | `PolygonCollider2D` from `Sprite.Editor` physics shape | `Image.alphaHitTestMinimumThreshold = 0.5f` on the region sprite |
| Per-frame render passes | 2 (ArtCamera into RT + UICamera draws RawImage) | 1 (UICamera draws everything) |
| Capture | read persistent RT | one-shot `CaptureCamera.Render()` |
| Coordinate gotchas | mismatches between screen / RT / world | none — everything is canvas-local |
If you ever need world-space effects (particle sparkles that physically explode outside the paper, free-draw brush stroke, pinch zoom on the artwork), revisit the RT approach. For the v1 tap-to-fill + drag-to-snap design, Canvas-only is correct.
--- ---
@@ -361,17 +389,22 @@ public interface IDrawingTemplate {
public readonly struct ShapePieceDTO { public readonly struct ShapePieceDTO {
public string PieceId { get; } public string PieceId { get; }
public Sprite Sprite { get; } public Sprite Sprite { get; } // assigned to Image.sprite
public Vector2 SlotPosition { get; } public Vector2 SlotAnchoredPosition { get; } // canvas units, relative to SlotsParent
public float SlotRotation { get; } public Vector2 SlotSizeDelta { get; } // canvas units — target size when snapped
public float SnapRadius { get; } // generous for toddlers public float SlotRotationZ { get; } // degrees, local rotation when snapped
public float SnapRadius { get; } // canvas units; ~80120 for toddlers
public float PreviewRadius { get; } // canvas units; ~2× snap radius
} }
public readonly struct ColorRegionDTO { public readonly struct ColorRegionDTO {
public string RegionId { get; } public string RegionId { get; }
public Sprite Sprite { get; } // sprite renderer source public Sprite Sprite { get; } // assigned to Image.sprite
public Vector2[] ColliderPath { get; } // polygon collider points public Vector2 AnchoredPosition { get; } // canvas units, relative to RegionsParent
public Color InitialColor { get; } // usually white public Vector2 SizeDelta { get; } // canvas units
public Color InitialColor { get; } // usually white
// Hit shape comes from the sprite alpha — set Image.alphaHitTestMinimumThreshold = 0.5.
// No polygon path needed; sprite import settings ("Read/Write Enabled") provide it.
} }
``` ```
@@ -394,31 +427,25 @@ public readonly struct PaintCommandDTO {
} }
``` ```
### Paper (RT rig + input bridge) ### Paper (canvas surface root)
> Contracts live in `Darkmatter.Core.Contracts.Features.Paper`. Files at `Core/Contracts/Features/Paper/`. > Contracts live in `Darkmatter.Core.Contracts.Features.Paper`. Files at `Core/Contracts/Features/Paper/`.
```csharp ```csharp
namespace Darkmatter.Core.Contracts.Features.Paper; namespace Darkmatter.Core.Contracts.Features.Paper;
public interface IPaperRig { public interface IPaperSurface {
Camera ArtCamera { get; } // offscreen, targetTexture = Surface RectTransform Root { get; } // PaperPanel — parent of slots/pieces/regions
RenderTexture Surface { get; } // 2048×2048 ARGB32; the paper itself RectTransform SlotsParent { get; } // parent for slot Images
Transform PaperRoot { get; } // parent of regions/pieces/paper bg RectTransform PiecesParent { get; } // parent for piece Images
Vector2 DesignSize { get; } // world units, e.g. (20, 20) RectTransform RegionsParent { get; } // parent for region Images
Rect DesignRect { get; } // centered on origin, DesignSize wide float DesignHalfSize { get; } // half of the reference square (e.g. 1024)
}
public interface IArtInputBridge {
// Converts a screen-space pointer (Input System) to art-world coords
// inside the RT. Returns false if the pointer is outside the RawImage.
bool TryScreenToArtWorld(Vector2 screenPos, out Vector2 artWorldPos);
} }
``` ```
- `IPaperRig` is implemented by `PaperRig : MonoBehaviour` in the ColorBook scene. - Implemented by `PaperSurface : MonoBehaviour` in the ColorBook scene (sits on the `PaperPanel` GameObject).
- `IArtInputBridge` does the screen → RawImage local → UV → `ArtCamera.ViewportToWorldPoint` chain. - All paper-side features (`Coloring`, `ShapeBuilder`, `Capture`) parent their UI under one of these `RectTransform` slots and use canvas-local coords throughout.
- All consumers (Coloring, ShapeBuilder, Capture, particle effects) read these from DI; they never touch `Screen.width/height` directly. - No `IPaperRig`. No `IArtInputBridge`. Input runs through Unity's `EventSystem` directly on the UI children.
### History ### History
@@ -444,7 +471,7 @@ public interface IUndoStack {
### Gallery & Capture ### Gallery & Capture
> `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`. > `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.
```csharp ```csharp
namespace Darkmatter.Core.Data.Dynamic.Features.Gallery; namespace Darkmatter.Core.Data.Dynamic.Features.Gallery;
@@ -469,13 +496,13 @@ public interface IGalleryService {
namespace Darkmatter.Core.Contracts.Services.Capture; namespace Darkmatter.Core.Contracts.Services.Capture;
public interface ICaptureService { public interface ICaptureService {
// No camera or paperBg args — capture reads directly from IPaperRig.Surface. // Allocates a temp RT, renders the CaptureCamera once (PaperUI layer only),
// Dimensions inherited from the RT; no resize, no compositing. // ReadPixels into a Texture2D, encodes PNG, releases the RT.
UniTask<byte[]> CaptureAsync(); UniTask<byte[]> CaptureAsync();
} }
``` ```
`ICaptureService` resolves `IPaperRig` via DI and reads `Surface` directly. The paper background is already baked into the RT because it sits in `PaperRoot` under the ArtCamera. No special compositing pass is ever needed. `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.
### Signals ### Signals
@@ -519,25 +546,27 @@ public readonly struct ArtworkSavedSignal {
### `Paper` ### `Paper`
- Scene-scoped infrastructure. Lives in `ColorBook.unity` only. - Scene-scoped infrastructure. Lives in `ColorBook.unity` only.
- Owns `PaperRig` (MonoBehaviour) — exposes `ArtCamera`, the `RenderTexture Surface`, `PaperRoot` transform, and the design rect. - Owns `PaperSurface` (MonoBehaviour) on the `PaperPanel` GameObject. Implements `IPaperSurface`, exposes `Root`, `SlotsParent`, `PiecesParent`, `RegionsParent`, `DesignHalfSize`.
- Owns `ArtInputBridge` — converts pointer screen positions to art-world coords inside the RT. - 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.
- Registered in `ColorBookLifetimeScope` via `PaperRigModule`. All other features in the scene resolve `IPaperRig` / `IArtInputBridge` from DI. - No render-target ownership. No input bridge. No coordinate conversion. The paper *is* the canvas children — nothing more.
- Lifetime is scene-scoped: created on scene load, destroyed on scene unload. RT is allocated in `Awake`, released in `OnDestroy`.
### `ShapeBuilder` ### `ShapeBuilder`
- Listens to `DrawingSelectedSignal`. - Listens to `DrawingSelectedSignal`.
- Loads template via `IDrawingTemplateLoader`, parents shape pieces under `IPaperRig.PaperRoot` at off-slot positions inside the design rect. - Loads template, spawns UI `Image` per piece under either `IPaperSurface.PiecesParent` or the HUD tray (depending on the FSM start state — usually tray).
- Per piece: drag with `ShapePieceView` (sprite + collider). Pointer events go through `IArtInputBridge.TryScreenToArtWorld`. On drop, check distance to `SlotPosition` against `SnapRadius`; if within, snap and lock. - 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.
- Fires `ShapeAssembledSignal` when all pieces locked. - Fires `ShapeAssembledSignal` when all pieces locked.
### `Coloring` ### `Coloring`
- Listens to `ShapeAssembledSignal`. - Listens to `ShapeAssembledSignal`.
- Spawns one `ColorRegionView` per `ColorRegionDTO` under `IPaperRig.PaperRoot` (sprite + polygon collider on `Artwork` layer). - 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.
- Each region has `IPointerClickHandler`. On click → `ColoringController.PaintRegion(view)`.
- Listens to palette selection (current color held in `ColoringStateRepository`). - Listens to palette selection (current color held in `ColoringStateRepository`).
- On pointer down: `IArtInputBridge.TryScreenToArtWorld(screenPos, out var artPos)``Physics2D.OverlapPoint(artPos, artworkMask)` → if hit, build `PaintRegionCommand(regionId, oldColor, newColor)`, push to `IUndoStack`. - Controller builds `PaintRegionCommand(regionId, oldColor, newColor)` and pushes to `IUndoStack`.
- Command sets `SpriteRenderer.color` on undo/redo. - Command sets `Image.color` on undo/redo.
- Fires `ColorAppliedSignal` for SFX / sparkle effects. - Fires `ColorAppliedSignal` for SFX / sparkle effects.
### `History` ### `History`
@@ -550,7 +579,7 @@ public readonly struct ArtworkSavedSignal {
### `Capture` ### `Capture`
- Bound to the "Capture" button. - Bound to the "Capture" button.
- Calls `ICaptureService.CaptureAsync()` → PNG bytes. Capture reads `IPaperRig.Surface` directly; no camera or paper-bg args needed. - 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(...)`. - Hands bytes to `IGalleryService.SaveAsync(...)`.
- Fires `ArtworkCapturedSignal` then `ArtworkSavedSignal`. - Fires `ArtworkCapturedSignal` then `ArtworkSavedSignal`.
- Shows a quick "saved!" toast with a thumbnail of the new entry. - Shows a quick "saved!" toast with a thumbnail of the new entry.
@@ -656,7 +685,7 @@ persistentDataPath/Gallery/
## 12. Capture Pipeline ## 12. Capture Pipeline
With the RT-paper-rig, capture has no setup phase. The RT is already the final image at all times. A dedicated `CaptureCamera` lives in the scene, disabled by default. It renders only the `PaperUI` layer into a temp `RenderTexture` when capture fires.
``` ```
[Capture button or Next button] [Capture button or Next button]
@@ -664,13 +693,17 @@ With the RT-paper-rig, capture has no setup phase. The RT is already the final i
ICaptureService.CaptureAsync() ICaptureService.CaptureAsync()
├─ rt = _paperRig.Surface (already populated each frame) ├─ rt = RenderTexture.GetTemporary(2048, 2048, 0, ARGB32)
├─ _captureCam.targetTexture = rt
├─ _captureCam.Render() (one-shot; cullingMask = PaperUI only)
├─ _captureCam.targetTexture = null
├─ prev = RenderTexture.active ├─ prev = RenderTexture.active
├─ RenderTexture.active = rt ├─ RenderTexture.active = rt
├─ tex = new Texture2D(rt.width, rt.height, RGBA32, false) ├─ tex = new Texture2D(2048, 2048, RGBA32, false)
├─ tex.ReadPixels(full rect, 0, 0); tex.Apply() ├─ tex.ReadPixels(full rect, 0, 0); tex.Apply()
├─ RenderTexture.active = prev ├─ RenderTexture.active = prev
├─ bytes = tex.EncodeToPNG() (on worker via UniTask.RunOnThreadPool) ├─ RenderTexture.ReleaseTemporary(rt)
├─ bytes = tex.EncodeToPNG() (on worker via UniTask.RunOnThreadPool)
├─ Object.Destroy(tex) ├─ Object.Destroy(tex)
└─ return bytes └─ return bytes
@@ -686,10 +719,11 @@ EventBus.Publish(new ArtworkSavedSignal(dto))
Notes: Notes:
- HUD never appears in capture because the HUD is on `UICamera` / Canvas — it is physically in a different render path. The RT only ever sees `ArtCamera`'s output. - HUD never appears in capture because `CaptureCamera.cullingMask` excludes `HUDUI`. Layer mask, not coincidence — even if you accidentally parent a HUD element under `PaperPanel`, putting it on the wrong layer keeps it out.
- Paper background is a sprite parented under `IPaperRig.PaperRoot` and is rendered into the RT every frame — already baked in. - Paper background is just an `Image` on `PaperUI`. Already in the right layer; no special compositing.
- Saved PNGs are byte-comparable across devices because the RT dimensions and ArtCamera matrix never depend on screen size. - Saved PNGs are 2048×2048 on every device. `CaptureCamera` has fixed `orthographicSize` and aspect, independent of screen size.
- `CaptureAsync` is safe to call repeatedly — no camera state is ever mutated. - `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.
--- ---
@@ -733,7 +767,7 @@ These shape several design decisions and are **non-negotiable**:
- **No fail states.** Drawings cannot be "wrong". - **No fail states.** Drawings cannot be "wrong".
- **No timers.** Nothing decays or runs out. - **No timers.** Nothing decays or runs out.
- **No tiny hitboxes.** Drag tolerance ≥ 40 px; snap radius ≥ 60 px for shape pieces. - **No tiny hitboxes.** Drag tolerance ≥ 40 canvas units; snap radius ≥ 80 canvas units for shape pieces. (Canvas reference resolution is 2048×2048 — these are anchored-position deltas, not screen px.)
- **Auto-snap on near-miss.** If a piece is dropped within `1.5 × SnapRadius`, snap anyway and play a happy sound. - **Auto-snap on near-miss.** If a piece is dropped within `1.5 × SnapRadius`, snap anyway and play a happy sound.
- **No text-heavy UI.** Icons everywhere. Single-word labels max. - **No text-heavy UI.** Icons everywhere. Single-word labels max.
- **Loud, immediate feedback.** Every tap plays a sound; every fill bursts a small particle effect. - **Loud, immediate feedback.** Every tap plays a sound; every fill bursts a small particle effect.
@@ -1101,48 +1135,67 @@ Same shape repeats for every feature's UI.
## 26. ShapeBuilder — Snap Algorithm ## 26. ShapeBuilder — Snap Algorithm
```csharp All math is in canvas-local space — `anchoredPosition`, `sizeDelta`, `localRotation`. No world coords.
// In ShapePieceView.OnPointerUp:
public void OnDragEnd(Vector2 worldPos) {
var slot = transform.position; // assigned target slot
var d = Vector2.Distance(worldPos, slot);
if (d <= _piece.SnapRadius) { ```csharp
// In ShapePieceFsm.OnDragEnd (state: Dragging or Preview):
public void OnDragEnd() {
var pieceRT = _ui.RectTransform;
var slotRT = _targetSlot.RectTransform;
var d = Vector2.Distance(pieceRT.anchoredPosition, slotRT.anchoredPosition);
if (d <= _cfg.SnapRadius) {
SnapToSlot(); SnapToSlot();
} else if (d <= _piece.SnapRadius * 1.5f) { } else if (d <= _cfg.SnapRadius * 1.5f) {
// Toddler grace zone — snap anyway, play happy sound // Toddler grace zone — snap anyway, play happy sound
SnapToSlot(); SnapToSlot();
_audio.PlayOneShot(_clips.NiceTry); _audio.PlayOneShot(SfxId.NiceTry);
} else { } else {
ReturnToTrayAnimated(); ReturnToTrayAnimated();
} }
} }
private void SnapToSlot() { private void SnapToSlot() {
_locked = true; _ui.RectTransform.SetParent(_paper.PiecesParent, worldPositionStays: false);
transform.DOMove(_piece.SlotPosition, 0.25f).SetEase(Ease.OutBack); var slot = _targetSlot.RectTransform;
_audio.PlayOneShot(_clips.Snap); _ui.RectTransform.DOAnchorPos(slot.anchoredPosition, 0.25f).SetEase(Ease.OutBack);
_bus.Publish(new PieceSnappedSignal(_piece.PieceId)); _ui.RectTransform.DOSizeDelta(slot.sizeDelta, 0.25f).SetEase(Ease.OutBack);
_ui.RectTransform.DOLocalRotateQuaternion(slot.localRotation, 0.25f).SetEase(Ease.OutBack);
_audio.PlayOneShot(SfxId.Snap);
_bus.Publish(new PieceSnappedSignal(_ui.PieceId));
} }
``` ```
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.
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. 80120), not world units. Same `CanvasScaler` reference resolution across devices = same hit feel.
Controller listens for `PieceSnappedSignal`, counts against expected piece count, fires `ShapeAssembledSignal` when complete. Controller listens for `PieceSnappedSignal`, counts against expected piece count, fires `ShapeAssembledSignal` when complete.
--- ---
## 27. Rendering Order & Sorting ## 27. Rendering Order & Sorting
URP 2D with a single `ArtCamera` ortho cam. Canvas-only — order is sibling index inside `PaperPanel` (front-most is last in hierarchy). No URP 2D sorting layers.
| Sorting Layer | Order | Contents | `PaperPanel` children (bottom → top):
|---|---|---|
| `PaperBackground` | 0 | Paper bg sprite (under everything) |
| `ArtworkRegions` | 100 | `ColorRegionView` sprites (the colorable shapes) |
| `ArtworkPieces` | 200 | `ShapePieceView` sprites (during build) |
| `Effects` | 300 | Particle bursts, sparkles |
| `UIWorld` | 400 | World-space prompts (rare; mostly Canvas) |
Canvas HUD lives on `UICamera` (Overlay), never sorts against `ArtCamera`. Capture renders only `ArtCamera`'s layers → HUD physically cannot leak into saved PNG. ```
PaperPanel
├─ BackgroundImage (paper texture)
├─ RegionsPanel (colorable region Images)
├─ SlotsPanel (slot outline Images — under pieces so snapped pieces hide them)
├─ PiecesPanel (draggable / snapped piece Images)
└─ EffectsPanel (sparkle / particle UI for completion FX, optional)
```
`HUDCanvas` is a separate Canvas at a higher sorting order (or Screen Space - Overlay). It never sorts against `PaperCanvas` because they're different canvases.
`CaptureCamera` renders only the `PaperUI` layer. The HUD physically cannot leak into the saved PNG because of the culling mask, regardless of sibling order.
> If you ever need particles outside the canvas (e.g. confetti falling across the full screen on completion), use a separate Canvas above the HUD with its own sub-tree of UI particles. Don't add `ParticleSystem`s under PaperPanel — they don't render in UI canvases without `UIParticleSystem` or similar packages.
--- ---
@@ -1209,13 +1262,13 @@ Toddler-mode error UI:
| Class | Layer | Asmdef | | Class | Layer | Asmdef |
|---|---|---| |---|---|---|
| `IDrawingTemplate`, `ShapePieceDTO`, `ColorRegionDTO` | Core | `Core` | | `IDrawingTemplate`, `ShapePieceDTO`, `ColorRegionDTO` | Core | `Core` |
| `IPaperRig`, `IArtInputBridge` | Core | `Core` | | `IPaperSurface` | Core | `Core` |
| `ICommand`, `IUndoStack` | Core | `Core` | | `ICommand`, `IUndoStack` | Core | `Core` |
| `BoundedUndoStack` | Libs | `Libs.CommandStack` | | `BoundedUndoStack` | Libs | `Libs.CommandStack` |
| `AddressableAssetProviderService` | Services | `Services.Assets` | | `AddressableAssetProviderService` | Services | `Services.Assets` |
| `FileGalleryService` | Services | `Services.Gallery` | | `FileGalleryService` | Services | `Services.Gallery` |
| `RenderTextureCaptureService` | Services | `Services.Capture` | | `RenderTextureCaptureService` | Services | `Services.Capture` |
| `PaperRig`, `ArtInputBridge`, `PaperRigModule` | Features | `Features.Paper` | | `PaperSurface`, `PaperSurfaceModule` | Features | `Features.Paper` |
| `ColoringController`, `PaintRegionCommand` | Features | `Features.Coloring` | | `ColoringController`, `PaintRegionCommand` | Features | `Features.Coloring` |
| `ShapeBuilderController`, `ShapePieceView` | Features | `Features.ShapeBuilder` | | `ShapeBuilderController`, `ShapePieceView` | Features | `Features.ShapeBuilder` |
| `HistoryController` | Features | `Features.History` | | `HistoryController` | Features | `Features.History` |
@@ -1305,26 +1358,18 @@ public interface ICaptureService {
} }
``` ```
#### `IPaperRig` *(Core/Contracts/Features/Paper — planned)* #### `IPaperSurface` *(Core/Contracts/Features/Paper — planned)*
Shared art rig. The single source of truth for everything that lives in the drawing world. The paper is just RectTransform real estate. Features parent their UI children under one of the role-specific roots.
```csharp ```csharp
public interface IPaperRig { public interface IPaperSurface {
Camera ArtCamera { get; } // offscreen, targetTexture = Surface RectTransform Root { get; } // PaperPanel itself
RenderTexture Surface { get; } // 2048×2048 ARGB32 — the paper itself RectTransform SlotsParent { get; } // child of Root — for ShapeBuilder slot outlines
Transform PaperRoot { get; } // parent of regions/pieces/paper bg RectTransform PiecesParent { get; } // child of Root — for ShapeBuilder pieces (post-snap)
Vector2 DesignSize { get; } // world units, e.g. (20, 20) RectTransform RegionsParent { get; } // child of Root — for Coloring region Images
Rect DesignRect { get; } // centered on origin 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.
#### `IArtInputBridge` *(Core/Contracts/Features/Paper — planned)*
Converts screen-space pointer coords to art-world coords inside the RT.
```csharp
public interface IArtInputBridge {
bool TryScreenToArtWorld(Vector2 screenPos, out Vector2 artWorldPos);
}
```
Returns `false` when the pointer is outside the displayed RawImage rect (toddler tapped the HUD or backdrop). Every art-world raycast goes through this.
#### `IProgressionService` *(Core/Contracts/Features/Progression — planned)* #### `IProgressionService` *(Core/Contracts/Features/Progression — planned)*
Tracks which templates the child has completed and what they last opened. Tracks which templates the child has completed and what they last opened.
@@ -1386,9 +1431,16 @@ Implements `IGalleryService`.
- **Delete flow:** delete png + thumb + json; missing files ignored (idempotent). - **Delete flow:** delete png + thumb + json; missing files ignored (idempotent).
#### `RenderTextureCaptureService` *(Services/Capture — planned)* #### `RenderTextureCaptureService` *(Services/Capture — planned)*
Implements `ICaptureService`. Implements `ICaptureService`. Drives the scene's disabled `CaptureCamera` once per capture.
- **Steps:** allocate `RenderTexture(width, height, 0, ARGB32)` → bind to `artCamera.targetTexture``artCamera.Render()``ReadPixels` into `Texture2D` → composite `paperBackground` underneath (single shader blit) → `EncodeToPNG` → release RT + textures. ```csharp
- **Threading:** PNG encode happens on a `UniTask.RunOnThreadPool` to avoid hitching the main thread on tablets. // fields:
// Camera _captureCam (scene-bound, registered via CaptureServiceModule)
// int _surfaceSize = 2048
// IPathProvider _paths (only if you want to expose paths — usually not needed here)
```
- **Steps:** `RenderTexture.GetTemporary(size, size, 0, ARGB32)` → set `_captureCam.targetTexture = rt``_captureCam.Render()``ReadPixels` into a `Texture2D` → null out the target texture → `RenderTexture.ReleaseTemporary(rt)``EncodeToPNG` → return bytes.
- **Threading:** PNG encode happens on `UniTask.RunOnThreadPool` to avoid hitching the main thread on tablets.
- **Camera setup:** `_captureCam` has `cullingMask = PaperUI`, `clearFlags = SolidColor` (white or paper color), `orthographicSize` and `aspect` cloned from `UICamera` once at scene start. Stays disabled — `Render()` is the only call site.
- **Sizing:** default 2048², overridable. Capped at device max texture size. - **Sizing:** default 2048², overridable. Capped at device max texture size.
#### `JsonPersistenceService` *(Services/Persistence — planned; today `Libs/PlayerPrefs` covers small-key state)* #### `JsonPersistenceService` *(Services/Persistence — planned; today `Libs/PlayerPrefs` covers small-key state)*
@@ -1508,9 +1560,10 @@ public interface IDrawingCatalogView {
#### `ShapeBuilderController` *(Systems)* #### `ShapeBuilderController` *(Systems)*
Spawns shape pieces for the selected template, tracks snap progress, fires `ShapeAssembledSignal` when complete. Spawns shape pieces for the selected template, tracks snap progress, fires `ShapeAssembledSignal` when complete.
```csharp ```csharp
// fields: IDrawingTemplateCatalog _catalog, ShapePieceFactory _factory, IEventBus _bus, ShapeBuilderConfig _cfg // fields: IDrawingTemplateCatalog _catalog, ShapePieceFactory _factory,
// IPaperSurface _paper, TrayPanel _tray, IEventBus _bus, ShapeBuilderConfig _cfg
public sealed class ShapeBuilderController : IDisposable { public sealed class ShapeBuilderController : IDisposable {
public IReadOnlyList<ShapePieceView> Active { get; } public IReadOnlyList<ShapePieceUI> Active { get; }
public UniTask BuildAsync(string templateId); // load template, spawn pieces in tray public UniTask BuildAsync(string templateId); // load template, spawn pieces in tray
public void Reset(); // clear, unsubscribe public void Reset(); // clear, unsubscribe
} }
@@ -1519,97 +1572,101 @@ public sealed class ShapeBuilderController : IDisposable {
``` ```
- **Internal:** counts `PieceSnappedSignal` against expected piece count. - **Internal:** counts `PieceSnappedSignal` against expected piece count.
#### `ShapePieceView : MonoBehaviour` *(Views)* #### `ShapePieceUI : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler` *(UI)*
World-space draggable sprite with collider. Source for snap-or-return logic shown in section 26. The UI Image that the toddler drags. Lives under `TrayPanel` while idle, reparents under `IPaperSurface.PiecesParent` when snapped.
```csharp ```csharp
public sealed class ShapePieceView : MonoBehaviour { public sealed class ShapePieceUI : MonoBehaviour,
IBeginDragHandler, IDragHandler, IEndDragHandler
{
public string PieceId { get; } public string PieceId { get; }
public bool IsLocked { get; } public bool IsLocked { get; }
public event Action<string> Snapped; // raised when piece locks into slot public RectTransform RectTransform { get; }
public void Initialize(ShapePieceDTO dto, IInputReader input, IAudioService audio); public event Action<string> Snapped;
public void Initialize(ShapePieceDTO dto, ShapePieceFsm fsm);
} }
``` ```
- **No public mutators** for position once locked — controller treats `IsLocked` as the source of truth. - Handlers forward to `ShapePieceFsm` (`OnDragBegin / OnDrag(localPos) / OnDragEnd`).
- `OnDrag` converts `PointerEventData.position` to canvas-local via `RectTransformUtility.ScreenPointToLocalPointInRectangle` against the piece's parent rect.
- No collider, no Physics2D anywhere.
#### `ShapePieceFsm` *(Systems)*
Per-piece state machine using `Libs.FSM`. States: `InTray → Dragging → Preview → (Snapped | Returning)`.
```csharp
// fields: ShapePieceUI _ui, SlotMarker _targetSlot, ShapeBuilderConfig _cfg,
// IAudioService _audio, IEventBus _bus
public sealed class ShapePieceFsm {
public void OnDragBegin();
public void OnDrag(Vector2 canvasLocalPos);
public void OnDragEnd();
public bool IsLocked { get; }
}
```
- **Preview-state update**: reactive lerp of `anchoredPosition / sizeDelta / localRotation` toward `_targetSlot`'s pose, driven by `1 - dist/PreviewRadius`. No DOTween while previewing — it's per-frame.
- **Snapped enter**: DOTween ease-out to exact slot pose (~0.2s), disable drag, fire `PieceSnappedSignal`.
- **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. Its `RectTransform` directly *is* the target pose — `ShapePieceFsm` reads `anchoredPosition`, `sizeDelta`, `localRotation` from it.
```csharp
public sealed class SlotMarker : MonoBehaviour {
public string SlotId;
public RectTransform RectTransform => transform as RectTransform;
}
```
#### `TrayPanel : MonoBehaviour` *(UI)*
HUD-side panel (on `HUDCanvas`) where pieces start out. Has a `HorizontalLayoutGroup` + `ContentSizeFitter`. Provides spawn anchors via `RectTransform Slot(int index)` for the controller.
#### `ShapePieceFactory` *(Systems)* #### `ShapePieceFactory` *(Systems)*
Instantiates `ShapePieceView` prefabs from a pool. Avoids re-instantiating across "Next" cycles on the same template family. Pool of `ShapePieceUI` GameObjects + their associated FSMs. Reused across template loads.
```csharp ```csharp
public sealed class ShapePieceFactory { public sealed class ShapePieceFactory {
public ShapePieceView Spawn(ShapePieceDTO dto, Transform parent); public ShapePieceUI Spawn(ShapePieceDTO dto, RectTransform parent);
public void Despawn(ShapePieceView view); public void Despawn(ShapePieceUI piece);
} }
``` ```
#### `ShapeBuilderInputBinder` *(Systems)*
With UI handlers on the piece itself, an explicit input binder isn't strictly needed — drag events route via the EventSystem. Keep this class only if you need to listen for "any tap outside any piece" (e.g. to dismiss a preview). Otherwise skip.
--- ---
### 32.5b Feature — `Paper` *(planned)* ### 32.5b Feature — `Paper` *(planned)*
The shared art rig — RT, offscreen camera, screen↔world bridge. Every other feature in the ColorBook scene resolves `IPaperRig` and `IArtInputBridge` from DI and never touches `Screen.*` or `Camera.*` directly. A tiny feature. Just exposes the paper RectTransforms via DI so consumers don't `Find` them.
#### `PaperRig : MonoBehaviour, IPaperRig` *(Rig)* #### `PaperSurface : MonoBehaviour, IPaperSurface` *(Surface)*
Scene-bound component placed on a GameObject in `ColorBook.unity`. Owns the RT lifecycle. Scene-bound component placed on the `PaperPanel` GameObject in `ColorBook.unity`.
```csharp ```csharp
// inspector fields: // inspector fields:
// Camera _artCamera (Orthographic, aspect=1, fixed ortho size) // RectTransform _slotsParent
// Transform _paperRoot (parent of regions/pieces) // RectTransform _piecesParent
// Vector2 _designSize = (20, 20) (world units; matches 2048×2048 at PPU=100) // RectTransform _regionsParent
// int _surfaceSize = 2048 (RT side length, square) // float _designHalfSize = 1024f // half of 2048 reference resolution
public sealed class PaperRig : MonoBehaviour, IPaperRig { public sealed class PaperSurface : MonoBehaviour, IPaperSurface {
public Camera ArtCamera => _artCamera; public RectTransform Root => (RectTransform)transform;
public RenderTexture Surface => _surface; public RectTransform SlotsParent => _slotsParent;
public Transform PaperRoot => _paperRoot; public RectTransform PiecesParent => _piecesParent;
public Vector2 DesignSize => _designSize; public RectTransform RegionsParent => _regionsParent;
public Rect DesignRect => new(-_designSize / 2f, _designSize); public float DesignHalfSize => _designHalfSize;
} }
``` ```
- **Awake:** allocate `_surface = new RenderTexture(_surfaceSize, _surfaceSize, 0, ARGB32) { name = "PaperSurface" };` then `_surface.Create()` and `_artCamera.targetTexture = _surface; _artCamera.aspect = 1f; _artCamera.orthographicSize = _designSize.y / 2f;`. - No `Awake` / `OnDestroy` logic. The component is a pure pass-through to the RectTransforms.
- **OnDestroy:** `_surface.Release(); Object.Destroy(_surface);`. - All four child rects share the same anchors and size as `Root` (anchored center, stretched to fill).
- **No update logic** — the camera renders every frame automatically because `targetTexture` is set.
- **Important:** `_artCamera`'s `orthographicSize` and `aspect` are set once and never touched again. The RT contents are deterministic.
#### `ArtInputBridge : MonoBehaviour, IArtInputBridge` *(Input)* #### `PaperSurfaceModule : MonoBehaviour, IServiceModule` *(Installers)*
Lives on the same UI Canvas as the paper `RawImage`. Scene-scoped installer. Dragged into `ColorBookLifetimeScope.sceneModules[]`.
```csharp ```csharp
// inspector fields: // inspector fields:
// RawImage _paperImage (the on-screen paper) // PaperSurface _surface
// RectTransform _paperRect (== _paperImage.rectTransform)
// Camera _uiCamera (Canvas event camera)
// IPaperRig _rig (injected via VContainer + IInjectable, or resolved in Start)
public bool TryScreenToArtWorld(Vector2 screenPos, out Vector2 artWorldPos) {
if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(
_paperRect, screenPos, _uiCamera, out var local)) {
artWorldPos = default; return false;
}
var rect = _paperRect.rect;
var uv = new Vector2(
(local.x - rect.xMin) / rect.width,
(local.y - rect.yMin) / rect.height);
if (uv.x < 0 || uv.x > 1 || uv.y < 0 || uv.y > 1) {
artWorldPos = default; return false;
}
artWorldPos = _rig.ArtCamera.ViewportToWorldPoint(uv);
return true;
}
```
- Returns `false` when the toddler tapped outside the RawImage (HUD button area, backdrop, off-screen).
- Used by every feature that does world-space picking — `Coloring`, `ShapeBuilder`, and any future feature like stickers.
#### `PaperRigModule : MonoBehaviour, IServiceModule` *(Installers)*
Scene-scoped installer. Dragged onto `ColorBookLifetimeScope._installers[]`.
```csharp
// inspector fields:
// PaperRig _rig
// ArtInputBridge _bridge
public void Register(IContainerBuilder builder) { public void Register(IContainerBuilder builder) {
builder.RegisterInstance<IPaperRig>(_rig); builder.RegisterInstance<IPaperSurface>(_surface);
builder.RegisterInstance<IArtInputBridge>(_bridge);
} }
``` ```
- Registers as `Instance` because both are MonoBehaviours already in the scene. Registers as `Instance` because `PaperSurface` is a MonoBehaviour already in the scene. Lifetime tied to the scene.
- Lifetime is implicitly tied to the scene (Unity destroys them on unload).
--- ---
@@ -1632,7 +1689,8 @@ public sealed class ColoringStateRepository {
#### `ColoringController` *(Systems)* — implements `IColoringController` #### `ColoringController` *(Systems)* — implements `IColoringController`
Builds and pushes `PaintRegionCommand` instances; spawns `ColorRegionView` per region. Builds and pushes `PaintRegionCommand` instances; spawns `ColorRegionView` per region.
```csharp ```csharp
// fields: IUndoStack _undo, ColoringStateRepository _state, ColorRegionFactory _factory, IEventBus _bus // fields: IUndoStack _undo, ColoringStateRepository _state, ColorRegionFactory _factory,
// IPaperSurface _paper, IEventBus _bus
public interface IColoringController { public interface IColoringController {
UniTask SpawnRegionsAsync(IDrawingTemplate template); UniTask SpawnRegionsAsync(IDrawingTemplate template);
void PaintRegion(ColorRegionView view); // builds command, pushes to undo stack void PaintRegion(ColorRegionView view); // builds command, pushes to undo stack
@@ -1641,28 +1699,23 @@ public interface IColoringController {
// sub: ShapeAssembledSignal (via flow controller, not direct) // sub: ShapeAssembledSignal (via flow controller, not direct)
// pub: ColorAppliedSignal (via PaintRegionCommand) // pub: ColorAppliedSignal (via PaintRegionCommand)
``` ```
Spawns each region as a UI `Image` under `_paper.RegionsParent`. No `Physics2D`.
#### `ColorRegionView : MonoBehaviour` *(Views)* #### `ColorRegionView : MonoBehaviour, IPointerClickHandler` *(UI)*
Sprite + `PolygonCollider2D`, on `Artwork` layer. Tapped via `Physics2D.OverlapPoint` from `ColoringInputBinder`. UI Image with alpha-based hit detection. Tap routes through Unity's EventSystem directly to `OnPointerClick`.
```csharp ```csharp
public sealed class ColorRegionView : MonoBehaviour { public sealed class ColorRegionView : MonoBehaviour, IPointerClickHandler {
public string RegionId { get; } public string RegionId { get; }
public Color Color { get; } // current paint public Color Color => _image.color;
public void Initialize(ColorRegionDTO dto); public void Initialize(ColorRegionDTO dto, IColoringController controller);
public void SetColor(Color c); // setter only; no logic public void SetColor(Color c); // setter only; no logic
public void OnPointerClick(PointerEventData e) => _controller.PaintRegion(this);
} }
``` ```
- **Required sprite setup:** sprite import inspector → **Read/Write Enabled = on**, **Generate Physics Shape = off** (not needed). `Image.alphaHitTestMinimumThreshold = 0.5f` on Initialize so taps on transparent pixels pass through to the next region below.
- **Sibling order matters** for stacked regions — top sibling gets first crack at the click; with alpha hit-test, transparent areas defer correctly.
#### `ColoringInputBinder` *(Systems)* — `IStartable, IDisposable` No `ColoringInputBinder` class needed. Unity's EventSystem fires `OnPointerClick` on the topmost UI element under the pointer that consumes it — exactly what we want.
Subscribes to `IInputReader.PointerDown`. On each tap:
1. `_bridge.TryScreenToArtWorld(screenPos, out var artPos)` — bail if outside the paper.
2. `Physics2D.OverlapPoint(artPos, _artworkMask)` against the `Artwork` layer.
3. If hit, `ColoringController.PaintRegion(hit.GetComponent<ColorRegionView>())`.
```csharp
// fields: IInputReader _input, IArtInputBridge _bridge, IColoringController _coloring, LayerMask _artworkMask
```
Note: `_bridge` is the same instance the entire scene uses — no per-feature coordinate math.
#### `PaintRegionCommand` *(Commands)* #### `PaintRegionCommand` *(Commands)*
Source in section 23. Holds `view`, `fromColor`, `toColor`, `bus`. Symmetrical execute/undo. Source in section 23. Holds `view`, `fromColor`, `toColor`, `bus`. Symmetrical execute/undo.
@@ -1721,7 +1774,7 @@ public sealed class CaptureController {
``` ```
- **Flow:** `_capture.CaptureAsync()``_gallery.SaveAsync(bytes, templateId)` → publish signals. - **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. - **Concurrency:** sets `IsCapturing = true` on entry; UI binds button enabled to `!IsCapturing` to prevent double-tap.
- **No camera or sprite args** — capture reads `IPaperRig.Surface` directly inside the service. - **No camera or sprite args** — the implementation owns a reference to the disabled `CaptureCamera` and drives the one-shot render internally.
#### `CaptureButtonPresenter` *(UI)* #### `CaptureButtonPresenter` *(UI)*
Wires button click → `CaptureController.CaptureCurrentAsync`. Disables button while in progress. Shows toast on `ArtworkSavedSignal`. Wires button click → `CaptureController.CaptureCurrentAsync`. Disables button while in progress. Shows toast on `ArtworkSavedSignal`.
@@ -1826,10 +1879,10 @@ All scope classes are thin: a serialized installer-MonoBehaviour list (+ optiona
### 32.13 Cross-cutting types ### 32.13 Cross-cutting types
#### `ColorBookSceneRefs : MonoBehaviour` *(App — planned)* #### `ColorBookSceneRefs : MonoBehaviour` *(App — planned, optional)*
Aggregates scene-bound Unity references that features need: `Camera artCamera`, `Transform catalogRoot`, `Transform builderRoot`, `Transform coloringRoot`, `RectTransform hudRoot`, `ColorPaletteView paletteView`, `HistoryButtonsView historyView`. Registered in `ColorBookLifetimeScope` via `builder.RegisterInstance(_sceneRefs)` so features don't `Find` things. 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.
> Most of these refs are subsumed by `IPaperRig` now (which owns `ArtCamera` and `PaperRoot`). `ColorBookSceneRefs` reduces to the HUD-side refs (palette view, history buttons, panel roots). > 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.
#### `IServiceModule` *(Libs/Installers — ✅ exists)* #### `IServiceModule` *(Libs/Installers — ✅ exists)*
```csharp ```csharp
@@ -1850,15 +1903,17 @@ Implemented as `MonoBehaviour` per feature/service so scopes can drag them in th
| `ColorBookLifetimeScope` | App | Scene DI | scene refs, installers | | `ColorBookLifetimeScope` | App | Scene DI | scene refs, installers |
| `DrawingCatalogController` | Feature | Grid logic | catalog, bus | | `DrawingCatalogController` | Feature | Grid logic | catalog, bus |
| `DrawingCatalogPresenter` | Feature | UI bridge | view, controller, catalog | | `DrawingCatalogPresenter` | Feature | UI bridge | view, controller, catalog |
| `ShapeBuilderController` | Feature | Piece spawn + snap tracking | catalog, factory, bus, cfg | | `ShapeBuilderController` | Feature | Piece spawn + snap tracking | catalog, factory, paper, tray, bus, cfg |
| `ShapePieceView` | Feature | Draggable piece MB | input, audio | | `ShapePieceUI` | Feature | Draggable UI piece (Image + drag handlers) | 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 | — | | `ColoringStateRepository` | Feature | Current color model | — |
| `ColoringController` | Feature | Region spawn + paint cmd | undo, state, factory, bus | | `ColoringController` | Feature | Region spawn + paint cmd | undo, state, factory, paper, bus |
| `ColorRegionView` | Feature | Region sprite MB | — | | `ColorRegionView` | Feature | Region UI Image + IPointerClickHandler | controller |
| `PaintRegionCommand` | Feature | Undoable paint | view, bus | | `PaintRegionCommand` | Feature | Undoable paint (sets Image.color) | view, bus |
| `PaperRig` | Feature | RT + ArtCamera owner | — | | `PaperSurface` | Feature | IPaperSurface (Root + child rects) | — |
| `ArtInputBridge` | Feature | Screen→art-world picking | rig, raw image, ui cam | | `PaperSurfaceModule` | Feature | DI registration | surface |
| `PaperRigModule` | Feature | DI registration | rig, bridge |
| `HistoryController` | Feature | Undo/redo facade | undo stack, bus | | `HistoryController` | Feature | Undo/redo facade | undo stack, bus |
| `CaptureController` | Feature | Capture+save orchestration | capture svc, gallery, bus | | `CaptureController` | Feature | Capture+save orchestration | capture svc, gallery, bus |
| `ColorBookFlowController` | Feature | Scene FSM | bus, catalog, builder, coloring, capture, progression | | `ColorBookFlowController` | Feature | Scene FSM | bus, catalog, builder, coloring, capture, progression |