Add new test file for phase 19 scenery with unique identifier

This commit is contained in:
2026-10-17 12:53:38 -04:00
parent 36032ebb1e
commit a0510e6405
74 changed files with 40948 additions and 149 deletions
@@ -0,0 +1,252 @@
# Phase 19 — Scenery, backgrounds, and surface finishes: architecture and implementation specification
> **Status:** Original Phase 19 scenery architecture and the approved Stage-relative sizing amendment are integrated; automated and render-capable checks pass. This specification follows the [accepted product brief](../plans/phase_19_scenery_backgrounds_surface_finishes.md), the [approved UI/UX specification](phase_19_scenery_backgrounds_ui_ux.md), and the [Stage panel UI system](stage_panel_ui_system.md). Full Phase 19 user/editor verification remains pending. The earlier sections describe the original implementation contract; the sizing amendment starts from the current `.scene` v1.5, `.prop` v1.1, Stage controller, and shared UI.
## Decisions and scope
1. Use independent, inert `Node2D` placements for wall panels and `.prop` Scenery artwork. The source `.prop` is a **live reference**: its current vector art is used on reopen. Missing art remains a saved, visible, repairable placement. A given `.prop` may be used as inert Scenery and, when it has valid collision geometry, as a functional Prop.
2. Sky is one adaptive, viewport-filling, screen-space vector drawing behind the world. Its details stay fixed during camera movement; it does not tint other objects. Existing scenes and Blank Scene use an internal neutral Sky. The four child-facing Sky presets are Clear Day, Cloudy Day, Sunset, and Starry Night.
3. The existing Prop Editor opens **inside the live Stage session** for Make artwork. The Stage and its unsaved document remain in memory, with Stage input suspended until the editor returns. The standalone F6 route remains valid.
4. Select several creates a temporary Scenery selection and snapshot-based transform preview. It does not create a persistent group or a parent that changes the saved hierarchy. Sky, Ground, characters, and functional Props are outside this transform.
5. Phase 19 adds neither Stage Undo/Redo nor a whole-set transform. Group preview has its own exact Cancel/Keep boundary. Stage-wide Undo/Redo is a separately planned phase and is logged in [technical debt](../architecture/tech_debt_and_optimizations.md).
## Existing constraints and ownership
`scenes/stage/sandbox_stage.tscn` contains a `SandboxStage` root, `Camera2D`, and `World`. The scene adapter serializes direct `World` children; `StageSceneIO` currently writes v1.4. `StageSelection` currently considers every direct world child selectable by world AABB. `StageSpatialIndex` indexes AABBs into 16 px cells and `StageTerrainPainter` treats any indexed overlap as an occupancy conflict. `PropBlock` currently draws at `z_index` -1, 0 (legacy), or 1, while terrain is 0. These are explicit integration points, not implicit behavior to rely on.
```mermaid
flowchart TD
Chrome[StageChrome / shared gallery] --> Intent[SandboxStage input arbitration]
Intent --> Decor[StageSceneryController]
Intent --> Terrain[StageTerrainPainter]
Decor --> World[World: WallPanel + SceneryArtwork]
Terrain --> World
World --> Adapter[StageSceneAdapter]
Sky[StageSkyView] --> Adapter
Adapter --> IO[StageSceneIO v1.5]
Art[PropFile + PropLibrary visual scan] --> Decor
Art --> Editor[Stage-hosted PropEditor]
```
| Owner / location | Typed contract or change | Boundary |
| --- | --- | --- |
| `scripts/stage/stage_draw_order.gd`, `class_name StageDrawOrder extends RefCounted` | Constants `WALL := -40`, `FAR := -30`, `NEAR := -20`, `TERRAIN := -10`, `FLOOR := -5`; `static func scenery_z(depth: StringName) -> int`; `static func apply_terrain(node: TerrainBlock) -> void` | One drawing policy. Leave the existing Prop `-1/0/1` and character relative order intact; rebase terrain drawing only. Never serialize raw `z_index` in place of a semantic depth/finish. |
| `scripts/stage/scenery/stage_sky_view.gd`, `class_name StageSkyView extends Control` | `func set_sky(id: StringName) -> void`; `func get_sky() -> StringName`; `func apply_theme(theme: SandboxTheme) -> void` | Full-rect child of a dedicated `CanvasLayer` with `layer = -1`, mouse ignored. Deterministic vector `_draw()` on viewport resize/theme/choice only. No world hit test or physics. |
| `scripts/stage/scenery/stage_wall_panel.gd`, `class_name StageWallPanel extends Node2D` | `var finish_id: StringName`; `var tint: Color`; `var panel_size: Vector2`; optional `var motif_origin_offset: Vector2`; `func configure(size: Vector2, finish: StringName, color: Color) -> void`; `func get_local_bounds() -> Rect2` | Finite world rectangle centered at `position`. No `CollisionObject2D`, navigation, support, or interaction registration. Resize changes `panel_size`, never root scale, so motifs retain their world pitch. The renderer lays the pattern out from each panel's current world transform, so eraser-created fragments keep continuous brick courses; the optional offset preserves source-wall lineage in saved data. |
| `scripts/stage/scenery/stage_scenery_artwork.gd`, `class_name StageSceneryArtwork extends Node2D` | `var asset_path: String`; `var display_name: String`; `var depth: StringName`; `var last_known_local_bounds: Rect2`; `var missing: bool`; `func load_visual(path: String) -> bool`; `func set_depth(value: StringName) -> void`; `func get_visual_world_bounds() -> Rect2`; `func get_saved_footprint() -> Rect2` | Build a `Visuals` child via `PropVisuals.build()` from the complete shape stack. No `PropBlock`, collision hull, physics, Use spots, anchors, or event identity. If loading fails, render a neutral marker/known footprint from saved data and preserve path/name/transform. |
| `scripts/stage/scenery/stage_surface_renderer.gd`, `class_name StageSurfaceRenderer extends RefCounted` | `static func is_wall_finish(id: StringName) -> bool`; `static func is_ground_finish(id: StringName) -> bool`; `static func draw_wall(canvas: CanvasItem, rect: Rect2, finish: StringName, tint: Color, world_origin: Vector2) -> void`; `static func draw_ground(canvas: CanvasItem, rect: Rect2, finish: StringName, tint: Color, world_origin: Vector2) -> void` | Shared vector definitions for Paint/Brick/Wood, with Grass/Dirt/Sidewalk/Asphalt for Ground. One motif vocabulary and world-coordinate phase; no raster scale blur. Drawing helpers clip motifs to finite panel/block bounds and bound work to visible tiles. |
| `scripts/terrain/terrain_block.gd`, `class_name TerrainBlock extends StaticBody2D` | Add `var finish_id: StringName` and `func set_finish(id: StringName) -> void` for `spawn_id == "ground"`; preserve `polygon_points`, fill, outline, and `CollisionPolygon2D` | Ground appearance changes without altering collision geometry, navigation, or terrain occupancy. Ramp/Step retain the old look. |
| `scripts/editors/stage/stage_scenery_controller.gd`, `class_name StageSceneryController extends Node` | `signal placement_committed(node: Node2D)`; `signal document_change_requested`; `signal preview_state_changed(active: bool)`; `func begin_artwork(path: String, depth: StringName) -> bool`; `func begin_wall(finish: StringName, initial_world_position: Vector2, tint: Color) -> void`; `func begin_wall_eraser(initial_world_position: Vector2) -> bool`; `func cancel_active_gesture() -> void`; `func exit_tool() -> void`; `func begin_group_transform(kind: StringName, members: Array[Node2D]) -> bool`; `func preview_group(delta: Vector2, factor: float) -> bool`; `func commit_group() -> void`; `func cancel_group() -> void` | Owns Scenery-only tool/substate and snapshots. A wall-eraser release subtracts its snapped rectangle from every intersected wall panel and emits one document change when anything changed. `SandboxStage` retains top-level mode, Save, dirty, and input priority. Typed intent signals do not pass UI controls into persistence. |
| `scripts/editors/stage/sandbox_stage.gd`, `class_name SandboxStage extends Node2D` | Route Scenery tools before ordinary selection/placement; gate save/PLAY while a group preview needs resolution; apply controller commits through existing `_mark_scene_dirty()` and `_save_object_state()` as appropriate | Do not append all Phase 19 interaction state to the root controller. Cancel pending drafts on tool/mode changes before existing direct/play transitions. |
| `scripts/stage/stage_selection.gd`, `class_name StageSelection extends RefCounted` | Add a typed selection context, for example `enum SelectionScope { ACTION, SCENERY }` plus `func set_scope(scope: SelectionScope) -> void`; make hit/hover/box use the same predicate | Ordinary EDIT excludes wall/artwork, including missing broad bounds. Edit Scenery includes only those; smaller cutouts outrank panels, with the overlap chooser for remaining ambiguity. Sky is never selectable. |
| `scripts/stage/stage_scene_io.gd`, `class_name StageSceneIO extends RefCounted`; `scripts/editors/stage/stage_scene_adapter.gd`, `class_name StageSceneAdapter extends RefCounted` | v1.5 migration/validation, sky and new object encode/decode, known footprint, Ground finish | Validate document before `_reset_scene_content()`. Invalid new fields cannot silently erase the current scene. Preserve missing Scenery entries across save cycles. |
`StageSpawner` remains the factory for physical terrain, Props, stickmen, and Areas. A small Scenery factory may live in `StageSceneryController` or `scripts/stage/scenery/` to create inert nodes; it must not route visual artwork through `spawn_prop_asset()`. `StageRunSession` and the event/rule engine must treat Scenery nodes as nonparticipants even in PLAY and paused EDIT. Keep Scenery outside the physical spatial index and terrain-paint candidate set, but include it in `World` serialization and Scenery-only selection.
## Scene v1.5 contract
All scene objects remain independent entries in the existing `objects` array. `file_id` is unique within a file, allocated by the adapter; it is not a durable group ID. Scenery object transforms use world coordinates. The Sky ID is a root property. The examples omit unrelated current fields.
```json
{
"version": "1.5",
"scene_name": "Living Room",
"sky_id": "sky_clear_day",
"camera": {"position": {"x": 0, "y": -400}, "zoom": 0.5},
"objects": [
{"file_id": "wall_panel_1", "type": "wall_panel", "position": {"x": -280, "y": -420}, "size": {"x": 320, "y": 400}, "finish_id": "finish_wall_paint", "color": "#5A9D70"},
{"file_id": "scenery_artwork_1", "type": "scenery_artwork", "asset_id": "res://content/props/window_frame.prop", "display_name": "Window frame", "depth": "near", "position": {"x": 0, "y": -450}, "rotation": 0, "scale": {"x": 1, "y": 1}, "last_known_local_bounds": {"position": {"x": -80, "y": -70}, "size": {"x": 160, "y": 140}}},
{"file_id": "terrain_1", "type": "terrain", "asset_id": "ground", "position": {"x": 0, "y": 0}, "rotation": 0, "scale": {"x": 1, "y": 1}, "finish_id": "finish_ground_wood"}
],
"event_rules": [],
"metadata": {}
}
```
The document may contain internal `sky_neutral` and `finish_ground_legacy` IDs to reproduce the prior blank/background and green Ground. They are valid for loading/migration but are not extra child-facing picker cards. For wall panels, `position` is the rectangle center, `size` has strictly positive finite x/y, and rotation is fixed at zero. Group resize writes the new size and center while leaving wall root scale at `(1, 1)`. For artwork, `scale` is finite, positive, and uniform for Phase 19 editing; `rotation` is finite and preserved for existing/scripted entries and repair previews, though the Phase 19 UI offers no rotation control. Depth IDs are exactly `far`, `near`, `floor`; Near is the placement default.
The manual **Erase wall area** tool adds one optional wall field, `motif_origin_offset: {x, y}`, recording the vector from a fragment's saved `position` to its source wall's origin; absent means `(0, 0)`, so existing v1.5 scenes remain valid and no version bump or migration is needed. When a rectangle intersects a wall panel, transform its four corners into that panel's local space, intersect with the finite local bounds, and replace the original with up to four non-overlapping rectangular fragments outside the intersection. Copy the finish, tint, placement ordering, and source-wall lineage to every fragment. Render each fragment's surface pattern using its current world origin, not the inherited source origin, so brick courses remain on the same world-space lattice across the new fragment boundaries. Remove the source panel when the intersection covers it fully; do not touch artwork or non-wall objects. This is visual editing only: no glass, collision, doorway, Ground, or physics behavior is inferred. Each valid release commits immediately through the existing document-dirty signal; Done/Cancel/RMB/Esc exits the tool, and canceling a held drag discards only that uncommitted drag. There is no Stage Undo in Phase 19.
`last_known_local_bounds` is cached geometry metadata, updated when a live asset loads and retained unchanged if it later goes missing. Include stroke width when deriving it, so horizontal/vertical line-only artwork still has a positive visible footprint without inventing collision geometry. It supports a repair footprint and safe group movement; it is not embedded artwork. Missing or malformed optional bounds fall back to a compact world marker and disable group resize until repaired. `display_name` is a short saved fallback label. A repaired placement changes only its selected `asset_id`, name, and known bounds after explicit confirmation; its position, scale, rotation, depth, and unrelated placements remain. The default repair is one placement. A future or optional explicit “Use for all matching places” path must show a count and ask separately before changing other references.
`StageSceneIO._migrate_to_current()` accepts v1.0 through v1.4 and adds `sky_neutral` plus `finish_ground_legacy` to old Ground entries, without changing old Prop role and draw-order migrations. v1.5 validation checks top-level Sky ID, object type payloads, finite vectors and numbers, positive wall sizes and artwork scales, allowed finish/depth IDs, type-safe color strings, unique `file_id`, and canonicalized asset paths. Malformed optional cached art bounds produce a warning and the no-footprint repair marker, rather than rejecting otherwise valid scene content. The adapter creates a missing Scenery node when the source cannot load; missing source is a warning and repair state, not document rejection. Unknown object types follow the existing warning/skip convention only after structural validation. The existing safe write and autosave paths write the same v1.5 snapshot. Manual Save asks to Keep or Cancel a pending group preview; background autosave waits until the preview is resolved. On mode switch, cancel the preview before the current PLAY-entry autosave. Draft values are never serialized.
## Visual layers, Sky, and finishes
The semantic draw order is Sky → wall panels → Far art → Near art → terrain → floor art → characters and Props. `StageDrawOrder` applies bands to direct `World` children. Set all existing terrain varieties to the terrain band for consistent physical-surface drawing; their collision and input remain unchanged. Prop `-1/0/1` choices and World child order among legacy depth-zero Props and stickmen are preserved. Reapply the policy on spawn, load, depth change, and reset. Selection must use the same semantic tier order plus smaller actual art footprint preference, rather than assuming child insertion order expresses depth.
`StageSkyView` lives in its own `CanvasLayer` behind `World`, not under the camera or a wall. Its draw rectangle follows viewport size, so panning/zooming cannot reveal an edge. Use deterministic vector gradient bands, stars, clouds, sun/moon and shapes with bounded point counts. The preset controls only this view. Rendering should be static between sky or viewport changes; no per-frame random placement, animation, parallax, or tint propagation. Sky thumbnails use the same drawing definitions at card size so the choice predicts the Stage.
`StageSurfaceRenderer` defines finite vector motifs with a world-aligned origin and fixed world-unit pitch. Wall and Ground may share Brick/Wood definitions but use context-aware fill/outline colors and scale. `StageWallPanel._draw()` uses its current finite rectangle as a clip and asks the renderer for only intersecting motif cells. For brick walls, cull to the camera-visible local rectangle; if that visible area exceeds 12,000 cells, increase the brick pitch uniformly for all visible courses instead of truncating rows or columns. This keeps large walls fully patterned and fragments continuous without unbounded draw work. Ground renders within its existing rectangle geometry; existing `CollisionPolygon2D` and navigation surfaces are never rebuilt merely for a finish change. The selected **New Ground look** is passed into both `StageTerrainPainter` ghost creation and each committed Ground block; it never repaints earlier blocks. During group resize, wall motif lines retain their pitch and phase. Ground blocks adjacent on a shared edge use the same world phase; shared same-finish interior borders are suppressed or visually joined so a painted run reads as one surface. The neighbor check refreshes on place, delete, move, load, and per-piece finish change, and considers only nearby Ground rectangles; it does not change occupancy/collision. Other motif types remain bounded by the camera-visible region or a maximum cell count per finite panel so `_draw()` work stays limited.
## Selection, input, and transform state
```mermaid
stateDiagram-v2
[*] --> OrdinaryEdit
OrdinaryEdit --> Chooser: Scenery palette
Chooser --> PlaceArt: artwork chosen
Chooser --> DrawWall: wall look chosen
Chooser --> EditScenery: Edit placed scenery
PlaceArt --> OrdinaryEdit: Done / Cancel / RMB / Esc
DrawWall --> OrdinaryEdit: Done / Cancel / RMB / Esc
EditScenery --> GroupPreview: Move together / Resize together
GroupPreview --> EditScenery: Keep changes / Cancel / RMB / Esc
EditScenery --> OrdinaryEdit: Done editing / Esc
```
The controller tracks one active Scenery tool at a time, the number of committed copies this session, and whether a wall/touch draft is active. A passive artwork ghost is never a committed edit. Right-click/Done/Cancel follow the approved [exit matrix](phase_19_scenery_backgrounds_ui_ux.md#placement-exit-cancel-and-mode-changes): committed pieces stay; an active draft is discarded; a late release cannot commit it. Existing terrain and Prop placement are canceled when a Scenery tool takes input. DIRECT/PLAY clears Scenery ghosts and Edit Scenery selection.
`StageSelection` filters both click and box paths. Ordinary EDIT includes existing actionable objects and excludes Scenery broad bounds. In Edit Scenery, candidate geometry comes from a wall rectangle or the full transformed vector art: filled polygons use point-in-polygon, open strokes use a screen-scaled segment tolerance, and missing art uses its marker or known footprint. A small cutout wins over a broad wall even if both are candidates; unresolved overlaps use the shared “Pieces here” popup. The small explicit missing-art **Find artwork** badge is a separate 44 px screen-space hit target that can open Edit Scenery without making the entire missing footprint selectable in ordinary EDIT. Sky never enters either selection scope.
For one selected artwork, a uniform scale changes root `scale` within finite positive bounds. For a wall, resizing changes `panel_size`, with a minimum visible world size and no mirrored/negative rectangle. Pointer/touch handle hit areas are at least 44 logical pixels after camera zoom compensation. `StageGizmos` can share outline/handle drawing helpers; a Scenery-specific adapter is justified for rectangle sizing, no rotation control, and the pending group preview. It must still use `SandboxTheme.overlays` selection colors and the Stage halo style, rather than duplicating visual tokens.
Group preview snapshots `global_position`, `rotation`, artwork `scale` or wall `panel_size`, and the initial world bounds of each selected member. For move, derive one delta from the selected union anchor, apply Snap once to that anchor, then add the same delta to every snapshot position. For resize, compute one positive factor around the opposite union corner `P`; for each artwork center `x`, set `x' = P + factor * (x - P)` and `scale' = factor * scale`; for each wall center/size, use the same center formula and `size' = factor * size`. Derive every preview frame from the snapshots, never the previous preview frame. Reject factor `<= 0`, any collapsed member, or any out-of-range art scale. Buttons use the same formula with top-left fixed. Missing members with no known footprint may move but block group resize with the approved explanation. Mouse release leaves the candidate preview pending. `Keep changes` saves each authored transform, marks dirty once, and returns to Edit Scenery. `Cancel`, RMB, or Esc restores all snapshots exactly and leaves the same selection. A Save request opens a Keep/Cancel decision for the preview and resumes Save only after explicit resolution; tool/mode changes cancel the preview. A temporary group selection and preview are never written to `.scene`.
## Phase 19 addition: authoring size and single-placement transforms
This addition implements the approved [Stage-relative sizing flow](phase_19_scenery_backgrounds_ui_ux.md#stage-relative-authoring-size-in-prop-editor) inside Phase 19. The standard standing guide spans **939.5 world units** from head-circle top to ankle line; write `H := 939.5` as one Prop Editor reference constant derived from `WholeStickmanPreview.GUIDE_JOINTS` and `GUIDE_HEAD_RADIUS`, so the UI never assumes the old 96-unit Normalize Size target. The presets are starting points based on the artwork's **longest visible dimension**, including stroke: Small = `0.35 H`, Room = `1.0 H`, Big = `2.0 H`. This covers both tall and wide drawings without forcing a wide rug or skyline to be one stickman tall. Each **Make bigger** step multiplies by `1.1`; **Make smaller** multiplies by `1 / 1.1`, so opposite steps return to the same size within floating-point tolerance. The labels are approximate comparison choices, not an inferred real-world category or a maximum size. Artist size inference is part of authoring/review instructions; no runtime name parsing, asset size tag, or automatic normalization is added.
### Prop Editor authoring transaction
`PropEditor` (`class_name PropEditor extends Control`) replaces the Arrange toolbar's immediate `_on_normalize_size_pressed()` action and `NORMALIZED_MAX_EXTENT` with **Size for Stage**. The action is disabled when the document has no drawable geometry or its longest visible extent is not finite and positive; its tooltip says **“Add visible artwork to use Size for Stage.”** It owns the toolbar controls, Keep/Cancel routing, and the existing `_dirty`/autosave boundary. `PropCanvas` (`class_name PropCanvas extends Control`) owns a temporary deep copy of its complete shape stack and the supplied editable-anchor dictionary; its normal `shapes` and the editor's `_anchors` remain the committed document while previewing. A small typed API is sufficient:
```gdscript
func begin_stage_size_preview(anchors: Dictionary) -> bool
func preview_stage_size(factor: float) -> bool
func commit_stage_size_preview() -> Dictionary # {"changed": bool, "anchors": Dictionary}
func cancel_stage_size_preview() -> void
func is_stage_size_preview_active() -> bool
func get_visible_shapes_bounds() -> Rect2 # Includes outline/stroke width.
func fit_stage_size_view(bounds: Rect2, padding_px: float) -> void
```
The preview records original internal shape dictionaries, anchors, canvas pan/zoom, and the entry bounds once. Preset factor is `target_longest_edge / entry_longest_edge`; fine buttons adjust the current absolute factor, but **every rendered candidate is derived from the entry snapshot**, never scaled from the previous candidate. The shared `origin` anchor remains at `(0, 0)`. Scale all polygon/line points, circle points and radius, each shape's `outline_width` (including line-only art), and every editable anchor position by the same factor about the document origin. Preserve colors, shape order, vertex flags, `reshaped`, anchor roles, and anchor angles. This is the established whole-document pivot, so Size for Stage does not silently recenter the saved origin or detach grips from their art. Reject a candidate atomically if factor, coordinate, radius, stroke width, anchor position, or visible extent is nonfinite or outside the existing representable coordinate/size limits; leave the last valid preview visible and the committed document untouched. Do not clamp one shape or anchor independently. Zero-width strokes remain zero. A no-op factor of 1 leaves the document clean when it was clean on entry.
Draw the size candidate in `PropCanvas._on_canvas_draw()` and then draw the guide **after all artwork**, with dark halo and light core for the bones, head, and floor line. Derive the comparison floor from the **candidate's current visual bottom**; align both guide ankles to that y and place the figure just to the artwork's left (or over its left portion when width is tight), so offset and centered authoring coordinates compare correctly without translating the artwork. With an empty canvas, draw the guide at its normal origin and ankle line; Size for Stage remains disabled until artwork exists. The floor cue can follow a changed candidate bottom. On entry, compute a union of candidate artwork plus the guide/floor and frame it with a bounded margin. Refit that union after a preset choice and when a fine step would clip it; do not reframe on every draw or override a child's deliberate pan/zoom while the union remains visible. The sizing view may temporarily fit below the canvas's normal manual `MIN_ZOOM` on narrow viewports and restores the prior pan/zoom on exit. Outside the sizing view, show the same foreground guide and floor whenever **Show Stickman Guide** is enabled. During sizing, force guide drawing on without changing or persisting that checkbox; restore its user preference on exit. The guide is canvas drawing only, never a saved shape, anchor, or Stage object.
**Keep size** validates the full candidate, swaps the complete shapes/anchors once, emits one document change, refreshes inspector/thumbnail state, and starts the existing dirty/autosave timer once if the result differs from the snapshot. **Cancel**, Esc, mode switch, New/Load, return to Stage, or closing the editor discards the candidate before another document action; no preview geometry enters Save/autosave. A Save request while the sizing view is open asks the child in the existing sizing controls to **Keep size** or **✕ Cancel** first, then the child can use ordinary Save/Save As; no new save dialog or asset-copy policy is introduced. `Transform Whole Prop`, Attach, Reshape, and canvas editing cannot run concurrently with this sizing transaction. The present whole-prop Transform action remains available in Arrange after sizing.
```mermaid
stateDiagram-v2
Arrange --> SizePreview: Size for Stage / snapshot
SizePreview --> SizePreview: Small, Room, Big, smaller, bigger / derive from snapshot
SizePreview --> Arrange: Keep size / validate, commit once, dirty once
SizePreview --> Arrange: Cancel or Esc / discard, no dirty
SizePreview --> Arrange: leave editor or switch mode / discard, no dirty
```
### Direct Stage move and resize of one Scenery piece
`StageSceneryController` (`class_name StageSceneryController extends Node`) takes ownership of the currently selected Scenery drag, replacing `SandboxStage`'s local `_scenery_drag_snapshots`/`_scenery_dragging` move path. It reuses its existing group snapshot math and `document_change_requested` signal but keeps the single gesture's commit boundary separate: release commits immediately; group release still leaves a candidate requiring **Keep changes**. Suggested typed controller contract:
```gdscript
signal single_gesture_changed(active: bool)
func begin_single_transform(node: Node2D, kind: StringName, pointer: Vector2, dragged_corner: Vector2 = Vector2.INF) -> bool
func preview_single_transform(pointer: Vector2) -> bool
func commit_single_transform() -> bool
func cancel_single_transform() -> void
func is_single_transform_active() -> bool
func step_single_size(node: Node2D, factor: float) -> bool
```
`kind` is exactly `&"move"` or `&"resize"`; the controller accepts only one selected `StageWallPanel` or `StageSceneryArtwork`. Snapshot `{node: Node2D, position: Vector2, rotation: float, scale: Vector2, panel_size: Vector2 when wall, bounds: Rect2, pointer: Vector2, dragged_corner: Vector2}`. This is transient in-memory state, never a `.scene` payload. A missing artwork without known bounds can move but cannot resize. Before applying any candidate, validate the whole result: finite position and scale, positive artwork scale components within the current Stage range, positive wall dimensions above `MIN_PANEL_SIZE`, valid node still in Edit Scenery, and a nondegenerate starting AABB **for resize**. A move preserves any existing positive nonuniform scale. A resize multiplies both scale components by one uniform factor, preserving the ratio of an imported/scripted placement without normalizing it. Rejecting a frame retains the last valid preview. An unchanged release emits no document change.
For move, derive `raw_delta = pointer - pointer0` from the snapshot. With Snap on, align the **visible world AABB's top-left corner** rather than the node's often offset origin: `delta = snap(bounds0.position + raw_delta) - bounds0.position`; apply `position0 + delta`. This preserves the existing top-left grid-alignment promise for placed wall panels and artwork. For uniform resize, take the world AABB corner opposite the dragged corner as the fixed point `P`. With `D = dragged_corner - P`, use `factor = 1 + (pointer - pointer0).dot(D) / D.length_squared()`; reject nonpositive/invalid factors. For artwork with saved local geometry, `position' = P + factor * (position0 - P)` and `scale' = scale0 * factor`, preserving rotation and depth. For walls, use the same position formula and `panel_size' = panel_size0 * factor`, leaving root scale at 1 so Brick/Wood motif pitch does not stretch. The fixed world corner stays fixed even when artwork's authored origin is offset. **Smaller**/**Bigger** in the selected-piece card call `step_single_size()` with reciprocal factors `1/1.1` and `1.1`, top-left pinned; each valid press is an immediate independent commit for that placement only. Other artwork instances, walls, Ground, Props, characters, and their interaction state are untouched.
`StageGizmos` supplies selection outline and corner drawing/hit geometry, using existing `SandboxTheme.overlays` colors and dark-halo/light-core selection language. Extend its Scenery adapter with at least **44 × 44 logical-pixel** hit regions converted through camera zoom, prioritizing a corner hit over the body; do not reuse its current `PropBlock`-only SCALE gesture or origin-pivot formula. The Stage root arbitrates input once: selected corner starts resize, selected body starts move, and group preview has priority while active. `StageSceneryController` previews directly on the selected node, then emits `document_change_requested` exactly once on changed release; `SandboxStage` updates its authored-state/save path and card. Esc, right-click, visible capture-bar **✕ Cancel**, mode/tool switch, selection removal, or node deletion restores the snapshot and clears the gesture. An input-generation token or equivalent active-state guard must consume a mouse/touch release after cancellation so it cannot commit an old draft. Manual Save first cancels an active single gesture and saves only prior commits; background autosave waits for it to end. Group preview retains its existing separate Keep/Cancel save gate.
```mermaid
flowchart LR
Pointer[Corner/body press] --> Hit[StageGizmos hit: corner first]
Hit --> Begin[SceneryController snapshot]
Begin --> Preview[Pointer motion from snapshot]
Preview --> Release[Release: commit once]
Preview --> Cancel[Esc/RMB/capture Cancel: restore]
Release --> Dirty[Stage dirty + scene v1.5 placement transform]
Cancel --> Idle[No dirty; stale release ignored]
```
The selected artwork card adopts `StageInspectorPanel` for fixed header/scrolling body, `StagePanelChoiceGroup` for Far/Near/On the floor, and `StageInspectorPanel.style_action_button()` for Smaller/Bigger and Erase. The selected wall card uses the same shell and action styling. Both call `set_safe_rect()` and preserve the compact Settings reopen behavior. `StagePanelToggleRow` is reserved for actual booleans; no new panel control is needed for size. The already shared `StageUseTargetCaptureBar.set_capture_actions()` shows **✕ Cancel** only while a single drag is held; group Keep/Cancel remains unchanged. New sizing controls in Prop Editor use its existing Arrange toolbar and `PropTheme`, because that editor is a distinct workflow from an in-stage selected-object card. No duplicate dialog, capture-bar, choice, or button styling is introduced.
The existing `.scene` v1.5 `position`, `scale`, `rotation`, and wall `size` fields already persist these per-placement changes; `.prop` v1.1 already stores points, circle radius, outline width, and anchors. This addition requires no schema bump and performs **no load-time resizing or migration of asset geometry or placement scale**. Saving an explicitly revised `.prop` at its existing path intentionally updates all live-reference Scenery and Props that use that path when reloaded; ordinary Save and Save As remain the only asset save actions. A scene's saved placement scale continues to multiply its current referenced art. There is no new asset-version enforcement, copy/update prompt, automatic compensation factor, or runtime name-based size inference.
## Asset catalog, tags, thumbnails, and missing-art repair
`PropFile` advances to v1.1 with optional top-level `tags: Array[String]`. It accepts v1.0 files with `tags = []`, normalizes a fixed set `nature`, `buildings`, `walls`, `roads`, `objects`, deduplicates, and writes only known IDs. Tags describe appearance, never physics. The same file remains the only vector authoring format; no `.scenery` file or new asset library root appears. Keep cutouts as immediate children of `content/props/` for existing scans; unapproved art stays under ignored `content/review/artist/<slug>/` and must not enter browser results. `ContentPaths.canonicalize()` handles saved paths, including old `props/` paths, while preserving external paths.
Split library eligibility by role without duplicating parsing: a shared parse/cache step returns `{id: String, path: String, name: String, shapes: Array[Dictionary], tags: Array[String], visual_bounds: Rect2, material_preset: int, collision_hull: PackedVector2Array}`. Scenery accepts nonempty visible vector geometry, including open lines and transparent-window frames, via `PropVisuals.build()`/shape bounds. A closed filled shape needs three noncollinear points; an open stroke needs two distinct points, positive width, and visible color. These rules accept line art without accepting a blank `.prop`. Props retain the existing valid closed-hull requirement. The existing `StageAssetBrowser` scan, asynchronous thumbnail queue, `ThumbnailCache`, and `PropThumbnail.render_shapes()` are reused for either role; a line-only artwork thumbnail must not go through `flatten_to_payload()` as a fake collision shape. Scenery keeps authored per-shape colors rather than applying a Prop material tint. All artwork includes untagged assets; Unsorted appears when appropriate. The same tag filters are available in Props, but its entries remain physically eligible only. Do not scan review directories or recursively change the existing immediate-child library behavior.
On scene open, load every live artwork reference. A missing/invalid source leaves a `StageSceneryArtwork` node with `missing = true`, saved transform/depth, remembered name, and last known footprint. Render a neutral translucent footprint with dashed light/dark edge when bounds exist, otherwise a compact world marker, with a constant-screen-size picture/question icon and “Artwork not found” label. It stays subdued in DIRECT/PLAY and is never solid. Show one themed missing-assets summary for the load with the UI spec’s **Show scenery** / **Later** actions, while retaining separate Prop/stickman warning semantics. The Scenery piece list has a **Needs artwork** section.
**Find artwork…** uses the established `.prop` chooser; **Choose another artwork** uses the common Artwork gallery. The candidate must pass Scenery visual validation before a ghost appears at the saved position/size/orientation/depth. The explicit **Use this artwork here?** confirmation commits only the selected placement. Cancel/failure leaves the original path, marker, and scene dirty state untouched. Replacing art marks dirty once and refreshes the known bounds, name, and visual tree; the original position, scale, rotation, and depth remain. A changed source file is picked up on reopen for every live reference; there is no embedded snapshot. The optional File details disclosure may show the path for an adult. The simple one-placement repair is required; an all-matching bulk repair is optional and must be explicitly confirmed with its count.
## Starter scenes and document lifecycle
Keep versioned starter documents under `content/stages/starters/` with stable IDs for Blank, Town Street, and Living Room. They are ordinary `.scene` snapshots, not locked nested instances. Blank may be assembled from the current neutral empty-scene defaults; Town Street and Living Room data use the same wall, artwork, terrain, Prop, Sky, and camera schema as a saved child scene. The bundled files reference only explicitly approved production art. A window is a genuine gap between wall rectangles with a transparent frame and Sky behind it; no cutting or glass behavior is inferred. Furniture Props receive deliberate still/block/support roles from the normal Prop contract, without promising Sit.
The current `PendingFileAction.NEW_SCENE` unsaved Save/Discard/Cancel gate remains authoritative. After it resolves, show the shared starter gallery **before** replacing the current document. Canceling the gallery leaves the existing scene and post-gate dirty state. Selecting a starter preflights its scene data, loads ordinary editable objects, resets `current_path` to empty, stamps a fresh creation time, proposes its editable name, and marks the new document unsaved. Failed starter validation leaves the prior scene intact and shows one themed error. No starter is inserted into another scene. A first-save path follows the existing Save As flow. CLI scene playback continues to load `.scene` directly, with no starter-specific runtime branch.
## UI component and theme reuse
| Surface | Reuse or shared extension | Feature-local work / justified exception |
| --- | --- | --- |
| File → New gate | `StageChrome` File menu; `StageModalViews` unsaved dialog; `StageDocumentController` pending action | Starter-choice result and fresh-document handoff only. |
| Starter, Scenery, Sky, finish and replacement galleries | Extract a responsive shared card-gallery body from `AssetSelector`/`scenes/stage/asset_selector.tscn`; retain `AssetCard`, `StageAssetBrowser`, thumbnail pipeline | Tabs, descriptive text, and filter meaning are feature-specific. Replace the current fixed four-column/12-per-page and `FOCUS_NONE` card limitation in the shared path: adaptive columns, scroll, native focus, visible focus ring, and keyboard activation. Non-placeable cards disable card drag. |
| Browse tags and authoring tags | One shared wrapping chip/choice treatment using native Buttons and `FlowContainer`/`HFlowContainer`, `SandboxTheme` popup tokens | Single-select browser filter versus multi-select authoring tags have distinct state rules. Do not use vertical `StagePanelChoiceGroup` as a chip row. |
| Wall color | Extend `ColorSwatchSelector` for wrap/selected/focus; native color picker in a `StageModalViews` themed window | Chosen wall color remains local to the placement/selected panel. |
| Selected artwork, missing art, wall, Ground, multi-selection | Direct use of `StageInspectorPanel`, `StagePanelChoiceGroup`, `stage_panels` tokens and `StagePropSettingsView` dismissal/compact Settings precedent | Domain rows and commands stay in a Stage Scenery/Ground view adapter. `StagePanelToggleRow` is used only for a real boolean, not depth or finish. |
| Member list / overlap popup / wall-area eraser | `StageInspectorPanel` action styling; generalize `StageModalViews.show_objects_here()` labels and candidate filter; reuse `StageUseTargetCaptureBar` for the eraser prompt and Done/Cancel | Membership, piece labels, the red rectangle preview, and wall subtraction are Scenery-specific. The selected wall card starts the tool; no new dialog, overlap popup, or capture-bar styling. |
| Placement/group capture | Extract themed responsive shell from `StageUseTargetCaptureBar`, preserving mode-pill-safe position, prompt/Cancel grouping, action stacking | Use-target and Scenery wording/actions stay in callers. No copied capture-bar CSS/StyleBox code. |
| Cursor and ghosts | `StageChrome` tooltip safe placement, `StagePlacementOverlay`, terrain ghost presentation, `SandboxTheme.overlays` | The draft geometry, validity, and missing marker are feature-specific. |
| Make artwork | Existing `PropEditor`, `PropCanvas`, `.prop` Save/recovery and `PropThumbnail` | Add a typed `AuthoringContext { PROP, SCENERY }`, `signal artwork_saved(path: String)`, and `signal return_requested`. In Scenery context hide material/mass/bounce, attachment, and other action settings; keep shape/color/name/tags/Save. |
The shared UI contracts should remain small and typed. Extract `scripts/shared/ui/asset_gallery_popup.gd`, `class_name AssetGalleryPopup extends PopupPanel`, as the fixed header/Close and scrollable responsive `AssetCard` grid used by the current `AssetSelector` and Phase 19 choosers. It emits `item_selected(entry: Dictionary)` and `cancelled`, and exposes `open_gallery(title: String, entries: Array[Dictionary]) -> void`, `set_thumbnail(entry_id: String, texture: Texture2D) -> void`, and `focus_entry(entry_id: String) -> void`. A card entry has `{id: String, label: String, thumbnail: Texture2D|null, helper: String, draggable: bool}`; the caller maps `id` to its domain item. `AssetSelector` retains its public `open(kind, entries)` facade and uses the shared gallery, so existing Prop/Stickman callers are not broken. `StageAssetBrowser` retains scanning/caching and adds a role-safe filtered query such as `scan_entries(kind: String, tag: StringName = &"") -> Array[Dictionary]`; no gallery owns asset parsing.
Extract `scripts/shared/stage_ui/stage_capture_bar.gd`, `class_name StageCaptureBar extends PanelContainer`, from the `StageUseTargetCaptureBar` shell. Its intended interface is `signal action_pressed(id: StringName)`, `func configure(stage_theme: SandboxTheme, mode: int, top_bar_height: float) -> void`, `func set_prompt(text: String) -> void`, `func set_actions(actions: Array[Dictionary]) -> void`, and `func hide_capture() -> void`; each action dictionary has `{id: StringName, label: String, prominent: bool}`. Keep `StageUseTargetCaptureBar` as a thin Use-specific wrapper or composition client preserving `cancel_requested` and `change_prop_requested`. Generalize `StageModalViews.show_objects_here()` with a typed title/label provider while retaining its existing facade and `object_selected(node: Node2D)` signal. Existing `StageInspectorPanel`/`StagePanelChoiceGroup` APIs need no new domain knowledge: caller-owned cards call `set_header`, `body`, `set_safe_rect`, `request_fit`, and `add_choice`/`select_no_signal` directly. `ColorSwatchSelector` may change its layout container to `FlowContainer` while preserving `configure(colors: Array[Color], selected: Color, theme: Variant)`, `set_selected`, and `color_selected(color: Color)`.
All new popup/native-window instances receive `SandboxTheme.control_theme()` explicitly, then `StageModalViews.apply_dialog_theme()` for child font overrides. The latter does not assign a window theme. Extend `SandboxTheme.DEFAULT_CONFIG` and `sandbox_theme.json` only for shared popup/gallery/chip/capture/selection measurements and colors. Use local StyleBox copies; do not mutate the cached shared `Theme`. Selected, hover, focus, and disabled remain distinguishable. All actionable controls and handle hit regions meet the approved minimum 44 × 44 logical pixels. Viewport-safe rectangles keep the mode pill and Close reachable; long content scrolls under a fixed header. The existing queue/rule/speech popups are distinct workflows and are not recast as gallery or inspector shells.
## Stage-hosted Prop Editor lifecycle
Instantiate the existing `scenes/editors/prop/prop_editor.tscn` in a full-viewport Stage-hosted UI layer, set Scenery context before its first user interaction, then focus its first drawing/name control. While active, `SandboxStage` routes no world selection, placement, camera mode, or shortcuts to the hidden Stage; dialogs and editor input remain usable. The underlying Stage document, dirty flag, selection, and current scene path remain in memory. The editor keeps its own `.prop` autosave and unsaved-work gate. Its F6 standalone exit still quits its own run; in hosted context Close/Cancel emits `return_requested` to remove the editor view and restore Stage input/focus without quitting the Stage. A successful Save emits `artwork_saved(path)` after the file write; **Place it as Scenery** returns to the Artwork chooser with a fresh scan/thumbnail generation and the saved asset focused. Merely opening/canceling the editor never changes the Stage document. A saved new `.prop` outside the Stage is a separate asset write, so returning without placement leaves the Stage unchanged.
## Implementation and art gates
1. Extend data boundaries first: `.prop` tags and role-safe visual scanning; scene v1.5 migration/validation; semantic draw order; inert visual nodes and shared finish/Sky definitions. Validate old scene/Prop compatibility before UI wiring.
2. Generalize the existing card gallery, tag treatment, capture bar, overlap popup, color treatment, confirmation style, and theme tokens. Build the Phase 19 chooser and inspectors from those shared controls. Avoid a parallel popup/card system.
3. Connect placement, selection scopes, missing repair, Ground finish, and group snapshot previews through `StageSceneryController` and the existing Stage save/mode gates. Add the starter gallery and Stage-hosted Prop Editor return path.
4. The original Phase 19 art manifest used `$asset-creation` and approved production assets. **This sizing amendment requires no new vector/raster deliverables**: it draws the existing stickman guide and floor cue in Prop Editor and extends existing Stage handles. An artist may deliberately revise a particular `.prop` through the ordinary asset-review workflow, but implementation does not batch resize built-ins or reopen the original art approval gate merely because this amendment is signed off.
5. Run syntax/import checks and focused existing suites after each integration slice. Add focused behavior checks where actual world state is at issue. Perform render-capable Stage/editor captures and real input-path checks before reporting visual/interactive outcomes as agent-verified.
Relevant existing regression starting points are `tests/test_phase19_scenery.gd`, `tests/test_phase10_prop_integration.gd`, `tests/test_prop_editor_file.gd`, `tests/test_prop_editor_canvas.gd`, `tests/test_prop_editor_round3.gd`, `tests/test_content_paths.gd`, and `tests/test_phase17a_prop_roles.gd`; run focused affected suites rather than the full historical matrix for each sizing edit. The original Phase 19 fixture covers scenery persistence and group behavior; extend focused coverage for Size for Stage's atomic Keep/Cancel and a real one-piece Stage resize, including late release after cancellation. Exercise production input and save/reopen paths, not only helper calculations.
## Observable acceptance and verification routes
Each row names a child path and a repeatable observation. Automated checks may create in-memory fixtures, but should use actual Stage spawning, document, input, and PLAY paths where practical; node/signal existence alone does not establish behavior.
| Child path / scenario | Observable outcome and measurement |
| --- | --- |
| File → New → cancel starter, then Town Street; save/reopen; repeat Living Room | Existing scene survives chooser Cancel. Each chosen starter is unsaved, individually editable, and preserves Sky, separate panels/art/Props/Ground and positions after save/reopen. Living Room Sky is visible through an actual wall gap. |
| Scenery → Artwork → Nature → Tree; place twice, change depth, RMB; reopen | Two distinct inert copies persist at saved transforms/depths. RMB removes ghost, preserves committed copies, returns to ordinary EDIT. Check `PhysicsDirectSpaceState2D` contacts, navigation candidates, and event participation remain absent for scenery while a normal Prop remains functional. |
| Make line-only artwork in Stage-hosted editor; Save → Place it as Scenery | `.prop` with open strokes appears in Scenery and has a valid thumbnail; it does not enter Props without valid closed collision geometry. Stage unsaved state and focus survive authoring return. Tags filter it without changing its behavior. |
| Draw Brick wall, resize it; paint a Wood Ground run, restyle one block | Wall is finite/inert; brick pitch stays constant. Ground collision and walking path remain valid; wood motifs line up across neighbors, shared seams do not interrupt the run, and only the selected block changes to the new finish. Measure character feet/contact over several physics ticks before and after restyling. |
| Ordinary EDIT click/box in front of broad wall; enter Edit Scenery; select art and wall through Pieces here | Gameplay objects remain reachable in ordinary EDIT. Scenery-only selection finds art ahead of broad panels, includes missing art in Edit Scenery, and never selects Sky. Real pointer/touch/keyboard activation produces the expected selection and popup dismissal. |
| Living Room: select two walls, window frame, rug; move preview then RMB; resize preview then Keep; Save/reopen | RMB restores each exact snapshot transform. Keep applies one positive factor around the opposite fixed corner, preserves artwork proportions/depth and wall motif pitch, leaves Ground/furniture untouched, and persists member transforms without a saved group. Save while pending asks Keep/Cancel and never writes draft values. |
| Prop Editor → Arrange → Size for Stage on a tall refrigerator, wide rug, and small item; try each preset/fine buttons, Cancel once and Keep once | The artwork and standard guide remain fully visible at ordinary and narrow viewport sizes. Longest visible edge reaches approximately `0.35H`, `1H`, or `2H` while proportions, circle radii, line widths, and editable anchor positions share the same factor; the foreground halo guide's ankles meet the floor cue at the candidate bottom. Cancel restores byte-equivalent authored geometry/anchors and the prior dirty state; Keep commits once. A Big preset cannot clip the guide merely because opening began on small artwork. Save/reopen the `.prop` through the normal path and compare geometry/anchors. |
| Edit Scenery → select two copies of one artwork; drag a corner on one, release; then cancel another drag with Esc/RMB/touch Cancel and release the pointer | The opposite visible corner stays at the same world position; only the edited placement changes, and it commits on release without Keep. A canceled drag restores its snapshot and the late release makes no change. With Snap on, a moved artwork or wall's visible top-left lands on a grid intersection. Smaller then Bigger returns near the initial bounds. Save/reopen and check the two copies retain distinct scales, while Props/Ground/PLAY behavior remains unchanged. A missing artwork without known bounds shows no resize handle. |
| Open a pre-amendment `.prop` and `.scene`, then explicitly overwrite one referenced `.prop` through ordinary Save | Opening alone does not rescale artwork or placement values. The explicit source overwrite changes every live reference to that source on reopen, while each scene placement retains its saved scale; there is no hidden compensation, copy prompt, or version migration. |
| Move a referenced `.prop` aside, reopen, repair selected missing artwork; cancel once then confirm | One neutral marker remains at saved position/depth, summary appears once, ordinary EDIT targets are not stolen, cancel keeps path/dirty state, and confirmed repair changes only that placement. Missing footprint/no-footprint group resize rules and PLAY marker presentation hold. Restore source and reopen to confirm live-reference visual update. |
| Select each Sky, pan/zoom, PLAY/stop, reopen | Sky fills screen without an edge or flicker; foreground pixel colors are unchanged by Sky choice. Layer order remains Sky/wall/Far/Near/Ground/floor/actors through camera movement, simulation, and reload; ghosts/handles/capture bars disappear in PLAY. |
Visual acceptance requires opening the affected scene in a render-capable Godot GUI, capturing the named chooser, normal/zoomed Stage, missing-art, group-preview, **Prop Editor sizing guide at Small/Big and narrow width, active single-piece handles/capture bar**, and PLAY states, and inspecting the pixels. Record capture location, scene/state, and observation. Interactive acceptance requires exercising the actual controls and input events, including RMB/Esc/touch cancellation, button sizing, grid-aligned move, commit on release, and a late release after cancellation. If GUI or input automation is unavailable after an attempted route, name the unverified outcomes and hand the user a focused reproducible check; a headless parse or screenshot file alone is insufficient. Phase 19 user/editor verification remains open until the child checks the integrated behavior.
## Trade-offs and future work
- Independent nodes preserve the current editable direct-child document model and avoid using `PropBlock` as a fake inert body. The cost is a separate artwork visual node and explicit scene-object decoding.
- Live `.prop` references keep scenes compact and make art edits visible on reopen. They can change a scene's appearance after the source file changes; the saved name/footprint and repair flow cover missing files, not version pinning. A future immutable asset-version policy would need a separate decision.
- Stage-relative authoring size is geometry stored in `.prop`, while a Stage Scenery resize is an instance transform stored in `.scene`. This keeps legacy loads stable and lets two placements of one source have different sizes. It also means an intentional overwrite of a live `.prop` updates all references; the user chose to retain ordinary Save/Save As without a new copy/update decision. The authoring preview scales outline width and editable anchors with geometry and uses origin as its pivot; its floor guide follows visual bounds solely for comparison.
- A one-piece Stage drag commits on release for speed; a multi-piece group retains Keep/Cancel because its wider effect is harder to judge during a gesture. Both use exact snapshots before commit, while Stage-wide Undo remains deferred to the separate phase logged in technical debt.
- Rebased terrain drawing enables the required floor-art tier without changing Prop-to-character depth semantics. It changes terrain's relative visual position beneath Props; physics and navigation stay unchanged and must be regression checked.
- Vector motifs retain clarity and world alignment without repeated raster textures, at the cost of careful clipping/culling and seam management. Keep motif definitions centralized so wall/Ground previews match runtime drawing.
- Stage-wide Undo/Redo deserves one command history across placement, deletion, style, repair, and group commits. The Phase 19 Keep/Cancel preview is a local transaction only. Whole-set transform needs its own design because Ground physics, character/Prop placement, rules, and camera all have different transform invariants.
@@ -0,0 +1,277 @@
# Phase 19 — Scenery, backgrounds, and surface finishes: UI/UX specification
> **Status:** Approved and implemented, including the Stage-relative asset-sizing addition (2026-10-16). Based on the [accepted product brief](../plans/phase_19_scenery_backgrounds_surface_finishes.md). Full Phase 19 user/editor verification remains pending.
>
> **Decision:** Use a responsive Scenery chooser followed by canvas tools. Placement stays active for repeated copies. Right-click exits placement. Scenery artwork keeps live `.prop` references, with a visible, repairable placeholder if a referenced file cannot be found. The Phase 19 addition standardizes new artwork around a standing-stickman size guide and adds single-placement Scenery resizing; direct single-piece transforms commit on release, while the existing group preview keeps its Keep/Cancel boundary. Phase 19 does not add Undo; Stage Undo is deferred to its own future phase and should be recorded in tech debt during architecture planning.
> **Reuse gate for every new dialog or control:** Before building it, inspect the existing shared UI, Stage panel kit, Stage modal/capture controls, and `SandboxTheme` tokens. Use a matching component directly; extend or extract shared behavior when Phase 19 uses it in more than one place. Keep only content and interaction that are genuinely unique to this feature local. Do not add a parallel card grid, overlap popup, capture bar, dialog skin, swatch style, or selected-object inspector. The later architecture specification must name any justified exception before implementation.
## Child promise and visual language
The Stage teaches three roles through entry points and short, visible words: **Scenery stays put**, **Props can be part of the action**, and **Ground is where stickmen walk**. An identical drawing can be placed through Scenery or Props without implying the copies behave alike. Sky is chosen through its card, never by selecting an enormous rectangle in the Stage.
Keep the existing mode language: EDIT uses cyan/blue and a visible grid, DIRECT uses amber/gold and its camera frame, and PLAY uses green with clean artwork. Each active tool also has an icon and words; color alone never identifies it. All buttons, cards, chips, handles' pointer hit regions, and dialog actions have at least a 44 × 44 logical-pixel target. Control heights come from `SandboxTheme` tokens rather than local constants.
## Flow at a glance
```mermaid
flowchart LR
New[File → New] --> Gate[Protect unsaved scene]
Gate --> Starter[Choose Blank, Town Street, or Living Room]
Starter --> Edit[EDIT Stage]
Edit --> Scenery[Scenery chooser]
Scenery --> Sky[Choose Sky: changes immediately]
Scenery --> Wall[Choose wall look → draw panels]
Scenery --> Art[Filter artwork → choose drawing → place copies]
Scenery --> Dress[Edit Scenery → select and change pieces]
Dress --> Several[Select several → move or resize scenery together]
Several --> Review[Keep changes or Cancel preview]
Review --> Dress
Edit --> Ground[Ground → choose look → paint walkable pieces]
Wall --> Edit
Art --> Edit
Dress --> Edit
Ground --> Edit
```
The chooser is for discovery and choice. Once an artwork or wall tool is chosen, it closes so the Stage is visible. A small capture bar below the top toolbar and mode pill carries the current task, **Done**, and **✕ Cancel**. The cursor pill and ghost carry the immediate instruction at the pointer.
## Concrete screen examples
Desktop, while placing a Tree:
```text
┌ File ─────────────── ✏️ Edit | 🎬 Direct | ▶️ Play ─── Grid Snap ┐
│ [Ground] │
│ [Ramp] ✏️ EDIT │
│ [Step] ┌──────────────────────────────────────────────┐ │
│ [Props] │ 🌳 Tree stays put [Far] [● Near] [Floor] │ │
│ [People] │ Place another tree [Done] [✕ Cancel] │ │
│ [Scenery] └──────────────────────────────────────────────┘ │
│ [Areas] │
│ (translucent Tree ghost) │
│ 🌳 Tree · Near │
│ ─── ground / existing set ─── │
└────────────────────────────────────────────────────────────────┘
```
The Scenery chooser, opened from the left palette:
```text
┌ Scenery: stays put ─────────────────────────────── [✕ Close] ┐
│ [Backgrounds] [● Artwork] [Edit placed scenery] │
│ [All artwork] [Nature] [Buildings] [Walls] │
│ [Roads & paths] [Objects] [Unsorted, when needed] │
│ │
│ [ Tree preview ] [ Storefront preview ] │
│ [ Tree ] [ Storefront ] │
│ [ Skyline preview ] [ Rug preview ] │
│ [ Skyline ] [ Rug ] │
│ │
│ [Make artwork…] │
└──────────────────────────────────────────────────────────────┘
```
Edit Scenery, with several pieces selected and a group resize preview:
```text
┌ File ─────────────── ✏️ Edit | 🎬 Direct | ▶️ Play ─── Grid Snap ┐
│ [Scenery] ✏️ EDIT [Edit Scenery · 4 pieces] [Done editing] │
│ ┌──────────────────────────────────────────┐ │
│ │ ◇ fixed corner old dashed bounds │ │
│ │ [wall] [window] [clock] [rug] │ │
│ │ new bright group outline ◉ │
│ └──────────────────────────────────────────┘ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Resize 4 scenery pieces · Ground and Props stay put │ │
│ │ [Smaller] [Bigger] [Keep changes] [✕ Cancel] │ │
│ └────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
```
These are relationship examples, not fixed pixel geometry. On a narrow viewport the chooser becomes a bounded, scrollable sheet with one card column if needed; filter chips wrap. It never opens wider or taller than the unobstructed screen. The top mode pill remains visible, and the chooser always has a reachable Close button. The capture bar wraps its text and stacks actions only when needed; it never hides the mode pill or the Stage point being placed.
## Control layout and navigation
### Stage palette and Scenery chooser
- Add one illustrated **Scenery** card to the existing vertically scrollable left palette. Keep **Props** and **Ground** as separate cards, with a short helper line in their chooser/tool: “Stays put”, “Can be part of the action”, or “Stickmen walk here.” The palette card opens the Scenery chooser rather than directly arming a tool.
- The chooser has a fixed title/Close row; **Backgrounds** and **Artwork** tabs; a scrollable body; and a fixed lower **Make artwork…** action when relevant. It is a chooser workflow, not a selected-object inspector. It receives `SandboxTheme.control_theme()` and its own popup tokens, while borrowing the Stage panel system's font, surface, selected, hover, focus, and target-size rules.
- **Backgrounds** shows two large category choices, **Sky** and **Wall panel**. **Artwork** shows a single-select filter row with **All artwork**, **Nature**, **Buildings**, **Walls**, **Roads & paths**, **Objects**, and conditional **Unsorted**. One drawing may carry several tags, so it appears under every matching filter. All artwork includes old, untagged drawings. Filters describe appearance, not behavior. Apply the same filters in the Props browser while keeping its title and placement role distinct.
- Use a `FlowContainer` for wrapping filter buttons and an adaptive card grid for previews. Each card has a large illustration, a readable name, and a visible keyboard focus ring. No card depends on hover to reveal its action. Changing tabs or tags preserves the chooser and moves focus to the first relevant control; closing returns focus to the Scenery palette card.
- A chooser Close or Esc dismisses only the chooser. Clicking outside may dismiss it when safe, but never chooses an asset or changes a Sky. A blank result shows **“No artwork here yet”** and offers **All artwork** and **Make artwork…** rather than an empty panel.
### File → New starter chooser
- Run the existing unsaved-work Save/Discard/Cancel gate before offering a starter. Do not replace the open scene until a starter card is chosen. If the starter chooser is canceled, the current scene remains open; its dirty state reflects whether the child saved it at the gate.
- The themed modal uses three large illustrated cards: **Blank Scene**, **Town Street**, and **Living Room**, plus **✕ Cancel**. Each card pairs a preview with one plain sentence: “Start from scratch”, “Shops, trees, sidewalk, and road”, or “A room with a window, rug, and furniture.” The cards wrap to fewer columns on narrower screens and remain scrollable. No tiny menu inside a card.
- Choosing a card creates a new unsaved scene with its suggested name in an editable name field. The name may be changed there or through the existing Rename Scene command. A small, dismissible on-Stage cue can say **“Try Scenery to change this place”** after Town Street or Living Room opens. It must not cover the Stage or block input.
- Blank starts with the existing neutral view and no Ground; the other two show editable ordinary pieces described in the accepted brief. Their cards must not promise a functional door, sitting, or automatic window cutting.
## Backgrounds and placement
### Sky
- **Backgrounds → Sky** opens four designed thumbnail cards: **Clear Day**, **Cloudy Day**, **Sunset**, **Starry Night**. The currently selected card has a strong border, filled selected state, and the text **“Current Sky”**; keyboard focus remains a separate outline.
- One tap or Enter/Space changes the Sky immediately; the chooser stays open so another card can be tried. Close means “stop choosing”, not “revert”. With no Undo in Phase 19, the child can restore the previous look by choosing its card again. Changing Sky never recolors or tints other pieces. Sky has no Stage selection handles or overlap entry.
- The thumbnail shows enough clouds, sun/moon, and color variation to predict the Stage result. A chosen Sky is static during PLAY and stable through camera movement.
### Wall panel
- **Backgrounds → Wall panel** offers picture choices **Paint**, **Brick**, and **Wood**, plus a large **Color** swatch/button. Common colors appear first; **More colors…** opens a themed color dialog with a visible Cancel. Finish and color choices are reflected in a small preview and then in the translucent Stage draft.
- Choosing the look closes the chooser and arms **Draw wall panel**. The capture bar says **“Drag across the space for a wall panel”**, with **Done** and **✕ Cancel**. The pointer has a corner reticle and a nearby pill **“Drag to draw wall”**. A press sets the first corner; dragging draws a world-anchored translucent patterned rectangle; release commits one finite panel. The pattern preview remains at sensible scale rather than stretching with the draft.
- Snap, when enabled, visibly aligns the draft to nearby grid lines. An unusably small draft shows a red outline and **“Make the wall bigger”**; release leaves no panel. A valid draft uses the EDIT accent plus a neutral outline and dimension cue. This is appearance only: a helper in the chooser says **“Looks like a wall. Stickmen can pass through.”**
- Touch can drag or tap the first and second corners. The first tap leaves a visible anchored corner and continuous rectangle preview; the second tap commits. The same visible Cancel and Done remain available. Right-click cancels a draft and exits the wall tool under the exit rules below.
### Artwork and depth
- Choosing an artwork card closes the chooser, starts a translucent ghost under the pointer, and shows a capture bar with the drawing's name, **Far**, **Near**, **On the floor**, **Done**, and **✕ Cancel**. **Near** is the default for a newly chosen drawing. Each depth choice has a simple layer illustration plus text; selected, hover, and focus states differ without relying on color. Changing depth updates the ghost's visible placement layer before commit.
- The cursor pill says **“Click to place [name]”**; on touch it becomes a small anchored Stage hint after the asset is chosen. Tap/click commits one copy. The ghost remains ready for another; after a commit the bar says **“Place another [name]”** and shows the count of copies placed in this tool session. Copies already committed remain when the tool closes. A repeat placement starts as another distinct piece.
- The selected artwork's source card may carry tag badges in the chooser, but tag words never suggest collision or interaction. The Stage preview says **“Stays put”**. Its depth affects appearance only; it does not grant blocking, pickup, support, Use, or event behavior.
### Opening a wall panel for a window or doorway
- Wall-area erasing is easy to discover from either entry point: a selected wall panel's shared Scenery inspector includes a touch-sized **Erase wall area** action, and the Edit Scenery capture bar shows **Erase wall area** whenever nothing is selected. Either starts a dedicated tool with a translucent red, crossed rectangle preview and a visible capture bar: **“Drag over the wall · release to erase”**, **Done**, and **✕ Cancel**. Snap-to-grid, when enabled, applies to both drag corners.
- Dragging a rectangle through one or more wall panels removes only the covered area. Each intersected panel is split into up to four remaining rectangles that retain its look and color. Brick/wood motifs keep a shared anchor, so the surrounding pattern does not restart at every cut edge. Existing artwork and other scenery are untouched; this is a visual opening, not a collision or doorway mechanic.
- Each valid release applies that opening immediately. **Done**, **✕ Cancel**, right-click, or Esc exits the tool; canceling an in-progress drag discards only that draft, while previously completed openings remain. Phase 19 has no Undo, so keep the capture copy clear about this commit-on-release behavior.
### Placement exit, cancel, and mode changes
| State | **Done** | **✕ Cancel** | Right-click / Esc |
| --- | --- | --- | --- |
| Chooser open, no tool | Close chooser | Close chooser | Close chooser |
| Tool armed, only passive artwork ghost or wall corner cursor, no placement yet | Exit without placing | Exit without placing | Exit without placing |
| Active uncommitted wall draw or resize | Discard the draft, then exit the tool | Discard the draft, then exit the tool | Discard the draft, then exit the tool |
| One or more pieces committed, no active gesture | Keep pieces and exit | Keep pieces; discard only the next passive ghost and exit | **Done**: keep pieces and exit |
| One or more pieces committed, another gesture active | Keep prior pieces; discard current draft and exit | Keep prior pieces; discard current draft and exit | **Cancel current draft**, keep prior pieces, then exit |
A passive artwork ghost does not count as an active gesture; a pressed wall corner, two-tap first corner, or resize drag does. Releasing a mouse button after cancellation cannot commit the old draft. The capture bar keeps a short line **“Placed pieces stay”** once any copy has committed, so Cancel does not falsely imply that all prior placements are erased. Right-click is an optional pointer shortcut; Done and Cancel stay visible for touch and keyboard users. Switching to DIRECT/PLAY or opening another Stage tool discards an active draft, preserves completed pieces, and removes all ghosts, handles, and capture prompts. No new Undo control or shortcut is introduced here.
## Edit Scenery and selected-object cards
- The Scenery chooser has a large **Edit placed scenery** action. It closes the chooser and enters a clearly labeled **“Edit Scenery”** state inside EDIT, with an on-Stage pill and prominent **Done editing** button. The global mode remains EDIT and its mode pill remains visible. Done editing, Esc when no edit gesture is active, or switching modes returns to ordinary selection. Esc during a resize or move first cancels that draft and leaves Edit Scenery active. The child can reopen Scenery to place more pieces.
- Ordinary EDIT click/box selection favors characters, Props, Ground, and existing interactive objects; broad wall panels, Far/Near artwork, and floor art do not enter those results. In Edit Scenery, only wall panels and Scenery artwork enter click/box selection. The selected piece gets a bright outline backed by a dark halo, a text name/depth label, and large move/resize handles with at least 44 × 44 logical-pixel hit regions at common zoom. A single-piece drag previews live and commits immediately on release; uniform resize keeps the opposite corner visibly pinned, and the transform affects only that placement. It has no extra Keep confirmation. While the pointer/finger is held, a compact capture bar offers **✕ Cancel** for touch users; Escape, right-click, or leaving Edit Scenery also cancels the active gesture and restores the pre-drag position/size. Releasing the gesture commits without waiting for another choice. A later Stage Undo action is deferred to its own phase and tracked in technical debt. The selected-artwork card also offers large **Smaller** and **Bigger** actions for users who prefer buttons; each applies one immediate, proportion-preserving size step to that placement only. Existing placement sizes are not automatically changed when this feature is added.
- When pieces overlap, a small cutout under the pointer takes priority over a broad panel. Clicking a panel's exposed area or edge selects it. If several pieces still match, show a themed **“Pieces here”** chooser with one large labeled row per candidate and a visible Close. A selected panel also remains reachable through that chooser when its visible area is covered. Box selection in Edit Scenery includes only Scenery pieces; ordinary box selection does not gather broad scenery.
- A selected Scenery artwork card uses `StageInspectorPanel` and `StagePanelChoiceGroup`. The header has the drawing name, icon, and Close. The body offers an illustrated **Where it appears** group with **Far**, **Near**, and **On the floor**, large **Smaller** / **Bigger** size steps, and **Erase scenery**. A selected wall card uses the same shell, with **Wall panel** header, **Look**, **Color**, and **Erase wall panel**. Each change applies only to that selected piece. These cards have a fixed Close, scrollable body, and the established compact **Settings** reopen button while the same piece remains selected. Closing the card does not exit Edit Scenery.
- An Erase action for a wall or artwork asks **“Erase this piece?”** with **Keep it** and **Erase** buttons before removing it, because Phase 19 has no Undo. The Delete key follows the same safeguard for selected Scenery.
- Sky is never listed in **Pieces here**, never box selected, and never shown in the selected-piece card. Its only edit path is **Backgrounds → Sky**.
### Artwork file missing after reopen
- A missing live `.prop` reference leaves a **real, saved Scenery placement** at its authored position and Far/Near/On the floor depth. On the Stage, draw a soft neutral translucent footprint with a dashed light-and-dark outline when the saved extent is known; otherwise draw a compact world-anchored marker at the saved point. A constant-screen-size picture/question icon and label say **“Artwork not found”**, followed by a short remembered drawing name or file name when available. Do not use a large red error block, expose a full file path on the canvas, or show an empty gap that looks like the child erased the art. Keep the marker legible on all four Skies and on common Ground/wall looks. In DIRECT and PLAY, the placement remains a subdued neutral marker at the same layer, without edit handles or repair controls, so the missing set piece is still evident without covering the action.
- On scene open, reuse the existing missing-assets summary dialog with calmer copy: **“Some artwork could not be found. Your scenery spots are still here.”** List the friendly names and offer **“Show scenery”** and **“Later”**. Show scenery enters Edit Scenery and focuses the first missing placement; Later closes the dialog and leaves every marker visible. This summary appears once for the load, not on every frame or selection. It does not replace the existing stickman/Prop warning semantics; the missing-Scenery wording is scoped to Scenery.
- In ordinary EDIT, the footprint and label are visible but the missing placement's broad bounds do **not** enter normal click or box selection. Its small, explicit **Find artwork** badge has at least a 44 × 44 hit target and can switch to Edit Scenery with that placement selected; the badge alone takes input, so a child can still select a character, Ground, or Prop in front of the marker. The Stage palette's **Edit placed scenery** path and a **Needs artwork** section in the Scenery piece list provide alternatives when the badge is covered or offscreen.
- In Edit Scenery, the missing placement participates in Scenery selection and **Pieces here** with a distinct picture/question icon and **“Artwork not found · [name]”** row. Selecting it shows the normal `StageInspectorPanel` shell headed **“Artwork not found”**, with the remembered name, its saved depth, and large actions **Find artwork…**, **Choose another artwork**, **Keep for now**, and **Erase scenery**. An optional **File details** disclosure may show the saved path for a helping adult; the path is not the primary instruction. The neutral marker remains visible when the card is closed, and the compact Settings reopen control works as for other Scenery. If the original size is unavailable, do not show misleading resize handles; explain **“Find artwork to change its size.”** Moving the marker and changing its depth remain possible without making it solid or interactive. It can join Select several for group moves; if any selected member lacks a known footprint, disable Resize together with the same plain-language reason until that artwork is repaired.
- **Find artwork…** opens the established themed `.prop` file chooser so the child can point to a moved file. **Choose another artwork** opens the same responsive Artwork gallery used for placement, explicitly labeled as a replacement choice. After either choice, show a ghost of the candidate at the saved position, size/scale, orientation, and depth where those saved values exist, and ask **“Use this artwork here?”** with **Use artwork** and **✕ Cancel**. The replacement can be adjusted afterward; do not silently alter Ground, Props, or other Scenery. Canceling the chooser, file dialog, or preview leaves the missing marker and saved reference untouched. An unsuccessful file load returns to the same marker/card with a plain **“Could not open that artwork. Try another file.”** message. Erase uses the shared **“Erase this piece?”** confirmation; only confirmed Erase removes the placement. If several places reference the same missing file, default to repairing the selected place only; an explicit **Use for all matching places** choice may be offered, with the affected count shown before confirmation.
- The placeholder, badge, piece-list row, preview actions, and dialog work by touch and keyboard: focus is visible and distinct from the missing state; Tab reaches the badge/list, actions are at least 44 × 44, Enter/Space activates them, Esc closes a chooser or cancels an uncommitted preview, and focus returns to the selected placeholder/card. On a narrow viewport, the label truncates gracefully to the remembered name, the inspector body scrolls below a fixed header/Close, and its actions wrap or stack without hiding the mode pill. A missing marker is identified by icon and words as well as outline, never by color alone.
### Move or resize several Scenery pieces
- **Select several** is a large, explicit action within Edit Scenery. While it is active, tapping pieces adds/removes them without beginning a move. A touch-friendly, scrollable piece list offers each piece's thumbnail/name/depth and a large selection row; it provides **Select all scenery** and **Clear selection**. Box selection may add Scenery pieces under the same filter. **Select all scenery** means every wall panel plus every Far, Near, and On the floor Scenery artwork placement in the open scene. Sky, Ground, characters, and interactive Props are never included. There is no automatic starter-wide group or persistent lock between pieces.
- With two or more pieces selected, show a halo-backed union outline, a separate outline on each member, and the text **“N scenery pieces selected”**. The selected-object card uses the shared `StageInspectorPanel` shell with **Move together**, **Resize together**, **Clear selection**, and a short explanation **“Ground and Props stay put.”** It does not show a single shared depth, finish, or color control, because mixed members may have different settings. The group outline and count appear on the canvas, not only in the card. Closing the card follows the normal compact Settings reopen behavior and leaves the selection visible.
- **Move together** starts a live preview. Dragging the group or using large on-screen arrow buttons applies the same world-space offset to every selected Scenery piece, preserving their relative spacing and Far/Near/On the floor choices. The original outlines remain faint/dashed while the candidate positions have bright outlines. Snap, if enabled, moves the selection as a unit from its anchor without rounding members independently. A cursor pill says **“Move N scenery pieces”**.
- **Resize together** starts a live uniform-size preview. Drag a group corner inward/outward; the opposite corner shows a fixed pin and does not move. Every selected artwork piece changes size and position by the same positive scale around that anchor, retaining its proportions and relative layout. Wall panel rectangles change size and position with the group, while Brick/Wood motifs retain their world-scale size and alignment instead of stretching. Far/Near/On the floor choices remain unchanged. Clamp or reject a preview that would collapse or flip any piece; show **“Too small”** with a blocked outline. The cursor pill says **“Resize N scenery pieces”** and identifies the fixed corner with a visible pin.
- During either group preview, the on-canvas capture bar shows **Keep changes** and **✕ Cancel**, the selection count, and **“Ground and Props stay put.”** Releasing a pointer leaves the preview pending; it does not save a change or newly mark the scene dirty. Keep changes commits the whole preview as one edit and marks the scene dirty once. Cancel, right-click, or Esc restores the exact pre-preview positions and sizes and returns to the same multi-selection in Edit Scenery. A later mouse/touch release cannot commit a canceled preview. **Done editing** or switching modes while a preview is pending first cancels it, then exits Edit Scenery. A Save request while a preview is pending asks the child to Keep changes or Cancel before saving; it never silently saves the draft.
- The group tools are operable without precise dragging: touch targets and corner hit areas are at least 44 × 44 logical pixels; large move arrows and **Smaller/Bigger** buttons give touch and keyboard users repeatable steps. Pointer users may drag any corner and see its opposite fixed pin; the button path starts with the top-left corner fixed and labels that choice. Every control is focusable, has a visible focus ring and text label, and responds to Enter/Space. On a narrow viewport, the multi-selection card scrolls beneath its fixed Close, the capture bar stacks its actions, and arrow/size buttons remain reachable even when a group corner lies offscreen. The mode pill and Stage preview remain unobstructed.
- A committed group transform saves each member's new position and size through the normal scene save; depth, finish, physics, and interaction roles do not change. The temporary multi-selection, preview, and group outline do not persist on reopen. A whole-set transform that also moves/scales Sky, Ground, characters, or interactive Props is outside Phase 19 and belongs to a separately designed future feature.
Single-artwork direct resizing and multi-piece group resizing intentionally have different commit gestures: one artwork placement commits on handle release, while a group retains the existing snapshot preview with **Keep changes** / **✕ Cancel**. Do not add a single-piece confirmation bar as a substitute for deferred Stage Undo.
### Stage-relative authoring size in Prop Editor
- In Arrange, replace the fixed-target **Normalize Size** action with **Size for Stage**. It opens a reversible sizing view with **Small**, **Room**, and **Big** starting sizes and fine **Make smaller** / **Make bigger** adjustments. The presets are rough visual starting points relative to the standard standing stickman, not rigid limits; preserve the asset's proportions and let the child inspect the actual comparison before keeping it. Exact preset ratios are an architecture/implementation detail, not new UI copy.
- Frame the complete artwork and guide together when the sizing view opens. Show a standing stickman at the standard 1:1 reference scale **in front of the artwork**, plus a clear floor/contact line near ankle level. Use a dark halo with a light core so the guide remains visible over any fill. The guide is editor-only and never becomes part of the saved artwork. The guide is visible whenever **Show Stickman Guide** is on and is forced on while **Size for Stage** is active.
- **Keep size** applies the preview to the authored shapes and returns to Arrange; **✕ Cancel** restores the exact pre-preview shape geometry. Existing whole-prop Transform behavior remains the way to move/rotate/resize all shapes and editable anchors together. Keep this sizing action within the established Prop Editor Arrange toolbar and `PropTheme`; do not reuse the Stage inspector card for a distinct editor workflow.
- Artist-created and newly authored vector artwork should already be drawn at its intended Stage size, so first placement is normally scale 1 relative to the authored artwork. The standard figure is about 940 world units tall. Prop names should guide an ordinary-object size estimate; the asset review records approximate intended bounds relative to the stickman (height, and width when useful) and includes a scale-check preview. The sizing view is a correction/authoring aid, not a required step for every asset.
- Loading existing `.prop` files and scenes performs no automatic normalization and retains saved placement transforms. **Keep size** deliberately changes the authored geometry; saving over the same `.prop` uses the existing live-reference behavior, so scenes using that drawing show its revised artwork on reopen. Keep the current Save and Save As workflow; this addition introduces no extra copy/version prompt or overwrite confirmation.
## Ground finish workflow
- The existing **Ground** palette entry gains a **New Ground look** chooser with large picture swatches: **Paint**, **Brick**, **Wood**, **Grass**, **Dirt**, **Sidewalk**, **Asphalt**. Paint, Brick, and Wood use the same names and comparable preview motif as wall finishes. A chosen swatch becomes the active look for future Ground only. The chooser states **“New Ground will look like this”** and never silently repaints existing pieces.
- Returning to the Stage shows a patterned Ground ghost for one tap or the existing drag-paint run. A small cursor pill says **“Click or drag to add Ground”** and the capture bar provides Done and Cancel under the exit rules above. Valid/blocked stroke previews use outline, symbols, and short words as well as color. Adjacent ghost pieces show a continuous-looking pattern. Ramp and Step keep their existing appearance and palette behavior.
- Selecting an existing Ground piece in ordinary EDIT opens a Ground `StageInspectorPanel` card with **This Ground's look**, the same finish swatches in a `StagePanelChoiceGroup`, and any existing Ground controls. Selecting a finish here changes only the selected piece. The different headings **New Ground look** and **This Ground's look** make scope visible before a tap.
- In PLAY, Ground keeps its walkable behavior and chosen look. Wall panels, road markings, and rugs never look like a selectable walking surface in the UI.
## Prop Editor route for Scenery artwork
- **Make artwork…** in the Scenery chooser uses the existing vector `.prop` authoring tool; there is no separate Scenery Editor or file type. When entered from Scenery, its first screen is framed as **“Make artwork for your scene”** and foregrounds vector drawing, color, shape arrangement, naming, and Save. Hide prop-only material, mass, bounce, collision, Use/attachment, and other action controls from this entry route. Do not require a pretend solid shape to save visual-only artwork.
- The Props entry route retains prop-relevant controls and its distinct **“Make a Prop”** framing. If a drawing is valid for both roles, the child may find it in both browsers; the Stage placement entry point decides what the copy does. After a successful artwork save, offer **“Place it as Scenery”** and return to the Artwork chooser focused on the saved drawing. Cancel/exit returns without changing the open Stage.
- Tag editing uses the same large multi-select choices **Nature**, **Buildings**, **Walls**, **Roads & paths**, and **Objects** in artwork authoring. The words are descriptive, optional, and never gate the All artwork view. An untagged saved asset remains discoverable under All artwork and conditional Unsorted.
The **Size for Stage** workflow above applies to both Scenery artwork and Props because they share the same `.prop` authoring canvas. It does not change the Stage role distinction: a Scenery placement remains inert and non-colliding, while a Prop retains its existing interaction and physics settings.
## Theme, components, and accessibility contract
- For selected wall, artwork, and Ground cards, reuse `StageInspectorPanel` for the fixed header, safe rect, body scroll, Close, focus, and reopened Settings button pattern. Reuse `StagePanelChoiceGroup` for exclusive depth/finish choices with stable labels and decorative preview cues. `StagePanelToggleRow` and `StagePanelSwitchVisual` remain available for true on/off settings but are not forced into this feature's depth or finish choices.
- The multi-selection card also reuses `StageInspectorPanel` and `stage_panels.action` for its large Move/Resize/Clear actions; its member list and preview controls add tokens under `stage_panels` rather than hardcoded colors or sizes. The group capture bar extends the same transient Stage capture pattern used for placement, keeping its instruction and Cancel together below the mode pill.
- Extend the `stage_panels` `SandboxTheme` tokens only where these cards need new preview-cue size, card spacing, or scenery-specific accent/selection outlines. Continue using `stage_panels.shell`, `.type`, `.choice`, and `.action` for geometry and typography, and `mode_colors.edit` for the EDIT accent. The Scenery chooser, starter modal, color dialog, and Ground chooser use popup/theme tokens and `SandboxTheme.control_theme()`; their layout is distinct from an in-stage inspector. Generalize the existing Stage capture-bar look for wall/artwork/Ground placement instead of copying fixed styling.
- On narrow or short viewports, chooser contents scroll, filter chips wrap, card columns collapse, the inspector header/Close remain fixed, and the capture bar wraps/stacks within safe margins. Never let a panel cover the EDIT/DIRECT/PLAY mode pill. Decorative thumbnails and cue drawings ignore pointer input; each actual choice is one native focusable Button with Space/Enter activation. Focus moves into an opened chooser/card and returns to the opener on Close when possible.
- Pointer, touch, and keyboard all reveal the selected state, hover where applicable, focus, disabled reasons, and confirmation. No instructional step exists only in a top header, bottom status bar, hover tooltip, or shortcut. Cursor pills remain near the pointer without covering it; they flip to stay in view. Touch users get the same instruction anchored near the draft plus always-visible Done/Cancel.
### Reuse and standardization audit
This matrix identifies current UI that already exists. “Extend/share” is a design requirement to converge repeated controls; it does not prescribe a class, signal, or file contract for the architect.
| Phase 19 surface | Existing source to use | Reuse decision and boundary |
| --- | --- | --- |
| **File → New** and unsaved-work gate | `StageChrome` File menu; `StageModalViews` unsaved `ConfirmationDialog`; Stage document flow | **Direct reuse.** Preserve Save/Discard/Cancel, focus, and the current scene until a starter is chosen. Do not make a second unsaved-work dialog. |
| Illustrated starter chooser | `AssetCard`, `AssetSelector` scene/shell, `StageModalViews` dialog theming and Rename Scene name entry | **Extend/share the card-gallery presentation.** The three starter cards need a responsive grid, scroll, Close/Cancel, and selected focus behavior also needed by Scenery/Sky/finishes. The editable suggested name follows existing name-entry styling/validation; starter composition remains feature-local. This is a modal choice, not `StageInspectorPanel`. |
| Scenery chooser and Artwork cards | `AssetSelector`, `AssetCard`, `StageAssetBrowser`, thumbnail cache/renderers, `scenes/stage/asset_selector.tscn` | **Extend/share the gallery shell and card behavior, reuse browsing/thumbnail work.** The current selector is a fixed four-column paged popup and sets card focus to `FOCUS_NONE`; these must be corrected for responsive layout and keyboard use in the common path rather than copied into a Scenery-only popup. Backgrounds/Artwork tabs use native button choice states and shared popup styling; placement-role text stays feature-local. |
| Sky and wall/Ground finish cards | `AssetCard` and the responsive gallery behavior above; `SandboxTheme` popup/card tokens | **Share the same preview-card and focus/selected-state rules.** Sky and finishes supply their own preview imagery and one-tap selection meaning; card dragging is disabled for these choices. They do not pretend to be `.prop` assets or reuse `StageAssetBrowser` asset scanning. Wall and Ground share names/preview motifs for Paint, Brick, and Wood. |
| Artwork tags, Props filters, authoring tags | No existing reusable tag-chip control; `StagePanelChoiceGroup` is a vertical exclusive inspector control; Stage speech UI demonstrates `HFlowContainer` wrapping | **Extract one shared wrapping filter/choice treatment.** Use native focusable buttons and one theme contract across Scenery browsing, Props browsing, and optional multi-tag authoring. Single-choice browser filters and multi-choice authoring use the same visual states but different selection rules. Do not clone local chip styling or force the vertical inspector group into a wrapping browser row. |
| Wall Color and More colors dialog | `ColorSwatchSelector`; native `ColorPickerButton` already used by the Prop Editor; `StageModalViews` dialog theme | **Extend/share the color-swatch treatment** so it wraps on narrow screens, has visible chosen/focus states, and uses Stage tokens. Use native color picker behavior in a themed Stage dialog. The Prop Editor's `PropTheme` styling is not a Stage style source; wall/color values remain feature-local. |
| Scenery overlap **Pieces here** | `StageModalViews.show_objects_here()` and its themed, viewport-clamped, scrollable `PopupPanel` | **Generalize the existing overlap chooser.** Supply Scenery-only candidates and child-facing piece names/depth, while retaining the same placement, 44 px rows, dismiss, and focus behavior. Do not create a second overlapping-items popup. |
| Missing Scenery artwork | Existing `MissingPropPlaceholder`/`MissingStickmanPlaceholder` preserve missing placements; `StageModalViews` missing-assets dialog; `StageInspectorPanel`; `StageAssetBrowser` gallery/file chooser | **Reuse and generalize the recovery path.** Keep saved placement continuity and one themed load summary, but give Scenery its neutral labeled marker, selection-card repair actions, and the existing artwork/file choosers. Do not copy the older red marker styling or create a second asset gallery or dialog skin. |
| **Erase this piece?** | `StageModalViews` confirmation/dialog theming; existing Erase Use spot `ConfirmationDialog` and safe/danger treatment in `SandboxStage` | **Reuse the confirmation pattern.** Share the button sizing, themed window, and danger/safe styles instead of copying the spot-erase styling into another private function. The affected piece and wording are local. |
| Placement and group-preview bar | `StageUseTargetCaptureBar` and the transient capture-bar contract in `stage_panel_ui_system.md` | **Generalize/extract the capture-bar shell** for EDIT prompts, mode accent, Done/Keep changes/Cancel, responsive stacking, and a safe position below the mode pill. Keep Use-target-specific wording/actions in its caller; do not build a second bar with copied colors/geometry. |
| Cursor pill and Stage ghosts | `StageChrome` action tooltip/cursor, current placement ghost/terrain overlay, `SandboxTheme.overlays` | **Extend/share presentation and safe cursor positioning** while the wall/artwork/Ground draft visuals remain feature-local. Keep one visual language for valid/blocked, mouse/touch feedback, and cancellation; do not copy the current tooltip's hardcoded colors into Phase 19. |
| Selected artwork, wall, Ground, and multi-selection card | `StageInspectorPanel`, `StagePanelChoiceGroup`, `StagePropSettingsView` dismissal/reopen behavior, `stage_panels` tokens | **Direct reuse of inspector shell and exclusive choice group; extend shared action styling only as needed.** These are in-stage selected-object cards. Their data, headings, member list, and commands stay feature-local. `StagePanelToggleRow`/switch are reserved for real on/off values and are not substitutes for depth or finish choices. |
| **Erase wall area** tool | Selected wall's `StageInspectorPanel` or the no-selection Edit Scenery `StageUseTargetCaptureBar`; wall ghost/placement logic in `StageSceneryController` | **Reuse the selected-object action styling and existing transient capture bar.** Only the cutout rectangle preview and rectangle subtraction are feature-specific; do not add another toolbar or dialog. |
| Select-several member list | `StageInspectorPanel` body/action styling; `StageModalViews` large selectable-row precedent | **Reuse row sizing, focus, selected, and scroll rules.** Membership logic and piece labels remain local to Edit Scenery; do not invent another inspector shell or tiny checkbox list. |
| **Make artwork…** and return | Existing Prop Editor scene/controller and `.prop` authoring; `StageAssetBrowser` refresh/thumbnail path | **Reuse the existing editor and asset browser.** The Scenery entry changes the visible authoring framing and hides prop-only controls; it does not create another editor, file picker, art card system, or `.scenery` format. The return-to-Stage handoff is feature-local. |
| Stage-relative size in Prop Editor | Existing Prop Editor Arrange toolbar, whole-prop transform, Normalize Size path, stickman guide, and `PropTheme` | **Extend the existing authoring workflow.** Replace the fixed 96-unit normalization target with the reversible Size for Stage preview and front-drawn guide; keep the authoring figure/editor chrome local to Prop Editor. Do not adopt `StageInspectorPanel` for this editor-only interaction or change existing saved assets automatically. |
For every row, new dialogs/popups explicitly receive `SandboxTheme.control_theme()` and the established Stage dialog style, with their visual values read from `SandboxTheme`/`sandbox_theme.json` (`popups`, `palette`, `stage_panels`, `chrome.dialogs`, `mode_colors`, and `overlays` as appropriate). The existing `StageModalViews.apply_dialog_theme()` handles font/child overrides but is not, by itself, a substitute for assigning the control theme to a new window. Reuse local stylebox instances rather than mutating the cached shared `Theme`. Keep popup gallery behavior separate from the in-stage inspector layout while making their type, spacing, focus, and selected states consistent.
The existing `QueuePanel`, `RulePanel`, `ActionEditor`, and speech popups in `scenes/editors/stage/` solve different editing tasks. They can inform theme and responsive-container choices, but are not replacement shells for a starter or art gallery. Any new Stage popup should pass the same theme, sizing, focus, and dismissal checks as those existing workflows.
## Exact child-facing copy map
| Context | Text |
| --- | --- |
| Palette entry | **Scenery**; helper **Stays put** |
| Props distinction | **Props**; helper **Can be part of the action** |
| Ground distinction | **Ground**; helper **Stickmen walk here** |
| Scenery tabs | **Backgrounds**, **Artwork** |
| Edit action/state | **Edit placed scenery**, **Edit Scenery**, **Done editing** |
| Multi-selection | **Select several**, **Select all scenery**, **Clear selection**, **N scenery pieces selected** |
| Group action/preview | **Move together**, **Resize together**, **Ground and Props stay put**, **Keep changes**, **✕ Cancel** |
| Group adjustment | **Smaller**, **Bigger**, **Too small** |
| Wall category/helper | **Wall panel**; **Looks like a wall. Stickmen can pass through.** |
| Wall cursor/draft | **Drag to draw wall**; **Make the wall bigger** |
| Artwork cursor | **Click to place [name]**; after first copy **Place another [name]**; during direct resize **Resize [name]** |
| Placement actions | **Done**, **✕ Cancel**; after a commit **Placed pieces stay** |
| Ground palette/inspector | **New Ground look**, **This Ground's look** |
| Overlap chooser | **Pieces here** |
| Missing artwork | **Artwork not found**, **Find artwork**, **Needs artwork**, **Show scenery**, **Later** |
| Missing artwork repair | **Find artwork…**, **Choose another artwork**, **Keep for now**, **Use this artwork here?**, **Use artwork**, **File details** |
| Repair failure | **Could not open that artwork. Try another file.** |
| Erase safeguard | **Erase this piece?**, **Keep it**, **Erase** |
| Starter cards | **Blank Scene**, **Town Street**, **Living Room** |
| Stage-relative size | **Size for Stage**, **Small**, **Room**, **Big**, **Make smaller**, **Make bigger**, **Keep size**, **✕ Cancel**, **Show Stickman Guide** |
| Single Scenery resize | **Smaller**, **Bigger**; a handle drag commits on release |
| Wall opening tool | **Erase wall area**, **Drag over the wall · release to erase**; **Erase another area · earlier openings stay** |
Do not expose “entity”, “collision hull”, “z-index”, “physics layer”, “finish ID”, or “depth tier” as child-facing copy.
## Observable UI review scenarios
1. Create Town Street from File → New, cancel at the starter chooser once, then choose it. The old scene persists on cancel; the starter appears as an editable unsaved scene with its sky, Ground, and separate set pieces.
2. Choose Tree under Scenery → Artwork → Nature, change Far/Near/On the floor, place two copies, and right-click. Both copies remain, the passive ghost disappears, and ordinary EDIT selection returns. Repeat with an unfinished wall drag: right-click discards only that draft.
3. In ordinary EDIT, click and box-select a character and Prop in front of a room wall without selecting the wall. Enter Edit Scenery, select a window cutout, then choose the broad wall through its exposed area or Pieces here. Start a direct resize and cancel the still-active gesture before release; its old bounds return. Release a second resize and verify it commits immediately, with no Keep prompt.
4. Change Sky through all four cards while the Living Room window remains visible. The room colors stay unchanged; Close leaves the last chosen Sky, and keyboard focus returns to Scenery.
5. Select Wood for New Ground, paint a run, then select one existing Ground piece and change only that piece to Brick through **This Ground's look**. A selected wall panel's own Finish/Color changes only that panel.
6. At a narrow viewport and with touch/keyboard input, reach every chooser card, depth choice, Cancel, Done, inspector Close, and overlapping-piece choice without clipping or relying on hover. Enter PLAY and confirm all ghosts, handles, and edit-only bars disappear.
7. In Living Room, use Select several to choose two wall panels, the window frame, and rug. Preview a shared move, cancel with right-click, and verify all four return exactly to their original locations while Wood Ground and furniture remain still. Preview a uniform corner resize, check the opposite anchor stays fixed and wall motifs do not stretch, then Keep changes; save and reopen. The four pieces retain their new positions/sizes and original depth/finish, while the temporary group selection is gone. Repeat the preview with touch or keyboard controls in a narrow viewport, using Esc to cancel.
8. Save a scene with two copies of one live `.prop` Scenery drawing, then make the file unavailable and reopen the scene. The friendly missing-art summary appears once; both labeled markers remain at their saved positions/depths through EDIT and PLAY. In ordinary EDIT, select a character in front of a marker without selecting Scenery. Use a marker's Find artwork badge or the Edit Scenery **Needs artwork** list to open its card. Cancel a file choice and verify both markers remain. Choose a replacement for one copy, preview then cancel once, then confirm it; only that copy changes. Confirm the second still has a marker after save/reopen until it too is repaired or deliberately erased. Repeat the repair path with keyboard and on a narrow touch viewport.
9. In Prop Editor, open **Size for Stage** on representative tall, wide, and small artwork. Confirm the standard stickman is drawn in front, the floor/contact guide is visible over every fill, the preview frames both together, presets and fine adjustments preserve proportions, **Keep size** applies, and **✕ Cancel** restores the exact prior geometry. Confirm new placement uses the authored size and loading an existing file performs no automatic normalization. Deliberately save revised geometry over a referenced asset and confirm the usual Save flow and live-reference update on reopen, with scene placement transforms retained. In Edit Scenery, resize one placement and confirm it commits on release and saves/reopens; cancel a still-active drag with Escape/right-click and with the touch capture bar; verify other scenery and Props stay unchanged. Compare this with the separate group Keep/Cancel flow. Repeat sizing-guide visibility and Stage handle/button access with keyboard/touch and at a narrow viewport.
@@ -0,0 +1,82 @@
# Phase 19 addition — selected Scenery artwork depth
> **Status:** Implemented and approved; focused automated and render-capable checks pass. Full Phase 19 user/editor verification remains open. **Scope:** Edit Scenery's in-stage card for exactly one selected `StageSceneryArtwork`, including a missing-artwork marker. This is an addition to Phase 19. It does not change placement, wall panels, physical Props, or the scene schema.
## Child-facing behavior
After placing artwork, a child enters **Edit Scenery** and selects one drawing. The existing card keeps its artwork icon, name, fixed **Close** button, and **Smaller** / **Bigger** controls. Above the size controls, it shows **Where it appears** with three large, full-width choices:
1. **Far**
2. **Near**
3. **On the floor**
The selected choice matches that particular drawing's current layer. A short note below the choices says: **“Far is behind Near. On the floor is in front of the ground.”** Pressing another choice updates the Stage immediately and keeps the same card open. It changes only that selected placement; other copies of the same artwork keep their own layers. Repeatedly pressing the current choice has no effect on the document. The child's placement cursor and its Far/Near/On the floor capture-bar choices keep their current behavior.
A missing-artwork marker has the same depth section, using its saved depth. Changing it moves the marker's draw layer and is retained if the picture is repaired. A wall panel has no depth section. No selection or several selected scenery pieces has no single-artwork depth section or active depth command. Existing repair actions and the missing-artwork explanation remain visible where applicable.
The three buttons use the shared Stage inspector's selected, hover, focus, and disabled visual states; their words never gain checkmarks. Each native button meets the shared 44-pixel minimum hit height and responds to click, touch, Enter, and Space. On a short viewport, the body scrolls while the header and Close remain visible. A depth change preserves the body's scroll position. The card stays within the Stage's safe rectangle and does not cover the mode pill.
## Existing contracts and target changes
| Class / file | Typed contract | Change |
| --- | --- | --- |
| `StageInspectorPanel extends PanelContainer`, `scripts/shared/stage_ui/stage_inspector_panel.gd` | `func body() -> VBoxContainer`; `func set_header(title: String, icon_text: String = "") -> void`; `func set_safe_rect(rect: Rect2) -> void`; `func request_fit() -> void`; `func reset_scroll() -> void` | Reuse the present scenery card shell, its fixed header/Close, fit, and scroll. No new component API. |
| `StagePanelChoiceGroup extends VBoxContainer`, `scripts/shared/stage_ui/stage_panel_choice_group.gd` | `signal choice_selected(id: StringName)`; `func add_choice(id: StringName, caption: String, cue: Control = null) -> Button`; `func select_no_signal(id: StringName) -> void`; `func apply_theme(stage_theme: SandboxTheme) -> void` | Instantiate one artwork depth group with IDs `&"far"`, `&"near"`, `&"floor"`. The caller owns the mapping to child-facing captions and the domain mutation. No new component API. |
| `SandboxStage extends Node2D`, `scripts/editors/stage/sandbox_stage.gd` | Existing `_build_scenery_inspector()`, `_refresh_scenery_inspector()`, `_mark_scene_dirty()`; add a feature-local handler such as `func _on_selected_scenery_depth_choice(id: StringName) -> void` | Own the section, selection guard, refresh, and dirty call. Keep it in the existing scenery inspector body before `_scenery_size_row`. |
| `StageSceneryArtwork extends Node2D`, `scripts/stage/scenery/stage_scenery_artwork.gd` | `var depth: StringName`; `func set_depth(value: StringName) -> void` | Existing setter remains authoritative. It applies `StageDrawOrder.scenery_z(depth)` immediately to live art and missing markers. No new API. |
| `StageSceneAdapter extends RefCounted`, `scripts/editors/stage/stage_scene_adapter.gd` | Existing Scenery artwork entry's `depth: String` | Existing snapshot/load round trip persists the changed layer. No version, migration, or payload change. |
The section is feature-local composition inside the shared shell:
```text
StageInspectorPanel (existing scenery card)
├─ fixed header: artwork name or “Artwork not found” + Close
└─ scrolling body
├─ missing-artwork explanation (only when missing)
├─ Where it appears (only for one selected artwork)
│ ├─ StagePanelChoiceGroup
│ │ ├─ Far [id: far]
│ │ ├─ Near [id: near]
│ │ └─ On the floor [id: floor]
│ └─ helper: Far is behind Near. On the floor is in front of the ground.
├─ Smaller / Bigger (existing availability rules)
└─ existing wall-erase or missing-artwork actions, as applicable
```
Style the heading and helper with the existing `SandboxTheme` `stage_panels.type.section_size`, `stage_panels.type.helper_size`, and text/muted colors. Apply the theme to the choice group so it uses `stage_panels.choice` sizing and states and `mode_colors.edit` accent. `StagePanelToggleRow` is for booleans and does not represent this three-way choice. No parallel button styling, theme token, popup, or dialog is required. This follows the approved [Stage panel UI system](stage_panel_ui_system.md) and [Phase 19 UI/UX specification](phase_19_scenery_backgrounds_ui_ux.md).
## Signal and document flow
```mermaid
flowchart LR
Press[Far / Near / On the floor pressed] --> Choice[StagePanelChoiceGroup.choice_selected: StringName]
Choice --> Guard[SandboxStage validates Edit Scenery and exactly one selected artwork]
Guard --> Same{New depth?}
Same -- No --> Sync[Refresh selected button without signal]
Same -- Yes --> Setter[StageSceneryArtwork.set_depth]
Setter --> Draw[StageDrawOrder z-index changes now]
Setter --> Dirty[SandboxStage._mark_scene_dirty]
Dirty --> Sync
Dirty --> Save[Normal .scene save snapshot stores depth]
```
The handler re-reads the current selection when the signal arrives; it must not retain a previously selected artwork reference in a button callback. It returns without mutation unless Stage is in Edit Scenery, the selection has exactly one valid `StageSceneryArtwork`, and the ID belongs to `far/near/floor`. It also returns without marking dirty when the requested ID already equals `artwork.depth`. For a real change, it calls `artwork.set_depth(id)` once, calls `_mark_scene_dirty()` once, and refreshes the card. Refresh uses `select_no_signal(artwork.depth)`, avoiding a second handler call. It does not call `reset_scroll()` for this value change or close/recreate the card; normal fitting can be requested without moving the child's reading position.
For example, a selected tree with `depth == &"near"` receives `&"far"` from the button. Its `z_index` changes from `StageDrawOrder.NEAR` (`-20`) to `StageDrawOrder.FAR` (`-30`), its position and scale stay the same, and the Stage becomes dirty. Saving writes `"depth": "far"` in that tree's scenery artwork entry. Reopening selects Far for that tree; another tree placement can still be Near. A missing-art marker follows the same layer path and keeps its saved `asset_id`, footprint, and repair state.
## Acceptance and verification
1. Place two overlapping copies of a drawing, enter Edit Scenery, select one, and change Near to Far and then On the floor. The selected button and visible overlap order change immediately; the other copy keeps its former layer and neither copy moves or resizes.
2. With one drawing selected, press its already selected layer. The document's dirty state does not change and no redundant depth mutation is applied. Change to another layer; the document becomes dirty, the card stays open, and its scroll position remains stable.
3. Save the changed scene and reopen it. The same placement retains the new layer and its selected-artwork card highlights the saved choice. No scene-version migration or new field is needed.
4. Select a missing-art marker with known or unknown bounds. Its saved layer is highlighted and can be changed before repair; the marker visibly follows the chosen layer, and a later successful repair keeps that latest layer. Existing Find/Choose/Keep actions and size availability remain intact.
5. Select a wall panel, clear selection, and select multiple Scenery pieces. None offers a single-artwork depth choice. The wall's existing controls and group selection behavior remain available.
6. On a short or narrow viewport and by keyboard or touch, reach all three depth choices and Close. The fixed header remains accessible; selected/focus styles are distinct, and the Stage mode pill remains clear.
## Architectural decisions and limits
- **Reuse the existing inspector and choice group.** The UI is one exclusive three-way setting inside a card that already exists, so an additional panel class, signal bus, generic inspector schema, or `Resource` model adds indirection without simplifying this change.
- **Keep the Stage controller responsible for mutations.** The shared UI emits a `StringName` intent. `SandboxStage` verifies the live selection, changes one `StageSceneryArtwork`, and uses its existing dirty path; the UI control knows nothing about scene persistence.
- **Keep per-placement depth as the source of truth.** The `.scene` adapter already saves this field and `set_depth()` already maps it to Godot draw order. Missing-art recovery preserves the placement's depth. The placement tool's default and capture-bar setting remain separate from editing an existing piece.
- **Keep Undo in its own phase.** Each depth press is an immediate committed Stage edit. A future Stage-wide Undo command can capture old/new depth alongside other scenery actions under the existing Phase 19 debt item.