From c1758efd3b223f4b313cb1e5cc2f3c9796fa9f40 Mon Sep 17 00:00:00 2001 From: Ryan Kegel Date: Wed, 21 Oct 2026 11:06:57 -0400 Subject: [PATCH] Add tests for scenery surface caching and preview behavior - Introduced `test_scenery_brick_preview.gd` to validate focused placement and CPU draw-cost for large brick walls. - Added `test_scenery_surface_cache.gd` to assess texture budget, visual equivalence, and cache invalidation. - Created `test_scenery_surface_preview.gd` to evaluate adaptive preview selection and placement behavior. - Implemented various helper functions to measure drawing performance and validate expected behaviors during tests. - Added unique identifiers for new test files to ensure proper tracking and management. --- .codex/agents/uiux.toml | 23 +- .codex/skills/spec-pipeline/SKILL.md | 3 +- .gitignore | 2 + AGENTS.md | 19 + content/stages/artifacts.scene | 372 +++++++ content/stages/distance.scene | 140 +++ content/stages/fps.scene | 417 ++++++++ .../background_rendering_guide.md | 87 ++ .../diagrams/background_erase.svg | 43 + .../diagrams/background_mipmaps.svg | 39 + .../diagrams/background_pipeline.svg | 51 + .../diagrams/background_reuse.svg | 44 + docs/architecture/scenery_surface_caching.md | 42 + .../tech_debt_and_optimizations.md | 4 + .../choose_scenery_artwork_option_a.png | Bin 0 -> 123373 bytes .../choose_scenery_artwork_option_b.png | Bin 0 -> 125844 bytes .../choose_scenery_artwork_option_c.png | Bin 0 -> 116989 bytes docs/specs/dialog_design_principles.md | 32 + .../phase_19_scenery_backgrounds_ui_ux.md | 6 +- docs/specs/scenery_bitmap_tile_cache.md | 78 ++ project.godot | 1 + sandbox_theme.json | 54 +- scenes/stage/asset_selector.tscn | 49 +- scripts/editors/stage/sandbox_stage.gd | 601 +++++++++-- scripts/editors/stage/stage_asset_browser.gd | 2 +- scripts/editors/stage/stage_scene_adapter.gd | 4 + .../editors/stage/stage_scenery_controller.gd | 285 +++++- .../stage/stage_use_target_capture_bar.gd | 517 +++++++++- .../stage_ui/stage_depth_choice_preview.gd | 97 ++ .../stage_depth_choice_preview.gd.uid | 1 + .../shared/stage_ui/stage_inspector_panel.gd | 49 +- .../stage_ui/stage_panel_choice_group.gd | 93 +- .../stage_ui/stage_surface_color_controls.gd | 132 ++- .../stage_ui/stage_surface_color_dialog.gd | 290 ++++-- .../stage_surface_color_picker_view.gd | 130 +++ .../stage_surface_color_picker_view.gd.uid | 1 + scripts/shared/ui/asset_selector.gd | 748 +++++++++++--- scripts/shared/ui/color_swatch_selector.gd | 16 +- scripts/stage/sandbox_theme.gd | 36 +- .../stage/scenery/stage_background_ground.gd | 10 +- scripts/stage/scenery/stage_rect_surface.gd | 183 +++- scripts/stage/scenery/stage_surface_cache.gd | 929 ++++++++++++++++++ .../stage/scenery/stage_surface_cache.gd.uid | 1 + .../stage/scenery/stage_surface_preview.gd | 55 +- .../stage/scenery/stage_surface_renderer.gd | 241 +++-- scripts/stage/scenery/stage_wall_panel.gd | 3 +- scripts/stage/stage_scene_io.gd | 2 + tests/test_phase19_surface_followup.gd | 2 +- tests/test_scenery_brick_preview.gd | 97 ++ tests/test_scenery_brick_preview.gd.uid | 1 + tests/test_scenery_surface_cache.gd | 284 ++++++ tests/test_scenery_surface_cache.gd.uid | 1 + tests/test_scenery_surface_preview.gd | 149 +++ tests/test_scenery_surface_preview.gd.uid | 1 + 54 files changed, 5941 insertions(+), 526 deletions(-) create mode 100644 content/stages/artifacts.scene create mode 100644 content/stages/distance.scene create mode 100644 content/stages/fps.scene create mode 100644 docs/architecture/background_rendering_guide.md create mode 100644 docs/architecture/diagrams/background_erase.svg create mode 100644 docs/architecture/diagrams/background_mipmaps.svg create mode 100644 docs/architecture/diagrams/background_pipeline.svg create mode 100644 docs/architecture/diagrams/background_reuse.svg create mode 100644 docs/architecture/scenery_surface_caching.md create mode 100644 docs/review/choose_scenery_artwork_option_a.png create mode 100644 docs/review/choose_scenery_artwork_option_b.png create mode 100644 docs/review/choose_scenery_artwork_option_c.png create mode 100644 docs/specs/dialog_design_principles.md create mode 100644 docs/specs/scenery_bitmap_tile_cache.md create mode 100644 scripts/shared/stage_ui/stage_depth_choice_preview.gd create mode 100644 scripts/shared/stage_ui/stage_depth_choice_preview.gd.uid create mode 100644 scripts/shared/stage_ui/stage_surface_color_picker_view.gd create mode 100644 scripts/shared/stage_ui/stage_surface_color_picker_view.gd.uid create mode 100644 scripts/stage/scenery/stage_surface_cache.gd create mode 100644 scripts/stage/scenery/stage_surface_cache.gd.uid create mode 100644 tests/test_scenery_brick_preview.gd create mode 100644 tests/test_scenery_brick_preview.gd.uid create mode 100644 tests/test_scenery_surface_cache.gd create mode 100644 tests/test_scenery_surface_cache.gd.uid create mode 100644 tests/test_scenery_surface_preview.gd create mode 100644 tests/test_scenery_surface_preview.gd.uid diff --git a/.codex/agents/uiux.toml b/.codex/agents/uiux.toml index c3e3c9e..dfa0292 100644 --- a/.codex/agents/uiux.toml +++ b/.codex/agents/uiux.toml @@ -15,6 +15,19 @@ developer_instructions = """ for dialogs or other editor workflows, reuse its visual principles only when their layout and interaction model fit, and call out any exception. +* **Dialogs & Popups:** Before designing a child-facing dialog, read + `docs/specs/dialog_design_principles.md`. Carry its rounded controls, thicker borders, blue + primary actions with bold white labels, comfortable button spacing, and large readable type + into the proposed UI. Prefer visible, labeled dismissal actions such as Cancel, Done, or Back; + do not default to a small corner × as the only way to close a dialog. Use the Choose Scenery + dialog and its theme tokens as the current implementation reference. + +* **Stable Dialog Anchoring:** When tabs, filters, or other in-place choices change a dialog's + size, keep its current anchor so the clicked control stays under the pointer. Grow from a + stable edge while space allows; when the safe viewport is reached, cap the dialog and scroll + its content. Nudge it only as much as needed to keep essential controls visible. Do not + recenter after every content change. + * **Touch & Click Target Rules:** * Minimum pointer hit-target: **44×44 logical pixels** (never build tiny 16px/24px buttons). * Control heights (buttons, dropdowns, rows) must obey `button_min_height` tokens from the theme. @@ -48,15 +61,17 @@ developer_instructions = """ * **Accessible Inputs:** * Prefer simple explicit buttons (e.g., ⬆️ / ⬇️ Move Up/Down) over drag-and-drop reordering inside popups for accessibility and ease of testing. - * Always provide prominent, clickable `[X Cancel]` buttons on active workflows so users don't need to know the `Esc` key. + * Provide a prominent, clickable, visibly labeled action such as **Cancel** on active workflows so users do not need to know the `Esc` key. Do not rely on a corner × as the sole dismissal control. ## 3. Pipeline Output Protocol When invoked during Phase 0a (UI/UX Proposal & Alternatives): 1. Analyze the initial proposal for usability hurdles, tiny click targets, or hidden menus. -2. Pitch 1 to 3 alternate visual/interaction approaches (e.g., compare floating cursor badges vs. top notification bars). -3. Mark your recommended choice with [Recommended] and ask up to 3 simple clarification questions. -4. STOP and wait for user feedback. +2. Present about three distinct UI concepts by default (usually exactly three). Apply the same default when the user directly asks you for UI ideas or mockups outside a formal spec-pipeline run. +3. Give each concept a visual mockup, not only a prose description. Use one comparison board when all three remain legible; otherwise create a separate mockup for each. Use supplied screenshots and the established product style as visual references when available. Make the concepts differ in interaction or information hierarchy, not only colors or decoration. If visual mockup generation is unavailable, provide annotated wireframes and say so. +4. Pair each mockup with a short title and concise tradeoff. Mark the recommended option with [Recommended] and explain why. Ask up to 3 focused clarification questions after presenting the concepts. +5. Do not require alternatives or mockups for implementation-only requests, narrow bug fixes, or critique-only questions unless the user asks. Follow any user-specified number or format. +6. STOP and wait for user feedback before writing the UI spec. When invoked during Phase 0b (UI Spec Generation): Output a structured spec covering: diff --git a/.codex/skills/spec-pipeline/SKILL.md b/.codex/skills/spec-pipeline/SKILL.md index f3d68df..92b7625 100644 --- a/.codex/skills/spec-pipeline/SKILL.md +++ b/.codex/skills/spec-pipeline/SKILL.md @@ -8,8 +8,7 @@ description: "Stickman Studio feature workflow: UI/UX and architecture specs, im 1. **Phase 0a: UI/UX Proposal & Alternative Ideas (User-Facing Features Only)** - `@uiux` analyzes the user's initial idea for cognitive friction, accessibility, and kid-friendliness. - For an in-stage selected-object inspector, `@uiux` first reviews `docs/specs/stage_panel_ui_system.md` and the current shared controls under `scripts/shared/stage_ui/`; proposals should build on that system unless the interaction needs justify a specific exception. - - `@uiux` pitches **1–3 alternative interaction approaches** (e.g., comparing floating tooltips vs. top-center banners, custom cursor reticles, or streamlined dialogs). - - `@uiux` highlights the most intuitive option with `[Recommended]` and asks up to 3 clarification questions. + - Follow the concept-count and visual-mockup protocol in `.codex/agents/uiux.toml`; it also applies when the user asks `@uiux` for ideas outside a formal pipeline run. - `@uiux` **STOP:** Waits for user feedback/selection before writing the UI spec. - _Bypass Rule:_ If the feature is strictly internal engine/backend logic (e.g., physics, serialization, scanning), skip directly to Phase 1a. diff --git a/.gitignore b/.gitignore index d19df72..38de615 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,8 @@ addons/ *.tmp .opencode/skills/playwright-cli *.import +# Local scratch captures, logs, and temporary browser profiles +/tmp/ content/review/artist/ content/examples/ /tests/godot_test_user_data/ diff --git a/AGENTS.md b/AGENTS.md index 9f0ae96..e8a295c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -72,6 +72,25 @@ the configured `run/main_scene`. Keep the prompt and visible Cancel together, leave the mode pill unobstructed, and reuse or generalize `StageUseTargetCaptureBar` instead of copying its styling. +## Child-facing dialogs and popups + +- When a user prefixes a UI/UX question or request with `uiux:` or `uiux,`, route it to the + custom UI/UX agent defined in `.codex/agents/uiux.toml`. Treat that prefix as an explicit + delegation request. The UI/UX agent is read-only; carry its design guidance into implementation + separately when implementation is requested. +- For new or revised child-facing dialogs, popups, and short setup flows, follow the + [Dialog Design Principles](docs/specs/dialog_design_principles.md). Use rounded containers + and controls, clear borders, blue primary actions with bold white labels, comfortable button + spacing, and large readable type. Prefer a visible, labeled action such as Cancel, Done, or + Back; do not make a small corner × the default or only way to close a dialog. +- When a dialog resizes after a tab, filter, or other in-place choice, preserve its current + anchor so the clicked control does not jump away from the pointer. Keep the dialog within the + safe viewport by limiting its height and scrolling content; nudge it only as much as needed + to keep essential controls visible. Avoid recentering after every content change. +- Keep reusable values in `SandboxTheme` or `PropTheme`. The Choose Scenery dialog in + `scripts/shared/ui/asset_selector.gd` and the `popups.asset_selector` tokens are the current + implementation reference. Adapt its layout to each workflow. + ## Required addon - **Scalable Vector Shapes 2D** (v2.27.7) at `addons/curved_lines_2d/` diff --git a/content/stages/artifacts.scene b/content/stages/artifacts.scene new file mode 100644 index 0000000..c83bbb3 --- /dev/null +++ b/content/stages/artifacts.scene @@ -0,0 +1,372 @@ +{ + "version": "1.6", + "scene_name": "Blank Scene", + "sky_id": "sky_neutral", + "camera": { + "position": { + "x": 1863.90759277344, + "y": -391.476318359375 + }, + "zoom": 0.665500044822693 + }, + "objects": [ + { + "file_id": "wall_panel_1", + "type": "wall_panel", + "position": { + "x": 448.0, + "y": -320.0 + }, + "rotation": 0.0, + "size": { + "x": 5120.0, + "y": 2816.0 + }, + "finish_id": "finish_wall_brick", + "color": "#b95f3dff" + }, + { + "file_id": "stickman_1", + "type": "stickman", + "position": { + "x": -128.0, + "y": -129.0 + }, + "rotation": 0.0, + "stk_path": "res://content/stickmen/break.stk", + "facing_profile": "FORWARD", + "action_queue": [ + { + "type": "move_to", + "target": { + "x": 2404.91528320313, + "y": 141.710083007813 + }, + "style": "walk" + } + ] + }, + { + "file_id": "terrain_1", + "type": "terrain", + "position": { + "x": -384.0, + "y": 256.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_asphalt" + }, + { + "file_id": "terrain_2", + "type": "terrain", + "position": { + "x": -384.0, + "y": 288.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_asphalt" + }, + { + "file_id": "terrain_3", + "type": "terrain", + "position": { + "x": 192.0, + "y": 256.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_asphalt" + }, + { + "file_id": "terrain_4", + "type": "terrain", + "position": { + "x": 384.0, + "y": 256.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_asphalt" + }, + { + "file_id": "terrain_5", + "type": "terrain", + "position": { + "x": 576.0, + "y": 256.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_asphalt" + }, + { + "file_id": "terrain_6", + "type": "terrain", + "position": { + "x": 768.0, + "y": 256.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_asphalt" + }, + { + "file_id": "terrain_7", + "type": "terrain", + "position": { + "x": 960.0, + "y": 256.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_asphalt" + }, + { + "file_id": "terrain_8", + "type": "terrain", + "position": { + "x": 1152.0, + "y": 224.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_asphalt" + }, + { + "file_id": "terrain_9", + "type": "terrain", + "position": { + "x": 1344.0, + "y": 224.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_asphalt" + }, + { + "file_id": "terrain_10", + "type": "terrain", + "position": { + "x": 1536.0, + "y": 224.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_asphalt" + }, + { + "file_id": "terrain_11", + "type": "terrain", + "position": { + "x": 1728.0, + "y": 224.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_asphalt" + }, + { + "file_id": "terrain_12", + "type": "terrain", + "position": { + "x": 1920.0, + "y": 224.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_asphalt" + }, + { + "file_id": "terrain_13", + "type": "terrain", + "position": { + "x": 2112.0, + "y": 224.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_asphalt" + }, + { + "file_id": "terrain_14", + "type": "terrain", + "position": { + "x": 2304.0, + "y": 224.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_asphalt" + }, + { + "file_id": "terrain_15", + "type": "terrain", + "position": { + "x": 2496.0, + "y": 224.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_asphalt" + }, + { + "file_id": "terrain_16", + "type": "terrain", + "position": { + "x": -192.0, + "y": 320.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_asphalt" + }, + { + "file_id": "terrain_17", + "type": "terrain", + "position": { + "x": 0.0, + "y": 320.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_asphalt" + }, + { + "file_id": "terrain_18", + "type": "terrain", + "position": { + "x": 192.0, + "y": 288.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_asphalt" + }, + { + "file_id": "terrain_19", + "type": "terrain", + "position": { + "x": 384.0, + "y": 288.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_asphalt" + }, + { + "file_id": "terrain_20", + "type": "terrain", + "position": { + "x": 576.0, + "y": 288.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_asphalt" + }, + { + "file_id": "wall_panel_2", + "type": "wall_panel", + "position": { + "x": 1152.0, + "y": -64.0 + }, + "rotation": 0.0, + "size": { + "x": 9984.0, + "y": 5504.0 + }, + "finish_id": "finish_wall_brick", + "color": "#b95f3dff" + } + ], + "event_rules": [], + "metadata": { + "created_unix": 1792022400, + "modified_unix": 1792470929.846, + "autosave": false, + "source_path": "" + } +} \ No newline at end of file diff --git a/content/stages/distance.scene b/content/stages/distance.scene new file mode 100644 index 0000000..70bf211 --- /dev/null +++ b/content/stages/distance.scene @@ -0,0 +1,140 @@ +{ + "version": "1.6", + "scene_name": "Untitled", + "sky_id": "sky_neutral", + "camera": { + "position": { + "x": 0.0, + "y": -400.0 + }, + "zoom": 0.5 + }, + "objects": [ + { + "file_id": "wall_panel_1", + "type": "wall_panel", + "position": { + "x": -768.0, + "y": -608.0 + }, + "rotation": 0.0, + "size": { + "x": 1152.0, + "y": 832.0 + }, + "finish_id": "finish_wall_brick", + "color": "#b95f3dff" + }, + { + "file_id": "scenery_artwork_1", + "type": "scenery_artwork", + "position": { + "x": -64.0, + "y": -868.5 + }, + "rotation": 0.0, + "asset_id": "res://content/props/art_city_skyline-v1.prop", + "display_name": "City Skyline", + "depth": "far", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "last_known_local_bounds": { + "position": { + "x": -321.5, + "y": -126.5 + }, + "size": { + "x": 643.0, + "y": 198.0 + } + } + }, + { + "file_id": "scenery_artwork_2", + "type": "scenery_artwork", + "position": { + "x": 320.0, + "y": -804.5 + }, + "rotation": 0.0, + "asset_id": "res://content/props/art_city_skyline-v1.prop", + "display_name": "City Skyline", + "depth": "floor", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "last_known_local_bounds": { + "position": { + "x": -321.5, + "y": -126.5 + }, + "size": { + "x": 643.0, + "y": 198.0 + } + } + }, + { + "file_id": "scenery_artwork_3", + "type": "scenery_artwork", + "position": { + "x": 768.0, + "y": -804.5 + }, + "rotation": 0.0, + "asset_id": "res://content/props/art_city_skyline-v1.prop", + "display_name": "City Skyline", + "depth": "near", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "last_known_local_bounds": { + "position": { + "x": -321.5, + "y": -126.5 + }, + "size": { + "x": 643.0, + "y": 198.0 + } + } + }, + { + "file_id": "scenery_artwork_4", + "type": "scenery_artwork", + "position": { + "x": -448.0, + "y": -1600.0 + }, + "rotation": 0.0, + "asset_id": "res://content/props/baseball.prop", + "display_name": "Baseball", + "depth": "near", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "last_known_local_bounds": { + "position": { + "x": -26.1999988555908, + "y": -26.1999988555908 + }, + "size": { + "x": 52.4000015258789, + "y": 52.4000015258789 + } + } + } + ], + "event_rules": [], + "metadata": { + "created_unix": 1792555445.055, + "modified_unix": 1792555445.055, + "autosave": false, + "source_path": "" + } +} \ No newline at end of file diff --git a/content/stages/fps.scene b/content/stages/fps.scene new file mode 100644 index 0000000..5156ae8 --- /dev/null +++ b/content/stages/fps.scene @@ -0,0 +1,417 @@ +{ + "version": "1.6", + "scene_name": "Untitled", + "sky_id": "sky_sunset", + "camera": { + "position": { + "x": -85.0290832519531, + "y": -612.394409179688 + }, + "zoom": 0.732050120830536 + }, + "objects": [ + { + "file_id": "wall_panel_1", + "type": "wall_panel", + "position": { + "x": 288.0, + "y": -224.0 + }, + "rotation": 0.0, + "size": { + "x": 4160.0, + "y": 2368.0 + }, + "finish_id": "finish_wall_brick", + "color": "#b95f3dff" + }, + { + "file_id": "wall_panel_2", + "type": "wall_panel", + "position": { + "x": 288.0, + "y": -352.0 + }, + "rotation": 0.0, + "size": { + "x": 5568.0, + "y": 3136.0 + }, + "finish_id": "finish_wall_wood", + "color": "#c87137ff" + }, + { + "file_id": "background_ground_1", + "type": "background_ground", + "position": { + "x": 256.0, + "y": -352.0 + }, + "rotation": 0.0, + "size": { + "x": 6144.0, + "y": 3392.0 + }, + "finish_id": "finish_background_concrete", + "color": "#a6aaa3ff" + }, + { + "file_id": "wall_panel_3", + "type": "wall_panel", + "position": { + "x": 128.0, + "y": -320.0 + }, + "rotation": 0.0, + "size": { + "x": 6016.0, + "y": 3456.0 + }, + "finish_id": "finish_wall_brick", + "color": "#22c6ffff" + }, + { + "file_id": "stickman_1", + "type": "stickman", + "position": { + "x": -1088.0, + "y": -641.0 + }, + "rotation": 0.0, + "stk_path": "res://content/stickmen/break.stk", + "facing_profile": "FORWARD", + "action_queue": [ + { + "type": "move_to", + "target": { + "x": 1562.8154296875, + "y": -145.01513671875 + }, + "style": "walk" + } + ] + }, + { + "file_id": "terrain_1", + "type": "terrain", + "position": { + "x": -2112.0, + "y": 0.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_grass" + }, + { + "file_id": "terrain_2", + "type": "terrain", + "position": { + "x": -1920.0, + "y": 0.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_grass" + }, + { + "file_id": "terrain_3", + "type": "terrain", + "position": { + "x": -1728.0, + "y": 0.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_grass" + }, + { + "file_id": "terrain_4", + "type": "terrain", + "position": { + "x": -1536.0, + "y": 0.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_grass" + }, + { + "file_id": "terrain_5", + "type": "terrain", + "position": { + "x": -1344.0, + "y": 0.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_grass" + }, + { + "file_id": "terrain_6", + "type": "terrain", + "position": { + "x": -1152.0, + "y": 0.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_grass" + }, + { + "file_id": "terrain_7", + "type": "terrain", + "position": { + "x": -960.0, + "y": 0.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_grass" + }, + { + "file_id": "terrain_8", + "type": "terrain", + "position": { + "x": -768.0, + "y": 0.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_grass" + }, + { + "file_id": "terrain_9", + "type": "terrain", + "position": { + "x": -576.0, + "y": 0.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_grass" + }, + { + "file_id": "terrain_10", + "type": "terrain", + "position": { + "x": -384.0, + "y": 0.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_grass" + }, + { + "file_id": "terrain_11", + "type": "terrain", + "position": { + "x": -192.0, + "y": -32.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_grass" + }, + { + "file_id": "terrain_12", + "type": "terrain", + "position": { + "x": 0.0, + "y": -32.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_grass" + }, + { + "file_id": "terrain_13", + "type": "terrain", + "position": { + "x": 192.0, + "y": -32.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_grass" + }, + { + "file_id": "terrain_14", + "type": "terrain", + "position": { + "x": 384.0, + "y": -32.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_grass" + }, + { + "file_id": "terrain_15", + "type": "terrain", + "position": { + "x": 576.0, + "y": -32.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_grass" + }, + { + "file_id": "terrain_16", + "type": "terrain", + "position": { + "x": 768.0, + "y": -32.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_grass" + }, + { + "file_id": "terrain_17", + "type": "terrain", + "position": { + "x": 960.0, + "y": -32.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_grass" + }, + { + "file_id": "terrain_18", + "type": "terrain", + "position": { + "x": 1152.0, + "y": -32.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_grass" + }, + { + "file_id": "terrain_19", + "type": "terrain", + "position": { + "x": 1344.0, + "y": -32.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_grass" + }, + { + "file_id": "terrain_20", + "type": "terrain", + "position": { + "x": 1536.0, + "y": -32.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_grass" + }, + { + "file_id": "terrain_21", + "type": "terrain", + "position": { + "x": 1728.0, + "y": -32.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_grass" + } + ], + "event_rules": [], + "metadata": { + "created_unix": 1792467978.761, + "modified_unix": 1792467978.761, + "autosave": false, + "source_path": "" + } +} \ No newline at end of file diff --git a/docs/architecture/background_rendering_guide.md b/docs/architecture/background_rendering_guide.md new file mode 100644 index 0000000..3621f77 --- /dev/null +++ b/docs/architecture/background_rendering_guide.md @@ -0,0 +1,87 @@ +# Patterned scenery rendering + +A patterned wall or background uses one small repeating image for each finish and tint. The camera samples that same image wherever the surface appears. A pan or zoom changes the visible rectangle and sampling scale; it does not create a new image for each camera position. + +![Patterned scenery render and cache pipeline](diagrams/background_pipeline.svg) + +## Render pipeline + +1. A visible, committed patterned surface requests its style from the scene's `StageSurfaceBakePool`. +2. The cache checks the style key. A ready entry is reused; a missing entry is queued for the shared baker. +3. At most one queued style is baked per process frame. The baker draws a four-world-unit overscan around the material period so edge details can be cropped without gaps. +4. After the submitted drawing reaches `frame_post_draw`, the pool captures the image, crops the period, generates mipmaps, and publishes an `ImageTexture`. +5. The temporary vector commands are cleared. The surface draws one camera-clipped quad using the shared image, repeat sampling, and mipmapped filtering. + +The GPU still renders the textured rectangle every frame. When the view changes, the CPU also updates that rectangle and its texture coordinates. This is a small drawing command; it does not rebuild brick faces, grain, pores, or other vector details. The cache counters distinguish image builds and readbacks from ordinary redraws. + +## Shared style images + +The cache is owned by a scene-scoped pool. Its key includes finish, tint, wall/background kind, material version, repeat period, pixel density, and mipmap configuration. It excludes surface position, camera position, zoom, rectangle size, and erase fragment. Thus same-style surfaces and fragments share one image, while a tint change correctly requests a different image. + +![Many surfaces sharing one finished material image](diagrams/background_reuse.svg) + +The current periods are finite, seamless repeat units baked at two bitmap pixels per world unit: + +| Finish | Repeat period in world units | Bitmap size | +| --- | ---: | ---: | +| Brick | 800 × 304 | 1600 × 608 | +| Wood | 960 × 384 | 1920 × 768 | +| Grass | 704 × 576 | 1408 × 1152 | +| Carpet | 544 × 416 | 1088 × 832 | +| Concrete | 1120 × 760 | 2240 × 1520 | + +For example, a 100-world-unit brick remains 100 world units wide. At 0.4× zoom it covers about 40 screen pixels; zoom changes how the GPU samples the image, not the brick geometry. The finite repeat unit makes detail bounded, so landmarks can become noticeable on very large surfaces. + +## Zoom and mipmaps + +The renderer captures the material once at fixed density, then builds smaller mip levels from that image. During zoom-out the GPU samples a smaller level to reduce shimmer; during zoom-in it samples a larger level. This replaces separately drawing the material at several CPU-selected zoom levels. At the Stage's maximum 6× zoom, the source image is magnified three times and can look softer. The current quality/performance tradeoff is fixed in the cache configuration. + +![Fixed material image with mipmap levels](diagrams/background_mipmaps.svg) + +## Erasing and pattern alignment + +The eraser still cuts a rectangle into up to four rectangular fragments. Each fragment draws only its own bounds, but requests the same style image; it does not bake a fragment-specific copy. Cancel restores the original geometry, and Commit retains the new fragments. Both use the existing material sampling rules. + +The sampling origin is significant: walls use `global_position`; background ground uses `global_position + motif_origin_offset`. Each surface computes its source UV from its local visible position plus that draw origin, wrapped by the material period. This preserves the current alignment convention across cuts. Avoid changing the origin rule as a generic transform correction: unusual transformed fragments need separate verification before making a broader alignment claim. + +![Erase fragments clipping the same repeating image](diagrams/background_erase.svg) + +## Memory and rebuild boundaries + +The scene cache has a 64 MiB resident texture budget. Accounting includes mip levels, textures waiting for safe retirement, and reserved space for a capture. Unused entries are evicted by least-recent use when needed. A style held by active surfaces can prevent admission of another style; the pool reports this as budget pressure rather than exceeding the cap. Under ordinary use, returning to a recently used style should reuse its warm image. Many tint variants can force eviction and a later rebuild. + +A style's first capture still costs CPU/GPU work and can briefly affect a frame. Once ready, moving the camera across a large wall does not enqueue position tiles or read back new images. The shared baker also clears its vector command list after capture, preventing the native drawing resources that previously accumulated in many per-tile viewports. + +These images are temporary runtime data. A saved `.scene` keeps surface settings and geometry; the cache recreates its images when needed in a new Stage session. Closing the scene releases its pool. + +## What causes another bake? + +| Action | New image work? | +| --- | --- | +| Pan left, then return right | No, while the style remains cached. | +| Zoom in or out | No. The GPU samples existing mipmaps. | +| Resize a wall | No. Change the rectangle and texture coordinates. | +| Erase, Cancel, or Commit | No, if the style is ready. Change geometry and clipping. | +| Use the same finish and tint on another wall | No, if the style is ready. Share its image. | +| Choose a new finish or tint | Bake once if that style is missing. | +| Return to an evicted style, or open a fresh session | Recreate the missing image. | + +## Terms used here + +- **Bake:** render the vector artwork once to make an image. +- **Texture:** the image the GPU samples when drawing. +- **Quad:** one rectangle, represented by two triangles. +- **UVs:** coordinates that select which part of a texture to sample. +- **Mipmaps:** smaller copies of an image, prepared in advance for zooming out. +- **Readback:** copy the rendered image from the GPU so the cache can keep an independent texture. + +## Scope and code map + +The bitmap cache is for committed patterned walls and backgrounds in render-capable runs. Plain paint surfaces, interactive placement previews, and headless runs retain the direct renderer path. Fully opaque scenery can suppress a fully hidden surface's texture request; partial coverage remains conservative. + +- [StageSurfaceCache](../../scripts/stage/scenery/stage_surface_cache.gd) owns style handles, the scene pool, bake queue, image capture, mipmaps, cache keys, and budget statistics. +- [StageRectSurface](../../scripts/stage/scenery/stage_rect_surface.gd) requests the style, clips the surface to the visible camera area, and draws the repeated texture. +- [StageSurfaceRenderer](../../scripts/stage/scenery/stage_surface_renderer.gd) defines patterned artwork, material periods, and the direct-drawing path. +- [Repeating scenery material cache spec](../specs/scenery_bitmap_tile_cache.md) records implementation constraints and acceptance checks. + +In one observed warm run of `fps.scene`, four style entries used 36,399,144 bytes (about 34.71 MiB); 720 warm camera frames recorded zero builds, readbacks, or evictions. This is a run snapshot, not a guarantee for other scenes, tint combinations, or hardware. diff --git a/docs/architecture/diagrams/background_erase.svg b/docs/architecture/diagrams/background_erase.svg new file mode 100644 index 0000000..ceb592f --- /dev/null +++ b/docs/architecture/diagrams/background_erase.svg @@ -0,0 +1,43 @@ + + Erasing changes rectangles while sharing the same background texture + A rectangular cut turns one wall into top, bottom, left and right fragments. All fragments sample the existing image using their pattern coordinates. The image itself is not painted over or copied. + + + + + + + Erase the wall geometry. Keep the shared image. + Fragments crop their own rectangles and use the existing pattern coordinates. + Before: one wall rectangle + + + + + Cut here + + + Erase area + + After: up to four fragments + + + + + + + + + + Top + Bottom + Left + Right + Empty area + Scene behind + + + + All remaining pieces still use the same cached texture. + Cancel restores the original geometry. Commit keeps the fragments. Neither paints into the image. + diff --git a/docs/architecture/diagrams/background_mipmaps.svg b/docs/architecture/diagrams/background_mipmaps.svg new file mode 100644 index 0000000..6504fd9 --- /dev/null +++ b/docs/architecture/diagrams/background_mipmaps.svg @@ -0,0 +1,39 @@ + + Zoom uses smaller copies already stored in the material texture + The brick material starts at 1600 by 608 bitmap pixels. Its mip chain includes 800 by 304, 400 by 152 and 200 by 76, continuing down to one pixel. These copies are prepared once. Brick world dimensions stay fixed. + + + + + + + + + + Zoom samples ready-made sizes of the same image. + Mipmaps are generated once with the material. The GPU selects and blends them during rendering. + + + + + + + + + … + + 1600 × 608 + Base image: 2 pixels / world unit + 800 × 304 + Half size + 400 × 152 + Quarter size + 200 × 76 + Eighth size + The actual chain continues down to 1 × 1. Diagram sizes are scaled for illustration. + + + World size, image size and screen size are different things. + A brick stays 100 world units wide. At 40% zoom, it covers about 40 screen pixels. + Camera zoom changes sampling detail; it does not bake another image or resize the brick geometry. + diff --git a/docs/architecture/diagrams/background_pipeline.svg b/docs/architecture/diagrams/background_pipeline.svg new file mode 100644 index 0000000..2171c1c --- /dev/null +++ b/docs/architecture/diagrams/background_pipeline.svg @@ -0,0 +1,51 @@ + + Background rendering: prepare a style once, reuse it while moving the camera + A visible surface requests its finish and tint from the scene cache. A cache hit goes directly to the GPU repeat sampler. A cache miss uses one temporary baker, captures an image and mipmaps, and releases vector commands. + + + + + + Prepare a material once. Reuse it as the camera moves. + Only visible, committed patterned scenery requests a cached material. + + + Visible surface + Finish + tint + Example: blue brick + + + + Scene style cache + Already have this image? + Shared by all matching surfaces + + Hit + + + GPU draws the visible rectangle + Repeat texture + sample its mipmaps + Pan / zoom updates sampling coordinates + + + Miss: prepare this style + + One temporary baker + Draw one pattern period + Wait for GPU; capture image + Then clear vector commands + + + + Store the reusable texture + Crop overscan; generate mipmaps + Keep the image, not the vector artwork + Ready styles stay warm within the cache budget + + Ready + + + Blue route: reuse during ordinary pan and zoom + + Purple route: bake only when a style is missing + diff --git a/docs/architecture/diagrams/background_reuse.svg b/docs/architecture/diagrams/background_reuse.svg new file mode 100644 index 0000000..d9ef1db --- /dev/null +++ b/docs/architecture/diagrams/background_reuse.svg @@ -0,0 +1,44 @@ + + Many wall rectangles share one material image + One blue-brick image supplies Wall A, Wall B, and an erased fragment. Different sizes, positions, and camera views do not create more images. A different tint or finish is a different cached style. + + + + + + + + + + Wall area can grow. The number of style images stays the same. + Position, wall size, camera position and zoom are not part of the style key. + + + + One shared image + Blue brick + Varied brick faces + Repeats both ways + + + + + + Wall A + + + + Wall B + + Same + style + + + Erased fragment + + Only the visible + rectangle changes + + A different finish or tint is a separate style. A matching warm style reuses the existing image. + Patterns shown here are schematic; the actual brick image contains 8 columns × 8 rows of varied artwork. + diff --git a/docs/architecture/scenery_surface_caching.md b/docs/architecture/scenery_surface_caching.md new file mode 100644 index 0000000..7e009dc --- /dev/null +++ b/docs/architecture/scenery_surface_caching.md @@ -0,0 +1,42 @@ +# Scenery surface caching + +`StageRectSurface` renders committed patterned scenery through reusable texture tiles. +Drawing previews use the adaptive lightweight renderer. Paint remains a single rectangle. + +## Camera movement + +Tiles use a fixed grid in the surface's local coordinates. Texture resolution changes in +powers of two, so adjacent wheel steps reuse existing tiles. Panning requests only newly +exposed tiles. Existing textures stay visible while missing tiles are prepared, with a +lightweight surface motif beneath them to prevent blank areas. + +`StageSurfaceCache` keeps queued tiles as metadata. A shared queue creates and renders at +most one tile per process frame across all surfaces. It also retires at most two obsolete +render targets per frame. This avoids large allocations, vector rebuilds, and batches of +GPU resource releases during camera input. + +Each tile has a 256 × 128 pixel core and a four pixel gutter. The gutter is cropped during +compositing to prevent seams. Each surface requests at most 192 visible tiles and retains +at most 224 tiles, evicting older unused entries. Half-resolution tiles retain full brick +texture and wood grain. Only quarter-resolution tiles and below omit fine texture, for +distant views. Brick and plank positions remain fixed. Detail can fill in progressively +after movement, including when returning from a distant view to a normal zoom level. + +## Invalidation and coverage + +Geometry, finish, tint, and motif-origin changes invalidate that surface's tiles. Camera +movement selects tiles without invalidating their artwork. Render targets belong to the +surface node; queued references are weak so deleted surfaces cannot keep jobs alive. + +Fully opaque rectangular sibling surfaces can suppress artwork entirely hidden behind +them. Coverage checks account for transforms, draw order, and visibility. Transparent +surfaces, custom materials, differing relative-Z conventions, and Y-sorted parents disable +this optimization conservatively. Revealing a surface resumes its pending tiles. + +## Verification + +`tests/test_scenery_surface_cache.gd` checks rendered appearance against the direct vector +renderer, tile reuse and invalidation, the shared build limit, allocation and retention +bounds, and covered-surface behavior. Run it with a graphical Godot renderer for pixel +comparisons. The adaptive preview regressions remain in +`tests/test_scenery_surface_preview.gd` and `tests/test_scenery_brick_preview.gd`. diff --git a/docs/architecture/tech_debt_and_optimizations.md b/docs/architecture/tech_debt_and_optimizations.md index e1f4384..bebac43 100644 --- a/docs/architecture/tech_debt_and_optimizations.md +++ b/docs/architecture/tech_debt_and_optimizations.md @@ -178,4 +178,8 @@ This document tracks known technical debt, optimization opportunities, and minor | 2026-10-17 | Wrote the Phase 19 selected-artwork depth inspector specification (`docs/specs/phase_19_selected_artwork_depth_inspector.md`) for final sign-off. It reuses the existing `StageInspectorPanel` and `StagePanelChoiceGroup`, applies Far/Near/On the floor immediately to one live or missing Scenery placement through `StageSceneryArtwork.set_depth()`, and follows the Stage's existing dirty/save path. The current `.scene` depth field, `StageDrawOrder`, and `stage_panels` tokens need no changes. Updated #72 to include immediate depth edits in a future Stage-wide Undo phase. No runtime code changed by this specification entry. | | 2026-10-17 | Wrote the Phase 19 Background ground and wall color architecture specification (`docs/specs/phase_19_background_ground_and_wall_color.md`) for final sign-off. It defines distinct inert wall/backdrop scene types over a small shared rectangle contract, world-aligned bounded vector materials, selected-piece color preview without mutating saved tint, one eight-color Stage settings list, scene v1.6 migration of old Brick/Wood placeholder green, shared Stage inspector controls, and confirmed single-piece backdrop erasure. Logged #74 for zoom-consistent motif budgets and #75 for the old unused wall-color ambiguity. No runtime code changed. | +| 2026-10-20 | Approved the scenery bitmap tile cache architecture (`docs/specs/scenery_bitmap_tile_cache.md`). Lightweight `StageSurfaceCache` handles share immutable bitmap tiles through one `StageSurfaceBakePool` under the current scene, with a 64 MiB resident bitmap budget and one reusable offscreen baker. Camera-visible tiles use four fixed zoom levels (0.125, 0.5, 1, 2); the coarsest tile bakes first and refined detail overlays it. At maximum 6× zoom, the 2× bitmap may look softer because it is magnified threefold. Erased fragments clip shared tiles only when their complete pattern sampling phase matches. The previous per-surface viewport cache retained vector commands and had no scene-wide resource limit, a likely contributor to `Element limit reached`; the exact RID pool remains unconfirmed. GPU readback during bitmap capture and regeneration after budget eviction are accepted costs to measure during implementation. | + +| 2026-10-20 | Replaced the positional scenery tile design after the user observed continuous buffering when panning after zoom. The accepted revision in `docs/specs/scenery_bitmap_tile_cache.md` bakes one seamless, 2× bitmap per finish/tint period and uses GPU repeat plus generated mipmaps for all camera positions and zooms. Erased fragments share the style texture and crop it using their existing sample coordinate. The scene-wide 64 MiB limit includes mipmaps; inactive style variants may be evicted, while recently used styles stay warm when space allows. This trades unique random detail across an unlimited wall for finite repeated variation and keeps the existing possible 3× bitmap enlargement at maximum zoom. A style change can bake once; repeated camera movement must not increment build or readback counts after styles are ready. | + _End of Log_ diff --git a/docs/review/choose_scenery_artwork_option_a.png b/docs/review/choose_scenery_artwork_option_a.png new file mode 100644 index 0000000000000000000000000000000000000000..26fe305d7bafa791bccaafa7bb54dd3f61d6f26d GIT binary patch literal 123373 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 X#Nl Texture2D`, `dispose()`, and `static get_stats(owner_node: Node = null) -> Dictionary`. Its build request changes from a positional tile to a style request. The payload is finish (`StringName`), tint (`Color`), wall/background kind (`bool`), renderer/material revision, and the fixed period and density derived from finish. `StageRectSurface` no longer retains a dictionary of camera-position tiles. One handle may be replaced when tint or finish changes; other surfaces with the same key keep their texture. + +`StageSurfaceCache.BakePool` has one entry per requested style. Entry identity includes all values that alter baked pixels, especially finish, tint, wall/background kind, and renderer revision. It excludes camera transform, surface bounds, eraser fragment, and sample phase. A queued entry with no live visible interested owner can be canceled. The pool serializes captures through one `SubViewport`, waits until the GPU has consumed its vector commands, reads the completed image, generates mipmaps, publishes an immutable `ImageTexture`, and clears baker vector commands. A failed capture leaves the base color visible and permits retry; it must not publish an empty ready texture. + +Keep a **64 MiB global resident texture budget per scene**, including all mip levels, retired textures awaiting a safe frame, and reserved bytes for a build. One copy each of the five listed periods is about 32 MiB before mipmaps and 43 MiB with mipmaps at RGBA8; additional tint variants can require eviction. Evict styles without visible interested owners before styles in active use, and release canvas references safely before freeing a texture. Keep recently used styles warm so panning away and back does not trigger a new capture under normal budget. If active styles collectively exceed the budget, retain the hard cap and show base color for a style that cannot be admitted; expose this through stats rather than silently exceeding the budget. Revisit period size or density if this occurs in ordinary scenes. + +`get_stats()` must expose at least resident texture bytes, entries, queued styles, active baker count, builds, readbacks, cache hits, and budget refusals. These counters make the reuse behavior observable without adding UI. + +## Data flow + +```mermaid +flowchart LR + Visible[Visible patterned surface] -->|finish + tint| Cache[Scene style cache] + Cache -->|ready texture| UV[Sample repeated texture at existing pattern offset] + Cache -->|first style request| Baker[One temporary bitmap baker] + Baker -->|capture + mipmaps| Cache + UV --> Clip[Clip quad to surface and camera] + Clip --> Stage[Stage canvas] + Camera[Pan or zoom] --> UV +``` + +## Observable acceptance and verification + +1. Open `content/stages/fps.scene` in EDIT, DIRECT, and PLAY. Allow visible brick, wood, and concrete styles to become ready. Pan left and right repeatedly and zoom in and out through the same region. `builds` and `readbacks` stay unchanged after those styles are ready; only a new finish or tint may add a style capture. Frame movement does not pause for repeated tile buffering, and `Element limit reached` does not recur. +2. Inspect the left/right and top/bottom period boundaries for every finish at distant, 40%, normal, and close zoom. There are no lines, shifted brick courses, broken planks, or mismatched decorative marks. Zoom changes sampling detail without changing motif geometry. +3. Erase through a brick wall and background ground, Cancel, then erase and Commit. Inspect both sides of each fragment edge, then save and reload. The cut stays clear, surrounding patterns stay aligned, and the cache still has one entry per shared style. +4. Change a wall's tint and finish. The changed style may bake once; unchanged surfaces keep their ready texture. Return to the earlier style and pan again to check that recently used styles stay warm within the 64 MiB budget. Observe total bytes, pending styles, and baker lifetime during a long Stage session. + +Use a render-capable run of the actual Stage for appearance and frame-time observations. This architecture update does not require a new test file. + +## Trade-offs + +A periodic material replaces unique random detail across an arbitrarily large wall with a bounded, repeating pattern. The chosen periods and seeded variation should make repetition unobtrusive at ordinary framing, but repeated landmarks can become noticeable on very large surfaces. GPU mipmaps replace four separately baked zoom levels and keep pan/zoom free of new builds. A one-time GPU readback remains when a finish/tint style first appears. Multiple color variants can evict warm styles under the 64 MiB cap, so those cases need measurement on representative scenes. diff --git a/project.godot b/project.godot index 1830b33..8d4b075 100644 --- a/project.godot +++ b/project.godot @@ -23,3 +23,4 @@ config/icon="res://icon.svg" window/size/initial_position_type=0 window/size/initial_position=Vector2i(4000, 200) +window/size/initial_screen=1 diff --git a/sandbox_theme.json b/sandbox_theme.json index c9e1a2c..7ea8d29 100644 --- a/sandbox_theme.json +++ b/sandbox_theme.json @@ -114,13 +114,57 @@ "surface_color_picker_min_size": [320, 340], "action_min_target_size": 48 }, + "confirmation": { + "scenery_erase_size": [520, 208], + "padding": 18, + "body_gap": 10, + "footer_gap": 12, + "corner_radius": 16, + "border_width": 2, + "title_font_size": 26, + "message_font_size": 20, + "button_font_size": 20, + "button_height": 48, + "button_radius": 12, + "button_border_width": 2, + "remove_button_width": 170, + "keep_button_width": 132, + "surface_color": "#202633FA", + "border_color": "#22C6FF", + "accent_color": "#22C6FF", + "action_color": "#087DBA", + "danger_color": "#FF626B", + "danger_hover_surface": "#3B252C", + "danger_pressed_surface": "#4A252D" + }, "asset_selector": { "backdrop_alpha": 0.55, "cell_size": [190, 220], "thumbnail_size": [156, 156], "surface_preview_size": [420, 132], "look_cue_size": [80, 42], - "setup_gap": 10 + "setup_gap": 8, + "setup_title_size": 30, + "setup_heading_size": 26, + "setup_choice_font_size": 22, + "setup_choice_height": 68, + "setup_choice_corner_radius": 22, + "setup_choice_border_width": 2, + "tab_font_size": 22, + "tab_height": 60, + "filter_chip_font_size": 20, + "filter_chip_height": 48, + "filter_chip_gap": 8, + "setup_button_font_size": 20, + "button_border_width": 2, + "footer_gap": 16, + "surface_corner_radius": 14, + "look_corner_radius": 8, + "corner_radius": 16, + "compact_swatch_size": 42, + "color_group_gap": 8, + "action_color": "#087DBA", + "separator_color": "#AAB2C0A0" } }, "palette": { @@ -185,7 +229,13 @@ "close_width": 88, "surface_color": "#171B24", "hover_surface": "#303A4B", - "selected_surface": "#12394A" + "selected_surface": "#12394A", + "primary_color": "#087DBA", + "primary_hover_color": "#1596D3", + "primary_pressed_color": "#066B9F", + "destructive_color": "#FF626B", + "destructive_hover_surface": "#3B252C", + "destructive_pressed_surface": "#4A252D" }, "prop": { "stickman_cue_color": "#F5F7FA", diff --git a/scenes/stage/asset_selector.tscn b/scenes/stage/asset_selector.tscn index e4956a4..ac70ce7 100644 --- a/scenes/stage/asset_selector.tscn +++ b/scenes/stage/asset_selector.tscn @@ -29,21 +29,18 @@ size_flags_horizontal = 3 theme_override_font_sizes/font_size = 18 text = "Choose" -[node name="CloseButton" type="Button" parent="MarginContainer/VBoxContainer/TitleBar"] -unique_name_in_owner = true -layout_mode = 2 -text = "×" - -[node name="GalleryTabs" type="HBoxContainer" parent="MarginContainer/VBoxContainer"] +[node name="GalleryTabs" type="HFlowContainer" parent="MarginContainer/VBoxContainer"] unique_name_in_owner = true layout_mode = 2 visible = false +h_separation = 8 +v_separation = 8 [node name="BackgroundsTab" type="Button" parent="MarginContainer/VBoxContainer/GalleryTabs"] unique_name_in_owner = true layout_mode = 2 size_flags_horizontal = 3 -text = "🌤 Backgrounds" +text = "☀️ Backgrounds" [node name="ArtworkTab" type="Button" parent="MarginContainer/VBoxContainer/GalleryTabs"] unique_name_in_owner = true @@ -51,11 +48,17 @@ layout_mode = 2 size_flags_horizontal = 3 text = "🎨 Artwork" -[node name="TagFilter" type="OptionButton" parent="MarginContainer/VBoxContainer/GalleryTabs"] +[node name="EditSceneryButton" type="Button" parent="MarginContainer/VBoxContainer/GalleryTabs"] +unique_name_in_owner = true +layout_mode = 2 +text = "✋ Edit placed scenery" + +[node name="ArtworkFilters" type="HFlowContainer" parent="MarginContainer/VBoxContainer"] unique_name_in_owner = true layout_mode = 2 -size_flags_horizontal = 3 visible = false +h_separation = 8 +v_separation = 8 [node name="GridScroll" type="ScrollContainer" parent="MarginContainer/VBoxContainer"] unique_name_in_owner = true @@ -77,6 +80,14 @@ theme_override_constants/h_separation = 8 theme_override_constants/v_separation = 8 columns = 4 +[node name="SurfaceCardsRow" type="HBoxContainer" parent="MarginContainer/VBoxContainer/GridScroll/ContentStack"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +alignment = 1 +theme_override_constants/separation = 8 +visible = false + [node name="EmptyLabel" type="Label" parent="MarginContainer/VBoxContainer"] unique_name_in_owner = true layout_mode = 2 @@ -89,6 +100,18 @@ vertical_alignment = 1 [node name="Footer" type="HBoxContainer" parent="MarginContainer/VBoxContainer"] layout_mode = 2 alignment = 1 +theme_override_constants/separation = 16 + +[node name="CloseButton" type="Button" parent="MarginContainer/VBoxContainer/Footer"] +unique_name_in_owner = true +layout_mode = 2 +text = "Close" + +[node name="FooterSpacer" type="Control" parent="MarginContainer/VBoxContainer/Footer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +visible = false [node name="PrevButton" type="Button" parent="MarginContainer/VBoxContainer/Footer"] unique_name_in_owner = true @@ -119,10 +142,4 @@ text = "Refresh" unique_name_in_owner = true layout_mode = 2 visible = false -text = "✏️ Make artwork" - -[node name="EditSceneryButton" type="Button" parent="MarginContainer/VBoxContainer/Footer"] -unique_name_in_owner = true -layout_mode = 2 -visible = false -text = "🎛 Edit placed Scenery" +text = "✏️ Make artwork…" diff --git a/scripts/editors/stage/sandbox_stage.gd b/scripts/editors/stage/sandbox_stage.gd index c0399dc..11eba1f 100644 --- a/scripts/editors/stage/sandbox_stage.gd +++ b/scripts/editors/stage/sandbox_stage.gd @@ -62,6 +62,8 @@ const STAGE_PANEL_CHOICE_GROUP_SCRIPT := preload("res://scripts/shared/stage_ui/ const STAGE_SURFACE_COLOR_CONTROLS_SCRIPT := preload("res://scripts/shared/stage_ui/stage_surface_color_controls.gd") const STAGE_SURFACE_COLOR_DIALOG_SCRIPT := preload("res://scripts/shared/stage_ui/stage_surface_color_dialog.gd") const STAGE_SURFACE_PREVIEW_SCRIPT := preload("res://scripts/stage/scenery/stage_surface_preview.gd") +const STAGE_DEPTH_CHOICE_PREVIEW_SCRIPT := preload("res://scripts/shared/stage_ui/stage_depth_choice_preview.gd") +const PROP_FILE_SCRIPT := preload("res://scripts/editors/prop/prop_file.gd") const PROP_EDITOR_SCENE := preload("res://scenes/editors/prop/prop_editor.tscn") # Phase 3c editor tools. @@ -287,10 +289,20 @@ var _scenery_depth_section: VBoxContainer var _scenery_depth_heading: Label var _scenery_depth_group: StagePanelChoiceGroup var _scenery_depth_helper: Label +var _scenery_depth_choice_cues: Array[Control] = [] +var _scenery_depth_preview_texture: Texture2D +var _scenery_depth_preview_path: String = "" +var _scenery_depth_preview_busy: bool = false +var _scenery_depth_preview_generation: int = 0 +var _scenery_depth_preview_scheduled_generation: int = -1 +var _scenery_depth_thumbnail_renderer: PropThumbnail +var _scenery_depth_cursor_badge: PanelContainer +var _scenery_depth_cursor_badge_label: Label var _scenery_size_row: HBoxContainer var _scenery_smaller_button: Button var _scenery_bigger_button: Button var _scenery_erase_wall_area_button: Button +var _scenery_erase_background_ground_area_button: Button var _scenery_erase_piece_button: Button var _scenery_find_button: Button var _scenery_choose_button: Button @@ -300,7 +312,13 @@ var _scenery_repair_candidate: StageSceneryArtwork var _scenery_repair_path: String = "" var _missing_scenery_dialog: ConfirmationDialog var _scenery_repair_confirm_dialog: ConfirmationDialog -var _scenery_erase_piece_dialog: ConfirmationDialog +var _scenery_erase_piece_dialog: Control +var _scenery_erase_piece_dialog_panel: PanelContainer +var _scenery_erase_piece_dialog_title: Label +var _scenery_erase_piece_dialog_message: Label +var _scenery_erase_piece_dialog_actions: BoxContainer +var _scenery_erase_piece_keep_button: Button +var _scenery_erase_piece_remove_button: Button var _scenery_erase_piece_targets: Array[WeakRef] = [] var _surface_color_dialog: StageSurfaceColorDialog var _surface_color_session: Dictionary = {} @@ -1376,6 +1394,7 @@ func _process(delta: float) -> void: _update_action_overlay() _sync_awaiting_cursor() _update_speech_badge_cursor() + _update_scenery_depth_cursor_badge() if _toast_timer > 0.0: _toast_timer -= delta if _toast_timer <= 0.0: @@ -1521,6 +1540,11 @@ func _unhandled_key_input(event: InputEvent) -> void: return if not (event is InputEventKey) or not event.pressed or event.echo: return + if _scenery_erase_piece_dialog != null and _scenery_erase_piece_dialog.visible: + if event.keycode == KEY_ESCAPE: + _dismiss_scenery_erase_piece_dialog() + get_viewport().set_input_as_handled() + return if _speech_composer_is_open(): if event.keycode == KEY_ESCAPE: if bool(_speech_composer.call("is_message_dialog_open")): @@ -1692,9 +1716,7 @@ func _handle_world_click(mb: InputEventMouseButton) -> void: _refresh_scenery_inspector() _refresh_scenery_capture_bar() elif _scenery_controller != null and _scenery_controller.is_group_preview_pending(): - _scenery_controller.cancel_group() - _scenery_group_mode = &"" - _refresh_scenery_capture_bar() + _keep_scenery_group_preview() else: _exit_scenery_edit_mode() get_viewport().set_input_as_handled() @@ -2557,6 +2579,8 @@ func _save_scene_to_path(path: String, is_autosave: bool = false) -> bool: _pending_file_action = PendingFileAction.NONE return false if not is_autosave: + if _scenery_controller != null: + _scenery_controller.commit_placement_session() _paused_edit_autosave_remaining = -1.0 _set_scene_dirty(false) _save_settings() @@ -3211,15 +3235,21 @@ func _build_scenery_inspector(ui: CanvasLayer) -> void: _scenery_depth_section.add_child(_scenery_depth_heading) _scenery_depth_group = STAGE_PANEL_CHOICE_GROUP_SCRIPT.new() as StagePanelChoiceGroup _scenery_depth_group.name = "SceneryDepthChoices" - _scenery_depth_group.add_choice(&"far", "Far") - _scenery_depth_group.add_choice(&"near", "Near") - _scenery_depth_group.add_choice(&"floor", "On the floor") + _scenery_depth_group.configure_layout(true, 118.0, 24, 16, 1, Color("#087DBA")) + _scenery_depth_choice_cues.clear() + var back_cue: Control = _make_scenery_depth_choice_cue(&"far", Vector2(128.0, 82.0)) + var middle_cue: Control = _make_scenery_depth_choice_cue(&"near", Vector2(128.0, 82.0)) + var front_cue: Control = _make_scenery_depth_choice_cue(&"floor", Vector2(128.0, 82.0)) + _scenery_depth_choice_cues = [back_cue, middle_cue, front_cue] + _scenery_depth_group.add_choice(&"far", "Back", back_cue, "Behind other scenery") + _scenery_depth_group.add_choice(&"near", "Middle", middle_cue, "In front of Back · behind tiles") + _scenery_depth_group.add_choice(&"floor", "Front", front_cue, "In front of ground tiles") _scenery_depth_group.apply_theme(_sandbox_theme as SandboxTheme) _scenery_depth_group.choice_selected.connect(_on_selected_scenery_depth_choice) _scenery_depth_section.add_child(_scenery_depth_group) _scenery_depth_helper = Label.new() _scenery_depth_helper.name = "SceneryDepthHelper" - _scenery_depth_helper.text = "Far is behind Near. On the floor is in front of the ground." + _scenery_depth_helper.text = "The pictures show what can cover the artwork." _scenery_depth_helper.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART _scenery_depth_helper.add_theme_font_size_override("font_size", _stage_panel_integer("type.helper_size", 16, 12, 32)) _scenery_depth_helper.add_theme_color_override("font_color", _stage_panel_color("type.muted_text_color", Color("#AAB2C0"))) @@ -3237,9 +3267,14 @@ func _build_scenery_inspector(ui: CanvasLayer) -> void: _scenery_size_row.add_child(_scenery_bigger_button) body.add_child(_scenery_size_row) _scenery_erase_wall_area_button = _make_scenery_repair_button("EraseWallArea", "Erase wall area", true) + _scenery_inspector.style_primary_action_button(_scenery_erase_wall_area_button) _scenery_erase_wall_area_button.pressed.connect(_begin_selected_wall_erase) body.add_child(_scenery_erase_wall_area_button) - _scenery_erase_piece_button = _make_scenery_repair_button("EraseSceneryPiece", "Erase wall panel", true) + _scenery_erase_background_ground_area_button = _make_scenery_repair_button("EraseBackgroundGroundArea", "Erase background ground area", true) + _scenery_inspector.style_primary_action_button(_scenery_erase_background_ground_area_button) + _scenery_erase_background_ground_area_button.pressed.connect(_begin_selected_background_ground_erase) + body.add_child(_scenery_erase_background_ground_area_button) + _scenery_erase_piece_button = _make_scenery_repair_button("EraseSceneryPiece", "Remove wall panel", false, true) _scenery_erase_piece_button.pressed.connect(_request_erase_selected_scenery) body.add_child(_scenery_erase_piece_button) _scenery_find_button = _make_scenery_repair_button("FindArtwork", "Find artwork…", true) @@ -3272,16 +3307,7 @@ func _build_scenery_inspector(ui: CanvasLayer) -> void: _scenery_repair_confirm_dialog.canceled.connect(_cancel_scenery_repair_preview) ui.add_child(_scenery_repair_confirm_dialog) _apply_dialog_theme(_scenery_repair_confirm_dialog) - _scenery_erase_piece_dialog = ConfirmationDialog.new() - _scenery_erase_piece_dialog.name = "EraseSceneryPieceDialog" - _scenery_erase_piece_dialog.title = "Erase this piece?" - _scenery_erase_piece_dialog.ok_button_text = "Erase" - _scenery_erase_piece_dialog.cancel_button_text = "Keep it" - _scenery_erase_piece_dialog.min_size = _theme_dialog_size("confirm_size", Vector2i(520, 260)) - _scenery_erase_piece_dialog.confirmed.connect(_confirm_erase_selected_scenery) - _scenery_erase_piece_dialog.canceled.connect(_clear_erase_scenery_targets) - ui.add_child(_scenery_erase_piece_dialog) - _apply_dialog_theme(_scenery_erase_piece_dialog) + _build_scenery_erase_piece_dialog(ui) _surface_color_dialog = STAGE_SURFACE_COLOR_DIALOG_SCRIPT.new() as StageSurfaceColorDialog _surface_color_dialog.name = "ScenerySurfaceColorDialog" ui.add_child(_surface_color_dialog) @@ -3301,10 +3327,314 @@ func _make_scenery_section_heading(label_text: String) -> Label: return heading +func _make_scenery_depth_choice_cue(depth: StringName, cue_size: Vector2) -> Control: + var cue: StageDepthChoicePreview = STAGE_DEPTH_CHOICE_PREVIEW_SCRIPT.new() as StageDepthChoicePreview + cue.configure(depth, _scenery_depth_preview_texture, cue_size) + return cue + + +func _make_scenery_depth_capture_action(action_id: StringName, depth: StringName, label: String, selected_depth: StringName) -> Dictionary: + return { + "id": action_id, + "label": label, + "choice": true, + "selected": depth == selected_depth, + "cue": _make_scenery_depth_choice_cue(depth, Vector2(38.0, 34.0)), + } + + +func _set_scenery_depth_preview_path(path: String) -> void: + var canonical_path: String = CONTENT_PATHS.canonicalize(path) if not path.is_empty() else "" + if canonical_path == _scenery_depth_preview_path and (_scenery_depth_preview_texture != null or _scenery_depth_preview_busy or _scenery_depth_preview_scheduled_generation == _scenery_depth_preview_generation): + return + if canonical_path != _scenery_depth_preview_path: + _scenery_depth_preview_path = canonical_path + _scenery_depth_preview_texture = null + _scenery_depth_preview_generation += 1 + _update_scenery_depth_choice_cues() + if canonical_path.is_empty() or not FileAccess.file_exists(canonical_path): + return + if not _scenery_depth_preview_busy: + _scenery_depth_preview_scheduled_generation = _scenery_depth_preview_generation + call_deferred("_render_scenery_depth_preview", canonical_path, _scenery_depth_preview_generation) + + +func _render_scenery_depth_preview(path: String, generation: int) -> void: + if _scenery_depth_preview_busy or path.is_empty(): + return + _scenery_depth_preview_scheduled_generation = -1 + _scenery_depth_preview_busy = true + if _scenery_depth_thumbnail_renderer == null: + _scenery_depth_thumbnail_renderer = PROP_THUMBNAIL.new() as PropThumbnail + _scenery_depth_thumbnail_renderer.name = "SceneryDepthThumbnailRenderer" + add_child(_scenery_depth_thumbnail_renderer) + await get_tree().process_frame + var parsed: Dictionary = PROP_FILE_SCRIPT.from_file_dict(PROP_FILE_SCRIPT.load(path)) + var shapes: Array[Dictionary] = parsed.get("shapes", []) as Array[Dictionary] + var texture: Texture2D + if not shapes.is_empty() and _scenery_depth_thumbnail_renderer != null: + texture = await _scenery_depth_thumbnail_renderer.render_shapes(shapes) + if generation == _scenery_depth_preview_generation and path == _scenery_depth_preview_path and texture != null: + _scenery_depth_preview_texture = texture + _update_scenery_depth_choice_cues() + if _scenery_controller != null and _scenery_controller.get_active_tool() == &"artwork": + _refresh_scenery_capture_bar() + _scenery_depth_preview_busy = false + if not _scenery_depth_preview_path.is_empty() and _scenery_depth_preview_path != path: + _scenery_depth_preview_scheduled_generation = _scenery_depth_preview_generation + call_deferred("_render_scenery_depth_preview", _scenery_depth_preview_path, _scenery_depth_preview_generation) + + +func _update_scenery_depth_choice_cues() -> void: + for cue_control: Control in _scenery_depth_choice_cues: + if cue_control is StageDepthChoicePreview: + (cue_control as StageDepthChoicePreview).set_artwork_texture(_scenery_depth_preview_texture) + + +func _build_scenery_erase_piece_dialog(ui: CanvasLayer) -> void: + _scenery_erase_piece_dialog = Control.new() + _scenery_erase_piece_dialog.name = "EraseSceneryPieceDialog" + _scenery_erase_piece_dialog.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + _scenery_erase_piece_dialog.mouse_filter = Control.MOUSE_FILTER_STOP + _scenery_erase_piece_dialog.z_index = 100 + _scenery_erase_piece_dialog.visible = false + ui.add_child(_scenery_erase_piece_dialog) + + var backdrop: ColorRect = ColorRect.new() + backdrop.name = "DialogBackdrop" + backdrop.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + backdrop.color = _confirmation_dialog_color("backdrop_color", Color("#10151D66")) + backdrop.mouse_filter = Control.MOUSE_FILTER_STOP + _scenery_erase_piece_dialog.add_child(backdrop) + + var center: CenterContainer = CenterContainer.new() + center.name = "DialogCenter" + center.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + center.mouse_filter = Control.MOUSE_FILTER_IGNORE + _scenery_erase_piece_dialog.add_child(center) + + _scenery_erase_piece_dialog_panel = PanelContainer.new() + _scenery_erase_piece_dialog_panel.name = "DialogCard" + center.add_child(_scenery_erase_piece_dialog_panel) + + var margin: MarginContainer = MarginContainer.new() + margin.name = "DialogMargin" + var padding: int = _confirmation_dialog_integer("padding", 18, 12, 32) + margin.add_theme_constant_override("margin_left", padding) + margin.add_theme_constant_override("margin_right", padding) + margin.add_theme_constant_override("margin_top", padding) + margin.add_theme_constant_override("margin_bottom", padding) + _scenery_erase_piece_dialog_panel.add_child(margin) + + var layout: VBoxContainer = VBoxContainer.new() + layout.name = "DialogLayout" + layout.size_flags_horizontal = Control.SIZE_EXPAND_FILL + layout.size_flags_vertical = Control.SIZE_EXPAND_FILL + layout.add_theme_constant_override("separation", _confirmation_dialog_integer("body_gap", 10, 4, 24)) + margin.add_child(layout) + + _scenery_erase_piece_dialog_title = Label.new() + _scenery_erase_piece_dialog_title.name = "DialogTitle" + _scenery_erase_piece_dialog_title.text = "Remove this scenery?" + _scenery_erase_piece_dialog_title.size_flags_horizontal = Control.SIZE_EXPAND_FILL + _scenery_erase_piece_dialog_title.add_theme_font_size_override("font_size", _confirmation_dialog_integer("title_font_size", 26, 20, 40)) + _scenery_erase_piece_dialog_title.add_theme_color_override("font_color", _stage_panel_color("type.text_color", Color("#F5F7FA"))) + _apply_ui_font(_scenery_erase_piece_dialog_title) + var title_font: Font = _ui_font + if title_font != null: + var strong_title_font: FontVariation = FontVariation.new() + strong_title_font.base_font = title_font + strong_title_font.variation_embolden = 0.55 + _scenery_erase_piece_dialog_title.add_theme_font_override("font", strong_title_font) + layout.add_child(_scenery_erase_piece_dialog_title) + + _scenery_erase_piece_dialog_message = Label.new() + _scenery_erase_piece_dialog_message.name = "DialogMessage" + _scenery_erase_piece_dialog_message.text = "Remove this whole piece? You can’t undo this." + _scenery_erase_piece_dialog_message.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + _scenery_erase_piece_dialog_message.size_flags_horizontal = Control.SIZE_EXPAND_FILL + _scenery_erase_piece_dialog_message.add_theme_font_size_override("font_size", _confirmation_dialog_integer("message_font_size", 20, 16, 32)) + _scenery_erase_piece_dialog_message.add_theme_color_override("font_color", _stage_panel_color("type.text_color", Color("#F5F7FA"))) + _apply_ui_font(_scenery_erase_piece_dialog_message) + layout.add_child(_scenery_erase_piece_dialog_message) + var actions_spacer: Control = Control.new() + actions_spacer.name = "ActionsSpacer" + actions_spacer.size_flags_vertical = Control.SIZE_EXPAND_FILL + layout.add_child(actions_spacer) + + _scenery_erase_piece_dialog_actions = BoxContainer.new() + _scenery_erase_piece_dialog_actions.name = "DialogActions" + _scenery_erase_piece_dialog_actions.alignment = BoxContainer.ALIGNMENT_END + _scenery_erase_piece_dialog_actions.add_theme_constant_override("separation", _confirmation_dialog_integer("footer_gap", 12, 8, 24)) + layout.add_child(_scenery_erase_piece_dialog_actions) + + _scenery_erase_piece_keep_button = Button.new() + _scenery_erase_piece_keep_button.name = "KeepSceneryButton" + _scenery_erase_piece_keep_button.text = "Keep it" + _scenery_erase_piece_keep_button.focus_mode = Control.FOCUS_ALL + _scenery_erase_piece_keep_button.pressed.connect(_dismiss_scenery_erase_piece_dialog) + _scenery_erase_piece_dialog_actions.add_child(_scenery_erase_piece_keep_button) + + _scenery_erase_piece_remove_button = Button.new() + _scenery_erase_piece_remove_button.name = "RemoveSceneryButton" + _scenery_erase_piece_remove_button.text = "Remove scenery" + _scenery_erase_piece_remove_button.focus_mode = Control.FOCUS_ALL + _scenery_erase_piece_remove_button.pressed.connect(_confirm_erase_scenery_from_dialog) + _scenery_erase_piece_dialog_actions.add_child(_scenery_erase_piece_remove_button) + _apply_scenery_erase_piece_dialog_style() + if not get_tree().root.size_changed.is_connected(_refresh_scenery_erase_piece_dialog_size): + get_tree().root.size_changed.connect(_refresh_scenery_erase_piece_dialog_size) + + +func _apply_scenery_erase_piece_dialog_style() -> void: + if _scenery_erase_piece_dialog == null: + return + var shell: StyleBoxFlat = StyleBoxFlat.new() + shell.bg_color = _confirmation_dialog_color("surface_color", Color("#202633FA")) + shell.border_color = _confirmation_dialog_color("border_color", Color("#AAB2C066")) + shell.set_border_width_all(_confirmation_dialog_integer("border_width", 1, 1, 4)) + shell.set_corner_radius_all(_confirmation_dialog_integer("corner_radius", 16, 10, 28)) + _scenery_erase_piece_dialog_panel.add_theme_stylebox_override("panel", shell) + _apply_scenery_erase_piece_action_style(_scenery_erase_piece_keep_button, true) + _apply_scenery_erase_piece_action_style(_scenery_erase_piece_remove_button, false) + + +func _apply_scenery_erase_piece_action_style(button: Button, primary: bool) -> void: + if button == null: + return + var accent: Color = _confirmation_dialog_color("accent_color", Color("#22C6FF")) + var blue: Color = _confirmation_dialog_color("action_color", Color("#087DBA")) + var danger: Color = _confirmation_dialog_color("danger_color", Color("#FF626B")) + var neutral: Color = _stage_panel_color("action.surface_color", Color("#171B24")) + var hover_neutral: Color = _confirmation_dialog_color("danger_hover_surface", Color("#3B252C")) + var pressed_neutral: Color = _confirmation_dialog_color("danger_pressed_surface", Color("#4A252D")) + var border_width: int = _confirmation_dialog_integer("button_border_width", 2, 1, 5) + var radius: int = _confirmation_dialog_integer("button_radius", 12, 8, 24) + var normal_color: Color = blue if primary else neutral + var hover_color: Color = blue.lightened(0.12) if primary else hover_neutral + var pressed_color: Color = blue.darkened(0.12) if primary else pressed_neutral + var edge_color: Color = accent if primary else danger + button.add_theme_stylebox_override("normal", _make_scenery_erase_button_style(normal_color, edge_color, border_width, radius)) + button.add_theme_stylebox_override("hover", _make_scenery_erase_button_style(hover_color, edge_color.lightened(0.12), border_width + 1, radius)) + button.add_theme_stylebox_override("pressed", _make_scenery_erase_button_style(pressed_color, edge_color, border_width + 1, radius)) + button.add_theme_stylebox_override("hover_pressed", _make_scenery_erase_button_style(pressed_color.lightened(0.05), edge_color, border_width + 1, radius)) + button.add_theme_stylebox_override("focus", _make_scenery_erase_button_style(normal_color, accent, border_width + 1, radius)) + var label_color: Color = Color.WHITE if primary else danger + button.add_theme_color_override("font_color", label_color) + button.add_theme_color_override("font_hover_color", label_color) + button.add_theme_color_override("font_pressed_color", label_color) + button.add_theme_font_size_override("font_size", _confirmation_dialog_integer("button_font_size", 20, 16, 32)) + button.custom_minimum_size = Vector2( + float(_confirmation_dialog_integer("remove_button_width", 170, 128, 240)) if not primary else float(_confirmation_dialog_integer("keep_button_width", 132, 112, 220)), + float(_confirmation_dialog_integer("button_height", 48, 44, 80)) + ) + var font: Font = _ui_font + if primary and font != null: + var strong_font: FontVariation = FontVariation.new() + strong_font.base_font = font + strong_font.variation_embolden = 0.55 + button.add_theme_font_override("font", strong_font) + else: + _apply_ui_font(button) + + +func _make_scenery_erase_button_style(background: Color, border: Color, width: int, radius: int) -> StyleBoxFlat: + var style: StyleBoxFlat = StyleBoxFlat.new() + style.bg_color = background + style.border_color = border + style.set_border_width_all(width) + style.set_corner_radius_all(radius) + style.content_margin_left = 12.0 + style.content_margin_right = 12.0 + style.content_margin_top = 8.0 + style.content_margin_bottom = 8.0 + return style + + +func _confirmation_dialog_integer(token: String, fallback: int, minimum: int, maximum: int) -> int: + return fallback if _sandbox_theme == null else _sandbox_theme.integer("popups.confirmation." + token, fallback, minimum, maximum) + + +func _confirmation_dialog_color(token: String, fallback: Color) -> Color: + return fallback if _sandbox_theme == null else _sandbox_theme.color("popups.confirmation." + token, fallback) + + +func _open_scenery_erase_piece_dialog() -> void: + if _scenery_erase_piece_dialog == null: + return + var popup_size: Vector2i = _scenery_erase_piece_dialog_size() + _apply_scenery_erase_piece_dialog_layout(popup_size.x) + if _scenery_erase_piece_dialog_panel != null: + _scenery_erase_piece_dialog_panel.custom_minimum_size = Vector2(popup_size) + _scenery_erase_piece_dialog.visible = true + call_deferred("_focus_scenery_erase_piece_keep_button") + + +func _scenery_erase_piece_dialog_size() -> Vector2i: + var preferred: Vector2i = Vector2i(520, 208) + var raw_size: Variant = _sandbox_theme.value("popups.confirmation.scenery_erase_size", [520, 208]) if _sandbox_theme != null else [520, 208] + if raw_size is Array and (raw_size as Array).size() == 2: + preferred = Vector2i(int((raw_size as Array)[0]), int((raw_size as Array)[1])) + var available: Vector2i = Vector2i(get_tree().root.size) + var usable_rect: Rect2i = DisplayServer.screen_get_usable_rect(DisplayServer.window_get_current_screen()) + if usable_rect.size.x > 0 and usable_rect.size.y > 0: + available.x = mini(available.x, usable_rect.size.x) + available.y = mini(available.y, usable_rect.size.y) + available = Vector2i(maxi(1, available.x - 32), maxi(1, available.y - 32)) + var width: int = mini(preferred.x, available.x) + var height: int = mini(preferred.y, available.y) + if width < 440: + height = mini(available.y, height + 56) + return Vector2i(width, height) + + +func _apply_scenery_erase_piece_dialog_layout(dialog_width: int) -> void: + if _scenery_erase_piece_dialog_actions == null or _scenery_erase_piece_keep_button == null or _scenery_erase_piece_remove_button == null: + return + var stacked: bool = dialog_width < 440 + _scenery_erase_piece_dialog_actions.vertical = stacked + _scenery_erase_piece_dialog_actions.alignment = BoxContainer.ALIGNMENT_END + _scenery_erase_piece_dialog_actions.add_theme_constant_override("separation", 8 if stacked else _confirmation_dialog_integer("footer_gap", 12, 8, 24)) + if stacked: + var button_width: float = maxf(1.0, float(dialog_width - _confirmation_dialog_integer("padding", 18, 12, 32) * 2)) + var button_height: float = float(_confirmation_dialog_integer("button_height", 48, 44, 80)) + _scenery_erase_piece_remove_button.custom_minimum_size = Vector2(button_width, button_height) + _scenery_erase_piece_keep_button.custom_minimum_size = Vector2(button_width, button_height) + else: + _scenery_erase_piece_remove_button.custom_minimum_size.x = float(_confirmation_dialog_integer("remove_button_width", 170, 128, 240)) + _scenery_erase_piece_keep_button.custom_minimum_size.x = float(_confirmation_dialog_integer("keep_button_width", 132, 112, 220)) + + +func _refresh_scenery_erase_piece_dialog_size() -> void: + if _scenery_erase_piece_dialog != null and _scenery_erase_piece_dialog.visible: + _open_scenery_erase_piece_dialog() + + +func _focus_scenery_erase_piece_keep_button() -> void: + if _scenery_erase_piece_dialog != null and _scenery_erase_piece_dialog.visible and _scenery_erase_piece_keep_button != null: + _scenery_erase_piece_keep_button.grab_focus() + + +func _dismiss_scenery_erase_piece_dialog() -> void: + if _scenery_erase_piece_dialog != null and _scenery_erase_piece_dialog.visible: + _scenery_erase_piece_dialog.hide() + _clear_erase_scenery_targets() + + +func _confirm_erase_scenery_from_dialog() -> void: + _confirm_erase_selected_scenery() + if _scenery_erase_piece_dialog != null and _scenery_erase_piece_dialog.visible: + _scenery_erase_piece_dialog.hide() + + func _make_surface_choice_cue(kind: StringName, finish: StringName) -> StageSurfacePreview: var cue: StageSurfacePreview = STAGE_SURFACE_PREVIEW_SCRIPT.new() as StageSurfacePreview cue.custom_minimum_size = Vector2(64.0, 40.0) cue.size_flags_horizontal = Control.SIZE_SHRINK_CENTER + cue.set_rounded_frame( + float(_sandbox_theme.integer("popups.asset_selector.look_corner_radius", 8, 2, 20)) if _sandbox_theme != null else 8.0, + _sandbox_theme.color("popups.style_defaults.bg_color", Color("#202633FA")) if _sandbox_theme != null else Color("#202633FA") + ) cue.configure(kind, finish, STAGE_SURFACE_RENDERER_SCRIPT.default_tint(finish)) return cue @@ -3319,14 +3649,17 @@ func _surface_color_presets() -> Array[Color]: return colors -func _make_scenery_repair_button(node_name: String, label: String, prominent: bool) -> Button: +func _make_scenery_repair_button(node_name: String, label: String, prominent: bool, destructive: bool = false) -> Button: var button: Button = Button.new() button.name = node_name button.text = label button.focus_mode = Control.FOCUS_ALL button.size_flags_horizontal = Control.SIZE_EXPAND_FILL button.custom_minimum_size.y = float(_stage_panel_integer("action.min_target_size", 48, 44, 96)) - _scenery_inspector.style_action_button(button, prominent) + if destructive: + _scenery_inspector.style_destructive_button(button) + else: + _scenery_inspector.style_action_button(button, prominent) return button @@ -3365,6 +3698,65 @@ func _build_use_target_capture_bar(ui: CanvasLayer) -> void: _use_target_capture_bar.change_prop_requested.connect(_choose_different_use_prop) _use_target_capture_bar.capture_action_requested.connect(_on_capture_action_requested) ui.add_child(_use_target_capture_bar) + _build_scenery_depth_cursor_badge(ui) + + +func _build_scenery_depth_cursor_badge(ui: CanvasLayer) -> void: + _scenery_depth_cursor_badge = PanelContainer.new() + _scenery_depth_cursor_badge.name = "SceneryDepthCursorBadge" + _scenery_depth_cursor_badge.mouse_filter = Control.MOUSE_FILTER_IGNORE + _scenery_depth_cursor_badge.z_index = 2 + var style := StyleBoxFlat.new() + style.bg_color = _stage_panel_color("choice.selected_surface", Color("#12394A")) + style.border_color = _sandbox_theme.color("mode_colors.edit", Color("#22C6FF")) if _sandbox_theme != null else Color("#22C6FF") + style.set_border_width_all(1) + style.set_corner_radius_all(10) + style.content_margin_left = 9.0 + style.content_margin_right = 9.0 + style.content_margin_top = 3.0 + style.content_margin_bottom = 3.0 + _scenery_depth_cursor_badge.add_theme_stylebox_override("panel", style) + _scenery_depth_cursor_badge_label = Label.new() + _scenery_depth_cursor_badge_label.name = "DepthLabel" + _scenery_depth_cursor_badge_label.text = "MIDDLE" + _scenery_depth_cursor_badge_label.add_theme_font_size_override("font_size", 13) + _scenery_depth_cursor_badge_label.add_theme_color_override("font_color", Color("#F5F7FA")) + _apply_ui_font(_scenery_depth_cursor_badge_label) + _scenery_depth_cursor_badge.add_child(_scenery_depth_cursor_badge_label) + _scenery_depth_cursor_badge.visible = false + ui.add_child(_scenery_depth_cursor_badge) + + +func _update_scenery_depth_cursor_badge() -> void: + if _scenery_depth_cursor_badge == null or _scenery_controller == null: + return + if current_mode != StageMode.EDIT or _scenery_controller.get_active_tool() != &"artwork": + _scenery_depth_cursor_badge.visible = false + return + var mouse_position: Vector2 = get_viewport().get_mouse_position() + if _use_target_capture_bar != null and _use_target_capture_bar.visible and _use_target_capture_bar.get_global_rect().has_point(mouse_position): + _scenery_depth_cursor_badge.visible = false + return + var ghost_bounds: Rect2 = _scenery_controller.get_active_artwork_preview_bounds() + if ghost_bounds.size.x <= 0.0 or ghost_bounds.size.y <= 0.0: + _scenery_depth_cursor_badge.visible = false + return + var depth: StringName = _scenery_controller.get_artwork_depth() + var depth_label: String = "Back" if depth == &"far" else "Front" if depth == &"floor" else "Middle" + _scenery_depth_cursor_badge_label.text = depth_label.to_upper() + var badge_size: Vector2 = _scenery_depth_cursor_badge.get_combined_minimum_size() + _scenery_depth_cursor_badge.size = badge_size + var viewport_size: Vector2 = get_viewport_rect().size + var screen_top_left: Vector2 = _world_to_screen(ghost_bounds.position) + var screen_bottom_right: Vector2 = _world_to_screen(ghost_bounds.end) + var ghost_screen_rect: Rect2 = Rect2(screen_top_left, screen_bottom_right - screen_top_left).abs() + var badge_position := Vector2(ghost_screen_rect.get_center().x - badge_size.x * 0.5, ghost_screen_rect.position.y - badge_size.y - 8.0) + if badge_position.y < 8.0: + badge_position.y = ghost_screen_rect.position.y + 8.0 + badge_position.x = clampf(badge_position.x, 8.0, maxf(8.0, viewport_size.x - badge_size.x - 8.0)) + badge_position.y = clampf(badge_position.y, 8.0, maxf(8.0, viewport_size.y - badge_size.y - 8.0)) + _scenery_depth_cursor_badge.position = badge_position + _scenery_depth_cursor_badge.visible = true func _build_ground_finish_popup(ui: CanvasLayer) -> void: @@ -4527,7 +4919,7 @@ func _refresh_scenery_inspector() -> void: _scenery_wall_look_group.visible = is_wall _scenery_background_ground_look_heading.visible = is_background_ground _scenery_background_ground_look_group.visible = is_background_ground - _scenery_surface_helper.visible = surface != null + _scenery_surface_helper.visible = is_wall _scenery_surface_colors.visible = false if surface != null: if is_wall: @@ -4535,7 +4927,6 @@ func _refresh_scenery_inspector() -> void: _scenery_surface_helper.text = "Looks like a wall · Stickmen can pass through." else: _scenery_background_ground_look_group.select_no_signal(surface.finish_id) - _scenery_surface_helper.text = "Decorative only · Stickmen don’t walk here." var color_editable: bool = is_wall or surface.finish_id == &"finish_background_carpet" _scenery_surface_colors.visible = color_editable if color_editable: @@ -4543,16 +4934,18 @@ func _refresh_scenery_inspector() -> void: _scenery_surface_colors.set_recent_colors(_recent_surface_colors) _scenery_depth_section.visible = is_artwork if artwork != null: + _set_scenery_depth_preview_path(artwork.asset_path) _scenery_depth_group.select_no_signal(artwork.depth) _scenery_size_row.visible = not is_missing or artwork.has_known_bounds _scenery_smaller_button.disabled = is_missing and not artwork.has_known_bounds _scenery_bigger_button.disabled = is_missing and not artwork.has_known_bounds _scenery_erase_wall_area_button.visible = is_wall + _scenery_erase_background_ground_area_button.visible = is_background_ground _scenery_erase_piece_button.visible = surface != null if is_background_ground: - _scenery_erase_piece_button.text = "Erase background ground" + _scenery_erase_piece_button.text = "Remove background ground" else: - _scenery_erase_piece_button.text = "Erase wall panel" + _scenery_erase_piece_button.text = "Remove wall panel" _scenery_find_button.visible = is_missing _scenery_choose_button.visible = is_missing _scenery_keep_button.visible = is_missing @@ -4595,6 +4988,16 @@ func _is_scenery_node(node: Node2D) -> bool: return node != null and (node.get_script() == STAGE_SCENERY_ARTWORK_SCRIPT or node.get_script() == STAGE_WALL_PANEL_SCRIPT or node.get_script() == STAGE_BACKGROUND_GROUND_SCRIPT) +func _has_remaining_scenery() -> bool: + if _world == null or not is_instance_valid(_world): + return false + for child: Node in _world.get_children(): + var node: Node2D = child as Node2D + if node != null and not node.is_queued_for_deletion() and _is_scenery_node(node): + return true + return false + + func _on_selected_surface_look_choice(finish: StringName) -> void: var surface: StageRectSurface = _selected_rect_surface() if surface == null: @@ -4737,10 +5140,10 @@ func _request_erase_selected_scenery() -> void: _scenery_erase_piece_targets.append(weakref(node)) var count: int = targets.size() if count == 1: - _scenery_erase_piece_dialog.dialog_text = "Erase this piece? You can’t undo this." + _scenery_erase_piece_dialog_message.text = "Remove this whole piece? You can’t undo this." else: - _scenery_erase_piece_dialog.dialog_text = "Erase these %d scenery pieces? You can’t undo this." % count - _scenery_erase_piece_dialog.popup_centered() + _scenery_erase_piece_dialog_message.text = "Remove these %d scenery pieces? You can’t undo this." % count + _open_scenery_erase_piece_dialog() func _confirm_erase_selected_scenery() -> void: @@ -4764,7 +5167,10 @@ func _confirm_erase_selected_scenery() -> void: _director_visuals.mark_dirty() object_deleted.emit(erased) _mark_scene_dirty() - _refresh_scenery_inspector() + if not _has_remaining_scenery(): + _exit_scenery_edit_mode() + else: + _refresh_scenery_inspector() _refresh_status() @@ -4790,13 +5196,31 @@ func _begin_selected_wall_erase() -> void: _begin_wall_erase_tool() +func _begin_selected_background_ground_erase() -> void: + var selected: Array[Node2D] = _selection.get_selected() if _selection != null else [] + if selected.size() != 1 or selected[0].get_script() != STAGE_BACKGROUND_GROUND_SCRIPT or _scenery_controller == null: + return + _begin_background_ground_erase_tool() + + func _begin_wall_erase_tool() -> void: + _begin_surface_erase_tool(&"wall") + + +func _begin_background_ground_erase_tool() -> void: + _begin_surface_erase_tool(&"background_ground") + + +func _begin_surface_erase_tool(surface_kind: StringName) -> void: if _scenery_controller == null or not _scenery_edit_mode: return _selection.clear_selection() _scenery_inspector.hide() - if not _scenery_controller.begin_wall_eraser(_viewport_to_world(get_viewport().get_mouse_position())): - _show_toast("Could not start the wall eraser. Try again.") + var pointer_world: Vector2 = _viewport_to_world(get_viewport().get_mouse_position()) + var started: bool = _scenery_controller.begin_surface_eraser(pointer_world, surface_kind) + if not started: + var surface_label: String = "wall and background ground" if surface_kind == &"all" else ("wall" if surface_kind == &"wall" else "background ground") + _show_toast("Could not start the %s eraser. Try again." % surface_label) _refresh_scenery_capture_bar() @@ -4926,7 +5350,6 @@ func _on_scenery_placement_committed(node: Node2D) -> void: if node == null or not is_instance_valid(node): return _scenery_placed_count += 1 - _mark_scene_dirty() _refresh_scenery_capture_bar() @@ -4941,34 +5364,40 @@ func _refresh_scenery_capture_bar() -> void: _use_target_capture_bar.clear_horizontal_safe_bounds() var actions: Array[Dictionary] = [] if _scenery_controller.get_active_tool() == &"artwork": - actions.append({"id": "depth_far", "label": "Far"}) - actions.append({"id": "depth_near", "label": "Near"}) - actions.append({"id": "depth_floor", "label": "On the floor"}) + _set_scenery_depth_preview_path(_scenery_controller.get_active_artwork_path()) + var selected_depth: StringName = _scenery_controller.get_artwork_depth() + actions.append(_make_scenery_depth_capture_action(&"depth_far", &"far", "Back", selected_depth)) + actions.append(_make_scenery_depth_capture_action(&"depth_near", &"near", "Middle", selected_depth)) + actions.append(_make_scenery_depth_capture_action(&"depth_floor", &"floor", "Front", selected_depth)) var artwork_name: String = String(_scenery_controller.get_active_artwork_name()) - var prompt: String = "Click to place %s" % artwork_name if _scenery_placed_count == 0 else "Place another %s · placed pieces stay" % artwork_name + var prompt: String = "Click to place %s" % artwork_name if _scenery_placed_count == 0 else "Place another · Cancel undoes" actions.append({"id": "done_tool", "label": "Done", "primary": true}) - actions.append({"id": "cancel_tool", "label": "✕ Cancel"}) - _use_target_capture_bar.set_capture_actions(prompt, actions) - elif _scenery_controller.get_active_tool() == &"wall_eraser": - var erase_prompt: String = "Drag over the wall · release to erase" - if _scenery_controller.get_wall_areas_erased() > 0: - erase_prompt = "Erase another area · earlier openings stay" + actions.append({"id": "cancel_tool", "label": "Cancel"}) + _use_target_capture_bar.set_scenery_capture_actions(prompt, actions) + elif _scenery_controller.get_active_tool() in [&"wall_eraser", &"background_ground_eraser", &"scenery_eraser"]: + var erase_surface: String = _scenery_controller.get_active_eraser_surface_label() + var erase_prompt: String = "Drag over the %s · release to erase" % erase_surface + if _scenery_controller.get_active_tool() == &"scenery_eraser": + erase_prompt = "Drag over wall or ground to erase" + if _scenery_controller.get_surface_areas_erased() > 0: + erase_prompt = "Erase another area · Cancel undoes" actions.append({"id": "done_tool", "label": "Done", "primary": true}) - actions.append({"id": "cancel_tool", "label": "✕ Cancel"}) - _use_target_capture_bar.set_capture_actions(erase_prompt, actions) + actions.append({"id": "cancel_tool", "label": "Cancel"}) + _use_target_capture_bar.set_scenery_capture_actions(erase_prompt, actions) else: + actions.append({"id": "choose_scenery", "label": "Choose scenery"}) actions.append({"id": "done_tool", "label": "Done", "primary": true}) - actions.append({"id": "cancel_tool", "label": "✕ Cancel"}) + actions.append({"id": "cancel_tool", "label": "Cancel"}) if _scenery_controller.get_active_tool() == &"background_ground": var ground_prompt: String = "Draw background ground · No walking" if _scenery_placed_count > 0: - ground_prompt = "Draw another · placed pieces stay" - _use_target_capture_bar.set_capture_actions(ground_prompt, actions) + ground_prompt = "Draw another · Cancel undoes" + _use_target_capture_bar.set_scenery_capture_actions(ground_prompt, actions) else: var wall_prompt: String = "Draw wall panel · Stickmen pass through" if _scenery_placed_count > 0: - wall_prompt = "Draw another · placed pieces stay" - _use_target_capture_bar.set_capture_actions(wall_prompt, actions) + wall_prompt = "Draw another · Cancel undoes" + _use_target_capture_bar.set_scenery_capture_actions(wall_prompt, actions) return if _scenery_controller != null and _scenery_controller.is_group_preview_pending(): _use_target_capture_bar.clear_horizontal_safe_bounds() @@ -4976,52 +5405,24 @@ func _refresh_scenery_capture_bar() -> void: {"id": "keep_group", "label": "Keep changes", "primary": true}, {"id": "cancel_group", "label": "✕ Cancel"}, ] - var group_prompt: String = "Move together · Ground and Props stay put" if _scenery_group_mode == &"move" else "Resize together · Ground and Props stay put" - _use_target_capture_bar.set_capture_actions(group_prompt, preview_actions) + var group_count: int = _selection.get_selected().size() + var group_verb: String = "Move" if _scenery_group_mode == &"move" else "Resize" + var group_prompt: String = "%s %d scenery pieces\nGround and Props stay put" % [group_verb, group_count] + _use_target_capture_bar.set_scenery_capture_actions(group_prompt, preview_actions) return if _scenery_controller != null and _scenery_controller.is_single_transform_active(): - _set_scenery_selection_capture_bounds() - var target: Node2D = _scenery_controller.get_single_transform_node() - var target_name: String = "scenery" - if target != null and target.get_script() == STAGE_SCENERY_ARTWORK_SCRIPT: - target_name = (target as StageSceneryArtwork).display_name - elif target != null and target.get_script() == STAGE_BACKGROUND_GROUND_SCRIPT: - target_name = "background ground" - elif target != null: - target_name = "wall panel" - var is_move: bool = _scenery_controller.get_single_transform_kind() == &"move" - var prompt_verb: String = "Move" if is_move else "Resize" - var release_verb: String = "place" if is_move else "resize" - var cancel_actions: Array[Dictionary] = [{"id": "cancel_single", "label": "✕ Cancel"}] - _use_target_capture_bar.set_capture_actions("%s %s · release to %s" % [prompt_verb, target_name, release_verb], cancel_actions) + _hide_scenery_capture_bar() return if _scenery_edit_mode: var count: int = _selection.get_selected().size() - if count == 1: - _set_scenery_selection_capture_bounds() - else: - _use_target_capture_bar.clear_horizontal_safe_bounds() - var prompt: String = "%d scenery piece%s selected" % [count, "" if count == 1 else "s"] - var edit_actions: Array[Dictionary] = [] - if count > 1: - edit_actions.append({"id": "move_group", "label": "Move together"}) - edit_actions.append({"id": "resize_group", "label": "Resize together"}) - edit_actions.append({"id": "clear_selection", "label": "Clear selection"}) - else: - edit_actions.append({"id": "select_several", "label": "Select several"}) - edit_actions.append({"id": "select_all", "label": "Select all scenery"}) - if count > 0: - edit_actions.append({"id": "clear_selection", "label": "Clear selection"}) - else: - edit_actions.append({"id": "erase_wall_area", "label": "Erase wall area"}) - edit_actions.append({"id": "done_edit", "label": "Done editing", "primary": true}) - _use_target_capture_bar.set_capture_actions(prompt, edit_actions) + _set_scenery_selection_capture_bounds(count == 1) + _use_target_capture_bar.set_scenery_edit_state(count) return _use_target_capture_bar.clear_horizontal_safe_bounds() _use_target_capture_bar.hide_capture() -func _set_scenery_selection_capture_bounds() -> void: +func _set_scenery_selection_capture_bounds(include_inspector: bool = true) -> void: if _use_target_capture_bar == null: return var viewport_width: float = get_viewport_rect().size.x @@ -5030,7 +5431,9 @@ func _set_scenery_selection_capture_bounds() -> void: var palette_width: float = float(palette_config.get("dock_width", 116.0)) var panel_width: float = float(_stage_panel_integer("shell.preferred_width", 380, 220, 720)) var left: float = palette_width + inset - var right: float = viewport_width - panel_width - inset * 2.0 + var right: float = viewport_width - inset + if include_inspector: + right = viewport_width - panel_width - inset * 2.0 _use_target_capture_bar.set_horizontal_safe_bounds(left, right) @@ -5042,6 +5445,8 @@ func _hide_scenery_capture_bar() -> void: func _on_capture_action_requested(action_id: StringName) -> void: match action_id: + &"choose_scenery": + _open_selector("scenery") &"depth_far": _scenery_controller.set_depth(&"far") _refresh_scenery_capture_bar() @@ -5051,9 +5456,12 @@ func _on_capture_action_requested(action_id: StringName) -> void: &"depth_floor": _scenery_controller.set_depth(&"floor") _refresh_scenery_capture_bar() - &"done_tool", &"cancel_tool": + &"done_tool": _scenery_controller.exit_tool() _hide_scenery_capture_bar() + &"cancel_tool": + _scenery_controller.cancel_active_tool() + _hide_scenery_capture_bar() &"select_several": _scenery_multi_select = true _show_toast("Tap scenery pieces to add them. Shift-click also works.") @@ -5065,27 +5473,30 @@ func _on_capture_action_requested(action_id: StringName) -> void: _refresh_scenery_capture_bar() &"erase_wall_area": _begin_wall_erase_tool() + &"erase_area": + _begin_surface_erase_tool(&"all") &"move_group": _begin_scenery_group_transform(&"move") &"resize_group": _begin_scenery_group_transform(&"resize") &"keep_group": - _scenery_controller.commit_group() - _scenery_group_mode = &"" - _refresh_scenery_capture_bar() + _keep_scenery_group_preview() &"cancel_group": _scenery_controller.cancel_group() _scenery_group_mode = &"" _refresh_scenery_capture_bar() - &"cancel_single": - _scenery_controller.cancel_single_transform() - _scenery_single_dragging = false - _refresh_scenery_inspector() - _refresh_scenery_capture_bar() &"done_edit": _exit_scenery_edit_mode() +func _keep_scenery_group_preview() -> void: + if _scenery_controller == null or not _scenery_controller.is_group_preview_pending(): + return + _scenery_controller.commit_group() + _scenery_group_mode = &"" + _refresh_scenery_capture_bar() + + func _begin_scenery_group_transform(kind: StringName) -> void: var selected: Array[Node2D] = _selection.get_selected().duplicate() if _scenery_controller == null or not _scenery_controller.begin_group_transform(kind, selected): diff --git a/scripts/editors/stage/stage_asset_browser.gd b/scripts/editors/stage/stage_asset_browser.gd index d1f0b20..f02458c 100644 --- a/scripts/editors/stage/stage_asset_browser.gd +++ b/scripts/editors/stage/stage_asset_browser.gd @@ -157,7 +157,7 @@ func _scenery_background_entries() -> Array[Dictionary]: {"id": "sky_sunset", "name": "Sunset", "category": "Sky", "gallery_tab": "backgrounds", "background_kind": "sky", "icon": "🌅"}, {"id": "sky_starry_night", "name": "Starry Night", "category": "Sky", "gallery_tab": "backgrounds", "background_kind": "sky", "icon": "🌙"}, {"id": "wall_panel_setup", "name": "Wall panel", "category": "Scenery", "gallery_tab": "backgrounds", "background_kind": "wall_setup", "icon": "🧱"}, - {"id": "background_ground_setup", "name": "Background ground", "category": "Scenery", "description": "Decorative · No walking", "gallery_tab": "backgrounds", "background_kind": "background_ground_setup", "icon": "🌿"} + {"id": "background_ground_setup", "name": "Background ground", "category": "Scenery", "gallery_tab": "backgrounds", "background_kind": "background_ground_setup", "icon": "🌿"} ] diff --git a/scripts/editors/stage/stage_scene_adapter.gd b/scripts/editors/stage/stage_scene_adapter.gd index a7a34ad..286fff0 100644 --- a/scripts/editors/stage/stage_scene_adapter.gd +++ b/scripts/editors/stage/stage_scene_adapter.gd @@ -60,6 +60,8 @@ func collect( entry["size"] = STAGE_SCENE_IO.vector_to_data(background.panel_size) entry["finish_id"] = String(background.finish_id) entry["color"] = "#" + background.tint.to_html(true) + if not background.motif_origin_offset.is_zero_approx(): + entry["motif_origin_offset"] = STAGE_SCENE_IO.vector_to_data(background.motif_origin_offset) elif stage_node.get_script() == STAGE_WALL_PANEL: var wall: StageWallPanel = stage_node as StageWallPanel entry["size"] = STAGE_SCENE_IO.vector_to_data(wall.panel_size) @@ -182,6 +184,8 @@ func apply(data: Dictionary, world: Node2D, spawner: StageSpawner) -> Dictionary StringName(String(entry.get("finish_id", "finish_background_grass"))), STAGE_SCENE_IO.color_from_data(entry.get("color", "#72A84CFF"), Color("#72A84C")) ) + if entry.has("motif_origin_offset") and STAGE_SCENE_IO.is_vector_data(entry.get("motif_origin_offset")): + background.motif_origin_offset = STAGE_SCENE_IO.data_to_vector(entry.get("motif_origin_offset"), Vector2.ZERO) world.add_child(background) node = background "scenery_artwork": diff --git a/scripts/editors/stage/stage_scenery_controller.gd b/scripts/editors/stage/stage_scenery_controller.gd index f479f82..8a87d9f 100644 --- a/scripts/editors/stage/stage_scenery_controller.gd +++ b/scripts/editors/stage/stage_scenery_controller.gd @@ -1,6 +1,6 @@ class_name StageSceneryController extends Node -## Owns inert Scenery placement drafts and snapshot-based group transforms. +## Owns scenery placement transactions, eraser transactions, and snapshot-based transforms. signal placement_committed(node: Node2D) signal document_change_requested @@ -17,7 +17,6 @@ const STAGE_SELECTION := preload("res://scripts/stage/stage_selection.gd") const MIN_PANEL_SIZE: float = 24.0 const MIN_WALL_ERASE_SIZE: float = 8.0 const DEFAULT_PANEL_SIZE: Vector2 = Vector2(240.0, 280.0) -const DEFAULT_BACKGROUND_SIZE: Vector2 = Vector2(800.0, 320.0) const WALL_ERASER_CURSOR_SIZE: Vector2 = Vector2(120.0, 120.0) const MIN_ART_SCALE: float = 0.08 const MAX_ART_SCALE: float = 12.0 @@ -28,6 +27,7 @@ var _camera: Camera2D var _ghost: Node2D var _active_tool: StringName = &"" var _asset_path: String = "" +var _placement_nodes: Array[Node2D] = [] var _depth: StringName = &"near" var _wall_finish: StringName = &"finish_wall_paint" var _wall_tint: Color = Color("#5A9D70") @@ -38,7 +38,12 @@ var _wall_anchor_active: bool = false var _wall_erase_start: Vector2 = Vector2.INF var _wall_erase_dragging: bool = false var _wall_erase_pointer: Vector2 = Vector2.ZERO -var _wall_areas_erased: int = 0 +var _surface_erase_kind: StringName = &"wall" +var _surface_areas_erased: int = 0 +# Keep original surfaces detached during an eraser session so Cancel can restore them. +var _surface_erase_originals: Array[Dictionary] = [] +var _surface_erase_detached: Array[Node2D] = [] +var _surface_erase_changed: bool = false var _snap_enabled: bool = false var _grid_size: float = 15.0 var _group_kind: StringName = &"" @@ -102,7 +107,21 @@ func get_active_tool() -> StringName: func get_wall_areas_erased() -> int: - return _wall_areas_erased + return _surface_areas_erased + + +func get_surface_areas_erased() -> int: + return _surface_areas_erased + + +func is_surface_eraser_active() -> bool: + return _is_surface_eraser() + + +func get_active_eraser_surface_label() -> String: + if _surface_erase_kind == &"all": + return "wall or ground" + return "background ground" if _surface_erase_kind == &"background_ground" else "wall" func get_group_bounds() -> Rect2: @@ -115,6 +134,20 @@ func get_active_artwork_name() -> String: return _asset_path.get_file().get_basename() +func get_active_artwork_path() -> String: + return _asset_path if _active_tool == &"artwork" else "" + + +func get_active_artwork_preview_bounds() -> Rect2: + if _active_tool != &"artwork" or _ghost == null or not is_instance_valid(_ghost) or _ghost.get_script() != SCENERY_ARTWORK: + return Rect2() + return (_ghost as StageSceneryArtwork).get_visual_world_bounds() + + +func get_artwork_depth() -> StringName: + return _depth + + func get_active_surface_label() -> String: if _active_tool == &"background_ground": return "background ground" @@ -123,7 +156,7 @@ func get_active_surface_label() -> String: func handle_key(event: InputEventKey) -> bool: if is_active() and event.pressed and not event.echo and event.keycode == KEY_ESCAPE: - exit_tool() + cancel_active_tool() return true if _group_pending and event.pressed and not event.echo and event.keycode == KEY_ESCAPE: cancel_group() @@ -171,13 +204,12 @@ func _begin_rect_surface(tool: StringName, finish: StringName, initial_world_pos _wall_dragging = false _wall_anchor_active = false var panel: StageRectSurface - var initial_size: Vector2 = DEFAULT_PANEL_SIZE if tool == &"background_ground": panel = BACKGROUND_GROUND.new() as StageBackgroundGround - initial_size = DEFAULT_BACKGROUND_SIZE else: panel = WALL_PANEL.new() as StageWallPanel - panel.configure(initial_size, _wall_finish, _wall_tint) + panel.interactive_preview = true + panel.configure(DEFAULT_PANEL_SIZE, _wall_finish, _wall_tint) panel.modulate = Color(1.0, 1.0, 1.0, 0.45) _ghost_holder.add_child(panel) _ghost = panel @@ -188,16 +220,37 @@ func _begin_rect_surface(tool: StringName, finish: StringName, initial_world_pos func begin_wall_eraser(initial_world_position: Vector2) -> bool: + return begin_surface_eraser(initial_world_position, &"wall") + + +func begin_background_ground_eraser(initial_world_position: Vector2) -> bool: + return begin_surface_eraser(initial_world_position, &"background_ground") + + +func begin_surface_eraser(initial_world_position: Vector2, surface_kind: StringName) -> bool: if _world == null or _ghost_holder == null: return false + if surface_kind not in [&"wall", &"background_ground", &"all"]: + return false exit_tool() - _active_tool = &"wall_eraser" + _surface_erase_kind = surface_kind + match surface_kind: + &"background_ground": + _active_tool = &"background_ground_eraser" + &"all": + _active_tool = &"scenery_eraser" + _: + _active_tool = &"wall_eraser" _wall_erase_start = Vector2.INF _wall_erase_dragging = false - _wall_areas_erased = 0 + _surface_areas_erased = 0 + _surface_erase_originals.clear() + _surface_erase_detached.clear() + _surface_erase_changed = false + _capture_surface_eraser_snapshot(surface_kind) _wall_erase_pointer = initial_world_position var preview := Node2D.new() - preview.name = "WallEraserPreview" + preview.name = "BackgroundGroundEraserPreview" if surface_kind == &"background_ground" else "WallEraserPreview" var fill := Polygon2D.new() fill.name = "EraseArea" fill.color = Color("#FF5B62", 0.2) @@ -237,7 +290,7 @@ func update_pointer(world_position: Vector2) -> void: elif _is_rect_placement_tool(): var preview_top_left: Vector2 = _snap(world_position) if _snap_enabled else world_position _ghost.global_position = _move_node_visible_top_left(_ghost, preview_top_left) - elif _active_tool == &"wall_eraser": + elif _is_surface_eraser(): _wall_erase_pointer = world_position if _wall_erase_dragging: _set_wall_eraser_preview_rect(_drag_rect(_wall_erase_start, _snap(world_position))) @@ -252,13 +305,17 @@ func handle_mouse_button(event: InputEventMouseButton, world_position: Vector2, if not is_active(): return false if event.button_index == MOUSE_BUTTON_RIGHT and event.pressed: - if _wall_dragging or _wall_anchor_active: - cancel_active_gesture() - exit_tool() + # Right-click finishes placement and area-erasure sessions. This commits + # completed work while dropping any unfinished preview gesture. Cancel + # and Escape remain the explicit rollback paths. + if _is_placement_tool() or _is_surface_eraser(): + exit_tool() + else: + cancel_active_tool() return true var completes_drag: bool = not event.pressed and event.button_index == MOUSE_BUTTON_LEFT and ( (_is_rect_placement_tool() and _wall_dragging) - or (_active_tool == &"wall_eraser" and _wall_erase_dragging) + or (_is_surface_eraser() and _wall_erase_dragging) ) if (over_ui and not completes_drag) or event.button_index != MOUSE_BUTTON_LEFT: return false @@ -292,7 +349,7 @@ func handle_mouse_button(event: InputEventMouseButton, world_position: Vector2, _wall_anchor_active = false _restore_rect_surface_ghost(world_position) return true - if _active_tool == &"wall_eraser": + if _is_surface_eraser(): if event.pressed: _wall_erase_start = _snap(world_position) if _snap_enabled else world_position _wall_erase_dragging = true @@ -304,10 +361,10 @@ func handle_mouse_button(event: InputEventMouseButton, world_position: Vector2, _wall_erase_dragging = false _wall_erase_start = Vector2.INF if erase_rect.size.x >= MIN_WALL_ERASE_SIZE and erase_rect.size.y >= MIN_WALL_ERASE_SIZE: - var changed: int = _erase_wall_region(erase_rect) + var changed: int = _erase_surface_region(erase_rect, _surface_erase_kind) if changed > 0: - _wall_areas_erased += 1 - document_change_requested.emit() + _surface_areas_erased += 1 + _surface_erase_changed = true update_pointer(world_position) return true return false @@ -329,13 +386,17 @@ func cancel_active_gesture() -> void: _wall_erase_start = Vector2.INF if _is_rect_placement_tool() and _ghost != null and is_instance_valid(_ghost): (_ghost as StageRectSurface).configure(_default_size_for_active_surface(), _wall_finish, _wall_tint) - elif _active_tool == &"wall_eraser" and _ghost != null and is_instance_valid(_ghost): + elif _is_surface_eraser() and _ghost != null and is_instance_valid(_ghost): _set_wall_eraser_preview_rect(Rect2(_wall_erase_pointer, WALL_ERASER_CURSOR_SIZE)) func exit_tool() -> void: if _single_active: cancel_single_transform() + if _is_placement_tool(): + commit_placement_session() + if _is_surface_eraser(): + _commit_surface_eraser() cancel_active_gesture() _free_ghost() _active_tool = &"" @@ -346,6 +407,115 @@ func exit_tool() -> void: preview_state_changed.emit(false) +func cancel_active_tool() -> void: + if _is_surface_eraser(): + cancel_surface_eraser() + return + if _is_placement_tool(): + _rollback_placement_session() + exit_tool() + + +func commit_placement_session() -> void: + if not _is_placement_tool(): + return + var has_placed_nodes: bool = false + for node: Node2D in _placement_nodes: + if node != null and is_instance_valid(node) and node.get_parent() != null: + has_placed_nodes = true + _placement_nodes.clear() + if has_placed_nodes: + document_change_requested.emit() + + +func _rollback_placement_session() -> void: + for node: Node2D in _placement_nodes: + if node == null or not is_instance_valid(node): + continue + var parent: Node = node.get_parent() + if parent != null: + parent.remove_child(node) + node.queue_free() + _placement_nodes.clear() + + +func cancel_surface_eraser() -> void: + if not _is_surface_eraser(): + return + if _surface_erase_changed: + _restore_surface_eraser_snapshot() + _surface_areas_erased = 0 + _clear_surface_eraser_transaction() + exit_tool() + + +func _commit_surface_eraser() -> void: + if _surface_erase_changed: + document_change_requested.emit() + for detached: Node2D in _surface_erase_detached: + if detached != null and is_instance_valid(detached): + detached.queue_free() + _clear_surface_eraser_transaction() + + +func _clear_surface_eraser_transaction() -> void: + _surface_erase_originals.clear() + _surface_erase_detached.clear() + _surface_erase_changed = false + + +func _capture_surface_eraser_snapshot(surface_kind: StringName) -> void: + if _world == null or not is_instance_valid(_world): + return + var target_scripts: Array[Script] = _surface_erase_target_scripts(surface_kind) + for child: Node in _world.get_children(): + if not target_scripts.has(child.get_script()): + continue + var surface: Node2D = child as Node2D + if surface == null: + continue + _surface_erase_originals.append({ + "node": surface, + "parent": surface.get_parent(), + "index": surface.get_index(), + }) + + +func _restore_surface_eraser_snapshot() -> void: + if _world == null or not is_instance_valid(_world): + return + var original_ids: Dictionary = {} + for snapshot: Dictionary in _surface_erase_originals: + var original: Node2D = snapshot.get("node") as Node2D + if original != null and is_instance_valid(original): + original_ids[original.get_instance_id()] = true + var target_scripts: Array[Script] = _surface_erase_target_scripts(_surface_erase_kind) + for child: Node in _world.get_children().duplicate(): + if not target_scripts.has(child.get_script()): + continue + _world.remove_child(child) + if not original_ids.has(child.get_instance_id()): + child.queue_free() + for detached: Node2D in _surface_erase_detached: + if detached == null or not is_instance_valid(detached): + continue + if not original_ids.has(detached.get_instance_id()): + detached.queue_free() + var restore_order: Array[Dictionary] = _surface_erase_originals.duplicate() + restore_order.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return int(a.get("index", 0)) < int(b.get("index", 0))) + for snapshot: Dictionary in restore_order: + var original: Node2D = snapshot.get("node") as Node2D + var parent: Node = snapshot.get("parent") as Node + if original == null or not is_instance_valid(original): + continue + if parent == null or not is_instance_valid(parent): + parent = _world + if original.get_parent() != null: + original.get_parent().remove_child(original) + parent.add_child(original) + parent.move_child(original, clampi(int(snapshot.get("index", 0)), 0, parent.get_child_count() - 1)) + + func begin_group_transform(kind: StringName, members: Array[Node2D]) -> bool: if _single_active or members.is_empty() or kind not in [&"move", &"resize"]: return false @@ -591,8 +761,8 @@ func _commit_artwork(position: Vector2) -> void: artwork.set_depth(_depth) artwork.global_position = _snap_visual_center(artwork, position) _world.add_child(artwork) + _placement_nodes.append(artwork) placement_committed.emit(artwork) - document_change_requested.emit() func _commit_rect_surface(first: Vector2, second: Vector2) -> void: @@ -605,8 +775,8 @@ func _commit_rect_surface(first: Vector2, second: Vector2) -> void: panel.configure(size, _wall_finish, _wall_tint) panel.global_position = (first + second) * 0.5 _world.add_child(panel) + _placement_nodes.append(panel) placement_committed.emit(panel) - document_change_requested.emit() func _restore_rect_surface_ghost(world_position: Vector2) -> void: @@ -620,23 +790,39 @@ func _is_rect_placement_tool() -> bool: return _active_tool in [&"wall", &"background_ground"] +func _is_placement_tool() -> bool: + return _active_tool in [&"artwork", &"wall", &"background_ground"] + + func _default_size_for_active_surface() -> Vector2: - return DEFAULT_BACKGROUND_SIZE if _active_tool == &"background_ground" else DEFAULT_PANEL_SIZE + return DEFAULT_PANEL_SIZE + + +func _is_surface_eraser() -> bool: + return _active_tool in [&"wall_eraser", &"background_ground_eraser", &"scenery_eraser"] func _erase_wall_region(world_rect: Rect2) -> int: + return _erase_surface_region(world_rect, &"wall") + + +func _erase_surface_region(world_rect: Rect2, surface_kind: StringName) -> int: if _world == null or world_rect.size.x <= 0.0 or world_rect.size.y <= 0.0: return 0 var changed_panels: int = 0 + var target_scripts: Array[Script] = _surface_erase_target_scripts(surface_kind) for child: Node in _world.get_children().duplicate(): - if child.get_script() != WALL_PANEL: + if not target_scripts.has(child.get_script()): continue - var wall: StageWallPanel = child as StageWallPanel - var panel_rect: Rect2 = wall.get_local_bounds() + var surface: StageRectSurface = child as StageRectSurface + if surface == null: + continue + var is_background_ground: bool = child.get_script() == BACKGROUND_GROUND + var panel_rect: Rect2 = surface.get_local_bounds() var erase_corners: Array[Vector2] = [world_rect.position, Vector2(world_rect.end.x, world_rect.position.y), world_rect.end, Vector2(world_rect.position.x, world_rect.end.y)] var local_erase := Rect2() for corner_index: int in erase_corners.size(): - var local_point: Vector2 = wall.to_local(erase_corners[corner_index]) + var local_point: Vector2 = surface.to_local(erase_corners[corner_index]) if corner_index == 0: local_erase = Rect2(local_point, Vector2.ZERO) else: @@ -647,27 +833,52 @@ func _erase_wall_region(world_rect: Rect2) -> int: if cut.size.x <= 0.0 or cut.size.y <= 0.0: continue var fragments: Array[Rect2] = _wall_rect_fragments(panel_rect, cut) - var parent: Node = wall.get_parent() + var parent: Node = surface.get_parent() if parent == null: continue - var sibling_index: int = wall.get_index() - var source_transform: Transform2D = wall.transform - var source_motif_origin: Vector2 = wall.get_effective_motif_origin_world() + var sibling_index: int = surface.get_index() + var source_transform: Transform2D = surface.transform + var source_draw_origin: Vector2 = surface.get_surface_draw_origin_world() + if surface is StageWallPanel: + source_draw_origin = (surface as StageWallPanel).get_effective_motif_origin_world() for fragment_rect: Rect2 in fragments: - var fragment := WALL_PANEL.new() as StageWallPanel + var fragment: StageRectSurface + if is_background_ground: + fragment = BACKGROUND_GROUND.new() as StageBackgroundGround + else: + fragment = WALL_PANEL.new() as StageWallPanel fragment.transform = source_transform fragment.position += source_transform.basis_xform(fragment_rect.get_center()) - fragment.configure(fragment_rect.size, wall.finish_id, wall.tint) + fragment.configure(fragment_rect.size, surface.finish_id, surface.tint) parent.add_child(fragment) - fragment.motif_origin_offset = source_motif_origin - fragment.global_position + if fragment is StageWallPanel: + (fragment as StageWallPanel).motif_origin_offset = source_draw_origin - fragment.global_position + elif fragment is StageBackgroundGround: + (fragment as StageBackgroundGround).motif_origin_offset = source_draw_origin - fragment.global_position parent.move_child(fragment, sibling_index) sibling_index += 1 - parent.remove_child(wall) - wall.queue_free() + parent.remove_child(surface) + if _is_surface_eraser(): + _surface_erase_detached.append(surface) + else: + surface.queue_free() changed_panels += 1 return changed_panels +func _surface_erase_target_scripts(surface_kind: StringName) -> Array[Script]: + var target_scripts: Array[Script] = [] + match surface_kind: + &"background_ground": + target_scripts.append(BACKGROUND_GROUND) + &"all": + target_scripts.append(WALL_PANEL) + target_scripts.append(BACKGROUND_GROUND) + _: + target_scripts.append(WALL_PANEL) + return target_scripts + + func _wall_rect_fragments(panel_rect: Rect2, cut: Rect2) -> Array[Rect2]: var fragments: Array[Rect2] = [] _add_wall_fragment(fragments, Rect2(panel_rect.position, Vector2(panel_rect.size.x, cut.position.y - panel_rect.position.y))) diff --git a/scripts/editors/stage/stage_use_target_capture_bar.gd b/scripts/editors/stage/stage_use_target_capture_bar.gd index c984058..e61f0a2 100644 --- a/scripts/editors/stage/stage_use_target_capture_bar.gd +++ b/scripts/editors/stage/stage_use_target_capture_bar.gd @@ -7,12 +7,16 @@ signal change_prop_requested signal capture_action_requested(action_id: StringName) const MAX_BAR_WIDTH: float = 800.0 +const SCENERY_CAPTURE_MAX_WIDTH: float = 900.0 const VIEWPORT_WIDTH_RATIO: float = 0.68 const HORIZONTAL_LAYOUT_MIN_WIDTH: float = 760.0 const ACTION_ROW_MIN_WIDTH: float = 480.0 const ACTION_BUTTON_WIDTH: float = 200.0 const DEFAULT_BUTTON_HEIGHT: float = 56.0 const NARROW_COMPACT_WIDTH: float = 244.0 +const SCENERY_EDIT_MAX_WIDTH: float = 920.0 +const SCENERY_EDIT_BUTTON_HEIGHT: float = 54.0 +const SCENERY_CAPTURE_BUTTON_HEIGHT: float = 56.0 var _stage_theme: SandboxTheme var _top_bar_height: float = 64.0 @@ -25,12 +29,28 @@ var _actions_row: HBoxContainer var _actions_column: VBoxContainer var _actions_parent: BoxContainer var _prompt: Label +var _scenery_edit_layout: VBoxContainer +var _scenery_edit_prompt: Label +var _scenery_edit_count: Label +var _scenery_edit_count_badge: PanelContainer +var _scenery_selection_group: PanelContainer +var _scenery_edit_actions: HBoxContainer +var _scenery_selection_actions: HBoxContainer +var _scenery_select_several_button: Button +var _scenery_select_all_button: Button +var _scenery_move_group_button: Button +var _scenery_resize_group_button: Button +var _scenery_clear_selection_button: Button +var _scenery_erase_area_button: Button +var _scenery_done_button: Button var _change_prop_button: Button var _cancel_button: Button var _custom_action_buttons: Array[Button] = [] var _has_horizontal_safe_bounds: bool = false var _safe_left: float = 0.0 var _safe_right: float = 0.0 +var _scenery_edit_active: bool = false +var _scenery_capture_active: bool = false func configure(stage_theme: SandboxTheme, top_bar_height: float) -> void: @@ -51,6 +71,7 @@ func _ready() -> void: func set_capture_state(prop_selected: bool, prop_has_use_spots: bool = true) -> void: if _prompt == null: return + _show_standard_capture_layout() _clear_custom_actions() if not prop_selected: _prompt.text = "Choose a prop to use" @@ -65,8 +86,17 @@ func set_capture_state(prop_selected: bool, prop_has_use_spots: bool = true) -> func set_capture_actions(prompt: String, actions: Array[Dictionary]) -> void: + _set_capture_actions(prompt, actions, false) + + +func set_scenery_capture_actions(prompt: String, actions: Array[Dictionary]) -> void: + _set_capture_actions(prompt, actions, true) + + +func _set_capture_actions(prompt: String, actions: Array[Dictionary], scenery_capture_style: bool) -> void: if _prompt == null: return + _show_standard_capture_layout() _prompt.text = prompt _change_prop_button.visible = false _cancel_button.visible = false @@ -77,14 +107,75 @@ func set_capture_actions(prompt: String, actions: Array[Dictionary]) -> void: continue var button := Button.new() button.name = "Action_%s" % String(action_id) - button.text = String(action.get("label", action_id)) + var action_label: String = String(action.get("label", action_id)) + var cue: Control = action.get("cue") as Control + button.text = "" if cue != null else action_label button.tooltip_text = button.text + if cue != null: + button.tooltip_text = action_label + var margin := MarginContainer.new() + margin.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + margin.add_theme_constant_override("margin_left", 4) + margin.add_theme_constant_override("margin_right", 4) + margin.add_theme_constant_override("margin_top", 3) + margin.add_theme_constant_override("margin_bottom", 3) + margin.mouse_filter = Control.MOUSE_FILTER_IGNORE + var cue_layout := BoxContainer.new() + cue_layout.vertical = true + cue_layout.add_theme_constant_override("separation", 2) + cue_layout.alignment = BoxContainer.ALIGNMENT_CENTER + cue_layout.mouse_filter = Control.MOUSE_FILTER_IGNORE + cue.mouse_filter = Control.MOUSE_FILTER_IGNORE + cue_layout.add_child(cue) + var cue_label := Label.new() + cue_label.text = action_label + cue_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL + cue_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + cue_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + cue_label.mouse_filter = Control.MOUSE_FILTER_IGNORE + cue_layout.add_child(cue_label) + margin.add_child(cue_layout) + button.add_child(margin) button.custom_minimum_size.x = ACTION_BUTTON_WIDTH button.focus_mode = Control.FOCUS_ALL + button.set_meta("capture_primary", bool(action.get("primary", false))) + button.set_meta("capture_choice", bool(action.get("choice", false))) + button.set_meta("capture_selected", bool(action.get("selected", false))) button.pressed.connect(func() -> void: capture_action_requested.emit(action_id)) _style_button(button, bool(action.get("primary", false))) _custom_action_buttons.append(button) _actions_parent.add_child(button) + _scenery_capture_active = scenery_capture_style + if _scenery_capture_active: + _set_layout(true, false) + visible = true + apply_theme() + _layout_capture_bar() + + +func set_scenery_edit_state(selected_count: int) -> void: + if _scenery_edit_layout == null: + return + _scenery_capture_active = false + _scenery_edit_active = true + _clear_custom_actions() + if _active_layout.get_parent() == self: + _active_layout.visible = false + remove_child(_active_layout) + _layout_stash.add_child(_active_layout) + if _scenery_edit_layout.get_parent() == _layout_stash: + _layout_stash.remove_child(_scenery_edit_layout) + add_child(_scenery_edit_layout) + _scenery_edit_layout.visible = true + _scenery_edit_prompt.text = "Tap scenery to select it" + _scenery_edit_count.text = "%d selected" % maxi(0, selected_count) + var has_group: bool = selected_count > 1 + _scenery_select_several_button.visible = not has_group + _scenery_select_all_button.visible = not has_group + _scenery_move_group_button.visible = has_group + _scenery_resize_group_button.visible = has_group + _scenery_erase_area_button.visible = selected_count == 0 + _scenery_clear_selection_button.visible = selected_count > 0 visible = true apply_theme() _layout_capture_bar() @@ -104,11 +195,18 @@ func clear_horizontal_safe_bounds() -> void: func hide_capture() -> void: visible = false + _show_standard_capture_layout() func apply_theme() -> void: if not is_node_ready() or _prompt == null: return + if _scenery_edit_active: + _apply_scenery_edit_theme() + return + if _scenery_capture_active: + _apply_scenery_capture_theme() + return theme = _stage_theme.control_theme() if _stage_theme != null else null var accent: Color = _color("mode_colors.direct", Color("#FFB300")) var shell: StyleBoxFlat = StyleBoxFlat.new() @@ -155,6 +253,197 @@ func apply_theme() -> void: _layout_capture_bar() +func _apply_scenery_edit_theme() -> void: + theme = _stage_theme.control_theme() if _stage_theme != null else null + var edit_accent: Color = _color("mode_colors.edit", Color("#22C6FF")) + var panel_style: StyleBoxFlat = StyleBoxFlat.new() + panel_style.bg_color = _color("stage_panels.shell.surface_color", Color("#202633FA")) + panel_style.border_color = Color("#667386") + panel_style.set_border_width_all(2) + panel_style.set_corner_radius_all(18) + panel_style.content_margin_left = 18.0 + panel_style.content_margin_right = 18.0 + panel_style.content_margin_top = 14.0 + panel_style.content_margin_bottom = 14.0 + panel_style.shadow_color = Color(0.0, 0.0, 0.0, 0.34) + panel_style.shadow_size = 8 + panel_style.shadow_offset = Vector2(0.0, 4.0) + add_theme_stylebox_override("panel", panel_style) + var font: Font = _stage_theme.font() if _stage_theme != null else ThemeDB.fallback_font + if font != null: + _scenery_edit_prompt.add_theme_font_override("font", font) + _scenery_edit_count.add_theme_font_override("font", font) + for button: Button in [_scenery_select_several_button, _scenery_select_all_button, _scenery_move_group_button, _scenery_resize_group_button, _scenery_clear_selection_button, _scenery_erase_area_button, _scenery_done_button]: + if font != null: + button.add_theme_font_override("font", font) + button.add_theme_font_size_override("font_size", 19) + button.custom_minimum_size.y = SCENERY_EDIT_BUTTON_HEIGHT + _style_scenery_edit_button(button, button == _scenery_done_button, false) + _scenery_edit_prompt.add_theme_font_size_override("font_size", 27) + _scenery_edit_prompt.add_theme_color_override("font_color", _color("stage_panels.type.text_color", Color("#F5F7FA"))) + _scenery_edit_count.add_theme_font_size_override("font_size", 17) + _scenery_edit_count.add_theme_color_override("font_color", Color("#F5F7FA")) + var badge_style: StyleBoxFlat = StyleBoxFlat.new() + badge_style.bg_color = edit_accent.darkened(0.78) + badge_style.border_color = edit_accent + badge_style.set_border_width_all(1) + badge_style.set_corner_radius_all(14) + badge_style.content_margin_left = 14.0 + badge_style.content_margin_right = 14.0 + badge_style.content_margin_top = 6.0 + badge_style.content_margin_bottom = 6.0 + _scenery_edit_count_badge.add_theme_stylebox_override("panel", badge_style) + var group_style: StyleBoxFlat = StyleBoxFlat.new() + group_style.bg_color = _color("stage_panels.action.surface_color", Color("#171B24")) + group_style.border_color = Color("#667386") + group_style.set_border_width_all(1) + group_style.set_corner_radius_all(14) + group_style.content_margin_left = 2.0 + group_style.content_margin_right = 2.0 + group_style.content_margin_top = 2.0 + group_style.content_margin_bottom = 2.0 + _scenery_selection_group.add_theme_stylebox_override("panel", group_style) + _scenery_edit_layout.add_theme_constant_override("separation", 12) + _scenery_selection_actions.add_theme_constant_override("separation", 0) + _style_scenery_selection_segment(_scenery_select_several_button, true, false) + _style_scenery_selection_segment(_scenery_select_all_button, false, true) + _layout_capture_bar() + + +func _apply_scenery_capture_theme() -> void: + theme = _stage_theme.control_theme() if _stage_theme != null else null + var panel_style: StyleBoxFlat = StyleBoxFlat.new() + panel_style.bg_color = _color("stage_panels.shell.surface_color", Color("#202633FA")) + panel_style.border_color = Color("#667386") + panel_style.set_border_width_all(2) + panel_style.set_corner_radius_all(18) + panel_style.content_margin_left = 18.0 + panel_style.content_margin_right = 18.0 + panel_style.content_margin_top = 14.0 + panel_style.content_margin_bottom = 14.0 + panel_style.shadow_color = Color(0.0, 0.0, 0.0, 0.34) + panel_style.shadow_size = 8 + panel_style.shadow_offset = Vector2(0.0, 4.0) + add_theme_stylebox_override("panel", panel_style) + var font: Font = _stage_theme.font() if _stage_theme != null else ThemeDB.fallback_font + if font != null: + _prompt.add_theme_font_override("font", font) + _prompt.add_theme_font_size_override("font_size", 25) + _prompt.add_theme_color_override("font_color", _color("stage_panels.type.text_color", Color("#F5F7FA"))) + for button: Button in _custom_action_buttons: + if font != null: + button.add_theme_font_override("font", font) + button.add_theme_font_size_override("font_size", 19) + button.custom_minimum_size.y = SCENERY_EDIT_BUTTON_HEIGHT + if bool(button.get_meta("capture_choice", false)): + _style_scenery_depth_choice(button, bool(button.get_meta("capture_selected", false))) + var margin: MarginContainer = button.get_child(0) as MarginContainer if button.get_child_count() > 0 else null + var cue_stack: VBoxContainer = margin.get_child(0) as VBoxContainer if margin != null and margin.get_child_count() > 0 else null + if cue_stack != null and cue_stack.get_child_count() > 1: + var cue_label: Label = cue_stack.get_child(1) as Label + if font != null: + cue_label.add_theme_font_override("font", font) + cue_label.add_theme_font_size_override("font_size", 17) + cue_label.add_theme_color_override("font_color", Color("#F5F7FA")) + else: + _style_scenery_edit_button(button, bool(button.get_meta("capture_primary", false)), false) + _actions_row.add_theme_constant_override("separation", 12) + _layout_capture_bar() + + +func _style_scenery_depth_choice(button: Button, selected: bool) -> void: + var accent: Color = _color("mode_colors.edit", Color("#22C6FF")) + var background: Color = _color("stage_panels.action.primary_color", Color("#087DBA")) if selected else _color("stage_panels.action.surface_color", Color("#171B24")) + var hover: Color = _color("stage_panels.action.primary_hover_color", Color("#1596D3")) if selected else _color("stage_panels.action.hover_surface", Color("#303A4B")) + var border: Color = accent if selected else Color("#667386") + var border_width: int = 2 if selected else 1 + button.add_theme_stylebox_override("normal", _scenery_depth_choice_button_style(background, border, border_width)) + button.add_theme_stylebox_override("hover", _scenery_depth_choice_button_style(hover, accent, 2)) + button.add_theme_stylebox_override("pressed", _scenery_depth_choice_button_style(background, accent, 2)) + button.add_theme_stylebox_override("hover_pressed", _scenery_depth_choice_button_style(hover, accent, 2)) + button.add_theme_stylebox_override("focus", _scenery_depth_choice_button_style(background, accent, 2)) + button.add_theme_color_override("font_color", Color("#F5F7FA")) + button.add_theme_color_override("font_hover_color", Color("#F5F7FA")) + button.add_theme_color_override("font_pressed_color", Color("#F5F7FA")) + button.add_theme_color_override("font_focus_color", Color("#F5F7FA")) + + +func _scenery_depth_choice_button_style(background: Color, border: Color, border_width: int) -> StyleBoxFlat: + var style: StyleBoxFlat = _scenery_button_style(background, border, border_width) + style.content_margin_left = 6.0 + style.content_margin_right = 6.0 + style.content_margin_top = 4.0 + style.content_margin_bottom = 4.0 + return style + + +func _style_scenery_edit_button(button: Button, primary: bool, destructive: bool) -> void: + var accent: Color = _color("mode_colors.edit", Color("#22C6FF")) + var danger: Color = _color("stage_panels.action.destructive_color", Color("#FF626B")) + var border: Color = danger if destructive else (accent if primary else Color("#667386")) + var background: Color = _color("stage_panels.action.primary_color", Color("#087DBA")) if primary else _color("stage_panels.action.surface_color", Color("#171B24")) + var hover_background: Color = _color("stage_panels.action.primary_hover_color", Color("#1596D3")) if primary else _color("stage_panels.action.hover_surface", Color("#303A4B")) + var pressed_background: Color = _color("stage_panels.action.primary_pressed_color", Color("#066B9F")) if primary else _color("stage_panels.action.selected_surface", Color("#12394A")) + var text_color: Color = Color("#F5F7FA") + button.add_theme_stylebox_override("normal", _scenery_button_style(background, border, 2 if primary or destructive else 1)) + button.add_theme_stylebox_override("hover", _scenery_button_style(hover_background, border.lightened(0.12), 2)) + button.add_theme_stylebox_override("pressed", _scenery_button_style(pressed_background, border, 2)) + button.add_theme_stylebox_override("hover_pressed", _scenery_button_style(hover_background, border.lightened(0.12), 2)) + button.add_theme_stylebox_override("focus", _scenery_button_style(background, accent, 2)) + button.add_theme_color_override("font_color", text_color) + button.add_theme_color_override("font_hover_color", text_color) + button.add_theme_color_override("font_pressed_color", text_color) + button.add_theme_color_override("font_focus_color", text_color) + + +func _style_scenery_selection_segment(button: Button, is_first: bool, is_last: bool) -> void: + var base: Color = _color("stage_panels.action.surface_color", Color("#171B24")) + var hover: Color = _color("stage_panels.action.hover_surface", Color("#303A4B")) + var pressed: Color = _color("stage_panels.action.selected_surface", Color("#12394A")) + var divider: Color = Color("#667386") + var accent: Color = _color("mode_colors.edit", Color("#22C6FF")) + button.add_theme_stylebox_override("normal", _scenery_segment_style(base, divider, is_first, is_last)) + button.add_theme_stylebox_override("hover", _scenery_segment_style(hover, divider, is_first, is_last)) + button.add_theme_stylebox_override("pressed", _scenery_segment_style(pressed, divider, is_first, is_last)) + button.add_theme_stylebox_override("hover_pressed", _scenery_segment_style(hover, divider, is_first, is_last)) + button.add_theme_stylebox_override("focus", _scenery_segment_style(base, accent, is_first, is_last, true)) + + +func _scenery_segment_style(background: Color, border: Color, is_first: bool, is_last: bool, is_focused: bool = false) -> StyleBoxFlat: + var style: StyleBoxFlat = StyleBoxFlat.new() + style.bg_color = background + style.border_color = border + if is_focused: + style.set_border_width_all(2) + else: + style.border_width_left = 0 if is_first else 1 + style.border_width_right = 0 + style.border_width_top = 0 + style.border_width_bottom = 0 + style.corner_radius_top_left = 12 if is_first else 0 + style.corner_radius_bottom_left = 12 if is_first else 0 + style.corner_radius_top_right = 12 if is_last else 0 + style.corner_radius_bottom_right = 12 if is_last else 0 + style.content_margin_left = 12.0 + style.content_margin_right = 12.0 + style.content_margin_top = 6.0 + style.content_margin_bottom = 6.0 + return style + + +func _scenery_button_style(background: Color, border: Color, border_width: int) -> StyleBoxFlat: + var style: StyleBoxFlat = StyleBoxFlat.new() + style.bg_color = background + style.border_color = border + style.set_border_width_all(border_width) + style.set_corner_radius_all(14) + style.content_margin_left = 12.0 + style.content_margin_right = 12.0 + style.content_margin_top = 6.0 + style.content_margin_bottom = 6.0 + return style + + func _build_controls() -> void: mouse_filter = Control.MOUSE_FILTER_STOP _layout_stash = Node.new() @@ -207,6 +496,100 @@ func _build_controls() -> void: _cancel_button.focus_mode = Control.FOCUS_ALL _cancel_button.pressed.connect(func() -> void: cancel_requested.emit()) _actions_row.add_child(_cancel_button) + _build_scenery_edit_controls() + + +func _build_scenery_edit_controls() -> void: + _scenery_edit_layout = VBoxContainer.new() + _scenery_edit_layout.name = "SceneryEditLayout" + _scenery_edit_layout.size_flags_horizontal = Control.SIZE_EXPAND_FILL + _scenery_edit_layout.visible = false + _layout_stash.add_child(_scenery_edit_layout) + var header_row: HBoxContainer = HBoxContainer.new() + header_row.size_flags_horizontal = Control.SIZE_EXPAND_FILL + header_row.add_theme_constant_override("separation", 12) + _scenery_edit_layout.add_child(header_row) + _scenery_edit_prompt = Label.new() + _scenery_edit_prompt.name = "SceneryPrompt" + _scenery_edit_prompt.text = "Tap scenery to select it" + _scenery_edit_prompt.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + _scenery_edit_prompt.max_lines_visible = 2 + _scenery_edit_prompt.size_flags_horizontal = Control.SIZE_EXPAND_FILL + _scenery_edit_prompt.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + _scenery_edit_prompt.mouse_filter = Control.MOUSE_FILTER_IGNORE + header_row.add_child(_scenery_edit_prompt) + _scenery_edit_count_badge = PanelContainer.new() + _scenery_edit_count_badge.name = "ScenerySelectionCountBadge" + header_row.add_child(_scenery_edit_count_badge) + _scenery_edit_count = Label.new() + _scenery_edit_count.name = "ScenerySelectionCount" + _scenery_edit_count.text = "0 selected" + _scenery_edit_count.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + _scenery_edit_count_badge.add_child(_scenery_edit_count) + _scenery_clear_selection_button = _make_scenery_action_button("ClearSelection", "Clear selection", &"clear_selection") + _scenery_clear_selection_button.custom_minimum_size.x = 150.0 + _scenery_clear_selection_button.visible = false + header_row.add_child(_scenery_clear_selection_button) + _scenery_edit_actions = HBoxContainer.new() + _scenery_edit_actions.name = "SceneryEditActions" + _scenery_edit_actions.size_flags_horizontal = Control.SIZE_EXPAND_FILL + _scenery_edit_actions.alignment = BoxContainer.ALIGNMENT_CENTER + _scenery_edit_actions.add_theme_constant_override("separation", 10) + _scenery_edit_layout.add_child(_scenery_edit_actions) + _scenery_selection_group = PanelContainer.new() + _scenery_selection_group.name = "SelectionActionsGroup" + _scenery_selection_group.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN + _scenery_edit_actions.add_child(_scenery_selection_group) + _scenery_selection_actions = HBoxContainer.new() + _scenery_selection_actions.add_theme_constant_override("separation", 6) + _scenery_selection_group.add_child(_scenery_selection_actions) + _scenery_select_several_button = _make_scenery_action_button("SelectSeveral", "Select several", &"select_several") + _scenery_select_several_button.custom_minimum_size.x = 158.0 + _scenery_selection_actions.add_child(_scenery_select_several_button) + _scenery_select_all_button = _make_scenery_action_button("SelectAll", "Select all", &"select_all") + _scenery_select_all_button.custom_minimum_size.x = 148.0 + _scenery_selection_actions.add_child(_scenery_select_all_button) + _scenery_move_group_button = _make_scenery_action_button("MoveGroup", "Move together", &"move_group") + _scenery_move_group_button.custom_minimum_size.x = 168.0 + _scenery_move_group_button.visible = false + _scenery_selection_actions.add_child(_scenery_move_group_button) + _scenery_resize_group_button = _make_scenery_action_button("ResizeGroup", "Resize together", &"resize_group") + _scenery_resize_group_button.custom_minimum_size.x = 182.0 + _scenery_resize_group_button.visible = false + _scenery_selection_actions.add_child(_scenery_resize_group_button) + _scenery_erase_area_button = _make_scenery_action_button("EraseArea", "Erase Area", &"erase_area") + _scenery_erase_area_button.custom_minimum_size.x = 152.0 + _scenery_edit_actions.add_child(_scenery_erase_area_button) + _scenery_done_button = _make_scenery_action_button("DoneEditing", "Done editing", &"done_edit") + _scenery_done_button.custom_minimum_size.x = 160.0 + _scenery_edit_actions.add_child(_scenery_done_button) + + +func _make_scenery_action_button(button_name: String, label: String, action_id: StringName) -> Button: + var button: Button = Button.new() + button.name = button_name + button.text = label + button.tooltip_text = label + button.focus_mode = Control.FOCUS_ALL + button.custom_minimum_size = Vector2(144.0, SCENERY_EDIT_BUTTON_HEIGHT) + button.size_flags_horizontal = Control.SIZE_SHRINK_CENTER + button.pressed.connect(func() -> void: capture_action_requested.emit(action_id)) + return button + + +func _show_standard_capture_layout() -> void: + _scenery_edit_active = false + _scenery_capture_active = false + if _scenery_edit_layout != null and _scenery_edit_layout.get_parent() == self: + _scenery_edit_layout.visible = false + remove_child(_scenery_edit_layout) + _layout_stash.add_child(_scenery_edit_layout) + if _active_layout != null and _active_layout.get_parent() == _layout_stash: + _layout_stash.remove_child(_active_layout) + add_child(_active_layout) + _active_layout.visible = true + if is_node_ready(): + apply_theme() func _clear_custom_actions() -> void: @@ -255,6 +638,12 @@ func _layout_capture_bar() -> void: if not is_node_ready() or _prompt == null: return var viewport_size: Vector2 = get_viewport_rect().size + if _scenery_edit_active: + _layout_scenery_edit_capture_bar(viewport_size) + return + if _scenery_capture_active: + _layout_scenery_capture_bar(viewport_size) + return var inset: float = float(_integer("stage_panels.shell.edge_inset", 16, 12, 48)) var safe_left: float = inset var safe_right: float = viewport_size.x - inset @@ -281,6 +670,123 @@ func _layout_capture_bar() -> void: call_deferred("_fit_height_to_content") +func _layout_scenery_edit_capture_bar(viewport_size: Vector2) -> void: + var inset: float = float(_integer("stage_panels.shell.edge_inset", 16, 12, 48)) + var safe_left: float = inset + var safe_right: float = viewport_size.x - inset + if _has_horizontal_safe_bounds: + safe_left = clampf(_safe_left, inset, maxf(inset, viewport_size.x - inset)) + safe_right = clampf(_safe_right, safe_left + 1.0, maxf(safe_left + 1.0, viewport_size.x - inset)) + var available_width: float = maxf(1.0, safe_right - safe_left) + var width: float = minf(SCENERY_EDIT_MAX_WIDTH, available_width) + var narrow: bool = width < 700.0 + custom_minimum_size.x = width + size.x = width + size_flags_horizontal = Control.SIZE_SHRINK_CENTER + clip_contents = false + position = Vector2(safe_left + (available_width - width) * 0.5, _top_bar_height + 56.0) + _scenery_edit_prompt.add_theme_font_size_override("font_size", 27 if width >= 700.0 else (21 if width >= 520.0 else 18)) + if narrow and _scenery_erase_area_button.get_parent() == _scenery_edit_actions: + _scenery_edit_actions.remove_child(_scenery_erase_area_button) + _scenery_edit_layout.get_child(0).add_child(_scenery_erase_area_button) + elif not narrow and _scenery_erase_area_button.get_parent() != _scenery_edit_actions: + var header_row: HBoxContainer = _scenery_edit_layout.get_child(0) as HBoxContainer + header_row.remove_child(_scenery_erase_area_button) + _scenery_edit_actions.add_child(_scenery_erase_area_button) + _scenery_edit_actions.move_child(_scenery_erase_area_button, 1) + _scenery_selection_actions.add_theme_constant_override("separation", 0) + _scenery_select_several_button.custom_minimum_size.x = 134.0 if narrow else 158.0 + _scenery_select_all_button.custom_minimum_size.x = 124.0 if narrow else 148.0 + _scenery_move_group_button.custom_minimum_size.x = 146.0 if narrow else 168.0 + _scenery_resize_group_button.custom_minimum_size.x = 158.0 if narrow else 182.0 + _scenery_done_button.custom_minimum_size.x = 142.0 if narrow else 160.0 + _scenery_erase_area_button.custom_minimum_size.x = 136.0 if narrow else 152.0 + _scenery_clear_selection_button.custom_minimum_size.x = 128.0 if narrow else 150.0 + for button: Button in [_scenery_select_several_button, _scenery_select_all_button, _scenery_move_group_button, _scenery_resize_group_button, _scenery_clear_selection_button, _scenery_erase_area_button, _scenery_done_button]: + button.add_theme_font_size_override("font_size", 19 if width >= 700.0 else 16) + button.custom_minimum_size.y = 52.0 if narrow else SCENERY_EDIT_BUTTON_HEIGHT + button.size_flags_horizontal = Control.SIZE_SHRINK_CENTER + _scenery_edit_actions.add_theme_constant_override("separation", 8 if narrow else 10) + call_deferred("_fit_scenery_edit_height") + + +func _layout_scenery_capture_bar(viewport_size: Vector2) -> void: + var inset: float = float(_integer("stage_panels.shell.edge_inset", 16, 12, 48)) + var available_width: float = maxf(1.0, viewport_size.x - inset * 2.0) + var width: float = minf(SCENERY_CAPTURE_MAX_WIDTH, available_width) + var button_count: int = maxi(1, _custom_action_buttons.size()) + var has_depth_choices: bool = button_count >= 5 + var horizontal_depth_cards: bool = has_depth_choices and width >= 700.0 + var compact_spacing: float = 8.0 if has_depth_choices and width < 760.0 else 12.0 + var minimum_row_width: float = 40.0 + 5.0 * 84.0 + float(button_count - 1) * compact_spacing + var stack_actions: bool = width < ACTION_ROW_MIN_WIDTH or (has_depth_choices and width < minimum_row_width) + custom_minimum_size.x = width + size.x = width + size_flags_horizontal = Control.SIZE_SHRINK_CENTER + clip_contents = false + position = Vector2((viewport_size.x - width) * 0.5, _top_bar_height + 56.0) + _prompt.add_theme_font_size_override("font_size", 25 if width >= 520.0 else 19) + _set_layout(true, stack_actions) + _actions_row.alignment = BoxContainer.ALIGNMENT_END + var available_button_width: float = maxf(1.0, width - 40.0 - compact_spacing * float(button_count - 1)) + var depth_button_width: float = available_button_width + var secondary_button_width: float = available_button_width + if stack_actions: + depth_button_width = maxf(120.0, width - 72.0) + secondary_button_width = depth_button_width + elif has_depth_choices: + secondary_button_width = clampf(available_button_width * 0.16, 84.0, 132.0) + depth_button_width = maxf(84.0, (available_button_width - secondary_button_width * 2.0) / 3.0) + else: + depth_button_width = minf(ACTION_BUTTON_WIDTH, maxf(120.0, available_button_width / float(button_count))) + secondary_button_width = depth_button_width + var cue_size := Vector2(56.0, 42.0) + var choice_button_height: float = 84.0 + if width >= 560.0: + cue_size = Vector2(64.0, 48.0) + choice_button_height = 88.0 + if width >= 700.0: + cue_size = Vector2(66.0, 50.0) + choice_button_height = 100.0 + if width >= 800.0: + cue_size = Vector2(96.0, 72.0) + choice_button_height = 112.0 + for button: Button in _custom_action_buttons: + var is_depth_choice: bool = bool(button.get_meta("capture_choice", false)) + button.custom_minimum_size = Vector2(depth_button_width if is_depth_choice else secondary_button_width, choice_button_height if has_depth_choices else SCENERY_CAPTURE_BUTTON_HEIGHT) + button.size_flags_horizontal = Control.SIZE_EXPAND_FILL if stack_actions else Control.SIZE_SHRINK_CENTER + if is_depth_choice: + var margin: MarginContainer = button.get_child(0) as MarginContainer if button.get_child_count() > 0 else null + var cue_layout: BoxContainer = margin.get_child(0) as BoxContainer if margin != null and margin.get_child_count() > 0 else null + if cue_layout != null and cue_layout.get_child_count() > 1: + cue_layout.vertical = not horizontal_depth_cards + cue_layout.add_theme_constant_override("separation", 8 if horizontal_depth_cards else 2) + var cue: Control = cue_layout.get_child(0) as Control + if cue != null: + cue.custom_minimum_size = cue_size + var cue_label: Label = cue_layout.get_child(1) as Label + if cue_label != null: + cue_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT if horizontal_depth_cards else HORIZONTAL_ALIGNMENT_CENTER + cue_label.add_theme_font_size_override("font_size", 20 if width >= 800.0 else (18 if width >= 620.0 else 17)) + button.add_theme_font_size_override("font_size", 17 if width < 760.0 else 19) + _actions_row.add_theme_constant_override("separation", int(compact_spacing)) + call_deferred("_fit_scenery_capture_height_after_layout") + + +func _fit_scenery_capture_height_after_layout() -> void: + await get_tree().process_frame + _fit_height_to_content() + + +func _fit_scenery_edit_height() -> void: + if not is_inside_tree() or not visible or not _scenery_edit_active: + return + var panel_style: StyleBox = get_theme_stylebox("panel") + var panel_height: float = panel_style.get_minimum_size().y if panel_style != null else 0.0 + custom_minimum_size.y = _scenery_edit_layout.get_combined_minimum_size().y + panel_height + size.y = custom_minimum_size.y + + func _set_layout(compact: bool, stack_actions: bool, wrapped: bool = false) -> void: var target_layout: Container = _wrapped_layout if wrapped else (_vertical_layout if compact else _horizontal_layout) var target_actions: BoxContainer = _actions_column if stack_actions else _actions_row @@ -348,12 +854,13 @@ func _apply_compact_action_labels(compact: bool) -> void: "cancel_group": "Cancel", "done_tool": "Done", "cancel_tool": "Cancel", - "cancel_single": "Cancel", - "depth_far": "Far", - "depth_near": "Near", - "depth_floor": "Floor", + "depth_far": "Back", + "depth_near": "Middle", + "depth_floor": "Front", } for button: Button in _custom_action_buttons: + if bool(button.get_meta("capture_choice", false)): + continue var action_id: String = button.name.trim_prefix("Action_") var full_label: String = button.tooltip_text button.text = String(compact_labels.get(action_id, full_label)) if compact else full_label diff --git a/scripts/shared/stage_ui/stage_depth_choice_preview.gd b/scripts/shared/stage_ui/stage_depth_choice_preview.gd new file mode 100644 index 0000000..217cba2 --- /dev/null +++ b/scripts/shared/stage_ui/stage_depth_choice_preview.gd @@ -0,0 +1,97 @@ +class_name StageDepthChoicePreview +extends Control +## Small fixed-size diagram showing how scenery depth changes draw order. + +const DEPTH_FAR := &"far" +const DEPTH_NEAR := &"near" +const DEPTH_FLOOR := &"floor" + +var depth_id: StringName = DEPTH_NEAR +var artwork_texture: Texture2D + + +func configure(depth: StringName, texture: Texture2D, preview_size: Vector2) -> void: + depth_id = depth if depth in [DEPTH_FAR, DEPTH_NEAR, DEPTH_FLOOR] else DEPTH_NEAR + artwork_texture = texture + custom_minimum_size = preview_size + size_flags_horizontal = Control.SIZE_SHRINK_CENTER + size_flags_vertical = Control.SIZE_SHRINK_CENTER + mouse_filter = Control.MOUSE_FILTER_IGNORE + clip_contents = true + queue_redraw() + + +func set_artwork_texture(texture: Texture2D) -> void: + artwork_texture = texture + queue_redraw() + + +func _draw() -> void: + var bounds := Rect2(Vector2.ZERO, size) + draw_rect(bounds, Color("#B9D9D9"), true) + _draw_hills() + if depth_id != DEPTH_FAR: + _draw_near_scenery() + if depth_id == DEPTH_FLOOR: + _draw_ground_tiles() + _draw_artwork() + if depth_id == DEPTH_FAR: + _draw_near_scenery() + if depth_id != DEPTH_FLOOR: + _draw_ground_tiles() + draw_rect(bounds, Color("#566273"), false, 1.0) + + +func _draw_hills() -> void: + var points := PackedVector2Array([ + Vector2(0.0, size.y * 0.68), + Vector2(0.0, size.y * 0.52), + Vector2(size.x * 0.18, size.y * 0.39), + Vector2(size.x * 0.38, size.y * 0.54), + Vector2(size.x * 0.62, size.y * 0.36), + Vector2(size.x * 0.84, size.y * 0.49), + Vector2(size.x, size.y * 0.41), + Vector2(size.x, size.y * 0.68), + ]) + draw_colored_polygon(points, Color("#79AFA4")) + + +func _draw_near_scenery() -> void: + var canopy := Color("#277447") + var trunk := Color("#84502B") + var canopy_center := Vector2(size.x * 0.49, size.y * 0.56) + var radius := maxf(4.0, size.y * 0.25) + draw_rect(Rect2(Vector2(size.x * 0.49 - 2.0, size.y * 0.46), Vector2(4.0, size.y * 0.23)), trunk, true) + draw_circle(canopy_center, radius, canopy) + draw_circle(canopy_center + Vector2(-radius * 0.52, radius * 0.08), radius * 0.62, Color("#48A84E")) + draw_circle(canopy_center + Vector2(radius * 0.48, radius * 0.02), radius * 0.55, Color("#36934A")) + + +func _draw_ground_tiles() -> void: + var top: float = size.y * 0.64 + var tile_height: float = size.y - top + var tile_count: int = 5 + var tile_width: float = size.x / float(tile_count) + for index: int in tile_count: + var tile := Rect2(Vector2(float(index) * tile_width, top), Vector2(tile_width + 0.5, tile_height)) + draw_rect(tile, Color("#87512C"), true) + draw_rect(tile, Color("#A96C3B"), false, 1.0) + draw_line(Vector2(float(index) * tile_width + 2.0, top + tile_height * 0.58), Vector2(float(index + 1) * tile_width - 2.0, top + tile_height * 0.58), Color("#C9894B"), 1.0) + draw_rect(Rect2(Vector2(0.0, top - 3.0), Vector2(size.x, 6.0)), Color("#62D34D"), true) + draw_line(Vector2(0.0, top - 3.0), Vector2(size.x, top - 3.0), Color("#D5F27B"), 1.0) + + +func _draw_artwork() -> void: + if artwork_texture == null: + var center := Vector2(size.x * 0.49, size.y * 0.59) + draw_circle(center, maxf(3.0, minf(size.x, size.y) * 0.20), Color("#F4F1E8")) + draw_circle(center, maxf(3.0, minf(size.x, size.y) * 0.20), Color("#58616D"), false, 1.0) + return + var texture_size := Vector2(artwork_texture.get_size()) + if texture_size.x <= 0.0 or texture_size.y <= 0.0: + return + var available := Vector2(size.x * 0.84, size.y * 0.84) + var scale_factor: float = minf(available.x / texture_size.x, available.y / texture_size.y) + var target_size := texture_size * scale_factor + var target := Rect2(Vector2((size.x - target_size.x) * 0.5, size.y * 0.59 - target_size.y * 0.5), target_size) + draw_texture_rect(artwork_texture, target, false) diff --git a/scripts/shared/stage_ui/stage_depth_choice_preview.gd.uid b/scripts/shared/stage_ui/stage_depth_choice_preview.gd.uid new file mode 100644 index 0000000..7200c41 --- /dev/null +++ b/scripts/shared/stage_ui/stage_depth_choice_preview.gd.uid @@ -0,0 +1 @@ +uid://cc5hn0jge7758 diff --git a/scripts/shared/stage_ui/stage_inspector_panel.gd b/scripts/shared/stage_ui/stage_inspector_panel.gd index bd8c546..b01f5a4 100644 --- a/scripts/shared/stage_ui/stage_inspector_panel.gd +++ b/scripts/shared/stage_ui/stage_inspector_panel.gd @@ -116,6 +116,52 @@ func style_action_button(button: Button, prominent: bool = false) -> void: _style_action_button(button, prominent) +func style_primary_action_button(button: Button) -> void: + if button == null: + return + var primary := _color("stage_panels.action.primary_color", Color("#087DBA")) + var hover := _color("stage_panels.action.primary_hover_color", Color("#1596D3")) + var pressed := _color("stage_panels.action.primary_pressed_color", Color("#066B9F")) + var accent := _color("mode_colors.edit", Color("#22C6FF")) + button.add_theme_stylebox_override("normal", _make_style(primary, accent.lightened(0.15), 2, 10, 6)) + button.add_theme_stylebox_override("hover", _make_style(hover, accent, 2, 10, 6)) + button.add_theme_stylebox_override("pressed", _make_style(pressed, accent, 2, 10, 6)) + button.add_theme_stylebox_override("hover_pressed", _make_style(pressed.lightened(0.05), accent, 2, 10, 6)) + button.add_theme_stylebox_override("disabled", _make_style(_color("stage_panels.choice.disabled_surface", Color("#252B38")), _color("global.border_color", Color("#FFFFFF33")), 1, 10, 6)) + button.add_theme_stylebox_override("focus", _make_style(Color(0, 0, 0, 0), accent, 2, 10, 6)) + button.add_theme_font_size_override("font_size", _integer("stage_panels.type.body_size", 20, 14, 40)) + button.add_theme_color_override("font_color", Color.WHITE) + button.add_theme_color_override("font_hover_color", Color.WHITE) + button.add_theme_color_override("font_pressed_color", Color.WHITE) + if _stage_theme != null: + var ui_font := _stage_theme.font() + if ui_font != null: + button.add_theme_font_override("font", ui_font) + + +func style_destructive_button(button: Button) -> void: + if button == null: + return + var danger := _color("stage_panels.action.destructive_color", Color("#FF626B")) + var neutral := _color("stage_panels.action.surface_color", Color("#171B24")) + var hover := _color("stage_panels.action.destructive_hover_surface", Color("#3B252C")) + var pressed := _color("stage_panels.action.destructive_pressed_surface", Color("#4A252D")) + var border := _color("global.border_color", Color("#FFFFFF33")) + var accent := _color("mode_colors.edit", Color("#22C6FF")) + button.add_theme_stylebox_override("normal", _make_style(neutral, danger, 2, 10, 6)) + button.add_theme_stylebox_override("hover", _make_style(hover, danger, 2, 10, 6)) + button.add_theme_stylebox_override("pressed", _make_style(pressed, danger, 2, 10, 6)) + button.add_theme_stylebox_override("hover_pressed", _make_style(pressed.lightened(0.06), danger, 2, 10, 6)) + button.add_theme_stylebox_override("disabled", _make_style(_color("stage_panels.choice.disabled_surface", Color("#252B38")), border, 1, 10, 6)) + button.add_theme_stylebox_override("focus", _make_style(Color(0, 0, 0, 0), accent, 2, 10, 6)) + button.add_theme_font_size_override("font_size", _integer("stage_panels.type.body_size", 20, 14, 40)) + button.add_theme_color_override("font_color", _color("stage_panels.type.text_color", Color("#F5F7FA"))) + if _stage_theme != null: + var ui_font := _stage_theme.font() + if ui_font != null: + button.add_theme_font_override("font", ui_font) + + func _build_shell() -> void: if _body != null: return @@ -145,11 +191,12 @@ func _build_shell() -> void: _title.vertical_alignment = VERTICAL_ALIGNMENT_CENTER _header.add_child(_title) _close_button = Button.new() - _close_button.text = "✕ Close" + _close_button.text = "Close" _close_button.custom_minimum_size = Vector2( float(_integer("stage_panels.action.close_width", 88, 72, 180)), float(_integer("stage_panels.action.min_target_size", 48, 44, 96)) ) + _close_button.size_flags_vertical = Control.SIZE_SHRINK_BEGIN _close_button.focus_mode = Control.FOCUS_ALL _close_button.pressed.connect(func() -> void: close_requested.emit()) _header.add_child(_close_button) diff --git a/scripts/shared/stage_ui/stage_panel_choice_group.gd b/scripts/shared/stage_ui/stage_panel_choice_group.gd index 76950e4..815674a 100644 --- a/scripts/shared/stage_ui/stage_panel_choice_group.gd +++ b/scripts/shared/stage_ui/stage_panel_choice_group.gd @@ -1,5 +1,5 @@ class_name StagePanelChoiceGroup -extends VBoxContainer +extends BoxContainer ## Exclusive, full-width choices with optional decorative cues. signal choice_selected(id: StringName) @@ -8,20 +8,43 @@ var _stage_theme: SandboxTheme = null var _group := ButtonGroup.new() var _buttons: Dictionary = {} var _selected_id: StringName = &"" +var _vertical_layout: bool = true +var _minimum_height_override: float = -1.0 +var _font_size_override: int = -1 +var _corner_radius_override: int = -1 +var _border_width_override: int = -1 +var _selected_surface_override: Color = Color.TRANSPARENT +var _has_selected_surface_override: bool = false func _ready() -> void: + vertical = _vertical_layout _group.allow_unpress = true add_theme_constant_override("separation", _integer("stage_panels.choice.row_gap", 6, 0, 32)) -func add_choice(id: StringName, caption: String, cue: Control = null) -> Button: +func configure_layout(vertical_layout: bool, minimum_height: float = -1.0, font_size: int = -1, corner_radius: int = -1, border_width: int = -1, selected_surface: Color = Color.TRANSPARENT) -> void: + _vertical_layout = vertical_layout + vertical = vertical_layout + _minimum_height_override = minimum_height + _font_size_override = font_size + _corner_radius_override = corner_radius + _border_width_override = border_width + _selected_surface_override = selected_surface + _has_selected_surface_override = selected_surface.a > 0.0 + for entry: Dictionary in _buttons.values(): + _apply_choice_theme(entry["button"] as Button, entry["label"] as Label, entry.get("detail") as Label) + + +func add_choice(id: StringName, caption: String, cue: Control = null, detail: String = "") -> Button: var button := Button.new() button.name = "Choice_%s" % String(id) button.toggle_mode = true button.button_group = _group - button.custom_minimum_size.y = float(_integer("stage_panels.choice.minimum_height", 56, 44, 112)) + button.custom_minimum_size.y = _minimum_height_override if _minimum_height_override > 0.0 else float(_integer("stage_panels.choice.minimum_height", 56, 44, 112)) button.size_flags_horizontal = Control.SIZE_EXPAND_FILL + if not _vertical_layout: + button.size_flags_vertical = Control.SIZE_EXPAND_FILL button.focus_mode = Control.FOCUS_ALL button.text = "" var margin := MarginContainer.new() @@ -42,16 +65,31 @@ func add_choice(id: StringName, caption: String, cue: Control = null) -> Button: var label := Label.new() label.text = caption label.size_flags_horizontal = Control.SIZE_EXPAND_FILL - label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + label.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT if not detail.is_empty() else HORIZONTAL_ALIGNMENT_CENTER label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER label.mouse_filter = Control.MOUSE_FILTER_IGNORE - row.add_child(label) + var detail_label: Label + if detail.is_empty(): + label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + row.add_child(label) + else: + var text_stack := VBoxContainer.new() + text_stack.add_theme_constant_override("separation", 0) + text_stack.size_flags_horizontal = Control.SIZE_EXPAND_FILL + text_stack.mouse_filter = Control.MOUSE_FILTER_IGNORE + text_stack.add_child(label) + detail_label = Label.new() + detail_label.text = detail + detail_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + detail_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + detail_label.mouse_filter = Control.MOUSE_FILTER_IGNORE + text_stack.add_child(detail_label) + row.add_child(text_stack) button.pressed.connect(_on_choice_pressed.bind(id, button)) add_child(button) - _buttons[id] = {"button": button, "label": label} + _buttons[id] = {"button": button, "label": label, "detail": detail_label} if _stage_theme != null: - _apply_choice_theme(button, label) + _apply_choice_theme(button, label, detail_label) return button @@ -75,7 +113,7 @@ func apply_theme(stage_theme: SandboxTheme) -> void: _stage_theme = stage_theme add_theme_constant_override("separation", _integer("stage_panels.choice.row_gap", 6, 0, 32)) for entry: Dictionary in _buttons.values(): - _apply_choice_theme(entry["button"] as Button, entry["label"] as Label) + _apply_choice_theme(entry["button"] as Button, entry["label"] as Label, entry.get("detail") as Label) func _on_choice_pressed(id: StringName, button: Button) -> void: @@ -87,26 +125,40 @@ func _on_choice_pressed(id: StringName, button: Button) -> void: choice_selected.emit(id) -func _apply_choice_theme(button: Button, label: Label) -> void: +func _apply_choice_theme(button: Button, label: Label, detail: Label = null) -> void: if _stage_theme == null: return var font := _stage_theme.font() if font != null: - label.add_theme_font_override("font", font) - label.add_theme_font_size_override("font_size", _integer("stage_panels.type.body_size", 20, 14, 40)) + if not _vertical_layout: + var strong_font := FontVariation.new() + strong_font.base_font = font + strong_font.variation_embolden = 0.25 + label.add_theme_font_override("font", strong_font) + else: + label.add_theme_font_override("font", font) + var font_size: int = _font_size_override if _font_size_override > 0 else _integer("stage_panels.type.body_size", 20, 14, 40) + label.add_theme_font_size_override("font_size", font_size) label.add_theme_color_override("font_color", _color("stage_panels.type.text_color", Color("#F5F7FA"))) + if detail != null: + if font != null: + detail.add_theme_font_override("font", font) + detail.add_theme_font_size_override("font_size", maxi(12, font_size - 4)) + detail.add_theme_color_override("font_color", _color("stage_panels.type.muted_text_color", Color("#AAB2C0"))) var neutral := _color("stage_panels.choice.neutral_surface", Color("#171B24")) var hover := _color("stage_panels.choice.hover_surface", Color("#303A4B")) - var selected := _color("stage_panels.choice.selected_surface", Color("#12394A")) + var selected := _selected_surface_override if _has_selected_surface_override else _color("stage_panels.choice.selected_surface", Color("#12394A")) var disabled := _color("stage_panels.choice.disabled_surface", Color("#252B38")) var border := _color("stage_panels.choice.border_color", Color("#FFFFFF33")) var accent := _color("mode_colors.edit", Color("#22C6FF")) - button.add_theme_stylebox_override("normal", _make_style(neutral, border, 1)) - button.add_theme_stylebox_override("hover", _make_style(hover, border, 1)) - button.add_theme_stylebox_override("pressed", _make_style(selected, accent, 2)) - button.add_theme_stylebox_override("hover_pressed", _make_style(selected.lightened(0.06), accent, 2)) - button.add_theme_stylebox_override("disabled", _make_style(disabled, border, 1)) - button.add_theme_stylebox_override("focus", _make_style(Color(0, 0, 0, 0), accent, 2)) + var border_width: int = _border_width_override if _border_width_override >= 0 else 1 + var selected_border_width: int = border_width + 1 if _border_width_override >= 0 else 2 + button.add_theme_stylebox_override("normal", _make_style(neutral, border, border_width)) + button.add_theme_stylebox_override("hover", _make_style(hover, border, border_width)) + button.add_theme_stylebox_override("pressed", _make_style(selected, accent, selected_border_width)) + button.add_theme_stylebox_override("hover_pressed", _make_style(selected.lightened(0.06), accent, selected_border_width)) + button.add_theme_stylebox_override("disabled", _make_style(disabled, border, border_width)) + button.add_theme_stylebox_override("focus", _make_style(Color(0, 0, 0, 0), accent, selected_border_width)) func _make_style(bg: Color, border: Color, width: int) -> StyleBoxFlat: @@ -114,7 +166,8 @@ func _make_style(bg: Color, border: Color, width: int) -> StyleBoxFlat: style.bg_color = bg style.border_color = border style.set_border_width_all(width) - style.set_corner_radius_all(_integer("stage_panels.shell.corner_radius", 10, 0, 32)) + var radius: int = _corner_radius_override if _corner_radius_override >= 0 else _integer("stage_panels.shell.corner_radius", 10, 0, 32) + style.set_corner_radius_all(radius) return style diff --git a/scripts/shared/stage_ui/stage_surface_color_controls.gd b/scripts/shared/stage_ui/stage_surface_color_controls.gd index 67158bf..6368ca2 100644 --- a/scripts/shared/stage_ui/stage_surface_color_controls.gd +++ b/scripts/shared/stage_ui/stage_surface_color_controls.gd @@ -17,7 +17,6 @@ var _current_value: Label var _recent_empty_label: Label var _recent_swatches: ColorSwatchSelector var _preset_swatches: ColorSwatchSelector -var _more_button: Button var _compact_mode: bool = false @@ -54,60 +53,107 @@ func set_recent_colors(colors: Array[Color]) -> void: func focus_custom_button() -> void: - if _more_button != null: - _more_button.grab_focus() + if _current_chip != null: + _current_chip.grab_focus() func _build() -> void: _built = true _apply_compact_layout() - var current_heading: Label = _make_heading("Color") - add_child(current_heading) + var current_parent: Control = self + var recent_parent: Control = self + var presets_parent: Control = self + if _compact_mode: + var row := HBoxContainer.new() + row.add_theme_constant_override("separation", _integer("popups.asset_selector.color_group_gap", 8, 4, 32)) + row.size_flags_horizontal = Control.SIZE_EXPAND_FILL + add_child(row) + current_parent = _make_compact_section(132.0) + row.add_child(current_parent) + row.add_child(_make_compact_separator()) + recent_parent = _make_compact_section(92.0) + row.add_child(recent_parent) + row.add_child(_make_compact_separator()) + presets_parent = _make_compact_section(180.0) + presets_parent.size_flags_horizontal = Control.SIZE_EXPAND_FILL + row.add_child(presets_parent) + _build_current_color_section(current_parent) + _build_recent_color_section(recent_parent) + _build_palette_section(presets_parent) + if _stage_theme != null: + var font: Font = _stage_theme.font() + if font != null: + if _compact_mode: + var strong_font := FontVariation.new() + strong_font.base_font = font + strong_font.variation_embolden = 0.2 + _current_value.add_theme_font_override("font", strong_font) + else: + _current_value.add_theme_font_override("font", font) + _current_value.add_theme_font_size_override("font_size", _integer("stage_panels.type.body_size", 20, 14, 40)) + + +func _build_current_color_section(parent: Control) -> void: + parent.add_child(_make_heading("Color")) var current_row := HBoxContainer.new() current_row.add_theme_constant_override("separation", _compact_gap()) - add_child(current_row) + parent.add_child(current_row) _current_chip = Button.new() var swatch_size: float = float(_integer("stage_surface_colors.swatch_size", 48, 44, 96)) if _compact_mode: - swatch_size = minf(swatch_size, 44.0) + swatch_size = maxf(swatch_size, 52.0) _current_chip.custom_minimum_size = Vector2(swatch_size, swatch_size) - _current_chip.focus_mode = Control.FOCUS_NONE - _current_chip.mouse_filter = Control.MOUSE_FILTER_IGNORE + _current_chip.focus_mode = Control.FOCUS_ALL + _current_chip.tooltip_text = "Choose a custom color" + _current_chip.pressed.connect(func() -> void: custom_color_requested.emit()) current_row.add_child(_current_chip) _current_value = Label.new() _current_value.size_flags_horizontal = Control.SIZE_EXPAND_FILL _current_value.vertical_alignment = VERTICAL_ALIGNMENT_CENTER current_row.add_child(_current_value) - add_child(_make_heading("Recently used")) + + +func _build_recent_color_section(parent: Control) -> void: + parent.add_child(_make_heading("Recently used")) _recent_empty_label = Label.new() - _recent_empty_label.text = "Colors you use will appear here" + _recent_empty_label.text = "No colors yet" if _compact_mode else "Colors you use will appear here" _recent_empty_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART _recent_empty_label.add_theme_color_override("font_color", _color("global.muted_text_color", Color("#AAB2C0"))) - add_child(_recent_empty_label) + if _compact_mode: + _recent_empty_label.add_theme_font_size_override("font_size", _integer("stage_panels.type.helper_size", 16, 12, 32)) + parent.add_child(_recent_empty_label) _recent_swatches = COLOR_SWATCH_SELECTOR.new() as ColorSwatchSelector + _recent_swatches.size_flags_horizontal = Control.SIZE_EXPAND_FILL + if _compact_mode: + _recent_swatches.set_swatch_layout_override(_integer("popups.asset_selector.compact_swatch_size", 42, 28, 52), 8) _recent_swatches.color_selected.connect(_on_swatch_selected) - add_child(_recent_swatches) - add_child(_make_heading("Colors")) + parent.add_child(_recent_swatches) + + +func _build_palette_section(parent: Control) -> void: + parent.add_child(_make_heading("Colors")) _preset_swatches = COLOR_SWATCH_SELECTOR.new() as ColorSwatchSelector + _preset_swatches.size_flags_horizontal = Control.SIZE_EXPAND_FILL + if _compact_mode: + _preset_swatches.set_swatch_layout_override(_integer("popups.asset_selector.compact_swatch_size", 42, 28, 52), 8) _preset_swatches.color_selected.connect(_on_swatch_selected) - add_child(_preset_swatches) - _more_button = Button.new() - _more_button.text = "More colors…" - var action_height: float = float(_integer("stage_panels.action.min_target_size", 48, 44, 96)) - _more_button.custom_minimum_size.y = minf(action_height, 44.0) if _compact_mode else action_height - _more_button.focus_mode = Control.FOCUS_ALL - _more_button.pressed.connect(func() -> void: custom_color_requested.emit()) - add_child(_more_button) - if _stage_theme != null: - var font: Font = _stage_theme.font() - if font != null: - _current_value.add_theme_font_override("font", font) - _more_button.add_theme_font_override("font", font) - _current_value.add_theme_font_size_override("font_size", _integer("stage_panels.type.body_size", 20, 14, 40)) - _more_button.add_theme_font_size_override("font_size", _integer("stage_panels.type.body_size", 20, 14, 40)) - _more_button.add_theme_color_override("font_color", _color("global.text_color", Color("#F5F7FA"))) - _more_button.add_theme_stylebox_override("normal", _stage_theme.stylebox("stage_panels.action")) - _more_button.add_theme_stylebox_override("hover", _stage_theme.stylebox("stage_panels.action").duplicate()) + parent.add_child(_preset_swatches) + + +func _make_compact_section(minimum_width: float) -> VBoxContainer: + var section := VBoxContainer.new() + section.custom_minimum_size.x = minimum_width + section.add_theme_constant_override("separation", 4) + return section + + +func _make_compact_separator() -> ColorRect: + var separator := ColorRect.new() + separator.color = _color("popups.asset_selector.separator_color", Color("#AAB2C0A0")) + separator.custom_minimum_size = Vector2(2.0, 64.0) + separator.size_flags_vertical = Control.SIZE_SHRINK_CENTER + separator.mouse_filter = Control.MOUSE_FILTER_IGNORE + return separator func _refresh_visuals() -> void: @@ -124,11 +170,16 @@ func _refresh_current_color() -> void: _current_value.text = "#%s" % _selected.to_html(false).to_upper() var chip_style := StyleBoxFlat.new() chip_style.bg_color = _selected - chip_style.border_color = _color("stage_panels.choice.border_color", Color("#FFFFFFAA")) - chip_style.set_border_width_all(_integer("stage_surface_colors.swatch_border_width", 2, 0, 8)) + chip_style.border_color = _color("mode_colors.edit", Color("#22C6FF")) + chip_style.set_border_width_all(_integer("stage_surface_colors.swatch_border_width", 3, 1, 8)) chip_style.set_corner_radius_all(_integer("stage_surface_colors.swatch_corner_radius", 8, 0, 24)) _current_chip.add_theme_stylebox_override("normal", chip_style) _current_chip.add_theme_stylebox_override("hover", chip_style.duplicate()) + _current_chip.add_theme_stylebox_override("pressed", chip_style.duplicate()) + var focus_style: StyleBoxFlat = chip_style.duplicate() as StyleBoxFlat + focus_style.border_color = Color.WHITE + focus_style.set_border_width_all(5) + _current_chip.add_theme_stylebox_override("focus", focus_style) func _on_swatch_selected(color: Color) -> void: @@ -143,18 +194,23 @@ func _make_heading(text_value: String) -> Label: var heading := Label.new() heading.text = text_value var heading_size: int = _integer("stage_panels.type.section_size", 20, 12, 40) - if _compact_mode: - heading_size = mini(heading_size, 18) heading.add_theme_font_size_override("font_size", heading_size) heading.add_theme_color_override("font_color", _color("stage_panels.type.text_color", Color("#F5F7FA"))) if _stage_theme != null and _stage_theme.font() != null: - heading.add_theme_font_override("font", _stage_theme.font()) + var font: Font = _stage_theme.font() + if _compact_mode: + var strong_font := FontVariation.new() + strong_font.base_font = font + strong_font.variation_embolden = 0.2 + heading.add_theme_font_override("font", strong_font) + else: + heading.add_theme_font_override("font", font) return heading func _apply_compact_layout() -> void: var section_gap: int = _integer("stage_surface_colors.section_gap", 8, 0, 28) - add_theme_constant_override("separation", mini(section_gap, 4) if _compact_mode else section_gap) + add_theme_constant_override("separation", section_gap) func _compact_gap() -> int: diff --git a/scripts/shared/stage_ui/stage_surface_color_dialog.gd b/scripts/shared/stage_ui/stage_surface_color_dialog.gd index 66948e2..0b70f08 100644 --- a/scripts/shared/stage_ui/stage_surface_color_dialog.gd +++ b/scripts/shared/stage_ui/stage_surface_color_dialog.gd @@ -1,22 +1,37 @@ class_name StageSurfaceColorDialog -extends Window -## Bounded custom-color dialog with a live Stage surface preview. +extends PopupPanel +## Rounded, reusable Stage color popup with the shared surface picker view. signal preview_color_changed(color: Color) signal color_confirmed(color: Color) signal cancelled -const SURFACE_PREVIEW := preload("res://scripts/stage/scenery/stage_surface_preview.gd") +const SURFACE_COLOR_PICKER_VIEW := preload("res://scripts/shared/stage_ui/stage_surface_color_picker_view.gd") +const COMPACT_LAYOUT_MAX_HEIGHT := 640 var _surface_kind: StringName = &"wall" var _finish_id: StringName = &"finish_wall_paint" var _initial_color: Color = Color.WHITE var _stage_theme: SandboxTheme -var _color_picker: ColorPicker -var _surface_preview: StageSurfacePreview +var _margin: MarginContainer +var _layout: VBoxContainer +var _heading: Label +var _view_scroll: ScrollContainer +var _picker_view: StageSurfaceColorPickerView +var _actions: HBoxContainer +var _cancel_button: Button +var _use_button: Button var _built: bool = false +func _ready() -> void: + exclusive = true + _build() + close_requested.connect(_cancel) + get_tree().root.size_changed.connect(_on_root_size_changed) + _apply_visual_style() + + func configure(surface_kind: StringName, finish: StringName, color: Color, stage_theme: SandboxTheme) -> void: _surface_kind = surface_kind _finish_id = finish @@ -24,26 +39,20 @@ func configure(surface_kind: StringName, finish: StringName, color: Color, stage _stage_theme = stage_theme if not _built: _build() - _color_picker.color = color - _update_preview(color) - title = "Choose a color" - exclusive = true - transient = true - theme = stage_theme.control_theme() if stage_theme != null else null + if _picker_view != null: + _picker_view.configure(_surface_kind, _finish_id, color, _stage_theme) + theme = _stage_theme.control_theme() if _stage_theme != null else null + _apply_visual_style() func open_dialog() -> void: - var max_size: Vector2i = _usable_window_size() - var preferred_size := Vector2i(560, 640) - var raw_size: Variant = _stage_theme.value("popups.dialog.surface_color_size", [560, 640]) if _stage_theme != null else [560, 640] - if raw_size is Array and (raw_size as Array).size() == 2: - preferred_size = Vector2i(int((raw_size as Array)[0]), int((raw_size as Array)[1])) - var dialog_size := Vector2i(mini(preferred_size.x, max_size.x), mini(preferred_size.y, max_size.y)) - dialog_size.x = maxi(1, dialog_size.x) - dialog_size.y = maxi(1, dialog_size.y) - size = dialog_size - popup_centered(dialog_size) - _color_picker.grab_focus() + if not _built or _picker_view == null: + return + var popup_size: Vector2i = _bounded_dialog_size() + _apply_responsive_layout(popup_size) + size = popup_size + popup_centered(popup_size) + call_deferred("_focus_picker") func _unhandled_key_input(event: InputEvent) -> void: @@ -53,98 +62,168 @@ func _unhandled_key_input(event: InputEvent) -> void: func _build() -> void: + if _built: + return _built = true - var margin := MarginContainer.new() - margin.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + _margin = MarginContainer.new() + _margin.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + add_child(_margin) + _layout = VBoxContainer.new() + _margin.add_child(_layout) + _heading = Label.new() + _heading.name = "ChooseColorHeading" + _heading.text = "Choose a color" + _heading.size_flags_horizontal = Control.SIZE_EXPAND_FILL + _heading.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + _heading.mouse_filter = Control.MOUSE_FILTER_IGNORE + _layout.add_child(_heading) + _view_scroll = ScrollContainer.new() + _view_scroll.name = "PickerScroll" + _view_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED + _view_scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL + _view_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL + _layout.add_child(_view_scroll) + _picker_view = SURFACE_COLOR_PICKER_VIEW.new() as StageSurfaceColorPickerView + _picker_view.name = "SurfaceColorPickerView" + _picker_view.size_flags_horizontal = Control.SIZE_EXPAND_FILL + _picker_view.preview_color_changed.connect(_on_preview_color_changed) + _view_scroll.add_child(_picker_view) + _actions = HBoxContainer.new() + _actions.name = "Actions" + _actions.alignment = BoxContainer.ALIGNMENT_END + _actions.add_theme_constant_override("separation", 8) + _layout.add_child(_actions) + _cancel_button = Button.new() + _cancel_button.name = "CancelButton" + _cancel_button.text = "Cancel" + _cancel_button.focus_mode = Control.FOCUS_ALL + _cancel_button.pressed.connect(_cancel) + _actions.add_child(_cancel_button) + _use_button = Button.new() + _use_button.name = "UseColorButton" + _use_button.text = "Use color" + _use_button.focus_mode = Control.FOCUS_ALL + _use_button.pressed.connect(_confirm_color) + _actions.add_child(_use_button) + + +func _apply_visual_style() -> void: + if _stage_theme == null or not _built: + return + theme = _stage_theme.control_theme() + var radius: int = _integer("popups.asset_selector.corner_radius", 16, 8, 32) + var shell: StyleBoxFlat = _stage_theme.stylebox("popups.asset_selector") + shell.set_corner_radius_all(radius) + shell.content_margin_left = 16.0 + shell.content_margin_right = 16.0 + shell.content_margin_top = 14.0 + shell.content_margin_bottom = 14.0 + add_theme_stylebox_override("panel", shell) + var text_color: Color = _color("stage_panels.type.text_color", Color("#F5F7FA")) + var font: Font = _stage_theme.font() + if font != null: + _heading.add_theme_font_override("font", font) + _cancel_button.add_theme_font_override("font", font) + var strong_font := FontVariation.new() + strong_font.base_font = font + strong_font.variation_embolden = 0.55 + _use_button.add_theme_font_override("font", strong_font) + _heading.add_theme_font_size_override("font_size", _integer("popups.asset_selector.setup_title_size", 30, 22, 48)) + _heading.add_theme_color_override("font_color", text_color) + _apply_action_button(_cancel_button, false) + _apply_action_button(_use_button, true) + + +func _apply_action_button(button: Button, primary: bool) -> void: + var accent: Color = _color("mode_colors.edit", Color("#22C6FF")) + var text_color: Color = _color("stage_panels.type.text_color", Color("#F5F7FA")) + var neutral: Color = _color("stage_panels.choice.neutral_surface", Color("#171B24")) + var hover: Color = _color("stage_panels.choice.hover_surface", Color("#303A4B")) + var selected: Color = _color("stage_panels.choice.selected_surface", Color("#12394A")) + var border: Color = _color("stage_panels.choice.border_color", Color("#FFFFFF33")) + var blue: Color = _color("popups.asset_selector.action_color", Color("#087DBA")) + var radius: int = _integer("popups.asset_selector.corner_radius", 16, 8, 32) + var border_width: int = _integer("popups.asset_selector.button_border_width", 2, 1, 5) + var normal_color: Color = blue if primary else neutral + var hover_color: Color = blue.lightened(0.12) if primary else hover + var pressed_color: Color = blue.darkened(0.12) if primary else selected + var edge: Color = accent.lightened(0.15) if primary else border + button.add_theme_stylebox_override("normal", _make_button_style(normal_color, edge, border_width, radius)) + button.add_theme_stylebox_override("hover", _make_button_style(hover_color, edge, border_width + 1, radius)) + button.add_theme_stylebox_override("pressed", _make_button_style(pressed_color, accent, border_width + 1, radius)) + button.add_theme_stylebox_override("hover_pressed", _make_button_style(pressed_color.lightened(0.05), accent, border_width + 1, radius)) + button.add_theme_stylebox_override("focus", _make_button_style(Color(0, 0, 0, 0), accent, border_width + 1, radius)) + button.add_theme_color_override("font_color", Color.WHITE if primary else text_color) + button.add_theme_color_override("font_hover_color", Color.WHITE if primary else text_color) + button.add_theme_color_override("font_pressed_color", Color.WHITE if primary else text_color) + button.add_theme_font_size_override("font_size", _integer("popups.asset_selector.setup_button_font_size", 20, 14, 40)) + button.custom_minimum_size.y = float(_integer("popups.dialog.action_min_target_size", 48, 44, 96)) + + +func _apply_responsive_layout(popup_size: Vector2i) -> void: + if not _built: + return + var compact: bool = popup_size.y < COMPACT_LAYOUT_MAX_HEIGHT + var margin_size: int = 8 if compact else 12 for side: String in ["left", "right", "top", "bottom"]: - margin.add_theme_constant_override("margin_" + side, 12) - add_child(margin) - var layout := VBoxContainer.new() - layout.add_theme_constant_override("separation", 8) - margin.add_child(layout) - var preview_heading := Label.new() - preview_heading.text = "Preview" - layout.add_child(preview_heading) - _surface_preview = SURFACE_PREVIEW.new() as StageSurfacePreview - var preview_size := Vector2(420.0, 140.0) - var raw_preview: Variant = _stage_theme.value("popups.dialog.surface_color_preview_size", [420, 140]) if _stage_theme != null else [420, 140] - if raw_preview is Array and (raw_preview as Array).size() == 2: - preview_size = Vector2(float((raw_preview as Array)[0]), float((raw_preview as Array)[1])) - preview_size.x = minf(preview_size.x, maxf(1.0, float(_usable_window_size().x - 56))) - _surface_preview.custom_minimum_size = preview_size - _surface_preview.size_flags_horizontal = Control.SIZE_EXPAND_FILL - _surface_preview.mouse_filter = Control.MOUSE_FILTER_IGNORE - layout.add_child(_surface_preview) - var picker_scroll := ScrollContainer.new() - picker_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED - picker_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL - picker_scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL - layout.add_child(picker_scroll) - _color_picker = ColorPicker.new() - _color_picker.name = "ColorPickerControl" - var picker_min_size := Vector2(320.0, 340.0) - var raw_picker_size: Variant = _stage_theme.value("popups.dialog.surface_color_picker_min_size", [320, 340]) if _stage_theme != null else [320, 340] - if raw_picker_size is Array and (raw_picker_size as Array).size() == 2: - picker_min_size = Vector2(float((raw_picker_size as Array)[0]), float((raw_picker_size as Array)[1])) - picker_min_size.x = minf(picker_min_size.x, maxf(1.0, float(_usable_window_size().x - 56))) - _color_picker.custom_minimum_size = picker_min_size - _color_picker.size_flags_horizontal = Control.SIZE_EXPAND_FILL - _color_picker.focus_mode = Control.FOCUS_ALL - _color_picker.color_changed.connect(_on_picker_color_changed) - picker_scroll.add_child(_color_picker) - var actions := HBoxContainer.new() - actions.alignment = BoxContainer.ALIGNMENT_END - actions.add_theme_constant_override("separation", 8) - layout.add_child(actions) - var use_button := Button.new() - use_button.text = "Use color" - var action_width: float = maxf(44.0, minf(120.0, (float(_usable_window_size().x) - 40.0) * 0.5)) - use_button.custom_minimum_size = Vector2(action_width, float(_integer("popups.dialog.action_min_target_size", 48, 44, 96))) - use_button.focus_mode = Control.FOCUS_ALL - use_button.pressed.connect(_confirm_color) - actions.add_child(use_button) - var cancel_button := Button.new() - cancel_button.text = "✕ Cancel" - cancel_button.custom_minimum_size = Vector2(action_width, float(_integer("popups.dialog.action_min_target_size", 48, 44, 96))) - cancel_button.focus_mode = Control.FOCUS_ALL - cancel_button.pressed.connect(_cancel) - actions.add_child(cancel_button) - close_requested.connect(_cancel) - if _stage_theme != null: - var font: Font = _stage_theme.font() - if font != null: - preview_heading.add_theme_font_override("font", font) - use_button.add_theme_font_override("font", font) - cancel_button.add_theme_font_override("font", font) - preview_heading.add_theme_font_size_override("font_size", _stage_theme.integer("stage_panels.type.section_size", 20, 12, 40)) - use_button.add_theme_stylebox_override("normal", _stage_theme.stylebox("stage_panels.action")) - cancel_button.add_theme_stylebox_override("normal", _stage_theme.stylebox("stage_panels.action")) - use_button.add_theme_color_override("font_color", _stage_theme.color("global.text_color", Color("#F5F7FA"))) - cancel_button.add_theme_color_override("font_color", _stage_theme.color("global.text_color", Color("#F5F7FA"))) + _margin.add_theme_constant_override("margin_" + side, margin_size) + _layout.add_theme_constant_override("separation", 4 if compact else 8) + _heading.add_theme_font_size_override("font_size", _integer("popups.asset_selector.setup_title_size", 30, 22, 48)) + var content_width: float = maxf(1.0, float(popup_size.x - margin_size * 2)) + _picker_view.set_available_size(Vector2(content_width, float(popup_size.y))) + _apply_action_layout(content_width) -func _on_picker_color_changed(color: Color) -> void: - _update_preview(color) +func _apply_action_layout(content_width: float) -> void: + if _actions == null or _cancel_button == null or _use_button == null: + return + var stack_buttons: bool = content_width < 344.0 + _actions.vertical = stack_buttons + _actions.alignment = BoxContainer.ALIGNMENT_END + var action_height: float = float(_integer("popups.dialog.action_min_target_size", 48, 44, 96)) + var button_width: float = content_width if stack_buttons else maxf(112.0, (content_width - 8.0) * 0.5) + _cancel_button.custom_minimum_size = Vector2(button_width, action_height) + _use_button.custom_minimum_size = Vector2(button_width, action_height) + + +func _on_preview_color_changed(color: Color) -> void: preview_color_changed.emit(color) -func _update_preview(color: Color) -> void: - if _surface_preview != null: - _surface_preview.configure(_surface_kind, _finish_id, color) - - func _confirm_color() -> void: - var confirmed_color: Color = _color_picker.color + if _picker_view == null: + return + var confirmed_color: Color = _picker_view.get_color() hide() color_confirmed.emit(confirmed_color) func _cancel() -> void: - _update_preview(_initial_color) + if _picker_view != null: + _picker_view.set_color_no_signal(_initial_color) hide() cancelled.emit() +func _focus_picker() -> void: + if visible and _picker_view != null: + _picker_view.focus_picker() + + +func _on_root_size_changed() -> void: + if visible: + open_dialog() + + +func _bounded_dialog_size() -> Vector2i: + var max_size: Vector2i = _usable_window_size() + var preferred_size := Vector2i(560, 640) + var raw_size: Variant = _stage_theme.value("popups.dialog.surface_color_size", [560, 640]) if _stage_theme != null else [560, 640] + if raw_size is Array and (raw_size as Array).size() == 2: + preferred_size = Vector2i(int((raw_size as Array)[0]), int((raw_size as Array)[1])) + return Vector2i(maxi(1, mini(preferred_size.x, max_size.x)), maxi(1, mini(preferred_size.y, max_size.y))) + + func _usable_window_size() -> Vector2i: var available: Vector2i = Vector2i(get_tree().root.size) var usable: Rect2i = DisplayServer.screen_get_usable_rect(DisplayServer.window_get_current_screen()) @@ -156,5 +235,22 @@ func _usable_window_size() -> Vector2i: return available +func _make_button_style(background: Color, border: Color, border_width: int, radius: int) -> StyleBoxFlat: + var style := StyleBoxFlat.new() + style.bg_color = background + style.border_color = border + style.set_border_width_all(border_width) + style.set_corner_radius_all(radius) + style.content_margin_left = 12.0 + style.content_margin_right = 12.0 + style.content_margin_top = 8.0 + style.content_margin_bottom = 8.0 + return style + + func _integer(path: String, fallback: int, minimum: int, maximum: int) -> int: return fallback if _stage_theme == null else _stage_theme.integer(path, fallback, minimum, maximum) + + +func _color(path: String, fallback: Color) -> Color: + return fallback if _stage_theme == null else _stage_theme.color(path, fallback) diff --git a/scripts/shared/stage_ui/stage_surface_color_picker_view.gd b/scripts/shared/stage_ui/stage_surface_color_picker_view.gd new file mode 100644 index 0000000..c111ef1 --- /dev/null +++ b/scripts/shared/stage_ui/stage_surface_color_picker_view.gd @@ -0,0 +1,130 @@ +class_name StageSurfaceColorPickerView +extends VBoxContainer +## Inline color picker content for the scenery chooser. + +const SURFACE_PREVIEW := preload("res://scripts/stage/scenery/stage_surface_preview.gd") +const COMPACT_LAYOUT_MAX_HEIGHT := 640.0 +const COMPACT_PREVIEW_HEIGHT := 64.0 +const COMPACT_PICKER_MIN_HEIGHT := 260.0 +const COMPACT_SV_HEIGHT := 144 +const REGULAR_SV_HEIGHT := 224 + +signal preview_color_changed(color: Color) + +var _surface_kind: StringName = &"wall" +var _finish_id: StringName = &"finish_wall_paint" +var _stage_theme: SandboxTheme +var _color_picker: ColorPicker +var _surface_preview: StageSurfacePreview +var _preview_heading: Label +var _preferred_preview_size: Vector2 = Vector2(420.0, 140.0) +var _preferred_preview_height: float = 140.0 +var _preferred_picker_min_size: Vector2 = Vector2(320.0, 340.0) +var _built: bool = false +var _suppress_color_signal: bool = false + + +func configure(surface_kind: StringName, finish: StringName, color: Color, stage_theme: SandboxTheme) -> void: + _surface_kind = surface_kind + _finish_id = finish + _stage_theme = stage_theme + if not _built: + _build() + set_color_no_signal(color) + theme = stage_theme.control_theme() if stage_theme != null else null + _apply_preview_heading_theme() + + +func get_color() -> Color: + return _color_picker.color if _color_picker != null else Color.WHITE + + +func set_color_no_signal(color: Color) -> void: + if _color_picker == null: + return + _suppress_color_signal = true + _color_picker.color = color + _suppress_color_signal = false + _update_preview(color) + + +func set_available_size(available_size: Vector2) -> void: + if not _built: + return + var compact: bool = available_size.y < COMPACT_LAYOUT_MAX_HEIGHT + var preview_size: Vector2 = _surface_preview.custom_minimum_size + preview_size.x = minf(_preferred_preview_size.x, maxf(1.0, available_size.x - 56.0)) + preview_size.y = minf(_preferred_preview_height, COMPACT_PREVIEW_HEIGHT) if compact else _preferred_preview_height + _surface_preview.custom_minimum_size = preview_size + var picker_size: Vector2 = _color_picker.custom_minimum_size + picker_size.x = minf(_preferred_picker_min_size.x, maxf(1.0, available_size.x - 56.0)) + picker_size.y = minf(_preferred_picker_min_size.y, COMPACT_PICKER_MIN_HEIGHT) if compact else _preferred_picker_min_size.y + _color_picker.custom_minimum_size = picker_size + _color_picker.add_theme_constant_override("sv_height", COMPACT_SV_HEIGHT if compact else REGULAR_SV_HEIGHT) + + +func focus_picker() -> void: + if _color_picker != null: + _color_picker.grab_focus() + + +func _build() -> void: + _built = true + add_theme_constant_override("separation", 8) + _surface_preview = SURFACE_PREVIEW.new() as StageSurfacePreview + _preview_heading = Label.new() + _preview_heading.name = "PreviewHeading" + _preview_heading.text = "Preview" + _preview_heading.mouse_filter = Control.MOUSE_FILTER_IGNORE + add_child(_preview_heading) + var preview_size := _preferred_preview_size + var raw_preview: Variant = _stage_theme.value("popups.dialog.surface_color_preview_size", [420, 140]) if _stage_theme != null else [420, 140] + if raw_preview is Array and (raw_preview as Array).size() == 2: + preview_size = Vector2(float((raw_preview as Array)[0]), float((raw_preview as Array)[1])) + _preferred_preview_size = preview_size + _preferred_preview_height = preview_size.y + _surface_preview.custom_minimum_size = preview_size + _surface_preview.size_flags_horizontal = Control.SIZE_EXPAND_FILL + _surface_preview.set_rounded_frame( + float(_stage_theme.integer("popups.asset_selector.surface_corner_radius", 14, 4, 32)) if _stage_theme != null else 14.0, + _stage_theme.color("popups.style_defaults.bg_color", Color("#202633FA")) if _stage_theme != null else Color("#202633FA") + ) + add_child(_surface_preview) + _color_picker = ColorPicker.new() + _color_picker.name = "ColorPickerControl" + var picker_size := _preferred_picker_min_size + var raw_picker_size: Variant = _stage_theme.value("popups.dialog.surface_color_picker_min_size", [320, 340]) if _stage_theme != null else [320, 340] + if raw_picker_size is Array and (raw_picker_size as Array).size() == 2: + picker_size = Vector2(float((raw_picker_size as Array)[0]), float((raw_picker_size as Array)[1])) + _preferred_picker_min_size = picker_size + _color_picker.custom_minimum_size = picker_size + _color_picker.presets_visible = false + _color_picker.sampler_visible = false + _color_picker.size_flags_horizontal = Control.SIZE_EXPAND_FILL + _color_picker.focus_mode = Control.FOCUS_ALL + _color_picker.color_changed.connect(_on_picker_color_changed) + add_child(_color_picker) + _apply_preview_heading_theme() + + +func _apply_preview_heading_theme() -> void: + if _preview_heading == null: + return + if _stage_theme == null: + return + _preview_heading.add_theme_font_size_override("font_size", _stage_theme.integer("stage_panels.type.section_size", 20, 12, 40)) + _preview_heading.add_theme_color_override("font_color", _stage_theme.color("stage_panels.type.text_color", Color("#F5F7FA"))) + var font: Font = _stage_theme.font() + if font != null: + _preview_heading.add_theme_font_override("font", font) + + +func _on_picker_color_changed(color: Color) -> void: + _update_preview(color) + if not _suppress_color_signal: + preview_color_changed.emit(color) + + +func _update_preview(color: Color) -> void: + if _surface_preview != null: + _surface_preview.configure(_surface_kind, _finish_id, color) diff --git a/scripts/shared/stage_ui/stage_surface_color_picker_view.gd.uid b/scripts/shared/stage_ui/stage_surface_color_picker_view.gd.uid new file mode 100644 index 0000000..4f808bc --- /dev/null +++ b/scripts/shared/stage_ui/stage_surface_color_picker_view.gd.uid @@ -0,0 +1 @@ +uid://dbvatqk4a287y diff --git a/scripts/shared/ui/asset_selector.gd b/scripts/shared/ui/asset_selector.gd index 93f486d..7eef4b6 100644 --- a/scripts/shared/ui/asset_selector.gd +++ b/scripts/shared/ui/asset_selector.gd @@ -17,6 +17,8 @@ const GRID_V_SEPARATION: float = 8.0 const POPUP_SIDE_PADDING: float = 40.0 const POPUP_VERTICAL_PADDING: float = 32.0 const POPUP_SAFE_SCREEN_MARGIN: float = 24.0 +const ARTWORK_FILTER_IDS: Array[String] = ["all", "nature", "buildings", "walls", "roads", "objects"] +const ARTWORK_FILTER_LABELS: Array[String] = ["All artwork", "Nature", "Buildings", "Walls", "Roads & paths", "Objects"] const CELL_MIN_SIZE := Vector2(190.0, 220.0) const THUMB_SIZE := Vector2(156.0, 156.0) @@ -26,7 +28,7 @@ const STARTER_CARD_PREVIEW := preload("res://scripts/stage/scenery/stage_starter const SURFACE_RENDERER := preload("res://scripts/stage/scenery/stage_surface_renderer.gd") const SURFACE_PREVIEW := preload("res://scripts/stage/scenery/stage_surface_preview.gd") const SURFACE_COLOR_CONTROLS := preload("res://scripts/shared/stage_ui/stage_surface_color_controls.gd") -const SURFACE_COLOR_DIALOG := preload("res://scripts/shared/stage_ui/stage_surface_color_dialog.gd") +const SURFACE_COLOR_PICKER_VIEW := preload("res://scripts/shared/stage_ui/stage_surface_color_picker_view.gd") const STAGE_PANEL_CHOICE_GROUP := preload("res://scripts/shared/stage_ui/stage_panel_choice_group.gd") var kind: String = "" @@ -38,31 +40,41 @@ var _cell_texrects: Dictionary = {} var _placeholder_tex: ImageTexture = null var _ui_font: Font = null var _emoji_font: Font = null +var _strong_ui_font: Font = null +var _strong_action_font: Font = null var _sandbox_theme: Variant = null var _gallery_tab: String = "backgrounds" +var _gallery_tab_group: ButtonGroup +var _filter_chip_group: ButtonGroup +var _artwork_filter_buttons: Array[Button] = [] var _tag_id: String = "all" var _current_sky_id: String = "" var _sky_card_group: ButtonGroup var _active_columns: int = COLUMNS var _page_capacity: int = PAGE_SIZE var _effective_cell_size: Vector2 = CELL_MIN_SIZE +var _surface_cell_size: Vector2 = CELL_MIN_SIZE var _surface_color_history: Array[Color] = [] var _setup_view: VBoxContainer +var _color_picker_view: StageSurfaceColorPickerView var _setup_kind: StringName = &"" var _setup_finish: StringName = &"" var _setup_color: Color = Color.WHITE var _setup_original_color: Color = Color.WHITE var _setup_color_chosen: bool = false +var _color_picker_open: bool = false var _setup_preview: StageSurfacePreview var _setup_color_controls: StageSurfaceColorControls -var _setup_color_dialog: StageSurfaceColorDialog var _setup_cancel_button: Button var _setup_draw_button: Button +var _color_back_button: Button +var _color_use_button: Button @onready var _title_label: Label = %TitleLabel @onready var _grid: GridContainer = %GridContainer @onready var _content_stack: VBoxContainer = %ContentStack @onready var _grid_scroll: ScrollContainer = %GridScroll +@onready var _surface_cards_row: HBoxContainer = %SurfaceCardsRow @onready var _empty_label: Label = %EmptyLabel @onready var _page_label: Label = %PageLabel @onready var _prev_button: Button = %PrevButton @@ -70,10 +82,11 @@ var _setup_draw_button: Button @onready var _browse_button: Button = %BrowseButton @onready var _refresh_button: Button = %RefreshButton @onready var _close_button: Button = %CloseButton -@onready var _tabs: HBoxContainer = %GalleryTabs +@onready var _footer_spacer: Control = %FooterSpacer +@onready var _tabs: HFlowContainer = %GalleryTabs @onready var _backgrounds_tab: Button = %BackgroundsTab @onready var _artwork_tab: Button = %ArtworkTab -@onready var _tag_filter: OptionButton = %TagFilter +@onready var _filter_chips: HFlowContainer = %ArtworkFilters @onready var _make_artwork_button: Button = %MakeArtworkButton @onready var _edit_scenery_button: Button = %EditSceneryButton @onready var _title_bar: Control = $MarginContainer/VBoxContainer/TitleBar @@ -82,10 +95,15 @@ var _setup_draw_button: Button func _ready() -> void: exclusive = true + _gallery_tab_group = ButtonGroup.new() + _gallery_tab_group.allow_unpress = false + _filter_chip_group = ButtonGroup.new() + _filter_chip_group.allow_unpress = false + for tab: Button in [_backgrounds_tab, _artwork_tab]: + tab.toggle_mode = true + tab.button_group = _gallery_tab_group _build_surface_setup_view() - if _tag_filter.item_count == 0: - for label: String in ["All artwork", "Nature", "Buildings", "Walls", "Roads", "Objects"]: - _tag_filter.add_item(label) + _rebuild_artwork_filter_chips() _prev_button.pressed.connect(_on_prev) _next_button.pressed.connect(_on_next) _browse_button.pressed.connect(func() -> void: browse_requested.emit()) @@ -93,10 +111,10 @@ func _ready() -> void: _close_button.pressed.connect(func() -> void: cancelled.emit()) _backgrounds_tab.pressed.connect(func() -> void: _set_gallery_tab("backgrounds")) _artwork_tab.pressed.connect(func() -> void: _set_gallery_tab("artwork")) - _tag_filter.item_selected.connect(_on_tag_filter_selected) _make_artwork_button.pressed.connect(func() -> void: make_artwork_requested.emit()) _edit_scenery_button.pressed.connect(func() -> void: edit_scenery_requested.emit()) get_tree().root.size_changed.connect(_on_root_size_changed) + _apply_chooser_visual_style() func _on_root_size_changed() -> void: @@ -108,11 +126,15 @@ func _on_root_size_changed() -> void: func open(p_kind: String, entries: Array[Dictionary], current_sky_id: String = "") -> void: kind = p_kind _setup_kind = &"" + _color_picker_open = false _setup_view.visible = false + if _color_picker_view != null: + _color_picker_view.visible = false _current_sky_id = current_sky_id if kind == "scenery" else "" _title_label.text = "Choose Your Stickman" if kind == "stickman" else ("Choose Scenery" if kind == "scenery" else ("Choose a starter scene" if kind == "starters" else "Choose a Prop")) + _apply_chooser_visual_style() _tabs.visible = kind == "scenery" - _tag_filter.visible = false + _filter_chips.visible = false _make_artwork_button.visible = false _edit_scenery_button.visible = false _browse_button.visible = kind == "stickman" or kind == "prop" or kind == "scenery" @@ -122,15 +144,16 @@ func open(p_kind: String, entries: Array[Dictionary], current_sky_id: String = " _tag_id = "all" _backgrounds_tab.set_pressed_no_signal(true) _artwork_tab.set_pressed_no_signal(false) - _tag_filter.select(0) set_entries(entries) if kind == "scenery": _set_gallery_tab("backgrounds") - popup_centered() + popup_centered(size) func set_entries(entries: Array[Dictionary]) -> void: _entries = entries + if kind == "scenery": + _rebuild_artwork_filter_chips() _thumbnails.clear() _cell_texrects.clear() _page = 0 @@ -167,12 +190,22 @@ func close() -> void: func apply_font(ui_font: Font, emoji_font: Font) -> void: _ui_font = ui_font _emoji_font = emoji_font + var base_font: Font = ui_font if ui_font != null else emoji_font + _strong_ui_font = _make_strong_font(base_font, 0.3) + _strong_action_font = _make_strong_font(base_font, 0.65) var controls: Array = [ _title_label, _empty_label, _page_label, _prev_button, _next_button, _browse_button, _refresh_button, _close_button, + _edit_scenery_button, _make_artwork_button, + _backgrounds_tab, _artwork_tab, + _setup_view.get_node_or_null("SetupHeading") if _setup_view != null else null, + get_meta("setup_helper", null) as Control, + _setup_cancel_button, _setup_draw_button, + _color_back_button, _color_use_button, ] for c: Control in controls: _apply_font_to(c) + _apply_chooser_visual_style() func apply_theme(theme: Variant) -> void: _sandbox_theme = theme @@ -193,49 +226,53 @@ func apply_theme(theme: Variant) -> void: ground_choices.apply_theme(theme as SandboxTheme) if _setup_color_controls != null: _setup_color_controls.configure(_setup_color, _surface_color_history, _preset_surface_colors(), theme as SandboxTheme) + if _color_picker_view != null: + _color_picker_view.configure(_setup_kind, _setup_finish, _setup_color, theme as SandboxTheme) + _apply_chooser_visual_style() func _build_surface_setup_view() -> void: _setup_view = VBoxContainer.new() _setup_view.name = "SurfaceSetupView" _setup_view.size_flags_horizontal = Control.SIZE_EXPAND_FILL - _setup_view.add_theme_constant_override("separation", mini(_theme_integer("popups.asset_selector.setup_gap", 10, 0, 32), 6)) + _setup_view.add_theme_constant_override("separation", _theme_integer("popups.asset_selector.setup_gap", 8, 4, 24)) _setup_view.visible = false _content_stack.add_child(_setup_view) - var heading_row := HBoxContainer.new() - heading_row.add_theme_constant_override("separation", 8) - _setup_view.add_child(heading_row) - var back_button := Button.new() - back_button.text = "‹ Back" - back_button.custom_minimum_size = Vector2(float(_theme_integer("stage_panels.action.close_width", 92, 72, 180)), float(_theme_integer("stage_panels.action.min_target_size", 48, 44, 96))) - back_button.focus_mode = Control.FOCUS_ALL - back_button.pressed.connect(_back_from_surface_setup) - heading_row.add_child(back_button) var heading := Label.new() heading.name = "SetupHeading" heading.size_flags_horizontal = Control.SIZE_EXPAND_FILL heading.vertical_alignment = VERTICAL_ALIGNMENT_CENTER - heading.add_theme_font_size_override("font_size", _theme_integer("stage_panels.type.title_size", 24, 12, 48)) - heading_row.add_child(heading) - var look_heading := Label.new() - look_heading.text = "Look" - look_heading.add_theme_font_size_override("font_size", _theme_integer("stage_panels.type.section_size", 20, 12, 40)) - _setup_view.add_child(look_heading) + heading.add_theme_font_size_override("font_size", _theme_integer("popups.asset_selector.setup_heading_size", 26, 18, 48)) + _setup_view.add_child(heading) var wall_choices: StagePanelChoiceGroup = STAGE_PANEL_CHOICE_GROUP.new() as StagePanelChoiceGroup wall_choices.name = "WallLookChoices" + wall_choices.configure_layout( + false, + float(_theme_integer("popups.asset_selector.setup_choice_height", 68, 48, 104)), + _theme_integer("popups.asset_selector.setup_choice_font_size", 22, 16, 40), + _theme_integer("popups.asset_selector.setup_choice_corner_radius", 22, 8, 32), + _theme_integer("popups.asset_selector.setup_choice_border_width", 2, 1, 5) + ) wall_choices.add_choice(&"finish_wall_paint", "Paint", _make_look_cue(&"wall", &"finish_wall_paint")) wall_choices.add_choice(&"finish_wall_brick", "Brick", _make_look_cue(&"wall", &"finish_wall_brick")) wall_choices.add_choice(&"finish_wall_wood", "Wood", _make_look_cue(&"wall", &"finish_wall_wood")) wall_choices.choice_selected.connect(_on_setup_look_choice) - _compact_look_choices(wall_choices) + wall_choices.size_flags_horizontal = Control.SIZE_EXPAND_FILL _setup_view.add_child(wall_choices) var ground_choices: StagePanelChoiceGroup = STAGE_PANEL_CHOICE_GROUP.new() as StagePanelChoiceGroup ground_choices.name = "BackgroundGroundLookChoices" + ground_choices.configure_layout( + false, + float(_theme_integer("popups.asset_selector.setup_choice_height", 68, 48, 104)), + _theme_integer("popups.asset_selector.setup_choice_font_size", 22, 16, 40), + _theme_integer("popups.asset_selector.setup_choice_corner_radius", 22, 8, 32), + _theme_integer("popups.asset_selector.setup_choice_border_width", 2, 1, 5) + ) ground_choices.add_choice(&"finish_background_grass", "Grass", _make_look_cue(&"background_ground", &"finish_background_grass")) ground_choices.add_choice(&"finish_background_concrete", "Concrete", _make_look_cue(&"background_ground", &"finish_background_concrete")) ground_choices.add_choice(&"finish_background_carpet", "Carpet", _make_look_cue(&"background_ground", &"finish_background_carpet")) ground_choices.choice_selected.connect(_on_setup_look_choice) - _compact_look_choices(ground_choices) + ground_choices.size_flags_horizontal = Control.SIZE_EXPAND_FILL _setup_view.add_child(ground_choices) set_meta("wall_look_group", wall_choices) set_meta("ground_look_group", ground_choices) @@ -246,6 +283,10 @@ func _build_surface_setup_view() -> void: preview_frame.custom_minimum_size.y = preview_size.y + 10.0 preview_frame.size_flags_horizontal = Control.SIZE_EXPAND_FILL _setup_preview = SURFACE_PREVIEW.new() as StageSurfacePreview + _setup_preview.set_rounded_frame( + float(_theme_integer("popups.asset_selector.surface_corner_radius", 14, 4, 32)), + _theme_color("popups.style_defaults.bg_color", Color("#202633FA")) + ) _setup_preview.custom_minimum_size = preview_size _setup_preview.size_flags_horizontal = Control.SIZE_EXPAND_FILL preview_frame.add_child(_setup_preview) @@ -254,42 +295,55 @@ func _build_surface_setup_view() -> void: _setup_color_controls.name = "SurfaceColorControls" _setup_color_controls.set_compact_mode(true) _setup_color_controls.color_selected.connect(_on_setup_color_selected) - _setup_color_controls.custom_color_requested.connect(_open_setup_color_dialog) + _setup_color_controls.custom_color_requested.connect(_open_setup_color_view) _setup_view.add_child(_setup_color_controls) var helper := Label.new() helper.name = "SurfaceRoleHelper" helper.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART helper.add_theme_color_override("font_color", _theme_color("global.muted_text_color", Color("#AAB2C0"))) _setup_view.add_child(helper) + _color_picker_view = SURFACE_COLOR_PICKER_VIEW.new() as StageSurfaceColorPickerView + _color_picker_view.name = "SurfaceColorPickerView" + _color_picker_view.size_flags_horizontal = Control.SIZE_EXPAND_FILL + _color_picker_view.preview_color_changed.connect(_on_setup_color_preview) + _color_picker_view.visible = false + _content_stack.add_child(_color_picker_view) _setup_cancel_button = Button.new() _setup_cancel_button.name = "CancelSurfaceSetupButton" _setup_cancel_button.text = "Cancel" - _setup_cancel_button.custom_minimum_size = Vector2(104.0, float(_theme_integer("stage_panels.action.min_target_size", 48, 44, 96))) + _setup_cancel_button.custom_minimum_size = Vector2(116.0, float(_theme_integer("stage_panels.action.min_target_size", 48, 44, 96))) _setup_cancel_button.focus_mode = Control.FOCUS_ALL _setup_cancel_button.pressed.connect(_cancel_surface_setup) _footer.add_child(_setup_cancel_button) _setup_draw_button = Button.new() _setup_draw_button.name = "DrawSurfaceButton" - _setup_draw_button.custom_minimum_size = Vector2(220.0, float(_theme_integer("stage_panels.choice.minimum_height", 56, 44, 112))) + _setup_draw_button.custom_minimum_size = Vector2(244.0, float(_theme_integer("popups.asset_selector.tab_height", 60, 48, 96))) _setup_draw_button.focus_mode = Control.FOCUS_ALL _setup_draw_button.pressed.connect(_draw_selected_surface) _footer.add_child(_setup_draw_button) + _color_back_button = Button.new() + _color_back_button.name = "BackToSurfaceButton" + _color_back_button.text = "Back to wall panel" + _color_back_button.custom_minimum_size = Vector2(200.0, float(_theme_integer("stage_panels.action.min_target_size", 48, 44, 96))) + _color_back_button.focus_mode = Control.FOCUS_ALL + _color_back_button.pressed.connect(_back_from_setup_color_view) + _footer.add_child(_color_back_button) + _color_use_button = Button.new() + _color_use_button.name = "UseColorButton" + _color_use_button.text = "Use color" + _color_use_button.custom_minimum_size = Vector2(160.0, float(_theme_integer("stage_panels.choice.minimum_height", 56, 44, 112))) + _color_use_button.focus_mode = Control.FOCUS_ALL + _color_use_button.pressed.connect(_use_setup_color) + _footer.add_child(_color_use_button) set_meta("setup_heading", heading) set_meta("setup_helper", helper) - _apply_font_to(back_button) _apply_font_to(heading) - _apply_font_to(look_heading) _apply_font_to(helper) _apply_font_to(_setup_cancel_button) _apply_font_to(_setup_draw_button) - if _sandbox_theme != null: - _setup_cancel_button.add_theme_stylebox_override("normal", _sandbox_theme.stylebox("stage_panels.choice")) - _setup_cancel_button.add_theme_stylebox_override("hover", _sandbox_theme.stylebox("stage_panels.choice")) - _setup_cancel_button.add_theme_font_size_override("font_size", _theme_integer("stage_panels.type.body_size", 20, 14, 40)) - _setup_draw_button.add_theme_stylebox_override("normal", _sandbox_theme.stylebox("stage_panels.action")) - _setup_draw_button.add_theme_stylebox_override("hover", _sandbox_theme.stylebox("stage_panels.action")) - _setup_draw_button.add_theme_color_override("font_color", _theme_color("stage_panels.type.text_color", Color("#F5F7FA"))) - _setup_draw_button.add_theme_font_size_override("font_size", _theme_integer("stage_panels.type.body_size", 20, 14, 40)) + _apply_font_to(_color_back_button) + _apply_font_to(_color_use_button) + _apply_chooser_visual_style() for choice: StagePanelChoiceGroup in [wall_choices, ground_choices]: choice.apply_theme(_sandbox_theme as SandboxTheme) _apply_font_to(choice) @@ -298,20 +352,17 @@ func _build_surface_setup_view() -> void: func _make_look_cue(kind: StringName, finish: StringName) -> Control: var preview: StageSurfacePreview = SURFACE_PREVIEW.new() as StageSurfacePreview var cue_size: Vector2 = _theme_vector2("popups.asset_selector.look_cue_size", Vector2(80.0, 42.0)) - cue_size.x = minf(cue_size.x, 68.0) - cue_size.y = minf(cue_size.y, 34.0) + cue_size.x = minf(cue_size.x, 56.0) + cue_size.y = minf(cue_size.y, 36.0) preview.custom_minimum_size = cue_size + preview.set_rounded_frame( + float(_theme_integer("popups.asset_selector.look_corner_radius", 8, 2, 20)), + _theme_color("popups.style_defaults.bg_color", Color("#202633FA")) + ) preview.configure(kind, finish, SURFACE_RENDERER.default_tint(finish)) return preview -func _compact_look_choices(group: StagePanelChoiceGroup) -> void: - for child: Node in group.get_children(): - if child is Button: - var button := child as Button - button.custom_minimum_size.y = minf(button.custom_minimum_size.y, 50.0) - - func _open_surface_setup(kind: StringName) -> void: _setup_kind = kind _setup_color_chosen = false @@ -325,29 +376,22 @@ func _open_surface_setup(kind: StringName) -> void: wall_choices.select_no_signal(_setup_finish) ground_choices.select_no_signal(_setup_finish) (get_meta("setup_heading") as Label).text = "Wall panel" if kind == &"wall" else "Background ground" - (get_meta("setup_helper") as Label).text = "Looks like a wall · Stickmen can pass through." if kind == &"wall" else "Decorative only · Stickmen don’t walk here." + var setup_helper: Label = get_meta("setup_helper") as Label + setup_helper.text = "Looks like a wall · Stickmen can pass through." if kind == &"wall" else "" + setup_helper.visible = kind == &"wall" _setup_draw_button.text = "Draw wall panel" if kind == &"wall" else "Draw background ground" + _color_back_button.text = "Back to wall panel" if kind == &"wall" else "Back to background ground" _setup_color_controls.visible = _supports_setup_color() _setup_color_controls.configure(_setup_color, _surface_color_history, _preset_surface_colors(), _sandbox_theme as SandboxTheme) _update_setup_preview() _page = 0 - _update_popup_layout() _rebuild() - back_button_focus() - - -func back_button_focus() -> void: - var back_button: Button = _setup_view.get_child(0).get_child(0) as Button - if back_button != null: - back_button.grab_focus() - - -func _back_from_surface_setup() -> void: - _setup_kind = &"" - _setup_view.visible = false + # The setup controls are first measured before the containers have recalculated + # their minimum sizes. Re-measure after the next layout pass so the popup can + # shrink to the setup content instead of keeping the gallery height. _update_popup_layout() - _rebuild() - _close_button.grab_focus() + call_deferred("_update_popup_layout_after_layout") + _setup_draw_button.grab_focus() func _on_setup_look_choice(id: StringName) -> void: @@ -368,7 +412,7 @@ func _supports_setup_color() -> bool: func _on_setup_color_selected(color: Color) -> void: - _setup_color = Color(color.r, color.g, color.b, 1.0) + _setup_color = _opaque_color(color) _setup_color_chosen = true _update_setup_preview() surface_color_confirmed.emit(_setup_color) @@ -387,38 +431,54 @@ func _preset_surface_colors() -> Array[Color]: return (_sandbox_theme as SandboxTheme).swatches() -func _open_setup_color_dialog() -> void: - if _setup_color_dialog == null or not is_instance_valid(_setup_color_dialog): - _setup_color_dialog = SURFACE_COLOR_DIALOG.new() as StageSurfaceColorDialog - add_child(_setup_color_dialog) - _setup_color_dialog.preview_color_changed.connect(_on_setup_color_preview) - _setup_color_dialog.color_confirmed.connect(_on_setup_color_confirmed) - _setup_color_dialog.cancelled.connect(_on_setup_color_cancelled) +func _open_setup_color_view() -> void: + if _setup_kind.is_empty() or not _supports_setup_color(): + return _setup_original_color = _setup_color - _setup_color_dialog.configure(_setup_kind, _setup_finish, _setup_original_color, _sandbox_theme as SandboxTheme) - _setup_color_dialog.open_dialog() + _color_picker_view.configure(_setup_kind, _setup_finish, _setup_original_color, _sandbox_theme as SandboxTheme) + _color_picker_open = true + _title_label.text = "Choose color" + _rebuild() + _update_popup_layout() + call_deferred("_update_popup_layout_after_layout") + _color_picker_view.focus_picker() func _on_setup_color_preview(color: Color) -> void: - _setup_color = Color(color.r, color.g, color.b, 1.0) + _setup_color = _opaque_color(color) _update_setup_preview() -func _on_setup_color_confirmed(color: Color) -> void: - _setup_color = Color(color.r, color.g, color.b, 1.0) +func _use_setup_color() -> void: + _setup_color = _opaque_color(_color_picker_view.get_color()) _setup_color_chosen = true _update_setup_preview() surface_color_confirmed.emit(_setup_color) + _close_setup_color_view() _setup_color_controls.focus_custom_button() -func _on_setup_color_cancelled() -> void: +func _back_from_setup_color_view() -> void: _setup_color = _setup_original_color _setup_color_controls.set_selected_no_signal(_setup_color) _update_setup_preview() + _close_setup_color_view() _setup_color_controls.focus_custom_button() +func _close_setup_color_view() -> void: + _color_picker_open = false + _title_label.text = "Choose Scenery" + _tabs.visible = kind == "scenery" + _rebuild() + _update_popup_layout() + call_deferred("_update_popup_layout_after_layout") + + +func _opaque_color(color: Color) -> Color: + return Color(color.r, color.g, color.b, 1.0) + + func _draw_selected_surface() -> void: if _setup_kind.is_empty(): return @@ -427,6 +487,8 @@ func _draw_selected_surface() -> void: func _cancel_surface_setup() -> void: + _color_picker_open = false + _color_picker_view.visible = false _setup_kind = &"" _setup_view.visible = false cancelled.emit() @@ -440,7 +502,10 @@ func _unhandled_input(event: InputEvent) -> void: var key := event as InputEventKey if key.pressed and not key.echo and key.keycode == KEY_ESCAPE: get_viewport().set_input_as_handled() - cancelled.emit() + if _color_picker_open: + _back_from_setup_color_view() + else: + cancelled.emit() static func page_bounds(total: int, page: int, page_size: int = PAGE_SIZE) -> Dictionary: @@ -458,28 +523,98 @@ func _entry_key(entry: Dictionary) -> String: return String(entry.get("id", "")) -func _on_tag_filter_selected(index: int) -> void: - var ids: Array[String] = ["all", "nature", "buildings", "walls", "roads", "objects"] - _tag_id = ids[clampi(index, 0, ids.size() - 1)] +func _rebuild_artwork_filter_chips() -> void: + if _filter_chips == null: + return + for child: Node in _filter_chips.get_children(): + _filter_chips.remove_child(child) + child.queue_free() + _artwork_filter_buttons.clear() + _filter_chip_group = ButtonGroup.new() + _filter_chip_group.allow_unpress = false + var filter_ids: Array[String] = ARTWORK_FILTER_IDS.duplicate() + var filter_labels: Array[String] = ARTWORK_FILTER_LABELS.duplicate() + if _has_unsorted_artwork(): + filter_ids.append("unsorted") + filter_labels.append("Unsorted") + if not filter_ids.has(_tag_id): + _tag_id = "all" + for index: int in range(filter_ids.size()): + var filter_id: String = filter_ids[index] + var chip: Button = Button.new() + chip.name = "ArtworkFilter_%s" % filter_id.capitalize() + chip.toggle_mode = true + chip.button_group = _filter_chip_group + chip.focus_mode = Control.FOCUS_ALL + chip.custom_minimum_size.y = float(_theme_integer("popups.asset_selector.filter_chip_height", 48, 44, 80)) + chip.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN + chip.pressed.connect(func() -> void: _on_artwork_filter_selected(filter_id)) + _filter_chips.add_child(chip) + _artwork_filter_buttons.append(chip) + _sync_artwork_filter_chips(filter_ids, filter_labels) + _apply_chooser_visual_style() + + +func _sync_artwork_filter_chips(filter_ids: Array[String] = [], filter_labels: Array[String] = []) -> void: + var ids: Array[String] = filter_ids if not filter_ids.is_empty() else ARTWORK_FILTER_IDS.duplicate() + var labels: Array[String] = filter_labels if not filter_labels.is_empty() else ARTWORK_FILTER_LABELS.duplicate() + if ids.size() != _artwork_filter_buttons.size() or labels.size() != _artwork_filter_buttons.size(): + return + for index: int in range(_artwork_filter_buttons.size()): + var chip: Button = _artwork_filter_buttons[index] + var selected: bool = ids[index] == _tag_id + chip.text = ("✓ " if selected else "") + labels[index] + chip.set_pressed_no_signal(selected) + + +func _on_artwork_filter_selected(filter_id: String) -> void: + _tag_id = filter_id + _sync_artwork_filter_chips(_current_filter_ids(), _current_filter_labels()) _page = 0 - _update_popup_layout() + _update_popup_layout(true) _rebuild() +func _current_filter_ids() -> Array[String]: + var ids: Array[String] = ARTWORK_FILTER_IDS.duplicate() + if _has_unsorted_artwork(): + ids.append("unsorted") + return ids + + +func _current_filter_labels() -> Array[String]: + var labels: Array[String] = ARTWORK_FILTER_LABELS.duplicate() + if _has_unsorted_artwork(): + labels.append("Unsorted") + return labels + + +func _has_unsorted_artwork() -> bool: + for entry: Dictionary in _entries: + if String(entry.get("gallery_tab", "artwork")) != "artwork": + continue + var tags: Variant = entry.get("tags", []) + if not tags is Array or (tags as Array).is_empty(): + return true + return false + + func _set_gallery_tab(tab: String) -> void: + _color_picker_open = false _setup_kind = &"" _setup_view.visible = false + _color_picker_view.visible = false _gallery_tab = tab _backgrounds_tab.set_pressed_no_signal(tab == "backgrounds") _artwork_tab.set_pressed_no_signal(tab == "artwork") - _tag_filter.visible = kind == "scenery" and tab == "artwork" + _filter_chips.visible = kind == "scenery" and tab == "artwork" _make_artwork_button.visible = kind == "scenery" and tab == "artwork" _edit_scenery_button.visible = kind == "scenery" - _browse_button.visible = kind == "stickman" or kind == "prop" or (kind == "scenery" and tab == "artwork") - _refresh_button.visible = kind == "stickman" or kind == "prop" or (kind == "scenery" and tab == "artwork") - _empty_label.text = "No artwork in this group yet." if tab == "artwork" else "Choose a Sky look or wall finish." + _browse_button.visible = kind == "stickman" or kind == "prop" + _refresh_button.visible = kind == "stickman" or kind == "prop" + _empty_label.text = "No artwork here yet." if tab == "artwork" else "Choose a Sky look or wall finish." _page = 0 - _update_popup_layout() + _update_popup_layout(true) _rebuild() @@ -491,8 +626,15 @@ func _filtered_entries() -> Array[Dictionary]: if String(entry.get("gallery_tab", "artwork")) != _gallery_tab: continue if _gallery_tab == "artwork" and _tag_id != "all": - var tags: Array = entry.get("tags", []) as Array - if not tags.has(_tag_id): + var tags: Variant = entry.get("tags", []) + var tag_list: Array = tags as Array if tags is Array else [] + if _tag_id == "unsorted": + if not tag_list.is_empty(): + continue + elif _tag_id == "roads": + if not tag_list.has("roads") and not tag_list.has("paths"): + continue + elif not tag_list.has(_tag_id): continue filtered.append(entry) return filtered @@ -513,6 +655,8 @@ func _on_next() -> void: func _page_count() -> int: + if _is_scenery_background_gallery() or _is_scenery_artwork_gallery(): + return 1 var entries: Array[Dictionary] = _filtered_entries() if entries.is_empty(): return 1 @@ -530,66 +674,180 @@ func _usable_screen_size() -> Vector2: return available -func _update_popup_layout() -> void: - var in_setup: bool = not _setup_kind.is_empty() - _position_footer(in_setup) +func _update_popup_layout(keep_visible_position: bool = false) -> void: + var previous_position: Vector2i = position + var in_color_picker: bool = _color_picker_open + var in_setup: bool = not _setup_kind.is_empty() and not in_color_picker + var scenery_background_gallery: bool = _is_scenery_background_gallery() and not in_setup and not in_color_picker + var scenery_artwork_gallery: bool = _is_scenery_artwork_gallery() and not in_setup and not in_color_picker + var filtered: Array[Dictionary] = _filtered_entries() + _tabs.visible = kind == "scenery" and not in_color_picker + _filter_chips.visible = scenery_artwork_gallery + _position_footer() + _close_button.visible = not in_setup and not in_color_picker _setup_cancel_button.visible = in_setup _setup_draw_button.visible = in_setup - _edit_scenery_button.visible = kind == "scenery" and not in_setup - _footer.alignment = BoxContainer.ALIGNMENT_END if in_setup else BoxContainer.ALIGNMENT_CENTER + _setup_view.visible = in_setup + _color_picker_view.visible = in_color_picker + _surface_cards_row.visible = scenery_background_gallery and _has_surface_entries(filtered) + _color_back_button.visible = in_color_picker + _color_use_button.visible = in_color_picker + _edit_scenery_button.visible = kind == "scenery" and not in_setup and not in_color_picker + _footer_spacer.visible = scenery_artwork_gallery + _empty_label.visible = filtered.is_empty() and not in_setup and not in_color_picker + _grid.visible = not filtered.is_empty() and not in_setup and not in_color_picker + _grid_scroll.visible = not filtered.is_empty() or in_setup or in_color_picker + _footer.alignment = BoxContainer.ALIGNMENT_BEGIN if scenery_artwork_gallery else (BoxContainer.ALIGNMENT_END if in_setup or in_color_picker else BoxContainer.ALIGNMENT_CENTER) + _footer.add_theme_constant_override( + "separation", + _theme_integer("popups.asset_selector.footer_gap", 16, 8, 32) if in_setup or in_color_picker or kind == "scenery" else 8 + ) var available: Vector2 = _usable_screen_size() - var max_width: float = maxf(1.0, minf(available.x * 0.94, available.x - POPUP_SAFE_SCREEN_MARGIN)) + var width_ratio: float = 0.98 if in_setup else 0.94 + var max_width: float = maxf(1.0, minf(available.x * width_ratio, available.x - POPUP_SAFE_SCREEN_MARGIN)) var max_height: float = maxf(1.0, minf(available.y * 0.86, available.y - POPUP_SAFE_SCREEN_MARGIN * 2.0)) var preferred_width: float = CELL_MIN_SIZE.x * COLUMNS + GRID_H_SEPARATION * (COLUMNS - 1) + POPUP_SIDE_PADDING + var preferred_height: float = max_height + if in_color_picker: + var color_dialog_size: Vector2 = _theme_vector2("popups.dialog.surface_color_size", Vector2(560.0, 640.0)) + preferred_width = color_dialog_size.x + preferred_height = color_dialog_size.y var popup_width: float = minf(preferred_width, max_width) var grid_width: float = maxf(1.0, popup_width - POPUP_SIDE_PADDING) _active_columns = clampi(floori((grid_width + GRID_H_SEPARATION) / (CELL_MIN_SIZE.x + GRID_H_SEPARATION)), 1, COLUMNS) - _page_capacity = _active_columns * ROWS + _page_capacity = maxi(1, filtered.size()) if scenery_background_gallery or scenery_artwork_gallery else _active_columns * ROWS _grid.columns = _active_columns var card_width: float = minf(CELL_MIN_SIZE.x, (grid_width - GRID_H_SEPARATION * (_active_columns - 1)) / float(_active_columns)) var card_height_limit: float = 248.0 if kind == "starters" else CELL_MIN_SIZE.y var card_height: float = minf(card_height_limit, card_width + (58.0 if kind == "starters" else 30.0)) _effective_cell_size = Vector2(maxf(96.0, card_width), maxf(180.0, card_height)) - var filtered_count: int = _filtered_entries().size() + var surface_width: float = minf(_effective_cell_size.x, maxf(96.0, (grid_width - GRID_H_SEPARATION) * 0.5)) + var surface_height: float = maxf(180.0, minf(card_height_limit, surface_width + 30.0)) + _surface_cell_size = Vector2(surface_width, surface_height) + var filtered_count: int = filtered.size() var page_count: int = maxi(1, int(ceil(float(filtered_count) / float(_page_capacity)))) _page = clampi(_page, 0, page_count - 1) var page_start: int = _page * _page_capacity var page_items: int = clampi(filtered_count - page_start, 0, _page_capacity) - var grid_rows: int = int(ceil(float(page_items) / float(_active_columns))) if page_items > 0 else 0 + var grid_item_count: int = _count_sky_entries(filtered) if scenery_background_gallery else page_items + var grid_rows: int = int(ceil(float(grid_item_count) / float(_active_columns))) if grid_item_count > 0 else 0 var body_height: float - if not _setup_kind.is_empty(): - body_height = maxf(180.0, _setup_view.get_combined_minimum_size().y) + if in_color_picker: + _color_picker_view.set_available_size(Vector2(popup_width, minf(preferred_height, max_height))) + body_height = maxf(220.0, _color_picker_view.get_combined_minimum_size().y) + elif in_setup: + body_height = maxf(180.0, _visible_content_minimum_height(_setup_view)) else: body_height = maxf(120.0, float(grid_rows) * _effective_cell_size.y + float(maxi(0, grid_rows - 1)) * GRID_V_SEPARATION) + if scenery_background_gallery and _has_surface_entries(filtered): + body_height += _surface_cell_size.y + GRID_V_SEPARATION if page_items == 0: body_height = maxf(body_height, _empty_label.get_combined_minimum_size().y) + _tabs.custom_minimum_size.x = grid_width + _filter_chips.custom_minimum_size.x = grid_width if _filter_chips.visible else 0.0 var fixed_height: float = _title_bar.get_combined_minimum_size().y + _footer.get_combined_minimum_size().y + POPUP_VERTICAL_PADDING var visible_sections: int = 3 if _tabs.visible: - fixed_height += _tabs.get_combined_minimum_size().y + # HFlowContainer can report zero height before its first layout pass. Use + # the child button target as a floor so the first popup measurement reserves + # the tab row instead of letting the gallery scroll area consume that space. + fixed_height += maxf( + _tabs.get_combined_minimum_size().y, + float(_theme_integer("popups.asset_selector.tab_height", 60, 48, 96)) + ) + visible_sections += 1 + if _filter_chips.visible: + fixed_height += _filter_chips.get_combined_minimum_size().y visible_sections += 1 fixed_height += float(visible_sections - 1) * 8.0 - var popup_height: float = minf(fixed_height + body_height, max_height) + if scenery_background_gallery: + var has_surface_row: bool = _has_surface_entries(filtered) + var content_rows: int = grid_rows + (1 if has_surface_row else 0) + var row_gaps: float = float(maxi(0, grid_rows - 1)) * GRID_V_SEPARATION + if has_surface_row and grid_rows > 0: + row_gaps += GRID_V_SEPARATION + if content_rows > 0: + var available_body_height: float = maxf(1.0, max_height - fixed_height) + var max_row_height: float = (available_body_height - row_gaps) / float(content_rows) + if max_row_height < _effective_cell_size.y or (has_surface_row and max_row_height < _surface_cell_size.y): + var compact_row_height: float = maxf(180.0, max_row_height) + if grid_rows > 0: + _effective_cell_size.y = minf(_effective_cell_size.y, compact_row_height) + if has_surface_row: + _surface_cell_size.y = minf(_surface_cell_size.y, compact_row_height) + body_height = 0.0 + if grid_rows > 0: + body_height += float(grid_rows) * _effective_cell_size.y + float(maxi(0, grid_rows - 1)) * GRID_V_SEPARATION + if has_surface_row: + if grid_rows > 0: + body_height += GRID_V_SEPARATION + body_height += _surface_cell_size.y + if body_height <= 0.0: + body_height = maxf(120.0, _empty_label.get_combined_minimum_size().y) + var popup_height: float = minf(fixed_height + body_height, minf(preferred_height, max_height)) + var popup_position: Vector2i = previous_position + if visible and keep_visible_position: + var minimum_anchor_height: float = minf(fixed_height + 120.0, minf(preferred_height, max_height)) + var minimum_y: int = mini(roundi(POPUP_SAFE_SCREEN_MARGIN), maxi(0, floori(available.y - minimum_anchor_height))) + var maximum_y: int = maxi(minimum_y, floori(available.y - minimum_anchor_height - POPUP_SAFE_SCREEN_MARGIN)) + popup_position.y = clampi(previous_position.y, minimum_y, maximum_y) + var height_below_anchor: float = maxf(1.0, available.y - float(popup_position.y) - POPUP_SAFE_SCREEN_MARGIN) + popup_height = minf(popup_height, height_below_anchor) + if in_color_picker: + var action_width: float = maxf(100.0, (popup_width - POPUP_SIDE_PADDING - 8.0) * 0.5) + _color_back_button.custom_minimum_size.x = action_width + _color_use_button.custom_minimum_size.x = action_width size = Vector2i(maxi(1, roundi(popup_width)), maxi(1, roundi(popup_height))) if visible: - popup_centered() + if keep_visible_position: + var max_x: int = maxi(0, floori(available.x - float(size.x) - POPUP_SAFE_SCREEN_MARGIN)) + var max_y: int = maxi(0, floori(available.y - float(size.y) - POPUP_SAFE_SCREEN_MARGIN)) + var min_x: int = mini(roundi(POPUP_SAFE_SCREEN_MARGIN), max_x) + var min_y: int = mini(roundi(POPUP_SAFE_SCREEN_MARGIN), max_y) + position = Vector2i( + clampi(previous_position.x, min_x, max_x), + clampi(popup_position.y, min_y, max_y) + ) + else: + popup_centered(size) + + +func _update_popup_layout_after_layout() -> void: + await get_tree().process_frame + if is_inside_tree(): + _update_popup_layout() func _rebuild() -> void: _sky_card_group = ButtonGroup.new() - for c: Node in _grid.get_children(): - c.queue_free() + for container: Control in [_grid, _surface_cards_row]: + for child: Node in container.get_children(): + container.remove_child(child) + child.queue_free() _cell_texrects.clear() var filtered: Array[Dictionary] = _filtered_entries() - var in_setup: bool = not _setup_kind.is_empty() - _position_footer(in_setup) + var in_color_picker: bool = _color_picker_open + var in_setup: bool = not _setup_kind.is_empty() and not in_color_picker + var scenery_background_gallery: bool = _is_scenery_background_gallery() and not in_setup and not in_color_picker + var scenery_artwork_gallery: bool = _is_scenery_artwork_gallery() and not in_setup and not in_color_picker + _position_footer() + _close_button.visible = not in_setup and not in_color_picker _setup_view.visible = in_setup + _color_picker_view.visible = in_color_picker + _filter_chips.visible = scenery_artwork_gallery + _surface_cards_row.visible = scenery_background_gallery and _has_surface_entries(filtered) _setup_cancel_button.visible = in_setup _setup_draw_button.visible = in_setup - _edit_scenery_button.visible = kind == "scenery" and not in_setup - _empty_label.visible = filtered.is_empty() and not in_setup - _grid.visible = not filtered.is_empty() and not in_setup - _grid_scroll.visible = not filtered.is_empty() or in_setup + _color_back_button.visible = in_color_picker + _color_use_button.visible = in_color_picker + _edit_scenery_button.visible = kind == "scenery" and not in_setup and not in_color_picker + _make_artwork_button.visible = scenery_artwork_gallery + _browse_button.visible = (kind == "stickman" or kind == "prop") and not in_setup and not in_color_picker + _refresh_button.visible = (kind == "stickman" or kind == "prop") and not in_setup and not in_color_picker + _footer_spacer.visible = scenery_artwork_gallery + _empty_label.visible = filtered.is_empty() and not in_setup and not in_color_picker + _grid.visible = not filtered.is_empty() and not in_setup and not in_color_picker + _grid_scroll.visible = not filtered.is_empty() or in_setup or in_color_picker var total := _page_count() var single := total <= 1 _prev_button.visible = not single @@ -598,7 +856,7 @@ func _rebuild() -> void: _prev_button.disabled = _page <= 0 _next_button.disabled = (_page + 1) * _page_capacity >= filtered.size() _page_label.text = "Page %d/%d" % [_page + 1, total] - if in_setup: + if in_setup or in_color_picker: _prev_button.visible = false _next_button.visible = false _page_label.visible = false @@ -608,37 +866,83 @@ func _rebuild() -> void: return if filtered.is_empty(): return + if scenery_background_gallery: + for entry: Dictionary in filtered: + var background_kind: String = String(entry.get("background_kind", "")) + if background_kind == "sky": + _grid.add_child(_build_cell(entry)) + elif background_kind == "wall_setup" or background_kind == "background_ground_setup": + _surface_cards_row.add_child(_build_cell(entry, _surface_cell_size)) + return var bounds := page_bounds(filtered.size(), _page, _page_capacity) for i: int in range(int(bounds["start"]), int(bounds["end"])): _grid.add_child(_build_cell(filtered[i])) -func _position_footer(in_setup: bool) -> void: +func _position_footer() -> void: var parent := _footer.get_parent() as VBoxContainer if parent == null: return - var target_index: int = _tabs.get_index() + 1 if in_setup else parent.get_child_count() - 1 + var target_index: int = parent.get_child_count() - 1 if _footer.get_index() != target_index: parent.move_child(_footer, target_index) + if _close_button.get_parent() == _footer and _close_button.get_index() != 0: + _footer.move_child(_close_button, 0) -func _build_cell(entry: Dictionary) -> Control: +func _is_scenery_background_gallery() -> bool: + return kind == "scenery" and _gallery_tab == "backgrounds" + + +func _is_scenery_artwork_gallery() -> bool: + return kind == "scenery" and _gallery_tab == "artwork" + + +func _count_sky_entries(entries: Array[Dictionary]) -> int: + var count: int = 0 + for entry: Dictionary in entries: + if String(entry.get("background_kind", "")) == "sky": + count += 1 + return count + + +func _has_surface_entries(entries: Array[Dictionary]) -> bool: + for entry: Dictionary in entries: + var background_kind: String = String(entry.get("background_kind", "")) + if background_kind == "wall_setup" or background_kind == "background_ground_setup": + return true + return false + + +func _visible_content_minimum_height(container: VBoxContainer) -> float: + var height: float = 0.0 + var visible_children: int = 0 + for child: Node in container.get_children(): + if not child is Control or not (child as Control).visible: + continue + height += (child as Control).get_combined_minimum_size().y + visible_children += 1 + if visible_children > 1: + height += float(visible_children - 1) * float(container.get_theme_constant("separation")) + return height + + +func _build_cell(entry: Dictionary, requested_size: Vector2 = Vector2.ZERO) -> Control: var key := _entry_key(entry) var cell: Variant = ASSET_CARD.new() - # Selector cells inherit the popup theme. Avoid a per-card state override so - # opening a selector never implies that one asset is pre-selected. + # Give scenery cards rounded resting and interaction states. The selected sky + # state is applied separately below, so opening the gallery stays neutral. var categories: Array = entry.get("tags", []) as Array var category: String = ", ".join(PackedStringArray(categories)) if not categories.is_empty() else String(entry.get("category", entry.get("material_label", ""))) - var card_size: Vector2 = _effective_cell_size + var card_size: Vector2 = _effective_cell_size if requested_size == Vector2.ZERO else requested_size var is_background_ground_setup: bool = String(entry.get("background_kind", "")) == "background_ground_setup" var thumb_height_budget: float = card_size.y - (74.0 if is_background_ground_setup else 64.0) var thumb_edge: float = minf(_thumbnail_size.x, minf(card_size.x - 20.0, thumb_height_budget)) var thumb_size := Vector2.ONE * maxf(72.0, thumb_edge) if kind == "starters": thumb_size = Vector2.ONE * minf(128.0, maxf(72.0, thumb_edge)) - var card_payload: Dictionary = {"placement_id": _entry_key(entry), "label": String(entry.get("name", entry.get("id", ""))), "icon": String(entry.get("icon", "")), "category": category, "description": String(entry.get("description", "")), "thumbnail_size": thumb_size} - if is_background_ground_setup: - card_payload["description_min_height"] = 42.0 + var description: String = "" if is_background_ground_setup else String(entry.get("description", "")) + var card_payload: Dictionary = {"placement_id": _entry_key(entry), "label": String(entry.get("name", entry.get("id", ""))), "icon": String(entry.get("icon", "")), "category": category, "description": description, "thumbnail_size": thumb_size} if entry.has("background_kind"): card_payload["background_kind"] = entry["background_kind"] card_payload["finish_id"] = entry.get("finish_id", "") @@ -649,6 +953,33 @@ func _build_cell(entry: Dictionary) -> Control: card_payload["status"] = "Current Sky" card_payload["status_color"] = _theme_color("mode_colors.edit", Color("#22C6FF")) cell.configure(card_payload, _thumbnails.get(key, null), null) + if kind == "scenery": + var card_button: Button = cell as Button + var card_border_width: int = _theme_integer("popups.asset_selector.button_border_width", 2, 1, 5) + card_button.add_theme_stylebox_override("normal", _make_rounded_style( + _theme_color("stage_panels.choice.neutral_surface", Color("#171B24")), + _theme_color("stage_panels.choice.border_color", Color("#FFFFFF33")), + card_border_width, + _theme_integer("popups.asset_selector.corner_radius", 16, 8, 32) + )) + card_button.add_theme_stylebox_override("hover", _make_rounded_style( + _theme_color("stage_panels.choice.hover_surface", Color("#303A4B")), + _theme_color("mode_colors.edit", Color("#22C6FF")), + card_border_width + 1, + _theme_integer("popups.asset_selector.corner_radius", 16, 8, 32) + )) + card_button.add_theme_stylebox_override("pressed", _make_rounded_style( + _theme_color("stage_panels.choice.selected_surface", Color("#12394A")), + _theme_color("mode_colors.edit", Color("#22C6FF")), + card_border_width + 1, + _theme_integer("popups.asset_selector.corner_radius", 16, 8, 32) + )) + card_button.add_theme_stylebox_override("focus", _make_rounded_style( + Color(0, 0, 0, 0), + _theme_color("mode_colors.edit", Color("#22C6FF")), + card_border_width + 1, + _theme_integer("popups.asset_selector.corner_radius", 16, 8, 32) + )) cell.custom_minimum_size = card_size cell.focus_mode = Control.FOCUS_ALL cell.activated.connect(func(_payload: Dictionary) -> void: _on_cell_pressed(entry)) @@ -690,6 +1021,173 @@ func _theme_vector2(path: String, fallback: Vector2) -> Vector2: return Vector2(float((raw as Array)[0]), float((raw as Array)[1])) +func _apply_chooser_visual_style() -> void: + var radius: int = _theme_integer("popups.asset_selector.corner_radius", 16, 8, 32) + var button_border_width: int = _theme_integer("popups.asset_selector.button_border_width", 2, 1, 5) + var accent: Color = _theme_color("mode_colors.edit", Color("#22C6FF")) + var text_color: Color = _theme_color("stage_panels.type.text_color", Color("#F5F7FA")) + var neutral: Color = _theme_color("stage_panels.choice.neutral_surface", Color("#171B24")) + var hover: Color = _theme_color("stage_panels.choice.hover_surface", Color("#303A4B")) + var selected: Color = _theme_color("stage_panels.choice.selected_surface", Color("#12394A")) + var border: Color = _theme_color("stage_panels.choice.border_color", Color("#FFFFFF33")) + if _sandbox_theme != null: + self.theme = _sandbox_theme.control_theme() + var shell: StyleBoxFlat = _sandbox_theme.stylebox("popups.asset_selector") + shell.set_corner_radius_all(radius) + shell.content_margin_left = 16.0 + shell.content_margin_right = 16.0 + shell.content_margin_top = 14.0 + shell.content_margin_bottom = 14.0 + add_theme_stylebox_override("panel", shell) + var preview_background: Color = _theme_color("popups.style_defaults.bg_color", Color("#202633FA")) + if _setup_preview != null: + _setup_preview.set_rounded_frame( + float(_theme_integer("popups.asset_selector.surface_corner_radius", 14, 4, 32)), + preview_background + ) + var preview_frame: PanelContainer = null + if _setup_view != null: + preview_frame = _setup_view.get_node_or_null("SurfacePreviewFrame") as PanelContainer + if preview_frame != null: + var frame_style := StyleBoxFlat.new() + frame_style.bg_color = preview_background + frame_style.set_corner_radius_all(_theme_integer("popups.asset_selector.surface_corner_radius", 14, 4, 32)) + preview_frame.add_theme_stylebox_override("panel", frame_style) + if _title_label != null: + _title_label.add_theme_font_size_override("font_size", _theme_integer("popups.asset_selector.setup_title_size", 30, 22, 48) if kind == "scenery" else 24) + _title_label.add_theme_color_override("font_color", text_color) + _apply_emphasis_font(_title_label) + for tab: Button in [_backgrounds_tab, _artwork_tab]: + if tab == null: + continue + tab.custom_minimum_size.y = float(_theme_integer("popups.asset_selector.tab_height", 60, 48, 96)) + tab.size_flags_horizontal = Control.SIZE_EXPAND_FILL + tab.focus_mode = Control.FOCUS_ALL + tab.add_theme_font_size_override("font_size", _theme_integer("popups.asset_selector.tab_font_size", 22, 16, 40)) + tab.add_theme_color_override("font_color", text_color) + tab.add_theme_color_override("font_hover_color", text_color) + tab.add_theme_color_override("font_pressed_color", text_color) + tab.add_theme_stylebox_override("normal", _make_rounded_style(neutral, border, button_border_width, radius)) + tab.add_theme_stylebox_override("hover", _make_rounded_style(hover, accent, button_border_width + 1, radius)) + tab.add_theme_stylebox_override("pressed", _make_rounded_style(selected, accent, button_border_width + 1, radius)) + tab.add_theme_stylebox_override("hover_pressed", _make_rounded_style(selected.lightened(0.06), accent, button_border_width + 1, radius)) + tab.add_theme_stylebox_override("focus", _make_rounded_style(Color(0, 0, 0, 0), accent, button_border_width + 1, radius)) + _apply_emphasis_font(tab) + if _tabs != null: + _tabs.add_theme_constant_override("h_separation", 8) + _tabs.add_theme_constant_override("v_separation", 8) + if _filter_chips != null: + _filter_chips.add_theme_constant_override("h_separation", _theme_integer("popups.asset_selector.filter_chip_gap", 8, 4, 24)) + _filter_chips.add_theme_constant_override("v_separation", _theme_integer("popups.asset_selector.filter_chip_gap", 8, 4, 24)) + for chip: Button in _artwork_filter_buttons: + chip.custom_minimum_size.y = float(_theme_integer("popups.asset_selector.filter_chip_height", 48, 44, 80)) + chip.add_theme_font_size_override("font_size", _theme_integer("popups.asset_selector.filter_chip_font_size", 20, 16, 32)) + chip.add_theme_color_override("font_color", text_color) + chip.add_theme_color_override("font_hover_color", text_color) + chip.add_theme_color_override("font_pressed_color", text_color) + chip.add_theme_stylebox_override("normal", _make_rounded_style(neutral, border, button_border_width, radius)) + chip.add_theme_stylebox_override("hover", _make_rounded_style(hover, accent, button_border_width + 1, radius)) + chip.add_theme_stylebox_override("pressed", _make_rounded_style(selected, accent, button_border_width + 1, radius)) + chip.add_theme_stylebox_override("hover_pressed", _make_rounded_style(selected.lightened(0.06), accent, button_border_width + 1, radius)) + chip.add_theme_stylebox_override("focus", _make_rounded_style(Color(0, 0, 0, 0), accent, button_border_width + 1, radius)) + _apply_emphasis_font(chip) + var heading: Label = get_meta("setup_heading", null) as Label + if heading != null: + heading.add_theme_font_size_override("font_size", _theme_integer("popups.asset_selector.setup_heading_size", 26, 18, 48)) + heading.add_theme_color_override("font_color", text_color) + _apply_emphasis_font(heading) + _apply_rounded_button(_setup_cancel_button, false, accent, text_color, neutral, hover, border, radius) + _apply_rounded_button(_setup_draw_button, true, accent, text_color, neutral, hover, border, radius) + _apply_emphasis_font(_setup_draw_button, true) + _apply_rounded_button(_color_back_button, false, accent, text_color, neutral, hover, border, radius) + _apply_rounded_button(_color_use_button, true, accent, text_color, neutral, hover, border, radius) + if _close_button != null: + _close_button.custom_minimum_size = Vector2(136.0, float(_theme_integer("stage_panels.action.min_target_size", 48, 44, 96))) + _apply_rounded_button(_close_button, false, accent, text_color, neutral, hover, border, radius) + if _make_artwork_button != null: + _make_artwork_button.custom_minimum_size = Vector2(224.0, float(_theme_integer("stage_panels.action.min_target_size", 48, 44, 96))) + _apply_rounded_button(_make_artwork_button, false, accent, text_color, neutral, hover, border, radius) + if _edit_scenery_button != null: + _edit_scenery_button.text = "✋ Edit placed scenery" + _edit_scenery_button.custom_minimum_size = Vector2(292.0, float(_theme_integer("popups.asset_selector.tab_height", 60, 48, 96))) + _edit_scenery_button.focus_mode = Control.FOCUS_ALL + _edit_scenery_button.add_theme_font_size_override("font_size", _theme_integer("popups.asset_selector.tab_font_size", 22, 16, 40)) + _edit_scenery_button.add_theme_color_override("font_color", text_color) + _edit_scenery_button.add_theme_color_override("font_hover_color", text_color) + _edit_scenery_button.add_theme_color_override("font_pressed_color", text_color) + _edit_scenery_button.add_theme_stylebox_override("normal", _make_rounded_style(neutral, border, button_border_width, radius)) + _edit_scenery_button.add_theme_stylebox_override("hover", _make_rounded_style(hover, accent, button_border_width + 1, radius)) + _edit_scenery_button.add_theme_stylebox_override("pressed", _make_rounded_style(selected, accent, button_border_width + 1, radius)) + _edit_scenery_button.add_theme_stylebox_override("hover_pressed", _make_rounded_style(selected.lightened(0.06), accent, button_border_width + 1, radius)) + _edit_scenery_button.add_theme_stylebox_override("focus", _make_rounded_style(Color(0, 0, 0, 0), accent, button_border_width + 1, radius)) + _apply_emphasis_font(_edit_scenery_button) + + +func _apply_rounded_button( + button: Button, + primary: bool, + accent: Color, + text_color: Color, + neutral: Color, + hover: Color, + border: Color, + radius: int +) -> void: + if button == null: + return + var primary_blue: Color = _theme_color("popups.asset_selector.action_color", Color("#087DBA")) + var base: Color = primary_blue if primary else neutral + var over: Color = primary_blue.lightened(0.12) if primary else hover + var down: Color = primary_blue.darkened(0.12) if primary else _selected_button_color() + var edge: Color = accent.lightened(0.15) if primary else border + var border_width: int = _theme_integer("popups.asset_selector.button_border_width", 2, 1, 5) + button.add_theme_stylebox_override("normal", _make_rounded_style(base, edge, border_width, radius)) + button.add_theme_stylebox_override("hover", _make_rounded_style(over, edge, border_width + 1, radius)) + button.add_theme_stylebox_override("pressed", _make_rounded_style(down, accent, border_width + 1, radius)) + button.add_theme_stylebox_override("hover_pressed", _make_rounded_style(down.lightened(0.05), accent, border_width + 1, radius)) + button.add_theme_stylebox_override("focus", _make_rounded_style(Color(0, 0, 0, 0), accent, border_width + 1, radius)) + var label_color: Color = Color.WHITE if primary else text_color + button.add_theme_color_override("font_color", label_color) + button.add_theme_color_override("font_hover_color", label_color) + button.add_theme_color_override("font_pressed_color", label_color) + button.add_theme_font_size_override("font_size", _theme_integer("popups.asset_selector.setup_button_font_size", 20, 14, 40)) + _apply_emphasis_font(button) + + +func _selected_button_color() -> Color: + return _theme_color("stage_panels.choice.selected_surface", Color("#12394A")) + + +func _make_rounded_style(background: Color, edge: Color, edge_width: int, radius: int) -> StyleBoxFlat: + var style := StyleBoxFlat.new() + style.bg_color = background + style.border_color = edge + style.set_border_width_all(edge_width) + style.set_corner_radius_all(radius) + style.content_margin_left = 12.0 + style.content_margin_right = 12.0 + style.content_margin_top = 8.0 + style.content_margin_bottom = 8.0 + return style + + +func _make_strong_font(font: Font, embolden: float) -> Font: + if font == null: + return null + var variation := FontVariation.new() + variation.base_font = font + variation.variation_embolden = embolden + return variation + + +func _apply_emphasis_font(control: Control, extra_bold: bool = false) -> void: + if control == null: + return + var font: Font = _strong_action_font if extra_bold else _strong_ui_font + if font != null: + control.add_theme_font_override("font", font) + + func _on_cell_pressed(entry: Dictionary) -> void: var background_kind: String = String(entry.get("background_kind", "")) if background_kind == "wall_setup": diff --git a/scripts/shared/ui/color_swatch_selector.gd b/scripts/shared/ui/color_swatch_selector.gd index 02658e5..6ce389c 100644 --- a/scripts/shared/ui/color_swatch_selector.gd +++ b/scripts/shared/ui/color_swatch_selector.gd @@ -8,6 +8,15 @@ var _group: ButtonGroup = ButtonGroup.new() var _buttons: Array[Button] = [] var _stage_theme: SandboxTheme var _colors: Array[Color] = [] +var _swatch_size_override: float = -1.0 +var _swatch_gap_override: int = -1 + + +func set_swatch_layout_override(size: float, gap: int) -> void: + _swatch_size_override = size + _swatch_gap_override = gap + if is_inside_tree() and not _colors.is_empty(): + configure(_colors, _selected, _stage_theme) func configure(colors: Array[Color], selected: Color, theme: Variant = null) -> void: @@ -25,8 +34,9 @@ func configure(colors: Array[Color], selected: Color, theme: Variant = null) -> for child: Node in get_children(): remove_child(child) child.queue_free() - add_theme_constant_override("h_separation", _integer("stage_surface_colors.swatch_gap", 8)) - add_theme_constant_override("v_separation", _integer("stage_surface_colors.swatch_gap", 8)) + var gap: int = _swatch_gap_override if _swatch_gap_override >= 0 else _integer("stage_surface_colors.swatch_gap", 8) + add_theme_constant_override("h_separation", gap) + add_theme_constant_override("v_separation", gap) var seen: Dictionary = {} for color: Color in _colors: var color_key: String = color.to_html(true).to_upper() @@ -34,7 +44,7 @@ func configure(colors: Array[Color], selected: Color, theme: Variant = null) -> continue seen[color_key] = true var button := Button.new() - var swatch_size: float = float(_integer("stage_surface_colors.swatch_size", 48)) + var swatch_size: float = _swatch_size_override if _swatch_size_override > 0.0 else float(_integer("stage_surface_colors.swatch_size", 48)) button.custom_minimum_size = Vector2(swatch_size, swatch_size) button.toggle_mode = true button.button_group = _group diff --git a/scripts/stage/sandbox_theme.gd b/scripts/stage/sandbox_theme.gd index 1e8df5f..2b471de 100644 --- a/scripts/stage/sandbox_theme.gd +++ b/scripts/stage/sandbox_theme.gd @@ -4,6 +4,29 @@ extends RefCounted const SCHEMA_VERSION: String = "2.0" const DEFAULT_PATH: String = "res://sandbox_theme.json" +const CONFIRMATION_DEFAULTS: Dictionary = { + "scenery_erase_size": [520, 208], + "padding": 18, + "body_gap": 10, + "footer_gap": 12, + "corner_radius": 16, + "border_width": 2, + "title_font_size": 26, + "message_font_size": 20, + "button_font_size": 20, + "button_height": 48, + "button_radius": 12, + "button_border_width": 2, + "remove_button_width": 170, + "keep_button_width": 132, + "surface_color": "#202633FA", + "border_color": "#22C6FF", + "accent_color": "#22C6FF", + "action_color": "#087DBA", + "danger_color": "#FF626B", + "danger_hover_surface": "#3B252C", + "danger_pressed_surface": "#4A252D" +} const DEFAULT_CONFIG: Dictionary = { "version": SCHEMA_VERSION, @@ -13,14 +36,14 @@ const DEFAULT_CONFIG: Dictionary = { "effects": {"impact_wobble": {"duration": 0.48, "maximum_angle_degrees": 7.0, "sideways_offset": 4.0}}, "grid": {"snap_size": 15.0}, "chrome": {"style_defaults": {}, "top_bar": {"height": 64}, "status_bar": {"height": 38}, "mode_switcher": {"segment_min_width": 124}, "mode_frame": {"border_width": 3}, "mode_badge": {"corner_radius": 12, "padding": 10}, "timestamp_badge": {"corner_radius": 10, "padding": 8, "bg_color": "#171B24F2"}, "tooltip": {"bg_color": "#101010E6", "border_width": 1}, "dialogs": {"queue_size":[680,520], "rule_size":[720,560], "action_size":[560,390], "confirm_size":[620,300], "font_size":22, "message_font_size":26}}, - "popups": {"style_defaults": {"bg_color": "#202633FA", "border_color": "#FFFFFF33", "corner_radius": 12, "padding": 12}, "action_menu": {"font_size": 24}, "dialog": {"surface_color_size":[560,640], "surface_color_preview_size":[420,140], "surface_color_picker_min_size":[320,340], "action_min_target_size":48}, "asset_selector": {"backdrop_alpha": 0.55, "cell_size":[190,220], "thumbnail_size":[156,156], "surface_preview_size":[420,132], "look_cue_size":[80,42], "setup_gap":10}}, + "popups": {"style_defaults": {"bg_color": "#202633FA", "border_color": "#FFFFFF33", "corner_radius": 12, "padding": 12}, "action_menu": {"font_size": 24}, "dialog": {"surface_color_size":[560,640], "surface_color_preview_size":[420,140], "surface_color_picker_min_size":[320,340], "action_min_target_size":48}, "asset_selector": {"backdrop_alpha": 0.55, "cell_size":[190,220], "thumbnail_size":[156,156], "surface_preview_size":[420,132], "look_cue_size":[80,42], "setup_gap":8, "setup_title_size":30, "setup_heading_size":26, "setup_choice_font_size":22, "setup_choice_height":68, "setup_choice_corner_radius":22, "setup_choice_border_width":2, "tab_font_size":22, "tab_height":60, "filter_chip_font_size":20, "filter_chip_height":48, "filter_chip_gap":8, "setup_button_font_size":20, "button_border_width":2, "footer_gap":16, "surface_corner_radius":14, "look_corner_radius":8, "corner_radius":16, "compact_swatch_size":42, "color_group_gap":8, "action_color":"#087DBA", "separator_color":"#AAB2C0A0"}}, "palette": {"style_defaults": {}, "dock_width": 116, "card_size": [96, 94], "icon_size": 30, "drag_threshold": 8, "selected_border_width": 3}, "stage_panels": { "shell": {"preferred_width": 380, "edge_inset": 12, "padding": 12, "header_gap": 8, "section_gap": 12, "corner_radius": 10, "surface_color": "#252B38FA", "border_color": "#FFFFFF33"}, "type": {"title_size": 24, "section_size": 20, "body_size": 20, "helper_size": 16, "text_color": "#F5F7FA", "muted_text_color": "#AAB2C0"}, "choice": {"minimum_height": 56, "row_gap": 6, "neutral_surface": "#171B24", "hover_surface": "#303A4B", "selected_surface": "#12394A", "disabled_surface": "#252B38", "border_color": "#FFFFFF33"}, "toggle": {"minimum_height": 64, "help_minimum_height": 72, "row_padding": 12, "switch_width": 64, "switch_height": 34, "knob_size": 26, "neutral_surface": "#202633", "hover_surface": "#2B3442", "selected_surface": "#173443", "disabled_surface": "#252B38", "border_color": "#FFFFFF33", "switch_on_color": "#22C6FF", "switch_off_color": "#4A5362", "switch_disabled_color": "#3B414D", "knob_color": "#F5F7FA"}, - "action": {"min_target_size": 48, "close_width": 88, "surface_color": "#171B24", "hover_surface": "#303A4B", "selected_surface": "#12394A"}, + "action": {"min_target_size": 48, "close_width": 88, "surface_color": "#171B24", "hover_surface": "#303A4B", "selected_surface": "#12394A", "primary_color": "#087DBA", "primary_hover_color": "#1596D3", "primary_pressed_color": "#066B9F", "destructive_color": "#FF626B", "destructive_hover_surface": "#3B252C", "destructive_pressed_surface": "#4A252D"}, "prop": {"stickman_cue_color": "#F5F7FA", "prop_cue_color": "#B7824D", "cue_outline_color": "#171B24"}, "spots": {"row_gap": 8, "pin_visual_size": 30, "pin_hit_size": 44, "pin_outline_width": 2, "focus_ring_width": 3, "name_pill_padding": 10, "pin_surface_color": "#F4F7FB", "pin_outline_color": "#171B24", "completion_duration": 0.6} }, @@ -36,9 +59,13 @@ var _style_cache: Dictionary = {} var _control_theme: Theme = null var _warned: Dictionary = {} +func _init() -> void: + _apply_confirmation_defaults() + func load_from(path: String = DEFAULT_PATH) -> bool: invalidate_caches() _config = DEFAULT_CONFIG.duplicate(true) + _apply_confirmation_defaults() if not FileAccess.file_exists(path): _warn_once(path, "SandboxTheme: '%s' was not found; using defaults." % path) return false @@ -55,6 +82,11 @@ func load_from(path: String = DEFAULT_PATH) -> bool: _config = _deep_merge(_config, parsed as Dictionary) return true +func _apply_confirmation_defaults() -> void: + var popups: Dictionary = _config.get("popups", {}).duplicate(true) + popups["confirmation"] = CONFIRMATION_DEFAULTS.duplicate(true) + _config["popups"] = popups + func raw_config() -> Dictionary: return _config.duplicate(true) diff --git a/scripts/stage/scenery/stage_background_ground.gd b/scripts/stage/scenery/stage_background_ground.gd index cc5ef30..0717c52 100644 --- a/scripts/stage/scenery/stage_background_ground.gd +++ b/scripts/stage/scenery/stage_background_ground.gd @@ -2,14 +2,20 @@ class_name StageBackgroundGround extends "res://scripts/stage/scenery/stage_rect_surface.gd" ## Non-walkable rectangular scenery surface behind the set. -const SURFACE_RENDERER := preload("res://scripts/stage/scenery/stage_surface_renderer.gd") const DRAW_ORDER := preload("res://scripts/stage/stage_draw_order.gd") +## Keeps procedural ground finishes aligned when this surface is split by the area eraser. +var motif_origin_offset: Vector2 = Vector2.ZERO + func configure(new_size: Vector2, finish: StringName, color: Color) -> void: super.configure(new_size, finish if SURFACE_RENDERER.is_background_finish(finish) else &"finish_background_grass", color) z_index = DRAW_ORDER.BACKGROUND_GROUND +func get_surface_draw_origin_world() -> Vector2: + return global_position + motif_origin_offset + + func _draw() -> void: - SURFACE_RENDERER.draw_background(self, get_local_bounds(), finish_id, get_render_tint(), get_surface_draw_origin_world()) + _draw_surface(false) diff --git a/scripts/stage/scenery/stage_rect_surface.gd b/scripts/stage/scenery/stage_rect_surface.gd index ce91991..71d1186 100644 --- a/scripts/stage/scenery/stage_rect_surface.gd +++ b/scripts/stage/scenery/stage_rect_surface.gd @@ -2,17 +2,47 @@ class_name StageRectSurface extends Node2D ## Shared rectangular geometry and transient tint preview for inert Stage scenery. +const SURFACE_RENDERER := preload("res://scripts/stage/scenery/stage_surface_renderer.gd") +const SURFACE_CACHE := preload("res://scripts/stage/scenery/stage_surface_cache.gd") +const PREVIEW_DRAW_BUDGET_USEC: int = 6000 +## Each rectangle needs one handle; the image itself is shared across rectangles. +const MAX_RESIDENT_TILES: int = 1 + var panel_size: Vector2 = Vector2(320.0, 400.0) var finish_id: StringName = &"" var tint: Color = Color.WHITE var _preview_tint: Color = Color.WHITE var _has_preview_tint: bool = false +var _preview_over_budget: bool = false +var _surface_tiles: Dictionary = {} +var _cache_style: Array = [] +var _cache_dirty: bool = true +var _last_view_transform: Transform2D +var _last_view_rect: Rect2 +var _was_covered: bool = false +var _cover_regions_frame: int = -1 +var _cover_regions: Array[Dictionary] = [] +var _last_cover_regions: Array[Dictionary] = [] +## Placement uses adaptive vector detail; committed surfaces share material textures. +var interactive_preview: bool = false: + set(value): + if interactive_preview == value: + return + interactive_preview = value + _preview_over_budget = false + queue_redraw() func configure(new_size: Vector2, finish: StringName, color: Color) -> void: - panel_size = Vector2(maxf(1.0, new_size.x), maxf(1.0, new_size.y)) + var next_size := Vector2(maxf(1.0, new_size.x), maxf(1.0, new_size.y)) + if panel_size == next_size and finish_id == finish and tint == color: + return + if finish_id != finish: + _preview_over_budget = false + panel_size = next_size finish_id = finish tint = color + _cache_dirty = true queue_redraw() @@ -37,3 +67,154 @@ func get_render_tint() -> Color: func get_surface_draw_origin_world() -> Vector2: return global_position + + +func _exit_tree() -> void: + # Erase/Cancel retains detached geometry, never native drawing resources. + for tile: SURFACE_CACHE in _surface_tiles.values(): + tile.dispose() + _surface_tiles.clear() + _cache_dirty = true + _cover_regions_frame = -1 + _last_cover_regions.clear() + RenderingServer.canvas_item_clear(get_canvas_item()) + + +func _process(_delta: float) -> void: + if not is_visible_in_tree(): + for tile: SURFACE_CACHE in _surface_tiles.values(): + tile.wanted = false + SURFACE_CACHE.build_next() + return + var covered: bool = not interactive_preview and _covered_by_surface() + var coverage_changed: bool = _get_cover_regions() != _last_cover_regions + if coverage_changed: + _last_cover_regions = _cover_regions.duplicate() + if covered: + for tile: SURFACE_CACHE in _surface_tiles.values(): + tile.wanted = false + SURFACE_CACHE.build_next() + # Camera pans, zooms, and window resizing can reveal previously culled motifs. + if coverage_changed or covered != _was_covered or get_global_transform_with_canvas() != _last_view_transform or get_viewport_rect() != _last_view_rect: + queue_redraw() + _was_covered = covered + + +func _draw_surface(wall: bool) -> void: + _last_view_transform = get_global_transform_with_canvas() + _last_view_rect = get_viewport_rect() + if not interactive_preview and DisplayServer.get_name() != "headless" and finish_id not in [&"finish_wall_paint", &"finish_ground_paint"]: + if _covered_by_surface(): + for tile: SURFACE_CACHE in _surface_tiles.values(): + tile.wanted = false + return + _draw_cached_surface(wall) + return + var started_usec: int = Time.get_ticks_usec() if interactive_preview else 0 + if wall: + SURFACE_RENDERER.draw_wall(self, get_local_bounds(), finish_id, get_render_tint(), get_surface_draw_origin_world(), Vector2.ONE, true, interactive_preview, _preview_over_budget) + else: + SURFACE_RENDERER.draw_background(self, get_local_bounds(), finish_id, get_render_tint(), get_surface_draw_origin_world(), Vector2.ONE, true, interactive_preview, _preview_over_budget) + # Latch for this placement session to avoid oscillating between detail levels. + # The complexity estimate handles large first draws before timing is available. + if interactive_preview and Time.get_ticks_usec() - started_usec > PREVIEW_DRAW_BUDGET_USEC: + _preview_over_budget = true + + +func _draw_cached_surface(wall: bool) -> void: + var visible_rect: Rect2 = get_local_bounds().intersection(SURFACE_RENDERER._visible_local_rect(self)) + var color: Color = get_render_tint() + var style: Array = [wall, finish_id, color] + if _cache_style != style: + for previous: SURFACE_CACHE in _surface_tiles.values(): + previous.dispose() + _surface_tiles.clear() + _cache_style = style + _cache_dirty = false + for previous: SURFACE_CACHE in _surface_tiles.values(): + previous.wanted = false + if visible_rect.size.x <= 0.0 or visible_rect.size.y <= 0.0: + return + var period: Vector2 = SURFACE_RENDERER.pattern_size_for(finish_id) + if period.x <= 0.0 or period.y <= 0.0: + draw_rect(visible_rect, SURFACE_RENDERER.surface_base_color(finish_id, color, wall)) + return + var pattern: SURFACE_CACHE = _surface_tiles.get(&"pattern") as SURFACE_CACHE + if pattern == null: + pattern = SURFACE_CACHE.new() as SURFACE_CACHE + pattern.attach_to(self) + pattern.request_pattern(wall, finish_id, color, period) + pattern.updated.connect(queue_redraw) + _surface_tiles[&"pattern"] = pattern + pattern.wanted = true + pattern.last_used_frame = Engine.get_process_frames() + pattern.ensure_queued() + var texture: Texture2D = pattern.get_texture() + if texture == null: + draw_rect(visible_rect, SURFACE_RENDERER.surface_base_color(finish_id, color, wall)) + return + texture_repeat = CanvasItem.TEXTURE_REPEAT_ENABLED + texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS + # One quad samples a repeating material. Camera motion, panel resizing and + # erase fragments change only these UVs, never the image or its mip levels. + var sample_origin: Vector2 = visible_rect.position + get_surface_draw_origin_world() + sample_origin = Vector2(fposmod(sample_origin.x, period.x), fposmod(sample_origin.y, period.y)) + var pixels_per_unit: Vector2 = Vector2(pattern.size) / period + var source := Rect2(sample_origin * pixels_per_unit, visible_rect.size * pixels_per_unit) + draw_texture_rect_region(texture, visible_rect, source, Color.WHITE, false, false) + + +func _covered_by_surface(local_visible: Rect2 = Rect2()) -> bool: + var visible: Rect2 = local_visible + if visible.size.x <= 0.0 or visible.size.y <= 0.0: + visible = get_local_bounds().intersection(SURFACE_RENDERER._visible_local_rect(self)) + if visible.size.x <= 0.0 or visible.size.y <= 0.0: + return true + var corners := PackedVector2Array([visible.position, Vector2(visible.end.x, visible.position.y), visible.end, Vector2(visible.position.x, visible.end.y)]) + for region: Dictionary in _get_cover_regions(): + var fully_covered: bool = true + var cover_bounds: Rect2 = region["bounds"] + var to_cover: Transform2D = region["transform"] + for corner: Vector2 in corners: + if not cover_bounds.has_point(to_cover * corner): + fully_covered = false + break + if fully_covered: + return true + return false + + +func _get_cover_regions() -> Array[Dictionary]: + var frame: int = Engine.get_process_frames() + if _cover_regions_frame == frame: + return _cover_regions + _cover_regions_frame = frame + _cover_regions.clear() + var parent: Node = get_parent() + if parent == null or (parent is Node2D and (parent as Node2D).y_sort_enabled): + return _cover_regions + # Transparent ancestors and custom materials can reveal underlying layers. + var ancestor: Node = self + while ancestor is CanvasItem: + if (ancestor as CanvasItem).modulate.a < 0.999 or (ancestor as CanvasItem).material != null: + return _cover_regions + ancestor = ancestor.get_parent() + for sibling: Node in parent.get_children(): + if sibling == self or not (sibling is StageRectSurface): + continue + var cover: StageRectSurface = sibling as StageRectSurface + if cover.z_as_relative != z_as_relative: + continue + if not cover.is_visible_in_tree() or cover.interactive_preview or cover.material != null or cover.modulate.a < 0.999 or cover.self_modulate.a < 0.999: + continue + if cover.z_index < z_index or (cover.z_index == z_index and cover.get_index() < get_index()): + continue + if SURFACE_RENDERER.surface_base_color(cover.finish_id, cover.get_render_tint(), SURFACE_RENDERER.is_wall_finish(cover.finish_id)).a < 0.999: + continue + if absf(cover.global_transform.determinant()) < 0.000001: + continue + _cover_regions.append({ + "bounds": cover.get_local_bounds().grow(-0.01), + "transform": cover.global_transform.affine_inverse() * global_transform, + }) + return _cover_regions diff --git a/scripts/stage/scenery/stage_surface_cache.gd b/scripts/stage/scenery/stage_surface_cache.gd new file mode 100644 index 0000000..147fbb4 --- /dev/null +++ b/scripts/stage/scenery/stage_surface_cache.gd @@ -0,0 +1,929 @@ +class_name StageSurfaceCache +extends RefCounted +## Lightweight per-surface handle into a scene-scoped immutable bitmap cache. + +const MAX_TEXTURE_EDGE: int = 4096 +const MAX_TEXTURE_PIXELS: float = 4194304.0 +const MAX_CACHE_BYTES: int = 64 * 1024 * 1024 +const MAX_CACHE_ENTRIES: int = 512 +const MAX_QUEUED_BUILDS: int = 512 +const MAX_QUEUE_SCAN_PER_FRAME: int = 8 +const RECENT_USE_PROTECTION_FRAMES: int = 3 +const MIN_DETAILED_SCALE: float = 0.5 +const PATTERN_CACHE_VERSION: int = 1 +const PATTERN_SOURCE_OVERSCAN: float = 4.0 +const PATTERN_PIXEL_SCALE: float = 2.0 + +static var _pool_refs: Dictionary = {} +static var _last_service_frame: int = -1 +static var _active_pool_ref: WeakRef + +signal updated + + +class TileEntry extends RefCounted: + var key: String + var canonical_rect: Rect2 + var source_scale: Vector2 + var wall: bool + var finish: StringName + var tint: Color + var detail_view_size: Vector2 + var size: Vector2i + var byte_count: int = 0 + var texture: Texture2D + var is_pattern: bool = false + var period: Vector2 = Vector2.ZERO + var watchers: Array[WeakRef] = [] + var canvas_owners: Array[WeakRef] = [] + var last_used_frame: int = 0 + var build_usec: int = 0 + var queued: bool = false + var building: bool = false + + +class SurfaceDrawing extends Node2D: + const RENDERER := preload("res://scripts/stage/scenery/stage_surface_renderer.gd") + var source_rect: Rect2 + var wall: bool + var finish: StringName + var tint: Color + var origin: Vector2 = Vector2.ZERO + var detail_view_size: Vector2 + var low_detail: bool = false + var active: bool = false + var pool_ref: WeakRef + + func _draw() -> void: + var pool: Variant = pool_ref.get_ref() if pool_ref != null else null + if not active or pool == null: + return + active = false + var started_usec: int = Time.get_ticks_usec() + if wall: + RENDERER.draw_wall(self, source_rect, finish, tint, origin, Vector2.ONE, false, low_detail, low_detail, detail_view_size) + else: + RENDERER.draw_background(self, source_rect, finish, tint, origin, Vector2.ONE, false, low_detail, low_detail) + pool._finish_vector_submission(Time.get_ticks_usec() - started_usec) + + +class BakePool extends Node: + const RENDERER := preload("res://scripts/stage/scenery/stage_surface_renderer.gd") + const BAKE_GUTTER_PIXELS: int = 4 + var scope_id: int = 0 + var entries: Dictionary = {} + var queue: Array[String] = [] + var _waiting_handles: Array[WeakRef] = [] + var resident_bytes: int = 0 + var builds: int = 0 + var cache_hits: int = 0 + var evictions: int = 0 + var refusals: int = 0 + var readbacks: int = 0 + var pattern_builds: int = 0 + var pattern_readbacks: int = 0 + var pattern_resident_bytes: int = 0 + var pattern_retired_bytes: int = 0 + var last_build_usec: int = 0 + var vector_submission_usec: int = 0 + var retired_bytes: int = 0 + var budget_blocked_requests: int = 0 + var budget_pressure: bool = false + var budget_pressure_frame: int = -1 + var _viewport: SubViewport + var _drawing: SurfaceDrawing + var _building_entry: TileEntry + var _building_started_usec: int = 0 + var _reserved_bytes: int = 0 + var _retired_textures: Array[Texture2D] = [] + var _retire_callback_pending: bool = false + var _capture_pending: bool = false + var _disposed: bool = false + + func _ready() -> void: + set_process(true) + _ensure_baker() + + func _process(_delta: float) -> void: + _prune_queue() + StageSurfaceCache._service_pools() + + func _ensure_baker() -> void: + if is_instance_valid(_viewport): + return + _viewport = SubViewport.new() + _viewport.name = "SurfaceBitmapBaker" + _viewport.disable_3d = true + _viewport.transparent_bg = true + _viewport.render_target_clear_mode = SubViewport.CLEAR_MODE_ALWAYS + _viewport.render_target_update_mode = SubViewport.UPDATE_DISABLED + _viewport.world_2d = World2D.new() + add_child(_viewport) + _drawing = SurfaceDrawing.new() + _drawing.name = "SurfaceVectorSource" + _drawing.pool_ref = weakref(self) + _viewport.add_child(_drawing) + + func queue_handle(handle: StageSurfaceCache) -> void: + if _disposed or handle._disposed: + return + var entry: TileEntry = _find_or_create_entry(handle) + if entry == null: + refusals += 1 + _add_waiter(handle) + return + _bind_handle(handle, entry) + if not _handle_is_interested(handle): + return + handle.last_used_frame = Engine.get_process_frames() + entry.last_used_frame = Engine.get_process_frames() + if entry.texture != null or entry.queued or entry.building: + return + if queue.size() >= MAX_QUEUED_BUILDS: + refusals += 1 + _add_waiter(handle) + return + entry.queued = true + queue.append(entry.key) + handle._enqueued = true + + func _find_or_create_entry(handle: StageSurfaceCache) -> TileEntry: + var existing: TileEntry = entries.get(handle._cache_key) as TileEntry + if existing != null: + cache_hits += 1 + return existing + while entries.size() + _retired_textures.size() >= MAX_CACHE_ENTRIES: + if not _retired_textures.is_empty(): + return null + if not _evict_one(null): + return null + if not _retired_textures.is_empty(): + return null + var arguments: Array = handle._pending_arguments + if arguments.size() < 6: + return null + var entry := TileEntry.new() + entry.key = handle._cache_key + entry.canonical_rect = arguments[0] as Rect2 + entry.source_scale = arguments[1] as Vector2 + entry.wall = bool(arguments[2]) + entry.finish = arguments[3] as StringName + entry.tint = arguments[4] as Color + entry.detail_view_size = arguments[5] as Vector2 + if arguments.size() >= 8: + entry.is_pattern = bool(arguments[6]) + entry.period = arguments[7] as Vector2 + entries[entry.key] = entry + return entry + + func _bind_handle(handle: StageSurfaceCache, entry: TileEntry) -> void: + _register_canvas_owner(entry, handle) + var current: TileEntry = handle._entry_ref.get_ref() as TileEntry if handle._entry_ref != null else null + if current == entry: + handle.ready_for_display = entry.texture != null + handle._building = entry.building + handle._enqueued = entry.queued + handle.build_usec = entry.build_usec + return + if current != null: + _unwatch_handle(current, handle) + handle._entry_ref = weakref(entry) + handle.ready_for_display = entry.texture != null + handle._building = entry.building + handle._enqueued = entry.queued + handle.build_usec = entry.build_usec + var found: bool = false + for watcher_ref: WeakRef in entry.watchers: + if watcher_ref.get_ref() == handle: + found = true + break + if not found: + entry.watchers.append(weakref(handle)) + + func _unwatch_handle(entry: TileEntry, handle: StageSurfaceCache) -> void: + for index: int in range(entry.watchers.size() - 1, -1, -1): + var watcher: Variant = entry.watchers[index].get_ref() + if watcher == null or watcher == handle: + entry.watchers.remove_at(index) + + func _register_canvas_owner(entry: TileEntry, handle: StageSurfaceCache) -> void: + var owner: Variant = handle._owner_ref.get_ref() if handle._owner_ref != null else null + if owner == null or not is_instance_valid(owner): + return + for index: int in range(entry.canvas_owners.size() - 1, -1, -1): + var registered_owner: Variant = entry.canvas_owners[index].get_ref() + if registered_owner == null or not is_instance_valid(registered_owner): + entry.canvas_owners.remove_at(index) + elif registered_owner == owner: + return + entry.canvas_owners.append(weakref(owner)) + + func _start_next_build() -> bool: + if _disposed or _capture_pending: + return false + _prune_queue() + var attempts: int = mini(queue.size(), MAX_QUEUE_SCAN_PER_FRAME) + while attempts > 0 and not queue.is_empty(): + attempts -= 1 + var key: String = _pop_next_build_key() + var entry: TileEntry = entries.get(key) as TileEntry + if entry == null or not entry.queued or entry.texture != null: + continue + entry.queued = false + if not _entry_has_live_watchers(entry): + _evict_entry(entry) + continue + var size: Vector2i = _baker_size_for(entry) + var required_bytes: int = _estimated_texture_bytes(entry) + if not _make_room(required_bytes, entry): + refusals += 1 + if queue.size() < MAX_QUEUED_BUILDS: + entry.queued = true + queue.append(entry.key) + budget_blocked_requests += 1 + budget_pressure = true + budget_pressure_frame = Engine.get_process_frames() + continue + _begin_build(entry, size) + return true + return false + + func _baker_rect_for(entry: TileEntry) -> Rect2: + return entry.canonical_rect.grow(PATTERN_SOURCE_OVERSCAN) if entry.is_pattern else entry.canonical_rect + + func _baker_size_for(entry: TileEntry) -> Vector2i: + var draw_rect: Rect2 = _baker_rect_for(entry) + return StageSurfaceCache.texture_size_for(draw_rect.size, entry.source_scale) + + func _pattern_output_size(entry: TileEntry) -> Vector2i: + var draw_rect: Rect2 = _baker_rect_for(entry) + var baker_size: Vector2i = StageSurfaceCache.texture_size_for(draw_rect.size, entry.source_scale) + return Vector2i( + maxi(1, roundi(float(baker_size.x) * entry.period.x / draw_rect.size.x)), + maxi(1, roundi(float(baker_size.y) * entry.period.y / draw_rect.size.y)) + ) + + func _estimated_texture_bytes(entry: TileEntry) -> int: + var base_size: Vector2i = _pattern_output_size(entry) if entry.is_pattern else _baker_size_for(entry) + return _mip_chain_byte_count(base_size) if entry.is_pattern else base_size.x * base_size.y * 4 + + func _crop_pattern_image(image: Image, entry: TileEntry) -> Image: + var draw_rect: Rect2 = _baker_rect_for(entry) + var output_size: Vector2i = _pattern_output_size(entry) + var scale: Vector2 = Vector2(image.get_size()) / draw_rect.size + var crop_position := Vector2i( + roundi((entry.canonical_rect.position.x - draw_rect.position.x) * scale.x), + roundi((entry.canonical_rect.position.y - draw_rect.position.y) * scale.y) + ) + crop_position.x = clampi(crop_position.x, 0, maxi(0, image.get_width() - output_size.x)) + crop_position.y = clampi(crop_position.y, 0, maxi(0, image.get_height() - output_size.y)) + var safe_size := Vector2i( + mini(output_size.x, image.get_width() - crop_position.x), + mini(output_size.y, image.get_height() - crop_position.y) + ) + if safe_size.x <= 0 or safe_size.y <= 0: + return Image.new() + return image.get_region(Rect2i(crop_position, safe_size)) + + func _mip_chain_byte_count(size: Vector2i) -> int: + var width: int = maxi(1, size.x) + var height: int = maxi(1, size.y) + var total: int = 0 + while true: + total += width * height * 4 + if width == 1 and height == 1: + break + width = maxi(1, floori(float(width) * 0.5)) + height = maxi(1, floori(float(height) * 0.5)) + return total + + func _pop_next_build_key() -> String: + var selected_index: int = 0 + var selected_scale: float = INF + for index: int in range(queue.size()): + var entry: TileEntry = entries.get(queue[index]) as TileEntry + if entry == null: + continue + var scale_rank: float = maxf(entry.source_scale.x, entry.source_scale.y) + if scale_rank < selected_scale: + selected_scale = scale_rank + selected_index = index + return queue.pop_at(selected_index) + + func _begin_build(entry: TileEntry, texture_size: Vector2i) -> void: + _ensure_baker() + RenderingServer.canvas_item_clear(_drawing.get_canvas_item()) + _viewport.render_target_update_mode = SubViewport.UPDATE_DISABLED + _viewport.size = texture_size + var draw_rect: Rect2 = _baker_rect_for(entry) + _drawing.source_rect = draw_rect + _drawing.wall = entry.wall + _drawing.finish = entry.finish + _drawing.tint = entry.tint + _drawing.origin = Vector2.ZERO + _drawing.detail_view_size = entry.period if entry.is_pattern else entry.detail_view_size + _drawing.low_detail = not entry.is_pattern and minf(entry.source_scale.x, entry.source_scale.y) < MIN_DETAILED_SCALE + var draw_scale := Vector2(texture_size) / draw_rect.size + _drawing.scale = draw_scale + _drawing.position = -draw_rect.position * draw_scale + _drawing.active = true + _building_entry = entry + _building_started_usec = Time.get_ticks_usec() + _reserved_bytes = _estimated_texture_bytes(entry) + entry.building = true + _capture_pending = true + _drawing.queue_redraw() + _viewport.render_target_update_mode = SubViewport.UPDATE_ONCE + if not RenderingServer.frame_post_draw.is_connected(_on_frame_post_draw): + RenderingServer.frame_post_draw.connect(_on_frame_post_draw, CONNECT_ONE_SHOT) + StageSurfaceCache._active_pool_ref = weakref(self) + + func _finish_vector_submission(elapsed_usec: int) -> void: + vector_submission_usec = elapsed_usec + + func _on_frame_post_draw() -> void: + if not _capture_pending or _building_entry == null: + return + var entry: TileEntry = _building_entry + var image: Image + if _entry_has_live_watchers(entry) and is_instance_valid(_viewport): + image = _viewport.get_texture().get_image() + if image != null and not image.is_empty(): + if entry.is_pattern: + image = _crop_pattern_image(image, entry) + if image != null and not image.is_empty(): + image.generate_mipmaps() + if image != null and not image.is_empty(): + var texture := ImageTexture.create_from_image(image) + entry.texture = texture + entry.size = image.get_size() + entry.byte_count = image.get_data_size() + resident_bytes += entry.byte_count + if entry.is_pattern: + pattern_builds += 1 + pattern_readbacks += 1 + pattern_resident_bytes += entry.byte_count + _reserved_bytes = 0 + builds += 1 + readbacks += 1 + budget_pressure = false + else: + _reserved_bytes = 0 + refusals += 1 + _notify_entry_failed(entry) + last_build_usec = Time.get_ticks_usec() - _building_started_usec + if entry.texture != null: + entry.build_usec = last_build_usec + _notify_entry_ready(entry) + entry.building = false + _building_entry = null + _capture_pending = false + if is_instance_valid(_viewport): + _viewport.render_target_update_mode = SubViewport.UPDATE_DISABLED + if is_instance_valid(_drawing): + _drawing.active = false + RenderingServer.canvas_item_clear(_drawing.get_canvas_item()) + if StageSurfaceCache._active_pool_ref != null and StageSurfaceCache._active_pool_ref.get_ref() == self: + StageSurfaceCache._active_pool_ref = null + _wake_waiters() + StageSurfaceCache._service_pools() + + func _notify_entry_ready(entry: TileEntry) -> void: + for index: int in range(entry.watchers.size() - 1, -1, -1): + var handle: Variant = entry.watchers[index].get_ref() + if handle == null: + entry.watchers.remove_at(index) + continue + handle._on_entry_ready(entry) + + func _notify_entry_failed(entry: TileEntry) -> void: + for index: int in range(entry.watchers.size() - 1, -1, -1): + var handle: Variant = entry.watchers[index].get_ref() + if handle == null: + entry.watchers.remove_at(index) + continue + handle._on_entry_failed(entry) + + func _entry_has_live_watchers(entry: TileEntry) -> bool: + var has_watcher: bool = false + for index: int in range(entry.watchers.size() - 1, -1, -1): + var handle: Variant = entry.watchers[index].get_ref() + if handle == null or handle._disposed: + entry.watchers.remove_at(index) + continue + if _handle_is_interested(handle): + has_watcher = true + return has_watcher + + func _make_room(required_bytes: int, protected_entry: TileEntry) -> bool: + if resident_bytes + retired_bytes + _reserved_bytes + required_bytes <= MAX_CACHE_BYTES: + return true + if retired_bytes > 0: + return false + if not _evict_one(protected_entry): + return false + # Keep the old texture alive until the frame that releases its CanvasItem RIDs. + # The request will retry after the post-draw retirement callback. + return resident_bytes + retired_bytes + _reserved_bytes + required_bytes <= MAX_CACHE_BYTES + + func _evict_one(protected_entry: TileEntry) -> bool: + var candidate: TileEntry + for value: Variant in entries.values(): + var entry: TileEntry = value as TileEntry + if entry == null or entry == protected_entry or entry.building: + continue + if _entry_is_protected(entry): + continue + if candidate == null or entry.last_used_frame < candidate.last_used_frame: + candidate = entry + if candidate == null: + return false + _evict_entry(candidate) + return true + + func _entry_is_protected(entry: TileEntry) -> bool: + if entry.texture == null: + return false + var current_frame: int = Engine.get_process_frames() + for index: int in range(entry.watchers.size() - 1, -1, -1): + var handle: Variant = entry.watchers[index].get_ref() + if handle == null or handle._disposed: + entry.watchers.remove_at(index) + continue + if _handle_is_visible(handle) and (handle.wanted or current_frame - handle.last_used_frame <= RECENT_USE_PROTECTION_FRAMES): + return true + return false + + func _evict_entry(entry: TileEntry) -> void: + if entry == null or entry.building: + return + if entry.queued: + queue.erase(entry.key) + entry.queued = false + _clear_entry_canvas_owners(entry) + for index: int in range(entry.watchers.size() - 1, -1, -1): + var handle: Variant = entry.watchers[index].get_ref() + if handle == null: + entry.watchers.remove_at(index) + continue + handle._on_entry_evicted(entry) + var retired_texture: Texture2D = entry.texture + var retired_size_bytes: int = entry.byte_count + entry.texture = null + resident_bytes = maxi(0, resident_bytes - entry.byte_count) + if entry.is_pattern: + pattern_resident_bytes = maxi(0, pattern_resident_bytes - entry.byte_count) + entry.byte_count = 0 + if retired_texture != null: + _retired_textures.append(retired_texture) + retired_bytes += retired_size_bytes + if entry.is_pattern: + pattern_retired_bytes += retired_size_bytes + if not _retire_callback_pending: + _retire_callback_pending = true + RenderingServer.frame_post_draw.connect(_on_retired_frame_post_draw, CONNECT_ONE_SHOT) + if entries.get(entry.key) == entry: + entries.erase(entry.key) + evictions += 1 + + func _clear_owner_canvas(owner: Variant) -> void: + if not (owner is CanvasItem) or not is_instance_valid(owner): + return + var canvas_item: CanvasItem = owner as CanvasItem + RenderingServer.canvas_item_clear(canvas_item.get_canvas_item()) + canvas_item.queue_redraw() + + func _clear_entry_canvas_owners(entry: TileEntry) -> void: + var cleared_owners: Dictionary = {} + for owner_ref: WeakRef in entry.canvas_owners: + var owner: Variant = owner_ref.get_ref() + if owner == null or not is_instance_valid(owner): + continue + var owner_id: int = owner.get_instance_id() + if cleared_owners.has(owner_id): + continue + _clear_owner_canvas(owner) + cleared_owners[owner_id] = true + entry.canvas_owners.clear() + + func _stats() -> Dictionary: + var queued_count: int = 0 + for value: Variant in entries.values(): + var entry: TileEntry = value as TileEntry + if entry != null and entry.queued: + queued_count += 1 + return { + "bytes": resident_bytes, + "resident_bytes": resident_bytes, + "retired_bytes": retired_bytes, + "entries": entries.size(), + "queued": queued_count, + "bakers": 1 if is_instance_valid(_viewport) else 0, + "live_vector_bakers": 1 if _capture_pending else 0, + "live_vector_items": 1 if _capture_pending else 0, + "builds": builds, + "cache_hits": cache_hits, + "evictions": evictions, + "refusals": refusals, + "budget_pressure": budget_pressure, + "budget_pressure_frame": budget_pressure_frame, + "budget_blocked_requests": budget_blocked_requests, + "waiting_handles": _waiting_handles.size(), + "readbacks": readbacks, + "pattern_builds": pattern_builds, + "pattern_readbacks": pattern_readbacks, + "pattern_resident_bytes": pattern_resident_bytes, + "pattern_retired_bytes": pattern_retired_bytes, + "last_build_usec": last_build_usec, + "vector_submission_usec": vector_submission_usec, + "active_build": _building_entry.key if _building_entry != null else "" + } + + func _exit_tree() -> void: + _disposed = true + if RenderingServer.frame_post_draw.is_connected(_on_frame_post_draw): + RenderingServer.frame_post_draw.disconnect(_on_frame_post_draw) + if _capture_pending and _building_entry != null: + _building_entry.building = false + _notify_entry_failed(_building_entry) + _building_entry = null + _capture_pending = false + if StageSurfaceCache._active_pool_ref != null and StageSurfaceCache._active_pool_ref.get_ref() == self: + StageSurfaceCache._active_pool_ref = null + for value: Variant in entries.values(): + var entry: TileEntry = value as TileEntry + if entry == null: + continue + _clear_entry_canvas_owners(entry) + for watcher_ref: WeakRef in entry.watchers: + var handle: Variant = watcher_ref.get_ref() + if handle != null: + handle._on_entry_evicted(entry) + entry.texture = null + entries.clear() + queue.clear() + _waiting_handles.clear() + _retired_textures.clear() + resident_bytes = 0 + retired_bytes = 0 + pattern_resident_bytes = 0 + pattern_retired_bytes = 0 + if is_instance_valid(_viewport): + _viewport.queue_free() + _viewport = null + _drawing = null + StageSurfaceCache._forget_pool(scope_id, self) + + func _prune_queue() -> void: + if queue.is_empty(): + return + var kept: Array[String] = [] + for key: String in queue: + var entry: TileEntry = entries.get(key) as TileEntry + if entry == null or not entry.queued or entry.texture != null or not _entry_has_live_watchers(entry): + if entry != null: + entry.queued = false + _set_entry_handle_queue_state(entry, false) + if entry.texture == null and not entry.building and not _entry_has_live_watchers(entry): + _evict_entry(entry) + continue + kept.append(key) + queue = kept + if queue.size() < MAX_QUEUED_BUILDS and not _waiting_handles.is_empty(): + _wake_waiters() + + func _entry_has_any_handles(entry: TileEntry) -> bool: + for index: int in range(entry.watchers.size() - 1, -1, -1): + var handle: Variant = entry.watchers[index].get_ref() + if handle == null or handle._disposed: + entry.watchers.remove_at(index) + else: + return true + return false + + func _set_entry_handle_queue_state(entry: TileEntry, is_queued: bool) -> void: + for watcher_ref: WeakRef in entry.watchers: + var handle: Variant = watcher_ref.get_ref() + if handle != null: + handle._enqueued = is_queued + + func _handle_is_interested(handle: StageSurfaceCache) -> bool: + return handle != null and not handle._disposed and handle.wanted and _handle_is_visible(handle) + + func _handle_is_visible(handle: StageSurfaceCache) -> bool: + if handle == null or handle._disposed: + return false + var owner: Variant = handle._owner_ref.get_ref() if handle._owner_ref != null else null + if owner == null or not is_instance_valid(owner) or not (owner is Node2D): + return false + if not owner.is_inside_tree() or not owner.is_visible_in_tree(): + return false + var visible_rect: Rect2 = RENDERER._visible_local_rect(owner) + if handle.is_pattern: + if not owner.has_method("get_local_bounds"): + return false + var local_bounds: Variant = owner.call("get_local_bounds") + if not (local_bounds is Rect2): + return false + var bounds: Rect2 = local_bounds + return bounds.intersects(visible_rect) + return handle.source_rect.intersects(visible_rect) or handle.core_rect.intersects(visible_rect) + + func _add_waiter(handle: StageSurfaceCache) -> void: + for waiter_ref: WeakRef in _waiting_handles: + if waiter_ref.get_ref() == handle: + return + _waiting_handles.append(weakref(handle)) + + func _wake_waiters() -> void: + if _waiting_handles.is_empty() or _disposed: + return + var pending: Array[WeakRef] = _waiting_handles + _waiting_handles = [] + for waiter_ref: WeakRef in pending: + var handle: Variant = waiter_ref.get_ref() + if handle == null or handle._disposed or not _handle_is_interested(handle): + continue + queue_handle(handle) + + func _on_retired_frame_post_draw() -> void: + _retired_textures.clear() + retired_bytes = 0 + pattern_retired_bytes = 0 + _retire_callback_pending = false + _wake_waiters() + StageSurfaceCache._service_pools() + + +var source_rect: Rect2 +var source_scale: Vector2 +var core_rect: Rect2 +var ready_for_display: bool = false +var wanted: bool = false +var last_used_frame: int = 0 +var build_usec: int = 0 +var _enqueued: bool = false +var _building: bool = false +var _pending_arguments: Array = [] +var _owner_ref: WeakRef +var _pool_ref: WeakRef +var _entry_ref: WeakRef +var _cache_key: String = "" +var _disposed: bool = false +var _finish: StringName +var _tint: Color +var _origin: Vector2 +var is_pattern: bool = false + +var size: Vector2i: + get: + var entry: TileEntry = _entry_ref.get_ref() as TileEntry if _entry_ref != null else null + return entry.size if entry != null else Vector2i.ZERO + +var render_target_update_mode: int: + get: + return SubViewport.UPDATE_DISABLED + + +func attach_to(owner_node: Node) -> void: + _owner_ref = weakref(owner_node) + var pool: BakePool = _get_or_create_pool(owner_node) + _pool_ref = weakref(pool) if pool != null else null + + +func dispose() -> void: + wanted = false + _disposed = true + var entry: TileEntry = _entry_ref.get_ref() as TileEntry if _entry_ref != null else null + var pool: BakePool = _pool_ref.get_ref() as BakePool if _pool_ref != null else null + if entry != null and pool != null: + pool._unwatch_handle(entry, self) + _entry_ref = null + ready_for_display = false + _building = false + _enqueued = false + + +func get_texture() -> Texture2D: + var entry: TileEntry = _entry_ref.get_ref() as TileEntry if _entry_ref != null else null + return entry.texture if entry != null else null + + +func request_build(core: Rect2, rect: Rect2, pixel_scale: Vector2, wall: bool, finish: StringName, tint: Color, origin: Vector2, detail_view_size: Vector2) -> void: + if _disposed: + return + is_pattern = false + core_rect = core + source_rect = rect + source_scale = pixel_scale + wanted = true + _finish = finish + _tint = tint + _origin = origin + var canonical_position: Vector2 = _snap_canonical_position(rect.position + origin, pixel_scale) + var canonical_rect := Rect2(canonical_position, rect.size) + _pending_arguments = [canonical_rect, pixel_scale, wall, finish, tint, detail_view_size] + _cache_key = _make_cache_key(canonical_rect, pixel_scale, wall, finish, tint, detail_view_size) + queue_if_needed() + + +func request_pattern(wall: bool, finish: StringName, tint: Color, period: Vector2) -> void: + if _disposed: + return + var safe_period := Vector2(maxf(1.0, period.x), maxf(1.0, period.y)) + is_pattern = true + core_rect = Rect2(Vector2.ZERO, safe_period) + source_rect = core_rect + source_scale = Vector2.ONE * PATTERN_PIXEL_SCALE + wanted = true + _finish = finish + _tint = tint + _origin = Vector2.ZERO + _pending_arguments = [core_rect, source_scale, wall, finish, tint, safe_period, true, safe_period] + _cache_key = _make_pattern_cache_key(wall, finish, tint, safe_period) + queue_if_needed() + + +func ensure_queued() -> void: + if _disposed: + return + queue_if_needed() + + +func queue_if_needed() -> void: + var pool: BakePool = _pool_ref.get_ref() as BakePool if _pool_ref != null else null + if pool == null: + var owner: Variant = _owner_ref.get_ref() if _owner_ref != null else null + if owner != null and is_instance_valid(owner): + pool = _get_or_create_pool(owner) + _pool_ref = weakref(pool) if pool != null else null + if pool != null: + pool.queue_handle(self) + + +func _on_entry_ready(entry: TileEntry) -> void: + if _disposed: + return + ready_for_display = true + _building = false + _enqueued = false + build_usec = entry.build_usec + updated.emit() + + +func _on_entry_failed(entry: TileEntry) -> void: + if _disposed: + return + if _entry_ref != null and _entry_ref.get_ref() == entry: + ready_for_display = false + _building = false + _enqueued = false + updated.emit() + + +func _on_entry_evicted(entry: TileEntry) -> void: + if _entry_ref != null and _entry_ref.get_ref() == entry: + _entry_ref = null + ready_for_display = false + _building = false + _enqueued = false + updated.emit() + + +static func build_next() -> void: + _service_pools() + + +static func _service_pools() -> void: + var frame: int = Engine.get_process_frames() + if frame == _last_service_frame: + return + _last_service_frame = frame + var active_pool: Variant = _active_pool_ref.get_ref() if _active_pool_ref != null else null + if active_pool != null and is_instance_valid(active_pool): + if not active_pool._capture_pending: + active_pool._start_next_build() + return + _active_pool_ref = null + for scope_id: Variant in _pool_refs.keys(): + var pool: Variant = _pool_refs[scope_id].get_ref() + if pool == null or not is_instance_valid(pool): + _pool_refs.erase(scope_id) + continue + if pool._start_next_build(): + return + + +static func _get_or_create_pool(owner_node: Node) -> BakePool: + if owner_node == null or not is_instance_valid(owner_node) or owner_node.get_tree() == null: + return null + var scope: Node = _get_scope_node(owner_node) + if scope == null or not is_instance_valid(scope): + return null + var scope_id: int = scope.get_instance_id() + var pool_ref: WeakRef = _pool_refs.get(scope_id) as WeakRef + var existing: BakePool = pool_ref.get_ref() as BakePool if pool_ref != null else null + if existing != null and is_instance_valid(existing): + return existing + var pool := BakePool.new() + pool.name = "StageSurfaceBakePool" + pool.scope_id = scope_id + scope.add_child(pool) + _pool_refs[scope_id] = weakref(pool) + return pool + + +static func _get_scope_node(owner_node: Node) -> Node: + var tree: SceneTree = owner_node.get_tree() + if tree == null: + return null + var current_scene: Node = tree.current_scene + if current_scene != null and (current_scene == owner_node or current_scene.is_ancestor_of(owner_node)): + return current_scene + var root: Window = tree.root + var scope: Node = owner_node + while scope.get_parent() != null and scope.get_parent() != root: + scope = scope.get_parent() + return scope + + +static func _forget_pool(scope_id: int, pool: BakePool) -> void: + var pool_ref: WeakRef = _pool_refs.get(scope_id) as WeakRef + if pool_ref != null and pool_ref.get_ref() == pool: + _pool_refs.erase(scope_id) + + +static func _make_cache_key(rect: Rect2, pixel_scale: Vector2, wall: bool, finish: StringName, tint: Color, detail_view_size: Vector2) -> String: + return var_to_str([wall, finish, tint, rect.position.x, rect.position.y, rect.size.x, rect.size.y, pixel_scale.x, pixel_scale.y, detail_view_size.x, detail_view_size.y]) + + +static func _make_pattern_cache_key(wall: bool, finish: StringName, tint: Color, period: Vector2) -> String: + return var_to_str([&"periodic_style", PATTERN_CACHE_VERSION, wall, finish, tint, period.x, period.y, PATTERN_PIXEL_SCALE, &"mipmaps"]) + + +static func _snap_canonical_position(position: Vector2, pixel_scale: Vector2) -> Vector2: + var scale_x: float = maxf(0.001, pixel_scale.x) + var scale_y: float = maxf(0.001, pixel_scale.y) + return Vector2(roundf(position.x * scale_x) / scale_x, roundf(position.y * scale_y) / scale_y) + + +static func get_stats(owner_node: Node = null) -> Dictionary: + if owner_node != null and is_instance_valid(owner_node): + var scope: Node = _get_scope_node(owner_node) + if scope != null: + var pool_ref: WeakRef = _pool_refs.get(scope.get_instance_id()) as WeakRef + var pool: BakePool = pool_ref.get_ref() as BakePool if pool_ref != null else null + return pool._stats() if pool != null else _empty_stats() + var totals: Dictionary = _empty_stats() + for scope_id: Variant in _pool_refs.keys(): + var pool: BakePool = _pool_refs[scope_id].get_ref() as BakePool + if pool == null or not is_instance_valid(pool): + continue + var stats: Dictionary = pool._stats() + for key: String in totals.keys(): + if key == "active_build": + if not str(stats[key]).is_empty(): + totals[key] = stats[key] + elif key == "last_build_usec" or key == "vector_submission_usec" or key == "budget_pressure_frame": + totals[key] = maxi(int(totals[key]), int(stats[key])) + else: + totals[key] = int(totals[key]) + int(stats[key]) + return totals + + +static func _empty_stats() -> Dictionary: + return { + "bytes": 0, + "resident_bytes": 0, + "retired_bytes": 0, + "entries": 0, + "queued": 0, + "bakers": 0, + "live_vector_bakers": 0, + "live_vector_items": 0, + "builds": 0, + "cache_hits": 0, + "evictions": 0, + "refusals": 0, + "budget_pressure": 0, + "budget_pressure_frame": -1, + "budget_blocked_requests": 0, + "waiting_handles": 0, + "readbacks": 0, + "pattern_builds": 0, + "pattern_readbacks": 0, + "pattern_resident_bytes": 0, + "pattern_retired_bytes": 0, + "last_build_usec": 0, + "vector_submission_usec": 0, + "active_build": "" + } + + +static func texture_size_for(local_size: Vector2, pixel_scale: Vector2) -> Vector2i: + var safe_size := Vector2(maxf(1.0, local_size.x), maxf(1.0, local_size.y)) + var safe_scale := Vector2(maxf(0.001, pixel_scale.x), maxf(0.001, pixel_scale.y)) + var pixels: Vector2 = safe_size * safe_scale + var reduction: float = maxf(1.0, maxf(pixels.x, pixels.y) / float(MAX_TEXTURE_EDGE)) + reduction = maxf(reduction, sqrt(pixels.x * pixels.y / MAX_TEXTURE_PIXELS)) + return Vector2i(maxi(1, floori(pixels.x / reduction)), maxi(1, floori(pixels.y / reduction))) diff --git a/scripts/stage/scenery/stage_surface_cache.gd.uid b/scripts/stage/scenery/stage_surface_cache.gd.uid new file mode 100644 index 0000000..3ae3420 --- /dev/null +++ b/scripts/stage/scenery/stage_surface_cache.gd.uid @@ -0,0 +1 @@ +uid://cwcf7h5yf3xi0 diff --git a/scripts/stage/scenery/stage_surface_preview.gd b/scripts/stage/scenery/stage_surface_preview.gd index e44b2c2..34e2cc1 100644 --- a/scripts/stage/scenery/stage_surface_preview.gd +++ b/scripts/stage/scenery/stage_surface_preview.gd @@ -7,6 +7,8 @@ const SURFACE_RENDERER := preload("res://scripts/stage/scenery/stage_surface_ren var surface_kind: StringName = &"wall" var finish_id: StringName = &"finish_wall_paint" var tint: Color = Color("#5A9D70") +var corner_radius: float = 0.0 +var outside_color: Color = Color("#202633FA") func configure(kind: StringName, finish: StringName, color: Color) -> void: @@ -17,6 +19,12 @@ func configure(kind: StringName, finish: StringName, color: Color) -> void: queue_redraw() +func set_rounded_frame(radius: float, background: Color) -> void: + corner_radius = maxf(0.0, radius) + outside_color = background + queue_redraw() + + func _draw() -> void: var rect: Rect2 = Rect2(Vector2(3.0, 3.0), size - Vector2(6.0, 6.0)) if rect.size.x <= 0.0 or rect.size.y <= 0.0: @@ -25,4 +33,49 @@ func _draw() -> void: SURFACE_RENDERER.draw_background(self, rect, finish_id, tint, Vector2.ZERO, Vector2.ONE, false) else: SURFACE_RENDERER.draw_wall(self, rect, finish_id, tint, Vector2.ZERO, Vector2.ONE, false) - draw_rect(rect, Color("#F5F7FA"), false, 2.0) + var radius: float = minf(corner_radius, minf(rect.size.x, rect.size.y) * 0.5) + if radius > 0.0: + var wall: bool = surface_kind != &"background_ground" + var base_color: Color = SURFACE_RENDERER.surface_base_color(finish_id, tint, wall) + _draw_rounded_corner(rect, radius, base_color, outside_color, 0) + _draw_rounded_corner(rect, radius, base_color, outside_color, 1) + _draw_rounded_corner(rect, radius, base_color, outside_color, 2) + _draw_rounded_corner(rect, radius, base_color, outside_color, 3) + var outline := StyleBoxFlat.new() + outline.bg_color = Color.TRANSPARENT + outline.draw_center = false + outline.border_color = Color("#F5F7FA") + outline.set_border_width_all(2) + outline.set_corner_radius_all(roundi(radius)) + draw_style_box(outline, rect) + else: + draw_rect(rect, Color("#F5F7FA"), false, 2.0) + + +func _draw_rounded_corner(rect: Rect2, radius: float, base_color: Color, background: Color, corner: int) -> void: + var square := Rect2() + var center := Vector2.ZERO + var start_angle: float = 0.0 + match corner: + 0: + square = Rect2(rect.position, Vector2.ONE * radius) + center = rect.position + Vector2.ONE * radius + start_angle = PI + 1: + square = Rect2(Vector2(rect.end.x - radius, rect.position.y), Vector2.ONE * radius) + center = Vector2(rect.end.x - radius, rect.position.y + radius) + start_angle = -PI * 0.5 + 2: + square = Rect2(rect.end - Vector2.ONE * radius, Vector2.ONE * radius) + center = rect.end - Vector2.ONE * radius + start_angle = 0.0 + _: + square = Rect2(Vector2(rect.position.x, rect.end.y - radius), Vector2.ONE * radius) + center = Vector2(rect.position.x + radius, rect.end.y - radius) + start_angle = PI * 0.5 + draw_rect(square, background, true) + var arc := PackedVector2Array([center]) + for point_index: int in 13: + var angle: float = start_angle + PI * 0.5 * float(point_index) / 12.0 + arc.append(center + Vector2(cos(angle), sin(angle)) * radius) + draw_colored_polygon(arc, base_color) diff --git a/scripts/stage/scenery/stage_surface_renderer.gd b/scripts/stage/scenery/stage_surface_renderer.gd index 0f2efd7..6fccb31 100644 --- a/scripts/stage/scenery/stage_surface_renderer.gd +++ b/scripts/stage/scenery/stage_surface_renderer.gd @@ -13,12 +13,37 @@ const WALL_BRICK_WIDTH: float = 100.0 const WALL_BRICK_HEIGHT: float = 38.0 const MAX_WALL_BRICK_CELLS: int = 1200 const MAX_BACKGROUND_CELLS: int = 12000 +## Finite material variations repeat independently of wall size and camera travel. +## Even row periods preserve staggered brick/plank courses across the seam. +const BRICK_PATTERN_CELLS := Vector2i(8, 8) +const WOOD_PATTERN_CELLS := Vector2i(3, 6) +const GRASS_PATTERN_CELLS := Vector2i(16, 16) +const CONCRETE_PATTERN_CELLS := Vector2i(4, 4) +const CARPET_PATTERN_CELLS := Vector2i(16, 16) +## Conservative work estimate to avoid an expensive first placement redraw. +## Runtime timing in StageRectSurface also catches slower machines. +const PREVIEW_WORK_BUDGET: float = 6000.0 +const MAX_PREVIEW_TEXTURE_CELLS: int = 128 static func is_wall_finish(id: StringName) -> bool: return WALL_FINISHES.has(id) +static func pattern_size_for(finish: StringName) -> Vector2: + match finish: + &"finish_wall_brick": return Vector2(BRICK_PATTERN_CELLS) * Vector2(WALL_BRICK_WIDTH, WALL_BRICK_HEIGHT) + &"finish_wall_wood": return Vector2(WOOD_PATTERN_CELLS) * Vector2(320.0, 64.0) + &"finish_background_grass": return Vector2(GRASS_PATTERN_CELLS) * Vector2(44.0, 36.0) + &"finish_background_concrete": return Vector2(CONCRETE_PATTERN_CELLS) * Vector2(280.0, 190.0) + &"finish_background_carpet": return Vector2(CARPET_PATTERN_CELLS) * Vector2(34.0, 26.0) + _: return Vector2.ZERO + + +static func _pattern_seed(row: int, column: int, cells: Vector2i, row_factor: int, column_factor: int, offset: int = 0) -> int: + return posmod(row, cells.y) * row_factor + posmod(column, cells.x) * column_factor + offset + + static func is_background_finish(id: StringName) -> bool: return BACKGROUND_FINISHES.has(id) @@ -38,6 +63,28 @@ static func is_ground_finish(id: StringName) -> bool: return GROUND_FINISHES.has(id) or id == &"finish_ground_legacy" +static func surface_base_color(finish: StringName, tint: Color, wall: bool) -> Color: + if finish == &"finish_ground_legacy": + return Color("#468B42") + if finish.ends_with("brick"): + return Color("#D2CBAF") if wall else Color("#994D35") + if finish.ends_with("wood"): + return Color("#9A673C") if wall else Color("#B88951") + if finish == &"finish_ground_grass" or finish == &"finish_background_grass": + return Color("#72A84C") + if finish == &"finish_background_concrete": + return Color("#A6AAA3") + if finish == &"finish_background_carpet": + return tint + if finish == &"finish_ground_dirt": + return Color("#8E633E") + if finish == &"finish_ground_sidewalk": + return Color("#B7B9B2") + if finish == &"finish_ground_asphalt": + return Color("#505A62") + return tint + + static func draw_wall( canvas: CanvasItem, rect: Rect2, @@ -45,9 +92,12 @@ static func draw_wall( tint: Color, world_origin: Vector2, drawing_scale: Vector2 = Vector2.ONE, - cull_to_viewport: bool = true + cull_to_viewport: bool = true, + interactive_preview: bool = false, + force_simple_preview: bool = false, + detail_view_size: Vector2 = Vector2.ZERO ) -> void: - _draw_surface(canvas, rect, finish, tint, world_origin, drawing_scale, true, false, cull_to_viewport) + _draw_surface(canvas, rect, finish, tint, world_origin, drawing_scale, true, false, cull_to_viewport, interactive_preview, force_simple_preview, detail_view_size) static func draw_ground( @@ -68,9 +118,11 @@ static func draw_background( tint: Color, world_origin: Vector2, drawing_scale: Vector2 = Vector2.ONE, - cull_to_viewport: bool = true + cull_to_viewport: bool = true, + interactive_preview: bool = false, + force_simple_preview: bool = false ) -> void: - _draw_surface(canvas, rect, finish, tint, world_origin, drawing_scale, false, true, cull_to_viewport) + _draw_surface(canvas, rect, finish, tint, world_origin, drawing_scale, false, true, cull_to_viewport, interactive_preview, force_simple_preview) static func _draw_surface( @@ -82,66 +134,47 @@ static func _draw_surface( drawing_scale: Vector2, wall: bool, background: bool, - cull_to_viewport: bool + cull_to_viewport: bool, + interactive_preview: bool = false, + force_simple_preview: bool = false, + detail_view_size: Vector2 = Vector2.ZERO ) -> void: var normalized := rect.abs() if normalized.size.x <= 0.0 or normalized.size.y <= 0.0: return var motif_scale: Vector2 = _normalized_scale(drawing_scale) - var base: Color = tint - if finish == &"finish_ground_legacy": - base = Color("#468B42") - elif finish.ends_with("brick"): - base = Color("#D2CBAF") if wall else Color("#994D35") - elif finish.ends_with("wood"): - base = Color("#9A673C") if wall else Color("#B88951") - elif finish == &"finish_ground_grass" or finish == &"finish_background_grass": - base = Color("#72A84C") - elif finish == &"finish_background_concrete": - base = Color("#A6AAA3") - elif finish == &"finish_background_carpet": - base = tint - elif finish == &"finish_ground_dirt": - base = Color("#8E633E") - elif finish == &"finish_ground_sidewalk": - base = Color("#B7B9B2") - elif finish == &"finish_ground_asphalt": - base = Color("#505A62") + var base: Color = surface_base_color(finish, tint, wall) canvas.draw_rect(normalized, base, true) + var view_rect: Rect2 = _visible_local_rect(canvas) if cull_to_viewport else normalized + var pattern_rect: Rect2 = normalized.intersection(view_rect) if not view_rect.size.is_zero_approx() else normalized + var simplified: bool = interactive_preview and (force_simple_preview or needs_light_preview(finish, pattern_rect.size, motif_scale)) if finish.ends_with("brick"): - var view_rect: Rect2 = _visible_local_rect(canvas) if cull_to_viewport else normalized - var pattern_rect: Rect2 = normalized.intersection(view_rect) if not view_rect.size.is_zero_approx() else normalized if pattern_rect.size.x > 0.0 and pattern_rect.size.y > 0.0: var detail_size: Vector2 = view_rect.size if not view_rect.size.is_zero_approx() else normalized.size + if detail_view_size.x > 0.0 and detail_view_size.y > 0.0: + detail_size = detail_view_size var detail_scale: float = brick_detail_scale_for_view(detail_size, motif_scale, wall) if wall: - _draw_wall_bricks(canvas, pattern_rect, world_origin, motif_scale, detail_scale, tint) + _draw_wall_bricks(canvas, pattern_rect, world_origin, motif_scale, detail_scale, tint, simplified) else: _draw_bricks(canvas, pattern_rect, world_origin, base, motif_scale, detail_scale) elif finish.ends_with("wood"): if wall: - var view_rect: Rect2 = _visible_local_rect(canvas) if cull_to_viewport else normalized - var pattern_rect: Rect2 = normalized.intersection(view_rect) if not view_rect.size.is_zero_approx() else normalized if pattern_rect.size.x > 0.0 and pattern_rect.size.y > 0.0: - var detail_scale: float = wood_detail_scale_for_view(pattern_rect.size, motif_scale) - _draw_wall_wood(canvas, pattern_rect, world_origin, motif_scale, detail_scale, tint) + var detail_size: Vector2 = detail_view_size if detail_view_size.x > 0.0 and detail_view_size.y > 0.0 else pattern_rect.size + var detail_scale: float = wood_detail_scale_for_view(detail_size, motif_scale) + _draw_wall_wood(canvas, pattern_rect, world_origin, motif_scale, detail_scale, tint, simplified) else: _draw_wood(canvas, normalized, world_origin, base, motif_scale) elif finish == &"finish_background_grass": - var view_rect: Rect2 = _visible_local_rect(canvas) if cull_to_viewport else normalized - var pattern_rect: Rect2 = normalized.intersection(view_rect) if not view_rect.size.is_zero_approx() else normalized if pattern_rect.size.x > 0.0 and pattern_rect.size.y > 0.0: - _draw_background_grass(canvas, pattern_rect, world_origin, motif_scale) + _draw_background_grass(canvas, pattern_rect, world_origin, motif_scale, simplified) elif finish == &"finish_background_concrete": - var view_rect: Rect2 = _visible_local_rect(canvas) if cull_to_viewport else normalized - var pattern_rect: Rect2 = normalized.intersection(view_rect) if not view_rect.size.is_zero_approx() else normalized if pattern_rect.size.x > 0.0 and pattern_rect.size.y > 0.0: - _draw_background_concrete(canvas, pattern_rect, world_origin, motif_scale) + _draw_background_concrete(canvas, pattern_rect, world_origin, motif_scale, simplified) elif finish == &"finish_background_carpet": - var view_rect: Rect2 = _visible_local_rect(canvas) if cull_to_viewport else normalized - var pattern_rect: Rect2 = normalized.intersection(view_rect) if not view_rect.size.is_zero_approx() else normalized if pattern_rect.size.x > 0.0 and pattern_rect.size.y > 0.0: - _draw_background_carpet(canvas, pattern_rect, world_origin, motif_scale, tint) + _draw_background_carpet(canvas, pattern_rect, world_origin, motif_scale, tint, simplified) elif finish == &"finish_ground_grass": _draw_grass(canvas, normalized, world_origin, motif_scale) elif finish == &"finish_ground_dirt": @@ -154,6 +187,39 @@ static func _draw_surface( push_warning("StageSurfaceRenderer: unknown finish '%s'; drawing paint." % String(finish)) +static func needs_light_preview(finish: StringName, visible_size: Vector2, motif_scale: Vector2 = Vector2.ONE) -> bool: + if visible_size.x <= 0.0 or visible_size.y <= 0.0: + return false + var cell_size: Vector2 + var work_per_cell: float + match finish: + &"finish_wall_brick": + cell_size = Vector2(WALL_BRICK_WIDTH, WALL_BRICK_HEIGHT) + work_per_cell = 240.0 + &"finish_wall_wood": + cell_size = Vector2(320.0, 64.0) + work_per_cell = 750.0 + &"finish_background_grass": + cell_size = Vector2(44.0, 36.0) + work_per_cell = 70.0 + &"finish_background_concrete": + cell_size = Vector2(280.0, 190.0) + work_per_cell = 20.0 + &"finish_background_carpet": + cell_size = Vector2(34.0, 26.0) + work_per_cell = 35.0 + _: + return false + var world_size: Vector2 = visible_size * _normalized_scale(motif_scale) + var cells: float = (ceil(world_size.x / cell_size.x) + 1.0) * (ceil(world_size.y / cell_size.y) + 1.0) + return cells * work_per_cell > PREVIEW_WORK_BUDGET + + +static func _preview_texture_stride(size: Vector2, motif_scale: Vector2, cell_size: Vector2) -> int: + var cells: Vector2 = size * motif_scale / cell_size + Vector2(3.0, 3.0) + return maxi(1, ceili(sqrt(cells.x * cells.y / float(MAX_PREVIEW_TEXTURE_CELLS)))) + + static func brick_detail_scale_for_view(view_size: Vector2, motif_scale: Vector2 = Vector2.ONE, wall: bool = false) -> float: var scale: float = 1.0 var safe_size: Vector2 = Vector2(maxf(1.0, view_size.x), maxf(1.0, view_size.y)) @@ -185,7 +251,7 @@ static func wood_detail_scale_for_view(view_size: Vector2, motif_scale: Vector2 return scale -static func _draw_wall_wood(canvas: CanvasItem, rect: Rect2, origin: Vector2, motif_scale: Vector2, detail_scale: float, tint: Color) -> void: +static func _draw_wall_wood(canvas: CanvasItem, rect: Rect2, origin: Vector2, motif_scale: Vector2, detail_scale: float, tint: Color, simplified: bool = false) -> void: const BOARD_HEIGHT: float = 64.0 const PLANK_WIDTH: float = 320.0 var safe_scale: Vector2 = _normalized_scale(motif_scale) @@ -234,14 +300,15 @@ static func _draw_wall_wood(canvas: CanvasItem, rect: Rect2, origin: Vector2, mo Vector2(plank_local_left, board_local_top + board_world_height / safe_scale.y), _color_with_alpha(seam_color, 0.88), 2.0 / safe_scale.x ) - _draw_wall_wood_grain( - canvas, rect, plank_world_left, board_world_top, plank_world_width, board_world_height, - origin, safe_scale, row, column, grain_color, highlight_color, detail_scale, tint, natural - ) - _draw_wall_wood_knot( - canvas, rect, plank_world_left, board_world_top, plank_world_width, board_world_height, - origin, safe_scale, row, column, detail_scale, tint, natural - ) + if not simplified: + _draw_wall_wood_grain( + canvas, rect, plank_world_left, board_world_top, plank_world_width, board_world_height, + origin, safe_scale, row, column, grain_color, highlight_color, detail_scale, tint, natural + ) + _draw_wall_wood_knot( + canvas, rect, plank_world_left, board_world_top, plank_world_width, board_world_height, + origin, safe_scale, row, column, detail_scale, tint, natural + ) _draw_clipped_line( canvas, rect, Vector2(rect.position.x, board_local_top), Vector2(rect.end.x, board_local_top), @@ -274,7 +341,7 @@ static func _draw_wall_wood_grain( ) -> void: const TRACK_COUNT: int = 5 const SAMPLES: int = 32 - var seed: int = row * 73856093 + column * 19349663 + var seed: int = _pattern_seed(row, column, WOOD_PATTERN_CELLS, 73856093, 19349663) for track: int in TRACK_COUNT: var track_seed: int = seed + track * 83492791 var lane: float = 0.16 + _wood_hash(track_seed) * 0.68 @@ -312,7 +379,7 @@ static func _draw_wall_wood_knot( tint: Color, natural: Color ) -> void: - var seed: int = row * 92821 + column * 68917 + 31 + var seed: int = _pattern_seed(row, column, WOOD_PATTERN_CELLS, 92821, 68917, 31) if _wood_hash(seed) > 0.24: return var center_world := Vector2( @@ -376,7 +443,7 @@ static func _material_color(source: Color, selected: Color, natural: Color) -> C return Color.from_hsv(selected.h, saturation, value, source.a) -static func _draw_background_grass(canvas: CanvasItem, rect: Rect2, origin: Vector2, motif_scale: Vector2) -> void: +static func _draw_background_grass(canvas: CanvasItem, rect: Rect2, origin: Vector2, motif_scale: Vector2, simplified: bool = false) -> void: const CELL_WIDTH: float = 44.0 const CELL_HEIGHT: float = 36.0 var world_left: float = rect.position.x * motif_scale.x + origin.x @@ -388,12 +455,16 @@ static func _draw_background_grass(canvas: CanvasItem, rect: Rect2, origin: Vect var first_row: int = floori(world_top / CELL_HEIGHT) - 1 var last_row: int = floori(world_bottom / CELL_HEIGHT) + 1 var remaining: int = MAX_BACKGROUND_CELLS - for row: int in range(first_row, last_row + 1): - for column: int in range(first_column, last_column + 1): + var stride: int = _preview_texture_stride(rect.size, motif_scale, Vector2(CELL_WIDTH, CELL_HEIGHT)) if simplified else 1 + # Sample the same world cells at a lower density, including in negative coordinates. + first_row = floori(float(first_row) / float(stride)) * stride + first_column = floori(float(first_column) / float(stride)) * stride + for row: int in range(first_row, last_row + 1, stride): + for column: int in range(first_column, last_column + 1, stride): if remaining <= 0: return remaining -= 1 - var seed: int = row * 73856093 + column * 19349663 + 197 + var seed: int = _pattern_seed(row, column, GRASS_PATTERN_CELLS, 73856093, 19349663, 197) var center_world := Vector2((float(column) + 0.5) * CELL_WIDTH, (float(row) + 0.5) * CELL_HEIGHT) center_world += Vector2((_wood_hash(seed) - 0.5) * 34.0, (_wood_hash(seed + 1) - 0.5) * 26.0) var center := Vector2((center_world.x - origin.x) / motif_scale.x, (center_world.y - origin.y) / motif_scale.y) @@ -403,7 +474,7 @@ static func _draw_background_grass(canvas: CanvasItem, rect: Rect2, origin: Vect var highlight: Color = Color("#C3DA74").darkened(_wood_hash(seed + 3) * 0.24) var radius: float = 1.8 + _wood_hash(seed + 4) * 1.6 canvas.draw_circle(center, radius, _color_with_alpha(tuft_color, 0.56)) - for blade: int in 4: + for blade: int in (1 if simplified else 4): var blade_seed: int = seed + blade * 47 + 11 var x_offset: float = (_wood_hash(blade_seed) - 0.5) * 9.0 / motif_scale.x var y_offset: float = (_wood_hash(blade_seed + 1) - 0.5) * 7.0 / motif_scale.y @@ -413,7 +484,7 @@ static func _draw_background_grass(canvas: CanvasItem, rect: Rect2, origin: Vect _draw_clipped_line(canvas, rect, start, tip, _color_with_alpha(highlight, 0.78), 1.25 / motif_scale.y) -static func _draw_background_concrete(canvas: CanvasItem, rect: Rect2, origin: Vector2, motif_scale: Vector2) -> void: +static func _draw_background_concrete(canvas: CanvasItem, rect: Rect2, origin: Vector2, motif_scale: Vector2, simplified: bool = false) -> void: const SLAB_WIDTH: float = 280.0 const SLAB_HEIGHT: float = 190.0 var world_left: float = rect.position.x * motif_scale.x + origin.x @@ -431,11 +502,13 @@ static func _draw_background_concrete(canvas: CanvasItem, rect: Rect2, origin: V var x: float = float(column) * SLAB_WIDTH var local_x: float = (x - origin.x) / motif_scale.x _draw_clipped_line(canvas, rect, Vector2(local_x, rect.position.y), Vector2(local_x, rect.end.y), Color("#68757A", 0.32), column_gap) + if simplified: + continue for row: int in range(first_row, mini(last_row + 1, first_row + MAX_BACKGROUND_CELLS)): if remaining <= 0: return remaining -= 1 - var seed: int = column * 92821 + row * 68917 + 51 + var seed: int = _pattern_seed(row, column, CONCRETE_PATTERN_CELLS, 68917, 92821, 51) var world_point := Vector2(x + 16.0 + _wood_hash(seed) * (SLAB_WIDTH - 32.0), float(row) * SLAB_HEIGHT + 14.0 + _wood_hash(seed + 1) * (SLAB_HEIGHT - 28.0)) var point := Vector2((world_point.x - origin.x) / motif_scale.x, (world_point.y - origin.y) / motif_scale.y) if rect.grow(3.0).has_point(point): @@ -447,25 +520,28 @@ static func _draw_background_concrete(canvas: CanvasItem, rect: Rect2, origin: V _draw_clipped_line(canvas, rect, Vector2(rect.position.x, local_y), Vector2(rect.end.x, local_y), Color("#68757A", 0.32), row_gap) -static func _draw_background_carpet(canvas: CanvasItem, rect: Rect2, origin: Vector2, motif_scale: Vector2, tint: Color) -> void: +static func _draw_background_carpet(canvas: CanvasItem, rect: Rect2, origin: Vector2, motif_scale: Vector2, tint: Color, simplified: bool = false) -> void: const CELL_WIDTH: float = 34.0 const CELL_HEIGHT: float = 26.0 var world_left: float = rect.position.x * motif_scale.x + origin.x var world_right: float = rect.end.x * motif_scale.x + origin.x var world_top: float = rect.position.y * motif_scale.y + origin.y var world_bottom: float = rect.end.y * motif_scale.y + origin.y - var first_column: int = floori(world_left / CELL_WIDTH) + var first_column: int = floori(world_left / CELL_WIDTH) - 1 var last_column: int = floori(world_right / CELL_WIDTH) var first_row: int = floori(world_top / CELL_HEIGHT) var last_row: int = floori(world_bottom / CELL_HEIGHT) var remaining: int = MAX_BACKGROUND_CELLS var natural: Color = default_tint(&"finish_background_carpet") - for row: int in range(first_row, last_row + 1): - for column: int in range(first_column, last_column + 1): + var stride: int = _preview_texture_stride(rect.size, motif_scale, Vector2(CELL_WIDTH, CELL_HEIGHT)) if simplified else 1 + first_row = floori(float(first_row) / float(stride)) * stride + first_column = floori(float(first_column) / float(stride)) * stride + for row: int in range(first_row, last_row + 1, stride): + for column: int in range(first_column, last_column + 1, stride): if remaining <= 0: return remaining -= 1 - var seed: int = row * 92821 + column * 68917 + 71 + var seed: int = _pattern_seed(row, column, CARPET_PATTERN_CELLS, 92821, 68917, 71) var cell_left: float = float(column) * CELL_WIDTH var cell_top: float = float(row) * CELL_HEIGHT for fiber: int in 2: @@ -479,7 +555,7 @@ static func _draw_background_carpet(canvas: CanvasItem, rect: Rect2, origin: Vec _draw_clipped_line(canvas, rect, start, finish, _color_with_alpha(fiber_color, 0.42), 0.85 / motif_scale.y) -static func _draw_wall_bricks(canvas: CanvasItem, rect: Rect2, origin: Vector2, motif_scale: Vector2, detail_scale: float, tint: Color) -> void: +static func _draw_wall_bricks(canvas: CanvasItem, rect: Rect2, origin: Vector2, motif_scale: Vector2, detail_scale: float, tint: Color, interactive_preview: bool = false) -> void: var brick_world_w: float = WALL_BRICK_WIDTH * detail_scale var brick_world_h: float = WALL_BRICK_HEIGHT * detail_scale var brick_w: float = brick_world_w / motif_scale.x @@ -508,7 +584,7 @@ static func _draw_wall_bricks(canvas: CanvasItem, rect: Rect2, origin: Vector2, var column_count: int = int(ceil(rect.size.x * motif_scale.x / brick_world_w)) + 2 for column_offset: int in column_count: var column: int = first_column + column_offset - var seed: int = row * 92821 + column * 68917 + 31 + var seed: int = _pattern_seed(row, column, BRICK_PATTERN_CELLS, 92821, 68917, 31) var brick_world_left: float = float(column) * brick_world_w + stagger_world var brick_local_left: float = (brick_world_left - origin.x) / motif_scale.x var width_variation: float = 0.92 + _wood_hash(seed + 1) * 0.14 @@ -526,6 +602,15 @@ static func _draw_wall_bricks(canvas: CanvasItem, rect: Rect2, origin: Vector2, continue if face_rect.end.y < rect.position.y or face_rect.position.y > rect.end.y: continue + var color_index: int = int(_wood_hash(seed + 5) * float(palette.size())) + var face_color: Color = palette[color_index] + if interactive_preview: + # Keep the committed pattern's world phase, dimensions, jitter, and palette. + # Fine outlines and texture are expensive to regenerate on every drag frame. + var visible_face: Rect2 = face_rect.intersection(rect) + if visible_face.size.x > 0.0 and visible_face.size.y > 0.0: + canvas.draw_rect(visible_face, face_color) + continue var roughness := Vector2(1.7 * detail_scale / motif_scale.x, 1.2 * detail_scale / motif_scale.y) var outline: PackedVector2Array = _rough_brick_outline(face_rect, seed, roughness) var shadow_offset := Vector2(1.3 * detail_scale / motif_scale.x, 1.8 * detail_scale / motif_scale.y) @@ -535,8 +620,6 @@ static func _draw_wall_bricks(canvas: CanvasItem, rect: Rect2, origin: Vector2, var face_points: PackedVector2Array = _clip_polygon_to_rect(outline, rect) if face_points.size() < 3: continue - var color_index: int = int(_wood_hash(seed + 5) * float(palette.size())) - var face_color: Color = palette[color_index] canvas.draw_colored_polygon(face_points, face_color) var closed_outline := face_points.duplicate() closed_outline.append(face_points[0]) @@ -628,12 +711,34 @@ static func _offset_polygon(points: PackedVector2Array, offset: Vector2) -> Pack static func _clip_polygon_to_rect(points: PackedVector2Array, rect: Rect2) -> PackedVector2Array: + var fully_inside: bool = true + for point: Vector2 in points: + if not rect.has_point(point): + fully_inside = false + break + if fully_inside: + return points var clipped := points for edge: int in 4: clipped = _clip_polygon_to_edge(clipped, rect, edge) if clipped.is_empty(): return clipped - return clipped + # Clipping at a corner can produce repeated vertices or a zero-area sliver. + # These cannot be triangulated, even when the array still has three entries. + var cleaned := PackedVector2Array() + for point: Vector2 in clipped: + if cleaned.is_empty() or cleaned[cleaned.size() - 1].distance_squared_to(point) > 0.000001: + cleaned.append(point) + if cleaned.size() > 1 and cleaned[0].distance_squared_to(cleaned[cleaned.size() - 1]) <= 0.000001: + cleaned.remove_at(cleaned.size() - 1) + if cleaned.size() < 3: + return PackedVector2Array() + var area: float = 0.0 + for index: int in range(1, cleaned.size() - 1): + area += (cleaned[index] - cleaned[0]).cross(cleaned[index + 1] - cleaned[0]) + if absf(area) <= 0.001 or Geometry2D.triangulate_polygon(cleaned).is_empty(): + return PackedVector2Array() + return cleaned static func _clip_polygon_to_edge(points: PackedVector2Array, rect: Rect2, edge: int) -> PackedVector2Array: diff --git a/scripts/stage/scenery/stage_wall_panel.gd b/scripts/stage/scenery/stage_wall_panel.gd index f8a9cf2..ff1448a 100644 --- a/scripts/stage/scenery/stage_wall_panel.gd +++ b/scripts/stage/scenery/stage_wall_panel.gd @@ -2,7 +2,6 @@ class_name StageWallPanel extends "res://scripts/stage/scenery/stage_rect_surface.gd" ## Finite decorative wall panel with world-scale finish motifs. -const SURFACE_RENDERER := preload("res://scripts/stage/scenery/stage_surface_renderer.gd") const DRAW_ORDER := preload("res://scripts/stage/stage_draw_order.gd") ## Saved source-wall anchor offset, retained to preserve erased-fragment metadata. ## Motif drawing uses the fragment's current world origin so its brick courses stay aligned. @@ -23,4 +22,4 @@ func get_surface_draw_origin_world() -> Vector2: func _draw() -> void: - SURFACE_RENDERER.draw_wall(self, get_local_bounds(), finish_id, get_render_tint(), get_surface_draw_origin_world()) + _draw_surface(true) diff --git a/scripts/stage/stage_scene_io.gd b/scripts/stage/stage_scene_io.gd index a312983..8e08715 100644 --- a/scripts/stage/stage_scene_io.gd +++ b/scripts/stage/stage_scene_io.gd @@ -307,6 +307,8 @@ static func _validate_root(data: Dictionary) -> Dictionary: return {"ok": false, "error": "Background ground size or finish is invalid."} if not _valid_color(entry.get("color", "")): return {"ok": false, "error": "Background ground color is invalid."} + if entry.has("motif_origin_offset") and not is_vector_data(entry.get("motif_origin_offset")): + return {"ok": false, "error": "Background ground motif origin is invalid."} elif String(entry.get("type", "")) == "scenery_artwork": var asset_id: Variant = entry.get("asset_id", "") if not asset_id is String or String(asset_id).strip_edges().is_empty(): diff --git a/tests/test_phase19_surface_followup.gd b/tests/test_phase19_surface_followup.gd index 1722b27..1e8a1d1 100644 --- a/tests/test_phase19_surface_followup.gd +++ b/tests/test_phase19_surface_followup.gd @@ -447,7 +447,7 @@ func _test_selector_setup_routes(controller: StageSceneryController) -> void: await process_frame await create_timer(0.2).timeout var ground_ghost: StageRectSurface = controller.get("_ghost") as StageRectSurface - _expect(ground_ghost != null and ground_ghost.panel_size.is_equal_approx(Vector2(800.0, 320.0)), "background-ground placement displays its patterned cursor ghost after production mouse motion") + _expect(ground_ghost != null and ground_ghost.panel_size.is_equal_approx(Vector2(240.0, 280.0)), "background-ground placement starts with a cursor ghost the same size as a wall panel") _capture_root_viewport("phase19_ground_placement.png") controller.exit_tool() selector.hide() diff --git a/tests/test_scenery_brick_preview.gd b/tests/test_scenery_brick_preview.gd new file mode 100644 index 0000000..ed340fb --- /dev/null +++ b/tests/test_scenery_brick_preview.gd @@ -0,0 +1,97 @@ +extends SceneTree +## Focused placement regression and CPU draw-cost comparison for large brick walls. + +const WALL := preload("res://scripts/stage/scenery/stage_wall_panel.gd") +const CONTROLLER := preload("res://scripts/editors/stage/stage_scenery_controller.gd") + +class TimedWall extends StageWallPanel: + var draw_times_usec: Array[int] = [] + + func _draw() -> void: + var started: int = Time.get_ticks_usec() + super._draw() + draw_times_usec.append(Time.get_ticks_usec() - started) + +var _failures: Array[String] = [] + + +func _initialize() -> void: + call_deferred("_run") + + +func _run() -> void: + root.size = Vector2i(2560, 1440) + var world := Node2D.new() + var ghosts := Node2D.new() + var controller := CONTROLLER.new() as StageSceneryController + root.add_child(world) + root.add_child(ghosts) + root.add_child(controller) + controller.configure(world, ghosts, null) + controller.set_grid_snap(true, 15.0) + var tint := Color("#995544") + controller.begin_wall(&"finish_wall_brick", Vector2(-300.0, -150.0), tint) + var ghost: StageWallPanel = ghosts.get_child(0) as StageWallPanel + _expect(ghost.interactive_preview, "placement uses the inexpensive brick preview") + var press := InputEventMouseButton.new() + press.button_index = MOUSE_BUTTON_LEFT + press.pressed = true + controller.handle_mouse_button(press, Vector2(-300.0, -150.0), false) + controller.handle_mouse_motion(Vector2(2100.0, 1150.0)) + var preview_bounds: Rect2 = ghost.global_transform * ghost.get_local_bounds() + var release := InputEventMouseButton.new() + release.button_index = MOUSE_BUTTON_LEFT + controller.handle_mouse_button(release, Vector2(2100.0, 1150.0), false) + _expect(world.get_child_count() == 1, "release commits one large wall") + var committed: StageWallPanel = world.get_child(0) as StageWallPanel + _expect(not committed.interactive_preview, "committed wall restores the full brick artwork") + _expect(committed.finish_id == &"finish_wall_brick" and committed.tint == tint, "commit preserves the chosen finish and tint") + _expect(preview_bounds.is_equal_approx(committed.global_transform * committed.get_local_bounds()), "preview and commit use the same snapped bounds across negative world coordinates") + controller.exit_tool() + world.free() + ghosts.free() + controller.free() + + var sample := TimedWall.new() + sample.position = Vector2(1280.0, 720.0) + sample.interactive_preview = true + sample.configure(Vector2(2400.0, 1320.0), &"finish_wall_brick", tint) + root.add_child(sample) + await process_frame + await process_frame + var draw_count: int = sample.draw_times_usec.size() + _expect(draw_count > 0, "large wall executes its drawing callback") + sample.configure(sample.panel_size, sample.finish_id, sample.tint) + await process_frame + await process_frame + _expect(sample.draw_times_usec.size() == draw_count, "unchanged snapped geometry does not rebuild the drawing") + var preview_usec: float = await _measure_redraws(sample) + sample.interactive_preview = false + await process_frame + await process_frame + var detailed_usec: float = await _measure_redraws(sample) + print("Brick draw CPU time (2400 x 1320): preview=%.2f ms, detailed=%.2f ms, speedup=%.1fx" % [preview_usec / 1000.0, detailed_usec / 1000.0, detailed_usec / maxf(1.0, preview_usec)]) + sample.free() + if _failures.is_empty(): + print("PASS: scenery brick placement preview") + else: + for failure: String in _failures: + push_error("FAIL: " + failure) + quit(0 if _failures.is_empty() else 1) + + +func _measure_redraws(sample: TimedWall) -> float: + sample.draw_times_usec.clear() + for _iteration: int in 5: + sample.queue_redraw() + await process_frame + await process_frame + var total_usec: float = 0.0 + for elapsed: int in sample.draw_times_usec: + total_usec += float(elapsed) + return total_usec / maxf(1.0, float(sample.draw_times_usec.size())) + + +func _expect(condition: bool, message: String) -> void: + if not condition: + _failures.append(message) diff --git a/tests/test_scenery_brick_preview.gd.uid b/tests/test_scenery_brick_preview.gd.uid new file mode 100644 index 0000000..d4da13a --- /dev/null +++ b/tests/test_scenery_brick_preview.gd.uid @@ -0,0 +1 @@ +uid://du36kyv22uneg diff --git a/tests/test_scenery_surface_cache.gd b/tests/test_scenery_surface_cache.gd new file mode 100644 index 0000000..1b42248 --- /dev/null +++ b/tests/test_scenery_surface_cache.gd @@ -0,0 +1,284 @@ +extends SceneTree +## Texture budget, full-detail visual equivalence, reuse, and cache invalidation. + +const CACHE := preload("res://scripts/stage/scenery/stage_surface_cache.gd") +const RENDERER := preload("res://scripts/stage/scenery/stage_surface_renderer.gd") + +class ReferenceSurface extends Node2D: + var finish: StringName + var panel_size := Vector2(320.0, 240.0) + var tint: Color + var simplified: bool = false + + func _draw() -> void: + var rect := Rect2(-panel_size * 0.5, panel_size) + if RENDERER.is_wall_finish(finish): + RENDERER.draw_wall(self, rect, finish, tint, global_position, Vector2.ONE, true, simplified, simplified) + else: + RENDERER.draw_background(self, rect, finish, tint, global_position, Vector2.ONE, true, simplified, simplified) + +var _failures: Array[String] = [] + + +func _initialize() -> void: + call_deferred("_run") + + +func _run() -> void: + _test_texture_budget() + _test_clipped_degenerate_polygon() + if DisplayServer.get_name() == "headless": + print("SKIP: texture rendering and invalidation checks require a GUI renderer") + else: + for finish: StringName in [&"finish_wall_brick", &"finish_wall_wood", &"finish_background_grass", &"finish_background_concrete", &"finish_background_carpet"]: + await _test_visual_equivalence(finish) + await _test_visual_equivalence(&"finish_wall_brick", 0.4) + await _test_visual_equivalence(&"finish_wall_wood", 0.4) + await _test_visual_equivalence(&"finish_wall_brick", 0.4, 0.2) + await _test_invalidation() + await _test_camera_tile_budget() + await _test_covered_surfaces() + if _failures.is_empty(): + print("PASS: full-detail scenery surface texture cache") + else: + for failure: String in _failures: + push_error("FAIL: " + failure) + quit(0 if _failures.is_empty() else 1) + + +func _test_texture_budget() -> void: + for extent: Vector2 in [Vector2(320.0, 240.0), Vector2(100000.0, 80000.0), Vector2(100000.0, 12.0), Vector2(1.0, 1.0)]: + var pixels: Vector2i = CACHE.texture_size_for(extent, Vector2(3.0, 3.0)) + _expect(pixels.x > 0 and pixels.y > 0 and pixels.x <= CACHE.MAX_TEXTURE_EDGE and pixels.y <= CACHE.MAX_TEXTURE_EDGE, "cache dimensions stay positive and bounded") + _expect(float(pixels.x) * float(pixels.y) <= CACHE.MAX_TEXTURE_PIXELS, "cache pixel allocation stays bounded") + _expect(CACHE.texture_size_for(Vector2(320.0, 240.0), Vector2(2.0, 2.0)) == Vector2i(640, 480), "ordinary surfaces rasterize at their current screen resolution") + + +func _test_clipped_degenerate_polygon() -> void: + var rect := Rect2(Vector2.ZERO, Vector2(100.0, 100.0)) + var touching := PackedVector2Array([Vector2(-10.0, -10.0), Vector2(0.0, 0.0), Vector2(-10.0, 10.0)]) + _expect(RENDERER._clip_polygon_to_rect(touching, rect).is_empty(), "a polygon touching only the clip edge is discarded") + var crossing := PackedVector2Array([Vector2(-10.0, 20.0), Vector2(0.0, 20.0), Vector2(30.0, 70.0), Vector2(-10.0, 70.0)]) + var clipped: PackedVector2Array = RENDERER._clip_polygon_to_rect(crossing, rect) + _expect(clipped.size() >= 3 and not Geometry2D.triangulate_polygon(clipped).is_empty(), "clipped polygons with vertices on the boundary remain triangulatable") + + +func _make_viewport() -> SubViewport: + var viewport := SubViewport.new() + viewport.size = Vector2i(384, 256) + viewport.transparent_bg = true + viewport.world_2d = World2D.new() + viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS + root.add_child(viewport) + return viewport + + +func _test_visual_equivalence(finish: StringName, zoom: float = 1.0, starting_zoom: float = 1.0) -> void: + var cached_view := _make_viewport() + var direct_view := _make_viewport() + var surface: StageRectSurface = StageWallPanel.new() if RENDERER.is_wall_finish(finish) else StageBackgroundGround.new() + var reference := ReferenceSurface.new() + var tint: Color = RENDERER.default_tint(finish) + var view_transform := Transform2D(Vector2(zoom, 0.0), Vector2(0.0, zoom), Vector2(192.0, 128.0) * (1.0 - zoom)) + cached_view.canvas_transform = Transform2D(Vector2(starting_zoom, 0.0), Vector2(0.0, starting_zoom), Vector2(192.0, 128.0) * (1.0 - starting_zoom)) + direct_view.canvas_transform = view_transform + surface.position = Vector2(192.0, 128.0) + surface.configure(Vector2(320.0, 240.0) / zoom, finish, tint) + cached_view.add_child(surface) + reference.position = surface.position + reference.finish = finish + reference.tint = tint + reference.panel_size = surface.panel_size + direct_view.add_child(reference) + await _settle() + if not is_equal_approx(starting_zoom, zoom): + cached_view.canvas_transform = view_transform + await _settle() + var cached_image: Image = cached_view.get_texture().get_image() + var direct_image: Image = direct_view.get_texture().get_image() + var difference: float = 0.0 + var samples: int = 0 + for y: int in range(16, 240, 2): + for x: int in range(40, 344, 2): + var cached_pixel: Color = cached_image.get_pixel(x, y) + var direct_pixel: Color = direct_image.get_pixel(x, y) + difference += absf(cached_pixel.r - direct_pixel.r) + absf(cached_pixel.g - direct_pixel.g) + absf(cached_pixel.b - direct_pixel.b) + samples += 3 + var mean_error: float = difference / float(samples) + print("%s zoom=%.2f->%.2f cached/reference mean color difference=%.5f" % [finish, starting_zoom, zoom, mean_error]) + # Fractional zoom resamples the cached artwork; compare it with both detailed + # and simplified references so a flat preview cannot pass as a settled texture. + var tolerance: float = 0.015 if is_equal_approx(zoom, 1.0) else 0.02 + _expect(mean_error < tolerance, "%s zoom=%.2f->%.2f cache restores full-detail appearance" % [finish, starting_zoom, zoom]) + if zoom < 1.0: + # Shared immutable bitmaps cannot be changed through a tile's private baker. + # Draw the simplified reference separately, keeping the full image above. + reference.simplified = true + reference.queue_redraw() + await _settle() + var simplified_image: Image = direct_view.get_texture().get_image() + var simplified_difference: float = 0.0 + for y: int in range(16, 240, 2): + for x: int in range(40, 344, 2): + var simple_pixel: Color = simplified_image.get_pixel(x, y) + var full_pixel: Color = direct_image.get_pixel(x, y) + simplified_difference += absf(simple_pixel.r - full_pixel.r) + absf(simple_pixel.g - full_pixel.g) + absf(simple_pixel.b - full_pixel.b) + var simple_error: float = simplified_difference / float(samples) + _expect(mean_error < simple_error * 0.95, "%s normal zoom retains more artwork detail than the flat preview" % finish) + cached_image.save_png("res://tmp/surface_detail_%s.png" % finish) + _expect(cached_image.get_pixel(192, 128).a > 0.95, "%s comparison includes visible surface artwork" % finish) + var tiles: Dictionary = surface.get("_surface_tiles") + _expect(not tiles.is_empty(), "%s has cached texture tiles" % finish) + # UPDATE_ONCE is consumed by the rendering server; the node may retain the enum. + for cache: CACHE in tiles.values(): + _expect(cache.ready_for_display and cache.render_target_update_mode in [SubViewport.UPDATE_ONCE, SubViewport.UPDATE_DISABLED], "%s tile requests a single rasterization" % finish) + cached_view.queue_free() + direct_view.queue_free() + await process_frame + + +func _test_invalidation() -> void: + var viewport := _make_viewport() + var surface := StageWallPanel.new() + surface.position = Vector2(192.0, 128.0) + surface.configure(Vector2(1600.0, 1200.0), &"finish_wall_brick", Color("#B95F3D")) + viewport.add_child(surface) + await _settle() + var initial_tiles: Dictionary = (surface.get("_surface_tiles") as Dictionary).duplicate() + var initial_builds: int = int(CACHE.get_stats(surface).get("builds", 0)) + surface.queue_redraw() + await _settle() + _expect(int(CACHE.get_stats(surface).get("builds", 0)) == initial_builds, "unchanged surfaces reuse the rasterized texture") + viewport.canvas_transform.origin = Vector2(-16.0, 0.0) + await _settle() + _expect((surface.get("_surface_tiles") as Dictionary).size() == initial_tiles.size() and int(CACHE.get_stats(surface).get("builds", 0)) == initial_builds, "small camera pans reuse cached tiles") + viewport.canvas_transform.origin = Vector2(-300.0, 0.0) + await _settle() + var tiles: Dictionary = surface.get("_surface_tiles") + var reused: int = 0 + for key: Variant in initial_tiles: + if tiles.get(key) == initial_tiles[key]: + reused += 1 + _expect(reused > 0, "large pans retain the same repeating material") + viewport.canvas_transform = Transform2D(Vector2(2.0, 0.0), Vector2(0.0, 2.0), Vector2.ZERO) + await _settle() + tiles = surface.get("_surface_tiles") + for cache: CACHE in tiles.values(): + if cache.wanted: + _expect(cache.ready_for_display and cache.source_scale.is_equal_approx(Vector2(2.0, 2.0)), "zoom keeps the same full-resolution mipmapped material") + viewport.size = Vector2i(512, 384) + await _settle() + _expect(_pending_tiles(root) == 0, "window resizing completes newly exposed tiles") + surface.set_preview_tint(Color("#22C6FF")) + await _settle() + for cache: CACHE in (surface.get("_surface_tiles") as Dictionary).values(): + _expect(cache.get("_tint") == Color("#22C6FF"), "temporary color edits refresh the tiles") + surface.clear_preview_tint() + await _settle() + for cache: CACHE in (surface.get("_surface_tiles") as Dictionary).values(): + _expect(cache.get("_tint") == surface.tint, "canceling a color preview restores tile colors") + viewport.canvas_transform = Transform2D.IDENTITY + var retained_material: CACHE = (surface.get("_surface_tiles") as Dictionary).values()[0] as CACHE + surface.configure(Vector2(48.0, 64.0), surface.finish_id, surface.tint) + await _settle() + for cache: CACHE in (surface.get("_surface_tiles") as Dictionary).values(): + _expect(cache == retained_material, "shrinking a panel keeps the shared material and changes only clipping") + surface.position += Vector2(30.0, 20.0) + await _settle() + for cache: CACHE in (surface.get("_surface_tiles") as Dictionary).values(): + _expect(cache == retained_material, "moving a panel keeps the material and changes only pattern coordinates") + surface.configure(surface.panel_size, &"finish_wall_wood", Color("#C87137")) + await _settle() + for cache: CACHE in (surface.get("_surface_tiles") as Dictionary).values(): + _expect(cache.get("_finish") == &"finish_wall_wood", "changing finish replaces cached artwork") + viewport.queue_free() + await process_frame + + +func _settle() -> void: + for frame: int in 1000: + await process_frame + await RenderingServer.frame_post_draw + if frame >= 3 and _pending_tiles(root) == 0: + return + _expect(false, "progressive tile builds complete") + + +func _test_camera_tile_budget() -> void: + var viewport := _make_viewport() + var surface := StageWallPanel.new() + surface.position = Vector2(192.0, 128.0) + surface.configure(Vector2(12000.0, 6000.0), &"finish_wall_brick", Color("#B95F3D")) + viewport.add_child(surface) + await _settle() + var original: Dictionary = (surface.get("_surface_tiles") as Dictionary).duplicate() + # Camera zoom uses mipmaps from the same material image. + for scale: float in [0.95, 0.9, 0.85]: + viewport.canvas_transform = Transform2D(Vector2(scale, 0.0), Vector2(0.0, scale), Vector2.ZERO) + await _settle() + for key: Variant in original: + _expect((surface.get("_surface_tiles") as Dictionary).get(key) == original[key], "wheel steps reuse the same material image") + var builds_by_frame: Dictionary = {} + for index: int in 28: + var builds_before: int = int(CACHE.get_stats(surface).get("builds", 0)) + viewport.canvas_transform.origin = Vector2(-float(index) * 220.0, -float(index % 5) * 70.0) + await process_frame + await RenderingServer.frame_post_draw + var frame: int = Engine.get_process_frames() + var tiles: Dictionary = surface.get("_surface_tiles") + _expect(tiles.size() <= surface.MAX_RESIDENT_TILES, "camera traversal keeps the tile cache bounded") + # Eviction also emits updated; count actual shared bitmap builds instead. + builds_by_frame[frame] = int(CACHE.get_stats(surface).get("builds", 0)) - builds_before + # Queued work is metadata only; all tiles share a single GPU viewport. + for cache: CACHE in tiles.values(): + if not cache.ready_for_display and not cache.get("_building"): + _expect(cache.get_texture() == null and int(CACHE.get_stats(surface).get("bakers", 0)) <= 1, "unbuilt tiles share one render target during camera input") + _expect(frame >= 0, "camera traversal advances frames") + await _settle() + for count: int in builds_by_frame.values(): + _expect(count <= 1, "the shared queue admits at most one tile build per frame") + viewport.queue_free() + await process_frame + + +func _test_covered_surfaces() -> void: + var viewport := _make_viewport() + var back := StageWallPanel.new() + back.position = Vector2(192.0, 128.0) + back.configure(Vector2(1600.0, 1200.0), &"finish_wall_wood", Color("#C87137")) + viewport.add_child(back) + var front := StageWallPanel.new() + front.position = back.position + front.configure(Vector2(2000.0, 1600.0), &"finish_wall_brick", Color("#B95F3D")) + viewport.add_child(front) + await _settle() + _expect((back.get("_surface_tiles") as Dictionary).is_empty(), "fully covered walls do not prepare hidden texture tiles") + front.hide() + await _settle() + _expect(not (back.get("_surface_tiles") as Dictionary).is_empty(), "revealing a wall resumes its tile preparation") + front.show() + front.modulate.a = 0.5 + await _settle() + _expect(not back.call("_covered_by_surface"), "transparent walls keep underlying artwork visible") + front.modulate.a = 1.0 + await _settle() + _expect(back.call("_covered_by_surface"), "opaque coverage is detected again after transparency is removed") + viewport.queue_free() + await process_frame + + +func _pending_tiles(node: Node) -> int: + var count: int = 0 + if node is StageRectSurface: + for cache: CACHE in (node.get("_surface_tiles") as Dictionary).values(): + if cache.wanted and not cache.ready_for_display: + count += 1 + return count + for child: Node in node.get_children(): + count += _pending_tiles(child) + return count + + +func _expect(condition: bool, message: String) -> void: + if not condition: + _failures.append(message) diff --git a/tests/test_scenery_surface_cache.gd.uid b/tests/test_scenery_surface_cache.gd.uid new file mode 100644 index 0000000..d478616 --- /dev/null +++ b/tests/test_scenery_surface_cache.gd.uid @@ -0,0 +1 @@ +uid://cuhlecbuev43k diff --git a/tests/test_scenery_surface_preview.gd b/tests/test_scenery_surface_preview.gd new file mode 100644 index 0000000..9a47a34 --- /dev/null +++ b/tests/test_scenery_surface_preview.gd @@ -0,0 +1,149 @@ +extends SceneTree +## Adaptive preview selection, placement/commit behavior, and per-finish CPU costs. + +const RENDERER := preload("res://scripts/stage/scenery/stage_surface_renderer.gd") +const CONTROLLER := preload("res://scripts/editors/stage/stage_scenery_controller.gd") + +class TimedWall extends StageWallPanel: + var draw_times_usec: Array[int] = [] + + func _draw() -> void: + var started: int = Time.get_ticks_usec() + super._draw() + draw_times_usec.append(Time.get_ticks_usec() - started) + +class TimedGround extends StageBackgroundGround: + var draw_times_usec: Array[int] = [] + + func _draw() -> void: + var started: int = Time.get_ticks_usec() + super._draw() + draw_times_usec.append(Time.get_ticks_usec() - started) + +class SlowWall extends StageWallPanel: + var inject_delay: bool = true + + func get_surface_draw_origin_world() -> Vector2: + if inject_delay: + OS.delay_usec(PREVIEW_DRAW_BUDGET_USEC + 1000) + return super.get_surface_draw_origin_world() + +var _failures: Array[String] = [] + + +func _initialize() -> void: + call_deferred("_run") + + +func _run() -> void: + root.size = Vector2i(2560, 1440) + for finish: StringName in RENDERER.WALL_FINISHES + RENDERER.BACKGROUND_FINISHES: + _expect(not RENDERER.needs_light_preview(finish, Vector2(16.0, 16.0)), "tiny %s keeps its detail" % finish) + _expect(not RENDERER.needs_light_preview(finish, Vector2.ZERO), "invisible %s has no preview work" % finish) + _test_placement(finish) + await _benchmark_finish(finish) + for finish: StringName in [&"finish_wall_brick", &"finish_wall_wood", &"finish_background_grass", &"finish_background_carpet"]: + _expect(RENDERER.needs_light_preview(finish, Vector2(2400.0, 1320.0)), "large %s reduces detail automatically" % finish) + _expect(not RENDERER.needs_light_preview(&"finish_wall_paint", Vector2(100000.0, 100000.0)), "paint stays detailed at any size") + _expect(not RENDERER.needs_light_preview(&"finish_background_concrete", Vector2(2400.0, 1320.0)), "ordinary concrete slab patterns stay detailed") + _expect(RENDERER.needs_light_preview(&"finish_background_concrete", Vector2(24000.0, 13200.0)), "very large concrete patterns can also simplify") + _expect(RENDERER.needs_light_preview(&"finish_wall_wood", Vector2(240.0, 132.0), Vector2(10.0, 10.0)), "work estimate accounts for motif scale") + await _test_runtime_fallback() + if _failures.is_empty(): + print("PASS: adaptive scenery surface previews") + else: + for failure: String in _failures: + push_error("FAIL: " + failure) + quit(0 if _failures.is_empty() else 1) + + +func _test_placement(finish: StringName) -> void: + var world := Node2D.new() + var ghosts := Node2D.new() + var controller := CONTROLLER.new() as StageSceneryController + root.add_child(world) + root.add_child(ghosts) + root.add_child(controller) + controller.configure(world, ghosts, null) + var wall: bool = RENDERER.is_wall_finish(finish) + var tint: Color = RENDERER.default_tint(finish) + var start := Vector2(-300.0, -150.0) + if wall: + controller.begin_wall(finish, start, tint) + else: + controller.begin_background_ground(finish, start, tint) + var ghost: StageRectSurface = ghosts.get_child(0) as StageRectSurface + _expect(ghost.interactive_preview, "%s placement opts into adaptive rendering" % finish) + var press := InputEventMouseButton.new() + press.button_index = MOUSE_BUTTON_LEFT + press.pressed = true + controller.handle_mouse_button(press, start, false) + controller.handle_mouse_motion(Vector2(2100.0, 1170.0)) + var bounds: Rect2 = ghost.global_transform * ghost.get_local_bounds() + var release := InputEventMouseButton.new() + release.button_index = MOUSE_BUTTON_LEFT + controller.handle_mouse_button(release, Vector2(2100.0, 1170.0), false) + _expect(world.get_child_count() == 1, "%s commits one surface" % finish) + var placed: StageRectSurface = world.get_child(0) as StageRectSurface + _expect(not placed.interactive_preview, "%s commit uses full detail" % finish) + _expect(placed.finish_id == finish and placed.tint == tint, "%s preserves finish and tint" % finish) + _expect(bounds.is_equal_approx(placed.global_transform * placed.get_local_bounds()), "%s preserves preview bounds" % finish) + controller.exit_tool() + world.free() + ghosts.free() + controller.free() + + +func _benchmark_finish(finish: StringName) -> void: + var sample: StageRectSurface = TimedWall.new() if RENDERER.is_wall_finish(finish) else TimedGround.new() + sample.position = Vector2(1280.0, 720.0) + sample.interactive_preview = true + sample.configure(Vector2(2400.0, 1320.0), finish, RENDERER.default_tint(finish)) + root.add_child(sample) + await process_frame + await process_frame + var preview_usec: float = await _measure_redraws(sample) + sample.interactive_preview = false + await process_frame + await process_frame + var full_usec: float = await _measure_redraws(sample) + print("%s CPU redraw: adaptive=%.2f ms, full=%.2f ms" % [finish, preview_usec / 1000.0, full_usec / 1000.0]) + sample.free() + + +func _measure_redraws(sample: StageRectSurface) -> float: + var timings: Array[int] = sample.get("draw_times_usec") + timings.clear() + for _iteration: int in 3: + sample.queue_redraw() + await process_frame + await process_frame + _expect(not timings.is_empty(), "surface draws during benchmark") + var total: float = 0.0 + for elapsed: int in timings: + total += float(elapsed) + return total / maxf(1.0, float(timings.size())) + + +func _test_runtime_fallback() -> void: + var sample := SlowWall.new() + sample.position = Vector2(100.0, 100.0) + sample.interactive_preview = true + sample.configure(Vector2(16.0, 16.0), &"finish_wall_wood", Color.WHITE) + root.add_child(sample) + await process_frame + await process_frame + _expect(sample.get("_preview_over_budget"), "measured slow redraw enables the fallback even for a small panel") + sample.inject_delay = false + sample.configure(Vector2(20.0, 20.0), sample.finish_id, sample.tint) + await process_frame + await process_frame + _expect(sample.get("_preview_over_budget"), "fallback stays stable during the current placement session") + sample.interactive_preview = false + _expect(not sample.get("_preview_over_budget"), "leaving preview resets the timing fallback") + sample.free() + + +func _expect(condition: bool, message: String) -> void: + if not condition: + _failures.append(message) diff --git a/tests/test_scenery_surface_preview.gd.uid b/tests/test_scenery_surface_preview.gd.uid new file mode 100644 index 0000000..7c46181 --- /dev/null +++ b/tests/test_scenery_surface_preview.gd.uid @@ -0,0 +1 @@ +uid://du5qxtgupm60f