Readme asset
This commit is contained in:
483
Readme.md
483
Readme.md
@@ -98,7 +98,7 @@ Assets/Darkmatter/
|
||||
│ ├── Compatibility/
|
||||
│ │ └── IsExternalInit.cs (C#9 init shim for older runtimes)
|
||||
│ ├── 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/
|
||||
│ │ ├── Assets/IAssetProviderService.cs
|
||||
│ │ ├── Audio/IAudioService.cs, ISfxPlayer.cs
|
||||
@@ -175,7 +175,7 @@ Rough landing order for ColorBook scene to be playable:
|
||||
|
||||
| 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/Gallery/IGalleryService.cs` | Gallery service contract |
|
||||
| `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/Enums/Services/Camera/CameraType.cs` | Add `ArtCamera` enum value to existing file |
|
||||
| `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 |
|
||||
| `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 |
|
||||
| `App/LifetimeScopes/{MainMenu,ColorBook,ArtBook}LifetimeScope.cs` | Per-scene scopes |
|
||||
| `App/Boot/AppBoot.cs` | Bootstrap entry point |
|
||||
@@ -283,59 +283,87 @@ Failures show a child-friendly retry screen; never crash.
|
||||
|
||||
## 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 │
|
||||
│ │ │ undo etc │
|
||||
│ │ ArtCamera renders → here │ │
|
||||
│ └────────────────────────────────────┘ │
|
||||
│ ┌──────────────────────────────────────────────────┐ │
|
||||
│ │ PaperPanel (RectTransform, 2048×2048 ref units) │ │
|
||||
│ │ ├─ BackgroundImage │ │
|
||||
│ │ ├─ SlotsPanel (slot Image outlines) │ │
|
||||
│ │ ├─ PiecesPanel (draggable piece Images) │ │
|
||||
│ │ └─ RegionsPanel (colorable region Images) │ │
|
||||
│ └──────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────┘
|
||||
▲
|
||||
│ rendered offscreen
|
||||
│
|
||||
ArtCamera (orthographicSize fixed, aspect = 1f)
|
||||
culling mask: Artwork, PaperBackground, Effects
|
||||
target texture: PaperRig.Surface (2048×2048 ARGB32)
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ HUDCanvas (Screen Space - Overlay, OR separate camera) │
|
||||
│ layer: HUDUI │
|
||||
│ ├─ Palette panel │
|
||||
│ ├─ Undo / Redo buttons │
|
||||
│ ├─ Capture / Next buttons │
|
||||
│ └─ Tray panel (during build phase) │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
|
||||
CaptureCamera (disabled by default, one-shot Render() on capture)
|
||||
orthographic, projection cloned from UICamera
|
||||
cullingMask = PaperUI only
|
||||
targetTexture = temp RT allocated per capture (2048×2048)
|
||||
```
|
||||
|
||||
### Cameras
|
||||
|
||||
| Camera | 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` | Camera (Screen-Space – Camera) | `UI` | Screen | Displays the paper RawImage + HUD. |
|
||||
| `UICamera` | Screen-Space - Camera (orthographic) | `PaperUI`, `HUDUI` | Screen | Normal display each frame. |
|
||||
| `CaptureCamera` | Orthographic, disabled | `PaperUI` only | temp `RenderTexture` | One-shot `Render()` invoked by `ICaptureService.CaptureAsync()`. |
|
||||
|
||||
`CaptureCamera` shares `UICamera`'s position, orthographic size, and clip planes so the captured frame matches what the player sees — minus the HUD because of the culling mask.
|
||||
|
||||
### Layers
|
||||
|
||||
| Layer | Used by |
|
||||
|---|---|
|
||||
| `Artwork` | Drawing region sprites, shape pieces, paper bg, all in ArtCamera world |
|
||||
| `Effects` | Particle bursts, sparkles — also in ArtCamera world (so they're captured into the PNG) |
|
||||
| `UI` | All Canvas elements (RawImage paper + HUD) |
|
||||
| `PaperUI` | `PaperCanvas` and all of its children (background, slots, pieces, regions, completion FX). Visible in capture. |
|
||||
| `HUDUI` | `HUDCanvas` and tray panel (palette, undo/redo, capture button, drawing catalog grid, etc.). Excluded from capture. |
|
||||
| `EventSystem` | Unity's input layer — managed automatically. |
|
||||
|
||||
### Why RT-as-paper
|
||||
### Why full UI
|
||||
|
||||
| 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. |
|
||||
| Drag/drop shape pieces | Sprites + Physics2D in art world | Same fixed bounds on every device — no per-aspect tray layout. |
|
||||
| Capture to PNG | `RT → Texture2D → PNG` | The RT *is* the saved image. No camera state override, no compositing pass, no determinism worries. |
|
||||
| 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. |
|
||||
| Color palette, buttons | Canvas above the RawImage | Anchors handle aspect ratios. Buttons + ScrollRect free. |
|
||||
| Drawing catalog grid | Canvas | `GridLayoutGroup` + ScrollRect, async thumbnail loader. |
|
||||
| Tap-to-paint region | `Image` + `Image.alphaHitTestMinimumThreshold` + `IPointerClickHandler` | Tight alpha-based hit shape per region. No mesh / collider authoring. Tap events route through `EventSystem` natively. |
|
||||
| Drag/drop shape pieces | `Image` + `IBeginDragHandler` / `IDragHandler` / `IEndDragHandler` | Standard Unity UI drag. Pointer events come in canvas-local coords already. No screen→world math anywhere. |
|
||||
| Visual transition during drag → snap | `DOAnchorPos`, `DOSizeDelta`, `DOLocalRotate` | All pose is in `RectTransform` units. The "transition" is a tween over canvas-local values — no swap of render context. |
|
||||
| Capture to PNG | Dedicated `CaptureCamera` with `cullingMask = PaperUI` | One `Render()` call into a temp RT. HUD physically can't appear. |
|
||||
| Multi-resolution support | `CanvasScaler` on `PaperCanvas` (Scale With Screen Size) | Reference resolution `2048 × 2048`, Match = 1 (height). All `anchoredPosition` units are constant across devices. |
|
||||
| HUD layout independent of paper | `HUDCanvas` (separate Canvas) | HUD scales/anchors per its own rules without affecting the paper layout. |
|
||||
| Drawing catalog grid, palette, etc. | Standard UI (`GridLayoutGroup`, `ScrollRect`, `Button`) | Anchors handle aspect ratios. Async thumbnail loader. |
|
||||
|
||||
### Multi-resolution rule
|
||||
|
||||
The 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 string PieceId { get; }
|
||||
public Sprite Sprite { get; }
|
||||
public Vector2 SlotPosition { get; }
|
||||
public float SlotRotation { get; }
|
||||
public float SnapRadius { get; } // generous for toddlers
|
||||
public Sprite Sprite { get; } // assigned to Image.sprite
|
||||
public Vector2 SlotAnchoredPosition { get; } // canvas units, relative to SlotsParent
|
||||
public Vector2 SlotSizeDelta { get; } // canvas units — target size when snapped
|
||||
public float SlotRotationZ { get; } // degrees, local rotation when snapped
|
||||
public float SnapRadius { get; } // canvas units; ~80–120 for toddlers
|
||||
public float PreviewRadius { get; } // canvas units; ~2× snap radius
|
||||
}
|
||||
|
||||
public readonly struct ColorRegionDTO {
|
||||
public string RegionId { get; }
|
||||
public Sprite Sprite { get; } // sprite renderer source
|
||||
public Vector2[] ColliderPath { get; } // polygon collider points
|
||||
public Sprite Sprite { get; } // assigned to Image.sprite
|
||||
public Vector2 AnchoredPosition { get; } // canvas units, relative to RegionsParent
|
||||
public Vector2 SizeDelta { get; } // canvas units
|
||||
public Color InitialColor { get; } // usually white
|
||||
// Hit shape comes from the sprite alpha — set Image.alphaHitTestMinimumThreshold = 0.5.
|
||||
// No polygon path needed; sprite import settings ("Read/Write Enabled") provide it.
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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/`.
|
||||
|
||||
```csharp
|
||||
namespace Darkmatter.Core.Contracts.Features.Paper;
|
||||
|
||||
public interface IPaperRig {
|
||||
Camera ArtCamera { get; } // offscreen, targetTexture = Surface
|
||||
RenderTexture Surface { get; } // 2048×2048 ARGB32; the paper itself
|
||||
Transform PaperRoot { get; } // parent of regions/pieces/paper bg
|
||||
Vector2 DesignSize { get; } // world units, e.g. (20, 20)
|
||||
Rect DesignRect { get; } // centered on origin, DesignSize wide
|
||||
}
|
||||
|
||||
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);
|
||||
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)
|
||||
}
|
||||
```
|
||||
|
||||
- `IPaperRig` is implemented by `PaperRig : MonoBehaviour` in the ColorBook scene.
|
||||
- `IArtInputBridge` does the screen → RawImage local → UV → `ArtCamera.ViewportToWorldPoint` chain.
|
||||
- All consumers (Coloring, ShapeBuilder, Capture, particle effects) read these from DI; they never touch `Screen.width/height` directly.
|
||||
- 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.
|
||||
|
||||
### History
|
||||
|
||||
@@ -444,7 +471,7 @@ public interface IUndoStack {
|
||||
|
||||
### 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
|
||||
namespace Darkmatter.Core.Data.Dynamic.Features.Gallery;
|
||||
@@ -469,13 +496,13 @@ public interface IGalleryService {
|
||||
namespace Darkmatter.Core.Contracts.Services.Capture;
|
||||
|
||||
public interface ICaptureService {
|
||||
// No camera or paperBg args — capture reads directly from IPaperRig.Surface.
|
||||
// Dimensions inherited from the RT; no resize, no compositing.
|
||||
// Allocates a temp RT, renders the CaptureCamera once (PaperUI layer only),
|
||||
// ReadPixels into a Texture2D, encodes PNG, releases the RT.
|
||||
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
|
||||
|
||||
@@ -519,25 +546,27 @@ public readonly struct ArtworkSavedSignal {
|
||||
### `Paper`
|
||||
|
||||
- Scene-scoped infrastructure. Lives in `ColorBook.unity` only.
|
||||
- Owns `PaperRig` (MonoBehaviour) — exposes `ArtCamera`, the `RenderTexture Surface`, `PaperRoot` transform, and the design rect.
|
||||
- Owns `ArtInputBridge` — converts pointer screen positions to art-world coords inside the RT.
|
||||
- Registered in `ColorBookLifetimeScope` via `PaperRigModule`. All other features in the scene resolve `IPaperRig` / `IArtInputBridge` from DI.
|
||||
- Lifetime is scene-scoped: created on scene load, destroyed on scene unload. RT is allocated in `Awake`, released in `OnDestroy`.
|
||||
- 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 via `IDrawingTemplateLoader`, parents shape pieces under `IPaperRig.PaperRoot` at off-slot positions inside the design rect.
|
||||
- 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.
|
||||
- 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`).
|
||||
- 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.
|
||||
|
||||
### `Coloring`
|
||||
|
||||
- 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`).
|
||||
- On pointer down: `IArtInputBridge.TryScreenToArtWorld(screenPos, out var artPos)` → `Physics2D.OverlapPoint(artPos, artworkMask)` → if hit, build `PaintRegionCommand(regionId, oldColor, newColor)`, push to `IUndoStack`.
|
||||
- Command sets `SpriteRenderer.color` on undo/redo.
|
||||
- Controller builds `PaintRegionCommand(regionId, oldColor, newColor)` and pushes to `IUndoStack`.
|
||||
- Command sets `Image.color` on undo/redo.
|
||||
- Fires `ColorAppliedSignal` for SFX / sparkle effects.
|
||||
|
||||
### `History`
|
||||
@@ -550,7 +579,7 @@ public readonly struct ArtworkSavedSignal {
|
||||
### `Capture`
|
||||
|
||||
- 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(...)`.
|
||||
- Fires `ArtworkCapturedSignal` then `ArtworkSavedSignal`.
|
||||
- Shows a quick "saved!" toast with a thumbnail of the new entry.
|
||||
@@ -656,7 +685,7 @@ persistentDataPath/Gallery/
|
||||
|
||||
## 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]
|
||||
@@ -664,12 +693,16 @@ With the RT-paper-rig, capture has no setup phase. The RT is already the final i
|
||||
▼
|
||||
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
|
||||
├─ 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()
|
||||
├─ RenderTexture.active = prev
|
||||
├─ RenderTexture.ReleaseTemporary(rt)
|
||||
├─ bytes = tex.EncodeToPNG() (on worker via UniTask.RunOnThreadPool)
|
||||
├─ Object.Destroy(tex)
|
||||
└─ return bytes
|
||||
@@ -686,10 +719,11 @@ EventBus.Publish(new ArtworkSavedSignal(dto))
|
||||
|
||||
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.
|
||||
- Paper background is a sprite parented under `IPaperRig.PaperRoot` and is rendered into the RT every frame — already baked in.
|
||||
- Saved PNGs are byte-comparable across devices because the RT dimensions and ArtCamera matrix never depend on screen size.
|
||||
- `CaptureAsync` is safe to call repeatedly — no camera state is ever mutated.
|
||||
- HUD never appears in capture because `CaptureCamera.cullingMask` excludes `HUDUI`. Layer mask, not coincidence — even if you accidentally parent a HUD element under `PaperPanel`, putting it on the wrong layer keeps it out.
|
||||
- Paper background is just an `Image` on `PaperUI`. Already in the right layer; no special compositing.
|
||||
- Saved PNGs are 2048×2048 on every device. `CaptureCamera` has fixed `orthographicSize` and aspect, independent of screen size.
|
||||
- `CaptureAsync` is safe to call repeatedly. The CaptureCamera's transform / projection are set once at scene start and never modified.
|
||||
- The temp RT is allocated via `RenderTexture.GetTemporary` so successive captures don't leak GPU memory.
|
||||
|
||||
---
|
||||
|
||||
@@ -733,7 +767,7 @@ These shape several design decisions and are **non-negotiable**:
|
||||
|
||||
- **No fail states.** Drawings cannot be "wrong".
|
||||
- **No timers.** Nothing decays or runs out.
|
||||
- **No tiny hitboxes.** Drag tolerance ≥ 40 px; snap radius ≥ 60 px for shape pieces.
|
||||
- **No tiny hitboxes.** Drag tolerance ≥ 40 canvas units; snap radius ≥ 80 canvas units for shape pieces. (Canvas reference resolution is 2048×2048 — these are anchored-position deltas, not screen px.)
|
||||
- **Auto-snap on near-miss.** If a piece is dropped within `1.5 × SnapRadius`, snap anyway and play a happy sound.
|
||||
- **No text-heavy UI.** Icons everywhere. Single-word labels max.
|
||||
- **Loud, immediate feedback.** Every tap plays a sound; every fill bursts a small particle effect.
|
||||
@@ -1101,48 +1135,67 @@ Same shape repeats for every feature's UI.
|
||||
|
||||
## 26. ShapeBuilder — Snap Algorithm
|
||||
|
||||
```csharp
|
||||
// In ShapePieceView.OnPointerUp:
|
||||
public void OnDragEnd(Vector2 worldPos) {
|
||||
var slot = transform.position; // assigned target slot
|
||||
var d = Vector2.Distance(worldPos, slot);
|
||||
All math is in canvas-local space — `anchoredPosition`, `sizeDelta`, `localRotation`. No world coords.
|
||||
|
||||
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();
|
||||
} else if (d <= _piece.SnapRadius * 1.5f) {
|
||||
} else if (d <= _cfg.SnapRadius * 1.5f) {
|
||||
// Toddler grace zone — snap anyway, play happy sound
|
||||
SnapToSlot();
|
||||
_audio.PlayOneShot(_clips.NiceTry);
|
||||
_audio.PlayOneShot(SfxId.NiceTry);
|
||||
} else {
|
||||
ReturnToTrayAnimated();
|
||||
}
|
||||
}
|
||||
|
||||
private void SnapToSlot() {
|
||||
_locked = true;
|
||||
transform.DOMove(_piece.SlotPosition, 0.25f).SetEase(Ease.OutBack);
|
||||
_audio.PlayOneShot(_clips.Snap);
|
||||
_bus.Publish(new PieceSnappedSignal(_piece.PieceId));
|
||||
_ui.RectTransform.SetParent(_paper.PiecesParent, worldPositionStays: false);
|
||||
var slot = _targetSlot.RectTransform;
|
||||
_ui.RectTransform.DOAnchorPos(slot.anchoredPosition, 0.25f).SetEase(Ease.OutBack);
|
||||
_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. 80–120), 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.
|
||||
|
||||
---
|
||||
|
||||
## 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 |
|
||||
|---|---|---|
|
||||
| `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) |
|
||||
`PaperPanel` children (bottom → top):
|
||||
|
||||
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 |
|
||||
|---|---|---|
|
||||
| `IDrawingTemplate`, `ShapePieceDTO`, `ColorRegionDTO` | Core | `Core` |
|
||||
| `IPaperRig`, `IArtInputBridge` | Core | `Core` |
|
||||
| `IPaperSurface` | Core | `Core` |
|
||||
| `ICommand`, `IUndoStack` | Core | `Core` |
|
||||
| `BoundedUndoStack` | Libs | `Libs.CommandStack` |
|
||||
| `AddressableAssetProviderService` | Services | `Services.Assets` |
|
||||
| `FileGalleryService` | Services | `Services.Gallery` |
|
||||
| `RenderTextureCaptureService` | Services | `Services.Capture` |
|
||||
| `PaperRig`, `ArtInputBridge`, `PaperRigModule` | Features | `Features.Paper` |
|
||||
| `PaperSurface`, `PaperSurfaceModule` | Features | `Features.Paper` |
|
||||
| `ColoringController`, `PaintRegionCommand` | Features | `Features.Coloring` |
|
||||
| `ShapeBuilderController`, `ShapePieceView` | Features | `Features.ShapeBuilder` |
|
||||
| `HistoryController` | Features | `Features.History` |
|
||||
@@ -1305,26 +1358,18 @@ public interface ICaptureService {
|
||||
}
|
||||
```
|
||||
|
||||
#### `IPaperRig` *(Core/Contracts/Features/Paper — planned)*
|
||||
Shared art rig. The single source of truth for everything that lives in the drawing world.
|
||||
#### `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 IPaperRig {
|
||||
Camera ArtCamera { get; } // offscreen, targetTexture = Surface
|
||||
RenderTexture Surface { get; } // 2048×2048 ARGB32 — the paper itself
|
||||
Transform PaperRoot { get; } // parent of regions/pieces/paper bg
|
||||
Vector2 DesignSize { get; } // world units, e.g. (20, 20)
|
||||
Rect DesignRect { get; } // centered on origin
|
||||
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
|
||||
}
|
||||
```
|
||||
|
||||
#### `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.
|
||||
No render-target ownership. No coordinate conversion. The contract just hands out RectTransforms so features don't have to `Find` them.
|
||||
|
||||
#### `IProgressionService` *(Core/Contracts/Features/Progression — planned)*
|
||||
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).
|
||||
|
||||
#### `RenderTextureCaptureService` *(Services/Capture — planned)*
|
||||
Implements `ICaptureService`.
|
||||
- **Steps:** allocate `RenderTexture(width, height, 0, ARGB32)` → bind to `artCamera.targetTexture` → `artCamera.Render()` → `ReadPixels` into `Texture2D` → composite `paperBackground` underneath (single shader blit) → `EncodeToPNG` → release RT + textures.
|
||||
- **Threading:** PNG encode happens on a `UniTask.RunOnThreadPool` to avoid hitching the main thread on tablets.
|
||||
Implements `ICaptureService`. Drives the scene's disabled `CaptureCamera` once per capture.
|
||||
```csharp
|
||||
// fields:
|
||||
// Camera _captureCam (scene-bound, registered via CaptureServiceModule)
|
||||
// int _surfaceSize = 2048
|
||||
// IPathProvider _paths (only if you want to expose paths — usually not needed here)
|
||||
```
|
||||
- **Steps:** `RenderTexture.GetTemporary(size, size, 0, ARGB32)` → set `_captureCam.targetTexture = rt` → `_captureCam.Render()` → `ReadPixels` into a `Texture2D` → null out the target texture → `RenderTexture.ReleaseTemporary(rt)` → `EncodeToPNG` → return bytes.
|
||||
- **Threading:** PNG encode happens on `UniTask.RunOnThreadPool` to avoid hitching the main thread on tablets.
|
||||
- **Camera setup:** `_captureCam` has `cullingMask = PaperUI`, `clearFlags = SolidColor` (white or paper color), `orthographicSize` and `aspect` cloned from `UICamera` once at scene start. Stays disabled — `Render()` is the only call site.
|
||||
- **Sizing:** default 2048², overridable. Capped at device max texture size.
|
||||
|
||||
#### `JsonPersistenceService` *(Services/Persistence — planned; today `Libs/PlayerPrefs` covers small-key state)*
|
||||
@@ -1508,9 +1560,10 @@ public interface IDrawingCatalogView {
|
||||
#### `ShapeBuilderController` *(Systems)*
|
||||
Spawns shape pieces for the selected template, tracks snap progress, fires `ShapeAssembledSignal` when complete.
|
||||
```csharp
|
||||
// fields: IDrawingTemplateCatalog _catalog, ShapePieceFactory _factory, IEventBus _bus, ShapeBuilderConfig _cfg
|
||||
// fields: IDrawingTemplateCatalog _catalog, ShapePieceFactory _factory,
|
||||
// IPaperSurface _paper, TrayPanel _tray, IEventBus _bus, ShapeBuilderConfig _cfg
|
||||
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 void Reset(); // clear, unsubscribe
|
||||
}
|
||||
@@ -1519,97 +1572,101 @@ public sealed class ShapeBuilderController : IDisposable {
|
||||
```
|
||||
- **Internal:** counts `PieceSnappedSignal` against expected piece count.
|
||||
|
||||
#### `ShapePieceView : MonoBehaviour` *(Views)*
|
||||
World-space draggable sprite with collider. Source for snap-or-return logic shown in section 26.
|
||||
#### `ShapePieceUI : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler` *(UI)*
|
||||
The UI Image that the toddler drags. Lives under `TrayPanel` while idle, reparents under `IPaperSurface.PiecesParent` when snapped.
|
||||
```csharp
|
||||
public sealed class ShapePieceView : MonoBehaviour {
|
||||
public sealed class ShapePieceUI : MonoBehaviour,
|
||||
IBeginDragHandler, IDragHandler, IEndDragHandler
|
||||
{
|
||||
public string PieceId { get; }
|
||||
public bool IsLocked { get; }
|
||||
public event Action<string> Snapped; // raised when piece locks into slot
|
||||
public void Initialize(ShapePieceDTO dto, IInputReader input, IAudioService audio);
|
||||
public RectTransform RectTransform { get; }
|
||||
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)*
|
||||
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
|
||||
public sealed class ShapePieceFactory {
|
||||
public ShapePieceView Spawn(ShapePieceDTO dto, Transform parent);
|
||||
public void Despawn(ShapePieceView view);
|
||||
public ShapePieceUI Spawn(ShapePieceDTO dto, RectTransform parent);
|
||||
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)*
|
||||
|
||||
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)*
|
||||
Scene-bound component placed on a GameObject in `ColorBook.unity`. Owns the RT lifecycle.
|
||||
#### `PaperSurface : MonoBehaviour, IPaperSurface` *(Surface)*
|
||||
Scene-bound component placed on the `PaperPanel` GameObject in `ColorBook.unity`.
|
||||
```csharp
|
||||
// inspector fields:
|
||||
// Camera _artCamera (Orthographic, aspect=1, fixed ortho size)
|
||||
// Transform _paperRoot (parent of regions/pieces)
|
||||
// Vector2 _designSize = (20, 20) (world units; matches 2048×2048 at PPU=100)
|
||||
// int _surfaceSize = 2048 (RT side length, square)
|
||||
// RectTransform _slotsParent
|
||||
// RectTransform _piecesParent
|
||||
// RectTransform _regionsParent
|
||||
// float _designHalfSize = 1024f // half of 2048 reference resolution
|
||||
|
||||
public sealed class PaperRig : MonoBehaviour, IPaperRig {
|
||||
public Camera ArtCamera => _artCamera;
|
||||
public RenderTexture Surface => _surface;
|
||||
public Transform PaperRoot => _paperRoot;
|
||||
public Vector2 DesignSize => _designSize;
|
||||
public Rect DesignRect => new(-_designSize / 2f, _designSize);
|
||||
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;
|
||||
}
|
||||
```
|
||||
- **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;`.
|
||||
- **OnDestroy:** `_surface.Release(); Object.Destroy(_surface);`.
|
||||
- **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.
|
||||
- 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).
|
||||
|
||||
#### `ArtInputBridge : MonoBehaviour, IArtInputBridge` *(Input)*
|
||||
Lives on the same UI Canvas as the paper `RawImage`.
|
||||
#### `PaperSurfaceModule : MonoBehaviour, IServiceModule` *(Installers)*
|
||||
Scene-scoped installer. Dragged into `ColorBookLifetimeScope.sceneModules[]`.
|
||||
```csharp
|
||||
// inspector fields:
|
||||
// RawImage _paperImage (the on-screen paper)
|
||||
// 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
|
||||
// PaperSurface _surface
|
||||
|
||||
public void Register(IContainerBuilder builder) {
|
||||
builder.RegisterInstance<IPaperRig>(_rig);
|
||||
builder.RegisterInstance<IArtInputBridge>(_bridge);
|
||||
builder.RegisterInstance<IPaperSurface>(_surface);
|
||||
}
|
||||
```
|
||||
- Registers as `Instance` because both are MonoBehaviours already in the scene.
|
||||
- Lifetime is implicitly tied to the scene (Unity destroys them on unload).
|
||||
Registers as `Instance` because `PaperSurface` is a MonoBehaviour already in the scene. Lifetime tied to the scene.
|
||||
|
||||
---
|
||||
|
||||
@@ -1632,7 +1689,8 @@ public sealed class ColoringStateRepository {
|
||||
#### `ColoringController` *(Systems)* — implements `IColoringController`
|
||||
Builds and pushes `PaintRegionCommand` instances; spawns `ColorRegionView` per region.
|
||||
```csharp
|
||||
// fields: IUndoStack _undo, ColoringStateRepository _state, ColorRegionFactory _factory, IEventBus _bus
|
||||
// fields: IUndoStack _undo, ColoringStateRepository _state, ColorRegionFactory _factory,
|
||||
// IPaperSurface _paper, IEventBus _bus
|
||||
public interface IColoringController {
|
||||
UniTask SpawnRegionsAsync(IDrawingTemplate template);
|
||||
void PaintRegion(ColorRegionView view); // builds command, pushes to undo stack
|
||||
@@ -1641,28 +1699,23 @@ public interface IColoringController {
|
||||
// sub: ShapeAssembledSignal (via flow controller, not direct)
|
||||
// pub: ColorAppliedSignal (via PaintRegionCommand)
|
||||
```
|
||||
Spawns each region as a UI `Image` under `_paper.RegionsParent`. No `Physics2D`.
|
||||
|
||||
#### `ColorRegionView : MonoBehaviour` *(Views)*
|
||||
Sprite + `PolygonCollider2D`, on `Artwork` layer. Tapped via `Physics2D.OverlapPoint` from `ColoringInputBinder`.
|
||||
#### `ColorRegionView : MonoBehaviour, IPointerClickHandler` *(UI)*
|
||||
UI Image with alpha-based hit detection. Tap routes through Unity's EventSystem directly to `OnPointerClick`.
|
||||
```csharp
|
||||
public sealed class ColorRegionView : MonoBehaviour {
|
||||
public sealed class ColorRegionView : MonoBehaviour, IPointerClickHandler {
|
||||
public string RegionId { get; }
|
||||
public Color Color { get; } // current paint
|
||||
public void Initialize(ColorRegionDTO dto);
|
||||
public Color Color => _image.color;
|
||||
public void Initialize(ColorRegionDTO dto, IColoringController controller);
|
||||
public void SetColor(Color c); // setter only; no logic
|
||||
public void OnPointerClick(PointerEventData e) => _controller.PaintRegion(this);
|
||||
}
|
||||
```
|
||||
- **Required sprite setup:** sprite import inspector → **Read/Write Enabled = on**, **Generate Physics Shape = off** (not needed). `Image.alphaHitTestMinimumThreshold = 0.5f` on Initialize so taps on transparent pixels pass through to the next region below.
|
||||
- **Sibling order matters** for stacked regions — top sibling gets first crack at the click; with alpha hit-test, transparent areas defer correctly.
|
||||
|
||||
#### `ColoringInputBinder` *(Systems)* — `IStartable, IDisposable`
|
||||
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.
|
||||
No `ColoringInputBinder` class needed. Unity's EventSystem fires `OnPointerClick` on the topmost UI element under the pointer that consumes it — exactly what we want.
|
||||
|
||||
#### `PaintRegionCommand` *(Commands)*
|
||||
Source in section 23. Holds `view`, `fromColor`, `toColor`, `bus`. Symmetrical execute/undo.
|
||||
@@ -1721,7 +1774,7 @@ public sealed class CaptureController {
|
||||
```
|
||||
- **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** — 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)*
|
||||
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
|
||||
|
||||
#### `ColorBookSceneRefs : MonoBehaviour` *(App — planned)*
|
||||
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.
|
||||
#### `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.
|
||||
|
||||
> 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)*
|
||||
```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 |
|
||||
| `DrawingCatalogController` | Feature | Grid logic | catalog, bus |
|
||||
| `DrawingCatalogPresenter` | Feature | UI bridge | view, controller, catalog |
|
||||
| `ShapeBuilderController` | Feature | Piece spawn + snap tracking | catalog, factory, bus, cfg |
|
||||
| `ShapePieceView` | Feature | Draggable piece MB | input, audio |
|
||||
| `ShapeBuilderController` | Feature | Piece spawn + snap tracking | catalog, factory, paper, tray, bus, cfg |
|
||||
| `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 | — |
|
||||
| `ColoringController` | Feature | Region spawn + paint cmd | undo, state, factory, bus |
|
||||
| `ColorRegionView` | Feature | Region sprite MB | — |
|
||||
| `PaintRegionCommand` | Feature | Undoable paint | view, bus |
|
||||
| `PaperRig` | Feature | RT + ArtCamera owner | — |
|
||||
| `ArtInputBridge` | Feature | Screen→art-world picking | rig, raw image, ui cam |
|
||||
| `PaperRigModule` | Feature | DI registration | rig, bridge |
|
||||
| `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 |
|
||||
|
||||
Reference in New Issue
Block a user